@ggui-ai/cli 0.6.3 → 0.8.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 +132 -105
- 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
|
@@ -18,10 +18,15 @@
|
|
|
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)
|
|
22
21
|
* 404: { error: 'not_found', message } — artifactId/version unknown
|
|
22
|
+
* OR a private artifact the caller may not read: the registry
|
|
23
|
+
* answers both identically so existence never leaks. Install
|
|
24
|
+
* sends no credentials, so every private row lands here — the
|
|
25
|
+
* 404 diagnosis carries that education.
|
|
23
26
|
* 410: { manifest, …, publishedAt, publishedBy } — yanked (body shape = ReadPkgResponse)
|
|
24
|
-
* other 4xx/5xx: { error, message }
|
|
27
|
+
* other 4xx/5xx: { error, message } — the registry itself never
|
|
28
|
+
* answers a read with 403; one can only arrive from a WAF or
|
|
29
|
+
* proxy in front of it and is reported as a bare status
|
|
25
30
|
*
|
|
26
31
|
* GET <bundleUrl> → raw esbuild output (presigned S3)
|
|
27
32
|
* GET <signatureUrl> → JSON-encoded {@link Ed25519Signature}
|
|
@@ -62,21 +67,10 @@ import { dirname, join } from 'node:path';
|
|
|
62
67
|
import { sha384 } from '@noble/hashes/sha512.js';
|
|
63
68
|
import { manifestToRegistryEntry, parseArtifactManifest, } from '@ggui-ai/artifact-manifest';
|
|
64
69
|
import { canonicalJson, derivePublicKeyId, isGadgetSignature, verifyBundleEd25519, verifyBundleSigstore, } from '@ggui-ai/gadget-signing';
|
|
70
|
+
import { FATAL_CATALOG_LINT_CODES, lintGadgetCatalog, resolveAppGadgets, strictGadgetDescriptorSchema, } from '@ggui-ai/protocol';
|
|
65
71
|
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';
|
|
72
|
+
import { FIND_MAX_DEPTH, findGguiJson, readGadgetsFromGguiJson, readGguiJson, writeGguiJson, } from './ggui-json.js';
|
|
73
|
+
import { DEFAULT_REGISTRY_URL, resolveRegistryUrl } from './registry-url.js';
|
|
80
74
|
/**
|
|
81
75
|
* Build verb-specific help text. Takes the `kind` so the rendered
|
|
82
76
|
* lines say `ggui gadget install` / `ggui blueprint install` rather
|
|
@@ -98,9 +92,9 @@ Arguments:
|
|
|
98
92
|
side SemVer rule (no \`latest\`, no ranges).
|
|
99
93
|
|
|
100
94
|
Options:
|
|
101
|
-
--registry <url> Override the registry URL.
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
--registry <url> Override the registry URL. Resolution:
|
|
96
|
+
flag > GGUI_REGISTRY env >
|
|
97
|
+
ggui.json#registry > default
|
|
104
98
|
(\`${DEFAULT_REGISTRY_URL}\`).
|
|
105
99
|
--no-prompt Skip interactive prompts for any
|
|
106
100
|
\`publicEnv\` keys the gadget's manifest
|
|
@@ -284,59 +278,6 @@ function decodeBase64(s) {
|
|
|
284
278
|
return undefined;
|
|
285
279
|
}
|
|
286
280
|
}
|
|
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
281
|
/**
|
|
341
282
|
* Decompose `@scope/name` into its `scope` + `name` parts so the
|
|
342
283
|
* read-handler URL `/pkg/{scope}/{name}/{version}` can be composed.
|
|
@@ -457,7 +398,7 @@ export async function runArtifactInstall(flags, deps = {}) {
|
|
|
457
398
|
// ---- step 2: find ggui.json ----
|
|
458
399
|
const gguiPath = findGguiJson(cwd);
|
|
459
400
|
if (gguiPath === null) {
|
|
460
|
-
stderr(`${verb}: no ggui.json found in ${cwd} or any ancestor (up to
|
|
401
|
+
stderr(`${verb}: no ggui.json found in ${cwd} or any ancestor (up to ${FIND_MAX_DEPTH} levels).\n`);
|
|
461
402
|
return 2;
|
|
462
403
|
}
|
|
463
404
|
const loaded = readGguiJson(gguiPath);
|
|
@@ -467,13 +408,16 @@ export async function runArtifactInstall(flags, deps = {}) {
|
|
|
467
408
|
}
|
|
468
409
|
const gguiJson = loaded.value;
|
|
469
410
|
// ---- step 3: resolve registry URL ----
|
|
470
|
-
|
|
411
|
+
// Shared resolver (`./registry-url.ts`), READ-verb posture: flag →
|
|
412
|
+
// GGUI_REGISTRY env → nearest ggui.json#registry → public default.
|
|
413
|
+
const registryRes = resolveRegistryUrl({
|
|
471
414
|
...(flags.registry !== undefined ? { flag: flags.registry } : {}),
|
|
472
|
-
|
|
473
|
-
env,
|
|
415
|
+
cwd,
|
|
416
|
+
env: { GGUI_REGISTRY: env['GGUI_REGISTRY'] },
|
|
417
|
+
defaultUrl: DEFAULT_REGISTRY_URL,
|
|
474
418
|
});
|
|
475
|
-
if (
|
|
476
|
-
stderr(`${verb}: ${registryRes.
|
|
419
|
+
if (!registryRes.ok) {
|
|
420
|
+
stderr(`${verb}: ${registryRes.message}\n`);
|
|
477
421
|
return 1;
|
|
478
422
|
}
|
|
479
423
|
const registryUrl = registryRes.url;
|
|
@@ -492,33 +436,47 @@ export async function runArtifactInstall(flags, deps = {}) {
|
|
|
492
436
|
stderr(`${verb}: network error fetching registry: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
493
437
|
return 1;
|
|
494
438
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
}
|
|
439
|
+
// Status checks BEFORE any body parse — a WAF/proxy in front of the
|
|
440
|
+
// registry may answer 404/410 with an HTML or empty body, and the
|
|
441
|
+
// status-specific diagnostics must still fire. Only the paths that
|
|
442
|
+
// actually consume the body parse it.
|
|
503
443
|
if (pkgRes.status === 404) {
|
|
504
|
-
|
|
444
|
+
// The registry answers an unauthorized private read with the
|
|
445
|
+
// SAME not-found shape as a true miss (no existence signal), so
|
|
446
|
+
// the CLI cannot tell "missing" from "private and not yours" —
|
|
447
|
+
// one message covers both honestly.
|
|
448
|
+
stderr(`${verb}: package ${flags.artifactId}@${flags.version} not found ` +
|
|
449
|
+
`(or not visible: private artifacts answer as not-found, and the CLI sends install requests without credentials — only public artifacts are installable).\n`);
|
|
505
450
|
return 1;
|
|
506
451
|
}
|
|
507
452
|
if (pkgRes.status === 410) {
|
|
508
453
|
stderr(`${verb}: ${flags.artifactId}@${flags.version} was yanked; choose a different version\n`);
|
|
509
454
|
return 1;
|
|
510
455
|
}
|
|
511
|
-
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`);
|
|
513
|
-
return 1;
|
|
514
|
-
}
|
|
515
456
|
if (!pkgRes.ok) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
457
|
+
// Structured registry error bodies carry `(code: message)` detail;
|
|
458
|
+
// a non-JSON body (proxy error page) degrades to the bare status.
|
|
459
|
+
let detail = '';
|
|
460
|
+
try {
|
|
461
|
+
const errBody = await pkgRes.json();
|
|
462
|
+
if (isReadErrorBody(errBody)) {
|
|
463
|
+
detail = ` (${errBody.error}: ${errBody.message})`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Non-JSON error body — the status line is all we can report.
|
|
468
|
+
}
|
|
519
469
|
stderr(`${verb}: registry returned ${pkgRes.status}${detail}\n`);
|
|
520
470
|
return 1;
|
|
521
471
|
}
|
|
472
|
+
let pkgBody;
|
|
473
|
+
try {
|
|
474
|
+
pkgBody = await pkgRes.json();
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
stderr(`${verb}: registry returned ${pkgRes.status} with non-JSON body: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
522
480
|
if (!isReadPkgResponse(pkgBody)) {
|
|
523
481
|
stderr(`${verb}: registry response did not match ReadPkgResponse shape\n`);
|
|
524
482
|
return 1;
|
|
@@ -700,11 +658,61 @@ export async function runArtifactInstall(flags, deps = {}) {
|
|
|
700
658
|
...(readPkg.bundleUrl !== undefined ? { bundleUrl: readPkg.bundleUrl } : {}),
|
|
701
659
|
...(readPkg.bundleSri !== undefined ? { bundleSri: readPkg.bundleSri } : {}),
|
|
702
660
|
});
|
|
661
|
+
// Write-boundary validation (gadget catalog foundations,
|
|
662
|
+
// 2026-08-09): the composed row is checked BEFORE it touches the
|
|
663
|
+
// in-memory catalog. The manifest schema and the descriptor schema
|
|
664
|
+
// are separate validators (e.g. manifest `connect[]` entries are
|
|
665
|
+
// free-form strings while descriptor `connect[]` entries must be
|
|
666
|
+
// full URLs), so a manifest that parsed clean can still compose to
|
|
667
|
+
// a row the catalog schema rejects.
|
|
668
|
+
const composedCheck = strictGadgetDescriptorSchema.safeParse(entry);
|
|
669
|
+
if (!composedCheck.success) {
|
|
670
|
+
stderr(`${verb}: composed descriptor failed validation: ${formatZodIssues(composedCheck.error)}\n`);
|
|
671
|
+
return 1;
|
|
672
|
+
}
|
|
703
673
|
const writeRes = appendGadget(gguiJson, entry);
|
|
704
674
|
if ('error' in writeRes) {
|
|
705
675
|
stderr(`${verb}: ${writeRes.error}\n`);
|
|
706
676
|
return 1;
|
|
707
677
|
}
|
|
678
|
+
// Post-append catalog lint — any fatal `lintGadgetCatalog` code
|
|
679
|
+
// aborts the install BEFORE `writeGguiJson`, so a failing install
|
|
680
|
+
// leaves ggui.json untouched. Two sequential checks, each
|
|
681
|
+
// reporting paths in its OWN array: first the declared array
|
|
682
|
+
// (duplicate package, immutable-bundle mutation), then the
|
|
683
|
+
// RESOLVED union with the first-party stdlib floor (a gadget
|
|
684
|
+
// re-exporting a stdlib export name is only a collision
|
|
685
|
+
// post-union — installing it would break every later generation).
|
|
686
|
+
const postAppendRead = readGadgetsFromGguiJson(gguiJson);
|
|
687
|
+
if (!postAppendRead.ok) {
|
|
688
|
+
// The freshly composed row was already validated above, so the
|
|
689
|
+
// offending row predates this install.
|
|
690
|
+
stderr(`${verb}: ${postAppendRead.error} (This entry was already present before this install — nothing was written.)\n`);
|
|
691
|
+
return 1;
|
|
692
|
+
}
|
|
693
|
+
const postAppend = postAppendRead.gadgets;
|
|
694
|
+
const declaredFatal = lintGadgetCatalog(postAppend).filter((w) => FATAL_CATALOG_LINT_CODES.has(w.code));
|
|
695
|
+
if (declaredFatal.length > 0) {
|
|
696
|
+
stderr(`${verb}: install would corrupt the gadget catalog:\n${declaredFatal
|
|
697
|
+
.map((w) => ` ${w.code} ${w.path}: ${w.message}`)
|
|
698
|
+
.join('\n')}\n`);
|
|
699
|
+
return 1;
|
|
700
|
+
}
|
|
701
|
+
const resolvedFatal = lintGadgetCatalog(resolveAppGadgets(postAppend)).filter((w) => FATAL_CATALOG_LINT_CODES.has(w.code));
|
|
702
|
+
if (resolvedFatal.length > 0) {
|
|
703
|
+
stderr(`${verb}: install would corrupt the RESOLVED gadget catalog (declared extensions + the first-party stdlib floor):\n${resolvedFatal
|
|
704
|
+
.map((w) => ` ${w.code} ${w.path}: ${w.message}`)
|
|
705
|
+
.join('\n')}\n`);
|
|
706
|
+
return 1;
|
|
707
|
+
}
|
|
708
|
+
// Version-replace notice — rows are keyed by package, so
|
|
709
|
+
// installing any version replaces the existing row. When the
|
|
710
|
+
// versions differ (upgrade OR downgrade), say what was replaced;
|
|
711
|
+
// a silent downgrade is data loss the operator should see.
|
|
712
|
+
if (writeRes.replacedVersion !== undefined &&
|
|
713
|
+
writeRes.replacedVersion !== entry.version) {
|
|
714
|
+
stdout(`replaced: ${entry.package} ${writeRes.replacedVersion} → ${entry.version}\n`);
|
|
715
|
+
}
|
|
708
716
|
// Required public-env keys. Gadget's `requires[]` lists the
|
|
709
717
|
// `GGUI_PUBLIC_APP_*` keys the wrapper reads via getPublicEnv().
|
|
710
718
|
// Prompt for each (or skip silently under --no-prompt).
|
|
@@ -770,9 +778,10 @@ export async function runArtifactInstall(flags, deps = {}) {
|
|
|
770
778
|
* via `resolveAppGadgets` in `@ggui-ai/protocol` — so declaring an
|
|
771
779
|
* extension never drops stdlib hooks from the generated UI.
|
|
772
780
|
*
|
|
773
|
-
* Idempotent
|
|
774
|
-
*
|
|
775
|
-
*
|
|
781
|
+
* Idempotent per PACKAGE — the catalog holds at most one descriptor
|
|
782
|
+
* per package (`LINT_GADGET_DUPLICATE_PACKAGE` is fatal); re-installing
|
|
783
|
+
* ANY version of an already-declared package replaces the package's
|
|
784
|
+
* row. A version upgrade is a replace, never a second row.
|
|
776
785
|
*/
|
|
777
786
|
function appendGadget(gguiJson, entry) {
|
|
778
787
|
const app = gguiJson['app'];
|
|
@@ -797,24 +806,42 @@ function appendGadget(gguiJson, entry) {
|
|
|
797
806
|
if (!Array.isArray(libs)) {
|
|
798
807
|
return { error: 'ggui.json#app.gadgets must be an array' };
|
|
799
808
|
}
|
|
800
|
-
// Idempotent replace by `
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
809
|
+
// Idempotent replace by `package` ALONE — an app's catalog holds at
|
|
810
|
+
// most ONE descriptor per package (the wire references a package by
|
|
811
|
+
// name and the server resolves exactly one descriptor; two rows for
|
|
812
|
+
// one package trip the fatal `LINT_GADGET_DUPLICATE_PACKAGE` lint).
|
|
813
|
+
// Re-installing any version of an already-declared package replaces
|
|
814
|
+
// its row, so a version upgrade never accumulates the older row.
|
|
804
815
|
const existingIdx = libs.findIndex((e) => {
|
|
805
816
|
if (e === null || typeof e !== 'object')
|
|
806
817
|
return false;
|
|
807
818
|
const cand = e;
|
|
808
|
-
return cand.package === entry.package
|
|
819
|
+
return cand.package === entry.package;
|
|
809
820
|
});
|
|
810
821
|
if (existingIdx >= 0) {
|
|
822
|
+
const prior = libs[existingIdx];
|
|
823
|
+
const replacedVersion = typeof prior.version === 'string' ? prior.version : undefined;
|
|
811
824
|
libs[existingIdx] = entry;
|
|
825
|
+
return {
|
|
826
|
+
ok: true,
|
|
827
|
+
...(replacedVersion !== undefined ? { replacedVersion } : {}),
|
|
828
|
+
};
|
|
812
829
|
}
|
|
813
|
-
|
|
814
|
-
libs.push(entry);
|
|
815
|
-
}
|
|
830
|
+
libs.push(entry);
|
|
816
831
|
return { ok: true };
|
|
817
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Format a zod error's issues as a one-line `path: message` list for
|
|
835
|
+
* stderr diagnostics.
|
|
836
|
+
*/
|
|
837
|
+
function formatZodIssues(error) {
|
|
838
|
+
return error.issues
|
|
839
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
840
|
+
.join('; ');
|
|
841
|
+
}
|
|
842
|
+
// The post-append catalog read routes through the package's ONE
|
|
843
|
+
// gadget-catalog reader — `readGadgetsFromGguiJson` in
|
|
844
|
+
// `./ggui-json.ts`, shared with the `ggui deploy` config push.
|
|
818
845
|
/* -------------------------------------------------------------------------- */
|
|
819
846
|
/* publicEnv prompts */
|
|
820
847
|
/* -------------------------------------------------------------------------- */
|
|
@@ -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.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
/**
|
|
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":"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|