@vellumai/cli 0.6.6 → 0.7.1
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 +8 -2
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/backup.test.ts +475 -0
- package/src/__tests__/config-utils.test.ts +146 -0
- package/src/__tests__/env-drift.test.ts +10 -32
- package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
- package/src/__tests__/multi-local.test.ts +0 -5
- package/src/__tests__/sleep.test.ts +1 -2
- package/src/__tests__/teleport.test.ts +988 -1266
- package/src/commands/backup.ts +117 -71
- package/src/commands/client.ts +10 -9
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +58 -13
- package/src/commands/login.ts +77 -12
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +144 -25
- package/src/commands/restore.ts +26 -47
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +17 -7
- package/src/commands/teleport.ts +462 -584
- package/src/commands/terminal.ts +9 -221
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +108 -7
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +328 -154
- package/src/index.ts +5 -7
- package/src/lib/__tests__/docker.test.ts +50 -74
- package/src/lib/__tests__/job-polling.test.ts +278 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/__tests__/runtime-url.test.ts +87 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +5 -21
- package/src/lib/assistant-config.ts +46 -24
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/docker.ts +75 -77
- package/src/lib/environments/__tests__/paths.test.ts +2 -0
- package/src/lib/environments/resolve.ts +89 -7
- package/src/lib/environments/seeds.ts +8 -5
- package/src/lib/environments/types.ts +10 -0
- package/src/lib/hatch-local.ts +15 -120
- package/src/lib/health-check.ts +98 -0
- package/src/lib/job-polling.ts +195 -0
- package/src/lib/local-runtime-client.ts +231 -0
- package/src/lib/local.ts +165 -72
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +190 -194
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/runtime-url.ts +30 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +536 -0
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
package/src/commands/login.ts
CHANGED
|
@@ -3,9 +3,11 @@ import { spawn } from "child_process";
|
|
|
3
3
|
import { randomBytes } from "crypto";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
findAssistantByName,
|
|
7
6
|
getActiveAssistant,
|
|
8
|
-
|
|
7
|
+
resolveAssistant,
|
|
8
|
+
loadAllAssistants,
|
|
9
|
+
removeAssistantEntry,
|
|
10
|
+
setActiveAssistant,
|
|
9
11
|
} from "../lib/assistant-config";
|
|
10
12
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
11
13
|
import {
|
|
@@ -13,13 +15,16 @@ import {
|
|
|
13
15
|
ensureSelfHostedLocalRegistration,
|
|
14
16
|
fetchCurrentUser,
|
|
15
17
|
fetchOrganizationId,
|
|
18
|
+
fetchPlatformAssistants,
|
|
16
19
|
getPlatformUrl,
|
|
20
|
+
getWebUrl,
|
|
17
21
|
injectCredentialsIntoAssistant,
|
|
18
22
|
readGatewayCredential,
|
|
19
23
|
readPlatformToken,
|
|
20
24
|
reprovisionAssistantApiKey,
|
|
21
25
|
savePlatformToken,
|
|
22
26
|
} from "../lib/platform-client";
|
|
27
|
+
import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
|
|
23
28
|
|
|
24
29
|
const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
|
|
25
30
|
|
|
@@ -45,7 +50,7 @@ function openBrowser(url: string): void {
|
|
|
45
50
|
* Start a local HTTP server, open the browser to the platform login page,
|
|
46
51
|
* and wait for the platform to redirect back with the session token.
|
|
47
52
|
*/
|
|
48
|
-
function browserLogin(
|
|
53
|
+
function browserLogin(webUrl: string): Promise<string> {
|
|
49
54
|
return new Promise((resolve, reject) => {
|
|
50
55
|
const state = randomBytes(32).toString("hex");
|
|
51
56
|
|
|
@@ -112,7 +117,7 @@ function browserLogin(platformUrl: string): Promise<string> {
|
|
|
112
117
|
|
|
113
118
|
const port = addr.port;
|
|
114
119
|
const returnTo = `/accounts/cli/callback?port=${port}&state=${state}`;
|
|
115
|
-
const loginUrl = `${
|
|
120
|
+
const loginUrl = `${webUrl}/account/login?returnTo=${encodeURIComponent(returnTo)}`;
|
|
116
121
|
|
|
117
122
|
console.log("Opening browser for login...");
|
|
118
123
|
console.log(`If the browser doesn't open, visit: ${loginUrl}`);
|
|
@@ -125,22 +130,30 @@ export async function login(): Promise<void> {
|
|
|
125
130
|
const args = process.argv.slice(3);
|
|
126
131
|
|
|
127
132
|
if (args.includes("--help") || args.includes("-h")) {
|
|
128
|
-
console.log("Usage: vellum login [--token <session-token>]");
|
|
133
|
+
console.log("Usage: vellum login [--token <session-token>] [--force]");
|
|
129
134
|
console.log("");
|
|
130
135
|
console.log("Log in to the Vellum platform.");
|
|
131
136
|
console.log("");
|
|
132
137
|
console.log("By default, opens a browser window for authentication.");
|
|
133
138
|
console.log("Alternatively, pass a session token directly with --token.");
|
|
134
139
|
console.log("");
|
|
140
|
+
console.log("On success, syncs cloud-managed assistants to the local");
|
|
141
|
+
console.log("lockfile so they appear in `vellum ps`.");
|
|
142
|
+
console.log("");
|
|
135
143
|
console.log("Options:");
|
|
136
144
|
console.log(" --token <token> Session token from the Vellum platform");
|
|
145
|
+
console.log(
|
|
146
|
+
" --force, -f Re-authenticate even if already logged in",
|
|
147
|
+
);
|
|
137
148
|
console.log("");
|
|
138
149
|
console.log("Examples:");
|
|
139
150
|
console.log(" vellum login");
|
|
140
151
|
console.log(" vellum login --token <session-token>");
|
|
152
|
+
console.log(" vellum login --force");
|
|
141
153
|
process.exit(0);
|
|
142
154
|
}
|
|
143
155
|
|
|
156
|
+
const forceFlag = args.includes("--force") || args.includes("-f");
|
|
144
157
|
let token: string | null = null;
|
|
145
158
|
|
|
146
159
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -154,11 +167,27 @@ export async function login(): Promise<void> {
|
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
// Block if already authenticated (unless --force)
|
|
171
|
+
if (!forceFlag && !token) {
|
|
172
|
+
const existingToken = readPlatformToken();
|
|
173
|
+
if (existingToken) {
|
|
174
|
+
try {
|
|
175
|
+
const existingUser = await fetchCurrentUser(existingToken);
|
|
176
|
+
console.error(
|
|
177
|
+
`Already logged in as ${existingUser.email}. Run \`vellum logout\` first, or use \`vellum login --force\` to re-authenticate.`,
|
|
178
|
+
);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
} catch {
|
|
181
|
+
// Token is stale/invalid — proceed with login
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
157
186
|
// If no --token flag, use browser-based login
|
|
158
187
|
if (!token) {
|
|
159
|
-
const
|
|
188
|
+
const webUrl = getWebUrl();
|
|
160
189
|
try {
|
|
161
|
-
token = await browserLogin(
|
|
190
|
+
token = await browserLogin(webUrl);
|
|
162
191
|
} catch (error) {
|
|
163
192
|
console.error(`❌ ${error instanceof Error ? error.message : error}`);
|
|
164
193
|
process.exit(1);
|
|
@@ -175,10 +204,7 @@ export async function login(): Promise<void> {
|
|
|
175
204
|
// Register the local assistant with the platform (non-fatal).
|
|
176
205
|
// Mirrors the desktop app's LocalAssistantBootstrapService flow.
|
|
177
206
|
try {
|
|
178
|
-
const
|
|
179
|
-
const entry = activeName
|
|
180
|
-
? findAssistantByName(activeName)
|
|
181
|
-
: loadLatestAssistant();
|
|
207
|
+
const entry = resolveAssistant();
|
|
182
208
|
|
|
183
209
|
// Skip managed ("vellum") assistants — they are handled by the platform.
|
|
184
210
|
if (entry && entry.cloud !== "vellum") {
|
|
@@ -247,6 +273,31 @@ export async function login(): Promise<void> {
|
|
|
247
273
|
} catch {
|
|
248
274
|
// Non-fatal — login succeeded even if registration fails
|
|
249
275
|
}
|
|
276
|
+
|
|
277
|
+
// Sync cloud assistants from the platform into the local lockfile.
|
|
278
|
+
// This ensures `vellum ps` shows managed assistants immediately
|
|
279
|
+
// after login (e.g. after a retire-and-rehatch cycle).
|
|
280
|
+
try {
|
|
281
|
+
const result = await syncCloudAssistants();
|
|
282
|
+
if (result) {
|
|
283
|
+
const total = result.added + result.removed;
|
|
284
|
+
if (total > 0) {
|
|
285
|
+
console.log(
|
|
286
|
+
`Synced cloud assistants (${result.added} added, ${result.removed} removed).`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// If no active assistant is set, activate the first cloud one.
|
|
292
|
+
if (!getActiveAssistant()) {
|
|
293
|
+
const platformAssistants = await fetchPlatformAssistants(token);
|
|
294
|
+
if (platformAssistants.length > 0) {
|
|
295
|
+
setActiveAssistant(platformAssistants[0].id);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// Non-fatal — login succeeded even if sync fails
|
|
300
|
+
}
|
|
250
301
|
} catch (error) {
|
|
251
302
|
console.error(
|
|
252
303
|
`❌ Login failed: ${error instanceof Error ? error.message : error}`,
|
|
@@ -261,11 +312,25 @@ export async function logout(): Promise<void> {
|
|
|
261
312
|
console.log("Usage: vellum logout");
|
|
262
313
|
console.log("");
|
|
263
314
|
console.log(
|
|
264
|
-
"Log out of the Vellum platform
|
|
315
|
+
"Log out of the Vellum platform, remove the stored session token,",
|
|
265
316
|
);
|
|
317
|
+
console.log("and remove cloud-managed assistants from the local lockfile.");
|
|
266
318
|
process.exit(0);
|
|
267
319
|
}
|
|
268
320
|
|
|
321
|
+
// Remove cloud-managed assistants from the lockfile.
|
|
322
|
+
const cloudAssistants = loadAllAssistants().filter(
|
|
323
|
+
(a) => a.cloud === "vellum",
|
|
324
|
+
);
|
|
325
|
+
for (const a of cloudAssistants) {
|
|
326
|
+
removeAssistantEntry(a.assistantId);
|
|
327
|
+
}
|
|
328
|
+
if (cloudAssistants.length > 0) {
|
|
329
|
+
console.log(
|
|
330
|
+
`Removed ${cloudAssistants.length} cloud assistant${cloudAssistants.length > 1 ? "s" : ""} from local lockfile.`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
269
334
|
clearPlatformToken();
|
|
270
335
|
console.log("Logged out. Platform token removed.");
|
|
271
336
|
}
|
package/src/commands/logs.ts
CHANGED
|
@@ -4,10 +4,7 @@ import { createInterface } from "readline";
|
|
|
4
4
|
import { watch } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
findAssistantByName,
|
|
9
|
-
loadLatestAssistant,
|
|
10
|
-
} from "../lib/assistant-config";
|
|
7
|
+
import { resolveAssistant } from "../lib/assistant-config";
|
|
11
8
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
12
9
|
import { dockerResourceNames } from "../lib/docker";
|
|
13
10
|
import { getLogDir } from "../lib/xdg-log";
|
|
@@ -593,9 +590,7 @@ async function showAwsLogs(
|
|
|
593
590
|
export async function logs(): Promise<void> {
|
|
594
591
|
const opts = parseArgs();
|
|
595
592
|
|
|
596
|
-
const entry = opts.name
|
|
597
|
-
? findAssistantByName(opts.name)
|
|
598
|
-
: loadLatestAssistant();
|
|
593
|
+
const entry = resolveAssistant(opts.name);
|
|
599
594
|
|
|
600
595
|
if (!entry) {
|
|
601
596
|
if (opts.name) {
|
package/src/commands/ps.ts
CHANGED
|
@@ -3,11 +3,18 @@ import { join } from "path";
|
|
|
3
3
|
import {
|
|
4
4
|
findAssistantByName,
|
|
5
5
|
getActiveAssistant,
|
|
6
|
+
getDaemonPidPath,
|
|
6
7
|
loadAllAssistants,
|
|
7
8
|
type AssistantEntry,
|
|
8
9
|
} from "../lib/assistant-config";
|
|
10
|
+
import { resolveEnvironmentSource } from "../lib/environments/resolve";
|
|
9
11
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
checkHealth,
|
|
14
|
+
checkManagedHealth,
|
|
15
|
+
fetchManagedPs,
|
|
16
|
+
type ManagedProcessEntry,
|
|
17
|
+
} from "../lib/health-check";
|
|
11
18
|
import { dockerResourceNames } from "../lib/docker";
|
|
12
19
|
import { existsSync } from "fs";
|
|
13
20
|
import {
|
|
@@ -21,6 +28,10 @@ import { pgrepExact } from "../lib/pgrep";
|
|
|
21
28
|
import { probePort } from "../lib/port-probe";
|
|
22
29
|
import { withStatusEmoji } from "../lib/status-emoji";
|
|
23
30
|
import { execOutput } from "../lib/step-runner";
|
|
31
|
+
import {
|
|
32
|
+
syncCloudAssistants,
|
|
33
|
+
type SyncLogger,
|
|
34
|
+
} from "../lib/sync-cloud-assistants";
|
|
24
35
|
|
|
25
36
|
// ── Table formatting helpers ────────────────────────────────────
|
|
26
37
|
|
|
@@ -65,6 +76,49 @@ function printTable(rows: TableRow[]): void {
|
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
// ── Managed process tree rendering ──────────────────────────────
|
|
80
|
+
|
|
81
|
+
const STATUS_LABELS: Record<ManagedProcessEntry["status"], string> = {
|
|
82
|
+
running: "running",
|
|
83
|
+
not_running: "not running",
|
|
84
|
+
unreachable: "unreachable",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function flattenProcessTree(
|
|
88
|
+
entries: ManagedProcessEntry[],
|
|
89
|
+
depth = 0,
|
|
90
|
+
): TableRow[] {
|
|
91
|
+
const rows: TableRow[] = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const children = entry.children ?? [];
|
|
94
|
+
|
|
95
|
+
rows.push({
|
|
96
|
+
name:
|
|
97
|
+
depth === 0 ? entry.name : `${" ".repeat(depth - 1)}├─ ${entry.name}`,
|
|
98
|
+
status: withStatusEmoji(STATUS_LABELS[entry.status]),
|
|
99
|
+
info: entry.info ?? "",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (let j = 0; j < children.length; j++) {
|
|
103
|
+
const child = children[j];
|
|
104
|
+
const isLast = j === children.length - 1;
|
|
105
|
+
const prefix = `${" ".repeat(depth)}${isLast ? "└─" : "├─"} ${child.name}`;
|
|
106
|
+
rows.push({
|
|
107
|
+
name: prefix,
|
|
108
|
+
status: withStatusEmoji(STATUS_LABELS[child.status]),
|
|
109
|
+
info: child.info ?? "",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Recurse into grandchildren
|
|
113
|
+
const grandchildren = child.children ?? [];
|
|
114
|
+
if (grandchildren.length > 0) {
|
|
115
|
+
rows.push(...flattenProcessTree(grandchildren, depth + 2));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return rows;
|
|
120
|
+
}
|
|
121
|
+
|
|
68
122
|
// ── Remote process listing via SSH ──────────────────────────────
|
|
69
123
|
|
|
70
124
|
const SSH_OPTS = [
|
|
@@ -215,37 +269,38 @@ async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
|
215
269
|
const resources = entry.resources;
|
|
216
270
|
const vellumDir = join(resources.instanceDir, ".vellum");
|
|
217
271
|
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
272
|
+
const assistantSpec: ProcessSpec = {
|
|
273
|
+
name: "assistant",
|
|
274
|
+
pgrepName: "vellum-daemon",
|
|
275
|
+
port: resources.daemonPort,
|
|
276
|
+
pidFile: getDaemonPidPath(resources),
|
|
277
|
+
};
|
|
278
|
+
const subSpecs: ProcessSpec[] = [
|
|
225
279
|
{
|
|
226
|
-
name: "qdrant",
|
|
280
|
+
name: "├─ qdrant",
|
|
227
281
|
pgrepName: "qdrant",
|
|
228
282
|
port: resources.qdrantPort,
|
|
229
283
|
pidFile: join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid"),
|
|
230
284
|
},
|
|
231
285
|
{
|
|
232
|
-
name: "
|
|
233
|
-
pgrepName: "vellum-gateway",
|
|
234
|
-
port: resources.gatewayPort,
|
|
235
|
-
pidFile: join(vellumDir, "gateway.pid"),
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
name: "embed-worker",
|
|
286
|
+
name: "└─ embed-worker",
|
|
239
287
|
pgrepName: "embed-worker",
|
|
240
288
|
port: 0,
|
|
241
289
|
pidFile: join(vellumDir, "workspace", "embed-worker.pid"),
|
|
242
290
|
},
|
|
243
291
|
];
|
|
292
|
+
const gatewaySpec: ProcessSpec = {
|
|
293
|
+
name: "gateway",
|
|
294
|
+
pgrepName: "vellum-gateway",
|
|
295
|
+
port: resources.gatewayPort,
|
|
296
|
+
pidFile: join(vellumDir, "gateway.pid"),
|
|
297
|
+
};
|
|
244
298
|
|
|
245
|
-
const
|
|
299
|
+
const allSpecs = [assistantSpec, ...subSpecs, gatewaySpec];
|
|
300
|
+
const results = await Promise.all(allSpecs.map(detectProcess));
|
|
246
301
|
|
|
247
|
-
return results.map((proc) => ({
|
|
248
|
-
name:
|
|
302
|
+
return results.map((proc, i) => ({
|
|
303
|
+
name: allSpecs[i].name,
|
|
249
304
|
status: withStatusEmoji(proc.running ? "running" : "not running"),
|
|
250
305
|
info: proc.running ? formatDetectionInfo(proc) : "not detected",
|
|
251
306
|
}));
|
|
@@ -335,6 +390,28 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
335
390
|
return;
|
|
336
391
|
}
|
|
337
392
|
|
|
393
|
+
if (cloud === "vellum") {
|
|
394
|
+
console.log(` Platform ID: ${entry.assistantId}\n`);
|
|
395
|
+
|
|
396
|
+
const psData = await fetchManagedPs(entry.runtimeUrl, entry.assistantId);
|
|
397
|
+
|
|
398
|
+
if (!psData) {
|
|
399
|
+
const rows: TableRow[] = [
|
|
400
|
+
{
|
|
401
|
+
name: "assistant",
|
|
402
|
+
status: withStatusEmoji("unreachable"),
|
|
403
|
+
info: "could not reach platform API — run `vellum login`",
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
printTable(rows);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const rows = flattenProcessTree(psData.processes);
|
|
411
|
+
printTable(rows);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
338
415
|
if (cloud === "apple-container") {
|
|
339
416
|
const mgmtSocket = entry.mgmtSocket as string | undefined;
|
|
340
417
|
const socketAlive = mgmtSocket ? existsSync(mgmtSocket) : false;
|
|
@@ -395,7 +472,40 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
395
472
|
|
|
396
473
|
// ── List all assistants (no arg) ────────────────────────────────
|
|
397
474
|
|
|
398
|
-
async function listAllAssistants(): Promise<void> {
|
|
475
|
+
async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
476
|
+
const { name: envName, source: envSource } = resolveEnvironmentSource();
|
|
477
|
+
const sourceLabels: Record<typeof envSource, string> = {
|
|
478
|
+
flag: "--environment flag",
|
|
479
|
+
env: "VELLUM_ENVIRONMENT",
|
|
480
|
+
config: "~/.config/vellum/environment",
|
|
481
|
+
default: "default",
|
|
482
|
+
};
|
|
483
|
+
console.log(`Environment: ${envName} (${sourceLabels[envSource]})`);
|
|
484
|
+
|
|
485
|
+
const log: SyncLogger | undefined = verbose
|
|
486
|
+
? (msg) => console.log(` [verbose] ${msg}`)
|
|
487
|
+
: undefined;
|
|
488
|
+
|
|
489
|
+
// Refresh cloud assistants from the platform before listing.
|
|
490
|
+
const syncResult = await syncCloudAssistants({ log });
|
|
491
|
+
|
|
492
|
+
// Show platform login status
|
|
493
|
+
if (syncResult) {
|
|
494
|
+
const parts = [`Platform: logged in`];
|
|
495
|
+
if (syncResult.email) parts[0] += ` as ${syncResult.email}`;
|
|
496
|
+
if (syncResult.added > 0 || syncResult.removed > 0) {
|
|
497
|
+
const changes: string[] = [];
|
|
498
|
+
if (syncResult.added > 0) changes.push(`${syncResult.added} added`);
|
|
499
|
+
if (syncResult.removed > 0)
|
|
500
|
+
changes.push(`${syncResult.removed} removed`);
|
|
501
|
+
parts.push(`(${changes.join(", ")})`);
|
|
502
|
+
}
|
|
503
|
+
console.log(parts.join(" "));
|
|
504
|
+
} else {
|
|
505
|
+
console.log("Platform: not logged in");
|
|
506
|
+
}
|
|
507
|
+
console.log("");
|
|
508
|
+
|
|
399
509
|
const assistants = loadAllAssistants();
|
|
400
510
|
const activeId = getActiveAssistant();
|
|
401
511
|
|
|
@@ -454,7 +564,9 @@ async function listAllAssistants(): Promise<void> {
|
|
|
454
564
|
let health: { status: string; detail: string | null; version?: string };
|
|
455
565
|
const resources = a.resources;
|
|
456
566
|
if (a.cloud === "local" && resources) {
|
|
457
|
-
|
|
567
|
+
// TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
|
|
568
|
+
// fetching daemon PIDs via the health API (Gateway Security Migration).
|
|
569
|
+
const pid = readPidFile(getDaemonPidPath(resources));
|
|
458
570
|
const alive = pid !== null && isProcessAlive(pid);
|
|
459
571
|
if (!alive) {
|
|
460
572
|
health = { status: "sleeping", detail: null };
|
|
@@ -515,21 +627,28 @@ async function listAllAssistants(): Promise<void> {
|
|
|
515
627
|
export async function ps(): Promise<void> {
|
|
516
628
|
const args = process.argv.slice(3);
|
|
517
629
|
if (args.includes("--help") || args.includes("-h")) {
|
|
518
|
-
console.log("Usage: vellum ps [<name>]");
|
|
630
|
+
console.log("Usage: vellum ps [<name>] [--verbose]");
|
|
519
631
|
console.log("");
|
|
520
632
|
console.log(
|
|
521
633
|
"List all assistants, or show processes for a specific assistant.",
|
|
522
634
|
);
|
|
523
635
|
console.log("");
|
|
524
636
|
console.log("Arguments:");
|
|
525
|
-
console.log(" <name>
|
|
637
|
+
console.log(" <name> Show processes for the named assistant");
|
|
638
|
+
console.log("");
|
|
639
|
+
console.log("Options:");
|
|
640
|
+
console.log(
|
|
641
|
+
" --verbose Show diagnostic logs (platform sync, auth issues)",
|
|
642
|
+
);
|
|
526
643
|
process.exit(0);
|
|
527
644
|
}
|
|
528
645
|
|
|
529
|
-
const
|
|
646
|
+
const verbose = args.includes("--verbose");
|
|
647
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
648
|
+
const assistantId = positional[0];
|
|
530
649
|
|
|
531
650
|
if (!assistantId) {
|
|
532
|
-
await listAllAssistants();
|
|
651
|
+
await listAllAssistants(verbose);
|
|
533
652
|
return;
|
|
534
653
|
}
|
|
535
654
|
|
package/src/commands/restore.ts
CHANGED
|
@@ -9,13 +9,11 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
readPlatformToken,
|
|
11
11
|
rollbackPlatformAssistant,
|
|
12
|
-
|
|
13
|
-
platformImportBundle,
|
|
14
|
-
platformRequestUploadUrl,
|
|
12
|
+
platformRequestSignedUrl,
|
|
15
13
|
platformUploadToSignedUrl,
|
|
16
14
|
platformImportPreflightFromGcs,
|
|
17
15
|
platformImportBundleFromGcs,
|
|
18
|
-
|
|
16
|
+
platformPollJobStatus,
|
|
19
17
|
} from "../lib/platform-client.js";
|
|
20
18
|
import { performDockerRollback } from "../lib/upgrade-lifecycle.js";
|
|
21
19
|
|
|
@@ -181,24 +179,14 @@ async function restorePlatform(
|
|
|
181
179
|
process.exit(1);
|
|
182
180
|
}
|
|
183
181
|
|
|
184
|
-
// Step 1.5 — Upload to GCS via signed URL
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log("Uploading bundle...");
|
|
193
|
-
await platformUploadToSignedUrl(uploadUrl, new Uint8Array(bundleData));
|
|
194
|
-
} catch (err) {
|
|
195
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
-
if (msg.includes("not available")) {
|
|
197
|
-
bundleKey = null;
|
|
198
|
-
} else {
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
182
|
+
// Step 1.5 — Upload to GCS via signed URL
|
|
183
|
+
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
184
|
+
{ operation: "upload" },
|
|
185
|
+
token,
|
|
186
|
+
entry.runtimeUrl,
|
|
187
|
+
);
|
|
188
|
+
console.log("Uploading bundle...");
|
|
189
|
+
await platformUploadToSignedUrl(uploadUrl, new Uint8Array(bundleData));
|
|
202
190
|
|
|
203
191
|
// Step 2 — Dry-run path
|
|
204
192
|
if (opts.dryRun) {
|
|
@@ -213,17 +201,11 @@ async function restorePlatform(
|
|
|
213
201
|
|
|
214
202
|
let preflightResult: { statusCode: number; body: Record<string, unknown> };
|
|
215
203
|
try {
|
|
216
|
-
preflightResult =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
: await platformImportPreflight(
|
|
223
|
-
new Uint8Array(bundleData),
|
|
224
|
-
token,
|
|
225
|
-
entry.runtimeUrl,
|
|
226
|
-
);
|
|
204
|
+
preflightResult = await platformImportPreflightFromGcs(
|
|
205
|
+
bundleKey,
|
|
206
|
+
token,
|
|
207
|
+
entry.runtimeUrl,
|
|
208
|
+
);
|
|
227
209
|
} catch (err) {
|
|
228
210
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
229
211
|
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
@@ -353,13 +335,11 @@ async function restorePlatform(
|
|
|
353
335
|
|
|
354
336
|
let importResult: { statusCode: number; body: Record<string, unknown> };
|
|
355
337
|
try {
|
|
356
|
-
importResult =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
entry.runtimeUrl,
|
|
362
|
-
);
|
|
338
|
+
importResult = await platformImportBundleFromGcs(
|
|
339
|
+
bundleKey,
|
|
340
|
+
token,
|
|
341
|
+
entry.runtimeUrl,
|
|
342
|
+
);
|
|
363
343
|
} catch (err) {
|
|
364
344
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
365
345
|
console.error("Error: Import request timed out after 5 minutes.");
|
|
@@ -420,13 +400,9 @@ async function restorePlatform(
|
|
|
420
400
|
while (Date.now() < deadline) {
|
|
421
401
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
422
402
|
|
|
423
|
-
let status:
|
|
424
|
-
status: string;
|
|
425
|
-
result?: Record<string, unknown>;
|
|
426
|
-
error?: string;
|
|
427
|
-
};
|
|
403
|
+
let status: Awaited<ReturnType<typeof platformPollJobStatus>>;
|
|
428
404
|
try {
|
|
429
|
-
status = await
|
|
405
|
+
status = await platformPollJobStatus(jobId, token, entry.runtimeUrl);
|
|
430
406
|
} catch (err) {
|
|
431
407
|
const msg = err instanceof Error ? err.message : String(err);
|
|
432
408
|
if (msg.includes("not found")) {
|
|
@@ -451,7 +427,10 @@ async function restorePlatform(
|
|
|
451
427
|
}
|
|
452
428
|
|
|
453
429
|
if (status.status === "complete") {
|
|
454
|
-
importResult = {
|
|
430
|
+
importResult = {
|
|
431
|
+
statusCode: 200,
|
|
432
|
+
body: (status.result as Record<string, unknown>) ?? {},
|
|
433
|
+
};
|
|
455
434
|
break;
|
|
456
435
|
}
|
|
457
436
|
|
package/src/commands/sleep.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getDaemonPidPath,
|
|
6
|
+
resolveTargetAssistant,
|
|
7
|
+
} from "../lib/assistant-config.js";
|
|
5
8
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
6
9
|
import { dockerResourceNames, sleepContainers } from "../lib/docker.js";
|
|
7
10
|
import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
|
|
@@ -93,7 +96,7 @@ export async function sleep(): Promise<void> {
|
|
|
93
96
|
process.exit(1);
|
|
94
97
|
}
|
|
95
98
|
const resources = entry.resources;
|
|
96
|
-
const assistantPidFile = resources
|
|
99
|
+
const assistantPidFile = getDaemonPidPath(resources);
|
|
97
100
|
const vellumDir = getAssistantRootDir(entry);
|
|
98
101
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
99
102
|
|
package/src/commands/ssh.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
findAssistantByName,
|
|
5
|
-
loadLatestAssistant,
|
|
6
|
-
} from "../lib/assistant-config";
|
|
3
|
+
import { resolveAssistant } from "../lib/assistant-config";
|
|
7
4
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
8
5
|
import { dockerResourceNames } from "../lib/docker";
|
|
6
|
+
import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
|
|
9
7
|
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
8
|
+
import { interactiveSession } from "../lib/terminal-session";
|
|
10
9
|
|
|
11
10
|
const SSH_OPTS = [
|
|
12
11
|
"-o",
|
|
@@ -56,7 +55,7 @@ export async function ssh(): Promise<void> {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
const name = process.argv[3];
|
|
59
|
-
const entry =
|
|
58
|
+
const entry = resolveAssistant(name);
|
|
60
59
|
|
|
61
60
|
if (!entry) {
|
|
62
61
|
if (name) {
|
|
@@ -121,8 +120,19 @@ export async function ssh(): Promise<void> {
|
|
|
121
120
|
{ stdio: "inherit" },
|
|
122
121
|
);
|
|
123
122
|
} else if (cloud === "vellum") {
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
const token = readPlatformToken();
|
|
124
|
+
if (!token) {
|
|
125
|
+
console.error(
|
|
126
|
+
"Not logged in. Run `vellum login` first to authenticate with the platform.",
|
|
127
|
+
);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
await interactiveSession({
|
|
131
|
+
assistantId: entry.assistantId,
|
|
132
|
+
token,
|
|
133
|
+
platformUrl: getPlatformUrl(),
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
126
136
|
} else if (cloud === "custom") {
|
|
127
137
|
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
128
138
|
const sshUser = entry.sshUser ?? "root";
|