@ricsam/r5d-worker 0.0.27 → 0.0.29

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
@@ -19,6 +19,10 @@ Visible branch checkouts are the user/agent workbench. Their `origin` remains th
19
19
 
20
20
  Worker labels are mandatory and unique per user. Choose labels that describe host capabilities, such as `macos`, `linux`, `ios`, or `ec2-build`.
21
21
 
22
+ `r5d-worker start` keeps a lightweight supervisor process attached to the launching terminal or service. When both r5d CLIs are updated from User Settings, the connected runtime verifies the installed versions, exits with a reload signal, and the supervisor reconnects using the new worker package. Updates only run while the worker has no active commands or shells.
23
+
24
+ One-click updates use the current user's global npm prefix and never invoke `sudo`. If that prefix is not writable, User Settings provides the exact command to run manually.
25
+
22
26
  The agent runs commands through the virtual shell command:
23
27
 
24
28
  ```sh
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var cli_update_exports = {};
20
+ __export(cli_update_exports, {
21
+ getManualCliUpdateCommand: () => getManualCliUpdateCommand,
22
+ installCliUpdate: () => installCliUpdate,
23
+ parseCliVersion: () => parseCliVersion,
24
+ readInstalledCliVersion: () => readInstalledCliVersion,
25
+ runCommand: () => runCommand
26
+ });
27
+ module.exports = __toCommonJS(cli_update_exports);
28
+ const UPDATE_OUTPUT_LIMIT = 8e3;
29
+ function collectOutput(value) {
30
+ if (!value || typeof value === "number") {
31
+ return Promise.resolve("");
32
+ }
33
+ return new Response(value).text();
34
+ }
35
+ async function runCommand(argv) {
36
+ const child = Bun.spawn(argv, {
37
+ stdout: "pipe",
38
+ stderr: "pipe",
39
+ env: process.env
40
+ });
41
+ const [stdout, stderr, exitCode] = await Promise.all([collectOutput(child.stdout), collectOutput(child.stderr), child.exited]);
42
+ return { exitCode, stdout, stderr };
43
+ }
44
+ function parseCliVersion(cliName, output) {
45
+ const escapedName = cliName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46
+ const match = output.trim().match(new RegExp(`(?:^|\\n)${escapedName}\\s+(\\S+)`));
47
+ return match?.[1]?.trim() || null;
48
+ }
49
+ function readInstalledCliVersion(cliName) {
50
+ try {
51
+ const result = Bun.spawnSync([cliName, "--version"], {
52
+ stdout: "pipe",
53
+ stderr: "pipe",
54
+ env: process.env
55
+ });
56
+ if (result.exitCode !== 0) {
57
+ return null;
58
+ }
59
+ return parseCliVersion(cliName, Buffer.from(result.stdout).toString("utf8"));
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ function shellQuote(value) {
65
+ return `'${value.replaceAll("'", `'\\''`)}'`;
66
+ }
67
+ function getManualCliUpdateCommand(input) {
68
+ return `npm install -g --omit=dev ${shellQuote(input.workerPackageSpec)} ${shellQuote(input.r5dctlPackageSpec)}`;
69
+ }
70
+ function commandErrorDetail(result) {
71
+ const detail = `${result.stderr.trim()}${result.stderr.trim() && result.stdout.trim() ? "\n" : ""}${result.stdout.trim()}`.trim();
72
+ if (!detail) {
73
+ return `exit code ${result.exitCode}`;
74
+ }
75
+ return detail.slice(-UPDATE_OUTPUT_LIMIT);
76
+ }
77
+ async function readVerifiedVersion(cliName, targetVersion, runner) {
78
+ const result = await runner([cliName, "--version"]);
79
+ const installedVersion = result.exitCode === 0 ? parseCliVersion(cliName, result.stdout) : null;
80
+ if (!installedVersion) {
81
+ throw new Error(`${cliName} was installed but is not runnable: ${commandErrorDetail(result)}`);
82
+ }
83
+ if (installedVersion !== targetVersion) {
84
+ throw new Error(`${cliName} ${installedVersion} was installed; expected ${targetVersion}`);
85
+ }
86
+ return installedVersion;
87
+ }
88
+ async function installCliUpdate(input, runner = runCommand) {
89
+ const installArgs = ["npm", "install", "-g", "--omit=dev", input.workerPackageSpec, input.r5dctlPackageSpec];
90
+ const result = await runner(installArgs);
91
+ if (result.exitCode !== 0) {
92
+ throw new Error(`CLI update failed: ${commandErrorDetail(result)}
93
+ Run manually: ${getManualCliUpdateCommand(input)}`);
94
+ }
95
+ const [installedWorkerVersion, installedR5dctlVersion] = await Promise.all([
96
+ readVerifiedVersion("r5d-worker", input.targetWorkerVersion, runner),
97
+ readVerifiedVersion("r5dctl", input.targetR5dctlVersion, runner)
98
+ ]);
99
+ return { installedWorkerVersion, installedR5dctlVersion };
100
+ }
101
+ // Annotate the CommonJS export names for ESM import in node:
102
+ 0 && (module.exports = {
103
+ getManualCliUpdateCommand,
104
+ installCliUpdate,
105
+ parseCliVersion,
106
+ readInstalledCliVersion,
107
+ runCommand
108
+ });
package/dist/cjs/main.cjs CHANGED
@@ -46,8 +46,10 @@ var import_node_path = __toESM(require("node:path"), 1);
46
46
  var import_node_crypto = require("node:crypto");
47
47
  var import_node_os = __toESM(require("node:os"), 1);
48
48
  var import_node_child_process = require("node:child_process");
49
+ var import_cli_update = require("./cli-update.cjs");
49
50
  var import_git_identity = require("./git-identity.cjs");
50
51
  var import_heartbeat = require("./heartbeat.cjs");
52
+ var import_supervisor = require("./supervisor.cjs");
51
53
  const import_meta = {};
52
54
  const DEFAULT_BASE_URL = "https://r5d.dev";
53
55
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
@@ -2358,6 +2360,9 @@ async function startWorker(options) {
2358
2360
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2359
2361
  const manifestByProjectId = /* @__PURE__ */ new Map();
2360
2362
  let repositorySyncQueue = Promise.resolve();
2363
+ let repositorySyncInProgress = false;
2364
+ let cliUpdateInProgress = false;
2365
+ let reloadAfterClose = false;
2361
2366
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2362
2367
  headers: {
2363
2368
  Authorization: `Bearer ${token}`
@@ -2391,6 +2396,10 @@ async function startWorker(options) {
2391
2396
  arch: process.arch,
2392
2397
  pid: process.pid,
2393
2398
  version: getWorkerVersion(),
2399
+ r5dctlVersion: (0, import_cli_update.readInstalledCliVersion)("r5dctl"),
2400
+ capabilities: {
2401
+ updateClis: true
2402
+ },
2394
2403
  projectRoot: projectsRoot,
2395
2404
  artifactRoot,
2396
2405
  planRoot
@@ -2418,32 +2427,49 @@ async function startWorker(options) {
2418
2427
  return;
2419
2428
  }
2420
2429
  if (message.type === "sync_projects") {
2430
+ if (cliUpdateInProgress) {
2431
+ sendWorkerMessage(ws, {
2432
+ type: "sync_projects_result",
2433
+ requestId: message.requestId,
2434
+ result: {
2435
+ status: "blocked",
2436
+ results: [],
2437
+ blockedBy: []
2438
+ }
2439
+ });
2440
+ return;
2441
+ }
2421
2442
  let syncResult = { status: "completed", results: [], blockedBy: [] };
2422
2443
  const executeSync = async () => {
2423
- syncResult = await syncManifestProjectsFromInternal({
2424
- baseUrl,
2425
- token,
2426
- projectsRoot,
2427
- syncRoot,
2428
- manifests: [...manifestByProjectId.values()],
2429
- projectIds: message.projectIds
2430
- });
2431
- for (const result of syncResult.results) {
2432
- if (result.status !== "synced") continue;
2433
- try {
2434
- await syncProjectPlans({
2435
- baseUrl,
2436
- token,
2437
- projectId: result.projectId,
2438
- branchName: result.branchName,
2439
- planRoot
2440
- });
2441
- } catch (error) {
2442
- process.stderr.write(
2443
- `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2444
+ repositorySyncInProgress = true;
2445
+ try {
2446
+ syncResult = await syncManifestProjectsFromInternal({
2447
+ baseUrl,
2448
+ token,
2449
+ projectsRoot,
2450
+ syncRoot,
2451
+ manifests: [...manifestByProjectId.values()],
2452
+ projectIds: message.projectIds
2453
+ });
2454
+ for (const result of syncResult.results) {
2455
+ if (result.status !== "synced") continue;
2456
+ try {
2457
+ await syncProjectPlans({
2458
+ baseUrl,
2459
+ token,
2460
+ projectId: result.projectId,
2461
+ branchName: result.branchName,
2462
+ planRoot
2463
+ });
2464
+ } catch (error) {
2465
+ process.stderr.write(
2466
+ `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2444
2467
  `
2445
- );
2468
+ );
2469
+ }
2446
2470
  }
2471
+ } finally {
2472
+ repositorySyncInProgress = false;
2447
2473
  }
2448
2474
  };
2449
2475
  const queued = repositorySyncQueue.then(executeSync, executeSync);
@@ -2475,6 +2501,43 @@ async function startWorker(options) {
2475
2501
  });
2476
2502
  return;
2477
2503
  }
2504
+ if (message.type === "update_clis") {
2505
+ if (cliUpdateInProgress) {
2506
+ sendWorkerMessage(ws, {
2507
+ type: "update_clis_result",
2508
+ requestId: message.requestId,
2509
+ error: "A CLI update is already in progress"
2510
+ });
2511
+ return;
2512
+ }
2513
+ if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2514
+ sendWorkerMessage(ws, {
2515
+ type: "update_clis_result",
2516
+ requestId: message.requestId,
2517
+ error: "Worker is busy. Stop active commands and shells, then retry the CLI update."
2518
+ });
2519
+ return;
2520
+ }
2521
+ cliUpdateInProgress = true;
2522
+ try {
2523
+ const result = await (0, import_cli_update.installCliUpdate)(message);
2524
+ sendWorkerMessage(ws, {
2525
+ type: "update_clis_result",
2526
+ requestId: message.requestId,
2527
+ ...result
2528
+ });
2529
+ reloadAfterClose = true;
2530
+ ws.close(1e3, "Activating updated CLIs");
2531
+ } catch (error) {
2532
+ cliUpdateInProgress = false;
2533
+ sendWorkerMessage(ws, {
2534
+ type: "update_clis_result",
2535
+ requestId: message.requestId,
2536
+ error: error instanceof Error ? error.message : String(error)
2537
+ });
2538
+ }
2539
+ return;
2540
+ }
2478
2541
  if (message.type === "ping") {
2479
2542
  lastServerHeartbeatAt = Date.now();
2480
2543
  ws.send(JSON.stringify({ type: "pong" }));
@@ -2499,6 +2562,15 @@ async function startWorker(options) {
2499
2562
  return;
2500
2563
  }
2501
2564
  if (message.type === "pty_open") {
2565
+ if (cliUpdateInProgress) {
2566
+ sendWorkerMessage(ws, {
2567
+ type: "pty_error",
2568
+ requestId: message.requestId,
2569
+ ptyId: message.ptyId,
2570
+ error: "Worker is activating a CLI update"
2571
+ });
2572
+ return;
2573
+ }
2502
2574
  await repositorySyncQueue;
2503
2575
  const manifest = manifestByProjectId.get(message.projectId);
2504
2576
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -2520,6 +2592,16 @@ async function startWorker(options) {
2520
2592
  return;
2521
2593
  }
2522
2594
  if (message.type === "exec") {
2595
+ if (cliUpdateInProgress) {
2596
+ sendWorkerMessage(ws, {
2597
+ type: "exec_result",
2598
+ requestId: message.requestId,
2599
+ stdout: "",
2600
+ stderr: "Worker is activating a CLI update",
2601
+ exitCode: 1
2602
+ });
2603
+ return;
2604
+ }
2523
2605
  await repositorySyncQueue;
2524
2606
  const manifest = manifestByProjectId.get(message.projectId);
2525
2607
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -2540,6 +2622,15 @@ async function startWorker(options) {
2540
2622
  return;
2541
2623
  }
2542
2624
  if (message.type === "exec_start") {
2625
+ if (cliUpdateInProgress) {
2626
+ sendWorkerMessage(ws, {
2627
+ type: "exec_start_error",
2628
+ requestId: message.requestId,
2629
+ runId: message.runId,
2630
+ error: "Worker is activating a CLI update"
2631
+ });
2632
+ return;
2633
+ }
2543
2634
  await repositorySyncQueue;
2544
2635
  sendWorkerMessage(ws, {
2545
2636
  type: "exec_accepted",
@@ -2637,6 +2728,9 @@ async function startWorker(options) {
2637
2728
  const reason = event.reason ? `: ${event.reason}` : "";
2638
2729
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
2639
2730
  `);
2731
+ if (reloadAfterClose) {
2732
+ process.exit(import_supervisor.WORKER_RELOAD_EXIT_CODE);
2733
+ }
2640
2734
  if (activeProcesses.size > 0) {
2641
2735
  const delayMs = 2e3;
2642
2736
  process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
@@ -2675,7 +2769,15 @@ async function main() {
2675
2769
  printVersion();
2676
2770
  return;
2677
2771
  }
2678
- await startWorker(parsed.options);
2772
+ if (process.env[import_supervisor.WORKER_RUNTIME_ENV] === "1") {
2773
+ delete process.env[import_supervisor.WORKER_RUNTIME_ENV];
2774
+ await startWorker(parsed.options);
2775
+ return;
2776
+ }
2777
+ const exitCode = await (0, import_supervisor.superviseWorkerRuntime)(process.argv.slice(2));
2778
+ if (exitCode !== 0) {
2779
+ process.exit(exitCode);
2780
+ }
2679
2781
  }
2680
2782
  function isCliEntrypoint() {
2681
2783
  if (import_meta.main) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var supervisor_exports = {};
20
+ __export(supervisor_exports, {
21
+ WORKER_RELOAD_EXIT_CODE: () => WORKER_RELOAD_EXIT_CODE,
22
+ WORKER_RUNTIME_ENV: () => WORKER_RUNTIME_ENV,
23
+ superviseWorkerRuntime: () => superviseWorkerRuntime
24
+ });
25
+ module.exports = __toCommonJS(supervisor_exports);
26
+ const WORKER_RUNTIME_ENV = "R5D_WORKER_INTERNAL_RUNTIME";
27
+ const WORKER_RELOAD_EXIT_CODE = 75;
28
+ function defaultRuntimeSpawner(argv, options) {
29
+ return Bun.spawn(argv, options);
30
+ }
31
+ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner) {
32
+ if (!process.argv[1]) {
33
+ throw new Error("Cannot locate the r5d-worker entrypoint");
34
+ }
35
+ while (true) {
36
+ const runtime = spawnRuntime([process.execPath, process.argv[1], ...argv], {
37
+ stdin: "inherit",
38
+ stdout: "inherit",
39
+ stderr: "inherit",
40
+ env: {
41
+ ...env,
42
+ [WORKER_RUNTIME_ENV]: "1"
43
+ }
44
+ });
45
+ const exitCode = await runtime.exited;
46
+ if (exitCode === WORKER_RELOAD_EXIT_CODE) {
47
+ process.stdout.write("[r5d-worker] activating updated worker runtime\n");
48
+ continue;
49
+ }
50
+ return exitCode;
51
+ }
52
+ }
53
+ // Annotate the CommonJS export names for ESM import in node:
54
+ 0 && (module.exports = {
55
+ WORKER_RELOAD_EXIT_CODE,
56
+ WORKER_RUNTIME_ENV,
57
+ superviseWorkerRuntime
58
+ });
@@ -0,0 +1,80 @@
1
+ const UPDATE_OUTPUT_LIMIT = 8e3;
2
+ function collectOutput(value) {
3
+ if (!value || typeof value === "number") {
4
+ return Promise.resolve("");
5
+ }
6
+ return new Response(value).text();
7
+ }
8
+ async function runCommand(argv) {
9
+ const child = Bun.spawn(argv, {
10
+ stdout: "pipe",
11
+ stderr: "pipe",
12
+ env: process.env
13
+ });
14
+ const [stdout, stderr, exitCode] = await Promise.all([collectOutput(child.stdout), collectOutput(child.stderr), child.exited]);
15
+ return { exitCode, stdout, stderr };
16
+ }
17
+ function parseCliVersion(cliName, output) {
18
+ const escapedName = cliName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ const match = output.trim().match(new RegExp(`(?:^|\\n)${escapedName}\\s+(\\S+)`));
20
+ return match?.[1]?.trim() || null;
21
+ }
22
+ function readInstalledCliVersion(cliName) {
23
+ try {
24
+ const result = Bun.spawnSync([cliName, "--version"], {
25
+ stdout: "pipe",
26
+ stderr: "pipe",
27
+ env: process.env
28
+ });
29
+ if (result.exitCode !== 0) {
30
+ return null;
31
+ }
32
+ return parseCliVersion(cliName, Buffer.from(result.stdout).toString("utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function shellQuote(value) {
38
+ return `'${value.replaceAll("'", `'\\''`)}'`;
39
+ }
40
+ function getManualCliUpdateCommand(input) {
41
+ return `npm install -g --omit=dev ${shellQuote(input.workerPackageSpec)} ${shellQuote(input.r5dctlPackageSpec)}`;
42
+ }
43
+ function commandErrorDetail(result) {
44
+ const detail = `${result.stderr.trim()}${result.stderr.trim() && result.stdout.trim() ? "\n" : ""}${result.stdout.trim()}`.trim();
45
+ if (!detail) {
46
+ return `exit code ${result.exitCode}`;
47
+ }
48
+ return detail.slice(-UPDATE_OUTPUT_LIMIT);
49
+ }
50
+ async function readVerifiedVersion(cliName, targetVersion, runner) {
51
+ const result = await runner([cliName, "--version"]);
52
+ const installedVersion = result.exitCode === 0 ? parseCliVersion(cliName, result.stdout) : null;
53
+ if (!installedVersion) {
54
+ throw new Error(`${cliName} was installed but is not runnable: ${commandErrorDetail(result)}`);
55
+ }
56
+ if (installedVersion !== targetVersion) {
57
+ throw new Error(`${cliName} ${installedVersion} was installed; expected ${targetVersion}`);
58
+ }
59
+ return installedVersion;
60
+ }
61
+ async function installCliUpdate(input, runner = runCommand) {
62
+ const installArgs = ["npm", "install", "-g", "--omit=dev", input.workerPackageSpec, input.r5dctlPackageSpec];
63
+ const result = await runner(installArgs);
64
+ if (result.exitCode !== 0) {
65
+ throw new Error(`CLI update failed: ${commandErrorDetail(result)}
66
+ Run manually: ${getManualCliUpdateCommand(input)}`);
67
+ }
68
+ const [installedWorkerVersion, installedR5dctlVersion] = await Promise.all([
69
+ readVerifiedVersion("r5d-worker", input.targetWorkerVersion, runner),
70
+ readVerifiedVersion("r5dctl", input.targetR5dctlVersion, runner)
71
+ ]);
72
+ return { installedWorkerVersion, installedR5dctlVersion };
73
+ }
74
+ export {
75
+ getManualCliUpdateCommand,
76
+ installCliUpdate,
77
+ parseCliVersion,
78
+ readInstalledCliVersion,
79
+ runCommand
80
+ };
package/dist/mjs/main.mjs CHANGED
@@ -4,8 +4,10 @@ import path from "node:path";
4
4
  import { createHash } from "node:crypto";
5
5
  import os, { hostname } from "node:os";
6
6
  import { spawn as spawnChildProcess } from "node:child_process";
7
+ import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
7
8
  import { configureVisibleGitIdentity } from "./git-identity.mjs";
8
9
  import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
10
+ import { superviseWorkerRuntime, WORKER_RELOAD_EXIT_CODE, WORKER_RUNTIME_ENV } from "./supervisor.mjs";
9
11
  const DEFAULT_BASE_URL = "https://r5d.dev";
10
12
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
11
13
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
@@ -2315,6 +2317,9 @@ async function startWorker(options) {
2315
2317
  fs.mkdirSync(planRoot, { recursive: true });
2316
2318
  const manifestByProjectId = /* @__PURE__ */ new Map();
2317
2319
  let repositorySyncQueue = Promise.resolve();
2320
+ let repositorySyncInProgress = false;
2321
+ let cliUpdateInProgress = false;
2322
+ let reloadAfterClose = false;
2318
2323
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2319
2324
  headers: {
2320
2325
  Authorization: `Bearer ${token}`
@@ -2348,6 +2353,10 @@ async function startWorker(options) {
2348
2353
  arch: process.arch,
2349
2354
  pid: process.pid,
2350
2355
  version: getWorkerVersion(),
2356
+ r5dctlVersion: readInstalledCliVersion("r5dctl"),
2357
+ capabilities: {
2358
+ updateClis: true
2359
+ },
2351
2360
  projectRoot: projectsRoot,
2352
2361
  artifactRoot,
2353
2362
  planRoot
@@ -2375,32 +2384,49 @@ async function startWorker(options) {
2375
2384
  return;
2376
2385
  }
2377
2386
  if (message.type === "sync_projects") {
2387
+ if (cliUpdateInProgress) {
2388
+ sendWorkerMessage(ws, {
2389
+ type: "sync_projects_result",
2390
+ requestId: message.requestId,
2391
+ result: {
2392
+ status: "blocked",
2393
+ results: [],
2394
+ blockedBy: []
2395
+ }
2396
+ });
2397
+ return;
2398
+ }
2378
2399
  let syncResult = { status: "completed", results: [], blockedBy: [] };
2379
2400
  const executeSync = async () => {
2380
- syncResult = await syncManifestProjectsFromInternal({
2381
- baseUrl,
2382
- token,
2383
- projectsRoot,
2384
- syncRoot,
2385
- manifests: [...manifestByProjectId.values()],
2386
- projectIds: message.projectIds
2387
- });
2388
- for (const result of syncResult.results) {
2389
- if (result.status !== "synced") continue;
2390
- try {
2391
- await syncProjectPlans({
2392
- baseUrl,
2393
- token,
2394
- projectId: result.projectId,
2395
- branchName: result.branchName,
2396
- planRoot
2397
- });
2398
- } catch (error) {
2399
- process.stderr.write(
2400
- `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2401
+ repositorySyncInProgress = true;
2402
+ try {
2403
+ syncResult = await syncManifestProjectsFromInternal({
2404
+ baseUrl,
2405
+ token,
2406
+ projectsRoot,
2407
+ syncRoot,
2408
+ manifests: [...manifestByProjectId.values()],
2409
+ projectIds: message.projectIds
2410
+ });
2411
+ for (const result of syncResult.results) {
2412
+ if (result.status !== "synced") continue;
2413
+ try {
2414
+ await syncProjectPlans({
2415
+ baseUrl,
2416
+ token,
2417
+ projectId: result.projectId,
2418
+ branchName: result.branchName,
2419
+ planRoot
2420
+ });
2421
+ } catch (error) {
2422
+ process.stderr.write(
2423
+ `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2401
2424
  `
2402
- );
2425
+ );
2426
+ }
2403
2427
  }
2428
+ } finally {
2429
+ repositorySyncInProgress = false;
2404
2430
  }
2405
2431
  };
2406
2432
  const queued = repositorySyncQueue.then(executeSync, executeSync);
@@ -2432,6 +2458,43 @@ async function startWorker(options) {
2432
2458
  });
2433
2459
  return;
2434
2460
  }
2461
+ if (message.type === "update_clis") {
2462
+ if (cliUpdateInProgress) {
2463
+ sendWorkerMessage(ws, {
2464
+ type: "update_clis_result",
2465
+ requestId: message.requestId,
2466
+ error: "A CLI update is already in progress"
2467
+ });
2468
+ return;
2469
+ }
2470
+ if (activeProcesses.size > 0 || activePtys.size > 0 || repositorySyncInProgress) {
2471
+ sendWorkerMessage(ws, {
2472
+ type: "update_clis_result",
2473
+ requestId: message.requestId,
2474
+ error: "Worker is busy. Stop active commands and shells, then retry the CLI update."
2475
+ });
2476
+ return;
2477
+ }
2478
+ cliUpdateInProgress = true;
2479
+ try {
2480
+ const result = await installCliUpdate(message);
2481
+ sendWorkerMessage(ws, {
2482
+ type: "update_clis_result",
2483
+ requestId: message.requestId,
2484
+ ...result
2485
+ });
2486
+ reloadAfterClose = true;
2487
+ ws.close(1e3, "Activating updated CLIs");
2488
+ } catch (error) {
2489
+ cliUpdateInProgress = false;
2490
+ sendWorkerMessage(ws, {
2491
+ type: "update_clis_result",
2492
+ requestId: message.requestId,
2493
+ error: error instanceof Error ? error.message : String(error)
2494
+ });
2495
+ }
2496
+ return;
2497
+ }
2435
2498
  if (message.type === "ping") {
2436
2499
  lastServerHeartbeatAt = Date.now();
2437
2500
  ws.send(JSON.stringify({ type: "pong" }));
@@ -2456,6 +2519,15 @@ async function startWorker(options) {
2456
2519
  return;
2457
2520
  }
2458
2521
  if (message.type === "pty_open") {
2522
+ if (cliUpdateInProgress) {
2523
+ sendWorkerMessage(ws, {
2524
+ type: "pty_error",
2525
+ requestId: message.requestId,
2526
+ ptyId: message.ptyId,
2527
+ error: "Worker is activating a CLI update"
2528
+ });
2529
+ return;
2530
+ }
2459
2531
  await repositorySyncQueue;
2460
2532
  const manifest = manifestByProjectId.get(message.projectId);
2461
2533
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -2477,6 +2549,16 @@ async function startWorker(options) {
2477
2549
  return;
2478
2550
  }
2479
2551
  if (message.type === "exec") {
2552
+ if (cliUpdateInProgress) {
2553
+ sendWorkerMessage(ws, {
2554
+ type: "exec_result",
2555
+ requestId: message.requestId,
2556
+ stdout: "",
2557
+ stderr: "Worker is activating a CLI update",
2558
+ exitCode: 1
2559
+ });
2560
+ return;
2561
+ }
2480
2562
  await repositorySyncQueue;
2481
2563
  const manifest = manifestByProjectId.get(message.projectId);
2482
2564
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -2497,6 +2579,15 @@ async function startWorker(options) {
2497
2579
  return;
2498
2580
  }
2499
2581
  if (message.type === "exec_start") {
2582
+ if (cliUpdateInProgress) {
2583
+ sendWorkerMessage(ws, {
2584
+ type: "exec_start_error",
2585
+ requestId: message.requestId,
2586
+ runId: message.runId,
2587
+ error: "Worker is activating a CLI update"
2588
+ });
2589
+ return;
2590
+ }
2500
2591
  await repositorySyncQueue;
2501
2592
  sendWorkerMessage(ws, {
2502
2593
  type: "exec_accepted",
@@ -2594,6 +2685,9 @@ async function startWorker(options) {
2594
2685
  const reason = event.reason ? `: ${event.reason}` : "";
2595
2686
  process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
2596
2687
  `);
2688
+ if (reloadAfterClose) {
2689
+ process.exit(WORKER_RELOAD_EXIT_CODE);
2690
+ }
2597
2691
  if (activeProcesses.size > 0) {
2598
2692
  const delayMs = 2e3;
2599
2693
  process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
@@ -2632,7 +2726,15 @@ async function main() {
2632
2726
  printVersion();
2633
2727
  return;
2634
2728
  }
2635
- await startWorker(parsed.options);
2729
+ if (process.env[WORKER_RUNTIME_ENV] === "1") {
2730
+ delete process.env[WORKER_RUNTIME_ENV];
2731
+ await startWorker(parsed.options);
2732
+ return;
2733
+ }
2734
+ const exitCode = await superviseWorkerRuntime(process.argv.slice(2));
2735
+ if (exitCode !== 0) {
2736
+ process.exit(exitCode);
2737
+ }
2636
2738
  }
2637
2739
  function isCliEntrypoint() {
2638
2740
  if (import.meta.main) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,32 @@
1
+ const WORKER_RUNTIME_ENV = "R5D_WORKER_INTERNAL_RUNTIME";
2
+ const WORKER_RELOAD_EXIT_CODE = 75;
3
+ function defaultRuntimeSpawner(argv, options) {
4
+ return Bun.spawn(argv, options);
5
+ }
6
+ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner) {
7
+ if (!process.argv[1]) {
8
+ throw new Error("Cannot locate the r5d-worker entrypoint");
9
+ }
10
+ while (true) {
11
+ const runtime = spawnRuntime([process.execPath, process.argv[1], ...argv], {
12
+ stdin: "inherit",
13
+ stdout: "inherit",
14
+ stderr: "inherit",
15
+ env: {
16
+ ...env,
17
+ [WORKER_RUNTIME_ENV]: "1"
18
+ }
19
+ });
20
+ const exitCode = await runtime.exited;
21
+ if (exitCode === WORKER_RELOAD_EXIT_CODE) {
22
+ process.stdout.write("[r5d-worker] activating updated worker runtime\n");
23
+ continue;
24
+ }
25
+ return exitCode;
26
+ }
27
+ }
28
+ export {
29
+ WORKER_RELOAD_EXIT_CODE,
30
+ WORKER_RUNTIME_ENV,
31
+ superviseWorkerRuntime
32
+ };
@@ -0,0 +1,21 @@
1
+ export type CommandResult = {
2
+ exitCode: number;
3
+ stdout: string;
4
+ stderr: string;
5
+ };
6
+ export type CommandRunner = (argv: string[]) => Promise<CommandResult>;
7
+ export type CliUpdateRequest = {
8
+ workerPackageSpec: string;
9
+ r5dctlPackageSpec: string;
10
+ targetWorkerVersion: string;
11
+ targetR5dctlVersion: string;
12
+ };
13
+ export type CliUpdateResult = {
14
+ installedWorkerVersion: string;
15
+ installedR5dctlVersion: string;
16
+ };
17
+ export declare function runCommand(argv: string[]): Promise<CommandResult>;
18
+ export declare function parseCliVersion(cliName: "r5d-worker" | "r5dctl", output: string): string | null;
19
+ export declare function readInstalledCliVersion(cliName: "r5d-worker" | "r5dctl"): string | null;
20
+ export declare function getManualCliUpdateCommand(input: Pick<CliUpdateRequest, "workerPackageSpec" | "r5dctlPackageSpec">): string;
21
+ export declare function installCliUpdate(input: CliUpdateRequest, runner?: CommandRunner): Promise<CliUpdateResult>;
@@ -0,0 +1,13 @@
1
+ export declare const WORKER_RUNTIME_ENV = "R5D_WORKER_INTERNAL_RUNTIME";
2
+ export declare const WORKER_RELOAD_EXIT_CODE = 75;
3
+ type RuntimeProcess = {
4
+ exited: Promise<number>;
5
+ };
6
+ export type RuntimeSpawner = (argv: string[], options: {
7
+ stdin: "inherit";
8
+ stdout: "inherit";
9
+ stderr: "inherit";
10
+ env: Record<string, string | undefined>;
11
+ }) => RuntimeProcess;
12
+ export declare function superviseWorkerRuntime(argv: string[], env?: Record<string, string | undefined>, spawnRuntime?: RuntimeSpawner): Promise<number>;
13
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",