@indigoai-us/hq-cli 5.345.32 → 5.345.33

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/command-catalog.generated.d.ts +92 -3
  3. package/dist/command-catalog.generated.js +111 -3
  4. package/dist/commands/db-dump.d.ts +8 -0
  5. package/dist/commands/db-dump.js +50 -0
  6. package/dist/commands/db-provision.d.ts +3 -10
  7. package/dist/commands/db-provision.js +26 -47
  8. package/dist/commands/db-remote-deps.d.ts +18 -0
  9. package/dist/commands/db-remote-deps.js +59 -0
  10. package/dist/commands/db-sql.d.ts +2 -1
  11. package/dist/commands/db-sql.js +30 -5
  12. package/dist/commands/db-usage.d.ts +7 -0
  13. package/dist/commands/db-usage.js +28 -0
  14. package/dist/commands/db.js +4 -0
  15. package/dist/commands/files.js +33 -5
  16. package/dist/commands/lanes-message.js +40 -8
  17. package/dist/commands/lanes-questions.d.ts +6 -1
  18. package/dist/commands/lanes-questions.js +19 -7
  19. package/dist/commands/lanes-resume.js +17 -6
  20. package/dist/commands/lanes-shared.d.ts +6 -1
  21. package/dist/commands/lanes.js +74 -1
  22. package/dist/commands/mesh.js +150 -26
  23. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +68 -604
  24. package/dist/hq-cloud-sync-runner/package.json +1 -1
  25. package/dist/lib/db/control-plane.d.ts +32 -0
  26. package/dist/lib/db/control-plane.js +50 -0
  27. package/dist/lib/db/redact.d.ts +4 -0
  28. package/dist/lib/db/redact.js +17 -0
  29. package/dist/lib/db/remote-engine.d.ts +67 -0
  30. package/dist/lib/db/remote-engine.js +165 -0
  31. package/dist/lib/db/usage-format.d.ts +4 -0
  32. package/dist/lib/db/usage-format.js +41 -0
  33. package/dist/lib/lanes/keep-alive.d.ts +12 -0
  34. package/dist/lib/lanes/keep-alive.js +25 -6
  35. package/dist/lib/mesh/api.d.ts +2 -0
  36. package/dist/lib/mesh/project-board.d.ts +17 -0
  37. package/dist/lib/mesh/project-board.js +28 -0
  38. package/dist/lib/mesh/story-status.d.ts +16 -0
  39. package/dist/lib/mesh/story-status.js +32 -7
  40. package/dist/utils/self-update.d.ts +2 -0
  41. package/dist/utils/self-update.js +61 -23
  42. package/dist/utils/version-check.d.ts +1 -1
  43. package/dist/utils/version-check.js +2 -2
  44. package/dist/utils/version-gate.d.ts +21 -14
  45. package/dist/utils/version-gate.js +119 -28
  46. package/package.json +4 -1
@@ -159,6 +159,8 @@ export interface SelfUpdateDeps {
159
159
  * the target we just installed. Defaults to {@link checkUpdateConvergence}.
160
160
  */
161
161
  checkConvergence?: (targetVersion: string) => boolean;
162
+ /** Probe the previous command path after a failed install has been rolled back. */
163
+ checkRestoredInstall?: (targetVersion: string) => boolean;
162
164
  /**
163
165
  * Persist that `version` installed but never took effect, so the startup path
164
166
  * stops auto-retrying it. Defaults to {@link markLatestIneffective}.
@@ -65,7 +65,7 @@ import chalk from "chalk";
65
65
  import { Sentry } from "../sentry.js";
66
66
  import { isCliTelemetryDisabled } from "./cli-telemetry.js";
67
67
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
68
- import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, } from "./version-gate.js";
68
+ import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
69
69
  import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
70
70
  import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
71
71
  import { markLatestIneffective } from "./version-check.js";
@@ -75,7 +75,7 @@ import { createCurrentPackageUseLeaseHandoff, PACKAGE_USE_LEASE_HANDOFF_ENV, rel
75
75
  * Set on the re-exec'd child so it can never self-update (and re-exec) again.
76
76
  * One update + one re-exec per user invocation, ever.
77
77
  */
78
- export const REEXEC_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
78
+ export const REEXEC_GUARD_ENV = UPDATE_RECURSION_GUARD_ENV;
79
79
  const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(CLI_NAME)}/latest`;
80
80
  const FETCH_TIMEOUT_MS = 3_000;
81
81
  /**
@@ -549,7 +549,7 @@ async function fetchLatestVersion() {
549
549
  */
550
550
  export function buildSelfUpdatePlan(install) {
551
551
  if (install.manager === "pnpm")
552
- return { cmd: "pnpm", args: buildPnpmInstallArgv() };
552
+ return { cmd: "pnpm", args: buildPnpmInstallArgv(install) };
553
553
  if (install.manager === "bun")
554
554
  return { cmd: "bun", args: buildBunInstallArgv() };
555
555
  if (install.prefix)
@@ -780,28 +780,43 @@ async function updateAndReexec(argv, flavor, known, deps) {
780
780
  }
781
781
  const staged = canStageInstall(install) && (await readStagedSelfUpdateFlag(deps, env));
782
782
  const renamePath = deps.renamePath ?? fs.renameSync;
783
- // The in-place update moves the package aside, and the installer's
784
- // supervisor ships inside it, so run the installer through a copy.
785
- const supervisor = staged ? null : (deps.detachSupervisor ?? detachUpdateSupervisor)();
786
783
  // The staged install keeps the running package in place until the swap and
787
784
  // restores it itself, so it takes no snapshot.
785
+ // The installer supervisor must be copied before snapshotInstall moves the
786
+ // active package that contains its source.
787
+ const supervisor = staged ? null : (deps.detachSupervisor ?? detachUpdateSupervisor)();
788
788
  const installSnapshot = staged ? null : snapshotInstall(install, renamePath);
789
789
  if (!staged && !installSnapshot) {
790
- console.error(chalk.dim(" No backup could be made for this hq-cli install; proceeding without rollback protection."));
790
+ (deps.markIneffective ?? markLatestIneffective)(latest);
791
+ console.error(chalk.dim(` Update to hq-cli ${latest} skipped; the current install could not be backed up.`));
792
+ supervisor?.dispose();
793
+ releaseLock();
794
+ return { action: "update-failed", latest };
791
795
  }
796
+ const checkRestoredInstall = deps.checkRestoredInstall ?? ((version) => checkUpdateConvergence(version, {
797
+ requireEvidence: true,
798
+ exactVersion: true,
799
+ currentInstall: () => install,
800
+ }));
792
801
  let result;
793
802
  const plan = buildSelfUpdatePlan(install);
794
- // A pnpm global install needs PNPM_HOME to find its global bin dir. A
795
- // minimal-environment parent (systemd, cron, non-login shell) lacks it and
796
- // `pnpm add -g` aborts with ERR_PNPM_NO_GLOBAL_BIN_DIR (exit 1), so the
797
- // self-update never lands until the box is next touched from an interactive
798
- // shell. Derive PNPM_HOME from the running install so the update works
799
- // regardless of the parent environment. No-op for npm/bun and when the
800
- // parent already sets PNPM_HOME.
801
- const updateEnv = pnpmUpdateEnv(install, env);
803
+ // pnpm needs its global bin path on PATH. Corepack must also ignore a
804
+ // packageManager pin in the caller's working directory so this install uses
805
+ // the system-wide package manager without changing its selected version.
806
+ const updateEnv = install.manager === "pnpm"
807
+ ? {
808
+ ...env,
809
+ ...(pnpmUpdateEnv(install, env) ?? {}),
810
+ // Package-manager lifecycle hooks can run hq while this install is still
811
+ // in progress. They must never start another update or manager lookup.
812
+ [REEXEC_GUARD_ENV]: "1",
813
+ }
814
+ : undefined;
802
815
  let restoredAfterFailure = false;
803
816
  try {
804
- console.error(chalk.dim(`Updating hq-cli ${current} → ${latest}…`));
817
+ if (flavor.noun !== "command") {
818
+ console.error(chalk.dim(`Updating hq-cli ${current} → ${latest}…`));
819
+ }
805
820
  const defaultRunner = flavor.verbose ? runUpdateCommand : runUpdateQuiet;
806
821
  const runner = deps.runner ?? defaultRunner;
807
822
  // Forward env only when PNPM_HOME had to be injected, so the common path
@@ -841,14 +856,21 @@ async function updateAndReexec(argv, flavor, known, deps) {
841
856
  }
842
857
  if (!result.ok && installSnapshot) {
843
858
  restoredAfterFailure = restoreInstall(installSnapshot, renamePath);
844
- if (restoredAfterFailure)
845
- discardInstallSnapshot(installSnapshot);
859
+ if (restoredAfterFailure) {
860
+ restoredAfterFailure = checkRestoredInstall(current);
861
+ if (restoredAfterFailure)
862
+ discardInstallSnapshot(installSnapshot);
863
+ }
846
864
  }
847
865
  }
848
866
  catch (error) {
849
867
  if (installSnapshot) {
850
- if (restoreInstall(installSnapshot, renamePath))
868
+ if (restoreInstall(installSnapshot, renamePath) && checkRestoredInstall(current)) {
851
869
  discardInstallSnapshot(installSnapshot);
870
+ }
871
+ else {
872
+ console.error(chalk.red(" Could not restore and verify the previous hq-cli install after the update process failed."));
873
+ }
852
874
  }
853
875
  throw error;
854
876
  }
@@ -857,6 +879,12 @@ async function updateAndReexec(argv, flavor, known, deps) {
857
879
  releaseLock();
858
880
  }
859
881
  if (!result.ok) {
882
+ (deps.markIneffective ?? markLatestIneffective)(latest);
883
+ if (flavor.noun === "command") {
884
+ await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result, env, false);
885
+ console.error(chalk.yellow(`⚠ hq-cli update to ${latest} failed; continuing on ${current}.`));
886
+ return { action: "update-failed", latest };
887
+ }
860
888
  if (restoredAfterFailure) {
861
889
  console.error(chalk.dim(" The previous hq-cli install was restored."));
862
890
  }
@@ -902,8 +930,6 @@ async function updateAndReexec(argv, flavor, known, deps) {
902
930
  }
903
931
  return { action: "update-failed", latest };
904
932
  }
905
- if (installSnapshot)
906
- discardInstallSnapshot(installSnapshot);
907
933
  // Read-your-writes: a "successful" `npm install -g` proves only that the
908
934
  // prefix was rewritten, NOT that the `hq` this user's PATH resolves is the
909
935
  // copy we just wrote. When a ghost/shadowing install keeps winning PATH
@@ -917,14 +943,26 @@ async function updateAndReexec(argv, flavor, known, deps) {
917
943
  // So verify before announcing success. On non-convergence: mark the target
918
944
  // ineffective (suppresses auto-retry for a cooldown), skip the pointless
919
945
  // re-exec into a stale copy, and let the command run on the current version.
920
- const checkConvergence = deps.checkConvergence ?? checkUpdateConvergence;
946
+ const checkConvergence = deps.checkConvergence ?? ((version) => checkUpdateConvergence(version, { requireEvidence: true, currentInstall: () => install }));
921
947
  const converged = checkConvergence(latestValid);
922
948
  if (converged === false) {
949
+ let restored = true;
950
+ if (installSnapshot) {
951
+ restored = restoreInstall(installSnapshot, renamePath);
952
+ if (restored)
953
+ restored = checkRestoredInstall(current);
954
+ if (restored)
955
+ discardInstallSnapshot(installSnapshot);
956
+ else
957
+ console.error(chalk.red(" Could not restore and verify the previous hq-cli install after verification failed."));
958
+ }
923
959
  (deps.markIneffective ?? markLatestIneffective)(latest);
924
960
  console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
925
961
  await captureSelfUpdateOutcome("update-ineffective", current, latestValid, install.manager, undefined, env, flavor.noun === "rescue");
926
- return { action: "update-ineffective", latest };
962
+ return { action: restored ? "update-ineffective" : "update-failed", latest };
927
963
  }
964
+ if (installSnapshot)
965
+ discardInstallSnapshot(installSnapshot);
928
966
  // A resident mesh daemon keeps the old JS loaded after a global install.
929
967
  // Service restart is best-effort and cannot turn a successful update red.
930
968
  try {
@@ -16,7 +16,7 @@ export declare function staleAgainstCachedLatest(now?: number): string | null;
16
16
  /**
17
17
  * Record that installing `version` did not move the on-PATH `hq` forward, so
18
18
  * {@link staleAgainstCachedLatest} stops auto-retrying it for a cooldown. Called
19
- * by the self-updater's convergence check. Best-effort: a write failure only
19
+ * by the self-updater after an install failure or failed convergence check. Best-effort: a write failure only
20
20
  * means the loop guard is skipped this once, never a broken CLI.
21
21
  */
22
22
  export declare function markLatestIneffective(version: string, now?: number): void;
@@ -15,7 +15,7 @@ export const CLI_VERSION_NUDGE_FLAG = "cli.version-update-nudge";
15
15
  * break the per-invocation loop on a busy agent box, short enough that a box
16
16
  * whose PATH is later fixed recovers on its own without a manual `hq rescue`.
17
17
  */
18
- const INEFFECTIVE_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6h
18
+ const INEFFECTIVE_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24h
19
19
  function cachePath() {
20
20
  return path.join(os.homedir(), ".hq", "version-check.json");
21
21
  }
@@ -157,7 +157,7 @@ export function staleAgainstCachedLatest(now = Date.now()) {
157
157
  /**
158
158
  * Record that installing `version` did not move the on-PATH `hq` forward, so
159
159
  * {@link staleAgainstCachedLatest} stops auto-retrying it for a cooldown. Called
160
- * by the self-updater's convergence check. Best-effort: a write failure only
160
+ * by the self-updater after an install failure or failed convergence check. Best-effort: a write failure only
161
161
  * means the loop guard is skipped this once, never a broken CLI.
162
162
  */
163
163
  export function markLatestIneffective(version, now = Date.now()) {
@@ -29,6 +29,8 @@
29
29
  */
30
30
  import { buildSpawnPlan, quoteForWindowsShell } from "./windows-spawn.js";
31
31
  import { type UpdateLockHandle } from "./update-lock.js";
32
+ /** Prevent an update package manager's lifecycle child from starting another updater. */
33
+ export declare const UPDATE_RECURSION_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
32
34
  export type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "verifying" | "rollback" | "complete" | "failed" | "unknown";
33
35
  /** The last closed version-gate phase observed in this process. */
34
36
  export declare function getVersionGatePhase(): VersionGatePhase;
@@ -205,19 +207,13 @@ export declare function nonWritablePrefixNote(prefix: string, platform?: NodeJS.
205
207
  */
206
208
  export declare function derivePnpmHome(install: RunningInstall): string | null;
207
209
  /**
208
- * Environment for the pnpm self-update spawn. `pnpm add -g` aborts with
209
- * `ERR_PNPM_NO_GLOBAL_BIN_DIR` (exit 1) when neither `PNPM_HOME` nor a
210
- * `global-bin-dir` setting is present — the exact failure seen when `hq`
211
- * self-updates from a minimal-environment parent (a systemd `--user` service,
212
- * cron, or any non-login shell that never sourced the profile exporting
213
- * `PNPM_HOME`). When the running install is pnpm-managed and the parent lacks
214
- * `PNPM_HOME`, inject one derived from the install's own path so the update can
215
- * find the global bin dir it is about to rewrite.
216
- *
217
- * A no-op for npm/bun installs, and for any environment that already sets
218
- * `PNPM_HOME` (never overridden — the caller's value wins). Returns `undefined`
219
- * when nothing needs injecting, so callers can spawn with the inherited
220
- * environment unchanged (and pass no `env` at all).
210
+ * Environment for the pnpm self-update spawn. pnpm requires its configured
211
+ * global bin directory to be on PATH; pnpm 12.5.1 reports
212
+ * `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
213
+ * may also lack PNPM_HOME, so derive it from the running install when needed.
214
+ *
215
+ * A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
216
+ * `undefined` only when the inherited environment already has both values.
221
217
  */
222
218
  export declare function pnpmUpdateEnv(install: RunningInstall, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined;
223
219
  export declare function buildPrefixedInstallArgv(prefix: string): string[];
@@ -226,7 +222,16 @@ export declare function buildPrefixedInstallArgv(prefix: string): string[];
226
222
  * PATH shim as part of the install, so the next invocation genuinely resolves
227
223
  * the new version — which is the whole point of routing here instead of npm.
228
224
  */
229
- export declare function buildPnpmInstallArgv(): string[];
225
+ export declare function buildPnpmInstallArgv(install?: RunningInstall): string[];
226
+ /**
227
+ * Read the pnpm global install's current store and virtual-store locations.
228
+ * Newer pnpm versions can change both defaults, so a self-update must carry
229
+ * forward the paths recorded for the install it is replacing.
230
+ */
231
+ export declare function pnpmInstallDirs(install: RunningInstall): {
232
+ storeDir: string;
233
+ virtualStoreDir: string;
234
+ } | null;
230
235
  /** Argv for updating a Bun-managed global install. */
231
236
  export declare function buildBunInstallArgv(): string[];
232
237
  /**
@@ -419,6 +424,8 @@ declare function probeCliVersion(bin: string): string | null;
419
424
  * here may crash or block the CLI.
420
425
  */
421
426
  export declare function checkUpdateConvergence(targetVersion: string, deps?: {
427
+ requireEvidence?: boolean;
428
+ exactVersion?: boolean;
422
429
  resolveBin?: () => string | null;
423
430
  probeVersion?: (bin: string) => string | null;
424
431
  currentInstall?: () => RunningInstall;
@@ -35,6 +35,7 @@ import os from "node:os";
35
35
  import path from "node:path";
36
36
  import { fileURLToPath } from "node:url";
37
37
  import chalk from "chalk";
38
+ import * as yaml from "js-yaml";
38
39
  import semver from "semver";
39
40
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
40
41
  import { isVersionRequest } from "../version-request.js";
@@ -42,6 +43,8 @@ import { Sentry } from "../sentry.js";
42
43
  import { isCliTelemetryDisabled } from "./cli-telemetry.js";
43
44
  import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
44
45
  import { acquireUpdateLock, } from "./update-lock.js";
46
+ /** Prevent an update package manager's lifecycle child from starting another updater. */
47
+ export const UPDATE_RECURSION_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
45
48
  const CLIENT_ID = "hq-cli";
46
49
  const ENDPOINT_PATH = "/v1/client-version/check";
47
50
  const FETCH_TIMEOUT_MS = 3_000;
@@ -572,27 +575,45 @@ export function derivePnpmHome(install) {
572
575
  return home || null;
573
576
  }
574
577
  /**
575
- * Environment for the pnpm self-update spawn. `pnpm add -g` aborts with
576
- * `ERR_PNPM_NO_GLOBAL_BIN_DIR` (exit 1) when neither `PNPM_HOME` nor a
577
- * `global-bin-dir` setting is present — the exact failure seen when `hq`
578
- * self-updates from a minimal-environment parent (a systemd `--user` service,
579
- * cron, or any non-login shell that never sourced the profile exporting
580
- * `PNPM_HOME`). When the running install is pnpm-managed and the parent lacks
581
- * `PNPM_HOME`, inject one derived from the install's own path so the update can
582
- * find the global bin dir it is about to rewrite.
578
+ * Environment for the pnpm self-update spawn. pnpm requires its configured
579
+ * global bin directory to be on PATH; pnpm 12.5.1 reports
580
+ * `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
581
+ * may also lack PNPM_HOME, so derive it from the running install when needed.
583
582
  *
584
- * A no-op for npm/bun installs, and for any environment that already sets
585
- * `PNPM_HOME` (never overridden — the caller's value wins). Returns `undefined`
586
- * when nothing needs injecting, so callers can spawn with the inherited
587
- * environment unchanged (and pass no `env` at all).
583
+ * A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
584
+ * `undefined` only when the inherited environment already has both values.
588
585
  */
589
586
  export function pnpmUpdateEnv(install, base = process.env) {
590
- if (install.manager !== "pnpm" || base.PNPM_HOME)
587
+ if (install.manager !== "pnpm")
591
588
  return undefined;
592
- const home = derivePnpmHome(install);
589
+ const home = base.PNPM_HOME || derivePnpmHome(install);
593
590
  if (!home)
594
591
  return undefined;
595
- return { ...base, PNPM_HOME: home };
592
+ const binDir = path.join(home, "bin");
593
+ const pathKey = Object.keys(base).find((key) => key.toLowerCase() === "path") ?? "PATH";
594
+ const currentPath = base[pathKey] ?? "";
595
+ const normalizedBinDir = path.resolve(binDir);
596
+ const alreadyOnPath = currentPath.split(path.delimiter).some((entry) => {
597
+ if (!entry)
598
+ return false;
599
+ const normalizedEntry = path.resolve(entry);
600
+ return process.platform === "win32"
601
+ ? normalizedEntry.toLowerCase() === normalizedBinDir.toLowerCase()
602
+ : normalizedEntry === normalizedBinDir;
603
+ });
604
+ const result = {
605
+ ...base,
606
+ [UPDATE_RECURSION_GUARD_ENV]: "1",
607
+ // Corepack otherwise honors the packageManager in the user's current
608
+ // project, which can silently switch the update subprocess to that version.
609
+ COREPACK_ENABLE_PROJECT_SPEC: "0",
610
+ COREPACK_DEFAULT_TO_LATEST: "0",
611
+ };
612
+ if (!base.PNPM_HOME)
613
+ result.PNPM_HOME = home;
614
+ if (!alreadyOnPath)
615
+ result[pathKey] = [currentPath, binDir].filter(Boolean).join(path.delimiter);
616
+ return Object.keys(result).some((key) => result[key] !== base[key]) ? result : undefined;
596
617
  }
597
618
  export function buildPrefixedInstallArgv(prefix) {
598
619
  return ["install", "-g", "--prefix", prefix, LATEST_PACKAGE_SPEC];
@@ -602,8 +623,51 @@ export function buildPrefixedInstallArgv(prefix) {
602
623
  * PATH shim as part of the install, so the next invocation genuinely resolves
603
624
  * the new version — which is the whole point of routing here instead of npm.
604
625
  */
605
- export function buildPnpmInstallArgv() {
606
- return ["add", "-g", LATEST_PACKAGE_SPEC];
626
+ export function buildPnpmInstallArgv(install) {
627
+ const args = ["add", "-g", LATEST_PACKAGE_SPEC];
628
+ const dirs = install ? pnpmInstallDirs(install) : null;
629
+ if (dirs)
630
+ args.push("--store-dir", dirs.storeDir, "--virtual-store-dir", dirs.virtualStoreDir);
631
+ return args;
632
+ }
633
+ /**
634
+ * Read the pnpm global install's current store and virtual-store locations.
635
+ * Newer pnpm versions can change both defaults, so a self-update must carry
636
+ * forward the paths recorded for the install it is replacing.
637
+ */
638
+ export function pnpmInstallDirs(install) {
639
+ if (install.manager !== "pnpm" || !install.packageRoot)
640
+ return null;
641
+ const normalized = install.packageRoot.replace(/\\/g, "/");
642
+ const global = normalized.match(/^(.*\/global\/\d+\/node_modules)(?:\/|$)|^(.*\/global\/v\d+\/[^/]+\/node_modules)(?:\/|$)/);
643
+ const globalNodeModules = global?.[1] ?? global?.[2];
644
+ if (!globalNodeModules)
645
+ return null;
646
+ const metadataPath = path.join(globalNodeModules, ".modules.yaml");
647
+ try {
648
+ const metadata = yaml.load(readFileSync(metadataPath, "utf8"));
649
+ const storeDir = typeof metadata?.storeDir === "string" && metadata.storeDir
650
+ ? metadata.storeDir
651
+ : path.join(derivePnpmHome(install) ?? path.dirname(path.dirname(globalNodeModules)), "store", "v11");
652
+ const configuredVirtualStore = typeof metadata?.virtualStoreDir === "string"
653
+ ? metadata.virtualStoreDir
654
+ : ".pnpm";
655
+ const virtualStoreDir = path.isAbsolute(configuredVirtualStore)
656
+ ? configuredVirtualStore
657
+ : path.resolve(globalNodeModules, configuredVirtualStore);
658
+ return { storeDir, virtualStoreDir };
659
+ }
660
+ catch {
661
+ // The metadata file is absent in older pnpm global layouts. The global
662
+ // install root still determines its virtual store and default PNPM_HOME.
663
+ const pnpmHome = derivePnpmHome(install);
664
+ if (!pnpmHome)
665
+ return null;
666
+ return {
667
+ storeDir: path.join(pnpmHome, "store", "v11"),
668
+ virtualStoreDir: path.join(globalNodeModules, ".pnpm"),
669
+ };
670
+ }
607
671
  }
608
672
  /** Argv for updating a Bun-managed global install. */
609
673
  export function buildBunInstallArgv() {
@@ -1078,7 +1142,7 @@ function performUpdate(command, runner = runUpdateCommand) {
1078
1142
  */
1079
1143
  function manualUpdateCommand(install, decision) {
1080
1144
  if (install.manager === "pnpm")
1081
- return `pnpm ${buildPnpmInstallArgv().join(" ")}`;
1145
+ return `pnpm ${buildPnpmInstallArgv(install).join(" ")}`;
1082
1146
  if (install.manager === "bun")
1083
1147
  return `bun ${buildBunInstallArgv().join(" ")}`;
1084
1148
  if (install.prefix)
@@ -1112,7 +1176,7 @@ function nudgeUpdateRecommended(decision, install = resolveRunningInstall()) {
1112
1176
  return;
1113
1177
  }
1114
1178
  const command = install.manager === "pnpm"
1115
- ? `pnpm ${buildPnpmInstallArgv().join(" ")}`
1179
+ ? `pnpm ${buildPnpmInstallArgv(install).join(" ")}`
1116
1180
  : install.manager === "bun"
1117
1181
  ? `bun ${buildBunInstallArgv().join(" ")}`
1118
1182
  : decision.updateCommand;
@@ -1193,7 +1257,8 @@ export function parseReportedCliVersion(stdout) {
1193
1257
  /** `<bin> --version` output (trimmed), or null on any failure/timeout. */
1194
1258
  function probeCliVersion(bin) {
1195
1259
  try {
1196
- const result = spawnSync(bin, ["--version"], {
1260
+ const isJavaScriptEntry = path.extname(bin).toLowerCase() === ".js";
1261
+ const result = spawnSync(isJavaScriptEntry ? process.execPath : bin, isJavaScriptEntry ? [bin, "--version"] : ["--version"], {
1197
1262
  encoding: "utf-8",
1198
1263
  timeout: CONVERGENCE_TIMEOUT_MS,
1199
1264
  });
@@ -1224,19 +1289,30 @@ function probeCliVersion(bin) {
1224
1289
  */
1225
1290
  export function checkUpdateConvergence(targetVersion, deps = {}) {
1226
1291
  try {
1227
- const bin = (deps.resolveBin ?? resolveHqOnPath)();
1292
+ let bin;
1293
+ try {
1294
+ bin = (deps.resolveBin ?? resolveHqOnPath)();
1295
+ }
1296
+ catch {
1297
+ bin = null;
1298
+ }
1228
1299
  if (!bin) {
1229
- console.error(chalk.yellow("⚠ Updated, but couldn't resolve `hq` on PATH to verify the new version took effect."));
1230
- // Unverifiable, not disproven — the caller keeps its success report.
1231
- return true;
1300
+ const install = (deps.currentInstall ?? resolveRunningInstall)();
1301
+ bin = installedHqEntryPoint(install);
1302
+ if (!bin) {
1303
+ console.error(chalk.yellow("⚠ Updated, but couldn't resolve `hq` on PATH or from the install to verify the new version took effect."));
1304
+ return !deps.requireEvidence;
1305
+ }
1232
1306
  }
1233
1307
  const reported = (deps.probeVersion ?? probeCliVersion)(bin);
1234
1308
  if (!reported) {
1235
1309
  console.error(chalk.yellow(`⚠ Updated, but \`${bin} --version\` did not respond — couldn't verify the new version took effect.`));
1236
- return true; // unverifiable, see above
1310
+ return !deps.requireEvidence; // unverifiable, see above
1237
1311
  }
1238
1312
  if (reported === targetVersion)
1239
1313
  return true; // converged — the normal case
1314
+ if (deps.exactVersion)
1315
+ return false;
1240
1316
  //
1241
1317
  // The install runs `@latest`, not `@<targetVersion>`, so it can legitimately
1242
1318
  // land a version NEWER than the `latestVersion` the gate was handed: the npm
@@ -1284,8 +1360,23 @@ export function checkUpdateConvergence(targetVersion, deps = {}) {
1284
1360
  return false;
1285
1361
  }
1286
1362
  catch {
1287
- // Verification is best-effort; never break the CLI over a probe.
1288
- return true;
1363
+ // Keep the current fail-closed behavior for a thrown version probe: an
1364
+ // exception may mean the newly-written executable cannot be loaded.
1365
+ return !deps.requireEvidence;
1366
+ }
1367
+ }
1368
+ function installedHqEntryPoint(install) {
1369
+ if (!install.packageRoot)
1370
+ return null;
1371
+ try {
1372
+ const manifest = JSON.parse(readFileSync(path.join(install.packageRoot, "package.json"), "utf8"));
1373
+ const entry = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.hq;
1374
+ return typeof entry === "string" && entry
1375
+ ? path.resolve(install.packageRoot, entry)
1376
+ : null;
1377
+ }
1378
+ catch {
1379
+ return null;
1289
1380
  }
1290
1381
  }
1291
1382
  /**
@@ -1404,7 +1495,7 @@ function attemptRequiredUpdate(decision, deps, install) {
1404
1495
  let primaryArgs;
1405
1496
  if (install.manager === "pnpm") {
1406
1497
  primaryCmd = "pnpm";
1407
- primaryArgs = buildPnpmInstallArgv();
1498
+ primaryArgs = buildPnpmInstallArgv(install);
1408
1499
  }
1409
1500
  else if (install.manager === "bun") {
1410
1501
  primaryCmd = "bun";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.32",
3
+ "version": "5.345.33",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -47,6 +47,7 @@
47
47
  "dependencies": {
48
48
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
49
49
  "@aws-sdk/client-s3": "^3.1049.0",
50
+ "@aws-sdk/dsql-signer": "^3.1146.0",
50
51
  "@indigoai-us/hq-cloud": "~6.18.33",
51
52
  "@indigoai-us/hq-flags-client": "^0.1.2",
52
53
  "@modelcontextprotocol/sdk": "1.29.0",
@@ -59,6 +60,7 @@
59
60
  "js-yaml": "^4.1.0",
60
61
  "mqtt": "5.15.2",
61
62
  "open": "^10.1.0",
63
+ "pg": "^8.23.1",
62
64
  "semver": "^7.8.5",
63
65
  "simple-git": "^3.27.0",
64
66
  "smol-toml": "1.6.1",
@@ -69,6 +71,7 @@
69
71
  "@types/better-sqlite3": "^7.6.13",
70
72
  "@types/js-yaml": "^4.0.9",
71
73
  "@types/node": "^22.0.0",
74
+ "@types/pg": "^8.23.1",
72
75
  "@types/semver": "^7.8.0",
73
76
  "@vitest/coverage-v8": "4.1.6",
74
77
  "aedes": "1.1.2",