agentroam 0.2.0-preview.24 → 0.2.0-preview.25

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.
@@ -7,7 +7,7 @@ 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
- const RELEASE_VERSION = "0.2.0-preview.24";
10
+ const RELEASE_VERSION = "0.2.0-preview.25";
11
11
  const RELEASE_BASE_URL = `https://gitee.com/caoqu/team-agent/releases/download/v${RELEASE_VERSION}`;
12
12
  const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
13
13
 
package/dist/args.js CHANGED
@@ -3,10 +3,11 @@ import { resolve } from "node:path";
3
3
  export function parseArgs(argv) {
4
4
  const values = [...argv];
5
5
  let command = "start";
6
- if (values[0] && ["start", "doctor", "version", "service", "update", "update-worker", "pair", "devices", "revoke", "approvals", "approve", "deny", "lock", "unlock", "audit"].includes(values[0])) {
6
+ if (values[0] && ["start", "doctor", "version", "service", "unlock-service", "update", "update-worker", "pair", "devices", "revoke", "approvals", "approve", "deny", "lock", "unlock", "audit"].includes(values[0])) {
7
7
  command = values.shift();
8
8
  }
9
9
  let serviceAction = null;
10
+ let unlockServiceAction = null;
10
11
  if (command === "service") {
11
12
  const action = values.shift();
12
13
  if (!action || !["install", "start", "stop", "status", "url", "logs", "restart", "uninstall"].includes(action)) {
@@ -17,6 +18,13 @@ export function parseArgs(argv) {
17
18
  throw cliError(`service ${serviceAction} does not accept options`);
18
19
  }
19
20
  }
21
+ if (command === "unlock-service") {
22
+ const action = values.shift();
23
+ if (!action || !["install", "uninstall", "status"].includes(action) || values.length > 0) {
24
+ throw cliError("unlock-service requires install, uninstall, or status and accepts no options");
25
+ }
26
+ unlockServiceAction = action;
27
+ }
20
28
  let revokeDeviceId = null;
21
29
  let revokeAll = false;
22
30
  if (command === "revoke") {
@@ -47,6 +55,7 @@ export function parseArgs(argv) {
47
55
  const options = {
48
56
  command,
49
57
  serviceAction,
58
+ unlockServiceAction,
50
59
  roots: [],
51
60
  port: null,
52
61
  relay: "auto",
package/dist/args.test.js CHANGED
@@ -6,9 +6,18 @@ describe("parseArgs", () => {
6
6
  const value = parseArgs([]);
7
7
  expect(value.command).toBe("start");
8
8
  expect(value.serviceAction).toBeNull();
9
+ expect(value.unlockServiceAction).toBeNull();
9
10
  expect(value.roots).toEqual([process.cwd()]);
10
11
  expect(value.relay).toBe("auto");
11
12
  });
13
+ it("parses Windows unlock service actions", () => {
14
+ for (const action of ["install", "uninstall", "status"]) {
15
+ expect(parseArgs(["unlock-service", action])).toMatchObject({ command: "unlock-service", unlockServiceAction: action });
16
+ }
17
+ expect(() => parseArgs(["unlock-service"])).toThrow("requires install");
18
+ expect(() => parseArgs(["unlock-service", "restart"])).toThrow("requires install");
19
+ expect(() => parseArgs(["unlock-service", "status", "--data-dir", "/tmp/x"])).toThrow("accepts no options");
20
+ });
12
21
  it("parses every supported relay", () => {
13
22
  expect(parseArgs(["--relay", "cloudflare"]).relay).toBe("cloudflare");
14
23
  expect(parseArgs(["--relay", "pinggy"]).relay).toBe("pinggy");
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { selectRelay } from "./tunnel/relay-orchestrator.js";
18
18
  import { currentCliPath, startUpdate } from "./update/update-command.js";
19
19
  import { runUpdateWorker } from "./update/update-worker.js";
20
20
  import { lockInstanceStartup, stopPreviousInstances } from "./instance-takeover.js";
21
+ import { runUnlockServiceCommand } from "./unlock-service.js";
21
22
  const VERSION = AGENTROAM_VERSION;
22
23
  export async function main(argv) {
23
24
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -35,6 +36,7 @@ export async function main(argv) {
35
36
  agentroam revoke <设备ID> 移除一台设备
36
37
  agentroam revoke --all 全部退出并取消待用配对码
37
38
  agentroam service install|start|stop|restart|status|url|logs|uninstall
39
+ agentroam unlock-service install|uninstall|status 管理 Windows 远程解锁服务(安装时弹出 UAC)
38
40
  设备命令支持 --data-dir PATH;配对码有效期 5 分钟,设备授权有效期 30 天。`);
39
41
  return;
40
42
  }
@@ -83,6 +85,12 @@ export async function main(argv) {
83
85
  return;
84
86
  }
85
87
  const target = detectPlatform();
88
+ if (options.command === "unlock-service") {
89
+ if (target !== "windows-amd64")
90
+ throw new Error("远程解锁服务仅支持 Windows 10/11 x64");
91
+ runUnlockServiceCommand(options.unlockServiceAction, resolvePlatformRuntime(target).runtimeRoot);
92
+ return;
93
+ }
86
94
  if (options.command === "update") {
87
95
  if (target !== "darwin-arm64" && target !== "windows-amd64")
88
96
  throw new Error(`updates are unavailable for ${target}`);
@@ -2,7 +2,7 @@ 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
- export const AGENTROAM_VERSION = "0.2.0-preview.24";
5
+ export const AGENTROAM_VERSION = "0.2.0-preview.25";
6
6
  export const PLATFORM_DEPENDENCY_VERSIONS = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).optionalDependencies;
7
7
  export const PLATFORM_PACKAGES = {
8
8
  "darwin-arm64": {
@@ -164,6 +164,7 @@ function options(serviceAction) {
164
164
  return {
165
165
  command: "service",
166
166
  serviceAction,
167
+ unlockServiceAction: null,
167
168
  roots: [resolve("workspace")],
168
169
  port: null,
169
170
  relay: "auto",
@@ -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.24" },
25
+ headers: { "user-agent": "agentroam-readiness/0.2.0-preview.25" },
26
26
  signal: options.signal,
27
27
  });
28
28
  if (response.ok) {
@@ -61,6 +61,7 @@ function cli(relay, localOnly = false) {
61
61
  return {
62
62
  command: "start",
63
63
  serviceAction: null,
64
+ unlockServiceAction: null,
64
65
  roots: [process.cwd()],
65
66
  port: null,
66
67
  relay,
@@ -0,0 +1,97 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ const SERVICE_NAME = "AgentRoamUnlock";
4
+ const DISPLAY_NAME = "AgentRoam Remote Unlock";
5
+ function psQuote(value) {
6
+ return `'${value.replaceAll("'", "''")}'`;
7
+ }
8
+ export function encodePowerShell(script) {
9
+ return Buffer.from(script, "utf16le").toString("base64");
10
+ }
11
+ export function unlockServiceExecutable(runtimeRoot) {
12
+ return resolve(runtimeRoot, "native", "agentroam-remote-unlock.exe");
13
+ }
14
+ export function buildUnlockServiceAdminScript(action, executable) {
15
+ const common = `$ErrorActionPreference = 'Stop'\n$serviceName = '${SERVICE_NAME}'`;
16
+ if (action === "uninstall") {
17
+ return `${common}
18
+ $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
19
+ if ($service) {
20
+ if ($service.Status -ne 'Stopped') { Stop-Service -Name $serviceName -Force -ErrorAction Stop }
21
+ & "$env:SystemRoot\\System32\\sc.exe" delete $serviceName | Out-Null
22
+ if ($LASTEXITCODE -ne 0) { throw "sc.exe delete failed: $LASTEXITCODE" }
23
+ }
24
+ `;
25
+ }
26
+ const binaryPath = `\"${executable}\" --service`;
27
+ return `${common}
28
+ $binary = ${psQuote(executable)}
29
+ if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { throw "Remote unlock helper is missing: $binary" }
30
+ $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
31
+ if ($service) {
32
+ if ($service.Status -ne 'Stopped') { Stop-Service -Name $serviceName -Force -ErrorAction Stop }
33
+ & "$env:SystemRoot\\System32\\sc.exe" delete $serviceName | Out-Null
34
+ if ($LASTEXITCODE -ne 0) { throw "sc.exe delete failed: $LASTEXITCODE" }
35
+ for ($attempt = 0; $attempt -lt 50 -and (Get-Service -Name $serviceName -ErrorAction SilentlyContinue); $attempt++) { Start-Sleep -Milliseconds 100 }
36
+ }
37
+ New-Service -Name $serviceName -BinaryPathName ${psQuote(binaryPath)} -DisplayName '${DISPLAY_NAME}' -StartupType Automatic | Out-Null
38
+ & "$env:SystemRoot\\System32\\sc.exe" description $serviceName "Allows an authorized AgentRoam session to wake and unlock the Windows secure desktop." | Out-Null
39
+ Start-Service -Name $serviceName -ErrorAction Stop
40
+ `;
41
+ }
42
+ export function buildElevatedPowerShell(adminScript) {
43
+ const payload = encodePowerShell(adminScript);
44
+ return `$ErrorActionPreference = 'Stop'
45
+ $powershell = Join-Path $PSHOME 'powershell.exe'
46
+ $process = Start-Process -FilePath $powershell -Verb RunAs -Wait -PassThru -ArgumentList @('-NoLogo','-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-EncodedCommand','${payload}')
47
+ exit $process.ExitCode
48
+ `;
49
+ }
50
+ export function parseUnlockServiceStatus(output) {
51
+ const value = output.trim().split(/\r?\n/).at(-1)?.trim().toLowerCase();
52
+ if (value === "running" || value === "stopped" || value === "missing")
53
+ return value;
54
+ throw new Error("无法读取远程解锁服务状态");
55
+ }
56
+ function runPowerShell(script, runner) {
57
+ return runner("powershell.exe", [
58
+ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
59
+ "-EncodedCommand", encodePowerShell(script),
60
+ ], { encoding: "utf8", windowsHide: true });
61
+ }
62
+ export function queryUnlockServiceStatus(runner = spawnSync) {
63
+ const script = `$service = Get-Service -Name '${SERVICE_NAME}' -ErrorAction SilentlyContinue
64
+ if (-not $service) { Write-Output 'missing' } elseif ($service.Status -eq 'Running') { Write-Output 'running' } else { Write-Output 'stopped' }
65
+ `;
66
+ const result = runPowerShell(script, runner);
67
+ if (result.error)
68
+ throw result.error;
69
+ if (result.status !== 0)
70
+ throw new Error((result.stderr || "远程解锁服务状态查询失败").trim());
71
+ return parseUnlockServiceStatus(result.stdout || "");
72
+ }
73
+ export function runUnlockServiceCommand(action, runtimeRoot, options = {}) {
74
+ const platform = options.platform ?? process.platform;
75
+ const runner = options.runner ?? spawnSync;
76
+ const log = options.log ?? console.log;
77
+ if (platform !== "win32")
78
+ throw new Error("远程解锁服务仅支持 Windows 10/11 x64");
79
+ if (action === "status") {
80
+ const status = queryUnlockServiceStatus(runner);
81
+ log(status === "running" ? "远程解锁服务正在运行" : status === "stopped" ? "远程解锁服务已安装但未运行" : "远程解锁服务未安装");
82
+ return;
83
+ }
84
+ const executable = unlockServiceExecutable(runtimeRoot);
85
+ const result = runPowerShell(buildElevatedPowerShell(buildUnlockServiceAdminScript(action, executable)), runner);
86
+ if (result.error)
87
+ throw result.error;
88
+ if (result.status !== 0) {
89
+ throw new Error((result.stderr || (action === "install" ? "远程解锁服务安装失败或已取消管理员授权" : "远程解锁服务卸载失败或已取消管理员授权")).trim());
90
+ }
91
+ const status = queryUnlockServiceStatus(runner);
92
+ if (action === "install" && status !== "running")
93
+ throw new Error("远程解锁服务安装后未能启动");
94
+ if (action === "uninstall" && status !== "missing")
95
+ throw new Error("远程解锁服务卸载后仍然存在");
96
+ log(action === "install" ? "远程解锁服务已安装并启动" : "远程解锁服务已卸载");
97
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { buildElevatedPowerShell, buildUnlockServiceAdminScript, encodePowerShell, parseUnlockServiceStatus, runUnlockServiceCommand, unlockServiceExecutable, } from "./unlock-service.js";
3
+ function result(stdout = "", status = 0, stderr = "") {
4
+ return { pid: 1, output: [null, stdout, stderr], stdout, stderr, status, signal: null, error: undefined };
5
+ }
6
+ describe("Windows unlock service command", () => {
7
+ it("builds an elevated install script without putting credentials on disk or the command line", () => {
8
+ const executable = "C:\\Program Files\\AgentRoam\\agentroam-remote-unlock.exe";
9
+ const admin = buildUnlockServiceAdminScript("install", executable);
10
+ expect(admin).toContain("New-Service");
11
+ expect(admin).toContain("AgentRoamUnlock");
12
+ expect(admin).toContain(`'${executable}'`);
13
+ expect(admin).toContain('"C:\\Program Files\\AgentRoam\\agentroam-remote-unlock.exe" --service');
14
+ const elevated = buildElevatedPowerShell(admin);
15
+ expect(elevated).toContain("-Verb RunAs");
16
+ expect(elevated).toContain("-EncodedCommand");
17
+ expect(Buffer.from(encodePowerShell(admin), "base64").toString("utf16le")).toBe(admin);
18
+ });
19
+ it("reports localized status messages and validates post-install state", () => {
20
+ const runner = vi.fn()
21
+ .mockReturnValueOnce(result())
22
+ .mockReturnValueOnce(result("running\r\n"));
23
+ const log = vi.fn();
24
+ runUnlockServiceCommand("install", "C:\\runtime", { platform: "win32", runner, log });
25
+ expect(unlockServiceExecutable("C:\\runtime")).toMatch(/agentroam-remote-unlock\.exe$/);
26
+ expect(runner).toHaveBeenCalledTimes(2);
27
+ expect(log).toHaveBeenCalledWith("远程解锁服务已安装并启动");
28
+ });
29
+ it("fails closed on unsupported platforms, canceled elevation, and unknown status output", () => {
30
+ expect(() => runUnlockServiceCommand("status", "/runtime", { platform: "darwin" })).toThrow("仅支持 Windows");
31
+ expect(() => runUnlockServiceCommand("install", "C:\\runtime", {
32
+ platform: "win32",
33
+ runner: () => result("", 1, "canceled"),
34
+ })).toThrow("canceled");
35
+ expect(() => parseUnlockServiceStatus("paused")).toThrow("无法读取");
36
+ });
37
+ });
@@ -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 = "0.2.0-preview.24"; ./install-agentroam.ps1
7
+ # 例如:$env:AGENTROAM_VERSION = "0.2.0-preview.25"; ./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"
@@ -4,7 +4,7 @@ set -eu
4
4
  NODE_VERSION="22.22.0"
5
5
  MINIMUM_NODE_VERSION="22.22.0"
6
6
  # Default to the newest published preview; pin an exact version via
7
- # AGENTROAM_VERSION=0.2.0-preview.24 sh install-agentroam.sh
7
+ # AGENTROAM_VERSION=0.2.0-preview.25 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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentroam",
3
- "version": "0.2.0-preview.24",
3
+ "version": "0.2.0-preview.25",
4
4
  "description": "Zero-install mobile remote terminal and file console",
5
5
  "type": "module",
6
6
  "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.24",
30
- "agentroam-runtime-win32-x64": "0.2.0-preview.24",
31
- "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.24",
32
- "agentroam-cloudflared-win32-x64": "0.2.0-preview.24",
33
- "agentroam-tui-darwin-arm64": "0.2.0-preview.24",
34
- "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.24"
29
+ "agentroam-runtime-darwin-arm64": "0.2.0-preview.25",
30
+ "agentroam-runtime-win32-x64": "0.2.0-preview.25",
31
+ "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.25",
32
+ "agentroam-cloudflared-win32-x64": "0.2.0-preview.25",
33
+ "agentroam-tui-darwin-arm64": "0.2.0-preview.25",
34
+ "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.25"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",