@indigoai-us/hq-cli 5.345.1 → 5.345.2
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/CHANGELOG.md +5 -0
- package/dist/commands/daemon.js +6 -2
- package/dist/commands/mcp-serve.js +15 -5
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +135416 -0
- package/dist/hq-cloud-sync-runner/package.json +1 -0
- package/dist/index.js +2 -1
- package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +22 -0
- package/dist/lib/daemon/hq-child-bootstrap-stage.js +127 -0
- package/dist/lib/daemon/hq-child-bootstrap.d.mts +3 -0
- package/dist/lib/daemon/hq-child-bootstrap.mjs +208 -0
- package/dist/lib/daemon/hq-cloud.d.ts +5 -0
- package/dist/lib/daemon/hq-cloud.js +14 -0
- package/dist/lib/daemon/run.d.ts +28 -2
- package/dist/lib/daemon/run.js +139 -10
- package/dist/lib/daemon/sync-runner-bundle.d.ts +40 -0
- package/dist/lib/daemon/sync-runner-bundle.js +199 -0
- package/dist/utils/package-use-lease.d.ts +6 -0
- package/dist/utils/package-use-lease.js +25 -2
- package/package.json +2 -2
package/dist/lib/daemon/run.js
CHANGED
|
@@ -10,10 +10,11 @@ import * as fs from "node:fs";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
13
14
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
14
15
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
15
16
|
import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
|
|
16
|
-
import { currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
|
|
17
|
+
import { currentPackageUseLeaseInstall, currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
|
|
17
18
|
import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
|
|
18
19
|
import { agentKitPaths } from "../agent-kit/paths.js";
|
|
19
20
|
import { readBotConfig } from "../bot/config.js";
|
|
@@ -36,7 +37,9 @@ import { startHqdServer } from "./server.js";
|
|
|
36
37
|
import { heartbeatFile, startHeartbeat } from "./heartbeat.js";
|
|
37
38
|
import { createProtocolTail, ensureProtocolFifo, openProtocolWriter } from "./protocol-tail.js";
|
|
38
39
|
import { takeControlRequests } from "./requests.js";
|
|
39
|
-
import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
|
|
40
|
+
import { currentHqCloud, currentHqCloudAfterLeaseRelease, hqCloudRunners } from "./hq-cloud.js";
|
|
41
|
+
import { stageSyncRunnerBundle } from "./sync-runner-bundle.js";
|
|
42
|
+
import { stageHqChildBootstrap } from "./hq-child-bootstrap-stage.js";
|
|
40
43
|
import { runnerSessionName } from "./remote-control.js";
|
|
41
44
|
import { adoptChild, createChildTracker, finishSuccession, handoffEligible, readProcStart, stopOrphans, successionPaths, systemdKillMode, takeSuccession, writeHandoff, } from "./succession.js";
|
|
42
45
|
import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
|
|
@@ -73,6 +76,48 @@ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
|
|
|
73
76
|
const LEGACY_PROBE_TTL_MS = 60_000;
|
|
74
77
|
/** A flag read that has not answered by then counts as off. */
|
|
75
78
|
const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
|
|
79
|
+
export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null) {
|
|
80
|
+
if (stagedRunnerPath)
|
|
81
|
+
return { kind: "staged-sync", runnerPath: stagedRunnerPath };
|
|
82
|
+
if (!hqCloud) {
|
|
83
|
+
return legacyBundledRunnerPath
|
|
84
|
+
? { kind: "bundled-full-package", runnerPath: legacyBundledRunnerPath }
|
|
85
|
+
: { kind: "unavailable", runnerPath: null };
|
|
86
|
+
}
|
|
87
|
+
// The caller has already selected a usable full package; preserve the
|
|
88
|
+
// conventional path here, including in tests with virtual package roots.
|
|
89
|
+
const runnerPath = path.join(hqCloud.root, "dist", "bin", "sync-runner.js");
|
|
90
|
+
return hqCloud.source === "managed"
|
|
91
|
+
? { kind: "managed-full-package", runnerPath }
|
|
92
|
+
: { kind: "bundled-full-package", runnerPath };
|
|
93
|
+
}
|
|
94
|
+
function isPathWithin(root, candidate) {
|
|
95
|
+
if (!root || !path.isAbsolute(candidate))
|
|
96
|
+
return false;
|
|
97
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
98
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
99
|
+
}
|
|
100
|
+
/** Whether every executable in the first service plan remains usable after the CLI package is replaced. */
|
|
101
|
+
export function daemonPlanCanReleasePackageUseLease(plans, replaceablePackageRoots, stagingFailed, hqBootstrapReady = false) {
|
|
102
|
+
if (stagingFailed)
|
|
103
|
+
return false;
|
|
104
|
+
return plans.every((unit) => {
|
|
105
|
+
const command = unit.command;
|
|
106
|
+
if (!command)
|
|
107
|
+
return true;
|
|
108
|
+
if (command.type === "hq")
|
|
109
|
+
return hqBootstrapReady;
|
|
110
|
+
if (command.type === "internal")
|
|
111
|
+
return true;
|
|
112
|
+
const executablePaths = command.type === "node" ? [command.script, ...command.args] : [command.file, ...command.args];
|
|
113
|
+
return executablePaths.every((candidate) => replaceablePackageRoots.every((root) => !isPathWithin(root, candidate)));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function daemonHqChildCommand(nodeBinary, hqBinary, args, bootstrapPath, packagePrefix) {
|
|
117
|
+
return bootstrapPath && packagePrefix
|
|
118
|
+
? [nodeBinary, [bootstrapPath, packagePrefix, CLI_VERSION, ...args]]
|
|
119
|
+
: [nodeBinary, [hqBinary, ...args]];
|
|
120
|
+
}
|
|
76
121
|
export function resolveSyncRunnerPath() {
|
|
77
122
|
try {
|
|
78
123
|
const require = createRequire(import.meta.url);
|
|
@@ -304,6 +349,60 @@ export async function runDaemon(opts) {
|
|
|
304
349
|
const platform = process.platform;
|
|
305
350
|
const paths = daemonPaths(home, env);
|
|
306
351
|
const log = makeLogger(paths, opts.foreground === true);
|
|
352
|
+
const syncRunnerBundleDirectory = opts.syncRunnerBundleDirectory ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../hq-cloud-sync-runner");
|
|
353
|
+
const cliPackageRoot = opts.cliPackageRoot === undefined ? resolveRunningInstall().packageRoot : opts.cliPackageRoot;
|
|
354
|
+
const cliPackagePrefix = opts.cliPackagePrefix === undefined
|
|
355
|
+
? currentPackageUseLeaseInstall()?.prefix ?? null
|
|
356
|
+
: opts.cliPackagePrefix;
|
|
357
|
+
let stagedSyncBundle = null;
|
|
358
|
+
let bundleStageChecked = false;
|
|
359
|
+
let bundleStageFailed = false;
|
|
360
|
+
let stagedHqChildBootstrap = null;
|
|
361
|
+
let hqChildBootstrapStageChecked = false;
|
|
362
|
+
let hqChildBootstrapStageFailed = false;
|
|
363
|
+
let legacySyncRunnerPathChecked = false;
|
|
364
|
+
let legacySyncRunnerPath = null;
|
|
365
|
+
let packageUseLeaseReleased = false;
|
|
366
|
+
let hqCloudVersionBeforeRelease = null;
|
|
367
|
+
let startupPlanOpen = true;
|
|
368
|
+
const startupPlans = [];
|
|
369
|
+
const getStagedSyncBundle = () => {
|
|
370
|
+
if (bundleStageChecked)
|
|
371
|
+
return stagedSyncBundle;
|
|
372
|
+
bundleStageChecked = true;
|
|
373
|
+
// Older and development installs have no publish-time payload; keep their
|
|
374
|
+
// existing hq-cloud selection path without treating absence as an error.
|
|
375
|
+
if (!fs.existsSync(syncRunnerBundleDirectory))
|
|
376
|
+
return null;
|
|
377
|
+
try {
|
|
378
|
+
stagedSyncBundle = stageSyncRunnerBundle({ payloadDirectory: syncRunnerBundleDirectory, daemonRoot: paths.root });
|
|
379
|
+
return stagedSyncBundle;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
bundleStageFailed = true;
|
|
383
|
+
log("warn", "could not stage the packaged sync runner; keeping the package-use lease");
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const getStagedHqChildBootstrap = () => {
|
|
388
|
+
if (hqChildBootstrapStageChecked)
|
|
389
|
+
return stagedHqChildBootstrap;
|
|
390
|
+
hqChildBootstrapStageChecked = true;
|
|
391
|
+
if (!cliPackagePrefix)
|
|
392
|
+
return null;
|
|
393
|
+
try {
|
|
394
|
+
stagedHqChildBootstrap = stageHqChildBootstrap({
|
|
395
|
+
sourcePath: opts.hqChildBootstrapSourcePath,
|
|
396
|
+
daemonRoot: paths.root,
|
|
397
|
+
});
|
|
398
|
+
return stagedHqChildBootstrap;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
hqChildBootstrapStageFailed = true;
|
|
402
|
+
log("warn", "could not stage the daemon hq bootstrap; keeping the package-use lease");
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
307
406
|
const lock = acquireDaemonLock(paths.lockPath);
|
|
308
407
|
const lockConflict = daemonLockConflictOutcome(lock, !lock.ok && lock.owner ? anywhereRuntimeEnabled(env) : false);
|
|
309
408
|
if (lockConflict) {
|
|
@@ -374,8 +473,6 @@ export async function runDaemon(opts) {
|
|
|
374
473
|
finishSuccession(succession, tracker.records(), log);
|
|
375
474
|
/** Service processes this daemon spawned, so a handoff can let go of them. */
|
|
376
475
|
const spawned = new Set();
|
|
377
|
-
if (!resolveSyncRunnerPath())
|
|
378
|
-
log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
|
|
379
476
|
const updatable = cliSelfUpdatable();
|
|
380
477
|
const hostName = os.hostname().split(".")[0] || "hq-host";
|
|
381
478
|
let legacyCache = null;
|
|
@@ -435,14 +532,22 @@ export async function runDaemon(opts) {
|
|
|
435
532
|
return `mesh daemon already running (pid ${owner.pid}); waiting for it to exit`;
|
|
436
533
|
};
|
|
437
534
|
let warnedMissingPin = null;
|
|
535
|
+
let warnedMissingSyncRunner = false;
|
|
438
536
|
const daemonSyncRunnerPids = new Set();
|
|
439
537
|
const plan = () => {
|
|
440
538
|
const config = readDaemonConfig(paths.configPath);
|
|
441
539
|
const legacy = legacyUnits();
|
|
442
540
|
const configured = config.hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
|
|
443
541
|
const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
|
|
444
|
-
|
|
445
|
-
const
|
|
542
|
+
getStagedHqChildBootstrap();
|
|
543
|
+
const staged = getStagedSyncBundle();
|
|
544
|
+
// Before release, preserve today's selection. Later plans can only use the
|
|
545
|
+
// daemon-owned full package; the CLI-bundled tree may have been replaced.
|
|
546
|
+
const hqCloud = packageUseLeaseReleased
|
|
547
|
+
? currentHqCloudAfterLeaseRelease(paths.root, hqCloudVersionBeforeRelease)
|
|
548
|
+
: currentHqCloud(paths.root);
|
|
549
|
+
if (!packageUseLeaseReleased)
|
|
550
|
+
hqCloudVersionBeforeRelease = hqCloud?.version ?? staged?.metadata.version ?? null;
|
|
446
551
|
const missingPin = hqCloud?.pinnedVersionMissing ?? null;
|
|
447
552
|
if (missingPin !== warnedMissingPin) {
|
|
448
553
|
warnedMissingPin = missingPin;
|
|
@@ -450,7 +555,17 @@ export async function runDaemon(opts) {
|
|
|
450
555
|
log("warn", `hq-cloud is pinned to ${missingPin}, which is not installed; running ${hqCloud.version} (${hqCloud.source}) until the updater installs it`);
|
|
451
556
|
}
|
|
452
557
|
}
|
|
453
|
-
|
|
558
|
+
if (!packageUseLeaseReleased && !legacySyncRunnerPathChecked) {
|
|
559
|
+
legacySyncRunnerPath = resolveSyncRunnerPath();
|
|
560
|
+
legacySyncRunnerPathChecked = true;
|
|
561
|
+
}
|
|
562
|
+
const syncSource = syncRunnerSourceForPlan(staged?.runnerPath ?? null, hqCloud, packageUseLeaseReleased ? null : legacySyncRunnerPath);
|
|
563
|
+
if (!syncSource.runnerPath && !warnedMissingSyncRunner) {
|
|
564
|
+
warnedMissingSyncRunner = true;
|
|
565
|
+
log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
|
|
566
|
+
}
|
|
567
|
+
const packageRunners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
|
|
568
|
+
const runners = { ...packageRunners, syncRunner: syncSource.runnerPath };
|
|
454
569
|
const services = enabledServices(machineType, config);
|
|
455
570
|
const isSignedIn = signedIn(env, home, log);
|
|
456
571
|
const syncOwner = effectiveSyncOwner(config, machineType);
|
|
@@ -477,7 +592,7 @@ export async function runDaemon(opts) {
|
|
|
477
592
|
}
|
|
478
593
|
}
|
|
479
594
|
}
|
|
480
|
-
|
|
595
|
+
const plans = planUnits(services, {
|
|
481
596
|
machineType,
|
|
482
597
|
platform,
|
|
483
598
|
home,
|
|
@@ -512,13 +627,23 @@ export async function runDaemon(opts) {
|
|
|
512
627
|
tmuxPath: findOnPath("tmux", env),
|
|
513
628
|
hostName,
|
|
514
629
|
});
|
|
630
|
+
if (startupPlanOpen) {
|
|
631
|
+
const replaceablePackageRoots = cliPackageRoot ? [cliPackageRoot] : [];
|
|
632
|
+
if (hqCloud?.source === "bundled")
|
|
633
|
+
replaceablePackageRoots.push(hqCloud.root);
|
|
634
|
+
else if (!hqCloud && legacySyncRunnerPath) {
|
|
635
|
+
replaceablePackageRoots.push(path.dirname(path.dirname(path.dirname(legacySyncRunnerPath))));
|
|
636
|
+
}
|
|
637
|
+
startupPlans.push({ plans, replaceablePackageRoots });
|
|
638
|
+
}
|
|
639
|
+
return plans;
|
|
515
640
|
};
|
|
516
641
|
const spawnUnit = (unit) => {
|
|
517
642
|
const command = unit.command;
|
|
518
643
|
if (!command || command.type === "internal")
|
|
519
644
|
throw new Error(`${unit.id} has no process to start`);
|
|
520
645
|
const [file, args] = command.type === "hq"
|
|
521
|
-
?
|
|
646
|
+
? daemonHqChildCommand(opts.nodeBinary, opts.hqBinary, command.args, stagedHqChildBootstrap?.bootstrapPath ?? null, cliPackagePrefix)
|
|
522
647
|
: command.type === "node"
|
|
523
648
|
? [opts.nodeBinary, [command.script, ...command.args]]
|
|
524
649
|
: [command.file, command.args];
|
|
@@ -831,7 +956,11 @@ export async function runDaemon(opts) {
|
|
|
831
956
|
resolve(1);
|
|
832
957
|
}
|
|
833
958
|
finally {
|
|
834
|
-
|
|
959
|
+
startupPlanOpen = false;
|
|
960
|
+
const startupPlanIsStable = startupPlans.length > 0 && startupPlans.every(({ plans, replaceablePackageRoots }) => daemonPlanCanReleasePackageUseLease(plans, replaceablePackageRoots, bundleStageFailed || hqChildBootstrapStageFailed, Boolean(stagedHqChildBootstrap && cliPackagePrefix)));
|
|
961
|
+
if (startupPlanIsStable) {
|
|
962
|
+
packageUseLeaseReleased = opts.onStartupReadsComplete?.() === true;
|
|
963
|
+
}
|
|
835
964
|
}
|
|
836
965
|
});
|
|
837
966
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Atomic staging for the sync-runner bundle shipped with hq-cli. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
export declare const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
|
|
4
|
+
export declare const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
|
|
5
|
+
export declare const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
|
|
6
|
+
export declare const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
|
|
7
|
+
/** package.json contract that the hq-cli build step emits beside the bundled bin directory. */
|
|
8
|
+
export interface SyncRunnerBundleMetadata {
|
|
9
|
+
name: "hq-cli-daemon-sync-runner";
|
|
10
|
+
type: "module";
|
|
11
|
+
version: string;
|
|
12
|
+
bundleSha256: string;
|
|
13
|
+
formatVersion: typeof SYNC_RUNNER_BUNDLE_FORMAT_VERSION;
|
|
14
|
+
}
|
|
15
|
+
export type SyncRunnerBundleStageErrorCode = "PAYLOAD_MISSING" | "MISSING_SIDECAR" | "INVALID_METADATA" | "VERSION_OUT_OF_RANGE" | "HASH_MISMATCH" | "STAGE_EXISTS_INVALID" | "COPY_FAILED" | "FSYNC_FAILED" | "RENAME_FAILED";
|
|
16
|
+
export declare class SyncRunnerBundleStageError extends Error {
|
|
17
|
+
readonly code: SyncRunnerBundleStageErrorCode;
|
|
18
|
+
constructor(code: SyncRunnerBundleStageErrorCode, message: string, options?: ErrorOptions);
|
|
19
|
+
}
|
|
20
|
+
export interface StagedSyncRunnerBundle {
|
|
21
|
+
directory: string;
|
|
22
|
+
runnerPath: string;
|
|
23
|
+
metadataPath: string;
|
|
24
|
+
metadata: SyncRunnerBundleMetadata;
|
|
25
|
+
}
|
|
26
|
+
export interface StageSyncRunnerBundleOptions {
|
|
27
|
+
/** Directory in the installed hq-cli dist containing both contract files. */
|
|
28
|
+
payloadDirectory: string;
|
|
29
|
+
/** Daemon-owned state root, normally daemonPaths().root. */
|
|
30
|
+
daemonRoot: string;
|
|
31
|
+
/** Test seams for the file copy and atomic directory rename. */
|
|
32
|
+
copyFile?: typeof fs.copyFileSync;
|
|
33
|
+
renameDirectory?: (from: string, to: string) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Stage one validated runner bundle into an immutable daemon-owned directory.
|
|
37
|
+
* Only a completed, fsynced directory is returned; temp siblings are ignored.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stageSyncRunnerBundle(options: StageSyncRunnerBundleOptions): StagedSyncRunnerBundle;
|
|
40
|
+
//# sourceMappingURL=sync-runner-bundle.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/** Atomic staging for the sync-runner bundle shipped with hq-cli. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import semver from "semver";
|
|
7
|
+
import { ensurePrivateDir } from "./paths.js";
|
|
8
|
+
export const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
|
|
9
|
+
export const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
|
|
10
|
+
export const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
|
|
11
|
+
export const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
|
|
12
|
+
export class SyncRunnerBundleStageError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message, options) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "SyncRunnerBundleStageError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fail(code, message, cause) {
|
|
21
|
+
if (cause instanceof SyncRunnerBundleStageError)
|
|
22
|
+
throw cause;
|
|
23
|
+
throw new SyncRunnerBundleStageError(code, message, cause === undefined ? undefined : { cause });
|
|
24
|
+
}
|
|
25
|
+
function readMetadata(file) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = fs.readFileSync(file, "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === "ENOENT") {
|
|
32
|
+
fail("MISSING_SIDECAR", "The sync-runner bundle metadata sidecar is missing", error);
|
|
33
|
+
}
|
|
34
|
+
fail("PAYLOAD_MISSING", "Unable to read the sync-runner bundle metadata", error);
|
|
35
|
+
}
|
|
36
|
+
let value;
|
|
37
|
+
try {
|
|
38
|
+
value = JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
fail("INVALID_METADATA", "The sync-runner bundle metadata is invalid JSON", error);
|
|
42
|
+
}
|
|
43
|
+
if (!value || typeof value !== "object") {
|
|
44
|
+
fail("INVALID_METADATA", "The sync-runner bundle metadata has an invalid shape");
|
|
45
|
+
}
|
|
46
|
+
const metadata = value;
|
|
47
|
+
if (metadata.name !== "hq-cli-daemon-sync-runner"
|
|
48
|
+
|| metadata.type !== "module"
|
|
49
|
+
|| metadata.formatVersion !== SYNC_RUNNER_BUNDLE_FORMAT_VERSION
|
|
50
|
+
|| typeof metadata.version !== "string"
|
|
51
|
+
|| !semver.valid(metadata.version)
|
|
52
|
+
|| typeof metadata.bundleSha256 !== "string"
|
|
53
|
+
|| !/^[a-f0-9]{64}$/.test(metadata.bundleSha256)) {
|
|
54
|
+
fail("INVALID_METADATA", "The sync-runner bundle metadata has an invalid shape");
|
|
55
|
+
}
|
|
56
|
+
return metadata;
|
|
57
|
+
}
|
|
58
|
+
function hqCloudRange() {
|
|
59
|
+
const cliPackagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../package.json");
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(fs.readFileSync(cliPackagePath, "utf8"));
|
|
62
|
+
const range = pkg.dependencies?.["@indigoai-us/hq-cloud"];
|
|
63
|
+
if (typeof range !== "string" || !semver.validRange(range))
|
|
64
|
+
throw new Error("invalid declared range");
|
|
65
|
+
return range;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
fail("INVALID_METADATA", "Unable to read the declared hq-cloud version range", error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function sha256(file) {
|
|
72
|
+
return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
73
|
+
}
|
|
74
|
+
function syncFile(file) {
|
|
75
|
+
const fd = fs.openSync(file, "r");
|
|
76
|
+
try {
|
|
77
|
+
fs.fsyncSync(fd);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
fs.closeSync(fd);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function syncDirectory(directory) {
|
|
84
|
+
const fd = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
85
|
+
try {
|
|
86
|
+
fs.fsyncSync(fd);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function readStaged(directory, expected, range) {
|
|
93
|
+
if (!fs.existsSync(directory))
|
|
94
|
+
return null;
|
|
95
|
+
const runnerPath = path.join(directory, SYNC_RUNNER_BUNDLE_RUNNER_FILE);
|
|
96
|
+
const metadataPath = path.join(directory, SYNC_RUNNER_BUNDLE_METADATA_FILE);
|
|
97
|
+
let metadata;
|
|
98
|
+
try {
|
|
99
|
+
metadata = readMetadata(metadataPath);
|
|
100
|
+
if (metadata.formatVersion !== expected.formatVersion
|
|
101
|
+
|| metadata.version !== expected.version
|
|
102
|
+
|| metadata.bundleSha256 !== expected.bundleSha256
|
|
103
|
+
|| metadata.type !== "module"
|
|
104
|
+
|| !semver.satisfies(metadata.version, range)
|
|
105
|
+
|| sha256(runnerPath) !== metadata.bundleSha256)
|
|
106
|
+
fail("STAGE_EXISTS_INVALID", "An existing sync-runner stage is invalid; preserving it");
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof SyncRunnerBundleStageError && error.code === "STAGE_EXISTS_INVALID")
|
|
110
|
+
throw error;
|
|
111
|
+
fail("STAGE_EXISTS_INVALID", "An existing sync-runner stage is invalid; preserving it", error);
|
|
112
|
+
}
|
|
113
|
+
return { directory, runnerPath, metadataPath, metadata };
|
|
114
|
+
}
|
|
115
|
+
function stagedLeaf(metadata) {
|
|
116
|
+
const version = metadata.version.replace(/[^A-Za-z0-9.-]/g, "_");
|
|
117
|
+
return `${version}-${metadata.bundleSha256}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Stage one validated runner bundle into an immutable daemon-owned directory.
|
|
121
|
+
* Only a completed, fsynced directory is returned; temp siblings are ignored.
|
|
122
|
+
*/
|
|
123
|
+
export function stageSyncRunnerBundle(options) {
|
|
124
|
+
const sourceRunner = path.join(options.payloadDirectory, SYNC_RUNNER_BUNDLE_RUNNER_FILE);
|
|
125
|
+
const sourceMetadata = path.join(options.payloadDirectory, SYNC_RUNNER_BUNDLE_METADATA_FILE);
|
|
126
|
+
const metadata = readMetadata(sourceMetadata);
|
|
127
|
+
const range = hqCloudRange();
|
|
128
|
+
if (!semver.satisfies(metadata.version, range)) {
|
|
129
|
+
fail("VERSION_OUT_OF_RANGE", "The sync-runner bundle version is outside the declared hq-cloud range");
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
if (!fs.statSync(sourceRunner).isFile())
|
|
133
|
+
fail("PAYLOAD_MISSING", "The sync-runner bundle file is missing");
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
fail("PAYLOAD_MISSING", "Unable to access the sync-runner bundle file", error);
|
|
137
|
+
}
|
|
138
|
+
if (sha256(sourceRunner) !== metadata.bundleSha256) {
|
|
139
|
+
fail("HASH_MISMATCH", "The sync-runner bundle sha256 does not match its sidecar");
|
|
140
|
+
}
|
|
141
|
+
const stageRoot = path.join(options.daemonRoot, "bundles", "sync-runner");
|
|
142
|
+
const selectedDirectory = path.join(stageRoot, stagedLeaf(metadata));
|
|
143
|
+
const existing = readStaged(selectedDirectory, metadata, range);
|
|
144
|
+
if (existing)
|
|
145
|
+
return existing;
|
|
146
|
+
let tempDirectory;
|
|
147
|
+
let failureCode = "COPY_FAILED";
|
|
148
|
+
try {
|
|
149
|
+
ensurePrivateDir(stageRoot);
|
|
150
|
+
tempDirectory = path.join(stageRoot, `.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`);
|
|
151
|
+
fs.mkdirSync(tempDirectory, { mode: 0o700 });
|
|
152
|
+
fs.mkdirSync(path.join(tempDirectory, "bin"), { mode: 0o700 });
|
|
153
|
+
(options.copyFile ?? fs.copyFileSync)(sourceRunner, path.join(tempDirectory, SYNC_RUNNER_BUNDLE_RUNNER_FILE));
|
|
154
|
+
(options.copyFile ?? fs.copyFileSync)(sourceMetadata, path.join(tempDirectory, SYNC_RUNNER_BUNDLE_METADATA_FILE));
|
|
155
|
+
if (sha256(path.join(tempDirectory, SYNC_RUNNER_BUNDLE_RUNNER_FILE)) !== metadata.bundleSha256) {
|
|
156
|
+
fail("HASH_MISMATCH", "The staged sync-runner bundle sha256 does not match its sidecar");
|
|
157
|
+
}
|
|
158
|
+
failureCode = "FSYNC_FAILED";
|
|
159
|
+
syncFile(path.join(tempDirectory, SYNC_RUNNER_BUNDLE_RUNNER_FILE));
|
|
160
|
+
syncFile(path.join(tempDirectory, SYNC_RUNNER_BUNDLE_METADATA_FILE));
|
|
161
|
+
syncDirectory(path.join(tempDirectory, "bin"));
|
|
162
|
+
syncDirectory(tempDirectory);
|
|
163
|
+
failureCode = "RENAME_FAILED";
|
|
164
|
+
try {
|
|
165
|
+
(options.renameDirectory ?? fs.renameSync)(tempDirectory, selectedDirectory);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.code === "EEXIST" || error.code === "ENOTEMPTY") {
|
|
169
|
+
const raced = readStaged(selectedDirectory, metadata, range);
|
|
170
|
+
if (raced) {
|
|
171
|
+
fs.rmSync(tempDirectory, { recursive: true, force: true });
|
|
172
|
+
return raced;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
fail("RENAME_FAILED", "Unable to atomically select the staged sync-runner bundle", error);
|
|
176
|
+
}
|
|
177
|
+
tempDirectory = undefined;
|
|
178
|
+
failureCode = "FSYNC_FAILED";
|
|
179
|
+
syncDirectory(stageRoot);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (tempDirectory) {
|
|
183
|
+
try {
|
|
184
|
+
fs.rmSync(tempDirectory, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
catch (cleanupError) {
|
|
187
|
+
fail("COPY_FAILED", "Unable to remove the temporary sync-runner stage", new AggregateError([error, cleanupError]));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (error instanceof SyncRunnerBundleStageError)
|
|
191
|
+
throw error;
|
|
192
|
+
fail(failureCode, "Unable to stage the sync-runner bundle", error);
|
|
193
|
+
}
|
|
194
|
+
const staged = readStaged(selectedDirectory, metadata, range);
|
|
195
|
+
if (!staged)
|
|
196
|
+
fail("FSYNC_FAILED", "The completed sync-runner stage failed validation");
|
|
197
|
+
return staged;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=sync-runner-bundle.js.map
|
|
@@ -34,7 +34,13 @@ export declare function createPackageUseLeaseHandoff(lease: PackageUseLease, tim
|
|
|
34
34
|
export declare function createCurrentPackageUseLeaseHandoff(timeoutMs?: number): PackageUseLeaseHandoff | null;
|
|
35
35
|
/** Publish this process's lease identity to a parent waiting in the same prefix. */
|
|
36
36
|
export declare function publishPackageUseLeaseHandoff(lease: PackageUseLease, env?: NodeJS.ProcessEnv): boolean;
|
|
37
|
+
export declare function currentPackageUseLeaseInstall(): {
|
|
38
|
+
prefix: string;
|
|
39
|
+
version: string;
|
|
40
|
+
} | null;
|
|
37
41
|
export declare function acquireCurrentPackageUseLease(argv?: readonly string[]): PackageUseLease | null;
|
|
42
|
+
/** Let a daemon bootstrap drop its temporary lease after this process published its own. */
|
|
43
|
+
export declare function signalPackageUseLeaseBootstrapHandoff(lease?: PackageUseLease | null): void;
|
|
38
44
|
/** Release this process's package-use lease after a long-running command has loaded its package modules. */
|
|
39
45
|
export declare function releaseCurrentPackageUseLease(): void;
|
|
40
46
|
/** The lease remains held through command execution and is released on process exit. */
|
|
@@ -50,6 +50,7 @@ export function packageUseLeasePurposeForArgv(argv) {
|
|
|
50
50
|
return "command";
|
|
51
51
|
}
|
|
52
52
|
let currentPackageUseLease = null;
|
|
53
|
+
const BOOTSTRAP_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_HANDOFF_TOKEN";
|
|
53
54
|
function errorLabel(error) {
|
|
54
55
|
if (error && typeof error === "object") {
|
|
55
56
|
const value = error;
|
|
@@ -308,7 +309,7 @@ function exists(file) {
|
|
|
308
309
|
throw error;
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
|
-
export function
|
|
312
|
+
export function currentPackageUseLeaseInstall() {
|
|
312
313
|
const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
|
|
313
314
|
if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
|
|
314
315
|
return null;
|
|
@@ -321,11 +322,33 @@ export function acquireCurrentPackageUseLease(argv = process.argv) {
|
|
|
321
322
|
if (nodeModules > 0 && parts[nodeModules - 1] !== "lib")
|
|
322
323
|
return null;
|
|
323
324
|
}
|
|
324
|
-
|
|
325
|
+
return { prefix, version: pkg.version };
|
|
326
|
+
}
|
|
327
|
+
export function acquireCurrentPackageUseLease(argv = process.argv) {
|
|
328
|
+
const install = currentPackageUseLeaseInstall();
|
|
329
|
+
if (!install)
|
|
330
|
+
return null;
|
|
331
|
+
const lease = acquirePackageUseLease(install.prefix, install.version, undefined, packageUseLeasePurposeForArgv(argv));
|
|
325
332
|
currentPackageUseLease = lease;
|
|
326
333
|
publishPackageUseLeaseHandoff(lease);
|
|
327
334
|
return lease;
|
|
328
335
|
}
|
|
336
|
+
/** Let a daemon bootstrap drop its temporary lease after this process published its own. */
|
|
337
|
+
export function signalPackageUseLeaseBootstrapHandoff(lease = currentPackageUseLease) {
|
|
338
|
+
const token = process.env[BOOTSTRAP_HANDOFF_ENV];
|
|
339
|
+
if (token === undefined)
|
|
340
|
+
return;
|
|
341
|
+
delete process.env[BOOTSTRAP_HANDOFF_ENV];
|
|
342
|
+
if (!lease || !/^[a-f0-9]{32}$/.test(token))
|
|
343
|
+
return;
|
|
344
|
+
const markerPath = path.join(path.dirname(lease.leasePath), `handoff-${process.pid}-${token}.handoff`);
|
|
345
|
+
try {
|
|
346
|
+
writeFileSync(markerPath, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
process.stderr.write(`hq-cli package-use lease handoff signal failed (${errorLabel(error)})\n`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
329
352
|
/** Release this process's package-use lease after a long-running command has loaded its package modules. */
|
|
330
353
|
export function releaseCurrentPackageUseLease() {
|
|
331
354
|
currentPackageUseLease?.release();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.2",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"scripts/mesh-daemon-if-stale.mjs"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs && tsc && node scripts/generate-command-catalog.mjs --check && node scripts/copy-schemas.mjs && node scripts/chmod-bins.mjs",
|
|
27
|
+
"build": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs && tsc && node scripts/generate-command-catalog.mjs --check && node scripts/copy-schemas.mjs && node scripts/chmod-bins.mjs && node scripts/bundle-sync-runner.mjs",
|
|
28
28
|
"prepublishOnly": "npm run build",
|
|
29
29
|
"pretypecheck": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs",
|
|
30
30
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.typetest.json",
|