@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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact-kind discriminator. Mirrors the `gadgetManifestSchema` /
|
|
3
|
+
* `blueprintManifestSchema` discriminators in
|
|
4
|
+
* `@ggui-ai/artifact-manifest`. Inlined here because the manifest
|
|
5
|
+
* package does not currently export a named `ArtifactKind` type — when
|
|
6
|
+
* it does, swap to a structural `import type` from there.
|
|
7
|
+
*/
|
|
8
|
+
export type ArtifactKind = 'gadget' | 'blueprint';
|
|
9
|
+
/**
|
|
10
|
+
* One row of `GET /search?…` results. Mirrors the registry's
|
|
11
|
+
* `SearchResultEntry` response type.
|
|
12
|
+
*/
|
|
13
|
+
export interface SearchResultEntry {
|
|
14
|
+
readonly artifactId: string;
|
|
15
|
+
readonly latestVersion: string;
|
|
16
|
+
readonly kind: ArtifactKind;
|
|
17
|
+
readonly description?: string;
|
|
18
|
+
readonly tags?: readonly string[];
|
|
19
|
+
readonly publishedAt: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `GET /search?…` response body. `nextCursor` is an opaque base64
|
|
23
|
+
* string the client roundtrips verbatim — never inspected client-side.
|
|
24
|
+
*/
|
|
25
|
+
export interface SearchResponse {
|
|
26
|
+
readonly results: readonly SearchResultEntry[];
|
|
27
|
+
readonly nextCursor?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Flag bundle accepted by {@link runArtifactSearch}. Mirrors the CLI's
|
|
31
|
+
* positional + flag surface. Built either from `process.argv` (via
|
|
32
|
+
* {@link parseArtifactSearchFlags}) or from object literals in tests.
|
|
33
|
+
*/
|
|
34
|
+
export interface ArtifactSearchFlags {
|
|
35
|
+
/** Optional positional. Maps to the `q` query param. */
|
|
36
|
+
readonly q?: string;
|
|
37
|
+
/** Filter on `kind`. */
|
|
38
|
+
readonly kind?: ArtifactKind;
|
|
39
|
+
/** Filter on `hook` (gadgets only). */
|
|
40
|
+
readonly hook?: string;
|
|
41
|
+
/** Filter on `tag` (exact match against any element of `row.tags`). */
|
|
42
|
+
readonly tag?: string;
|
|
43
|
+
/** Filter on `author` (caller subject id OR substring of authorName). */
|
|
44
|
+
readonly author?: string;
|
|
45
|
+
/** Page size. Registry caps at 200; default 50. */
|
|
46
|
+
readonly limit?: number;
|
|
47
|
+
/** Opaque cursor from a previous response. */
|
|
48
|
+
readonly cursor?: string;
|
|
49
|
+
/** Override registry URL. Beats env + ggui.json. */
|
|
50
|
+
readonly registry?: string;
|
|
51
|
+
/** Emit raw response JSON instead of the human-readable table. */
|
|
52
|
+
readonly json?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface ParsedSearchFlags {
|
|
55
|
+
readonly flags?: ArtifactSearchFlags;
|
|
56
|
+
/** `'__help__'` for `--help`; other strings = usage error. */
|
|
57
|
+
readonly error?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Flag parser. Returns a discriminated `{error}` vs. `{flags}` so the
|
|
61
|
+
* caller doesn't throw on user input. The `lockedKind` argument is the
|
|
62
|
+
* verb the operator typed — when supplied (the kind-discriminated
|
|
63
|
+
* router path), the `--kind` flag is rejected because the verb already
|
|
64
|
+
* pins the value. When `undefined` (legacy generic-search path, not
|
|
65
|
+
* exposed by the CLI today), `--kind` is honored as a freeform filter.
|
|
66
|
+
*
|
|
67
|
+
* For `lockedKind === 'blueprint'`, `--hook` is rejected because the
|
|
68
|
+
* hook filter is a gadget-only concern.
|
|
69
|
+
*
|
|
70
|
+
* Accepts both `--flag value` and `--flag=value` for every option.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseArtifactSearchFlags(lockedKind: ArtifactKind | undefined, args: readonly string[]): ParsedSearchFlags;
|
|
73
|
+
/**
|
|
74
|
+
* Three-layer resolution: `--registry` flag > `GGUI_REGISTRY` env >
|
|
75
|
+
* `ggui.json#registry` > error.
|
|
76
|
+
*
|
|
77
|
+
* `cwd` + `env` are injectable for tests. Returns either `{ url }` on
|
|
78
|
+
* success or `{ error }` with a stderr-ready diagnostic. Trailing
|
|
79
|
+
* slashes on the URL are normalized away so the URL builder can append
|
|
80
|
+
* `/search?…` deterministically.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveRegistryUrl(args: {
|
|
83
|
+
flag?: string;
|
|
84
|
+
cwd: string;
|
|
85
|
+
env: {
|
|
86
|
+
readonly GGUI_REGISTRY?: string | undefined;
|
|
87
|
+
};
|
|
88
|
+
}): {
|
|
89
|
+
url: string;
|
|
90
|
+
} | {
|
|
91
|
+
error: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Build a query string from {@link ArtifactSearchFlags}. Pulled out so
|
|
95
|
+
* tests can assert the wire shape without a real fetch. Empty / absent
|
|
96
|
+
* filters are dropped entirely (rather than emitted as `&foo=`).
|
|
97
|
+
*
|
|
98
|
+
* The returned string always starts with `?` unless every filter is
|
|
99
|
+
* absent, in which case the empty string is returned.
|
|
100
|
+
*/
|
|
101
|
+
export declare function buildSearchQueryString(flags: ArtifactSearchFlags): string;
|
|
102
|
+
/** Successful response — keeps a normalized snapshot for rendering. */
|
|
103
|
+
export interface SearchSuccess {
|
|
104
|
+
readonly ok: true;
|
|
105
|
+
readonly registryUrl: string;
|
|
106
|
+
readonly response: SearchResponse;
|
|
107
|
+
/**
|
|
108
|
+
* Pre-built human-readable lines, in order. Empty when `--json` is
|
|
109
|
+
* passed (the CLI emits the raw JSON body instead). Pre-baking the
|
|
110
|
+
* lines here keeps `gadget-command.ts` free of formatting logic and
|
|
111
|
+
* lets tests assert the exact strings.
|
|
112
|
+
*/
|
|
113
|
+
readonly lines: readonly string[];
|
|
114
|
+
/** Convenience — the same data the JSON output would emit. */
|
|
115
|
+
readonly json: string;
|
|
116
|
+
}
|
|
117
|
+
/** Structured failure. `code` is the stable error surface for tests. */
|
|
118
|
+
export interface SearchFailure {
|
|
119
|
+
readonly ok: false;
|
|
120
|
+
/** Machine-readable failure code. */
|
|
121
|
+
readonly code: 'no-registry' | 'invalid-registry' | 'network-error' | 'http-error' | 'bad-response';
|
|
122
|
+
/** Human-readable diagnostic — safe to write to stderr verbatim. */
|
|
123
|
+
readonly message: string;
|
|
124
|
+
}
|
|
125
|
+
export type SearchOutput = SearchSuccess | SearchFailure;
|
|
126
|
+
/**
|
|
127
|
+
* Dependencies for {@link runArtifactSearch}. All injectable so tests
|
|
128
|
+
* can stub `fetch` + env + cwd without touching globals.
|
|
129
|
+
*/
|
|
130
|
+
export interface RunArtifactSearchDeps {
|
|
131
|
+
readonly cwd: string;
|
|
132
|
+
readonly env: {
|
|
133
|
+
readonly GGUI_REGISTRY?: string | undefined;
|
|
134
|
+
};
|
|
135
|
+
readonly fetch: typeof fetch;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Run a search end-to-end:
|
|
139
|
+
*
|
|
140
|
+
* 1. Resolve the registry URL via the three-layer chain.
|
|
141
|
+
* 2. Build the query string.
|
|
142
|
+
* 3. GET `<registry>/search?<qs>` (no auth; public endpoint).
|
|
143
|
+
* 4. Parse + validate the response shape.
|
|
144
|
+
* 5. Format human-readable lines (or stash raw JSON for `--json`).
|
|
145
|
+
*
|
|
146
|
+
* Never throws — every error path returns a {@link SearchFailure}.
|
|
147
|
+
*/
|
|
148
|
+
export declare function runArtifactSearch(flags: ArtifactSearchFlags, deps: RunArtifactSearchDeps): Promise<SearchOutput>;
|
|
149
|
+
/**
|
|
150
|
+
* Build verb-specific help text. Takes the `kind` so the rendered
|
|
151
|
+
* lines say `ggui gadget search` / `ggui blueprint search` and the
|
|
152
|
+
* filter list omits gadget-only options on the blueprint surface.
|
|
153
|
+
*/
|
|
154
|
+
export declare function buildSearchHelp(kind: ArtifactKind): string;
|
|
155
|
+
//# sourceMappingURL=artifact-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact-search.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ggui gadget search [q]` — query the marketplace registry's
|
|
3
|
+
* `GET /search` endpoint and render results.
|
|
4
|
+
*
|
|
5
|
+
* The consumer-side discovery surface, companion to `gadget create`
|
|
6
|
+
* and `gadget publish`. Public and unauthenticated; the registry's
|
|
7
|
+
* `/search` route returns only `visibility: "public"` rows.
|
|
8
|
+
*
|
|
9
|
+
* Three-layer registry resolution (highest priority wins, mirrors
|
|
10
|
+
* `gadget publish` + `ggui install`):
|
|
11
|
+
*
|
|
12
|
+
* 1. `--registry <url>` flag.
|
|
13
|
+
* 2. `GGUI_REGISTRY` environment variable.
|
|
14
|
+
* 3. `ggui.json#registry` field (walks up from `cwd`).
|
|
15
|
+
* 4. Error if unset — no hard-coded default.
|
|
16
|
+
*
|
|
17
|
+
* Kept pure / testable — no `process.exit`, no direct stdout writes.
|
|
18
|
+
* Returns a `SearchOutput` discriminated union; the CLI driver in
|
|
19
|
+
* `gadget-command.ts` renders copy + picks the exit code.
|
|
20
|
+
*
|
|
21
|
+
* The wire shapes below mirror the registry's HTTP response types.
|
|
22
|
+
* Adding fields is a minor change; removing or renaming a field is
|
|
23
|
+
* breaking.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { dirname, join, resolve } from 'node:path';
|
|
27
|
+
import { SEARCH_ERROR_CODES } from '@ggui-ai/registry-core';
|
|
28
|
+
/**
|
|
29
|
+
* Filename constant for the project manifest. Duplicated from
|
|
30
|
+
* `@ggui-ai/project-config` rather than imported to keep this
|
|
31
|
+
* subcommand from pulling in the full project-config dependency tree.
|
|
32
|
+
*/
|
|
33
|
+
const GGUI_JSON_FILENAME = 'ggui.json';
|
|
34
|
+
/** How many parent directories the upward walk will inspect. */
|
|
35
|
+
const FIND_MAX_DEPTH = 8;
|
|
36
|
+
/**
|
|
37
|
+
* Flag parser. Returns a discriminated `{error}` vs. `{flags}` so the
|
|
38
|
+
* caller doesn't throw on user input. The `lockedKind` argument is the
|
|
39
|
+
* verb the operator typed — when supplied (the kind-discriminated
|
|
40
|
+
* router path), the `--kind` flag is rejected because the verb already
|
|
41
|
+
* pins the value. When `undefined` (legacy generic-search path, not
|
|
42
|
+
* exposed by the CLI today), `--kind` is honored as a freeform filter.
|
|
43
|
+
*
|
|
44
|
+
* For `lockedKind === 'blueprint'`, `--hook` is rejected because the
|
|
45
|
+
* hook filter is a gadget-only concern.
|
|
46
|
+
*
|
|
47
|
+
* Accepts both `--flag value` and `--flag=value` for every option.
|
|
48
|
+
*/
|
|
49
|
+
export function parseArtifactSearchFlags(lockedKind, args) {
|
|
50
|
+
let q;
|
|
51
|
+
let kind = lockedKind;
|
|
52
|
+
let hook;
|
|
53
|
+
let tag;
|
|
54
|
+
let author;
|
|
55
|
+
let limit;
|
|
56
|
+
let cursor;
|
|
57
|
+
let registry;
|
|
58
|
+
let json = false;
|
|
59
|
+
/**
|
|
60
|
+
* Pull the value for `--name`. Supports either `--name value` or
|
|
61
|
+
* `--name=value`. Returns `null` if no value is available so the
|
|
62
|
+
* caller can render the usage error.
|
|
63
|
+
*/
|
|
64
|
+
function readValue(arg, nextIdx) {
|
|
65
|
+
const eq = arg.indexOf('=');
|
|
66
|
+
if (eq !== -1) {
|
|
67
|
+
const v = arg.slice(eq + 1);
|
|
68
|
+
if (v.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
return { value: v, advance: 0 };
|
|
71
|
+
}
|
|
72
|
+
const next = args[nextIdx];
|
|
73
|
+
if (typeof next !== 'string' || next.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
return { value: next, advance: 1 };
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < args.length; i++) {
|
|
78
|
+
const arg = args[i];
|
|
79
|
+
if (arg === '--help' || arg === '-h') {
|
|
80
|
+
return { error: '__help__' };
|
|
81
|
+
}
|
|
82
|
+
if (arg === '--json') {
|
|
83
|
+
json = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg.startsWith('--kind=') ||
|
|
87
|
+
arg === '--kind' ||
|
|
88
|
+
arg.startsWith('--hook=') ||
|
|
89
|
+
arg === '--hook' ||
|
|
90
|
+
arg.startsWith('--tag=') ||
|
|
91
|
+
arg === '--tag' ||
|
|
92
|
+
arg.startsWith('--author=') ||
|
|
93
|
+
arg === '--author' ||
|
|
94
|
+
arg.startsWith('--limit=') ||
|
|
95
|
+
arg === '--limit' ||
|
|
96
|
+
arg.startsWith('--cursor=') ||
|
|
97
|
+
arg === '--cursor' ||
|
|
98
|
+
arg.startsWith('--registry=') ||
|
|
99
|
+
arg === '--registry') {
|
|
100
|
+
const flagName = arg.startsWith('--')
|
|
101
|
+
? arg.split('=')[0].slice(2)
|
|
102
|
+
: '';
|
|
103
|
+
const read = readValue(arg, i + 1);
|
|
104
|
+
if (read === null) {
|
|
105
|
+
return { error: `--${flagName} requires a value` };
|
|
106
|
+
}
|
|
107
|
+
i += read.advance;
|
|
108
|
+
const v = read.value;
|
|
109
|
+
switch (flagName) {
|
|
110
|
+
case 'kind':
|
|
111
|
+
if (v !== 'gadget' && v !== 'blueprint') {
|
|
112
|
+
return { error: '--kind must be "gadget" or "blueprint"' };
|
|
113
|
+
}
|
|
114
|
+
if (lockedKind !== undefined && v !== lockedKind) {
|
|
115
|
+
return {
|
|
116
|
+
error: `--kind=${v} conflicts with \`ggui ${lockedKind} search\`. Drop --kind, or use the matching verb (\`ggui ${v} search\`).`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
kind = v;
|
|
120
|
+
break;
|
|
121
|
+
case 'hook':
|
|
122
|
+
if (lockedKind === 'blueprint') {
|
|
123
|
+
return {
|
|
124
|
+
error: '--hook is a gadget-only filter; blueprints do not expose hooks. Use `ggui gadget search --hook <name>` instead.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
hook = v;
|
|
128
|
+
break;
|
|
129
|
+
case 'tag':
|
|
130
|
+
tag = v;
|
|
131
|
+
break;
|
|
132
|
+
case 'author':
|
|
133
|
+
author = v;
|
|
134
|
+
break;
|
|
135
|
+
case 'limit': {
|
|
136
|
+
const n = Number.parseInt(v, 10);
|
|
137
|
+
if (!Number.isInteger(n) || n < 1 || n > 200) {
|
|
138
|
+
return {
|
|
139
|
+
error: '--limit must be an integer in [1, 200]',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
limit = n;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'cursor':
|
|
146
|
+
cursor = v;
|
|
147
|
+
break;
|
|
148
|
+
case 'registry':
|
|
149
|
+
registry = v;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg.startsWith('--')) {
|
|
155
|
+
return { error: `unknown flag: ${arg}` };
|
|
156
|
+
}
|
|
157
|
+
if (q === undefined) {
|
|
158
|
+
q = arg;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
return { error: `unexpected positional argument: ${arg}` };
|
|
162
|
+
}
|
|
163
|
+
const flags = {
|
|
164
|
+
...(q !== undefined ? { q } : {}),
|
|
165
|
+
...(kind !== undefined ? { kind } : {}),
|
|
166
|
+
...(hook !== undefined ? { hook } : {}),
|
|
167
|
+
...(tag !== undefined ? { tag } : {}),
|
|
168
|
+
...(author !== undefined ? { author } : {}),
|
|
169
|
+
...(limit !== undefined ? { limit } : {}),
|
|
170
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
171
|
+
...(registry !== undefined ? { registry } : {}),
|
|
172
|
+
...(json ? { json: true } : {}),
|
|
173
|
+
};
|
|
174
|
+
return { flags };
|
|
175
|
+
}
|
|
176
|
+
/* -------------------------------------------------------------------------- */
|
|
177
|
+
/* Registry URL resolution */
|
|
178
|
+
/* -------------------------------------------------------------------------- */
|
|
179
|
+
/**
|
|
180
|
+
* Three-layer resolution: `--registry` flag > `GGUI_REGISTRY` env >
|
|
181
|
+
* `ggui.json#registry` > error.
|
|
182
|
+
*
|
|
183
|
+
* `cwd` + `env` are injectable for tests. Returns either `{ url }` on
|
|
184
|
+
* success or `{ error }` with a stderr-ready diagnostic. Trailing
|
|
185
|
+
* slashes on the URL are normalized away so the URL builder can append
|
|
186
|
+
* `/search?…` deterministically.
|
|
187
|
+
*/
|
|
188
|
+
export function resolveRegistryUrl(args) {
|
|
189
|
+
const candidate = args.flag ??
|
|
190
|
+
(typeof args.env.GGUI_REGISTRY === 'string' && args.env.GGUI_REGISTRY.length > 0
|
|
191
|
+
? args.env.GGUI_REGISTRY
|
|
192
|
+
: undefined);
|
|
193
|
+
if (typeof candidate === 'string' && candidate.length > 0) {
|
|
194
|
+
return normalizeRegistryUrl(candidate);
|
|
195
|
+
}
|
|
196
|
+
const gguiJsonPath = findGguiJson(args.cwd);
|
|
197
|
+
if (gguiJsonPath !== null) {
|
|
198
|
+
const loaded = readGguiJsonRegistryField(gguiJsonPath);
|
|
199
|
+
if ('error' in loaded) {
|
|
200
|
+
return { error: loaded.error };
|
|
201
|
+
}
|
|
202
|
+
if (loaded.registry !== undefined) {
|
|
203
|
+
return normalizeRegistryUrl(loaded.registry);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
error: 'no registry configured. Pass --registry <url>, set GGUI_REGISTRY, or add `registry` to ggui.json.',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Walk up from `startDir` looking for a `ggui.json`. Returns the
|
|
212
|
+
* absolute path of the first match, or `null` if none found within
|
|
213
|
+
* {@link FIND_MAX_DEPTH} levels. Mirrors the algorithm in
|
|
214
|
+
* `@ggui-ai/project-config/node`'s `findGguiJson` — duplicated here
|
|
215
|
+
* to keep this subcommand free of the project-config dependency.
|
|
216
|
+
*/
|
|
217
|
+
function findGguiJson(startDir) {
|
|
218
|
+
let dir = resolve(startDir);
|
|
219
|
+
for (let i = 0; i <= FIND_MAX_DEPTH; i++) {
|
|
220
|
+
const candidate = join(dir, GGUI_JSON_FILENAME);
|
|
221
|
+
if (existsSync(candidate))
|
|
222
|
+
return candidate;
|
|
223
|
+
const parent = dirname(dir);
|
|
224
|
+
if (parent === dir)
|
|
225
|
+
return null;
|
|
226
|
+
dir = parent;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Read just the `registry` field from a `ggui.json`. We don't run the
|
|
232
|
+
* full schema parser here — that would couple this command to every
|
|
233
|
+
* other field in the manifest, and a corrupt `gadgets` block
|
|
234
|
+
* (unrelated to search) would prevent `gadget search` from running.
|
|
235
|
+
* The field is validated structurally: must be a string. Schema-level
|
|
236
|
+
* URL validation happens downstream in {@link normalizeRegistryUrl}.
|
|
237
|
+
*/
|
|
238
|
+
function readGguiJsonRegistryField(path) {
|
|
239
|
+
let raw;
|
|
240
|
+
try {
|
|
241
|
+
raw = readFileSync(path, 'utf-8');
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
return {
|
|
245
|
+
error: `failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
let decoded;
|
|
249
|
+
try {
|
|
250
|
+
decoded = JSON.parse(raw);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
return {
|
|
254
|
+
error: `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (decoded === null || typeof decoded !== 'object' || Array.isArray(decoded)) {
|
|
258
|
+
return { error: `${path} is not a JSON object` };
|
|
259
|
+
}
|
|
260
|
+
const reg = decoded.registry;
|
|
261
|
+
if (reg === undefined)
|
|
262
|
+
return { registry: undefined };
|
|
263
|
+
if (typeof reg !== 'string') {
|
|
264
|
+
return { error: `${path}#registry must be a string` };
|
|
265
|
+
}
|
|
266
|
+
return { registry: reg };
|
|
267
|
+
}
|
|
268
|
+
function normalizeRegistryUrl(raw) {
|
|
269
|
+
let parsed;
|
|
270
|
+
try {
|
|
271
|
+
parsed = new URL(raw);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return { error: `registry URL is malformed: ${raw}` };
|
|
275
|
+
}
|
|
276
|
+
// Strip trailing slashes so we can append `/search` deterministically.
|
|
277
|
+
// `URL` already normalized scheme + host casing.
|
|
278
|
+
let serialized = parsed.toString();
|
|
279
|
+
while (serialized.endsWith('/'))
|
|
280
|
+
serialized = serialized.slice(0, -1);
|
|
281
|
+
return { url: serialized };
|
|
282
|
+
}
|
|
283
|
+
/* -------------------------------------------------------------------------- */
|
|
284
|
+
/* Query string */
|
|
285
|
+
/* -------------------------------------------------------------------------- */
|
|
286
|
+
/**
|
|
287
|
+
* Build a query string from {@link ArtifactSearchFlags}. Pulled out so
|
|
288
|
+
* tests can assert the wire shape without a real fetch. Empty / absent
|
|
289
|
+
* filters are dropped entirely (rather than emitted as `&foo=`).
|
|
290
|
+
*
|
|
291
|
+
* The returned string always starts with `?` unless every filter is
|
|
292
|
+
* absent, in which case the empty string is returned.
|
|
293
|
+
*/
|
|
294
|
+
export function buildSearchQueryString(flags) {
|
|
295
|
+
const params = new URLSearchParams();
|
|
296
|
+
if (flags.q !== undefined)
|
|
297
|
+
params.set('q', flags.q);
|
|
298
|
+
if (flags.kind !== undefined)
|
|
299
|
+
params.set('kind', flags.kind);
|
|
300
|
+
if (flags.hook !== undefined)
|
|
301
|
+
params.set('hook', flags.hook);
|
|
302
|
+
if (flags.tag !== undefined)
|
|
303
|
+
params.set('tag', flags.tag);
|
|
304
|
+
if (flags.author !== undefined)
|
|
305
|
+
params.set('author', flags.author);
|
|
306
|
+
if (flags.limit !== undefined)
|
|
307
|
+
params.set('limit', String(flags.limit));
|
|
308
|
+
if (flags.cursor !== undefined)
|
|
309
|
+
params.set('cursor', flags.cursor);
|
|
310
|
+
const qs = params.toString();
|
|
311
|
+
return qs.length === 0 ? '' : `?${qs}`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Run a search end-to-end:
|
|
315
|
+
*
|
|
316
|
+
* 1. Resolve the registry URL via the three-layer chain.
|
|
317
|
+
* 2. Build the query string.
|
|
318
|
+
* 3. GET `<registry>/search?<qs>` (no auth; public endpoint).
|
|
319
|
+
* 4. Parse + validate the response shape.
|
|
320
|
+
* 5. Format human-readable lines (or stash raw JSON for `--json`).
|
|
321
|
+
*
|
|
322
|
+
* Never throws — every error path returns a {@link SearchFailure}.
|
|
323
|
+
*/
|
|
324
|
+
export async function runArtifactSearch(flags, deps) {
|
|
325
|
+
const resolved = resolveRegistryUrl({
|
|
326
|
+
...(flags.registry !== undefined ? { flag: flags.registry } : {}),
|
|
327
|
+
cwd: deps.cwd,
|
|
328
|
+
env: deps.env,
|
|
329
|
+
});
|
|
330
|
+
if ('error' in resolved) {
|
|
331
|
+
const isNoRegistryError = resolved.error.startsWith('no registry');
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
code: isNoRegistryError ? 'no-registry' : 'invalid-registry',
|
|
335
|
+
message: resolved.error,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const qs = buildSearchQueryString(flags);
|
|
339
|
+
const url = `${resolved.url}/search${qs}`;
|
|
340
|
+
let res;
|
|
341
|
+
try {
|
|
342
|
+
res = await deps.fetch(url, {
|
|
343
|
+
method: 'GET',
|
|
344
|
+
headers: { 'content-type': 'application/json' },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
code: 'network-error',
|
|
351
|
+
message: `failed to reach registry at ${resolved.url}: ${err instanceof Error ? err.message : String(err)}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (!res.ok) {
|
|
355
|
+
// Try to surface the registry's structured error body. If parsing
|
|
356
|
+
// fails we fall back to the status line — better than swallowing.
|
|
357
|
+
let detail;
|
|
358
|
+
try {
|
|
359
|
+
const body = await res.json();
|
|
360
|
+
if (isSearchErrorBody(body)) {
|
|
361
|
+
detail = `${body.error}: ${body.message}`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
detail = undefined;
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
code: 'http-error',
|
|
370
|
+
message: `registry returned ${res.status}${detail !== undefined ? ` — ${detail}` : ''}`,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
let body;
|
|
374
|
+
try {
|
|
375
|
+
body = await res.json();
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
code: 'bad-response',
|
|
381
|
+
message: `registry response is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (!isSearchResponse(body)) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
code: 'bad-response',
|
|
388
|
+
message: 'registry response did not match the expected SearchResponse shape',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const json = JSON.stringify(body, null, 2);
|
|
392
|
+
const lines = flags.json === true ? [] : renderHumanLines(body, resolved.url);
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
registryUrl: resolved.url,
|
|
396
|
+
response: body,
|
|
397
|
+
lines,
|
|
398
|
+
json,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/* -------------------------------------------------------------------------- */
|
|
402
|
+
/* Response validation */
|
|
403
|
+
/* -------------------------------------------------------------------------- */
|
|
404
|
+
/**
|
|
405
|
+
* Structural typeguard for {@link SearchResponse}. Rejects anything
|
|
406
|
+
* that doesn't have a `results` array of entry-shaped objects.
|
|
407
|
+
*/
|
|
408
|
+
function isSearchResponse(v) {
|
|
409
|
+
if (v === null || typeof v !== 'object')
|
|
410
|
+
return false;
|
|
411
|
+
const obj = v;
|
|
412
|
+
if (!Array.isArray(obj.results))
|
|
413
|
+
return false;
|
|
414
|
+
for (const entry of obj.results) {
|
|
415
|
+
if (!isSearchResultEntry(entry))
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
if (obj.nextCursor !== undefined && typeof obj.nextCursor !== 'string') {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
function isSearchResultEntry(v) {
|
|
424
|
+
if (v === null || typeof v !== 'object')
|
|
425
|
+
return false;
|
|
426
|
+
const o = v;
|
|
427
|
+
if (typeof o.artifactId !== 'string')
|
|
428
|
+
return false;
|
|
429
|
+
if (typeof o.latestVersion !== 'string')
|
|
430
|
+
return false;
|
|
431
|
+
if (o.kind !== 'gadget' && o.kind !== 'blueprint')
|
|
432
|
+
return false;
|
|
433
|
+
if (typeof o.publishedAt !== 'string')
|
|
434
|
+
return false;
|
|
435
|
+
if (o.description !== undefined && typeof o.description !== 'string') {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
if (o.tags !== undefined) {
|
|
439
|
+
if (!Array.isArray(o.tags))
|
|
440
|
+
return false;
|
|
441
|
+
for (const t of o.tags) {
|
|
442
|
+
if (typeof t !== 'string')
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
function isSearchErrorBody(v) {
|
|
449
|
+
if (v === null || typeof v !== 'object')
|
|
450
|
+
return false;
|
|
451
|
+
const o = v;
|
|
452
|
+
if (typeof o.error !== 'string' || typeof o.message !== 'string')
|
|
453
|
+
return false;
|
|
454
|
+
return SEARCH_ERROR_CODES.includes(o.error);
|
|
455
|
+
}
|
|
456
|
+
/* -------------------------------------------------------------------------- */
|
|
457
|
+
/* Human-readable rendering */
|
|
458
|
+
/* -------------------------------------------------------------------------- */
|
|
459
|
+
/**
|
|
460
|
+
* Render the default human-readable view. Each entry takes two lines:
|
|
461
|
+
*
|
|
462
|
+
* gadget @my-org/weather-card@0.1.0 — Beautiful weather card
|
|
463
|
+
* install: ggui install @my-org/weather-card@0.1.0 --registry=registry.example.com
|
|
464
|
+
*
|
|
465
|
+
* The `install:` hint inlines the registry hostname so operators can
|
|
466
|
+
* copy-paste without re-deriving it. If `nextCursor` is present, a
|
|
467
|
+
* pagination hint follows the entry list.
|
|
468
|
+
*/
|
|
469
|
+
function renderHumanLines(body, registryUrl) {
|
|
470
|
+
const out = [];
|
|
471
|
+
const host = hostnameOf(registryUrl);
|
|
472
|
+
for (const entry of body.results) {
|
|
473
|
+
const desc = entry.description !== undefined ? ` — ${entry.description}` : '';
|
|
474
|
+
out.push(`${entry.kind} ${entry.artifactId}@${entry.latestVersion}${desc}`);
|
|
475
|
+
// Mirror the install verb against the entry's kind so the hint
|
|
476
|
+
// points operators at `ggui blueprint install` for blueprint rows,
|
|
477
|
+
// `ggui gadget install` for gadget rows.
|
|
478
|
+
out.push(` install: ggui ${entry.kind} install ${entry.artifactId}@${entry.latestVersion} --registry=${host}`);
|
|
479
|
+
}
|
|
480
|
+
if (body.nextCursor !== undefined) {
|
|
481
|
+
out.push(`… (more: rerun with --cursor=${body.nextCursor})`);
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
function hostnameOf(url) {
|
|
486
|
+
try {
|
|
487
|
+
return new URL(url).host;
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return url;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/* -------------------------------------------------------------------------- */
|
|
494
|
+
/* Help text */
|
|
495
|
+
/* -------------------------------------------------------------------------- */
|
|
496
|
+
/**
|
|
497
|
+
* Build verb-specific help text. Takes the `kind` so the rendered
|
|
498
|
+
* lines say `ggui gadget search` / `ggui blueprint search` and the
|
|
499
|
+
* filter list omits gadget-only options on the blueprint surface.
|
|
500
|
+
*/
|
|
501
|
+
export function buildSearchHelp(kind) {
|
|
502
|
+
const verb = `ggui ${kind} search`;
|
|
503
|
+
const hookLines = kind === 'gadget'
|
|
504
|
+
? ` --hook <name> Filter gadgets by exported hook name
|
|
505
|
+
(e.g. \`--hook useMap\`).
|
|
506
|
+
`
|
|
507
|
+
: '';
|
|
508
|
+
return `${verb} — query the marketplace registry for ${kind}s
|
|
509
|
+
|
|
510
|
+
Usage:
|
|
511
|
+
${verb} [q] [options]
|
|
512
|
+
|
|
513
|
+
Arguments:
|
|
514
|
+
[q] Optional full-text query. Matches against
|
|
515
|
+
${kind} name, description, and tags.
|
|
516
|
+
|
|
517
|
+
Options:
|
|
518
|
+
${hookLines} --tag <tag> Filter by tag (exact match).
|
|
519
|
+
--author <id> Filter by author (caller id or substring
|
|
520
|
+
of authorName).
|
|
521
|
+
--limit <n> Page size; 1..200, default 50.
|
|
522
|
+
--cursor <opaque> Opaque cursor from a previous response —
|
|
523
|
+
roundtrip verbatim for pagination.
|
|
524
|
+
--registry <url> Override the registry URL. Three-layer
|
|
525
|
+
resolution: flag > GGUI_REGISTRY env >
|
|
526
|
+
ggui.json#registry > error if unset.
|
|
527
|
+
--json Emit the raw JSON response body instead of
|
|
528
|
+
the human-readable table.
|
|
529
|
+
--help, -h Show this help.
|
|
530
|
+
|
|
531
|
+
Examples:
|
|
532
|
+
${verb} # all ${kind}s, default page
|
|
533
|
+
${verb} weather # full-text on name+description+tags
|
|
534
|
+
${verb} --tag map --author @my-org
|
|
535
|
+
${verb} --registry https://registry.example.com
|
|
536
|
+
${verb} --limit 100 --cursor <opaque>
|
|
537
|
+
${verb} --json
|
|
538
|
+
`;
|
|
539
|
+
}
|