@vellumai/cli 0.8.3 → 0.8.5
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/AGENTS.md +29 -7
- package/package.json +1 -1
- package/src/__tests__/api-key-check.test.ts +78 -0
- package/src/__tests__/assistant-config.test.ts +108 -0
- package/src/__tests__/assistant-target-args.test.ts +30 -0
- package/src/__tests__/host-image-loader.test.ts +206 -0
- package/src/__tests__/ps-platform-status.test.ts +100 -22
- package/src/__tests__/retire.test.ts +241 -0
- package/src/__tests__/use.test.ts +144 -0
- package/src/commands/client.ts +27 -24
- package/src/commands/ps.ts +107 -105
- package/src/commands/retire.ts +144 -34
- package/src/commands/roadmap.ts +449 -0
- package/src/commands/use.ts +24 -10
- package/src/components/DefaultMainScreen.tsx +27 -115
- package/src/index.ts +3 -0
- package/src/lib/__tests__/port-allocator.test.ts +117 -0
- package/src/lib/__tests__/step-runner.test.ts +85 -0
- package/src/lib/api-key-check.ts +40 -0
- package/src/lib/assistant-config.ts +84 -5
- package/src/lib/assistant-target-args.ts +21 -0
- package/src/lib/docker.ts +67 -16
- package/src/lib/hatch-local.ts +11 -0
- package/src/lib/host-image-loader.ts +138 -0
- package/src/lib/platform-releases.ts +12 -5
- package/src/lib/port-allocator.ts +93 -0
- package/src/lib/statefulset.ts +0 -10
- package/src/lib/step-runner.ts +40 -7
- package/src/shared/provider-env-vars.ts +1 -0
package/src/commands/client.ts
CHANGED
|
@@ -4,9 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
findAssistantByName,
|
|
7
|
+
formatAssistantLookupError,
|
|
7
8
|
getActiveAssistant,
|
|
9
|
+
lookupAssistantByIdentifier,
|
|
8
10
|
resolveAssistant,
|
|
9
11
|
saveAssistantEntry,
|
|
12
|
+
type AssistantEntry,
|
|
10
13
|
} from "../lib/assistant-config";
|
|
11
14
|
import {
|
|
12
15
|
DAEMON_INTERNAL_ASSISTANT_ID,
|
|
@@ -20,6 +23,7 @@ import {
|
|
|
20
23
|
WEB_INTERFACE_ID,
|
|
21
24
|
getClientRegistrationHeaders,
|
|
22
25
|
} from "../lib/client-identity";
|
|
26
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
23
27
|
import {
|
|
24
28
|
fetchOrganizationId,
|
|
25
29
|
fetchPlatformAssistants,
|
|
@@ -51,13 +55,9 @@ interface ParsedArgs {
|
|
|
51
55
|
bearerToken?: string;
|
|
52
56
|
/** Interface identifier sent as X-Vellum-Interface-Id on all requests. */
|
|
53
57
|
interfaceId: SupportedInterface;
|
|
54
|
-
project?: string;
|
|
55
|
-
zone?: string;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
function readAssistantName(
|
|
59
|
-
entry: ReturnType<typeof findAssistantByName>,
|
|
60
|
-
): string | undefined {
|
|
60
|
+
function readAssistantName(entry: AssistantEntry | null): string | undefined {
|
|
61
61
|
const rawName = entry?.name ?? entry?.assistantName;
|
|
62
62
|
return typeof rawName === "string" && rawName.trim()
|
|
63
63
|
? rawName.trim()
|
|
@@ -67,7 +67,14 @@ function readAssistantName(
|
|
|
67
67
|
function parseArgs(): ParsedArgs {
|
|
68
68
|
const args = process.argv.slice(3);
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const positionalName = parseAssistantTargetArg(args, [
|
|
71
|
+
"--url",
|
|
72
|
+
"-u",
|
|
73
|
+
"--assistant-id",
|
|
74
|
+
"-a",
|
|
75
|
+
"--interface",
|
|
76
|
+
"-i",
|
|
77
|
+
]);
|
|
71
78
|
const flagArgs: string[] = [];
|
|
72
79
|
for (let i = 0; i < args.length; i++) {
|
|
73
80
|
const arg = args[i];
|
|
@@ -84,29 +91,29 @@ function parseArgs(): ParsedArgs {
|
|
|
84
91
|
args[i + 1]
|
|
85
92
|
) {
|
|
86
93
|
flagArgs.push(arg, args[++i]);
|
|
87
|
-
} else if (!arg.startsWith("-") && positionalName === undefined) {
|
|
88
|
-
positionalName = arg;
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
let entry:
|
|
97
|
+
let entry: AssistantEntry | null = null;
|
|
93
98
|
if (positionalName) {
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
console.error(
|
|
97
|
-
`No assistant instance found with name '${positionalName}'.`,
|
|
98
|
-
);
|
|
99
|
+
const result = lookupAssistantByIdentifier(positionalName);
|
|
100
|
+
if (result.status !== "found") {
|
|
101
|
+
console.error(formatAssistantLookupError(positionalName, result));
|
|
99
102
|
process.exit(1);
|
|
100
103
|
}
|
|
104
|
+
entry = result.entry;
|
|
101
105
|
} else {
|
|
102
106
|
const hasExplicitUrl =
|
|
103
107
|
flagArgs.includes("--url") || flagArgs.includes("-u");
|
|
104
108
|
const active = getActiveAssistant();
|
|
105
109
|
if (active) {
|
|
106
|
-
|
|
110
|
+
const result = lookupAssistantByIdentifier(active);
|
|
111
|
+
if (result.status === "found") {
|
|
112
|
+
entry = result.entry;
|
|
113
|
+
}
|
|
107
114
|
if (!entry && !hasExplicitUrl) {
|
|
108
115
|
console.error(
|
|
109
|
-
`Active assistant '${active}' not found in lockfile. Set an active assistant with 'vellum use <name>'.`,
|
|
116
|
+
`Active assistant '${active}' not found in lockfile. Set an active assistant with 'vellum use <name-or-id>'.`,
|
|
110
117
|
);
|
|
111
118
|
process.exit(1);
|
|
112
119
|
}
|
|
@@ -116,7 +123,7 @@ function parseArgs(): ParsedArgs {
|
|
|
116
123
|
entry = resolveAssistant();
|
|
117
124
|
} else if (!entry) {
|
|
118
125
|
console.error(
|
|
119
|
-
"No active assistant set. Set one with 'vellum use <name>' or specify
|
|
126
|
+
"No active assistant set. Set one with 'vellum use <name-or-id>' or specify one: 'vellum client <name-or-id>'.",
|
|
120
127
|
);
|
|
121
128
|
process.exit(1);
|
|
122
129
|
}
|
|
@@ -169,8 +176,6 @@ function parseArgs(): ParsedArgs {
|
|
|
169
176
|
platformToken,
|
|
170
177
|
bearerToken,
|
|
171
178
|
interfaceId,
|
|
172
|
-
project: entry?.project,
|
|
173
|
-
zone: entry?.zone,
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
181
|
|
|
@@ -217,10 +222,10 @@ function printUsage(): void {
|
|
|
217
222
|
console.log(`${ANSI.bold}vellum client${ANSI.reset} - Connect to a hatched assistant
|
|
218
223
|
|
|
219
224
|
${ANSI.bold}USAGE:${ANSI.reset}
|
|
220
|
-
vellum client [name] [options]
|
|
225
|
+
vellum client [name-or-id] [options]
|
|
221
226
|
|
|
222
227
|
${ANSI.bold}ARGUMENTS:${ANSI.reset}
|
|
223
|
-
[name]
|
|
228
|
+
[name-or-id] Assistant display name or ID (default: active)
|
|
224
229
|
|
|
225
230
|
${ANSI.bold}OPTIONS:${ANSI.reset}
|
|
226
231
|
-u, --url <url> Runtime URL
|
|
@@ -343,8 +348,6 @@ export async function client(): Promise<void> {
|
|
|
343
348
|
platformToken,
|
|
344
349
|
bearerToken,
|
|
345
350
|
interfaceId,
|
|
346
|
-
project,
|
|
347
|
-
zone,
|
|
348
351
|
} = parseArgs();
|
|
349
352
|
|
|
350
353
|
if (interfaceId === WEB_INTERFACE_ID) {
|
|
@@ -404,6 +407,6 @@ export async function client(): Promise<void> {
|
|
|
404
407
|
console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
|
|
405
408
|
process.exit(0);
|
|
406
409
|
},
|
|
407
|
-
{ auth,
|
|
410
|
+
{ auth, assistantName },
|
|
408
411
|
);
|
|
409
412
|
}
|
package/src/commands/ps.ts
CHANGED
|
@@ -2,11 +2,16 @@ import { join } from "path";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
findAssistantByName,
|
|
5
|
+
formatAssistantLookupError,
|
|
6
|
+
formatAssistantReference,
|
|
5
7
|
getActiveAssistant,
|
|
8
|
+
getAssistantDisplayName,
|
|
6
9
|
getDaemonPidPath,
|
|
7
10
|
loadAllAssistants,
|
|
11
|
+
lookupAssistantByIdentifier,
|
|
8
12
|
type AssistantEntry,
|
|
9
13
|
} from "../lib/assistant-config";
|
|
14
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
10
15
|
import { resolveEnvironmentSource } from "../lib/environments/resolve";
|
|
11
16
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
12
17
|
import {
|
|
@@ -52,8 +57,10 @@ function pad(s: string, w: number): string {
|
|
|
52
57
|
return s + " ".repeat(Math.max(0, w - s.length));
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
function computeColWidths(
|
|
56
|
-
|
|
60
|
+
function computeColWidths(
|
|
61
|
+
rows: TableRow[],
|
|
62
|
+
headers: TableRow = { name: "NAME", status: "STATUS", info: "INFO" },
|
|
63
|
+
): ColWidths {
|
|
57
64
|
const all = [headers, ...rows];
|
|
58
65
|
return {
|
|
59
66
|
name: Math.max(...all.map((r) => r.name.length)),
|
|
@@ -66,9 +73,11 @@ function formatRow(r: TableRow, colWidths: ColWidths): string {
|
|
|
66
73
|
return ` ${pad(r.name, colWidths.name)} ${pad(r.status, colWidths.status)} ${r.info}`;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
function printTable(
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
function printTable(
|
|
77
|
+
rows: TableRow[],
|
|
78
|
+
headers: TableRow = { name: "PROCESS", status: "STATUS", info: "INFO" },
|
|
79
|
+
): void {
|
|
80
|
+
const colWidths = computeColWidths(rows, headers);
|
|
72
81
|
console.log(formatRow(headers, colWidths));
|
|
73
82
|
const sep = ` ${"-".repeat(colWidths.name)} ${"-".repeat(colWidths.status)} ${"-".repeat(colWidths.info)}`;
|
|
74
83
|
console.log(sep);
|
|
@@ -377,7 +386,7 @@ async function getDockerProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
|
377
386
|
async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
378
387
|
const cloud = resolveCloud(entry);
|
|
379
388
|
|
|
380
|
-
console.log(`Processes for ${entry
|
|
389
|
+
console.log(`Processes for ${formatAssistantReference(entry)} (${cloud}):\n`);
|
|
381
390
|
|
|
382
391
|
if (cloud === "local") {
|
|
383
392
|
const rows = await getLocalProcesses(entry);
|
|
@@ -473,6 +482,75 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
473
482
|
|
|
474
483
|
// ── List all assistants (no arg) ────────────────────────────────
|
|
475
484
|
|
|
485
|
+
type AssistantHealth = {
|
|
486
|
+
status: string;
|
|
487
|
+
detail: string | null;
|
|
488
|
+
version?: string;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
async function getAssistantListHealth(
|
|
492
|
+
entry: AssistantEntry,
|
|
493
|
+
): Promise<AssistantHealth> {
|
|
494
|
+
const resources = entry.resources;
|
|
495
|
+
if (entry.cloud === "local" && resources) {
|
|
496
|
+
// TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
|
|
497
|
+
// fetching daemon PIDs via the health API (Gateway Security Migration).
|
|
498
|
+
const pid = readPidFile(getDaemonPidPath(resources));
|
|
499
|
+
const alive = pid !== null && isProcessAlive(pid);
|
|
500
|
+
if (!alive) {
|
|
501
|
+
return { status: "sleeping", detail: null };
|
|
502
|
+
}
|
|
503
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
504
|
+
return checkHealth(entry.localUrl ?? entry.runtimeUrl, token);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (entry.cloud === "docker") {
|
|
508
|
+
const res = dockerResourceNames(entry.assistantId);
|
|
509
|
+
const state = await getDockerContainerState(res.assistantContainer);
|
|
510
|
+
if (!state || state !== "running") {
|
|
511
|
+
return { status: "sleeping", detail: null };
|
|
512
|
+
}
|
|
513
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
514
|
+
return checkHealth(entry.localUrl ?? entry.runtimeUrl, token);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (entry.cloud === "apple-container") {
|
|
518
|
+
// Apple containers are managed by the macOS app. Probe the gateway
|
|
519
|
+
// (runtimeUrl is always written to the lockfile during hatch).
|
|
520
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
521
|
+
return entry.runtimeUrl
|
|
522
|
+
? checkHealth(entry.runtimeUrl, token)
|
|
523
|
+
: { status: "unknown", detail: "no runtime URL" };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (entry.cloud === "vellum") {
|
|
527
|
+
return checkManagedHealth(entry.runtimeUrl, entry.assistantId);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
531
|
+
return checkHealth(entry.localUrl ?? entry.runtimeUrl, token);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function formatAssistantListRow(
|
|
535
|
+
entry: AssistantEntry,
|
|
536
|
+
activeAssistantId: string | null,
|
|
537
|
+
health: AssistantHealth,
|
|
538
|
+
): TableRow {
|
|
539
|
+
const infoParts: string[] = [];
|
|
540
|
+
infoParts.push(`id: ${entry.assistantId}`);
|
|
541
|
+
if (entry.runtimeUrl) infoParts.push(entry.runtimeUrl);
|
|
542
|
+
if (entry.cloud) infoParts.push(`cloud: ${entry.cloud}`);
|
|
543
|
+
if (entry.species) infoParts.push(`species: ${entry.species}`);
|
|
544
|
+
if (health.detail) infoParts.push(health.detail);
|
|
545
|
+
|
|
546
|
+
const prefix = entry.assistantId === activeAssistantId ? "* " : " ";
|
|
547
|
+
return {
|
|
548
|
+
name: prefix + getAssistantDisplayName(entry),
|
|
549
|
+
status: withStatusEmoji(health.status),
|
|
550
|
+
info: infoParts.join(" | "),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
476
554
|
export async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
477
555
|
const { name: envName, source: envSource } = resolveEnvironmentSource();
|
|
478
556
|
const sourceLabels: Record<typeof envSource, string> = {
|
|
@@ -519,6 +597,9 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
|
519
597
|
|
|
520
598
|
const assistants = loadAllAssistants();
|
|
521
599
|
const activeId = getActiveAssistant();
|
|
600
|
+
const activeAssistantId = activeId
|
|
601
|
+
? (findAssistantByName(activeId)?.assistantId ?? activeId)
|
|
602
|
+
: null;
|
|
522
603
|
|
|
523
604
|
if (assistants.length === 0) {
|
|
524
605
|
console.log("No assistants found.");
|
|
@@ -540,97 +621,17 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
|
540
621
|
return;
|
|
541
622
|
}
|
|
542
623
|
|
|
543
|
-
const rows
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
name: prefix + a.assistantId,
|
|
552
|
-
status: withStatusEmoji("checking..."),
|
|
553
|
-
info: infoParts.join(" | "),
|
|
554
|
-
};
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
const colWidths = computeColWidths(rows);
|
|
558
|
-
|
|
559
|
-
const headers: TableRow = { name: "NAME", status: "STATUS", info: "INFO" };
|
|
560
|
-
console.log(formatRow(headers, colWidths));
|
|
561
|
-
const sep = ` ${"-".repeat(colWidths.name)} ${"-".repeat(colWidths.status)} ${"-".repeat(colWidths.info)}`;
|
|
562
|
-
console.log(sep);
|
|
563
|
-
for (const row of rows) {
|
|
564
|
-
console.log(formatRow(row, colWidths));
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const totalDataRows = rows.length;
|
|
568
|
-
|
|
569
|
-
await Promise.all(
|
|
570
|
-
assistants.map(async (a, rowIndex) => {
|
|
571
|
-
// For local assistants, check if the daemon process is alive before
|
|
572
|
-
// hitting the health endpoint. If the PID file is missing or the
|
|
573
|
-
// process isn't running, the assistant is sleeping — skip the
|
|
574
|
-
// network health check to avoid a misleading "unreachable" status.
|
|
575
|
-
let health: { status: string; detail: string | null; version?: string };
|
|
576
|
-
const resources = a.resources;
|
|
577
|
-
if (a.cloud === "local" && resources) {
|
|
578
|
-
// TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
|
|
579
|
-
// fetching daemon PIDs via the health API (Gateway Security Migration).
|
|
580
|
-
const pid = readPidFile(getDaemonPidPath(resources));
|
|
581
|
-
const alive = pid !== null && isProcessAlive(pid);
|
|
582
|
-
if (!alive) {
|
|
583
|
-
health = { status: "sleeping", detail: null };
|
|
584
|
-
} else {
|
|
585
|
-
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
586
|
-
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
587
|
-
}
|
|
588
|
-
} else if (a.cloud === "docker") {
|
|
589
|
-
const res = dockerResourceNames(a.assistantId);
|
|
590
|
-
const state = await getDockerContainerState(res.assistantContainer);
|
|
591
|
-
if (!state || state !== "running") {
|
|
592
|
-
health = { status: "sleeping", detail: null };
|
|
593
|
-
} else {
|
|
594
|
-
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
595
|
-
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
596
|
-
}
|
|
597
|
-
} else if (a.cloud === "apple-container") {
|
|
598
|
-
// Apple containers are managed by the macOS app. Probe the gateway
|
|
599
|
-
// (runtimeUrl is always written to the lockfile during hatch).
|
|
600
|
-
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
601
|
-
health = a.runtimeUrl
|
|
602
|
-
? await checkHealth(a.runtimeUrl, token)
|
|
603
|
-
: { status: "unknown" as const, detail: "no runtime URL" };
|
|
604
|
-
} else if (a.cloud === "vellum") {
|
|
605
|
-
health = await checkManagedHealth(a.runtimeUrl, a.assistantId);
|
|
606
|
-
} else {
|
|
607
|
-
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
608
|
-
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const infoParts: string[] = [];
|
|
612
|
-
if (a.runtimeUrl) infoParts.push(a.runtimeUrl);
|
|
613
|
-
if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
|
|
614
|
-
if (a.species) infoParts.push(`species: ${a.species}`);
|
|
615
|
-
if (health.detail) infoParts.push(health.detail);
|
|
616
|
-
|
|
617
|
-
const prefix = a.assistantId === activeId ? "* " : " ";
|
|
618
|
-
const updatedRow: TableRow = {
|
|
619
|
-
name: prefix + a.assistantId,
|
|
620
|
-
status: withStatusEmoji(health.status),
|
|
621
|
-
info: infoParts.join(" | "),
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
const linesUp = totalDataRows - rowIndex;
|
|
625
|
-
process.stdout.write(
|
|
626
|
-
`\x1b[${linesUp}A` +
|
|
627
|
-
`\r\x1b[K` +
|
|
628
|
-
formatRow(updatedRow, colWidths) +
|
|
629
|
-
`\n` +
|
|
630
|
-
(linesUp > 1 ? `\x1b[${linesUp - 1}B` : ""),
|
|
631
|
-
);
|
|
632
|
-
}),
|
|
624
|
+
const rows = await Promise.all(
|
|
625
|
+
assistants.map(async (entry) =>
|
|
626
|
+
formatAssistantListRow(
|
|
627
|
+
entry,
|
|
628
|
+
activeAssistantId,
|
|
629
|
+
await getAssistantListHealth(entry),
|
|
630
|
+
),
|
|
631
|
+
),
|
|
633
632
|
);
|
|
633
|
+
|
|
634
|
+
printTable(rows, { name: "NAME", status: "STATUS", info: "INFO" });
|
|
634
635
|
}
|
|
635
636
|
|
|
636
637
|
// ── Entry point ─────────────────────────────────────────────────
|
|
@@ -638,14 +639,16 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
|
638
639
|
export async function ps(): Promise<void> {
|
|
639
640
|
const args = process.argv.slice(3);
|
|
640
641
|
if (args.includes("--help") || args.includes("-h")) {
|
|
641
|
-
console.log("Usage: vellum ps [<name>] [--verbose]");
|
|
642
|
+
console.log("Usage: vellum ps [<name-or-id>] [--verbose]");
|
|
642
643
|
console.log("");
|
|
643
644
|
console.log(
|
|
644
645
|
"List all assistants, or show processes for a specific assistant.",
|
|
645
646
|
);
|
|
646
647
|
console.log("");
|
|
647
648
|
console.log("Arguments:");
|
|
648
|
-
console.log(
|
|
649
|
+
console.log(
|
|
650
|
+
" <name-or-id> Show processes for the assistant display name or ID",
|
|
651
|
+
);
|
|
649
652
|
console.log("");
|
|
650
653
|
console.log("Options:");
|
|
651
654
|
console.log(
|
|
@@ -655,19 +658,18 @@ export async function ps(): Promise<void> {
|
|
|
655
658
|
}
|
|
656
659
|
|
|
657
660
|
const verbose = args.includes("--verbose");
|
|
658
|
-
const
|
|
659
|
-
const assistantId = positional[0];
|
|
661
|
+
const assistantIdentifier = parseAssistantTargetArg(args);
|
|
660
662
|
|
|
661
|
-
if (!
|
|
663
|
+
if (!assistantIdentifier) {
|
|
662
664
|
await listAllAssistants(verbose);
|
|
663
665
|
return;
|
|
664
666
|
}
|
|
665
667
|
|
|
666
|
-
const
|
|
667
|
-
if (
|
|
668
|
-
console.error(
|
|
668
|
+
const result = lookupAssistantByIdentifier(assistantIdentifier);
|
|
669
|
+
if (result.status !== "found") {
|
|
670
|
+
console.error(formatAssistantLookupError(assistantIdentifier, result));
|
|
669
671
|
process.exit(1);
|
|
670
672
|
}
|
|
671
673
|
|
|
672
|
-
await showAssistantProcesses(entry);
|
|
674
|
+
await showAssistantProcesses(result.entry);
|
|
673
675
|
}
|
package/src/commands/retire.ts
CHANGED
|
@@ -2,30 +2,34 @@ import { existsSync, unlinkSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
formatAssistantLookupError,
|
|
6
|
+
formatAssistantReference,
|
|
7
|
+
getAssistantDisplayName,
|
|
6
8
|
loadAllAssistants,
|
|
9
|
+
lookupAssistantByIdentifier,
|
|
7
10
|
removeAssistantEntry,
|
|
8
|
-
} from "../lib/assistant-config";
|
|
9
|
-
import type { AssistantEntry } from "../lib/assistant-config";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
} from "../lib/assistant-config.js";
|
|
12
|
+
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
13
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
14
|
+
import { getConfigDir } from "../lib/environments/paths.js";
|
|
15
|
+
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
12
16
|
import {
|
|
13
17
|
authHeaders,
|
|
14
18
|
getPlatformUrl,
|
|
15
19
|
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";
|
|
20
|
+
} from "../lib/platform-client.js";
|
|
21
|
+
import { retireInstance as retireAwsInstance } from "../lib/aws.js";
|
|
22
|
+
import { retireDocker } from "../lib/docker.js";
|
|
23
|
+
import { retireInstance as retireGcpInstance } from "../lib/gcp.js";
|
|
24
|
+
import { retireLocal } from "../lib/retire-local.js";
|
|
25
|
+
import { retireAppleContainer } from "../lib/retire-apple-container.js";
|
|
26
|
+
import { exec } from "../lib/step-runner.js";
|
|
23
27
|
import {
|
|
24
28
|
openLogFile,
|
|
25
29
|
closeLogFile,
|
|
26
30
|
resetLogFile,
|
|
27
31
|
writeToLogFile,
|
|
28
|
-
} from "../lib/xdg-log";
|
|
32
|
+
} from "../lib/xdg-log.js";
|
|
29
33
|
|
|
30
34
|
function resolveCloud(entry: AssistantEntry): string {
|
|
31
35
|
if (entry.cloud) {
|
|
@@ -51,6 +55,12 @@ function extractHostFromUrl(url: string): string {
|
|
|
51
55
|
|
|
52
56
|
export { retireLocal };
|
|
53
57
|
|
|
58
|
+
interface RetireArgs {
|
|
59
|
+
name?: string;
|
|
60
|
+
source?: string;
|
|
61
|
+
yes: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
async function retireCustom(entry: AssistantEntry): Promise<void> {
|
|
55
65
|
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
56
66
|
const sshUser = entry.sshUser ?? "root";
|
|
@@ -129,14 +139,82 @@ async function retireVellum(
|
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
|
|
132
|
-
function
|
|
133
|
-
|
|
142
|
+
function parseRetireArgs(args: string[]): RetireArgs {
|
|
143
|
+
let source: string | undefined;
|
|
134
144
|
for (let i = 0; i < args.length; i++) {
|
|
135
145
|
if (args[i] === "--source" && args[i + 1]) {
|
|
136
|
-
|
|
146
|
+
source = args[i + 1];
|
|
147
|
+
i++;
|
|
137
148
|
}
|
|
138
149
|
}
|
|
139
|
-
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
name: parseAssistantTargetArg(args, ["--source"]),
|
|
153
|
+
source,
|
|
154
|
+
yes: args.includes("--yes"),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatRuntimeUrl(entry: AssistantEntry): string {
|
|
159
|
+
return entry.localUrl ?? entry.runtimeUrl;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function printRetireTarget(entry: AssistantEntry, cloud: string): void {
|
|
163
|
+
const displayName = getAssistantDisplayName(entry);
|
|
164
|
+
|
|
165
|
+
console.log("Assistant to retire:");
|
|
166
|
+
if (displayName !== entry.assistantId) {
|
|
167
|
+
console.log(` Name: ${displayName}`);
|
|
168
|
+
}
|
|
169
|
+
console.log(` ID: ${entry.assistantId}`);
|
|
170
|
+
console.log(` Cloud: ${cloud}`);
|
|
171
|
+
console.log(` Runtime: ${formatRuntimeUrl(entry)}`);
|
|
172
|
+
console.log("");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function canPromptForRetireConfirmation(): boolean {
|
|
176
|
+
return (
|
|
177
|
+
process.stdin.isTTY === true &&
|
|
178
|
+
process.stdout.isTTY === true &&
|
|
179
|
+
typeof process.stdin.setRawMode === "function"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function confirmRetireInteractive(): Promise<boolean> {
|
|
184
|
+
const stdin = process.stdin;
|
|
185
|
+
const stdout = process.stdout;
|
|
186
|
+
const wasRaw = stdin.isRaw === true;
|
|
187
|
+
const wasPaused = stdin.isPaused();
|
|
188
|
+
|
|
189
|
+
stdout.write("Press Enter to retire, or Esc/q to cancel: ");
|
|
190
|
+
stdin.setRawMode(true);
|
|
191
|
+
stdin.resume();
|
|
192
|
+
|
|
193
|
+
return await new Promise<boolean>((resolve) => {
|
|
194
|
+
const cleanup = () => {
|
|
195
|
+
stdin.off("data", onData);
|
|
196
|
+
stdin.setRawMode(wasRaw);
|
|
197
|
+
if (wasPaused) {
|
|
198
|
+
stdin.pause();
|
|
199
|
+
}
|
|
200
|
+
stdout.write("\n");
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const onData = (chunk: Buffer) => {
|
|
204
|
+
const byte = chunk[0];
|
|
205
|
+
if (byte === 13 || byte === 10) {
|
|
206
|
+
cleanup();
|
|
207
|
+
resolve(true);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (byte === 27 || byte === 3 || byte === 113 || byte === 81) {
|
|
211
|
+
cleanup();
|
|
212
|
+
resolve(false);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
stdin.on("data", onData);
|
|
217
|
+
});
|
|
140
218
|
}
|
|
141
219
|
|
|
142
220
|
/** Patch console methods to also append output to the given log file descriptor. */
|
|
@@ -188,38 +266,70 @@ export async function retire(): Promise<void> {
|
|
|
188
266
|
async function retireInner(): Promise<void> {
|
|
189
267
|
const args = process.argv.slice(3);
|
|
190
268
|
if (args.includes("--help") || args.includes("-h")) {
|
|
191
|
-
console.log(
|
|
269
|
+
console.log(
|
|
270
|
+
"Usage: vellum retire <name-or-id> [--source <source>] [--yes]",
|
|
271
|
+
);
|
|
192
272
|
console.log("");
|
|
193
273
|
console.log("Delete an assistant instance and archive its data.");
|
|
274
|
+
console.log(
|
|
275
|
+
"By default, retire prints the assistant name, ID, cloud, and runtime before asking for confirmation.",
|
|
276
|
+
);
|
|
194
277
|
console.log("");
|
|
195
278
|
console.log("Arguments:");
|
|
196
|
-
console.log(
|
|
279
|
+
console.log(
|
|
280
|
+
" <name-or-id> Assistant display name or ID to retire",
|
|
281
|
+
);
|
|
197
282
|
console.log("");
|
|
198
283
|
console.log("Options:");
|
|
199
284
|
console.log(" --source <source> Source identifier for the retirement");
|
|
285
|
+
console.log(
|
|
286
|
+
" --yes Skip the interactive confirmation prompt",
|
|
287
|
+
);
|
|
200
288
|
process.exit(0);
|
|
201
289
|
}
|
|
202
290
|
|
|
203
|
-
const
|
|
291
|
+
const parsed = parseRetireArgs(args);
|
|
292
|
+
const name = parsed.name;
|
|
204
293
|
|
|
205
294
|
if (!name) {
|
|
206
|
-
console.error("Error:
|
|
207
|
-
console.error(
|
|
295
|
+
console.error("Error: Assistant name or ID is required.");
|
|
296
|
+
console.error(
|
|
297
|
+
"Usage: vellum retire <name-or-id> [--source <source>] [--yes]",
|
|
298
|
+
);
|
|
208
299
|
process.exit(1);
|
|
209
300
|
}
|
|
210
301
|
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
console.error(
|
|
302
|
+
const lookup = lookupAssistantByIdentifier(name);
|
|
303
|
+
if (lookup.status !== "found") {
|
|
304
|
+
console.error(formatAssistantLookupError(name, lookup));
|
|
214
305
|
console.error("Run 'vellum hatch' first, or check the instance name.");
|
|
215
306
|
process.exit(1);
|
|
216
307
|
}
|
|
217
308
|
|
|
218
|
-
const
|
|
309
|
+
const entry = lookup.entry;
|
|
310
|
+
const assistantId = entry.assistantId;
|
|
311
|
+
const source = parsed.source;
|
|
219
312
|
const cloud = resolveCloud(entry);
|
|
313
|
+
printRetireTarget(entry, cloud);
|
|
314
|
+
|
|
315
|
+
if (!parsed.yes) {
|
|
316
|
+
if (!canPromptForRetireConfirmation()) {
|
|
317
|
+
console.error(
|
|
318
|
+
"Error: Refusing to retire without confirmation in a non-interactive terminal.",
|
|
319
|
+
);
|
|
320
|
+
console.error("Re-run with --yes to confirm from automation.");
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const confirmed = await confirmRetireInteractive();
|
|
325
|
+
if (!confirmed) {
|
|
326
|
+
console.log("Retire cancelled.");
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
220
330
|
|
|
221
331
|
if (cloud === "apple-container") {
|
|
222
|
-
await retireAppleContainer(
|
|
332
|
+
await retireAppleContainer(assistantId, entry);
|
|
223
333
|
} else if (cloud === "gcp") {
|
|
224
334
|
const project = entry.project;
|
|
225
335
|
const zone = entry.zone;
|
|
@@ -229,29 +339,29 @@ async function retireInner(): Promise<void> {
|
|
|
229
339
|
);
|
|
230
340
|
process.exit(1);
|
|
231
341
|
}
|
|
232
|
-
await retireGcpInstance(
|
|
342
|
+
await retireGcpInstance(assistantId, project, zone, source);
|
|
233
343
|
} else if (cloud === "aws") {
|
|
234
344
|
const region = entry.region;
|
|
235
345
|
if (!region) {
|
|
236
346
|
console.error("Error: AWS region not found in assistant config.");
|
|
237
347
|
process.exit(1);
|
|
238
348
|
}
|
|
239
|
-
await retireAwsInstance(
|
|
349
|
+
await retireAwsInstance(assistantId, region, source);
|
|
240
350
|
} else if (cloud === "docker") {
|
|
241
|
-
await retireDocker(
|
|
351
|
+
await retireDocker(assistantId);
|
|
242
352
|
} else if (cloud === "local") {
|
|
243
|
-
await retireLocal(
|
|
353
|
+
await retireLocal(assistantId, entry);
|
|
244
354
|
} else if (cloud === "custom") {
|
|
245
355
|
await retireCustom(entry);
|
|
246
356
|
} else if (cloud === "vellum") {
|
|
247
|
-
await retireVellum(
|
|
357
|
+
await retireVellum(assistantId, entry.runtimeUrl);
|
|
248
358
|
} else {
|
|
249
359
|
console.error(`Error: Unknown cloud type '${cloud}'.`);
|
|
250
360
|
process.exit(1);
|
|
251
361
|
}
|
|
252
362
|
|
|
253
|
-
removeAssistantEntry(
|
|
254
|
-
console.log(`Removed ${
|
|
363
|
+
removeAssistantEntry(assistantId);
|
|
364
|
+
console.log(`Removed ${formatAssistantReference(entry)} from config.`);
|
|
255
365
|
|
|
256
366
|
// When no assistants remain, remove the dock-display-name sentinel so
|
|
257
367
|
// the next build.sh run falls back to "Vellum" instead of using the
|