agentroam 0.2.0-preview.2 → 0.2.0-preview.4
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 +12 -11
- package/dist/args.js +24 -8
- package/dist/args.test.js +25 -1
- package/dist/cli.js +112 -52
- package/dist/cloudflared/bundled-asset.js +37 -0
- package/dist/cloudflared/bundled-asset.test.js +50 -0
- package/dist/cloudflared/installer.js +83 -40
- package/dist/cloudflared/installer.test.js +63 -4
- package/dist/cloudflared/manifest.js +3 -5
- package/dist/tunnel/child-process.js +23 -0
- package/dist/tunnel/cloudflare-provider.js +59 -8
- package/dist/tunnel/cloudflare-provider.test.js +33 -0
- package/dist/tunnel/custom-command-provider.js +54 -4
- package/dist/tunnel/pinggy-provider.js +84 -0
- package/dist/tunnel/pinggy-provider.test.js +40 -0
- package/dist/tunnel/proxy-settings.js +141 -0
- package/dist/tunnel/proxy-settings.test.js +111 -0
- package/dist/tunnel/public-readiness.js +82 -0
- package/dist/tunnel/public-readiness.test.js +104 -0
- package/dist/tunnel/relay-orchestrator.js +43 -0
- package/dist/tunnel/relay-orchestrator.test.js +72 -0
- package/package.json +29 -8
- package/runtime/.next/BUILD_ID +1 -1
- package/runtime/.next/app-build-manifest.json +4 -4
- package/runtime/.next/app-path-routes-manifest.json +1 -1
- package/runtime/.next/build-manifest.json +7 -7
- package/runtime/.next/prerender-manifest.json +1 -1
- package/runtime/.next/react-loadable-manifest.json +2 -2
- package/runtime/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/runtime/.next/server/app/_not-found.html +1 -1
- package/runtime/.next/server/app/_not-found.rsc +1 -1
- package/runtime/.next/server/app/api/agent/abort/route.js +1 -1
- package/runtime/.next/server/app/api/agent/answer/route.js +1 -1
- package/runtime/.next/server/app/api/agent/model/route.js +1 -0
- package/runtime/.next/server/app/api/agent/model/route.js.nft.json +1 -0
- package/runtime/.next/server/app/api/agent/model.body +1 -0
- package/runtime/.next/server/app/api/agent/model.meta +1 -0
- package/runtime/.next/server/app/api/agent/run/route.js +1 -1
- package/runtime/.next/server/app/api/agent/steer/route.js +1 -0
- package/runtime/.next/server/app/api/agent/steer/route.js.nft.json +1 -0
- package/runtime/.next/server/app/api/agent/stream/route.js +3 -2
- package/runtime/.next/server/app/api/mcp/connect/route.js +1 -1
- package/runtime/.next/server/app/api/plugins/load/route.js +1 -1
- package/runtime/.next/server/app/api/remote-tools/register/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/route.js +1 -1
- package/runtime/.next/server/app/api/skills/route.js +1 -1
- package/runtime/.next/server/app/api/tools/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/login/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/logout/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/revoke-all/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/setup/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/status/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/bootstrap/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/device-state/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/history/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/history/route.js.nft.json +1 -1
- package/runtime/.next/server/app/api/web-console/preferences/route.js +1 -1
- package/runtime/.next/server/app/web/page.js +10 -10
- package/runtime/.next/server/app/web/page_client-reference-manifest.js +1 -1
- package/runtime/.next/server/app/web.html +1 -1
- package/runtime/.next/server/app/web.rsc +2 -2
- package/runtime/.next/server/app-paths-manifest.json +15 -13
- package/runtime/.next/server/chunks/322.js +28 -12
- package/runtime/.next/server/middleware-build-manifest.js +1 -1
- package/runtime/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/runtime/.next/server/pages/404.html +1 -1
- package/runtime/.next/server/pages/500.html +1 -1
- package/runtime/.next/server/server-reference-manifest.json +1 -1
- package/runtime/.next/static/chunks/982.9e87a5cc0e6b13cf.js +1 -0
- package/runtime/.next/static/chunks/app/web/page-02e0df331bb14489.js +1 -0
- package/runtime/.next/static/chunks/{main-a7dd8c98b48af038.js → main-30618c50527ce4e9.js} +1 -1
- package/runtime/.next/static/chunks/{webpack-e56f3a1950168597.js → webpack-0c872a6ee48cbbdf.js} +1 -1
- package/runtime/node_modules/.package-lock.json +3 -3
- package/runtime/node_modules/@agent/core/dist/domain/context/ContextLoader.d.ts +2 -0
- package/runtime/node_modules/@agent/core/dist/domain/context/ContextLoader.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/index.js +114 -12
- package/runtime/node_modules/@agent/core/dist/index.js.map +7 -6
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.d.ts +13 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.test.d.ts +2 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteAnonymousWebStore.test.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteDatabase.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/infrastructure/SQLiteSessionStore.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/infrastructure/index.d.ts +1 -0
- package/runtime/node_modules/@agent/core/dist/infrastructure/index.d.ts.map +1 -1
- package/runtime/node_modules/node-abi/abi_registry.json +7 -0
- package/runtime/node_modules/node-abi/package.json +1 -1
- package/runtime/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/runtime/package-lock.json +3 -3
- package/runtime/webapp/dist/assets/App-lXIeMH5u.js +52 -0
- package/runtime/webapp/dist/assets/index-BAFMhDtY.js +42 -0
- package/runtime/webapp/dist/assets/index-Beg9M2xI.css +1 -0
- package/runtime/webapp/dist/index.html +17 -0
- package/runtime/webapp/dist/pcm-audio-worklet.js +126 -0
- package/runtime/ws-server.mjs +148 -18
- package/runtime/.next/static/chunks/804.0e20fa702013ffef.js +0 -1
- package/runtime/.next/static/chunks/app/web/page-800bcc7d84eb45e4.js +0 -1
- /package/runtime/.next/static/{47UkTmBGxP5qz2mnMbNEp → dclalTmecO-HMVQvSKG7g}/_buildManifest.js +0 -0
- /package/runtime/.next/static/{47UkTmBGxP5qz2mnMbNEp → dclalTmecO-HMVQvSKG7g}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Zero-install mobile remote terminal and file console. **No desktop app, no VPN**
|
|
|
7
7
|
- **Node.js 22** (required)
|
|
8
8
|
- macOS arm64
|
|
9
9
|
|
|
10
|
-
> Preview status: `0.2.0-preview.
|
|
10
|
+
> Preview status: `0.2.0-preview.3` is intentionally limited to macOS arm64.
|
|
11
11
|
> macOS x64 and Windows x64 remain future formal-release targets.
|
|
12
12
|
|
|
13
13
|
## Quick start
|
|
@@ -19,13 +19,13 @@ npx agentroam@preview
|
|
|
19
19
|
Or install from tarball:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install ./agentroam-0.2.0-preview.2.tgz
|
|
22
|
+
npm install ./agentroam-cloudflared-darwin-arm64-0.2.0-preview.3.tgz ./agentroam-0.2.0-preview.3.tgz
|
|
23
23
|
npx agentroam
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
On first run:
|
|
27
27
|
|
|
28
|
-
1.
|
|
28
|
+
1. AgentRoam tries a bundled **Cloudflare Quick Tunnel**, then a **Pinggy HTTPS tunnel** over SSH port 443.
|
|
29
29
|
2. A **QR code** prints in the terminal — scan it on your phone.
|
|
30
30
|
3. Complete **one-time pairing** (5-minute token) and create the admin account.
|
|
31
31
|
4. Use the web terminal + file tree remotely.
|
|
@@ -44,19 +44,21 @@ agentroam version
|
|
|
44
44
|
|------|-------------|
|
|
45
45
|
| `--root <path>` | Workspace root (repeatable). Default: cwd |
|
|
46
46
|
| `--local-only` | Skip tunnel; LAN/localhost only |
|
|
47
|
-
| `--relay
|
|
47
|
+
| `--relay auto` | Cloudflare, then Pinggy, then LAN (default) |
|
|
48
|
+
| `--relay cloudflare` | Cloudflare only, then LAN |
|
|
49
|
+
| `--relay pinggy` | Pinggy only, then LAN |
|
|
48
50
|
| `--relay custom --tunnel-command "<cmd>"` | Your own tunnel command |
|
|
49
51
|
| `--no-qr` | Don't print QR code |
|
|
50
52
|
| `--port <n>` | Fixed local port |
|
|
51
53
|
| `--data-dir <path>` | State directory (default: `~/.agentroam`) |
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
The macOS arm64 cloudflared binary is installed from npm as a verified platform package, so startup does not download from GitHub. If Cloudflare cannot connect, automatic mode tries Pinggy over outbound SSH port 443. Pinggy free tunnels last up to 60 minutes and may show a one-time security confirmation in the phone browser. If neither public relay works, the QR code uses a LAN address and the phone must be on the same local network.
|
|
54
56
|
|
|
55
57
|
## What's included
|
|
56
58
|
|
|
57
59
|
- Next.js **standalone** web gateway + WebSocket PTY server
|
|
58
60
|
- `@agent/core` bundled runtime
|
|
59
|
-
- Optional **cloudflared**
|
|
61
|
+
- Optional platform **cloudflared** package (verified size + SHA-256 before execution)
|
|
60
62
|
- **Pairing token** + secure cookies behind HTTPS tunnel
|
|
61
63
|
|
|
62
64
|
Not included: Electron desktop app, SDK, dev `.env`, session caches.
|
|
@@ -64,11 +66,10 @@ Not included: Electron desktop app, SDK, dev `.env`, session caches.
|
|
|
64
66
|
## Release verification (maintainers)
|
|
65
67
|
|
|
66
68
|
```bash
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
node
|
|
70
|
-
node
|
|
71
|
-
node ../../scripts/verify-cli-tunnel.mjs --node /opt/homebrew/opt/node@22/bin/node
|
|
69
|
+
npm run fetch:cloudflared
|
|
70
|
+
npm run pack:cli
|
|
71
|
+
node scripts/verify-cli-install.mjs --node /path/to/node22
|
|
72
|
+
node scripts/verify-cli-tunnel.mjs --node /path/to/node22 --provider auto
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
Windows ConPTY remains a formal-release target and is not included in this preview.
|
package/dist/args.js
CHANGED
|
@@ -5,11 +5,24 @@ export function parseArgs(argv) {
|
|
|
5
5
|
let command = "start";
|
|
6
6
|
if (values[0] && ["start", "doctor", "version"].includes(values[0]))
|
|
7
7
|
command = values.shift();
|
|
8
|
-
const options = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const options = {
|
|
9
|
+
command,
|
|
10
|
+
roots: [],
|
|
11
|
+
port: null,
|
|
12
|
+
relay: "auto",
|
|
13
|
+
tunnelCommand: null,
|
|
14
|
+
localOnly: false,
|
|
15
|
+
qr: true,
|
|
16
|
+
dataDir: resolve(homedir(), ".agentroam"),
|
|
17
|
+
};
|
|
18
|
+
for (let index = 0; index < values.length; index++) {
|
|
19
|
+
const arg = values[index];
|
|
20
|
+
const next = () => {
|
|
21
|
+
const value = values[++index];
|
|
22
|
+
if (!value)
|
|
23
|
+
throw cliError(`missing value for ${arg}`);
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
13
26
|
if (arg === "--root")
|
|
14
27
|
options.roots.push(resolve(next()));
|
|
15
28
|
else if (arg === "--port") {
|
|
@@ -20,8 +33,9 @@ export function parseArgs(argv) {
|
|
|
20
33
|
}
|
|
21
34
|
else if (arg === "--relay") {
|
|
22
35
|
const relay = next();
|
|
23
|
-
if (
|
|
24
|
-
throw cliError("relay must be cloudflare or custom");
|
|
36
|
+
if (!["auto", "cloudflare", "pinggy", "custom"].includes(relay)) {
|
|
37
|
+
throw cliError("relay must be auto, cloudflare, pinggy, or custom");
|
|
38
|
+
}
|
|
25
39
|
options.relay = relay;
|
|
26
40
|
}
|
|
27
41
|
else if (arg === "--tunnel-command")
|
|
@@ -43,4 +57,6 @@ export function parseArgs(argv) {
|
|
|
43
57
|
throw cliError("--relay custom requires --tunnel-command");
|
|
44
58
|
return options;
|
|
45
59
|
}
|
|
46
|
-
function cliError(message) {
|
|
60
|
+
function cliError(message) {
|
|
61
|
+
return Object.assign(new Error(message), { exitCode: 2 });
|
|
62
|
+
}
|
package/dist/args.test.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { parseArgs } from "./args.js";
|
|
3
|
-
describe("parseArgs", () => {
|
|
3
|
+
describe("parseArgs", () => {
|
|
4
|
+
it("uses auto relay and the current directory by default", () => {
|
|
5
|
+
const value = parseArgs([]);
|
|
6
|
+
expect(value.command).toBe("start");
|
|
7
|
+
expect(value.roots).toEqual([process.cwd()]);
|
|
8
|
+
expect(value.relay).toBe("auto");
|
|
9
|
+
});
|
|
10
|
+
it("parses every supported relay", () => {
|
|
11
|
+
expect(parseArgs(["--relay", "cloudflare"]).relay).toBe("cloudflare");
|
|
12
|
+
expect(parseArgs(["--relay", "pinggy"]).relay).toBe("pinggy");
|
|
13
|
+
expect(parseArgs(["--relay", "custom", "--tunnel-command", "relay {port}"]).relay).toBe("custom");
|
|
14
|
+
});
|
|
15
|
+
it("parses repeatable roots and custom relay options", () => {
|
|
16
|
+
const value = parseArgs([
|
|
17
|
+
"start", "--root", ".", "--root", "..", "--relay", "custom", "--tunnel-command", "relay {port}", "--no-qr",
|
|
18
|
+
]);
|
|
19
|
+
expect(value.roots).toHaveLength(2);
|
|
20
|
+
expect(value.tunnelCommand).toContain("{port}");
|
|
21
|
+
expect(value.qr).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it("rejects invalid ports and relay names", () => {
|
|
24
|
+
expect(() => parseArgs(["--port", "70000"])).toThrow();
|
|
25
|
+
expect(() => parseArgs(["--relay", "unknown"])).toThrow("relay must be auto");
|
|
26
|
+
});
|
|
27
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -1,66 +1,126 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
1
3
|
import { parseArgs } from "./args.js";
|
|
2
|
-
import { detectPlatform } from "./platform.js";
|
|
3
|
-
import { RuntimeManager } from "./runtime-manager.js";
|
|
4
|
-
import { createPairingSecret } from "./pairing.js";
|
|
5
|
-
import { renderQr } from "./qr.js";
|
|
6
4
|
import { ensureCloudflared } from "./cloudflared/installer.js";
|
|
7
|
-
import { CloudflareTunnelProvider } from "./tunnel/cloudflare-provider.js";
|
|
8
|
-
import { CustomCommandTunnelProvider } from "./tunnel/custom-command-provider.js";
|
|
9
|
-
import { findLanUrl } from "./network.js";
|
|
10
5
|
import { repairNativeRuntimePermissions } from "./native-runtime.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
6
|
+
import { findLanUrl } from "./network.js";
|
|
7
|
+
import { createPairingSecret } from "./pairing.js";
|
|
8
|
+
import { detectPlatform } from "./platform.js";
|
|
9
|
+
import { renderQr } from "./qr.js";
|
|
10
|
+
import { RuntimeManager } from "./runtime-manager.js";
|
|
11
|
+
import { selectRelay } from "./tunnel/relay-orchestrator.js";
|
|
12
|
+
const VERSION = "0.2.0-preview.4";
|
|
13
|
+
export async function main(argv) {
|
|
14
|
+
const options = parseArgs(argv);
|
|
15
|
+
const target = detectPlatform();
|
|
16
|
+
if (options.command === "version") {
|
|
17
|
+
console.log(`agentroam ${VERSION}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (options.command === "doctor") {
|
|
21
|
+
await doctor(target, options.dataDir);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(`AgentRoam ${VERSION}\n✓ Node ${process.versions.node} · ${target}`);
|
|
25
|
+
const pairing = createPairingSecret();
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
let runtime = null;
|
|
28
|
+
let relay = null;
|
|
29
|
+
let closing = false;
|
|
30
|
+
const close = async () => {
|
|
31
|
+
if (closing)
|
|
32
|
+
return;
|
|
33
|
+
closing = true;
|
|
34
|
+
controller.abort();
|
|
35
|
+
await relay?.tunnel?.close().catch(() => { });
|
|
36
|
+
await runtime?.close().catch(() => { });
|
|
37
|
+
};
|
|
38
|
+
const requestClose = () => void close();
|
|
39
|
+
process.once("SIGINT", requestClose);
|
|
40
|
+
process.once("SIGTERM", requestClose);
|
|
41
|
+
try {
|
|
42
|
+
runtime = await new RuntimeManager().start(options, pairing);
|
|
43
|
+
console.log(`✓ Local server: ${runtime.localUrl}/web`);
|
|
44
|
+
const lanUrl = findLanUrl(runtime.port) ?? runtime.localUrl;
|
|
45
|
+
relay = await selectRelay({
|
|
46
|
+
cli: options,
|
|
47
|
+
target,
|
|
48
|
+
localUrl: runtime.localUrl,
|
|
49
|
+
lanUrl,
|
|
50
|
+
port: runtime.port,
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
log: (line) => process.stderr.write(`${line}\n`),
|
|
53
|
+
onAttempt: (provider) => console.log(`▲ Trying ${providerDisplayName(provider)} relay...`),
|
|
54
|
+
onFailure: (provider, message) => console.error(`⚠ ${providerDisplayName(provider)} unavailable: ${message}`),
|
|
55
|
+
});
|
|
56
|
+
if (controller.signal.aborted)
|
|
57
|
+
return;
|
|
58
|
+
if (relay.provider === "lan") {
|
|
59
|
+
if (relay.failures.length > 0)
|
|
60
|
+
console.error(` Continuing with local network access: ${relay.publicUrl}/web`);
|
|
61
|
+
else
|
|
62
|
+
console.log(`✓ Local network: ${relay.publicUrl}/web`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(`✓ ${providerDisplayName(relay.provider)} tunnel ready: ${relay.publicUrl}`);
|
|
66
|
+
}
|
|
67
|
+
if (relay.provider === "pinggy") {
|
|
68
|
+
console.log(" Pinggy free tunnels expire after 60 minutes and use a new hostname each session.");
|
|
69
|
+
console.log(" The phone browser may show a one-time Pinggy security confirmation before pairing.");
|
|
24
70
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
71
|
+
const accessUrl = `${relay.publicUrl}/web${runtime.needsSetup ? `?pair=${encodeURIComponent(pairing.token)}` : ""}`;
|
|
72
|
+
console.log(`\nOpen: ${accessUrl}`);
|
|
73
|
+
if (options.qr)
|
|
74
|
+
console.log(`\n${await renderQr(accessUrl)}`);
|
|
75
|
+
if (runtime.needsSetup)
|
|
76
|
+
console.log("First pairing link expires in 5 minutes.");
|
|
77
|
+
console.log("Ctrl+C stops the tunnel and local server.");
|
|
78
|
+
await Promise.race([runtime.exited, relay.tunnel?.exited ?? new Promise(() => { })]);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
process.removeListener("SIGINT", requestClose);
|
|
82
|
+
process.removeListener("SIGTERM", requestClose);
|
|
83
|
+
await close();
|
|
29
84
|
}
|
|
30
85
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} const accessUrl = `${publicUrl}/web${runtime.needsSetup ? `?pair=${encodeURIComponent(pairing.token)}` : ""}`; console.log(`\nOpen: ${accessUrl}`); if (options.qr)
|
|
37
|
-
console.log(`\n${await renderQr(accessUrl)}`); if (runtime.needsSetup)
|
|
38
|
-
console.log("First pairing link expires in 5 minutes."); console.log("Ctrl+C stops the tunnel and local server."); let closing = false; const close = async () => { if (closing)
|
|
39
|
-
return; closing = true; controller.abort(); await tunnel?.close().catch(() => { }); await runtime.close(); }; process.once("SIGINT", () => void close()); process.once("SIGTERM", () => void close()); await Promise.race([runtime.exited, tunnel?.exited ?? new Promise(() => { })]); await close(); }
|
|
40
|
-
async function doctor(target, dataDir) { console.log(`✓ Node ${process.versions.node}`); console.log(`✓ Platform ${target}`); const runtimeRoot = fileURLToPath(new URL("../runtime/", import.meta.url)); const runtimeRequire = createRequire(fileURLToPath(new URL("../runtime/package.json", import.meta.url))); try {
|
|
41
|
-
await repairNativeRuntimePermissions(runtimeRoot);
|
|
42
|
-
console.log("✓ node-pty spawn-helper executable");
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
console.log(`✗ ${error instanceof Error ? error.message : error}`);
|
|
46
|
-
process.exitCode = 1;
|
|
47
|
-
} for (const module of ["node-pty", "better-sqlite3"]) {
|
|
86
|
+
async function doctor(target, dataDir) {
|
|
87
|
+
console.log(`✓ Node ${process.versions.node}`);
|
|
88
|
+
console.log(`✓ Platform ${target}`);
|
|
89
|
+
const runtimeRoot = fileURLToPath(new URL("../runtime/", import.meta.url));
|
|
90
|
+
const runtimeRequire = createRequire(fileURLToPath(new URL("../runtime/package.json", import.meta.url)));
|
|
48
91
|
try {
|
|
49
|
-
|
|
50
|
-
console.log(
|
|
92
|
+
await repairNativeRuntimePermissions(runtimeRoot);
|
|
93
|
+
console.log("✓ node-pty spawn-helper executable");
|
|
51
94
|
}
|
|
52
95
|
catch (error) {
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
reportDoctorFailure(error);
|
|
97
|
+
}
|
|
98
|
+
for (const module of ["node-pty", "better-sqlite3"]) {
|
|
99
|
+
try {
|
|
100
|
+
runtimeRequire(module);
|
|
101
|
+
console.log(`✓ ${module}`);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
reportDoctorFailure(error, `${module}: `);
|
|
105
|
+
}
|
|
55
106
|
}
|
|
56
|
-
} console.log(`✓ Data directory ${dataDir}`); }
|
|
57
|
-
async function retry(fn, count) { let last; for (let attempt = 1; attempt <= count; attempt++) {
|
|
58
107
|
try {
|
|
59
|
-
|
|
108
|
+
const executable = await ensureCloudflared(target, dataDir);
|
|
109
|
+
console.log(`✓ cloudflared ${executable}`);
|
|
60
110
|
}
|
|
61
111
|
catch (error) {
|
|
62
|
-
|
|
63
|
-
if (attempt < count)
|
|
64
|
-
await new Promise(resolve => setTimeout(resolve, attempt * 750));
|
|
112
|
+
reportDoctorFailure(error, "cloudflared: ");
|
|
65
113
|
}
|
|
66
|
-
|
|
114
|
+
console.log(`✓ Data directory ${dataDir}`);
|
|
115
|
+
}
|
|
116
|
+
function reportDoctorFailure(error, prefix = "") {
|
|
117
|
+
console.log(`✗ ${prefix}${error instanceof Error ? error.message : error}`);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
}
|
|
120
|
+
function providerDisplayName(provider) {
|
|
121
|
+
if (provider === "cloudflare")
|
|
122
|
+
return "Cloudflare";
|
|
123
|
+
if (provider === "pinggy")
|
|
124
|
+
return "Pinggy";
|
|
125
|
+
return "Custom";
|
|
126
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { CLOUDFLARED_PACKAGES, CLOUDFLARED_PACKAGE_VERSION } from "./manifest.js";
|
|
4
|
+
export async function resolveBundledCloudflared(target, requireFromCli = createRequire(import.meta.url)) {
|
|
5
|
+
const packageName = CLOUDFLARED_PACKAGES[target];
|
|
6
|
+
if (!packageName)
|
|
7
|
+
return null;
|
|
8
|
+
let manifestPath;
|
|
9
|
+
let archivePath;
|
|
10
|
+
try {
|
|
11
|
+
manifestPath = requireFromCli.resolve(`${packageName}/manifest.json`);
|
|
12
|
+
archivePath = requireFromCli.resolve(`${packageName}/archive`);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error?.code === "MODULE_NOT_FOUND")
|
|
16
|
+
return null;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
20
|
+
if (manifest.packageVersion !== CLOUDFLARED_PACKAGE_VERSION ||
|
|
21
|
+
manifest.target !== target ||
|
|
22
|
+
typeof manifest.upstreamVersion !== "string" ||
|
|
23
|
+
typeof manifest.fileName !== "string" ||
|
|
24
|
+
typeof manifest.size !== "number" ||
|
|
25
|
+
!Number.isSafeInteger(manifest.size) ||
|
|
26
|
+
typeof manifest.sha256 !== "string" ||
|
|
27
|
+
!/^[a-f0-9]{64}$/.test(manifest.sha256)) {
|
|
28
|
+
throw new Error(`invalid cloudflared platform manifest: ${manifestPath}`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
archivePath,
|
|
32
|
+
sha256: manifest.sha256,
|
|
33
|
+
size: manifest.size,
|
|
34
|
+
fileName: manifest.fileName,
|
|
35
|
+
version: manifest.upstreamVersion,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } 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 { resolveBundledCloudflared } from "./bundled-asset.js";
|
|
6
|
+
describe("resolveBundledCloudflared", () => {
|
|
7
|
+
it("resolves a valid platform package manifest and archive", async () => {
|
|
8
|
+
const root = await mkdtemp(resolve(tmpdir(), "agentroam-bundled-resolver-"));
|
|
9
|
+
const manifestPath = resolve(root, "manifest.json");
|
|
10
|
+
const archivePath = resolve(root, "cloudflared.tgz");
|
|
11
|
+
await writeFile(archivePath, "archive");
|
|
12
|
+
await writeFile(manifestPath, JSON.stringify({
|
|
13
|
+
packageVersion: "0.2.0-preview.4",
|
|
14
|
+
upstreamVersion: "2026.8.2",
|
|
15
|
+
target: "darwin-arm64",
|
|
16
|
+
fileName: "cloudflared",
|
|
17
|
+
size: 7,
|
|
18
|
+
sha256: "a".repeat(64),
|
|
19
|
+
}));
|
|
20
|
+
const asset = await resolveBundledCloudflared("darwin-arm64", fakeRequire(manifestPath, archivePath));
|
|
21
|
+
expect(asset).toEqual({
|
|
22
|
+
archivePath,
|
|
23
|
+
version: "2026.8.2",
|
|
24
|
+
fileName: "cloudflared",
|
|
25
|
+
size: 7,
|
|
26
|
+
sha256: "a".repeat(64),
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it("rejects a mismatched platform package version", async () => {
|
|
30
|
+
const root = await mkdtemp(resolve(tmpdir(), "agentroam-bundled-version-"));
|
|
31
|
+
const manifestPath = resolve(root, "manifest.json");
|
|
32
|
+
const archivePath = resolve(root, "cloudflared.tgz");
|
|
33
|
+
await writeFile(archivePath, "archive");
|
|
34
|
+
await writeFile(manifestPath, JSON.stringify({
|
|
35
|
+
packageVersion: "0.2.0-preview.2",
|
|
36
|
+
upstreamVersion: "2026.8.2",
|
|
37
|
+
target: "darwin-arm64",
|
|
38
|
+
fileName: "cloudflared",
|
|
39
|
+
size: 7,
|
|
40
|
+
sha256: "a".repeat(64),
|
|
41
|
+
}));
|
|
42
|
+
await expect(resolveBundledCloudflared("darwin-arm64", fakeRequire(manifestPath, archivePath)))
|
|
43
|
+
.rejects.toThrow("invalid cloudflared platform manifest");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
function fakeRequire(manifestPath, archivePath) {
|
|
47
|
+
return {
|
|
48
|
+
resolve: (id) => id.endsWith("/manifest.json") ? manifestPath : archivePath,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -1,48 +1,91 @@
|
|
|
1
|
-
import { chmod, mkdir, readFile, rename, rm, stat, writeFile, access } from "node:fs/promises";
|
|
2
|
-
import { constants, createWriteStream } from "node:fs";
|
|
3
|
-
import { resolve, delimiter } from "node:path";
|
|
4
|
-
import { pipeline } from "node:stream/promises";
|
|
5
1
|
import { spawn } from "node:child_process";
|
|
6
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { delimiter, resolve } from "node:path";
|
|
5
|
+
import { resolveBundledCloudflared } from "./bundled-asset.js";
|
|
7
6
|
import { sha256File } from "./checksum.js";
|
|
8
|
-
export async function ensureCloudflared(target, dataDir
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export async function ensureCloudflared(target, dataDir, options = {}) {
|
|
8
|
+
const binDir = resolve(dataDir, "bin");
|
|
9
|
+
const finalPath = resolve(binDir, process.platform === "win32" ? "cloudflared.exe" : "cloudflared");
|
|
10
|
+
const marker = `${finalPath}.source.sha256`;
|
|
11
|
+
await mkdir(binDir, { recursive: true });
|
|
12
|
+
const resolveBundled = options.resolveBundled ?? resolveBundledCloudflared;
|
|
13
|
+
const findSystem = options.findSystem ?? findSystemCloudflared;
|
|
14
|
+
const failures = [];
|
|
15
|
+
try {
|
|
16
|
+
const bundled = await resolveBundled(target);
|
|
17
|
+
if (bundled && (await cached(finalPath, marker, bundled.sha256)))
|
|
18
|
+
return finalPath;
|
|
19
|
+
if (bundled)
|
|
20
|
+
return await installBundledCloudflared(bundled, binDir, finalPath, marker);
|
|
21
|
+
failures.push(`platform package missing for ${target}`);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
failures.push(describeError(error));
|
|
25
|
+
}
|
|
26
|
+
const systemBinary = await findSystem();
|
|
27
|
+
if (systemBinary)
|
|
28
|
+
return systemBinary;
|
|
29
|
+
throw new Error(`cloudflared unavailable: ${failures.join("; ")}; no executable on PATH`);
|
|
30
|
+
}
|
|
31
|
+
export async function findSystemCloudflared(pathValue = process.env.PATH ?? "", platform = process.platform) {
|
|
32
|
+
const name = platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
33
|
+
for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
34
|
+
const candidate = resolve(directory, name);
|
|
35
|
+
try {
|
|
36
|
+
await access(candidate, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
12
42
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
await
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
await
|
|
43
|
+
async function installBundledCloudflared(asset, binDir, finalPath, marker) {
|
|
44
|
+
const archiveInfo = await stat(asset.archivePath);
|
|
45
|
+
if (!archiveInfo.isFile() || archiveInfo.size !== asset.size)
|
|
46
|
+
throw new Error("bundled cloudflared size mismatch");
|
|
47
|
+
if ((await sha256File(asset.archivePath)) !== asset.sha256)
|
|
48
|
+
throw new Error("bundled cloudflared checksum mismatch");
|
|
49
|
+
const temporaryDir = resolve(binDir, `.cloudflared-${process.pid}-${Date.now()}`);
|
|
50
|
+
const markerTemp = `${marker}.${process.pid}.tmp`;
|
|
51
|
+
await mkdir(temporaryDir, { recursive: true });
|
|
52
|
+
try {
|
|
53
|
+
await extractTgz(asset.archivePath, temporaryDir);
|
|
54
|
+
const entries = await readdir(temporaryDir);
|
|
55
|
+
if (entries.length !== 1 || entries[0] !== asset.fileName) {
|
|
56
|
+
throw new Error(`unexpected bundled cloudflared contents: ${entries.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
const extracted = resolve(temporaryDir, asset.fileName);
|
|
59
|
+
if (!(await stat(extracted)).isFile())
|
|
60
|
+
throw new Error("bundled cloudflared executable missing");
|
|
61
|
+
if (process.platform !== "win32")
|
|
62
|
+
await chmod(extracted, 0o755);
|
|
63
|
+
await rename(extracted, finalPath);
|
|
64
|
+
await writeFile(markerTemp, `${asset.sha256}\n`);
|
|
65
|
+
await rename(markerTemp, marker);
|
|
66
|
+
return finalPath;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(temporaryDir, { recursive: true, force: true });
|
|
70
|
+
await rm(markerTemp, { force: true });
|
|
71
|
+
}
|
|
22
72
|
}
|
|
23
|
-
|
|
24
|
-
await rename(temp, finalPath); if (process.platform !== "win32")
|
|
25
|
-
await chmod(finalPath, 0o755); if (!await exists(finalPath))
|
|
26
|
-
throw new Error("cloudflared binary missing after install"); await writeFile(marker, asset.sha256); return finalPath; }
|
|
27
|
-
export async function findSystemCloudflared(pathValue = process.env.PATH ?? "", platform = process.platform) { const name = platform === "win32" ? "cloudflared.exe" : "cloudflared"; for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
28
|
-
const candidate = resolve(directory, name);
|
|
73
|
+
async function cached(path, marker, hash) {
|
|
29
74
|
try {
|
|
30
|
-
await
|
|
31
|
-
return candidate;
|
|
75
|
+
return (await stat(path)).isFile() && (await readFile(marker, "utf8")).trim() === hash;
|
|
32
76
|
}
|
|
33
|
-
catch {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function describeError(error) {
|
|
82
|
+
const value = error;
|
|
83
|
+
return [value.cause?.code, value.message, value.cause?.message].filter(Boolean).join(": ") || String(error);
|
|
37
84
|
}
|
|
38
|
-
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
85
|
+
function extractTgz(archive, dir) {
|
|
86
|
+
return new Promise((resolvePromise, reject) => {
|
|
87
|
+
const child = spawn("tar", ["-xzf", archive, "-C", dir], { stdio: "ignore" });
|
|
88
|
+
child.once("exit", (code) => (code === 0 ? resolvePromise() : reject(new Error(`tar exited ${code}`))));
|
|
89
|
+
child.once("error", reject);
|
|
90
|
+
});
|
|
43
91
|
}
|
|
44
|
-
catch {
|
|
45
|
-
return false;
|
|
46
|
-
} }
|
|
47
|
-
function describeError(error) { const value = error; const cause = value?.cause; return [cause?.code, value?.message, cause?.message].filter(Boolean).join(": ") || String(error); }
|
|
48
|
-
function extractTgz(archive, dir) { return new Promise((resolve, reject) => { const child = spawn("tar", ["-xzf", archive, "-C", dir], { stdio: "ignore" }); child.once("exit", code => code === 0 ? resolve() : reject(new Error(`tar exited ${code}`))); child.once("error", reject); }); }
|
|
@@ -1,11 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { access, chmod, mkdtemp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
5
|
import { tmpdir } from "node:os";
|
|
3
6
|
import { resolve } from "node:path";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { findSystemCloudflared } from "./installer.js";
|
|
7
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { ensureCloudflared, findSystemCloudflared } from "./installer.js";
|
|
9
|
+
describe("ensureCloudflared", () => {
|
|
10
|
+
it("extracts a verified bundled archive and repairs executable permissions", async () => {
|
|
11
|
+
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-bundle-"));
|
|
12
|
+
const asset = await createArchive(root, "#!/bin/sh\necho bundled\n");
|
|
13
|
+
const executable = await ensureCloudflared("darwin-arm64", resolve(root, "data"), {
|
|
14
|
+
resolveBundled: async () => asset,
|
|
15
|
+
findSystem: async () => null,
|
|
16
|
+
});
|
|
17
|
+
expect(await readFile(executable, "utf8")).toContain("bundled");
|
|
18
|
+
await access(executable, constants.X_OK);
|
|
19
|
+
expect(await readFile(`${executable}.source.sha256`, "utf8")).toBe(`${asset.sha256}\n`);
|
|
20
|
+
});
|
|
21
|
+
it("reuses the verified cache", async () => {
|
|
22
|
+
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-cache-"));
|
|
23
|
+
const asset = await createArchive(root, "#!/bin/sh\n");
|
|
24
|
+
const resolver = vi.fn(async () => asset);
|
|
25
|
+
const options = { resolveBundled: resolver, findSystem: async () => null };
|
|
26
|
+
const first = await ensureCloudflared("darwin-arm64", resolve(root, "data"), options);
|
|
27
|
+
const second = await ensureCloudflared("darwin-arm64", resolve(root, "data"), options);
|
|
28
|
+
expect(second).toBe(first);
|
|
29
|
+
expect(resolver).toHaveBeenCalledTimes(2);
|
|
30
|
+
});
|
|
31
|
+
it("falls back to PATH when the bundled checksum is invalid", async () => {
|
|
32
|
+
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-fallback-"));
|
|
33
|
+
const asset = await createArchive(root, "#!/bin/sh\n");
|
|
34
|
+
const system = resolve(root, "system-cloudflared");
|
|
35
|
+
await writeFile(system, "#!/bin/sh\n", { mode: 0o755 });
|
|
36
|
+
const result = await ensureCloudflared("darwin-arm64", resolve(root, "data"), {
|
|
37
|
+
resolveBundled: async () => ({ ...asset, sha256: "0".repeat(64) }),
|
|
38
|
+
findSystem: async () => system,
|
|
39
|
+
});
|
|
40
|
+
expect(result).toBe(system);
|
|
41
|
+
});
|
|
42
|
+
it("reports package and PATH failures without downloading", async () => {
|
|
43
|
+
const root = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-missing-"));
|
|
44
|
+
await expect(ensureCloudflared("darwin-arm64", resolve(root, "data"), {
|
|
45
|
+
resolveBundled: async () => null,
|
|
46
|
+
findSystem: async () => null,
|
|
47
|
+
})).rejects.toThrow("platform package missing for darwin-arm64; no executable on PATH");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
6
50
|
describe("findSystemCloudflared", () => {
|
|
7
51
|
it("reuses an executable cloudflared already installed on PATH", async () => {
|
|
8
|
-
const directory = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-"));
|
|
52
|
+
const directory = await mkdtemp(resolve(tmpdir(), "agentroam-cloudflared-path-"));
|
|
9
53
|
const executable = resolve(directory, "cloudflared");
|
|
10
54
|
await writeFile(executable, "#!/bin/sh\n", { mode: 0o644 });
|
|
11
55
|
expect(await findSystemCloudflared(directory, "darwin")).toBeNull();
|
|
@@ -13,3 +57,18 @@ describe("findSystemCloudflared", () => {
|
|
|
13
57
|
expect(await findSystemCloudflared(directory, "darwin")).toBe(executable);
|
|
14
58
|
});
|
|
15
59
|
});
|
|
60
|
+
async function createArchive(root, content) {
|
|
61
|
+
const source = resolve(root, "source");
|
|
62
|
+
await mkdir(source, { recursive: true });
|
|
63
|
+
await writeFile(resolve(source, "cloudflared"), content, { mode: 0o644 });
|
|
64
|
+
const archivePath = resolve(root, "cloudflared.tgz");
|
|
65
|
+
execFileSync("tar", ["-czf", archivePath, "-C", source, "cloudflared"]);
|
|
66
|
+
const bytes = await readFile(archivePath);
|
|
67
|
+
return {
|
|
68
|
+
archivePath,
|
|
69
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
70
|
+
size: (await stat(archivePath)).size,
|
|
71
|
+
fileName: "cloudflared",
|
|
72
|
+
version: "test",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export const CLOUDFLARED_VERSION = "2026.8.2";
|
|
2
|
-
const
|
|
3
|
-
export const
|
|
4
|
-
"darwin-arm64":
|
|
5
|
-
"darwin-amd64": { url: `${base}/cloudflared-darwin-amd64.tgz`, sha256: "f1727723c586500e2092368ae21871b3df7ddfd2cb097f22d81bee4a9c458bb4", size: 21116242, archive: true, fileName: "cloudflared" },
|
|
6
|
-
"windows-amd64": { url: `${base}/cloudflared-windows-amd64.exe`, sha256: "c29eee2b121f5436a642eed69fd9767da7e7b8c510fa50aaa130337f931357b5", size: 54893480, archive: false, fileName: "cloudflared.exe" }
|
|
2
|
+
export const CLOUDFLARED_PACKAGE_VERSION = "0.2.0-preview.4";
|
|
3
|
+
export const CLOUDFLARED_PACKAGES = {
|
|
4
|
+
"darwin-arm64": "agentroam-cloudflared-darwin-arm64",
|
|
7
5
|
};
|