agentroam 0.2.0-preview.16 → 0.2.0-preview.18

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.
@@ -2,15 +2,15 @@ import { describe, expect, it } from "vitest";
2
2
  import { detectPlatform, detectPlatformTarget } from "./platform.js";
3
3
  describe("detectPlatform", () => {
4
4
  it("maps supported targets", () => {
5
- expect(detectPlatform("darwin", "arm64", "22.1.0")).toBe("darwin-arm64");
6
- expect(detectPlatform("win32", "x64", "22.1.0")).toBe("windows-amd64");
5
+ expect(detectPlatform("darwin", "arm64", "22.22.0")).toBe("darwin-arm64");
6
+ expect(detectPlatform("win32", "x64", "25.8.0")).toBe("windows-amd64");
7
7
  });
8
8
  it("detects a target before Node version validation", () => {
9
9
  expect(detectPlatformTarget("darwin", "arm64")).toBe("darwin-arm64");
10
10
  expect(detectPlatformTarget("win32", "x64")).toBe("windows-amd64");
11
11
  });
12
12
  it("rejects unsupported versions", () => {
13
- expect(() => detectPlatform("darwin", "arm64", "23.0.0")).toThrow("Node.js 22");
13
+ expect(() => detectPlatform("darwin", "arm64", "22.21.9")).toThrow("Node.js >=22.22.0");
14
14
  });
15
15
  it("rejects unsupported platforms", () => {
16
16
  expect(() => detectPlatformTarget("linux", "x64")).toThrow("unsupported platform");
@@ -18,7 +18,7 @@ export class RuntimeManager {
18
18
  this.report = report;
19
19
  this.opencodeResolver = opencodeResolver;
20
20
  }
21
- async start(options, pairing, target) {
21
+ async start(options, target) {
22
22
  const port = await findAvailablePort(options.port);
23
23
  const dataDir = resolve(options.dataDir);
24
24
  await Promise.all(["data", "bin", "cache", "logs"].map((name) => mkdir(resolve(dataDir, name), { recursive: true })));
@@ -27,19 +27,17 @@ export class RuntimeManager {
27
27
  const { runtimeRoot } = resolvePlatformRuntime(target);
28
28
  await repairNativeRuntimePermissions(runtimeRoot);
29
29
  const gateway = resolve(runtimeRoot, "ws-server.mjs");
30
- const child = this.supervisor.spawn(process.execPath, [gateway], {
30
+ const child = this.supervisor.spawn(process.execPath, [gateway, ...(options.testNoPairing ? ["--test-no-pairing"] : [])], {
31
31
  cwd: runtimeRoot,
32
32
  env: {
33
33
  ...childEnvironment,
34
34
  NODE_ENV: "production",
35
35
  NEXT_DIST_DIR: ".next",
36
36
  PORT: String(port),
37
- HOST: "127.0.0.1",
37
+ HOST: options.localOnly ? "0.0.0.0" : "127.0.0.1",
38
38
  AGENT_DATA_DIR: resolve(dataDir, "data"),
39
39
  AGENT_WEB_ROOTS: options.roots.join(delimiter),
40
40
  AGENT_TRUST_TUNNEL_PROXY: "1",
41
- AGENT_PAIRING_HASH: pairing?.hashHex || "",
42
- AGENT_PAIRING_EXPIRES_AT: pairing?.expiresAt || "",
43
41
  AGENTROAM_VERSION: resolvePlatformRuntime(target).manifest.packageVersion,
44
42
  AGENTROAM_DATA_DIR: dataDir,
45
43
  AGENTROAM_CLI_PATH: fileURLToPath(new URL("../bin/agentroam.mjs", import.meta.url)),
@@ -1,9 +1,11 @@
1
1
  import { delimiter, dirname, isAbsolute, resolve, win32 } from "node:path";
2
2
  import { resolveCodexRuntime } from "../codex-runtime-manager.js";
3
3
  import { detectPlatform } from "../platform.js";
4
+ import { printPairingCode } from "../device-pairing.js";
4
5
  import { renderQr } from "../qr.js";
5
6
  import { MacLaunchAgent } from "./macos-launch-agent.js";
6
7
  import { WindowsTaskService } from "./windows-task-service.js";
8
+ import { assertSupportedNodeVersion } from "../../bin/runtime-policy.mjs";
7
9
  export async function runServiceCommand(options, context) {
8
10
  const platform = context.platform ?? process.platform;
9
11
  const log = context.log ?? console.log;
@@ -19,9 +21,7 @@ export async function runServiceCommand(options, context) {
19
21
  switch (options.serviceAction) {
20
22
  case "install": {
21
23
  const nodeVersion = context.nodeVersion ?? process.versions.node;
22
- if (Number(nodeVersion.split(".")[0]) !== 22) {
23
- throw cliError(`Node.js 22 is required to install the service (current ${nodeVersion})`);
24
- }
24
+ assertSupportedNodeVersion(nodeVersion);
25
25
  const now = context.now?.() ?? new Date();
26
26
  const environment = context.environment ?? process.env;
27
27
  const environmentPath = buildServiceEnvironmentPath(context.nodePath, environment.PATH ?? environment.Path, platform);
@@ -57,10 +57,14 @@ export async function runServiceCommand(options, context) {
57
57
  };
58
58
  const { definition, state } = await controller.install(config);
59
59
  log(`✓ AgentRoam service installed: ${definition}`);
60
- if (state?.status === "ready" && state.accessUrl)
61
- await logAccessUrl(state.accessUrl);
60
+ if (state?.status === "ready" && state.accessUrl) {
61
+ log(`Open: ${state.accessUrl}`);
62
+ if (state.localUrl)
63
+ log(`电脑本机:${state.localUrl.replace(/\/$/, "")}/web(配对后可进行远程授权)`);
64
+ await (context.pairingPrinter ?? printPairingCode)(options.dataDir, log, { accessUrl: state.accessUrl, qr: options.qr, interactive: stdoutIsTTY });
65
+ }
62
66
  else
63
- log("Service is starting. Run `agentroam service url` shortly.");
67
+ log("Service is starting. Run `agentroam service url` and `agentroam pair` shortly.");
64
68
  return;
65
69
  }
66
70
  case "start": {
@@ -2,6 +2,26 @@ import { resolve } from "node:path";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import { buildServiceEnvironmentPath, runServiceCommand } from "./service-command.js";
4
4
  describe("runServiceCommand", () => {
5
+ it.each(["22.22.0", "24.0.0", "25.8.0", "26.0.0"])("keeps the selected Node %s when installing a service", async (nodeVersion) => {
6
+ const install = vi.fn(async () => ({ definition: "/test/service.plist" }));
7
+ const nodePath = `/runtimes/node-${nodeVersion}/bin/node`;
8
+ const codexResolver = vi.fn(async () => ({ executable: "/tools/codex", version: "test", source: "global" }));
9
+ await runServiceCommand(options("install"), {
10
+ platform: "darwin", arch: "arm64", nodeVersion, nodePath,
11
+ cliPath: "/agentroam.mjs", version: "test", environment: { PATH: "/usr/bin" },
12
+ codexResolver, controller: controller({ install }), log: vi.fn(),
13
+ });
14
+ expect(install).toHaveBeenCalledWith(expect.objectContaining({ nodePath }));
15
+ expect(codexResolver).toHaveBeenCalledWith(expect.objectContaining({ nodeExecutable: nodePath }));
16
+ });
17
+ it("rejects a below-minimum service runtime before installation", async () => {
18
+ const install = vi.fn();
19
+ await expect(runServiceCommand(options("install"), {
20
+ platform: "darwin", nodePath: "/node", nodeVersion: "22.21.9", cliPath: "/agentroam.mjs",
21
+ version: "test", controller: controller({ install }),
22
+ })).rejects.toThrow("Node.js >=22.22.0");
23
+ expect(install).not.toHaveBeenCalled();
24
+ });
5
25
  it("installs the service using absolute runtime context and parsed start options", async () => {
6
26
  const install = vi.fn(async (config) => ({
7
27
  paths: { plistPath: "/Users/test/Library/LaunchAgents/com.agentroam.service.plist" },
@@ -20,6 +40,7 @@ describe("runServiceCommand", () => {
20
40
  now: () => new Date("2026-09-03T00:00:00.000Z"),
21
41
  log,
22
42
  controller: controller({ install }),
43
+ pairingPrinter: vi.fn(async () => { }),
23
44
  });
24
45
  expect(install).toHaveBeenCalledWith(expect.objectContaining({
25
46
  nodePath: "/node",
@@ -22,7 +22,7 @@ export async function waitForPublicReadiness(publicUrl, options = {}) {
22
22
  try {
23
23
  const response = await fetchImpl(endpoint, {
24
24
  cache: "no-store",
25
- headers: { "user-agent": "agentroam-readiness/0.2.0-preview.16" },
25
+ headers: { "user-agent": "agentroam-readiness/0.2.0-preview.18" },
26
26
  signal: options.signal,
27
27
  });
28
28
  if (response.ok) {
package/install/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # AgentRoam bootstrap installers
2
2
 
3
- These versioned installers support macOS Apple Silicon and Windows x64. They reuse a visible Node.js 22 installation when available. Otherwise they install 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.
3
+ These versioned installers support macOS Apple Silicon and Windows x64. 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://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.16/install-agentroam.sh | sh
8
+ curl -fsSL https://gitee.com/caoqu/team-agent/raw/master/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,14 +13,14 @@ The command installs the wrapper at `~/.local/bin/agentroam`. If that directory
13
13
  ## Windows x64
14
14
 
15
15
  ```powershell
16
- irm https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.16/install-agentroam.ps1 | iex
16
+ irm https://gitee.com/caoqu/team-agent/raw/master/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`.
20
20
 
21
21
  ## Service behavior
22
22
 
23
- Run the installer from the project directory AgentRoam should expose, or set `AGENTROAM_ROOT` explicitly. An installer launched from the home directory without an explicit root stops before service registration so it cannot expose the entire home directory accidentally.
23
+ Run the installer from the project directory AgentRoam should expose, or set `AGENTROAM_ROOT` explicitly. The current directory is accepted even when it is the home directory.
24
24
 
25
25
  The installer registers and starts the service after Node, CLI, and `doctor` validation. When a previous AgentRoam user service is installed, the new CLI stops it, waits for its process and platform registration to exit, removes only the old service registration, and then registers the replacement. Session data, pairing state, managed runtimes, and logs remain in the data directory. While foreground AgentRoam or the service is running, it prevents idle system sleep but still allows the display to dim and turn off. Lid close, explicit sleep, hibernation, shutdown, and low-battery forced sleep remain controlled by the operating system.
26
26
 
@@ -40,8 +40,16 @@ 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/0.2.0-preview.16`.
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/0.2.0-preview.18`.
44
44
 
45
45
  Each AgentRoam release pins its own Node patch and installer assets. Updates happen only by running a newer versioned installer or 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
+
49
+ ## Optional desktop app
50
+
51
+ After CLI setup succeeds, interactive installers ask whether to download the desktop app. Answer `y` to download the matching macOS DMG or Windows installer into a unique folder under Downloads, verify its release-manifest SHA-256, and show the file. Open it to complete installation. Answer `n` or press Enter to keep using CLI only. Noninteractive runs skip the prompt.
52
+
53
+ Set `AGENTROAM_INSTALL_DESKTOP=yes` or `no` explicitly for automation. The Shell installer reads `/dev/tty`, so `curl ... | sh` remains interactive. A missing desktop artifact or failed download does not undo CLI installation. This requires a published CLI that includes `bin/desktop-download.mjs` and matching desktop release assets.
54
+
55
+ On macOS, the desktop app opens its own permission guide on first launch, even before connecting to a service. Screen Recording enables phone viewing; Accessibility enables keyboard and mouse control. Returning from System Settings rechecks permissions and resumes an enabled session. Restart the desktop app if macOS requires it. Desktop live publishing follows the selected CLI service, including its actual port. Windows desktop download is supported, but the current screen/input helper is macOS-only.
@@ -2,7 +2,10 @@ $ErrorActionPreference = "Stop"
2
2
  $ProgressPreference = "SilentlyContinue"
3
3
 
4
4
  $NodeVersion = "22.22.0"
5
- $AgentRoamVersion = "0.2.0-preview.16"
5
+ $MinimumNodeVersion = "22.22.0"
6
+ # 默认跟随 npm preview 最新标签;可用 $env:AGENTROAM_VERSION 钉精确版本
7
+ $AgentRoamVersionRequest = if ($env:AGENTROAM_VERSION) { $env:AGENTROAM_VERSION } else { "preview" }
8
+ $AgentRoamVersion = "pending-version"
6
9
  $NodeArchive = "node-v22.22.0-win-x64.zip"
7
10
  $NodeSha256 = "c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
8
11
  $NodeUrl = "https://nodejs.org/dist/v22.22.0/$NodeArchive"
@@ -20,16 +23,16 @@ $ServiceRootExplicit = -not [string]::IsNullOrWhiteSpace($env:AGENTROAM_ROOT)
20
23
  $ServiceRoot = if ($ServiceRootExplicit) { $env:AGENTROAM_ROOT } else { (Get-Location).Path }
21
24
  if (-not (Test-Path -LiteralPath $ServiceRoot -PathType Container)) { throw "AgentRoam root is not a directory: $ServiceRoot" }
22
25
  $ServiceRoot = [System.IO.Path]::GetFullPath($ServiceRoot).TrimEnd([System.IO.Path]::DirectorySeparatorChar)
23
- $HomeRoot = [System.IO.Path]::GetFullPath($HOME).TrimEnd([System.IO.Path]::DirectorySeparatorChar)
24
- if (-not $ServiceRootExplicit -and $ServiceRoot -eq $HomeRoot) {
25
- throw "Refusing to expose the entire home directory implicitly; run from a project directory or set AGENTROAM_ROOT explicitly"
26
- }
27
-
28
26
  New-Item -ItemType Directory -Force -Path $NodeParent, $LauncherParent, $WrapperDir | Out-Null
29
27
 
30
- function Test-Node22([string]$NodeBin) {
28
+ function Test-NodeVersion([string]$VersionText) {
29
+ if ($VersionText -notmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$') { return $false }
30
+ try { return [Version]($VersionText.Split('+')[0]) -ge [Version]$MinimumNodeVersion } catch { return $false }
31
+ }
32
+
33
+ function Test-SupportedNode([string]$NodeBin) {
31
34
  if (-not (Test-Path -LiteralPath $NodeBin -PathType Leaf)) { return $false }
32
- try { return (& $NodeBin -p 'process.versions.node.split(".")[0]' 2>$null) -eq "22" } catch { return $false }
35
+ try { return Test-NodeVersion ((& $NodeBin -p 'process.versions.node' 2>$null).Trim()) } catch { return $false }
33
36
  }
34
37
 
35
38
  function Get-NvmRoots {
@@ -50,15 +53,15 @@ function Get-NvmRoots {
50
53
  @($Candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) } | Select-Object -Unique)
51
54
  }
52
55
 
53
- function Find-NvmNode22 {
56
+ function Find-NvmNode {
54
57
  $VersionMatches = foreach ($NvmRoot in (Get-NvmRoots)) {
55
58
  foreach ($VersionDirectory in Get-ChildItem -LiteralPath $NvmRoot -Directory -ErrorAction SilentlyContinue) {
56
59
  $Candidate = Join-Path $VersionDirectory.FullName "node.exe"
57
60
  if (-not (Test-Path -LiteralPath $Candidate -PathType Leaf)) { continue }
58
61
  try {
59
62
  $VersionText = (& $Candidate -p 'process.versions.node' 2>$null).Trim()
60
- $Version = [Version]$VersionText
61
- if ($Version.Major -eq 22) {
63
+ if (Test-NodeVersion $VersionText) {
64
+ $Version = [Version]($VersionText.Split('+')[0])
62
65
  [pscustomobject]@{ Path = $Candidate; Version = $Version }
63
66
  }
64
67
  } catch {}
@@ -97,10 +100,10 @@ function Enter-InstallLock([string]$Path) {
97
100
 
98
101
  $SystemNode = Get-Command node.exe -ErrorAction SilentlyContinue
99
102
  if ($env:AGENTROAM_BOOTSTRAP_TEST -eq "1" -and $env:AGENTROAM_FORCE_PRIVATE_NODE -eq "1") { $SystemNode = $null }
100
- if ($SystemNode -and (Test-Node22 $SystemNode.Source)) {
103
+ if ($SystemNode -and (Test-SupportedNode $SystemNode.Source)) {
101
104
  $NodeBin = $SystemNode.Source
102
105
  } else {
103
- $NvmNode = if ($env:AGENTROAM_BOOTSTRAP_TEST -eq "1" -and $env:AGENTROAM_FORCE_PRIVATE_NODE -eq "1") { $null } else { Find-NvmNode22 }
106
+ $NvmNode = if ($env:AGENTROAM_BOOTSTRAP_TEST -eq "1" -and $env:AGENTROAM_FORCE_PRIVATE_NODE -eq "1") { $null } else { Find-NvmNode }
104
107
  if ($NvmNode) {
105
108
  $NodeBin = $NvmNode
106
109
  Write-Host "Using Node.js $(& $NodeBin --version) from NVM: $NodeBin"
@@ -158,6 +161,17 @@ $NpmCandidates = @(
158
161
  $NpmCli = $NpmCandidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
159
162
  if (-not $NpmCli) { throw "npm CLI was not found beside $NodeBin" }
160
163
 
164
+ # 解析 dist-tag → 精确版本(安装最总是最新发布)
165
+ if ($AgentRoamVersionRequest -notmatch '\d$') {
166
+ $Resolved = (& $NodeBin $NpmCli view "agentroam@$AgentRoamVersionRequest" version --registry $NpmRegistry 2>$null | Select-Object -Last 1)
167
+ if (-not $Resolved) { throw "could not resolve agentroam@$AgentRoamVersionRequest from $NpmRegistry" }
168
+ $AgentRoamVersion = ([string]$Resolved).Trim()
169
+ Write-Host "Installing AgentRoam $AgentRoamVersion (resolved from $AgentRoamVersionRequest)"
170
+ } else {
171
+ $AgentRoamVersion = $AgentRoamVersionRequest
172
+ }
173
+ $LauncherRoot = Join-Path $LauncherParent $AgentRoamVersion
174
+ $LauncherLock = Join-Path $LauncherRoot ".lock"
161
175
  $PackageSpec = "agentroam@$AgentRoamVersion"
162
176
  $ExtraPackageSpecs = @()
163
177
  if ($env:AGENTROAM_BOOTSTRAP_TEST -eq "1" -and $env:AGENTROAM_PACKAGE_SPEC) {
@@ -221,3 +235,21 @@ if ($env:AGENTROAM_INSTALL_SKIP_SERVICE -eq "1") {
221
235
  }
222
236
  Write-Host "`nAgentRoam $AgentRoamVersion installed: $WrapperPath"
223
237
  Write-Host "Open a new terminal, then run: agentroam service status"
238
+
239
+ $DesktopChoice = if ($env:AGENTROAM_INSTALL_DESKTOP) { $env:AGENTROAM_INSTALL_DESKTOP } else { "ask" }
240
+ if ($env:AGENTROAM_INSTALL_SKIP_SERVICE -eq "1" -and $DesktopChoice -eq "ask") { $DesktopChoice = "no" }
241
+ if ($DesktopChoice -eq "ask") {
242
+ $DesktopChoice = "no"
243
+ if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected) {
244
+ try { $DesktopChoice = Read-Host "是否下载桌面端?用于手机查看和控制电脑桌面 [y/N]" } catch { $DesktopChoice = "no" }
245
+ }
246
+ }
247
+ if ($DesktopChoice -match '^(y|yes|是)$') {
248
+ $DesktopHelper = Join-Path $LauncherRoot "node_modules\agentroam\bin\desktop-download.mjs"
249
+ if (Test-Path -LiteralPath $DesktopHelper -PathType Leaf) {
250
+ & $NodeBin $DesktopHelper $AgentRoamVersion
251
+ if ($LASTEXITCODE -ne 0) { Write-Warning "CLI 安装已完成,桌面端可稍后重新下载。" }
252
+ } else {
253
+ Write-Warning "当前发布的 CLI 尚未包含桌面下载功能,请在新版发布后重试。CLI 安装已完成。"
254
+ }
255
+ } else { Write-Host "已跳过桌面端下载,CLI 可正常使用。" }
@@ -2,7 +2,11 @@
2
2
  set -eu
3
3
 
4
4
  NODE_VERSION="22.22.0"
5
- AGENTROAM_VERSION="0.2.0-preview.16"
5
+ MINIMUM_NODE_VERSION="22.22.0"
6
+ # Default to the newest published preview; pin an exact version via
7
+ # AGENTROAM_VERSION=0.2.0-preview.18 sh install-agentroam.sh
8
+ AGENTROAM_VERSION_REQUEST="${AGENTROAM_VERSION:-preview}"
9
+ AGENTROAM_VERSION=""
6
10
  NODE_ARCHIVE="node-v22.22.0-darwin-arm64.tar.xz"
7
11
  NODE_SHA256="2bd596bbfc4a275ceb8721a5954ee97daea5ebe673e96a185ebd732f6fb023ac"
8
12
  NODE_URL="https://nodejs.org/dist/v22.22.0/$NODE_ARCHIVE"
@@ -12,14 +16,12 @@ NODE_PARENT="$DATA_DIR/runtimes/node"
12
16
  NODE_ROOT="$NODE_PARENT/$NODE_VERSION"
13
17
  NODE_LOCK="$NODE_ROOT.lock"
14
18
  LAUNCHER_PARENT="$DATA_DIR/launcher"
15
- LAUNCHER_ROOT="$LAUNCHER_PARENT/$AGENTROAM_VERSION"
19
+ LAUNCHER_ROOT="$LAUNCHER_PARENT/pending-version"
16
20
  LAUNCHER_LOCK="$LAUNCHER_ROOT.lock"
17
21
  WRAPPER_DIR="$HOME/.local/bin"
18
22
  WRAPPER_PATH="$WRAPPER_DIR/agentroam"
19
- SERVICE_ROOT_EXPLICIT=0
20
23
  if [ "${AGENTROAM_ROOT+x}" = "x" ]; then
21
24
  SERVICE_ROOT="$AGENTROAM_ROOT"
22
- SERVICE_ROOT_EXPLICIT=1
23
25
  else
24
26
  SERVICE_ROOT="$PWD"
25
27
  fi
@@ -48,33 +50,44 @@ command -v shasum >/dev/null 2>&1 || fail "shasum is required"
48
50
  [ -x /usr/bin/tar ] || fail "/usr/bin/tar is required"
49
51
  [ -d "$SERVICE_ROOT" ] || fail "AgentRoam root is not a directory: $SERVICE_ROOT"
50
52
  service_root_physical=$(cd "$SERVICE_ROOT" && pwd -P)
51
- home_physical=$(cd "$HOME" && pwd -P)
52
- if [ "$SERVICE_ROOT_EXPLICIT" -eq 0 ] && [ "$service_root_physical" = "$home_physical" ]; then
53
- fail "refusing to expose the entire home directory implicitly; run from a project directory or set AGENTROAM_ROOT explicitly"
54
- fi
55
53
  SERVICE_ROOT="$service_root_physical"
56
54
 
57
55
  mkdir -p "$NODE_PARENT" "$LAUNCHER_PARENT" "$WRAPPER_DIR"
58
56
 
59
- node_is_22() {
60
- [ -x "$1" ] && [ "$("$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)" = "22" ]
57
+ version_is_supported() {
58
+ awk -v version="$1" -v minimum="$MINIMUM_NODE_VERSION" 'BEGIN {
59
+ if (version !~ /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/) exit 1;
60
+ sub(/\+.*/, "", version);
61
+ split(version, actual, "."); split(minimum, required, ".");
62
+ for (component = 1; component <= 3; component++) {
63
+ if (actual[component] + 0 > required[component] + 0) exit 0;
64
+ if (actual[component] + 0 < required[component] + 0) exit 1;
65
+ }
66
+ exit 0;
67
+ }'
68
+ }
69
+
70
+ node_is_supported() {
71
+ [ -x "$1" ] && version_is_supported "$("$1" -p 'process.versions.node' 2>/dev/null || true)"
61
72
  }
62
73
 
63
- find_nvm_node_22() {
74
+ find_nvm_node() {
64
75
  best_bin=""
76
+ best_major=-1
65
77
  best_minor=-1
66
78
  best_patch=-1
67
79
  for nvm_root in "${NVM_DIR:-}" "$HOME/.nvm"; do
68
80
  [ -n "$nvm_root" ] || continue
69
- for candidate in "$nvm_root"/versions/node/v22*/bin/node; do
81
+ for candidate in "$nvm_root"/versions/node/v*/bin/node; do
70
82
  [ -x "$candidate" ] || continue
71
83
  version=$("$candidate" -p 'process.versions.node' 2>/dev/null || true)
84
+ version_is_supported "$version" || continue
72
85
  major=$(printf '%s\n' "$version" | awk -F. '{ print $1 + 0 }')
73
86
  minor=$(printf '%s\n' "$version" | awk -F. '{ print $2 + 0 }')
74
87
  patch=$(printf '%s\n' "$version" | awk -F. '{ sub(/[^0-9].*$/, "", $3); print $3 + 0 }')
75
- [ "$major" -eq 22 ] 2>/dev/null || continue
76
- if [ "$minor" -gt "$best_minor" ] || { [ "$minor" -eq "$best_minor" ] && [ "$patch" -gt "$best_patch" ]; }; then
88
+ if [ "$major" -gt "$best_major" ] || { [ "$major" -eq "$best_major" ] && { [ "$minor" -gt "$best_minor" ] || { [ "$minor" -eq "$best_minor" ] && [ "$patch" -gt "$best_patch" ]; }; }; }; then
77
89
  best_bin="$candidate"
90
+ best_major=$major
78
91
  best_minor=$minor
79
92
  best_patch=$patch
80
93
  fi
@@ -112,13 +125,13 @@ SYSTEM_NODE=$(command -v node 2>/dev/null || true)
112
125
  if [ "${AGENTROAM_BOOTSTRAP_TEST:-}" = "1" ] && [ "${AGENTROAM_FORCE_PRIVATE_NODE:-}" = "1" ]; then
113
126
  SYSTEM_NODE=""
114
127
  fi
115
- if [ -n "$SYSTEM_NODE" ] && node_is_22 "$SYSTEM_NODE"; then
128
+ if [ -n "$SYSTEM_NODE" ] && node_is_supported "$SYSTEM_NODE"; then
116
129
  NODE_BIN="$SYSTEM_NODE"
117
130
  else
118
131
  if [ "${AGENTROAM_BOOTSTRAP_TEST:-}" = "1" ] && [ "${AGENTROAM_FORCE_PRIVATE_NODE:-}" = "1" ]; then
119
132
  NVM_NODE=""
120
133
  else
121
- NVM_NODE=$(find_nvm_node_22 || true)
134
+ NVM_NODE=$(find_nvm_node || true)
122
135
  fi
123
136
  if [ -n "$NVM_NODE" ]; then
124
137
  NODE_BIN="$NVM_NODE"
@@ -184,7 +197,23 @@ find_npm_cli() {
184
197
  }
185
198
 
186
199
  NPM_CLI=$(find_npm_cli)
200
+ case "$AGENTROAM_VERSION_REQUEST" in
201
+ *[0-9])
202
+ # Looks like an exact version — honor the pin as-is.
203
+ AGENTROAM_VERSION="$AGENTROAM_VERSION_REQUEST"
204
+ ;;
205
+ *)
206
+ # A dist-tag (default "preview") — resolve to the newest published version
207
+ # so installs always pick up the latest release without touching this file.
208
+ resolved_version="$("$NODE_BIN" "$NPM_CLI" view "agentroam@$AGENTROAM_VERSION_REQUEST" version --registry "$NPM_REGISTRY" 2>/dev/null | tail -1 | tr -d '[:space:]')"
209
+ [ -n "$resolved_version" ] || fail "could not resolve agentroam@$AGENTROAM_VERSION_REQUEST from $NPM_REGISTRY"
210
+ AGENTROAM_VERSION="$resolved_version"
211
+ printf 'Installing AgentRoam %s (resolved from %s)\n' "$AGENTROAM_VERSION" "$AGENTROAM_VERSION_REQUEST"
212
+ ;;
213
+ esac
187
214
  PACKAGE_SPEC="agentroam@$AGENTROAM_VERSION"
215
+ LAUNCHER_ROOT="$LAUNCHER_PARENT/$AGENTROAM_VERSION"
216
+ LAUNCHER_LOCK="$LAUNCHER_ROOT.lock"
188
217
  EXTRA_PACKAGE_SPECS=""
189
218
  if [ "${AGENTROAM_BOOTSTRAP_TEST:-}" = "1" ] && [ -n "${AGENTROAM_PACKAGE_SPEC:-}" ]; then
190
219
  PACKAGE_SPEC="$AGENTROAM_PACKAGE_SPEC"
@@ -235,3 +264,26 @@ case ":${PATH:-}:" in
235
264
  *":$WRAPPER_DIR:"*) ;;
236
265
  *) printf 'Add this directory to PATH: export PATH="%s:$PATH"\n' "$WRAPPER_DIR" ;;
237
266
  esac
267
+
268
+ # stdin is the script itself when launched with curl | sh. Read the choice
269
+ # from the controlling terminal instead; unattended installs never block.
270
+ desktop_choice="${AGENTROAM_INSTALL_DESKTOP:-ask}"
271
+ if [ "${AGENTROAM_INSTALL_SKIP_SERVICE:-}" = "1" ] && [ "$desktop_choice" = "ask" ]; then desktop_choice=no; fi
272
+ if [ "$desktop_choice" = "ask" ]; then
273
+ desktop_choice=no
274
+ if ( : </dev/tty ) 2>/dev/null; then
275
+ printf '\n是否下载桌面端?用于手机查看和控制电脑桌面 [y/N]:' >/dev/tty
276
+ IFS= read -r desktop_choice </dev/tty || desktop_choice=no
277
+ fi
278
+ fi
279
+ case "$desktop_choice" in
280
+ y|Y|yes|YES|是)
281
+ desktop_helper="$LAUNCHER_ROOT/node_modules/agentroam/bin/desktop-download.mjs"
282
+ if [ -f "$desktop_helper" ]; then
283
+ "$NODE_BIN" "$desktop_helper" "$AGENTROAM_VERSION" || printf 'CLI 安装已完成,桌面端可稍后重新下载。\n'
284
+ else
285
+ printf '当前发布的 CLI 尚未包含桌面下载功能,请在新版发布后重试。CLI 安装已完成。\n'
286
+ fi
287
+ ;;
288
+ *) printf '已跳过桌面端下载,CLI 可正常使用。\n' ;;
289
+ esac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentroam",
3
- "version": "0.2.0-preview.16",
3
+ "version": "0.2.0-preview.18",
4
4
  "description": "Zero-install mobile remote terminal and file console",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "agent-tui": "./bin/agent-tui.mjs"
9
9
  },
10
10
  "engines": {
11
- "node": ">=22 <23"
11
+ "node": ">=22.22.0"
12
12
  },
13
13
  "files": [
14
14
  "bin",
@@ -26,12 +26,12 @@
26
26
  "undici": "^6.25.0"
27
27
  },
28
28
  "optionalDependencies": {
29
- "agentroam-runtime-darwin-arm64": "0.2.0-preview.16",
30
- "agentroam-runtime-win32-x64": "0.2.0-preview.16",
31
- "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.16",
32
- "agentroam-cloudflared-win32-x64": "0.2.0-preview.16",
33
- "agentroam-tui-darwin-arm64": "0.2.0-preview.16",
34
- "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.16"
29
+ "agentroam-runtime-darwin-arm64": "0.2.0-preview.18",
30
+ "agentroam-runtime-win32-x64": "0.2.0-preview.18",
31
+ "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.18",
32
+ "agentroam-cloudflared-win32-x64": "0.2.0-preview.18",
33
+ "agentroam-tui-darwin-arm64": "0.2.0-preview.18",
34
+ "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.18"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",
package/dist/pairing.js DELETED
@@ -1,2 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- export function createPairingSecret(now = Date.now()) { const token = randomBytes(32).toString("base64url"); return { token, hashHex: createHash("sha256").update(token).digest("hex"), expiresAt: new Date(now + 5 * 60 * 1000).toISOString() }; }