metheus-governance-mcp-cli 0.2.292 → 0.2.293
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/cli.mjs +500 -83
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -7501,26 +7501,31 @@ async function syncRunnerRequestLedgerForProjectToServer({ normalizedRoute, runt
|
|
|
7501
7501
|
}
|
|
7502
7502
|
}
|
|
7503
7503
|
|
|
7504
|
-
function normalizeRunnerProcessLaunchEntry(rawEntry) {
|
|
7505
|
-
const entry = safeObject(rawEntry);
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7504
|
+
function normalizeRunnerProcessLaunchEntry(rawEntry) {
|
|
7505
|
+
const entry = safeObject(rawEntry);
|
|
7506
|
+
const routeKeys = ensureArray(entry.route_keys || entry.routeKeys).map((item) => String(item || "").trim()).filter(Boolean);
|
|
7507
|
+
return cleanupRunnerStateRecord({
|
|
7508
|
+
launch_id: String(entry.launch_id || entry.launchID || "").trim(),
|
|
7509
|
+
pid: intFromRawAllowZero(entry.pid, 0) || undefined,
|
|
7510
|
+
started_at: String(entry.started_at || entry.startedAt || "").trim(),
|
|
7511
|
+
command: String(entry.command || "").trim(),
|
|
7512
|
+
cli_path: String(entry.cli_path || entry.cliPath || "").trim(),
|
|
7513
|
+
working_directory: sanitizeWorkspaceCandidate(entry.working_directory || entry.workingDirectory) || String(entry.working_directory || entry.workingDirectory || "").trim(),
|
|
7514
|
+
route_set_signature: String(entry.route_set_signature || entry.routeSetSignature || "").trim(),
|
|
7515
|
+
route_keys: routeKeys,
|
|
7516
|
+
route_names: ensureArray(entry.route_names || entry.routeNames).map((item) => String(item || "").trim()).filter(Boolean),
|
|
7517
|
+
project_ids: ensureArray(entry.project_ids || entry.projectIds).map((item) => String(item || "").trim()).filter(Boolean),
|
|
7518
|
+
provider: normalizeBotProvider(entry.provider),
|
|
7519
|
+
destination_labels: ensureArray(entry.destination_labels || entry.destinationLabels).map((item) => String(item || "").trim()).filter(Boolean),
|
|
7520
|
+
scheduling_target_keys: uniqueOrderedStrings([
|
|
7521
|
+
...ensureArray(entry.scheduling_target_keys || entry.schedulingTargetKeys).map((item) => String(item || "").trim()),
|
|
7522
|
+
...routeKeys.map((routeKey) => runnerSchedulingTargetKeyFromRouteKey(routeKey)).filter(Boolean),
|
|
7523
|
+
], (value) => String(value || "").trim()),
|
|
7524
|
+
log_file: String(entry.log_file || entry.logFile || "").trim(),
|
|
7525
|
+
created_by_pid: intFromRawAllowZero(entry.created_by_pid || entry.createdByPid, 0) || undefined,
|
|
7526
|
+
source_command: String(entry.source_command || entry.sourceCommand || "").trim(),
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
7524
7529
|
|
|
7525
7530
|
function normalizeBotRunnerProcessRegistryContents(rawValue) {
|
|
7526
7531
|
const launchesInput = safeObject(rawValue).launches;
|
|
@@ -12324,6 +12329,9 @@ function buildRunnerProjectTUISnapshot({
|
|
|
12324
12329
|
const summaryPayload = safeObject(normalizedAuditResult.summaryPayload);
|
|
12325
12330
|
const matchingRoutes = ensureArray(normalizedAuditResult.matchingRoutes)
|
|
12326
12331
|
.map((route) => normalizeRunnerRoute(route));
|
|
12332
|
+
const matchingRouteNames = matchingRoutes
|
|
12333
|
+
.map((route) => String(route.name || runnerRouteKey(route)).trim())
|
|
12334
|
+
.filter(Boolean);
|
|
12327
12335
|
const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
|
|
12328
12336
|
const existingLaunch = matchingRoutes.length
|
|
12329
12337
|
? findExistingDetachedRunnerLaunch(registry, matchingRoutes)
|
|
@@ -12334,22 +12342,60 @@ function buildRunnerProjectTUISnapshot({
|
|
|
12334
12342
|
alive: true,
|
|
12335
12343
|
}
|
|
12336
12344
|
: null;
|
|
12345
|
+
const destinationID = String(summaryPayload.destination_id || "").trim();
|
|
12346
|
+
const destinationLabel = String(summaryPayload.destination_label || "").trim();
|
|
12347
|
+
const projectID = String(summaryPayload.project_id || "").trim();
|
|
12348
|
+
const relatedLaunches = Object.values(safeObject(registry).launches || {})
|
|
12349
|
+
.map((entry) => normalizeRunnerProcessLaunchEntry(entry))
|
|
12350
|
+
.filter((entry) => isProcessAlive(entry.pid))
|
|
12351
|
+
.filter((entry) => {
|
|
12352
|
+
const sameProject = ensureArray(entry.project_ids)
|
|
12353
|
+
.map((item) => String(item || "").trim())
|
|
12354
|
+
.includes(projectID);
|
|
12355
|
+
if (!sameProject) return false;
|
|
12356
|
+
const sameDestinationLabel = destinationLabel
|
|
12357
|
+
? ensureArray(entry.destination_labels)
|
|
12358
|
+
.map((item) => String(item || "").trim().toLowerCase())
|
|
12359
|
+
.includes(destinationLabel.toLowerCase())
|
|
12360
|
+
: false;
|
|
12361
|
+
const sameDestinationID = destinationID
|
|
12362
|
+
? ensureArray(entry.route_keys)
|
|
12363
|
+
.map((item) => String(item || "").trim())
|
|
12364
|
+
.some((item) => item.includes(`::${destinationID}`))
|
|
12365
|
+
: false;
|
|
12366
|
+
return sameDestinationLabel || sameDestinationID;
|
|
12367
|
+
})
|
|
12368
|
+
.map((entry) => {
|
|
12369
|
+
const routeNames = ensureArray(entry.route_names)
|
|
12370
|
+
.map((item) => String(item || "").trim())
|
|
12371
|
+
.filter(Boolean);
|
|
12372
|
+
const sharedRouteNames = routeNames.filter((routeName) => matchingRouteNames.includes(routeName));
|
|
12373
|
+
return {
|
|
12374
|
+
launch_id: String(entry.launch_id || "").trim(),
|
|
12375
|
+
pid: intFromRawAllowZero(entry.pid, 0),
|
|
12376
|
+
route_names: routeNames,
|
|
12377
|
+
shared_route_names: sharedRouteNames,
|
|
12378
|
+
source_command: String(entry.source_command || "").trim(),
|
|
12379
|
+
same_selection: Boolean(launch && String(launch.launch_id || "").trim() === String(entry.launch_id || "").trim()),
|
|
12380
|
+
};
|
|
12381
|
+
});
|
|
12337
12382
|
const logFilePath = String(launch?.log_file || "").trim();
|
|
12338
12383
|
const logLines = readTextFileTailLines(logFilePath, 12);
|
|
12339
12384
|
return {
|
|
12340
|
-
project_id:
|
|
12341
|
-
destination_label:
|
|
12342
|
-
destination_id:
|
|
12385
|
+
project_id: projectID,
|
|
12386
|
+
destination_label: destinationLabel,
|
|
12387
|
+
destination_id: destinationID,
|
|
12343
12388
|
room_probe_ok: Boolean(summaryPayload.room_probe_ok),
|
|
12344
12389
|
route_apply_requested: Boolean(summaryPayload.route_apply_requested),
|
|
12345
12390
|
route_apply_changed: Boolean(summaryPayload.route_apply_changed),
|
|
12346
12391
|
route_config_file: String(summaryPayload.route_config_file || "").trim(),
|
|
12347
12392
|
enabled_routes: ensureArray(summaryPayload.enabled_routes_for_selection).map((item) => String(item || "").trim()).filter(Boolean),
|
|
12348
|
-
matching_routes:
|
|
12393
|
+
matching_routes: matchingRouteNames,
|
|
12349
12394
|
next_steps: ensureArray(summaryPayload.next_steps).map((item) => String(item || "").trim()).filter(Boolean),
|
|
12350
12395
|
warning: String(summaryPayload.warning || "").trim(),
|
|
12351
12396
|
error: String(summaryPayload.error || errorMessage || "").trim(),
|
|
12352
12397
|
launch,
|
|
12398
|
+
related_launches: relatedLaunches,
|
|
12353
12399
|
auto_start: autoStart,
|
|
12354
12400
|
status_message: String(statusMessage || "").trim(),
|
|
12355
12401
|
last_audit_at: String(lastAuditAt || "").trim() || new Date().toISOString(),
|
|
@@ -12360,6 +12406,25 @@ function buildRunnerProjectTUISnapshot({
|
|
|
12360
12406
|
};
|
|
12361
12407
|
}
|
|
12362
12408
|
|
|
12409
|
+
function buildRunnerProjectTUIKeyLine(label, value, labelWidth = 14) {
|
|
12410
|
+
return `${bootstrapPadRight(`${label}:`, labelWidth)} ${String(value || "-").trim() || "-"}`;
|
|
12411
|
+
}
|
|
12412
|
+
|
|
12413
|
+
function buildRunnerProjectTUIList(items = [], {
|
|
12414
|
+
emptyText = "-",
|
|
12415
|
+
bullet = "- ",
|
|
12416
|
+
indent = " ",
|
|
12417
|
+
width = 140,
|
|
12418
|
+
} = {}) {
|
|
12419
|
+
const normalizedItems = ensureArray(items)
|
|
12420
|
+
.map((item) => String(item || "").trim())
|
|
12421
|
+
.filter(Boolean);
|
|
12422
|
+
if (!normalizedItems.length) {
|
|
12423
|
+
return [`${indent}${emptyText}`];
|
|
12424
|
+
}
|
|
12425
|
+
return normalizedItems.map((item) => `${indent}${bullet}${truncateRunnerTUIText(item, width)}`);
|
|
12426
|
+
}
|
|
12427
|
+
|
|
12363
12428
|
function buildRunnerProjectTUIFrame({
|
|
12364
12429
|
snapshot,
|
|
12365
12430
|
now = Date.now(),
|
|
@@ -12368,41 +12433,105 @@ function buildRunnerProjectTUIFrame({
|
|
|
12368
12433
|
}) {
|
|
12369
12434
|
const normalizedSnapshot = safeObject(snapshot);
|
|
12370
12435
|
const alive = Boolean(safeObject(normalizedSnapshot.launch).alive);
|
|
12371
|
-
const statusLabel = alive ? "RUNNING" : "STOPPED";
|
|
12436
|
+
const statusLabel = alive ? "[ RUNNING ]" : "[ STOPPED ]";
|
|
12372
12437
|
const statusDetail = alive
|
|
12373
12438
|
? `launch=${String(safeObject(normalizedSnapshot.launch).launch_id || "").trim() || "-"} pid=${String(safeObject(normalizedSnapshot.launch).pid || "").trim() || "-"}`
|
|
12374
12439
|
: "no detached runner is active for this selection";
|
|
12440
|
+
const enabledRoutes = ensureArray(normalizedSnapshot.enabled_routes);
|
|
12441
|
+
const matchingRoutes = ensureArray(normalizedSnapshot.matching_routes);
|
|
12442
|
+
const relatedLaunches = ensureArray(normalizedSnapshot.related_launches);
|
|
12443
|
+
const selectedLaunch = safeObject(normalizedSnapshot.launch);
|
|
12444
|
+
const otherLaunches = relatedLaunches.filter((launch) => !safeObject(launch).same_selection);
|
|
12445
|
+
const overlappingLaunches = otherLaunches.filter((launch) => ensureArray(safeObject(launch).shared_route_names).length > 0);
|
|
12446
|
+
const alertLines = [];
|
|
12447
|
+
if (String(normalizedSnapshot.warning || "").trim()) {
|
|
12448
|
+
alertLines.push(`warning: ${String(normalizedSnapshot.warning || "").trim()}`);
|
|
12449
|
+
}
|
|
12450
|
+
if (String(normalizedSnapshot.error || "").trim()) {
|
|
12451
|
+
alertLines.push(`error: ${String(normalizedSnapshot.error || "").trim()}`);
|
|
12452
|
+
}
|
|
12453
|
+
if (overlappingLaunches.length) {
|
|
12454
|
+
alertLines.push(`overlap: ${overlappingLaunches.length} other detached launch(es) share at least one route in this room`);
|
|
12455
|
+
} else if (otherLaunches.length) {
|
|
12456
|
+
alertLines.push(`notice: ${otherLaunches.length} other detached launch(es) are active for this project/destination`);
|
|
12457
|
+
}
|
|
12458
|
+
if (!alive) {
|
|
12459
|
+
alertLines.push("runner is not active; press s to start or reuse a detached runner");
|
|
12460
|
+
}
|
|
12461
|
+
const stateHint = alive
|
|
12462
|
+
? "This screen can be closed with q. The detached runner keeps running."
|
|
12463
|
+
: "No detached runner is attached to this selection yet.";
|
|
12375
12464
|
const lines = [
|
|
12376
|
-
"
|
|
12377
|
-
"|
|
|
12378
|
-
"|
|
|
12379
|
-
"
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
`
|
|
12383
|
-
`
|
|
12384
|
-
`
|
|
12385
|
-
`Routes: ${ensureArray(normalizedSnapshot.enabled_routes).join(", ") || "-"}`,
|
|
12386
|
-
`Room Probe OK: ${normalizedSnapshot.room_probe_ok ? "true" : "false"}`,
|
|
12387
|
-
`Route Apply Changed: ${normalizedSnapshot.route_apply_changed ? "true" : "false"}`,
|
|
12388
|
-
`Log File: ${String(normalizedSnapshot.log_file || "").trim() || "-"}`,
|
|
12389
|
-
`Last Audit: ${String(normalizedSnapshot.last_audit_at || "").trim() || "-"}`,
|
|
12390
|
-
`Updated: ${new Date(now).toLocaleString("sv-SE", { hour12: false })}`,
|
|
12391
|
-
`Message: ${String(normalizedSnapshot.status_message || "").trim() || "-"}`,
|
|
12392
|
-
`Warning: ${String(normalizedSnapshot.warning || "").trim() || "-"}`,
|
|
12393
|
-
`Error: ${String(normalizedSnapshot.error || "").trim() || "-"}`,
|
|
12465
|
+
"+======================================================================================+",
|
|
12466
|
+
"| METHEUS PROJECT RUNNER |",
|
|
12467
|
+
"| Detached runner dashboard |",
|
|
12468
|
+
"+======================================================================================+",
|
|
12469
|
+
"",
|
|
12470
|
+
"[ STATUS ]",
|
|
12471
|
+
` ${statusLabel}${stopping ? " (closing TUI)" : ""}`,
|
|
12472
|
+
` ${statusDetail}`,
|
|
12473
|
+
` ${stateHint}`,
|
|
12394
12474
|
"",
|
|
12395
|
-
"
|
|
12475
|
+
"[ PROJECT ]",
|
|
12476
|
+
` ${buildRunnerProjectTUIKeyLine("Project", normalizedSnapshot.project_id)}`,
|
|
12477
|
+
` ${buildRunnerProjectTUIKeyLine("Destination", String(normalizedSnapshot.destination_label || normalizedSnapshot.destination_id || "").trim() || "-")}`,
|
|
12478
|
+
` ${buildRunnerProjectTUIKeyLine("Updated", new Date(now).toLocaleString("sv-SE", { hour12: false }))}`,
|
|
12479
|
+
` ${buildRunnerProjectTUIKeyLine("Last Audit", String(normalizedSnapshot.last_audit_at || "").trim() || "-")}`,
|
|
12396
12480
|
"",
|
|
12397
|
-
"
|
|
12398
|
-
|
|
12481
|
+
"[ RUNNER ]",
|
|
12482
|
+
` ${buildRunnerProjectTUIKeyLine("Auto Start", normalizedSnapshot.auto_start ? "enabled" : "prepare-only")}`,
|
|
12483
|
+
` ${buildRunnerProjectTUIKeyLine("Room Probe", normalizedSnapshot.room_probe_ok ? "ok" : "failed")}`,
|
|
12484
|
+
` ${buildRunnerProjectTUIKeyLine("Apply Result", normalizedSnapshot.route_apply_changed ? "route file changed" : "no route file change")}`,
|
|
12485
|
+
` ${buildRunnerProjectTUIKeyLine("Active Launches", `${relatedLaunches.length}`)}`,
|
|
12486
|
+
` ${buildRunnerProjectTUIKeyLine("This Launch", alive ? String(selectedLaunch.launch_id || "-").trim() || "-" : "-")}`,
|
|
12487
|
+
` ${buildRunnerProjectTUIKeyLine("PID", alive ? String(selectedLaunch.pid || "-").trim() || "-" : "-")}`,
|
|
12488
|
+
` ${buildRunnerProjectTUIKeyLine("Log File", String(normalizedSnapshot.log_file || "").trim() || "-")}`,
|
|
12489
|
+
"",
|
|
12490
|
+
"[ ROUTES ]",
|
|
12491
|
+
` ${buildRunnerProjectTUIKeyLine("Enabled", `${enabledRoutes.length}`)}`,
|
|
12492
|
+
` ${buildRunnerProjectTUIKeyLine("Matched", `${matchingRoutes.length}`)}`,
|
|
12493
|
+
...buildRunnerProjectTUIList(matchingRoutes.length ? matchingRoutes : enabledRoutes, {
|
|
12494
|
+
emptyText: "no routes selected",
|
|
12495
|
+
bullet: "- ",
|
|
12496
|
+
indent: " ",
|
|
12497
|
+
width: 96,
|
|
12498
|
+
}),
|
|
12499
|
+
"",
|
|
12500
|
+
"[ ALERTS ]",
|
|
12501
|
+
...buildRunnerProjectTUIList(alertLines, {
|
|
12502
|
+
emptyText: "none",
|
|
12503
|
+
bullet: "- ",
|
|
12504
|
+
indent: " ",
|
|
12505
|
+
width: 96,
|
|
12506
|
+
}),
|
|
12507
|
+
"",
|
|
12508
|
+
"[ CONTROLS ]",
|
|
12509
|
+
" s = start/reuse detached runner",
|
|
12510
|
+
" x = stop detached runner",
|
|
12511
|
+
" r = refresh status",
|
|
12512
|
+
" a = re-audit route selection",
|
|
12513
|
+
" q = quit this dashboard",
|
|
12514
|
+
"",
|
|
12515
|
+
"[ RECENT LOG TAIL ]",
|
|
12516
|
+
...ensureArray(normalizedSnapshot.log_lines).map((line) => ` ${truncateRunnerTUIText(line, 108)}`),
|
|
12399
12517
|
];
|
|
12518
|
+
if (otherLaunches.length) {
|
|
12519
|
+
lines.push("");
|
|
12520
|
+
lines.push("[ OTHER ACTIVE LAUNCHES ]");
|
|
12521
|
+
for (const launch of otherLaunches) {
|
|
12522
|
+
const currentLaunch = safeObject(launch);
|
|
12523
|
+
const sharedRoutes = ensureArray(currentLaunch.shared_route_names).join(", ") || "-";
|
|
12524
|
+
lines.push(` - ${String(currentLaunch.launch_id || "-").trim() || "-"} pid=${String(currentLaunch.pid || "-").trim() || "-"} source=${String(currentLaunch.source_command || "-").trim() || "-"}`);
|
|
12525
|
+
lines.push(` shared routes: ${truncateRunnerTUIText(sharedRoutes, 96)}`);
|
|
12526
|
+
lines.push(` all routes: ${truncateRunnerTUIText(ensureArray(currentLaunch.route_names).join(", ") || "-", 96)}`);
|
|
12527
|
+
}
|
|
12528
|
+
}
|
|
12400
12529
|
const frame = lines.join("\n");
|
|
12401
12530
|
if (!useColor) {
|
|
12402
12531
|
return frame;
|
|
12403
12532
|
}
|
|
12404
12533
|
const statusColor = alive ? "\u001b[32m" : "\u001b[33m";
|
|
12405
|
-
return frame.replace(
|
|
12534
|
+
return frame.replace(statusLabel, `${statusColor}${statusLabel}\u001b[0m`);
|
|
12406
12535
|
}
|
|
12407
12536
|
|
|
12408
12537
|
async function runRunnerProjectTUI(flags) {
|
|
@@ -13083,13 +13212,121 @@ function serializeCLIFlags(flags, options = {}) {
|
|
|
13083
13212
|
return args;
|
|
13084
13213
|
}
|
|
13085
13214
|
|
|
13086
|
-
function runnerDetachedRouteSetSignature(routes) {
|
|
13087
|
-
return ensureArray(routes)
|
|
13088
|
-
.map((route) => runnerRouteKey(normalizeRunnerRoute(route)))
|
|
13089
|
-
.filter(Boolean)
|
|
13090
|
-
.sort()
|
|
13091
|
-
.join("||");
|
|
13092
|
-
}
|
|
13215
|
+
function runnerDetachedRouteSetSignature(routes) {
|
|
13216
|
+
return ensureArray(routes)
|
|
13217
|
+
.map((route) => runnerRouteKey(normalizeRunnerRoute(route)))
|
|
13218
|
+
.filter(Boolean)
|
|
13219
|
+
.sort()
|
|
13220
|
+
.join("||");
|
|
13221
|
+
}
|
|
13222
|
+
|
|
13223
|
+
function runnerSchedulingTargetKeyFromRouteKey(routeKey) {
|
|
13224
|
+
const parts = String(routeKey || "").trim().split("::").filter(Boolean);
|
|
13225
|
+
if (parts.length < 6) {
|
|
13226
|
+
return "";
|
|
13227
|
+
}
|
|
13228
|
+
return [
|
|
13229
|
+
String(parts[parts.length - 5] || "").trim(),
|
|
13230
|
+
String(parts[parts.length - 4] || "").trim(),
|
|
13231
|
+
String(parts[parts.length - 1] || "").trim(),
|
|
13232
|
+
].join("::");
|
|
13233
|
+
}
|
|
13234
|
+
|
|
13235
|
+
function collectRunnerDetachedLaunchSchedulingTargetKeys(entryRaw) {
|
|
13236
|
+
const entry = normalizeRunnerProcessLaunchEntry(entryRaw);
|
|
13237
|
+
const explicitKeys = uniqueOrderedStrings(
|
|
13238
|
+
ensureArray(entry.scheduling_target_keys || entry.schedulingTargetKeys),
|
|
13239
|
+
(value) => String(value || "").trim(),
|
|
13240
|
+
).filter(Boolean);
|
|
13241
|
+
if (explicitKeys.length > 0) {
|
|
13242
|
+
return explicitKeys;
|
|
13243
|
+
}
|
|
13244
|
+
const routeKeyDerived = uniqueOrderedStrings(
|
|
13245
|
+
ensureArray(entry.route_keys).map((value) => runnerSchedulingTargetKeyFromRouteKey(value)).filter(Boolean),
|
|
13246
|
+
(value) => String(value || "").trim(),
|
|
13247
|
+
).filter(Boolean);
|
|
13248
|
+
if (routeKeyDerived.length > 0) {
|
|
13249
|
+
return routeKeyDerived;
|
|
13250
|
+
}
|
|
13251
|
+
const provider = normalizeBotProvider(entry.provider);
|
|
13252
|
+
const projectIDs = ensureArray(entry.project_ids).map((value) => String(value || "").trim()).filter(Boolean);
|
|
13253
|
+
const destinations = ensureArray(entry.destination_labels).map((value) => String(value || "").trim()).filter(Boolean);
|
|
13254
|
+
return uniqueOrderedStrings(
|
|
13255
|
+
projectIDs.flatMap((projectID) => destinations.map((destination) => [projectID, provider, destination].join("::"))),
|
|
13256
|
+
(value) => String(value || "").trim(),
|
|
13257
|
+
).filter(Boolean);
|
|
13258
|
+
}
|
|
13259
|
+
|
|
13260
|
+
function describeDetachedRunnerLaunch(entryRaw) {
|
|
13261
|
+
const entry = normalizeRunnerProcessLaunchEntry(entryRaw);
|
|
13262
|
+
return `launch_id=${entry.launch_id || "-"} pid=${entry.pid || "-"} routes=${ensureArray(entry.route_names).join(", ") || "-"}`;
|
|
13263
|
+
}
|
|
13264
|
+
|
|
13265
|
+
function classifyDetachedRunnerLaunchReuse(registry, routes) {
|
|
13266
|
+
const normalizedRoutes = ensureArray(routes).map((route) => normalizeRunnerRoute(route));
|
|
13267
|
+
const targetSignature = runnerDetachedRouteSetSignature(normalizedRoutes);
|
|
13268
|
+
const targetRouteKeys = normalizedRoutes.map((route) => runnerRouteKey(route)).filter(Boolean);
|
|
13269
|
+
const targetRouteKeySet = new Set(targetRouteKeys);
|
|
13270
|
+
const targetSchedulingTargetKeys = uniqueOrderedStrings(
|
|
13271
|
+
normalizedRoutes.map((route) => runnerRouteSchedulingGroupKey(route)).filter(Boolean),
|
|
13272
|
+
(value) => String(value || "").trim(),
|
|
13273
|
+
).filter(Boolean);
|
|
13274
|
+
const aliveLaunches = Object.values(safeObject(registry).launches || {})
|
|
13275
|
+
.map((entryRaw) => normalizeRunnerProcessLaunchEntry(entryRaw))
|
|
13276
|
+
.filter((entry) => entry.launch_id && entry.pid && isProcessAlive(entry.pid));
|
|
13277
|
+
const exact = aliveLaunches.find((entry) => entry.route_set_signature === targetSignature) || null;
|
|
13278
|
+
if (exact) {
|
|
13279
|
+
return {
|
|
13280
|
+
kind: "reuse",
|
|
13281
|
+
reason: "exact",
|
|
13282
|
+
launch: exact,
|
|
13283
|
+
};
|
|
13284
|
+
}
|
|
13285
|
+
if (!targetSchedulingTargetKeys.length) {
|
|
13286
|
+
return {
|
|
13287
|
+
kind: "none",
|
|
13288
|
+
};
|
|
13289
|
+
}
|
|
13290
|
+
const sameTargetLaunches = aliveLaunches.filter((entry) => {
|
|
13291
|
+
const entryTargetKeys = collectRunnerDetachedLaunchSchedulingTargetKeys(entry);
|
|
13292
|
+
return entryTargetKeys.some((key) => targetSchedulingTargetKeys.includes(key));
|
|
13293
|
+
});
|
|
13294
|
+
if (!sameTargetLaunches.length) {
|
|
13295
|
+
return {
|
|
13296
|
+
kind: "none",
|
|
13297
|
+
};
|
|
13298
|
+
}
|
|
13299
|
+
const supersets = sameTargetLaunches.filter((entry) => targetRouteKeys.every((routeKey) => ensureArray(entry.route_keys).includes(routeKey)));
|
|
13300
|
+
if (supersets.length === 1) {
|
|
13301
|
+
return {
|
|
13302
|
+
kind: "reuse",
|
|
13303
|
+
reason: "superset",
|
|
13304
|
+
launch: supersets[0],
|
|
13305
|
+
};
|
|
13306
|
+
}
|
|
13307
|
+
const conflictingLaunches = supersets.length > 1 ? supersets : sameTargetLaunches;
|
|
13308
|
+
const overlapRouteNames = Array.from(
|
|
13309
|
+
new Set(
|
|
13310
|
+
conflictingLaunches.flatMap((entry) => ensureArray(entry.route_keys))
|
|
13311
|
+
.filter((routeKey) => targetRouteKeySet.has(routeKey))
|
|
13312
|
+
.map((routeKey) => {
|
|
13313
|
+
const matchedRoute = normalizedRoutes.find((route) => runnerRouteKey(route) === routeKey);
|
|
13314
|
+
return matchedRoute?.name || routeKey;
|
|
13315
|
+
})
|
|
13316
|
+
.filter(Boolean),
|
|
13317
|
+
),
|
|
13318
|
+
);
|
|
13319
|
+
const detail = supersets.length > 1
|
|
13320
|
+
? `multiple detached runners already cover the requested route set for the same project/provider/destination target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")})`
|
|
13321
|
+
: `another detached runner already owns the same project/provider/destination target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")}). Stop it first or use runner project tui so one detached runner owns the full route set for that destination`;
|
|
13322
|
+
return {
|
|
13323
|
+
kind: "conflict",
|
|
13324
|
+
detail,
|
|
13325
|
+
launch: conflictingLaunches.length === 1 ? conflictingLaunches[0] : null,
|
|
13326
|
+
conflicting_launches: conflictingLaunches,
|
|
13327
|
+
overlapping_route_names: overlapRouteNames,
|
|
13328
|
+
};
|
|
13329
|
+
}
|
|
13093
13330
|
|
|
13094
13331
|
function buildRunnerDetachedPosixLaunchScript({
|
|
13095
13332
|
scriptPath,
|
|
@@ -13149,25 +13386,28 @@ function buildRunnerDetachedLaunchRecord(childPID, routes, flags, sourceCommand,
|
|
|
13149
13386
|
cli_path: cliPath,
|
|
13150
13387
|
working_directory: path.dirname(cliPath),
|
|
13151
13388
|
route_set_signature: runnerDetachedRouteSetSignature(normalizedRoutes),
|
|
13152
|
-
route_keys: normalizedRoutes.map((route) => runnerRouteKey(route)),
|
|
13153
|
-
route_names: normalizedRoutes.map((route) => route.name || runnerRouteKey(route)),
|
|
13154
|
-
project_ids: Array.from(new Set(normalizedRoutes.map((route) => String(route.projectID || "").trim()).filter(Boolean))),
|
|
13155
|
-
provider: firstNonEmptyString(normalizedRoutes.map((route) => route.provider)),
|
|
13156
|
-
destination_labels: Array.from(new Set(normalizedRoutes.map((route) => String(route.destinationLabel || route.destinationID || "").trim()).filter(Boolean))),
|
|
13157
|
-
|
|
13158
|
-
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13389
|
+
route_keys: normalizedRoutes.map((route) => runnerRouteKey(route)),
|
|
13390
|
+
route_names: normalizedRoutes.map((route) => route.name || runnerRouteKey(route)),
|
|
13391
|
+
project_ids: Array.from(new Set(normalizedRoutes.map((route) => String(route.projectID || "").trim()).filter(Boolean))),
|
|
13392
|
+
provider: firstNonEmptyString(normalizedRoutes.map((route) => route.provider)),
|
|
13393
|
+
destination_labels: Array.from(new Set(normalizedRoutes.map((route) => String(route.destinationLabel || route.destinationID || "").trim()).filter(Boolean))),
|
|
13394
|
+
scheduling_target_keys: uniqueOrderedStrings(
|
|
13395
|
+
normalizedRoutes.map((route) => runnerRouteSchedulingGroupKey(route)).filter(Boolean),
|
|
13396
|
+
(value) => String(value || "").trim(),
|
|
13397
|
+
),
|
|
13398
|
+
log_file: String(logFilePath || "").trim(),
|
|
13399
|
+
created_by_pid: process.pid,
|
|
13400
|
+
source_command: sourceCommand,
|
|
13401
|
+
});
|
|
13402
|
+
}
|
|
13162
13403
|
|
|
13163
|
-
function findExistingDetachedRunnerLaunch(registry, routes) {
|
|
13164
|
-
const
|
|
13165
|
-
if (
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
}
|
|
13404
|
+
function findExistingDetachedRunnerLaunch(registry, routes) {
|
|
13405
|
+
const decision = classifyDetachedRunnerLaunchReuse(registry, routes);
|
|
13406
|
+
if (decision.kind === "reuse" && decision.launch) {
|
|
13407
|
+
return decision.launch;
|
|
13408
|
+
}
|
|
13409
|
+
return null;
|
|
13410
|
+
}
|
|
13171
13411
|
|
|
13172
13412
|
async function launchDetachedRunnerProcess(flags, routes, sourceCommand) {
|
|
13173
13413
|
const cliPath = fileURLToPath(import.meta.url);
|
|
@@ -13359,13 +13599,15 @@ async function runRunnerStop(flags) {
|
|
|
13359
13599
|
async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand = "runner start-detached", options = {}) {
|
|
13360
13600
|
const silent = boolFromRaw(safeObject(options).silent, false);
|
|
13361
13601
|
const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
|
|
13362
|
-
const
|
|
13363
|
-
if (
|
|
13602
|
+
const reuseDecision = classifyDetachedRunnerLaunchReuse(registry, routes);
|
|
13603
|
+
if (reuseDecision.kind === "reuse" && reuseDecision.launch) {
|
|
13604
|
+
const existing = reuseDecision.launch;
|
|
13364
13605
|
const payload = {
|
|
13365
13606
|
ok: true,
|
|
13366
|
-
already_running: true,
|
|
13367
|
-
|
|
13368
|
-
|
|
13607
|
+
already_running: true,
|
|
13608
|
+
reuse_reason: reuseDecision.reason,
|
|
13609
|
+
registry_file: registry.filePath,
|
|
13610
|
+
launch: {
|
|
13369
13611
|
...existing,
|
|
13370
13612
|
alive: true,
|
|
13371
13613
|
},
|
|
@@ -13375,15 +13617,24 @@ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand
|
|
|
13375
13617
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
13376
13618
|
return payload;
|
|
13377
13619
|
}
|
|
13378
|
-
process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}${existing.log_file ? ` log_file=${existing.log_file}` : ""}\n`);
|
|
13620
|
+
process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}${existing.log_file ? ` log_file=${existing.log_file}` : ""}${reuseDecision.reason ? ` reuse=${reuseDecision.reason}` : ""}\n`);
|
|
13379
13621
|
}
|
|
13380
13622
|
return payload;
|
|
13381
13623
|
}
|
|
13624
|
+
if (reuseDecision.kind === "conflict") {
|
|
13625
|
+
const overlappingRouteNames = ensureArray(reuseDecision.overlapping_route_names)
|
|
13626
|
+
.map((item) => String(item || "").trim())
|
|
13627
|
+
.filter(Boolean);
|
|
13628
|
+
const overlapSuffix = overlappingRouteNames.length
|
|
13629
|
+
? ` overlapping_routes=${overlappingRouteNames.join(", ")}`
|
|
13630
|
+
: "";
|
|
13631
|
+
throw new Error(`${String(reuseDecision.detail || "detached runner conflict").trim()}${overlapSuffix}`);
|
|
13632
|
+
}
|
|
13382
13633
|
const launch = await launchDetachedRunnerProcess(flags, routes, sourceCommand);
|
|
13383
13634
|
const nextLaunches = {
|
|
13384
13635
|
...safeObject(registry).launches,
|
|
13385
13636
|
[launch.launch_id]: launch,
|
|
13386
|
-
};
|
|
13637
|
+
};
|
|
13387
13638
|
saveBotRunnerProcessRegistry({ launches: nextLaunches });
|
|
13388
13639
|
const payload = {
|
|
13389
13640
|
ok: true,
|
|
@@ -19658,10 +19909,176 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
19658
19909
|
} catch (err) {
|
|
19659
19910
|
push("detached_runner_launch_record_persists_log_file", false, String(err?.message || err));
|
|
19660
19911
|
}
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
|
|
19664
|
-
|
|
19912
|
+
|
|
19913
|
+
try {
|
|
19914
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
19915
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
19916
|
+
projectID: selftestProjectID,
|
|
19917
|
+
provider: "telegram",
|
|
19918
|
+
role: "monitor",
|
|
19919
|
+
botID: "bot-1",
|
|
19920
|
+
botName: "SelftestBot1",
|
|
19921
|
+
destinationID: "dest-1",
|
|
19922
|
+
destinationLabel: "Selftest Room",
|
|
19923
|
+
});
|
|
19924
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
19925
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
19926
|
+
projectID: selftestProjectID,
|
|
19927
|
+
provider: "telegram",
|
|
19928
|
+
role: "monitor",
|
|
19929
|
+
botID: "bot-2",
|
|
19930
|
+
botName: "SelftestBot2",
|
|
19931
|
+
destinationID: "dest-1",
|
|
19932
|
+
destinationLabel: "Selftest Room",
|
|
19933
|
+
});
|
|
19934
|
+
const supersetLaunch = buildRunnerDetachedLaunchRecord(
|
|
19935
|
+
process.pid,
|
|
19936
|
+
[routeBot1, routeBot2],
|
|
19937
|
+
{ tui: false },
|
|
19938
|
+
"runner start-detached",
|
|
19939
|
+
);
|
|
19940
|
+
const decision = classifyDetachedRunnerLaunchReuse(
|
|
19941
|
+
{ launches: { [supersetLaunch.launch_id]: supersetLaunch } },
|
|
19942
|
+
[routeBot2],
|
|
19943
|
+
);
|
|
19944
|
+
push(
|
|
19945
|
+
"detached_runner_reuses_existing_superset_launch_for_same_target",
|
|
19946
|
+
decision.kind === "reuse"
|
|
19947
|
+
&& decision.reason === "superset"
|
|
19948
|
+
&& String(safeObject(decision.launch).launch_id || "").trim() === supersetLaunch.launch_id,
|
|
19949
|
+
`kind=${String(decision.kind || "")} reason=${String(decision.reason || "")} launch=${String(safeObject(decision.launch).launch_id || "")}`,
|
|
19950
|
+
);
|
|
19951
|
+
} catch (err) {
|
|
19952
|
+
push("detached_runner_reuses_existing_superset_launch_for_same_target", false, String(err?.message || err));
|
|
19953
|
+
}
|
|
19954
|
+
|
|
19955
|
+
try {
|
|
19956
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
19957
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
19958
|
+
projectID: selftestProjectID,
|
|
19959
|
+
provider: "telegram",
|
|
19960
|
+
role: "monitor",
|
|
19961
|
+
botID: "bot-1",
|
|
19962
|
+
botName: "SelftestBot1",
|
|
19963
|
+
destinationID: "dest-1",
|
|
19964
|
+
destinationLabel: "Selftest Room",
|
|
19965
|
+
});
|
|
19966
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
19967
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
19968
|
+
projectID: selftestProjectID,
|
|
19969
|
+
provider: "telegram",
|
|
19970
|
+
role: "monitor",
|
|
19971
|
+
botID: "bot-2",
|
|
19972
|
+
botName: "SelftestBot2",
|
|
19973
|
+
destinationID: "dest-1",
|
|
19974
|
+
destinationLabel: "Selftest Room",
|
|
19975
|
+
});
|
|
19976
|
+
const supersetLaunch = buildRunnerDetachedLaunchRecord(
|
|
19977
|
+
process.pid,
|
|
19978
|
+
[routeBot1, routeBot2],
|
|
19979
|
+
{ tui: false },
|
|
19980
|
+
"runner project tui",
|
|
19981
|
+
);
|
|
19982
|
+
const existing = findExistingDetachedRunnerLaunch(
|
|
19983
|
+
{ launches: { [supersetLaunch.launch_id]: supersetLaunch } },
|
|
19984
|
+
[routeBot2],
|
|
19985
|
+
);
|
|
19986
|
+
push(
|
|
19987
|
+
"detached_runner_existing_launch_lookup_reuses_superset_for_tui_snapshot",
|
|
19988
|
+
String(safeObject(existing).launch_id || "").trim() === supersetLaunch.launch_id,
|
|
19989
|
+
`launch=${String(safeObject(existing).launch_id || "").trim() || "(none)"}`,
|
|
19990
|
+
);
|
|
19991
|
+
} catch (err) {
|
|
19992
|
+
push("detached_runner_existing_launch_lookup_reuses_superset_for_tui_snapshot", false, String(err?.message || err));
|
|
19993
|
+
}
|
|
19994
|
+
|
|
19995
|
+
try {
|
|
19996
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
19997
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
19998
|
+
projectID: selftestProjectID,
|
|
19999
|
+
provider: "telegram",
|
|
20000
|
+
role: "monitor",
|
|
20001
|
+
botID: "bot-1",
|
|
20002
|
+
botName: "SelftestBot1",
|
|
20003
|
+
destinationID: "dest-1",
|
|
20004
|
+
destinationLabel: "Selftest Room",
|
|
20005
|
+
});
|
|
20006
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
20007
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
20008
|
+
projectID: selftestProjectID,
|
|
20009
|
+
provider: "telegram",
|
|
20010
|
+
role: "monitor",
|
|
20011
|
+
botID: "bot-2",
|
|
20012
|
+
botName: "SelftestBot2",
|
|
20013
|
+
destinationID: "dest-1",
|
|
20014
|
+
destinationLabel: "Selftest Room",
|
|
20015
|
+
});
|
|
20016
|
+
const subsetLaunch = buildRunnerDetachedLaunchRecord(
|
|
20017
|
+
process.pid,
|
|
20018
|
+
[routeBot2],
|
|
20019
|
+
{ tui: false },
|
|
20020
|
+
"runner start-detached",
|
|
20021
|
+
);
|
|
20022
|
+
const decision = classifyDetachedRunnerLaunchReuse(
|
|
20023
|
+
{ launches: { [subsetLaunch.launch_id]: subsetLaunch } },
|
|
20024
|
+
[routeBot1, routeBot2],
|
|
20025
|
+
);
|
|
20026
|
+
push(
|
|
20027
|
+
"detached_runner_blocks_partial_overlap_for_same_target",
|
|
20028
|
+
decision.kind === "conflict"
|
|
20029
|
+
&& ensureArray(decision.overlapping_route_names).includes("telegram-monitor-selftest-bot-2")
|
|
20030
|
+
&& String(decision.detail || "").includes("runner project tui"),
|
|
20031
|
+
`kind=${String(decision.kind || "")} overlap=${ensureArray(decision.overlapping_route_names).join(", ")} detail=${String(decision.detail || "")}`,
|
|
20032
|
+
);
|
|
20033
|
+
} catch (err) {
|
|
20034
|
+
push("detached_runner_blocks_partial_overlap_for_same_target", false, String(err?.message || err));
|
|
20035
|
+
}
|
|
20036
|
+
|
|
20037
|
+
try {
|
|
20038
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
20039
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
20040
|
+
projectID: selftestProjectID,
|
|
20041
|
+
provider: "telegram",
|
|
20042
|
+
role: "monitor",
|
|
20043
|
+
botID: "bot-1",
|
|
20044
|
+
botName: "SelftestBot1",
|
|
20045
|
+
destinationID: "dest-1",
|
|
20046
|
+
destinationLabel: "Selftest Room",
|
|
20047
|
+
});
|
|
20048
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
20049
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
20050
|
+
projectID: selftestProjectID,
|
|
20051
|
+
provider: "telegram",
|
|
20052
|
+
role: "monitor",
|
|
20053
|
+
botID: "bot-2",
|
|
20054
|
+
botName: "SelftestBot2",
|
|
20055
|
+
destinationID: "dest-1",
|
|
20056
|
+
destinationLabel: "Selftest Room",
|
|
20057
|
+
});
|
|
20058
|
+
const singleLaunch = buildRunnerDetachedLaunchRecord(
|
|
20059
|
+
process.pid,
|
|
20060
|
+
[routeBot1],
|
|
20061
|
+
{ tui: false },
|
|
20062
|
+
"runner start-detached",
|
|
20063
|
+
);
|
|
20064
|
+
const decision = classifyDetachedRunnerLaunchReuse(
|
|
20065
|
+
{ launches: { [singleLaunch.launch_id]: singleLaunch } },
|
|
20066
|
+
[routeBot2],
|
|
20067
|
+
);
|
|
20068
|
+
push(
|
|
20069
|
+
"detached_runner_blocks_disjoint_same_target_parallel_launches",
|
|
20070
|
+
decision.kind === "conflict"
|
|
20071
|
+
&& ensureArray(decision.overlapping_route_names).length === 0
|
|
20072
|
+
&& String(decision.detail || "").includes("same project/provider/destination target"),
|
|
20073
|
+
`kind=${String(decision.kind || "")} overlap=${ensureArray(decision.overlapping_route_names).join(", ")} detail=${String(decision.detail || "")}`,
|
|
20074
|
+
);
|
|
20075
|
+
} catch (err) {
|
|
20076
|
+
push("detached_runner_blocks_disjoint_same_target_parallel_launches", false, String(err?.message || err));
|
|
20077
|
+
}
|
|
20078
|
+
|
|
20079
|
+
let detachedRunnerPosixTempDir = "";
|
|
20080
|
+
try {
|
|
20081
|
+
detachedRunnerPosixTempDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-detached-script-selftest-"));
|
|
19665
20082
|
const scriptPath = path.join(detachedRunnerPosixTempDir, "start-runner.sh");
|
|
19666
20083
|
const pidFilePath = path.join(detachedRunnerPosixTempDir, "runner.pid");
|
|
19667
20084
|
buildRunnerDetachedPosixLaunchScript({
|