@vellumai/cli 0.5.5 → 0.5.7
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/knip.json +4 -1
- package/package.json +1 -1
- package/src/__tests__/health-check.test.ts +26 -1
- package/src/commands/backup.ts +28 -13
- package/src/commands/hatch.ts +96 -60
- package/src/commands/ps.ts +6 -1
- package/src/commands/restore.ts +50 -30
- package/src/commands/retire.ts +5 -5
- package/src/commands/rollback.ts +443 -0
- package/src/commands/upgrade.ts +586 -120
- package/src/commands/wake.ts +8 -0
- package/src/index.ts +5 -0
- package/src/lib/assistant-config.ts +62 -7
- package/src/lib/aws.ts +2 -0
- package/src/lib/backup-ops.ts +213 -0
- package/src/lib/cli-error.ts +91 -0
- package/src/lib/config-utils.ts +59 -0
- package/src/lib/docker.ts +82 -10
- package/src/lib/doctor-client.ts +11 -1
- package/src/lib/gcp.ts +5 -1
- package/src/lib/guardian-token.ts +46 -1
- package/src/lib/health-check.ts +4 -0
- package/src/lib/local.ts +29 -9
- package/src/lib/platform-client.ts +19 -4
- package/src/lib/platform-releases.ts +112 -0
- package/src/lib/upgrade-lifecycle.ts +237 -0
- package/src/lib/version-compat.ts +45 -0
- package/src/lib/workspace-git.ts +39 -0
package/knip.json
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
"entry": [
|
|
3
3
|
"src/**/*.test.ts",
|
|
4
4
|
"src/**/__tests__/**/*.ts",
|
|
5
|
-
"src/adapters/openclaw-http-server.ts"
|
|
5
|
+
"src/adapters/openclaw-http-server.ts",
|
|
6
|
+
"src/lib/version-compat.ts",
|
|
7
|
+
"src/lib/platform-releases.ts",
|
|
8
|
+
"src/lib/cli-error.ts"
|
|
6
9
|
],
|
|
7
10
|
"project": ["src/**/*.ts", "src/**/*.tsx"]
|
|
8
11
|
}
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ describe("checkHealth", () => {
|
|
|
10
10
|
test("returns unreachable for non-existent host", async () => {
|
|
11
11
|
const result = await checkHealth("http://127.0.0.1:1");
|
|
12
12
|
expect(["unreachable", "timeout"]).toContain(result.status);
|
|
13
|
+
expect(result.version).toBeUndefined();
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
test("returns healthy for a mock healthy endpoint", async () => {
|
|
@@ -24,6 +25,24 @@ describe("checkHealth", () => {
|
|
|
24
25
|
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
25
26
|
expect(result.status).toBe("healthy");
|
|
26
27
|
expect(result.detail).toBeNull();
|
|
28
|
+
expect(result.version).toBeUndefined();
|
|
29
|
+
} finally {
|
|
30
|
+
server.stop(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns version when present in response", async () => {
|
|
35
|
+
const server = Bun.serve({
|
|
36
|
+
port: 0,
|
|
37
|
+
fetch() {
|
|
38
|
+
return Response.json({ status: "healthy", version: "1.2.3" });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
44
|
+
expect(result.status).toBe("healthy");
|
|
45
|
+
expect(result.version).toBe("1.2.3");
|
|
27
46
|
} finally {
|
|
28
47
|
server.stop(true);
|
|
29
48
|
}
|
|
@@ -33,7 +52,11 @@ describe("checkHealth", () => {
|
|
|
33
52
|
const server = Bun.serve({
|
|
34
53
|
port: 0,
|
|
35
54
|
fetch() {
|
|
36
|
-
return Response.json({
|
|
55
|
+
return Response.json({
|
|
56
|
+
status: "degraded",
|
|
57
|
+
message: "high latency",
|
|
58
|
+
version: "0.9.0",
|
|
59
|
+
});
|
|
37
60
|
},
|
|
38
61
|
});
|
|
39
62
|
|
|
@@ -41,6 +64,7 @@ describe("checkHealth", () => {
|
|
|
41
64
|
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
42
65
|
expect(result.status).toBe("degraded");
|
|
43
66
|
expect(result.detail).toBe("high latency");
|
|
67
|
+
expect(result.version).toBe("0.9.0");
|
|
44
68
|
} finally {
|
|
45
69
|
server.stop(true);
|
|
46
70
|
}
|
|
@@ -57,6 +81,7 @@ describe("checkHealth", () => {
|
|
|
57
81
|
try {
|
|
58
82
|
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
59
83
|
expect(result.status).toBe("error (500)");
|
|
84
|
+
expect(result.version).toBeUndefined();
|
|
60
85
|
} finally {
|
|
61
86
|
server.stop(true);
|
|
62
87
|
}
|
package/src/commands/backup.ts
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { dirname, join } from "path";
|
|
4
3
|
|
|
5
4
|
import { findAssistantByName } from "../lib/assistant-config";
|
|
5
|
+
import { getBackupsDir, formatSize } from "../lib/backup-ops.js";
|
|
6
6
|
import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
|
|
7
7
|
|
|
8
|
-
function getBackupsDir(): string {
|
|
9
|
-
const dataHome =
|
|
10
|
-
process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
|
|
11
|
-
return join(dataHome, "vellum", "backups");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function formatSize(bytes: number): string {
|
|
15
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
16
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
17
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
8
|
export async function backup(): Promise<void> {
|
|
21
9
|
const args = process.argv.slice(3);
|
|
22
10
|
|
|
@@ -104,6 +92,33 @@ export async function backup(): Promise<void> {
|
|
|
104
92
|
body: JSON.stringify({ description: "CLI backup" }),
|
|
105
93
|
signal: AbortSignal.timeout(120_000),
|
|
106
94
|
});
|
|
95
|
+
|
|
96
|
+
// Retry once with a fresh token on 401 — the cached token may be stale
|
|
97
|
+
// after a container restart that generated a new gateway signing key.
|
|
98
|
+
if (response.status === 401) {
|
|
99
|
+
let refreshedToken: string | null = null;
|
|
100
|
+
try {
|
|
101
|
+
const freshToken = await leaseGuardianToken(
|
|
102
|
+
entry.runtimeUrl,
|
|
103
|
+
entry.assistantId,
|
|
104
|
+
);
|
|
105
|
+
refreshedToken = freshToken.accessToken;
|
|
106
|
+
} catch {
|
|
107
|
+
// If token refresh fails, fall through to the !response.ok handler below
|
|
108
|
+
}
|
|
109
|
+
if (refreshedToken) {
|
|
110
|
+
accessToken = refreshedToken;
|
|
111
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${accessToken}`,
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({ description: "CLI backup" }),
|
|
118
|
+
signal: AbortSignal.timeout(120_000),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
107
122
|
} catch (err) {
|
|
108
123
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
109
124
|
console.error("Error: Export request timed out after 2 minutes.");
|
package/src/commands/hatch.ts
CHANGED
|
@@ -39,12 +39,14 @@ import type { RemoteHost, Species } from "../lib/constants";
|
|
|
39
39
|
import { hatchDocker } from "../lib/docker";
|
|
40
40
|
import { hatchGcp } from "../lib/gcp";
|
|
41
41
|
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
42
|
+
import { buildNestedConfig, writeInitialConfig } from "../lib/config-utils";
|
|
42
43
|
import {
|
|
43
44
|
startLocalDaemon,
|
|
44
45
|
startGateway,
|
|
45
46
|
stopLocalProcesses,
|
|
46
47
|
} from "../lib/local";
|
|
47
48
|
import { maybeStartNgrokTunnel } from "../lib/ngrok";
|
|
49
|
+
import { getPlatformUrl } from "../lib/platform-client";
|
|
48
50
|
import { httpHealthCheck } from "../lib/http-client";
|
|
49
51
|
import { detectOrphanedProcesses } from "../lib/orphan-detection";
|
|
50
52
|
import { isProcessAlive, stopProcess } from "../lib/process";
|
|
@@ -99,8 +101,9 @@ export async function buildStartupScript(
|
|
|
99
101
|
providerApiKeys: Record<string, string>,
|
|
100
102
|
instanceName: string,
|
|
101
103
|
cloud: RemoteHost,
|
|
104
|
+
configValues: Record<string, string> = {},
|
|
102
105
|
): Promise<string> {
|
|
103
|
-
const platformUrl =
|
|
106
|
+
const platformUrl = getPlatformUrl();
|
|
104
107
|
const logPath =
|
|
105
108
|
cloud === "custom"
|
|
106
109
|
? "/tmp/vellum-startup.log"
|
|
@@ -130,6 +133,22 @@ export async function buildStartupScript(
|
|
|
130
133
|
.map((envVar) => `${envVar}=\$${envVar}`)
|
|
131
134
|
.join("\n");
|
|
132
135
|
|
|
136
|
+
// Write --config key=value pairs to a temp JSON file on the remote host
|
|
137
|
+
// and export the env var so the daemon reads it on first boot.
|
|
138
|
+
let configWriteBlock = "";
|
|
139
|
+
if (Object.keys(configValues).length > 0) {
|
|
140
|
+
const configJson = JSON.stringify(buildNestedConfig(configValues), null, 2);
|
|
141
|
+
configWriteBlock = `
|
|
142
|
+
echo "Writing default workspace config..."
|
|
143
|
+
VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH="/tmp/vellum-initial-config-$$.json"
|
|
144
|
+
cat > "\$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH" << 'VELLUM_CONFIG_EOF'
|
|
145
|
+
${configJson}
|
|
146
|
+
VELLUM_CONFIG_EOF
|
|
147
|
+
export VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH
|
|
148
|
+
echo "Default workspace config written to \$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH"
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
133
152
|
return `#!/bin/bash
|
|
134
153
|
set -e
|
|
135
154
|
|
|
@@ -146,9 +165,10 @@ RUNTIME_HTTP_PORT=7821
|
|
|
146
165
|
DOTENV_EOF
|
|
147
166
|
|
|
148
167
|
${ownershipFixup}
|
|
149
|
-
|
|
168
|
+
${configWriteBlock}
|
|
150
169
|
export VELLUM_SSH_USER="\$SSH_USER"
|
|
151
170
|
export VELLUM_ASSISTANT_NAME="\$VELLUM_ASSISTANT_NAME"
|
|
171
|
+
export VELLUM_CLOUD="${cloud}"
|
|
152
172
|
echo "Downloading install script from ${platformUrl}/install.sh..."
|
|
153
173
|
curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
|
|
154
174
|
echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes)"
|
|
@@ -166,9 +186,9 @@ interface HatchArgs {
|
|
|
166
186
|
keepAlive: boolean;
|
|
167
187
|
name: string | null;
|
|
168
188
|
remote: RemoteHost;
|
|
169
|
-
daemonOnly: boolean;
|
|
170
189
|
restart: boolean;
|
|
171
190
|
watch: boolean;
|
|
191
|
+
configValues: Record<string, string>;
|
|
172
192
|
}
|
|
173
193
|
|
|
174
194
|
function parseArgs(): HatchArgs {
|
|
@@ -178,9 +198,9 @@ function parseArgs(): HatchArgs {
|
|
|
178
198
|
let keepAlive = false;
|
|
179
199
|
let name: string | null = null;
|
|
180
200
|
let remote: RemoteHost = DEFAULT_REMOTE;
|
|
181
|
-
let daemonOnly = false;
|
|
182
201
|
let restart = false;
|
|
183
202
|
let watch = false;
|
|
203
|
+
const configValues: Record<string, string> = {};
|
|
184
204
|
|
|
185
205
|
for (let i = 0; i < args.length; i++) {
|
|
186
206
|
const arg = args[i];
|
|
@@ -199,9 +219,6 @@ function parseArgs(): HatchArgs {
|
|
|
199
219
|
console.log(
|
|
200
220
|
" --remote <host> Remote host (local, gcp, aws, docker, custom)",
|
|
201
221
|
);
|
|
202
|
-
console.log(
|
|
203
|
-
" --daemon-only Start assistant only, skip gateway",
|
|
204
|
-
);
|
|
205
222
|
console.log(
|
|
206
223
|
" --restart Restart processes without onboarding side effects",
|
|
207
224
|
);
|
|
@@ -211,11 +228,12 @@ function parseArgs(): HatchArgs {
|
|
|
211
228
|
console.log(
|
|
212
229
|
" --keep-alive Stay alive after hatch, exit when gateway stops",
|
|
213
230
|
);
|
|
231
|
+
console.log(
|
|
232
|
+
" --config <key=value> Set a workspace config value (repeatable)",
|
|
233
|
+
);
|
|
214
234
|
process.exit(0);
|
|
215
235
|
} else if (arg === "-d") {
|
|
216
236
|
detached = true;
|
|
217
|
-
} else if (arg === "--daemon-only") {
|
|
218
|
-
daemonOnly = true;
|
|
219
237
|
} else if (arg === "--restart") {
|
|
220
238
|
restart = true;
|
|
221
239
|
} else if (arg === "--watch") {
|
|
@@ -248,11 +266,28 @@ function parseArgs(): HatchArgs {
|
|
|
248
266
|
}
|
|
249
267
|
remote = next as RemoteHost;
|
|
250
268
|
i++;
|
|
269
|
+
} else if (arg === "--config") {
|
|
270
|
+
const next = args[i + 1];
|
|
271
|
+
if (!next || next.startsWith("-")) {
|
|
272
|
+
console.error("Error: --config requires a key=value argument");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
const eqIndex = next.indexOf("=");
|
|
276
|
+
if (eqIndex <= 0) {
|
|
277
|
+
console.error(
|
|
278
|
+
`Error: --config value must be in key=value format, got '${next}'`,
|
|
279
|
+
);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
const key = next.slice(0, eqIndex);
|
|
283
|
+
const value = next.slice(eqIndex + 1);
|
|
284
|
+
configValues[key] = value;
|
|
285
|
+
i++;
|
|
251
286
|
} else if (VALID_SPECIES.includes(arg as Species)) {
|
|
252
287
|
species = arg as Species;
|
|
253
288
|
} else {
|
|
254
289
|
console.error(
|
|
255
|
-
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --
|
|
290
|
+
`Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --restart, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>`,
|
|
256
291
|
);
|
|
257
292
|
process.exit(1);
|
|
258
293
|
}
|
|
@@ -264,9 +299,9 @@ function parseArgs(): HatchArgs {
|
|
|
264
299
|
keepAlive,
|
|
265
300
|
name,
|
|
266
301
|
remote,
|
|
267
|
-
daemonOnly,
|
|
268
302
|
restart,
|
|
269
303
|
watch,
|
|
304
|
+
configValues,
|
|
270
305
|
};
|
|
271
306
|
}
|
|
272
307
|
|
|
@@ -574,10 +609,10 @@ function installCLISymlink(): void {
|
|
|
574
609
|
async function hatchLocal(
|
|
575
610
|
species: Species,
|
|
576
611
|
name: string | null,
|
|
577
|
-
daemonOnly: boolean = false,
|
|
578
612
|
restart: boolean = false,
|
|
579
613
|
watch: boolean = false,
|
|
580
614
|
keepAlive: boolean = false,
|
|
615
|
+
configValues: Record<string, string> = {},
|
|
581
616
|
): Promise<void> {
|
|
582
617
|
if (restart && !name && !process.env.VELLUM_ASSISTANT_NAME) {
|
|
583
618
|
console.error(
|
|
@@ -709,46 +744,44 @@ async function hatchLocal(
|
|
|
709
744
|
process.env.APP_VERSION = cliPkg.version;
|
|
710
745
|
}
|
|
711
746
|
|
|
712
|
-
|
|
747
|
+
const defaultWorkspaceConfigPath = writeInitialConfig(configValues);
|
|
748
|
+
|
|
749
|
+
await startLocalDaemon(watch, resources, { defaultWorkspaceConfigPath });
|
|
713
750
|
|
|
714
|
-
// When daemonOnly is set, skip gateway and ngrok — the caller only wants
|
|
715
|
-
// the daemon restarted (e.g. macOS app bootstrap retry).
|
|
716
751
|
let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
752
|
+
try {
|
|
753
|
+
runtimeUrl = await startGateway(watch, resources);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
// Gateway failed — stop the daemon we just started so we don't leave
|
|
756
|
+
// orphaned processes with no lock file entry.
|
|
757
|
+
console.error(
|
|
758
|
+
`\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
|
|
759
|
+
);
|
|
760
|
+
await stopLocalProcesses(resources);
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
729
763
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
764
|
+
// Lease a guardian token so the desktop app can import it on first launch
|
|
765
|
+
// instead of hitting /v1/guardian/init itself.
|
|
766
|
+
try {
|
|
767
|
+
await leaseGuardianToken(runtimeUrl, instanceName);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
console.error(`⚠️ Guardian token lease failed: ${err}`);
|
|
770
|
+
}
|
|
737
771
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
772
|
+
// Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
|
|
773
|
+
// Set BASE_DATA_DIR so ngrok reads the correct instance config.
|
|
774
|
+
const prevBaseDataDir = process.env.BASE_DATA_DIR;
|
|
775
|
+
process.env.BASE_DATA_DIR = resources.instanceDir;
|
|
776
|
+
const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
|
|
777
|
+
if (ngrokChild?.pid) {
|
|
778
|
+
const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
|
|
779
|
+
writeFileSync(ngrokPidFile, String(ngrokChild.pid));
|
|
780
|
+
}
|
|
781
|
+
if (prevBaseDataDir !== undefined) {
|
|
782
|
+
process.env.BASE_DATA_DIR = prevBaseDataDir;
|
|
783
|
+
} else {
|
|
784
|
+
delete process.env.BASE_DATA_DIR;
|
|
752
785
|
}
|
|
753
786
|
|
|
754
787
|
const localEntry: AssistantEntry = {
|
|
@@ -761,7 +794,7 @@ async function hatchLocal(
|
|
|
761
794
|
serviceGroupVersion: cliPkg.version ? `v${cliPkg.version}` : undefined,
|
|
762
795
|
resources,
|
|
763
796
|
};
|
|
764
|
-
if (!
|
|
797
|
+
if (!restart) {
|
|
765
798
|
saveAssistantEntry(localEntry);
|
|
766
799
|
setActiveAssistant(instanceName);
|
|
767
800
|
syncConfigToLockfile();
|
|
@@ -780,12 +813,8 @@ async function hatchLocal(
|
|
|
780
813
|
}
|
|
781
814
|
|
|
782
815
|
if (keepAlive) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const healthUrl = daemonOnly
|
|
786
|
-
? `http://127.0.0.1:${resources.daemonPort}/healthz`
|
|
787
|
-
: `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
788
|
-
const healthTarget = daemonOnly ? "Assistant" : "Gateway";
|
|
816
|
+
const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
817
|
+
const healthTarget = "Gateway";
|
|
789
818
|
const POLL_INTERVAL_MS = 5000;
|
|
790
819
|
const MAX_FAILURES = 3;
|
|
791
820
|
let consecutiveFailures = 0;
|
|
@@ -839,9 +868,9 @@ export async function hatch(): Promise<void> {
|
|
|
839
868
|
keepAlive,
|
|
840
869
|
name,
|
|
841
870
|
remote,
|
|
842
|
-
daemonOnly,
|
|
843
871
|
restart,
|
|
844
872
|
watch,
|
|
873
|
+
configValues,
|
|
845
874
|
} = parseArgs();
|
|
846
875
|
|
|
847
876
|
if (restart && remote !== "local") {
|
|
@@ -859,22 +888,29 @@ export async function hatch(): Promise<void> {
|
|
|
859
888
|
}
|
|
860
889
|
|
|
861
890
|
if (remote === "local") {
|
|
862
|
-
await hatchLocal(species, name,
|
|
891
|
+
await hatchLocal(species, name, restart, watch, keepAlive, configValues);
|
|
863
892
|
return;
|
|
864
893
|
}
|
|
865
894
|
|
|
866
895
|
if (remote === "gcp") {
|
|
867
|
-
await hatchGcp(
|
|
896
|
+
await hatchGcp(
|
|
897
|
+
species,
|
|
898
|
+
detached,
|
|
899
|
+
name,
|
|
900
|
+
buildStartupScript,
|
|
901
|
+
watchHatching,
|
|
902
|
+
configValues,
|
|
903
|
+
);
|
|
868
904
|
return;
|
|
869
905
|
}
|
|
870
906
|
|
|
871
907
|
if (remote === "aws") {
|
|
872
|
-
await hatchAws(species, detached, name);
|
|
908
|
+
await hatchAws(species, detached, name, configValues);
|
|
873
909
|
return;
|
|
874
910
|
}
|
|
875
911
|
|
|
876
912
|
if (remote === "docker") {
|
|
877
|
-
await hatchDocker(species, detached, name, watch);
|
|
913
|
+
await hatchDocker(species, detached, name, watch, configValues);
|
|
878
914
|
return;
|
|
879
915
|
}
|
|
880
916
|
|
package/src/commands/ps.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
findAssistantByName,
|
|
5
5
|
getActiveAssistant,
|
|
6
6
|
loadAllAssistants,
|
|
7
|
+
updateServiceGroupVersion,
|
|
7
8
|
type AssistantEntry,
|
|
8
9
|
} from "../lib/assistant-config";
|
|
9
10
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
@@ -424,7 +425,7 @@ async function listAllAssistants(): Promise<void> {
|
|
|
424
425
|
// hitting the health endpoint. If the PID file is missing or the
|
|
425
426
|
// process isn't running, the assistant is sleeping — skip the
|
|
426
427
|
// network health check to avoid a misleading "unreachable" status.
|
|
427
|
-
let health: { status: string; detail: string | null };
|
|
428
|
+
let health: { status: string; detail: string | null; version?: string };
|
|
428
429
|
const resources = a.resources;
|
|
429
430
|
if (a.cloud === "local" && resources) {
|
|
430
431
|
const pid = readPidFile(resources.pidFile);
|
|
@@ -451,6 +452,10 @@ async function listAllAssistants(): Promise<void> {
|
|
|
451
452
|
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
if (health.status === "healthy" && health.version) {
|
|
456
|
+
updateServiceGroupVersion(a.assistantId, health.version);
|
|
457
|
+
}
|
|
458
|
+
|
|
454
459
|
const infoParts = [a.runtimeUrl];
|
|
455
460
|
if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
|
|
456
461
|
if (a.species) infoParts.push(`species: ${a.species}`);
|
package/src/commands/restore.ts
CHANGED
|
@@ -89,28 +89,39 @@ interface PreflightFileEntry {
|
|
|
89
89
|
action: string;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
interface StructuredError {
|
|
93
|
+
code: string;
|
|
94
|
+
message: string;
|
|
95
|
+
path?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
interface PreflightResponse {
|
|
93
99
|
can_import: boolean;
|
|
94
|
-
|
|
100
|
+
validation?: {
|
|
101
|
+
is_valid: false;
|
|
102
|
+
errors: StructuredError[];
|
|
103
|
+
};
|
|
95
104
|
files?: PreflightFileEntry[];
|
|
96
105
|
summary?: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
files_to_create: number;
|
|
107
|
+
files_to_overwrite: number;
|
|
108
|
+
files_unchanged: number;
|
|
109
|
+
total_files: number;
|
|
101
110
|
};
|
|
102
|
-
conflicts?:
|
|
111
|
+
conflicts?: StructuredError[];
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
interface ImportResponse {
|
|
106
115
|
success: boolean;
|
|
107
116
|
reason?: string;
|
|
108
|
-
errors?:
|
|
117
|
+
errors?: StructuredError[];
|
|
118
|
+
message?: string;
|
|
109
119
|
warnings?: string[];
|
|
110
120
|
summary?: {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
total_files: number;
|
|
122
|
+
files_created: number;
|
|
123
|
+
files_overwritten: number;
|
|
124
|
+
files_skipped: number;
|
|
114
125
|
backups_created: number;
|
|
115
126
|
};
|
|
116
127
|
}
|
|
@@ -201,30 +212,38 @@ export async function restore(): Promise<void> {
|
|
|
201
212
|
const result = (await response.json()) as PreflightResponse;
|
|
202
213
|
|
|
203
214
|
if (!result.can_import) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
if (result.validation?.errors?.length) {
|
|
216
|
+
console.error("Import blocked by validation errors:");
|
|
217
|
+
for (const err of result.validation.errors) {
|
|
218
|
+
console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (result.conflicts?.length) {
|
|
222
|
+
console.error("Import blocked by conflicts:");
|
|
223
|
+
for (const conflict of result.conflicts) {
|
|
224
|
+
console.error(` - ${conflict.message}${conflict.path ? ` (${conflict.path})` : ""}`);
|
|
225
|
+
}
|
|
207
226
|
}
|
|
208
227
|
process.exit(1);
|
|
209
228
|
}
|
|
210
229
|
|
|
211
230
|
// Print summary table
|
|
212
231
|
const summary = result.summary ?? {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
files_to_create: 0,
|
|
233
|
+
files_to_overwrite: 0,
|
|
234
|
+
files_unchanged: 0,
|
|
235
|
+
total_files: 0,
|
|
217
236
|
};
|
|
218
237
|
console.log("Preflight analysis:");
|
|
219
|
-
console.log(` Files to create: ${summary.
|
|
220
|
-
console.log(` Files to overwrite: ${summary.
|
|
221
|
-
console.log(` Files unchanged: ${summary.
|
|
222
|
-
console.log(` Total: ${summary.
|
|
238
|
+
console.log(` Files to create: ${summary.files_to_create}`);
|
|
239
|
+
console.log(` Files to overwrite: ${summary.files_to_overwrite}`);
|
|
240
|
+
console.log(` Files unchanged: ${summary.files_unchanged}`);
|
|
241
|
+
console.log(` Total: ${summary.total_files}`);
|
|
223
242
|
console.log("");
|
|
224
243
|
|
|
225
244
|
const conflicts = result.conflicts ?? [];
|
|
226
245
|
console.log(
|
|
227
|
-
`Conflicts: ${conflicts.length > 0 ? conflicts.join(", ") : "none"}`,
|
|
246
|
+
`Conflicts: ${conflicts.length > 0 ? conflicts.map((c) => c.message).join(", ") : "none"}`,
|
|
228
247
|
);
|
|
229
248
|
|
|
230
249
|
// List individual files with their action
|
|
@@ -276,25 +295,26 @@ export async function restore(): Promise<void> {
|
|
|
276
295
|
|
|
277
296
|
if (!result.success) {
|
|
278
297
|
console.error(
|
|
279
|
-
`Error: Import failed — ${result.reason ?? "unknown reason"}`,
|
|
298
|
+
`Error: Import failed — ${result.message ?? result.reason ?? "unknown reason"}`,
|
|
280
299
|
);
|
|
281
300
|
for (const err of result.errors ?? []) {
|
|
282
|
-
console.error(` - ${err}`);
|
|
301
|
+
console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
|
|
283
302
|
}
|
|
284
303
|
process.exit(1);
|
|
285
304
|
}
|
|
286
305
|
|
|
287
306
|
// Print import report
|
|
288
307
|
const summary = result.summary ?? {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
total_files: 0,
|
|
309
|
+
files_created: 0,
|
|
310
|
+
files_overwritten: 0,
|
|
311
|
+
files_skipped: 0,
|
|
292
312
|
backups_created: 0,
|
|
293
313
|
};
|
|
294
314
|
console.log("✅ Restore complete.");
|
|
295
|
-
console.log(` Files created: ${summary.
|
|
296
|
-
console.log(` Files overwritten: ${summary.
|
|
297
|
-
console.log(` Files skipped: ${summary.
|
|
315
|
+
console.log(` Files created: ${summary.files_created}`);
|
|
316
|
+
console.log(` Files overwritten: ${summary.files_overwritten}`);
|
|
317
|
+
console.log(` Files skipped: ${summary.files_skipped}`);
|
|
298
318
|
console.log(` Backups created: ${summary.backups_created}`);
|
|
299
319
|
|
|
300
320
|
// Print warnings if any
|
package/src/commands/retire.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
3
|
-
import { homedir } from "os";
|
|
4
3
|
import { basename, dirname, join } from "path";
|
|
5
4
|
|
|
6
5
|
import {
|
|
7
6
|
findAssistantByName,
|
|
7
|
+
getBaseDir,
|
|
8
8
|
loadAllAssistants,
|
|
9
9
|
removeAssistantEntry,
|
|
10
10
|
} from "../lib/assistant-config";
|
|
@@ -109,10 +109,10 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
|
|
|
109
109
|
await stopOrphanedDaemonProcesses();
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// For named instances (instanceDir differs from
|
|
113
|
-
// remove the entire instance directory. For the default
|
|
114
|
-
//
|
|
115
|
-
const isNamedInstance = resources.instanceDir !==
|
|
112
|
+
// For named instances (instanceDir differs from the base directory),
|
|
113
|
+
// archive and remove the entire instance directory. For the default
|
|
114
|
+
// instance, archive only the .vellum subdirectory.
|
|
115
|
+
const isNamedInstance = resources.instanceDir !== getBaseDir();
|
|
116
116
|
const dirToArchive = isNamedInstance ? resources.instanceDir : vellumDir;
|
|
117
117
|
|
|
118
118
|
// Move the data directory out of the way so the path is immediately available
|