@vellumai/cli 0.8.6 → 0.8.7-dev.202606052118.34cd356
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/bun.lock +8 -0
- package/knip.json +5 -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 +22 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +108 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +109 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +92 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +48 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +133 -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/src/wake.ts +78 -0
- package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
- package/package.json +12 -1
- package/src/__tests__/assistant-client-refresh.test.ts +182 -0
- package/src/__tests__/clean.test.ts +179 -0
- package/src/__tests__/client-token.test.ts +87 -0
- package/src/__tests__/client-tui-refresh.test.ts +170 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
- package/src/__tests__/connect-import.test.ts +317 -0
- package/src/__tests__/devices.test.ts +272 -0
- package/src/__tests__/env-drift.test.ts +32 -44
- package/src/__tests__/flags.test.ts +248 -0
- package/src/__tests__/guardian-token.test.ts +126 -2
- package/src/__tests__/multi-local.test.ts +1 -1
- package/src/__tests__/orphan-detection.test.ts +8 -6
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/segments-to-plain-text.test.ts +37 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/commands/client.ts +511 -11
- package/src/commands/connect/import.ts +217 -0
- package/src/commands/connect.ts +31 -0
- package/src/commands/devices.ts +247 -0
- package/src/commands/env.ts +1 -1
- package/src/commands/flags.ts +89 -17
- package/src/commands/pair.ts +222 -0
- package/src/commands/ps.ts +16 -0
- package/src/commands/retire.ts +20 -47
- package/src/commands/sleep.ts +7 -0
- package/src/commands/tunnel.ts +46 -2
- package/src/commands/unpair.ts +118 -0
- package/src/commands/wake.ts +7 -0
- package/src/components/DefaultMainScreen.tsx +100 -14
- package/src/index.ts +16 -0
- package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +15 -3
- package/src/lib/cloudflare-tunnel.ts +276 -0
- package/src/lib/confirm-action.ts +57 -0
- package/src/lib/docker.ts +25 -1
- 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 +11 -35
- package/src/lib/guardian-token.ts +132 -9
- package/src/lib/hatch-local.ts +73 -33
- package/src/lib/lifecycle-reporter.ts +31 -0
- package/src/lib/local.ts +20 -6
- package/src/lib/retire-local.ts +28 -14
- package/src/lib/segments-to-plain-text.ts +35 -0
- /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
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
|
@@ -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,22 @@
|
|
|
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
|
+
"./contract": "./src/lockfile-contract.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "bunx tsc --noEmit",
|
|
13
|
+
"test": "bun test src/"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@vellumai/environments": "file:../environments"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/bun": "1.3.11",
|
|
20
|
+
"typescript": "5.9.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
defaultEnvironmentFilePath,
|
|
8
|
+
readDefaultEnvironment,
|
|
9
|
+
resolveEnvironmentName,
|
|
10
|
+
} from "../environment";
|
|
11
|
+
import { resolveLockfilePaths, resolveConfigDir } from "../config";
|
|
12
|
+
|
|
13
|
+
let configHome: string;
|
|
14
|
+
|
|
15
|
+
/** Write the persisted default-environment file under the temp config home. */
|
|
16
|
+
function persistDefault(name: string): void {
|
|
17
|
+
const file = path.join(configHome, "vellum", "environment");
|
|
18
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
19
|
+
writeFileSync(file, name + "\n", "utf-8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
configHome = mkdtempSync(path.join(os.tmpdir(), "vellum-env-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(configHome, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("defaultEnvironmentFilePath", () => {
|
|
31
|
+
test("honors XDG_CONFIG_HOME", () => {
|
|
32
|
+
expect(defaultEnvironmentFilePath({ XDG_CONFIG_HOME: configHome })).toBe(
|
|
33
|
+
path.join(configHome, "vellum", "environment"),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("falls back to ~/.config", () => {
|
|
38
|
+
expect(defaultEnvironmentFilePath({})).toBe(
|
|
39
|
+
path.join(os.homedir(), ".config", "vellum", "environment"),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("readDefaultEnvironment", () => {
|
|
45
|
+
test("returns undefined when no file exists", () => {
|
|
46
|
+
expect(
|
|
47
|
+
readDefaultEnvironment({ XDG_CONFIG_HOME: configHome }),
|
|
48
|
+
).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("returns the trimmed persisted name", () => {
|
|
52
|
+
persistDefault("dev");
|
|
53
|
+
expect(readDefaultEnvironment({ XDG_CONFIG_HOME: configHome })).toBe("dev");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("treats an empty file as no default", () => {
|
|
57
|
+
const file = path.join(configHome, "vellum", "environment");
|
|
58
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
59
|
+
writeFileSync(file, " \n", "utf-8");
|
|
60
|
+
expect(
|
|
61
|
+
readDefaultEnvironment({ XDG_CONFIG_HOME: configHome }),
|
|
62
|
+
).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("resolveEnvironmentName", () => {
|
|
67
|
+
test("prefers VELLUM_ENVIRONMENT over the persisted default", () => {
|
|
68
|
+
persistDefault("dev");
|
|
69
|
+
expect(
|
|
70
|
+
resolveEnvironmentName({
|
|
71
|
+
XDG_CONFIG_HOME: configHome,
|
|
72
|
+
VELLUM_ENVIRONMENT: "staging",
|
|
73
|
+
}),
|
|
74
|
+
).toBe("staging");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("falls back to the persisted default when the env var is unset", () => {
|
|
78
|
+
persistDefault("dev");
|
|
79
|
+
expect(resolveEnvironmentName({ XDG_CONFIG_HOME: configHome })).toBe("dev");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("falls back to production when neither is set", () => {
|
|
83
|
+
expect(resolveEnvironmentName({ XDG_CONFIG_HOME: configHome })).toBe(
|
|
84
|
+
"production",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("path resolvers honor the persisted default", () => {
|
|
90
|
+
test("resolveLockfilePaths points at the persisted environment", () => {
|
|
91
|
+
persistDefault("dev");
|
|
92
|
+
const env = { XDG_CONFIG_HOME: configHome };
|
|
93
|
+
expect(resolveLockfilePaths(env)).toEqual([
|
|
94
|
+
path.join(configHome, "vellum-dev", "lockfile.json"),
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("resolveConfigDir points at the persisted environment", () => {
|
|
99
|
+
persistDefault("dev");
|
|
100
|
+
expect(resolveConfigDir({ XDG_CONFIG_HOME: configHome })).toBe(
|
|
101
|
+
path.join(configHome, "vellum-dev"),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("VELLUM_ENVIRONMENT still wins for path resolution", () => {
|
|
106
|
+
persistDefault("dev");
|
|
107
|
+
const env = {
|
|
108
|
+
XDG_CONFIG_HOME: configHome,
|
|
109
|
+
VELLUM_ENVIRONMENT: "production",
|
|
110
|
+
};
|
|
111
|
+
expect(resolveLockfilePaths(env)).toEqual([
|
|
112
|
+
path.join(os.homedir(), ".vellum.lock.json"),
|
|
113
|
+
path.join(os.homedir(), ".vellum.lockfile.json"),
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { resolveGatewayProxyTarget } from "../gateway-proxy";
|
|
4
|
+
|
|
5
|
+
const allow =
|
|
6
|
+
(...ports: number[]) =>
|
|
7
|
+
() =>
|
|
8
|
+
new Set<number>(ports);
|
|
9
|
+
|
|
10
|
+
describe("resolveGatewayProxyTarget", () => {
|
|
11
|
+
test("passes non-gateway pathnames through untouched", () => {
|
|
12
|
+
expect(resolveGatewayProxyTarget("/index.html", allow(8080))).toEqual({
|
|
13
|
+
kind: "pass",
|
|
14
|
+
});
|
|
15
|
+
expect(resolveGatewayProxyTarget("/assistant/assets/app.js", allow())).toEqual({
|
|
16
|
+
kind: "pass",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("forwards an allowlisted port to its loopback target", () => {
|
|
21
|
+
expect(
|
|
22
|
+
resolveGatewayProxyTarget("/__gateway/8080/v1/assistants", allow(8080)),
|
|
23
|
+
).toEqual({
|
|
24
|
+
kind: "forward",
|
|
25
|
+
target: { port: 8080, path: "/v1/assistants" },
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("accepts the renderer's `/assistant` mount prefix", () => {
|
|
30
|
+
expect(
|
|
31
|
+
resolveGatewayProxyTarget("/assistant/__gateway/8080/auth/token", allow(8080)),
|
|
32
|
+
).toEqual({
|
|
33
|
+
kind: "forward",
|
|
34
|
+
target: { port: 8080, path: "/auth/token" },
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("defaults a portless tail to the gateway root", () => {
|
|
39
|
+
expect(resolveGatewayProxyTarget("/__gateway/8080", allow(8080))).toEqual({
|
|
40
|
+
kind: "forward",
|
|
41
|
+
target: { port: 8080, path: "/" },
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("rejects ports outside the 1024–65535 range as invalid", () => {
|
|
46
|
+
expect(resolveGatewayProxyTarget("/__gateway/80/v1", allow(80))).toEqual({
|
|
47
|
+
kind: "invalid-port",
|
|
48
|
+
});
|
|
49
|
+
expect(resolveGatewayProxyTarget("/__gateway/70000/v1", allow(70000))).toEqual({
|
|
50
|
+
kind: "invalid-port",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("forbids a well-formed port that isn't registered in the lockfile", () => {
|
|
55
|
+
expect(
|
|
56
|
+
resolveGatewayProxyTarget("/__gateway/9999/v1", allow(8080)),
|
|
57
|
+
).toEqual({ kind: "forbidden-port", port: 9999 });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("forbids every gateway port when the allowlist is empty", () => {
|
|
61
|
+
expect(resolveGatewayProxyTarget("/__gateway/8080/v1", allow())).toEqual({
|
|
62
|
+
kind: "forbidden-port",
|
|
63
|
+
port: 8080,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("never reads the allowlist for non-gateway or invalid-port paths", () => {
|
|
68
|
+
let reads = 0;
|
|
69
|
+
const counting = () => {
|
|
70
|
+
reads += 1;
|
|
71
|
+
return new Set<number>([8080]);
|
|
72
|
+
};
|
|
73
|
+
resolveGatewayProxyTarget("/index.html", counting);
|
|
74
|
+
resolveGatewayProxyTarget("/__gateway/80/v1", counting);
|
|
75
|
+
expect(reads).toBe(0);
|
|
76
|
+
resolveGatewayProxyTarget("/__gateway/8080/v1", counting);
|
|
77
|
+
expect(reads).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
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("parses the assistant id from a Docker hatch banner", async () => {
|
|
49
|
+
const pending = runHatch(invocation, "vellum", { remote: "docker" });
|
|
50
|
+
lastChild.stdout.emit(
|
|
51
|
+
"data",
|
|
52
|
+
Buffer.from("🥚 Hatching Docker assistant: asst-docker\n"),
|
|
53
|
+
);
|
|
54
|
+
lastChild.emit("close", 0);
|
|
55
|
+
|
|
56
|
+
expect(await pending).toEqual({ ok: true, assistantId: "asst-docker" });
|
|
57
|
+
expect(spawnArgs[0]).toEqual([
|
|
58
|
+
"bun",
|
|
59
|
+
["run", "cli", "hatch", "vellum", "--remote", "docker"],
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a non-zero exit resolves to a failure carrying the CLI's output", async () => {
|
|
64
|
+
const pending = runHatch(invocation, "vellum");
|
|
65
|
+
lastChild.stderr.emit("data", Buffer.from("daemon already running"));
|
|
66
|
+
lastChild.emit("close", 1);
|
|
67
|
+
|
|
68
|
+
expect(await pending).toEqual({
|
|
69
|
+
ok: false,
|
|
70
|
+
status: 500,
|
|
71
|
+
error: "daemon already running",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a non-zero exit with no output carries a descriptive fallback error", async () => {
|
|
76
|
+
const pending = runHatch(invocation, "vellum");
|
|
77
|
+
lastChild.emit("close", 1);
|
|
78
|
+
|
|
79
|
+
const result = await pending;
|
|
80
|
+
expect(result.ok).toBe(false);
|
|
81
|
+
if (!result.ok) {
|
|
82
|
+
expect(result.error).toContain("exited with code 1");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a zero exit whose stdout has no parseable id fails instead of returning a blank id", async () => {
|
|
87
|
+
const pending = runHatch(invocation, "vellum");
|
|
88
|
+
lastChild.stdout.emit("data", Buffer.from("done, but no id line\n"));
|
|
89
|
+
lastChild.emit("close", 0);
|
|
90
|
+
|
|
91
|
+
const result = await pending;
|
|
92
|
+
expect(result.ok).toBe(false);
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
expect(result.error).toContain("no assistant id");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a spawn failure resolves to a failure rather than rejecting", async () => {
|
|
99
|
+
const pending = runHatch(invocation, "vellum");
|
|
100
|
+
lastChild.emit("error", new Error("ENOENT"));
|
|
101
|
+
|
|
102
|
+
const result = await pending;
|
|
103
|
+
expect(result.ok).toBe(false);
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
expect(result.error).toContain("ENOENT");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|