@indigoai-us/hq-cli 5.345.79 → 5.345.81

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 (38) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/command-catalog.generated.d.ts +142 -0
  3. package/dist/command-catalog.generated.js +173 -0
  4. package/dist/command-help.js +2 -0
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/storage-core.d.ts +98 -0
  8. package/dist/commands/storage-core.js +223 -0
  9. package/dist/commands/storage-gate.d.ts +35 -0
  10. package/dist/commands/storage-gate.js +85 -0
  11. package/dist/commands/storage-offload.d.ts +342 -0
  12. package/dist/commands/storage-offload.js +943 -0
  13. package/dist/commands/storage.d.ts +199 -0
  14. package/dist/commands/storage.js +931 -0
  15. package/dist/fast-scaffold.js +5 -0
  16. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +5140 -3412
  17. package/dist/hq-cloud-sync-runner/package.json +1 -1
  18. package/dist/lib/cli-flag-keys.d.ts +6 -0
  19. package/dist/lib/cli-flag-keys.js +6 -0
  20. package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
  21. package/dist/lib/doctor/checks/sync-health.js +19 -0
  22. package/dist/lib/doctor/fix/apply.d.ts +14 -10
  23. package/dist/lib/doctor/fix/apply.js +30 -16
  24. package/dist/lib/doctor/fix/remediation.d.ts +5 -3
  25. package/dist/lib/doctor/fix/remediation.js +21 -7
  26. package/dist/lib/doctor/types.d.ts +1 -1
  27. package/dist/lib/doctor/types.js +1 -0
  28. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  29. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  30. package/dist/lib/storage-feature-flag.d.ts +56 -0
  31. package/dist/lib/storage-feature-flag.js +104 -0
  32. package/dist/main.js +12 -1
  33. package/dist/sentry-before-send.js +1 -0
  34. package/dist/utils/resident-package-preload.js +3 -1
  35. package/dist/utils/self-update.js +2 -2
  36. package/dist/utils/version-gate.d.ts +15 -0
  37. package/dist/utils/version-gate.js +80 -10
  38. package/package.json +2 -2
@@ -0,0 +1,56 @@
1
+ /**
2
+ * HQ Storage allowlist gate (hq-flags key `hq-storage`).
3
+ *
4
+ * Owner decision (Corey, 2026-10-11): "let's fully ship the storage feature
5
+ * but all behind a feature flag for indigo users only." That is an explicit
6
+ * hold on the default-on kill-switch rule
7
+ * (`indigo-feature-flags-default-on-as-fail-safes`): the registry row is
8
+ * `defaultValue: false` with a company override for Indigo, so this gate is
9
+ * an ALLOWLIST, not a kill switch. It therefore FAILS CLOSED: a missing,
10
+ * stale, discovered, archived or unreadable value means Storage is off.
11
+ *
12
+ * Who it is on for is decided by hq-pro's shared evaluator (person override,
13
+ * then company override by uid, then slug, then default), resolved
14
+ * server-side for the caller. A company override only fires when the read
15
+ * names that company, so the caller is "on" when the flag resolves true for
16
+ * any company they are an active member of. hq-pro refuses a resolve for a
17
+ * company the caller is not a member of, so the tenant boundary is the
18
+ * membership check on the server, never a local list.
19
+ *
20
+ * This module is the synchronous, cache-only half used to hide the command
21
+ * from root help. It reads only fresh persisted snapshots and never touches
22
+ * the network. The authoritative, live check that runs before any storage
23
+ * subcommand lives in `commands/storage-gate.ts`.
24
+ */
25
+ import type { Command } from "commander";
26
+ import { HQ_STORAGE_FLAG } from "./cli-flag-keys.js";
27
+ export { HQ_STORAGE_FLAG };
28
+ /** Machine-readable refusal code the desktop app keys on. */
29
+ export declare const STORAGE_FEATURE_DISABLED_CODE = "feature_disabled";
30
+ export declare const STORAGE_FEATURE_DISABLED_MESSAGE = "Storage tools aren't available on your account yet.";
31
+ export interface StorageFlagCacheDeps {
32
+ env?: NodeJS.ProcessEnv;
33
+ /** Company uids whose persisted snapshots may be consulted. */
34
+ companyUids?: () => string[];
35
+ readCompanySnapshot?: (companyUid: string) => {
36
+ flags: Record<string, boolean>;
37
+ } | null;
38
+ readPersonSnapshot?: () => {
39
+ flags: Record<string, boolean>;
40
+ } | null;
41
+ }
42
+ /**
43
+ * True only when a fresh persisted snapshot already says `hq-storage` is on
44
+ * for this caller (person scope or one of their companies). Everything else,
45
+ * including any read error, is false.
46
+ */
47
+ export declare function storageFeatureEnabledFromCache(deps?: StorageFlagCacheDeps): boolean;
48
+ /** The JSON refusal printed on stdout for `--json` callers such as the desktop app. */
49
+ export declare function storageFeatureDisabledJson(): string;
50
+ /**
51
+ * Hide the catalog's `storage` root from root help unless the cached gate is
52
+ * on. Lives here, not in commands/storage-gate.ts, so the root-help path only
53
+ * loads this cache-only module.
54
+ */
55
+ export declare function hideStorageInCatalog(program: Command, enabled?: () => boolean): void;
56
+ //# sourceMappingURL=storage-feature-flag.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { readLocalOverride } from "@indigoai-us/hq-flags-client";
2
+ import { HQ_STORAGE_FLAG } from "./cli-flag-keys.js";
3
+ import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from "./flag-snapshot-file.js";
4
+ import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
5
+ import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
6
+ import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
7
+ export { HQ_STORAGE_FLAG };
8
+ /** Machine-readable refusal code the desktop app keys on. */
9
+ export const STORAGE_FEATURE_DISABLED_CODE = "feature_disabled";
10
+ export const STORAGE_FEATURE_DISABLED_MESSAGE = "Storage tools aren't available on your account yet.";
11
+ const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
12
+ function knownCompanyUids(env) {
13
+ const uids = new Set();
14
+ try {
15
+ const context = resolveProcessFlagCompanyContext(env);
16
+ if (context.companyUid)
17
+ uids.add(context.companyUid);
18
+ }
19
+ catch {
20
+ // No context: fall through to the manifest.
21
+ }
22
+ try {
23
+ const manifest = readCompaniesManifestMap(resolveDefaultHqRoot({ env }));
24
+ for (const entry of Object.values(manifest ?? {})) {
25
+ const uid = entry?.cloud_uid;
26
+ if (typeof uid === "string" && COMPANY_UID_RE.test(uid.trim()))
27
+ uids.add(uid.trim());
28
+ }
29
+ }
30
+ catch {
31
+ // An unreadable manifest only means fewer cached snapshots to check.
32
+ }
33
+ return [...uids];
34
+ }
35
+ /**
36
+ * True only when a fresh persisted snapshot already says `hq-storage` is on
37
+ * for this caller (person scope or one of their companies). Everything else,
38
+ * including any read error, is false.
39
+ */
40
+ export function storageFeatureEnabledFromCache(deps = {}) {
41
+ const env = deps.env ?? process.env;
42
+ try {
43
+ const local = readLocalOverride(HQ_STORAGE_FLAG, env);
44
+ if (local !== null)
45
+ return local;
46
+ }
47
+ catch {
48
+ // Treat a broken override like no override.
49
+ }
50
+ const on = (snapshot) => snapshot?.flags?.[HQ_STORAGE_FLAG] === true;
51
+ try {
52
+ if (on((deps.readPersonSnapshot ?? (() => readPersonFlagSnapshotFile({ env })))()))
53
+ return true;
54
+ }
55
+ catch {
56
+ // fail closed
57
+ }
58
+ const readCompany = deps.readCompanySnapshot ?? ((uid) => readFlagSnapshotFile(uid, { env }));
59
+ let uids = [];
60
+ try {
61
+ uids = (deps.companyUids ?? (() => knownCompanyUids(env)))();
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ for (const uid of uids) {
67
+ try {
68
+ if (on(readCompany(uid)))
69
+ return true;
70
+ }
71
+ catch {
72
+ // fail closed for this company
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+ /** The JSON refusal printed on stdout for `--json` callers such as the desktop app. */
78
+ export function storageFeatureDisabledJson() {
79
+ return JSON.stringify({
80
+ ok: false,
81
+ code: STORAGE_FEATURE_DISABLED_CODE,
82
+ message: STORAGE_FEATURE_DISABLED_MESSAGE,
83
+ });
84
+ }
85
+ /**
86
+ * Hide the catalog's `storage` root from root help unless the cached gate is
87
+ * on. Lives here, not in commands/storage-gate.ts, so the root-help path only
88
+ * loads this cache-only module.
89
+ */
90
+ export function hideStorageInCatalog(program, enabled = storageFeatureEnabledFromCache) {
91
+ const storage = program.commands.find((command) => command.name() === "storage");
92
+ if (!storage)
93
+ return;
94
+ let visible = false;
95
+ try {
96
+ visible = enabled();
97
+ }
98
+ catch {
99
+ visible = false;
100
+ }
101
+ if (!visible)
102
+ storage._hidden = true;
103
+ }
104
+ //# sourceMappingURL=storage-feature-flag.js.map
package/dist/main.js CHANGED
@@ -273,11 +273,22 @@ export async function runCli() {
273
273
  const registration = await registerCommandsWithRecovery({
274
274
  register: async () => {
275
275
  const lazy = findLazyCommand(process.argv);
276
- if (lazy) {
276
+ if (lazy?.name === "storage") {
277
+ // HQ Storage is an allowlist gate (owner hold 2026-10-11, Indigo-only):
278
+ // resolve it here, where awaiting is free, and fail closed.
279
+ const gate = await import("./commands/storage-gate.js");
280
+ if (await gate.storageFeatureEnabled())
281
+ await lazy.register(program);
282
+ else
283
+ gate.registerStorageRefusal(program, process.argv);
284
+ }
285
+ else if (lazy) {
277
286
  await lazy.register(program);
278
287
  }
279
288
  else {
280
289
  registerCommandCatalog(program);
290
+ const { hideStorageInCatalog } = await import("./lib/storage-feature-flag.js");
291
+ hideStorageInCatalog(program);
281
292
  }
282
293
  },
283
294
  argv: process.argv,
@@ -192,6 +192,7 @@ export function beforeSend(event, _hint) {
192
192
  "core-version-unavailable",
193
193
  "core-update-available",
194
194
  "no-local-tree",
195
+ "legacy-personal-journal",
195
196
  "never-synced",
196
197
  "invalid-last-sync",
197
198
  "stale-threshold",
@@ -41,7 +41,7 @@ export const RESIDENT_PACKAGE_MODULES = {
41
41
  ],
42
42
  "bot-logs-follow": [],
43
43
  mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../lib/anywhere-person-setting.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
44
- childWrapper: ["../main.js", "../lib/service-manager/startup-migration.js"],
44
+ childWrapper: ["../main.js", "../lib/service-manager/startup-migration.js", "../commands/storage-gate.js", "../lib/storage-feature-flag.js"],
45
45
  coreJobRun: ["../commands/core-native-jobs-flag.js", "../commands/core-native-family-flag.js", "../lib/flag-registry.js", "../lib/flag-snapshot-file.js"],
46
46
  "daemon-call": ["../lib/daemon/protocol.js"],
47
47
  daemon: [
@@ -201,6 +201,8 @@ const LOADERS = {
201
201
  childWrapper: [
202
202
  () => import("../main.js"),
203
203
  () => import("../lib/service-manager/startup-migration.js"),
204
+ () => import("../commands/storage-gate.js"),
205
+ () => import("../lib/storage-feature-flag.js"),
204
206
  ],
205
207
  coreJobRun: [
206
208
  () => import("../commands/core-native-jobs-flag.js"),
@@ -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, resolvePnpmPathInstall, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
68
+ import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, normalizeRunningInstall, nonWritablePrefixNote, openInstallOutput, resolvePnpmPathInstall, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, 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";
@@ -926,7 +926,7 @@ async function attemptUpdateAndReexec(argv, flavor, known, deps, env) {
926
926
  // actually running would stay exactly as stale as it started while the CLI
927
927
  // reported success and re-exec'd into the same build. That is the shape of
928
928
  // the 2026-09-02 gate loop — see isLocalDependencyInstall in version-gate.ts.
929
- let install = (deps.resolveInstall ?? resolveRunningInstall)();
929
+ let install = normalizeRunningInstall((deps.resolveInstall ?? resolveRunningInstall)());
930
930
  if (isLocalDependencyInstall(install)) {
931
931
  console.error(chalk.dim(`hq-cli ${latest} is available, but this copy is a local dependency (${install.packageRoot}) — update the project that owns it.`));
932
932
  return { action: "skipped", latest };
@@ -31,6 +31,9 @@ import { buildSpawnPlan, quoteForWindowsShell } from "./windows-spawn.js";
31
31
  import { type UpdateLockHandle } from "./update-lock.js";
32
32
  /** Prevent an update package manager's lifecycle child from starting another updater. */
33
33
  export declare const UPDATE_RECURSION_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
34
+ /** First-party hq-cli is published by this org; the 24h pnpm quarantine must not
35
+ * block the version-gate from installing the server-required build. */
36
+ export declare const PNPM_FIRST_PARTY_RELEASE_AGE_EXCLUDE: string;
34
37
  export type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "verifying" | "rollback" | "complete" | "failed" | "unknown";
35
38
  /** The last closed version-gate phase observed in this process. */
36
39
  export declare function getVersionGatePhase(): VersionGatePhase;
@@ -86,6 +89,8 @@ export declare function npmPrefixFromPackageDir(pkgDir: string): string | null;
86
89
  * Those layouts fall through to the ordinary npm-prefix/manual handling.
87
90
  */
88
91
  export declare function isPnpmManagedPackageDir(pkgDir: string): boolean;
92
+ /** pnpm's content-addressed `store/vN/links/…` layout (not `store/vN/tmp` dlx). */
93
+ export declare function isPnpmContentStoreLinksPackageDir(pkgDir: string): boolean;
89
94
  /** Whether the running package lives inside Bun's global install tree. */
90
95
  export declare function isBunManagedPackageDir(pkgDir: string): boolean;
91
96
  /**
@@ -164,6 +169,13 @@ export interface RunningInstall {
164
169
  packageRoot: string | null;
165
170
  }
166
171
  export declare function resolveRunningInstall(): RunningInstall;
172
+ /**
173
+ * Repair a RunningInstall whose package root is a pnpm content store but whose
174
+ * manager was recorded as npm (the pre-fix classification of a realpath'd
175
+ * pnpm-global shim). Callers that inject an install snapshot must run this
176
+ * before the local-dependency hard stop, or they replay the 2420 lockout.
177
+ */
178
+ export declare function normalizeRunningInstall(install: RunningInstall): RunningInstall;
167
179
  /** Convenience view of {@link resolveRunningInstall} for callers needing one field. */
168
180
  export declare function resolveRunningManager(): InstallManager;
169
181
  /** Convenience view of {@link resolveRunningInstall} for callers needing one field. */
@@ -564,8 +576,11 @@ export declare const __test__: {
564
576
  isLocalDependencyInstall: typeof isLocalDependencyInstall;
565
577
  isLocalDependencyPackageDir: typeof isLocalDependencyPackageDir;
566
578
  isNewerVersion: typeof isNewerVersion;
579
+ isPnpmContentStoreLinksPackageDir: typeof isPnpmContentStoreLinksPackageDir;
567
580
  isPnpmManagedPackageDir: typeof isPnpmManagedPackageDir;
568
581
  isPnpmVirtualStorePackageDir: typeof isPnpmVirtualStorePackageDir;
582
+ normalizeRunningInstall: typeof normalizeRunningInstall;
583
+ PNPM_FIRST_PARTY_RELEASE_AGE_EXCLUDE: string;
569
584
  isPrefixWritable: typeof isPrefixWritable;
570
585
  nonWritablePrefixNote: typeof nonWritablePrefixNote;
571
586
  npmPrefixFromPackageDir: typeof npmPrefixFromPackageDir;
@@ -49,6 +49,9 @@ const CLIENT_ID = "hq-cli";
49
49
  const ENDPOINT_PATH = "/v1/client-version/check";
50
50
  const FETCH_TIMEOUT_MS = 3_000;
51
51
  const LATEST_PACKAGE_SPEC = `${CLI_NAME}@latest`;
52
+ /** First-party hq-cli is published by this org; the 24h pnpm quarantine must not
53
+ * block the version-gate from installing the server-required build. */
54
+ export const PNPM_FIRST_PARTY_RELEASE_AGE_EXCLUDE = `--config.minimumReleaseAgeExclude=${CLI_NAME}`;
52
55
  let currentVersionGatePhase = "unknown";
53
56
  /** The last closed version-gate phase observed in this process. */
54
57
  export function getVersionGatePhase() {
@@ -294,6 +297,29 @@ export function isPnpmManagedPackageDir(pkgDir) {
294
297
  return true;
295
298
  }
296
299
  }
300
+ // Node realpaths pnpm's generated shim into the content-addressed store:
301
+ // $PNPM_HOME/store/v11/links/@indigoai-us/hq-cli/<ver>/<hash>/node_modules/@indigoai-us/hq-cli
302
+ // There is no `global/` segment after realpath, so the checks above miss it
303
+ // and the gate used to label this a "local dependency" (feedback 2420).
304
+ // `store/vN/tmp/` is pnpm dlx and must stay non-global.
305
+ if (isPnpmContentStoreLinksPackageDir(normalized))
306
+ return true;
307
+ return false;
308
+ }
309
+ /** pnpm's content-addressed `store/vN/links/…` layout (not `store/vN/tmp` dlx). */
310
+ export function isPnpmContentStoreLinksPackageDir(pkgDir) {
311
+ const normalized = pkgDir.replace(/\\/g, "/").replace(/\/+$/, "");
312
+ const segments = normalized.split("/").filter(Boolean);
313
+ for (let i = 0; i < segments.length - 2; i += 1) {
314
+ if (segments[i] !== "store")
315
+ continue;
316
+ if (!/^v\d+$/.test(segments[i + 1]))
317
+ continue;
318
+ if (segments[i + 2] === "tmp")
319
+ return false;
320
+ if (segments[i + 2] === "links")
321
+ return true;
322
+ }
297
323
  return false;
298
324
  }
299
325
  /** Whether the running package lives inside Bun's global install tree. */
@@ -449,6 +475,21 @@ export function resolveRunningInstall() {
449
475
  return { manager: "npm", prefix: null, packageRoot: null };
450
476
  }
451
477
  }
478
+ /**
479
+ * Repair a RunningInstall whose package root is a pnpm content store but whose
480
+ * manager was recorded as npm (the pre-fix classification of a realpath'd
481
+ * pnpm-global shim). Callers that inject an install snapshot must run this
482
+ * before the local-dependency hard stop, or they replay the 2420 lockout.
483
+ */
484
+ export function normalizeRunningInstall(install) {
485
+ if (!install.packageRoot)
486
+ return install;
487
+ if (install.manager === "pnpm")
488
+ return install;
489
+ if (!isPnpmManagedPackageDir(install.packageRoot))
490
+ return install;
491
+ return { manager: "pnpm", prefix: null, packageRoot: install.packageRoot };
492
+ }
452
493
  /** Convenience view of {@link resolveRunningInstall} for callers needing one field. */
453
494
  export function resolveRunningManager() {
454
495
  return resolveRunningInstall().manager;
@@ -652,11 +693,15 @@ export function derivePnpmHome(install) {
652
693
  if (install.manager !== "pnpm" || !install.packageRoot)
653
694
  return null;
654
695
  const normalized = install.packageRoot.replace(/\\/g, "/");
655
- const marker = normalized.indexOf("/global/");
656
- if (marker === -1)
657
- return null;
658
- const home = normalized.slice(0, marker);
659
- return home || null;
696
+ const globalMarker = normalized.indexOf("/global/");
697
+ if (globalMarker !== -1) {
698
+ const home = normalized.slice(0, globalMarker);
699
+ return home || null;
700
+ }
701
+ const storeMatch = normalized.match(/^(.*)\/store\/v\d+\/links\//);
702
+ if (storeMatch)
703
+ return storeMatch[1] || null;
704
+ return null;
660
705
  }
661
706
  /**
662
707
  * Environment for the pnpm self-update spawn. pnpm requires its configured
@@ -713,7 +758,7 @@ export function buildPrefixedInstallArgv(prefix) {
713
758
  * the new version — which is the whole point of routing here instead of npm.
714
759
  */
715
760
  export function buildPnpmInstallArgv(install) {
716
- const args = ["add", "-g", LATEST_PACKAGE_SPEC];
761
+ const args = ["add", "-g", LATEST_PACKAGE_SPEC, PNPM_FIRST_PARTY_RELEASE_AGE_EXCLUDE];
717
762
  const dirs = install ? pnpmInstallDirs(install) : null;
718
763
  if (dirs) {
719
764
  args.push("--global-dir", dirs.globalDir, "--store-dir", dirs.storeDir);
@@ -732,8 +777,18 @@ export function pnpmInstallDirs(install) {
732
777
  const normalized = install.packageRoot.replace(/\\/g, "/");
733
778
  const global = normalized.match(/^(.*\/global\/\d+\/node_modules)(?:\/|$)|^(.*\/global\/v\d+\/[^/]+\/node_modules)(?:\/|$)/);
734
779
  const globalNodeModules = global?.[1] ?? global?.[2];
735
- if (!globalNodeModules)
736
- return null;
780
+ if (!globalNodeModules) {
781
+ const storeMatch = normalized.match(/^(.*)\/store\/(v\d+)\/links\//);
782
+ if (!storeMatch)
783
+ return null;
784
+ const pnpmHome = storeMatch[1];
785
+ const storeVersion = storeMatch[2];
786
+ return {
787
+ globalDir: path.join(pnpmHome, "global"),
788
+ storeDir: path.join(pnpmHome, "store", storeVersion),
789
+ virtualStoreDir: path.join(pnpmHome, "global", storeVersion, "node_modules", ".pnpm"),
790
+ };
791
+ }
737
792
  const metadataPath = path.join(globalNodeModules, ".modules.yaml");
738
793
  try {
739
794
  const metadata = yaml.load(readFileSync(metadataPath, "utf8"));
@@ -966,6 +1021,10 @@ async function fetchVersionDecision(timeoutMs = FETCH_TIMEOUT_MS) {
966
1021
  return null;
967
1022
  }
968
1023
  }
1024
+ function isReleaseAgeInstallFailure(result) {
1025
+ const text = `${result.detail ?? ""}\n${result.stdout ?? ""}`;
1026
+ return /minimumReleaseAge|minimum-release-age|published less than/i.test(text);
1027
+ }
969
1028
  // The Windows spawn recipe (`buildSpawnPlan` + `quoteForWindowsShell`) lives in
970
1029
  // ./windows-spawn.ts so the self-update path and the qmd spawn path share one
971
1030
  // implementation. Imported at the top and re-exported here to keep this module's
@@ -1353,6 +1412,7 @@ function manualUpdateCommand(install, decision) {
1353
1412
  * remains for callers that pass no handler.
1354
1413
  */
1355
1414
  function nudgeUpdateRecommended(decision, install = resolveRunningInstall()) {
1415
+ install = normalizeRunningInstall(install);
1356
1416
  if (!isNewerVersion(decision.latestVersion, CLI_VERSION))
1357
1417
  return;
1358
1418
  const msg = chalk.yellow(`⚠ A new version of hq-cli is available: ${decision.latestVersion} (current: ${CLI_VERSION}).`);
@@ -1663,7 +1723,7 @@ function enforceUpdateRequired(decision, deps = {}) {
1663
1723
  // server's `updateCommand` and any npm prefix are wrong for those layouts, so
1664
1724
  // `resolveRunningInstall`
1665
1725
  // already reports `prefix: null` there and neither is consulted below.
1666
- const install = (deps.resolveInstall ?? resolveRunningInstall)();
1726
+ const install = normalizeRunningInstall((deps.resolveInstall ?? resolveRunningInstall)());
1667
1727
  // Loop protection #1 — nothing here can update a copy this process does not
1668
1728
  // own. Installing globally would leave the local copy that is actually
1669
1729
  // running stale, so the gate would fire again on the very next invocation.
@@ -1886,6 +1946,13 @@ function attemptRequiredUpdate(decision, deps, install) {
1886
1946
  console.error(chalk.dim(" Continuing on the current hq-cli version."));
1887
1947
  return "continue";
1888
1948
  }
1949
+ // A 24h pnpm quarantine that still blocks first-party hq-cli must not
1950
+ // lock the user out of secrets (or any other command) for the rest of
1951
+ // that window. Warn and run the current CLI — same as a timed-out updater.
1952
+ if (isReleaseAgeInstallFailure(result)) {
1953
+ console.error(chalk.dim(" The package manager refused a build newer than its 24h install guard; continuing on the current hq-cli version."));
1954
+ return "continue";
1955
+ }
1889
1956
  return noticeFlush ? noticeFlush.then(() => 75) : 75;
1890
1957
  }
1891
1958
  // Read-your-writes: a "successful" install into the npm prefix does not
@@ -1914,7 +1981,7 @@ export async function enforceVersionGate(onUpdateRecommended, options = {}) {
1914
1981
  return "continue";
1915
1982
  // Resolved once, here, so the up-to-date case never pays for the walk and
1916
1983
  // neither downstream path repeats it.
1917
- const install = resolveRunningInstall();
1984
+ const install = normalizeRunningInstall(resolveRunningInstall());
1918
1985
  if (decision.updateRequired) {
1919
1986
  // Load the staged installer only when an npm prefix is resolved. The version
1920
1987
  // gate is imported during every CLI startup, so keep the filesystem-heavy
@@ -1985,8 +2052,11 @@ export const __test__ = {
1985
2052
  isLocalDependencyInstall,
1986
2053
  isLocalDependencyPackageDir,
1987
2054
  isNewerVersion,
2055
+ isPnpmContentStoreLinksPackageDir,
1988
2056
  isPnpmManagedPackageDir,
1989
2057
  isPnpmVirtualStorePackageDir,
2058
+ normalizeRunningInstall,
2059
+ PNPM_FIRST_PARTY_RELEASE_AGE_EXCLUDE,
1990
2060
  isPrefixWritable,
1991
2061
  nonWritablePrefixNote,
1992
2062
  npmPrefixFromPackageDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.79",
3
+ "version": "5.345.81",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
49
49
  "@aws-sdk/client-s3": "^3.1049.0",
50
50
  "@aws-sdk/dsql-signer": "^3.1146.0",
51
- "@indigoai-us/hq-cloud": "~6.18.54",
51
+ "@indigoai-us/hq-cloud": "~6.18.61",
52
52
  "@indigoai-us/hq-flags-client": "^0.1.2",
53
53
  "@modelcontextprotocol/sdk": "1.29.0",
54
54
  "@sentry/node": "^10.49.0",