@vellumai/cli 0.8.5 → 0.8.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/AGENTS.md +6 -0
- package/bun.lock +8 -0
- package/knip.json +6 -1
- package/node_modules/@vellumai/environments/bun.lock +24 -0
- package/node_modules/@vellumai/environments/package.json +18 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
- package/node_modules/@vellumai/environments/src/index.ts +11 -0
- package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
- package/node_modules/@vellumai/environments/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/bun.lock +29 -0
- package/node_modules/@vellumai/local-mode/package.json +21 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +93 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +59 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +67 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +74 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +26 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +131 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
- package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
- package/package.json +12 -1
- package/src/__tests__/backup.test.ts +38 -0
- package/src/__tests__/env-drift.test.ts +32 -44
- package/src/__tests__/flags.test.ts +248 -0
- package/src/__tests__/multi-local.test.ts +1 -1
- package/src/__tests__/orphan-detection.test.ts +8 -6
- package/src/__tests__/recover.test.ts +307 -0
- package/src/__tests__/segments-to-plain-text.test.ts +37 -0
- package/src/__tests__/wake.test.ts +215 -0
- package/src/commands/backup.ts +2 -0
- package/src/commands/client.ts +471 -30
- package/src/commands/env.ts +1 -1
- package/src/commands/flags.ts +269 -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/components/DefaultMainScreen.tsx +16 -1
- package/src/index.ts +6 -0
- package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
- package/src/lib/__tests__/step-runner.test.ts +49 -1
- package/src/lib/assistant-config.ts +16 -3
- package/src/lib/config-utils.ts +24 -3
- package/src/lib/docker.ts +57 -7
- package/src/lib/environments/__tests__/paths.test.ts +2 -1
- package/src/lib/environments/__tests__/seeds.test.ts +2 -1
- package/src/lib/environments/paths.ts +1 -1
- package/src/lib/environments/resolve.ts +2 -5
- package/src/lib/guardian-token.ts +12 -5
- package/src/lib/hatch-local.ts +75 -33
- package/src/lib/http-client.ts +1 -3
- package/src/lib/lifecycle-reporter.ts +31 -0
- 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/retire-local.ts +28 -14
- package/src/lib/segments-to-plain-text.ts +35 -0
- package/src/lib/step-runner.ts +67 -7
- package/src/lib/sync-cloud-assistants.ts +17 -0
- /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
package/AGENTS.md
CHANGED
|
@@ -63,6 +63,12 @@ The CLI must **never** read from or write to the `.vellum/` directory (e.g. `~/.
|
|
|
63
63
|
|
|
64
64
|
For example, the signing key used for JWT auth between the daemon and gateway is persisted in the lockfile (`resources.signingKey`) so that client actor tokens survive daemon/gateway restarts. On first start (or when the key is missing), the CLI generates a new key via `generateLocalSigningKey()` in `lib/local.ts`, saves it to the lockfile entry, and passes it to both `startLocalDaemon` and `startGateway` as the `ACTOR_TOKEN_SIGNING_KEY` env var. The CLI does **not** read or write to the `.vellum/` directory for signing keys — it uses the lockfile instead.
|
|
65
65
|
|
|
66
|
+
## Process liveness
|
|
67
|
+
|
|
68
|
+
Use `resolveProcessState()` from `lib/process.ts` when checking whether a daemon or gateway should be (re)started. It combines PID existence with an HTTP `/healthz` probe, a readiness grace period, and a [`isVellumProcess()`](https://man7.org/linux/man-pages/man1/ps.1.html) guard against PID reuse — see the function's JSDoc for the full flow.
|
|
69
|
+
|
|
70
|
+
Reserve `isProcessAlive()` for teardown paths (`sleep`, `retire`) where you need to kill a process regardless of its health.
|
|
71
|
+
|
|
66
72
|
## Docker Volume Management
|
|
67
73
|
|
|
68
74
|
The CLI creates and manages six per-instance Docker volumes with strict per-service access boundaries (least-privilege at the container level).
|
package/bun.lock
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@vellumai/cli",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"@vellumai/environments": "file:../packages/environments",
|
|
9
|
+
"@vellumai/local-mode": "file:../packages/local-mode",
|
|
8
10
|
"chalk": "5.6.2",
|
|
9
11
|
"ink": "6.8.0",
|
|
10
12
|
"nanoid": "5.1.7",
|
|
@@ -135,6 +137,10 @@
|
|
|
135
137
|
|
|
136
138
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
|
|
137
139
|
|
|
140
|
+
"@vellumai/environments": ["@vellumai/environments@file:../packages/environments", { "devDependencies": { "@types/bun": "1.3.11", "typescript": "5.9.3" } }],
|
|
141
|
+
|
|
142
|
+
"@vellumai/local-mode": ["@vellumai/local-mode@file:../packages/local-mode", { "dependencies": { "@vellumai/environments": "file:../environments" }, "devDependencies": { "@types/bun": "1.3.11", "typescript": "5.9.3" } }],
|
|
143
|
+
|
|
138
144
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
|
139
145
|
|
|
140
146
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
|
@@ -397,6 +403,8 @@
|
|
|
397
403
|
|
|
398
404
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
|
399
405
|
|
|
406
|
+
"@vellumai/local-mode/@vellumai/environments": ["@vellumai/environments@file:../packages/environments", {}],
|
|
407
|
+
|
|
400
408
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
|
401
409
|
|
|
402
410
|
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
package/knip.json
CHANGED
|
@@ -7,5 +7,10 @@
|
|
|
7
7
|
"src/lib/platform-releases.ts",
|
|
8
8
|
"src/lib/cli-error.ts"
|
|
9
9
|
],
|
|
10
|
-
"project": ["src/**/*.ts", "src/**/*.tsx"]
|
|
10
|
+
"project": ["src/**/*.ts", "src/**/*.tsx"],
|
|
11
|
+
"ignoreDependencies": [
|
|
12
|
+
"@vellumai/environments",
|
|
13
|
+
"@vellumai/local-mode",
|
|
14
|
+
"@vellumai/web"
|
|
15
|
+
]
|
|
11
16
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "@vellumai/environments",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "1.3.11",
|
|
9
|
+
"typescript": "5.9.3",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
|
15
|
+
|
|
16
|
+
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
|
17
|
+
|
|
18
|
+
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
|
19
|
+
|
|
20
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
21
|
+
|
|
22
|
+
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vellumai/environments",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "bunx tsc --noEmit",
|
|
12
|
+
"test": "bun test src/"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.11",
|
|
16
|
+
"typescript": "5.9.3"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package boundary tests for @vellumai/environments.
|
|
3
|
+
*
|
|
4
|
+
* This package is the lowest layer of the shared-packages hierarchy: pure
|
|
5
|
+
* environment types and constants with no runtime dependencies. It must
|
|
6
|
+
* stay a leaf so any consumer (CLI, assistant, local-mode host) can depend
|
|
7
|
+
* on it without dragging in a dependency tree.
|
|
8
|
+
*
|
|
9
|
+
* Enforces that the package:
|
|
10
|
+
* 1. Imports only node builtins and its own relative modules — no `@vellumai/*`
|
|
11
|
+
* packages and no third-party runtime imports.
|
|
12
|
+
* 2. Declares no runtime `dependencies` (devDependencies only).
|
|
13
|
+
* 3. Is marked `private`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from "bun:test";
|
|
17
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
18
|
+
import { join, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
const PACKAGE_ROOT = resolve(import.meta.dirname, "../..");
|
|
21
|
+
const SRC_DIR = join(PACKAGE_ROOT, "src");
|
|
22
|
+
|
|
23
|
+
function collectSourceFiles(dir: string): string[] {
|
|
24
|
+
const files: string[] = [];
|
|
25
|
+
for (const entry of readdirSync(dir)) {
|
|
26
|
+
const full = join(dir, entry);
|
|
27
|
+
const stat = statSync(full);
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
if (entry === "node_modules" || entry === "__tests__") continue;
|
|
30
|
+
files.push(...collectSourceFiles(full));
|
|
31
|
+
} else if (
|
|
32
|
+
entry.endsWith(".ts") &&
|
|
33
|
+
!entry.endsWith(".test.ts") &&
|
|
34
|
+
!entry.endsWith(".d.ts")
|
|
35
|
+
) {
|
|
36
|
+
files.push(full);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Matches the module specifier of any `import ... from "<spec>"` /
|
|
44
|
+
* `export ... from "<spec>"` / `require("<spec>")` statement.
|
|
45
|
+
*/
|
|
46
|
+
const IMPORT_SPEC = /(?:from|require\s*\(\s*)["']([^"']+)["']/g;
|
|
47
|
+
|
|
48
|
+
/** A bare specifier is anything that is not a relative or node-builtin import. */
|
|
49
|
+
function isForbiddenSpecifier(spec: string): boolean {
|
|
50
|
+
if (spec.startsWith(".") || spec.startsWith("/")) return false;
|
|
51
|
+
if (spec.startsWith("node:")) return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("package boundary", () => {
|
|
56
|
+
const sourceFiles = collectSourceFiles(SRC_DIR);
|
|
57
|
+
|
|
58
|
+
test("has source files to validate", () => {
|
|
59
|
+
expect(sourceFiles.length).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("imports only node builtins and relative modules", () => {
|
|
63
|
+
const violations: string[] = [];
|
|
64
|
+
|
|
65
|
+
for (const file of sourceFiles) {
|
|
66
|
+
const lines = readFileSync(file, "utf-8").split("\n");
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
for (const match of lines[i]!.matchAll(IMPORT_SPEC)) {
|
|
69
|
+
const spec = match[1]!;
|
|
70
|
+
if (isForbiddenSpecifier(spec)) {
|
|
71
|
+
const relative = file.replace(PACKAGE_ROOT + "/", "");
|
|
72
|
+
violations.push(`${relative}:${i + 1}: ${spec}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (violations.length > 0) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Found ${violations.length} forbidden import(s) in @vellumai/environments:\n` +
|
|
81
|
+
violations.map((v) => ` - ${v}`).join("\n") +
|
|
82
|
+
"\n\n@vellumai/environments is a pure types/constants leaf and must\n" +
|
|
83
|
+
"import only node builtins and its own relative modules.",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("package.json declares it as private with no runtime dependencies", () => {
|
|
89
|
+
const pkg = JSON.parse(
|
|
90
|
+
readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
|
|
91
|
+
);
|
|
92
|
+
expect(pkg.private).toBe(true);
|
|
93
|
+
expect(pkg.dependencies ?? {}).toEqual({});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vellumai/environments — the single source of truth for Vellum's known
|
|
3
|
+
* deployment environments (names, platform/web URLs, per-service port
|
|
4
|
+
* blocks). Consumed by the CLI, the assistant daemon, and the local-mode
|
|
5
|
+
* host library so the environment list is defined exactly once on the TS
|
|
6
|
+
* side. The Swift client mirrors the same set in `VellumEnvironment.swift`.
|
|
7
|
+
*
|
|
8
|
+
* Intentionally free of runtime dependencies: pure types and constants only.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./types.js";
|
|
11
|
+
export * from "./seeds.js";
|
|
@@ -25,15 +25,11 @@ function portBlock(base: number): PortMap {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Built-in environment definitions
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* Drift between these two sites is caught at test time by
|
|
34
|
-
* `cli/src/__tests__/env-drift.test.ts`. Fast follow: hoist the shared
|
|
35
|
-
* list into a `packages/environments` package so both sites import
|
|
36
|
-
* from one place.
|
|
28
|
+
* Built-in environment definitions and the TS-side source of truth for the
|
|
29
|
+
* set of known environment names. The Swift client mirrors this list in
|
|
30
|
+
* `clients/macos/vellum-assistant/App/VellumEnvironment.swift`; since Swift
|
|
31
|
+
* can't import TypeScript, drift between the two is caught at test time by
|
|
32
|
+
* `cli/src/__tests__/env-drift.test.ts`.
|
|
37
33
|
*
|
|
38
34
|
* Custom environments via a user config file are a future phase — see the
|
|
39
35
|
* "Coexisting environments" design doc. Until then, a call site that needs a
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"types": ["bun-types"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "@vellumai/local-mode",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@vellumai/environments": "file:../environments",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "1.3.11",
|
|
12
|
+
"typescript": "5.9.3",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"packages": {
|
|
17
|
+
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
|
18
|
+
|
|
19
|
+
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
|
20
|
+
|
|
21
|
+
"@vellumai/environments": ["@vellumai/environments@file:../environments", { "devDependencies": { "@types/bun": "1.3.11", "typescript": "5.9.3" } }],
|
|
22
|
+
|
|
23
|
+
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
|
24
|
+
|
|
25
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
26
|
+
|
|
27
|
+
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vellumai/local-mode",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "bunx tsc --noEmit",
|
|
12
|
+
"test": "bun test src/"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@vellumai/environments": "file:../environments"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/bun": "1.3.11",
|
|
19
|
+
"typescript": "5.9.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
|
|
4
|
+
import type { CliInvocation } from "../util";
|
|
5
|
+
|
|
6
|
+
class FakeChild extends EventEmitter {
|
|
7
|
+
stdout = new EventEmitter();
|
|
8
|
+
stderr = new EventEmitter();
|
|
9
|
+
kill = mock(() => true);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let lastChild: FakeChild;
|
|
13
|
+
const spawnArgs: Array<[string, string[]]> = [];
|
|
14
|
+
const spawnMock = mock((command: string, args: string[]) => {
|
|
15
|
+
spawnArgs.push([command, args]);
|
|
16
|
+
lastChild = new FakeChild();
|
|
17
|
+
return lastChild;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
mock.module("node:child_process", () => ({ spawn: spawnMock }));
|
|
21
|
+
|
|
22
|
+
let runHatch: typeof import("../hatch").runHatch;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
({ runHatch } = await import("../hatch"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
spawnArgs.length = 0;
|
|
30
|
+
spawnMock.mockClear();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const invocation: CliInvocation = { command: "bun", baseArgs: ["run", "cli"] };
|
|
34
|
+
|
|
35
|
+
describe("runHatch", () => {
|
|
36
|
+
test("spawns the CLI and parses the assistant id from stdout", async () => {
|
|
37
|
+
const pending = runHatch(invocation, "vellum");
|
|
38
|
+
lastChild.stdout.emit(
|
|
39
|
+
"data",
|
|
40
|
+
Buffer.from("Hatching local assistant: asst-42\n"),
|
|
41
|
+
);
|
|
42
|
+
lastChild.emit("close", 0);
|
|
43
|
+
|
|
44
|
+
expect(await pending).toEqual({ ok: true, assistantId: "asst-42" });
|
|
45
|
+
expect(spawnArgs[0]).toEqual(["bun", ["run", "cli", "hatch", "vellum"]]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a non-zero exit resolves to a failure carrying the CLI's output", async () => {
|
|
49
|
+
const pending = runHatch(invocation, "vellum");
|
|
50
|
+
lastChild.stderr.emit("data", Buffer.from("daemon already running"));
|
|
51
|
+
lastChild.emit("close", 1);
|
|
52
|
+
|
|
53
|
+
expect(await pending).toEqual({
|
|
54
|
+
ok: false,
|
|
55
|
+
status: 500,
|
|
56
|
+
error: "daemon already running",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a non-zero exit with no output carries a descriptive fallback error", async () => {
|
|
61
|
+
const pending = runHatch(invocation, "vellum");
|
|
62
|
+
lastChild.emit("close", 1);
|
|
63
|
+
|
|
64
|
+
const result = await pending;
|
|
65
|
+
expect(result.ok).toBe(false);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
expect(result.error).toContain("exited with code 1");
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a zero exit whose stdout has no parseable id fails instead of returning a blank id", async () => {
|
|
72
|
+
const pending = runHatch(invocation, "vellum");
|
|
73
|
+
lastChild.stdout.emit("data", Buffer.from("done, but no id line\n"));
|
|
74
|
+
lastChild.emit("close", 0);
|
|
75
|
+
|
|
76
|
+
const result = await pending;
|
|
77
|
+
expect(result.ok).toBe(false);
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
expect(result.error).toContain("no assistant id");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a spawn failure resolves to a failure rather than rejecting", async () => {
|
|
84
|
+
const pending = runHatch(invocation, "vellum");
|
|
85
|
+
lastChild.emit("error", new Error("ENOENT"));
|
|
86
|
+
|
|
87
|
+
const result = await pending;
|
|
88
|
+
expect(result.ok).toBe(false);
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
expect(result.error).toContain("ENOENT");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package boundary tests for @vellumai/local-mode.
|
|
3
|
+
*
|
|
4
|
+
* This package is the shared local-assistant host surface. It sits one layer
|
|
5
|
+
* above @vellumai/environments (its only allowed @vellumai dependency) and
|
|
6
|
+
* uses node builtins for filesystem, child-process, and network work.
|
|
7
|
+
*
|
|
8
|
+
* Enforces that the package:
|
|
9
|
+
* 1. Imports only node builtins, its own relative modules, and
|
|
10
|
+
* `@vellumai/environments` — no other `@vellumai/*` packages and no
|
|
11
|
+
* third-party runtime imports (so any bundler host can inline it).
|
|
12
|
+
* 2. Declares exactly one runtime dependency: `@vellumai/environments`.
|
|
13
|
+
* 3. Is marked `private`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from "bun:test";
|
|
17
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
18
|
+
import { join, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
const PACKAGE_ROOT = resolve(import.meta.dirname, "../..");
|
|
21
|
+
const SRC_DIR = join(PACKAGE_ROOT, "src");
|
|
22
|
+
|
|
23
|
+
const ALLOWED_PACKAGES = new Set(["@vellumai/environments"]);
|
|
24
|
+
|
|
25
|
+
function collectSourceFiles(dir: string): string[] {
|
|
26
|
+
const files: string[] = [];
|
|
27
|
+
for (const entry of readdirSync(dir)) {
|
|
28
|
+
const full = join(dir, entry);
|
|
29
|
+
const stat = statSync(full);
|
|
30
|
+
if (stat.isDirectory()) {
|
|
31
|
+
if (entry === "node_modules" || entry === "__tests__") continue;
|
|
32
|
+
files.push(...collectSourceFiles(full));
|
|
33
|
+
} else if (
|
|
34
|
+
entry.endsWith(".ts") &&
|
|
35
|
+
!entry.endsWith(".test.ts") &&
|
|
36
|
+
!entry.endsWith(".d.ts")
|
|
37
|
+
) {
|
|
38
|
+
files.push(full);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Matches the module specifier of any `import ... from "<spec>"` /
|
|
46
|
+
* `export ... from "<spec>"` / `require("<spec>")` statement.
|
|
47
|
+
*/
|
|
48
|
+
const IMPORT_SPEC = /(?:from|require\s*\(\s*)["']([^"']+)["']/g;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A specifier is forbidden when it is neither relative, a node builtin, nor
|
|
52
|
+
* one of the explicitly allowed `@vellumai/*` packages.
|
|
53
|
+
*/
|
|
54
|
+
function isForbiddenSpecifier(spec: string): boolean {
|
|
55
|
+
if (spec.startsWith(".") || spec.startsWith("/")) return false;
|
|
56
|
+
if (spec.startsWith("node:")) return false;
|
|
57
|
+
if (ALLOWED_PACKAGES.has(spec)) return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("package boundary", () => {
|
|
62
|
+
const sourceFiles = collectSourceFiles(SRC_DIR);
|
|
63
|
+
|
|
64
|
+
test("has source files to validate", () => {
|
|
65
|
+
expect(sourceFiles.length).toBeGreaterThan(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("imports only node builtins, relative modules, and @vellumai/environments", () => {
|
|
69
|
+
const violations: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (const file of sourceFiles) {
|
|
72
|
+
const lines = readFileSync(file, "utf-8").split("\n");
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
for (const match of lines[i]!.matchAll(IMPORT_SPEC)) {
|
|
75
|
+
const spec = match[1]!;
|
|
76
|
+
if (isForbiddenSpecifier(spec)) {
|
|
77
|
+
const relative = file.replace(PACKAGE_ROOT + "/", "");
|
|
78
|
+
violations.push(`${relative}:${i + 1}: ${spec}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (violations.length > 0) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Found ${violations.length} forbidden import(s) in @vellumai/local-mode:\n` +
|
|
87
|
+
violations.map((v) => ` - ${v}`).join("\n") +
|
|
88
|
+
"\n\n@vellumai/local-mode may import only node builtins, its own\n" +
|
|
89
|
+
"relative modules, and @vellumai/environments. Any other dependency\n" +
|
|
90
|
+
"would break bundler hosts that inline this source-only package.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("package.json declares it as private with only the @vellumai/environments dependency", () => {
|
|
96
|
+
const pkg = JSON.parse(
|
|
97
|
+
readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
|
|
98
|
+
);
|
|
99
|
+
expect(pkg.private).toBe(true);
|
|
100
|
+
expect(pkg.dependencies ?? {}).toEqual({
|
|
101
|
+
"@vellumai/environments": "file:../environments",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { SEEDS } from "@vellumai/environments";
|
|
5
|
+
|
|
6
|
+
const PRODUCTION_ENVIRONMENT_NAME = "production";
|
|
7
|
+
|
|
8
|
+
export interface LocalEndpointConfig {
|
|
9
|
+
lockfilePaths: string[];
|
|
10
|
+
configDir: string;
|
|
11
|
+
webUrl: string;
|
|
12
|
+
platformUrl: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve config from environment variables (Vite plugin context, where
|
|
17
|
+
* `env` comes from `loadEnv` and process.env).
|
|
18
|
+
*/
|
|
19
|
+
export function resolveLocalConfigFromEnv(
|
|
20
|
+
env: Record<string, string>,
|
|
21
|
+
): LocalEndpointConfig {
|
|
22
|
+
const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
|
|
23
|
+
const seed = SEEDS[vellumEnv] ?? SEEDS[PRODUCTION_ENVIRONMENT_NAME]!;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
lockfilePaths: resolveLockfilePaths(env),
|
|
27
|
+
configDir: resolveConfigDir(env),
|
|
28
|
+
webUrl: env.VELLUM_WEB_URL || seed.webUrl,
|
|
29
|
+
platformUrl: env.VELLUM_PLATFORM_URL || seed.platformUrl,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveLockfilePaths(env: Record<string, string>): string[] {
|
|
34
|
+
const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
|
|
35
|
+
const lockfileDir = env.VELLUM_LOCKFILE_DIR;
|
|
36
|
+
|
|
37
|
+
if (vellumEnv === PRODUCTION_ENVIRONMENT_NAME) {
|
|
38
|
+
const dir = lockfileDir ?? os.homedir();
|
|
39
|
+
return [
|
|
40
|
+
path.join(dir, ".vellum.lock.json"),
|
|
41
|
+
path.join(dir, ".vellum.lockfile.json"),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const xdgConfigHome =
|
|
46
|
+
env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
47
|
+
const dir = lockfileDir ?? path.join(xdgConfigHome, `vellum-${vellumEnv}`);
|
|
48
|
+
return [path.join(dir, "lockfile.json")];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveConfigDir(env: Record<string, string>): string {
|
|
52
|
+
const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
|
|
53
|
+
const xdgConfigHome =
|
|
54
|
+
env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
55
|
+
if (vellumEnv === PRODUCTION_ENVIRONMENT_NAME) {
|
|
56
|
+
return path.join(xdgConfigHome, "vellum");
|
|
57
|
+
}
|
|
58
|
+
return path.join(xdgConfigHome, `vellum-${vellumEnv}`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
const GATEWAY_PATTERN = /^(?:\/assistant)?\/__gateway\/(\d+)(\/.*)?$/;
|
|
4
|
+
|
|
5
|
+
export interface GatewayTarget {
|
|
6
|
+
port: number;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type GatewayParseResult =
|
|
11
|
+
| { match: true; valid: true; target: GatewayTarget }
|
|
12
|
+
| { match: true; valid: false }
|
|
13
|
+
| { match: false };
|
|
14
|
+
|
|
15
|
+
export function parseGatewayUrl(pathname: string): GatewayParseResult {
|
|
16
|
+
const match = pathname.match(GATEWAY_PATTERN);
|
|
17
|
+
if (!match) return { match: false };
|
|
18
|
+
|
|
19
|
+
const port = parseInt(match[1]!, 10);
|
|
20
|
+
if (port < 1024 || port > 65535) return { match: true, valid: false };
|
|
21
|
+
|
|
22
|
+
return { match: true, valid: true, target: { port, path: match[2] || "/" } };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addPortFromUrl(url: unknown, ports: Set<number>): void {
|
|
26
|
+
if (typeof url !== "string") return;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
if (parsed.hostname !== "127.0.0.1" && parsed.hostname !== "localhost") return;
|
|
30
|
+
const port = Number(parsed.port);
|
|
31
|
+
if (Number.isInteger(port) && port >= 1024 && port <= 65535) {
|
|
32
|
+
ports.add(port);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// malformed URL — skip
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readAllowedGatewayPorts(lockfilePaths: string[]): Set<number> {
|
|
40
|
+
const ports = new Set<number>();
|
|
41
|
+
for (const candidate of lockfilePaths) {
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(candidate, "utf-8");
|
|
44
|
+
const data = JSON.parse(raw) as {
|
|
45
|
+
assistants?: Array<{
|
|
46
|
+
gatewayUrl?: unknown;
|
|
47
|
+
localUrl?: unknown;
|
|
48
|
+
resources?: { gatewayPort?: unknown };
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
const assistants = Array.isArray(data.assistants) ? data.assistants : [];
|
|
52
|
+
for (const assistant of assistants) {
|
|
53
|
+
if (!assistant) continue;
|
|
54
|
+
addPortFromUrl(assistant.gatewayUrl, ports);
|
|
55
|
+
addPortFromUrl(assistant.localUrl, ports);
|
|
56
|
+
const gp = assistant.resources?.gatewayPort;
|
|
57
|
+
if (typeof gp === "number" && Number.isInteger(gp) && gp >= 1024 && gp <= 65535) {
|
|
58
|
+
ports.add(gp);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (ports.size > 0) return ports;
|
|
62
|
+
} catch (err: unknown) {
|
|
63
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") return new Set<number>();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return ports;
|
|
67
|
+
}
|