@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/recover.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
} from "fs";
|
|
2
8
|
import { homedir } from "os";
|
|
3
|
-
import { join } from "path";
|
|
9
|
+
import { basename, dirname, join } from "path";
|
|
4
10
|
|
|
5
11
|
import { saveAssistantEntry } from "../lib/assistant-config";
|
|
6
12
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
@@ -21,8 +27,22 @@ export async function recover(): Promise<void> {
|
|
|
21
27
|
"Restore a previously retired local assistant from its archive.",
|
|
22
28
|
);
|
|
23
29
|
console.log("");
|
|
30
|
+
console.log(
|
|
31
|
+
"Extracts the archived workspace data back to its original location,",
|
|
32
|
+
);
|
|
33
|
+
console.log(
|
|
34
|
+
"restores the lockfile entry, and starts the assistant and gateway.",
|
|
35
|
+
);
|
|
36
|
+
console.log(
|
|
37
|
+
"Archives are stored in $XDG_DATA_HOME/vellum/retired/ (default: ~/.local/share/vellum/retired/).",
|
|
38
|
+
);
|
|
39
|
+
console.log("");
|
|
24
40
|
console.log("Arguments:");
|
|
25
41
|
console.log(" <name> Name of the retired assistant to recover");
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log("Examples:");
|
|
44
|
+
console.log(" $ vellum recover my-assistant");
|
|
45
|
+
console.log(" $ vellum recover aria-7f3a");
|
|
26
46
|
process.exit(0);
|
|
27
47
|
}
|
|
28
48
|
|
|
@@ -61,11 +81,27 @@ export async function recover(): Promise<void> {
|
|
|
61
81
|
process.exit(1);
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
// 4.
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
84
|
+
// 4. Determine the original target directory, then extract and rename.
|
|
85
|
+
//
|
|
86
|
+
// retireLocal archives either the full instanceDir (named instances) or just
|
|
87
|
+
// the .vellum/ subdirectory (default instance whose instanceDir === homedir()).
|
|
88
|
+
// The directory is staged under `<archive>.staging` inside the retired dir
|
|
89
|
+
// before being packed with `tar -C <retiredDir> <stagingBasename>`, so the
|
|
90
|
+
// top-level entry inside the tarball is always `<name>.tar.gz.staging`.
|
|
91
|
+
//
|
|
92
|
+
// Correct restoration: extract to retiredDir, then rename the staging entry
|
|
93
|
+
// back to the original target path. Using homedir() as the -C target was
|
|
94
|
+
// wrong for any instance stored outside the home directory.
|
|
95
|
+
const isNamedInstance = entry.resources.instanceDir !== homedir();
|
|
96
|
+
const targetDir = isNamedInstance
|
|
97
|
+
? entry.resources.instanceDir
|
|
98
|
+
: join(entry.resources.instanceDir, ".vellum");
|
|
99
|
+
const retiredDir = dirname(archivePath);
|
|
100
|
+
const extractedPath = join(retiredDir, basename(archivePath) + ".staging");
|
|
101
|
+
|
|
102
|
+
await exec("tar", ["xzf", archivePath, "-C", retiredDir]);
|
|
103
|
+
mkdirSync(dirname(targetDir), { recursive: true });
|
|
104
|
+
renameSync(extractedPath, targetDir);
|
|
69
105
|
|
|
70
106
|
// 5. Restore lockfile entry
|
|
71
107
|
saveAssistantEntry(entry);
|
|
@@ -74,14 +110,16 @@ export async function recover(): Promise<void> {
|
|
|
74
110
|
unlinkSync(archivePath);
|
|
75
111
|
unlinkSync(metadataPath);
|
|
76
112
|
|
|
77
|
-
// 7. Persist signing key so
|
|
113
|
+
// 7. Persist signing key and bootstrap secret so they survive daemon/gateway restarts
|
|
78
114
|
const signingKey = generateLocalSigningKey();
|
|
115
|
+
const bootstrapSecret = generateLocalSigningKey();
|
|
79
116
|
entry.resources = { ...entry.resources, signingKey };
|
|
117
|
+
entry.guardianBootstrapSecret = bootstrapSecret;
|
|
80
118
|
saveAssistantEntry(entry);
|
|
81
119
|
|
|
82
120
|
// 8. Start daemon + gateway
|
|
83
121
|
await startLocalDaemon(false, entry.resources, { signingKey });
|
|
84
|
-
await startGateway(false, entry.resources, { signingKey });
|
|
122
|
+
await startGateway(false, entry.resources, { signingKey, bootstrapSecret });
|
|
85
123
|
|
|
86
124
|
console.log(`✅ Recovered assistant '${name}'.`);
|
|
87
125
|
}
|
package/src/commands/restore.ts
CHANGED
|
@@ -97,6 +97,7 @@ async function getAccessToken(
|
|
|
97
97
|
runtimeUrl: string,
|
|
98
98
|
assistantId: string,
|
|
99
99
|
displayName: string,
|
|
100
|
+
bootstrapSecret?: string,
|
|
100
101
|
): Promise<string> {
|
|
101
102
|
const tokenData = loadGuardianToken(assistantId);
|
|
102
103
|
|
|
@@ -105,7 +106,11 @@ async function getAccessToken(
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
try {
|
|
108
|
-
const freshToken = await leaseGuardianToken(
|
|
109
|
+
const freshToken = await leaseGuardianToken(
|
|
110
|
+
runtimeUrl,
|
|
111
|
+
assistantId,
|
|
112
|
+
bootstrapSecret,
|
|
113
|
+
);
|
|
109
114
|
return freshToken.accessToken;
|
|
110
115
|
} catch (err) {
|
|
111
116
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -574,6 +579,7 @@ export async function restore(): Promise<void> {
|
|
|
574
579
|
entry.runtimeUrl,
|
|
575
580
|
entry.assistantId,
|
|
576
581
|
name,
|
|
582
|
+
entry.guardianBootstrapSecret,
|
|
577
583
|
);
|
|
578
584
|
|
|
579
585
|
if (dryRun) {
|
|
@@ -679,6 +685,7 @@ export async function restore(): Promise<void> {
|
|
|
679
685
|
entry.runtimeUrl,
|
|
680
686
|
entry.assistantId,
|
|
681
687
|
name,
|
|
688
|
+
entry.guardianBootstrapSecret,
|
|
682
689
|
);
|
|
683
690
|
}
|
|
684
691
|
|
package/src/commands/retire.ts
CHANGED
|
@@ -2,14 +2,16 @@ import { existsSync, unlinkSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
extractHostFromUrl,
|
|
5
6
|
formatAssistantLookupError,
|
|
6
7
|
formatAssistantReference,
|
|
7
8
|
getAssistantDisplayName,
|
|
8
9
|
loadAllAssistants,
|
|
9
10
|
lookupAssistantByIdentifier,
|
|
10
11
|
removeAssistantEntry,
|
|
12
|
+
resolveCloud,
|
|
13
|
+
type AssistantEntry,
|
|
11
14
|
} from "../lib/assistant-config.js";
|
|
12
|
-
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
13
15
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
14
16
|
import { getConfigDir } from "../lib/environments/paths.js";
|
|
15
17
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
@@ -31,28 +33,6 @@ import {
|
|
|
31
33
|
writeToLogFile,
|
|
32
34
|
} from "../lib/xdg-log.js";
|
|
33
35
|
|
|
34
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
35
|
-
if (entry.cloud) {
|
|
36
|
-
return entry.cloud;
|
|
37
|
-
}
|
|
38
|
-
if (entry.project) {
|
|
39
|
-
return "gcp";
|
|
40
|
-
}
|
|
41
|
-
if (entry.sshUser) {
|
|
42
|
-
return "custom";
|
|
43
|
-
}
|
|
44
|
-
return "local";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function extractHostFromUrl(url: string): string {
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL(url);
|
|
50
|
-
return parsed.hostname;
|
|
51
|
-
} catch {
|
|
52
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
36
|
export { retireLocal };
|
|
57
37
|
|
|
58
38
|
interface RetireArgs {
|
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
|
});
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
findAssistantByName,
|
|
8
8
|
getActiveAssistant,
|
|
9
9
|
loadAllAssistants,
|
|
10
|
+
resolveCloud,
|
|
10
11
|
saveAssistantEntry,
|
|
12
|
+
type AssistantEntry,
|
|
11
13
|
} from "../lib/assistant-config";
|
|
12
|
-
import type { AssistantEntry } from "../lib/assistant-config";
|
|
13
14
|
import {
|
|
14
15
|
captureImageRefs,
|
|
15
16
|
GATEWAY_INTERNAL_PORT,
|
|
@@ -145,19 +146,6 @@ function parseArgs(): UpgradeArgs {
|
|
|
145
146
|
return { name, version, latest, prepare, finalize };
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
149
|
-
if (entry.cloud) {
|
|
150
|
-
return entry.cloud;
|
|
151
|
-
}
|
|
152
|
-
if (entry.project) {
|
|
153
|
-
return "gcp";
|
|
154
|
-
}
|
|
155
|
-
if (entry.sshUser) {
|
|
156
|
-
return "custom";
|
|
157
|
-
}
|
|
158
|
-
return "local";
|
|
159
|
-
}
|
|
160
|
-
|
|
161
149
|
/**
|
|
162
150
|
* Resolve which assistant to target for the upgrade command. Priority:
|
|
163
151
|
* 1. Explicit name argument
|
|
@@ -512,7 +500,10 @@ async function upgradeDocker(
|
|
|
512
500
|
} else {
|
|
513
501
|
console.error(`\n❌ Containers failed to become ready within the timeout.`);
|
|
514
502
|
|
|
515
|
-
const logDir = await captureUpgradeFailureLogs(
|
|
503
|
+
const logDir = await captureUpgradeFailureLogs(
|
|
504
|
+
res,
|
|
505
|
+
`${instanceName}-upgrade-failure`,
|
|
506
|
+
);
|
|
516
507
|
if (logDir) {
|
|
517
508
|
console.log(`📋 Container logs saved to: ${logDir}`);
|
|
518
509
|
}
|
|
@@ -938,7 +929,8 @@ async function resolveLatestAndMaybeSelfUpdate(
|
|
|
938
929
|
);
|
|
939
930
|
if (installResult.error || installResult.status !== 0) {
|
|
940
931
|
const detail =
|
|
941
|
-
installResult.error?.message ??
|
|
932
|
+
installResult.error?.message ??
|
|
933
|
+
`exited with code ${installResult.status}`;
|
|
942
934
|
console.error(`\n❌ CLI self-update failed: ${detail}`);
|
|
943
935
|
emitCliError("CLI_UPDATE_FAILED", "CLI self-update failed", detail);
|
|
944
936
|
process.exit(1);
|
package/src/commands/wake.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "../lib/assistant-config.js";
|
|
9
9
|
import { dockerResourceNames, wakeContainers } from "../lib/docker.js";
|
|
10
10
|
import { seedGuardianTokenFromSiblingEnv } from "../lib/guardian-token.js";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveProcessState, stopProcessByPidFile } from "../lib/process";
|
|
12
12
|
import {
|
|
13
13
|
generateLocalSigningKey,
|
|
14
14
|
isAssistantWatchModeAvailable,
|
|
@@ -85,36 +85,26 @@ export async function wake(): Promise<void> {
|
|
|
85
85
|
|
|
86
86
|
const pidFile = getDaemonPidPath(resources);
|
|
87
87
|
|
|
88
|
-
// Check if daemon is already running
|
|
89
88
|
let daemonRunning = false;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
);
|
|
110
|
-
await stopProcessByPidFile(pidFile, "assistant");
|
|
111
|
-
daemonRunning = false;
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
console.log(`Assistant already running (pid ${pid}).`);
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
// Process not alive, will start below
|
|
89
|
+
const daemonState = await resolveProcessState(
|
|
90
|
+
pidFile,
|
|
91
|
+
resources.daemonPort,
|
|
92
|
+
"Assistant",
|
|
93
|
+
);
|
|
94
|
+
if (daemonState.status === "healthy") {
|
|
95
|
+
if (watch && isAssistantWatchModeAvailable()) {
|
|
96
|
+
console.log(
|
|
97
|
+
`Assistant running (pid ${daemonState.pid}) — restarting in watch mode...`,
|
|
98
|
+
);
|
|
99
|
+
await stopProcessByPidFile(pidFile, "assistant");
|
|
100
|
+
} else {
|
|
101
|
+
daemonRunning = true;
|
|
102
|
+
if (watch) {
|
|
103
|
+
console.log(
|
|
104
|
+
`Assistant running (pid ${daemonState.pid}) — watch mode not available (no source files). Keeping existing process.`,
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
console.log(`Assistant already running (pid ${daemonState.pid}).`);
|
|
118
108
|
}
|
|
119
109
|
}
|
|
120
110
|
}
|
|
@@ -153,6 +143,15 @@ export async function wake(): Promise<void> {
|
|
|
153
143
|
saveAssistantEntry(entry);
|
|
154
144
|
}
|
|
155
145
|
|
|
146
|
+
let bootstrapSecret = entry.guardianBootstrapSecret;
|
|
147
|
+
let bootstrapSecretBackfilled = false;
|
|
148
|
+
if (!bootstrapSecret) {
|
|
149
|
+
bootstrapSecret = generateLocalSigningKey();
|
|
150
|
+
entry.guardianBootstrapSecret = bootstrapSecret;
|
|
151
|
+
saveAssistantEntry(entry);
|
|
152
|
+
bootstrapSecretBackfilled = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
156
155
|
if (!daemonRunning) {
|
|
157
156
|
await startLocalDaemon(watch, resources, { foreground, signingKey });
|
|
158
157
|
}
|
|
@@ -161,26 +160,47 @@ export async function wake(): Promise<void> {
|
|
|
161
160
|
{
|
|
162
161
|
const vellumDir = join(resources.instanceDir, ".vellum");
|
|
163
162
|
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
const gatewayState = await resolveProcessState(
|
|
164
|
+
gatewayPidFile,
|
|
165
|
+
resources.gatewayPort,
|
|
166
|
+
"Gateway",
|
|
167
|
+
);
|
|
168
|
+
const gatewayAlive = gatewayState.status === "healthy";
|
|
169
|
+
const needsRestart = bootstrapSecretBackfilled && gatewayAlive;
|
|
170
|
+
if (needsRestart) {
|
|
171
|
+
const restartWithWatch = watch && isGatewayWatchModeAvailable();
|
|
172
|
+
if (restartWithWatch) {
|
|
173
|
+
console.log(
|
|
174
|
+
`Gateway running (pid ${gatewayState.pid}) — restarting to apply bootstrap secret...`,
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(
|
|
178
|
+
`Gateway running (pid ${gatewayState.pid}) — restarting without watch mode to apply bootstrap secret...`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
182
|
+
await startGateway(restartWithWatch, resources, {
|
|
183
|
+
signingKey,
|
|
184
|
+
bootstrapSecret,
|
|
185
|
+
});
|
|
186
|
+
} else if (gatewayAlive) {
|
|
187
|
+
if (watch && isGatewayWatchModeAvailable()) {
|
|
188
|
+
console.log(
|
|
189
|
+
`Gateway running (pid ${gatewayState.pid}) — restarting in watch mode...`,
|
|
190
|
+
);
|
|
191
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
192
|
+
await startGateway(watch, resources, { signingKey, bootstrapSecret });
|
|
193
|
+
} else {
|
|
194
|
+
if (watch) {
|
|
169
195
|
console.log(
|
|
170
|
-
`Gateway running (pid ${pid}) — watch mode not available (no source files). Keeping existing process.`,
|
|
196
|
+
`Gateway running (pid ${gatewayState.pid}) — watch mode not available (no source files). Keeping existing process.`,
|
|
171
197
|
);
|
|
172
198
|
} else {
|
|
173
|
-
console.log(
|
|
174
|
-
`Gateway running (pid ${pid}) — restarting in watch mode...`,
|
|
175
|
-
);
|
|
176
|
-
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
177
|
-
await startGateway(watch, resources, { signingKey });
|
|
199
|
+
console.log(`Gateway already running (pid ${gatewayState.pid}).`);
|
|
178
200
|
}
|
|
179
|
-
} else {
|
|
180
|
-
console.log(`Gateway already running (pid ${pid}).`);
|
|
181
201
|
}
|
|
182
202
|
} else {
|
|
183
|
-
await startGateway(watch, resources, { signingKey });
|
|
203
|
+
await startGateway(watch, resources, { signingKey, bootstrapSecret });
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
|
|
@@ -196,7 +216,10 @@ export async function wake(): Promise<void> {
|
|
|
196
216
|
|
|
197
217
|
// Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
|
|
198
218
|
const workspaceDir = join(resources.instanceDir, ".vellum", "workspace");
|
|
199
|
-
const ngrokChild = await maybeStartNgrokTunnel(
|
|
219
|
+
const ngrokChild = await maybeStartNgrokTunnel(
|
|
220
|
+
resources.gatewayPort,
|
|
221
|
+
workspaceDir,
|
|
222
|
+
);
|
|
200
223
|
if (ngrokChild?.pid) {
|
|
201
224
|
const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
|
|
202
225
|
writeFileSync(ngrokPidFile, String(ngrokChild.pid));
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { client } from "./commands/client";
|
|
|
7
7
|
import { env } from "./commands/env";
|
|
8
8
|
import { events } from "./commands/events";
|
|
9
9
|
import { exec } from "./commands/exec";
|
|
10
|
+
import { flags } from "./commands/flags";
|
|
11
|
+
import { gateway } from "./commands/gateway";
|
|
10
12
|
import { hatch } from "./commands/hatch";
|
|
11
13
|
import { login, logout, whoami } from "./commands/login";
|
|
12
14
|
import { logs } from "./commands/logs";
|
|
@@ -37,6 +39,8 @@ const commands = {
|
|
|
37
39
|
env,
|
|
38
40
|
events,
|
|
39
41
|
exec,
|
|
42
|
+
flags,
|
|
43
|
+
gateway,
|
|
40
44
|
hatch,
|
|
41
45
|
login,
|
|
42
46
|
logout,
|
|
@@ -72,6 +76,8 @@ function printHelp(): void {
|
|
|
72
76
|
console.log(" env Manage the default CLI environment");
|
|
73
77
|
console.log(" events Stream events from a running assistant");
|
|
74
78
|
console.log(" exec Execute a command inside an assistant's container");
|
|
79
|
+
console.log(" flags Show and toggle feature flags");
|
|
80
|
+
console.log(" gateway Gateway management commands");
|
|
75
81
|
console.log(" hatch Create a new assistant instance");
|
|
76
82
|
console.log(" logs View logs from an assistant instance");
|
|
77
83
|
console.log(" login Log in to the Vellum platform");
|