@vellumai/cli 0.6.2 → 0.6.4
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 +12 -2
- package/README.md +3 -3
- package/bunfig.toml +6 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +124 -0
- package/src/__tests__/env-drift.test.ts +87 -0
- package/src/__tests__/guardian-token.test.ts +172 -0
- package/src/__tests__/multi-local.test.ts +61 -14
- package/src/__tests__/orphan-detection.test.ts +214 -0
- package/src/__tests__/platform-client.test.ts +204 -0
- package/src/__tests__/preload.ts +27 -0
- package/src/__tests__/ssh-user-guard.test.ts +28 -0
- package/src/__tests__/teleport.test.ts +1073 -57
- package/src/commands/backup.ts +8 -0
- package/src/commands/hatch.ts +5 -28
- package/src/commands/login.ts +178 -9
- package/src/commands/logs.ts +652 -0
- package/src/commands/pair.ts +9 -1
- package/src/commands/ps.ts +37 -7
- package/src/commands/recover.ts +8 -4
- package/src/commands/restore.ts +124 -12
- package/src/commands/retire.ts +17 -3
- package/src/commands/rollback.ts +32 -33
- package/src/commands/sleep.ts +7 -0
- package/src/commands/ssh-apple-container.ts +162 -0
- package/src/commands/ssh.ts +7 -0
- package/src/commands/teleport.ts +307 -3
- package/src/commands/upgrade.ts +43 -52
- package/src/commands/wake.ts +21 -10
- package/src/components/DefaultMainScreen.tsx +7 -1
- package/src/index.ts +3 -0
- package/src/lib/__tests__/docker.test.ts +78 -0
- package/src/lib/assistant-config.ts +54 -87
- package/src/lib/aws.ts +12 -1
- package/src/lib/constants.ts +0 -10
- package/src/lib/docker.ts +73 -4
- package/src/lib/environments/__tests__/paths.test.ts +234 -0
- package/src/lib/environments/__tests__/resolve.test.ts +226 -0
- package/src/lib/environments/paths.ts +110 -0
- package/src/lib/environments/resolve.ts +96 -0
- package/src/lib/environments/seeds.ts +46 -0
- package/src/lib/environments/types.ts +60 -0
- package/src/lib/gcp.ts +12 -1
- package/src/lib/guardian-token.ts +8 -10
- package/src/lib/hatch-local.ts +30 -35
- package/src/lib/local.ts +46 -5
- package/src/lib/orphan-detection.ts +28 -12
- package/src/lib/platform-client.ts +261 -25
- package/src/lib/retire-apple-container.ts +102 -0
- package/src/lib/upgrade-lifecycle.ts +101 -28
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { createReadStream, existsSync, statSync } from "fs";
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
|
+
import { watch } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
findAssistantByName,
|
|
9
|
+
loadLatestAssistant,
|
|
10
|
+
} from "../lib/assistant-config";
|
|
11
|
+
import type { AssistantEntry } from "../lib/assistant-config";
|
|
12
|
+
import { dockerResourceNames } from "../lib/docker";
|
|
13
|
+
import { getLogDir } from "../lib/xdg-log";
|
|
14
|
+
import { execOutput } from "../lib/step-runner";
|
|
15
|
+
|
|
16
|
+
// ── Arg parsing ─────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
interface LogsArgs {
|
|
19
|
+
name?: string;
|
|
20
|
+
follow: boolean;
|
|
21
|
+
tail?: number;
|
|
22
|
+
timestamps: boolean;
|
|
23
|
+
since?: string;
|
|
24
|
+
until?: string;
|
|
25
|
+
service?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function printHelp(): void {
|
|
29
|
+
console.log("Usage: vellum logs [<name>] [options]");
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log("View logs from an assistant instance.");
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log("Arguments:");
|
|
34
|
+
console.log(
|
|
35
|
+
" <name> Name of the assistant (defaults to latest)",
|
|
36
|
+
);
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("Options:");
|
|
39
|
+
console.log(" -f, --follow Follow log output (stream new lines)");
|
|
40
|
+
console.log(" -n, --tail <N> Show last N lines (default: all)");
|
|
41
|
+
console.log(" -t, --timestamps Show timestamps on each line");
|
|
42
|
+
console.log(
|
|
43
|
+
" --since <time> Show logs since timestamp or relative (e.g. 10m, 2h)",
|
|
44
|
+
);
|
|
45
|
+
console.log(" --until <time> Show logs until timestamp or relative");
|
|
46
|
+
console.log(
|
|
47
|
+
" -s, --service <name> Filter to a specific service (e.g. assistant, gateway)",
|
|
48
|
+
);
|
|
49
|
+
console.log(" -h, --help Show this help");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseArgs(): LogsArgs {
|
|
53
|
+
const args = process.argv.slice(3);
|
|
54
|
+
const result: LogsArgs = {
|
|
55
|
+
follow: false,
|
|
56
|
+
timestamps: false,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < args.length; i++) {
|
|
60
|
+
const arg = args[i];
|
|
61
|
+
if (arg === "--help" || arg === "-h") {
|
|
62
|
+
printHelp();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
} else if (arg === "-f" || arg === "--follow") {
|
|
65
|
+
result.follow = true;
|
|
66
|
+
} else if (arg === "-t" || arg === "--timestamps") {
|
|
67
|
+
result.timestamps = true;
|
|
68
|
+
} else if (arg === "-n" || arg === "--tail") {
|
|
69
|
+
const next = args[i + 1];
|
|
70
|
+
if (!next || next.startsWith("-")) {
|
|
71
|
+
console.error("Error: --tail requires a numeric value");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const n = parseInt(next, 10);
|
|
75
|
+
if (isNaN(n) || n < 0) {
|
|
76
|
+
console.error("Error: --tail must be a non-negative integer");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
result.tail = n;
|
|
80
|
+
i++;
|
|
81
|
+
} else if (arg === "--since") {
|
|
82
|
+
const next = args[i + 1];
|
|
83
|
+
if (!next || next.startsWith("-")) {
|
|
84
|
+
console.error("Error: --since requires a value");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
result.since = next;
|
|
88
|
+
i++;
|
|
89
|
+
} else if (arg === "--until") {
|
|
90
|
+
const next = args[i + 1];
|
|
91
|
+
if (!next || next.startsWith("-")) {
|
|
92
|
+
console.error("Error: --until requires a value");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
result.until = next;
|
|
96
|
+
i++;
|
|
97
|
+
} else if (arg === "-s" || arg === "--service") {
|
|
98
|
+
const next = args[i + 1];
|
|
99
|
+
if (!next || next.startsWith("-")) {
|
|
100
|
+
console.error("Error: --service requires a value");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
result.service = next;
|
|
104
|
+
i++;
|
|
105
|
+
} else if (!arg.startsWith("-") && !result.name) {
|
|
106
|
+
result.name = arg;
|
|
107
|
+
} else {
|
|
108
|
+
console.error(`Error: Unknown argument '${arg}'`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function resolveCloud(entry: AssistantEntry): string {
|
|
119
|
+
if (entry.cloud) return entry.cloud;
|
|
120
|
+
if (entry.project) return "gcp";
|
|
121
|
+
if (entry.sshUser) return "custom";
|
|
122
|
+
return "local";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parse a relative time string like "10m", "2h", "30s" into a Date.
|
|
127
|
+
* Returns null if the string doesn't look like a relative time.
|
|
128
|
+
*/
|
|
129
|
+
function parseRelativeTime(input: string): Date | null {
|
|
130
|
+
const match = input.match(/^(\d+)([smhd])$/);
|
|
131
|
+
if (!match) return null;
|
|
132
|
+
const amount = parseInt(match[1], 10);
|
|
133
|
+
const unit = match[2];
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const ms: Record<string, number> = {
|
|
136
|
+
s: 1000,
|
|
137
|
+
m: 60_000,
|
|
138
|
+
h: 3_600_000,
|
|
139
|
+
d: 86_400_000,
|
|
140
|
+
};
|
|
141
|
+
return new Date(now - amount * (ms[unit] ?? 0));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse a --since/--until value into a Date.
|
|
146
|
+
* Accepts relative times (10m, 2h) or ISO timestamps.
|
|
147
|
+
*/
|
|
148
|
+
function parseTimeFilter(input: string): Date | null {
|
|
149
|
+
const relative = parseRelativeTime(input);
|
|
150
|
+
if (relative) return relative;
|
|
151
|
+
const date = new Date(input);
|
|
152
|
+
return isNaN(date.getTime()) ? null : date;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract the ISO timestamp from a log line that starts with one.
|
|
157
|
+
* Local log lines have format: `2024-01-15T10:30:00.000Z [tag] message`
|
|
158
|
+
*/
|
|
159
|
+
function extractTimestamp(line: string): Date | null {
|
|
160
|
+
const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\d.]*Z?)\s/);
|
|
161
|
+
if (!match) return null;
|
|
162
|
+
const date = new Date(match[1]);
|
|
163
|
+
return isNaN(date.getTime()) ? null : date;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extract the service tag from a local log line.
|
|
168
|
+
* Format: `2024-01-15T10:30:00.000Z [daemon] message` → "daemon"
|
|
169
|
+
*/
|
|
170
|
+
function extractServiceTag(line: string): string | null {
|
|
171
|
+
const match = line.match(/^\S+\s+\[(\w+[-\w]*)\]\s/);
|
|
172
|
+
return match ? match[1] : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Local topology ──────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
async function showLocalLogs(
|
|
178
|
+
entry: AssistantEntry,
|
|
179
|
+
opts: LogsArgs,
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const logDir = getLogDir();
|
|
182
|
+
const logFile = join(logDir, "hatch.log");
|
|
183
|
+
|
|
184
|
+
if (!existsSync(logFile)) {
|
|
185
|
+
console.error(
|
|
186
|
+
`No log file found at ${logFile}. Has the assistant been started?`,
|
|
187
|
+
);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sinceDate = opts.since ? parseTimeFilter(opts.since) : null;
|
|
192
|
+
const untilDate = opts.until ? parseTimeFilter(opts.until) : null;
|
|
193
|
+
|
|
194
|
+
if (opts.since && !sinceDate) {
|
|
195
|
+
console.error(
|
|
196
|
+
`Error: Could not parse --since value '${opts.since}'. Use relative (e.g. 10m, 2h) or ISO format.`,
|
|
197
|
+
);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
if (opts.until && !untilDate) {
|
|
201
|
+
console.error(
|
|
202
|
+
`Error: Could not parse --until value '${opts.until}'. Use relative (e.g. 10m, 2h) or ISO format.`,
|
|
203
|
+
);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function matchesFilters(line: string): boolean {
|
|
208
|
+
if (opts.service) {
|
|
209
|
+
const tag = extractServiceTag(line);
|
|
210
|
+
if (tag && tag !== opts.service) return false;
|
|
211
|
+
}
|
|
212
|
+
if (sinceDate || untilDate) {
|
|
213
|
+
const ts = extractTimestamp(line);
|
|
214
|
+
if (ts) {
|
|
215
|
+
if (sinceDate && ts < sinceDate) return false;
|
|
216
|
+
if (untilDate && ts > untilDate) return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Read existing file content
|
|
223
|
+
const lines: string[] = [];
|
|
224
|
+
const rl = createInterface({
|
|
225
|
+
input: createReadStream(logFile, { encoding: "utf-8" }),
|
|
226
|
+
crlfDelay: Infinity,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
for await (const line of rl) {
|
|
230
|
+
if (!matchesFilters(line)) continue;
|
|
231
|
+
lines.push(line);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Apply --tail (explicit check for 0 since slice(-0) returns the whole array)
|
|
235
|
+
const output =
|
|
236
|
+
opts.tail != null
|
|
237
|
+
? opts.tail === 0
|
|
238
|
+
? []
|
|
239
|
+
: lines.slice(-opts.tail)
|
|
240
|
+
: lines;
|
|
241
|
+
for (const line of output) {
|
|
242
|
+
console.log(line);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Follow mode: watch for changes
|
|
246
|
+
if (opts.follow) {
|
|
247
|
+
let fileSize = statSync(logFile).size;
|
|
248
|
+
|
|
249
|
+
watch(logFile, () => {
|
|
250
|
+
let newSize: number;
|
|
251
|
+
try {
|
|
252
|
+
newSize = statSync(logFile).size;
|
|
253
|
+
} catch {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (newSize <= fileSize) {
|
|
257
|
+
fileSize = newSize;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const stream = createReadStream(logFile, {
|
|
262
|
+
start: fileSize,
|
|
263
|
+
encoding: "utf-8",
|
|
264
|
+
});
|
|
265
|
+
const followRl = createInterface({
|
|
266
|
+
input: stream,
|
|
267
|
+
crlfDelay: Infinity,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
followRl.on("line", (line: string) => {
|
|
271
|
+
if (matchesFilters(line)) {
|
|
272
|
+
console.log(line);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
followRl.on("close", () => {
|
|
277
|
+
try {
|
|
278
|
+
fileSize = statSync(logFile).size;
|
|
279
|
+
} catch {
|
|
280
|
+
// File may have been removed
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Keep process alive
|
|
286
|
+
await new Promise<void>((resolve) => {
|
|
287
|
+
process.on("SIGINT", () => {
|
|
288
|
+
resolve();
|
|
289
|
+
});
|
|
290
|
+
process.on("SIGTERM", () => {
|
|
291
|
+
resolve();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Docker topology ─────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
async function showDockerLogs(
|
|
300
|
+
entry: AssistantEntry,
|
|
301
|
+
opts: LogsArgs,
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
const res = dockerResourceNames(entry.assistantId);
|
|
304
|
+
|
|
305
|
+
const containers: { name: string; containerName: string }[] = [
|
|
306
|
+
{ name: "assistant", containerName: res.assistantContainer },
|
|
307
|
+
{ name: "gateway", containerName: res.gatewayContainer },
|
|
308
|
+
{ name: "credential-executor", containerName: res.cesContainer },
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
// Filter to specific service if requested
|
|
312
|
+
const targets = opts.service
|
|
313
|
+
? containers.filter((c) => c.name === opts.service)
|
|
314
|
+
: containers;
|
|
315
|
+
|
|
316
|
+
if (targets.length === 0) {
|
|
317
|
+
console.error(
|
|
318
|
+
`Unknown service '${opts.service}'. Available: ${containers.map((c) => c.name).join(", ")}`,
|
|
319
|
+
);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Build docker logs args
|
|
324
|
+
function buildDockerArgs(containerName: string): string[] {
|
|
325
|
+
const args = ["logs"];
|
|
326
|
+
if (opts.follow) args.push("--follow");
|
|
327
|
+
if (opts.tail != null) args.push("--tail", String(opts.tail));
|
|
328
|
+
if (opts.timestamps) args.push("--timestamps");
|
|
329
|
+
if (opts.since) args.push("--since", opts.since);
|
|
330
|
+
if (opts.until) args.push("--until", opts.until);
|
|
331
|
+
args.push(containerName);
|
|
332
|
+
return args;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (targets.length === 1) {
|
|
336
|
+
// Single container — stream directly to stdout/stderr
|
|
337
|
+
const target = targets[0];
|
|
338
|
+
const args = buildDockerArgs(target.containerName);
|
|
339
|
+
const child = spawn("docker", args, { stdio: "inherit" });
|
|
340
|
+
|
|
341
|
+
await new Promise<void>((resolve, reject) => {
|
|
342
|
+
child.on("close", (code) => {
|
|
343
|
+
if (code === 0 || (opts.follow && code === null)) {
|
|
344
|
+
resolve();
|
|
345
|
+
} else {
|
|
346
|
+
reject(
|
|
347
|
+
new Error(
|
|
348
|
+
`docker logs for ${target.name} exited with code ${code}`,
|
|
349
|
+
),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
child.on("error", (err) => {
|
|
354
|
+
if (err.message.includes("ENOENT")) {
|
|
355
|
+
console.error("Error: docker is not installed or not on PATH.");
|
|
356
|
+
}
|
|
357
|
+
reject(err);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
// Multiple containers — prefix each line with service name
|
|
362
|
+
const children = targets.map((target) => {
|
|
363
|
+
const args = buildDockerArgs(target.containerName);
|
|
364
|
+
const child = spawn("docker", args, {
|
|
365
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const prefix = `[${target.name}] `;
|
|
369
|
+
|
|
370
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
371
|
+
if (!stream) continue;
|
|
372
|
+
const rl = createInterface({
|
|
373
|
+
input: stream,
|
|
374
|
+
crlfDelay: Infinity,
|
|
375
|
+
});
|
|
376
|
+
rl.on("line", (line: string) => {
|
|
377
|
+
console.log(prefix + line);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { target, child };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Wait for all children to exit and track failures
|
|
385
|
+
const errors: string[] = [];
|
|
386
|
+
await Promise.all(
|
|
387
|
+
children.map(
|
|
388
|
+
({ target, child }) =>
|
|
389
|
+
new Promise<void>((resolve) => {
|
|
390
|
+
child.on("close", (code) => {
|
|
391
|
+
if (code !== 0 && code !== null) {
|
|
392
|
+
errors.push(
|
|
393
|
+
`docker logs for ${target.name} exited with code ${code}`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
resolve();
|
|
397
|
+
});
|
|
398
|
+
child.on("error", (err) => {
|
|
399
|
+
errors.push(
|
|
400
|
+
`docker logs for ${target.name} failed: ${err.message}`,
|
|
401
|
+
);
|
|
402
|
+
resolve();
|
|
403
|
+
});
|
|
404
|
+
}),
|
|
405
|
+
),
|
|
406
|
+
);
|
|
407
|
+
if (errors.length > 0) {
|
|
408
|
+
for (const msg of errors) {
|
|
409
|
+
console.error(msg);
|
|
410
|
+
}
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── Remote topologies (GCP / Custom / AWS) ──────────────────────
|
|
417
|
+
|
|
418
|
+
const SSH_OPTS = [
|
|
419
|
+
"-o",
|
|
420
|
+
"StrictHostKeyChecking=no",
|
|
421
|
+
"-o",
|
|
422
|
+
"UserKnownHostsFile=/dev/null",
|
|
423
|
+
"-o",
|
|
424
|
+
"ConnectTimeout=10",
|
|
425
|
+
"-o",
|
|
426
|
+
"LogLevel=ERROR",
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
function buildRemoteLogCommand(opts: LogsArgs): string {
|
|
430
|
+
const logFile = "/var/log/startup-script.log";
|
|
431
|
+
const parts: string[] = [];
|
|
432
|
+
|
|
433
|
+
if (opts.follow) {
|
|
434
|
+
const tailN = opts.tail != null ? `-n ${opts.tail}` : "-n +1";
|
|
435
|
+
parts.push(`tail ${tailN} -f ${logFile}`);
|
|
436
|
+
} else if (opts.tail != null) {
|
|
437
|
+
parts.push(`tail -n ${opts.tail} ${logFile}`);
|
|
438
|
+
} else {
|
|
439
|
+
parts.push(`cat ${logFile}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return parts.join(" ");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function showGcpLogs(
|
|
446
|
+
entry: AssistantEntry,
|
|
447
|
+
opts: LogsArgs,
|
|
448
|
+
): Promise<void> {
|
|
449
|
+
const project = entry.project;
|
|
450
|
+
const zone = entry.zone;
|
|
451
|
+
if (!project || !zone) {
|
|
452
|
+
console.error("Error: GCP project and zone not found in assistant config.");
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const remoteCmd = buildRemoteLogCommand(opts);
|
|
457
|
+
const sshTarget = entry.sshUser
|
|
458
|
+
? `${entry.sshUser}@${entry.assistantId}`
|
|
459
|
+
: entry.assistantId;
|
|
460
|
+
|
|
461
|
+
const args = [
|
|
462
|
+
"compute",
|
|
463
|
+
"ssh",
|
|
464
|
+
sshTarget,
|
|
465
|
+
`--project=${project}`,
|
|
466
|
+
`--zone=${zone}`,
|
|
467
|
+
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
468
|
+
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
469
|
+
"--ssh-flag=-o ConnectTimeout=10",
|
|
470
|
+
"--ssh-flag=-o LogLevel=ERROR",
|
|
471
|
+
`--command=${remoteCmd}`,
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
if (opts.follow) {
|
|
475
|
+
// For follow mode, stream output directly to terminal
|
|
476
|
+
const child = spawn("gcloud", args, { stdio: "inherit" });
|
|
477
|
+
await new Promise<void>((resolve, reject) => {
|
|
478
|
+
child.on("close", (code) => {
|
|
479
|
+
if (code !== 0 && code !== null) {
|
|
480
|
+
reject(new Error(`gcloud ssh exited with code ${code}`));
|
|
481
|
+
} else {
|
|
482
|
+
resolve();
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
child.on("error", (err) => reject(err));
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
try {
|
|
489
|
+
const output = await execOutput("gcloud", args);
|
|
490
|
+
console.log(output);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.error(
|
|
493
|
+
`Failed to fetch logs: ${err instanceof Error ? err.message : err}`,
|
|
494
|
+
);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function extractHostFromUrl(url: string): string {
|
|
501
|
+
try {
|
|
502
|
+
const parsed = new URL(url);
|
|
503
|
+
return parsed.hostname;
|
|
504
|
+
} catch {
|
|
505
|
+
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function showCustomLogs(
|
|
510
|
+
entry: AssistantEntry,
|
|
511
|
+
opts: LogsArgs,
|
|
512
|
+
): Promise<void> {
|
|
513
|
+
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
514
|
+
const sshUser = entry.sshUser ?? "root";
|
|
515
|
+
const sshTarget = `${sshUser}@${host}`;
|
|
516
|
+
|
|
517
|
+
const remoteCmd = buildRemoteLogCommand(opts);
|
|
518
|
+
|
|
519
|
+
if (opts.follow) {
|
|
520
|
+
const child = spawn("ssh", [...SSH_OPTS, sshTarget, remoteCmd], {
|
|
521
|
+
stdio: "inherit",
|
|
522
|
+
});
|
|
523
|
+
await new Promise<void>((resolve, reject) => {
|
|
524
|
+
child.on("close", (code) => {
|
|
525
|
+
if (code !== 0 && code !== null) {
|
|
526
|
+
reject(new Error(`ssh exited with code ${code}`));
|
|
527
|
+
} else {
|
|
528
|
+
resolve();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
child.on("error", (err) => reject(err));
|
|
532
|
+
});
|
|
533
|
+
} else {
|
|
534
|
+
try {
|
|
535
|
+
const output = await execOutput("ssh", [
|
|
536
|
+
...SSH_OPTS,
|
|
537
|
+
sshTarget,
|
|
538
|
+
remoteCmd,
|
|
539
|
+
]);
|
|
540
|
+
console.log(output);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.error(
|
|
543
|
+
`Failed to fetch logs: ${err instanceof Error ? err.message : err}`,
|
|
544
|
+
);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function showAwsLogs(
|
|
551
|
+
entry: AssistantEntry,
|
|
552
|
+
opts: LogsArgs,
|
|
553
|
+
): Promise<void> {
|
|
554
|
+
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
555
|
+
const sshUser = entry.sshUser ?? "admin";
|
|
556
|
+
const sshTarget = `${sshUser}@${host}`;
|
|
557
|
+
|
|
558
|
+
const remoteCmd = buildRemoteLogCommand(opts);
|
|
559
|
+
|
|
560
|
+
if (opts.follow) {
|
|
561
|
+
const child = spawn("ssh", [...SSH_OPTS, sshTarget, remoteCmd], {
|
|
562
|
+
stdio: "inherit",
|
|
563
|
+
});
|
|
564
|
+
await new Promise<void>((resolve, reject) => {
|
|
565
|
+
child.on("close", (code) => {
|
|
566
|
+
if (code !== 0 && code !== null) {
|
|
567
|
+
reject(new Error(`ssh exited with code ${code}`));
|
|
568
|
+
} else {
|
|
569
|
+
resolve();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
child.on("error", (err) => reject(err));
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
try {
|
|
576
|
+
const output = await execOutput("ssh", [
|
|
577
|
+
...SSH_OPTS,
|
|
578
|
+
sshTarget,
|
|
579
|
+
remoteCmd,
|
|
580
|
+
]);
|
|
581
|
+
console.log(output);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
console.error(
|
|
584
|
+
`Failed to fetch logs: ${err instanceof Error ? err.message : err}`,
|
|
585
|
+
);
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ── Entry point ─────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
export async function logs(): Promise<void> {
|
|
594
|
+
const opts = parseArgs();
|
|
595
|
+
|
|
596
|
+
const entry = opts.name
|
|
597
|
+
? findAssistantByName(opts.name)
|
|
598
|
+
: loadLatestAssistant();
|
|
599
|
+
|
|
600
|
+
if (!entry) {
|
|
601
|
+
if (opts.name) {
|
|
602
|
+
console.error(`No assistant found with name '${opts.name}'.`);
|
|
603
|
+
} else {
|
|
604
|
+
console.error("No assistant found. Run `vellum hatch` first.");
|
|
605
|
+
}
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const cloud = resolveCloud(entry);
|
|
610
|
+
|
|
611
|
+
switch (cloud) {
|
|
612
|
+
case "local":
|
|
613
|
+
await showLocalLogs(entry, opts);
|
|
614
|
+
break;
|
|
615
|
+
|
|
616
|
+
case "docker":
|
|
617
|
+
await showDockerLogs(entry, opts);
|
|
618
|
+
break;
|
|
619
|
+
|
|
620
|
+
case "gcp":
|
|
621
|
+
await showGcpLogs(entry, opts);
|
|
622
|
+
break;
|
|
623
|
+
|
|
624
|
+
case "custom":
|
|
625
|
+
await showCustomLogs(entry, opts);
|
|
626
|
+
break;
|
|
627
|
+
|
|
628
|
+
case "aws":
|
|
629
|
+
await showAwsLogs(entry, opts);
|
|
630
|
+
break;
|
|
631
|
+
|
|
632
|
+
case "vellum":
|
|
633
|
+
console.error(
|
|
634
|
+
"Logs for Vellum-managed instances are not yet supported.\n" +
|
|
635
|
+
"View logs in the Vellum platform dashboard.",
|
|
636
|
+
);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
break;
|
|
639
|
+
|
|
640
|
+
case "apple-container":
|
|
641
|
+
console.error(
|
|
642
|
+
"Logs for Apple Container instances are not yet supported.\n" +
|
|
643
|
+
`Use 'vellum ssh ${entry.assistantId}' to access the container directly.`,
|
|
644
|
+
);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
break;
|
|
647
|
+
|
|
648
|
+
default:
|
|
649
|
+
console.error(`Unsupported topology '${cloud}' for log viewing.`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
}
|
package/src/commands/pair.ts
CHANGED
|
@@ -36,8 +36,16 @@ function decodeQRCodeFromPng(pngPath: string): string {
|
|
|
36
36
|
return code.data;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function safeUserInfoUsername(): string {
|
|
40
|
+
try {
|
|
41
|
+
return userInfo().username;
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
function getDeviceId(): string {
|
|
40
|
-
const raw = hostname() +
|
|
48
|
+
const raw = hostname() + safeUserInfoUsername();
|
|
41
49
|
return createHash("sha256").update(raw).digest("hex");
|
|
42
50
|
}
|
|
43
51
|
|