@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.
Files changed (117) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +96 -0
  3. package/dist/agent-resolution.d.ts +41 -0
  4. package/dist/agent-resolution.d.ts.map +1 -0
  5. package/dist/agent-resolution.js +80 -0
  6. package/dist/api-client.d.ts +59 -0
  7. package/dist/api-client.d.ts.map +1 -0
  8. package/dist/api-client.js +145 -0
  9. package/dist/auth-keys.d.ts +10 -0
  10. package/dist/auth-keys.d.ts.map +1 -0
  11. package/dist/auth-keys.js +382 -0
  12. package/dist/auth-login.d.ts +11 -0
  13. package/dist/auth-login.d.ts.map +1 -0
  14. package/dist/auth-login.js +231 -0
  15. package/dist/auth-logout.d.ts +3 -0
  16. package/dist/auth-logout.d.ts.map +1 -0
  17. package/dist/auth-logout.js +32 -0
  18. package/dist/auth-store.d.ts +67 -0
  19. package/dist/auth-store.d.ts.map +1 -0
  20. package/dist/auth-store.js +150 -0
  21. package/dist/auth-whoami.d.ts +3 -0
  22. package/dist/auth-whoami.d.ts.map +1 -0
  23. package/dist/auth-whoami.js +52 -0
  24. package/dist/blueprint-command.d.ts +3 -0
  25. package/dist/blueprint-command.d.ts.map +1 -0
  26. package/dist/blueprint-command.js +203 -0
  27. package/dist/blueprint-create.d.ts +71 -0
  28. package/dist/blueprint-create.d.ts.map +1 -0
  29. package/dist/blueprint-create.js +377 -0
  30. package/dist/byok-resolver.d.ts +141 -0
  31. package/dist/byok-resolver.d.ts.map +1 -0
  32. package/dist/byok-resolver.js +89 -0
  33. package/dist/cli.d.ts +3 -0
  34. package/dist/cli.d.ts.map +1 -0
  35. package/dist/cli.js +1066 -0
  36. package/dist/dev-command.d.ts +169 -0
  37. package/dist/dev-command.d.ts.map +1 -0
  38. package/dist/dev-command.js +222 -0
  39. package/dist/gadget-command.d.ts +3 -0
  40. package/dist/gadget-command.d.ts.map +1 -0
  41. package/dist/gadget-command.js +181 -0
  42. package/dist/gadget-create.d.ts +106 -0
  43. package/dist/gadget-create.d.ts.map +1 -0
  44. package/dist/gadget-create.js +492 -0
  45. package/dist/generation-probe.d.ts +135 -0
  46. package/dist/generation-probe.d.ts.map +1 -0
  47. package/dist/generation-probe.js +132 -0
  48. package/dist/internal/artifact-install.d.ts +150 -0
  49. package/dist/internal/artifact-install.d.ts.map +1 -0
  50. package/dist/internal/artifact-install.js +1019 -0
  51. package/dist/internal/artifact-publish.d.ts +204 -0
  52. package/dist/internal/artifact-publish.d.ts.map +1 -0
  53. package/dist/internal/artifact-publish.js +1141 -0
  54. package/dist/internal/artifact-search.d.ts +155 -0
  55. package/dist/internal/artifact-search.d.ts.map +1 -0
  56. package/dist/internal/artifact-search.js +539 -0
  57. package/dist/internal/artifact-uninstall.d.ts +30 -0
  58. package/dist/internal/artifact-uninstall.d.ts.map +1 -0
  59. package/dist/internal/artifact-uninstall.js +205 -0
  60. package/dist/internal/auth-cache.d.ts +40 -0
  61. package/dist/internal/auth-cache.d.ts.map +1 -0
  62. package/dist/internal/auth-cache.js +133 -0
  63. package/dist/internal/auth-strategy.d.ts +89 -0
  64. package/dist/internal/auth-strategy.d.ts.map +1 -0
  65. package/dist/internal/auth-strategy.js +132 -0
  66. package/dist/internal/ggui-json.d.ts +53 -0
  67. package/dist/internal/ggui-json.d.ts.map +1 -0
  68. package/dist/internal/ggui-json.js +92 -0
  69. package/dist/internal/key-store.d.ts +43 -0
  70. package/dist/internal/key-store.d.ts.map +1 -0
  71. package/dist/internal/key-store.js +128 -0
  72. package/dist/internal/oidc-token.d.ts +52 -0
  73. package/dist/internal/oidc-token.d.ts.map +1 -0
  74. package/dist/internal/oidc-token.js +351 -0
  75. package/dist/internal/register-author-key.d.ts +44 -0
  76. package/dist/internal/register-author-key.d.ts.map +1 -0
  77. package/dist/internal/register-author-key.js +212 -0
  78. package/dist/local-keys.d.ts +47 -0
  79. package/dist/local-keys.d.ts.map +1 -0
  80. package/dist/local-keys.js +115 -0
  81. package/dist/mcp-backend.d.ts +356 -0
  82. package/dist/mcp-backend.d.ts.map +1 -0
  83. package/dist/mcp-backend.js +668 -0
  84. package/dist/paths.d.ts +77 -0
  85. package/dist/paths.d.ts.map +1 -0
  86. package/dist/paths.js +119 -0
  87. package/dist/persistent-secrets.d.ts +28 -0
  88. package/dist/persistent-secrets.d.ts.map +1 -0
  89. package/dist/persistent-secrets.js +94 -0
  90. package/dist/persistent-short-code-index.d.ts +14 -0
  91. package/dist/persistent-short-code-index.d.ts.map +1 -0
  92. package/dist/persistent-short-code-index.js +39 -0
  93. package/dist/persistent-stores.d.ts +12 -0
  94. package/dist/persistent-stores.d.ts.map +1 -0
  95. package/dist/persistent-stores.js +42 -0
  96. package/dist/serve-command.d.ts +500 -0
  97. package/dist/serve-command.d.ts.map +1 -0
  98. package/dist/serve-command.js +616 -0
  99. package/dist/serve-fallback.d.ts +64 -0
  100. package/dist/serve-fallback.d.ts.map +1 -0
  101. package/dist/serve-fallback.js +122 -0
  102. package/dist/spawn-cloudflared.d.ts +61 -0
  103. package/dist/spawn-cloudflared.d.ts.map +1 -0
  104. package/dist/spawn-cloudflared.js +167 -0
  105. package/dist/theme-command.d.ts +4 -0
  106. package/dist/theme-command.d.ts.map +1 -0
  107. package/dist/theme-command.js +154 -0
  108. package/dist/theme-file-uploader.d.ts +8 -0
  109. package/dist/theme-file-uploader.d.ts.map +1 -0
  110. package/dist/theme-file-uploader.js +58 -0
  111. package/dist/theme-writer.d.ts +8 -0
  112. package/dist/theme-writer.d.ts.map +1 -0
  113. package/dist/theme-writer.js +103 -0
  114. package/dist/tunnel-provider.d.ts +273 -0
  115. package/dist/tunnel-provider.d.ts.map +1 -0
  116. package/dist/tunnel-provider.js +142 -0
  117. package/package.json +81 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * `ggui keys <list|create|revoke|register>` — manage CLI keys.
3
+ *
4
+ * - `list` / `create` / `revoke` operate on `ggui_user_*` connector
5
+ * keys (MCP transport bearers). One-time reveal model on `create`.
6
+ * - `register` registers a per-scope Ed25519 PUBLISHER key with the
7
+ * marketplace registry's `POST /author-keys` endpoint. Distinct
8
+ * from connector keys: the
9
+ * publisher key is auto-generated locally by `ggui gadget publish`
10
+ * / `ggui blueprint publish`; `register` ships its public half to
11
+ * the registry so future signed publishes validate.
12
+ */
13
+ import process from 'node:process';
14
+ import { ApiError, createKey, listKeys, revokeKey } from './api-client.js';
15
+ import { formatLocalKeyTable, loadLocalKeys, mintLocalKey, revokeLocalKey, saveLocalKeys, } from './local-keys.js';
16
+ import { runRegisterAuthorKey } from './internal/register-author-key.js';
17
+ export const KEYS_HELP = `ggui keys — manage CLI keys
18
+
19
+ Usage:
20
+ ggui keys list [--keys-file <path>]
21
+ ggui keys create [--name <label>] [--expires-at <iso8601>] [--keys-file <path>]
22
+ ggui keys revoke <id> [--keys-file <path>]
23
+ ggui keys register --scope <@scope> [--registry <url>]
24
+
25
+ list Print the caller's keys (id, prefix, name, status, dates).
26
+ create Mint a new connector key. The full secret is printed ONCE — copy it.
27
+ revoke Soft-revoke a connector key by id (the row stays for audit).
28
+ register Register a per-scope Ed25519 PUBLISHER key with the marketplace
29
+ registry so future \`ggui gadget publish\` / \`ggui blueprint publish\`
30
+ signed by the matching private key validate. The keypair is
31
+ auto-generated on first publish under
32
+ ~/.ggui/keys/<scope>/{private,public}.key — this verb ships
33
+ the public half to the registry's POST /author-keys.
34
+
35
+ Pass --keys-file <path> to operate on the LOCAL JSON file instead of
36
+ the cloud (api.ggui.ai). The file format is the one written by
37
+ \`ggui serve --keys-file\` — the two commands share the same store, so
38
+ you can mint a bearer here and have \`ggui serve\` accept it on next
39
+ boot. Use this for self-host / personal-mode workflows. --keys-file
40
+ applies to list/create/revoke only; \`register\` always talks to the
41
+ marketplace registry.
42
+ `;
43
+ export async function runKeysCommand(args) {
44
+ const [sub, ...rest] = args;
45
+ if (!sub || sub === '--help' || sub === '-h') {
46
+ process.stdout.write(KEYS_HELP);
47
+ return sub ? 0 : 2;
48
+ }
49
+ switch (sub) {
50
+ case 'list':
51
+ return runKeysList(rest);
52
+ case 'create':
53
+ return runKeysCreate(rest);
54
+ case 'revoke':
55
+ return runKeysRevoke(rest);
56
+ case 'register':
57
+ return runKeysRegister(rest);
58
+ default:
59
+ process.stderr.write(`ggui keys: unknown subcommand "${sub}"\n\n`);
60
+ process.stderr.write(KEYS_HELP);
61
+ return 2;
62
+ }
63
+ }
64
+ /**
65
+ * Pull `--keys-file <path>` out of an arg list. Returns the path
66
+ * (when present) and the residual args for downstream parsers.
67
+ * `--keys-file=<path>` long-form is also accepted because operators
68
+ * sometimes type it that way.
69
+ */
70
+ function takeKeysFile(args) {
71
+ const rest = [];
72
+ let keysFile;
73
+ for (let i = 0; i < args.length; i++) {
74
+ const arg = args[i];
75
+ if (arg === '--keys-file') {
76
+ const value = args[i + 1];
77
+ if (typeof value === 'string' && value.length > 0) {
78
+ keysFile = value;
79
+ i += 1;
80
+ continue;
81
+ }
82
+ }
83
+ else if (arg.startsWith('--keys-file=')) {
84
+ keysFile = arg.slice('--keys-file='.length);
85
+ continue;
86
+ }
87
+ rest.push(arg);
88
+ }
89
+ return keysFile === undefined ? { rest } : { keysFile, rest };
90
+ }
91
+ async function runKeysList(args) {
92
+ const { keysFile, rest } = takeKeysFile(args);
93
+ if (keysFile) {
94
+ if (rest.length > 0) {
95
+ process.stderr.write(`ggui keys list: unexpected extra args ${rest.join(' ')}\n`);
96
+ return 2;
97
+ }
98
+ try {
99
+ const state = loadLocalKeys(keysFile);
100
+ process.stdout.write(formatLocalKeyTable(state));
101
+ return 0;
102
+ }
103
+ catch (err) {
104
+ process.stderr.write(`ggui keys list: ${err instanceof Error ? err.message : String(err)}\n`);
105
+ return 1;
106
+ }
107
+ }
108
+ if (rest.length > 0) {
109
+ process.stderr.write(`ggui keys list: takes no arguments\n`);
110
+ return 2;
111
+ }
112
+ try {
113
+ const { keys } = await listKeys();
114
+ if (keys.length === 0) {
115
+ process.stdout.write(`No keys. Run \`ggui keys create\` to mint one.\n`);
116
+ return 0;
117
+ }
118
+ // Compact aligned table. Columns are sized by content width with a
119
+ // sane minimum so empty rows still render readably.
120
+ const rows = keys.map((k) => [
121
+ k.id,
122
+ `ggui_user_${k.apiKeyPrefix}…`,
123
+ k.name ?? '',
124
+ k.status,
125
+ k.lastUsedAt ?? '—',
126
+ ]);
127
+ const headers = ['ID', 'PREFIX', 'NAME', 'STATUS', 'LAST USED'];
128
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)));
129
+ const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i] ?? 0)).join(' ');
130
+ process.stdout.write(`${fmt(headers)}\n`);
131
+ for (const row of rows) {
132
+ process.stdout.write(`${fmt(row)}\n`);
133
+ }
134
+ return 0;
135
+ }
136
+ catch (err) {
137
+ return printApiError('keys list', err);
138
+ }
139
+ }
140
+ function parseCreateFlags(args) {
141
+ let name;
142
+ let expiresAt;
143
+ let help = false;
144
+ for (let i = 0; i < args.length; i++) {
145
+ const arg = args[i];
146
+ if (arg === '--help' || arg === '-h') {
147
+ help = true;
148
+ continue;
149
+ }
150
+ if (arg === '--name') {
151
+ const value = args[i + 1];
152
+ if (typeof value !== 'string' || value.length === 0) {
153
+ return { help, error: '--name requires a value' };
154
+ }
155
+ name = value;
156
+ i += 1;
157
+ continue;
158
+ }
159
+ if (arg === '--expires-at') {
160
+ const value = args[i + 1];
161
+ if (typeof value !== 'string' || value.length === 0) {
162
+ return { help, error: '--expires-at requires an ISO 8601 value' };
163
+ }
164
+ expiresAt = value;
165
+ i += 1;
166
+ continue;
167
+ }
168
+ return { help, error: `unknown flag: ${arg}` };
169
+ }
170
+ return { name, expiresAt, help };
171
+ }
172
+ async function runKeysCreate(args) {
173
+ const { keysFile, rest: afterKeysFile } = takeKeysFile(args);
174
+ if (keysFile) {
175
+ const flags = parseCreateFlags(afterKeysFile);
176
+ if (flags.help) {
177
+ process.stdout.write(KEYS_HELP);
178
+ return 0;
179
+ }
180
+ if (flags.error) {
181
+ process.stderr.write(`ggui keys create: ${flags.error}\n`);
182
+ return 2;
183
+ }
184
+ if (flags.expiresAt) {
185
+ process.stderr.write(`ggui keys create: --expires-at is not supported with --keys-file (local store has no TTL).\n`);
186
+ return 2;
187
+ }
188
+ try {
189
+ const state = loadLocalKeys(keysFile);
190
+ const row = mintLocalKey(state, {
191
+ deviceName: flags.name ?? `cli-${new Date().toISOString().slice(0, 10)}`,
192
+ });
193
+ saveLocalKeys(keysFile, state);
194
+ process.stdout.write(`\n`);
195
+ process.stdout.write(`API key: ${row.token}\n`);
196
+ process.stdout.write(`ID: ${row.pairingId}\n`);
197
+ process.stdout.write(`Name: ${row.deviceName}\n`);
198
+ process.stdout.write(`Created: ${new Date(row.createdAt).toISOString()}\n`);
199
+ process.stdout.write(`File: ${keysFile}\n`);
200
+ process.stdout.write(`\n`);
201
+ process.stdout.write(`IMPORTANT: copy the API key now — it will NEVER be shown again.\n`);
202
+ process.stdout.write(`Use it as the bearer token for any client that hits this server's /mcp.\n`);
203
+ return 0;
204
+ }
205
+ catch (err) {
206
+ process.stderr.write(`ggui keys create: ${err instanceof Error ? err.message : String(err)}\n`);
207
+ return 1;
208
+ }
209
+ }
210
+ const flags = parseCreateFlags(afterKeysFile);
211
+ if (flags.help) {
212
+ process.stdout.write(KEYS_HELP);
213
+ return 0;
214
+ }
215
+ if (flags.error) {
216
+ process.stderr.write(`ggui keys create: ${flags.error}\n`);
217
+ return 2;
218
+ }
219
+ try {
220
+ const result = await createKey({
221
+ name: flags.name,
222
+ expiresAt: flags.expiresAt,
223
+ });
224
+ process.stdout.write(`\n`);
225
+ process.stdout.write(`API key: ${result.apiKey}\n`);
226
+ process.stdout.write(`ID: ${result.id}\n`);
227
+ process.stdout.write(`Prefix: ${result.prefix}\n`);
228
+ process.stdout.write(`Created: ${result.createdAt}\n`);
229
+ process.stdout.write(`\n`);
230
+ process.stdout.write(`IMPORTANT: copy the API key now — it will NEVER be shown again.\n`);
231
+ process.stdout.write(`Use it as the bearer token for the ggui-protocol-user MCP server.\n`);
232
+ return 0;
233
+ }
234
+ catch (err) {
235
+ return printApiError('keys create', err);
236
+ }
237
+ }
238
+ async function runKeysRevoke(args) {
239
+ const { keysFile, rest } = takeKeysFile(args);
240
+ if (rest.length === 0 || rest[0] === '--help' || rest[0] === '-h') {
241
+ process.stdout.write(KEYS_HELP);
242
+ return rest.length === 0 ? 2 : 0;
243
+ }
244
+ if (rest.length !== 1) {
245
+ process.stderr.write(`ggui keys revoke: takes exactly one <id> argument\n`);
246
+ return 2;
247
+ }
248
+ const id = rest[0];
249
+ if (keysFile) {
250
+ try {
251
+ const state = loadLocalKeys(keysFile);
252
+ const removed = revokeLocalKey(state, id);
253
+ if (!removed) {
254
+ process.stderr.write(`ggui keys revoke: no key with id "${id}" in ${keysFile}\n`);
255
+ return 1;
256
+ }
257
+ saveLocalKeys(keysFile, state);
258
+ process.stdout.write(`Revoked ${id} from ${keysFile}.\n`);
259
+ return 0;
260
+ }
261
+ catch (err) {
262
+ process.stderr.write(`ggui keys revoke: ${err instanceof Error ? err.message : String(err)}\n`);
263
+ return 1;
264
+ }
265
+ }
266
+ try {
267
+ await revokeKey(id);
268
+ process.stdout.write(`Revoked key ${id}.\n`);
269
+ return 0;
270
+ }
271
+ catch (err) {
272
+ return printApiError('keys revoke', err);
273
+ }
274
+ }
275
+ function printApiError(label, err) {
276
+ if (err instanceof ApiError) {
277
+ process.stderr.write(`ggui ${label}: ${err.code}: ${err.message}\n`);
278
+ }
279
+ else if (err instanceof Error) {
280
+ process.stderr.write(`ggui ${label}: ${err.message}\n`);
281
+ }
282
+ else {
283
+ process.stderr.write(`ggui ${label}: unknown error\n`);
284
+ }
285
+ return 1;
286
+ }
287
+ export function parseRegisterFlags(args) {
288
+ let scope;
289
+ let registry;
290
+ let help = false;
291
+ for (let i = 0; i < args.length; i++) {
292
+ const arg = args[i];
293
+ if (arg === '--help' || arg === '-h') {
294
+ help = true;
295
+ continue;
296
+ }
297
+ if (arg === '--scope') {
298
+ const value = args[i + 1];
299
+ if (typeof value !== 'string' || value.length === 0) {
300
+ return { help, error: '--scope requires a value (e.g. `@my-scope`)' };
301
+ }
302
+ scope = value;
303
+ i += 1;
304
+ continue;
305
+ }
306
+ if (arg && arg.startsWith('--scope=')) {
307
+ scope = arg.slice('--scope='.length);
308
+ continue;
309
+ }
310
+ if (arg === '--registry') {
311
+ const value = args[i + 1];
312
+ if (typeof value !== 'string' || value.length === 0) {
313
+ return { help, error: '--registry requires a URL' };
314
+ }
315
+ registry = value;
316
+ i += 1;
317
+ continue;
318
+ }
319
+ if (arg && arg.startsWith('--registry=')) {
320
+ registry = arg.slice('--registry='.length);
321
+ continue;
322
+ }
323
+ return { help, error: `unknown flag: ${arg}` };
324
+ }
325
+ return {
326
+ help,
327
+ ...(scope !== undefined ? { scope } : {}),
328
+ ...(registry !== undefined ? { registry } : {}),
329
+ };
330
+ }
331
+ async function runKeysRegister(args) {
332
+ const flags = parseRegisterFlags(args);
333
+ if (flags.help) {
334
+ process.stdout.write(KEYS_HELP);
335
+ return 0;
336
+ }
337
+ if (flags.error) {
338
+ process.stderr.write(`ggui keys register: ${flags.error}\n`);
339
+ return 2;
340
+ }
341
+ if (!flags.scope) {
342
+ process.stderr.write('ggui keys register: --scope is required (e.g. `--scope @my-scope`)\n');
343
+ return 2;
344
+ }
345
+ if (!flags.scope.startsWith('@')) {
346
+ process.stderr.write(`ggui keys register: --scope must start with \`@\` (got "${flags.scope}")\n`);
347
+ return 2;
348
+ }
349
+ const outcome = await runRegisterAuthorKey({
350
+ scope: flags.scope,
351
+ ...(flags.registry !== undefined ? { registry: flags.registry } : {}),
352
+ }, {
353
+ cwd: process.cwd(),
354
+ env: process.env,
355
+ fetch,
356
+ // Unix-epoch SECONDS (matches the convention publish.ts uses for
357
+ // `expiresAt` in the registry-token cache — see auth-cache.ts:31).
358
+ // `Date.now()` returns milliseconds; passing it raw caused
359
+ // `isTokenFresh` to always return false against a real Cognito
360
+ // exp claim (also in seconds), forcing a re-auth on every run.
361
+ now: () => Math.floor(Date.now() / 1000),
362
+ });
363
+ if (outcome.ok) {
364
+ const verb = outcome.status === 201 ? 'Registered' : 'Already registered';
365
+ process.stdout.write(`${verb} publisher key for ${flags.scope}.\n` +
366
+ ` registry: ${outcome.registryUrl}\n` +
367
+ ` subject: ${outcome.subject}\n` +
368
+ ` keyId: ${outcome.keyId}\n`);
369
+ return 0;
370
+ }
371
+ process.stderr.write(`ggui keys register: ${outcome.code}: ${outcome.message}\n`);
372
+ // Exit-code policy: `key_conflict` is a distinct, scriptable state
373
+ // (the registry already holds a different publicKey for this
374
+ // subject+keyId — vanishingly rare hash collision OR a stale row).
375
+ // Scripts that wrap `ggui keys register` should be able to detect
376
+ // it without parsing the message. Exit 2 is reserved for usage
377
+ // errors (handled above before this point), exit 1 covers
378
+ // everything else.
379
+ if (outcome.code === 'key_conflict')
380
+ return 3;
381
+ return 1;
382
+ }
@@ -0,0 +1,11 @@
1
+ export declare const LOGIN_HELP = "ggui login \u2014 sign into ggui.ai for hosted-key management\n\nUsage:\n ggui login [--name <label>] [--no-open]\n\nOptions:\n --name <label> Device label to show in the console (default:\n \"ggui CLI on <hostname>\").\n --no-open Don't auto-open the browser (also implied by\n non-TTY stdout / BROWSER=none).\n\nEndpoint resolution:\n 1. GGUI_API_URL env override.\n 2. https://api.ggui.ai (default; Route53 swap pending S6).\n\nAfter success, tokens are written to ~/.ggui/auth.json with mode 0600.\n";
2
+ interface LoginFlags {
3
+ readonly name: string;
4
+ readonly autoOpen: boolean;
5
+ readonly help: boolean;
6
+ readonly error?: string;
7
+ }
8
+ export declare function parseLoginFlags(args: readonly string[]): LoginFlags;
9
+ export declare function runLoginCommand(args: readonly string[]): Promise<number>;
10
+ export {};
11
+ //# sourceMappingURL=auth-login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-login.d.ts","sourceRoot":"","sources":["../src/auth-login.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,UAAU,8iBAgBtB,CAAC;AAEF,UAAU,UAAU;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAwCnE;AAuFD,wBAAsB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0E9E"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * `ggui login` — RFC 8628 device-flow client.
3
+ *
4
+ * Steps:
5
+ * 1. POST /v1/auth/device → device_code + user_code + verification_uri.
6
+ * 2. Print the verification URL + user_code, attempt to open the
7
+ * browser (best-effort — non-TTY / `BROWSER=none` skips).
8
+ * 3. Poll /v1/auth/poll at the server-recommended interval until
9
+ * we get tokens, an error, or expiry.
10
+ * 4. Persist tokens to `~/.ggui/auth.json` via `saveAuthSession`.
11
+ */
12
+ import { hostname } from 'node:os';
13
+ import { spawn } from 'node:child_process';
14
+ import { ApiError, postAuthDevice, postAuthPoll, } from './api-client.js';
15
+ import { resolveEndpoint, saveAuthSession } from './auth-store.js';
16
+ export const LOGIN_HELP = `ggui login — sign into ggui.ai for hosted-key management
17
+
18
+ Usage:
19
+ ggui login [--name <label>] [--no-open]
20
+
21
+ Options:
22
+ --name <label> Device label to show in the console (default:
23
+ "ggui CLI on <hostname>").
24
+ --no-open Don't auto-open the browser (also implied by
25
+ non-TTY stdout / BROWSER=none).
26
+
27
+ Endpoint resolution:
28
+ 1. GGUI_API_URL env override.
29
+ 2. https://api.ggui.ai (default; Route53 swap pending S6).
30
+
31
+ After success, tokens are written to ~/.ggui/auth.json with mode 0600.
32
+ `;
33
+ export function parseLoginFlags(args) {
34
+ let name;
35
+ let autoOpen = shouldAutoOpenDefault();
36
+ let help = false;
37
+ for (let i = 0; i < args.length; i++) {
38
+ const arg = args[i];
39
+ if (arg === '--help' || arg === '-h') {
40
+ help = true;
41
+ continue;
42
+ }
43
+ if (arg === '--no-open') {
44
+ autoOpen = false;
45
+ continue;
46
+ }
47
+ if (arg === '--name') {
48
+ const value = args[i + 1];
49
+ if (typeof value !== 'string' || value.length === 0) {
50
+ return {
51
+ name: '',
52
+ autoOpen,
53
+ help,
54
+ error: '--name requires a value',
55
+ };
56
+ }
57
+ name = value;
58
+ i += 1;
59
+ continue;
60
+ }
61
+ return {
62
+ name: '',
63
+ autoOpen,
64
+ help,
65
+ error: `unknown flag: ${arg}`,
66
+ };
67
+ }
68
+ return {
69
+ name: name ?? defaultClientName(),
70
+ autoOpen,
71
+ help,
72
+ };
73
+ }
74
+ function defaultClientName() {
75
+ let host = 'unknown-host';
76
+ try {
77
+ host = hostname();
78
+ }
79
+ catch {
80
+ // best-effort
81
+ }
82
+ return `ggui CLI on ${host}`;
83
+ }
84
+ function shouldAutoOpenDefault() {
85
+ if (process.env['BROWSER'] === 'none')
86
+ return false;
87
+ if (process.env['CI'] === '1' || process.env['CI'] === 'true')
88
+ return false;
89
+ if (!process.stdout.isTTY)
90
+ return false;
91
+ return true;
92
+ }
93
+ function tryOpenBrowser(url) {
94
+ // Best-effort cross-platform browser open. macOS `open`, Linux
95
+ // `xdg-open`, Windows `cmd /c start ""`. Never throws — failure is
96
+ // non-fatal because the URL is also printed for the user.
97
+ const platform = process.platform;
98
+ let command;
99
+ let args;
100
+ if (platform === 'darwin') {
101
+ command = 'open';
102
+ args = [url];
103
+ }
104
+ else if (platform === 'win32') {
105
+ command = 'cmd';
106
+ args = ['/c', 'start', '""', url];
107
+ }
108
+ else {
109
+ command = 'xdg-open';
110
+ args = [url];
111
+ }
112
+ try {
113
+ const child = spawn(command, args, {
114
+ stdio: 'ignore',
115
+ detached: true,
116
+ });
117
+ child.unref();
118
+ child.on('error', () => {
119
+ // platform may lack the opener — silently swallow
120
+ });
121
+ }
122
+ catch {
123
+ // ignore
124
+ }
125
+ }
126
+ function sleep(ms) {
127
+ return new Promise((resolve) => setTimeout(resolve, ms));
128
+ }
129
+ async function pollForTokens(endpoint, device) {
130
+ const startMs = Date.now();
131
+ const expiryMs = startMs + device.expires_in * 1000;
132
+ let interval = device.interval * 1000;
133
+ while (Date.now() < expiryMs) {
134
+ await sleep(interval);
135
+ try {
136
+ return await postAuthPoll(endpoint, device.device_code);
137
+ }
138
+ catch (err) {
139
+ if (!(err instanceof ApiError))
140
+ throw err;
141
+ if (err.code === 'authorization_pending') {
142
+ // Keep polling at the same interval.
143
+ continue;
144
+ }
145
+ if (err.code === 'slow_down') {
146
+ // RFC 8628 §3.5 — server asks us to back off. Add 5s per spec.
147
+ interval += 5000;
148
+ continue;
149
+ }
150
+ // expired_token | invalid_grant | server_error → terminal.
151
+ throw err;
152
+ }
153
+ }
154
+ throw new ApiError(400, 'expired_token', 'Login window expired before approval. Run `ggui login` again.');
155
+ }
156
+ export async function runLoginCommand(args) {
157
+ const flags = parseLoginFlags(args);
158
+ if (flags.help) {
159
+ process.stdout.write(LOGIN_HELP);
160
+ return 0;
161
+ }
162
+ if (flags.error) {
163
+ process.stderr.write(`ggui login: ${flags.error}\n`);
164
+ return 2;
165
+ }
166
+ const endpoint = resolveEndpoint();
167
+ process.stdout.write(`Endpoint: ${endpoint.url} (${endpoint.source})\n`);
168
+ let device;
169
+ try {
170
+ device = await postAuthDevice(endpoint.url, flags.name);
171
+ }
172
+ catch (err) {
173
+ process.stderr.write(`ggui login: failed to start device flow: ${describeError(err)}\n`);
174
+ return 1;
175
+ }
176
+ process.stdout.write(`\n`);
177
+ process.stdout.write(`Open this URL in your browser to approve:\n`);
178
+ process.stdout.write(` ${device.verification_uri_complete}\n`);
179
+ process.stdout.write(`\n`);
180
+ process.stdout.write(`Verification code: ${device.user_code}\n`);
181
+ process.stdout.write(`(Confirm this matches what the browser shows.)\n`);
182
+ process.stdout.write(`\n`);
183
+ if (flags.autoOpen) {
184
+ tryOpenBrowser(device.verification_uri_complete);
185
+ }
186
+ process.stdout.write(`Waiting for approval…\n`);
187
+ let tokens;
188
+ try {
189
+ tokens = await pollForTokens(endpoint.url, device);
190
+ }
191
+ catch (err) {
192
+ process.stderr.write(`ggui login: ${describeError(err)}\n`);
193
+ return 1;
194
+ }
195
+ // Get user info to populate auth.json.userId. The /v1/me endpoint
196
+ // returns userId from the freshly-minted access token. If this 401s
197
+ // there's a server bug, but we still have valid tokens — fall back to
198
+ // empty userId so we can persist what we have.
199
+ const now = Math.floor(Date.now() / 1000);
200
+ const accessExpiresAt = now + tokens.expires_in;
201
+ // Refresh tokens have ~30d TTL on the server — see backend
202
+ // `REFRESH_TOKEN_TTL_SECONDS`. Mirror that so the CLI doesn't need
203
+ // to call /v1/me just to find out.
204
+ const refreshExpiresAt = now + 30 * 24 * 60 * 60;
205
+ // Persist with a placeholder userId; the next /v1/me call (whoami)
206
+ // will fetch the real value. Alternatively we could call /v1/me here
207
+ // — but that adds a round-trip on the hot login path. Defer.
208
+ saveAuthSession({
209
+ version: 1,
210
+ endpoint: endpoint.url,
211
+ userId: '',
212
+ sessionId: tokens.session_id,
213
+ accessToken: tokens.access_token,
214
+ accessExpiresAt,
215
+ refreshToken: tokens.refresh_token,
216
+ refreshExpiresAt,
217
+ clientName: flags.name,
218
+ writtenAt: new Date().toISOString(),
219
+ });
220
+ process.stdout.write(`\nSigned in. Tokens saved to ~/.ggui/auth.json.\n`);
221
+ process.stdout.write(`Try \`ggui whoami\` or \`ggui keys list\`.\n`);
222
+ return 0;
223
+ }
224
+ function describeError(err) {
225
+ if (err instanceof ApiError) {
226
+ return `${err.code}: ${err.message}`;
227
+ }
228
+ if (err instanceof Error)
229
+ return err.message;
230
+ return String(err);
231
+ }
@@ -0,0 +1,3 @@
1
+ export declare const LOGOUT_HELP = "ggui logout \u2014 clear the local CLI session\n\nUsage:\n ggui logout\n\nDeletes ~/.ggui/auth.json. Server-side tokens stay valid until their\nTTL expires (~1h access, ~30d refresh).\n";
2
+ export declare function runLogoutCommand(args: readonly string[]): number;
3
+ //# sourceMappingURL=auth-logout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-logout.d.ts","sourceRoot":"","sources":["../src/auth-logout.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,WAAW,+LAOvB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAYhE"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `ggui logout` — discard the local `~/.ggui/auth.json` session.
3
+ *
4
+ * Does NOT call a server-side revoke (no `/v1/auth/logout` endpoint
5
+ * at v1). The local-only delete means the access token stays valid on
6
+ * the server until its TTL expires (~1h). For sensitive scenarios,
7
+ * follow with `ggui keys revoke <id>` for any keys that were
8
+ * provisioned during the session.
9
+ */
10
+ import { deleteAuthSession } from './auth-store.js';
11
+ export const LOGOUT_HELP = `ggui logout — clear the local CLI session
12
+
13
+ Usage:
14
+ ggui logout
15
+
16
+ Deletes ~/.ggui/auth.json. Server-side tokens stay valid until their
17
+ TTL expires (~1h access, ~30d refresh).
18
+ `;
19
+ export function runLogoutCommand(args) {
20
+ if (args.includes('--help') || args.includes('-h')) {
21
+ process.stdout.write(LOGOUT_HELP);
22
+ return 0;
23
+ }
24
+ const { deleted } = deleteAuthSession();
25
+ if (deleted) {
26
+ process.stdout.write(`Signed out. ~/.ggui/auth.json removed.\n`);
27
+ }
28
+ else {
29
+ process.stdout.write(`No active session found.\n`);
30
+ }
31
+ return 0;
32
+ }