@vellumai/cli 0.8.5 → 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 +6 -0
- package/knip.json +2 -1
- package/package.json +1 -1
- package/src/__tests__/backup.test.ts +38 -0
- package/src/__tests__/recover.test.ts +307 -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 +3 -23
- 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 +6 -0
- package/src/lib/__tests__/step-runner.test.ts +49 -1
- package/src/lib/assistant-config.ts +13 -0
- package/src/lib/config-utils.ts +24 -3
- package/src/lib/docker.ts +57 -7
- package/src/lib/hatch-local.ts +4 -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/process.ts +109 -39
- package/src/lib/step-runner.ts +67 -7
- package/src/lib/sync-cloud-assistants.ts +17 -0
package/src/commands/client.ts
CHANGED
|
@@ -278,18 +278,29 @@ async function maybeHydratePlatformAssistantName(
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
const SPA_BASE = "/assistant/";
|
|
282
|
+
|
|
281
283
|
/**
|
|
282
|
-
*
|
|
284
|
+
* Locate the pre-built @vellumai/web dist directory.
|
|
283
285
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* `--interface web` path as source-checkout-only.
|
|
286
|
+
* Resolution order:
|
|
287
|
+
* 1. npm-installed package — require.resolve('@vellumai/web/package.json')
|
|
288
|
+
* 2. Source checkout — walk up from cli/ to find apps/web/dist/
|
|
288
289
|
*/
|
|
289
|
-
function
|
|
290
|
+
function findWebDistDir(): string | null {
|
|
291
|
+
try {
|
|
292
|
+
const pkgPath = require.resolve("@vellumai/web/package.json");
|
|
293
|
+
const distDir = path.join(path.dirname(pkgPath), "dist");
|
|
294
|
+
if (existsSync(path.join(distDir, "index.html"))) {
|
|
295
|
+
return distDir;
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Package not installed; try source checkout.
|
|
299
|
+
}
|
|
300
|
+
|
|
290
301
|
let dir = import.meta.dir;
|
|
291
302
|
for (let depth = 0; depth < 8; depth++) {
|
|
292
|
-
const candidate = path.join(dir, "
|
|
303
|
+
const candidate = path.join(dir, "apps", "web", "dist", "index.html");
|
|
293
304
|
if (existsSync(candidate)) {
|
|
294
305
|
return path.dirname(candidate);
|
|
295
306
|
}
|
|
@@ -300,42 +311,61 @@ function findClientsWebDir(): string | null {
|
|
|
300
311
|
return null;
|
|
301
312
|
}
|
|
302
313
|
|
|
303
|
-
/**
|
|
304
|
-
* Spawn the `clients/web` package's `local` script and proxy its lifecycle.
|
|
305
|
-
*
|
|
306
|
-
* The web client is deliberately not declared as a dependency of `@vellumai/cli`:
|
|
307
|
-
* the CLI is published, the web package is not. Locating it on disk and
|
|
308
|
-
* shelling out keeps the two packages independent.
|
|
309
|
-
*/
|
|
310
314
|
async function runWebInterface(): Promise<void> {
|
|
311
|
-
const
|
|
312
|
-
if (!
|
|
315
|
+
const distDir = findWebDistDir();
|
|
316
|
+
if (!distDir) {
|
|
313
317
|
console.error(
|
|
314
318
|
`${ANSI.bold}--interface web${ANSI.reset}: unable to locate ` +
|
|
315
|
-
|
|
316
|
-
`
|
|
319
|
+
`@vellumai/web assets.\n\n` +
|
|
320
|
+
` npm/bunx install: npm install @vellumai/web\n` +
|
|
321
|
+
` source checkout: cd apps/web && VITE_PLATFORM_MODE=false bun run build`,
|
|
317
322
|
);
|
|
318
323
|
process.exit(1);
|
|
319
324
|
}
|
|
320
325
|
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
const indexHtml = await Bun.file(path.join(distDir, "index.html")).text();
|
|
327
|
+
|
|
328
|
+
const server = Bun.serve({
|
|
329
|
+
port: 3000,
|
|
330
|
+
hostname: "127.0.0.1",
|
|
331
|
+
fetch: async (req) => {
|
|
332
|
+
const url = new URL(req.url);
|
|
333
|
+
const { pathname } = url;
|
|
334
|
+
|
|
335
|
+
if (pathname === "/") {
|
|
336
|
+
return Response.redirect(SPA_BASE, 302);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (pathname.startsWith(SPA_BASE)) {
|
|
340
|
+
const relPath = pathname.slice(SPA_BASE.length);
|
|
341
|
+
if (relPath) {
|
|
342
|
+
const filePath = path.join(distDir, relPath);
|
|
343
|
+
const file = Bun.file(filePath);
|
|
344
|
+
if (await file.exists()) {
|
|
345
|
+
return new Response(file);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return new Response(indexHtml, {
|
|
349
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return new Response("Not Found", { status: 404 });
|
|
354
|
+
},
|
|
325
355
|
});
|
|
326
356
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
357
|
+
console.log(
|
|
358
|
+
`Vellum web interface: http://${server.hostname}:${server.port}${SPA_BASE}`,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const shutdown = (): void => {
|
|
362
|
+
server.stop();
|
|
363
|
+
process.exit(0);
|
|
333
364
|
};
|
|
334
|
-
process.on("SIGINT",
|
|
335
|
-
process.on("SIGTERM",
|
|
365
|
+
process.on("SIGINT", shutdown);
|
|
366
|
+
process.on("SIGTERM", shutdown);
|
|
336
367
|
|
|
337
|
-
|
|
338
|
-
process.exit(typeof exitCode === "number" ? exitCode : 0);
|
|
368
|
+
await new Promise(() => {});
|
|
339
369
|
}
|
|
340
370
|
|
|
341
371
|
export async function client(): Promise<void> {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { AssistantClient } from "../lib/assistant-client.js";
|
|
2
|
+
|
|
3
|
+
type FeatureFlagEntry = {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
defaultEnabled: boolean;
|
|
8
|
+
description: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type FlagsResponse = {
|
|
12
|
+
flags: FeatureFlagEntry[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function pad(s: string, w: number): string {
|
|
16
|
+
return s + " ".repeat(Math.max(0, w - s.length));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printFlagTable(flags: FeatureFlagEntry[]): void {
|
|
20
|
+
const headers = { key: "KEY", enabled: "ENABLED", default: "DEFAULT", label: "LABEL" };
|
|
21
|
+
|
|
22
|
+
const rows = flags
|
|
23
|
+
.slice()
|
|
24
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
25
|
+
.map((f) => ({
|
|
26
|
+
key: f.enabled !== f.defaultEnabled ? `* ${f.key}` : ` ${f.key}`,
|
|
27
|
+
enabled: String(f.enabled),
|
|
28
|
+
default: String(f.defaultEnabled),
|
|
29
|
+
label: f.label,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const all = [headers, ...rows];
|
|
33
|
+
const colWidths = {
|
|
34
|
+
key: Math.max(...all.map((r) => r.key.length)),
|
|
35
|
+
enabled: Math.max(...all.map((r) => r.enabled.length)),
|
|
36
|
+
default: Math.max(...all.map((r) => r.default.length)),
|
|
37
|
+
label: Math.max(...all.map((r) => r.label.length)),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const formatRow = (r: typeof headers) =>
|
|
41
|
+
`${pad(r.key, colWidths.key)} ${pad(r.enabled, colWidths.enabled)} ${pad(r.default, colWidths.default)} ${r.label}`;
|
|
42
|
+
|
|
43
|
+
console.log(formatRow(headers));
|
|
44
|
+
console.log(
|
|
45
|
+
`${"-".repeat(colWidths.key)} ${"-".repeat(colWidths.enabled)} ${"-".repeat(colWidths.default)} ${"-".repeat(colWidths.label)}`,
|
|
46
|
+
);
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
console.log(formatRow(row));
|
|
49
|
+
}
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log("* = overridden (differs from default)");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printHelp(): void {
|
|
55
|
+
console.log("Usage: vellum flags [subcommand] [options]");
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log("Show and toggle feature flags for the active assistant.");
|
|
58
|
+
console.log("Reads from the gateway's merged flag state (persisted overrides > remote > defaults).");
|
|
59
|
+
console.log("");
|
|
60
|
+
console.log("Subcommands:");
|
|
61
|
+
console.log(" (none) List all feature flags in a table");
|
|
62
|
+
console.log(" get <key> Show details for a single flag");
|
|
63
|
+
console.log(" set <key> <bool> Set a flag override to true or false");
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log("Options:");
|
|
66
|
+
console.log(" --help, -h Show this help");
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log("Examples:");
|
|
69
|
+
console.log(" $ vellum flags # list all flags");
|
|
70
|
+
console.log(" $ vellum flags get query-complexity-routing # inspect one flag");
|
|
71
|
+
console.log(" $ vellum flags set voice-mode true # enable a flag");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createClient(): AssistantClient {
|
|
75
|
+
try {
|
|
76
|
+
return new AssistantClient();
|
|
77
|
+
} catch {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"No assistant found. Hatch one with 'vellum hatch' first.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rethrowFetchError(err: unknown): never {
|
|
85
|
+
if (
|
|
86
|
+
err instanceof TypeError &&
|
|
87
|
+
(err.message.includes("fetch") || err.message.includes("connect"))
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Could not reach the assistant gateway. Is it running? Try 'vellum wake'.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function listFlags(): Promise<void> {
|
|
97
|
+
const client = createClient();
|
|
98
|
+
let res: Response;
|
|
99
|
+
try {
|
|
100
|
+
res = await client.get("/feature-flags");
|
|
101
|
+
} catch (err) {
|
|
102
|
+
rethrowFetchError(err);
|
|
103
|
+
}
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
const body = await res.text().catch(() => "");
|
|
106
|
+
throw new Error(`Failed to fetch flags: HTTP ${res.status} ${body}`.trim());
|
|
107
|
+
}
|
|
108
|
+
const data = (await res.json()) as FlagsResponse;
|
|
109
|
+
if (data.flags.length === 0) {
|
|
110
|
+
console.log("No feature flags found.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
printFlagTable(data.flags);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function getFlag(key: string): Promise<void> {
|
|
117
|
+
const client = createClient();
|
|
118
|
+
let res: Response;
|
|
119
|
+
try {
|
|
120
|
+
res = await client.get("/feature-flags");
|
|
121
|
+
} catch (err) {
|
|
122
|
+
rethrowFetchError(err);
|
|
123
|
+
}
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
const body = await res.text().catch(() => "");
|
|
126
|
+
throw new Error(`Failed to fetch flags: HTTP ${res.status} ${body}`.trim());
|
|
127
|
+
}
|
|
128
|
+
const data = (await res.json()) as FlagsResponse;
|
|
129
|
+
const flag = data.flags.find((f) => f.key === key);
|
|
130
|
+
if (!flag) {
|
|
131
|
+
throw new Error(`Flag "${key}" not found.`);
|
|
132
|
+
}
|
|
133
|
+
console.log(`Key: ${flag.key}`);
|
|
134
|
+
console.log(`Enabled: ${flag.enabled}`);
|
|
135
|
+
console.log(`Default: ${flag.defaultEnabled}`);
|
|
136
|
+
console.log(`Description: ${flag.description || "(none)"}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function setFlag(key: string, value: boolean): Promise<void> {
|
|
140
|
+
const client = createClient();
|
|
141
|
+
let res: Response;
|
|
142
|
+
try {
|
|
143
|
+
res = await client.patch(`/feature-flags/${key}`, { enabled: value });
|
|
144
|
+
} catch (err) {
|
|
145
|
+
rethrowFetchError(err);
|
|
146
|
+
}
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const body = await res.text().catch(() => "");
|
|
149
|
+
throw new Error(`Failed to set flag: HTTP ${res.status} ${body}`.trim());
|
|
150
|
+
}
|
|
151
|
+
console.log(`Flag "${key}" set to ${value}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function flags(): Promise<void> {
|
|
155
|
+
const args = process.argv.slice(3);
|
|
156
|
+
|
|
157
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
158
|
+
printHelp();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const subcommand = args[0];
|
|
163
|
+
|
|
164
|
+
if (!subcommand) {
|
|
165
|
+
await listFlags();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (subcommand === "get") {
|
|
170
|
+
const key = args[1];
|
|
171
|
+
if (!key) {
|
|
172
|
+
console.error("Usage: vellum flags get <key>");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
await getFlag(key);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (subcommand === "set") {
|
|
180
|
+
const key = args[1];
|
|
181
|
+
const rawValue = args[2];
|
|
182
|
+
if (!key || rawValue === undefined) {
|
|
183
|
+
console.error("Usage: vellum flags set <key> <true|false>");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
if (rawValue !== "true" && rawValue !== "false") {
|
|
187
|
+
console.error(`Invalid value "${rawValue}". Must be "true" or "false".`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
await setFlag(key, rawValue === "true");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
195
|
+
printHelp();
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lookupAssistantByIdentifier,
|
|
3
|
+
formatAssistantLookupError,
|
|
4
|
+
} from "../../lib/assistant-config.js";
|
|
5
|
+
import {
|
|
6
|
+
loadGuardianToken,
|
|
7
|
+
refreshGuardianToken,
|
|
8
|
+
} from "../../lib/guardian-token.js";
|
|
9
|
+
|
|
10
|
+
function printUsage(): void {
|
|
11
|
+
console.log("Usage: vellum gateway token <subcommand> <assistantId>");
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log("Manage gateway authentication tokens.");
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log("Subcommands:");
|
|
16
|
+
console.log(" get Print the current guardian access token");
|
|
17
|
+
console.log(" refresh Refresh an expired access token and print it");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function gatewayToken(): Promise<void> {
|
|
21
|
+
const args = process.argv.slice(4);
|
|
22
|
+
const subcommand = args[0];
|
|
23
|
+
|
|
24
|
+
if (subcommand === "--help" || subcommand === "-h" || !subcommand) {
|
|
25
|
+
printUsage();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (subcommand !== "get" && subcommand !== "refresh") {
|
|
30
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
31
|
+
printUsage();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const assistantId = args[1];
|
|
36
|
+
if (!assistantId) {
|
|
37
|
+
console.error("Missing required argument: <assistantId>");
|
|
38
|
+
printUsage();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = lookupAssistantByIdentifier(assistantId);
|
|
43
|
+
if (result.status !== "found") {
|
|
44
|
+
console.error(formatAssistantLookupError(assistantId, result));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const entry = result.entry;
|
|
48
|
+
|
|
49
|
+
const tokenData = loadGuardianToken(entry.assistantId);
|
|
50
|
+
if (!tokenData) {
|
|
51
|
+
console.error("No guardian token found for this assistant.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (subcommand === "get") {
|
|
56
|
+
console.log(tokenData.accessToken);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const gatewayUrl = entry.localUrl || entry.runtimeUrl;
|
|
61
|
+
if (!gatewayUrl) {
|
|
62
|
+
console.error("No gateway URL found for this assistant.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const refreshed = await refreshGuardianToken(gatewayUrl, entry.assistantId);
|
|
67
|
+
if (!refreshed) {
|
|
68
|
+
console.error("Failed to refresh guardian token.");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(refreshed.accessToken);
|
|
73
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { gatewayToken } from "./gateway/token.js";
|
|
2
|
+
|
|
3
|
+
function printUsage(): void {
|
|
4
|
+
console.log("Usage: vellum gateway <subcommand>");
|
|
5
|
+
console.log("");
|
|
6
|
+
console.log("Gateway management commands.");
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log("Subcommands:");
|
|
9
|
+
console.log(" token Manage gateway authentication tokens");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function gateway(): Promise<void> {
|
|
13
|
+
const args = process.argv.slice(3);
|
|
14
|
+
const subcommand = args[0];
|
|
15
|
+
|
|
16
|
+
if (subcommand === "--help" || subcommand === "-h" || !subcommand) {
|
|
17
|
+
printUsage();
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (subcommand === "token") {
|
|
22
|
+
await gatewayToken();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
27
|
+
printUsage();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
package/src/commands/logs.ts
CHANGED
|
@@ -4,8 +4,12 @@ import { createInterface } from "readline";
|
|
|
4
4
|
import { watch } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
extractHostFromUrl,
|
|
9
|
+
resolveAssistant,
|
|
10
|
+
resolveCloud,
|
|
11
|
+
type AssistantEntry,
|
|
12
|
+
} from "../lib/assistant-config";
|
|
9
13
|
import { dockerResourceNames } from "../lib/docker";
|
|
10
14
|
import { getLogDir } from "../lib/xdg-log";
|
|
11
15
|
import { execOutput } from "../lib/step-runner";
|
|
@@ -112,13 +116,6 @@ function parseArgs(): LogsArgs {
|
|
|
112
116
|
|
|
113
117
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
114
118
|
|
|
115
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
116
|
-
if (entry.cloud) return entry.cloud;
|
|
117
|
-
if (entry.project) return "gcp";
|
|
118
|
-
if (entry.sshUser) return "custom";
|
|
119
|
-
return "local";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
119
|
/**
|
|
123
120
|
* Parse a relative time string like "10m", "2h", "30s" into a Date.
|
|
124
121
|
* Returns null if the string doesn't look like a relative time.
|
|
@@ -494,15 +491,6 @@ async function showGcpLogs(
|
|
|
494
491
|
}
|
|
495
492
|
}
|
|
496
493
|
|
|
497
|
-
function extractHostFromUrl(url: string): string {
|
|
498
|
-
try {
|
|
499
|
-
const parsed = new URL(url);
|
|
500
|
-
return parsed.hostname;
|
|
501
|
-
} catch {
|
|
502
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
494
|
async function showCustomLogs(
|
|
507
495
|
entry: AssistantEntry,
|
|
508
496
|
opts: LogsArgs,
|
package/src/commands/ps.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
extractHostFromUrl,
|
|
4
5
|
findAssistantByName,
|
|
5
6
|
formatAssistantLookupError,
|
|
6
7
|
formatAssistantReference,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
getDaemonPidPath,
|
|
10
11
|
loadAllAssistants,
|
|
11
12
|
lookupAssistantByIdentifier,
|
|
13
|
+
resolveCloud,
|
|
12
14
|
type AssistantEntry,
|
|
13
15
|
} from "../lib/assistant-config";
|
|
14
16
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
@@ -26,7 +28,7 @@ import { existsSync } from "fs";
|
|
|
26
28
|
import {
|
|
27
29
|
classifyProcess,
|
|
28
30
|
detectOrphanedProcesses,
|
|
29
|
-
|
|
31
|
+
isPidAlive,
|
|
30
32
|
parseRemotePs,
|
|
31
33
|
readPidFile,
|
|
32
34
|
} from "../lib/orphan-detection";
|
|
@@ -149,35 +151,25 @@ const REMOTE_PS_CMD = [
|
|
|
149
151
|
"| grep -v grep",
|
|
150
152
|
].join(" ");
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const parsed = new URL(url);
|
|
155
|
-
return parsed.hostname;
|
|
156
|
-
} catch {
|
|
157
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
162
|
-
if (entry.cloud) return entry.cloud;
|
|
163
|
-
if (entry.project) return "gcp";
|
|
164
|
-
if (entry.sshUser) return "custom";
|
|
165
|
-
return "local";
|
|
166
|
-
}
|
|
154
|
+
const REMOTE_SSH_TIMEOUT_MS = 30_000;
|
|
167
155
|
|
|
168
156
|
async function getRemoteProcessesGcp(entry: AssistantEntry): Promise<string> {
|
|
169
|
-
return execOutput(
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
157
|
+
return execOutput(
|
|
158
|
+
"gcloud",
|
|
159
|
+
[
|
|
160
|
+
"compute",
|
|
161
|
+
"ssh",
|
|
162
|
+
`${entry.sshUser ?? entry.assistantId}@${entry.assistantId}`,
|
|
163
|
+
`--zone=${entry.zone}`,
|
|
164
|
+
`--project=${entry.project}`,
|
|
165
|
+
`--command=${REMOTE_PS_CMD}`,
|
|
166
|
+
"--ssh-flag=-o StrictHostKeyChecking=no",
|
|
167
|
+
"--ssh-flag=-o UserKnownHostsFile=/dev/null",
|
|
168
|
+
"--ssh-flag=-o ConnectTimeout=10",
|
|
169
|
+
"--ssh-flag=-o LogLevel=ERROR",
|
|
170
|
+
],
|
|
171
|
+
{ timeoutMs: REMOTE_SSH_TIMEOUT_MS },
|
|
172
|
+
);
|
|
181
173
|
}
|
|
182
174
|
|
|
183
175
|
async function getRemoteProcessesCustom(
|
|
@@ -185,7 +177,9 @@ async function getRemoteProcessesCustom(
|
|
|
185
177
|
): Promise<string> {
|
|
186
178
|
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
187
179
|
const sshUser = entry.sshUser ?? "root";
|
|
188
|
-
return execOutput("ssh", [...SSH_OPTS, `${sshUser}@${host}`, REMOTE_PS_CMD]
|
|
180
|
+
return execOutput("ssh", [...SSH_OPTS, `${sshUser}@${host}`, REMOTE_PS_CMD], {
|
|
181
|
+
timeoutMs: REMOTE_SSH_TIMEOUT_MS,
|
|
182
|
+
});
|
|
189
183
|
}
|
|
190
184
|
|
|
191
185
|
interface ProcessSpec {
|
|
@@ -203,9 +197,13 @@ interface DetectedProcess {
|
|
|
203
197
|
watch: boolean;
|
|
204
198
|
}
|
|
205
199
|
|
|
200
|
+
const LOCAL_CMD_TIMEOUT_MS = 5_000;
|
|
201
|
+
|
|
206
202
|
async function isWatchMode(pid: string): Promise<boolean> {
|
|
207
203
|
try {
|
|
208
|
-
const args = await execOutput("ps", ["-p", pid, "-o", "args="]
|
|
204
|
+
const args = await execOutput("ps", ["-p", pid, "-o", "args="], {
|
|
205
|
+
timeoutMs: LOCAL_CMD_TIMEOUT_MS,
|
|
206
|
+
});
|
|
209
207
|
return args.includes("--watch");
|
|
210
208
|
} catch {
|
|
211
209
|
return false;
|
|
@@ -242,7 +240,7 @@ async function detectProcess(spec: ProcessSpec): Promise<DetectedProcess> {
|
|
|
242
240
|
|
|
243
241
|
// Tier 3: PID file fallback
|
|
244
242
|
const filePid = readPidFile(spec.pidFile);
|
|
245
|
-
if (filePid &&
|
|
243
|
+
if (filePid && isPidAlive(filePid)) {
|
|
246
244
|
const watch = await isWatchMode(filePid);
|
|
247
245
|
return {
|
|
248
246
|
name: spec.name,
|
|
@@ -320,12 +318,11 @@ async function getDockerContainerState(
|
|
|
320
318
|
containerName: string,
|
|
321
319
|
): Promise<string | null> {
|
|
322
320
|
try {
|
|
323
|
-
const output = await execOutput(
|
|
324
|
-
"
|
|
325
|
-
"--format",
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
]);
|
|
321
|
+
const output = await execOutput(
|
|
322
|
+
"docker",
|
|
323
|
+
["inspect", "--format", "{{.State.Status}}", containerName],
|
|
324
|
+
{ timeoutMs: LOCAL_CMD_TIMEOUT_MS },
|
|
325
|
+
);
|
|
329
326
|
return output.trim() || "unknown";
|
|
330
327
|
} catch {
|
|
331
328
|
return null;
|
|
@@ -458,9 +455,12 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
458
455
|
process.exit(1);
|
|
459
456
|
}
|
|
460
457
|
} catch (error) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
458
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
459
|
+
if (msg.includes("timed out")) {
|
|
460
|
+
console.warn(`Warning: remote process listing timed out — ${msg}`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
console.error(`Failed to list processes: ${msg}`);
|
|
464
464
|
process.exit(1);
|
|
465
465
|
}
|
|
466
466
|
|
|
@@ -496,7 +496,7 @@ async function getAssistantListHealth(
|
|
|
496
496
|
// TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
|
|
497
497
|
// fetching daemon PIDs via the health API (Gateway Security Migration).
|
|
498
498
|
const pid = readPidFile(getDaemonPidPath(resources));
|
|
499
|
-
const alive = pid !== null &&
|
|
499
|
+
const alive = pid !== null && isPidAlive(pid);
|
|
500
500
|
if (!alive) {
|
|
501
501
|
return { status: "sleeping", detail: null };
|
|
502
502
|
}
|