@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,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ggui blueprint create <scope/name>` — scaffold a new blueprint repo.
|
|
3
|
+
*
|
|
4
|
+
* Sibling of `gadget-create.ts`. Blueprints differ from gadgets in two
|
|
5
|
+
* ways that justify a separate scaffolder (rather than a
|
|
6
|
+
* kind-discriminated single template):
|
|
7
|
+
*
|
|
8
|
+
* - **Source body** — blueprints are TSX components, not bundled
|
|
9
|
+
* hook libraries. The scaffold writes a `src/blueprint.tsx` with
|
|
10
|
+
* a minimal React component + a `src/contract.ts` stub that
|
|
11
|
+
* declares a `DataContract` for the LLM-driven cache-match path.
|
|
12
|
+
* - **Manifest shape** — `ggui.blueprint.json` carries `source`,
|
|
13
|
+
* `contract`, `fixtureProps`, `variance`. The scaffold synthesizes
|
|
14
|
+
* a working manifest with the inline source body, so an author can
|
|
15
|
+
* `cd <dir> && ggui blueprint publish` immediately after generation.
|
|
16
|
+
*
|
|
17
|
+
* The scaffold validates the synthesized manifest with
|
|
18
|
+
* `parseBlueprintManifest` before any write — template regressions
|
|
19
|
+
* fail LOUD at the scaffolder rather than silently shipping a broken
|
|
20
|
+
* `ggui.blueprint.json`.
|
|
21
|
+
*
|
|
22
|
+
* Kept pure / testable — no `process.exit`, no direct stdout writes.
|
|
23
|
+
* `blueprint-command.ts` composes this with the real argv + writes
|
|
24
|
+
* the banner.
|
|
25
|
+
*/
|
|
26
|
+
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
27
|
+
import { join, resolve } from 'node:path';
|
|
28
|
+
import { GGUI_BLUEPRINT_JSON_FILENAME, parseBlueprintManifest, } from '@ggui-ai/artifact-manifest';
|
|
29
|
+
import { parseScopeName } from './gadget-create.js';
|
|
30
|
+
/** Default version for a freshly scaffolded blueprint. */
|
|
31
|
+
const DEFAULT_VERSION = '0.0.1';
|
|
32
|
+
/** Default visibility — public matches the marketplace's primary path. */
|
|
33
|
+
const DEFAULT_VISIBILITY = 'public';
|
|
34
|
+
/**
|
|
35
|
+
* Parse the `ggui blueprint create` flag tail. Returns a discriminated
|
|
36
|
+
* `{error}` vs. successful flags so the caller doesn't throw on user
|
|
37
|
+
* input.
|
|
38
|
+
*/
|
|
39
|
+
export function parseBlueprintCreateFlags(args) {
|
|
40
|
+
if (args.length === 0) {
|
|
41
|
+
return { error: '__help__' };
|
|
42
|
+
}
|
|
43
|
+
let scopeName;
|
|
44
|
+
let dir;
|
|
45
|
+
let description;
|
|
46
|
+
let visibility;
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i];
|
|
49
|
+
if (arg === '--help' || arg === '-h')
|
|
50
|
+
return { error: '__help__' };
|
|
51
|
+
if (arg === '--dir') {
|
|
52
|
+
const value = args[i + 1];
|
|
53
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
54
|
+
return { error: '--dir requires a value' };
|
|
55
|
+
}
|
|
56
|
+
dir = value;
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === '--description') {
|
|
61
|
+
const value = args[i + 1];
|
|
62
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
63
|
+
return { error: '--description requires a value' };
|
|
64
|
+
}
|
|
65
|
+
description = value;
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === '--visibility') {
|
|
70
|
+
const value = args[i + 1];
|
|
71
|
+
if (value !== 'public' && value !== 'private') {
|
|
72
|
+
return { error: '--visibility must be "public" or "private"' };
|
|
73
|
+
}
|
|
74
|
+
visibility = value;
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg.startsWith('--')) {
|
|
79
|
+
return { error: `unknown flag: ${arg}` };
|
|
80
|
+
}
|
|
81
|
+
if (scopeName === undefined) {
|
|
82
|
+
scopeName = arg;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return { error: `unexpected positional argument: ${arg}` };
|
|
86
|
+
}
|
|
87
|
+
if (scopeName === undefined) {
|
|
88
|
+
return { error: 'missing required <scope/name> positional argument' };
|
|
89
|
+
}
|
|
90
|
+
const flags = {
|
|
91
|
+
scopeName,
|
|
92
|
+
...(dir !== undefined ? { dir } : {}),
|
|
93
|
+
...(description !== undefined ? { description } : {}),
|
|
94
|
+
...(visibility !== undefined ? { visibility } : {}),
|
|
95
|
+
};
|
|
96
|
+
return { flags };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build the minimal TSX source body the scaffold ships as
|
|
100
|
+
* `src/blueprint.tsx`. Authors edit this in-place; once the source
|
|
101
|
+
* stabilizes the publish flow re-reads + inlines it into the manifest's
|
|
102
|
+
* `source` field at upload time.
|
|
103
|
+
*/
|
|
104
|
+
function buildBlueprintTsx(parsed) {
|
|
105
|
+
const componentName = toPascalCase(parsed.name);
|
|
106
|
+
return `/**
|
|
107
|
+
* ${parsed.scope}/${parsed.name} — ggui blueprint.
|
|
108
|
+
*
|
|
109
|
+
* Default-exports a React component the runtime renders against the
|
|
110
|
+
* declared DataContract. Replace this placeholder with your real UI.
|
|
111
|
+
*/
|
|
112
|
+
export interface ${componentName}Props {
|
|
113
|
+
readonly title?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default function ${componentName}(props: ${componentName}Props): JSX.Element {
|
|
117
|
+
return (
|
|
118
|
+
<section data-blueprint="${parsed.scope}/${parsed.name}">
|
|
119
|
+
<h1>{props.title ?? '${parsed.scope}/${parsed.name}'}</h1>
|
|
120
|
+
</section>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the matching `src/contract.ts` stub — a minimal `DataContract`
|
|
127
|
+
* the blueprint registers under. Authors flesh out `propsSchema` to
|
|
128
|
+
* match the component's prop shape and add `clientCapabilities` /
|
|
129
|
+
* `commitSpec` as needed.
|
|
130
|
+
*/
|
|
131
|
+
function buildContractStub(parsed) {
|
|
132
|
+
return `/**
|
|
133
|
+
* DataContract for ${parsed.scope}/${parsed.name}. The runtime feeds
|
|
134
|
+
* the matching props into the default-exported component on render;
|
|
135
|
+
* the contract's hash also drives cache-match for the cold-gen short-
|
|
136
|
+
* circuit.
|
|
137
|
+
*/
|
|
138
|
+
export const contract = {
|
|
139
|
+
schemaVersion: '1' as const,
|
|
140
|
+
contextSpec: {
|
|
141
|
+
propsSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
title: { type: 'string' },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
} as const;
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
/** Convert a kebab-case blueprint name → PascalCase component name. */
|
|
152
|
+
function toPascalCase(name) {
|
|
153
|
+
return name
|
|
154
|
+
.split('-')
|
|
155
|
+
.filter((s) => s.length > 0)
|
|
156
|
+
.map((s) => s[0].toUpperCase() + s.slice(1))
|
|
157
|
+
.join('');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build + validate the {@link BlueprintManifest} BEFORE any write.
|
|
161
|
+
* A template regression that drifts (e.g. drops `source`) blows up
|
|
162
|
+
* here, before any file lands on disk, so authors don't see a corrupt
|
|
163
|
+
* scaffold.
|
|
164
|
+
*/
|
|
165
|
+
export function buildScaffoldManifest(args) {
|
|
166
|
+
const description = args.description ??
|
|
167
|
+
`${args.scope}/${args.name} — ggui marketplace blueprint.`;
|
|
168
|
+
const raw = {
|
|
169
|
+
kind: 'blueprint',
|
|
170
|
+
scope: args.scope,
|
|
171
|
+
name: args.name,
|
|
172
|
+
version: DEFAULT_VERSION,
|
|
173
|
+
visibility: args.visibility,
|
|
174
|
+
source: args.source,
|
|
175
|
+
description,
|
|
176
|
+
tags: [],
|
|
177
|
+
};
|
|
178
|
+
return parseBlueprintManifest(raw);
|
|
179
|
+
}
|
|
180
|
+
function buildPackageJson(args) {
|
|
181
|
+
return {
|
|
182
|
+
name: `${args.scope}/${args.name}`,
|
|
183
|
+
version: DEFAULT_VERSION,
|
|
184
|
+
...(args.description !== undefined ? { description: args.description } : {}),
|
|
185
|
+
type: 'module',
|
|
186
|
+
files: ['src', GGUI_BLUEPRINT_JSON_FILENAME, 'README.md'],
|
|
187
|
+
devDependencies: {
|
|
188
|
+
typescript: '^5.0.0',
|
|
189
|
+
'@types/react': '^19.0.0',
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function buildReadme(args) {
|
|
194
|
+
const summary = args.description ??
|
|
195
|
+
`${args.scope}/${args.name} — ggui marketplace blueprint.`;
|
|
196
|
+
return `# ${args.scope}/${args.name}
|
|
197
|
+
|
|
198
|
+
${summary}
|
|
199
|
+
|
|
200
|
+
## Develop
|
|
201
|
+
|
|
202
|
+
\`\`\`bash
|
|
203
|
+
pnpm install
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
Edit \`src/blueprint.tsx\` (component body) and \`src/contract.ts\`
|
|
207
|
+
(matching \`DataContract\`).
|
|
208
|
+
|
|
209
|
+
## Publish
|
|
210
|
+
|
|
211
|
+
\`\`\`bash
|
|
212
|
+
ggui blueprint publish
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
See [ggui blueprint docs](https://ggui.ai/docs/blueprints) for the full
|
|
216
|
+
marketplace flow.
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
/** Refuse-to-overwrite check, matching gadget-create's posture. */
|
|
220
|
+
async function isDirectoryEmpty(path) {
|
|
221
|
+
try {
|
|
222
|
+
const entries = await readdir(path);
|
|
223
|
+
return { exists: true, empty: entries.length === 0 };
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
227
|
+
return { exists: false, empty: true };
|
|
228
|
+
}
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Run the blueprint scaffold end-to-end. Resolves the target dir,
|
|
234
|
+
* builds + validates the manifest, writes files. Returns a structured
|
|
235
|
+
* result so the CLI driver can render copy + exit code without
|
|
236
|
+
* re-deriving paths.
|
|
237
|
+
*/
|
|
238
|
+
export async function runBlueprintCreate(flags, options) {
|
|
239
|
+
const parsed = parseScopeName(flags.scopeName);
|
|
240
|
+
if (!parsed) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
code: 'invalid-scope-name',
|
|
244
|
+
message: `"${flags.scopeName}" is not a valid <scope>/<name>. Expected \`@org/blueprint-name\` (e.g. \`@my-org/login-form\`).`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// Blueprint name regex (`^[a-z0-9][a-z0-9_-]{0,63}$`) is enforced by
|
|
248
|
+
// the manifest schema; a structurally-valid scope/name with an
|
|
249
|
+
// invalid blueprint slug surfaces at parse time below as
|
|
250
|
+
// `manifest-invalid`. Bail early on the more obvious mismatches so
|
|
251
|
+
// the operator sees a crisp message.
|
|
252
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(parsed.name)) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
code: 'invalid-name',
|
|
256
|
+
message: `"${parsed.name}" is not a valid blueprint name. Expected lowercase alphanum + hyphens / underscores, 1..64 chars, first char alphanumeric (e.g. \`login-form\`).`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const { scope, name } = parsed;
|
|
260
|
+
const visibility = flags.visibility ?? DEFAULT_VISIBILITY;
|
|
261
|
+
const source = buildBlueprintTsx(parsed);
|
|
262
|
+
let manifest;
|
|
263
|
+
try {
|
|
264
|
+
manifest = buildScaffoldManifest({
|
|
265
|
+
scope,
|
|
266
|
+
name,
|
|
267
|
+
visibility,
|
|
268
|
+
source,
|
|
269
|
+
...(flags.description !== undefined
|
|
270
|
+
? { description: flags.description }
|
|
271
|
+
: {}),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
code: 'manifest-invalid',
|
|
278
|
+
message: err instanceof Error ? err.message : 'failed to validate manifest',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// `resolve` (not `join`): when `flags.dir` is an absolute path, it must
|
|
282
|
+
// win over `options.cwd`. `join('/repo', '/tmp/foo')` concatenates to
|
|
283
|
+
// `/repo/tmp/foo`; `resolve('/repo', '/tmp/foo')` correctly returns
|
|
284
|
+
// `/tmp/foo`. Relative paths still resolve against cwd.
|
|
285
|
+
const targetDir = resolve(options.cwd, flags.dir ?? name);
|
|
286
|
+
let dirState;
|
|
287
|
+
try {
|
|
288
|
+
dirState = await isDirectoryEmpty(targetDir);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
code: 'fs-error',
|
|
294
|
+
message: err instanceof Error ? err.message : 'failed to read target dir',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (dirState.exists && !dirState.empty) {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
code: 'target-not-empty',
|
|
301
|
+
message: `target directory ${targetDir} is not empty. Refusing to overwrite — choose a different --dir or remove existing files.`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await mkdir(targetDir, { recursive: true });
|
|
306
|
+
await mkdir(join(targetDir, 'src'), { recursive: true });
|
|
307
|
+
const manifestPath = join(targetDir, GGUI_BLUEPRINT_JSON_FILENAME);
|
|
308
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
309
|
+
const pkg = buildPackageJson({
|
|
310
|
+
scope,
|
|
311
|
+
name,
|
|
312
|
+
...(flags.description !== undefined
|
|
313
|
+
? { description: flags.description }
|
|
314
|
+
: {}),
|
|
315
|
+
});
|
|
316
|
+
await writeFile(join(targetDir, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf-8');
|
|
317
|
+
await writeFile(join(targetDir, 'src', 'blueprint.tsx'), source, 'utf-8');
|
|
318
|
+
await writeFile(join(targetDir, 'src', 'contract.ts'), buildContractStub(parsed), 'utf-8');
|
|
319
|
+
await writeFile(join(targetDir, 'README.md'), buildReadme({
|
|
320
|
+
scope,
|
|
321
|
+
name,
|
|
322
|
+
...(flags.description !== undefined
|
|
323
|
+
? { description: flags.description }
|
|
324
|
+
: {}),
|
|
325
|
+
}), 'utf-8');
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
code: 'fs-error',
|
|
331
|
+
message: err instanceof Error ? err.message : 'failed to write files',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
targetDir,
|
|
337
|
+
files: [
|
|
338
|
+
GGUI_BLUEPRINT_JSON_FILENAME,
|
|
339
|
+
'package.json',
|
|
340
|
+
'src/blueprint.tsx',
|
|
341
|
+
'src/contract.ts',
|
|
342
|
+
'README.md',
|
|
343
|
+
],
|
|
344
|
+
manifest,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
export const BLUEPRINT_CREATE_HELP = `ggui blueprint create — scaffold a new blueprint repo
|
|
348
|
+
|
|
349
|
+
Usage:
|
|
350
|
+
ggui blueprint create <scope/name> [options]
|
|
351
|
+
|
|
352
|
+
Arguments:
|
|
353
|
+
<scope/name> Required. \`@my-org/login-form\` style identifier.
|
|
354
|
+
The scope (with leading \`@\`) becomes the manifest's
|
|
355
|
+
\`scope\`; the segment after \`/\` becomes the \`name\`
|
|
356
|
+
(must match \`^[a-z0-9][a-z0-9_-]{0,63}$\`).
|
|
357
|
+
|
|
358
|
+
Options:
|
|
359
|
+
--dir <path> Target directory. Defaults to \`./<name>\`.
|
|
360
|
+
--description <text> One-line description threaded into the
|
|
361
|
+
manifest + README.
|
|
362
|
+
--visibility <p> \`public\` (default, sigstore-signed) or
|
|
363
|
+
\`private\` (Ed25519-signed, org-only).
|
|
364
|
+
--help, -h Show this help.
|
|
365
|
+
|
|
366
|
+
Output:
|
|
367
|
+
<dir>/ggui.blueprint.json Strict-schema manifest (validated before write).
|
|
368
|
+
<dir>/package.json Workspace shell.
|
|
369
|
+
<dir>/src/blueprint.tsx Default-exported React component stub.
|
|
370
|
+
<dir>/src/contract.ts Matching \`DataContract\` stub.
|
|
371
|
+
<dir>/README.md One-paragraph orientation.
|
|
372
|
+
|
|
373
|
+
After scaffold:
|
|
374
|
+
cd <dir> && pnpm install
|
|
375
|
+
# implement the component in src/blueprint.tsx, then:
|
|
376
|
+
ggui blueprint publish # publishes to the resolved registry.
|
|
377
|
+
`;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYOK ("bring your own key") provider-key resolver — the `ggui` CLI's
|
|
3
|
+
* personal-mode credential plumbing for local UI generation.
|
|
4
|
+
*
|
|
5
|
+
* Resolves a {@link ProviderKeyRef} for a given LLM provider through a
|
|
6
|
+
* deterministic two-step lookup. Used by `buildMcpServerBackend` to
|
|
7
|
+
* decide whether to call a real provider or fail fast with the
|
|
8
|
+
* canonical `'no-credentials'` error.
|
|
9
|
+
*
|
|
10
|
+
* **Resolution order, locked:**
|
|
11
|
+
*
|
|
12
|
+
* 1. **Environment variables** — checked first, always win. The
|
|
13
|
+
* env-var names match the conventions every Node ecosystem uses
|
|
14
|
+
* so operators who already export them for other tools (Claude
|
|
15
|
+
* Code, OpenAI CLI, etc.) get OSS generation working with zero
|
|
16
|
+
* extra config:
|
|
17
|
+
*
|
|
18
|
+
* - `anthropic` → `ANTHROPIC_API_KEY`
|
|
19
|
+
* - `google` → `GOOGLE_API_KEY`, then `GEMINI_API_KEY`
|
|
20
|
+
* (alias — Google publishes both)
|
|
21
|
+
* - `openai` → `OPENAI_API_KEY`
|
|
22
|
+
* - `openrouter` → `OPENROUTER_API_KEY`
|
|
23
|
+
* - `bedrock` → not env-resolvable. Bedrock auth flows
|
|
24
|
+
* through `AWS_*` env / IAM role chain at
|
|
25
|
+
* the SDK level; the resolver returns null
|
|
26
|
+
* and the (future) bedrock adapter pulls
|
|
27
|
+
* from the standard AWS credential chain.
|
|
28
|
+
*
|
|
29
|
+
* 2. **`~/.ggui/credentials.json`** — operator-persistent personal
|
|
30
|
+
* store. Backed by `PlaintextFileProviderKeyStore` from
|
|
31
|
+
* `@ggui-ai/mcp-server-core/plaintext` at the **global** app
|
|
32
|
+
* scope. Single-user, single-tenant; multi-app or multi-user
|
|
33
|
+
* deployments swap in a different `ProviderKeyStore` binding at
|
|
34
|
+
* composition time and bypass this resolver entirely.
|
|
35
|
+
*
|
|
36
|
+
* **Explicitly NOT a source:** `ggui.json#secrets` or any other
|
|
37
|
+
* project-file location. Plaintext secrets in a project file that
|
|
38
|
+
* gets committed is a footgun, called out in the OSS-split plan
|
|
39
|
+
* (§2.A). This resolver never reads project config.
|
|
40
|
+
*
|
|
41
|
+
* **Failure mode:** missing key returns `null`. Callers map `null` to
|
|
42
|
+
* the canonical `ProviderError{kind:'no-credentials'}` from
|
|
43
|
+
* `@ggui-ai/ui-gen/provider-adapter` so OSS surfaces one consistent
|
|
44
|
+
* structured failure shape regardless of whether the operator never
|
|
45
|
+
* configured a key vs. configured the wrong provider.
|
|
46
|
+
*/
|
|
47
|
+
import type { LlmProvider, ProviderKeyStore } from '@ggui-ai/mcp-server-core';
|
|
48
|
+
/**
|
|
49
|
+
* Per-process env interface. Narrow enough that tests can pass a
|
|
50
|
+
* plain object instead of mutating `process.env` globally — vitest
|
|
51
|
+
* workers share `process.env` so a clean injection seam matters.
|
|
52
|
+
*/
|
|
53
|
+
export type EnvLike = Readonly<Record<string, string | undefined>>;
|
|
54
|
+
/** App-scope key under which the resolver reads + writes the store. */
|
|
55
|
+
export declare const BYOK_GLOBAL_APP_SCOPE = "global";
|
|
56
|
+
/**
|
|
57
|
+
* Provider → ordered list of env-var names to check. First non-empty
|
|
58
|
+
* wins. Single-name tuple for most providers; google has the
|
|
59
|
+
* `GOOGLE_API_KEY` / `GEMINI_API_KEY` alias pair.
|
|
60
|
+
*
|
|
61
|
+
* `bedrock` is intentionally absent — its auth chain lives outside
|
|
62
|
+
* this resolver (see module JSDoc).
|
|
63
|
+
*/
|
|
64
|
+
export declare const PROVIDER_ENV_NAMES: Readonly<Record<Exclude<LlmProvider, 'bedrock'>, readonly string[]>>;
|
|
65
|
+
/**
|
|
66
|
+
* Successful resolution. The `source` field is load-bearing — it
|
|
67
|
+
* lets callers surface "BYOK from env" vs. "BYOK from credentials
|
|
68
|
+
* file" in operator-facing logs (banner / `ggui auth status` later)
|
|
69
|
+
* AND lets tests assert the precedence rule deterministically.
|
|
70
|
+
*/
|
|
71
|
+
export interface ByokKeyResolution {
|
|
72
|
+
readonly key: string;
|
|
73
|
+
readonly source: 'env' | 'credentials-file';
|
|
74
|
+
readonly provider: LlmProvider;
|
|
75
|
+
/**
|
|
76
|
+
* Which env-var name produced the key (for `source: 'env'`). Lets
|
|
77
|
+
* the operator-facing banner say "GEMINI_API_KEY" specifically when
|
|
78
|
+
* that's what fired, instead of a generic "google env".
|
|
79
|
+
*/
|
|
80
|
+
readonly envName?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface ByokResolverOptions {
|
|
83
|
+
/**
|
|
84
|
+
* Env source. Defaults to `process.env`. Tests pass a plain object
|
|
85
|
+
* so they don't have to mutate `process.env` (which leaks across
|
|
86
|
+
* vitest's shared worker pool).
|
|
87
|
+
*/
|
|
88
|
+
readonly env?: EnvLike;
|
|
89
|
+
/**
|
|
90
|
+
* Override the credentials-file backed store. Defaults to a
|
|
91
|
+
* `PlaintextFileProviderKeyStore` reading {@link getCredentialsFile}
|
|
92
|
+
* — i.e. `~/.ggui/credentials.json` (or `$GGUI_CONFIG_DIR/credentials.json`
|
|
93
|
+
* when set). Production callers leave this unset; tests inject a
|
|
94
|
+
* fixture store or `null` to disable the file leg.
|
|
95
|
+
*
|
|
96
|
+
* Pass `null` (NOT `undefined`) to explicitly disable the file
|
|
97
|
+
* lookup. `undefined` triggers the default-store construction.
|
|
98
|
+
*/
|
|
99
|
+
readonly fileStore?: ProviderKeyStore | null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Per-call resolver options. Threaded through the request → handler →
|
|
103
|
+
* generator chain so the BYOK lookup at gen-time honors the request's
|
|
104
|
+
* authenticated identity.
|
|
105
|
+
*/
|
|
106
|
+
export interface ByokResolveOptions {
|
|
107
|
+
/**
|
|
108
|
+
* The end-user scope to consult AFTER the operator's platform key
|
|
109
|
+
* (env + credentials-file at scope='global') has been checked and
|
|
110
|
+
* missed. Operator-first fallback: if the operator set a key, every
|
|
111
|
+
* user uses it (operator pays). If the operator opted out, each
|
|
112
|
+
* user's own key (stored at this scope) is consulted.
|
|
113
|
+
*
|
|
114
|
+
* Absent or equal to `'global'`: no per-user fallback — only env +
|
|
115
|
+
* global file are checked. This is the personal-mode default.
|
|
116
|
+
*
|
|
117
|
+
* Production callers thread the request's authenticated identity
|
|
118
|
+
* here (e.g. `userId` for `kind:'user'`, `appId` for `kind:'app'`).
|
|
119
|
+
*/
|
|
120
|
+
readonly userScope?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Wire-level resolver type. Returned by {@link createByokResolver};
|
|
124
|
+
* shaped so call sites can DI a fake without importing the class.
|
|
125
|
+
*/
|
|
126
|
+
export interface ByokResolver {
|
|
127
|
+
resolve(provider: LlmProvider, opts?: ByokResolveOptions): Promise<ByokKeyResolution | null>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Construct a {@link ByokResolver} bound to a specific env + file
|
|
131
|
+
* store. Returned object is stateless; one resolver instance can
|
|
132
|
+
* serve many lookups across a process.
|
|
133
|
+
*
|
|
134
|
+
* The resolver never throws. Missing key → `null`. Malformed
|
|
135
|
+
* credentials file → propagates the underlying file store's error
|
|
136
|
+
* (today: `PlaintextFileProviderKeyStore` throws a "not a valid v1
|
|
137
|
+
* document" error so the operator gets a clear signal instead of a
|
|
138
|
+
* silent miss).
|
|
139
|
+
*/
|
|
140
|
+
export declare function createByokResolver(opts?: ByokResolverOptions): ByokResolver;
|
|
141
|
+
//# sourceMappingURL=byok-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byok-resolver.d.ts","sourceRoot":"","sources":["../src/byok-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,KAAK,EACV,WAAW,EAEX,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAIlC;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEnE,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CACvC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC,CAM3D,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,kBAAkB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC9C;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,CACL,QAAQ,EAAE,WAAW,EACrB,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CACtC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,GAAE,mBAAwB,GAC7B,YAAY,CA4Ed"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PlaintextFileProviderKeyStore } from '@ggui-ai/mcp-server-core/plaintext';
|
|
2
|
+
import { getCredentialsFile } from './paths.js';
|
|
3
|
+
/** App-scope key under which the resolver reads + writes the store. */
|
|
4
|
+
export const BYOK_GLOBAL_APP_SCOPE = 'global';
|
|
5
|
+
/**
|
|
6
|
+
* Provider → ordered list of env-var names to check. First non-empty
|
|
7
|
+
* wins. Single-name tuple for most providers; google has the
|
|
8
|
+
* `GOOGLE_API_KEY` / `GEMINI_API_KEY` alias pair.
|
|
9
|
+
*
|
|
10
|
+
* `bedrock` is intentionally absent — its auth chain lives outside
|
|
11
|
+
* this resolver (see module JSDoc).
|
|
12
|
+
*/
|
|
13
|
+
export const PROVIDER_ENV_NAMES = {
|
|
14
|
+
anthropic: ['ANTHROPIC_API_KEY'],
|
|
15
|
+
google: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
16
|
+
openai: ['OPENAI_API_KEY'],
|
|
17
|
+
openrouter: ['OPENROUTER_API_KEY'],
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Construct a {@link ByokResolver} bound to a specific env + file
|
|
21
|
+
* store. Returned object is stateless; one resolver instance can
|
|
22
|
+
* serve many lookups across a process.
|
|
23
|
+
*
|
|
24
|
+
* The resolver never throws. Missing key → `null`. Malformed
|
|
25
|
+
* credentials file → propagates the underlying file store's error
|
|
26
|
+
* (today: `PlaintextFileProviderKeyStore` throws a "not a valid v1
|
|
27
|
+
* document" error so the operator gets a clear signal instead of a
|
|
28
|
+
* silent miss).
|
|
29
|
+
*/
|
|
30
|
+
export function createByokResolver(opts = {}) {
|
|
31
|
+
const env = opts.env ?? process.env;
|
|
32
|
+
const fileStore = opts.fileStore !== undefined
|
|
33
|
+
? opts.fileStore
|
|
34
|
+
: new PlaintextFileProviderKeyStore({ filename: getCredentialsFile() });
|
|
35
|
+
// File-store helper. Used twice in the fallback chain (once at the
|
|
36
|
+
// operator's 'global' scope, optionally once at the end-user's
|
|
37
|
+
// identity scope).
|
|
38
|
+
const fileLookupAtScope = async (scope, provider) => {
|
|
39
|
+
if (fileStore === null)
|
|
40
|
+
return null;
|
|
41
|
+
const ref = await fileStore.get(scope, provider);
|
|
42
|
+
if (!ref)
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
key: ref.key,
|
|
46
|
+
source: 'credentials-file',
|
|
47
|
+
provider,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
async resolve(provider, resolveOpts) {
|
|
52
|
+
// Step 1: env. The PROVIDER_ENV_NAMES table is the locked
|
|
53
|
+
// mapping; bedrock isn't in it on purpose, so destructuring to
|
|
54
|
+
// `undefined` gives us the "skip env" path automatically.
|
|
55
|
+
const envNames = provider === 'bedrock'
|
|
56
|
+
? undefined
|
|
57
|
+
: PROVIDER_ENV_NAMES[provider];
|
|
58
|
+
if (envNames) {
|
|
59
|
+
for (const name of envNames) {
|
|
60
|
+
const value = env[name];
|
|
61
|
+
if (value !== undefined && value.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
key: value,
|
|
64
|
+
source: 'env',
|
|
65
|
+
provider,
|
|
66
|
+
envName: name,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Step 2: credentials file at operator's 'global' scope. If the
|
|
72
|
+
// operator set a key here, every user uses it (operator pays).
|
|
73
|
+
const globalHit = await fileLookupAtScope(BYOK_GLOBAL_APP_SCOPE, provider);
|
|
74
|
+
if (globalHit)
|
|
75
|
+
return globalHit;
|
|
76
|
+
// Step 3: credentials file at the end-user's identity scope.
|
|
77
|
+
// Only consulted when the operator opted out of paying (no
|
|
78
|
+
// global key). The /settings UI under --multi-tenant writes
|
|
79
|
+
// here.
|
|
80
|
+
if (resolveOpts?.userScope &&
|
|
81
|
+
resolveOpts.userScope !== BYOK_GLOBAL_APP_SCOPE) {
|
|
82
|
+
const userHit = await fileLookupAtScope(resolveOpts.userScope, provider);
|
|
83
|
+
if (userHit)
|
|
84
|
+
return userHit;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|