@rynx-ai/daemon 0.1.0 → 0.1.9

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.
Files changed (89) hide show
  1. package/dist/browser-cli-args.d.ts +20 -0
  2. package/dist/browser-cli-args.js +100 -0
  3. package/dist/browser-runner-session-context.d.ts +24 -0
  4. package/dist/browser-runner-session-context.js +113 -0
  5. package/dist/cdp-keyboard-input.d.ts +6 -0
  6. package/dist/cdp-keyboard-input.js +209 -0
  7. package/dist/channel-store.d.ts +0 -6
  8. package/dist/channel-store.js +2 -18
  9. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  10. package/dist/chrome-for-testing-release-resolver.js +214 -0
  11. package/dist/chrome-for-testing-store.d.ts +85 -0
  12. package/dist/chrome-for-testing-store.js +836 -0
  13. package/dist/cli.js +467 -30
  14. package/dist/control-client.d.ts +113 -0
  15. package/dist/control-client.js +792 -0
  16. package/dist/control-deps.js +37 -6
  17. package/dist/control-endpoint.d.ts +14 -0
  18. package/dist/control-endpoint.js +124 -0
  19. package/dist/control-link-store.d.ts +16 -0
  20. package/dist/control-link-store.js +53 -0
  21. package/dist/daemon-owner.d.ts +14 -0
  22. package/dist/daemon-owner.js +104 -0
  23. package/dist/daemon-server.d.ts +12 -2
  24. package/dist/daemon-server.js +559 -28
  25. package/dist/db.js +140 -10
  26. package/dist/desktop-browser-host-client.d.ts +34 -0
  27. package/dist/desktop-browser-host-client.js +413 -0
  28. package/dist/direct-runtime-authenticator.d.ts +13 -0
  29. package/dist/direct-runtime-authenticator.js +127 -0
  30. package/dist/direct-runtime-config.d.ts +20 -0
  31. package/dist/direct-runtime-config.js +101 -0
  32. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  33. package/dist/embedded-browser-profile-cleanup.js +338 -0
  34. package/dist/headless-browser-host.d.ts +14 -0
  35. package/dist/headless-browser-host.js +2156 -0
  36. package/dist/index-daemon.js +28 -5
  37. package/dist/installation-id.d.ts +33 -0
  38. package/dist/installation-id.js +692 -0
  39. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  40. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  41. package/dist/plugin-cli.d.ts +30 -0
  42. package/dist/plugin-cli.js +270 -0
  43. package/dist/plugin-host-rpc.d.ts +85 -0
  44. package/dist/plugin-host-rpc.js +878 -0
  45. package/dist/plugin-install-restart.d.ts +13 -0
  46. package/dist/plugin-install-restart.js +24 -0
  47. package/dist/plugin-installer.d.ts +36 -15
  48. package/dist/plugin-installer.js +420 -155
  49. package/dist/plugin-package.d.ts +38 -0
  50. package/dist/plugin-package.js +553 -0
  51. package/dist/plugin-runtime-control.d.ts +2 -0
  52. package/dist/plugin-runtime-control.js +37 -0
  53. package/dist/plugin-store.d.ts +37 -8
  54. package/dist/plugin-store.js +80 -15
  55. package/dist/plugin-supervisor.d.ts +160 -0
  56. package/dist/plugin-supervisor.js +1143 -0
  57. package/dist/pm2.d.ts +3 -1
  58. package/dist/pm2.js +55 -13
  59. package/dist/provider-cli-service.d.ts +46 -0
  60. package/dist/provider-cli-service.js +510 -0
  61. package/dist/registry.d.ts +6 -19
  62. package/dist/registry.js +22 -87
  63. package/dist/remote-runtime-access-store.d.ts +61 -0
  64. package/dist/remote-runtime-access-store.js +386 -0
  65. package/dist/remote-runtime-admin.d.ts +32 -0
  66. package/dist/remote-runtime-admin.js +107 -0
  67. package/dist/remote-runtime-connection-manager.d.ts +92 -0
  68. package/dist/remote-runtime-connection-manager.js +1409 -0
  69. package/dist/remote-runtime-credential-store.d.ts +45 -0
  70. package/dist/remote-runtime-credential-store.js +639 -0
  71. package/dist/remote-runtime-target-control.d.ts +47 -0
  72. package/dist/remote-runtime-target-control.js +433 -0
  73. package/dist/remote-runtime-target-store.d.ts +53 -0
  74. package/dist/remote-runtime-target-store.js +307 -0
  75. package/dist/runtime-share-addresses.d.ts +21 -0
  76. package/dist/runtime-share-addresses.js +50 -0
  77. package/dist/session-browser-capability-store.d.ts +13 -0
  78. package/dist/session-browser-capability-store.js +58 -0
  79. package/dist/session-emulator-binding-store.d.ts +10 -0
  80. package/dist/session-emulator-binding-store.js +61 -0
  81. package/dist/session-launch-operations.d.ts +41 -0
  82. package/dist/session-launch-operations.js +124 -0
  83. package/dist/session-meta-store.d.ts +3 -2
  84. package/dist/session-meta-store.js +57 -7
  85. package/dist/setup.js +9 -4
  86. package/dist/skills-catalog.js +8 -4
  87. package/dist/update.d.ts +11 -2
  88. package/dist/update.js +0 -27
  89. package/package.json +18 -10
package/dist/pm2.d.ts CHANGED
@@ -8,9 +8,11 @@ export declare function formatControlUrl(host: string, port: number): string;
8
8
  * entry from scratch (delete + start) so config flags are always fresh; the
9
9
  * `force === false` path short-circuits when it's already online.
10
10
  */
11
- export declare function startDaemon({ force }: {
11
+ export declare function startDaemon({ force, quiet }: {
12
12
  force: boolean;
13
+ quiet?: boolean;
13
14
  }): number;
15
+ export declare function isPm2DaemonOnline(): boolean;
14
16
  /** Stop and remove the daemon from pm2's process list. */
15
17
  export declare function stopDaemon(): number;
16
18
  /** Print a one-line status; exit 0 only when online. */
package/dist/pm2.js CHANGED
@@ -17,6 +17,7 @@ import { loadConfig } from "@rynx-ai/core";
17
17
  const require = createRequire(import.meta.url);
18
18
  /** pm2 app name — single resident daemon per machine. */
19
19
  const APP_NAME = "rynx";
20
+ const PM2_COMMAND_TIMEOUT_MS = 5_000;
20
21
  /**
21
22
  * Build a dialable control-console URL from the daemon's bind host/port. Bind-all
22
23
  * addresses (`0.0.0.0`, `::`) aren't dialable, so show a loopback host instead.
@@ -57,17 +58,37 @@ function logsDir() {
57
58
  mkdirSync(dir, { recursive: true });
58
59
  return dir;
59
60
  }
61
+ function nodeRuntimeEnv() {
62
+ if (!process.versions.electron) {
63
+ return process.env;
64
+ }
65
+ return { ...process.env, ELECTRON_RUN_AS_NODE: "1" };
66
+ }
60
67
  /** Run a pm2 command inheriting stdio; returns its exit code. */
61
- function runPm2(args) {
68
+ function runPm2(args, quiet = false) {
62
69
  const res = spawnSync(process.execPath, [pm2Bin(), ...args], {
63
- stdio: "inherit",
64
- env: process.env,
70
+ stdio: quiet ? "ignore" : "inherit",
71
+ env: nodeRuntimeEnv(),
72
+ timeout: PM2_COMMAND_TIMEOUT_MS,
73
+ killSignal: "SIGTERM",
65
74
  });
75
+ if (res.error) {
76
+ if (!quiet)
77
+ console.error(`pm2 command failed: ${pm2ErrorMessage(res.error)}`);
78
+ return 1;
79
+ }
66
80
  return res.status ?? 1;
67
81
  }
68
82
  /** Read the daemon's current pm2 record, or null if not registered. */
69
83
  function describe() {
70
- const res = spawnSync(process.execPath, [pm2Bin(), "jlist"], { encoding: "utf8" });
84
+ const res = spawnSync(process.execPath, [pm2Bin(), "jlist"], {
85
+ encoding: "utf8",
86
+ env: nodeRuntimeEnv(),
87
+ timeout: PM2_COMMAND_TIMEOUT_MS,
88
+ killSignal: "SIGTERM",
89
+ });
90
+ if (res.error)
91
+ throw new Error(`pm2 query failed: ${pm2ErrorMessage(res.error)}`);
71
92
  if (res.status !== 0 || !res.stdout)
72
93
  return null;
73
94
  let list;
@@ -79,21 +100,39 @@ function describe() {
79
100
  }
80
101
  return list.find((proc) => proc.name === APP_NAME) ?? null;
81
102
  }
103
+ function pm2ErrorMessage(error) {
104
+ return error.code === "ETIMEDOUT"
105
+ ? `timed out after ${PM2_COMMAND_TIMEOUT_MS}ms`
106
+ : error.message;
107
+ }
82
108
  /**
83
109
  * Start (or, with `force`, reload) the resident daemon. We recreate the pm2
84
110
  * entry from scratch (delete + start) so config flags are always fresh; the
85
111
  * `force === false` path short-circuits when it's already online.
86
112
  */
87
- export function startDaemon({ force }) {
88
- const existing = describe();
113
+ export function startDaemon({ force, quiet = false }) {
114
+ let existing;
115
+ try {
116
+ existing = describe();
117
+ }
118
+ catch (error) {
119
+ if (!quiet)
120
+ console.error(error instanceof Error ? error.message : String(error));
121
+ return 1;
122
+ }
89
123
  if (!force && existing?.pm2_env?.status === "online") {
90
- console.log(`rynx is already running (pid ${existing.pid ?? "?"}). Use \`rynx restart\` to reload.`);
91
- console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
124
+ if (!quiet) {
125
+ console.log(`rynx is already running (pid ${existing.pid ?? "?"}). Use \`rynx restart\` to reload.`);
126
+ console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
127
+ }
92
128
  return 0;
93
129
  }
94
130
  const dir = logsDir();
95
- if (existing)
96
- runPm2(["delete", APP_NAME]);
131
+ if (existing) {
132
+ const removed = runPm2(["delete", APP_NAME], quiet);
133
+ if (removed !== 0)
134
+ return removed;
135
+ }
97
136
  const status = runPm2([
98
137
  "start",
99
138
  daemonEntryPath(),
@@ -112,13 +151,16 @@ export function startDaemon({ force }) {
112
151
  join(dir, "err.log"),
113
152
  "--merge-logs",
114
153
  "--time",
115
- ]);
116
- if (status === 0) {
154
+ ], quiet);
155
+ if (status === 0 && !quiet) {
117
156
  console.log(`rynx ${force ? "restarted" : "started"} (pm2 app "${APP_NAME}", logs: ${dir}).`);
118
157
  console.log(`Control console: ${styleUrl(controlConsoleUrl())}`);
119
158
  }
120
159
  return status;
121
160
  }
161
+ export function isPm2DaemonOnline() {
162
+ return describe()?.pm2_env?.status === "online";
163
+ }
122
164
  /** Stop and remove the daemon from pm2's process list. */
123
165
  export function stopDaemon() {
124
166
  if (!describe()) {
@@ -147,7 +189,7 @@ export function statusDaemon() {
147
189
  export function streamLogs() {
148
190
  const child = spawn(process.execPath, [pm2Bin(), "logs", APP_NAME, "--lines", "50"], {
149
191
  stdio: "inherit",
150
- env: process.env,
192
+ env: nodeRuntimeEnv(),
151
193
  });
152
194
  child.on("exit", (code) => process.exit(code ?? 0));
153
195
  }
@@ -0,0 +1,46 @@
1
+ import type { ProviderCliId, ProviderCliJob, ProviderCliOperation, ProviderCliStatusList } from "@rynx-ai/protocol/control";
2
+ interface CommandResult {
3
+ exitCode: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ }
7
+ interface ProviderCliServiceDeps {
8
+ env?: NodeJS.ProcessEnv;
9
+ platform?: NodeJS.Platform;
10
+ now?: () => Date;
11
+ run?: (executable: string, args: string[], options: {
12
+ timeoutMs: number;
13
+ env: NodeJS.ProcessEnv;
14
+ }) => Promise<CommandResult>;
15
+ resolveExecutable?: (binary: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform) => Promise<string | null>;
16
+ download?: (url: string, maximumBytes: number) => Promise<Uint8Array>;
17
+ }
18
+ export declare class ProviderCliService {
19
+ private readonly env;
20
+ private readonly platform;
21
+ private readonly now;
22
+ private readonly runCommand;
23
+ private readonly resolveCommand;
24
+ private readonly download;
25
+ private readonly latestCache;
26
+ private readonly jobs;
27
+ private readonly activeJobs;
28
+ constructor(deps?: ProviderCliServiceDeps);
29
+ listStatuses(): Promise<ProviderCliStatusList>;
30
+ startOperation(provider: ProviderCliId, operation: ProviderCliOperation): Promise<ProviderCliJob>;
31
+ getJob(id: string): ProviderCliJob | undefined;
32
+ private inspect;
33
+ private resolveProviderExecutable;
34
+ private authStatus;
35
+ private latestVersion;
36
+ private execute;
37
+ private install;
38
+ private update;
39
+ private installTraex;
40
+ private pruneJobs;
41
+ }
42
+ export declare class ProviderCliOperationError extends Error {
43
+ readonly code: "failed_precondition";
44
+ constructor(code: "failed_precondition", message: string);
45
+ }
46
+ export {};
@@ -0,0 +1,510 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { delimiter, join } from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { getRuntimeProfile } from "@rynx-ai/core";
8
+ const STATUS_COMMAND_TIMEOUT_MS = 8_000;
9
+ const OPERATION_TIMEOUT_MS = 10 * 60_000;
10
+ const COMMAND_TERMINATION_GRACE_MS = 2_000;
11
+ const OUTPUT_MAX_BYTES = 16 * 1024;
12
+ const LATEST_CACHE_MS = 15 * 60_000;
13
+ const TRAEX_INSTALLER_MAX_BYTES = 2 * 1024 * 1024;
14
+ const TRAEX_INSTALLER_URL = "https://tosv-myabc.byted.org/obj/trae-common-2-asiasebd/traex/install/install_all_platforms.sh";
15
+ const PROVIDER_PACKAGES = {
16
+ codex: "@openai/codex",
17
+ claude: "@anthropic-ai/claude-code",
18
+ };
19
+ const PROVIDER_CLI_IDS = ["codex", "claude", "traex"];
20
+ export class ProviderCliService {
21
+ env;
22
+ platform;
23
+ now;
24
+ runCommand;
25
+ resolveCommand;
26
+ download;
27
+ latestCache = new Map();
28
+ jobs = new Map();
29
+ activeJobs = new Map();
30
+ constructor(deps = {}) {
31
+ this.env = { ...process.env, ...deps.env };
32
+ this.platform = deps.platform ?? process.platform;
33
+ this.now = deps.now ?? (() => new Date());
34
+ this.runCommand = deps.run ?? runCommand;
35
+ this.resolveCommand = deps.resolveExecutable ?? resolveExecutable;
36
+ this.download = deps.download ?? downloadBounded;
37
+ }
38
+ async listStatuses() {
39
+ const providerClis = await Promise.all(PROVIDER_CLI_IDS.map((id) => this.inspect(id)));
40
+ return { providerClis };
41
+ }
42
+ async startOperation(provider, operation) {
43
+ const activeId = this.activeJobs.get(provider);
44
+ if (activeId) {
45
+ const active = this.jobs.get(activeId);
46
+ if (active?.state === "running") {
47
+ if (active.operation === operation)
48
+ return { ...active };
49
+ throw new ProviderCliOperationError("failed_precondition", `${provider} already has a ${active.operation} operation in progress`);
50
+ }
51
+ }
52
+ const status = await this.inspect(provider, operation === "update");
53
+ if (operation === "install" && status.availability === "installed") {
54
+ throw new ProviderCliOperationError("failed_precondition", `${status.name} is already installed`);
55
+ }
56
+ if (operation === "update" && status.availability !== "installed") {
57
+ throw new ProviderCliOperationError("failed_precondition", `${status.name} is not installed`);
58
+ }
59
+ if (operation === "install" && !status.canInstall) {
60
+ throw new ProviderCliOperationError("failed_precondition", `${status.name} cannot be installed automatically on this machine`);
61
+ }
62
+ if (operation === "update" && !status.canUpdate) {
63
+ throw new ProviderCliOperationError("failed_precondition", `${status.name} cannot be updated automatically on this machine`);
64
+ }
65
+ const job = {
66
+ id: randomUUID(),
67
+ provider,
68
+ operation,
69
+ state: "running",
70
+ stage: "starting",
71
+ startedAt: this.now().toISOString(),
72
+ };
73
+ this.jobs.set(job.id, job);
74
+ this.activeJobs.set(provider, job.id);
75
+ this.pruneJobs();
76
+ void this.execute(job.id, status).catch(() => undefined);
77
+ return { ...job };
78
+ }
79
+ getJob(id) {
80
+ const job = this.jobs.get(id);
81
+ return job ? { ...job } : undefined;
82
+ }
83
+ async inspect(provider, includeLatest = true) {
84
+ const profile = getRuntimeProfile(provider);
85
+ const checkedAt = this.now().toISOString();
86
+ const executablePath = await this.resolveProviderExecutable(provider);
87
+ const npmPath = provider === "traex"
88
+ ? null
89
+ : await this.resolveCommand("npm", this.env, this.platform);
90
+ const canInstall = provider === "traex"
91
+ ? this.platform !== "win32"
92
+ : npmPath !== null && this.platform !== "win32";
93
+ if (!executablePath) {
94
+ const latestVersion = includeLatest
95
+ ? await this.latestVersion(provider, undefined, npmPath)
96
+ : undefined;
97
+ return {
98
+ id: provider,
99
+ name: profile.displayName,
100
+ binary: profile.defaultBinary,
101
+ availability: "missing",
102
+ ...(latestVersion ? { latestVersion } : {}),
103
+ updateState: "unknown",
104
+ authState: "unknown",
105
+ canInstall,
106
+ canUpdate: false,
107
+ checkedAt,
108
+ };
109
+ }
110
+ let currentVersion;
111
+ let versionError;
112
+ try {
113
+ const result = await this.runCommand(executablePath, ["--version"], {
114
+ timeoutMs: STATUS_COMMAND_TIMEOUT_MS,
115
+ env: this.env,
116
+ });
117
+ if (result.exitCode !== 0)
118
+ throw new Error(commandFailure(result));
119
+ currentVersion = extractVersion(`${result.stdout}\n${result.stderr}`);
120
+ if (!currentVersion)
121
+ throw new Error("version output was not recognized");
122
+ }
123
+ catch (error) {
124
+ versionError = errorMessage(error);
125
+ }
126
+ const [auth, latestVersion] = await Promise.all([
127
+ currentVersion
128
+ ? this.authStatus(provider, executablePath)
129
+ : Promise.resolve({ state: "unknown" }),
130
+ includeLatest
131
+ ? this.latestVersion(provider, executablePath, npmPath)
132
+ : Promise.resolve(undefined),
133
+ ]);
134
+ const updateState = currentVersion && latestVersion
135
+ ? (compareVersions(currentVersion, latestVersion) < 0 ? "available" : "current")
136
+ : "unknown";
137
+ return {
138
+ id: provider,
139
+ name: profile.displayName,
140
+ binary: profile.defaultBinary,
141
+ executablePath,
142
+ availability: currentVersion ? "installed" : "broken",
143
+ ...(currentVersion ? { currentVersion } : {}),
144
+ ...(latestVersion ? { latestVersion } : {}),
145
+ updateState,
146
+ authState: auth.state,
147
+ ...(auth.mode ? { authMode: auth.mode } : {}),
148
+ canInstall: !currentVersion && canInstall,
149
+ canUpdate: Boolean(currentVersion) && this.platform !== "win32",
150
+ checkedAt,
151
+ ...(versionError ? { error: versionError } : {}),
152
+ };
153
+ }
154
+ async resolveProviderExecutable(provider) {
155
+ const candidates = provider === "traex" ? ["traex", "traecli"] : [provider];
156
+ for (const candidate of candidates) {
157
+ const resolved = await this.resolveCommand(candidate, this.env, this.platform);
158
+ if (resolved)
159
+ return resolved;
160
+ }
161
+ return null;
162
+ }
163
+ async authStatus(provider, executablePath) {
164
+ try {
165
+ const args = provider === "claude" ? ["auth", "status", "--json"] : ["login", "status"];
166
+ const result = await this.runCommand(executablePath, args, {
167
+ timeoutMs: STATUS_COMMAND_TIMEOUT_MS,
168
+ env: this.env,
169
+ });
170
+ const output = `${result.stdout}\n${result.stderr}`.trim();
171
+ if (provider === "claude") {
172
+ const parsed = JSON.parse(output);
173
+ if (parsed.loggedIn === true) {
174
+ const mode = [parsed.authMethod, parsed.apiProvider]
175
+ .filter((value) => typeof value === "string" && value.length > 0)
176
+ .join(" · ");
177
+ return { state: "ready", ...(mode ? { mode } : {}) };
178
+ }
179
+ if (parsed.loggedIn === false)
180
+ return { state: "required" };
181
+ return { state: "unknown" };
182
+ }
183
+ if (result.exitCode === 0 && /logged in/i.test(output)) {
184
+ const match = /logged in(?: using| as)?\s+(.+)/i.exec(output);
185
+ return { state: "ready", ...(match?.[1]?.trim() ? { mode: match[1].trim() } : {}) };
186
+ }
187
+ if (/not logged in|logged out|login required/i.test(output))
188
+ return { state: "required" };
189
+ return { state: result.exitCode === 0 ? "unknown" : "required" };
190
+ }
191
+ catch {
192
+ return { state: "unknown" };
193
+ }
194
+ }
195
+ async latestVersion(provider, executablePath, npmPath) {
196
+ const now = this.now().getTime();
197
+ const cached = this.latestCache.get(provider);
198
+ if (cached && cached.expiresAt > now)
199
+ return cached.value;
200
+ let value;
201
+ try {
202
+ if (provider === "traex") {
203
+ if (!executablePath)
204
+ return undefined;
205
+ const result = await this.runCommand(executablePath, ["update", "check"], {
206
+ timeoutMs: STATUS_COMMAND_TIMEOUT_MS,
207
+ env: this.env,
208
+ });
209
+ if (result.exitCode === 0) {
210
+ value = /Latest TRAE CLI version:\s*([^\s]+)/i.exec(`${result.stdout}\n${result.stderr}`)?.[1];
211
+ }
212
+ }
213
+ else if (npmPath) {
214
+ const packageName = PROVIDER_PACKAGES[provider];
215
+ const result = await this.runCommand(npmPath, ["view", `${packageName}@latest`, "version", "--json"], { timeoutMs: STATUS_COMMAND_TIMEOUT_MS, env: this.env });
216
+ if (result.exitCode === 0)
217
+ value = extractVersion(result.stdout);
218
+ }
219
+ }
220
+ catch {
221
+ value = undefined;
222
+ }
223
+ this.latestCache.set(provider, { value, expiresAt: now + LATEST_CACHE_MS });
224
+ return value;
225
+ }
226
+ async execute(jobId, previousStatus) {
227
+ const job = this.jobs.get(jobId);
228
+ if (!job)
229
+ return;
230
+ try {
231
+ job.stage = job.operation === "install" ? "installing" : "updating";
232
+ if (job.operation === "install") {
233
+ await this.install(job.provider);
234
+ }
235
+ else {
236
+ await this.update(job.provider, previousStatus.executablePath);
237
+ }
238
+ job.stage = "verifying";
239
+ this.latestCache.delete(job.provider);
240
+ const verified = await this.inspect(job.provider, false);
241
+ if (verified.availability !== "installed") {
242
+ throw new Error(verified.error ?? `${verified.name} was not found on the daemon PATH after the operation`);
243
+ }
244
+ if (job.operation === "update") {
245
+ const previousVersion = previousStatus.currentVersion;
246
+ const verifiedVersion = verified.currentVersion;
247
+ if (!previousVersion || !verifiedVersion || compareVersions(verifiedVersion, previousVersion) <= 0) {
248
+ throw new Error(`${verified.name} reported success but its version did not change`);
249
+ }
250
+ if (previousStatus.latestVersion
251
+ && compareVersions(verifiedVersion, previousStatus.latestVersion) < 0) {
252
+ throw new Error(`${verified.name} updated to ${verifiedVersion}, below the expected ${previousStatus.latestVersion}`);
253
+ }
254
+ }
255
+ job.stage = "complete";
256
+ job.state = "succeeded";
257
+ job.finishedAt = this.now().toISOString();
258
+ }
259
+ catch (error) {
260
+ job.state = "failed";
261
+ job.finishedAt = this.now().toISOString();
262
+ job.error = boundError(errorMessage(error));
263
+ }
264
+ finally {
265
+ if (this.activeJobs.get(job.provider) === job.id)
266
+ this.activeJobs.delete(job.provider);
267
+ }
268
+ }
269
+ async install(provider) {
270
+ if (provider === "traex") {
271
+ await this.installTraex();
272
+ return;
273
+ }
274
+ const npmPath = await this.resolveCommand("npm", this.env, this.platform);
275
+ if (!npmPath)
276
+ throw new Error("npm is not available on PATH");
277
+ const result = await this.runCommand(npmPath, ["install", "--global", `${PROVIDER_PACKAGES[provider]}@latest`, "--no-audit", "--no-fund"], { timeoutMs: OPERATION_TIMEOUT_MS, env: this.env });
278
+ if (result.exitCode !== 0)
279
+ throw new Error(commandFailure(result));
280
+ }
281
+ async update(provider, executablePath) {
282
+ const result = await this.runCommand(executablePath, ["update"], {
283
+ timeoutMs: OPERATION_TIMEOUT_MS,
284
+ env: this.env,
285
+ });
286
+ if (result.exitCode !== 0)
287
+ throw new Error(commandFailure(result));
288
+ }
289
+ async installTraex() {
290
+ if (this.platform === "win32")
291
+ throw new Error("automatic Traex installation is not supported on Windows");
292
+ const script = await this.download(TRAEX_INSTALLER_URL, TRAEX_INSTALLER_MAX_BYTES);
293
+ const directory = await mkdtemp(join(tmpdir(), "rynx-traex-install-"));
294
+ const scriptPath = join(directory, "install.sh");
295
+ try {
296
+ await writeFile(scriptPath, script, { mode: 0o700 });
297
+ const shPath = await this.resolveCommand("sh", this.env, this.platform);
298
+ if (!shPath)
299
+ throw new Error("sh is not available on PATH");
300
+ const result = await this.runCommand(shPath, [scriptPath], {
301
+ timeoutMs: OPERATION_TIMEOUT_MS,
302
+ env: installerEnvironment(this.env, {
303
+ TRAEX_INSTALL_ASSUME_YES: "1",
304
+ TRAEX_INSTALL_MIGRATE_BREW: "0",
305
+ TRAEX_INSTALL_REMOVE_BREW: "0",
306
+ TRAEX_INSTALL_CHANNEL: "stable",
307
+ }),
308
+ });
309
+ if (result.exitCode !== 0)
310
+ throw new Error(commandFailure(result));
311
+ }
312
+ finally {
313
+ await rm(directory, { recursive: true, force: true });
314
+ }
315
+ }
316
+ pruneJobs() {
317
+ if (this.jobs.size <= 50)
318
+ return;
319
+ const finished = [...this.jobs.values()]
320
+ .filter((job) => job.state !== "running")
321
+ .sort((left, right) => left.startedAt.localeCompare(right.startedAt));
322
+ for (const job of finished.slice(0, this.jobs.size - 50))
323
+ this.jobs.delete(job.id);
324
+ }
325
+ }
326
+ export class ProviderCliOperationError extends Error {
327
+ code;
328
+ constructor(code, message) {
329
+ super(message);
330
+ this.code = code;
331
+ this.name = "ProviderCliOperationError";
332
+ }
333
+ }
334
+ async function resolveExecutable(binary, env, platform) {
335
+ const pathValue = env.PATH ?? env.Path ?? env.path ?? "";
336
+ const extensions = platform === "win32"
337
+ ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
338
+ : [""];
339
+ for (const directory of pathValue.split(delimiter).filter(Boolean)) {
340
+ for (const extension of extensions) {
341
+ const candidate = join(directory, platform === "win32" ? `${binary}${extension}` : binary);
342
+ try {
343
+ await access(candidate, fsConstants.X_OK);
344
+ return candidate;
345
+ }
346
+ catch {
347
+ // Continue through the daemon's effective PATH.
348
+ }
349
+ }
350
+ }
351
+ return null;
352
+ }
353
+ function runCommand(executable, args, options) {
354
+ return new Promise((resolve, reject) => {
355
+ const detached = process.platform !== "win32";
356
+ const child = spawn(executable, args, {
357
+ env: options.env,
358
+ stdio: ["ignore", "pipe", "pipe"],
359
+ windowsHide: true,
360
+ detached,
361
+ });
362
+ let timedOut = false;
363
+ let settled = false;
364
+ let forceTimer;
365
+ let stdout = Buffer.alloc(0);
366
+ let stderr = Buffer.alloc(0);
367
+ const append = (current, chunk) => Buffer.concat([current, chunk]).subarray(0, OUTPUT_MAX_BYTES);
368
+ child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
369
+ child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
370
+ const clearTimers = () => {
371
+ clearTimeout(timer);
372
+ if (forceTimer)
373
+ clearTimeout(forceTimer);
374
+ };
375
+ const rejectOnce = (error) => {
376
+ if (settled)
377
+ return;
378
+ settled = true;
379
+ clearTimers();
380
+ reject(error);
381
+ };
382
+ const timer = setTimeout(() => {
383
+ timedOut = true;
384
+ terminateCommand(child.pid, child, "SIGTERM", detached);
385
+ forceTimer = setTimeout(() => {
386
+ terminateCommand(child.pid, child, "SIGKILL", detached);
387
+ rejectOnce(new Error(`command timed out after ${options.timeoutMs}ms`));
388
+ }, COMMAND_TERMINATION_GRACE_MS);
389
+ forceTimer.unref();
390
+ }, options.timeoutMs);
391
+ timer.unref();
392
+ child.once("error", (error) => {
393
+ if (!timedOut)
394
+ rejectOnce(error);
395
+ });
396
+ child.once("close", (code) => {
397
+ if (settled || timedOut)
398
+ return;
399
+ settled = true;
400
+ clearTimers();
401
+ resolve({
402
+ exitCode: code ?? -1,
403
+ stdout: stdout.toString("utf8"),
404
+ stderr: stderr.toString("utf8"),
405
+ });
406
+ });
407
+ });
408
+ }
409
+ async function downloadBounded(url, maximumBytes) {
410
+ const requestedUrl = new URL(url);
411
+ if (requestedUrl.protocol !== "https:")
412
+ throw new Error("installer URL must use HTTPS");
413
+ const response = await fetch(url, { redirect: "follow" });
414
+ if (!response.ok)
415
+ throw new Error(`installer download failed with HTTP ${response.status}`);
416
+ const finalUrl = new URL(response.url || url);
417
+ if (finalUrl.protocol !== "https:" || finalUrl.origin !== requestedUrl.origin) {
418
+ throw new Error("installer download redirected outside the trusted origin");
419
+ }
420
+ const expectedMd5 = response.headers.get("content-md5");
421
+ if (!expectedMd5)
422
+ throw new Error("installer response did not include integrity metadata");
423
+ const declaredLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
424
+ if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
425
+ throw new Error("installer download exceeded the accepted size");
426
+ }
427
+ if (!response.body)
428
+ throw new Error("installer download returned an empty response");
429
+ const reader = response.body.getReader();
430
+ const chunks = [];
431
+ let length = 0;
432
+ for (;;) {
433
+ const { value, done } = await reader.read();
434
+ if (done)
435
+ break;
436
+ if (!value)
437
+ continue;
438
+ length += value.byteLength;
439
+ if (length > maximumBytes) {
440
+ await reader.cancel();
441
+ throw new Error("installer download exceeded the accepted size");
442
+ }
443
+ chunks.push(value);
444
+ }
445
+ if (length === 0)
446
+ throw new Error("installer download returned an empty response");
447
+ const bytes = new Uint8Array(length);
448
+ let offset = 0;
449
+ for (const chunk of chunks) {
450
+ bytes.set(chunk, offset);
451
+ offset += chunk.byteLength;
452
+ }
453
+ const actualMd5 = createHash("md5").update(bytes).digest("base64");
454
+ if (actualMd5 !== expectedMd5)
455
+ throw new Error("installer response failed its integrity check");
456
+ return bytes;
457
+ }
458
+ function terminateCommand(pid, child, signal, detached) {
459
+ try {
460
+ if (detached && pid)
461
+ process.kill(-pid, signal);
462
+ else
463
+ child.kill(signal);
464
+ }
465
+ catch {
466
+ // The process may have exited between the timeout and signal delivery.
467
+ }
468
+ }
469
+ function installerEnvironment(env, additions) {
470
+ const allowed = [
471
+ "PATH", "HOME", "USER", "SHELL", "TMPDIR",
472
+ "LANG", "LC_ALL",
473
+ "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME",
474
+ "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
475
+ "http_proxy", "https_proxy", "no_proxy",
476
+ "SSL_CERT_FILE", "SSL_CERT_DIR",
477
+ ];
478
+ const result = { ...additions };
479
+ for (const key of allowed) {
480
+ if (env[key] !== undefined)
481
+ result[key] = env[key];
482
+ }
483
+ return result;
484
+ }
485
+ function extractVersion(output) {
486
+ return /\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/.exec(output)?.[0];
487
+ }
488
+ function compareVersions(left, right) {
489
+ const a = left.split(/[.+-]/).slice(0, 3).map((value) => Number.parseInt(value, 10));
490
+ const b = right.split(/[.+-]/).slice(0, 3).map((value) => Number.parseInt(value, 10));
491
+ for (let index = 0; index < 3; index += 1) {
492
+ const delta = (a[index] ?? 0) - (b[index] ?? 0);
493
+ if (delta !== 0)
494
+ return delta;
495
+ }
496
+ const leftPrerelease = left.includes("-");
497
+ const rightPrerelease = right.includes("-");
498
+ if (leftPrerelease !== rightPrerelease)
499
+ return leftPrerelease ? -1 : 1;
500
+ return left.localeCompare(right);
501
+ }
502
+ function commandFailure(result) {
503
+ return boundError(result.stderr.trim() || result.stdout.trim() || `command exited ${result.exitCode}`);
504
+ }
505
+ function errorMessage(error) {
506
+ return error instanceof Error ? error.message : String(error);
507
+ }
508
+ function boundError(message) {
509
+ return message.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, 1024) || "operation failed";
510
+ }