agentroam 0.2.0-preview.25 → 0.2.0-preview.27
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/README.md +13 -12
- package/bin/desktop-download.mjs +4 -1
- package/bin/node-preflight.mjs +5 -4
- package/dist/cli.js +7 -0
- package/dist/device-pairing.js +46 -5
- package/dist/error-log.js +113 -0
- package/dist/platform-packages.js +9 -2
- package/dist/runtime-manager.js +3 -0
- package/dist/tunnel/public-readiness.js +2 -1
- package/dist/update/update-command.js +6 -12
- package/dist/update/update-install.js +34 -0
- package/dist/update/update-worker.js +12 -3
- package/install/README.md +5 -5
- package/install/install-agentroam.ps1 +1 -1
- package/install/install-agentroam.sh +2 -2
- package/install/install-manifest.json +15 -0
- package/package.json +8 -8
- package/dist/args.test.js +0 -84
- package/dist/cloudflared/bundled-asset.test.js +0 -91
- package/dist/cloudflared/installer.test.js +0 -95
- package/dist/codex-runtime-manager.test.js +0 -275
- package/dist/desktop-download.test.js +0 -83
- package/dist/device-pairing.test.js +0 -33
- package/dist/install-script-contract.test.js +0 -176
- package/dist/instance-takeover.test.js +0 -121
- package/dist/native-runtime.test.js +0 -40
- package/dist/network.test.js +0 -20
- package/dist/node-preflight.test.js +0 -136
- package/dist/node-runtime-manager.test.js +0 -184
- package/dist/opencode-runtime-manager.test.js +0 -111
- package/dist/pairing.js +0 -2
- package/dist/platform-packages.test.js +0 -86
- package/dist/platform.test.js +0 -18
- package/dist/port.test.js +0 -3
- package/dist/power/sleep-inhibitor.test.js +0 -104
- package/dist/process-supervisor.test.js +0 -42
- package/dist/runtime-manager.test.js +0 -38
- package/dist/service/launcher-cleanup.test.js +0 -81
- package/dist/service/macos-launch-agent.test.js +0 -327
- package/dist/service/runtime-state.test.js +0 -46
- package/dist/service/service-command.test.js +0 -189
- package/dist/service/service-files.test.js +0 -48
- package/dist/service/windows-service-host.test.js +0 -52
- package/dist/service/windows-task-service.test.js +0 -230
- package/dist/tunnel/cloudflare-provider.test.js +0 -45
- package/dist/tunnel/pinggy-provider.test.js +0 -40
- package/dist/tunnel/proxy-settings.test.js +0 -111
- package/dist/tunnel/public-readiness.test.js +0 -104
- package/dist/tunnel/relay-orchestrator.test.js +0 -84
- package/dist/unlock-service.test.js +0 -37
- package/dist/update/update-command.test.js +0 -70
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { ensurePrivateFile, readServiceConfig, resolveServicePaths, writePrivateJson, writePrivateText, } from "./service-files.js";
|
|
6
|
-
describe("service files", () => {
|
|
7
|
-
it("resolves control files under the home directory and runtime files under data-dir", async () => {
|
|
8
|
-
const home = await mkdtemp(resolve(tmpdir(), "agentroam-service-home-"));
|
|
9
|
-
const data = resolve(home, "custom-data");
|
|
10
|
-
const paths = resolveServicePaths(home, data);
|
|
11
|
-
expect(paths.plistPath).toBe(resolve(home, "Library/LaunchAgents/com.agentroam.service.plist"));
|
|
12
|
-
expect(paths.configPath).toBe(resolve(home, ".agentroam/service/config.json"));
|
|
13
|
-
expect(paths.statePath).toBe(resolve(home, ".agentroam/service/state.json"));
|
|
14
|
-
expect(paths.urlPath).toBe(resolve(data, "tunnel.url"));
|
|
15
|
-
expect(paths.stdoutPath).toBe(resolve(data, "logs/service.stdout.log"));
|
|
16
|
-
});
|
|
17
|
-
it("atomically replaces private JSON and text files", async () => {
|
|
18
|
-
const home = await mkdtemp(resolve(tmpdir(), "agentroam-service-private-"));
|
|
19
|
-
const paths = resolveServicePaths(home);
|
|
20
|
-
const first = config(paths.dataDir, "first");
|
|
21
|
-
const second = config(paths.dataDir, "second");
|
|
22
|
-
await writePrivateJson(paths.configPath, first);
|
|
23
|
-
await writePrivateJson(paths.configPath, second);
|
|
24
|
-
await writePrivateText(paths.urlPath, "https://example.trycloudflare.com/web\n");
|
|
25
|
-
await ensurePrivateFile(paths.stdoutPath);
|
|
26
|
-
expect(await readServiceConfig(paths)).toEqual(second);
|
|
27
|
-
expect(await readFile(paths.urlPath, "utf8")).toContain("trycloudflare.com");
|
|
28
|
-
for (const path of [paths.configPath, paths.urlPath, paths.stdoutPath]) {
|
|
29
|
-
expect((await stat(path)).mode & 0o777).toBe(0o600);
|
|
30
|
-
}
|
|
31
|
-
expect((await stat(paths.controlDir)).mode & 0o777).toBe(0o700);
|
|
32
|
-
expect((await stat(paths.logsDir)).mode & 0o777).toBe(0o700);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
function config(dataDir, version) {
|
|
36
|
-
return {
|
|
37
|
-
version,
|
|
38
|
-
nodePath: "/node",
|
|
39
|
-
cliPath: "/agentroam.mjs",
|
|
40
|
-
roots: ["/workspace"],
|
|
41
|
-
port: null,
|
|
42
|
-
relay: "auto",
|
|
43
|
-
tunnelCommand: null,
|
|
44
|
-
localOnly: false,
|
|
45
|
-
dataDir,
|
|
46
|
-
installedAt: "2026-09-03T00:00:00.000Z",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { mkdtemp, mkdir } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
import { describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { writePrivateJson } from "./service-files.js";
|
|
7
|
-
import { runWindowsServiceHost } from "./windows-service-host.js";
|
|
8
|
-
describe("Windows service host", () => {
|
|
9
|
-
it("starts the configured CLI in service mode with private log descriptors", async () => {
|
|
10
|
-
const root = await mkdtemp(resolve(tmpdir(), "agentroam-windows-host-"));
|
|
11
|
-
const workspace = resolve(root, "workspace with spaces");
|
|
12
|
-
const configPath = resolve(root, "service/config.json");
|
|
13
|
-
await mkdir(workspace, { recursive: true });
|
|
14
|
-
const config = {
|
|
15
|
-
version: "0.2.0-preview.9",
|
|
16
|
-
nodePath: "C:\\Node Runtime\\node.exe",
|
|
17
|
-
cliPath: "C:\\AgentRoam\\bin\\agentroam.mjs",
|
|
18
|
-
environmentPath: "C:\\Node Runtime;C:\\Windows\\System32",
|
|
19
|
-
codexPath: "C:\\Users\\test\\.nvm\\v22.22.2\\codex.exe",
|
|
20
|
-
codexHome: "C:\\Users\\test\\.codex",
|
|
21
|
-
roots: [workspace],
|
|
22
|
-
port: 3010,
|
|
23
|
-
relay: "cloudflare",
|
|
24
|
-
tunnelCommand: null,
|
|
25
|
-
localOnly: false,
|
|
26
|
-
dataDir: resolve(root, "data"),
|
|
27
|
-
installedAt: "2026-09-04T00:00:00.000Z",
|
|
28
|
-
};
|
|
29
|
-
await writePrivateJson(configPath, config);
|
|
30
|
-
const child = new EventEmitter();
|
|
31
|
-
Object.assign(child, { kill: vi.fn(() => true) });
|
|
32
|
-
const spawnProcess = vi.fn(() => {
|
|
33
|
-
queueMicrotask(() => child.emit("exit", 0, null));
|
|
34
|
-
return child;
|
|
35
|
-
});
|
|
36
|
-
await expect(runWindowsServiceHost(configPath, { spawnProcess: spawnProcess })).resolves.toBe(0);
|
|
37
|
-
const calls = spawnProcess.mock.calls;
|
|
38
|
-
const [command, args, options] = calls[0];
|
|
39
|
-
expect(command).toBe(config.nodePath);
|
|
40
|
-
expect(args).toEqual([
|
|
41
|
-
config.cliPath, "start", "--root", workspace, "--port", "3010", "--relay", "cloudflare",
|
|
42
|
-
"--data-dir", config.dataDir, "--no-qr",
|
|
43
|
-
]);
|
|
44
|
-
expect(options).toMatchObject({ cwd: workspace, windowsHide: true });
|
|
45
|
-
expect(options.env.AGENTROAM_SERVICE).toBe("1");
|
|
46
|
-
expect(options.env.PATH).toBe(config.environmentPath);
|
|
47
|
-
expect(options.env.AGENT_CODEX_BIN).toBe(config.codexPath);
|
|
48
|
-
expect(options.env.CODEX_HOME).toBe(config.codexHome);
|
|
49
|
-
expect(typeof options.stdio[1]).toBe("number");
|
|
50
|
-
expect(typeof options.stdio[2]).toBe("number");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, mkdir, readFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { resolveServicePaths, writePrivateText } from "./service-files.js";
|
|
6
|
-
import { WindowsTaskService, buildRegisterTaskScript, quoteWindowsCommandLineArgument, resolveWindowsServiceHost, } from "./windows-task-service.js";
|
|
7
|
-
describe("WindowsTaskService", () => {
|
|
8
|
-
it("registers, stops, starts, and uninstalls a current-user task", async () => {
|
|
9
|
-
const home = await mkdtemp(resolve(tmpdir(), "agentroam-windows-task-"));
|
|
10
|
-
const paths = resolveServicePaths(home, resolve(home, "data"));
|
|
11
|
-
const value = config(home, paths.dataDir);
|
|
12
|
-
await mkdir(value.roots[0], { recursive: true });
|
|
13
|
-
let installed = false;
|
|
14
|
-
let running = false;
|
|
15
|
-
let processRunning = false;
|
|
16
|
-
let pid = 800;
|
|
17
|
-
const scripts = [];
|
|
18
|
-
const runner = async (command, args) => {
|
|
19
|
-
expect(command).toBe("powershell.exe");
|
|
20
|
-
const script = Buffer.from(args.at(-1), "base64").toString("utf16le");
|
|
21
|
-
scripts.push(script);
|
|
22
|
-
if (script.includes("Register-ScheduledTask"))
|
|
23
|
-
installed = true;
|
|
24
|
-
if (script.includes("Start-ScheduledTask")) {
|
|
25
|
-
running = true;
|
|
26
|
-
processRunning = true;
|
|
27
|
-
pid++;
|
|
28
|
-
await writePrivateText(paths.statePath, JSON.stringify({
|
|
29
|
-
status: "ready", pid, version: value.version, startedAt: "x", updatedAt: "x",
|
|
30
|
-
accessUrl: "https://ready.example/web",
|
|
31
|
-
}));
|
|
32
|
-
await writePrivateText(paths.urlPath, "https://ready.example/web\n");
|
|
33
|
-
}
|
|
34
|
-
if (script.includes("Stop-ScheduledTask")) {
|
|
35
|
-
running = false;
|
|
36
|
-
processRunning = false;
|
|
37
|
-
}
|
|
38
|
-
if (script.includes("Unregister-ScheduledTask"))
|
|
39
|
-
installed = false;
|
|
40
|
-
if (script.includes("Get-ScheduledTask")) {
|
|
41
|
-
return { code: 0, stdout: JSON.stringify({ installed, running }), stderr: "" };
|
|
42
|
-
}
|
|
43
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
44
|
-
};
|
|
45
|
-
const service = new WindowsTaskService({
|
|
46
|
-
homeDir: home,
|
|
47
|
-
runner,
|
|
48
|
-
processExists: () => processRunning,
|
|
49
|
-
validateConfig: async () => undefined,
|
|
50
|
-
readyTimeoutMs: 50,
|
|
51
|
-
stopTimeoutMs: 50,
|
|
52
|
-
pollIntervalMs: 1,
|
|
53
|
-
});
|
|
54
|
-
const installedResult = await service.install(value);
|
|
55
|
-
expect(installedResult.state).toMatchObject({ status: "ready", pid: 801 });
|
|
56
|
-
expect(installedResult.definition).toBe("AgentRoam");
|
|
57
|
-
expect(await service.url()).toBe("https://ready.example/web");
|
|
58
|
-
await service.stop();
|
|
59
|
-
expect((await service.status()).running).toBe(false);
|
|
60
|
-
await service.start();
|
|
61
|
-
expect((await service.status()).running).toBe(true);
|
|
62
|
-
await writePrivateText(resolve(paths.dataDir, "data/keep.txt"), "keep");
|
|
63
|
-
await writePrivateText(paths.stdoutPath, "keep logs");
|
|
64
|
-
const result = await service.uninstall();
|
|
65
|
-
expect(result).toEqual({ removed: true, preservedDataDir: paths.dataDir });
|
|
66
|
-
expect(await readFile(resolve(paths.dataDir, "data/keep.txt"), "utf8")).toBe("keep");
|
|
67
|
-
expect(await readFile(paths.stdoutPath, "utf8")).toBe("keep logs");
|
|
68
|
-
expect(scripts.some((script) => script.includes("New-ScheduledTaskPrincipal") && script.includes("RunLevel Limited"))).toBe(true);
|
|
69
|
-
expect(scripts.some((script) => script.includes("cmd.exe") || script.includes("ES_DISPLAY_REQUIRED"))).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
it("stops and unregisters a running task before installing its replacement", async () => {
|
|
72
|
-
const home = await mkdtemp(resolve(tmpdir(), "agentroam-windows-task-reinstall-"));
|
|
73
|
-
const paths = resolveServicePaths(home, resolve(home, "data"));
|
|
74
|
-
const value = config(home, paths.dataDir);
|
|
75
|
-
await mkdir(value.roots[0], { recursive: true });
|
|
76
|
-
await writePrivateText(paths.configPath, JSON.stringify({ ...value, version: "0.2.0-preview.8" }));
|
|
77
|
-
await writePrivateText(paths.statePath, JSON.stringify({
|
|
78
|
-
status: "ready", pid: 800, version: "0.2.0-preview.8", startedAt: "x", updatedAt: "x",
|
|
79
|
-
}));
|
|
80
|
-
await writePrivateText(resolve(paths.dataDir, "data/keep.txt"), "keep");
|
|
81
|
-
await writePrivateText(paths.stdoutPath, "keep logs");
|
|
82
|
-
let installed = true;
|
|
83
|
-
let running = true;
|
|
84
|
-
let processChecks = 0;
|
|
85
|
-
const lifecycle = [];
|
|
86
|
-
const runner = async (_command, args) => {
|
|
87
|
-
const script = Buffer.from(args.at(-1), "base64").toString("utf16le");
|
|
88
|
-
if (script.includes("Get-ScheduledTask")) {
|
|
89
|
-
return { code: 0, stdout: JSON.stringify({ installed, running }), stderr: "" };
|
|
90
|
-
}
|
|
91
|
-
if (script.includes("Stop-ScheduledTask")) {
|
|
92
|
-
lifecycle.push("stop");
|
|
93
|
-
running = false;
|
|
94
|
-
}
|
|
95
|
-
if (script.includes("Unregister-ScheduledTask")) {
|
|
96
|
-
lifecycle.push("unregister");
|
|
97
|
-
installed = false;
|
|
98
|
-
}
|
|
99
|
-
if (script.includes("Register-ScheduledTask")) {
|
|
100
|
-
lifecycle.push("register");
|
|
101
|
-
installed = true;
|
|
102
|
-
}
|
|
103
|
-
if (script.includes("Start-ScheduledTask")) {
|
|
104
|
-
lifecycle.push("start");
|
|
105
|
-
running = true;
|
|
106
|
-
await writePrivateText(paths.statePath, JSON.stringify({
|
|
107
|
-
status: "ready", pid: 801, version: value.version, startedAt: "y", updatedAt: "y",
|
|
108
|
-
}));
|
|
109
|
-
}
|
|
110
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
111
|
-
};
|
|
112
|
-
const processExists = vi.fn(() => {
|
|
113
|
-
processChecks++;
|
|
114
|
-
if (processChecks === 1)
|
|
115
|
-
return true;
|
|
116
|
-
lifecycle.push("process-exit");
|
|
117
|
-
return false;
|
|
118
|
-
});
|
|
119
|
-
const result = await new WindowsTaskService({
|
|
120
|
-
homeDir: home,
|
|
121
|
-
runner,
|
|
122
|
-
processExists,
|
|
123
|
-
validateConfig: async () => undefined,
|
|
124
|
-
readyTimeoutMs: 20,
|
|
125
|
-
stopTimeoutMs: 50,
|
|
126
|
-
pollIntervalMs: 1,
|
|
127
|
-
}).install(value);
|
|
128
|
-
expect(result.state).toMatchObject({ status: "ready", pid: 801 });
|
|
129
|
-
expect(lifecycle).toEqual(["stop", "process-exit", "unregister", "register", "start"]);
|
|
130
|
-
expect(processExists).toHaveBeenCalledWith(800);
|
|
131
|
-
expect(await readFile(resolve(paths.dataDir, "data/keep.txt"), "utf8")).toBe("keep");
|
|
132
|
-
expect(await readFile(paths.stdoutPath, "utf8")).toBe("keep logs");
|
|
133
|
-
});
|
|
134
|
-
it("replaces an installed stopped task without stopping a process", async () => {
|
|
135
|
-
const home = await mkdtemp(resolve(tmpdir(), "agentroam-windows-task-reinstall-stopped-"));
|
|
136
|
-
const paths = resolveServicePaths(home, resolve(home, "data"));
|
|
137
|
-
const value = config(home, paths.dataDir);
|
|
138
|
-
await mkdir(value.roots[0], { recursive: true });
|
|
139
|
-
const lifecycle = [];
|
|
140
|
-
let installed = true;
|
|
141
|
-
const runner = async (_command, args) => {
|
|
142
|
-
const script = Buffer.from(args.at(-1), "base64").toString("utf16le");
|
|
143
|
-
if (script.includes("Get-ScheduledTask")) {
|
|
144
|
-
return { code: 0, stdout: JSON.stringify({ installed, running: false }), stderr: "" };
|
|
145
|
-
}
|
|
146
|
-
if (script.includes("Stop-ScheduledTask"))
|
|
147
|
-
lifecycle.push("stop");
|
|
148
|
-
if (script.includes("Unregister-ScheduledTask")) {
|
|
149
|
-
lifecycle.push("unregister");
|
|
150
|
-
installed = false;
|
|
151
|
-
}
|
|
152
|
-
if (script.includes("Register-ScheduledTask")) {
|
|
153
|
-
lifecycle.push("register");
|
|
154
|
-
installed = true;
|
|
155
|
-
}
|
|
156
|
-
if (script.includes("Start-ScheduledTask")) {
|
|
157
|
-
lifecycle.push("start");
|
|
158
|
-
await writePrivateText(paths.statePath, JSON.stringify({
|
|
159
|
-
status: "ready", pid: 801, version: value.version, startedAt: "y", updatedAt: "y",
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
162
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
163
|
-
};
|
|
164
|
-
await new WindowsTaskService({
|
|
165
|
-
homeDir: home,
|
|
166
|
-
runner,
|
|
167
|
-
processExists: vi.fn(() => false),
|
|
168
|
-
validateConfig: async () => undefined,
|
|
169
|
-
readyTimeoutMs: 20,
|
|
170
|
-
pollIntervalMs: 1,
|
|
171
|
-
}).install(value);
|
|
172
|
-
expect(lifecycle).toEqual(["unregister", "register", "start"]);
|
|
173
|
-
});
|
|
174
|
-
it("does not replace a running task while its previous process is still alive", async () => {
|
|
175
|
-
const home = await mkdtemp(resolve(tmpdir(), "agentroam-windows-task-reinstall-timeout-"));
|
|
176
|
-
const paths = resolveServicePaths(home, resolve(home, "data"));
|
|
177
|
-
const value = config(home, paths.dataDir);
|
|
178
|
-
await mkdir(value.roots[0], { recursive: true });
|
|
179
|
-
await writePrivateText(paths.statePath, JSON.stringify({
|
|
180
|
-
status: "ready", pid: 800, version: "0.2.0-preview.8", startedAt: "x", updatedAt: "x",
|
|
181
|
-
}));
|
|
182
|
-
const lifecycle = [];
|
|
183
|
-
const runner = async (_command, args) => {
|
|
184
|
-
const script = Buffer.from(args.at(-1), "base64").toString("utf16le");
|
|
185
|
-
if (script.includes("Get-ScheduledTask")) {
|
|
186
|
-
return { code: 0, stdout: JSON.stringify({ installed: true, running: true }), stderr: "" };
|
|
187
|
-
}
|
|
188
|
-
if (script.includes("Stop-ScheduledTask"))
|
|
189
|
-
lifecycle.push("stop");
|
|
190
|
-
if (script.includes("Unregister-ScheduledTask"))
|
|
191
|
-
lifecycle.push("unregister");
|
|
192
|
-
if (script.includes("Register-ScheduledTask"))
|
|
193
|
-
lifecycle.push("register");
|
|
194
|
-
return { code: 0, stdout: "", stderr: "" };
|
|
195
|
-
};
|
|
196
|
-
await expect(new WindowsTaskService({
|
|
197
|
-
homeDir: home,
|
|
198
|
-
runner,
|
|
199
|
-
processExists: () => true,
|
|
200
|
-
validateConfig: async () => undefined,
|
|
201
|
-
stopTimeoutMs: 0,
|
|
202
|
-
pollIntervalMs: 1,
|
|
203
|
-
}).install(value)).rejects.toThrow("did not stop after task termination");
|
|
204
|
-
expect(lifecycle).toEqual(["stop"]);
|
|
205
|
-
});
|
|
206
|
-
it("builds a safely quoted task action for paths with spaces and apostrophes", () => {
|
|
207
|
-
const value = config("C:\\Users\\O'Brien", "C:\\Users\\O'Brien\\.agentroam");
|
|
208
|
-
const host = resolveWindowsServiceHost(value.cliPath);
|
|
209
|
-
const script = buildRegisterTaskScript(value, host, "C:\\Users\\O'Brien\\config.json");
|
|
210
|
-
expect(host).toBe("C:\\Agent Roam\\node_modules\\agentroam\\dist\\service\\windows-service-host.js");
|
|
211
|
-
expect(script).toContain("O''Brien");
|
|
212
|
-
expect(script).toContain("New-ScheduledTaskAction");
|
|
213
|
-
expect(script).toContain("RestartCount 999");
|
|
214
|
-
expect(quoteWindowsCommandLineArgument("C:\\path with spaces\\")).toBe('"C:\\path with spaces\\\\"');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
function config(home, dataDir) {
|
|
218
|
-
return {
|
|
219
|
-
version: "0.2.0-preview.9",
|
|
220
|
-
nodePath: "C:\\Node Runtime\\node.exe",
|
|
221
|
-
cliPath: "C:\\Agent Roam\\node_modules\\agentroam\\bin\\agentroam.mjs",
|
|
222
|
-
roots: [resolve(home, "workspace")],
|
|
223
|
-
port: null,
|
|
224
|
-
relay: "auto",
|
|
225
|
-
tunnelCommand: null,
|
|
226
|
-
localOnly: false,
|
|
227
|
-
dataDir,
|
|
228
|
-
installedAt: "2026-09-04T00:00:00.000Z",
|
|
229
|
-
};
|
|
230
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { PassThrough } from "node:stream";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { buildCloudflaredArguments, waitForConnectedUrl } from "./cloudflare-provider.js";
|
|
5
|
-
describe("buildCloudflaredArguments", () => {
|
|
6
|
-
it("uses HTTP/2 so launchd tunnels do not depend on outbound UDP", () => {
|
|
7
|
-
expect(buildCloudflaredArguments("http://127.0.0.1:3000")).toEqual([
|
|
8
|
-
"tunnel",
|
|
9
|
-
"--no-autoupdate",
|
|
10
|
-
"--protocol",
|
|
11
|
-
"http2",
|
|
12
|
-
"--url",
|
|
13
|
-
"http://127.0.0.1:3000",
|
|
14
|
-
]);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
describe("waitForConnectedUrl", () => {
|
|
18
|
-
it("waits for both the public URL and a registered tunnel connection", async () => {
|
|
19
|
-
const { child, stderr } = fakeChild();
|
|
20
|
-
let resolved = false;
|
|
21
|
-
const ready = waitForConnectedUrl(child, vi.fn(), 100).then((url) => {
|
|
22
|
-
resolved = true;
|
|
23
|
-
return url;
|
|
24
|
-
});
|
|
25
|
-
stderr.write("https://ready.trycloudflare.com\n");
|
|
26
|
-
await new Promise((resolvePromise) => setImmediate(resolvePromise));
|
|
27
|
-
expect(resolved).toBe(false);
|
|
28
|
-
stderr.write("INF Registered tunnel ");
|
|
29
|
-
stderr.write("connection connIndex=0 protocol=quic\n");
|
|
30
|
-
await expect(ready).resolves.toBe("https://ready.trycloudflare.com");
|
|
31
|
-
});
|
|
32
|
-
it("supports registration output arriving before the URL", async () => {
|
|
33
|
-
const { child, stdout, stderr } = fakeChild();
|
|
34
|
-
const ready = waitForConnectedUrl(child, vi.fn(), 100);
|
|
35
|
-
stdout.write("INF Registered tunnel connection connIndex=0 protocol=http2\n");
|
|
36
|
-
stderr.write("Visit https://later.trycloudflare.com when ready\n");
|
|
37
|
-
await expect(ready).resolves.toBe("https://later.trycloudflare.com");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
function fakeChild() {
|
|
41
|
-
const stdout = new PassThrough();
|
|
42
|
-
const stderr = new PassThrough();
|
|
43
|
-
const child = Object.assign(new EventEmitter(), { stdout, stderr });
|
|
44
|
-
return { child, stdout, stderr };
|
|
45
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { PassThrough } from "node:stream";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { PinggyTunnelProvider, parsePinggyUrl } from "./pinggy-provider.js";
|
|
5
|
-
describe("parsePinggyUrl", () => {
|
|
6
|
-
it("accepts only Pinggy free HTTPS URLs", () => {
|
|
7
|
-
expect(parsePinggyUrl("open https://abc-123.run.pinggy-free.link now")).toBe("https://abc-123.run.pinggy-free.link");
|
|
8
|
-
expect(parsePinggyUrl("https://example.com")).toBeNull();
|
|
9
|
-
expect(parsePinggyUrl("http://abc.run.pinggy-free.link")).toBeNull();
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
describe("PinggyTunnelProvider", () => {
|
|
13
|
-
it("starts SSH on port 443 with persistent host-key checking", async () => {
|
|
14
|
-
const child = fakeChild();
|
|
15
|
-
const spawnImpl = vi.fn((_command, _args) => child);
|
|
16
|
-
const provider = new PinggyTunnelProvider(43210, "/tmp/agentroam-test", spawnImpl);
|
|
17
|
-
const started = provider.start({ localUrl: "http://127.0.0.1:43210", signal: new AbortController().signal, log: vi.fn() });
|
|
18
|
-
queueMicrotask(() => child.stderr.write("https://sample.run.pinggy-free.link\n"));
|
|
19
|
-
const handle = await started;
|
|
20
|
-
expect(handle.publicUrl).toBe("https://sample.run.pinggy-free.link");
|
|
21
|
-
const args = spawnImpl.mock.calls[0][1];
|
|
22
|
-
expect(args).toContain("443");
|
|
23
|
-
expect(args).toContain("StrictHostKeyChecking=accept-new");
|
|
24
|
-
expect(args).toContain("0:localhost:43210");
|
|
25
|
-
expect(args.some((value) => value.startsWith("UserKnownHostsFile="))).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
function fakeChild() {
|
|
29
|
-
const child = new EventEmitter();
|
|
30
|
-
child.stdout = new PassThrough();
|
|
31
|
-
child.stderr = new PassThrough();
|
|
32
|
-
Object.defineProperty(child, "exitCode", { value: null, writable: true });
|
|
33
|
-
Object.defineProperty(child, "pid", { value: undefined, writable: true });
|
|
34
|
-
child.kill = vi.fn(() => {
|
|
35
|
-
child.exitCode = 0;
|
|
36
|
-
child.emit("exit", 0, null);
|
|
37
|
-
return true;
|
|
38
|
-
});
|
|
39
|
-
return child;
|
|
40
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { parseMacSystemProxy, resolveProxyForUrl } from "./proxy-settings.js";
|
|
3
|
-
const target = new URL("https://api.example.test/status");
|
|
4
|
-
describe("resolveProxyForUrl", () => {
|
|
5
|
-
it("prefers the lowercase HTTPS proxy and normalizes an omitted scheme", async () => {
|
|
6
|
-
await expect(resolveProxyForUrl(target, {
|
|
7
|
-
env: {
|
|
8
|
-
https_proxy: "127.0.0.1:7897",
|
|
9
|
-
HTTPS_PROXY: "http://uppercase.test:8080",
|
|
10
|
-
HTTP_PROXY: "http://http.test:8080",
|
|
11
|
-
},
|
|
12
|
-
platform: "linux",
|
|
13
|
-
})).resolves.toBe("http://127.0.0.1:7897/");
|
|
14
|
-
});
|
|
15
|
-
it("uses HTTP_PROXY as the HTTPS fallback and ALL_PROXY for HTTP", async () => {
|
|
16
|
-
await expect(resolveProxyForUrl(target, { env: { HTTP_PROXY: "http://http.test:8080" }, platform: "linux" })).resolves.toBe("http://http.test:8080/");
|
|
17
|
-
await expect(resolveProxyForUrl(new URL("http://example.test"), {
|
|
18
|
-
env: { ALL_PROXY: "https://all.test:8443" },
|
|
19
|
-
platform: "linux",
|
|
20
|
-
})).resolves.toBe("https://all.test:8443/");
|
|
21
|
-
});
|
|
22
|
-
it.each([
|
|
23
|
-
["*", "https://api.example.test/status"],
|
|
24
|
-
["api.example.test", "https://api.example.test/status"],
|
|
25
|
-
[".example.test", "https://api.example.test/status"],
|
|
26
|
-
["example.test", "https://api.example.test/status"],
|
|
27
|
-
["api.example.test:443", "https://api.example.test/status"],
|
|
28
|
-
["api.example.test:8443", "https://api.example.test:8443/status"],
|
|
29
|
-
])("bypasses the proxy for NO_PROXY=%s", async (noProxy, url) => {
|
|
30
|
-
await expect(resolveProxyForUrl(new URL(url), {
|
|
31
|
-
env: { HTTPS_PROXY: "http://proxy.test:8080", NO_PROXY: noProxy },
|
|
32
|
-
platform: "darwin",
|
|
33
|
-
readMacSystemProxy: vi.fn(async () => macProxyOutput()),
|
|
34
|
-
})).resolves.toBeNull();
|
|
35
|
-
});
|
|
36
|
-
it("does not bypass a suffix lookalike or a rule with a different port", async () => {
|
|
37
|
-
await expect(resolveProxyForUrl(target, {
|
|
38
|
-
env: { HTTPS_PROXY: "http://proxy.test:8080", no_proxy: "notexample.test,api.example.test:8443" },
|
|
39
|
-
platform: "linux",
|
|
40
|
-
})).resolves.toBe("http://proxy.test:8080/");
|
|
41
|
-
});
|
|
42
|
-
it("falls back to uppercase NO_PROXY when the lowercase value is empty", async () => {
|
|
43
|
-
await expect(resolveProxyForUrl(target, {
|
|
44
|
-
env: { HTTPS_PROXY: "http://proxy.test:8080", no_proxy: "", NO_PROXY: "example.test" },
|
|
45
|
-
platform: "linux",
|
|
46
|
-
})).resolves.toBeNull();
|
|
47
|
-
});
|
|
48
|
-
it("falls back to the enabled macOS HTTPS proxy", async () => {
|
|
49
|
-
await expect(resolveProxyForUrl(target, {
|
|
50
|
-
env: {},
|
|
51
|
-
platform: "darwin",
|
|
52
|
-
readMacSystemProxy: async () => macProxyOutput(),
|
|
53
|
-
})).resolves.toBe("http://127.0.0.1:7897/");
|
|
54
|
-
});
|
|
55
|
-
it("uses the macOS HTTP proxy when HTTPS proxying is disabled", async () => {
|
|
56
|
-
await expect(resolveProxyForUrl(target, {
|
|
57
|
-
env: {},
|
|
58
|
-
platform: "darwin",
|
|
59
|
-
readMacSystemProxy: async () => macProxyOutput({ httpsEnabled: false }),
|
|
60
|
-
})).resolves.toBe("http://127.0.0.1:7898/");
|
|
61
|
-
});
|
|
62
|
-
it("returns direct mode for command failures and non-macOS platforms", async () => {
|
|
63
|
-
await expect(resolveProxyForUrl(target, {
|
|
64
|
-
env: {},
|
|
65
|
-
platform: "darwin",
|
|
66
|
-
readMacSystemProxy: async () => {
|
|
67
|
-
throw new Error("scutil failed");
|
|
68
|
-
},
|
|
69
|
-
})).resolves.toBeNull();
|
|
70
|
-
const reader = vi.fn(async () => macProxyOutput());
|
|
71
|
-
await expect(resolveProxyForUrl(target, { env: {}, platform: "linux", readMacSystemProxy: reader })).resolves.toBeNull();
|
|
72
|
-
expect(reader).not.toHaveBeenCalled();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
describe("parseMacSystemProxy", () => {
|
|
76
|
-
it("parses enabled HTTP and HTTPS settings", () => {
|
|
77
|
-
expect(parseMacSystemProxy(macProxyOutput())).toEqual({
|
|
78
|
-
httpProxy: "http://127.0.0.1:7898/",
|
|
79
|
-
httpsProxy: "http://127.0.0.1:7897/",
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
it("ignores disabled and malformed settings", () => {
|
|
83
|
-
expect(parseMacSystemProxy(`
|
|
84
|
-
HTTPEnable : 0
|
|
85
|
-
HTTPProxy : 127.0.0.1
|
|
86
|
-
HTTPPort : 7898
|
|
87
|
-
HTTPSEnable : 1
|
|
88
|
-
HTTPSProxy : 127.0.0.1
|
|
89
|
-
HTTPSPort : not-a-port
|
|
90
|
-
`)).toEqual({ httpProxy: null, httpsProxy: null });
|
|
91
|
-
expect(parseMacSystemProxy("HTTPSEnable : 1\nHTTPSProxy : 127.0.0.1\nHTTPSPort : 70000")).toEqual({
|
|
92
|
-
httpProxy: null,
|
|
93
|
-
httpsProxy: null,
|
|
94
|
-
});
|
|
95
|
-
expect(parseMacSystemProxy("HTTPSEnable : 1\nHTTPSProxy : proxy.test/path\nHTTPSPort : 7897")).toEqual({
|
|
96
|
-
httpProxy: null,
|
|
97
|
-
httpsProxy: null,
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
function macProxyOutput({ httpsEnabled = true } = {}) {
|
|
102
|
-
return `
|
|
103
|
-
<dictionary> {
|
|
104
|
-
HTTPEnable : 1
|
|
105
|
-
HTTPPort : 7898
|
|
106
|
-
HTTPProxy : 127.0.0.1
|
|
107
|
-
HTTPSEnable : ${httpsEnabled ? "1" : "0"}
|
|
108
|
-
HTTPSPort : 7897
|
|
109
|
-
HTTPSProxy : 127.0.0.1
|
|
110
|
-
}`;
|
|
111
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { waitForPublicReadiness } from "./public-readiness.js";
|
|
3
|
-
describe("waitForPublicReadiness", () => {
|
|
4
|
-
it("retries a transient 530 and accepts the expected auth status", async () => {
|
|
5
|
-
const fetchImpl = vi
|
|
6
|
-
.fn()
|
|
7
|
-
.mockResolvedValueOnce(new Response("unavailable", { status: 530 }))
|
|
8
|
-
.mockResolvedValueOnce(Response.json({ authenticated: false, needsSetup: true }, { status: 200 }));
|
|
9
|
-
await waitForPublicReadiness("https://example.test", {
|
|
10
|
-
fetchImpl,
|
|
11
|
-
intervalMs: 1,
|
|
12
|
-
timeoutMs: 100,
|
|
13
|
-
});
|
|
14
|
-
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
15
|
-
});
|
|
16
|
-
it("rejects an invalid HTTP 200 payload", async () => {
|
|
17
|
-
const fetchImpl = vi.fn().mockResolvedValue(Response.json({ ok: true }));
|
|
18
|
-
await expect(waitForPublicReadiness("https://example.test", { fetchImpl, intervalMs: 1, timeoutMs: 5 })).rejects.toThrow("invalid auth status JSON");
|
|
19
|
-
});
|
|
20
|
-
it("includes the last HTTP status in timeout diagnostics", async () => {
|
|
21
|
-
const fetchImpl = vi.fn().mockResolvedValue(new Response("bad gateway", { status: 502 }));
|
|
22
|
-
await expect(waitForPublicReadiness("https://example.test", { fetchImpl, intervalMs: 1, timeoutMs: 5 })).rejects.toThrow("HTTP 502");
|
|
23
|
-
});
|
|
24
|
-
it("reuses one proxy handle across retries and closes it after success", async () => {
|
|
25
|
-
const close = vi.fn(async () => { });
|
|
26
|
-
const proxiedFetch = vi
|
|
27
|
-
.fn()
|
|
28
|
-
.mockResolvedValueOnce(new Response("unavailable", { status: 530 }))
|
|
29
|
-
.mockResolvedValueOnce(Response.json({ authenticated: false, needsSetup: true }, { status: 200 }));
|
|
30
|
-
const proxyFetchFactory = vi.fn(() => ({ fetch: proxiedFetch, close }));
|
|
31
|
-
await waitForPublicReadiness("https://example.test", {
|
|
32
|
-
intervalMs: 1,
|
|
33
|
-
timeoutMs: 100,
|
|
34
|
-
proxyResolver: async () => "http://127.0.0.1:7897/",
|
|
35
|
-
proxyFetchFactory,
|
|
36
|
-
});
|
|
37
|
-
expect(proxyFetchFactory).toHaveBeenCalledOnce();
|
|
38
|
-
expect(proxiedFetch).toHaveBeenCalledTimes(2);
|
|
39
|
-
expect(close).toHaveBeenCalledOnce();
|
|
40
|
-
});
|
|
41
|
-
it.each([
|
|
42
|
-
{
|
|
43
|
-
name: "a fatal response",
|
|
44
|
-
fetchImpl: vi.fn().mockResolvedValue(new Response("forbidden", { status: 403 })),
|
|
45
|
-
timeoutMs: 100,
|
|
46
|
-
expected: "HTTP 403",
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
name: "a timeout",
|
|
50
|
-
fetchImpl: vi.fn().mockResolvedValue(new Response("unavailable", { status: 530 })),
|
|
51
|
-
timeoutMs: 2,
|
|
52
|
-
expected: "HTTP 530",
|
|
53
|
-
},
|
|
54
|
-
])("closes the proxy handle after $name", async ({ fetchImpl, timeoutMs, expected }) => {
|
|
55
|
-
const close = vi.fn(async () => { });
|
|
56
|
-
await expect(waitForPublicReadiness("https://example.test", {
|
|
57
|
-
intervalMs: 1,
|
|
58
|
-
timeoutMs,
|
|
59
|
-
proxyResolver: async () => "http://127.0.0.1:7897/",
|
|
60
|
-
proxyFetchFactory: () => ({ fetch: fetchImpl, close }),
|
|
61
|
-
})).rejects.toThrow(expected);
|
|
62
|
-
expect(close).toHaveBeenCalledOnce();
|
|
63
|
-
});
|
|
64
|
-
it("closes the proxy handle when readiness is aborted", async () => {
|
|
65
|
-
const controller = new AbortController();
|
|
66
|
-
const close = vi.fn(async () => { });
|
|
67
|
-
const proxiedFetch = vi.fn().mockImplementation(async () => {
|
|
68
|
-
controller.abort();
|
|
69
|
-
throw new Error("request aborted");
|
|
70
|
-
});
|
|
71
|
-
await expect(waitForPublicReadiness("https://example.test", {
|
|
72
|
-
signal: controller.signal,
|
|
73
|
-
proxyResolver: async () => "http://127.0.0.1:7897/",
|
|
74
|
-
proxyFetchFactory: () => ({ fetch: proxiedFetch, close }),
|
|
75
|
-
})).rejects.toThrow("public tunnel readiness aborted");
|
|
76
|
-
expect(close).toHaveBeenCalledOnce();
|
|
77
|
-
});
|
|
78
|
-
it("does not create a proxy handle in direct mode", async () => {
|
|
79
|
-
const proxyFetchFactory = vi.fn();
|
|
80
|
-
const directFetch = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(Response.json({ authenticated: false, needsSetup: true }));
|
|
81
|
-
try {
|
|
82
|
-
await waitForPublicReadiness("https://example.test", {
|
|
83
|
-
proxyResolver: async () => null,
|
|
84
|
-
proxyFetchFactory,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
directFetch.mockRestore();
|
|
89
|
-
}
|
|
90
|
-
expect(proxyFetchFactory).not.toHaveBeenCalled();
|
|
91
|
-
});
|
|
92
|
-
it("skips proxy discovery when a fetch implementation is injected", async () => {
|
|
93
|
-
const proxyResolver = vi.fn(async () => "http://127.0.0.1:7897/");
|
|
94
|
-
const proxyFetchFactory = vi.fn();
|
|
95
|
-
const fetchImpl = vi.fn().mockResolvedValue(Response.json({ authenticated: false, needsSetup: true }));
|
|
96
|
-
await waitForPublicReadiness("https://example.test", {
|
|
97
|
-
fetchImpl,
|
|
98
|
-
proxyResolver,
|
|
99
|
-
proxyFetchFactory,
|
|
100
|
-
});
|
|
101
|
-
expect(proxyResolver).not.toHaveBeenCalled();
|
|
102
|
-
expect(proxyFetchFactory).not.toHaveBeenCalled();
|
|
103
|
-
});
|
|
104
|
-
});
|