@ricsam/r5d-worker 0.0.28 → 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 +4 -0
- package/dist/cjs/cli-update.cjs +108 -0
- package/dist/cjs/main.cjs +125 -23
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/supervisor.cjs +58 -0
- package/dist/mjs/cli-update.mjs +80 -0
- package/dist/mjs/main.mjs +125 -23
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/supervisor.mjs +32 -0
- package/dist/types/cli-update.d.ts +21 -0
- package/dist/types/supervisor.d.ts +13 -0
- package/package.json +1 -1
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
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/cjs/package.json
CHANGED
|
@@ -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
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/mjs/package.json
CHANGED
|
@@ -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 {};
|