@indigoai-us/hq-cli 5.345.3 → 5.345.5
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 +7 -0
- package/dist/commands/daemon.js +91 -7
- package/dist/lib/daemon/hq-cloud.d.ts +8 -0
- package/dist/lib/daemon/hq-cloud.js +17 -3
- package/dist/lib/daemon/run.d.ts +4 -1
- package/dist/lib/daemon/run.js +18 -7
- package/dist/lib/daemon/sync-runner-bundle.d.ts +4 -0
- package/dist/lib/daemon/sync-runner-bundle.js +10 -1
- package/dist/lib/daemon/sync-runner.d.ts +2 -0
- package/dist/lib/daemon/sync-runner.js +8 -0
- package/dist/lib/install/claude-targets.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.5] — 2026-10-05
|
|
6
|
+
- `hq daemon sync status` now identifies the executing runner and derives its hq-cloud version from the owner status or runner package when needed.
|
|
7
|
+
|
|
8
|
+
## [5.345.4] — 2026-10-05
|
|
9
|
+
- Run a pinned or newer managed hq-cloud sync runner ahead of the staged sync bundle, so daemon sync no longer stays on the hq-cloud version bundled with the CLI after an update or pin (hq-cli#1547).
|
|
10
|
+
- Avoid dispatching the global hook when the working directory is inside an HQ root.
|
|
11
|
+
|
|
5
12
|
## [5.345.3] — 2026-10-05
|
|
6
13
|
- Retry a blocker freshness lane-inbox fallback after a transient append failure.
|
|
7
14
|
- Scope bundled-script stdin checks to changes in their workflow, build inputs, CLI sources, tests, or shell assets.
|
package/dist/commands/daemon.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
import { major as semverMajor, valid as validSemver } from "semver";
|
|
19
|
+
import { compare as semverCompare, major as semverMajor, valid as validSemver } from "semver";
|
|
20
20
|
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
|
|
21
21
|
import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
22
22
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
@@ -24,13 +24,14 @@ import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/d
|
|
|
24
24
|
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
25
25
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
26
26
|
import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
27
|
-
import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
|
|
27
|
+
import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy } from "../lib/daemon/hq-cloud.js";
|
|
28
28
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
29
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
30
30
|
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
31
31
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
32
|
import { runDaemonCall } from "../lib/daemon/command-call.js";
|
|
33
|
-
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
33
|
+
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
34
|
+
import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
|
|
34
35
|
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
35
36
|
import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
|
|
36
37
|
import { hqdRequest, hqdSocketPath, HqdClientError } from "../lib/daemon/protocol.js";
|
|
@@ -440,6 +441,38 @@ function daemonSyncModeClient(d) {
|
|
|
440
441
|
return new VaultClient(buildVaultConfig(token));
|
|
441
442
|
})();
|
|
442
443
|
}
|
|
444
|
+
function pathIsWithin(candidate, root, platform) {
|
|
445
|
+
const pathApi = platform === "win32" ? path.win32 : path;
|
|
446
|
+
const relative = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate));
|
|
447
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(".." + pathApi.sep) && !pathApi.isAbsolute(relative));
|
|
448
|
+
}
|
|
449
|
+
function syncRunnerSource(runnerPath, daemonRoot, platform) {
|
|
450
|
+
if (!runnerPath)
|
|
451
|
+
return "unknown";
|
|
452
|
+
if (pathIsWithin(runnerPath, syncRunnerBundleStageRoot(daemonRoot), platform))
|
|
453
|
+
return "staged-sync";
|
|
454
|
+
if (pathIsWithin(runnerPath, managedHqCloudRoot(daemonRoot), platform))
|
|
455
|
+
return "managed-full-package";
|
|
456
|
+
const bundled = bundledHqCloud();
|
|
457
|
+
if (bundled && pathIsWithin(runnerPath, bundled.root, platform))
|
|
458
|
+
return "bundled-full-package";
|
|
459
|
+
return "unknown";
|
|
460
|
+
}
|
|
461
|
+
function syncVersionMismatch(status) {
|
|
462
|
+
const running = status.hqCloudVersion;
|
|
463
|
+
if (!running)
|
|
464
|
+
return null;
|
|
465
|
+
if (status.pinnedHqCloudVersion && running !== status.pinnedHqCloudVersion) {
|
|
466
|
+
return "Sync runs hq-cloud " + running + ", while the configured pin expects " + status.pinnedHqCloudVersion + ". Run hq daemon restart sync after updating hq-cli to apply it.";
|
|
467
|
+
}
|
|
468
|
+
if (status.managedHqCloudVersion
|
|
469
|
+
&& validSemver(status.managedHqCloudVersion)
|
|
470
|
+
&& validSemver(running)
|
|
471
|
+
&& semverCompare(status.managedHqCloudVersion, running) > 0) {
|
|
472
|
+
return "Sync runs hq-cloud " + running + ", while managed hq-cloud " + status.managedHqCloudVersion + " is newer. Run hq daemon restart sync after updating hq-cli to apply it.";
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
443
476
|
function daemonSyncStatus(d) {
|
|
444
477
|
const host = currentHost(d);
|
|
445
478
|
const { paths } = host;
|
|
@@ -461,22 +494,64 @@ function daemonSyncStatus(d) {
|
|
|
461
494
|
const ownerCommandLine = ownerStatusCandidate
|
|
462
495
|
? processCommandLine(ownerStatusCandidate.pid, { platform: d.platform, run: d.run })
|
|
463
496
|
: null;
|
|
464
|
-
const
|
|
465
|
-
?
|
|
497
|
+
const ownerRunnerPath = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
|
|
498
|
+
? syncRunnerPathFromCommandLine(ownerCommandLine)
|
|
466
499
|
: null;
|
|
467
|
-
|
|
500
|
+
const ownerStatus = ownerRunnerPath ? ownerStatusCandidate : null;
|
|
501
|
+
const unitCommandLine = ownerRunnerPath || !syncUnit?.pid
|
|
502
|
+
? null
|
|
503
|
+
: processCommandLine(syncUnit.pid, { platform: d.platform, run: d.run });
|
|
504
|
+
const unitRunnerPath = unitCommandLine && isSyncRunnerCommandLine(unitCommandLine)
|
|
505
|
+
? syncRunnerPathFromCommandLine(unitCommandLine)
|
|
506
|
+
: null;
|
|
507
|
+
const runnerPath = ownerRunnerPath ?? unitRunnerPath;
|
|
508
|
+
const runnerSource = syncRunnerSource(runnerPath, paths.root, d.platform);
|
|
509
|
+
const managedCopy = managedHqCloud(paths.root);
|
|
510
|
+
const managedVersion = managedCopy?.version ?? null;
|
|
511
|
+
let hqCloudVersion = ownerStatus?.version ?? null;
|
|
512
|
+
let hqCloudVersionSource = hqCloudVersion ? "owner-status" : "unknown";
|
|
513
|
+
if (!hqCloudVersion && runnerPath) {
|
|
514
|
+
try {
|
|
515
|
+
const copy = runnerSource === "staged-sync"
|
|
516
|
+
? { root: "", version: stagedSyncRunnerVersion(runnerPath) }
|
|
517
|
+
: runnerSource === "managed-full-package"
|
|
518
|
+
? readHqCloudCopy(managedHqCloudRoot(paths.root))
|
|
519
|
+
: runnerSource === "bundled-full-package"
|
|
520
|
+
? bundledHqCloud()
|
|
521
|
+
: null;
|
|
522
|
+
if (copy) {
|
|
523
|
+
hqCloudVersion = copy.version;
|
|
524
|
+
hqCloudVersionSource = "runner-path";
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
d.err("Could not read sync runner version metadata; treating it as unavailable.");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Compare the executing version, not the installed managed copy: the copy can be updated on disk while an older runner still runs.
|
|
532
|
+
const runnerMatchesPin = runnerSource === "managed-full-package" && config.hqCloudVersion && hqCloudVersion
|
|
533
|
+
? hqCloudVersion === config.hqCloudVersion
|
|
534
|
+
: null;
|
|
535
|
+
const status = {
|
|
468
536
|
running: syncUnit?.status === "running",
|
|
469
537
|
paused: config.syncPaused === true,
|
|
470
538
|
syncOwner: effectiveSyncOwner(config, host.detected.type),
|
|
471
539
|
owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
|
|
472
540
|
runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
|
|
473
|
-
hqCloudVersion
|
|
541
|
+
hqCloudVersion,
|
|
542
|
+
hqCloudVersionSource,
|
|
543
|
+
runnerMatchesPin,
|
|
544
|
+
runnerPath,
|
|
545
|
+
runnerSource,
|
|
546
|
+
managedHqCloudVersion: managedVersion,
|
|
547
|
+
pinnedHqCloudVersion: config.hqCloudVersion ?? null,
|
|
474
548
|
lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
|
|
475
549
|
lastPassResult: syncUnit?.lastPassResult ?? null,
|
|
476
550
|
unitStatus: syncUnit?.status ?? "not-running",
|
|
477
551
|
reason: syncUnit?.reason ?? null,
|
|
478
552
|
logPath: paths.unitLogPath("sync"),
|
|
479
553
|
};
|
|
554
|
+
return status;
|
|
480
555
|
}
|
|
481
556
|
async function runDaemonSyncStatus(d, opts) {
|
|
482
557
|
const status = daemonSyncStatus(d);
|
|
@@ -487,6 +562,15 @@ async function runDaemonSyncStatus(d, opts) {
|
|
|
487
562
|
d.out(`Sync ${status.running ? "running" : status.unitStatus}${status.paused ? " (paused)" : ""}.`);
|
|
488
563
|
d.out(`Configured sync owner: ${status.syncOwner}.`);
|
|
489
564
|
d.out(`Owner: ${status.owner ?? "none"}; runner pid: ${status.runnerPid ?? "none"}; hq-cloud: ${status.hqCloudVersion ?? "unknown"}.`);
|
|
565
|
+
d.out(`Sync runner: ${status.runnerPath ?? "unknown"} (${status.runnerSource}); hq-cloud ${status.hqCloudVersion ?? "unknown"}.`);
|
|
566
|
+
d.out("Managed hq-cloud: " + (status.managedHqCloudVersion ?? "unknown") + "; pinned hq-cloud: " + (status.pinnedHqCloudVersion ?? "none") + ".");
|
|
567
|
+
if (status.runnerSource === "managed-full-package") {
|
|
568
|
+
const pinMatch = status.runnerMatchesPin === null ? "unknown" : status.runnerMatchesPin ? "yes" : "no";
|
|
569
|
+
d.out("Managed runner matches pin: " + pinMatch + ".");
|
|
570
|
+
}
|
|
571
|
+
const versionMismatch = syncVersionMismatch(status);
|
|
572
|
+
if (versionMismatch)
|
|
573
|
+
d.out(versionMismatch);
|
|
490
574
|
if (status.reason)
|
|
491
575
|
d.out(`Reason: ${status.reason}`);
|
|
492
576
|
d.out(`Log: ${status.logPath}`);
|
|
@@ -23,6 +23,11 @@ export type ManagedHqCloud = PickedHqCloud & {
|
|
|
23
23
|
export declare function managedHqCloudDir(daemonRoot: string): string;
|
|
24
24
|
/** The package folder inside the updater's install prefix. */
|
|
25
25
|
export declare function managedHqCloudRoot(daemonRoot: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* True only when both versions are valid semver and `a` is older than `b`.
|
|
28
|
+
* Prereleases count, so 6.18.33-alpha.1 is older than 6.18.33.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hqCloudVersionOlder(a: string, b: string): boolean;
|
|
26
31
|
/**
|
|
27
32
|
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
28
33
|
* that fails it (an interrupted or damaged install) is passed over.
|
|
@@ -55,6 +60,9 @@ export declare function hqCloudInstallCommand(prefix: string, version: string, p
|
|
|
55
60
|
args: string[];
|
|
56
61
|
shell: boolean;
|
|
57
62
|
};
|
|
63
|
+
export declare function readHqCloudCopy(root: string): HqCloudCopy | null;
|
|
64
|
+
/** The hq-cloud copy installed in the daemon-owned managed package root. */
|
|
65
|
+
export declare function managedHqCloud(daemonRoot: string): HqCloudCopy | null;
|
|
58
66
|
/** The copy bundled with this CLI. */
|
|
59
67
|
export declare function bundledHqCloud(): HqCloudCopy | null;
|
|
60
68
|
/** The hq-cloud the daemon should run right now. */
|
|
@@ -10,6 +10,7 @@ import { spawnSync } from "node:child_process";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { lt as semverLt, valid as validSemver } from "semver";
|
|
13
14
|
import { readDaemonConfig } from "./config.js";
|
|
14
15
|
import { buildSpawnPlan } from "../../utils/windows-spawn.js";
|
|
15
16
|
export const HQ_CLOUD_PACKAGE = "@indigoai-us/hq-cloud";
|
|
@@ -31,6 +32,15 @@ function newer(a, b) {
|
|
|
31
32
|
}
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* True only when both versions are valid semver and `a` is older than `b`.
|
|
37
|
+
* Prereleases count, so 6.18.33-alpha.1 is older than 6.18.33.
|
|
38
|
+
*/
|
|
39
|
+
export function hqCloudVersionOlder(a, b) {
|
|
40
|
+
const va = validSemver(a.trim());
|
|
41
|
+
const vb = validSemver(b.trim());
|
|
42
|
+
return !!va && !!vb && semverLt(va, vb);
|
|
43
|
+
}
|
|
34
44
|
/**
|
|
35
45
|
* `usable` says whether a copy has what the daemon runs; a newer managed copy
|
|
36
46
|
* that fails it (an interrupted or damaged install) is passed over.
|
|
@@ -108,7 +118,7 @@ export function hqCloudInstallCommand(prefix, version, platform = process.platfo
|
|
|
108
118
|
const plan = buildSpawnPlan("npm", ["install", "--prefix", prefix, "--no-audit", "--no-fund", "--omit=dev", `${HQ_CLOUD_PACKAGE}@${version}`], platform);
|
|
109
119
|
return { file: plan.cmd, args: plan.args, shell: plan.shell };
|
|
110
120
|
}
|
|
111
|
-
function readHqCloudCopy(root) {
|
|
121
|
+
export function readHqCloudCopy(root) {
|
|
112
122
|
try {
|
|
113
123
|
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
114
124
|
return typeof pkg.version === "string" ? { root, version: pkg.version } : null;
|
|
@@ -117,6 +127,10 @@ function readHqCloudCopy(root) {
|
|
|
117
127
|
return null;
|
|
118
128
|
}
|
|
119
129
|
}
|
|
130
|
+
/** The hq-cloud copy installed in the daemon-owned managed package root. */
|
|
131
|
+
export function managedHqCloud(daemonRoot) {
|
|
132
|
+
return readHqCloudCopy(managedHqCloudRoot(daemonRoot));
|
|
133
|
+
}
|
|
120
134
|
/** The copy bundled with this CLI. */
|
|
121
135
|
export function bundledHqCloud() {
|
|
122
136
|
try {
|
|
@@ -130,12 +144,12 @@ export function bundledHqCloud() {
|
|
|
130
144
|
/** The hq-cloud the daemon should run right now. */
|
|
131
145
|
export function currentHqCloud(daemonRoot) {
|
|
132
146
|
const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
|
|
133
|
-
return pickHqCloud(bundledHqCloud(),
|
|
147
|
+
return pickHqCloud(bundledHqCloud(), managedHqCloud(daemonRoot), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
|
|
134
148
|
}
|
|
135
149
|
/** After the CLI package lease is released, only daemon-owned package files are eligible. */
|
|
136
150
|
export function currentHqCloudAfterLeaseRelease(daemonRoot, expectedVersion) {
|
|
137
151
|
const config = readDaemonConfig(path.join(daemonRoot, "config.json"));
|
|
138
|
-
const managed =
|
|
152
|
+
const managed = managedHqCloud(daemonRoot);
|
|
139
153
|
if (!managed || !hqCloudRunners(managed.root).syncRunner)
|
|
140
154
|
return null;
|
|
141
155
|
if (config.hqCloudVersion && config.hqCloudVersion !== managed.version)
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -59,7 +59,10 @@ export type SyncRunnerSource = {
|
|
|
59
59
|
kind: "unavailable";
|
|
60
60
|
runnerPath: null;
|
|
61
61
|
};
|
|
62
|
-
export declare function syncRunnerSourceForPlan(stagedRunnerPath: string | null, hqCloud: PickedHqCloud | null, legacyBundledRunnerPath?: string | null
|
|
62
|
+
export declare function syncRunnerSourceForPlan(stagedRunnerPath: string | null, hqCloud: PickedHqCloud | null, legacyBundledRunnerPath?: string | null, staged?: {
|
|
63
|
+
version: string | null;
|
|
64
|
+
pinnedVersion?: string | null;
|
|
65
|
+
}): SyncRunnerSource;
|
|
63
66
|
/** Whether every executable in the first service plan remains usable after the CLI package is replaced. */
|
|
64
67
|
export declare function daemonPlanCanReleasePackageUseLease(plans: readonly UnitPlan[], replaceablePackageRoots: readonly string[], stagingFailed: boolean, hqBootstrapReady?: boolean): boolean;
|
|
65
68
|
export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null): [string, string[]];
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -37,7 +37,7 @@ import { startHqdServer } from "./server.js";
|
|
|
37
37
|
import { heartbeatFile, startHeartbeat } from "./heartbeat.js";
|
|
38
38
|
import { createProtocolTail, ensureProtocolFifo, openProtocolWriter } from "./protocol-tail.js";
|
|
39
39
|
import { takeControlRequests } from "./requests.js";
|
|
40
|
-
import { currentHqCloud, currentHqCloudAfterLeaseRelease, hqCloudRunners } from "./hq-cloud.js";
|
|
40
|
+
import { currentHqCloud, currentHqCloudAfterLeaseRelease, hqCloudRunners, hqCloudVersionOlder } from "./hq-cloud.js";
|
|
41
41
|
import { stageSyncRunnerBundle } from "./sync-runner-bundle.js";
|
|
42
42
|
import { stageHqChildBootstrap } from "./hq-child-bootstrap-stage.js";
|
|
43
43
|
import { runnerSessionName } from "./remote-control.js";
|
|
@@ -76,7 +76,21 @@ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
|
|
|
76
76
|
const LEGACY_PROBE_TTL_MS = 60_000;
|
|
77
77
|
/** A flag read that has not answered by then counts as off. */
|
|
78
78
|
const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
|
|
79
|
-
export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null) {
|
|
79
|
+
export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null, staged = { version: null }) {
|
|
80
|
+
// A managed copy lives in the daemon directory, so it survives a CLI
|
|
81
|
+
// replacement. Before lease release pickHqCloud returns it only when it is
|
|
82
|
+
// pinned or newer than the bundled copy; after release it is the only
|
|
83
|
+
// eligible package, so it must not displace a newer staged bundle unless it
|
|
84
|
+
// is the pinned version. The staged bundle is built from the bundled copy and
|
|
85
|
+
// otherwise only stands in for that copy (hq-cli#1547).
|
|
86
|
+
const managedYieldsToStaged = stagedRunnerPath !== null &&
|
|
87
|
+
hqCloud?.source === "managed" &&
|
|
88
|
+
hqCloud.version !== staged.pinnedVersion &&
|
|
89
|
+
staged.version !== null &&
|
|
90
|
+
hqCloudVersionOlder(hqCloud.version, staged.version);
|
|
91
|
+
if (hqCloud?.source === "managed" && !managedYieldsToStaged) {
|
|
92
|
+
return { kind: "managed-full-package", runnerPath: path.join(hqCloud.root, "dist", "bin", "sync-runner.js") };
|
|
93
|
+
}
|
|
80
94
|
if (stagedRunnerPath)
|
|
81
95
|
return { kind: "staged-sync", runnerPath: stagedRunnerPath };
|
|
82
96
|
if (!hqCloud) {
|
|
@@ -86,10 +100,7 @@ export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundled
|
|
|
86
100
|
}
|
|
87
101
|
// The caller has already selected a usable full package; preserve the
|
|
88
102
|
// conventional path here, including in tests with virtual package roots.
|
|
89
|
-
|
|
90
|
-
return hqCloud.source === "managed"
|
|
91
|
-
? { kind: "managed-full-package", runnerPath }
|
|
92
|
-
: { kind: "bundled-full-package", runnerPath };
|
|
103
|
+
return { kind: "bundled-full-package", runnerPath: path.join(hqCloud.root, "dist", "bin", "sync-runner.js") };
|
|
93
104
|
}
|
|
94
105
|
function isPathWithin(root, candidate) {
|
|
95
106
|
if (!root || !path.isAbsolute(candidate))
|
|
@@ -559,7 +570,7 @@ export async function runDaemon(opts) {
|
|
|
559
570
|
legacySyncRunnerPath = resolveSyncRunnerPath();
|
|
560
571
|
legacySyncRunnerPathChecked = true;
|
|
561
572
|
}
|
|
562
|
-
const syncSource = syncRunnerSourceForPlan(staged?.runnerPath ?? null, hqCloud, packageUseLeaseReleased ? null : legacySyncRunnerPath);
|
|
573
|
+
const syncSource = syncRunnerSourceForPlan(staged?.runnerPath ?? null, hqCloud, packageUseLeaseReleased ? null : legacySyncRunnerPath, { version: staged?.metadata.version ?? null, pinnedVersion: config.hqCloudVersion ?? null });
|
|
563
574
|
if (!syncSource.runnerPath && !warnedMissingSyncRunner) {
|
|
564
575
|
warnedMissingSyncRunner = true;
|
|
565
576
|
log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
|
|
@@ -4,6 +4,8 @@ export declare const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
|
|
|
4
4
|
export declare const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
|
|
5
5
|
export declare const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
|
|
6
6
|
export declare const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
|
|
7
|
+
/** Root directory where validated sync-runner bundles are selected. */
|
|
8
|
+
export declare function syncRunnerBundleStageRoot(daemonRoot: string): string;
|
|
7
9
|
/** package.json contract that the hq-cli build step emits beside the bundled bin directory. */
|
|
8
10
|
export interface SyncRunnerBundleMetadata {
|
|
9
11
|
name: "hq-cli-daemon-sync-runner";
|
|
@@ -32,6 +34,8 @@ export interface StageSyncRunnerBundleOptions {
|
|
|
32
34
|
copyFile?: typeof fs.copyFileSync;
|
|
33
35
|
renameDirectory?: (from: string, to: string) => void;
|
|
34
36
|
}
|
|
37
|
+
/** Read the validated metadata version for a runner inside its staged bundle. */
|
|
38
|
+
export declare function stagedSyncRunnerVersion(runnerPath: string): string;
|
|
35
39
|
/**
|
|
36
40
|
* Stage one validated runner bundle into an immutable daemon-owned directory.
|
|
37
41
|
* Only a completed, fsynced directory is returned; temp siblings are ignored.
|
|
@@ -9,6 +9,10 @@ export const SYNC_RUNNER_BUNDLE_RUNNER_FILE = "bin/sync-runner.js";
|
|
|
9
9
|
export const SYNC_RUNNER_BUNDLE_METADATA_FILE = "package.json";
|
|
10
10
|
export const SYNC_RUNNER_BUNDLE_DIRECTORY = "hq-cloud-sync-runner";
|
|
11
11
|
export const SYNC_RUNNER_BUNDLE_FORMAT_VERSION = 1;
|
|
12
|
+
/** Root directory where validated sync-runner bundles are selected. */
|
|
13
|
+
export function syncRunnerBundleStageRoot(daemonRoot) {
|
|
14
|
+
return path.join(daemonRoot, "bundles", "sync-runner");
|
|
15
|
+
}
|
|
12
16
|
export class SyncRunnerBundleStageError extends Error {
|
|
13
17
|
code;
|
|
14
18
|
constructor(code, message, options) {
|
|
@@ -55,6 +59,11 @@ function readMetadata(file) {
|
|
|
55
59
|
}
|
|
56
60
|
return metadata;
|
|
57
61
|
}
|
|
62
|
+
/** Read the validated metadata version for a runner inside its staged bundle. */
|
|
63
|
+
export function stagedSyncRunnerVersion(runnerPath) {
|
|
64
|
+
const bundleDirectory = path.dirname(path.dirname(runnerPath));
|
|
65
|
+
return readMetadata(path.join(bundleDirectory, SYNC_RUNNER_BUNDLE_METADATA_FILE)).version;
|
|
66
|
+
}
|
|
58
67
|
function hqCloudRange() {
|
|
59
68
|
const cliPackagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../package.json");
|
|
60
69
|
try {
|
|
@@ -138,7 +147,7 @@ export function stageSyncRunnerBundle(options) {
|
|
|
138
147
|
if (sha256(sourceRunner) !== metadata.bundleSha256) {
|
|
139
148
|
fail("HASH_MISMATCH", "The sync-runner bundle sha256 does not match its sidecar");
|
|
140
149
|
}
|
|
141
|
-
const stageRoot =
|
|
150
|
+
const stageRoot = syncRunnerBundleStageRoot(options.daemonRoot);
|
|
142
151
|
const selectedDirectory = path.join(stageRoot, stagedLeaf(metadata));
|
|
143
152
|
const existing = readStaged(selectedDirectory, metadata, range);
|
|
144
153
|
if (existing)
|
|
@@ -44,6 +44,8 @@ export declare function watchRunnerOwnerStatusPath(hqRoot: string, home?: string
|
|
|
44
44
|
export declare function readWatchRunnerOwnerStatus(statusPath: string): WatchRunnerOwnerStatus | null;
|
|
45
45
|
/** Match a live process command line to the hq-cloud watch runner executable. */
|
|
46
46
|
export declare function isSyncRunnerCommandLine(commandLine: string): boolean;
|
|
47
|
+
/** Return the script path from a command line already verified as a sync runner. */
|
|
48
|
+
export declare function syncRunnerPathFromCommandLine(commandLine: string): string | null;
|
|
47
49
|
/** Return a clear wait reason when an unleased runner already targets this HQ root. */
|
|
48
50
|
export declare function syncRunnerUnleasedBlockReason(hqRoot: string, processes: readonly SyncRunnerProcess[], ownerPid: number | null, platform?: NodeJS.Platform, daemonRunnerPids?: ReadonlySet<number>): string | undefined;
|
|
49
51
|
/** Read complete process command lines so root matching can include trailing arguments. */
|
|
@@ -59,6 +59,14 @@ export function readWatchRunnerOwnerStatus(statusPath) {
|
|
|
59
59
|
export function isSyncRunnerCommandLine(commandLine) {
|
|
60
60
|
return SYNC_RUNNER_COMMAND.test(commandLine);
|
|
61
61
|
}
|
|
62
|
+
/** Return the script path from a command line already verified as a sync runner. */
|
|
63
|
+
export function syncRunnerPathFromCommandLine(commandLine) {
|
|
64
|
+
if (!isSyncRunnerCommandLine(commandLine))
|
|
65
|
+
return null;
|
|
66
|
+
const args = commandLineArgs(commandLine);
|
|
67
|
+
return args.find((arg) => (path.isAbsolute(arg) || path.win32.isAbsolute(arg))
|
|
68
|
+
&& /(?:^|[\\/])(?:sync-runner\.js|hq-sync-runner(?:\.(?:cmd|exe))?)$/i.test(arg)) ?? null;
|
|
69
|
+
}
|
|
62
70
|
function commandLineArgs(commandLine) {
|
|
63
71
|
const args = [];
|
|
64
72
|
let current = "";
|
|
@@ -49,7 +49,7 @@ export function hqSkillsSource(hqRoot) {
|
|
|
49
49
|
}
|
|
50
50
|
/** The exact hook command registered for one event (absolute path, quoted). */
|
|
51
51
|
export function hookCommand(hqRoot, event, target = 'master') {
|
|
52
|
-
const skipInsideRoot = `if [ -d "\${CLAUDE_PROJECT_DIR:-}" ] && [ "\${CLAUDE_PROJECT_DIR}" -ef ${shellQuote(hqRoot)} ]; then exit 0; fi; `;
|
|
52
|
+
const skipInsideRoot = `if [ -d "\${CLAUDE_PROJECT_DIR:-}" ] && [ "\${CLAUDE_PROJECT_DIR}" -ef ${shellQuote(hqRoot)} ]; then exit 0; fi; cwd=$(pwd -P 2>/dev/null || pwd); while [ -n "$cwd" ]; do if [ -f "$cwd/core/core.yaml" ] && [ -f "$cwd/companies/manifest.yaml" ]; then exit 0; fi; [ "$cwd" = / ] && break; cwd=\${cwd%/*}; [ -n "$cwd" ] || cwd=/; done; `;
|
|
53
53
|
if (target === 'shim')
|
|
54
54
|
return `${skipInsideRoot}exec /bin/sh ${shellQuote(shimPath(hqRoot))} ${shellQuote(event)} --runtime claude`;
|
|
55
55
|
return `${skipInsideRoot}BASH_ENV=/dev/null exec bash ${shellQuote(masterHookPath(hqRoot))} ${shellQuote(event)}`;
|