@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.
- 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
|
@@ -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 (
|
|
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.
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
env,
|
|
412
|
+
cwd,
|
|
413
|
+
env: { GGUI_REGISTRY: env['GGUI_REGISTRY'] },
|
|
414
|
+
defaultUrl: DEFAULT_REGISTRY_URL,
|
|
474
415
|
});
|
|
475
|
-
if (
|
|
476
|
-
stderr(`${verb}: ${registryRes.
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
517
|
-
|
|
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
|
|
774
|
-
*
|
|
775
|
-
*
|
|
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 `
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
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);
|