@vellumai/cli 0.5.0 → 0.5.2
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 +5 -1
- package/package.json +1 -1
- package/src/commands/client.ts +7 -2
- package/src/commands/hatch.ts +9 -0
- package/src/commands/upgrade.ts +70 -9
- package/src/lib/docker.ts +48 -0
- package/src/lib/guardian-token.ts +7 -1
- package/src/lib/local.ts +2 -2
package/knip.json
CHANGED
package/package.json
CHANGED
package/src/commands/client.ts
CHANGED
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
getActiveAssistant,
|
|
6
6
|
loadLatestAssistant,
|
|
7
7
|
} from "../lib/assistant-config";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
DAEMON_INTERNAL_ASSISTANT_ID,
|
|
10
|
+
GATEWAY_PORT,
|
|
11
|
+
type Species,
|
|
12
|
+
} from "../lib/constants";
|
|
9
13
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
10
14
|
import { getLocalLanIPv4, getMacLocalHostname } from "../lib/local";
|
|
11
15
|
|
|
@@ -84,7 +88,8 @@ function parseArgs(): ParsedArgs {
|
|
|
84
88
|
|
|
85
89
|
let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
|
|
86
90
|
let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
|
|
87
|
-
const bearerToken =
|
|
91
|
+
const bearerToken =
|
|
92
|
+
loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined;
|
|
88
93
|
const species: Species = (entry?.species as Species) ?? "vellum";
|
|
89
94
|
|
|
90
95
|
for (let i = 0; i < flagArgs.length; i++) {
|
package/src/commands/hatch.ts
CHANGED
|
@@ -50,6 +50,7 @@ import { detectOrphanedProcesses } from "../lib/orphan-detection";
|
|
|
50
50
|
import { isProcessAlive, stopProcess } from "../lib/process";
|
|
51
51
|
import { generateInstanceName } from "../lib/random-name";
|
|
52
52
|
import { validateAssistantName } from "../lib/retire-archive";
|
|
53
|
+
import { leaseGuardianToken } from "../lib/guardian-token";
|
|
53
54
|
import { archiveLogFile, resetLogFile } from "../lib/xdg-log";
|
|
54
55
|
|
|
55
56
|
export type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
@@ -717,6 +718,14 @@ async function hatchLocal(
|
|
|
717
718
|
throw error;
|
|
718
719
|
}
|
|
719
720
|
|
|
721
|
+
// Lease a guardian token so the desktop app can import it on first launch
|
|
722
|
+
// instead of hitting /v1/guardian/init itself.
|
|
723
|
+
try {
|
|
724
|
+
await leaseGuardianToken(runtimeUrl, instanceName);
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.error(`⚠️ Guardian token lease failed: ${err}`);
|
|
727
|
+
}
|
|
728
|
+
|
|
720
729
|
// Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
|
|
721
730
|
// Set BASE_DATA_DIR so ngrok reads the correct instance config.
|
|
722
731
|
const prevBaseDataDir = process.env.BASE_DATA_DIR;
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "../lib/assistant-config";
|
|
8
8
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
9
9
|
import {
|
|
10
|
+
captureImageRefs,
|
|
10
11
|
DOCKERHUB_IMAGES,
|
|
11
12
|
DOCKER_READY_TIMEOUT_MS,
|
|
12
13
|
GATEWAY_INTERNAL_PORT,
|
|
@@ -212,11 +213,21 @@ async function upgradeDocker(
|
|
|
212
213
|
`🔄 Upgrading Docker assistant '${instanceName}' to ${versionTag}...\n`,
|
|
213
214
|
);
|
|
214
215
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
console.log("
|
|
216
|
+
// Capture rollback state from existing containers BEFORE pulling new
|
|
217
|
+
// images or stopping anything. captureImageRefs uses the immutable
|
|
218
|
+
// image digest ({{.Image}}), but capturing first keeps the intent
|
|
219
|
+
// explicit and avoids relying on container-inspect ordering subtleties.
|
|
220
|
+
console.log("📸 Capturing current image references for rollback...");
|
|
221
|
+
const previousImageRefs = await captureImageRefs(res);
|
|
222
|
+
if (previousImageRefs) {
|
|
223
|
+
console.log(
|
|
224
|
+
` Captured refs for ${Object.keys(previousImageRefs).length} service(s)\n`,
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(
|
|
228
|
+
" Could not capture all container refs (fresh install or partial deployment)\n",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
220
231
|
|
|
221
232
|
console.log("💾 Capturing existing container environment...");
|
|
222
233
|
const capturedEnv = await captureContainerEnv(res.assistantContainer);
|
|
@@ -224,6 +235,12 @@ async function upgradeDocker(
|
|
|
224
235
|
` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
|
|
225
236
|
);
|
|
226
237
|
|
|
238
|
+
console.log("📦 Pulling new Docker images...");
|
|
239
|
+
await exec("docker", ["pull", imageTags.assistant]);
|
|
240
|
+
await exec("docker", ["pull", imageTags.gateway]);
|
|
241
|
+
await exec("docker", ["pull", imageTags["credential-executor"]]);
|
|
242
|
+
console.log("✅ Docker images pulled\n");
|
|
243
|
+
|
|
227
244
|
console.log("🛑 Stopping existing containers...");
|
|
228
245
|
await stopContainers(res);
|
|
229
246
|
console.log("✅ Containers stopped\n");
|
|
@@ -281,10 +298,54 @@ async function upgradeDocker(
|
|
|
281
298
|
`\n✅ Docker assistant '${instanceName}' upgraded to ${versionTag}.`,
|
|
282
299
|
);
|
|
283
300
|
} else {
|
|
284
|
-
console.
|
|
285
|
-
|
|
286
|
-
)
|
|
287
|
-
|
|
301
|
+
console.error(`\n❌ Containers failed to become ready within the timeout.`);
|
|
302
|
+
|
|
303
|
+
if (previousImageRefs) {
|
|
304
|
+
console.log(`\n🔄 Rolling back to previous images...`);
|
|
305
|
+
try {
|
|
306
|
+
await stopContainers(res);
|
|
307
|
+
|
|
308
|
+
await startContainers(
|
|
309
|
+
{
|
|
310
|
+
extraAssistantEnv,
|
|
311
|
+
gatewayPort,
|
|
312
|
+
imageTags: previousImageRefs,
|
|
313
|
+
instanceName,
|
|
314
|
+
res,
|
|
315
|
+
},
|
|
316
|
+
(msg) => console.log(msg),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const rollbackReady = await waitForReady(entry.runtimeUrl);
|
|
320
|
+
if (rollbackReady) {
|
|
321
|
+
console.log(
|
|
322
|
+
`\n⚠️ Rolled back to previous version. Upgrade to ${versionTag} failed.`,
|
|
323
|
+
);
|
|
324
|
+
} else {
|
|
325
|
+
console.error(
|
|
326
|
+
`\n❌ Rollback also failed. Manual intervention required.`,
|
|
327
|
+
);
|
|
328
|
+
console.log(
|
|
329
|
+
` Check logs with: docker logs -f ${res.assistantContainer}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
} catch (rollbackErr) {
|
|
333
|
+
console.error(
|
|
334
|
+
`\n❌ Rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
335
|
+
);
|
|
336
|
+
console.error(` Manual intervention required.`);
|
|
337
|
+
console.log(
|
|
338
|
+
` Check logs with: docker logs -f ${res.assistantContainer}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
console.log(` No previous images available for rollback.`);
|
|
343
|
+
console.log(
|
|
344
|
+
` Check logs with: docker logs -f ${res.assistantContainer}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
process.exit(1);
|
|
288
349
|
}
|
|
289
350
|
}
|
|
290
351
|
|
package/src/lib/docker.ts
CHANGED
|
@@ -569,6 +569,54 @@ export async function stopContainers(
|
|
|
569
569
|
await removeContainer(res.assistantContainer);
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Capture the current image references for running service containers.
|
|
574
|
+
* Returns a complete record of service → immutable image ID (sha256 digest)
|
|
575
|
+
* for all three services. Uses `{{.Image}}` rather than `{{.Config.Image}}`
|
|
576
|
+
* so rollback targets the exact image that was running, even if the tag has
|
|
577
|
+
* since been retagged to a different image.
|
|
578
|
+
*
|
|
579
|
+
* Returns null if any container could not be inspected (e.g. fresh install
|
|
580
|
+
* or partial deployment).
|
|
581
|
+
*/
|
|
582
|
+
export async function captureImageRefs(
|
|
583
|
+
res: ReturnType<typeof dockerResourceNames>,
|
|
584
|
+
): Promise<Record<ServiceName, string> | null> {
|
|
585
|
+
const containerForService: Record<ServiceName, string> = {
|
|
586
|
+
assistant: res.assistantContainer,
|
|
587
|
+
"credential-executor": res.cesContainer,
|
|
588
|
+
gateway: res.gatewayContainer,
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const refs: Partial<Record<ServiceName, string>> = {};
|
|
592
|
+
|
|
593
|
+
for (const [service, container] of Object.entries(containerForService)) {
|
|
594
|
+
try {
|
|
595
|
+
const imageRef = (
|
|
596
|
+
await execOutput("docker", [
|
|
597
|
+
"inspect",
|
|
598
|
+
"--format",
|
|
599
|
+
"{{.Image}}",
|
|
600
|
+
container,
|
|
601
|
+
])
|
|
602
|
+
).trim();
|
|
603
|
+
if (imageRef) {
|
|
604
|
+
refs[service as ServiceName] = imageRef;
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
// Container doesn't exist or can't be inspected — skip
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const allServices: ServiceName[] = [
|
|
612
|
+
"assistant",
|
|
613
|
+
"credential-executor",
|
|
614
|
+
"gateway",
|
|
615
|
+
];
|
|
616
|
+
const hasAll = allServices.every((s) => s in refs);
|
|
617
|
+
return hasAll ? (refs as Record<ServiceName, string>) : null;
|
|
618
|
+
}
|
|
619
|
+
|
|
572
620
|
/**
|
|
573
621
|
* Determine which services are affected by a changed file path relative
|
|
574
622
|
* to the repository root.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "fs";
|
|
4
10
|
import { homedir, platform } from "os";
|
|
5
11
|
import { dirname, join } from "path";
|
|
6
12
|
|
package/src/lib/local.ts
CHANGED
|
@@ -805,8 +805,8 @@ export function isAssistantWatchModeAvailable(): boolean {
|
|
|
805
805
|
*/
|
|
806
806
|
export function isGatewayWatchModeAvailable(): boolean {
|
|
807
807
|
try {
|
|
808
|
-
resolveGatewayDir();
|
|
809
|
-
return
|
|
808
|
+
const dir = resolveGatewayDir();
|
|
809
|
+
return existsSync(join(dir, "src", "index.ts"));
|
|
810
810
|
} catch {
|
|
811
811
|
return false;
|
|
812
812
|
}
|