@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.
- package/dist/auth-keys.d.ts +3 -1
- package/dist/auth-keys.d.ts.map +1 -1
- package/dist/auth-keys.js +42 -2
- package/dist/blueprint-command.d.ts.map +1 -1
- package/dist/blueprint-command.js +13 -13
- package/dist/cli.js +10 -0
- package/dist/config-push.d.ts +0 -8
- package/dist/config-push.d.ts.map +1 -1
- package/dist/config-push.js +10 -35
- package/dist/gadget-command.d.ts +1 -1
- package/dist/gadget-command.d.ts.map +1 -1
- package/dist/gadget-command.js +35 -12
- package/dist/internal/artifact-install.d.ts.map +1 -1
- package/dist/internal/artifact-install.js +125 -100
- package/dist/internal/artifact-publish.d.ts +37 -35
- package/dist/internal/artifact-publish.d.ts.map +1 -1
- package/dist/internal/artifact-publish.js +33 -97
- package/dist/internal/artifact-search.d.ts +27 -21
- package/dist/internal/artifact-search.d.ts.map +1 -1
- package/dist/internal/artifact-search.js +14 -128
- package/dist/internal/artifact-uninstall.d.ts +8 -5
- package/dist/internal/artifact-uninstall.d.ts.map +1 -1
- package/dist/internal/artifact-uninstall.js +133 -26
- package/dist/internal/ggui-json.d.ts +35 -4
- package/dist/internal/ggui-json.d.ts.map +1 -1
- package/dist/internal/ggui-json.js +58 -7
- package/dist/internal/register-author-key.d.ts +10 -0
- package/dist/internal/register-author-key.d.ts.map +1 -1
- package/dist/internal/register-author-key.js +14 -7
- package/dist/internal/registry-url.d.ts +49 -0
- package/dist/internal/registry-url.d.ts.map +1 -0
- package/dist/internal/registry-url.js +137 -0
- package/dist/mcp-backend.d.ts +6 -0
- package/dist/mcp-backend.d.ts.map +1 -1
- package/dist/mcp-backend.js +9 -0
- package/dist/serve-command.d.ts +14 -1
- package/dist/serve-command.d.ts.map +1 -1
- package/dist/serve-command.js +29 -0
- 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
|
|
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":"
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
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
|
|
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 (
|
|
332
|
-
const isNoRegistryError = resolved.error.startsWith('no registry');
|
|
217
|
+
if (!resolved.ok) {
|
|
333
218
|
return {
|
|
334
219
|
ok: false,
|
|
335
|
-
code:
|
|
336
|
-
message: resolved.
|
|
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.
|
|
526
|
-
|
|
527
|
-
ggui.json#registry >
|
|
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
|
|
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
|
|
23
|
-
* `ggui.json#blueprints.include` (i.e. no
|
|
24
|
-
* subdirs remained). `false` when the glob was
|
|
25
|
-
* still on disk)
|
|
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":"
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
39
|
-
const
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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":"
|
|
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
|
|
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.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
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
|
+
}
|