@gmickel/gno 1.24.0 → 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 (96) hide show
  1. package/README.md +18 -4
  2. package/assets/skill/SKILL.md +40 -17
  3. package/assets/skill/recipes/capture-and-file.md +20 -5
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/PRIVACY.md +55 -0
  7. package/browser-extension/dist/chunk-vn5f663b.js +50 -0
  8. package/browser-extension/dist/chunk-ydfx5d7p.css +1 -0
  9. package/browser-extension/dist/content.js +1 -0
  10. package/browser-extension/dist/manifest.json +25 -0
  11. package/browser-extension/dist/preview.html +13 -0
  12. package/browser-extension/dist/service-worker.js +40 -0
  13. package/package.json +13 -3
  14. package/spec/cli.md +196 -4
  15. package/spec/db/schema.sql +102 -1
  16. package/spec/mcp.md +10 -0
  17. package/spec/output-schemas/browser-clip-preview.schema.json +83 -0
  18. package/spec/output-schemas/browser-clip.schema.json +586 -0
  19. package/spec/output-schemas/capture-receipt.schema.json +22 -1
  20. package/spec/output-schemas/clipper-csrf.schema.json +12 -0
  21. package/spec/output-schemas/clipper-error.schema.json +46 -0
  22. package/spec/output-schemas/clipper-pair-approval.schema.json +17 -0
  23. package/spec/output-schemas/clipper-pair-start.schema.json +26 -0
  24. package/spec/output-schemas/clipper-pair-status.schema.json +46 -0
  25. package/spec/output-schemas/clipper-revoke.schema.json +28 -0
  26. package/spec/output-schemas/mcp-capture-result.schema.json +12 -1
  27. package/spec/output-schemas/project-profile-apply.schema.json +209 -0
  28. package/spec/output-schemas/project-profile-command.schema.json +160 -0
  29. package/spec/output-schemas/query-diagnose.schema.json +7 -1
  30. package/spec/output-schemas/setup-profile-result.schema.json +87 -0
  31. package/spec/project-profile.schema.json +301 -0
  32. package/src/cli/commands/collection/add.ts +39 -45
  33. package/src/cli/commands/collection/remove.ts +28 -28
  34. package/src/cli/commands/collection/rename.ts +55 -73
  35. package/src/cli/commands/context/add.ts +37 -26
  36. package/src/cli/commands/context/rm.ts +47 -20
  37. package/src/cli/commands/init.ts +55 -125
  38. package/src/cli/commands/models/use.ts +43 -38
  39. package/src/cli/commands/profile-apply.ts +334 -0
  40. package/src/cli/commands/profile.ts +409 -0
  41. package/src/cli/commands/setup-activation.ts +205 -54
  42. package/src/cli/commands/setup-profile.ts +223 -0
  43. package/src/cli/commands/setup.ts +3 -0
  44. package/src/cli/program.ts +110 -9
  45. package/src/config/index.ts +3 -0
  46. package/src/config/project-profile.ts +367 -0
  47. package/src/config/saver.ts +16 -7
  48. package/src/config/types.ts +42 -0
  49. package/src/core/browser-clip-provenance.ts +139 -0
  50. package/src/core/browser-clip.ts +473 -0
  51. package/src/core/capture-write.ts +5 -0
  52. package/src/core/capture.ts +75 -18
  53. package/src/core/config-mutation.ts +138 -76
  54. package/src/core/config-write-lock.ts +89 -0
  55. package/src/core/context-identity.ts +16 -0
  56. package/src/core/context-resolver.ts +2 -12
  57. package/src/core/file-lock.ts +20 -6
  58. package/src/core/folder-setup-planning.ts +6 -21
  59. package/src/core/folder-setup.ts +30 -2
  60. package/src/core/path-rules.ts +53 -0
  61. package/src/core/project-affinity-surface.ts +102 -7
  62. package/src/core/project-profile-apply-state.ts +268 -0
  63. package/src/core/project-profile-apply-validation.ts +95 -0
  64. package/src/core/project-profile-apply.ts +408 -0
  65. package/src/core/project-profile-canonical.ts +71 -0
  66. package/src/core/project-profile-diff.ts +302 -0
  67. package/src/core/project-profile-discovery.ts +519 -0
  68. package/src/core/project-profile-file.ts +37 -0
  69. package/src/core/project-profile-parser.ts +98 -0
  70. package/src/core/project-profile.ts +490 -0
  71. package/src/ingestion/walker.ts +85 -44
  72. package/src/llm/cache.ts +21 -0
  73. package/src/serve/capture-service.ts +420 -0
  74. package/src/serve/clipper-body.ts +62 -0
  75. package/src/serve/clipper-capture.ts +248 -0
  76. package/src/serve/clipper-contract.ts +57 -0
  77. package/src/serve/clipper-idempotency.ts +35 -0
  78. package/src/serve/clipper-pairing.ts +297 -0
  79. package/src/serve/clipper-security-errors.ts +23 -0
  80. package/src/serve/clipper-security.ts +449 -0
  81. package/src/serve/config-sync.ts +2 -2
  82. package/src/serve/public/app.tsx +8 -1
  83. package/src/serve/public/globals.built.css +1 -1
  84. package/src/serve/public/index.html +1 -0
  85. package/src/serve/public/lib/clipper-approval.ts +206 -0
  86. package/src/serve/public/pages/ClipperPairing.tsx +210 -0
  87. package/src/serve/resident-runtime.ts +1 -0
  88. package/src/serve/routes/api.ts +20 -115
  89. package/src/serve/routes/clipper.ts +394 -0
  90. package/src/serve/server.ts +22 -0
  91. package/src/store/migrations/020-browser-clipper-security.ts +128 -0
  92. package/src/store/migrations/021-multi-context-identity.ts +37 -0
  93. package/src/store/migrations/index.ts +4 -0
  94. package/src/store/sqlite/adapter.ts +83 -0
  95. package/src/store/sqlite/clipper-store-types.ts +104 -0
  96. package/src/store/sqlite/clipper-store.ts +496 -0
@@ -0,0 +1,301 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://gno.sh/schemas/project-profile-1.0.json",
4
+ "title": "GNO project retrieval profile",
5
+ "description": "Portable, declarative retrieval state for a single project collection.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "collection"],
9
+ "properties": {
10
+ "schemaVersion": {
11
+ "const": "1.0"
12
+ },
13
+ "collection": {
14
+ "$ref": "#/definitions/collection"
15
+ },
16
+ "contexts": {
17
+ "type": "array",
18
+ "maxItems": 64,
19
+ "uniqueItems": true,
20
+ "items": {
21
+ "$ref": "#/definitions/context"
22
+ },
23
+ "default": []
24
+ },
25
+ "contentTypes": {
26
+ "type": "object",
27
+ "maxProperties": 64,
28
+ "propertyNames": {
29
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
30
+ },
31
+ "additionalProperties": {
32
+ "$ref": "#/definitions/contentType"
33
+ },
34
+ "default": {}
35
+ },
36
+ "affinityDefaults": {
37
+ "$ref": "#/definitions/affinityDefaults"
38
+ },
39
+ "recommendedCapabilities": {
40
+ "type": "array",
41
+ "maxItems": 32,
42
+ "uniqueItems": true,
43
+ "items": {
44
+ "type": "string",
45
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
46
+ },
47
+ "default": []
48
+ }
49
+ },
50
+ "definitions": {
51
+ "portableRule": {
52
+ "type": "string",
53
+ "minLength": 1,
54
+ "maxLength": 512,
55
+ "allOf": [
56
+ {
57
+ "not": {
58
+ "type": "string",
59
+ "pattern": "^(?:[/\\\\]|[A-Za-z]:|~)"
60
+ }
61
+ },
62
+ {
63
+ "not": {
64
+ "type": "string",
65
+ "pattern": "(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)"
66
+ }
67
+ },
68
+ {
69
+ "not": {
70
+ "type": "string",
71
+ "pattern": "[{}]"
72
+ }
73
+ },
74
+ {
75
+ "not": {
76
+ "type": "string",
77
+ "pattern": "^!"
78
+ }
79
+ },
80
+ {
81
+ "not": {
82
+ "type": "string",
83
+ "pattern": "[<>:\"|\\u0000-\\u001f]"
84
+ }
85
+ },
86
+ {
87
+ "not": {
88
+ "type": "string",
89
+ "pattern": "(?:^|[\\\\/])(?:[cC][oO][nN]|[pP][rR][nN]|[aA][uU][xX]|[nN][uU][lL]|[cC][oO][mM][1-9]|[lL][pP][tT][1-9])(?:\\.[^\\\\/]*)?(?:[\\\\/]|$)"
90
+ }
91
+ },
92
+ {
93
+ "not": {
94
+ "type": "string",
95
+ "pattern": "(?:^|[\\\\/])(?:[^.\\\\/][^\\\\/]*|\\.[^\\\\/]+)[. ](?:[\\\\/]|$)"
96
+ }
97
+ },
98
+ {
99
+ "not": {
100
+ "type": "string",
101
+ "pattern": "(?:\\$\\{|\\$[A-Za-z_]|%[A-Za-z_][A-Za-z0-9_]*%)"
102
+ }
103
+ },
104
+ {
105
+ "not": {
106
+ "type": "string",
107
+ "pattern": "\\u0000"
108
+ }
109
+ }
110
+ ]
111
+ },
112
+ "portablePath": {
113
+ "allOf": [
114
+ {
115
+ "$ref": "#/definitions/portableRule"
116
+ },
117
+ {
118
+ "not": {
119
+ "type": "string",
120
+ "pattern": "[*?\\[\\]{}]"
121
+ }
122
+ },
123
+ {
124
+ "not": {
125
+ "type": "string",
126
+ "pattern": "(?:^|[\\\\/])\\.[gG][nN][oO](?:[\\\\/]|$)"
127
+ }
128
+ },
129
+ {
130
+ "not": {
131
+ "type": "string",
132
+ "pattern": "\\.(?:[dD][bB]|[sS][qQ][lL][iI][tT][eE](?:3)?|[gG][gG][uU][fF]|[lL][oO][cC][kK])$"
133
+ }
134
+ }
135
+ ]
136
+ },
137
+ "portableInclude": {
138
+ "allOf": [
139
+ {
140
+ "$ref": "#/definitions/portableRule"
141
+ },
142
+ {
143
+ "not": {
144
+ "type": "string",
145
+ "pattern": "(?:^|[\\\\/])\\.[gG][nN][oO](?:[\\\\/]|$)|\\.(?:[dD][bB]|[sS][qQ][lL][iI][tT][eE](?:3)?|[gG][gG][uU][fF]|[lL][oO][cC][kK])$"
146
+ }
147
+ }
148
+ ]
149
+ },
150
+ "collection": {
151
+ "type": "object",
152
+ "additionalProperties": false,
153
+ "required": ["name"],
154
+ "properties": {
155
+ "name": {
156
+ "type": "string",
157
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
158
+ },
159
+ "root": {
160
+ "$ref": "#/definitions/portablePath",
161
+ "default": "."
162
+ },
163
+ "include": {
164
+ "type": "array",
165
+ "minItems": 1,
166
+ "maxItems": 128,
167
+ "uniqueItems": true,
168
+ "items": {
169
+ "$ref": "#/definitions/portableInclude"
170
+ },
171
+ "default": ["**/*"]
172
+ },
173
+ "exclude": {
174
+ "type": "array",
175
+ "maxItems": 128,
176
+ "uniqueItems": true,
177
+ "items": {
178
+ "$ref": "#/definitions/portableRule"
179
+ },
180
+ "default": []
181
+ },
182
+ "languageHint": {
183
+ "type": "string",
184
+ "pattern": "^[A-Za-z]{2,3}(?:-[A-Za-z]{2}|-[A-Za-z]{4})?$"
185
+ },
186
+ "modelPreset": {
187
+ "type": "string",
188
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
189
+ }
190
+ }
191
+ },
192
+ "inlineContext": {
193
+ "type": "object",
194
+ "additionalProperties": false,
195
+ "required": ["text"],
196
+ "properties": {
197
+ "text": {
198
+ "type": "string",
199
+ "minLength": 1,
200
+ "maxLength": 65536
201
+ }
202
+ }
203
+ },
204
+ "fileContext": {
205
+ "type": "object",
206
+ "additionalProperties": false,
207
+ "required": ["file"],
208
+ "properties": {
209
+ "file": {
210
+ "allOf": [
211
+ {
212
+ "$ref": "#/definitions/portablePath"
213
+ },
214
+ {
215
+ "not": {
216
+ "type": "string",
217
+ "pattern": "(?:^|[\\\\/])(?:\\.[eE][nN][vV](?:\\..*)?|[cC][rR][eE][dD][eE][nN][tT][iI][aA][lL][sS]?(?:\\..*)?|[iI][dD]_(?:[rR][sS][aA]|[dD][sS][aA]|[eE][cC][dD][sS][aA]|[eE][dD]25519)(?:\\..*)?|[sS][eE][cC][rR][eE][tT][sS]?(?:\\..*)?|[^\\\\/]+\\.(?:[kK][eE][yY]|[pP][eE][mM]|[pP]12|[pP][fF][xX]))$"
218
+ }
219
+ }
220
+ ]
221
+ }
222
+ }
223
+ },
224
+ "context": {
225
+ "oneOf": [
226
+ {
227
+ "$ref": "#/definitions/inlineContext"
228
+ },
229
+ {
230
+ "$ref": "#/definitions/fileContext"
231
+ }
232
+ ]
233
+ },
234
+ "contentType": {
235
+ "type": "object",
236
+ "additionalProperties": false,
237
+ "required": ["prefixes", "preset"],
238
+ "properties": {
239
+ "prefixes": {
240
+ "type": "array",
241
+ "minItems": 1,
242
+ "maxItems": 64,
243
+ "uniqueItems": true,
244
+ "items": {
245
+ "$ref": "#/definitions/portablePath"
246
+ }
247
+ },
248
+ "preset": {
249
+ "enum": [
250
+ "blank",
251
+ "project-note",
252
+ "research-note",
253
+ "decision-note",
254
+ "prompt-pattern",
255
+ "source-summary",
256
+ "idea-original",
257
+ "person",
258
+ "company-project",
259
+ "meeting"
260
+ ]
261
+ },
262
+ "graphHints": {
263
+ "type": "array",
264
+ "maxItems": 16,
265
+ "uniqueItems": true,
266
+ "items": {
267
+ "type": "string",
268
+ "minLength": 1,
269
+ "maxLength": 64
270
+ }
271
+ },
272
+ "searchBoost": {
273
+ "type": "number"
274
+ },
275
+ "temporal": {
276
+ "type": "boolean"
277
+ }
278
+ }
279
+ },
280
+ "affinityDefaults": {
281
+ "type": "object",
282
+ "additionalProperties": false,
283
+ "properties": {
284
+ "enabled": {
285
+ "type": "boolean",
286
+ "default": true
287
+ },
288
+ "contribution": {
289
+ "type": "number",
290
+ "minimum": 0,
291
+ "maximum": 0.03,
292
+ "default": 0.03
293
+ }
294
+ },
295
+ "default": {
296
+ "enabled": true,
297
+ "contribution": 0.03
298
+ }
299
+ }
300
+ }
301
+ }
@@ -3,12 +3,8 @@
3
3
  */
4
4
 
5
5
  import { addCollection } from "../../../collection";
6
- import {
7
- loadConfig,
8
- pathExists,
9
- saveConfig,
10
- toAbsolutePath,
11
- } from "../../../config";
6
+ import { pathExists, toAbsolutePath } from "../../../config";
7
+ import { applyConfigFileChange } from "../../../core/config-mutation";
12
8
  import { CliError } from "../../errors";
13
9
 
14
10
  interface AddOptions {
@@ -18,6 +14,7 @@ interface AddOptions {
18
14
  include?: string;
19
15
  exclude?: string;
20
16
  update?: string;
17
+ configPath?: string;
21
18
  }
22
19
 
23
20
  export async function collectionAdd(
@@ -36,52 +33,49 @@ export async function collectionAdd(
36
33
  throw new CliError("VALIDATION", `Path does not exist: ${absolutePath}`);
37
34
  }
38
35
 
39
- // Load config
40
- const configResult = await loadConfig();
41
- if (!configResult.ok) {
42
- throw new CliError(
43
- "RUNTIME",
44
- `Failed to load config: ${configResult.error.message}`
45
- );
46
- }
47
-
48
- // Add collection using shared module
49
- const result = await addCollection(configResult.value, {
50
- path,
51
- name: options.name,
52
- pattern: options.pattern,
53
- include: options.include,
54
- exclude: options.exclude,
55
- models: options.embedModel
56
- ? {
57
- embed: options.embedModel,
58
- }
59
- : undefined,
60
- updateCmd: options.update,
61
- });
36
+ const mutation = await applyConfigFileChange(
37
+ { configPath: options.configPath },
38
+ async (config) => {
39
+ const result = await addCollection(config, {
40
+ path,
41
+ name: options.name!,
42
+ pattern: options.pattern,
43
+ include: options.include,
44
+ exclude: options.exclude,
45
+ models: options.embedModel
46
+ ? {
47
+ embed: options.embedModel,
48
+ }
49
+ : undefined,
50
+ updateCmd: options.update,
51
+ });
52
+ return result.ok
53
+ ? {
54
+ ok: true as const,
55
+ config: result.config,
56
+ value: result.collection,
57
+ }
58
+ : {
59
+ ok: false as const,
60
+ error: result.message,
61
+ code: result.code,
62
+ };
63
+ }
64
+ );
62
65
 
63
- if (!result.ok) {
66
+ if (!mutation.ok) {
64
67
  // Map collection error codes to CLI error codes
65
68
  const cliCode =
66
- result.code === "VALIDATION" ||
67
- result.code === "PATH_NOT_FOUND" ||
68
- result.code === "DUPLICATE"
69
+ mutation.code === "VALIDATION" ||
70
+ mutation.code === "PATH_NOT_FOUND" ||
71
+ mutation.code === "DUPLICATE"
69
72
  ? "VALIDATION"
70
73
  : "RUNTIME";
71
- throw new CliError(cliCode, result.message);
72
- }
73
-
74
- // Save config
75
- const saveResult = await saveConfig(result.config);
76
- if (!saveResult.ok) {
77
- throw new CliError(
78
- "RUNTIME",
79
- `Failed to save config: ${saveResult.error.message}`
80
- );
74
+ throw new CliError(cliCode, mutation.error);
81
75
  }
82
76
 
83
77
  process.stdout.write(
84
- `Collection "${result.collection.name}" added successfully\n`
78
+ `Collection "${mutation.value?.name}" added successfully\n`
85
79
  );
86
- process.stdout.write(`Path: ${result.collection.path}\n`);
80
+ process.stdout.write(`Path: ${mutation.value?.path}\n`);
87
81
  }
@@ -3,43 +3,43 @@
3
3
  */
4
4
 
5
5
  import { removeCollection } from "../../../collection";
6
- import { loadConfig, saveConfig } from "../../../config";
6
+ import { applyConfigFileChange } from "../../../core/config-mutation";
7
7
  import { CliError } from "../../errors";
8
8
 
9
- export async function collectionRemove(name: string): Promise<void> {
10
- // Load config
11
- const configResult = await loadConfig();
12
- if (!configResult.ok) {
13
- throw new CliError(
14
- "RUNTIME",
15
- `Failed to load config: ${configResult.error.message}`
16
- );
17
- }
18
-
19
- // Remove collection using shared module
20
- const result = removeCollection(configResult.value, { name });
9
+ export async function collectionRemove(
10
+ name: string,
11
+ options: { configPath?: string } = {}
12
+ ): Promise<void> {
13
+ const mutation = await applyConfigFileChange(
14
+ { configPath: options.configPath },
15
+ (config) => {
16
+ const result = removeCollection(config, { name });
17
+ return result.ok
18
+ ? {
19
+ ok: true as const,
20
+ config: result.config,
21
+ value: result.collection,
22
+ }
23
+ : {
24
+ ok: false as const,
25
+ error: result.message,
26
+ code: result.code,
27
+ };
28
+ }
29
+ );
21
30
 
22
- if (!result.ok) {
31
+ if (!mutation.ok) {
23
32
  // Map collection error codes to CLI error codes
24
33
  const cliCode =
25
- result.code === "VALIDATION" ||
26
- result.code === "NOT_FOUND" ||
27
- result.code === "HAS_REFERENCES"
34
+ mutation.code === "VALIDATION" ||
35
+ mutation.code === "NOT_FOUND" ||
36
+ mutation.code === "HAS_REFERENCES"
28
37
  ? "VALIDATION"
29
38
  : "RUNTIME";
30
- throw new CliError(cliCode, result.message);
31
- }
32
-
33
- // Save config
34
- const saveResult = await saveConfig(result.config);
35
- if (!saveResult.ok) {
36
- throw new CliError(
37
- "RUNTIME",
38
- `Failed to save config: ${saveResult.error.message}`
39
- );
39
+ throw new CliError(cliCode, mutation.error);
40
40
  }
41
41
 
42
42
  process.stdout.write(
43
- `Collection "${result.collection.name}" removed successfully\n`
43
+ `Collection "${mutation.value?.name}" removed successfully\n`
44
44
  );
45
45
  }
@@ -2,90 +2,72 @@
2
2
  * gno collection rename - Rename a collection
3
3
  */
4
4
 
5
- import {
6
- CollectionSchema,
7
- getCollectionFromScope,
8
- loadConfig,
9
- saveConfig,
10
- } from "../../../config";
5
+ import { CollectionSchema, getCollectionFromScope } from "../../../config";
6
+ import { applyConfigFileChange } from "../../../core/config-mutation";
11
7
  import { CliError } from "../../errors";
12
8
 
13
9
  export async function collectionRename(
14
10
  oldName: string,
15
- newName: string
11
+ newName: string,
12
+ options: { configPath?: string } = {}
16
13
  ): Promise<void> {
17
14
  const oldCollectionName = oldName.toLowerCase();
18
15
  const newCollectionName = newName.toLowerCase();
19
16
 
20
- // Load config
21
- const result = await loadConfig();
22
- if (!result.ok) {
23
- throw new CliError(
24
- "RUNTIME",
25
- `Failed to load config: ${result.error.message}`
26
- );
27
- }
28
-
29
- const config = result.value;
30
-
31
- // Find old collection
32
- const collection = config.collections.find(
33
- (c) => c.name === oldCollectionName
34
- );
35
- if (!collection) {
36
- throw new CliError(
37
- "VALIDATION",
38
- `Collection "${oldCollectionName}" not found`
39
- );
40
- }
41
-
42
- // Check if new name already exists
43
- const existingNew = config.collections.find(
44
- (c) => c.name === newCollectionName
45
- );
46
- if (existingNew) {
47
- throw new CliError(
48
- "VALIDATION",
49
- `Collection "${newCollectionName}" already exists`
50
- );
51
- }
52
-
53
- // Validate new name
54
- const testCollection = { ...collection, name: newCollectionName };
55
- const validation = CollectionSchema.safeParse(testCollection);
56
- if (!validation.success) {
57
- throw new CliError(
58
- "VALIDATION",
59
- `Invalid collection name: ${validation.error.issues[0]?.message ?? "unknown error"}`
60
- );
61
- }
62
-
63
- // Rename collection
64
- collection.name = newCollectionName;
65
-
66
- // Update contexts that reference this collection
67
- for (const context of config.contexts) {
68
- const collFromScope = getCollectionFromScope(context.scopeKey);
69
- if (collFromScope === oldCollectionName) {
70
- // Update scope key
71
- if (context.scopeType === "collection") {
72
- context.scopeKey = `${newCollectionName}:`;
73
- } else if (context.scopeType === "prefix") {
74
- // Replace collection name in URI
75
- context.scopeKey = context.scopeKey.replace(
76
- `gno://${oldCollectionName}/`,
77
- `gno://${newCollectionName}/`
78
- );
17
+ const mutation = await applyConfigFileChange(
18
+ { configPath: options.configPath },
19
+ (config) => {
20
+ const collection = config.collections.find(
21
+ (item) => item.name === oldCollectionName
22
+ );
23
+ if (!collection) {
24
+ return {
25
+ ok: false as const,
26
+ error: `Collection "${oldCollectionName}" not found`,
27
+ code: "NOT_FOUND",
28
+ };
79
29
  }
30
+ if (config.collections.some((item) => item.name === newCollectionName)) {
31
+ return {
32
+ ok: false as const,
33
+ error: `Collection "${newCollectionName}" already exists`,
34
+ code: "DUPLICATE",
35
+ };
36
+ }
37
+ const validation = CollectionSchema.safeParse({
38
+ ...collection,
39
+ name: newCollectionName,
40
+ });
41
+ if (!validation.success) {
42
+ return {
43
+ ok: false as const,
44
+ error: `Invalid collection name: ${validation.error.issues[0]?.message ?? "unknown error"}`,
45
+ code: "VALIDATION",
46
+ };
47
+ }
48
+ collection.name = newCollectionName;
49
+ for (const context of config.contexts) {
50
+ if (getCollectionFromScope(context.scopeKey) !== oldCollectionName) {
51
+ continue;
52
+ }
53
+ if (context.scopeType === "collection") {
54
+ context.scopeKey = `${newCollectionName}:`;
55
+ } else if (context.scopeType === "prefix") {
56
+ context.scopeKey = context.scopeKey.replace(
57
+ `gno://${oldCollectionName}/`,
58
+ `gno://${newCollectionName}/`
59
+ );
60
+ }
61
+ }
62
+ return { ok: true as const, config };
80
63
  }
81
- }
82
-
83
- // Save config
84
- const saveResult = await saveConfig(config);
85
- if (!saveResult.ok) {
64
+ );
65
+ if (!mutation.ok) {
86
66
  throw new CliError(
87
- "RUNTIME",
88
- `Failed to save config: ${saveResult.error.message}`
67
+ ["NOT_FOUND", "DUPLICATE", "VALIDATION"].includes(mutation.code)
68
+ ? "VALIDATION"
69
+ : "RUNTIME",
70
+ mutation.error
89
71
  );
90
72
  }
91
73