agentroam 0.2.0-preview.24 → 0.2.0-preview.26
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/bin/node-preflight.mjs +1 -1
- package/dist/args.js +10 -1
- package/dist/args.test.js +9 -0
- package/dist/cli.js +15 -0
- package/dist/error-log.js +113 -0
- package/dist/platform-packages.js +1 -1
- package/dist/runtime-manager.js +3 -0
- package/dist/service/service-command.test.js +1 -0
- package/dist/tunnel/public-readiness.js +1 -1
- package/dist/tunnel/relay-orchestrator.test.js +1 -0
- package/dist/unlock-service.js +97 -0
- package/dist/unlock-service.test.js +37 -0
- package/install/install-agentroam.ps1 +1 -1
- package/install/install-agentroam.sh +1 -1
- package/package.json +7 -7
package/bin/node-preflight.mjs
CHANGED
|
@@ -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.
|
|
10
|
+
const RELEASE_VERSION = "0.2.0-preview.26";
|
|
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
|
@@ -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";
|
|
@@ -18,6 +19,8 @@ import { selectRelay } from "./tunnel/relay-orchestrator.js";
|
|
|
18
19
|
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";
|
|
22
|
+
import { runUnlockServiceCommand } from "./unlock-service.js";
|
|
23
|
+
import { createCliErrorLog } from "./error-log.js";
|
|
21
24
|
const VERSION = AGENTROAM_VERSION;
|
|
22
25
|
export async function main(argv) {
|
|
23
26
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -35,10 +38,16 @@ export async function main(argv) {
|
|
|
35
38
|
agentroam revoke <设备ID> 移除一台设备
|
|
36
39
|
agentroam revoke --all 全部退出并取消待用配对码
|
|
37
40
|
agentroam service install|start|stop|restart|status|url|logs|uninstall
|
|
41
|
+
agentroam unlock-service install|uninstall|status 管理 Windows 远程解锁服务(安装时弹出 UAC)
|
|
38
42
|
设备命令支持 --data-dir PATH;配对码有效期 5 分钟,设备授权有效期 30 天。`);
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
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();
|
|
42
51
|
if (["pair", "devices", "revoke", "approvals", "approve", "deny", "lock", "unlock", "audit"].includes(options.command)) {
|
|
43
52
|
const dataDir = argv.includes("--data-dir") ? options.dataDir : await installedDataDir(options.dataDir);
|
|
44
53
|
if (options.command === "pair")
|
|
@@ -83,6 +92,12 @@ export async function main(argv) {
|
|
|
83
92
|
return;
|
|
84
93
|
}
|
|
85
94
|
const target = detectPlatform();
|
|
95
|
+
if (options.command === "unlock-service") {
|
|
96
|
+
if (target !== "windows-amd64")
|
|
97
|
+
throw new Error("远程解锁服务仅支持 Windows 10/11 x64");
|
|
98
|
+
runUnlockServiceCommand(options.unlockServiceAction, resolvePlatformRuntime(target).runtimeRoot);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
86
101
|
if (options.command === "update") {
|
|
87
102
|
if (target !== "darwin-arm64" && target !== "windows-amd64")
|
|
88
103
|
throw new Error(`updates are unavailable for ${target}`);
|
|
@@ -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,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.
|
|
5
|
+
export const AGENTROAM_VERSION = "0.2.0-preview.26";
|
|
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": {
|
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,
|
|
@@ -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.
|
|
25
|
+
headers: { "user-agent": "agentroam-readiness/0.2.0-preview.26" },
|
|
26
26
|
signal: options.signal,
|
|
27
27
|
});
|
|
28
28
|
if (response.ok) {
|
|
@@ -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.
|
|
7
|
+
# 例如:$env:AGENTROAM_VERSION = "0.2.0-preview.26"; ./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.
|
|
7
|
+
# AGENTROAM_VERSION=0.2.0-preview.26 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.
|
|
3
|
+
"version": "0.2.0-preview.26",
|
|
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.
|
|
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.26",
|
|
30
|
+
"agentroam-runtime-win32-x64": "0.2.0-preview.26",
|
|
31
|
+
"agentroam-cloudflared-darwin-arm64": "0.2.0-preview.26",
|
|
32
|
+
"agentroam-cloudflared-win32-x64": "0.2.0-preview.26",
|
|
33
|
+
"agentroam-tui-darwin-arm64": "0.2.0-preview.26",
|
|
34
|
+
"@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.26"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|