agentroam 0.2.0-preview.23 → 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.
- package/bin/node-preflight.mjs +1 -1
- package/dist/args.js +10 -1
- package/dist/args.test.js +9 -0
- package/dist/cli.js +12 -0
- package/dist/local-desktop-url.js +8 -0
- package/dist/platform-packages.js +1 -1
- package/dist/service/runtime-state.js +3 -0
- package/dist/service/runtime-state.test.js +3 -0
- package/dist/service/service-command.js +9 -5
- package/dist/service/service-command.test.js +14 -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.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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { printLocalDesktopUrl } from './local-desktop-url.js';
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { parseArgs } from "./args.js";
|
|
@@ -17,6 +18,7 @@ import { selectRelay } from "./tunnel/relay-orchestrator.js";
|
|
|
17
18
|
import { currentCliPath, startUpdate } from "./update/update-command.js";
|
|
18
19
|
import { runUpdateWorker } from "./update/update-worker.js";
|
|
19
20
|
import { lockInstanceStartup, stopPreviousInstances } from "./instance-takeover.js";
|
|
21
|
+
import { runUnlockServiceCommand } from "./unlock-service.js";
|
|
20
22
|
const VERSION = AGENTROAM_VERSION;
|
|
21
23
|
export async function main(argv) {
|
|
22
24
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -34,6 +36,7 @@ export async function main(argv) {
|
|
|
34
36
|
agentroam revoke <设备ID> 移除一台设备
|
|
35
37
|
agentroam revoke --all 全部退出并取消待用配对码
|
|
36
38
|
agentroam service install|start|stop|restart|status|url|logs|uninstall
|
|
39
|
+
agentroam unlock-service install|uninstall|status 管理 Windows 远程解锁服务(安装时弹出 UAC)
|
|
37
40
|
设备命令支持 --data-dir PATH;配对码有效期 5 分钟,设备授权有效期 30 天。`);
|
|
38
41
|
return;
|
|
39
42
|
}
|
|
@@ -82,6 +85,12 @@ export async function main(argv) {
|
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
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
|
+
}
|
|
85
94
|
if (options.command === "update") {
|
|
86
95
|
if (target !== "darwin-arm64" && target !== "windows-amd64")
|
|
87
96
|
throw new Error(`updates are unavailable for ${target}`);
|
|
@@ -137,6 +146,9 @@ export async function main(argv) {
|
|
|
137
146
|
}
|
|
138
147
|
void runtime.exited.then(() => controller.abort());
|
|
139
148
|
console.log(`✓ Local server: ${runtime.localUrl}/web`);
|
|
149
|
+
await serviceReporter?.localReady(runtime.localUrl);
|
|
150
|
+
if (!serviceReporter)
|
|
151
|
+
printLocalDesktopUrl(runtime.localUrl);
|
|
140
152
|
const lanUrl = options.localOnly ? findLanUrl(runtime.port) ?? runtime.localUrl : runtime.localUrl;
|
|
141
153
|
relay = await selectRelay({
|
|
142
154
|
cli: options,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function printLocalDesktopUrl(localUrl, log = console.log, platform = process.platform) {
|
|
2
|
+
log(`远程桌面授权地址:${localUrl.replace(/\/$/, '')}/web`);
|
|
3
|
+
log('请在运行 CLI 的这台电脑上用浏览器打开此地址,配对后点击“远程授权”(盾牌图标)。');
|
|
4
|
+
if (platform === 'win32')
|
|
5
|
+
log('点击“开启共享”。请保持 Windows 已登录且未锁屏;暂不支持 UAC 安全桌面和管理员窗口控制。');
|
|
6
|
+
else
|
|
7
|
+
log('按提示授予“屏幕录制”和“辅助功能”权限,然后点击“开启共享”。');
|
|
8
|
+
}
|
|
@@ -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.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": {
|
|
@@ -16,6 +16,9 @@ export class ServiceRuntimeReporter {
|
|
|
16
16
|
await removeIfExists(this.paths.urlPath);
|
|
17
17
|
await this.write({ status: "starting" });
|
|
18
18
|
}
|
|
19
|
+
async localReady(localUrl) {
|
|
20
|
+
await this.write({ status: "starting", localUrl });
|
|
21
|
+
}
|
|
19
22
|
async ready(ready) {
|
|
20
23
|
await this.write({ status: "ready", ...ready });
|
|
21
24
|
await writePrivateText(this.paths.urlPath, `${ready.accessUrl}\n`);
|
|
@@ -13,6 +13,9 @@ describe("ServiceRuntimeReporter", () => {
|
|
|
13
13
|
await reporter.starting();
|
|
14
14
|
expect(await readServiceState(paths)).toMatchObject({ status: "starting", pid: 123, version: "0.2.0-preview.9" });
|
|
15
15
|
await expect(access(paths.urlPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
16
|
+
await reporter.localReady("http://127.0.0.1:49157");
|
|
17
|
+
expect(await readServiceState(paths)).toMatchObject({ status: "starting", localUrl: "http://127.0.0.1:49157", pid: 123 });
|
|
18
|
+
await expect(access(paths.urlPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
16
19
|
await reporter.ready({
|
|
17
20
|
localUrl: "http://127.0.0.1:3000",
|
|
18
21
|
publicUrl: "https://example.trycloudflare.com",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { printLocalDesktopUrl } from '../local-desktop-url.js';
|
|
1
2
|
import { delimiter, dirname, isAbsolute, resolve, win32 } from "node:path";
|
|
2
3
|
import { resolveCodexRuntime } from "../codex-runtime-manager.js";
|
|
3
4
|
import { detectPlatform } from "../platform.js";
|
|
@@ -60,13 +61,10 @@ export async function runServiceCommand(options, context) {
|
|
|
60
61
|
log(`✓ AgentRoam service installed: ${definition}`);
|
|
61
62
|
if (state?.status === "ready" && state.version === config.version)
|
|
62
63
|
await cleanupOldLaunchers(config, log);
|
|
64
|
+
if (state?.localUrl && state.status !== "stopped")
|
|
65
|
+
printLocalDesktopUrl(state.localUrl, log, platform);
|
|
63
66
|
if (state?.status === "ready" && state.accessUrl) {
|
|
64
67
|
log(`Open: ${state.accessUrl}`);
|
|
65
|
-
if (state.localUrl) {
|
|
66
|
-
log(`远程桌面授权地址:${state.localUrl.replace(/\/$/, "")}/web`);
|
|
67
|
-
log('请在运行 CLI 的这台电脑上用浏览器打开此地址,配对后点击“远程授权”(盾牌图标)。');
|
|
68
|
-
log('按提示授予“屏幕录制”和“辅助功能”权限,然后点击“开启共享”。');
|
|
69
|
-
}
|
|
70
68
|
await (context.pairingPrinter ?? printPairingCode)(options.dataDir, log, { accessUrl: state.accessUrl, qr: options.qr, interactive: stdoutIsTTY });
|
|
71
69
|
}
|
|
72
70
|
else
|
|
@@ -78,6 +76,8 @@ export async function runServiceCommand(options, context) {
|
|
|
78
76
|
log("✓ AgentRoam service started");
|
|
79
77
|
if (state?.status === "ready" && state.accessUrl)
|
|
80
78
|
await logAccessUrl(state.accessUrl);
|
|
79
|
+
if (state?.localUrl && state.status !== "stopped")
|
|
80
|
+
printLocalDesktopUrl(state.localUrl, log, platform);
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
case "stop":
|
|
@@ -95,6 +95,8 @@ export async function runServiceCommand(options, context) {
|
|
|
95
95
|
log(`Version: ${status.config.version}`);
|
|
96
96
|
log(`Root: ${status.config.roots.join(", ")}`);
|
|
97
97
|
}
|
|
98
|
+
if (status.running && status.state?.localUrl)
|
|
99
|
+
printLocalDesktopUrl(status.state.localUrl, log, platform);
|
|
98
100
|
if (status.running && status.state?.status === "ready" && status.state.accessUrl) {
|
|
99
101
|
await logAccessUrl(status.state.accessUrl);
|
|
100
102
|
}
|
|
@@ -118,6 +120,8 @@ export async function runServiceCommand(options, context) {
|
|
|
118
120
|
log("✓ AgentRoam service restarted");
|
|
119
121
|
if (state?.status === "ready" && state.accessUrl)
|
|
120
122
|
await logAccessUrl(state.accessUrl);
|
|
123
|
+
if (state?.localUrl && state.status !== "stopped")
|
|
124
|
+
printLocalDesktopUrl(state.localUrl, log, platform);
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
123
127
|
case "uninstall": {
|
|
@@ -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",
|
|
@@ -173,3 +174,16 @@ function options(serviceAction) {
|
|
|
173
174
|
dataDir: resolve("data"),
|
|
174
175
|
};
|
|
175
176
|
}
|
|
177
|
+
it.each(['install', 'start', 'restart', 'status'])('prints the actual Windows local address for %s before tunnel readiness', async (action) => {
|
|
178
|
+
const state = { status: 'starting', localUrl: 'http://127.0.0.1:49157', pid: 123 };
|
|
179
|
+
const log = vi.fn();
|
|
180
|
+
const control = controller({
|
|
181
|
+
install: vi.fn(async () => ({ state, definition: 'task' })), start: vi.fn(async () => state), restart: vi.fn(async () => state),
|
|
182
|
+
status: vi.fn(async () => ({ installed: true, running: true, state, config: null })),
|
|
183
|
+
});
|
|
184
|
+
await runServiceCommand(options(action), { platform: 'win32', arch: 'x64', nodePath: 'C:\\node.exe', cliPath: 'C:\\agentroam.mjs', version: 'test', nodeVersion: '22.22.0', controller: control, log,
|
|
185
|
+
codexResolver: vi.fn(async () => ({ executable: 'C:\\codex.exe', version: 'test', source: 'global' })) });
|
|
186
|
+
expect(log).toHaveBeenCalledWith('远程桌面授权地址:http://127.0.0.1:49157/web');
|
|
187
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining('保持 Windows 已登录'));
|
|
188
|
+
expect(log.mock.calls.some(([line]) => line.includes('屏幕录制') || line.includes('辅助功能'))).toBe(false);
|
|
189
|
+
});
|
|
@@ -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.25" },
|
|
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.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.
|
|
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.
|
|
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.
|
|
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.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",
|