agentroam 0.2.0-preview.14 → 0.2.0-preview.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/bin/node-preflight.mjs +1 -1
- package/dist/args.js +17 -2
- package/dist/args.test.js +5 -0
- package/dist/cli.js +13 -0
- package/dist/platform-packages.js +1 -1
- package/dist/runtime-manager.js +4 -0
- package/dist/tunnel/public-readiness.js +1 -1
- package/dist/update/update-channel.js +38 -0
- package/dist/update/update-command.js +102 -0
- package/dist/update/update-command.test.js +70 -0
- package/dist/update/update-state.js +43 -0
- package/dist/update/update-worker.js +71 -0
- package/install/README.md +3 -3
- package/install/install-agentroam.ps1 +1 -1
- package/install/install-agentroam.sh +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -20,20 +20,20 @@ npx agentroam@preview
|
|
|
20
20
|
Or install from tarball:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm install ./agentroam-cloudflared-darwin-arm64-0.2.0-preview.
|
|
24
|
-
./agentroam-runtime-darwin-arm64-0.2.0-preview.
|
|
25
|
-
./agentroam-tui-darwin-arm64-0.2.0-preview.
|
|
26
|
-
./agentroam-0.2.0-preview.
|
|
23
|
+
npm install ./agentroam-cloudflared-darwin-arm64-0.2.0-preview.16.tgz \
|
|
24
|
+
./agentroam-runtime-darwin-arm64-0.2.0-preview.16.tgz \
|
|
25
|
+
./agentroam-tui-darwin-arm64-0.2.0-preview.16.tgz \
|
|
26
|
+
./agentroam-0.2.0-preview.16.tgz
|
|
27
27
|
npx agentroam
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Windows PowerShell:
|
|
31
31
|
|
|
32
32
|
```powershell
|
|
33
|
-
npm install ./agentroam-cloudflared-win32-x64-0.2.0-preview.
|
|
34
|
-
./agentroam-runtime-win32-x64-0.2.0-preview.
|
|
35
|
-
./caoqu-agentroam-tui-win32-x64-0.2.0-preview.
|
|
36
|
-
./agentroam-0.2.0-preview.
|
|
33
|
+
npm install ./agentroam-cloudflared-win32-x64-0.2.0-preview.16.tgz `
|
|
34
|
+
./agentroam-runtime-win32-x64-0.2.0-preview.16.tgz `
|
|
35
|
+
./caoqu-agentroam-tui-win32-x64-0.2.0-preview.16.tgz `
|
|
36
|
+
./agentroam-0.2.0-preview.16.tgz
|
|
37
37
|
npx agentroam
|
|
38
38
|
```
|
|
39
39
|
|
package/bin/node-preflight.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { promisify } from "node:util";
|
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
const MANAGED_NODE_VERSION = "22.22.0";
|
|
10
|
-
const RELEASE_VERSION = "0.2.0-preview.
|
|
10
|
+
const RELEASE_VERSION = "0.2.0-preview.16";
|
|
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,7 +3,7 @@ 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"].includes(values[0])) {
|
|
6
|
+
if (values[0] && ["start", "doctor", "version", "service", "update", "update-worker"].includes(values[0])) {
|
|
7
7
|
command = values.shift();
|
|
8
8
|
}
|
|
9
9
|
let serviceAction = null;
|
|
@@ -17,6 +17,16 @@ export function parseArgs(argv) {
|
|
|
17
17
|
throw cliError(`service ${serviceAction} does not accept options`);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
let updateVersion = null;
|
|
21
|
+
let updateStateFile = null;
|
|
22
|
+
if (command === "update" && values[0] && !values[0].startsWith("--"))
|
|
23
|
+
updateVersion = values.shift();
|
|
24
|
+
if (command === "update-worker") {
|
|
25
|
+
const stateFile = values.shift();
|
|
26
|
+
if (!stateFile || values.length > 0)
|
|
27
|
+
throw cliError("update-worker requires one state file");
|
|
28
|
+
updateStateFile = resolve(stateFile);
|
|
29
|
+
}
|
|
20
30
|
const options = {
|
|
21
31
|
command,
|
|
22
32
|
serviceAction,
|
|
@@ -27,8 +37,10 @@ export function parseArgs(argv) {
|
|
|
27
37
|
localOnly: false,
|
|
28
38
|
qr: true,
|
|
29
39
|
dataDir: resolve(homedir(), ".agentroam"),
|
|
40
|
+
updateVersion,
|
|
41
|
+
updateStateFile,
|
|
30
42
|
};
|
|
31
|
-
for (let index = 0; index < values.length; index++) {
|
|
43
|
+
for (let index = 0; index < values.length && command !== "update-worker"; index++) {
|
|
32
44
|
const arg = values[index];
|
|
33
45
|
const next = () => {
|
|
34
46
|
const value = values[++index];
|
|
@@ -64,6 +76,9 @@ export function parseArgs(argv) {
|
|
|
64
76
|
else
|
|
65
77
|
throw cliError(`unknown argument: ${arg}`);
|
|
66
78
|
}
|
|
79
|
+
if (command === "update" && updateVersion && !/^\d+\.\d+\.\d+$/.test(updateVersion)) {
|
|
80
|
+
throw cliError("update version must be an exact stable X.Y.Z version");
|
|
81
|
+
}
|
|
67
82
|
if (!options.roots.length)
|
|
68
83
|
options.roots = [process.cwd()];
|
|
69
84
|
if (options.relay === "custom" && !options.tunnelCommand)
|
package/dist/args.test.js
CHANGED
|
@@ -51,4 +51,9 @@ describe("parseArgs", () => {
|
|
|
51
51
|
expect(() => parseArgs(["service", "start", "--port", "3000"])).toThrow("does not accept options");
|
|
52
52
|
expect(() => parseArgs(["service", "stop", "--local-only"])).toThrow("does not accept options");
|
|
53
53
|
});
|
|
54
|
+
it("parses update with an optional exact stable version", () => {
|
|
55
|
+
expect(parseArgs(["update"]).updateVersion).toBeNull();
|
|
56
|
+
expect(parseArgs(["update", "1.2.3", "--data-dir", "./data"])).toMatchObject({ command: "update", updateVersion: "1.2.3" });
|
|
57
|
+
expect(() => parseArgs(["update", "1.2.3-preview.1"])).toThrow("exact stable");
|
|
58
|
+
});
|
|
54
59
|
});
|
package/dist/cli.js
CHANGED
|
@@ -15,9 +15,15 @@ import { ServiceRuntimeReporter } from "./service/runtime-state.js";
|
|
|
15
15
|
import { runServiceCommand } from "./service/service-command.js";
|
|
16
16
|
import { resolveServicePaths } from "./service/service-files.js";
|
|
17
17
|
import { selectRelay } from "./tunnel/relay-orchestrator.js";
|
|
18
|
+
import { currentCliPath, startUpdate } from "./update/update-command.js";
|
|
19
|
+
import { runUpdateWorker } from "./update/update-worker.js";
|
|
18
20
|
const VERSION = AGENTROAM_VERSION;
|
|
19
21
|
export async function main(argv) {
|
|
20
22
|
const options = parseArgs(argv);
|
|
23
|
+
if (options.command === "update-worker") {
|
|
24
|
+
await runUpdateWorker(options.updateStateFile);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
21
27
|
if (options.command === "service") {
|
|
22
28
|
await runServiceCommand(options, {
|
|
23
29
|
nodePath: process.execPath,
|
|
@@ -27,6 +33,13 @@ export async function main(argv) {
|
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
29
35
|
const target = detectPlatform();
|
|
36
|
+
if (options.command === "update") {
|
|
37
|
+
if (target !== "darwin-arm64" && target !== "windows-amd64")
|
|
38
|
+
throw new Error(`updates are unavailable for ${target}`);
|
|
39
|
+
const state = await startUpdate({ currentVersion: VERSION, requestedVersion: options.updateVersion ?? null, dataDir: options.dataDir, target, cliPath: currentCliPath() });
|
|
40
|
+
console.log(`AgentRoam ${state.targetVersion} update started in the background.`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
30
43
|
if (options.command === "version") {
|
|
31
44
|
console.log(`agentroam ${VERSION}`);
|
|
32
45
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
export const AGENTROAM_VERSION = "0.2.0-preview.
|
|
4
|
+
export const AGENTROAM_VERSION = "0.2.0-preview.16";
|
|
5
5
|
export const PLATFORM_PACKAGES = {
|
|
6
6
|
"darwin-arm64": {
|
|
7
7
|
runtime: "agentroam-runtime-darwin-arm64",
|
package/dist/runtime-manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import { delimiter, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { resolveCodexRuntime, } from "./codex-runtime-manager.js";
|
|
4
5
|
import { repairNativeRuntimePermissions } from "./native-runtime.js";
|
|
5
6
|
import { resolveOpenCodeRuntime, } from "./opencode-runtime-manager.js";
|
|
@@ -39,6 +40,9 @@ export class RuntimeManager {
|
|
|
39
40
|
AGENT_TRUST_TUNNEL_PROXY: "1",
|
|
40
41
|
AGENT_PAIRING_HASH: pairing?.hashHex || "",
|
|
41
42
|
AGENT_PAIRING_EXPIRES_AT: pairing?.expiresAt || "",
|
|
43
|
+
AGENTROAM_VERSION: resolvePlatformRuntime(target).manifest.packageVersion,
|
|
44
|
+
AGENTROAM_DATA_DIR: dataDir,
|
|
45
|
+
AGENTROAM_CLI_PATH: fileURLToPath(new URL("../bin/agentroam.mjs", import.meta.url)),
|
|
42
46
|
},
|
|
43
47
|
});
|
|
44
48
|
const log = (stream) => stream?.on("data", (chunk) => process.stderr.write(chunk));
|
|
@@ -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.16" },
|
|
26
26
|
signal: options.signal,
|
|
27
27
|
});
|
|
28
28
|
if (response.ok) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-preview\.(0|[1-9]\d*))?$/;
|
|
2
|
+
export function parseAgentRoamVersion(value) {
|
|
3
|
+
const match = VERSION.exec(value);
|
|
4
|
+
if (!match)
|
|
5
|
+
return null;
|
|
6
|
+
return {
|
|
7
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
8
|
+
preview: match[4] === undefined ? null : Number(match[4]),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
// A prerelease installation follows the preview channel; a stable installation
|
|
12
|
+
// follows latest. Preview users are also offered a newer stable release so they
|
|
13
|
+
// can migrate to the stable track without reinstalling.
|
|
14
|
+
export function resolveUpdateChannel(currentVersion) {
|
|
15
|
+
const parsed = parseAgentRoamVersion(currentVersion);
|
|
16
|
+
if (!parsed)
|
|
17
|
+
throw new Error("invalid AgentRoam version");
|
|
18
|
+
return parsed.preview === null ? "latest" : "preview";
|
|
19
|
+
}
|
|
20
|
+
export function resolveReleaseChannel(version) {
|
|
21
|
+
const parsed = parseAgentRoamVersion(version);
|
|
22
|
+
if (!parsed)
|
|
23
|
+
throw new Error("invalid AgentRoam version");
|
|
24
|
+
return parsed.preview === null ? "latest" : "preview";
|
|
25
|
+
}
|
|
26
|
+
export function registryUrlsForChannel(channel, registryBase) {
|
|
27
|
+
return channel === "preview" ? [`${registryBase}/latest`, `${registryBase}/preview`] : [`${registryBase}/latest`];
|
|
28
|
+
}
|
|
29
|
+
export function parseChannelVersion(value, channel) {
|
|
30
|
+
if (typeof value !== "string")
|
|
31
|
+
return null;
|
|
32
|
+
const parsed = parseAgentRoamVersion(value);
|
|
33
|
+
if (!parsed)
|
|
34
|
+
return null;
|
|
35
|
+
if (parsed.preview === null)
|
|
36
|
+
return value;
|
|
37
|
+
return channel === "preview" ? value : null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { updatePaths, writeUpdateState } from "./update-state.js";
|
|
7
|
+
import { parseChannelVersion, registryUrlsForChannel, resolveReleaseChannel, resolveUpdateChannel, } from "./update-channel.js";
|
|
8
|
+
const REGISTRY_BASE = "https://registry.npmjs.org/agentroam";
|
|
9
|
+
const RELEASE_BASE = "https://gitee.com/caoqu/team-agent/releases/download";
|
|
10
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
11
|
+
export async function resolveUpdate(options, fetchImpl = fetch) {
|
|
12
|
+
const channel = resolveUpdateChannel(options.currentVersion);
|
|
13
|
+
const target = await readRegistryCandidate(fetchImpl, channel);
|
|
14
|
+
if (!target)
|
|
15
|
+
throw new Error("unable to check the npm release channel");
|
|
16
|
+
if (options.requestedVersion && options.requestedVersion !== target)
|
|
17
|
+
throw new Error("requested version is not the current npm candidate");
|
|
18
|
+
if (compare(target, options.currentVersion) <= 0)
|
|
19
|
+
throw new Error("AgentRoam is already up to date");
|
|
20
|
+
const response = await fetchWithTimeout(fetchImpl, `${RELEASE_BASE}/v${target}/release-manifest.json`);
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
throw new Error("release manifest is unavailable");
|
|
23
|
+
const manifest = await response.json();
|
|
24
|
+
const asset = manifest?.installers?.cli?.[options.target];
|
|
25
|
+
const fileName = options.target === "darwin-arm64" ? "install-agentroam.sh" : "install-agentroam.ps1";
|
|
26
|
+
if (manifest?.schemaVersion !== 2 || manifest.version !== target || manifest.channel !== resolveReleaseChannel(target) || asset?.fileName !== fileName || !SHA.test(asset?.sha256 ?? "")) {
|
|
27
|
+
throw new Error("release manifest is invalid for this platform");
|
|
28
|
+
}
|
|
29
|
+
return { targetVersion: target, fileName, sha256: asset.sha256 };
|
|
30
|
+
}
|
|
31
|
+
// Preview installations follow the higher of the preview and latest npm
|
|
32
|
+
// dist-tags so a newer stable release can move them onto the stable track.
|
|
33
|
+
async function readRegistryCandidate(fetchImpl, channel) {
|
|
34
|
+
let best = null;
|
|
35
|
+
for (const url of registryUrlsForChannel(channel, REGISTRY_BASE)) {
|
|
36
|
+
const response = await fetchWithTimeout(fetchImpl, url).catch(() => null);
|
|
37
|
+
if (!response?.ok)
|
|
38
|
+
continue;
|
|
39
|
+
const candidate = parseChannelVersion((await response.json()).version, channel);
|
|
40
|
+
if (!candidate)
|
|
41
|
+
continue;
|
|
42
|
+
if (!best || compare(candidate, best) > 0)
|
|
43
|
+
best = candidate;
|
|
44
|
+
}
|
|
45
|
+
return best;
|
|
46
|
+
}
|
|
47
|
+
export async function startUpdate(options) {
|
|
48
|
+
const release = await resolveUpdate(options);
|
|
49
|
+
const paths = updatePaths(options.dataDir);
|
|
50
|
+
const service = await readServiceConfig();
|
|
51
|
+
const state = {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
phase: "downloading",
|
|
54
|
+
currentVersion: options.currentVersion,
|
|
55
|
+
...release,
|
|
56
|
+
dataDir: resolve(options.dataDir),
|
|
57
|
+
roots: service?.roots?.length ? service.roots : [process.cwd()],
|
|
58
|
+
port: service?.port ?? null,
|
|
59
|
+
relay: service?.relay ?? "auto",
|
|
60
|
+
tunnelCommand: service?.tunnelCommand ?? null,
|
|
61
|
+
localOnly: service?.localOnly ?? false,
|
|
62
|
+
cliPath: resolve(options.cliPath),
|
|
63
|
+
updatedAt: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
await writeUpdateState(paths.stateFile, state);
|
|
66
|
+
const child = spawn(process.execPath, [options.cliPath, "update-worker", paths.stateFile], { detached: true, stdio: "ignore", windowsHide: true });
|
|
67
|
+
child.unref();
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
async function readServiceConfig() {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(await readFile(resolve(homedir(), ".agentroam", "service", "config.json"), "utf8"));
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
async function fetchWithTimeout(fetchImpl, url) {
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(), 4_000);
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
try {
|
|
82
|
+
return await fetchImpl(url, { headers: { accept: "application/json" }, signal: controller.signal });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function compare(left, right) {
|
|
89
|
+
const parse = (value) => {
|
|
90
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-preview\.(\d+))?$/.exec(value);
|
|
91
|
+
if (!match)
|
|
92
|
+
throw new Error("invalid AgentRoam version");
|
|
93
|
+
return [...match.slice(1, 4).map(Number), match[4] === undefined ? Number.MAX_SAFE_INTEGER : Number(match[4])];
|
|
94
|
+
};
|
|
95
|
+
const a = parse(left);
|
|
96
|
+
const b = parse(right);
|
|
97
|
+
for (let i = 0; i < a.length; i += 1)
|
|
98
|
+
if (a[i] !== b[i])
|
|
99
|
+
return a[i] > b[i] ? 1 : -1;
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
export const currentCliPath = () => fileURLToPath(new URL("../../bin/agentroam.mjs", import.meta.url));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdtemp } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { resolveUpdate } from "./update-command.js";
|
|
6
|
+
import { acquireUpdateLock, readUpdateState, updatePaths, writeUpdateState } from "./update-state.js";
|
|
7
|
+
const sha256 = "c".repeat(64);
|
|
8
|
+
const response = (body) => new Response(JSON.stringify(body), { status: 200 });
|
|
9
|
+
describe("resolveUpdate", () => {
|
|
10
|
+
it("accepts only npm latest with an audited Gitee installer", async () => {
|
|
11
|
+
const fetcher = vi.fn()
|
|
12
|
+
.mockResolvedValueOnce(response({ version: "1.2.3" }))
|
|
13
|
+
.mockResolvedValueOnce(response({ schemaVersion: 2, version: "1.2.3", channel: "latest", installers: { cli: { "windows-amd64": { fileName: "install-agentroam.ps1", sha256 } } } }));
|
|
14
|
+
await expect(resolveUpdate({ currentVersion: "1.2.2", requestedVersion: null, dataDir: "data", target: "windows-amd64", cliPath: "cli" }, fetcher))
|
|
15
|
+
.resolves.toEqual({ targetVersion: "1.2.3", fileName: "install-agentroam.ps1", sha256 });
|
|
16
|
+
});
|
|
17
|
+
it("rejects a requested version that is not npm latest", async () => {
|
|
18
|
+
const fetcher = vi.fn().mockResolvedValue(response({ version: "1.2.3" }));
|
|
19
|
+
await expect(resolveUpdate({ currentVersion: "1.0.0", requestedVersion: "1.2.2", dataDir: "data", target: "darwin-arm64", cliPath: "cli" }, fetcher)).rejects.toThrow("not the current npm candidate");
|
|
20
|
+
});
|
|
21
|
+
it("follows the preview channel and offers a higher preview", async () => {
|
|
22
|
+
const fetcher = vi.fn((url) => {
|
|
23
|
+
const u = String(url);
|
|
24
|
+
if (u.endsWith("/preview"))
|
|
25
|
+
return Promise.resolve(response({ version: "1.2.3-preview.4" }));
|
|
26
|
+
if (u.endsWith("/latest"))
|
|
27
|
+
return Promise.resolve(response({ version: "1.2.2" }));
|
|
28
|
+
return Promise.resolve(response({ schemaVersion: 2, version: "1.2.3-preview.4", channel: "preview", installers: { cli: { "darwin-arm64": { fileName: "install-agentroam.sh", sha256 } } } }));
|
|
29
|
+
});
|
|
30
|
+
await expect(resolveUpdate({ currentVersion: "1.2.3-preview.3", requestedVersion: null, dataDir: "data", target: "darwin-arm64", cliPath: "cli" }, fetcher))
|
|
31
|
+
.resolves.toEqual({ targetVersion: "1.2.3-preview.4", fileName: "install-agentroam.sh", sha256 });
|
|
32
|
+
});
|
|
33
|
+
it("offers a newer stable release to preview users", async () => {
|
|
34
|
+
const fetcher = vi.fn((url) => {
|
|
35
|
+
const u = String(url);
|
|
36
|
+
if (u.endsWith("/preview"))
|
|
37
|
+
return Promise.resolve(response({ version: "1.2.3-preview.4" }));
|
|
38
|
+
if (u.endsWith("/latest"))
|
|
39
|
+
return Promise.resolve(response({ version: "1.2.4" }));
|
|
40
|
+
return Promise.resolve(response({ schemaVersion: 2, version: "1.2.4", channel: "latest", installers: { cli: { "darwin-arm64": { fileName: "install-agentroam.sh", sha256 } } } }));
|
|
41
|
+
});
|
|
42
|
+
await expect(resolveUpdate({ currentVersion: "1.2.3-preview.4", requestedVersion: null, dataDir: "data", target: "darwin-arm64", cliPath: "cli" }, fetcher))
|
|
43
|
+
.resolves.toEqual({ targetVersion: "1.2.4", fileName: "install-agentroam.sh", sha256 });
|
|
44
|
+
});
|
|
45
|
+
it("stable channel never queries the preview tag", async () => {
|
|
46
|
+
const fetcher = vi.fn((url) => {
|
|
47
|
+
const u = String(url);
|
|
48
|
+
expect(u).not.toContain("/preview");
|
|
49
|
+
if (u.endsWith("/latest"))
|
|
50
|
+
return Promise.resolve(response({ version: "1.2.4" }));
|
|
51
|
+
return Promise.resolve(response({ schemaVersion: 2, version: "1.2.4", channel: "latest", installers: { cli: { "darwin-arm64": { fileName: "install-agentroam.sh", sha256 } } } }));
|
|
52
|
+
});
|
|
53
|
+
await resolveUpdate({ currentVersion: "1.2.3", requestedVersion: null, dataDir: "data", target: "darwin-arm64", cliPath: "cli" }, fetcher);
|
|
54
|
+
expect(fetcher.mock.calls.map((call) => String(call[0])).some((url) => url.includes("/preview"))).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it("writes durable state atomically and excludes concurrent workers", async () => {
|
|
57
|
+
const dataDir = await mkdtemp(resolve(tmpdir(), "agentroam-update-state-"));
|
|
58
|
+
const paths = updatePaths(dataDir);
|
|
59
|
+
const state = {
|
|
60
|
+
schemaVersion: 1, phase: "downloading", currentVersion: "1.0.0", targetVersion: "1.1.0",
|
|
61
|
+
dataDir, roots: ["D:\\project"], port: 3000, relay: "auto", tunnelCommand: null,
|
|
62
|
+
localOnly: false, fileName: "install-agentroam.ps1", sha256, cliPath: "C:\\agentroam.mjs", updatedAt: 1,
|
|
63
|
+
};
|
|
64
|
+
await writeUpdateState(paths.stateFile, state);
|
|
65
|
+
await expect(readUpdateState(paths.stateFile)).resolves.toEqual(state);
|
|
66
|
+
const release = await acquireUpdateLock(paths.lockFile);
|
|
67
|
+
await expect(acquireUpdateLock(paths.lockFile)).rejects.toThrow("already running");
|
|
68
|
+
await release();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
export function updatePaths(dataDir) {
|
|
4
|
+
const directory = resolve(dataDir, "updates");
|
|
5
|
+
return { directory, stateFile: resolve(directory, "state.json"), lockFile: resolve(directory, "update.lock") };
|
|
6
|
+
}
|
|
7
|
+
export async function readUpdateState(path) {
|
|
8
|
+
try {
|
|
9
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
10
|
+
return value?.schemaVersion === 1 ? value : null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function writeUpdateState(path, state) {
|
|
17
|
+
await mkdir(dirname(path), { recursive: true });
|
|
18
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
19
|
+
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
20
|
+
await rename(temp, path);
|
|
21
|
+
}
|
|
22
|
+
export async function acquireUpdateLock(path, now = Date.now()) {
|
|
23
|
+
await mkdir(dirname(path), { recursive: true });
|
|
24
|
+
try {
|
|
25
|
+
const handle = await open(path, "wx", 0o600);
|
|
26
|
+
await handle.writeFile(`${process.pid}\n${now}\n`);
|
|
27
|
+
await handle.close();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== "EEXIST")
|
|
31
|
+
throw error;
|
|
32
|
+
const age = now - (await stat(path)).mtimeMs;
|
|
33
|
+
if (age <= 30 * 60 * 1_000)
|
|
34
|
+
throw new Error("an AgentRoam update is already running");
|
|
35
|
+
await unlink(path);
|
|
36
|
+
return acquireUpdateLock(path, now);
|
|
37
|
+
}
|
|
38
|
+
return async () => { await unlink(path).catch(() => undefined); };
|
|
39
|
+
}
|
|
40
|
+
export function sanitizeUpdateError(error) {
|
|
41
|
+
const value = error instanceof Error ? error.message : String(error);
|
|
42
|
+
return value.replace(/[A-Za-z]:\\[^\s]+|\/(?:Users|home|var|tmp)\/[^\s]+/g, "<path>").slice(0, 300);
|
|
43
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { chmod, readFile, unlink } from "node:fs/promises";
|
|
4
|
+
import { basename, resolve } from "node:path";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { acquireUpdateLock, readUpdateState, sanitizeUpdateError, updatePaths, writeUpdateState } from "./update-state.js";
|
|
9
|
+
const RELEASE_BASE = "https://gitee.com/caoqu/team-agent/releases/download";
|
|
10
|
+
export async function runUpdateWorker(stateFile, fetchImpl = fetch) {
|
|
11
|
+
const initial = await readUpdateState(stateFile);
|
|
12
|
+
if (!initial)
|
|
13
|
+
throw new Error("update state is missing");
|
|
14
|
+
const paths = updatePaths(initial.dataDir);
|
|
15
|
+
if (resolve(stateFile) !== paths.stateFile)
|
|
16
|
+
throw new Error("update state is outside the data directory");
|
|
17
|
+
const releaseLock = await acquireUpdateLock(paths.lockFile);
|
|
18
|
+
const partial = resolve(paths.directory, `${basename(initial.fileName)}.partial`);
|
|
19
|
+
let state = initial;
|
|
20
|
+
const update = async (next) => {
|
|
21
|
+
state = { ...state, ...next, updatedAt: Date.now() };
|
|
22
|
+
await writeUpdateState(stateFile, state);
|
|
23
|
+
};
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetchImpl(`${RELEASE_BASE}/v${state.targetVersion}/${encodeURIComponent(state.fileName)}`);
|
|
26
|
+
if (!response.ok || !response.body)
|
|
27
|
+
throw new Error("installer download failed");
|
|
28
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(partial, { mode: 0o700 }));
|
|
29
|
+
const actual = createHash("sha256").update(await readFile(partial)).digest("hex");
|
|
30
|
+
if (actual !== state.sha256)
|
|
31
|
+
throw new Error("installer checksum mismatch");
|
|
32
|
+
await chmod(partial, 0o700).catch(() => undefined);
|
|
33
|
+
await update({ phase: "installing" });
|
|
34
|
+
const command = process.platform === "win32" ? "powershell.exe" : "/bin/sh";
|
|
35
|
+
const args = process.platform === "win32" ? ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", partial] : [partial];
|
|
36
|
+
const code = await run(command, args, { ...process.env, AGENTROAM_DATA_DIR: state.dataDir, AGENTROAM_ROOT: state.roots[0], AGENTROAM_INSTALL_SKIP_SERVICE: "1" });
|
|
37
|
+
if (code !== 0)
|
|
38
|
+
throw new Error(`installer exited with code ${code}`);
|
|
39
|
+
const nextEntry = resolve(state.dataDir, "launcher", state.targetVersion, "node_modules", "agentroam", "bin", "agentroam.mjs");
|
|
40
|
+
const serviceArgs = [nextEntry, "service", "install"];
|
|
41
|
+
for (const root of state.roots)
|
|
42
|
+
serviceArgs.push("--root", root);
|
|
43
|
+
serviceArgs.push("--data-dir", state.dataDir, "--relay", state.relay, "--no-qr");
|
|
44
|
+
if (state.port)
|
|
45
|
+
serviceArgs.push("--port", String(state.port));
|
|
46
|
+
if (state.tunnelCommand)
|
|
47
|
+
serviceArgs.push("--tunnel-command", state.tunnelCommand);
|
|
48
|
+
if (state.localOnly)
|
|
49
|
+
serviceArgs.push("--local-only");
|
|
50
|
+
await update({ phase: "reconnecting" });
|
|
51
|
+
const serviceCode = await run(process.execPath, serviceArgs, process.env);
|
|
52
|
+
if (serviceCode !== 0)
|
|
53
|
+
throw new Error(`service activation exited with code ${serviceCode}`);
|
|
54
|
+
await update({ phase: "complete", message: `AgentRoam ${state.targetVersion} installed` });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
await update({ phase: "failed", message: sanitizeUpdateError(error) });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await unlink(partial).catch(() => undefined);
|
|
62
|
+
await releaseLock();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function run(command, args, env) {
|
|
66
|
+
return new Promise((resolveRun, reject) => {
|
|
67
|
+
const child = spawn(command, args, { env, stdio: "ignore", windowsHide: true });
|
|
68
|
+
child.once("error", reject);
|
|
69
|
+
child.once("exit", resolveRun);
|
|
70
|
+
});
|
|
71
|
+
}
|
package/install/README.md
CHANGED
|
@@ -5,7 +5,7 @@ These versioned installers support macOS Apple Silicon and Windows x64. They reu
|
|
|
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.
|
|
8
|
+
curl -fsSL https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.16/install-agentroam.sh | sh
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
The command installs the wrapper at `~/.local/bin/agentroam`. If that directory is not already in `PATH`, the installer prints the required shell setting without editing shell profiles.
|
|
@@ -13,7 +13,7 @@ The command installs the wrapper at `~/.local/bin/agentroam`. If that directory
|
|
|
13
13
|
## Windows x64
|
|
14
14
|
|
|
15
15
|
```powershell
|
|
16
|
-
irm https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.
|
|
16
|
+
irm https://gitee.com/caoqu/team-agent/releases/download/v0.2.0-preview.16/install-agentroam.ps1 | iex
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The command installs `%USERPROFILE%\.agentroam\bin\agentroam.cmd` and adds only that user directory to the user-level `PATH`. It does not require administrator access or change the machine-level `PATH`.
|
|
@@ -40,7 +40,7 @@ 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.
|
|
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`.
|
|
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
|
|
|
@@ -2,7 +2,7 @@ $ErrorActionPreference = "Stop"
|
|
|
2
2
|
$ProgressPreference = "SilentlyContinue"
|
|
3
3
|
|
|
4
4
|
$NodeVersion = "22.22.0"
|
|
5
|
-
$AgentRoamVersion = "0.2.0-preview.
|
|
5
|
+
$AgentRoamVersion = "0.2.0-preview.16"
|
|
6
6
|
$NodeArchive = "node-v22.22.0-win-x64.zip"
|
|
7
7
|
$NodeSha256 = "c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
|
|
8
8
|
$NodeUrl = "https://nodejs.org/dist/v22.22.0/$NodeArchive"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -eu
|
|
3
3
|
|
|
4
4
|
NODE_VERSION="22.22.0"
|
|
5
|
-
AGENTROAM_VERSION="0.2.0-preview.
|
|
5
|
+
AGENTROAM_VERSION="0.2.0-preview.16"
|
|
6
6
|
NODE_ARCHIVE="node-v22.22.0-darwin-arm64.tar.xz"
|
|
7
7
|
NODE_SHA256="2bd596bbfc4a275ceb8721a5954ee97daea5ebe673e96a185ebd732f6fb023ac"
|
|
8
8
|
NODE_URL="https://nodejs.org/dist/v22.22.0/$NODE_ARCHIVE"
|
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.16",
|
|
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.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"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|