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
package/README.md
CHANGED
|
@@ -17,23 +17,24 @@ on a real Windows runner before release.
|
|
|
17
17
|
npx agentroam@preview
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Or install from
|
|
20
|
+
Or install from the release tarballs. `<version>` is per-file: platform packages
|
|
21
|
+
may each reuse an older version, so use the actual file names from `dist/cli-release`.
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
npm install ./agentroam-cloudflared-darwin-arm64
|
|
24
|
-
./agentroam-runtime-darwin-arm64
|
|
25
|
-
./agentroam-tui-darwin-arm64
|
|
26
|
-
./agentroam
|
|
24
|
+
npm install ./agentroam-cloudflared-darwin-arm64-<version>.tgz \
|
|
25
|
+
./agentroam-runtime-darwin-arm64-<version>.tgz \
|
|
26
|
+
./agentroam-tui-darwin-arm64-<version>.tgz \
|
|
27
|
+
./agentroam-<version>.tgz
|
|
27
28
|
npx agentroam
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
Windows PowerShell:
|
|
31
32
|
|
|
32
33
|
```powershell
|
|
33
|
-
npm install ./agentroam-cloudflared-win32-x64
|
|
34
|
-
./agentroam-runtime-win32-x64
|
|
35
|
-
./caoqu-agentroam-tui-win32-x64
|
|
36
|
-
./agentroam
|
|
34
|
+
npm install ./agentroam-cloudflared-win32-x64-<version>.tgz `
|
|
35
|
+
./agentroam-runtime-win32-x64-<version>.tgz `
|
|
36
|
+
./caoqu-agentroam-tui-win32-x64-<version>.tgz `
|
|
37
|
+
./agentroam-<version>.tgz
|
|
37
38
|
npx agentroam
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -55,7 +56,7 @@ agent-tui # start the bundled terminal Agent UI
|
|
|
55
56
|
|
|
56
57
|
## Install and run in the background
|
|
57
58
|
|
|
58
|
-
Run the
|
|
59
|
+
Run the Shell installer on macOS or the PowerShell installer on Windows
|
|
59
60
|
from the project directory AgentRoam should expose. The installer verifies or
|
|
60
61
|
installs a compatible Node and the matching CLI, registers the current-user service, starts
|
|
61
62
|
it, checks readiness, and prints the access URL. Set `AGENTROAM_ROOT` to select a
|
|
@@ -71,7 +72,7 @@ versions are never rejected or downgraded solely because of their major version.
|
|
|
71
72
|
The service keeps running after the terminal closes, restarts after failure, and
|
|
72
73
|
starts again when the user logs in. macOS uses a user LaunchAgent; Windows uses a
|
|
73
74
|
current-user Task Scheduler task. Neither requires administrator privileges.
|
|
74
|
-
|
|
75
|
+
Re-running the installer automatically stops and replaces an existing
|
|
75
76
|
AgentRoam user service before starting the new version. Session data, pairing
|
|
76
77
|
state, managed runtimes, and service logs are preserved during the upgrade.
|
|
77
78
|
|
|
@@ -88,7 +89,7 @@ agentroam service uninstall
|
|
|
88
89
|
The current phone URL is stored privately at `~/.agentroam/tunnel.url`.
|
|
89
90
|
Cloudflared keeps the same Quick Tunnel hostname while its process reconnects;
|
|
90
91
|
a full service or computer restart creates a new random hostname. Re-run the
|
|
91
|
-
|
|
92
|
+
installer after moving or replacing Node/npm so the service receives
|
|
92
93
|
the new absolute executable paths.
|
|
93
94
|
|
|
94
95
|
Both foreground `agentroam start` and the background service prevent idle system
|
package/bin/desktop-download.mjs
CHANGED
|
@@ -9,7 +9,10 @@ import { Transform, Readable } from 'node:stream';
|
|
|
9
9
|
import { pipeline } from 'node:stream/promises';
|
|
10
10
|
import { execFile } from 'node:child_process';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// 桌面端安装包是构建产物(dmg / exe),进不了只放源码的仓库,只能走 Releases 资产。
|
|
13
|
+
// 资产由 `node scripts/publish-agentroam-release.mjs sync-github --commit <sha>` 上传;
|
|
14
|
+
// 仓库还没有对应版本的 release 时这里必然取不到包(CLI 本体不受影响,它走 raw 安装脚本)。
|
|
15
|
+
const RELEASE_BASE = 'https://github.com/tclxqu-cao/team-agent/releases/download';
|
|
13
16
|
|
|
14
17
|
export function desktopAsset(manifest, version, platform, arch) {
|
|
15
18
|
if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-preview\.(0|[1-9]\d*))?$/.test(version)) throw new Error('Invalid AgentRoam version');
|
package/bin/node-preflight.mjs
CHANGED
|
@@ -7,8 +7,9 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { MINIMUM_NODE_VERSION, compareNodeVersions, isSupportedNodeVersion, parseNodeVersion } from "./runtime-policy.mjs";
|
|
8
8
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// 安装引导地址:指向仓库主分支上的安装脚本(raw 路径)。
|
|
11
|
+
// 不走 release 产物 —— 避免依赖「发布流程是否产出 asset」,用户拿到的始终是最新安装器。
|
|
12
|
+
const INSTALL_BASE_URL = "https://github.com/tclxqu-cao/team-agent/raw/main/packages/cli/install";
|
|
12
13
|
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
13
14
|
|
|
14
15
|
export async function runNodePreflight(options = {}) {
|
|
@@ -160,9 +161,9 @@ function waitForChild(childPromise, processHost) {
|
|
|
160
161
|
|
|
161
162
|
function standaloneInstallerMessage(platform) {
|
|
162
163
|
if (platform === "win32") {
|
|
163
|
-
return `Node.js 18+ is required for automatic bootstrap. Run: irm ${
|
|
164
|
+
return `Node.js 18+ is required for automatic bootstrap. Run: irm ${INSTALL_BASE_URL}/install-agentroam.ps1 | iex`;
|
|
164
165
|
}
|
|
165
|
-
return `Node.js 18+ is required for automatic bootstrap. Run: curl -fsSL ${
|
|
166
|
+
return `Node.js 18+ is required for automatic bootstrap. Run: curl -fsSL ${INSTALL_BASE_URL}/install-agentroam.sh | sh`;
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
function cliError(message) {
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { printLocalDesktopUrl } from './local-desktop-url.js';
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { parseArgs } from "./args.js";
|
|
5
6
|
import { ensureCloudflared } from "./cloudflared/installer.js";
|
|
6
7
|
import { resolveCodexRuntime } from "./codex-runtime-manager.js";
|
|
@@ -19,6 +20,7 @@ import { currentCliPath, startUpdate } from "./update/update-command.js";
|
|
|
19
20
|
import { runUpdateWorker } from "./update/update-worker.js";
|
|
20
21
|
import { lockInstanceStartup, stopPreviousInstances } from "./instance-takeover.js";
|
|
21
22
|
import { runUnlockServiceCommand } from "./unlock-service.js";
|
|
23
|
+
import { createCliErrorLog } from "./error-log.js";
|
|
22
24
|
const VERSION = AGENTROAM_VERSION;
|
|
23
25
|
export async function main(argv) {
|
|
24
26
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -41,6 +43,11 @@ export async function main(argv) {
|
|
|
41
43
|
return;
|
|
42
44
|
}
|
|
43
45
|
const options = parseArgs(argv);
|
|
46
|
+
// CLI 进程自身的按天错误日志:<dataDir>/logs/YYYY-MM-DD.log(NDJSON,与
|
|
47
|
+
// server/desktop 的 core logger 同格式)。uncaughtException 记录后按默认
|
|
48
|
+
// 语义退出(1),unhandledRejection 只记录。
|
|
49
|
+
const cliErrorLog = createCliErrorLog(join(options.dataDir, "logs"));
|
|
50
|
+
cliErrorLog.installGlobalErrorHandlers();
|
|
44
51
|
if (["pair", "devices", "revoke", "approvals", "approve", "deny", "lock", "unlock", "audit"].includes(options.command)) {
|
|
45
52
|
const dataDir = argv.includes("--data-dir") ? options.dataDir : await installedDataDir(options.dataDir);
|
|
46
53
|
if (options.command === "pair")
|
package/dist/device-pairing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { createReadStream, openSync } from "node:fs";
|
|
3
4
|
import { readdir, readFile } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { join, resolve } from "node:path";
|
|
@@ -67,12 +68,44 @@ export async function printPairingCode(dataDir, log = console.log, options = {})
|
|
|
67
68
|
else
|
|
68
69
|
log("生成 App 授权二维码:agentroam pair --url <手机可访问的服务器地址>");
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const approvalInput = resolveApprovalInput(options);
|
|
72
|
+
if (approvalInput) {
|
|
73
|
+
await watchPairingApproval(dataDir, result.expiresAt, { log, signal: options.signal, input: approvalInput });
|
|
72
74
|
}
|
|
73
75
|
else
|
|
74
76
|
log("查看请求:agentroam approvals;批准:agentroam approve <请求ID> --phrase <手机核对短语>");
|
|
75
77
|
}
|
|
78
|
+
/** Open the controlling terminal (`/dev/tty`) — the same source install-agentroam.sh reads from. */
|
|
79
|
+
function openControllingTerminal() {
|
|
80
|
+
if (process.platform === "win32")
|
|
81
|
+
return undefined;
|
|
82
|
+
let fd;
|
|
83
|
+
try {
|
|
84
|
+
fd = openSync("/dev/tty", "r");
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return createReadStream("/dev/tty", { fd, autoClose: true });
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Pick the stream the approval phrase is read from.
|
|
93
|
+
*
|
|
94
|
+
* Never fall back to `process.stdin` when it is not a TTY: the installer runs as
|
|
95
|
+
* `curl … | sh`, so stdin is *the install script itself*. Reading it consumes
|
|
96
|
+
* the rest of the install, echoes it back to the terminal, and answers the
|
|
97
|
+
* prompt with the next script line instead of the user's "yes" — which silently
|
|
98
|
+
* rejects the device and swallows the remaining install steps.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveApprovalInput(context = {}) {
|
|
101
|
+
const stdinIsTTY = context.stdinIsTTY ?? process.stdin.isTTY === true;
|
|
102
|
+
const stdoutIsTTY = context.stdoutIsTTY ?? process.stdout.isTTY === true;
|
|
103
|
+
if (!(context.interactive ?? (stdinIsTTY && stdoutIsTTY)))
|
|
104
|
+
return undefined;
|
|
105
|
+
if (stdinIsTTY)
|
|
106
|
+
return process.stdin;
|
|
107
|
+
return (context.openTerminal ?? openControllingTerminal)();
|
|
108
|
+
}
|
|
76
109
|
/** Watch only while the printed code is usable; approval gets the request's own deadline. */
|
|
77
110
|
export async function watchPairingApproval(dataDir, codeExpiresAt, options = {}) {
|
|
78
111
|
const admin = options.admin ?? pairingAdmin;
|
|
@@ -93,7 +126,8 @@ export async function watchPairingApproval(dataDir, codeExpiresAt, options = {})
|
|
|
93
126
|
log(`新设备请求连接:${terminalText(request.name)}`);
|
|
94
127
|
log(`请求 ID:${request.id}`);
|
|
95
128
|
log(`核对短语:${request.phrase}`);
|
|
96
|
-
const
|
|
129
|
+
const confirm = options.confirm ?? ((request, signal) => confirmOnTerminal(request, signal, options.input));
|
|
130
|
+
const approved = await confirm(request, options.signal);
|
|
97
131
|
if (options.signal?.aborted)
|
|
98
132
|
return;
|
|
99
133
|
if (Date.now() >= request.expires) {
|
|
@@ -114,10 +148,17 @@ export async function watchPairingApproval(dataDir, codeExpiresAt, options = {})
|
|
|
114
148
|
if (!options.signal?.aborted)
|
|
115
149
|
log("配对码已过期,执行 agentroam pair 生成新码。");
|
|
116
150
|
}
|
|
117
|
-
async function confirmOnTerminal(request, signal) {
|
|
151
|
+
async function confirmOnTerminal(request, signal, terminal = resolveApprovalInput()) {
|
|
152
|
+
// A piped stdin is the install script, and a closed one can never answer.
|
|
153
|
+
if (!terminal || (terminal === process.stdin && process.stdin.isTTY !== true))
|
|
154
|
+
return false;
|
|
118
155
|
const controller = new AbortController();
|
|
119
156
|
const timer = setTimeout(() => controller.abort(), Math.max(0, request.expires - Date.now()));
|
|
120
|
-
|
|
157
|
+
// Read the terminal in canonical mode: the tty echoes the keystrokes itself,
|
|
158
|
+
// so letting readline echo them again would double every character.
|
|
159
|
+
const input = createInterface(terminal === process.stdin
|
|
160
|
+
? { input: terminal, output: process.stdout }
|
|
161
|
+
: { input: terminal, output: process.stdout, terminal: false });
|
|
121
162
|
input.once("SIGINT", () => controller.abort());
|
|
122
163
|
try {
|
|
123
164
|
const answer = await input.question("确认手机显示相同短语?输入 yes 批准,其他输入拒绝:", { signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal });
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// Self-contained daily error log for the CLI process. The CLI cannot depend
|
|
4
|
+
// on @agent/core (it ships as a thin launcher), so this mirrors the exact
|
|
5
|
+
// NDJSON entry format of core's DailyFileLogger
|
|
6
|
+
// (`{ts, level, source, message, error?, data?}`) into `<dataDir>/logs/`.
|
|
7
|
+
// It records uncaughtException / unhandledRejection so launcher failures are
|
|
8
|
+
// strictly persisted, matching the server/desktop log series.
|
|
9
|
+
const LEVELS = new Set(["debug", "info", "warn", "error", "fatal"]);
|
|
10
|
+
function localDateString(date) {
|
|
11
|
+
const y = date.getFullYear();
|
|
12
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
13
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
14
|
+
return `${y}-${m}-${d}`;
|
|
15
|
+
}
|
|
16
|
+
function serializeError(err) {
|
|
17
|
+
if (err === undefined || err === null)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (err instanceof Error) {
|
|
20
|
+
const out = { name: err.name, message: err.message };
|
|
21
|
+
if (err.stack)
|
|
22
|
+
out.stack = err.stack;
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
if (typeof err === "object") {
|
|
26
|
+
const anyErr = err;
|
|
27
|
+
const out = {
|
|
28
|
+
name: typeof anyErr.name === "string" ? anyErr.name : "NonError",
|
|
29
|
+
message: typeof anyErr.message === "string" ? anyErr.message : String(err),
|
|
30
|
+
};
|
|
31
|
+
if (typeof anyErr.stack === "string")
|
|
32
|
+
out.stack = anyErr.stack;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
return { name: "NonError", message: String(err) };
|
|
36
|
+
}
|
|
37
|
+
function safeStringify(value) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(value) ?? "null";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return '"[unserializable]"';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function createCliErrorLog(logsDir) {
|
|
46
|
+
let reportedWriteFailure = false;
|
|
47
|
+
const write = (line) => {
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(logsDir))
|
|
50
|
+
mkdirSync(logsDir, { recursive: true });
|
|
51
|
+
appendFileSync(join(logsDir, `${localDateString(new Date())}.log`), line, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
if (!reportedWriteFailure) {
|
|
55
|
+
reportedWriteFailure = true;
|
|
56
|
+
try {
|
|
57
|
+
process.stderr.write(`[agentroam] failed to write error log to ${logsDir}; further failures are silent\n`);
|
|
58
|
+
}
|
|
59
|
+
catch { /* stderr gone */ }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const cleanupOldFiles = () => {
|
|
64
|
+
// 30-day retention, aligned with the core logger default. Best effort.
|
|
65
|
+
try {
|
|
66
|
+
if (!existsSync(logsDir))
|
|
67
|
+
return;
|
|
68
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
69
|
+
for (const name of readdirSync(logsDir)) {
|
|
70
|
+
if (!/^\d{4}-\d{2}-\d{2}\.log$/.test(name))
|
|
71
|
+
continue;
|
|
72
|
+
const full = join(logsDir, name);
|
|
73
|
+
try {
|
|
74
|
+
if (statSync(full).mtimeMs < cutoff)
|
|
75
|
+
unlinkSync(full);
|
|
76
|
+
}
|
|
77
|
+
catch { /* concurrent removal */ }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* best effort */ }
|
|
81
|
+
};
|
|
82
|
+
const log = (level, message, error, data) => {
|
|
83
|
+
if (!LEVELS.has(level))
|
|
84
|
+
level = "error";
|
|
85
|
+
const entry = {
|
|
86
|
+
ts: new Date().toISOString(),
|
|
87
|
+
level,
|
|
88
|
+
source: "cli",
|
|
89
|
+
message: typeof message === "string" ? message.slice(0, 4000) : String(message),
|
|
90
|
+
};
|
|
91
|
+
const serialized = serializeError(error);
|
|
92
|
+
if (serialized)
|
|
93
|
+
entry.error = serialized;
|
|
94
|
+
if (data !== undefined)
|
|
95
|
+
entry.data = data;
|
|
96
|
+
write(`${safeStringify(entry)}\n`);
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
log,
|
|
100
|
+
installGlobalErrorHandlers: (options = {}) => {
|
|
101
|
+
const exitOnUncaughtException = options.exitOnUncaughtException ?? true;
|
|
102
|
+
process.on("uncaughtException", (err) => {
|
|
103
|
+
log("fatal", "uncaughtException", err);
|
|
104
|
+
if (exitOnUncaughtException)
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
|
107
|
+
process.on("unhandledRejection", (reason) => {
|
|
108
|
+
log("error", "unhandledRejection", reason);
|
|
109
|
+
});
|
|
110
|
+
cleanupOldFiles();
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -2,8 +2,15 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { MINIMUM_NODE_VERSION, assertSupportedNodeVersion } from "../bin/runtime-policy.mjs";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const CLI_PACKAGE_JSON = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
6
|
+
/**
|
|
7
|
+
* CLI 自身版本。
|
|
8
|
+
*
|
|
9
|
+
* **唯一源头是 packages/cli/package.json 的 version** —— 发版时只改那一处,
|
|
10
|
+
* 本文件、bin/node-preflight.mjs、tunnel/public-readiness.ts 全部自动跟随。
|
|
11
|
+
*/
|
|
12
|
+
export const AGENTROAM_VERSION = CLI_PACKAGE_JSON.version;
|
|
13
|
+
export const PLATFORM_DEPENDENCY_VERSIONS = CLI_PACKAGE_JSON.optionalDependencies;
|
|
7
14
|
export const PLATFORM_PACKAGES = {
|
|
8
15
|
"darwin-arm64": {
|
|
9
16
|
runtime: "agentroam-runtime-darwin-arm64",
|
package/dist/runtime-manager.js
CHANGED
|
@@ -36,6 +36,9 @@ export class RuntimeManager {
|
|
|
36
36
|
PORT: String(port),
|
|
37
37
|
HOST: options.localOnly ? "0.0.0.0" : "127.0.0.1",
|
|
38
38
|
AGENT_DATA_DIR: resolve(dataDir, "data"),
|
|
39
|
+
// 应用日志收口到 <dataDir>/logs,与 launchd 的 service.stdout/stderr.log
|
|
40
|
+
// 及 CLI 自身的错误日志同一个目录,用户排障时只需打包这一处。
|
|
41
|
+
AGENT_LOG_DIR: resolve(dataDir, "logs"),
|
|
39
42
|
AGENT_WEB_ROOTS: options.roots.join(delimiter),
|
|
40
43
|
AGENT_TRUST_TUNNEL_PROXY: "1",
|
|
41
44
|
AGENTROAM_VERSION: resolvePlatformRuntime(target).manifest.packageVersion,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetch as undiciFetch, ProxyAgent } from "undici";
|
|
2
2
|
import { resolveProxyForUrl } from "./proxy-settings.js";
|
|
3
|
+
import { AGENTROAM_VERSION } from "../platform-packages.js";
|
|
3
4
|
export async function waitForPublicReadiness(publicUrl, options = {}) {
|
|
4
5
|
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
5
6
|
const intervalMs = options.intervalMs ?? 750;
|
|
@@ -22,7 +23,7 @@ export async function waitForPublicReadiness(publicUrl, options = {}) {
|
|
|
22
23
|
try {
|
|
23
24
|
const response = await fetchImpl(endpoint, {
|
|
24
25
|
cache: "no-store",
|
|
25
|
-
headers: { "user-agent":
|
|
26
|
+
headers: { "user-agent": `agentroam-readiness/${AGENTROAM_VERSION}` },
|
|
26
27
|
signal: options.signal,
|
|
27
28
|
});
|
|
28
29
|
if (response.ok) {
|
|
@@ -4,10 +4,9 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { updatePaths, writeUpdateState } from "./update-state.js";
|
|
7
|
-
import { parseChannelVersion, registryUrlsForChannel,
|
|
7
|
+
import { parseChannelVersion, registryUrlsForChannel, resolveUpdateChannel, } from "./update-channel.js";
|
|
8
|
+
import { buildCliInstallManifestUrl, validateCliInstallManifest } from "./update-install.js";
|
|
8
9
|
const REGISTRY_BASE = "https://registry.npmjs.org/agentroam";
|
|
9
|
-
const RELEASE_BASE = "https://gitee.com/caoqu/team-agent/releases/download";
|
|
10
|
-
const SHA = /^[a-f0-9]{64}$/;
|
|
11
10
|
export async function resolveUpdate(options, fetchImpl = fetch) {
|
|
12
11
|
const channel = resolveUpdateChannel(options.currentVersion);
|
|
13
12
|
const target = await readRegistryCandidate(fetchImpl, channel);
|
|
@@ -17,16 +16,11 @@ export async function resolveUpdate(options, fetchImpl = fetch) {
|
|
|
17
16
|
throw new Error("requested version is not the current npm candidate");
|
|
18
17
|
if (compare(target, options.currentVersion) <= 0)
|
|
19
18
|
throw new Error("AgentRoam is already up to date");
|
|
20
|
-
const response = await fetchWithTimeout(fetchImpl,
|
|
19
|
+
const response = await fetchWithTimeout(fetchImpl, buildCliInstallManifestUrl());
|
|
21
20
|
if (!response.ok)
|
|
22
|
-
throw new Error("
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const fileName = options.target === "darwin-arm64" ? "install-agentroam.sh" : "install-agentroam.ps1";
|
|
26
|
-
if (manifest?.schemaVersion !== 2 || manifest.version !== target || manifest.channel !== resolveReleaseChannel(target) || asset?.fileName !== fileName || !SHA.test(asset?.sha256 ?? "")) {
|
|
27
|
-
throw new Error("release manifest is invalid for this platform");
|
|
28
|
-
}
|
|
29
|
-
return { targetVersion: target, fileName, sha256: asset.sha256 };
|
|
21
|
+
throw new Error("CLI install manifest is unavailable");
|
|
22
|
+
const asset = validateCliInstallManifest(await response.json(), options.target);
|
|
23
|
+
return { targetVersion: target, fileName: asset.fileName, sha256: asset.sha256 };
|
|
30
24
|
}
|
|
31
25
|
// Preview installations follow the higher of the preview and latest npm
|
|
32
26
|
// dist-tags so a newer stable release can move them onto the stable track.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const REPOSITORY_BASE = "https://github.com/tclxqu-cao/team-agent";
|
|
2
|
+
const REPOSITORY_BRANCH = "main";
|
|
3
|
+
const INSTALL_BASE = `${REPOSITORY_BASE}/raw/${REPOSITORY_BRANCH}/packages/cli/install`;
|
|
4
|
+
export const CLI_INSTALL_FILE_NAMES = {
|
|
5
|
+
"darwin-arm64": "install-agentroam.sh",
|
|
6
|
+
"windows-amd64": "install-agentroam.ps1",
|
|
7
|
+
};
|
|
8
|
+
export function buildCliInstallManifestUrl() {
|
|
9
|
+
return `${INSTALL_BASE}/install-manifest.json`;
|
|
10
|
+
}
|
|
11
|
+
export function buildCliInstallScriptUrl(fileName) {
|
|
12
|
+
if (!Object.values(CLI_INSTALL_FILE_NAMES).includes(fileName))
|
|
13
|
+
throw new Error("invalid CLI installer file name");
|
|
14
|
+
return `${INSTALL_BASE}/${fileName}`;
|
|
15
|
+
}
|
|
16
|
+
export function validateCliInstallManifest(value, platform) {
|
|
17
|
+
const expected = CLI_INSTALL_FILE_NAMES[platform];
|
|
18
|
+
if (!expected)
|
|
19
|
+
throw new Error("CLI install manifest does not support this platform");
|
|
20
|
+
if (typeof value !== "object" || value === null)
|
|
21
|
+
throw new Error("invalid CLI install manifest identity");
|
|
22
|
+
const manifest = value;
|
|
23
|
+
if (manifest.schemaVersion !== 1 || typeof manifest.installers !== "object" || manifest.installers === null) {
|
|
24
|
+
throw new Error("invalid CLI install manifest identity");
|
|
25
|
+
}
|
|
26
|
+
const asset = manifest.installers[platform];
|
|
27
|
+
if (!asset || asset.fileName !== expected || typeof asset.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(asset.sha256)) {
|
|
28
|
+
throw new Error("invalid CLI installer metadata");
|
|
29
|
+
}
|
|
30
|
+
if (asset.size !== undefined && (!Number.isSafeInteger(asset.size) || asset.size <= 0)) {
|
|
31
|
+
throw new Error("invalid CLI installer size");
|
|
32
|
+
}
|
|
33
|
+
return { fileName: expected, sha256: asset.sha256, ...(asset.size === undefined ? {} : { size: asset.size }) };
|
|
34
|
+
}
|
|
@@ -6,7 +6,7 @@ import { pipeline } from "node:stream/promises";
|
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { acquireUpdateLock, readUpdateState, sanitizeUpdateError, updatePaths, writeUpdateState } from "./update-state.js";
|
|
9
|
-
|
|
9
|
+
import { buildCliInstallScriptUrl } from "./update-install.js";
|
|
10
10
|
export async function runUpdateWorker(stateFile, fetchImpl = fetch) {
|
|
11
11
|
const initial = await readUpdateState(stateFile);
|
|
12
12
|
if (!initial)
|
|
@@ -22,7 +22,7 @@ export async function runUpdateWorker(stateFile, fetchImpl = fetch) {
|
|
|
22
22
|
await writeUpdateState(stateFile, state);
|
|
23
23
|
};
|
|
24
24
|
try {
|
|
25
|
-
const response = await fetchImpl(
|
|
25
|
+
const response = await fetchImpl(buildCliInstallScriptUrl(state.fileName));
|
|
26
26
|
if (!response.ok || !response.body)
|
|
27
27
|
throw new Error("installer download failed");
|
|
28
28
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(partial, { mode: 0o700 }));
|
|
@@ -33,7 +33,16 @@ export async function runUpdateWorker(stateFile, fetchImpl = fetch) {
|
|
|
33
33
|
await update({ phase: "installing" });
|
|
34
34
|
const command = process.platform === "win32" ? "powershell.exe" : "/bin/sh";
|
|
35
35
|
const args = process.platform === "win32" ? ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", partial] : [partial];
|
|
36
|
-
|
|
36
|
+
// 用 AGENTROAM_VERSION 钉住版本,而不是让安装脚本自己解析 dist-tag:
|
|
37
|
+
// 一是装的必须正是检查阶段向用户承诺的那一版,二是下一步要按
|
|
38
|
+
// launcher/<targetVersion> 定位入口,版本漂了就找不到。
|
|
39
|
+
const code = await run(command, args, {
|
|
40
|
+
...process.env,
|
|
41
|
+
AGENTROAM_VERSION: state.targetVersion,
|
|
42
|
+
AGENTROAM_DATA_DIR: state.dataDir,
|
|
43
|
+
AGENTROAM_ROOT: state.roots[0],
|
|
44
|
+
AGENTROAM_INSTALL_SKIP_SERVICE: "1",
|
|
45
|
+
});
|
|
37
46
|
if (code !== 0)
|
|
38
47
|
throw new Error(`installer exited with code ${code}`);
|
|
39
48
|
const nextEntry = resolve(state.dataDir, "launcher", state.targetVersion, "node_modules", "agentroam", "bin", "agentroam.mjs");
|
package/install/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# AgentRoam bootstrap installers
|
|
2
2
|
|
|
3
|
-
These
|
|
3
|
+
These installers support macOS Apple Silicon and Windows x64. They install the newest published `preview` release by default (resolved from the npm dist-tag); pin an exact release with `AGENTROAM_VERSION=<x.y.z-preview.N>`. They reuse the active Node.js installation when it meets `>=22.22.0`, with no upper version bound. Otherwise they search NVM for the highest compatible version, then fall back to the verified private Node.js `22.22.0` runtime under the AgentRoam data directory. They then register and start the current-user AgentRoam background service. They never modify, replace, or uninstall system Node.js or global power settings.
|
|
4
4
|
|
|
5
5
|
## macOS Apple Silicon
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
curl -fsSL https://
|
|
8
|
+
curl -fsSL https://github.com/tclxqu-cao/team-agent/raw/main/packages/cli/install/install-agentroam.sh | sh
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
The command installs the wrapper at `~/.local/bin/agentroam`. If that directory is not already in `PATH`, the installer prints the required shell setting without editing shell profiles.
|
|
@@ -13,7 +13,7 @@ The command installs the wrapper at `~/.local/bin/agentroam`. If that directory
|
|
|
13
13
|
## Windows x64
|
|
14
14
|
|
|
15
15
|
```powershell
|
|
16
|
-
irm https://
|
|
16
|
+
irm https://github.com/tclxqu-cao/team-agent/raw/main/packages/cli/install/install-agentroam.ps1 | iex
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The command installs `%USERPROFILE%\.agentroam\bin\agentroam.cmd` and adds only that user directory to the user-level `PATH`. It does not require administrator access or change the machine-level `PATH`.
|
|
@@ -40,9 +40,9 @@ agentroam service uninstall
|
|
|
40
40
|
|
|
41
41
|
## Data and updates
|
|
42
42
|
|
|
43
|
-
Set `AGENTROAM_DATA_DIR` before running either installer to override the default `~/.agentroam` data directory. Node is stored at `<data-dir>/runtimes/node/22.22.0`, and the launcher is stored at `<data-dir>/launcher
|
|
43
|
+
Set `AGENTROAM_DATA_DIR` before running either installer to override the default `~/.agentroam` data directory. Node is stored at `<data-dir>/runtimes/node/22.22.0`, and the launcher is stored at `<data-dir>/launcher/<version>`.
|
|
44
44
|
|
|
45
|
-
Each AgentRoam release pins its own Node patch
|
|
45
|
+
Each AgentRoam release pins its own Node patch. Updates happen by running the installer again (it resolves the newest published `preview`) or by installing a newer AgentRoam release; startup never follows a moving Node.js release channel.
|
|
46
46
|
|
|
47
47
|
To uninstall the wrapper, remove `~/.local/bin/agentroam` on macOS or `%USERPROFILE%\.agentroam\bin\agentroam.cmd` on Windows. The private runtime, launcher, session data, and configuration under the data directory are preserved until explicitly removed by the user.
|
|
48
48
|
|
|
@@ -4,7 +4,7 @@ $ProgressPreference = "SilentlyContinue"
|
|
|
4
4
|
$NodeVersion = "22.22.0"
|
|
5
5
|
$MinimumNodeVersion = "22.22.0"
|
|
6
6
|
# 默认跟随 npm preview 最新标签;可用 $env:AGENTROAM_VERSION 钉精确版本
|
|
7
|
-
# 例如:$env:AGENTROAM_VERSION = "
|
|
7
|
+
# 例如:$env:AGENTROAM_VERSION = "<x.y.z-preview.N>"; ./install-agentroam.ps1
|
|
8
8
|
$AgentRoamVersionRequest = if ($env:AGENTROAM_VERSION) { $env:AGENTROAM_VERSION } else { "preview" }
|
|
9
9
|
$AgentRoamVersion = "pending-version"
|
|
10
10
|
$NodeArchive = "node-v22.22.0-win-x64.zip"
|
|
@@ -3,8 +3,8 @@ set -eu
|
|
|
3
3
|
|
|
4
4
|
NODE_VERSION="22.22.0"
|
|
5
5
|
MINIMUM_NODE_VERSION="22.22.0"
|
|
6
|
-
# Default to the newest published preview; pin an exact version via
|
|
7
|
-
# AGENTROAM_VERSION
|
|
6
|
+
# Default to the newest published preview (npm dist-tag); pin an exact version via
|
|
7
|
+
# AGENTROAM_VERSION=<x.y.z-preview.N> sh install-agentroam.sh
|
|
8
8
|
AGENTROAM_VERSION_REQUEST="${AGENTROAM_VERSION:-preview}"
|
|
9
9
|
AGENTROAM_VERSION=""
|
|
10
10
|
NODE_ARCHIVE="node-v22.22.0-darwin-arm64.tar.xz"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"installers": {
|
|
4
|
+
"darwin-arm64": {
|
|
5
|
+
"fileName": "install-agentroam.sh",
|
|
6
|
+
"sha256": "db22342cd22272c739f6633deff65c4b54b9dc919778141b7d8d55c37be221a3",
|
|
7
|
+
"size": 11625
|
|
8
|
+
},
|
|
9
|
+
"windows-amd64": {
|
|
10
|
+
"fileName": "install-agentroam.ps1",
|
|
11
|
+
"sha256": "4a3683a3e6027e15356dab7a5c7d02a81ad1be3f4bad61a6119328ed2ab89298",
|
|
12
|
+
"size": 13155
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentroam",
|
|
3
|
-
"version": "0.2.0-preview.
|
|
3
|
+
"version": "0.2.0-preview.27",
|
|
4
4
|
"description": "Zero-install mobile remote terminal and file console",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
21
|
"test": "vitest run --root ../.. packages/cli/src",
|
|
22
22
|
"prepack": "npm run build"
|
|
23
23
|
},
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"undici": "^6.25.0"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"agentroam-runtime-darwin-arm64": "0.2.0-preview.
|
|
30
|
-
"agentroam-runtime-win32-x64": "0.2.0-preview.
|
|
31
|
-
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.
|
|
32
|
-
"agentroam-cloudflared-win32-x64": "0.2.0-preview.
|
|
33
|
-
"agentroam-tui-darwin-arm64": "0.2.0-preview.
|
|
34
|
-
"@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.
|
|
29
|
+
"agentroam-runtime-darwin-arm64": "0.2.0-preview.27",
|
|
30
|
+
"agentroam-runtime-win32-x64": "0.2.0-preview.27",
|
|
31
|
+
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.27",
|
|
32
|
+
"agentroam-cloudflared-win32-x64": "0.2.0-preview.27",
|
|
33
|
+
"agentroam-tui-darwin-arm64": "0.2.0-preview.27",
|
|
34
|
+
"@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.27"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|