@gmickel/gno 1.25.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +10 -4
  2. package/assets/skill/SKILL.md +29 -17
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.26.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +146 -4
  8. package/spec/db/schema.sql +1 -1
  9. package/spec/output-schemas/project-profile-apply.schema.json +209 -0
  10. package/spec/output-schemas/project-profile-command.schema.json +160 -0
  11. package/spec/output-schemas/query-diagnose.schema.json +7 -1
  12. package/spec/output-schemas/setup-profile-result.schema.json +87 -0
  13. package/spec/project-profile.schema.json +301 -0
  14. package/src/cli/commands/collection/add.ts +39 -45
  15. package/src/cli/commands/collection/remove.ts +28 -28
  16. package/src/cli/commands/collection/rename.ts +55 -73
  17. package/src/cli/commands/context/add.ts +37 -26
  18. package/src/cli/commands/context/rm.ts +47 -20
  19. package/src/cli/commands/init.ts +55 -125
  20. package/src/cli/commands/models/use.ts +43 -38
  21. package/src/cli/commands/profile-apply.ts +334 -0
  22. package/src/cli/commands/profile.ts +409 -0
  23. package/src/cli/commands/setup-activation.ts +205 -54
  24. package/src/cli/commands/setup-profile.ts +223 -0
  25. package/src/cli/commands/setup.ts +3 -0
  26. package/src/cli/program.ts +110 -9
  27. package/src/config/index.ts +3 -0
  28. package/src/config/project-profile.ts +367 -0
  29. package/src/config/saver.ts +16 -7
  30. package/src/config/types.ts +42 -0
  31. package/src/core/config-mutation.ts +138 -76
  32. package/src/core/config-write-lock.ts +89 -0
  33. package/src/core/context-identity.ts +16 -0
  34. package/src/core/context-resolver.ts +2 -12
  35. package/src/core/folder-setup-planning.ts +6 -21
  36. package/src/core/folder-setup.ts +30 -2
  37. package/src/core/path-rules.ts +53 -0
  38. package/src/core/project-affinity-surface.ts +102 -7
  39. package/src/core/project-profile-apply-state.ts +268 -0
  40. package/src/core/project-profile-apply-validation.ts +95 -0
  41. package/src/core/project-profile-apply.ts +408 -0
  42. package/src/core/project-profile-canonical.ts +71 -0
  43. package/src/core/project-profile-diff.ts +302 -0
  44. package/src/core/project-profile-discovery.ts +519 -0
  45. package/src/core/project-profile-file.ts +37 -0
  46. package/src/core/project-profile-parser.ts +98 -0
  47. package/src/core/project-profile.ts +490 -0
  48. package/src/ingestion/walker.ts +85 -44
  49. package/src/llm/cache.ts +21 -0
  50. package/src/serve/config-sync.ts +2 -2
  51. package/src/serve/resident-runtime.ts +1 -0
  52. package/src/serve/routes/api.ts +1 -0
  53. package/src/store/migrations/021-multi-context-identity.ts +37 -0
  54. package/src/store/migrations/index.ts +2 -0
  55. package/src/store/sqlite/adapter.ts +83 -0
  56. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Read-only project profile CLI composition.
3
+ *
4
+ * All outputs are deterministic and redact machine-local paths. This module
5
+ * never saves config, opens an index, or downloads a model.
6
+ *
7
+ * @module src/cli/commands/profile
8
+ */
9
+
10
+ // node:fs/promises provides realpath; Bun has no equivalent for canonical
11
+ // collection-path comparison.
12
+ import { realpath } from "node:fs/promises";
13
+
14
+ import type { Config } from "../../config/types";
15
+ import type {
16
+ ProjectProfileDesiredState,
17
+ ProjectProfileDiagnostic,
18
+ } from "../../core/project-profile";
19
+ import type { ProjectProfileDiff } from "../../core/project-profile-diff";
20
+ import type {
21
+ ProjectProfileDiscoveryDependencies,
22
+ ProjectProfileDiscoverySummary,
23
+ } from "../../core/project-profile-discovery";
24
+
25
+ import { getModelsCachePath } from "../../app/constants";
26
+ import { createDefaultConfig, loadConfig, type LoadResult } from "../../config";
27
+ import { compileProjectProfileYaml } from "../../core/project-profile";
28
+ import { buildProjectProfileDiff } from "../../core/project-profile-diff";
29
+ import {
30
+ discoverProjectProfile,
31
+ type ProjectProfileDiscoveryDiagnostic,
32
+ } from "../../core/project-profile-discovery";
33
+ import {
34
+ ProjectProfileFileError,
35
+ readProjectProfileFile,
36
+ } from "../../core/project-profile-file";
37
+ import { ModelCache } from "../../llm/cache";
38
+
39
+ export const PROJECT_PROFILE_COMMAND_SCHEMA_VERSION = "1.0" as const;
40
+
41
+ export type ProjectProfileCommandName = "check" | "show" | "diff";
42
+
43
+ export interface ProjectProfileCommandDiagnostic {
44
+ code: string;
45
+ severity: "error" | "warning";
46
+ path: string;
47
+ message: string;
48
+ remediation: string;
49
+ }
50
+
51
+ export interface ProjectProfileCommandResult {
52
+ schemaVersion: typeof PROJECT_PROFILE_COMMAND_SCHEMA_VERSION;
53
+ command: ProjectProfileCommandName;
54
+ status: "valid" | "invalid" | "not_found" | "disabled";
55
+ valid: boolean;
56
+ discovery: ProjectProfileDiscoverySummary;
57
+ profile: {
58
+ fingerprint: string;
59
+ desiredState: ProjectProfileDesiredState | null;
60
+ } | null;
61
+ diff: ProjectProfileDiff | null;
62
+ diagnostics: ProjectProfileCommandDiagnostic[];
63
+ }
64
+
65
+ export interface ProjectProfileCommandOutcome {
66
+ result: ProjectProfileCommandResult;
67
+ exitCode: 0 | 1 | 2;
68
+ }
69
+
70
+ export interface ProjectProfileCommandOptions {
71
+ command: ProjectProfileCommandName;
72
+ path?: string;
73
+ cwd?: string;
74
+ channel?: "local" | "remote";
75
+ configPath?: string;
76
+ offline?: boolean;
77
+ discoveryDependencies?: Partial<ProjectProfileDiscoveryDependencies>;
78
+ loadConfigFn?: (path?: string) => Promise<LoadResult<Config>>;
79
+ readProfileFn?: (path: string) => Promise<string>;
80
+ canonicalizePath?: (path: string) => Promise<string>;
81
+ isModelAvailableOffline?: (
82
+ modelUri: string,
83
+ modelType: "embed" | "rerank" | "expand" | "gen"
84
+ ) => Promise<boolean>;
85
+ }
86
+
87
+ const compareCodeUnits = (left: string, right: string): number =>
88
+ left < right ? -1 : left > right ? 1 : 0;
89
+
90
+ const remediationForCompilerDiagnostic = (
91
+ diagnostic: ProjectProfileDiagnostic
92
+ ): string => {
93
+ switch (diagnostic.code) {
94
+ case "MODEL_PRESET_NOT_FOUND":
95
+ return "Configure the named local preset alias or select a configured alias.";
96
+ case "MODEL_PRESET_UNAVAILABLE_OFFLINE":
97
+ return "Cache every model in the named preset before retrying with --offline.";
98
+ case "MODEL_CACHE_CHECK_FAILED":
99
+ return "Repair local model-cache metadata and retry; no download was attempted.";
100
+ case "MIGRATION_REQUIRED":
101
+ case "UNSUPPORTED_SCHEMA_MAJOR":
102
+ case "UNSUPPORTED_SCHEMA_MINOR":
103
+ return "Migrate .gno/index.yml to the supported schemaVersion.";
104
+ case "PATH_NOT_FOUND":
105
+ return "Create the referenced repository-relative path or update the profile.";
106
+ case "SYMLINK_ESCAPE":
107
+ return "Replace the escaping symlink with a path contained by the profile root.";
108
+ case "CONTEXT_FILE_INVALID":
109
+ case "CONTEXT_FILE_UNREADABLE":
110
+ return "Replace the context file with readable UTF-8 text.";
111
+ case "UNSAFE_PATH":
112
+ return "Use a repository-relative path without traversal, expansion, or runtime state.";
113
+ default:
114
+ return "Repair the reported profile field and rerun gno profile check.";
115
+ }
116
+ };
117
+
118
+ export const compilerDiagnostic = (
119
+ issue: ProjectProfileDiagnostic
120
+ ): ProjectProfileCommandDiagnostic => ({
121
+ ...issue,
122
+ remediation: remediationForCompilerDiagnostic(issue),
123
+ });
124
+
125
+ export const discoveryDiagnostic = (
126
+ issue: ProjectProfileDiscoveryDiagnostic
127
+ ): ProjectProfileCommandDiagnostic => ({ ...issue });
128
+
129
+ export const sortDiagnostics = (
130
+ diagnostics: ProjectProfileCommandDiagnostic[]
131
+ ): ProjectProfileCommandDiagnostic[] =>
132
+ diagnostics.sort(
133
+ (left, right) =>
134
+ compareCodeUnits(left.path, right.path) ||
135
+ compareCodeUnits(left.code, right.code) ||
136
+ compareCodeUnits(left.message, right.message)
137
+ );
138
+
139
+ const resultWithoutProfile = (
140
+ command: ProjectProfileCommandName,
141
+ status: "invalid" | "not_found" | "disabled",
142
+ discovery: ProjectProfileDiscoverySummary,
143
+ diagnostics: ProjectProfileCommandDiagnostic[]
144
+ ): ProjectProfileCommandResult => ({
145
+ schemaVersion: PROJECT_PROFILE_COMMAND_SCHEMA_VERSION,
146
+ command,
147
+ status,
148
+ valid: false,
149
+ discovery,
150
+ profile: null,
151
+ diff: null,
152
+ diagnostics: sortDiagnostics(diagnostics),
153
+ });
154
+
155
+ const loadProfileConfig = async (
156
+ options: ProjectProfileCommandOptions
157
+ ): Promise<
158
+ | {
159
+ ok: true;
160
+ config: Config;
161
+ diagnostics: ProjectProfileCommandDiagnostic[];
162
+ }
163
+ | { ok: false; diagnostic: ProjectProfileCommandDiagnostic }
164
+ > => {
165
+ const loaded = await (options.loadConfigFn ?? loadConfig)(options.configPath);
166
+ if (!loaded.ok) {
167
+ if (loaded.error.code === "NOT_FOUND") {
168
+ return {
169
+ ok: true,
170
+ config: createDefaultConfig(),
171
+ diagnostics: [
172
+ {
173
+ code: "CONFIG_NOT_FOUND",
174
+ severity: "warning",
175
+ path: "config",
176
+ message:
177
+ "No local GNO config exists; diff uses an empty current state.",
178
+ remediation:
179
+ "Run gno profile diff, then apply the profile in a later explicit step.",
180
+ },
181
+ ],
182
+ };
183
+ }
184
+ return {
185
+ ok: false,
186
+ diagnostic: {
187
+ code: "CONFIG_INVALID",
188
+ severity: "error",
189
+ path: "config",
190
+ message: "The selected local GNO config could not be loaded safely.",
191
+ remediation: "Repair the local config and rerun gno profile check.",
192
+ },
193
+ };
194
+ }
195
+ return {
196
+ ok: true,
197
+ config: loaded.value,
198
+ diagnostics: loaded.warnings.map((warning) => ({
199
+ code: "CONFIG_WARNING",
200
+ severity: "warning",
201
+ path: warning.path,
202
+ message: warning.message,
203
+ remediation: "Repair the warned local config entry before applying.",
204
+ })),
205
+ };
206
+ };
207
+
208
+ const defaultReadProfile = readProjectProfileFile;
209
+
210
+ /**
211
+ * Execute one read-only profile command and return a classified exit code.
212
+ */
213
+ export async function runProjectProfileCommand(
214
+ options: ProjectProfileCommandOptions
215
+ ): Promise<ProjectProfileCommandOutcome> {
216
+ const discovery = await discoverProjectProfile(
217
+ {
218
+ channel: options.channel ?? "local",
219
+ cwd: options.cwd,
220
+ rootOverride: options.path,
221
+ },
222
+ options.discoveryDependencies
223
+ );
224
+ const discoveryDiagnostics = discovery.diagnostics.map(discoveryDiagnostic);
225
+ if (discovery.summary.status !== "found") {
226
+ const status =
227
+ discovery.summary.status === "not_found"
228
+ ? "not_found"
229
+ : discovery.summary.status === "disabled"
230
+ ? "disabled"
231
+ : "invalid";
232
+ return {
233
+ result: resultWithoutProfile(
234
+ options.command,
235
+ status,
236
+ discovery.summary,
237
+ discoveryDiagnostics
238
+ ),
239
+ exitCode: discoveryDiagnostics.some(
240
+ (diagnostic) => diagnostic.code === "PROFILE_DISCOVERY_FAILED"
241
+ )
242
+ ? 2
243
+ : 1,
244
+ };
245
+ }
246
+ if (!(discovery.profilePath && discovery.profileRoot)) {
247
+ return {
248
+ result: resultWithoutProfile(
249
+ options.command,
250
+ "invalid",
251
+ discovery.summary,
252
+ [
253
+ ...discoveryDiagnostics,
254
+ {
255
+ code: "PROFILE_DISCOVERY_FAILED",
256
+ severity: "error",
257
+ path: ".gno/index.yml",
258
+ message: "Discovery returned no trusted local profile identity.",
259
+ remediation: "Rerun with an exact readable project root.",
260
+ },
261
+ ]
262
+ ),
263
+ exitCode: 2,
264
+ };
265
+ }
266
+
267
+ const configResult = await loadProfileConfig(options);
268
+ if (!configResult.ok) {
269
+ return {
270
+ result: resultWithoutProfile(
271
+ options.command,
272
+ "invalid",
273
+ discovery.summary,
274
+ [...discoveryDiagnostics, configResult.diagnostic]
275
+ ),
276
+ exitCode: 1,
277
+ };
278
+ }
279
+
280
+ let yaml: string;
281
+ try {
282
+ yaml = await (options.readProfileFn ?? defaultReadProfile)(
283
+ discovery.profilePath
284
+ );
285
+ } catch (error) {
286
+ const invalidFile = error instanceof ProjectProfileFileError ? error : null;
287
+ return {
288
+ result: resultWithoutProfile(
289
+ options.command,
290
+ "invalid",
291
+ discovery.summary,
292
+ [
293
+ ...discoveryDiagnostics,
294
+ {
295
+ code: invalidFile?.code ?? "PROFILE_READ_FAILED",
296
+ severity: "error",
297
+ path: ".gno/index.yml",
298
+ message:
299
+ invalidFile?.message ??
300
+ "The selected project profile could not be read.",
301
+ remediation: invalidFile
302
+ ? "Reduce or repair .gno/index.yml and retry."
303
+ : "Repair local file permissions and retry.",
304
+ },
305
+ ]
306
+ ),
307
+ exitCode: invalidFile ? 1 : 2,
308
+ };
309
+ }
310
+
311
+ const modelCache = options.offline
312
+ ? new ModelCache(getModelsCachePath())
313
+ : null;
314
+ const offlineAvailability =
315
+ options.isModelAvailableOffline ??
316
+ (modelCache ? modelCache.isCachedReadOnly.bind(modelCache) : undefined);
317
+ const compiled = await compileProjectProfileYaml(yaml, {
318
+ profileRoot: discovery.profileRoot,
319
+ config: configResult.config,
320
+ ...(offlineAvailability
321
+ ? { isModelAvailableOffline: offlineAvailability }
322
+ : {}),
323
+ });
324
+ if (!compiled.ok) {
325
+ return {
326
+ result: resultWithoutProfile(
327
+ options.command,
328
+ "invalid",
329
+ discovery.summary,
330
+ [
331
+ ...discoveryDiagnostics,
332
+ ...configResult.diagnostics,
333
+ ...compiled.diagnostics.map(compilerDiagnostic),
334
+ ]
335
+ ),
336
+ exitCode: 1,
337
+ };
338
+ }
339
+
340
+ let diff: ProjectProfileDiff | null = null;
341
+ const diagnostics = [
342
+ ...discoveryDiagnostics,
343
+ ...configResult.diagnostics,
344
+ ...compiled.value.diagnostics.map(compilerDiagnostic),
345
+ ];
346
+ if (options.command === "diff") {
347
+ const built = await buildProjectProfileDiff({
348
+ desiredState: compiled.value.desiredState,
349
+ expectedCollectionRoot: compiled.value.resolvedPaths.collectionRoot,
350
+ profileBinding: {
351
+ path: discovery.profilePath,
352
+ fingerprint: compiled.value.fingerprint,
353
+ collection: compiled.value.desiredState.collection.name,
354
+ },
355
+ config: configResult.config,
356
+ canonicalizePath: options.canonicalizePath ?? realpath,
357
+ });
358
+ diff = built.diff;
359
+ diagnostics.push(...built.diagnostics);
360
+ }
361
+
362
+ return {
363
+ result: {
364
+ schemaVersion: PROJECT_PROFILE_COMMAND_SCHEMA_VERSION,
365
+ command: options.command,
366
+ status: "valid",
367
+ valid: true,
368
+ discovery: discovery.summary,
369
+ profile: {
370
+ fingerprint: compiled.value.fingerprint,
371
+ desiredState:
372
+ options.command === "check" ? null : compiled.value.desiredState,
373
+ },
374
+ diff,
375
+ diagnostics: sortDiagnostics(diagnostics),
376
+ },
377
+ exitCode: 0,
378
+ };
379
+ }
380
+
381
+ export function formatProjectProfileResult(
382
+ result: ProjectProfileCommandResult,
383
+ options: { json: boolean }
384
+ ): string {
385
+ if (options.json) {
386
+ return JSON.stringify(result, null, 2);
387
+ }
388
+ const lines = [
389
+ `Profile ${result.status}: ${result.discovery.profile ?? "(not found)"}`,
390
+ ];
391
+ if (result.profile) {
392
+ lines.push(`Fingerprint: ${result.profile.fingerprint}`);
393
+ }
394
+ if (result.command === "show" && result.profile?.desiredState) {
395
+ lines.push(Bun.YAML.stringify(result.profile.desiredState).trimEnd());
396
+ }
397
+ if (result.diff) {
398
+ lines.push(`Diff: ${result.diff.status}`);
399
+ for (const change of result.diff.changes) {
400
+ lines.push(` ${change.action} ${change.field}: ${change.summary}`);
401
+ }
402
+ }
403
+ for (const issue of result.diagnostics) {
404
+ lines.push(
405
+ `${issue.severity === "error" ? "Error" : "Warning"} ${issue.code}: ${issue.message} ${issue.remediation}`
406
+ );
407
+ }
408
+ return lines.join("\n");
409
+ }