@ggui-ai/cli 0.6.2 → 0.7.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 (39) hide show
  1. package/dist/auth-keys.d.ts +3 -1
  2. package/dist/auth-keys.d.ts.map +1 -1
  3. package/dist/auth-keys.js +42 -2
  4. package/dist/blueprint-command.d.ts.map +1 -1
  5. package/dist/blueprint-command.js +13 -13
  6. package/dist/cli.js +10 -0
  7. package/dist/config-push.d.ts +0 -8
  8. package/dist/config-push.d.ts.map +1 -1
  9. package/dist/config-push.js +10 -35
  10. package/dist/gadget-command.d.ts +1 -1
  11. package/dist/gadget-command.d.ts.map +1 -1
  12. package/dist/gadget-command.js +35 -12
  13. package/dist/internal/artifact-install.d.ts.map +1 -1
  14. package/dist/internal/artifact-install.js +125 -100
  15. package/dist/internal/artifact-publish.d.ts +37 -35
  16. package/dist/internal/artifact-publish.d.ts.map +1 -1
  17. package/dist/internal/artifact-publish.js +33 -97
  18. package/dist/internal/artifact-search.d.ts +27 -21
  19. package/dist/internal/artifact-search.d.ts.map +1 -1
  20. package/dist/internal/artifact-search.js +14 -128
  21. package/dist/internal/artifact-uninstall.d.ts +8 -5
  22. package/dist/internal/artifact-uninstall.d.ts.map +1 -1
  23. package/dist/internal/artifact-uninstall.js +133 -26
  24. package/dist/internal/ggui-json.d.ts +35 -4
  25. package/dist/internal/ggui-json.d.ts.map +1 -1
  26. package/dist/internal/ggui-json.js +58 -7
  27. package/dist/internal/register-author-key.d.ts +10 -0
  28. package/dist/internal/register-author-key.d.ts.map +1 -1
  29. package/dist/internal/register-author-key.js +14 -7
  30. package/dist/internal/registry-url.d.ts +49 -0
  31. package/dist/internal/registry-url.d.ts.map +1 -0
  32. package/dist/internal/registry-url.js +137 -0
  33. package/dist/mcp-backend.d.ts +6 -0
  34. package/dist/mcp-backend.d.ts.map +1 -1
  35. package/dist/mcp-backend.js +9 -0
  36. package/dist/serve-command.d.ts +14 -1
  37. package/dist/serve-command.d.ts.map +1 -1
  38. package/dist/serve-command.js +29 -0
  39. package/package.json +17 -17
@@ -1,3 +1,28 @@
1
+ /**
2
+ * `ggui gadget search [q]` — query the marketplace registry's
3
+ * `GET /search` endpoint and render results.
4
+ *
5
+ * The consumer-side discovery surface, companion to `gadget create`
6
+ * and `gadget publish`. Public and unauthenticated; the registry's
7
+ * `/search` route returns only `visibility: "public"` rows.
8
+ *
9
+ * Registry resolution rides the shared resolver (`./registry-url.ts`)
10
+ * with the READ-verb posture (highest priority wins):
11
+ *
12
+ * 1. `--registry <url>` flag.
13
+ * 2. `GGUI_REGISTRY` environment variable.
14
+ * 3. `ggui.json#registry` field (walks up from `cwd`).
15
+ * 4. The public default registry.
16
+ *
17
+ * Kept pure / testable — no `process.exit`, no direct stdout writes.
18
+ * Returns a `SearchOutput` discriminated union; the CLI driver in
19
+ * `gadget-command.ts` renders copy + picks the exit code.
20
+ *
21
+ * The wire shapes (`SearchResultEntry` / `SearchResponse` /
22
+ * `ArtifactKind`) are imported from `@ggui-ai/registry-core` — the
23
+ * canonical owner of the registry HTTP response types — and
24
+ * re-exported for this module's consumers.
25
+ */
1
26
  import { type ArtifactKind, type SearchResponse, type SearchResultEntry } from '@ggui-ai/registry-core';
2
27
  export type { ArtifactKind, SearchResponse, SearchResultEntry };
3
28
  /**
@@ -44,26 +69,6 @@ export interface ParsedSearchFlags {
44
69
  * Accepts both `--flag value` and `--flag=value` for every option.
45
70
  */
46
71
  export declare function parseArtifactSearchFlags(lockedKind: ArtifactKind | undefined, args: readonly string[]): ParsedSearchFlags;
47
- /**
48
- * Three-layer resolution: `--registry` flag > `GGUI_REGISTRY` env >
49
- * `ggui.json#registry` > error.
50
- *
51
- * `cwd` + `env` are injectable for tests. Returns either `{ url }` on
52
- * success or `{ error }` with a stderr-ready diagnostic. Trailing
53
- * slashes on the URL are normalized away so the URL builder can append
54
- * `/search?…` deterministically.
55
- */
56
- export declare function resolveRegistryUrl(args: {
57
- flag?: string;
58
- cwd: string;
59
- env: {
60
- readonly GGUI_REGISTRY?: string | undefined;
61
- };
62
- }): {
63
- url: string;
64
- } | {
65
- error: string;
66
- };
67
72
  /**
68
73
  * Build a query string from {@link ArtifactSearchFlags}. Pulled out so
69
74
  * tests can assert the wire shape without a real fetch. Empty / absent
@@ -111,7 +116,8 @@ export interface RunArtifactSearchDeps {
111
116
  /**
112
117
  * Run a search end-to-end:
113
118
  *
114
- * 1. Resolve the registry URL via the three-layer chain.
119
+ * 1. Resolve the registry URL via the shared chain (READ-verb
120
+ * posture: flag > env > ggui.json > public default).
115
121
  * 2. Build the query string.
116
122
  * 3. GET `<registry>/search?<qs>` (no auth; public endpoint).
117
123
  * 4. Parse + validate the response shape.
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-search.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-search.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"artifact-search.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAEL,KAAK,YAAY,EAEjB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACvB,MAAM,wBAAwB,CAAC;AAGhC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,CAAC;AAMhE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAC7B,uCAAuC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;IACrC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,YAAY,GAAG,SAAS,EACpC,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,iBAAiB,CAuInB;AAMD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,CAWzE;AAMD,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EACT,aAAa,GACb,kBAAkB,GAClB,eAAe,GACf,YAAY,GACZ,cAAc,CAAC;IACnB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,aAAa,CAAC;AAMzD;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC9D,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;CAC9B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAuFvB;AAwGD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAwC1D"}
@@ -6,13 +6,13 @@
6
6
  * and `gadget publish`. Public and unauthenticated; the registry's
7
7
  * `/search` route returns only `visibility: "public"` rows.
8
8
  *
9
- * Three-layer registry resolution (highest priority wins, mirrors
10
- * `gadget publish` + `ggui install`):
9
+ * Registry resolution rides the shared resolver (`./registry-url.ts`)
10
+ * with the READ-verb posture (highest priority wins):
11
11
  *
12
12
  * 1. `--registry <url>` flag.
13
13
  * 2. `GGUI_REGISTRY` environment variable.
14
14
  * 3. `ggui.json#registry` field (walks up from `cwd`).
15
- * 4. Error if unset — no hard-coded default.
15
+ * 4. The public default registry.
16
16
  *
17
17
  * Kept pure / testable — no `process.exit`, no direct stdout writes.
18
18
  * Returns a `SearchOutput` discriminated union; the CLI driver in
@@ -23,17 +23,8 @@
23
23
  * canonical owner of the registry HTTP response types — and
24
24
  * re-exported for this module's consumers.
25
25
  */
26
- import { existsSync, readFileSync } from 'node:fs';
27
- import { dirname, join, resolve } from 'node:path';
28
26
  import { SEARCH_ERROR_CODES, } from '@ggui-ai/registry-core';
29
- /**
30
- * Filename constant for the project manifest. Duplicated from
31
- * `@ggui-ai/project-config` rather than imported to keep this
32
- * subcommand from pulling in the full project-config dependency tree.
33
- */
34
- const GGUI_JSON_FILENAME = 'ggui.json';
35
- /** How many parent directories the upward walk will inspect. */
36
- const FIND_MAX_DEPTH = 8;
27
+ import { DEFAULT_REGISTRY_URL, resolveRegistryUrl } from './registry-url.js';
37
28
  /**
38
29
  * Flag parser. Returns a discriminated `{error}` vs. `{flags}` so the
39
30
  * caller doesn't throw on user input. The `lockedKind` argument is the
@@ -175,113 +166,6 @@ export function parseArtifactSearchFlags(lockedKind, args) {
175
166
  return { flags };
176
167
  }
177
168
  /* -------------------------------------------------------------------------- */
178
- /* Registry URL resolution */
179
- /* -------------------------------------------------------------------------- */
180
- /**
181
- * Three-layer resolution: `--registry` flag > `GGUI_REGISTRY` env >
182
- * `ggui.json#registry` > error.
183
- *
184
- * `cwd` + `env` are injectable for tests. Returns either `{ url }` on
185
- * success or `{ error }` with a stderr-ready diagnostic. Trailing
186
- * slashes on the URL are normalized away so the URL builder can append
187
- * `/search?…` deterministically.
188
- */
189
- export function resolveRegistryUrl(args) {
190
- const candidate = args.flag ??
191
- (typeof args.env.GGUI_REGISTRY === 'string' && args.env.GGUI_REGISTRY.length > 0
192
- ? args.env.GGUI_REGISTRY
193
- : undefined);
194
- if (typeof candidate === 'string' && candidate.length > 0) {
195
- return normalizeRegistryUrl(candidate);
196
- }
197
- const gguiJsonPath = findGguiJson(args.cwd);
198
- if (gguiJsonPath !== null) {
199
- const loaded = readGguiJsonRegistryField(gguiJsonPath);
200
- if ('error' in loaded) {
201
- return { error: loaded.error };
202
- }
203
- if (loaded.registry !== undefined) {
204
- return normalizeRegistryUrl(loaded.registry);
205
- }
206
- }
207
- return {
208
- error: 'no registry configured. Pass --registry <url>, set GGUI_REGISTRY, or add `registry` to ggui.json.',
209
- };
210
- }
211
- /**
212
- * Walk up from `startDir` looking for a `ggui.json`. Returns the
213
- * absolute path of the first match, or `null` if none found within
214
- * {@link FIND_MAX_DEPTH} levels. Mirrors the algorithm in
215
- * `@ggui-ai/project-config/node`'s `findGguiJson` — duplicated here
216
- * to keep this subcommand free of the project-config dependency.
217
- */
218
- function findGguiJson(startDir) {
219
- let dir = resolve(startDir);
220
- for (let i = 0; i <= FIND_MAX_DEPTH; i++) {
221
- const candidate = join(dir, GGUI_JSON_FILENAME);
222
- if (existsSync(candidate))
223
- return candidate;
224
- const parent = dirname(dir);
225
- if (parent === dir)
226
- return null;
227
- dir = parent;
228
- }
229
- return null;
230
- }
231
- /**
232
- * Read just the `registry` field from a `ggui.json`. We don't run the
233
- * full schema parser here — that would couple this command to every
234
- * other field in the manifest, and a corrupt `gadgets` block
235
- * (unrelated to search) would prevent `gadget search` from running.
236
- * The field is validated structurally: must be a string. Schema-level
237
- * URL validation happens downstream in {@link normalizeRegistryUrl}.
238
- */
239
- function readGguiJsonRegistryField(path) {
240
- let raw;
241
- try {
242
- raw = readFileSync(path, 'utf-8');
243
- }
244
- catch (err) {
245
- return {
246
- error: `failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`,
247
- };
248
- }
249
- let decoded;
250
- try {
251
- decoded = JSON.parse(raw);
252
- }
253
- catch (err) {
254
- return {
255
- error: `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
256
- };
257
- }
258
- if (decoded === null || typeof decoded !== 'object' || Array.isArray(decoded)) {
259
- return { error: `${path} is not a JSON object` };
260
- }
261
- const reg = decoded.registry;
262
- if (reg === undefined)
263
- return { registry: undefined };
264
- if (typeof reg !== 'string') {
265
- return { error: `${path}#registry must be a string` };
266
- }
267
- return { registry: reg };
268
- }
269
- function normalizeRegistryUrl(raw) {
270
- let parsed;
271
- try {
272
- parsed = new URL(raw);
273
- }
274
- catch {
275
- return { error: `registry URL is malformed: ${raw}` };
276
- }
277
- // Strip trailing slashes so we can append `/search` deterministically.
278
- // `URL` already normalized scheme + host casing.
279
- let serialized = parsed.toString();
280
- while (serialized.endsWith('/'))
281
- serialized = serialized.slice(0, -1);
282
- return { url: serialized };
283
- }
284
- /* -------------------------------------------------------------------------- */
285
169
  /* Query string */
286
170
  /* -------------------------------------------------------------------------- */
287
171
  /**
@@ -314,7 +198,8 @@ export function buildSearchQueryString(flags) {
314
198
  /**
315
199
  * Run a search end-to-end:
316
200
  *
317
- * 1. Resolve the registry URL via the three-layer chain.
201
+ * 1. Resolve the registry URL via the shared chain (READ-verb
202
+ * posture: flag > env > ggui.json > public default).
318
203
  * 2. Build the query string.
319
204
  * 3. GET `<registry>/search?<qs>` (no auth; public endpoint).
320
205
  * 4. Parse + validate the response shape.
@@ -327,13 +212,13 @@ export async function runArtifactSearch(flags, deps) {
327
212
  ...(flags.registry !== undefined ? { flag: flags.registry } : {}),
328
213
  cwd: deps.cwd,
329
214
  env: deps.env,
215
+ defaultUrl: DEFAULT_REGISTRY_URL,
330
216
  });
331
- if ('error' in resolved) {
332
- const isNoRegistryError = resolved.error.startsWith('no registry');
217
+ if (!resolved.ok) {
333
218
  return {
334
219
  ok: false,
335
- code: isNoRegistryError ? 'no-registry' : 'invalid-registry',
336
- message: resolved.error,
220
+ code: resolved.code,
221
+ message: resolved.message,
337
222
  };
338
223
  }
339
224
  const qs = buildSearchQueryString(flags);
@@ -522,9 +407,10 @@ ${hookLines} --tag <tag> Filter by tag (exact match).
522
407
  --limit <n> Page size; 1..200, default 50.
523
408
  --cursor <opaque> Opaque cursor from a previous response —
524
409
  roundtrip verbatim for pagination.
525
- --registry <url> Override the registry URL. Three-layer
526
- resolution: flag > GGUI_REGISTRY env >
527
- ggui.json#registry > error if unset.
410
+ --registry <url> Override the registry URL. Resolution:
411
+ flag > GGUI_REGISTRY env >
412
+ ggui.json#registry > default
413
+ (\`${DEFAULT_REGISTRY_URL}\`).
528
414
  --json Emit the raw JSON response body instead of
529
415
  the human-readable table.
530
416
  --help, -h Show this help.
@@ -16,13 +16,16 @@ export interface ArtifactUninstallDeps {
16
16
  }
17
17
  export interface ArtifactUninstallResult {
18
18
  readonly exitCode: number;
19
- /** `true` when a directory was actually removed. `false` on idempotent no-ops. */
19
+ /** `true` when something was actually removed (a blueprint's install
20
+ * directory, or a gadget's `app.gadgets` row). `false` on idempotent
21
+ * no-ops. */
20
22
  readonly removed: boolean;
21
23
  /**
22
- * `true` when `INSTALLED_BLUEPRINTS_GLOB` was stripped from
23
- * `ggui.json#blueprints.include` (i.e. no installed-blueprints
24
- * subdirs remained). `false` when the glob was kept (other installs
25
- * still on disk) or wasn't present to begin with.
24
+ * Blueprint-only signal: `true` when `INSTALLED_BLUEPRINTS_GLOB` was
25
+ * stripped from `ggui.json#blueprints.include` (i.e. no
26
+ * installed-blueprints subdirs remained). `false` when the glob was
27
+ * kept (other installs still on disk), wasn't present to begin with,
28
+ * or the uninstall was a gadget (gadgets never touch the glob).
26
29
  */
27
30
  readonly globRemoved: boolean;
28
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-uninstall.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-uninstall.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAM1D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CA4B7D;AAMD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,uBAAuB,GAC/B,4BAA4B,GAC5B;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/B,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,uBAAuB,CAwCzB;AAMD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,4BAA4B,EACnC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,uBAAuB,CAAC,CAmFlC"}
1
+ {"version":3,"file":"artifact-uninstall.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-uninstall.ts"],"names":[],"mappings":"AAsDA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAM1D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CA0C7D;AAMD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,uBAAuB,GAC/B,4BAA4B,GAC5B;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/B,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,uBAAuB,CAwCzB;AAMD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;iBAEa;IACb,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,4BAA4B,EACnC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,uBAAuB,CAAC,CA6HlC"}
@@ -1,12 +1,23 @@
1
1
  /**
2
- * `ggui blueprint uninstall <scope/name>@<version>`.
2
+ * `ggui {gadget,blueprint} uninstall <scope/name>@<version>` — the
3
+ * kind-discriminated reverse of `runArtifactInstall`, one branch per
4
+ * install effect:
3
5
  *
4
- * Reverse of `runArtifactInstall`'s on-disk materialization. Removes
5
- * `.ggui/installed-blueprints/<scope-bare>__<name>__<version>/`
6
- * from the project root and — when this was the LAST remaining
7
- * installed-blueprint subdir — strips the `INSTALLED_BLUEPRINTS_GLOB`
8
- * entry from `ggui.json#blueprints.include` so a future `ggui serve`
9
- * doesn't walk an empty glob.
6
+ * - **blueprint** — removes
7
+ * `.ggui/installed-blueprints/<scope-bare>__<name>__<version>/`
8
+ * from the project root and — when this was the LAST remaining
9
+ * installed-blueprint subdir — strips the `INSTALLED_BLUEPRINTS_GLOB`
10
+ * entry from `ggui.json#blueprints.include` so a future `ggui serve`
11
+ * doesn't walk an empty glob.
12
+ * - **gadget** — removes the PACKAGE's row from
13
+ * `ggui.json#app.gadgets` (the row `runArtifactInstall` appended).
14
+ * Rows are keyed by package — one row per package is the catalog
15
+ * invariant, and install replaces the row on a version change — so
16
+ * the supplied version does not gate the match; when it differs
17
+ * from the installed one, the actual removed version is reported.
18
+ * When it was the last row, the empty `gadgets` array is dropped
19
+ * too, keeping the manifest tidy. Nothing on disk to delete —
20
+ * gadget installs never materialize files.
10
21
  *
11
22
  * What this does NOT touch:
12
23
  *
@@ -28,28 +39,16 @@
28
39
  * shouldn't be punished for re-running the command.
29
40
  */
30
41
  import { existsSync, readdirSync, rmSync } from 'node:fs';
31
- import { join } from 'node:path';
32
- import { findGguiJson, readGguiJson, writeGguiJson, } from './ggui-json.js';
42
+ import { dirname, join } from 'node:path';
43
+ import { FIND_MAX_DEPTH, findGguiJson, readGguiJson, writeGguiJson, } from './ggui-json.js';
33
44
  import { blueprintInstallSubdir, INSTALLED_BLUEPRINTS_GLOB, INSTALLED_BLUEPRINTS_SUBDIR, } from './artifact-install.js';
34
45
  /* -------------------------------------------------------------------------- */
35
46
  /* Help text */
36
47
  /* -------------------------------------------------------------------------- */
37
48
  export function buildUninstallHelp(kind) {
38
- const noun = kind === 'blueprint' ? 'blueprint' : 'gadget';
39
- const verb = `ggui ${noun} uninstall`;
40
- return `${verb} — remove a marketplace-installed ${noun} from this project.
41
-
42
- Usage:
43
- ${verb} <scope/name>@<version> [options]
44
-
45
- Arguments:
46
- <scope/name>@<version> Full install identifier. Same shape as install.
47
-
48
- Options:
49
- --help Show this help.
50
-
51
- Behavior:
52
- - Removes \`.ggui/installed-blueprints/<scope-bare>__<name>__<version>/\` from disk.
49
+ const verb = `ggui ${kind} uninstall`;
50
+ const behavior = kind === 'blueprint'
51
+ ? ` - Removes \`.ggui/installed-blueprints/<scope-bare>__<name>__<version>/\` from disk.
53
52
  - When this was the LAST installed-blueprint subdir, strips the
54
53
  \`${INSTALLED_BLUEPRINTS_GLOB}\` entry from \`ggui.json#blueprints.include\`.
55
54
  Other globs in the include list are preserved verbatim.
@@ -61,7 +60,32 @@ Caveats:
61
60
  (the install-to-cache bridge is per-scope idempotent within a server
62
61
  lifetime). Restart to reclaim the cache slot.
63
62
  - This is local-only. The published marketplace artifact is unchanged.
63
+ `
64
+ : ` - Removes the package's entry from \`ggui.json#app.gadgets\`
65
+ (the entry install appended). Entries are keyed by package — if the
66
+ installed version differs from the one you supply, the entry is
67
+ still removed and the actual version is reported. When it was
68
+ the last entry, the empty \`gadgets\` array is dropped too. Other
69
+ entries are preserved verbatim.
70
+ - Idempotent: uninstalling a never-installed identifier exits 0
71
+ with a stderr note. Re-runs are safe.
72
+
73
+ Caveats:
74
+ - This is local-only. The published marketplace artifact is unchanged.
64
75
  `;
76
+ return `${verb} — remove a marketplace-installed ${kind} from this project.
77
+
78
+ Usage:
79
+ ${verb} <scope/name>@<version> [options]
80
+
81
+ Arguments:
82
+ <scope/name>@<version> Full install identifier. Same shape as install.
83
+
84
+ Options:
85
+ --help Show this help.
86
+
87
+ Behavior:
88
+ ${behavior}`;
65
89
  }
66
90
  export function parseArtifactUninstallFlags(kind, args) {
67
91
  let positional;
@@ -109,7 +133,7 @@ export async function runArtifactUninstall(flags, deps) {
109
133
  const stderr = deps.stderr ?? ((s) => void process.stderr.write(s));
110
134
  const gguiPath = findGguiJson(deps.cwd);
111
135
  if (gguiPath === null) {
112
- stderr(`${verb}: no ggui.json found in ${deps.cwd} or any ancestor (up to 8 levels).\n`);
136
+ stderr(`${verb}: no ggui.json found in ${deps.cwd} or any ancestor (up to ${FIND_MAX_DEPTH} levels).\n`);
113
137
  return { exitCode: 2, removed: false, globRemoved: false };
114
138
  }
115
139
  const loaded = readGguiJson(gguiPath);
@@ -118,7 +142,38 @@ export async function runArtifactUninstall(flags, deps) {
118
142
  return { exitCode: 2, removed: false, globRemoved: false };
119
143
  }
120
144
  const gguiJson = loaded.value;
121
- const projectRoot = gguiPath.slice(0, gguiPath.lastIndexOf('/'));
145
+ // ---- gadget branch: remove the ggui.json#app.gadgets row ----
146
+ // Gadget installs never materialize files; the whole install effect
147
+ // is the package-keyed row `appendGadget` wrote. Reverse it: rows
148
+ // are keyed by PACKAGE (one row per package is the catalog
149
+ // invariant, and install replaces the row on a version change), so
150
+ // uninstall matches by package alone — otherwise uninstalling the
151
+ // originally-installed version after an upgrade would report
152
+ // "nothing to remove" while the row is still live.
153
+ if (flags.kind === 'gadget') {
154
+ const rowRes = removeGadgetRow(gguiJson, flags.artifactId);
155
+ if ('error' in rowRes) {
156
+ stderr(`${verb}: ${rowRes.error}\n`);
157
+ return { exitCode: 1, removed: false, globRemoved: false };
158
+ }
159
+ if (!rowRes.removed) {
160
+ stderr(`${verb}: ${flags.artifactId} is not installed (no matching entry in ${gguiPath}#app.gadgets) — nothing to remove.\n`);
161
+ return { exitCode: 0, removed: false, globRemoved: false };
162
+ }
163
+ writeGguiJson(gguiPath, gguiJson);
164
+ for (const v of rowRes.removedVersions) {
165
+ stdout(`removed: ${flags.artifactId}@${v} from ggui.json#app.gadgets\n`);
166
+ }
167
+ const mismatched = rowRes.removedVersions.filter((v) => v !== flags.version);
168
+ if (mismatched.length > 0) {
169
+ stdout(`note: you supplied @${flags.version}, but the installed version was ${mismatched.join(', ')} — gadget rows are keyed by package (one row per package), so the package's row was removed.\n`);
170
+ }
171
+ stdout(`\nUninstall complete.\n`);
172
+ return { exitCode: 0, removed: true, globRemoved: false };
173
+ }
174
+ // ---- blueprint branch: remove the materialized install directory ----
175
+ // `dirname` (not a hand-rolled '/'-slice) so Windows paths resolve.
176
+ const projectRoot = dirname(gguiPath);
122
177
  const scope = flags.artifactId.slice(1, flags.artifactId.indexOf('/'));
123
178
  const name = flags.artifactId.slice(flags.artifactId.indexOf('/') + 1);
124
179
  const subdir = blueprintInstallSubdir({
@@ -172,6 +227,58 @@ export async function runArtifactUninstall(flags, deps) {
172
227
  }
173
228
  return { exitCode: 0, removed, globRemoved };
174
229
  }
230
+ /**
231
+ * Remove every row of `artifactId`'s PACKAGE from
232
+ * `ggui.json#app.gadgets`. Exact reverse of the append: rows are keyed
233
+ * by package (one row per package is the catalog invariant, and
234
+ * install replaces the row on any version change), so the supplied
235
+ * version is NOT part of the match — uninstalling with a stale version
236
+ * still removes the package's row; the caller reports the versions
237
+ * that actually went away. Install keeps the catalog at one row per
238
+ * package, but hand-edited manifests can carry duplicates — uninstall
239
+ * removes ALL matches so "Uninstall complete" is never a half-truth.
240
+ * When the removal empties the array, the `gadgets` key is dropped so
241
+ * the manifest stays tidy (mirrors the blueprint branch's last-install
242
+ * glob cleanup).
243
+ *
244
+ * Structural errors (`app` / `app.gadgets` present but wrong-shaped)
245
+ * surface as `{ error }`; a missing block is simply "not installed".
246
+ */
247
+ function removeGadgetRow(gguiJson, artifactId) {
248
+ const app = gguiJson['app'];
249
+ if (app === undefined)
250
+ return { removed: false, removedVersions: [] };
251
+ if (typeof app !== 'object' || app === null || Array.isArray(app)) {
252
+ return { error: 'ggui.json#app must be an object' };
253
+ }
254
+ const appObj = app;
255
+ const gadgets = appObj['gadgets'];
256
+ if (gadgets === undefined)
257
+ return { removed: false, removedVersions: [] };
258
+ if (!Array.isArray(gadgets)) {
259
+ return { error: 'ggui.json#app.gadgets must be an array' };
260
+ }
261
+ const removedVersions = [];
262
+ const remaining = gadgets.filter((e) => {
263
+ if (e === null || typeof e !== 'object')
264
+ return true;
265
+ const cand = e;
266
+ if (cand.package !== artifactId)
267
+ return true;
268
+ removedVersions.push(typeof cand.version === 'string' ? cand.version : '(unknown)');
269
+ return false;
270
+ });
271
+ if (remaining.length === gadgets.length) {
272
+ return { removed: false, removedVersions: [] };
273
+ }
274
+ if (remaining.length === 0) {
275
+ delete appObj['gadgets'];
276
+ }
277
+ else {
278
+ appObj['gadgets'] = remaining;
279
+ }
280
+ return { removed: true, removedVersions };
281
+ }
175
282
  /**
176
283
  * Strip `INSTALLED_BLUEPRINTS_GLOB` from `gguiJson.blueprints.include`
177
284
  * when present. Returns `true` when a write is needed. Leaves the
@@ -1,11 +1,15 @@
1
+ import { type GadgetDescriptor } from '@ggui-ai/protocol';
1
2
  /** Canonical filename — duplicated from `@ggui-ai/project-config` so
2
3
  * this helper stays free of that dependency. The constant lives in one
3
4
  * place per package; updates ride along on a project-config bump. */
4
5
  export declare const GGUI_JSON_FILENAME = "ggui.json";
5
- /** How many parent directories to walk before giving up. Mirrors the
6
- * convention used by `gadget-search.ts#findGguiJson` + the upstream
7
- * `@ggui-ai/project-config/node` helper. */
8
- export declare const FIND_MAX_DEPTH = 8;
6
+ /** How many parent directories to walk before giving up. The single
7
+ * shared depth for every ggui.json walk in this package — manifest
8
+ * discovery (install/uninstall) AND registry-field resolution
9
+ * (`./registry-url.ts`). 16 levels covers deep monorepo layouts; the
10
+ * historical publish resolver walked 16 while search/install walked 8,
11
+ * and unifying DOWN would have broken 9-16-level-deep publishes. */
12
+ export declare const FIND_MAX_DEPTH = 16;
9
13
  /**
10
14
  * Walk up from `startDir` looking for the first `ggui.json` ancestor.
11
15
  * Returns the absolute path or `null` if none found within
@@ -50,4 +54,31 @@ export declare function readGguiJson(path: string): {
50
54
  * already.
51
55
  */
52
56
  export declare function writeGguiJson(path: string, value: GguiJsonObject): void;
57
+ /**
58
+ * Result of {@link readGadgetsFromGguiJson}: the validated catalog, or
59
+ * a row-indexed error string ready for stderr.
60
+ */
61
+ export type GadgetsReadResult = {
62
+ readonly ok: true;
63
+ readonly gadgets: GadgetDescriptor[];
64
+ } | {
65
+ readonly ok: false;
66
+ readonly error: string;
67
+ };
68
+ /**
69
+ * THE one ggui.json gadget-catalog reader for this package — the
70
+ * install write gate and the `ggui deploy` config push both call it,
71
+ * so the two surfaces cannot drift on optionality or error shape.
72
+ *
73
+ * Navigates to `app.gadgets` via a nested non-strict schema (other
74
+ * manifest fields ride through untouched) and strict-parses every row
75
+ * (`strictGadgetDescriptorSchema` — the same posture every other
76
+ * catalog boundary applies). Absent `app` / `app.gadgets` reads as an
77
+ * empty catalog.
78
+ *
79
+ * On failure the error names the offending ROW by index and points at
80
+ * editing `ggui.json` — callers surface it verbatim, plus their own
81
+ * context, and MUST NOT imply the current command created the row.
82
+ */
83
+ export declare function readGadgetsFromGguiJson(gguiJson: unknown): GadgetsReadResult;
53
84
  //# sourceMappingURL=ggui-json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ggui-json.d.ts","sourceRoot":"","sources":["../../src/internal/ggui-json.ts"],"names":[],"mappings":"AAqBA;;qEAEqE;AACrE,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAE9C;;4CAE4C;AAC5C,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU5D;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAyB/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAEvE"}
1
+ {"version":3,"file":"ggui-json.d.ts","sourceRoot":"","sources":["../../src/internal/ggui-json.ts"],"names":[],"mappings":"AAsBA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAG3B;;qEAEqE;AACrE,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAE9C;;;;;oEAKoE;AACpE,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU5D;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,GACX;IAAE,KAAK,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAyB/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAEvE;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,EAAE,CAAA;CAAE,GAC3D;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,GAAG,iBAAiB,CA+B5E"}
@@ -2,10 +2,12 @@
2
2
  * Minimal ggui.json read/write helpers shared by gadget subcommands.
3
3
  *
4
4
  * Shared by the publish and install subcommands so neither has to
5
- * duplicate the path-walk + JSON IO scaffolding. The parser is
6
- * deliberately structural (no schema parse): install touches
5
+ * duplicate the path-walk + JSON IO scaffolding. The file-level parser
6
+ * is deliberately structural (no schema parse): install touches
7
7
  * `app.gadgets` only, so a malformed `theme` block elsewhere in the
8
- * file shouldn't prevent an install from rewriting the array.
8
+ * file shouldn't prevent an install from rewriting the array. The one
9
+ * schema-validating helper here is {@link readGadgetsFromGguiJson} —
10
+ * the package's single gadget-catalog reader (install + deploy).
9
11
  *
10
12
  * The shape returned is the raw decoded JSON. Callers narrow the
11
13
  * sub-objects they touch (e.g. `app.gadgets`) and run the
@@ -14,14 +16,19 @@
14
16
  */
15
17
  import { existsSync, readFileSync, writeFileSync, } from 'node:fs';
16
18
  import { dirname, join, resolve } from 'node:path';
19
+ import { strictGadgetDescriptorSchema, } from '@ggui-ai/protocol';
20
+ import { z } from 'zod';
17
21
  /** Canonical filename — duplicated from `@ggui-ai/project-config` so
18
22
  * this helper stays free of that dependency. The constant lives in one
19
23
  * place per package; updates ride along on a project-config bump. */
20
24
  export const GGUI_JSON_FILENAME = 'ggui.json';
21
- /** How many parent directories to walk before giving up. Mirrors the
22
- * convention used by `gadget-search.ts#findGguiJson` + the upstream
23
- * `@ggui-ai/project-config/node` helper. */
24
- export const FIND_MAX_DEPTH = 8;
25
+ /** How many parent directories to walk before giving up. The single
26
+ * shared depth for every ggui.json walk in this package — manifest
27
+ * discovery (install/uninstall) AND registry-field resolution
28
+ * (`./registry-url.ts`). 16 levels covers deep monorepo layouts; the
29
+ * historical publish resolver walked 16 while search/install walked 8,
30
+ * and unifying DOWN would have broken 9-16-level-deep publishes. */
31
+ export const FIND_MAX_DEPTH = 16;
25
32
  /**
26
33
  * Walk up from `startDir` looking for the first `ggui.json` ancestor.
27
34
  * Returns the absolute path or `null` if none found within
@@ -90,3 +97,47 @@ export function readGguiJson(path) {
90
97
  export function writeGguiJson(path, value) {
91
98
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
92
99
  }
100
+ /**
101
+ * THE one ggui.json gadget-catalog reader for this package — the
102
+ * install write gate and the `ggui deploy` config push both call it,
103
+ * so the two surfaces cannot drift on optionality or error shape.
104
+ *
105
+ * Navigates to `app.gadgets` via a nested non-strict schema (other
106
+ * manifest fields ride through untouched) and strict-parses every row
107
+ * (`strictGadgetDescriptorSchema` — the same posture every other
108
+ * catalog boundary applies). Absent `app` / `app.gadgets` reads as an
109
+ * empty catalog.
110
+ *
111
+ * On failure the error names the offending ROW by index and points at
112
+ * editing `ggui.json` — callers surface it verbatim, plus their own
113
+ * context, and MUST NOT imply the current command created the row.
114
+ */
115
+ export function readGadgetsFromGguiJson(gguiJson) {
116
+ const schema = z.object({
117
+ app: z
118
+ .object({
119
+ gadgets: z.array(strictGadgetDescriptorSchema).optional(),
120
+ })
121
+ .optional(),
122
+ });
123
+ const result = schema.safeParse(gguiJson);
124
+ if (!result.success) {
125
+ const issue = result.error.issues[0];
126
+ // `issue.path` is ['app', 'gadgets', <rowIndex>, ...rest] when a
127
+ // row failed; shallower when the container itself is malformed.
128
+ const path = issue !== undefined && issue.path.length > 0
129
+ ? issue.path
130
+ .map((p, i) => typeof p === 'number'
131
+ ? `[${p}]`
132
+ : i === 0
133
+ ? String(p)
134
+ : `.${String(p)}`)
135
+ .join('')
136
+ : 'app.gadgets';
137
+ return {
138
+ ok: false,
139
+ error: `ggui.json#${path} failed validation — ${issue?.message ?? 'malformed descriptor'}. Edit ggui.json#app.gadgets to fix or remove the offending entry.`,
140
+ };
141
+ }
142
+ return { ok: true, gadgets: [...(result.data.app?.gadgets ?? [])] };
143
+ }