@vellumai/cli 0.8.4 → 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 +11 -1
- package/package.json +1 -1
- package/src/__tests__/api-key-check.test.ts +78 -0
- package/src/__tests__/retire.test.ts +241 -0
- package/src/commands/retire.ts +144 -34
- package/src/commands/roadmap.ts +449 -0
- 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/docker.ts +22 -8
- package/src/lib/hatch-local.ts +11 -0
- 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/AGENTS.md
CHANGED
|
@@ -16,7 +16,17 @@ Examples: `hatch`, `wake`, `sleep`, `retire`, `ps`, `ssh` belong here. `config`,
|
|
|
16
16
|
|
|
17
17
|
## Assistant targeting convention
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
New or modified commands that act on a specific assistant should accept an assistant display name or ID as an argument. Exact assistant ID matches must win over display-name matches. Unique display-name matches may resolve to the matching assistant ID, but ambiguous display names must fail with an error that lists the matching IDs.
|
|
20
|
+
|
|
21
|
+
Use the shared helpers from `lib/assistant-config` instead of hand-rolled lookup:
|
|
22
|
+
|
|
23
|
+
- `lookupAssistantByIdentifier()` for commands that require an explicit target and need custom error handling.
|
|
24
|
+
- `resolveTargetAssistant()` for commands that may fall back to the active assistant or sole lockfile entry.
|
|
25
|
+
- `formatAssistantReference()` for user-facing output that should include both display name and ID when they differ.
|
|
26
|
+
|
|
27
|
+
Use `parseAssistantTargetArg()` from `lib/assistant-target-args` when parsing command arguments that may contain an unquoted multi-word display name. Do not store raw display names in `activeAssistant`; persist the resolved `assistantId`.
|
|
28
|
+
|
|
29
|
+
New or modified destructive lifecycle commands must be explicit and safe. A command that deletes, retires, archives, or removes assistant state must print the resolved assistant identity before acting and require an interactive confirmation, with a documented `--yes` bypass only for automation or higher-level clients that already own confirmation. Do not expose destructive lifecycle actions as `vellum client` slash commands.
|
|
20
30
|
|
|
21
31
|
## Conventions
|
|
22
32
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { checkProviderApiKey } from "../lib/api-key-check.js";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_KEYS = [
|
|
6
|
+
"ANTHROPIC_API_KEY",
|
|
7
|
+
"OPENAI_API_KEY",
|
|
8
|
+
"GEMINI_API_KEY",
|
|
9
|
+
"FIREWORKS_API_KEY",
|
|
10
|
+
"OPENROUTER_API_KEY",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
for (const key of PROVIDER_KEYS) {
|
|
15
|
+
delete process.env[key];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
for (const key of PROVIDER_KEYS) {
|
|
21
|
+
delete process.env[key];
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("checkProviderApiKey", () => {
|
|
26
|
+
test("returns hasKey:false when no provider keys are in process.env", () => {
|
|
27
|
+
const result = checkProviderApiKey();
|
|
28
|
+
expect(result.hasKey).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns hasKey:false when ANTHROPIC_API_KEY is a placeholder", () => {
|
|
32
|
+
process.env.ANTHROPIC_API_KEY = "sk-ant-...";
|
|
33
|
+
const result = checkProviderApiKey();
|
|
34
|
+
expect(result.hasKey).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("returns hasKey:false when OPENAI_API_KEY is a placeholder", () => {
|
|
38
|
+
process.env.OPENAI_API_KEY = "sk-...";
|
|
39
|
+
const result = checkProviderApiKey();
|
|
40
|
+
expect(result.hasKey).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("returns hasKey:false when key is empty", () => {
|
|
44
|
+
process.env.ANTHROPIC_API_KEY = "";
|
|
45
|
+
const result = checkProviderApiKey();
|
|
46
|
+
expect(result.hasKey).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns hasKey:true when ANTHROPIC_API_KEY is a real key", () => {
|
|
50
|
+
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-realkey123";
|
|
51
|
+
const result = checkProviderApiKey();
|
|
52
|
+
expect(result.hasKey).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns hasKey:true when OPENAI_API_KEY is a real key", () => {
|
|
56
|
+
process.env.OPENAI_API_KEY = "sk-proj-realkey123";
|
|
57
|
+
const result = checkProviderApiKey();
|
|
58
|
+
expect(result.hasKey).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns hasKey:true when GEMINI_API_KEY is a real key", () => {
|
|
62
|
+
process.env.GEMINI_API_KEY = "AIzaSyRealKey123";
|
|
63
|
+
const result = checkProviderApiKey();
|
|
64
|
+
expect(result.hasKey).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns hasKey:true when FIREWORKS_API_KEY is a real key", () => {
|
|
68
|
+
process.env.FIREWORKS_API_KEY = "fw-realkey123";
|
|
69
|
+
const result = checkProviderApiKey();
|
|
70
|
+
expect(result.hasKey).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("returns hasKey:true when OPENROUTER_API_KEY is a real key", () => {
|
|
74
|
+
process.env.OPENROUTER_API_KEY = "sk-or-realkey123";
|
|
75
|
+
const result = checkProviderApiKey();
|
|
76
|
+
expect(result.hasKey).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
mock,
|
|
8
|
+
spyOn,
|
|
9
|
+
test,
|
|
10
|
+
} from "bun:test";
|
|
11
|
+
import {
|
|
12
|
+
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
22
|
+
import { loadAllAssistants } from "../lib/assistant-config.js";
|
|
23
|
+
import * as retireLocalModule from "../lib/retire-local.js";
|
|
24
|
+
|
|
25
|
+
const testDir = mkdtempSync(join(tmpdir(), "cli-retire-test-"));
|
|
26
|
+
const originalArgv = [...process.argv];
|
|
27
|
+
const originalExit = process.exit;
|
|
28
|
+
const originalLockfileDir = process.env.VELLUM_LOCKFILE_DIR;
|
|
29
|
+
const originalStdinIsTTY = process.stdin.isTTY;
|
|
30
|
+
const originalStdoutIsTTY = process.stdout.isTTY;
|
|
31
|
+
const originalStdinIsRaw = process.stdin.isRaw;
|
|
32
|
+
const originalSetRawMode = process.stdin.setRawMode;
|
|
33
|
+
const originalStdoutWrite = process.stdout.write;
|
|
34
|
+
const realRetireLocalModule = { ...retireLocalModule };
|
|
35
|
+
|
|
36
|
+
const retireLocalMock = mock(async () => {});
|
|
37
|
+
|
|
38
|
+
mock.module("../lib/retire-local.js", () => ({
|
|
39
|
+
...realRetireLocalModule,
|
|
40
|
+
retireLocal: retireLocalMock,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
import { retire } from "../commands/retire.js";
|
|
44
|
+
|
|
45
|
+
let consoleLogSpy: ReturnType<typeof spyOn>;
|
|
46
|
+
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
47
|
+
|
|
48
|
+
function makeEntry(
|
|
49
|
+
assistantId: string,
|
|
50
|
+
extra: Partial<AssistantEntry> = {},
|
|
51
|
+
): AssistantEntry {
|
|
52
|
+
return {
|
|
53
|
+
assistantId,
|
|
54
|
+
runtimeUrl: `http://127.0.0.1:${7800 + assistantId.length}`,
|
|
55
|
+
cloud: "local",
|
|
56
|
+
resources: {
|
|
57
|
+
instanceDir: join(testDir, assistantId),
|
|
58
|
+
daemonPort: 7801,
|
|
59
|
+
gatewayPort: 7831,
|
|
60
|
+
qdrantPort: 6334,
|
|
61
|
+
cesPort: 7790,
|
|
62
|
+
},
|
|
63
|
+
...extra,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeLockfile(entries: AssistantEntry[]): void {
|
|
68
|
+
mkdirSync(testDir, { recursive: true });
|
|
69
|
+
writeFileSync(
|
|
70
|
+
join(testDir, ".vellum.lock.json"),
|
|
71
|
+
JSON.stringify({ assistants: entries }, null, 2) + "\n",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readLockfile(): string {
|
|
76
|
+
return readFileSync(join(testDir, ".vellum.lock.json"), "utf-8");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setTerminalMode(isTTY: boolean): void {
|
|
80
|
+
Object.defineProperty(process.stdin, "isTTY", {
|
|
81
|
+
configurable: true,
|
|
82
|
+
value: isTTY,
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
85
|
+
configurable: true,
|
|
86
|
+
value: isTTY,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setInteractiveTerminal(): void {
|
|
91
|
+
setTerminalMode(true);
|
|
92
|
+
Object.defineProperty(process.stdin, "isRaw", {
|
|
93
|
+
configurable: true,
|
|
94
|
+
value: false,
|
|
95
|
+
});
|
|
96
|
+
Object.defineProperty(process.stdin, "setRawMode", {
|
|
97
|
+
configurable: true,
|
|
98
|
+
value: mock(() => process.stdin),
|
|
99
|
+
});
|
|
100
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function restoreTerminal(): void {
|
|
104
|
+
Object.defineProperty(process.stdin, "isTTY", {
|
|
105
|
+
configurable: true,
|
|
106
|
+
value: originalStdinIsTTY,
|
|
107
|
+
});
|
|
108
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
109
|
+
configurable: true,
|
|
110
|
+
value: originalStdoutIsTTY,
|
|
111
|
+
});
|
|
112
|
+
Object.defineProperty(process.stdin, "isRaw", {
|
|
113
|
+
configurable: true,
|
|
114
|
+
value: originalStdinIsRaw,
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(process.stdin, "setRawMode", {
|
|
117
|
+
configurable: true,
|
|
118
|
+
value: originalSetRawMode,
|
|
119
|
+
});
|
|
120
|
+
process.stdout.write = originalStdoutWrite;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe("vellum retire", () => {
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
126
|
+
rmSync(join(testDir, ".vellum.lock.json"), { force: true });
|
|
127
|
+
process.argv = ["bun", "vellum", "retire"];
|
|
128
|
+
process.exit = ((code?: number) => {
|
|
129
|
+
throw new Error(`process.exit:${code}`);
|
|
130
|
+
}) as typeof process.exit;
|
|
131
|
+
retireLocalMock.mockReset();
|
|
132
|
+
retireLocalMock.mockResolvedValue(undefined);
|
|
133
|
+
setTerminalMode(false);
|
|
134
|
+
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
135
|
+
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
process.argv = originalArgv;
|
|
140
|
+
process.exit = originalExit;
|
|
141
|
+
restoreTerminal();
|
|
142
|
+
consoleLogSpy.mockRestore();
|
|
143
|
+
consoleErrorSpy.mockRestore();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterAll(() => {
|
|
147
|
+
mock.module("../lib/retire-local.js", () => realRetireLocalModule);
|
|
148
|
+
if (originalLockfileDir === undefined) {
|
|
149
|
+
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
150
|
+
} else {
|
|
151
|
+
process.env.VELLUM_LOCKFILE_DIR = originalLockfileDir;
|
|
152
|
+
}
|
|
153
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("--yes retires by unquoted display name and removes by assistant ID", async () => {
|
|
157
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
158
|
+
writeLockfile([entry]);
|
|
159
|
+
process.argv = ["bun", "vellum", "retire", "Example", "Assistant", "--yes"];
|
|
160
|
+
|
|
161
|
+
await retire();
|
|
162
|
+
|
|
163
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
164
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
165
|
+
const output = consoleLogSpy.mock.calls.flat().join("\n");
|
|
166
|
+
expect(output).toContain("Name: Example Assistant");
|
|
167
|
+
expect(output).toContain("ID: assistant-1");
|
|
168
|
+
expect(output).toContain(
|
|
169
|
+
"Removed Example Assistant (assistant-1) from config.",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("non-interactive retire without --yes fails before deleting", async () => {
|
|
174
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
175
|
+
writeLockfile([entry]);
|
|
176
|
+
const before = readLockfile();
|
|
177
|
+
process.argv = ["bun", "vellum", "retire", "Example", "Assistant"];
|
|
178
|
+
|
|
179
|
+
await expect(retire()).rejects.toThrow("process.exit:1");
|
|
180
|
+
|
|
181
|
+
expect(retireLocalMock).not.toHaveBeenCalled();
|
|
182
|
+
expect(readLockfile()).toBe(before);
|
|
183
|
+
const output = consoleErrorSpy.mock.calls.flat().join("\n");
|
|
184
|
+
expect(output).toContain("Refusing to retire without confirmation");
|
|
185
|
+
expect(output).toContain("--yes");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("interactive cancel leaves the assistant untouched", async () => {
|
|
189
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
190
|
+
writeLockfile([entry]);
|
|
191
|
+
const before = readLockfile();
|
|
192
|
+
setInteractiveTerminal();
|
|
193
|
+
process.argv = ["bun", "vellum", "retire", "Example", "Assistant"];
|
|
194
|
+
|
|
195
|
+
const pending = retire();
|
|
196
|
+
queueMicrotask(() => {
|
|
197
|
+
process.stdin.emit("data", Buffer.from("q"));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await expect(pending).rejects.toThrow("process.exit:1");
|
|
201
|
+
expect(retireLocalMock).not.toHaveBeenCalled();
|
|
202
|
+
expect(readLockfile()).toBe(before);
|
|
203
|
+
expect(consoleLogSpy.mock.calls.flat().join("\n")).toContain(
|
|
204
|
+
"Retire cancelled.",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("interactive confirmation retires the assistant", async () => {
|
|
209
|
+
const entry = makeEntry("assistant-1", { name: "Example Assistant" });
|
|
210
|
+
writeLockfile([entry]);
|
|
211
|
+
setInteractiveTerminal();
|
|
212
|
+
process.argv = ["bun", "vellum", "retire", "Example", "Assistant"];
|
|
213
|
+
|
|
214
|
+
const pending = retire();
|
|
215
|
+
queueMicrotask(() => {
|
|
216
|
+
process.stdin.emit("data", Buffer.from([13]));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await pending;
|
|
220
|
+
expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
|
|
221
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("ambiguous display names fail before deleting", async () => {
|
|
225
|
+
writeLockfile([
|
|
226
|
+
makeEntry("assistant-1", { name: "Example Assistant" }),
|
|
227
|
+
makeEntry("assistant-2", { name: "Example Assistant" }),
|
|
228
|
+
]);
|
|
229
|
+
const before = readLockfile();
|
|
230
|
+
process.argv = ["bun", "vellum", "retire", "Example", "Assistant", "--yes"];
|
|
231
|
+
|
|
232
|
+
await expect(retire()).rejects.toThrow("process.exit:1");
|
|
233
|
+
|
|
234
|
+
expect(retireLocalMock).not.toHaveBeenCalled();
|
|
235
|
+
expect(readLockfile()).toBe(before);
|
|
236
|
+
const output = consoleErrorSpy.mock.calls.flat().join("\n");
|
|
237
|
+
expect(output).toContain("Multiple assistants match 'Example Assistant'");
|
|
238
|
+
expect(output).toContain("assistant-1");
|
|
239
|
+
expect(output).toContain("assistant-2");
|
|
240
|
+
});
|
|
241
|
+
});
|
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
|