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 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.14.tgz \
24
- ./agentroam-runtime-darwin-arm64-0.2.0-preview.14.tgz \
25
- ./agentroam-tui-darwin-arm64-0.2.0-preview.14.tgz \
26
- ./agentroam-0.2.0-preview.14.tgz
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.14.tgz `
34
- ./agentroam-runtime-win32-x64-0.2.0-preview.14.tgz `
35
- ./caoqu-agentroam-tui-win32-x64-0.2.0-preview.14.tgz `
36
- ./agentroam-0.2.0-preview.14.tgz
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
 
@@ -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.14";
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.14";
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",
@@ -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.14" },
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.14/install-agentroam.sh | sh
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.14/install-agentroam.ps1 | iex
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.14`.
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.14"
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.14"
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.14",
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.14",
30
- "agentroam-runtime-win32-x64": "0.2.0-preview.14",
31
- "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.14",
32
- "agentroam-cloudflared-win32-x64": "0.2.0-preview.14",
33
- "agentroam-tui-darwin-arm64": "0.2.0-preview.14",
34
- "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.14"
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",