@vellumai/cli 0.8.4 → 0.8.6

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 (43) hide show
  1. package/AGENTS.md +17 -1
  2. package/knip.json +2 -1
  3. package/package.json +1 -1
  4. package/src/__tests__/api-key-check.test.ts +78 -0
  5. package/src/__tests__/backup.test.ts +38 -0
  6. package/src/__tests__/recover.test.ts +307 -0
  7. package/src/__tests__/retire.test.ts +241 -0
  8. package/src/__tests__/wake.test.ts +215 -0
  9. package/src/commands/backup.ts +2 -0
  10. package/src/commands/client.ts +62 -32
  11. package/src/commands/flags.ts +197 -0
  12. package/src/commands/gateway/token.ts +73 -0
  13. package/src/commands/gateway.ts +29 -0
  14. package/src/commands/logs.ts +6 -18
  15. package/src/commands/ps.ts +41 -41
  16. package/src/commands/recover.ts +47 -9
  17. package/src/commands/restore.ts +8 -1
  18. package/src/commands/retire.ts +145 -55
  19. package/src/commands/roadmap.ts +449 -0
  20. package/src/commands/rollback.ts +2 -14
  21. package/src/commands/ssh.ts +5 -24
  22. package/src/commands/teleport.ts +34 -26
  23. package/src/commands/upgrade.ts +8 -16
  24. package/src/commands/wake.ts +68 -45
  25. package/src/index.ts +9 -0
  26. package/src/lib/__tests__/port-allocator.test.ts +117 -0
  27. package/src/lib/__tests__/step-runner.test.ts +133 -0
  28. package/src/lib/api-key-check.ts +40 -0
  29. package/src/lib/assistant-config.ts +13 -0
  30. package/src/lib/config-utils.ts +24 -3
  31. package/src/lib/docker.ts +72 -8
  32. package/src/lib/hatch-local.ts +15 -2
  33. package/src/lib/http-client.ts +1 -3
  34. package/src/lib/local.ts +173 -292
  35. package/src/lib/orphan-detection.ts +9 -5
  36. package/src/lib/pgrep.ts +5 -1
  37. package/src/lib/platform-client.ts +97 -49
  38. package/src/lib/port-allocator.ts +93 -0
  39. package/src/lib/process.ts +109 -39
  40. package/src/lib/statefulset.ts +0 -10
  41. package/src/lib/step-runner.ts +102 -9
  42. package/src/lib/sync-cloud-assistants.ts +17 -0
  43. package/src/shared/provider-env-vars.ts +1 -0
@@ -2,55 +2,45 @@ import { existsSync, unlinkSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
4
  import {
5
- findAssistantByName,
5
+ extractHostFromUrl,
6
+ formatAssistantLookupError,
7
+ formatAssistantReference,
8
+ getAssistantDisplayName,
6
9
  loadAllAssistants,
10
+ lookupAssistantByIdentifier,
7
11
  removeAssistantEntry,
8
- } from "../lib/assistant-config";
9
- import type { AssistantEntry } from "../lib/assistant-config";
10
- import { getConfigDir } from "../lib/environments/paths";
11
- import { getCurrentEnvironment } from "../lib/environments/resolve";
12
+ resolveCloud,
13
+ type AssistantEntry,
14
+ } from "../lib/assistant-config.js";
15
+ import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
16
+ import { getConfigDir } from "../lib/environments/paths.js";
17
+ import { getCurrentEnvironment } from "../lib/environments/resolve.js";
12
18
  import {
13
19
  authHeaders,
14
20
  getPlatformUrl,
15
21
  readPlatformToken,
16
- } from "../lib/platform-client";
17
- import { retireInstance as retireAwsInstance } from "../lib/aws";
18
- import { retireDocker } from "../lib/docker";
19
- import { retireInstance as retireGcpInstance } from "../lib/gcp";
20
- import { retireLocal } from "../lib/retire-local";
21
- import { retireAppleContainer } from "../lib/retire-apple-container";
22
- import { exec } from "../lib/step-runner";
22
+ } from "../lib/platform-client.js";
23
+ import { retireInstance as retireAwsInstance } from "../lib/aws.js";
24
+ import { retireDocker } from "../lib/docker.js";
25
+ import { retireInstance as retireGcpInstance } from "../lib/gcp.js";
26
+ import { retireLocal } from "../lib/retire-local.js";
27
+ import { retireAppleContainer } from "../lib/retire-apple-container.js";
28
+ import { exec } from "../lib/step-runner.js";
23
29
  import {
24
30
  openLogFile,
25
31
  closeLogFile,
26
32
  resetLogFile,
27
33
  writeToLogFile,
28
- } from "../lib/xdg-log";
34
+ } from "../lib/xdg-log.js";
29
35
 
30
- function resolveCloud(entry: AssistantEntry): string {
31
- if (entry.cloud) {
32
- return entry.cloud;
33
- }
34
- if (entry.project) {
35
- return "gcp";
36
- }
37
- if (entry.sshUser) {
38
- return "custom";
39
- }
40
- return "local";
41
- }
36
+ export { retireLocal };
42
37
 
43
- function extractHostFromUrl(url: string): string {
44
- try {
45
- const parsed = new URL(url);
46
- return parsed.hostname;
47
- } catch {
48
- return url.replace(/^https?:\/\//, "").split(":")[0];
49
- }
38
+ interface RetireArgs {
39
+ name?: string;
40
+ source?: string;
41
+ yes: boolean;
50
42
  }
51
43
 
52
- export { retireLocal };
53
-
54
44
  async function retireCustom(entry: AssistantEntry): Promise<void> {
55
45
  const host = extractHostFromUrl(entry.runtimeUrl);
56
46
  const sshUser = entry.sshUser ?? "root";
@@ -129,14 +119,82 @@ async function retireVellum(
129
119
  }
130
120
  }
131
121
 
132
- function parseSource(): string | undefined {
133
- const args = process.argv.slice(4);
122
+ function parseRetireArgs(args: string[]): RetireArgs {
123
+ let source: string | undefined;
134
124
  for (let i = 0; i < args.length; i++) {
135
125
  if (args[i] === "--source" && args[i + 1]) {
136
- return args[i + 1];
126
+ source = args[i + 1];
127
+ i++;
137
128
  }
138
129
  }
139
- return undefined;
130
+
131
+ return {
132
+ name: parseAssistantTargetArg(args, ["--source"]),
133
+ source,
134
+ yes: args.includes("--yes"),
135
+ };
136
+ }
137
+
138
+ function formatRuntimeUrl(entry: AssistantEntry): string {
139
+ return entry.localUrl ?? entry.runtimeUrl;
140
+ }
141
+
142
+ function printRetireTarget(entry: AssistantEntry, cloud: string): void {
143
+ const displayName = getAssistantDisplayName(entry);
144
+
145
+ console.log("Assistant to retire:");
146
+ if (displayName !== entry.assistantId) {
147
+ console.log(` Name: ${displayName}`);
148
+ }
149
+ console.log(` ID: ${entry.assistantId}`);
150
+ console.log(` Cloud: ${cloud}`);
151
+ console.log(` Runtime: ${formatRuntimeUrl(entry)}`);
152
+ console.log("");
153
+ }
154
+
155
+ function canPromptForRetireConfirmation(): boolean {
156
+ return (
157
+ process.stdin.isTTY === true &&
158
+ process.stdout.isTTY === true &&
159
+ typeof process.stdin.setRawMode === "function"
160
+ );
161
+ }
162
+
163
+ async function confirmRetireInteractive(): Promise<boolean> {
164
+ const stdin = process.stdin;
165
+ const stdout = process.stdout;
166
+ const wasRaw = stdin.isRaw === true;
167
+ const wasPaused = stdin.isPaused();
168
+
169
+ stdout.write("Press Enter to retire, or Esc/q to cancel: ");
170
+ stdin.setRawMode(true);
171
+ stdin.resume();
172
+
173
+ return await new Promise<boolean>((resolve) => {
174
+ const cleanup = () => {
175
+ stdin.off("data", onData);
176
+ stdin.setRawMode(wasRaw);
177
+ if (wasPaused) {
178
+ stdin.pause();
179
+ }
180
+ stdout.write("\n");
181
+ };
182
+
183
+ const onData = (chunk: Buffer) => {
184
+ const byte = chunk[0];
185
+ if (byte === 13 || byte === 10) {
186
+ cleanup();
187
+ resolve(true);
188
+ return;
189
+ }
190
+ if (byte === 27 || byte === 3 || byte === 113 || byte === 81) {
191
+ cleanup();
192
+ resolve(false);
193
+ }
194
+ };
195
+
196
+ stdin.on("data", onData);
197
+ });
140
198
  }
141
199
 
142
200
  /** Patch console methods to also append output to the given log file descriptor. */
@@ -188,38 +246,70 @@ export async function retire(): Promise<void> {
188
246
  async function retireInner(): Promise<void> {
189
247
  const args = process.argv.slice(3);
190
248
  if (args.includes("--help") || args.includes("-h")) {
191
- console.log("Usage: vellum retire <name> [--source <source>]");
249
+ console.log(
250
+ "Usage: vellum retire <name-or-id> [--source <source>] [--yes]",
251
+ );
192
252
  console.log("");
193
253
  console.log("Delete an assistant instance and archive its data.");
254
+ console.log(
255
+ "By default, retire prints the assistant name, ID, cloud, and runtime before asking for confirmation.",
256
+ );
194
257
  console.log("");
195
258
  console.log("Arguments:");
196
- console.log(" <name> Name of the assistant to retire");
259
+ console.log(
260
+ " <name-or-id> Assistant display name or ID to retire",
261
+ );
197
262
  console.log("");
198
263
  console.log("Options:");
199
264
  console.log(" --source <source> Source identifier for the retirement");
265
+ console.log(
266
+ " --yes Skip the interactive confirmation prompt",
267
+ );
200
268
  process.exit(0);
201
269
  }
202
270
 
203
- const name = process.argv[3];
271
+ const parsed = parseRetireArgs(args);
272
+ const name = parsed.name;
204
273
 
205
274
  if (!name) {
206
- console.error("Error: Instance name is required.");
207
- console.error("Usage: vellum retire <name> [--source <source>]");
275
+ console.error("Error: Assistant name or ID is required.");
276
+ console.error(
277
+ "Usage: vellum retire <name-or-id> [--source <source>] [--yes]",
278
+ );
208
279
  process.exit(1);
209
280
  }
210
281
 
211
- const entry = findAssistantByName(name);
212
- if (!entry) {
213
- console.error(`No assistant found with name '${name}'.`);
282
+ const lookup = lookupAssistantByIdentifier(name);
283
+ if (lookup.status !== "found") {
284
+ console.error(formatAssistantLookupError(name, lookup));
214
285
  console.error("Run 'vellum hatch' first, or check the instance name.");
215
286
  process.exit(1);
216
287
  }
217
288
 
218
- const source = parseSource();
289
+ const entry = lookup.entry;
290
+ const assistantId = entry.assistantId;
291
+ const source = parsed.source;
219
292
  const cloud = resolveCloud(entry);
293
+ printRetireTarget(entry, cloud);
294
+
295
+ if (!parsed.yes) {
296
+ if (!canPromptForRetireConfirmation()) {
297
+ console.error(
298
+ "Error: Refusing to retire without confirmation in a non-interactive terminal.",
299
+ );
300
+ console.error("Re-run with --yes to confirm from automation.");
301
+ process.exit(1);
302
+ }
303
+
304
+ const confirmed = await confirmRetireInteractive();
305
+ if (!confirmed) {
306
+ console.log("Retire cancelled.");
307
+ process.exit(1);
308
+ }
309
+ }
220
310
 
221
311
  if (cloud === "apple-container") {
222
- await retireAppleContainer(name, entry);
312
+ await retireAppleContainer(assistantId, entry);
223
313
  } else if (cloud === "gcp") {
224
314
  const project = entry.project;
225
315
  const zone = entry.zone;
@@ -229,29 +319,29 @@ async function retireInner(): Promise<void> {
229
319
  );
230
320
  process.exit(1);
231
321
  }
232
- await retireGcpInstance(name, project, zone, source);
322
+ await retireGcpInstance(assistantId, project, zone, source);
233
323
  } else if (cloud === "aws") {
234
324
  const region = entry.region;
235
325
  if (!region) {
236
326
  console.error("Error: AWS region not found in assistant config.");
237
327
  process.exit(1);
238
328
  }
239
- await retireAwsInstance(name, region, source);
329
+ await retireAwsInstance(assistantId, region, source);
240
330
  } else if (cloud === "docker") {
241
- await retireDocker(name);
331
+ await retireDocker(assistantId);
242
332
  } else if (cloud === "local") {
243
- await retireLocal(name, entry);
333
+ await retireLocal(assistantId, entry);
244
334
  } else if (cloud === "custom") {
245
335
  await retireCustom(entry);
246
336
  } else if (cloud === "vellum") {
247
- await retireVellum(entry.assistantId, entry.runtimeUrl);
337
+ await retireVellum(assistantId, entry.runtimeUrl);
248
338
  } else {
249
339
  console.error(`Error: Unknown cloud type '${cloud}'.`);
250
340
  process.exit(1);
251
341
  }
252
342
 
253
- removeAssistantEntry(name);
254
- console.log(`Removed ${name} from config.`);
343
+ removeAssistantEntry(assistantId);
344
+ console.log(`Removed ${formatAssistantReference(entry)} from config.`);
255
345
 
256
346
  // When no assistants remain, remove the dock-display-name sentinel so
257
347
  // the next build.sh run falls back to "Vellum" instead of using the