@indigoai-us/hq-cli 5.345.4 → 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 CHANGED
@@ -2,6 +2,9 @@
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
+
5
8
  ## [5.345.4] — 2026-10-05
6
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).
7
10
  - Avoid dispatching the global hook when the working directory is inside an HQ root.
@@ -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 ownerStatus = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
465
- ? ownerStatusCandidate
497
+ const ownerRunnerPath = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
498
+ ? syncRunnerPathFromCommandLine(ownerCommandLine)
466
499
  : null;
467
- return {
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: ownerStatus?.version ?? null,
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}`);
@@ -60,6 +60,9 @@ export declare function hqCloudInstallCommand(prefix: string, version: string, p
60
60
  args: string[];
61
61
  shell: boolean;
62
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;
63
66
  /** The copy bundled with this CLI. */
64
67
  export declare function bundledHqCloud(): HqCloudCopy | null;
65
68
  /** The hq-cloud the daemon should run right now. */
@@ -118,7 +118,7 @@ export function hqCloudInstallCommand(prefix, version, platform = process.platfo
118
118
  const plan = buildSpawnPlan("npm", ["install", "--prefix", prefix, "--no-audit", "--no-fund", "--omit=dev", `${HQ_CLOUD_PACKAGE}@${version}`], platform);
119
119
  return { file: plan.cmd, args: plan.args, shell: plan.shell };
120
120
  }
121
- function readHqCloudCopy(root) {
121
+ export function readHqCloudCopy(root) {
122
122
  try {
123
123
  const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
124
124
  return typeof pkg.version === "string" ? { root, version: pkg.version } : null;
@@ -127,6 +127,10 @@ function readHqCloudCopy(root) {
127
127
  return null;
128
128
  }
129
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
+ }
130
134
  /** The copy bundled with this CLI. */
131
135
  export function bundledHqCloud() {
132
136
  try {
@@ -140,12 +144,12 @@ export function bundledHqCloud() {
140
144
  /** The hq-cloud the daemon should run right now. */
141
145
  export function currentHqCloud(daemonRoot) {
142
146
  const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
143
- return pickHqCloud(bundledHqCloud(), readHqCloudCopy(managedHqCloudRoot(daemonRoot)), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
147
+ return pickHqCloud(bundledHqCloud(), managedHqCloud(daemonRoot), (copy) => hqCloudRunners(copy.root).syncRunner !== null, pinnedVersion);
144
148
  }
145
149
  /** After the CLI package lease is released, only daemon-owned package files are eligible. */
146
150
  export function currentHqCloudAfterLeaseRelease(daemonRoot, expectedVersion) {
147
151
  const config = readDaemonConfig(path.join(daemonRoot, "config.json"));
148
- const managed = readHqCloudCopy(managedHqCloudRoot(daemonRoot));
152
+ const managed = managedHqCloud(daemonRoot);
149
153
  if (!managed || !hqCloudRunners(managed.root).syncRunner)
150
154
  return null;
151
155
  if (config.hqCloudVersion && config.hqCloudVersion !== managed.version)
@@ -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 = path.join(options.daemonRoot, "bundles", "sync-runner");
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 = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.4",
3
+ "version": "5.345.5",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {