@indigoai-us/hq-cli 5.345.108 → 5.345.109

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.109] — 2026-10-13
6
+ - `hq` commands no longer fail with "The HQ CLI package is being updated" while
7
+ the desktop app updates the CLI. They wait for the update (up to 4 minutes,
8
+ `HQ_CLI_UPDATE_WAIT_MS` to tune) and, if the install changed underneath them,
9
+ re-run once from the new version.
10
+
5
11
  ## [5.345.108] — 2026-10-13
6
12
  - On macOS, `hq` reads your sign-in from the Keychain copy kept by the desktop app and uses whichever of the Keychain and `~/.hq/cognito-tokens.json` is newer (hq-cloud 6.18.67). macOS asks once for access; if you deny it, `hq` keeps using the file until you run `hq auth keychain --retry`.
7
13
 
package/dist/index.d.ts CHANGED
@@ -5,13 +5,21 @@ import { isVersionRequest } from "./version-request.js";
5
5
  import { isFastCoreRequest } from "./fast-scaffold.js";
6
6
  type PackageUseLeaseLoader = () => Promise<void>;
7
7
  type PackageUseUpdateProbe = () => Promise<boolean>;
8
+ /** Resolves true once the command was re-run (exit code already set). */
9
+ type PackageUseUpdateReexec = () => Promise<boolean>;
10
+ /**
11
+ * Wait (bounded) for a live updater to finish, then re-run this command from
12
+ * the new install via the same bin path. Guarded against loops by an env flag.
13
+ */
14
+ declare function waitForUpdateThenReexec(argv?: readonly string[], env?: NodeJS.ProcessEnv, waitForUpdate?: () => Promise<boolean>, run?: (command: string, args: string[], childEnv: NodeJS.ProcessEnv) => Promise<number>): Promise<boolean>;
8
15
  declare function isNpmPackageEntry(entryUrl: string): boolean;
9
- declare function dispatchWithPackageUseLease(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader, updateRequestPending?: PackageUseUpdateProbe): void | Promise<void>;
16
+ declare function dispatchWithPackageUseLease(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader, updateRequestPending?: PackageUseUpdateProbe, waitThenReexec?: PackageUseUpdateReexec): void | Promise<void>;
10
17
  declare function runCliEntry(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
11
18
  export declare const __test__: {
12
19
  isVersionRequest: typeof isVersionRequest;
13
20
  isFastCoreRequest: typeof isFastCoreRequest;
14
21
  dispatchWithPackageUseLease: typeof dispatchWithPackageUseLease;
22
+ waitForUpdateThenReexec: typeof waitForUpdateThenReexec;
15
23
  runCliEntry: typeof runCliEntry;
16
24
  isNpmPackageEntry: typeof isNpmPackageEntry;
17
25
  acquirePackageUseLease: (prefix: string, version: string, stateDirectory?: string) => Promise<import("./utils/package-use-lease.js").PackageUseLease>;
package/dist/index.js CHANGED
@@ -230,6 +230,36 @@ function dispatchCliEntry() {
230
230
  void runMainWithInstallTreeRecovery();
231
231
  }
232
232
  }
233
+ const REEXEC_ENV = "HQ_CLI_UPDATE_REEXEC";
234
+ /**
235
+ * Wait (bounded) for a live updater to finish, then re-run this command from
236
+ * the new install via the same bin path. Guarded against loops by an env flag.
237
+ */
238
+ async function waitForUpdateThenReexec(argv = process.argv, env = process.env, waitForUpdate = async () => {
239
+ const { waitForCurrentPackageUseUpdate } = await import("./utils/package-use-lease.js");
240
+ return waitForCurrentPackageUseUpdate();
241
+ }, run = async (command, args, childEnv) => {
242
+ const { spawn } = await import("node:child_process");
243
+ return await new Promise((resolve) => {
244
+ const child = spawn(command, args, { stdio: "inherit", env: childEnv });
245
+ child.on("error", () => resolve(1));
246
+ child.on("exit", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
247
+ });
248
+ }) {
249
+ if (env[REEXEC_ENV] === "1" || !argv[1])
250
+ return false;
251
+ let finished = false;
252
+ try {
253
+ finished = await waitForUpdate();
254
+ }
255
+ catch {
256
+ return false;
257
+ }
258
+ if (!finished)
259
+ return false;
260
+ process.exitCode = await run(process.execPath, argv.slice(1), { ...env, [REEXEC_ENV]: "1" });
261
+ return true;
262
+ }
233
263
  function isNpmPackageEntry(entryUrl) {
234
264
  return /\/node_modules\/@indigoai-us\/hq-cli\/dist\/index\.js$/i.test(entryUrl);
235
265
  }
@@ -260,7 +290,7 @@ function dispatchWithPackageUseLease(entryUrl, dispatch, loadLease = async () =>
260
290
  }, updateRequestPending = async () => {
261
291
  const { currentPackageUseUpdatePending } = await import("./utils/package-use-lease.js");
262
292
  return currentPackageUseUpdatePending();
263
- }) {
293
+ }, waitThenReexec = waitForUpdateThenReexec) {
264
294
  if (!isNpmPackageEntry(entryUrl)) {
265
295
  dispatch();
266
296
  return;
@@ -270,6 +300,12 @@ function dispatchWithPackageUseLease(entryUrl, dispatch, loadLease = async () =>
270
300
  throw error;
271
301
  try {
272
302
  if (await updateRequestPending()) {
303
+ // The install tree is being replaced under this process, so the lease
304
+ // module (or a dependency) failed to load. Wait for the updater, then
305
+ // run the command again from the freshly installed package instead of
306
+ // failing a running session's command outright.
307
+ if (await waitThenReexec())
308
+ return;
273
309
  const pending = new Error("The HQ CLI package is being updated; retry after the update finishes");
274
310
  pending.name = "PackageUseUpdatePendingError";
275
311
  throw pending;
@@ -311,6 +347,7 @@ export const __test__ = {
311
347
  isVersionRequest,
312
348
  isFastCoreRequest,
313
349
  dispatchWithPackageUseLease,
350
+ waitForUpdateThenReexec,
314
351
  runCliEntry,
315
352
  isNpmPackageEntry,
316
353
  acquirePackageUseLease: async (prefix, version, stateDirectory) => {
@@ -1,4 +1,12 @@
1
1
  export declare const PACKAGE_USE_LEASE_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_LEASE_HANDOFF";
2
+ /**
3
+ * How long a command waits for a live updater before giving up. A desktop
4
+ * `npm install -g` after a reboot routinely runs for minutes (seen ~3 min on
5
+ * 2026-10-13), so a 30s bound made ordinary commands fail mid-session.
6
+ * Override with HQ_CLI_UPDATE_WAIT_MS (0 disables waiting).
7
+ */
8
+ export declare const DEFAULT_UPDATE_WAIT_MS = 240000;
9
+ export declare function updateWaitTimeoutMs(env?: NodeJS.ProcessEnv): number;
2
10
  export declare const PACKAGE_USE_LEASE_PURPOSES: readonly ["command", "resident-preload", "mcp-serve", "hook", "daemon"];
3
11
  export type PackageUseLeasePurpose = (typeof PACKAGE_USE_LEASE_PURPOSES)[number];
4
12
  export declare class PackageUseUpdatePendingError extends Error {
@@ -17,7 +25,7 @@ export interface PackageUseLeaseHandoff {
17
25
  }
18
26
  export declare function npmPrefix(root: string, platform?: NodeJS.Platform): string | null;
19
27
  export declare function packageUseLeaseDirectory(prefix: string, stateDirectory?: string): string;
20
- declare function waitForUpdateRequest(requestPath: string, timeoutMs: number): void;
28
+ declare function waitForUpdateRequest(requestPath: string, timeoutMs: number, notify?: (() => void) | null): void;
21
29
  export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string, purpose?: PackageUseLeasePurpose): PackageUseLease;
22
30
  /**
23
31
  * Create a bounded, per-reexec rendezvous in the current lease directory.
@@ -42,6 +50,12 @@ export declare function releasePackageUseLeaseAfterCommandLoad(_argv?: readonly
42
50
  export declare function releasePackageUseLeaseAfterResidentPreload(preload: () => Promise<void>, reportFailure: (error: unknown) => void, release?: () => void): Promise<void>;
43
51
  /** True only when a valid update request belongs to a live updater process. */
44
52
  export declare function packageUseUpdatePending(prefix: string, stateDirectory?: string): boolean;
53
+ /**
54
+ * Block (bounded) until a live updater's request for this install clears.
55
+ * Returns true when no update is pending any more, false on timeout or when
56
+ * this is not a lease-managed install.
57
+ */
58
+ export declare function waitForCurrentPackageUseUpdate(timeoutMs?: number): boolean;
45
59
  /** True while the desktop updater owns the per-prefix update request. */
46
60
  export declare function currentPackageUseUpdatePending(): boolean;
47
61
  /** True while the desktop updater owns the per-prefix update request. */
@@ -9,6 +9,19 @@ const CLI_NAME = "@indigoai-us/hq-cli";
9
9
  const UPDATE_REQUEST = "update.pending.json";
10
10
  export const PACKAGE_USE_LEASE_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_LEASE_HANDOFF";
11
11
  const HANDOFF_TIMEOUT_MS = 2_000;
12
+ /**
13
+ * How long a command waits for a live updater before giving up. A desktop
14
+ * `npm install -g` after a reboot routinely runs for minutes (seen ~3 min on
15
+ * 2026-10-13), so a 30s bound made ordinary commands fail mid-session.
16
+ * Override with HQ_CLI_UPDATE_WAIT_MS (0 disables waiting).
17
+ */
18
+ export const DEFAULT_UPDATE_WAIT_MS = 240_000;
19
+ export function updateWaitTimeoutMs(env = process.env) {
20
+ const raw = env.HQ_CLI_UPDATE_WAIT_MS?.trim();
21
+ if (!raw || !/^\d+$/.test(raw))
22
+ return DEFAULT_UPDATE_WAIT_MS;
23
+ return Math.min(Number(raw), 30 * 60 * 1000);
24
+ }
12
25
  const PACKAGE_USE_LEASE_HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000;
13
26
  let packageUseLeaseHeartbeatIntervalMs = PACKAGE_USE_LEASE_HEARTBEAT_INTERVAL_MS;
14
27
  export const PACKAGE_USE_LEASE_PURPOSES = [
@@ -139,8 +152,9 @@ export function packageUseLeaseDirectory(prefix, stateDirectory = packageUseStat
139
152
  function packageUseUpdateRequestPath(prefix, stateDirectory) {
140
153
  return path.join(packageUseLeaseDirectory(prefix, stateDirectory), UPDATE_REQUEST);
141
154
  }
142
- function waitForUpdateRequest(requestPath, timeoutMs) {
155
+ function waitForUpdateRequest(requestPath, timeoutMs, notify = null) {
143
156
  const started = Date.now();
157
+ let notified = false;
144
158
  const sleeper = new Int32Array(new SharedArrayBuffer(4));
145
159
  while (true) {
146
160
  try {
@@ -167,9 +181,16 @@ function waitForUpdateRequest(requestPath, timeoutMs) {
167
181
  if (Date.now() - started >= timeoutMs) {
168
182
  throw new PackageUseUpdatePendingError("The HQ CLI package is being updated; retry after the update finishes");
169
183
  }
184
+ if (!notified && notify) {
185
+ notified = true;
186
+ notify();
187
+ }
170
188
  Atomics.wait(sleeper, 0, 0, 100);
171
189
  }
172
190
  }
191
+ function notifyWaitingForUpdate() {
192
+ process.stderr.write("hq-cli: waiting for the HQ CLI update to finish...\n");
193
+ }
173
194
  export function acquirePackageUseLease(prefix, hqVersion, stateDirectory, purpose = "command") {
174
195
  const directory = packageUseLeaseDirectory(prefix, stateDirectory);
175
196
  mkdirSync(directory, { recursive: true, mode: 0o700 });
@@ -219,7 +240,7 @@ export function acquirePackageUseLease(prefix, hqVersion, stateDirectory, purpos
219
240
  };
220
241
  const releaseOnExit = () => release();
221
242
  for (let attempt = 0; attempt < 2; attempt += 1) {
222
- waitForUpdateRequest(requestPath, 30_000);
243
+ waitForUpdateRequest(requestPath, updateWaitTimeoutMs(), notifyWaitingForUpdate);
223
244
  try {
224
245
  writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { flag: "wx", mode: 0o600 });
225
246
  renameSync(tempPath, leasePath);
@@ -407,6 +428,28 @@ export function packageUseUpdatePending(prefix, stateDirectory) {
407
428
  throw error;
408
429
  }
409
430
  }
431
+ /**
432
+ * Block (bounded) until a live updater's request for this install clears.
433
+ * Returns true when no update is pending any more, false on timeout or when
434
+ * this is not a lease-managed install.
435
+ */
436
+ export function waitForCurrentPackageUseUpdate(timeoutMs = updateWaitTimeoutMs()) {
437
+ const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
438
+ if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
439
+ return false;
440
+ const prefix = npmPrefix(pkg.root);
441
+ if (!prefix)
442
+ return false;
443
+ try {
444
+ waitForUpdateRequest(packageUseUpdateRequestPath(prefix), timeoutMs, notifyWaitingForUpdate);
445
+ return true;
446
+ }
447
+ catch (error) {
448
+ if (error instanceof PackageUseUpdatePendingError)
449
+ return false;
450
+ throw error;
451
+ }
452
+ }
410
453
  /** True while the desktop updater owns the per-prefix update request. */
411
454
  export function currentPackageUseUpdatePending() {
412
455
  const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.108",
3
+ "version": "5.345.109",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {