@ggui-ai/cli 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +96 -0
- package/dist/agent-resolution.d.ts +41 -0
- package/dist/agent-resolution.d.ts.map +1 -0
- package/dist/agent-resolution.js +80 -0
- package/dist/api-client.d.ts +59 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +145 -0
- package/dist/auth-keys.d.ts +10 -0
- package/dist/auth-keys.d.ts.map +1 -0
- package/dist/auth-keys.js +382 -0
- package/dist/auth-login.d.ts +11 -0
- package/dist/auth-login.d.ts.map +1 -0
- package/dist/auth-login.js +231 -0
- package/dist/auth-logout.d.ts +3 -0
- package/dist/auth-logout.d.ts.map +1 -0
- package/dist/auth-logout.js +32 -0
- package/dist/auth-store.d.ts +67 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +150 -0
- package/dist/auth-whoami.d.ts +3 -0
- package/dist/auth-whoami.d.ts.map +1 -0
- package/dist/auth-whoami.js +52 -0
- package/dist/blueprint-command.d.ts +3 -0
- package/dist/blueprint-command.d.ts.map +1 -0
- package/dist/blueprint-command.js +203 -0
- package/dist/blueprint-create.d.ts +71 -0
- package/dist/blueprint-create.d.ts.map +1 -0
- package/dist/blueprint-create.js +377 -0
- package/dist/byok-resolver.d.ts +141 -0
- package/dist/byok-resolver.d.ts.map +1 -0
- package/dist/byok-resolver.js +89 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1066 -0
- package/dist/dev-command.d.ts +169 -0
- package/dist/dev-command.d.ts.map +1 -0
- package/dist/dev-command.js +222 -0
- package/dist/gadget-command.d.ts +3 -0
- package/dist/gadget-command.d.ts.map +1 -0
- package/dist/gadget-command.js +181 -0
- package/dist/gadget-create.d.ts +106 -0
- package/dist/gadget-create.d.ts.map +1 -0
- package/dist/gadget-create.js +492 -0
- package/dist/generation-probe.d.ts +135 -0
- package/dist/generation-probe.d.ts.map +1 -0
- package/dist/generation-probe.js +132 -0
- package/dist/internal/artifact-install.d.ts +150 -0
- package/dist/internal/artifact-install.d.ts.map +1 -0
- package/dist/internal/artifact-install.js +1019 -0
- package/dist/internal/artifact-publish.d.ts +204 -0
- package/dist/internal/artifact-publish.d.ts.map +1 -0
- package/dist/internal/artifact-publish.js +1141 -0
- package/dist/internal/artifact-search.d.ts +155 -0
- package/dist/internal/artifact-search.d.ts.map +1 -0
- package/dist/internal/artifact-search.js +539 -0
- package/dist/internal/artifact-uninstall.d.ts +30 -0
- package/dist/internal/artifact-uninstall.d.ts.map +1 -0
- package/dist/internal/artifact-uninstall.js +205 -0
- package/dist/internal/auth-cache.d.ts +40 -0
- package/dist/internal/auth-cache.d.ts.map +1 -0
- package/dist/internal/auth-cache.js +133 -0
- package/dist/internal/auth-strategy.d.ts +89 -0
- package/dist/internal/auth-strategy.d.ts.map +1 -0
- package/dist/internal/auth-strategy.js +132 -0
- package/dist/internal/ggui-json.d.ts +53 -0
- package/dist/internal/ggui-json.d.ts.map +1 -0
- package/dist/internal/ggui-json.js +92 -0
- package/dist/internal/key-store.d.ts +43 -0
- package/dist/internal/key-store.d.ts.map +1 -0
- package/dist/internal/key-store.js +128 -0
- package/dist/internal/oidc-token.d.ts +52 -0
- package/dist/internal/oidc-token.d.ts.map +1 -0
- package/dist/internal/oidc-token.js +351 -0
- package/dist/internal/register-author-key.d.ts +44 -0
- package/dist/internal/register-author-key.d.ts.map +1 -0
- package/dist/internal/register-author-key.js +212 -0
- package/dist/local-keys.d.ts +47 -0
- package/dist/local-keys.d.ts.map +1 -0
- package/dist/local-keys.js +115 -0
- package/dist/mcp-backend.d.ts +356 -0
- package/dist/mcp-backend.d.ts.map +1 -0
- package/dist/mcp-backend.js +668 -0
- package/dist/paths.d.ts +77 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +119 -0
- package/dist/persistent-secrets.d.ts +28 -0
- package/dist/persistent-secrets.d.ts.map +1 -0
- package/dist/persistent-secrets.js +94 -0
- package/dist/persistent-short-code-index.d.ts +14 -0
- package/dist/persistent-short-code-index.d.ts.map +1 -0
- package/dist/persistent-short-code-index.js +39 -0
- package/dist/persistent-stores.d.ts +12 -0
- package/dist/persistent-stores.d.ts.map +1 -0
- package/dist/persistent-stores.js +42 -0
- package/dist/serve-command.d.ts +500 -0
- package/dist/serve-command.d.ts.map +1 -0
- package/dist/serve-command.js +616 -0
- package/dist/serve-fallback.d.ts +64 -0
- package/dist/serve-fallback.d.ts.map +1 -0
- package/dist/serve-fallback.js +122 -0
- package/dist/spawn-cloudflared.d.ts +61 -0
- package/dist/spawn-cloudflared.d.ts.map +1 -0
- package/dist/spawn-cloudflared.js +167 -0
- package/dist/theme-command.d.ts +4 -0
- package/dist/theme-command.d.ts.map +1 -0
- package/dist/theme-command.js +154 -0
- package/dist/theme-file-uploader.d.ts +8 -0
- package/dist/theme-file-uploader.d.ts.map +1 -0
- package/dist/theme-file-uploader.js +58 -0
- package/dist/theme-writer.d.ts +8 -0
- package/dist/theme-writer.d.ts.map +1 -0
- package/dist/theme-writer.js +103 -0
- package/dist/tunnel-provider.d.ts +273 -0
- package/dist/tunnel-provider.d.ts.map +1 -0
- package/dist/tunnel-provider.js +142 -0
- package/package.json +81 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type GadgetManifest } from '@ggui-ai/artifact-manifest';
|
|
2
|
+
/**
|
|
3
|
+
* Parsed `<scope>/<name>` identifier. Scope INCLUDES the leading `@`
|
|
4
|
+
* (mirrors the manifest schema's `ArtifactScopeSchema`).
|
|
5
|
+
*/
|
|
6
|
+
export interface ParsedScopeName {
|
|
7
|
+
readonly scope: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse a `<scope>/<name>` positional argument. Accepts only the
|
|
12
|
+
* canonical npm-style `@scope/name` shape. Returns `null` on any
|
|
13
|
+
* structural mismatch — caller renders the help text + exits non-zero.
|
|
14
|
+
*
|
|
15
|
+
* Loose at this layer (no character-class checks); the strict regex
|
|
16
|
+
* lives on the manifest schema and runs at the validation step before
|
|
17
|
+
* write. Keeps this parser focused on the one thing it can know:
|
|
18
|
+
* "is there a scope/name structure here at all?".
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseScopeName(raw: string): ParsedScopeName | null;
|
|
21
|
+
/**
|
|
22
|
+
* Convert a kebab-case gadget name into a `use<PascalCase>` hook name.
|
|
23
|
+
* Matches the brief: `weather-card` → `useWeatherCard`. Numeric
|
|
24
|
+
* segments collapse alongside hyphens — `mapbox-v2` → `useMapboxV2`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deriveHookName(gadgetName: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Flag bundle accepted by {@link runGadgetCreate}. Mirrors the CLI's
|
|
29
|
+
* positional + flag surface; pulled out so `cli.ts` can build it from
|
|
30
|
+
* `process.argv` and tests can build it from object literals.
|
|
31
|
+
*/
|
|
32
|
+
export interface GadgetCreateFlags {
|
|
33
|
+
/** Positional `<scope>/<name>`. Validated in {@link runGadgetCreate}. */
|
|
34
|
+
readonly scopeName: string;
|
|
35
|
+
/**
|
|
36
|
+
* Target directory. Defaults to the `name` portion of `scopeName`
|
|
37
|
+
* (i.e. `@my-org/weather-card` → `./weather-card`). Resolved relative
|
|
38
|
+
* to `cwd` at write time.
|
|
39
|
+
*/
|
|
40
|
+
readonly dir?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Hook export name. Defaults to {@link deriveHookName}(`name`).
|
|
43
|
+
*/
|
|
44
|
+
readonly hook?: string;
|
|
45
|
+
/** Optional one-line `description` for the manifest. */
|
|
46
|
+
readonly description?: string;
|
|
47
|
+
/** Manifest `visibility`. Defaults to {@link DEFAULT_VISIBILITY}. */
|
|
48
|
+
readonly visibility?: GadgetManifest['visibility'];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Flag-parse helper. Mirrors the shape used by other CLI subcommands —
|
|
52
|
+
* returns a discriminated `{error}` vs. successful flags so the caller
|
|
53
|
+
* doesn't throw on user input.
|
|
54
|
+
*/
|
|
55
|
+
export interface ParsedCreateFlags {
|
|
56
|
+
readonly flags?: GadgetCreateFlags;
|
|
57
|
+
/** `'__help__'` for `--help`; other strings = usage error. */
|
|
58
|
+
readonly error?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function parseGadgetCreateFlags(args: readonly string[]): ParsedCreateFlags;
|
|
61
|
+
/** Successful scaffold output — list of relative paths written. */
|
|
62
|
+
export interface GadgetCreateSuccess {
|
|
63
|
+
readonly ok: true;
|
|
64
|
+
/** Absolute path to the scaffolded gadget directory. */
|
|
65
|
+
readonly targetDir: string;
|
|
66
|
+
/** Files written, relative to `targetDir`. */
|
|
67
|
+
readonly files: readonly string[];
|
|
68
|
+
/** Resolved hook name (auto-derived or operator-supplied). */
|
|
69
|
+
readonly hook: string;
|
|
70
|
+
/** Resolved manifest — useful for callers that want to chain on it. */
|
|
71
|
+
readonly manifest: GadgetManifest;
|
|
72
|
+
}
|
|
73
|
+
/** Structured failure. `code` is the stable error surface for tests. */
|
|
74
|
+
export interface GadgetCreateFailure {
|
|
75
|
+
readonly ok: false;
|
|
76
|
+
/** Machine-readable failure code. */
|
|
77
|
+
readonly code: 'invalid-scope-name' | 'target-not-empty' | 'manifest-invalid' | 'fs-error';
|
|
78
|
+
/** Human-readable diagnostic — safe to write to stderr verbatim. */
|
|
79
|
+
readonly message: string;
|
|
80
|
+
}
|
|
81
|
+
export type GadgetCreateResult = GadgetCreateSuccess | GadgetCreateFailure;
|
|
82
|
+
/**
|
|
83
|
+
* Build a {@link GadgetManifest} from CLI flags. Separated from the
|
|
84
|
+
* write step so tests can assert the manifest shape without touching
|
|
85
|
+
* the filesystem. Runs through {@link parseGadgetManifest} so a
|
|
86
|
+
* template regression (missing field, typo in `kind`) fails LOUD at
|
|
87
|
+
* the scaffolder rather than silently shipping a broken `ggui.gadget.
|
|
88
|
+
* json` that explodes on the first `ggui gadget publish`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function buildScaffoldManifest(args: {
|
|
91
|
+
scope: string;
|
|
92
|
+
name: string;
|
|
93
|
+
hook: string;
|
|
94
|
+
visibility: GadgetManifest['visibility'];
|
|
95
|
+
description?: string;
|
|
96
|
+
}): GadgetManifest;
|
|
97
|
+
/**
|
|
98
|
+
* Run the scaffold end-to-end. Resolves the target dir, validates the
|
|
99
|
+
* manifest, writes files. Returns a structured result so the CLI
|
|
100
|
+
* driver can render copy + exit code without re-deriving paths.
|
|
101
|
+
*/
|
|
102
|
+
export declare function runGadgetCreate(flags: GadgetCreateFlags, options: {
|
|
103
|
+
cwd: string;
|
|
104
|
+
}): Promise<GadgetCreateResult>;
|
|
105
|
+
export declare const GADGET_CREATE_HELP = "ggui gadget create \u2014 scaffold a new gadget repo\n\nUsage:\n ggui gadget create <scope/name> [options]\n\nArguments:\n <scope/name> Required. `@my-org/weather-card` style identifier.\n The scope (with leading `@`) becomes the manifest's\n `scope`; the segment after `/` becomes the `name`.\n\nOptions:\n --dir <path> Target directory. Defaults to `./<name>`.\n --hook <hookName> Exported hook name. Defaults to\n `use<NameInPascalCase>` derived from <name>\n (e.g. `weather-card` \u2192 `useWeatherCard`).\n --description <text> One-line description threaded into the\n manifest + README.\n --visibility <p> `public` (default, sigstore-signed) or\n `private` (Ed25519-signed, org-only).\n --help, -h Show this help.\n\nOutput:\n <dir>/ggui.gadget.json Strict-schema manifest (validated before write).\n <dir>/package.json Workspace shell (`type: module`, peerDeps).\n <dir>/tsconfig.json Strict TS, ESNext, bundler resolution.\n <dir>/src/index.ts `GadgetHook`-shaped hook stub.\n <dir>/README.md One-paragraph orientation + usage.\n\nAfter scaffold:\n cd <dir> && pnpm install\n # implement the hook in src/index.ts, then:\n ggui gadget publish # Slice 3.4 \u2014 publishes to the resolved registry.\n";
|
|
106
|
+
//# sourceMappingURL=gadget-create.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gadget-create.d.ts","sourceRoot":"","sources":["../src/gadget-create.ts"],"names":[],"mappings":"AAoBA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,4BAA4B,CAAC;AAQpC;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CASlE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;CACpD;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC;IACnC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,iBAAiB,CA4EnB;AAED,mEAAmE;AACnE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;CACnC;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EACT,oBAAoB,GACpB,kBAAkB,GAClB,kBAAkB,GAClB,UAAU,CAAC;IACf,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,cAAc,CAoCjB;AA0ND;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GACvB,OAAO,CAAC,kBAAkB,CAAC,CA+H7B;AAED,eAAO,MAAM,kBAAkB,w6CAgC9B,CAAC"}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ggui gadget create <scope/name>` — scaffold a new gadget repo.
|
|
3
|
+
*
|
|
4
|
+
* Generates the minimal file set an author needs to start writing a
|
|
5
|
+
* gadget: manifest, package.json, tsconfig, src entry, README. The
|
|
6
|
+
* generated manifest is validated against `gadgetManifestSchema` from
|
|
7
|
+
* `@ggui-ai/artifact-manifest` before any file is written — defense in
|
|
8
|
+
* depth that catches template regressions, not author error (authors
|
|
9
|
+
* never see this code path).
|
|
10
|
+
*
|
|
11
|
+
* Distinct from `ggui gadget publish`, which reads the scaffolded
|
|
12
|
+
* manifest, builds the bundle, and uploads to the resolved registry.
|
|
13
|
+
* Create is local-only; no network calls.
|
|
14
|
+
*
|
|
15
|
+
* Kept pure / testable — no `process.exit`, no direct stdout writes
|
|
16
|
+
* (errors return as structured results). `cli.ts` composes this with
|
|
17
|
+
* the real argv + writes the banner.
|
|
18
|
+
*/
|
|
19
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { GGUI_GADGET_JSON_FILENAME, parseGadgetManifest, } from '@ggui-ai/artifact-manifest';
|
|
22
|
+
/** Default version for a freshly scaffolded gadget. */
|
|
23
|
+
const DEFAULT_VERSION = '0.0.1';
|
|
24
|
+
/** Default visibility — public matches the marketplace's primary path. */
|
|
25
|
+
const DEFAULT_VISIBILITY = 'public';
|
|
26
|
+
/**
|
|
27
|
+
* Parse a `<scope>/<name>` positional argument. Accepts only the
|
|
28
|
+
* canonical npm-style `@scope/name` shape. Returns `null` on any
|
|
29
|
+
* structural mismatch — caller renders the help text + exits non-zero.
|
|
30
|
+
*
|
|
31
|
+
* Loose at this layer (no character-class checks); the strict regex
|
|
32
|
+
* lives on the manifest schema and runs at the validation step before
|
|
33
|
+
* write. Keeps this parser focused on the one thing it can know:
|
|
34
|
+
* "is there a scope/name structure here at all?".
|
|
35
|
+
*/
|
|
36
|
+
export function parseScopeName(raw) {
|
|
37
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
if (!raw.startsWith('@'))
|
|
40
|
+
return null;
|
|
41
|
+
const slash = raw.indexOf('/');
|
|
42
|
+
if (slash === -1)
|
|
43
|
+
return null;
|
|
44
|
+
const scope = raw.slice(0, slash);
|
|
45
|
+
const name = raw.slice(slash + 1);
|
|
46
|
+
if (scope.length < 2 || name.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
return { scope, name };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Convert a kebab-case gadget name into a `use<PascalCase>` hook name.
|
|
52
|
+
* Matches the brief: `weather-card` → `useWeatherCard`. Numeric
|
|
53
|
+
* segments collapse alongside hyphens — `mapbox-v2` → `useMapboxV2`.
|
|
54
|
+
*/
|
|
55
|
+
export function deriveHookName(gadgetName) {
|
|
56
|
+
const segments = gadgetName
|
|
57
|
+
.split('-')
|
|
58
|
+
.filter((s) => s.length > 0)
|
|
59
|
+
.map((s) => s[0].toUpperCase() + s.slice(1));
|
|
60
|
+
return `use${segments.join('')}`;
|
|
61
|
+
}
|
|
62
|
+
export function parseGadgetCreateFlags(args) {
|
|
63
|
+
if (args.length === 0) {
|
|
64
|
+
return { error: '__help__' };
|
|
65
|
+
}
|
|
66
|
+
let scopeName;
|
|
67
|
+
let dir;
|
|
68
|
+
let hook;
|
|
69
|
+
let description;
|
|
70
|
+
let visibility;
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
const arg = args[i];
|
|
73
|
+
if (arg === '--help' || arg === '-h') {
|
|
74
|
+
return { error: '__help__' };
|
|
75
|
+
}
|
|
76
|
+
if (arg === '--dir') {
|
|
77
|
+
const value = args[i + 1];
|
|
78
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
79
|
+
return { error: '--dir requires a value' };
|
|
80
|
+
}
|
|
81
|
+
dir = value;
|
|
82
|
+
i += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg === '--hook') {
|
|
86
|
+
const value = args[i + 1];
|
|
87
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
88
|
+
return { error: '--hook requires a value' };
|
|
89
|
+
}
|
|
90
|
+
hook = value;
|
|
91
|
+
i += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === '--description') {
|
|
95
|
+
const value = args[i + 1];
|
|
96
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
97
|
+
return { error: '--description requires a value' };
|
|
98
|
+
}
|
|
99
|
+
description = value;
|
|
100
|
+
i += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (arg === '--visibility') {
|
|
104
|
+
const value = args[i + 1];
|
|
105
|
+
if (value !== 'public' && value !== 'private') {
|
|
106
|
+
return {
|
|
107
|
+
error: '--visibility must be "public" or "private"',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
visibility = value;
|
|
111
|
+
i += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg.startsWith('--')) {
|
|
115
|
+
return { error: `unknown flag: ${arg}` };
|
|
116
|
+
}
|
|
117
|
+
if (scopeName === undefined) {
|
|
118
|
+
scopeName = arg;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
return { error: `unexpected positional argument: ${arg}` };
|
|
122
|
+
}
|
|
123
|
+
if (scopeName === undefined) {
|
|
124
|
+
return { error: 'missing required <scope/name> positional argument' };
|
|
125
|
+
}
|
|
126
|
+
const flags = {
|
|
127
|
+
scopeName,
|
|
128
|
+
...(dir !== undefined ? { dir } : {}),
|
|
129
|
+
...(hook !== undefined ? { hook } : {}),
|
|
130
|
+
...(description !== undefined ? { description } : {}),
|
|
131
|
+
...(visibility !== undefined ? { visibility } : {}),
|
|
132
|
+
};
|
|
133
|
+
return { flags };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build a {@link GadgetManifest} from CLI flags. Separated from the
|
|
137
|
+
* write step so tests can assert the manifest shape without touching
|
|
138
|
+
* the filesystem. Runs through {@link parseGadgetManifest} so a
|
|
139
|
+
* template regression (missing field, typo in `kind`) fails LOUD at
|
|
140
|
+
* the scaffolder rather than silently shipping a broken `ggui.gadget.
|
|
141
|
+
* json` that explodes on the first `ggui gadget publish`.
|
|
142
|
+
*/
|
|
143
|
+
export function buildScaffoldManifest(args) {
|
|
144
|
+
// A gadget manifest describes a PACKAGE: the package-level
|
|
145
|
+
// `description` is required at registry side, and each `exports[*]`
|
|
146
|
+
// entry carries its own required `description` / `usage` / `example`
|
|
147
|
+
// teaching text. Scaffold sensible defaults so a freshly-created repo
|
|
148
|
+
// passes `parseGadgetManifest` immediately. Authors should edit
|
|
149
|
+
// `ggui.gadget.json` to refine the LLM teaching text before publish.
|
|
150
|
+
const description = args.description ?? `${args.scope}/${args.name} — ggui gadget.`;
|
|
151
|
+
const exportUsage = `Use \`${args.hook}\` whenever the agent needs to expose ${args.scope}/${args.name}'s capabilities to the UI. ` +
|
|
152
|
+
`Edit this field in ggui.gadget.json before publish to refine the LLM's context-of-use prompt.`;
|
|
153
|
+
const exportExample = {};
|
|
154
|
+
const raw = {
|
|
155
|
+
kind: 'gadget',
|
|
156
|
+
scope: args.scope,
|
|
157
|
+
name: args.name,
|
|
158
|
+
version: DEFAULT_VERSION,
|
|
159
|
+
bundle: 'src/index.ts',
|
|
160
|
+
visibility: args.visibility,
|
|
161
|
+
description,
|
|
162
|
+
exports: [
|
|
163
|
+
{
|
|
164
|
+
hook: args.hook,
|
|
165
|
+
description,
|
|
166
|
+
usage: exportUsage,
|
|
167
|
+
example: exportExample,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
tags: [],
|
|
171
|
+
peerDeps: { react: '^19.0.0' },
|
|
172
|
+
};
|
|
173
|
+
// Defense-in-depth — `parseGadgetManifest` enforces the schema. A
|
|
174
|
+
// hand-edited template that drifts (e.g. drops `bundle`) blows up
|
|
175
|
+
// here, before any write, so authors don't see a corrupt scaffold.
|
|
176
|
+
return parseGadgetManifest(raw);
|
|
177
|
+
}
|
|
178
|
+
function buildPackageJson(args) {
|
|
179
|
+
return {
|
|
180
|
+
name: `${args.scope}/${args.name}`,
|
|
181
|
+
version: DEFAULT_VERSION,
|
|
182
|
+
...(args.description !== undefined ? { description: args.description } : {}),
|
|
183
|
+
type: 'module',
|
|
184
|
+
main: 'dist/index.js',
|
|
185
|
+
types: 'dist/index.d.ts',
|
|
186
|
+
files: ['dist', GGUI_GADGET_JSON_FILENAME, 'README.md'],
|
|
187
|
+
scripts: {
|
|
188
|
+
build: 'tsc -p tsconfig.json',
|
|
189
|
+
typecheck: 'tsc --noEmit',
|
|
190
|
+
},
|
|
191
|
+
peerDependencies: {
|
|
192
|
+
'@ggui-ai/gadgets': '*',
|
|
193
|
+
react: '^19.0.0',
|
|
194
|
+
},
|
|
195
|
+
devDependencies: {
|
|
196
|
+
typescript: '^5.0.0',
|
|
197
|
+
'@types/react': '^19.0.0',
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** `tsconfig.json` template — strict TS, ESNext, bundler resolution. */
|
|
202
|
+
function buildTsconfig() {
|
|
203
|
+
return {
|
|
204
|
+
compilerOptions: {
|
|
205
|
+
target: 'ES2022',
|
|
206
|
+
module: 'ESNext',
|
|
207
|
+
moduleResolution: 'bundler',
|
|
208
|
+
jsx: 'react-jsx',
|
|
209
|
+
strict: true,
|
|
210
|
+
declaration: true,
|
|
211
|
+
esModuleInterop: true,
|
|
212
|
+
skipLibCheck: true,
|
|
213
|
+
outDir: 'dist',
|
|
214
|
+
rootDir: 'src',
|
|
215
|
+
},
|
|
216
|
+
include: ['src'],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* `src/index.ts` template — a typed-hook stub conforming to
|
|
221
|
+
* {@link import('@ggui-ai/protocol').GadgetHook}. The
|
|
222
|
+
* marketplace renders this hook into a contract's
|
|
223
|
+
* `clientCapabilities.gadgets` entry; the UI generator emits a
|
|
224
|
+
* matching call site. Authors fill in the body.
|
|
225
|
+
*/
|
|
226
|
+
function buildSrcIndex(args) {
|
|
227
|
+
return `import { useCallback, useState } from 'react';
|
|
228
|
+
import type { GadgetHook, GadgetStatus } from '@ggui-ai/protocol';
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Output shape for ${args.hook}. Returned by the hook's \`value\`
|
|
232
|
+
* field when \`status === 'completed'\` or \`'active'\`.
|
|
233
|
+
*
|
|
234
|
+
* TODO: replace with the real shape your gadget exposes.
|
|
235
|
+
*/
|
|
236
|
+
export interface ${capitalizeHookOutput(args.hook)} {
|
|
237
|
+
readonly placeholder: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Options accepted by ${args.hook}. Optional; gadgets that take no
|
|
242
|
+
* options can leave this as \`void\` on the type parameter and drop
|
|
243
|
+
* the interface entirely.
|
|
244
|
+
*
|
|
245
|
+
* TODO: replace with real options or remove if unused.
|
|
246
|
+
*/
|
|
247
|
+
export interface ${capitalizeHookOptions(args.hook)} {
|
|
248
|
+
readonly placeholder?: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* ${args.hook} — gadget hook entry. Conforms to
|
|
253
|
+
* \`GadgetHook<TOutput, TOptions>\` so the ggui registry can
|
|
254
|
+
* register it as a client library.
|
|
255
|
+
*
|
|
256
|
+
* Lifecycle (see \`GadgetStatus\` for the full set):
|
|
257
|
+
* - \`idle\` — initial.
|
|
258
|
+
* - \`active\` / \`completed\` — value available.
|
|
259
|
+
* - \`denied\` / \`error\` — see \`error\` for diagnostics.
|
|
260
|
+
*/
|
|
261
|
+
export const ${args.hook}: GadgetHook<
|
|
262
|
+
${capitalizeHookOutput(args.hook)},
|
|
263
|
+
${capitalizeHookOptions(args.hook)}
|
|
264
|
+
> = (_options) => {
|
|
265
|
+
const [value, _setValue] = useState<${capitalizeHookOutput(args.hook)} | undefined>(
|
|
266
|
+
undefined,
|
|
267
|
+
);
|
|
268
|
+
const [status, _setStatus] = useState<GadgetStatus>('idle');
|
|
269
|
+
|
|
270
|
+
const start = useCallback(async (): Promise<
|
|
271
|
+
${capitalizeHookOutput(args.hook)} | undefined
|
|
272
|
+
> => {
|
|
273
|
+
// TODO: implement the gadget. Update \`status\` + \`value\` as the
|
|
274
|
+
// capability progresses through its lifecycle.
|
|
275
|
+
return undefined;
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
return { value, status, start };
|
|
279
|
+
};
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
function capitalizeHookOutput(hook) {
|
|
283
|
+
// `useFoo` → `FooValue`. Suffixed with `Value` so it doesn't clash
|
|
284
|
+
// with the hook name if the gadget ever wants both exported.
|
|
285
|
+
const stripped = hook.startsWith('use') ? hook.slice(3) : hook;
|
|
286
|
+
const cap = stripped.length > 0 ? stripped[0].toUpperCase() + stripped.slice(1) : 'Result';
|
|
287
|
+
return `${cap}Value`;
|
|
288
|
+
}
|
|
289
|
+
function capitalizeHookOptions(hook) {
|
|
290
|
+
const stripped = hook.startsWith('use') ? hook.slice(3) : hook;
|
|
291
|
+
const cap = stripped.length > 0 ? stripped[0].toUpperCase() + stripped.slice(1) : 'Options';
|
|
292
|
+
return `${cap}Options`;
|
|
293
|
+
}
|
|
294
|
+
/** README — single short paragraph + usage snippet. No frontmatter. */
|
|
295
|
+
function buildReadme(args) {
|
|
296
|
+
const summary = args.description ?? `${args.scope}/${args.name} — ggui gadget.`;
|
|
297
|
+
return `# ${args.scope}/${args.name}
|
|
298
|
+
|
|
299
|
+
${summary}
|
|
300
|
+
|
|
301
|
+
## Develop
|
|
302
|
+
|
|
303
|
+
\`\`\`bash
|
|
304
|
+
pnpm install
|
|
305
|
+
pnpm typecheck
|
|
306
|
+
\`\`\`
|
|
307
|
+
|
|
308
|
+
## Publish
|
|
309
|
+
|
|
310
|
+
\`\`\`bash
|
|
311
|
+
ggui gadget publish
|
|
312
|
+
\`\`\`
|
|
313
|
+
|
|
314
|
+
See [ggui gadget docs](https://ggui.ai/docs/gadgets) for the full
|
|
315
|
+
marketplace flow.
|
|
316
|
+
|
|
317
|
+
## Use
|
|
318
|
+
|
|
319
|
+
Contract authors declare this gadget on a \`DataContract\`:
|
|
320
|
+
|
|
321
|
+
\`\`\`ts
|
|
322
|
+
clientCapabilities: {
|
|
323
|
+
gadgets: {
|
|
324
|
+
${stripUsePrefix(args.hook)}: { hook: '${args.hook}' },
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
\`\`\`
|
|
328
|
+
|
|
329
|
+
The UI generator emits the matching \`import { ${args.hook} } from '${args.scope}/${args.name}'\`
|
|
330
|
+
and call site automatically.
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
function stripUsePrefix(hook) {
|
|
334
|
+
const stripped = hook.startsWith('use') ? hook.slice(3) : hook;
|
|
335
|
+
return stripped.length > 0
|
|
336
|
+
? stripped[0].toLowerCase() + stripped.slice(1)
|
|
337
|
+
: 'capability';
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Refuse-to-overwrite check. A non-empty target directory is the only
|
|
341
|
+
* destructive case we guard against — empty + missing both green-light
|
|
342
|
+
* the scaffold. Matches the discipline `ggui dev` and `ggui serve`
|
|
343
|
+
* apply elsewhere (no silent file rewrites).
|
|
344
|
+
*/
|
|
345
|
+
async function isDirectoryEmpty(path) {
|
|
346
|
+
try {
|
|
347
|
+
const entries = await readdir(path);
|
|
348
|
+
return { exists: true, empty: entries.length === 0 };
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
352
|
+
return { exists: false, empty: true };
|
|
353
|
+
}
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Run the scaffold end-to-end. Resolves the target dir, validates the
|
|
359
|
+
* manifest, writes files. Returns a structured result so the CLI
|
|
360
|
+
* driver can render copy + exit code without re-deriving paths.
|
|
361
|
+
*/
|
|
362
|
+
export async function runGadgetCreate(flags, options) {
|
|
363
|
+
const parsed = parseScopeName(flags.scopeName);
|
|
364
|
+
if (!parsed) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
code: 'invalid-scope-name',
|
|
368
|
+
message: `"${flags.scopeName}" is not a valid <scope>/<name>. Expected \`@org/gadget-name\` (e.g. \`@my-org/weather-card\`).`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const { scope, name } = parsed;
|
|
372
|
+
const hook = flags.hook ?? deriveHookName(name);
|
|
373
|
+
const visibility = flags.visibility ?? DEFAULT_VISIBILITY;
|
|
374
|
+
// Build + validate the manifest BEFORE any filesystem write. If the
|
|
375
|
+
// schema rejects (e.g. scope/name regex fails) we abort with a
|
|
376
|
+
// structured error and the target dir stays untouched.
|
|
377
|
+
let manifest;
|
|
378
|
+
try {
|
|
379
|
+
manifest = buildScaffoldManifest({
|
|
380
|
+
scope,
|
|
381
|
+
name,
|
|
382
|
+
hook,
|
|
383
|
+
visibility,
|
|
384
|
+
...(flags.description !== undefined
|
|
385
|
+
? { description: flags.description }
|
|
386
|
+
: {}),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
code: 'manifest-invalid',
|
|
393
|
+
message: err instanceof Error ? err.message : 'failed to validate manifest',
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const targetDir = join(options.cwd, flags.dir ?? name);
|
|
397
|
+
let dirState;
|
|
398
|
+
try {
|
|
399
|
+
dirState = await isDirectoryEmpty(targetDir);
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
code: 'fs-error',
|
|
405
|
+
message: err instanceof Error ? err.message : 'failed to read target dir',
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (dirState.exists && !dirState.empty) {
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
code: 'target-not-empty',
|
|
412
|
+
message: `target directory ${targetDir} is not empty. Refusing to overwrite — choose a different --dir or remove existing files.`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
await mkdir(targetDir, { recursive: true });
|
|
417
|
+
await mkdir(join(targetDir, 'src'), { recursive: true });
|
|
418
|
+
const manifestPath = join(targetDir, GGUI_GADGET_JSON_FILENAME);
|
|
419
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
420
|
+
const pkg = buildPackageJson({
|
|
421
|
+
scope,
|
|
422
|
+
name,
|
|
423
|
+
...(flags.description !== undefined
|
|
424
|
+
? { description: flags.description }
|
|
425
|
+
: {}),
|
|
426
|
+
});
|
|
427
|
+
await writeFile(join(targetDir, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf-8');
|
|
428
|
+
await writeFile(join(targetDir, 'tsconfig.json'), `${JSON.stringify(buildTsconfig(), null, 2)}\n`, 'utf-8');
|
|
429
|
+
await writeFile(join(targetDir, 'src', 'index.ts'), buildSrcIndex({ hook }), 'utf-8');
|
|
430
|
+
await writeFile(join(targetDir, 'README.md'), buildReadme({
|
|
431
|
+
scope,
|
|
432
|
+
name,
|
|
433
|
+
hook,
|
|
434
|
+
...(flags.description !== undefined
|
|
435
|
+
? { description: flags.description }
|
|
436
|
+
: {}),
|
|
437
|
+
}), 'utf-8');
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
code: 'fs-error',
|
|
443
|
+
message: err instanceof Error ? err.message : 'failed to write files',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
ok: true,
|
|
448
|
+
targetDir,
|
|
449
|
+
files: [
|
|
450
|
+
GGUI_GADGET_JSON_FILENAME,
|
|
451
|
+
'package.json',
|
|
452
|
+
'tsconfig.json',
|
|
453
|
+
'src/index.ts',
|
|
454
|
+
'README.md',
|
|
455
|
+
],
|
|
456
|
+
hook,
|
|
457
|
+
manifest,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
export const GADGET_CREATE_HELP = `ggui gadget create — scaffold a new gadget repo
|
|
461
|
+
|
|
462
|
+
Usage:
|
|
463
|
+
ggui gadget create <scope/name> [options]
|
|
464
|
+
|
|
465
|
+
Arguments:
|
|
466
|
+
<scope/name> Required. \`@my-org/weather-card\` style identifier.
|
|
467
|
+
The scope (with leading \`@\`) becomes the manifest's
|
|
468
|
+
\`scope\`; the segment after \`/\` becomes the \`name\`.
|
|
469
|
+
|
|
470
|
+
Options:
|
|
471
|
+
--dir <path> Target directory. Defaults to \`./<name>\`.
|
|
472
|
+
--hook <hookName> Exported hook name. Defaults to
|
|
473
|
+
\`use<NameInPascalCase>\` derived from <name>
|
|
474
|
+
(e.g. \`weather-card\` → \`useWeatherCard\`).
|
|
475
|
+
--description <text> One-line description threaded into the
|
|
476
|
+
manifest + README.
|
|
477
|
+
--visibility <p> \`public\` (default, sigstore-signed) or
|
|
478
|
+
\`private\` (Ed25519-signed, org-only).
|
|
479
|
+
--help, -h Show this help.
|
|
480
|
+
|
|
481
|
+
Output:
|
|
482
|
+
<dir>/ggui.gadget.json Strict-schema manifest (validated before write).
|
|
483
|
+
<dir>/package.json Workspace shell (\`type: module\`, peerDeps).
|
|
484
|
+
<dir>/tsconfig.json Strict TS, ESNext, bundler resolution.
|
|
485
|
+
<dir>/src/index.ts \`GadgetHook\`-shaped hook stub.
|
|
486
|
+
<dir>/README.md One-paragraph orientation + usage.
|
|
487
|
+
|
|
488
|
+
After scaffold:
|
|
489
|
+
cd <dir> && pnpm install
|
|
490
|
+
# implement the hook in src/index.ts, then:
|
|
491
|
+
ggui gadget publish # Slice 3.4 — publishes to the resolved registry.
|
|
492
|
+
`;
|