@vellumai/cli 0.11.2 → 0.11.3-staging.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 +1 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
- package/node_modules/@vellumai/environments/src/index.ts +17 -6
- package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
- package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/ipc-framing.ts +295 -0
- package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +444 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +236 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/index.ts +17 -6
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/LICENSE +20 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/README.md +38 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/bin/nanoid.js +55 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.browser.js +29 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.d.ts +106 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.js +47 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/nanoid.js +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.d.ts +48 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.js +21 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/package.json +46 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/url-alphabet/index.js +2 -0
- package/node_modules/@vellumai/local-mode/package.json +1 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +63 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +503 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token.test.ts +203 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/loopback-auth.test.ts +46 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +6 -4
- package/node_modules/@vellumai/local-mode/src/__tests__/pair.test.ts +621 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +5 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/unpair.test.ts +232 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +126 -20
- package/node_modules/@vellumai/local-mode/src/environment.ts +36 -15
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +313 -25
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +222 -13
- package/node_modules/@vellumai/local-mode/src/index.ts +35 -3
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +18 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +72 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +130 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +134 -45
- package/node_modules/@vellumai/local-mode/src/pair.ts +354 -0
- package/node_modules/@vellumai/local-mode/src/paths.ts +49 -0
- package/node_modules/@vellumai/local-mode/src/status.ts +10 -8
- package/node_modules/@vellumai/local-mode/src/unpair.ts +101 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +34 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/package.json +3 -1
- package/src/__tests__/client-token.test.ts +26 -1
- package/src/__tests__/guardian-token.test.ts +4 -0
- package/src/__tests__/nginx-ingress-command.test.ts +176 -16
- package/src/__tests__/nginx-ingress.test.ts +1821 -101
- package/src/__tests__/pair.test.ts +104 -2
- package/src/__tests__/sleep.test.ts +7 -3
- package/src/__tests__/tailscale-tunnel.test.ts +0 -1
- package/src/__tests__/tunnel.test.ts +1099 -55
- package/src/__tests__/wake.test.ts +166 -99
- package/src/commands/client.ts +182 -27
- package/src/commands/connect/import.ts +35 -156
- package/src/commands/nginx-ingress.ts +87 -110
- package/src/commands/pair.ts +84 -21
- package/src/commands/tunnel.ts +196 -85
- package/src/commands/upgrade.ts +3 -2
- package/src/commands/wake.ts +5 -163
- package/src/index.ts +1 -1
- package/src/lib/__tests__/local-ces.test.ts +10 -1
- package/src/lib/cloudflare-tunnel.ts +3 -16
- package/src/lib/environments/__tests__/paths.test.ts +20 -1
- package/src/lib/environments/paths.ts +29 -41
- package/src/lib/environments/resolve.ts +7 -5
- package/src/lib/guardian-token.ts +22 -62
- package/src/lib/ingress-config.ts +25 -0
- package/src/lib/local.ts +77 -20
- package/src/lib/nginx-ingress.ts +523 -76
- package/src/lib/ngrok.ts +154 -61
- package/src/lib/orphan-detection.test.ts +167 -0
- package/src/lib/orphan-detection.ts +40 -0
- package/src/lib/pair.test.ts +87 -0
- package/src/lib/pair.ts +55 -0
- package/src/lib/platform-client.ts +8 -5
- package/src/lib/tailscale-tunnel.ts +7 -18
- package/src/lib/tunnel-edge.ts +114 -0
- package/src/lib/xdg-log.ts +2 -2
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { guardianTokenPath } from "../config";
|
|
7
|
+
import { unpairAssistant } from "../unpair";
|
|
8
|
+
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
let lockfilePath: string;
|
|
11
|
+
let configDir: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vellum-unpair-"));
|
|
15
|
+
lockfilePath = path.join(tmpDir, "lockfile.json");
|
|
16
|
+
configDir = path.join(tmpDir, "config");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const writeLockfile = (data: Record<string, unknown>): void => {
|
|
24
|
+
fs.writeFileSync(lockfilePath, JSON.stringify(data));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readLockfileFromDisk = (): Record<string, unknown> =>
|
|
28
|
+
JSON.parse(fs.readFileSync(lockfilePath, "utf-8")) as Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
const seedGuardianToken = (assistantId: string): string => {
|
|
31
|
+
const tokenPath = guardianTokenPath(configDir, assistantId);
|
|
32
|
+
fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(tokenPath, JSON.stringify({ accessToken: "tok" }));
|
|
34
|
+
return tokenPath;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("unpairAssistant", () => {
|
|
38
|
+
test("removes the paired entry, deletes its guardian token, and reassigns the active pointer", () => {
|
|
39
|
+
writeLockfile({
|
|
40
|
+
assistants: [
|
|
41
|
+
{ assistantId: "paired-1", cloud: "paired", runtimeUrl: "https://h" },
|
|
42
|
+
{ assistantId: "local-1", cloud: "local", futureField: "keep-me" },
|
|
43
|
+
],
|
|
44
|
+
activeAssistant: "paired-1",
|
|
45
|
+
});
|
|
46
|
+
const tokenPath = seedGuardianToken("paired-1");
|
|
47
|
+
|
|
48
|
+
const result = unpairAssistant([lockfilePath], configDir, "paired-1");
|
|
49
|
+
|
|
50
|
+
expect(result.ok).toBe(true);
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Active falls back to the first remaining entry, matching the CLI's
|
|
55
|
+
// removeAssistantEntry semantics.
|
|
56
|
+
expect(result.lockfile.activeAssistant).toBe("local-1");
|
|
57
|
+
expect(result.lockfile.assistants.map((a) => a.assistantId)).toEqual([
|
|
58
|
+
"local-1",
|
|
59
|
+
]);
|
|
60
|
+
expect(fs.existsSync(tokenPath)).toBe(false);
|
|
61
|
+
|
|
62
|
+
// Unknown fields on the remaining entries survive the rewrite.
|
|
63
|
+
const onDisk = readLockfileFromDisk();
|
|
64
|
+
expect(onDisk.assistants).toEqual([
|
|
65
|
+
{ assistantId: "local-1", cloud: "local", futureField: "keep-me" },
|
|
66
|
+
]);
|
|
67
|
+
expect(onDisk.activeAssistant).toBe("local-1");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("removing the sole (active) entry leaves no active pointer", () => {
|
|
71
|
+
writeLockfile({
|
|
72
|
+
assistants: [{ assistantId: "paired-1", cloud: "paired" }],
|
|
73
|
+
activeAssistant: "paired-1",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = unpairAssistant([lockfilePath], configDir, "paired-1");
|
|
77
|
+
|
|
78
|
+
expect(result.ok).toBe(true);
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
expect(result.lockfile.activeAssistant).toBeNull();
|
|
83
|
+
expect(result.lockfile.assistants).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("reassigns the active pointer past tolerated malformed entries", () => {
|
|
87
|
+
writeLockfile({
|
|
88
|
+
assistants: [
|
|
89
|
+
{ assistantId: "paired-1", cloud: "paired" },
|
|
90
|
+
{ notAnAssistant: true },
|
|
91
|
+
{ assistantId: "local-1", cloud: "local" },
|
|
92
|
+
],
|
|
93
|
+
activeAssistant: "paired-1",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = unpairAssistant([lockfilePath], configDir, "paired-1");
|
|
97
|
+
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
expect(result.lockfile.activeAssistant).toBe("local-1");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a failed token delete aborts before touching the lockfile", () => {
|
|
106
|
+
writeLockfile({
|
|
107
|
+
assistants: [{ assistantId: "paired-1", cloud: "paired" }],
|
|
108
|
+
activeAssistant: "paired-1",
|
|
109
|
+
});
|
|
110
|
+
// A non-empty directory at the token path makes rmSync (non-recursive)
|
|
111
|
+
// throw, standing in for EPERM-style failures.
|
|
112
|
+
const tokenPath = guardianTokenPath(configDir, "paired-1");
|
|
113
|
+
fs.mkdirSync(path.join(tokenPath, "oops"), { recursive: true });
|
|
114
|
+
|
|
115
|
+
const result = unpairAssistant([lockfilePath], configDir, "paired-1");
|
|
116
|
+
|
|
117
|
+
expect(result.ok).toBe(false);
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
expect(result.status).toBe(500);
|
|
122
|
+
expect(result.error).toContain("guardian token");
|
|
123
|
+
// The entry is untouched, so the unpair can be retried.
|
|
124
|
+
expect(readLockfileFromDisk().assistants).toEqual([
|
|
125
|
+
{ assistantId: "paired-1", cloud: "paired" },
|
|
126
|
+
]);
|
|
127
|
+
expect(readLockfileFromDisk().activeAssistant).toBe("paired-1");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("restores the guardian token when the lockfile write fails", () => {
|
|
131
|
+
writeLockfile({
|
|
132
|
+
assistants: [{ assistantId: "paired-1", cloud: "paired" }],
|
|
133
|
+
activeAssistant: null,
|
|
134
|
+
});
|
|
135
|
+
const tokenPath = seedGuardianToken("paired-1");
|
|
136
|
+
// A read-only lockfile directory makes the tmp-file write fail after the
|
|
137
|
+
// token has already been deleted.
|
|
138
|
+
fs.chmodSync(tmpDir, 0o500);
|
|
139
|
+
try {
|
|
140
|
+
const result = unpairAssistant([lockfilePath], configDir, "paired-1");
|
|
141
|
+
|
|
142
|
+
expect(result.ok).toBe(false);
|
|
143
|
+
expect(fs.existsSync(tokenPath)).toBe(true);
|
|
144
|
+
expect(fs.readFileSync(tokenPath, "utf-8")).toBe(
|
|
145
|
+
JSON.stringify({ accessToken: "tok" }),
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
fs.chmodSync(tmpDir, 0o700);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("leaves the active pointer alone when it names another assistant", () => {
|
|
153
|
+
writeLockfile({
|
|
154
|
+
assistants: [
|
|
155
|
+
{ assistantId: "paired-1", cloud: "paired" },
|
|
156
|
+
{ assistantId: "local-1", cloud: "local" },
|
|
157
|
+
],
|
|
158
|
+
activeAssistant: "local-1",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = unpairAssistant([lockfilePath], configDir, "paired-1");
|
|
162
|
+
|
|
163
|
+
expect(result.ok).toBe(true);
|
|
164
|
+
if (!result.ok) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
expect(result.lockfile.activeAssistant).toBe("local-1");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("succeeds when the guardian token file is already gone", () => {
|
|
171
|
+
writeLockfile({
|
|
172
|
+
assistants: [{ assistantId: "paired-1", cloud: "paired" }],
|
|
173
|
+
activeAssistant: null,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = unpairAssistant([lockfilePath], configDir, "paired-1");
|
|
177
|
+
|
|
178
|
+
expect(result.ok).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("refuses an unknown assistant without touching disk", () => {
|
|
182
|
+
writeLockfile({
|
|
183
|
+
assistants: [{ assistantId: "paired-1", cloud: "paired" }],
|
|
184
|
+
activeAssistant: "paired-1",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = unpairAssistant([lockfilePath], configDir, "nope");
|
|
188
|
+
|
|
189
|
+
expect(result).toEqual({
|
|
190
|
+
ok: false,
|
|
191
|
+
status: 404,
|
|
192
|
+
error: "No such assistant",
|
|
193
|
+
});
|
|
194
|
+
expect(readLockfileFromDisk().activeAssistant).toBe("paired-1");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("refuses a non-paired assistant and leaves its entry and token alone", () => {
|
|
198
|
+
writeLockfile({
|
|
199
|
+
assistants: [{ assistantId: "local-1", cloud: "local" }],
|
|
200
|
+
activeAssistant: "local-1",
|
|
201
|
+
});
|
|
202
|
+
const tokenPath = seedGuardianToken("local-1");
|
|
203
|
+
|
|
204
|
+
const result = unpairAssistant([lockfilePath], configDir, "local-1");
|
|
205
|
+
|
|
206
|
+
expect(result.ok).toBe(false);
|
|
207
|
+
if (result.ok) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
expect(result.status).toBe(400);
|
|
211
|
+
expect(result.error).toContain("paired");
|
|
212
|
+
expect(fs.existsSync(tokenPath)).toBe(true);
|
|
213
|
+
expect(readLockfileFromDisk().assistants).toEqual([
|
|
214
|
+
{ assistantId: "local-1", cloud: "local" },
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("refuses a cloudless entry (defaults to local) rather than treating it as paired", () => {
|
|
219
|
+
writeLockfile({
|
|
220
|
+
assistants: [{ assistantId: "bare-1" }],
|
|
221
|
+
activeAssistant: null,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = unpairAssistant([lockfilePath], configDir, "bare-1");
|
|
225
|
+
|
|
226
|
+
expect(result.ok).toBe(false);
|
|
227
|
+
if (result.ok) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
expect(result.status).toBe(400);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
|
|
4
3
|
import { SEEDS } from "@vellumai/environments";
|
|
5
4
|
|
|
6
5
|
import { resolveEnvironmentName } from "./environment";
|
|
6
|
+
import {
|
|
7
|
+
assertSafePathSegment,
|
|
8
|
+
joinLocalPath,
|
|
9
|
+
resolveConfigHomes,
|
|
10
|
+
resolveDataHome,
|
|
11
|
+
type LocalPathOptions,
|
|
12
|
+
} from "./paths";
|
|
7
13
|
|
|
8
14
|
const PRODUCTION_ENVIRONMENT_NAME = "production";
|
|
9
15
|
|
|
@@ -21,13 +27,14 @@ export interface LocalEndpointConfig {
|
|
|
21
27
|
*/
|
|
22
28
|
export function resolveLocalConfigFromEnv(
|
|
23
29
|
env: Record<string, string | undefined>,
|
|
30
|
+
options: LocalPathOptions = {},
|
|
24
31
|
): LocalEndpointConfig {
|
|
25
|
-
const vellumEnv = resolveEnvironmentName(env);
|
|
32
|
+
const vellumEnv = resolveEnvironmentName(env, options);
|
|
26
33
|
const seed = SEEDS[vellumEnv] ?? SEEDS[PRODUCTION_ENVIRONMENT_NAME]!;
|
|
27
34
|
|
|
28
35
|
return {
|
|
29
|
-
lockfilePaths: resolveLockfilePaths(env),
|
|
30
|
-
configDir: resolveConfigDir(env),
|
|
36
|
+
lockfilePaths: resolveLockfilePaths(env, options),
|
|
37
|
+
configDir: resolveConfigDir(env, options),
|
|
31
38
|
webUrl: env.VELLUM_WEB_URL || seed.webUrl,
|
|
32
39
|
platformUrl: env.VELLUM_PLATFORM_URL || seed.platformUrl,
|
|
33
40
|
};
|
|
@@ -35,34 +42,125 @@ export function resolveLocalConfigFromEnv(
|
|
|
35
42
|
|
|
36
43
|
export function resolveLockfilePaths(
|
|
37
44
|
env: Record<string, string | undefined>,
|
|
45
|
+
options: LocalPathOptions = {},
|
|
38
46
|
): string[] {
|
|
39
|
-
const vellumEnv = resolveEnvironmentName(env);
|
|
40
|
-
const lockfileDir =
|
|
47
|
+
const vellumEnv = resolveEnvironmentName(env, options);
|
|
48
|
+
const lockfileDir = Object.hasOwn(options, "lockfileDirOverride")
|
|
49
|
+
? options.lockfileDirOverride
|
|
50
|
+
: env.VELLUM_LOCKFILE_DIR?.trim();
|
|
51
|
+
|
|
52
|
+
if ((options.platform ?? process.platform) === "win32") {
|
|
53
|
+
const canonicalDir = lockfileDir ?? resolveConfigDir(env, options);
|
|
54
|
+
const canonical = joinLocalPath(options, canonicalDir, "lockfile.json");
|
|
55
|
+
const legacyDir = lockfileDir ?? options.homeDir ?? os.homedir();
|
|
56
|
+
const legacy =
|
|
57
|
+
vellumEnv === PRODUCTION_ENVIRONMENT_NAME
|
|
58
|
+
? [".vellum.lock.json", ".vellum.lockfile.json"].map((name) =>
|
|
59
|
+
joinLocalPath(options, legacyDir, name),
|
|
60
|
+
)
|
|
61
|
+
: [
|
|
62
|
+
joinLocalPath(
|
|
63
|
+
options,
|
|
64
|
+
lockfileDir ?? resolveConfigDirPaths(env, options).at(-1)!,
|
|
65
|
+
"lockfile.json",
|
|
66
|
+
),
|
|
67
|
+
];
|
|
68
|
+
return [
|
|
69
|
+
canonical,
|
|
70
|
+
...legacy.filter((candidate) => candidate !== canonical),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
41
73
|
|
|
42
74
|
if (vellumEnv === PRODUCTION_ENVIRONMENT_NAME) {
|
|
43
|
-
const dir = lockfileDir ?? os.homedir();
|
|
75
|
+
const dir = lockfileDir ?? options.homeDir ?? os.homedir();
|
|
44
76
|
return [
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
joinLocalPath(options, dir, ".vellum.lock.json"),
|
|
78
|
+
joinLocalPath(options, dir, ".vellum.lockfile.json"),
|
|
47
79
|
];
|
|
48
80
|
}
|
|
49
81
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const dir = lockfileDir ?? path.join(xdgConfigHome, `vellum-${vellumEnv}`);
|
|
53
|
-
return [path.join(dir, "lockfile.json")];
|
|
82
|
+
const dir = lockfileDir ?? resolveConfigDir(env, options);
|
|
83
|
+
return [joinLocalPath(options, dir, "lockfile.json")];
|
|
54
84
|
}
|
|
55
85
|
|
|
56
86
|
export function resolveConfigDir(
|
|
57
87
|
env: Record<string, string | undefined>,
|
|
88
|
+
options: LocalPathOptions = {},
|
|
58
89
|
): string {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
return resolveConfigDirPaths(env, options)[0]!;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveConfigDirPaths(
|
|
94
|
+
env: Record<string, string | undefined>,
|
|
95
|
+
options: LocalPathOptions = {},
|
|
96
|
+
): string[] {
|
|
97
|
+
if (options.configDirOverride) {
|
|
98
|
+
return [options.configDirOverride];
|
|
64
99
|
}
|
|
65
|
-
|
|
100
|
+
const vellumEnv = resolveEnvironmentName(env, options);
|
|
101
|
+
return resolveConfigHomes(env, options).map((home) =>
|
|
102
|
+
joinLocalPath(options, home, environmentDirectoryName(vellumEnv, options)),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function environmentDirectoryName(
|
|
107
|
+
vellumEnv: string,
|
|
108
|
+
options: LocalPathOptions,
|
|
109
|
+
): string {
|
|
110
|
+
assertSafePathSegment(vellumEnv, "environment name", options);
|
|
111
|
+
return vellumEnv === PRODUCTION_ENVIRONMENT_NAME
|
|
112
|
+
? "vellum"
|
|
113
|
+
: `vellum-${vellumEnv}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveDataDir(
|
|
117
|
+
env: Record<string, string | undefined>,
|
|
118
|
+
options: LocalPathOptions,
|
|
119
|
+
): string {
|
|
120
|
+
const vellumEnv = resolveEnvironmentName(env, options);
|
|
121
|
+
return joinLocalPath(
|
|
122
|
+
options,
|
|
123
|
+
resolveDataHome(env, options),
|
|
124
|
+
environmentDirectoryName(vellumEnv, options),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function resolveRuntimeDir(
|
|
129
|
+
env: Record<string, string | undefined>,
|
|
130
|
+
options: LocalPathOptions = {},
|
|
131
|
+
): string {
|
|
132
|
+
return resolveDataDir(env, options);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveLogDir(
|
|
136
|
+
env: Record<string, string | undefined>,
|
|
137
|
+
options: LocalPathOptions = {},
|
|
138
|
+
): string {
|
|
139
|
+
const root =
|
|
140
|
+
(options.platform ?? process.platform) === "win32"
|
|
141
|
+
? resolveDataDir(env, options)
|
|
142
|
+
: resolveConfigDir(env, options);
|
|
143
|
+
return joinLocalPath(options, root, "logs");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function resolveAssistantsDir(
|
|
147
|
+
env: Record<string, string | undefined>,
|
|
148
|
+
options: LocalPathOptions = {},
|
|
149
|
+
): string {
|
|
150
|
+
return joinLocalPath(options, resolveDataDir(env, options), "assistants");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolveInstanceDir(
|
|
154
|
+
env: Record<string, string | undefined>,
|
|
155
|
+
assistantId: string,
|
|
156
|
+
options: LocalPathOptions = {},
|
|
157
|
+
): string {
|
|
158
|
+
assertSafePathSegment(assistantId, "assistant ID", options);
|
|
159
|
+
return joinLocalPath(
|
|
160
|
+
options,
|
|
161
|
+
resolveAssistantsDir(env, options),
|
|
162
|
+
assistantId,
|
|
163
|
+
);
|
|
66
164
|
}
|
|
67
165
|
|
|
68
166
|
/**
|
|
@@ -74,6 +172,14 @@ export function resolveConfigDir(
|
|
|
74
172
|
export function guardianTokenPath(
|
|
75
173
|
configDir: string,
|
|
76
174
|
assistantId: string,
|
|
175
|
+
options: LocalPathOptions = {},
|
|
77
176
|
): string {
|
|
78
|
-
|
|
177
|
+
assertSafePathSegment(assistantId, "assistant ID", options);
|
|
178
|
+
return joinLocalPath(
|
|
179
|
+
options,
|
|
180
|
+
configDir,
|
|
181
|
+
"assistants",
|
|
182
|
+
assistantId,
|
|
183
|
+
"guardian-token.json",
|
|
184
|
+
);
|
|
79
185
|
}
|
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
import
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
joinLocalPath,
|
|
5
|
+
resolveConfigHomes,
|
|
6
|
+
type LocalPathOptions,
|
|
7
|
+
} from "./paths";
|
|
4
8
|
|
|
5
9
|
const PRODUCTION_ENVIRONMENT_NAME = "production";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Location of the persisted default-environment file, written by
|
|
9
13
|
* `vellum env set`. It lives at a fixed, environment-agnostic path so it can
|
|
10
|
-
* be read before the environment is known.
|
|
11
|
-
*
|
|
14
|
+
* be read before the environment is known. Uses AppData on Windows and the
|
|
15
|
+
* XDG config home on macOS and Linux.
|
|
12
16
|
*/
|
|
17
|
+
export function defaultEnvironmentFilePaths(
|
|
18
|
+
env: Record<string, string | undefined>,
|
|
19
|
+
options: LocalPathOptions = {},
|
|
20
|
+
): string[] {
|
|
21
|
+
return resolveConfigHomes(env, options).map((home) =>
|
|
22
|
+
joinLocalPath(options, home, "vellum", "environment"),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
export function defaultEnvironmentFilePath(
|
|
14
27
|
env: Record<string, string | undefined>,
|
|
28
|
+
options: LocalPathOptions = {},
|
|
15
29
|
): string {
|
|
16
|
-
|
|
17
|
-
env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config");
|
|
18
|
-
return path.join(xdgConfigHome, "vellum", "environment");
|
|
30
|
+
return defaultEnvironmentFilePaths(env, options)[0]!;
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
/**
|
|
@@ -24,15 +36,22 @@ export function defaultEnvironmentFilePath(
|
|
|
24
36
|
*/
|
|
25
37
|
export function readDefaultEnvironment(
|
|
26
38
|
env: Record<string, string | undefined>,
|
|
39
|
+
options: LocalPathOptions = {},
|
|
27
40
|
): string | undefined {
|
|
28
|
-
const filePath
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
for (const filePath of defaultEnvironmentFilePaths(env, options)) {
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(filePath)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const content = readFileSync(filePath, "utf-8").trim();
|
|
47
|
+
if (content.length > 0) {
|
|
48
|
+
return content;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Try the next compatible location.
|
|
52
|
+
}
|
|
35
53
|
}
|
|
54
|
+
return undefined;
|
|
36
55
|
}
|
|
37
56
|
|
|
38
57
|
/**
|
|
@@ -53,10 +72,12 @@ export function readDefaultEnvironment(
|
|
|
53
72
|
*/
|
|
54
73
|
export function resolveEnvironmentName(
|
|
55
74
|
env: Record<string, string | undefined>,
|
|
75
|
+
options: LocalPathOptions = {},
|
|
56
76
|
): string {
|
|
57
77
|
return (
|
|
78
|
+
options.environmentName ||
|
|
58
79
|
env.VELLUM_ENVIRONMENT?.trim() ||
|
|
59
|
-
readDefaultEnvironment(env) ||
|
|
80
|
+
readDefaultEnvironment(env, options) ||
|
|
60
81
|
PRODUCTION_ENVIRONMENT_NAME
|
|
61
82
|
);
|
|
62
83
|
}
|