@ggui-ai/cli 0.6.3 → 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
@@ -18,7 +18,9 @@
18
18
  *
19
19
  * GET <registry>/pkg/<scope>/<name>/<version>
20
20
  * 200: { manifest, bundleUrl?, bundleSri?, signatureUrl?, compiledBytes?, publishedAt, publishedBy }
21
- * 403: { error: 'forbidden', message } — private artifact (JWT not yet threaded)
21
+ * 403: { error: 'forbidden', message } — private artifact (install
22
+ * sends no credentials, so private rows are uninstallable from
23
+ * the CLI today; the error copy says exactly that)
22
24
  * 404: { error: 'not_found', message } — artifactId/version unknown
23
25
  * 410: { manifest, …, publishedAt, publishedBy } — yanked (body shape = ReadPkgResponse)
24
26
  * other 4xx/5xx: { error, message }
@@ -62,21 +64,10 @@ import { dirname, join } from 'node:path';
62
64
  import { sha384 } from '@noble/hashes/sha512.js';
63
65
  import { manifestToRegistryEntry, parseArtifactManifest, } from '@ggui-ai/artifact-manifest';
64
66
  import { canonicalJson, derivePublicKeyId, isGadgetSignature, verifyBundleEd25519, verifyBundleSigstore, } from '@ggui-ai/gadget-signing';
67
+ import { FATAL_CATALOG_LINT_CODES, lintGadgetCatalog, resolveAppGadgets, strictGadgetDescriptorSchema, } from '@ggui-ai/protocol';
65
68
  import { READ_ERROR_CODES } from '@ggui-ai/registry-core';
66
- import { findGguiJson, readGguiJson, writeGguiJson, } from './ggui-json.js';
67
- /* -------------------------------------------------------------------------- */
68
- /* Defaults */
69
- /* -------------------------------------------------------------------------- */
70
- /**
71
- * Fallback registry URL when neither `--registry`, `GGUI_REGISTRY`,
72
- * nor a `ggui.json#registry` field is set.
73
- *
74
- * Defaults to the PRODUCTION marketplace registry — an install with no
75
- * explicit config must reach the real registry, not a sandbox/dev host. The
76
- * `--registry` flag and the `GGUI_REGISTRY` env var always take precedence
77
- * (point them at `<env>.registry.sandbox.ggui.ai` for non-prod testing).
78
- */
79
- const DEFAULT_REGISTRY_URL = 'https://registry.ggui.ai';
69
+ import { FIND_MAX_DEPTH, findGguiJson, readGadgetsFromGguiJson, readGguiJson, writeGguiJson, } from './ggui-json.js';
70
+ import { DEFAULT_REGISTRY_URL, resolveRegistryUrl } from './registry-url.js';
80
71
  /**
81
72
  * Build verb-specific help text. Takes the `kind` so the rendered
82
73
  * lines say `ggui gadget install` / `ggui blueprint install` rather
@@ -98,9 +89,9 @@ Arguments:
98
89
  side SemVer rule (no \`latest\`, no ranges).
99
90
 
100
91
  Options:
101
- --registry <url> Override the registry URL. Three-layer
102
- resolution: flag > ggui.json#registry >
103
- GGUI_REGISTRY env > default
92
+ --registry <url> Override the registry URL. Resolution:
93
+ flag > GGUI_REGISTRY env >
94
+ ggui.json#registry > default
104
95
  (\`${DEFAULT_REGISTRY_URL}\`).
105
96
  --no-prompt Skip interactive prompts for any
106
97
  \`publicEnv\` keys the gadget's manifest
@@ -284,59 +275,6 @@ function decodeBase64(s) {
284
275
  return undefined;
285
276
  }
286
277
  }
287
- /**
288
- * Resolve the registry URL the install should target.
289
- *
290
- * Precedence: `--registry` flag > `ggui.json#registry` > env var >
291
- * built-in {@link DEFAULT_REGISTRY_URL}. Mirrors publish + search's
292
- * three-layer chain, with the addition of a hardcoded fallback so an
293
- * install with no manifest still works against the sandbox registry
294
- * (consumers without a project yet shouldn't have to type
295
- * `--registry=…` every call).
296
- *
297
- * Returns the URL with any trailing `/` stripped so route concat below
298
- * (e.g. `${url}/pkg/…`) doesn't double-slash.
299
- */
300
- function resolveRegistry(args) {
301
- const candidates = [];
302
- if (args.flag !== undefined && args.flag.length > 0) {
303
- candidates.push({ raw: args.flag, source: '--registry' });
304
- }
305
- else {
306
- const registryField = args.gguiJson?.['registry'];
307
- if (typeof registryField === 'string' && registryField.length > 0) {
308
- candidates.push({ raw: registryField, source: 'ggui.json#registry' });
309
- }
310
- else {
311
- const envValue = args.env?.['GGUI_REGISTRY'];
312
- if (typeof envValue === 'string' && envValue.length > 0) {
313
- candidates.push({ raw: envValue, source: 'GGUI_REGISTRY' });
314
- }
315
- else {
316
- candidates.push({ raw: DEFAULT_REGISTRY_URL, source: '(default)' });
317
- }
318
- }
319
- }
320
- const picked = candidates[0];
321
- let parsed;
322
- try {
323
- parsed = new URL(picked.raw);
324
- }
325
- catch {
326
- return {
327
- error: `registry URL is malformed (${picked.source}): ${picked.raw}`,
328
- };
329
- }
330
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
331
- return {
332
- error: `registry URL must be http(s) (${picked.source}): ${picked.raw}`,
333
- };
334
- }
335
- let serialized = parsed.toString();
336
- while (serialized.endsWith('/'))
337
- serialized = serialized.slice(0, -1);
338
- return { url: serialized };
339
- }
340
278
  /**
341
279
  * Decompose `@scope/name` into its `scope` + `name` parts so the
342
280
  * read-handler URL `/pkg/{scope}/{name}/{version}` can be composed.
@@ -457,7 +395,7 @@ export async function runArtifactInstall(flags, deps = {}) {
457
395
  // ---- step 2: find ggui.json ----
458
396
  const gguiPath = findGguiJson(cwd);
459
397
  if (gguiPath === null) {
460
- stderr(`${verb}: no ggui.json found in ${cwd} or any ancestor (up to 8 levels).\n`);
398
+ stderr(`${verb}: no ggui.json found in ${cwd} or any ancestor (up to ${FIND_MAX_DEPTH} levels).\n`);
461
399
  return 2;
462
400
  }
463
401
  const loaded = readGguiJson(gguiPath);
@@ -467,13 +405,16 @@ export async function runArtifactInstall(flags, deps = {}) {
467
405
  }
468
406
  const gguiJson = loaded.value;
469
407
  // ---- step 3: resolve registry URL ----
470
- const registryRes = resolveRegistry({
408
+ // Shared resolver (`./registry-url.ts`), READ-verb posture: flag →
409
+ // GGUI_REGISTRY env → nearest ggui.json#registry → public default.
410
+ const registryRes = resolveRegistryUrl({
471
411
  ...(flags.registry !== undefined ? { flag: flags.registry } : {}),
472
- gguiJson,
473
- env,
412
+ cwd,
413
+ env: { GGUI_REGISTRY: env['GGUI_REGISTRY'] },
414
+ defaultUrl: DEFAULT_REGISTRY_URL,
474
415
  });
475
- if ('error' in registryRes) {
476
- stderr(`${verb}: ${registryRes.error}\n`);
416
+ if (!registryRes.ok) {
417
+ stderr(`${verb}: ${registryRes.message}\n`);
477
418
  return 1;
478
419
  }
479
420
  const registryUrl = registryRes.url;
@@ -492,14 +433,10 @@ export async function runArtifactInstall(flags, deps = {}) {
492
433
  stderr(`${verb}: network error fetching registry: ${err instanceof Error ? err.message : String(err)}\n`);
493
434
  return 1;
494
435
  }
495
- let pkgBody;
496
- try {
497
- pkgBody = await pkgRes.json();
498
- }
499
- catch (err) {
500
- stderr(`${verb}: registry returned ${pkgRes.status} with non-JSON body: ${err instanceof Error ? err.message : String(err)}\n`);
501
- return 1;
502
- }
436
+ // Status checks BEFORE any body parse — a WAF/proxy in front of the
437
+ // registry may answer 403/404/410 with an HTML or empty body, and the
438
+ // status-specific diagnostics must still fire. Only the paths that
439
+ // actually consume the body parse it.
503
440
  if (pkgRes.status === 404) {
504
441
  stderr(`${verb}: package ${flags.artifactId}@${flags.version} not found\n`);
505
442
  return 1;
@@ -509,16 +446,35 @@ export async function runArtifactInstall(flags, deps = {}) {
509
446
  return 1;
510
447
  }
511
448
  if (pkgRes.status === 403) {
512
- stderr(`${verb}: ${flags.artifactId}@${flags.version} is private — log in with \`ggui auth login\` (JWT auth for install is not yet supported)\n`);
449
+ stderr(`${verb}: ${flags.artifactId}@${flags.version} is private (HTTP 403). ` +
450
+ `Installing private artifacts is not supported yet — the CLI sends install requests without credentials, so logging in does not help. ` +
451
+ `Only artifacts published with public visibility are installable.\n`);
513
452
  return 1;
514
453
  }
515
454
  if (!pkgRes.ok) {
516
- const detail = isReadErrorBody(pkgBody)
517
- ? ` (${pkgBody.error}: ${pkgBody.message})`
518
- : '';
455
+ // Structured registry error bodies carry `(code: message)` detail;
456
+ // a non-JSON body (proxy error page) degrades to the bare status.
457
+ let detail = '';
458
+ try {
459
+ const errBody = await pkgRes.json();
460
+ if (isReadErrorBody(errBody)) {
461
+ detail = ` (${errBody.error}: ${errBody.message})`;
462
+ }
463
+ }
464
+ catch {
465
+ // Non-JSON error body — the status line is all we can report.
466
+ }
519
467
  stderr(`${verb}: registry returned ${pkgRes.status}${detail}\n`);
520
468
  return 1;
521
469
  }
470
+ let pkgBody;
471
+ try {
472
+ pkgBody = await pkgRes.json();
473
+ }
474
+ catch (err) {
475
+ stderr(`${verb}: registry returned ${pkgRes.status} with non-JSON body: ${err instanceof Error ? err.message : String(err)}\n`);
476
+ return 1;
477
+ }
522
478
  if (!isReadPkgResponse(pkgBody)) {
523
479
  stderr(`${verb}: registry response did not match ReadPkgResponse shape\n`);
524
480
  return 1;
@@ -700,11 +656,61 @@ export async function runArtifactInstall(flags, deps = {}) {
700
656
  ...(readPkg.bundleUrl !== undefined ? { bundleUrl: readPkg.bundleUrl } : {}),
701
657
  ...(readPkg.bundleSri !== undefined ? { bundleSri: readPkg.bundleSri } : {}),
702
658
  });
659
+ // Write-boundary validation (gadget catalog foundations,
660
+ // 2026-08-09): the composed row is checked BEFORE it touches the
661
+ // in-memory catalog. The manifest schema and the descriptor schema
662
+ // are separate validators (e.g. manifest `connect[]` entries are
663
+ // free-form strings while descriptor `connect[]` entries must be
664
+ // full URLs), so a manifest that parsed clean can still compose to
665
+ // a row the catalog schema rejects.
666
+ const composedCheck = strictGadgetDescriptorSchema.safeParse(entry);
667
+ if (!composedCheck.success) {
668
+ stderr(`${verb}: composed descriptor failed validation: ${formatZodIssues(composedCheck.error)}\n`);
669
+ return 1;
670
+ }
703
671
  const writeRes = appendGadget(gguiJson, entry);
704
672
  if ('error' in writeRes) {
705
673
  stderr(`${verb}: ${writeRes.error}\n`);
706
674
  return 1;
707
675
  }
676
+ // Post-append catalog lint — any fatal `lintGadgetCatalog` code
677
+ // aborts the install BEFORE `writeGguiJson`, so a failing install
678
+ // leaves ggui.json untouched. Two sequential checks, each
679
+ // reporting paths in its OWN array: first the declared array
680
+ // (duplicate package, immutable-bundle mutation), then the
681
+ // RESOLVED union with the first-party stdlib floor (a gadget
682
+ // re-exporting a stdlib export name is only a collision
683
+ // post-union — installing it would break every later generation).
684
+ const postAppendRead = readGadgetsFromGguiJson(gguiJson);
685
+ if (!postAppendRead.ok) {
686
+ // The freshly composed row was already validated above, so the
687
+ // offending row predates this install.
688
+ stderr(`${verb}: ${postAppendRead.error} (This entry was already present before this install — nothing was written.)\n`);
689
+ return 1;
690
+ }
691
+ const postAppend = postAppendRead.gadgets;
692
+ const declaredFatal = lintGadgetCatalog(postAppend).filter((w) => FATAL_CATALOG_LINT_CODES.has(w.code));
693
+ if (declaredFatal.length > 0) {
694
+ stderr(`${verb}: install would corrupt the gadget catalog:\n${declaredFatal
695
+ .map((w) => ` ${w.code} ${w.path}: ${w.message}`)
696
+ .join('\n')}\n`);
697
+ return 1;
698
+ }
699
+ const resolvedFatal = lintGadgetCatalog(resolveAppGadgets(postAppend)).filter((w) => FATAL_CATALOG_LINT_CODES.has(w.code));
700
+ if (resolvedFatal.length > 0) {
701
+ stderr(`${verb}: install would corrupt the RESOLVED gadget catalog (declared extensions + the first-party stdlib floor):\n${resolvedFatal
702
+ .map((w) => ` ${w.code} ${w.path}: ${w.message}`)
703
+ .join('\n')}\n`);
704
+ return 1;
705
+ }
706
+ // Version-replace notice — rows are keyed by package, so
707
+ // installing any version replaces the existing row. When the
708
+ // versions differ (upgrade OR downgrade), say what was replaced;
709
+ // a silent downgrade is data loss the operator should see.
710
+ if (writeRes.replacedVersion !== undefined &&
711
+ writeRes.replacedVersion !== entry.version) {
712
+ stdout(`replaced: ${entry.package} ${writeRes.replacedVersion} → ${entry.version}\n`);
713
+ }
708
714
  // Required public-env keys. Gadget's `requires[]` lists the
709
715
  // `GGUI_PUBLIC_APP_*` keys the wrapper reads via getPublicEnv().
710
716
  // Prompt for each (or skip silently under --no-prompt).
@@ -770,9 +776,10 @@ export async function runArtifactInstall(flags, deps = {}) {
770
776
  * via `resolveAppGadgets` in `@ggui-ai/protocol` — so declaring an
771
777
  * extension never drops stdlib hooks from the generated UI.
772
778
  *
773
- * Idempotent on `(scope, name, version)` — re-running install on the
774
- * same identifier replaces the existing row rather than adding a
775
- * duplicate.
779
+ * Idempotent per PACKAGE — the catalog holds at most one descriptor
780
+ * per package (`LINT_GADGET_DUPLICATE_PACKAGE` is fatal); re-installing
781
+ * ANY version of an already-declared package replaces the package's
782
+ * row. A version upgrade is a replace, never a second row.
776
783
  */
777
784
  function appendGadget(gguiJson, entry) {
778
785
  const app = gguiJson['app'];
@@ -797,24 +804,42 @@ function appendGadget(gguiJson, entry) {
797
804
  if (!Array.isArray(libs)) {
798
805
  return { error: 'ggui.json#app.gadgets must be an array' };
799
806
  }
800
- // Idempotent replace by `(package, version)` — a gadget descriptor
801
- // is a PACKAGE, and `(package, version)` is its frozen registry
802
- // identity tuple. Re-installing the same package version replaces
803
- // the existing row rather than adding a duplicate.
807
+ // Idempotent replace by `package` ALONE — an app's catalog holds at
808
+ // most ONE descriptor per package (the wire references a package by
809
+ // name and the server resolves exactly one descriptor; two rows for
810
+ // one package trip the fatal `LINT_GADGET_DUPLICATE_PACKAGE` lint).
811
+ // Re-installing any version of an already-declared package replaces
812
+ // its row, so a version upgrade never accumulates the older row.
804
813
  const existingIdx = libs.findIndex((e) => {
805
814
  if (e === null || typeof e !== 'object')
806
815
  return false;
807
816
  const cand = e;
808
- return cand.package === entry.package && cand.version === entry.version;
817
+ return cand.package === entry.package;
809
818
  });
810
819
  if (existingIdx >= 0) {
820
+ const prior = libs[existingIdx];
821
+ const replacedVersion = typeof prior.version === 'string' ? prior.version : undefined;
811
822
  libs[existingIdx] = entry;
823
+ return {
824
+ ok: true,
825
+ ...(replacedVersion !== undefined ? { replacedVersion } : {}),
826
+ };
812
827
  }
813
- else {
814
- libs.push(entry);
815
- }
828
+ libs.push(entry);
816
829
  return { ok: true };
817
830
  }
831
+ /**
832
+ * Format a zod error's issues as a one-line `path: message` list for
833
+ * stderr diagnostics.
834
+ */
835
+ function formatZodIssues(error) {
836
+ return error.issues
837
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
838
+ .join('; ');
839
+ }
840
+ // The post-append catalog read routes through the package's ONE
841
+ // gadget-catalog reader — `readGadgetsFromGguiJson` in
842
+ // `./ggui-json.ts`, shared with the `ggui deploy` config push.
818
843
  /* -------------------------------------------------------------------------- */
819
844
  /* publicEnv prompts */
820
845
  /* -------------------------------------------------------------------------- */
@@ -7,29 +7,18 @@ import type { ArtifactKind } from '@ggui-ai/registry-core';
7
7
  * match. */
8
8
  export type { ArtifactKind };
9
9
  /**
10
- * Shape of the resolved options the publish core consumes. Built from
11
- * argv by {@link parseArtifactPublishFlags} so the core function is
12
- * dependency-injectable for tests.
10
+ * Shape of the resolved options the publish core consumes. Extends the
11
+ * flag subset of {@link ParsedPublishFlags} (via
12
+ * {@link PublishFlagOptions}) so the parser and the core cannot drift:
13
+ * a flag added to the parser lands in this type automatically, and
14
+ * routers forward the whole parsed object via
15
+ * {@link publishOptionsFromFlags} instead of hand-copying fields —
16
+ * hand-copying is exactly how `--identity-token` was silently dropped
17
+ * once.
13
18
  */
14
- export interface ArtifactPublishOptions {
19
+ export interface ArtifactPublishOptions extends PublishFlagOptions {
15
20
  /** CLI verb the operator typed — enforced against the manifest kind. */
16
21
  readonly kind: ArtifactKind;
17
- /** Override the registry URL — highest precedence. */
18
- readonly registry?: string;
19
- /** Skip the actual POST; run everything else. */
20
- readonly dryRun: boolean;
21
- /** Override the private-key path. */
22
- readonly key?: string;
23
- /**
24
- * OIDC identity token for sigstore signing (public gadgets only).
25
- * Mirrors cosign's `--identity-token` flag. If absent the CLI falls
26
- * back through `GGUI_OIDC_TOKEN` → GH-Actions ambient → interactive.
27
- * Ignored for private-gadget publishes (which sign with Ed25519).
28
- */
29
- readonly identityToken?: string;
30
- /** Auth strategy + bearer token. `auth === undefined` → the stored
31
- * `ggui login` session. */
32
- readonly auth?: AuthFlags;
33
22
  /** Starting directory; defaults to `process.cwd()`. */
34
23
  readonly cwd?: string;
35
24
  /**
@@ -72,6 +61,9 @@ export interface PublishSuccess {
72
61
  export type PublishError = {
73
62
  readonly code: 'no_registry_resolved';
74
63
  readonly message: string;
64
+ } | {
65
+ readonly code: 'invalid_registry';
66
+ readonly message: string;
75
67
  } | {
76
68
  readonly code: 'manifest_missing';
77
69
  readonly message: string;
@@ -122,15 +114,39 @@ export interface ConformanceIssue {
122
114
  * Returned by {@link parseArtifactPublishFlags}.
123
115
  */
124
116
  export interface ParsedPublishFlags {
117
+ /** Override the registry URL — highest precedence. */
125
118
  readonly registry?: string;
119
+ /** Skip the actual POST; run everything else. */
126
120
  readonly dryRun: boolean;
121
+ /** Override the private-key path. */
127
122
  readonly key?: string;
128
- /** Parsed `--identity-token <jwt>` flag (sigstore / public gadgets). */
123
+ /**
124
+ * OIDC identity token for sigstore signing (public artifacts only).
125
+ * Mirrors cosign's `--identity-token` flag. If absent the CLI falls
126
+ * back through `GGUI_OIDC_TOKEN` → GH-Actions ambient → interactive.
127
+ * Ignored for private publishes (which sign with Ed25519).
128
+ */
129
129
  readonly identityToken?: string;
130
+ /** Auth strategy + bearer token. `auth === undefined` → the stored
131
+ * `ggui login` session. */
130
132
  readonly auth?: AuthFlags;
131
133
  readonly help: boolean;
132
134
  readonly error?: string;
133
135
  }
136
+ /**
137
+ * The subset of {@link ParsedPublishFlags} the publish core consumes —
138
+ * everything except the parse-control fields (`help` / `error`).
139
+ * {@link ArtifactPublishOptions} extends this type, so parser and core
140
+ * stay structurally coupled by the compiler.
141
+ */
142
+ export type PublishFlagOptions = Omit<ParsedPublishFlags, 'help' | 'error'>;
143
+ /**
144
+ * Project a parsed flag bundle onto the options the publish core
145
+ * consumes. THE way routers hand flags to {@link runArtifactPublish} —
146
+ * forwarding the whole object means a future flag cannot be silently
147
+ * dropped in a router's hand-written spread.
148
+ */
149
+ export declare function publishOptionsFromFlags(flags: ParsedPublishFlags): PublishFlagOptions;
134
150
  /**
135
151
  * Build verb-specific help text. Takes the `kind` so the rendered
136
152
  * lines say `ggui gadget publish` / `ggui blueprint publish` rather
@@ -155,20 +171,6 @@ export declare function parseArtifactPublishFlags(args: readonly string[]): Pars
155
171
  * surfaces a friendly redirect at step 2 + exits non-zero.
156
172
  */
157
173
  export declare function runArtifactPublish(opts: ArtifactPublishOptions): Promise<PublishResult>;
158
- interface RegistryResolution {
159
- readonly ok: true;
160
- readonly url: string;
161
- readonly source: 'flag' | 'env' | 'ggui.json';
162
- }
163
- interface RegistryUnresolved {
164
- readonly ok: false;
165
- readonly message: string;
166
- }
167
- export declare function resolveRegistryUrl(opts: {
168
- readonly flag?: string;
169
- readonly env: NodeJS.ProcessEnv;
170
- readonly cwd: string;
171
- }): RegistryResolution | RegistryUnresolved;
172
174
  /** Which credential the publish flow is sending to the registry.
173
175
  * Drives the 401 diagnostics — a rejected `ggui login` session points
174
176
  * the operator at the session/bearer split; a rejected bearer token
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-publish.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-publish.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"artifact-publish.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-publish.ts"],"names":[],"mappings":"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"}
@@ -58,8 +58,7 @@
58
58
  */
59
59
  import { build as esbuild } from 'esbuild';
60
60
  import { existsSync, readFileSync } from 'node:fs';
61
- import { dirname, isAbsolute, join, resolve } from 'node:path';
62
- import { URL } from 'node:url';
61
+ import { dirname, isAbsolute, join } from 'node:path';
63
62
  import { GGUI_BLUEPRINT_JSON_FILENAME, GGUI_GADGET_JSON_FILENAME, parseBlueprintManifest, parseGadgetManifest, } from '@ggui-ai/artifact-manifest';
64
63
  import { canonicalJson, derivePublicKeyId, generateEd25519Keypair, publicKeyFromPrivate, signBundleEd25519, signBundleSigstore, } from '@ggui-ai/gadget-signing';
65
64
  import { PROTOCOL_VERSION } from '@ggui-ai/protocol';
@@ -69,6 +68,17 @@ import { saveAuthSession, tryLoadAuthSession, } from '../auth-store.js';
69
68
  import { ApiError, postAuthRefresh } from '../api-client.js';
70
69
  import { getPrivateKeyPath, hasKeypair, readPrivateKey, scopeOf, writePrivateKey, } from './key-store.js';
71
70
  import { acquireAuthToken, parseAuthFlags, AUTH_HELP_FRAGMENT, } from './auth-strategy.js';
71
+ import { resolveRegistryUrl } from './registry-url.js';
72
+ /**
73
+ * Project a parsed flag bundle onto the options the publish core
74
+ * consumes. THE way routers hand flags to {@link runArtifactPublish} —
75
+ * forwarding the whole object means a future flag cannot be silently
76
+ * dropped in a router's hand-written spread.
77
+ */
78
+ export function publishOptionsFromFlags(flags) {
79
+ const { help: _help, error: _error, ...options } = flags;
80
+ return options;
81
+ }
72
82
  /**
73
83
  * Build verb-specific help text. Takes the `kind` so the rendered
74
84
  * lines say `ggui gadget publish` / `ggui blueprint publish` rather
@@ -166,7 +176,12 @@ export function parseArtifactPublishFlags(args) {
166
176
  value = rest[i + 1];
167
177
  i += 1;
168
178
  }
169
- if (typeof value !== 'string' || value.length === 0) {
179
+ // Leading `--` means the "value" is actually the next flag —
180
+ // `--identity-token --dry-run` must not swallow `--dry-run` as
181
+ // the token (JWTs never start with `--`).
182
+ if (typeof value !== 'string' ||
183
+ value.length === 0 ||
184
+ value.startsWith('--')) {
170
185
  return { dryRun, help, error: '--identity-token requires a value' };
171
186
  }
172
187
  identityToken = value;
@@ -205,10 +220,23 @@ export async function runArtifactPublish(opts) {
205
220
  const now = opts.now ?? (() => Math.floor(Date.now() / 1000));
206
221
  const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
207
222
  // ---- step 1: resolve registry URL ----
208
- const registryRes = resolveRegistryUrl({ flag: opts.registry, env: process.env, cwd });
223
+ // Shared resolver (`./registry-url.ts`), WRITE-verb posture: flag →
224
+ // GGUI_REGISTRY env → nearest ggui.json#registry, and NO default —
225
+ // operators must opt into a publish target explicitly.
226
+ const registryRes = resolveRegistryUrl({
227
+ ...(opts.registry !== undefined ? { flag: opts.registry } : {}),
228
+ env: process.env,
229
+ cwd,
230
+ });
209
231
  if (!registryRes.ok) {
210
232
  stderr(`${verb}: ${registryRes.message}\n`);
211
- return { ok: false, exitCode: 1, error: { code: 'no_registry_resolved', message: registryRes.message } };
233
+ // Preserve the resolver's code split (mirrors search + keys
234
+ // register): "nothing configured" and "configured but unusable"
235
+ // are different operator problems.
236
+ const error = registryRes.code === 'no-registry'
237
+ ? { code: 'no_registry_resolved', message: registryRes.message }
238
+ : { code: 'invalid_registry', message: registryRes.message };
239
+ return { ok: false, exitCode: 1, error };
212
240
  }
213
241
  const registryUrl = registryRes.url;
214
242
  stdout(`registry: ${registryUrl} (${registryRes.source})\n`);
@@ -445,98 +473,6 @@ export async function runArtifactPublish(opts) {
445
473
  },
446
474
  };
447
475
  }
448
- export function resolveRegistryUrl(opts) {
449
- if (opts.flag && opts.flag.length > 0) {
450
- if (!isValidUrl(opts.flag)) {
451
- return {
452
- ok: false,
453
- message: `--registry value is not a valid URL: ${opts.flag}`,
454
- };
455
- }
456
- return { ok: true, url: stripTrailingSlash(opts.flag), source: 'flag' };
457
- }
458
- const envValue = opts.env['GGUI_REGISTRY'];
459
- if (envValue && envValue.length > 0) {
460
- if (!isValidUrl(envValue)) {
461
- return {
462
- ok: false,
463
- message: `GGUI_REGISTRY is not a valid URL: ${envValue}`,
464
- };
465
- }
466
- return { ok: true, url: stripTrailingSlash(envValue), source: 'env' };
467
- }
468
- const ggui = findAndReadGguiJsonRegistry(opts.cwd);
469
- if (ggui.found) {
470
- if (!isValidUrl(ggui.registry)) {
471
- return {
472
- ok: false,
473
- message: `ggui.json#registry at ${ggui.path} is not a valid URL: ${ggui.registry}`,
474
- };
475
- }
476
- return { ok: true, url: stripTrailingSlash(ggui.registry), source: 'ggui.json' };
477
- }
478
- return {
479
- ok: false,
480
- message: 'no registry resolved. Set one of:\n' +
481
- ' --registry <url>\n' +
482
- ' GGUI_REGISTRY env var\n' +
483
- ' "registry": "<url>" in the nearest ggui.json',
484
- };
485
- }
486
- /**
487
- * Walk up from `startDir` looking for the nearest `ggui.json` with a
488
- * top-level `registry` string field. Returns the first match.
489
- *
490
- * This is intentionally hand-rolled (not via `@ggui-ai/project-config`)
491
- * because the schema there has not grown a typed `registry` field yet;
492
- * until it does, the field is read opportunistically without forcing a
493
- * parallel schema migration.
494
- */
495
- function findAndReadGguiJsonRegistry(startDir) {
496
- let dir = resolve(startDir);
497
- for (let i = 0; i <= 16; i++) {
498
- const candidate = join(dir, 'ggui.json');
499
- if (existsSync(candidate)) {
500
- const raw = readFileSync(candidate, 'utf8');
501
- let parsed;
502
- try {
503
- parsed = JSON.parse(raw);
504
- }
505
- catch {
506
- // Malformed — fall through; the operator's `ggui serve` will
507
- // raise a friendlier error than we can synthesize here.
508
- return { found: false };
509
- }
510
- const registry = extractRegistryField(parsed);
511
- if (typeof registry === 'string' && registry.length > 0) {
512
- return { found: true, registry, path: candidate };
513
- }
514
- return { found: false };
515
- }
516
- const parent = dirname(dir);
517
- if (parent === dir)
518
- break;
519
- dir = parent;
520
- }
521
- return { found: false };
522
- }
523
- function extractRegistryField(x) {
524
- if (typeof x !== 'object' || x === null)
525
- return undefined;
526
- return x.registry;
527
- }
528
- function isValidUrl(s) {
529
- try {
530
- const u = new URL(s);
531
- return u.protocol === 'http:' || u.protocol === 'https:';
532
- }
533
- catch {
534
- return false;
535
- }
536
- }
537
- function stripTrailingSlash(s) {
538
- return s.endsWith('/') ? s.slice(0, -1) : s;
539
- }
540
476
  function loadManifest(cwd) {
541
477
  const gadgetPath = join(cwd, GGUI_GADGET_JSON_FILENAME);
542
478
  const blueprintPath = join(cwd, GGUI_BLUEPRINT_JSON_FILENAME);