@mycadet/cli 0.12.6-dev.202610082233.54244ee → 0.12.7

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 (89) hide show
  1. package/eslint.config.mjs +7 -0
  2. package/node_modules/@mycadet/local-mode/src/__tests__/environment.test.ts +1 -4
  3. package/node_modules/@mycadet/local-mode/src/config.ts +14 -19
  4. package/node_modules/@mycadet/local-mode/src/index.ts +5 -0
  5. package/node_modules/@mycadet/local-mode/src/local-signing-key.ts +73 -0
  6. package/node_modules/@mycadet/local-mode/src/lockfile.ts +18 -3
  7. package/package.json +4 -3
  8. package/src/__tests__/assistant-config.test.ts +30 -28
  9. package/src/__tests__/debug.test.ts +8 -8
  10. package/src/__tests__/default-main-screen-formatting.test.ts +5 -4
  11. package/src/__tests__/flags.test.ts +2 -2
  12. package/src/__tests__/hatch-provider-secrets.test.ts +3 -3
  13. package/src/__tests__/ingress-config.test.ts +2 -2
  14. package/src/__tests__/login-loopback.test.ts +1 -1
  15. package/src/__tests__/nginx-ingress.test.ts +68 -76
  16. package/src/__tests__/pair.test.ts +135 -121
  17. package/src/__tests__/platform-client.test.ts +2 -2
  18. package/src/__tests__/recover.test.ts +96 -3
  19. package/src/__tests__/retire.test.ts +5 -3
  20. package/src/__tests__/signing-key-continuity.test.ts +261 -0
  21. package/src/__tests__/sleep.test.ts +189 -6
  22. package/src/__tests__/tailscale-tunnel.test.ts +1 -1
  23. package/src/__tests__/uninstall.test.ts +171 -0
  24. package/src/__tests__/upgrade-local.test.ts +16 -3
  25. package/src/__tests__/use.test.ts +2 -2
  26. package/src/__tests__/wake-error.test.ts +76 -0
  27. package/src/__tests__/wake.test.ts +309 -36
  28. package/src/commands/backup.ts +6 -2
  29. package/src/commands/clean.ts +6 -2
  30. package/src/commands/client.ts +91 -41
  31. package/src/commands/confirm.ts +6 -2
  32. package/src/commands/connect.ts +6 -2
  33. package/src/commands/debug.ts +6 -2
  34. package/src/commands/devices.ts +6 -2
  35. package/src/commands/events.ts +6 -2
  36. package/src/commands/exec.ts +50 -12
  37. package/src/commands/flags.ts +6 -2
  38. package/src/commands/gateway.ts +6 -2
  39. package/src/commands/hatch.ts +6 -2
  40. package/src/commands/login.ts +26 -356
  41. package/src/commands/logs.ts +6 -2
  42. package/src/commands/message.ts +6 -2
  43. package/src/commands/pair.ts +6 -2
  44. package/src/commands/ps.ts +7 -2
  45. package/src/commands/recover.ts +21 -16
  46. package/src/commands/restore.ts +6 -2
  47. package/src/commands/retire.ts +6 -2
  48. package/src/commands/roadmap.ts +6 -2
  49. package/src/commands/rollback.ts +6 -2
  50. package/src/commands/setup.ts +76 -9
  51. package/src/commands/sleep.ts +131 -31
  52. package/src/commands/ssh.ts +6 -2
  53. package/src/commands/teleport.ts +6 -2
  54. package/src/commands/terminal.ts +6 -2
  55. package/src/commands/tunnel.ts +6 -2
  56. package/src/commands/uninstall.ts +228 -0
  57. package/src/commands/unpair.ts +6 -2
  58. package/src/commands/upgrade.ts +13 -8
  59. package/src/commands/use.ts +6 -2
  60. package/src/commands/voice.ts +6 -2
  61. package/src/commands/wake.ts +69 -37
  62. package/src/commands/workflows.ts +6 -2
  63. package/src/components/DefaultMainScreen.tsx +250 -36
  64. package/src/index.ts +23 -55
  65. package/src/lib/assistant-config.ts +27 -13
  66. package/src/lib/backup-ops.ts +3 -4
  67. package/src/lib/cli-error.ts +5 -0
  68. package/src/lib/client-identity.ts +8 -10
  69. package/src/lib/cloud-login.ts +422 -0
  70. package/src/lib/color.ts +16 -0
  71. package/src/lib/default-main-screen-formatting.ts +32 -8
  72. package/src/lib/environments/__tests__/paths.test.ts +9 -11
  73. package/src/lib/environments/paths.ts +2 -20
  74. package/src/lib/guardian-token.ts +72 -18
  75. package/src/lib/hatch-local.ts +52 -1
  76. package/src/lib/health-check.ts +24 -15
  77. package/src/lib/lifecycle-reporter.ts +9 -1
  78. package/src/lib/local-hatch-lock.ts +19 -27
  79. package/src/lib/local-runtime-client.ts +25 -14
  80. package/src/lib/local.ts +17 -43
  81. package/src/lib/ngrok.ts +48 -16
  82. package/src/lib/platform-client.ts +141 -72
  83. package/src/lib/retire-archive.ts +4 -4
  84. package/src/lib/retire-local.ts +2 -25
  85. package/src/lib/teleport-backup.ts +23 -9
  86. package/src/lib/terminal-client.ts +21 -9
  87. package/src/lib/tunnel-edge.ts +11 -13
  88. package/src/lib/upgrade-lifecycle.ts +16 -4
  89. package/src/lib/workos-pkce.ts +52 -21
@@ -1,303 +1,29 @@
1
1
  import { isManagedCloud } from "../lib/managed-cloud.js";
2
- import { randomBytes } from "crypto";
3
- import { createServer } from "http";
4
- import type { AddressInfo } from "net";
5
2
 
6
3
  import {
7
4
  getActiveAssistant,
8
5
  loadAllAssistants,
9
6
  removeAssistantEntry,
10
- resolveAssistant,
11
- saveAssistantEntry,
12
7
  setActiveAssistant,
13
8
  } from "../lib/assistant-config";
14
- import { resolveAssistantApiKeyForInjection } from "../lib/assistant-api-key-resolution";
15
- import { computeDeviceId } from "../lib/guardian-token";
16
- import { openBrowser } from "../lib/open-browser";
17
9
  import {
18
10
  clearPlatformToken,
19
- ensureSelfHostedLocalRegistration,
20
11
  fetchCurrentUser,
21
- fetchOrganizationId,
22
12
  fetchPlatformAssistants,
23
- getPlatformUrl,
24
- injectCredentialsIntoAssistant,
25
13
  readPlatformToken,
26
14
  savePlatformToken,
27
15
  } from "../lib/platform-client";
28
- import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
29
- import {
30
- fetchAssistantIngressUrl,
31
- fetchCurrentVersion,
32
- } from "../lib/upgrade-lifecycle.js";
33
16
  import {
34
- CALLBACK_PATH,
35
- buildAuthorizeUrl,
36
- exchangeAccessTokenForSession,
37
- exchangeCodeWithWorkos,
38
- fetchWorkosClientId,
39
- generatePkcePair,
40
- } from "../lib/workos-pkce";
41
-
42
- const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
43
-
44
- function escapeHtml(s: string): string {
45
- return s
46
- .replace(/&/g, "&")
47
- .replace(/</g, "&lt;")
48
- .replace(/>/g, "&gt;")
49
- .replace(/"/g, "&quot;")
50
- .replace(/'/g, "&#39;");
51
- }
52
-
53
- function renderLoginPage(
54
- title: string,
55
- subtitle: string,
56
- success: boolean,
57
- ): string {
58
- const checkmarkSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
59
- <circle cx="28" cy="28" r="28" fill="var(--positive-bg)"/>
60
- <path class="check" d="M17 28.5L24.5 36L39 21" stroke="var(--positive-fg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
61
- </svg>`;
62
-
63
- const errorSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
64
- <circle cx="28" cy="28" r="28" fill="var(--negative-bg)"/>
65
- <path class="cross cross-1" d="M20 20L36 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
66
- <path class="cross cross-2" d="M36 20L20 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
67
- </svg>`;
68
-
69
- return `<!DOCTYPE html>
70
- <html lang="en">
71
- <head>
72
- <meta charset="utf-8">
73
- <meta name="viewport" content="width=device-width, initial-scale=1">
74
- <title>${escapeHtml(title)}</title>
75
- <style>
76
- :root {
77
- --surface: #F5F3EB;
78
- --surface-card: #FFFFFF;
79
- --card-border: #E8E6DA;
80
- --text-primary: #2A2A28;
81
- --text-secondary: #4A4A46;
82
- --positive-bg: #D4DFD0;
83
- --positive-fg: #516748;
84
- --negative-bg: #F7DAC9;
85
- --negative-fg: #DA491A;
86
- --shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06);
87
- --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
88
- }
89
- @media (prefers-color-scheme: dark) {
90
- :root {
91
- --surface: #1A1A18;
92
- --surface-card: #2A2A28;
93
- --card-border: #3A3A37;
94
- --text-primary: #F5F3EB;
95
- --text-secondary: #BDB9A9;
96
- --positive-bg: #1A2316;
97
- --positive-fg: #7A8B6F;
98
- --negative-bg: #4E281D;
99
- --negative-fg: #E86B40;
100
- --shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
101
- }
102
- }
103
- * { margin: 0; padding: 0; box-sizing: border-box; }
104
- body {
105
- font-family: var(--font);
106
- background: var(--surface);
107
- color: var(--text-primary);
108
- display: flex;
109
- align-items: center;
110
- justify-content: center;
111
- min-height: 100vh;
112
- -webkit-font-smoothing: antialiased;
113
- }
114
- .card {
115
- text-align: center;
116
- padding: 48px 40px 40px;
117
- background: var(--surface-card);
118
- border: 1px solid var(--card-border);
119
- border-radius: 16px;
120
- box-shadow: var(--shadow);
121
- max-width: 380px;
122
- width: 100%;
123
- opacity: 0;
124
- transform: translateY(8px) scale(0.98);
125
- animation: cardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
126
- }
127
- @keyframes cardIn {
128
- to { opacity: 1; transform: translateY(0) scale(1); }
129
- }
130
- .icon {
131
- width: 56px;
132
- height: 56px;
133
- margin-bottom: 20px;
134
- }
135
- .check {
136
- stroke-dasharray: 32;
137
- stroke-dashoffset: 32;
138
- animation: draw 0.4s ease-out 0.45s forwards;
139
- }
140
- .cross {
141
- stroke-dasharray: 22;
142
- stroke-dashoffset: 22;
143
- }
144
- .cross-1 { animation: draw 0.3s ease-out 0.45s forwards; }
145
- .cross-2 { animation: draw 0.3s ease-out 0.55s forwards; }
146
- @keyframes draw {
147
- to { stroke-dashoffset: 0; }
148
- }
149
- h1 {
150
- font-size: 18px;
151
- font-weight: 600;
152
- letter-spacing: -0.2px;
153
- color: var(--text-primary);
154
- margin-bottom: 6px;
155
- }
156
- p {
157
- font-size: 13px;
158
- line-height: 1.5;
159
- color: var(--text-secondary);
160
- }
161
- </style>
162
- </head>
163
- <body>
164
- <div class="card">
165
- ${success ? checkmarkSvg : errorSvg}
166
- <h1>${escapeHtml(title)}</h1>
167
- <p>${escapeHtml(subtitle)}</p>
168
- </div>
169
- </body>
170
- </html>`;
171
- }
172
-
173
- export interface LoopbackListener {
174
- /** The full `http://127.0.0.1:<port>/auth/callback` redirect URI. */
175
- redirectUri: string;
176
- /** Resolves with the authorization code once the state-matched callback arrives. */
177
- waitForCode: Promise<string>;
178
- /** Tear down the server, rejecting any pending waiter with `reason`. */
179
- close: (reason?: string) => void;
180
- }
181
-
182
- /**
183
- * Bind an ephemeral 127.0.0.1 listener and wait for the OAuth redirect.
184
- * Exported for tests; production callers go through `workosPkceLogin`.
185
- */
186
- export function startLoopbackListener(
187
- expectedState: string,
188
- ): Promise<LoopbackListener> {
189
- return new Promise((resolveListener, rejectListener) => {
190
- let settle: {
191
- resolve: (code: string) => void;
192
- reject: (err: Error) => void;
193
- };
194
- const waitForCode = new Promise<string>((resolve, reject) => {
195
- settle = { resolve, reject };
196
- });
197
-
198
- const server = createServer((req, res) => {
199
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
200
- if (
201
- url.pathname !== CALLBACK_PATH ||
202
- url.searchParams.get("state") !== expectedState
203
- ) {
204
- res.writeHead(404, { "Content-Type": "text/plain" });
205
- res.end("Not found");
206
- return;
207
- }
208
-
209
- const error = url.searchParams.get("error");
210
- const code = url.searchParams.get("code");
211
- if (error || !code) {
212
- res.writeHead(400, { "Content-Type": "text/html" });
213
- res.end(
214
- renderLoginPage(
215
- "Login Failed",
216
- "Please try again from your terminal.",
217
- false,
218
- ),
219
- );
220
- server.close();
221
- settle.reject(
222
- new Error(
223
- `Authentication failed: ${error ?? "no authorization code received"}`,
224
- ),
225
- );
226
- return;
227
- }
228
-
229
- res.writeHead(200, { "Content-Type": "text/html" });
230
- res.end(
231
- renderLoginPage(
232
- "Login Successful",
233
- "You can close this window and return to your terminal.",
234
- true,
235
- ),
236
- );
237
- server.close();
238
- settle.resolve(code);
239
- });
240
-
241
- server.on("error", rejectListener);
242
- server.listen(0, "127.0.0.1", () => {
243
- const addr = server.address();
244
- if (!addr || typeof addr === "string") {
245
- rejectListener(new Error("Failed to start local server."));
246
- return;
247
- }
248
- const { port } = addr as AddressInfo;
249
- resolveListener({
250
- redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
251
- waitForCode,
252
- close: (reason?: string) => {
253
- server.close();
254
- settle.reject(new Error(reason ?? "Login cancelled."));
255
- },
256
- });
257
- });
258
- });
259
- }
260
-
261
- /** App-held WorkOS PKCE login */
262
- async function workosPkceLogin(platformUrl: string): Promise<string> {
263
- const clientId = await fetchWorkosClientId(platformUrl);
264
- const { verifier, challenge } = generatePkcePair();
265
- const state = randomBytes(32).toString("hex");
266
-
267
- const listener = await startLoopbackListener(state);
268
- const timeout = setTimeout(() => {
269
- listener.close("Login timed out. Please try again.");
270
- }, LOGIN_TIMEOUT_MS);
271
-
272
- try {
273
- const authorizeUrl = buildAuthorizeUrl({
274
- clientId,
275
- redirectUri: listener.redirectUri,
276
- challenge,
277
- state,
278
- });
279
-
280
- console.log("Opening browser for login...");
281
- console.log(`If the browser doesn't open, visit: ${authorizeUrl}`);
282
- openBrowser(authorizeUrl);
17
+ describeLoginError,
18
+ registerLocalAssistantWithPlatform,
19
+ workosPkceLogin,
20
+ } from "../lib/cloud-login.js";
21
+ import { getPlatformUrl } from "../lib/platform-client";
22
+ import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
283
23
 
284
- const code = await listener.waitForCode;
285
- const accessToken = await exchangeCodeWithWorkos({
286
- clientId,
287
- code,
288
- verifier,
289
- });
290
- return await exchangeAccessTokenForSession(
291
- platformUrl,
292
- clientId,
293
- accessToken,
294
- );
295
- } finally {
296
- clearTimeout(timeout);
297
- }
298
- }
299
24
 
300
- export async function login(): Promise<void> {
25
+ export const login = Object.assign(
26
+ async function login(): Promise<void> {
301
27
  const args = process.argv.slice(3);
302
28
 
303
29
  if (args.includes("--help") || args.includes("-h")) {
@@ -359,7 +85,7 @@ export async function login(): Promise<void> {
359
85
  try {
360
86
  token = await workosPkceLogin(getPlatformUrl());
361
87
  } catch (error) {
362
- console.error(`❌ ${error instanceof Error ? error.message : error}`);
88
+ console.error(`❌ ${describeLoginError(error)}`);
363
89
  process.exit(1);
364
90
  }
365
91
  }
@@ -374,74 +100,7 @@ export async function login(): Promise<void> {
374
100
  // Register the local assistant with the platform (non-fatal).
375
101
  // Mirrors the desktop app's LocalAssistantBootstrapService flow.
376
102
  try {
377
- const entry = resolveAssistant();
378
-
379
- // Skip managed ("cadet") assistants — they are handled by the platform.
380
- if (entry && !isManagedCloud(entry.cloud)) {
381
- const orgId = await fetchOrganizationId(token);
382
- const clientInstallationId = computeDeviceId();
383
- const [assistantVersion, ingressUrl] = await Promise.all([
384
- fetchCurrentVersion(entry.runtimeUrl),
385
- fetchAssistantIngressUrl(entry.runtimeUrl, entry.bearerToken),
386
- ]);
387
- const platformUrl = getPlatformUrl();
388
- const registration = await ensureSelfHostedLocalRegistration(
389
- token,
390
- orgId,
391
- clientInstallationId,
392
- entry.assistantId,
393
- "cli",
394
- assistantVersion,
395
- platformUrl,
396
- ingressUrl,
397
- );
398
- saveAssistantEntry({
399
- ...entry,
400
- platformAssistantId: registration.assistant.id,
401
- platformBaseUrl: platformUrl,
402
- platformOrganizationId: orgId,
403
- });
404
- console.log(
405
- `Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
406
- );
407
-
408
- // Which key to hand the assistant, and why. Shared with teleport so
409
- // the two injection paths cannot drift.
410
- const resolved = await resolveAssistantApiKeyForInjection({
411
- registrationApiKey: registration.assistant_api_key,
412
- runtimeUrl: entry.runtimeUrl,
413
- bearerToken: entry.bearerToken,
414
- token,
415
- organizationId: orgId,
416
- clientInstallationId,
417
- runtimeAssistantId: entry.assistantId,
418
- clientPlatform: "cli",
419
- });
420
- const assistantApiKey = resolved.apiKey;
421
- if (resolved.source === "reprovisioned") {
422
- console.log("Provisioning a replacement API key...");
423
- }
424
-
425
- // Inject credentials into the running assistant via the gateway,
426
- // mirroring the desktop app's LocalAssistantBootstrapService flow.
427
- const allInjected = await injectCredentialsIntoAssistant({
428
- gatewayUrl: entry.runtimeUrl,
429
- bearerToken: entry.bearerToken,
430
- assistantApiKey,
431
- platformAssistantId: registration.assistant.id,
432
- platformBaseUrl: platformUrl,
433
- organizationId: orgId,
434
- userId: user.id,
435
- webhookSecret: registration.webhook_secret,
436
- });
437
- if (allInjected) {
438
- console.log("Injected cloud credentials into assistant.");
439
- } else {
440
- console.warn(
441
- "Some credentials could not be injected into the assistant.",
442
- );
443
- }
444
- }
103
+ await registerLocalAssistantWithPlatform(token, user);
445
104
  } catch {
446
105
  // Non-fatal — login succeeded even if registration fails
447
106
  }
@@ -477,9 +136,13 @@ export async function login(): Promise<void> {
477
136
  );
478
137
  process.exit(1);
479
138
  }
480
- }
139
+ },
140
+ { description: "Log in to Cadet Cloud" },
141
+ );
481
142
 
482
- export async function logout(): Promise<void> {
143
+
144
+ export const logout = Object.assign(
145
+ async function logout(): Promise<void> {
483
146
  const args = process.argv.slice(3);
484
147
  if (args.includes("--help") || args.includes("-h")) {
485
148
  console.log("Usage: cadet logout");
@@ -504,9 +167,13 @@ export async function logout(): Promise<void> {
504
167
 
505
168
  clearPlatformToken();
506
169
  console.log("Logged out. Cadet Cloud token removed.");
507
- }
170
+ },
171
+ { description: "Log out of Cadet Cloud" },
172
+ );
173
+
508
174
 
509
- export async function whoami(): Promise<void> {
175
+ export const whoami = Object.assign(
176
+ async function whoami(): Promise<void> {
510
177
  const args = process.argv.slice(3);
511
178
  if (args.includes("--help") || args.includes("-h")) {
512
179
  console.log("Usage: cadet whoami");
@@ -532,4 +199,7 @@ export async function whoami(): Promise<void> {
532
199
  console.error(`Error: ${error instanceof Error ? error.message : error}`);
533
200
  process.exit(1);
534
201
  }
535
- }
202
+ },
203
+ { description: "Show current logged-in user" },
204
+ );
205
+
@@ -583,7 +583,8 @@ async function showAwsLogs(
583
583
 
584
584
  // ── Entry point ─────────────────────────────────────────────────
585
585
 
586
- export async function logs(): Promise<void> {
586
+ export const logs = Object.assign(
587
+ async function logs(): Promise<void> {
587
588
  const opts = parseArgs();
588
589
 
589
590
  const entry = resolveAssistant(opts.name);
@@ -640,4 +641,7 @@ export async function logs(): Promise<void> {
640
641
  console.error(`Unsupported topology '${cloud}' for log viewing.`);
641
642
  process.exit(1);
642
643
  }
643
- }
644
+ },
645
+ { description: "View logs from an assistant instance" },
646
+ );
647
+
@@ -123,7 +123,8 @@ function exitWithUsage(error: string): never {
123
123
  process.exit(1);
124
124
  }
125
125
 
126
- export async function message(): Promise<void> {
126
+ export const message = Object.assign(
127
+ async function message(): Promise<void> {
127
128
  const rawArgs = process.argv.slice(3);
128
129
 
129
130
  if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
@@ -192,4 +193,7 @@ export async function message(): Promise<void> {
192
193
  console.log(`Message rejected (id: ${result.messageId})`);
193
194
  }
194
195
  }
195
- }
196
+ },
197
+ { description: "Send a message to a running assistant" },
198
+ );
199
+
@@ -258,7 +258,8 @@ async function approveRemoteWebPairing(
258
258
  return (await response.json()) as RemoteWebPairingVerificationResponse;
259
259
  }
260
260
 
261
- export async function pair(): Promise<void> {
261
+ export const pair = Object.assign(
262
+ async function pair(): Promise<void> {
262
263
  const rawArgs = process.argv.slice(3);
263
264
 
264
265
  if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
@@ -497,4 +498,7 @@ export async function pair(): Promise<void> {
497
498
  console.log(`On another computer, run: cadet connect import "${pairUrl}"`);
498
499
  console.log("");
499
500
  console.log(`Expires: ${challenge.expiresAt}`);
500
- }
501
+ },
502
+ { description: "Print a pairing link and QR code for another device [beta]" },
503
+ );
504
+
@@ -677,11 +677,13 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
677
677
  );
678
678
 
679
679
  printTable(rows, { name: "NAME", status: "STATUS", info: "INFO" });
680
+ console.log("");
680
681
  }
681
682
 
682
683
  // ── Entry point ─────────────────────────────────────────────────
683
684
 
684
- export async function ps(): Promise<void> {
685
+ export const ps = Object.assign(
686
+ async function ps(): Promise<void> {
685
687
  const args = process.argv.slice(3);
686
688
  if (args.includes("--help") || args.includes("-h")) {
687
689
  console.log("Usage: cadet ps [<name-or-id>] [--verbose]");
@@ -717,4 +719,7 @@ export async function ps(): Promise<void> {
717
719
  }
718
720
 
719
721
  await showAssistantProcesses(result.entry);
720
- }
722
+ },
723
+ { description: "List assistants (or processes for a specific assistant)" },
724
+ );
725
+
@@ -1,3 +1,4 @@
1
+ import { resolveExistingLocalSigningKey } from "@mycadet/local-mode";
1
2
  import {
2
3
  existsSync,
3
4
  mkdirSync,
@@ -18,7 +19,8 @@ import {
18
19
  import { getArchivePath, getMetadataPath } from "../lib/retire-archive";
19
20
  import { exec } from "../lib/step-runner";
20
21
 
21
- export async function recover(): Promise<void> {
22
+ export const recover = Object.assign(
23
+ async function recover(): Promise<void> {
22
24
  const args = process.argv.slice(3);
23
25
  if (args.includes("--help") || args.includes("-h")) {
24
26
  console.log("Usage: cadet recover <name>");
@@ -96,24 +98,21 @@ export async function recover(): Promise<void> {
96
98
  const extractedPath = join(retiredDir, basename(archivePath) + ".staging");
97
99
 
98
100
  await exec("tar", ["xzf", archivePath, "-C", retiredDir]);
99
- mkdirSync(dirname(targetDir), { recursive: true });
100
- renameSync(extractedPath, targetDir);
101
-
102
- // 5. Restore lockfile entry
103
- saveAssistantEntry(entry);
104
-
105
- // 6. Clean up archive
106
- unlinkSync(archivePath);
107
- unlinkSync(metadataPath);
108
-
109
- // 7. Persist signing key and bootstrap secret so they survive daemon/gateway restarts
110
- const signingKey = generateLocalSigningKey();
111
- const bootstrapSecret = generateLocalSigningKey();
101
+ // Validate the staged identity before occupying the destination or updating
102
+ // the registry. A failed validation leaves the archive available for retry.
103
+ const signingKey = resolveExistingLocalSigningKey(
104
+ entry.resources,
105
+ extractedPath,
106
+ );
107
+ const bootstrapSecret =
108
+ entry.guardianBootstrapSecret ?? generateLocalSigningKey();
112
109
  entry.resources = { ...entry.resources, signingKey };
113
110
  entry.guardianBootstrapSecret = bootstrapSecret;
111
+ mkdirSync(dirname(targetDir), { recursive: true });
112
+ renameSync(extractedPath, targetDir);
114
113
  saveAssistantEntry(entry);
115
114
 
116
- // 8. Start CES sibling + daemon + gateway in parallel, the way the
115
+ // 7. Start CES sibling + daemon + gateway in parallel, the way the
117
116
  // Docker topology brings its sibling processes up together. startCes
118
117
  // always launches the CES sibling.
119
118
  await Promise.all([
@@ -122,5 +121,11 @@ export async function recover(): Promise<void> {
122
121
  startGateway(false, entry.resources, { signingKey, bootstrapSecret }),
123
122
  ]);
124
123
 
124
+ unlinkSync(archivePath);
125
+ unlinkSync(metadataPath);
126
+
125
127
  console.log(`✅ Recovered assistant '${name}'.`);
126
- }
128
+ },
129
+ { description: "Restore a previously retired local assistant" },
130
+ );
131
+
@@ -515,7 +515,8 @@ async function restorePlatform(
515
515
  }
516
516
  }
517
517
 
518
- export async function restore(): Promise<void> {
518
+ export const restore = Object.assign(
519
+ async function restore(): Promise<void> {
519
520
  const { name, fromPath, version, dryRun, help } = parseArgs(process.argv);
520
521
 
521
522
  if (help) {
@@ -623,7 +624,10 @@ export async function restore(): Promise<void> {
623
624
  await cleanupStaged();
624
625
  }
625
626
  }
626
- }
627
+ },
628
+ { description: "Restore data (and optionally version) from a .vbundle backup" },
629
+ );
630
+
627
631
 
628
632
  async function runLocalRestore(opts: {
629
633
  entry: AssistantEntry;
@@ -364,7 +364,8 @@ function teeConsoleToLogFile(fd: number | "ignore"): void {
364
364
  };
365
365
  }
366
366
 
367
- export async function retire(): Promise<void> {
367
+ export const retire = Object.assign(
368
+ async function retire(): Promise<void> {
368
369
  if (process.env.CADET_DESKTOP_APP) {
369
370
  resetLogFile("retire.log");
370
371
  }
@@ -378,7 +379,10 @@ export async function retire(): Promise<void> {
378
379
  } finally {
379
380
  closeLogFile(logFd);
380
381
  }
381
- }
382
+ },
383
+ { description: "Delete an assistant instance" },
384
+ );
385
+
382
386
 
383
387
  async function retireInner(): Promise<void> {
384
388
  const args = process.argv.slice(3);
@@ -413,7 +413,8 @@ async function roadmapUnvote(args: string[]): Promise<void> {
413
413
 
414
414
  // ── main ──
415
415
 
416
- export async function roadmap(): Promise<void> {
416
+ export const roadmap = Object.assign(
417
+ async function roadmap(): Promise<void> {
417
418
  const args = process.argv.slice(3);
418
419
  const sub = args[0];
419
420
 
@@ -452,4 +453,7 @@ export async function roadmap(): Promise<void> {
452
453
  printUsage();
453
454
  process.exitCode = 1;
454
455
  }
455
- }
456
+ },
457
+ { description: "Manage roadmap items" },
458
+ );
459
+
@@ -211,7 +211,8 @@ async function rollbackPlatformViaEndpoint(
211
211
  }
212
212
  }
213
213
 
214
- export async function rollback(): Promise<void> {
214
+ export const rollback = Object.assign(
215
+ async function rollback(): Promise<void> {
215
216
  const { name, version } = parseArgs();
216
217
  const entry = resolveTargetAssistant(name);
217
218
  const cloud = entry.cloud;
@@ -478,4 +479,7 @@ export async function rollback(): Promise<void> {
478
479
  emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
479
480
  process.exit(1);
480
481
  }
481
- }
482
+ },
483
+ { description: "Roll back an assistant to a previous version" },
484
+ );
485
+