@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.
- package/AGENTS.md +17 -1
- package/knip.json +2 -1
- package/package.json +1 -1
- package/src/__tests__/api-key-check.test.ts +78 -0
- package/src/__tests__/backup.test.ts +38 -0
- package/src/__tests__/recover.test.ts +307 -0
- package/src/__tests__/retire.test.ts +241 -0
- package/src/__tests__/wake.test.ts +215 -0
- package/src/commands/backup.ts +2 -0
- package/src/commands/client.ts +62 -32
- package/src/commands/flags.ts +197 -0
- package/src/commands/gateway/token.ts +73 -0
- package/src/commands/gateway.ts +29 -0
- package/src/commands/logs.ts +6 -18
- package/src/commands/ps.ts +41 -41
- package/src/commands/recover.ts +47 -9
- package/src/commands/restore.ts +8 -1
- package/src/commands/retire.ts +145 -55
- package/src/commands/roadmap.ts +449 -0
- package/src/commands/rollback.ts +2 -14
- package/src/commands/ssh.ts +5 -24
- package/src/commands/teleport.ts +34 -26
- package/src/commands/upgrade.ts +8 -16
- package/src/commands/wake.ts +68 -45
- package/src/index.ts +9 -0
- package/src/lib/__tests__/port-allocator.test.ts +117 -0
- package/src/lib/__tests__/step-runner.test.ts +133 -0
- package/src/lib/api-key-check.ts +40 -0
- package/src/lib/assistant-config.ts +13 -0
- package/src/lib/config-utils.ts +24 -3
- package/src/lib/docker.ts +72 -8
- package/src/lib/hatch-local.ts +15 -2
- package/src/lib/http-client.ts +1 -3
- package/src/lib/local.ts +173 -292
- package/src/lib/orphan-detection.ts +9 -5
- package/src/lib/pgrep.ts +5 -1
- package/src/lib/platform-client.ts +97 -49
- package/src/lib/port-allocator.ts +93 -0
- package/src/lib/process.ts +109 -39
- package/src/lib/statefulset.ts +0 -10
- package/src/lib/step-runner.ts +102 -9
- package/src/lib/sync-cloud-assistants.ts +17 -0
- package/src/shared/provider-env-vars.ts +1 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { readPlatformToken, getWebUrl } from "../lib/platform-client.js";
|
|
2
|
+
|
|
3
|
+
function printUsage(): void {
|
|
4
|
+
console.log("Usage: vellum roadmap <subcommand>");
|
|
5
|
+
console.log("");
|
|
6
|
+
console.log("Manage roadmap items.");
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log("Subcommands:");
|
|
9
|
+
console.log(
|
|
10
|
+
" list [--query <q>] [--status <s>] [--tag <slug>] [--sort upvotes|created] [--limit <n>]",
|
|
11
|
+
);
|
|
12
|
+
console.log(" get <slug>");
|
|
13
|
+
console.log(
|
|
14
|
+
" create --title <title> [--description <desc>] [--tag <slug>...]",
|
|
15
|
+
);
|
|
16
|
+
console.log(
|
|
17
|
+
" update <slug> [--title <title>] [--description <desc>] [--status <s>] [--tag <slug>...]",
|
|
18
|
+
);
|
|
19
|
+
console.log(" delete <slug>");
|
|
20
|
+
console.log(" upvote <slug>");
|
|
21
|
+
console.log(" unvote <slug>");
|
|
22
|
+
console.log("");
|
|
23
|
+
console.log("Examples:");
|
|
24
|
+
console.log(' $ vellum roadmap list --query "dark mode"');
|
|
25
|
+
console.log(" $ vellum roadmap list --status planned --sort upvotes");
|
|
26
|
+
console.log(" $ vellum roadmap get my-feature-slug");
|
|
27
|
+
console.log(' $ vellum roadmap create --title "Add dark mode"');
|
|
28
|
+
console.log(
|
|
29
|
+
' $ vellum roadmap update my-feature --status planned --tag integrations',
|
|
30
|
+
);
|
|
31
|
+
console.log(" $ vellum roadmap upvote my-feature-slug");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function consumeValue(args: string[], i: number, flag: string): string {
|
|
35
|
+
const next = args[i + 1];
|
|
36
|
+
if (next === undefined || next.startsWith("--")) {
|
|
37
|
+
console.error(`Error: ${flag} requires a value.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireAuth(): string {
|
|
44
|
+
const token = readPlatformToken();
|
|
45
|
+
if (!token) {
|
|
46
|
+
console.error("Not logged in. Run `vellum login` first.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return token;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireSlug(args: string[], command: string): string {
|
|
53
|
+
const slug = args[0];
|
|
54
|
+
if (!slug || slug.startsWith("--")) {
|
|
55
|
+
console.error(`Usage: vellum roadmap ${command} <slug>`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return slug;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line no-control-regex
|
|
62
|
+
const ANSI_RE = /[\x00-\x08\x0b-\x1f\x7f]|\x1b(?:\[[0-9;]*[A-Za-z]|\].*?(?:\x07|\x1b\\))/g;
|
|
63
|
+
function sanitize(text: string): string {
|
|
64
|
+
return text.replace(ANSI_RE, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeLink(url: string): string {
|
|
68
|
+
return `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function apiFetch(
|
|
72
|
+
path: string,
|
|
73
|
+
options: {
|
|
74
|
+
method?: string;
|
|
75
|
+
token?: string;
|
|
76
|
+
body?: Record<string, unknown>;
|
|
77
|
+
params?: Record<string, string>;
|
|
78
|
+
} = {},
|
|
79
|
+
): Promise<Response> {
|
|
80
|
+
const webUrl = getWebUrl();
|
|
81
|
+
let url = `${webUrl}/api/marketing${path}`;
|
|
82
|
+
if (options.params) {
|
|
83
|
+
const qs = new URLSearchParams(options.params).toString();
|
|
84
|
+
if (qs) url += `?${qs}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const headers: Record<string, string> = {};
|
|
88
|
+
if (options.token) headers["X-Session-Token"] = options.token;
|
|
89
|
+
if (options.body) headers["Content-Type"] = "application/json";
|
|
90
|
+
|
|
91
|
+
return fetch(url, {
|
|
92
|
+
method: options.method ?? "GET",
|
|
93
|
+
headers,
|
|
94
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleError(
|
|
99
|
+
response: Response,
|
|
100
|
+
action: string,
|
|
101
|
+
): Promise<never> {
|
|
102
|
+
const text = await response.text().catch(() => "");
|
|
103
|
+
console.error(`Failed to ${action} (${response.status}): ${text}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── list ──
|
|
108
|
+
|
|
109
|
+
interface ListItem {
|
|
110
|
+
slug: string;
|
|
111
|
+
title: string;
|
|
112
|
+
status: string;
|
|
113
|
+
upvote_count: number;
|
|
114
|
+
comment_count: number;
|
|
115
|
+
tags: { slug: string; name: string }[];
|
|
116
|
+
viewer_upvoted: boolean | null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function roadmapList(args: string[]): Promise<void> {
|
|
120
|
+
const params: Record<string, string> = {};
|
|
121
|
+
const token = readPlatformToken() ?? undefined;
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < args.length; i++) {
|
|
124
|
+
switch (args[i]) {
|
|
125
|
+
case "--query":
|
|
126
|
+
case "-q":
|
|
127
|
+
params.q = consumeValue(args, i, "--query");
|
|
128
|
+
i++;
|
|
129
|
+
break;
|
|
130
|
+
case "--status":
|
|
131
|
+
params.status = consumeValue(args, i, "--status");
|
|
132
|
+
i++;
|
|
133
|
+
break;
|
|
134
|
+
case "--tag":
|
|
135
|
+
params.tag = consumeValue(args, i, "--tag");
|
|
136
|
+
i++;
|
|
137
|
+
break;
|
|
138
|
+
case "--sort":
|
|
139
|
+
params.sort = consumeValue(args, i, "--sort");
|
|
140
|
+
i++;
|
|
141
|
+
break;
|
|
142
|
+
case "--limit":
|
|
143
|
+
params.limit = consumeValue(args, i, "--limit");
|
|
144
|
+
i++;
|
|
145
|
+
break;
|
|
146
|
+
case "--offset":
|
|
147
|
+
params.offset = consumeValue(args, i, "--offset");
|
|
148
|
+
i++;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const response = await apiFetch("/v1/roadmap", { params, token });
|
|
154
|
+
if (!response.ok) return handleError(response, "list roadmap items");
|
|
155
|
+
|
|
156
|
+
const data = (await response.json()) as {
|
|
157
|
+
items: ListItem[];
|
|
158
|
+
total: number;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (data.items.length === 0) {
|
|
162
|
+
console.log("No roadmap items found.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const webUrl = getWebUrl();
|
|
167
|
+
console.log(`Showing ${data.items.length} of ${data.total} items:\n`);
|
|
168
|
+
|
|
169
|
+
for (const item of data.items) {
|
|
170
|
+
const upvoted = item.viewer_upvoted ? " (upvoted)" : "";
|
|
171
|
+
const tags = item.tags.length > 0
|
|
172
|
+
? ` [${item.tags.map((t) => sanitize(t.slug)).join(", ")}]`
|
|
173
|
+
: "";
|
|
174
|
+
console.log(
|
|
175
|
+
` ${sanitize(item.title)} ▲${item.upvote_count}${upvoted} 💬${item.comment_count} ${item.status}${tags}`,
|
|
176
|
+
);
|
|
177
|
+
console.log(` ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── get ──
|
|
182
|
+
|
|
183
|
+
async function roadmapGet(args: string[]): Promise<void> {
|
|
184
|
+
const slug = requireSlug(args, "get");
|
|
185
|
+
const token = readPlatformToken() ?? undefined;
|
|
186
|
+
const response = await apiFetch(`/v1/roadmap/${slug}`, { token });
|
|
187
|
+
if (!response.ok) return handleError(response, "get roadmap item");
|
|
188
|
+
|
|
189
|
+
const item = (await response.json()) as {
|
|
190
|
+
slug: string;
|
|
191
|
+
title: string;
|
|
192
|
+
description: string;
|
|
193
|
+
status: string;
|
|
194
|
+
upvote_count: number;
|
|
195
|
+
comment_count: number;
|
|
196
|
+
tags: { slug: string; name: string }[];
|
|
197
|
+
viewer_upvoted: boolean | null;
|
|
198
|
+
creator_username: string;
|
|
199
|
+
created: string;
|
|
200
|
+
comments: {
|
|
201
|
+
id: string;
|
|
202
|
+
author_username: string;
|
|
203
|
+
author_is_staff: boolean;
|
|
204
|
+
body: string;
|
|
205
|
+
created: string;
|
|
206
|
+
}[];
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const webUrl = getWebUrl();
|
|
210
|
+
const upvoted = item.viewer_upvoted ? " (upvoted)" : "";
|
|
211
|
+
const tags =
|
|
212
|
+
item.tags.length > 0
|
|
213
|
+
? item.tags.map((t) => sanitize(t.slug)).join(", ")
|
|
214
|
+
: "none";
|
|
215
|
+
|
|
216
|
+
console.log(sanitize(item.title));
|
|
217
|
+
console.log(` slug: ${item.slug}`);
|
|
218
|
+
console.log(` status: ${item.status}`);
|
|
219
|
+
console.log(` upvotes: ${item.upvote_count}${upvoted}`);
|
|
220
|
+
console.log(` tags: ${tags}`);
|
|
221
|
+
console.log(` by: ${sanitize(item.creator_username)}`);
|
|
222
|
+
console.log(` created: ${item.created}`);
|
|
223
|
+
console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
224
|
+
if (item.description) {
|
|
225
|
+
console.log(`\n${sanitize(item.description)}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (item.comments.length > 0) {
|
|
229
|
+
console.log(`\nComments (${item.comments.length}):`);
|
|
230
|
+
for (const c of item.comments) {
|
|
231
|
+
const staff = c.author_is_staff ? " [staff]" : "";
|
|
232
|
+
console.log(` ${sanitize(c.author_username)}${staff} (${c.created}):`);
|
|
233
|
+
console.log(` ${sanitize(c.body)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── create ──
|
|
239
|
+
|
|
240
|
+
async function roadmapCreate(args: string[]): Promise<void> {
|
|
241
|
+
let title: string | undefined;
|
|
242
|
+
let description: string | undefined;
|
|
243
|
+
const tags: string[] = [];
|
|
244
|
+
|
|
245
|
+
for (let i = 0; i < args.length; i++) {
|
|
246
|
+
switch (args[i]) {
|
|
247
|
+
case "--title":
|
|
248
|
+
title = consumeValue(args, i, "--title");
|
|
249
|
+
i++;
|
|
250
|
+
break;
|
|
251
|
+
case "--description":
|
|
252
|
+
description = consumeValue(args, i, "--description");
|
|
253
|
+
i++;
|
|
254
|
+
break;
|
|
255
|
+
case "--tag":
|
|
256
|
+
tags.push(consumeValue(args, i, "--tag"));
|
|
257
|
+
i++;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!title) {
|
|
263
|
+
console.error("Error: --title is required.");
|
|
264
|
+
console.error('Usage: vellum roadmap create --title "My feature request"');
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const token = requireAuth();
|
|
270
|
+
const body: Record<string, unknown> = { title };
|
|
271
|
+
if (description) body.description = description;
|
|
272
|
+
if (tags.length > 0) body.tags = tags;
|
|
273
|
+
|
|
274
|
+
const response = await apiFetch("/v1/roadmap", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
token,
|
|
277
|
+
body,
|
|
278
|
+
});
|
|
279
|
+
if (!response.ok) return handleError(response, "create roadmap item");
|
|
280
|
+
|
|
281
|
+
const item = (await response.json()) as {
|
|
282
|
+
slug: string;
|
|
283
|
+
title: string;
|
|
284
|
+
status: string;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const webUrl = getWebUrl();
|
|
288
|
+
console.log(`Created roadmap item: ${sanitize(item.title)}`);
|
|
289
|
+
console.log(` slug: ${item.slug}`);
|
|
290
|
+
console.log(` status: ${item.status}`);
|
|
291
|
+
console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── update ──
|
|
295
|
+
|
|
296
|
+
async function roadmapUpdate(args: string[]): Promise<void> {
|
|
297
|
+
const slug = requireSlug(args, "update");
|
|
298
|
+
|
|
299
|
+
let title: string | undefined;
|
|
300
|
+
let description: string | undefined;
|
|
301
|
+
let status: string | undefined;
|
|
302
|
+
const tags: string[] = [];
|
|
303
|
+
|
|
304
|
+
for (let i = 1; i < args.length; i++) {
|
|
305
|
+
switch (args[i]) {
|
|
306
|
+
case "--title":
|
|
307
|
+
title = consumeValue(args, i, "--title");
|
|
308
|
+
i++;
|
|
309
|
+
break;
|
|
310
|
+
case "--description":
|
|
311
|
+
description = consumeValue(args, i, "--description");
|
|
312
|
+
i++;
|
|
313
|
+
break;
|
|
314
|
+
case "--status":
|
|
315
|
+
status = consumeValue(args, i, "--status");
|
|
316
|
+
i++;
|
|
317
|
+
break;
|
|
318
|
+
case "--tag":
|
|
319
|
+
tags.push(consumeValue(args, i, "--tag"));
|
|
320
|
+
i++;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const body: Record<string, unknown> = {};
|
|
326
|
+
if (title !== undefined) body.title = title;
|
|
327
|
+
if (description !== undefined) body.description = description;
|
|
328
|
+
if (status !== undefined) body.status = status;
|
|
329
|
+
if (tags.length > 0) body.tags = tags;
|
|
330
|
+
|
|
331
|
+
if (Object.keys(body).length === 0) {
|
|
332
|
+
console.error("Error: at least one field to update is required.");
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const token = requireAuth();
|
|
338
|
+
const response = await apiFetch(`/v1/roadmap/${slug}`, {
|
|
339
|
+
method: "PATCH",
|
|
340
|
+
token,
|
|
341
|
+
body,
|
|
342
|
+
});
|
|
343
|
+
if (!response.ok) return handleError(response, "update roadmap item");
|
|
344
|
+
|
|
345
|
+
const item = (await response.json()) as {
|
|
346
|
+
slug: string;
|
|
347
|
+
title: string;
|
|
348
|
+
status: string;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const webUrl = getWebUrl();
|
|
352
|
+
console.log(`Updated roadmap item: ${sanitize(item.title)}`);
|
|
353
|
+
console.log(` slug: ${item.slug}`);
|
|
354
|
+
console.log(` status: ${item.status}`);
|
|
355
|
+
console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── delete ──
|
|
359
|
+
|
|
360
|
+
async function roadmapDelete(args: string[]): Promise<void> {
|
|
361
|
+
const slug = requireSlug(args, "delete");
|
|
362
|
+
const token = requireAuth();
|
|
363
|
+
const response = await apiFetch(`/v1/roadmap/${slug}`, {
|
|
364
|
+
method: "DELETE",
|
|
365
|
+
token,
|
|
366
|
+
});
|
|
367
|
+
if (!response.ok) return handleError(response, "delete roadmap item");
|
|
368
|
+
|
|
369
|
+
console.log(`Deleted roadmap item: ${slug}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── upvote / unvote ──
|
|
373
|
+
|
|
374
|
+
async function roadmapUpvote(args: string[]): Promise<void> {
|
|
375
|
+
const slug = requireSlug(args, "upvote");
|
|
376
|
+
const token = requireAuth();
|
|
377
|
+
const response = await apiFetch(`/v1/roadmap/${slug}/upvote`, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
token,
|
|
380
|
+
});
|
|
381
|
+
if (!response.ok) return handleError(response, "upvote roadmap item");
|
|
382
|
+
|
|
383
|
+
const data = (await response.json()) as {
|
|
384
|
+
slug: string;
|
|
385
|
+
upvote_count: number;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
console.log(`Upvoted: ${data.slug} (${data.upvote_count} total)`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function roadmapUnvote(args: string[]): Promise<void> {
|
|
392
|
+
const slug = requireSlug(args, "unvote");
|
|
393
|
+
const token = requireAuth();
|
|
394
|
+
const response = await apiFetch(`/v1/roadmap/${slug}/upvote`, {
|
|
395
|
+
method: "DELETE",
|
|
396
|
+
token,
|
|
397
|
+
});
|
|
398
|
+
if (!response.ok) return handleError(response, "remove upvote");
|
|
399
|
+
|
|
400
|
+
const data = (await response.json()) as {
|
|
401
|
+
slug: string;
|
|
402
|
+
upvote_count: number;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
console.log(`Removed upvote: ${data.slug} (${data.upvote_count} total)`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── main ──
|
|
409
|
+
|
|
410
|
+
export async function roadmap(): Promise<void> {
|
|
411
|
+
const args = process.argv.slice(3);
|
|
412
|
+
const sub = args[0];
|
|
413
|
+
|
|
414
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
415
|
+
printUsage();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
switch (sub) {
|
|
420
|
+
case "list":
|
|
421
|
+
case "ls":
|
|
422
|
+
await roadmapList(args.slice(1));
|
|
423
|
+
break;
|
|
424
|
+
case "get":
|
|
425
|
+
case "show":
|
|
426
|
+
await roadmapGet(args.slice(1));
|
|
427
|
+
break;
|
|
428
|
+
case "create":
|
|
429
|
+
await roadmapCreate(args.slice(1));
|
|
430
|
+
break;
|
|
431
|
+
case "update":
|
|
432
|
+
await roadmapUpdate(args.slice(1));
|
|
433
|
+
break;
|
|
434
|
+
case "delete":
|
|
435
|
+
case "rm":
|
|
436
|
+
await roadmapDelete(args.slice(1));
|
|
437
|
+
break;
|
|
438
|
+
case "upvote":
|
|
439
|
+
await roadmapUpvote(args.slice(1));
|
|
440
|
+
break;
|
|
441
|
+
case "unvote":
|
|
442
|
+
await roadmapUnvote(args.slice(1));
|
|
443
|
+
break;
|
|
444
|
+
default:
|
|
445
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
446
|
+
printUsage();
|
|
447
|
+
process.exitCode = 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
package/src/commands/rollback.ts
CHANGED
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
findAssistantByName,
|
|
5
5
|
getActiveAssistant,
|
|
6
6
|
loadAllAssistants,
|
|
7
|
+
resolveCloud,
|
|
7
8
|
saveAssistantEntry,
|
|
9
|
+
type AssistantEntry,
|
|
8
10
|
} from "../lib/assistant-config";
|
|
9
|
-
import type { AssistantEntry } from "../lib/assistant-config";
|
|
10
11
|
import {
|
|
11
12
|
captureImageRefs,
|
|
12
13
|
GATEWAY_INTERNAL_PORT,
|
|
@@ -90,19 +91,6 @@ function parseArgs(): { name: string | null; version: string | null } {
|
|
|
90
91
|
return { name, version };
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
94
|
-
if (entry.cloud) {
|
|
95
|
-
return entry.cloud;
|
|
96
|
-
}
|
|
97
|
-
if (entry.project) {
|
|
98
|
-
return "gcp";
|
|
99
|
-
}
|
|
100
|
-
if (entry.sshUser) {
|
|
101
|
-
return "custom";
|
|
102
|
-
}
|
|
103
|
-
return "local";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
94
|
/**
|
|
107
95
|
* Resolve which assistant to target for the rollback command. Priority:
|
|
108
96
|
* 1. Explicit name argument
|
package/src/commands/ssh.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
extractHostFromUrl,
|
|
5
|
+
resolveAssistant,
|
|
6
|
+
resolveCloud,
|
|
7
|
+
} from "../lib/assistant-config";
|
|
5
8
|
import { dockerResourceNames } from "../lib/docker";
|
|
6
9
|
import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
|
|
7
10
|
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
@@ -18,28 +21,6 @@ const SSH_OPTS = [
|
|
|
18
21
|
"LogLevel=ERROR",
|
|
19
22
|
];
|
|
20
23
|
|
|
21
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
22
|
-
if (entry.cloud) {
|
|
23
|
-
return entry.cloud;
|
|
24
|
-
}
|
|
25
|
-
if (entry.project) {
|
|
26
|
-
return "gcp";
|
|
27
|
-
}
|
|
28
|
-
if (entry.sshUser) {
|
|
29
|
-
return "custom";
|
|
30
|
-
}
|
|
31
|
-
return "local";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function extractHostFromUrl(url: string): string {
|
|
35
|
-
try {
|
|
36
|
-
const parsed = new URL(url);
|
|
37
|
-
return parsed.hostname;
|
|
38
|
-
} catch {
|
|
39
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
24
|
export async function ssh(): Promise<void> {
|
|
44
25
|
const args = process.argv.slice(3);
|
|
45
26
|
if (args.includes("--help") || args.includes("-h")) {
|
package/src/commands/teleport.ts
CHANGED
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
loadAllAssistants,
|
|
4
4
|
getDaemonPidPath,
|
|
5
5
|
removeAssistantEntry,
|
|
6
|
+
resolveCloud,
|
|
6
7
|
saveAssistantEntry,
|
|
7
8
|
setActiveAssistant,
|
|
9
|
+
type AssistantEntry,
|
|
8
10
|
} from "../lib/assistant-config.js";
|
|
9
|
-
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
10
11
|
import {
|
|
11
12
|
loadGuardianToken,
|
|
12
13
|
leaseGuardianToken,
|
|
@@ -214,12 +215,6 @@ export function parseArgs(argv: string[]): {
|
|
|
214
215
|
return { from, to, targetEnv, targetName, keepSource, dryRun, help };
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
218
|
-
return (
|
|
219
|
-
entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local")
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
218
|
// ---------------------------------------------------------------------------
|
|
224
219
|
// Auth helper — same pattern as restore.ts
|
|
225
220
|
// ---------------------------------------------------------------------------
|
|
@@ -228,7 +223,7 @@ async function getAccessToken(
|
|
|
228
223
|
runtimeUrl: string,
|
|
229
224
|
assistantId: string,
|
|
230
225
|
displayName: string,
|
|
231
|
-
options?: { forceRefresh?: boolean },
|
|
226
|
+
options?: { forceRefresh?: boolean; bootstrapSecret?: string },
|
|
232
227
|
): Promise<string> {
|
|
233
228
|
// When forceRefresh is set (e.g. after a runtime 401 on the cached token)
|
|
234
229
|
// we skip the cache and lease a brand-new token from the gateway, so a
|
|
@@ -242,7 +237,11 @@ async function getAccessToken(
|
|
|
242
237
|
}
|
|
243
238
|
|
|
244
239
|
try {
|
|
245
|
-
const freshToken = await leaseGuardianToken(
|
|
240
|
+
const freshToken = await leaseGuardianToken(
|
|
241
|
+
runtimeUrl,
|
|
242
|
+
assistantId,
|
|
243
|
+
options?.bootstrapSecret,
|
|
244
|
+
);
|
|
246
245
|
return freshToken.accessToken;
|
|
247
246
|
} catch (err) {
|
|
248
247
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -281,11 +280,15 @@ function isRuntime401(err: unknown): boolean {
|
|
|
281
280
|
* — propagates to the caller.
|
|
282
281
|
*/
|
|
283
282
|
async function callRuntimeWithAuthRetry<T>(
|
|
284
|
-
|
|
285
|
-
assistantId: string,
|
|
283
|
+
entry: AssistantEntry,
|
|
286
284
|
fn: (token: string) => Promise<T>,
|
|
287
285
|
): Promise<T> {
|
|
288
|
-
const firstToken = await getAccessToken(
|
|
286
|
+
const firstToken = await getAccessToken(
|
|
287
|
+
entry.runtimeUrl,
|
|
288
|
+
entry.assistantId,
|
|
289
|
+
entry.assistantId,
|
|
290
|
+
{ bootstrapSecret: entry.guardianBootstrapSecret },
|
|
291
|
+
);
|
|
289
292
|
try {
|
|
290
293
|
return await fn(firstToken);
|
|
291
294
|
} catch (err) {
|
|
@@ -293,10 +296,13 @@ async function callRuntimeWithAuthRetry<T>(
|
|
|
293
296
|
throw err;
|
|
294
297
|
}
|
|
295
298
|
const refreshedToken = await getAccessToken(
|
|
296
|
-
runtimeUrl,
|
|
297
|
-
assistantId,
|
|
298
|
-
assistantId,
|
|
299
|
-
{
|
|
299
|
+
entry.runtimeUrl,
|
|
300
|
+
entry.assistantId,
|
|
301
|
+
entry.assistantId,
|
|
302
|
+
{
|
|
303
|
+
forceRefresh: true,
|
|
304
|
+
bootstrapSecret: entry.guardianBootstrapSecret,
|
|
305
|
+
},
|
|
300
306
|
);
|
|
301
307
|
return await fn(refreshedToken);
|
|
302
308
|
}
|
|
@@ -386,8 +392,7 @@ async function exportFromAssistant(
|
|
|
386
392
|
let sourceRuntimeVersion: string;
|
|
387
393
|
try {
|
|
388
394
|
const identity = await callRuntimeWithAuthRetry(
|
|
389
|
-
entry
|
|
390
|
-
entry.assistantId,
|
|
395
|
+
entry,
|
|
391
396
|
async (token) => localRuntimeIdentity(entry, token),
|
|
392
397
|
);
|
|
393
398
|
sourceRuntimeVersion = identity.version;
|
|
@@ -423,8 +428,7 @@ async function exportFromAssistant(
|
|
|
423
428
|
let accessToken: string;
|
|
424
429
|
try {
|
|
425
430
|
const result = await callRuntimeWithAuthRetry(
|
|
426
|
-
entry
|
|
427
|
-
entry.assistantId,
|
|
431
|
+
entry,
|
|
428
432
|
async (token) => {
|
|
429
433
|
const r = await localRuntimeExportToGcs(entry, token, {
|
|
430
434
|
uploadUrl,
|
|
@@ -462,7 +466,10 @@ async function exportFromAssistant(
|
|
|
462
466
|
entry.runtimeUrl,
|
|
463
467
|
entry.assistantId,
|
|
464
468
|
entry.assistantId,
|
|
465
|
-
{
|
|
469
|
+
{
|
|
470
|
+
forceRefresh: true,
|
|
471
|
+
bootstrapSecret: entry.guardianBootstrapSecret,
|
|
472
|
+
},
|
|
466
473
|
);
|
|
467
474
|
},
|
|
468
475
|
});
|
|
@@ -728,8 +735,7 @@ async function importToAssistant(
|
|
|
728
735
|
let targetRuntimeVersion: string;
|
|
729
736
|
try {
|
|
730
737
|
const identity = await callRuntimeWithAuthRetry(
|
|
731
|
-
entry
|
|
732
|
-
entry.assistantId,
|
|
738
|
+
entry,
|
|
733
739
|
(token) => localRuntimeIdentity(entry, token),
|
|
734
740
|
);
|
|
735
741
|
targetRuntimeVersion = identity.version;
|
|
@@ -774,8 +780,7 @@ async function importToAssistant(
|
|
|
774
780
|
let accessToken: string;
|
|
775
781
|
try {
|
|
776
782
|
const result = await callRuntimeWithAuthRetry(
|
|
777
|
-
entry
|
|
778
|
-
entry.assistantId,
|
|
783
|
+
entry,
|
|
779
784
|
async (token) => {
|
|
780
785
|
const r = await localRuntimeImportFromGcs(entry, token, {
|
|
781
786
|
bundleUrl,
|
|
@@ -806,7 +811,10 @@ async function importToAssistant(
|
|
|
806
811
|
entry.runtimeUrl,
|
|
807
812
|
entry.assistantId,
|
|
808
813
|
entry.assistantId,
|
|
809
|
-
{
|
|
814
|
+
{
|
|
815
|
+
forceRefresh: true,
|
|
816
|
+
bootstrapSecret: entry.guardianBootstrapSecret,
|
|
817
|
+
},
|
|
810
818
|
);
|
|
811
819
|
},
|
|
812
820
|
});
|