@indigoai-us/hq-cli 5.115.6 → 5.117.0

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 (76) hide show
  1. package/CHANGELOG.md +169 -11
  2. package/dist/command-catalog.generated.d.ts +220 -2
  3. package/dist/command-catalog.generated.js +281 -2
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +105 -0
  7. package/dist/commands/agent-enroll.js +273 -0
  8. package/dist/commands/agent-kit.d.ts +53 -0
  9. package/dist/commands/agent-kit.js +260 -0
  10. package/dist/commands/agent-mcp.d.ts +22 -0
  11. package/dist/commands/agent-mcp.js +104 -0
  12. package/dist/commands/agent-probe.d.ts +71 -0
  13. package/dist/commands/agent-probe.js +294 -0
  14. package/dist/commands/agent.d.ts +12 -0
  15. package/dist/commands/agent.js +23 -0
  16. package/dist/commands/agents.d.ts +27 -0
  17. package/dist/commands/agents.js +280 -6
  18. package/dist/commands/bot.d.ts +140 -1
  19. package/dist/commands/bot.js +757 -22
  20. package/dist/commands/secrets.js +17 -5
  21. package/dist/lib/agent-kit/creds.d.ts +60 -0
  22. package/dist/lib/agent-kit/creds.js +123 -0
  23. package/dist/lib/agent-kit/kit-config.d.ts +29 -0
  24. package/dist/lib/agent-kit/kit-config.js +54 -0
  25. package/dist/lib/agent-kit/log.d.ts +17 -0
  26. package/dist/lib/agent-kit/log.js +46 -0
  27. package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +84 -0
  28. package/dist/lib/agent-kit/mcp/jsonrpc.js +164 -0
  29. package/dist/lib/agent-kit/mcp/tools.d.ts +45 -0
  30. package/dist/lib/agent-kit/mcp/tools.js +280 -0
  31. package/dist/lib/agent-kit/paths.d.ts +42 -0
  32. package/dist/lib/agent-kit/paths.js +56 -0
  33. package/dist/lib/agent-kit/run/heartbeat.d.ts +52 -0
  34. package/dist/lib/agent-kit/run/heartbeat.js +97 -0
  35. package/dist/lib/agent-kit/run/inbox.d.ts +59 -0
  36. package/dist/lib/agent-kit/run/inbox.js +152 -0
  37. package/dist/lib/agent-kit/run/mesh-listener.d.ts +58 -0
  38. package/dist/lib/agent-kit/run/mesh-listener.js +193 -0
  39. package/dist/lib/agent-kit/run/sync.d.ts +33 -0
  40. package/dist/lib/agent-kit/run/sync.js +58 -0
  41. package/dist/lib/agent-kit/services.d.ts +21 -0
  42. package/dist/lib/agent-kit/services.js +46 -0
  43. package/dist/lib/agent-kit/skills.d.ts +18 -0
  44. package/dist/lib/agent-kit/skills.js +149 -0
  45. package/dist/lib/bot/api.d.ts +51 -0
  46. package/dist/lib/bot/api.js +32 -0
  47. package/dist/lib/bot/daemon.d.ts +17 -0
  48. package/dist/lib/bot/daemon.js +44 -3
  49. package/dist/lib/bot/index.d.ts +4 -0
  50. package/dist/lib/bot/index.js +4 -0
  51. package/dist/lib/bot/inflight.d.ts +14 -0
  52. package/dist/lib/bot/local-config.d.ts +70 -0
  53. package/dist/lib/bot/local-config.js +147 -0
  54. package/dist/lib/bot/local-name.d.ts +54 -0
  55. package/dist/lib/bot/local-name.js +114 -0
  56. package/dist/lib/bot/run.d.ts +9 -0
  57. package/dist/lib/bot/run.js +117 -24
  58. package/dist/lib/bot/runnable.d.ts +51 -0
  59. package/dist/lib/bot/runnable.js +65 -0
  60. package/dist/lib/bot/self-heal.d.ts +52 -0
  61. package/dist/lib/bot/self-heal.js +79 -0
  62. package/dist/lib/bot/split.d.ts +32 -0
  63. package/dist/lib/bot/split.js +241 -0
  64. package/dist/lib/service-manager/index.d.ts +43 -0
  65. package/dist/lib/service-manager/index.js +114 -0
  66. package/dist/lib/service-manager/launchd.d.ts +23 -0
  67. package/dist/lib/service-manager/launchd.js +81 -0
  68. package/dist/lib/service-manager/systemd.d.ts +19 -0
  69. package/dist/lib/service-manager/systemd.js +72 -0
  70. package/dist/lib/service-manager/types.d.ts +32 -0
  71. package/dist/lib/service-manager/types.js +26 -0
  72. package/dist/utils/self-update.js +2 -30
  73. package/dist/utils/update-command-supervisor.cjs +194 -0
  74. package/dist/utils/version-gate.d.ts +18 -0
  75. package/dist/utils/version-gate.js +126 -7
  76. package/package.json +2 -2
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Linux systemd USER unit rendering + activation for kit services.
3
+ *
4
+ * Units live at ~/.config/systemd/user/<name>.service and are enabled with
5
+ * `systemctl --user enable --now`. A host without a user session bus (some
6
+ * containers, headless boxes) cannot run user units; the kit's Docker image
7
+ * is the escape hatch there. Command execution is injected for tests.
8
+ */
9
+ import * as path from "node:path";
10
+ import { shellQuote, } from "./types.js";
11
+ export const SYSTEMD_PATH_ENV = "/usr/local/bin:/usr/bin:/bin";
12
+ export function systemdUserDir(home) {
13
+ return path.join(home, ".config", "systemd", "user");
14
+ }
15
+ export function systemdUnitName(spec) {
16
+ return `hq-agent-${spec.name}.service`;
17
+ }
18
+ export function systemdUnitPath(home, spec) {
19
+ return path.join(systemdUserDir(home), systemdUnitName(spec));
20
+ }
21
+ export function renderSystemdUserUnit(spec, host) {
22
+ const exec = [host.nodeBinary, host.hqBinary, ...spec.args].map(shellQuote).join(" ");
23
+ const env = {
24
+ HOME: host.home,
25
+ PATH: SYSTEMD_PATH_ENV,
26
+ ...spec.env,
27
+ };
28
+ const envLines = Object.entries(env)
29
+ .sort(([a], [b]) => a.localeCompare(b))
30
+ .map(([k, v]) => `Environment=${shellQuote(`${k}=${v}`)}`)
31
+ .join("\n");
32
+ return `[Unit]
33
+ Description=${spec.description}
34
+ After=network-online.target
35
+ Wants=network-online.target
36
+
37
+ [Service]
38
+ Type=simple
39
+ ExecStart=${exec}
40
+ WorkingDirectory=${spec.workingDir}
41
+ Restart=always
42
+ RestartSec=${spec.restartSec}
43
+ ${envLines}
44
+ StandardOutput=append:${spec.logPath}
45
+ StandardError=append:${spec.logPath}
46
+
47
+ [Install]
48
+ WantedBy=default.target
49
+ `;
50
+ }
51
+ export function activateSystemd(unitName, run) {
52
+ const reload = run("systemctl", ["--user", "daemon-reload"]);
53
+ if (reload.status !== 0) {
54
+ throw new Error(`systemctl --user daemon-reload failed: ${reload.stderr.trim() || `exit ${reload.status}`}. ` +
55
+ `No user session bus? Use the hq-agent-kit Docker image instead.`);
56
+ }
57
+ const r = run("systemctl", ["--user", "enable", "--now", unitName]);
58
+ if (r.status !== 0) {
59
+ throw new Error(`systemctl --user enable --now ${unitName} failed: ${r.stderr.trim() || `exit ${r.status}`}`);
60
+ }
61
+ // A re-install with a changed unit must restart the running instance.
62
+ run("systemctl", ["--user", "restart", unitName]);
63
+ }
64
+ export function deactivateSystemd(unitName, run) {
65
+ run("systemctl", ["--user", "disable", "--now", unitName]);
66
+ run("systemctl", ["--user", "daemon-reload"]);
67
+ }
68
+ export function systemdIsActive(unitName, run) {
69
+ const r = run("systemctl", ["--user", "is-active", "--quiet", unitName]);
70
+ return r.status === 0;
71
+ }
72
+ //# sourceMappingURL=systemd.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Platform-neutral description of a user-level background service. The
3
+ * launchd and systemd renderers turn one of these into a unit; the kit
4
+ * installer owns the list. Every service is just `node hq <args…>` so the
5
+ * unit files carry no logic of their own.
6
+ */
7
+ export interface ServiceSpec {
8
+ /** Short name — unit file stem and log file stem (`sync`, `mesh`, …). */
9
+ name: string;
10
+ /** Reverse-DNS launchd label; systemd derives its unit name from `name`. */
11
+ label: string;
12
+ description: string;
13
+ /** Arguments passed to the hq binary (after `node hq`). */
14
+ args: string[];
15
+ /** Absolute log path (stdout + stderr appended). */
16
+ logPath: string;
17
+ workingDir: string;
18
+ /** Extra environment for the unit (HOME/PATH are always set). */
19
+ env: Record<string, string>;
20
+ /** Seconds to wait before a restart after exit. */
21
+ restartSec: number;
22
+ }
23
+ export interface ServiceHostPaths {
24
+ home: string;
25
+ nodeBinary: string;
26
+ hqBinary: string;
27
+ }
28
+ export type ServicePlatform = "darwin" | "linux" | "other";
29
+ export declare function detectServicePlatform(platform?: NodeJS.Platform): ServicePlatform;
30
+ export declare function escapeXml(value: string): string;
31
+ export declare function shellQuote(value: string): string;
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Platform-neutral description of a user-level background service. The
3
+ * launchd and systemd renderers turn one of these into a unit; the kit
4
+ * installer owns the list. Every service is just `node hq <args…>` so the
5
+ * unit files carry no logic of their own.
6
+ */
7
+ export function detectServicePlatform(platform = process.platform) {
8
+ if (platform === "darwin")
9
+ return "darwin";
10
+ if (platform === "linux")
11
+ return "linux";
12
+ return "other";
13
+ }
14
+ export function escapeXml(value) {
15
+ return value
16
+ .replace(/&/g, "&amp;")
17
+ .replace(/</g, "&lt;")
18
+ .replace(/>/g, "&gt;")
19
+ .replace(/"/g, "&quot;");
20
+ }
21
+ export function shellQuote(value) {
22
+ if (/^[A-Za-z0-9_./:=-]+$/.test(value))
23
+ return value;
24
+ return `'${value.replace(/'/g, `'\\''`)}'`;
25
+ }
26
+ //# sourceMappingURL=types.js.map
@@ -60,7 +60,7 @@ import { spawnSync } from "node:child_process";
60
60
  import semver from "semver";
61
61
  import chalk from "chalk";
62
62
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
63
- import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, checkUpdateConvergence, inOwnProcessGroup, isLocalDependencyInstall, isPrefixWritable, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, } from "./version-gate.js";
63
+ import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, checkUpdateConvergence, isLocalDependencyInstall, isPrefixWritable, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, } from "./version-gate.js";
64
64
  import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
65
65
  import { markLatestIneffective } from "./version-check.js";
66
66
  /**
@@ -70,8 +70,6 @@ import { markLatestIneffective } from "./version-check.js";
70
70
  export const REEXEC_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
71
71
  const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(CLI_NAME)}/latest`;
72
72
  const FETCH_TIMEOUT_MS = 3_000;
73
- /** Tail of captured package-manager stderr kept for the failure warning. */
74
- const DETAIL_MAX_CHARS = 400;
75
73
  /** npm `latest` for this package, or null on any failure (offline, 5xx, bad body). */
76
74
  async function fetchLatestVersion() {
77
75
  try {
@@ -115,33 +113,7 @@ export function runUpdateQuiet(cmd, args, env) {
115
113
  // the installer down with it mid-write. See `openInstallOutput`.
116
114
  const output = openInstallOutput(false);
117
115
  try {
118
- const plan = buildSpawnPlan(cmd, args);
119
- // `inOwnProcessGroup`: the install must survive a signal aimed at this
120
- // process, or a killed `hq` leaves a corrupted global install behind.
121
- const result = spawnSync(plan.cmd, plan.args, inOwnProcessGroup({
122
- stdio: output.stdio,
123
- shell: plan.shell,
124
- ...(env ? { env } : {}),
125
- }));
126
- if (result.error) {
127
- const code = result.error.code;
128
- return { ok: false, code, detail: result.error.message };
129
- }
130
- if (result.status !== 0) {
131
- const tail = output.stderrTail().slice(-DETAIL_MAX_CHARS);
132
- return {
133
- ok: false,
134
- detail: tail || `exit ${result.status ?? "signal"}`,
135
- };
136
- }
137
- return { ok: true };
138
- }
139
- catch (err) {
140
- return {
141
- ok: false,
142
- code: err?.code,
143
- detail: err instanceof Error ? err.message : String(err),
144
- };
116
+ return runSupervisedUpdateCommand(cmd, args, output, env);
145
117
  }
146
118
  finally {
147
119
  output.dispose();
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+
3
+ const { spawn: nativeSpawn } = require("node:child_process");
4
+ const fs = require("node:fs");
5
+
6
+ function windowsTaskkillArgs(pid) {
7
+ return ["/PID", String(pid), "/T", "/F"];
8
+ }
9
+
10
+ function errorInfo(error) {
11
+ return {
12
+ code: error && typeof error.code === "string" ? error.code : undefined,
13
+ detail:
14
+ error && typeof error.message === "string" ? error.message : String(error ?? "unknown error"),
15
+ };
16
+ }
17
+
18
+ function writeMarker(filename, contents, log) {
19
+ try {
20
+ fs.writeFileSync(filename, contents);
21
+ } catch (error) {
22
+ const { detail } = errorInfo(error);
23
+ log(`[hq update supervisor] failed to record updater status: ${detail}`);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Spawn and supervise one package-manager update. On POSIX the detached child
29
+ * is its own process-group leader, so signals reach the leader and every
30
+ * descendant. Windows command shims run below a shell, therefore taskkill is
31
+ * used there to terminate the full descendant tree instead of only that shell.
32
+ *
33
+ * Dependencies are injectable to pin failure handling without requiring root
34
+ * privileges or a Windows host in the test suite.
35
+ */
36
+ function runUpdateSupervisor(config, dependencies = {}) {
37
+ const spawn = dependencies.spawn || nativeSpawn;
38
+ const signal = dependencies.kill || process.kill.bind(process);
39
+ const setTimer = dependencies.setTimeout || setTimeout;
40
+ const clearTimer = dependencies.clearTimeout || clearTimeout;
41
+ const platform = dependencies.platform || process.platform;
42
+ const exit = dependencies.exit || ((code) => process.exit(code));
43
+ const log = dependencies.log || console.error;
44
+
45
+ let child;
46
+ let deadlineTimer;
47
+ let graceTimer;
48
+ let reapTimer;
49
+ let finished = false;
50
+ let timedOut = false;
51
+ let childClosed = false;
52
+ let treeTerminationConfirmed = false;
53
+
54
+ function clear(timer) {
55
+ if (timer !== undefined) clearTimer(timer);
56
+ }
57
+
58
+ function finish(code, options = {}) {
59
+ if (finished) return;
60
+ finished = true;
61
+ clear(deadlineTimer);
62
+ clear(graceTimer);
63
+ clear(reapTimer);
64
+ if (options.timedOut) writeMarker(config.timeoutMarker, "timed-out", log);
65
+ if (options.error) writeMarker(config.errorMarker, JSON.stringify(options.error), log);
66
+ exit(code);
67
+ }
68
+
69
+ function timeoutFailure(code, detail) {
70
+ finish(124, {
71
+ timedOut: true,
72
+ error: { code, detail },
73
+ });
74
+ }
75
+
76
+ function timeoutAfterReap() {
77
+ finish(124, { timedOut: true });
78
+ }
79
+
80
+ function waitForReap(after) {
81
+ if (childClosed) {
82
+ timeoutAfterReap();
83
+ return;
84
+ }
85
+ reapTimer = setTimer(() => {
86
+ timeoutFailure(
87
+ "EREAP",
88
+ `timed out after ${config.timeoutMs}ms and ${after} did not close the updater process`,
89
+ );
90
+ }, config.reapMs);
91
+ }
92
+
93
+ function signalProcessGroup(signalName) {
94
+ if (!child || !child.pid) {
95
+ timeoutFailure("ESRCH", `failed to send ${signalName}: updater has no process ID`);
96
+ return "failed";
97
+ }
98
+ try {
99
+ signal(-child.pid, signalName);
100
+ return "sent";
101
+ } catch (error) {
102
+ const { code, detail } = errorInfo(error);
103
+ if (code === "ESRCH") return "gone";
104
+ // An EPERM here is possible after the sudo retry: waiting would violate
105
+ // the deadline, while claiming the process tree was reaped would lie.
106
+ timeoutFailure(code, `failed to send ${signalName} to updater process group: ${detail}`);
107
+ return "failed";
108
+ }
109
+ }
110
+
111
+ function terminateWindowsTree() {
112
+ if (!child || !child.pid) {
113
+ timeoutFailure("ESRCH", "failed to start taskkill: updater has no process ID");
114
+ return;
115
+ }
116
+ let killer;
117
+ try {
118
+ killer = spawn("taskkill", windowsTaskkillArgs(child.pid), {
119
+ stdio: "ignore",
120
+ windowsHide: true,
121
+ });
122
+ } catch (error) {
123
+ const { code, detail } = errorInfo(error);
124
+ timeoutFailure(code, `failed to start taskkill for updater process tree: ${detail}`);
125
+ return;
126
+ }
127
+ killer.once("error", (error) => {
128
+ const { code, detail } = errorInfo(error);
129
+ timeoutFailure(code, `taskkill failed for updater process tree: ${detail}`);
130
+ });
131
+ killer.once("close", (code) => {
132
+ if (code !== 0) {
133
+ timeoutFailure("ETASKKILL", `taskkill exited ${code ?? "without a status"} for updater process tree`);
134
+ return;
135
+ }
136
+ treeTerminationConfirmed = true;
137
+ waitForReap("taskkill");
138
+ });
139
+ }
140
+
141
+ try {
142
+ child = spawn(config.cmd, config.args, {
143
+ detached: true,
144
+ stdio: ["ignore", "inherit", "inherit"],
145
+ shell: config.shell,
146
+ });
147
+ } catch (error) {
148
+ const { code, detail } = errorInfo(error);
149
+ finish(127, { error: { code, detail } });
150
+ return;
151
+ }
152
+
153
+ child.once("error", (error) => {
154
+ const { code, detail } = errorInfo(error);
155
+ finish(127, { error: { code, detail } });
156
+ });
157
+ child.once("close", (code) => {
158
+ childClosed = true;
159
+ if (!timedOut) {
160
+ finish(code === 0 ? 0 : (code ?? 1));
161
+ return;
162
+ }
163
+ if (treeTerminationConfirmed) timeoutAfterReap();
164
+ });
165
+
166
+ deadlineTimer = setTimer(() => {
167
+ if (finished) return;
168
+ timedOut = true;
169
+ if (platform === "win32") {
170
+ terminateWindowsTree();
171
+ return;
172
+ }
173
+ const term = signalProcessGroup("SIGTERM");
174
+ if (term === "failed") return;
175
+ if (term === "gone") {
176
+ treeTerminationConfirmed = true;
177
+ waitForReap("the already-exited process group");
178
+ return;
179
+ }
180
+ graceTimer = setTimer(() => {
181
+ if (finished) return;
182
+ const kill = signalProcessGroup("SIGKILL");
183
+ if (kill === "failed") return;
184
+ treeTerminationConfirmed = true;
185
+ waitForReap(kill === "gone" ? "the already-exited process group" : "SIGKILL");
186
+ }, config.graceMs);
187
+ }, config.timeoutMs);
188
+ }
189
+
190
+ if (require.main === module) {
191
+ runUpdateSupervisor(JSON.parse(process.argv[2]));
192
+ }
193
+
194
+ module.exports = { runUpdateSupervisor, windowsTaskkillArgs };
@@ -283,6 +283,7 @@ export { buildSpawnPlan, quoteForWindowsShell };
283
283
  * protection silently disappearing.
284
284
  */
285
285
  export declare function inOwnProcessGroup<T extends object>(options: T): T;
286
+ declare function updateTimeoutMs(env?: NodeJS.ProcessEnv): number;
286
287
  /**
287
288
  * Where an install's output goes — the other half of surviving a killed parent.
288
289
  *
@@ -307,9 +308,21 @@ export interface InstallOutput {
307
308
  stdio: ("ignore" | "inherit" | number)[];
308
309
  /** Captured stderr tail, or "" when output was inherited by a terminal. */
309
310
  stderrTail: () => string;
311
+ /** Private side-channel from the supervisor, kept out of installer output. */
312
+ timeoutMarker: string;
313
+ /** Private spawn-error side-channel that preserves the prior result contract. */
314
+ errorMarker: string;
315
+ /** Whether the asynchronous installer supervisor reported a deadline expiry. */
316
+ timedOut: () => boolean;
317
+ /** Original package-manager launch error, if the supervisor could start. */
318
+ spawnError: () => {
319
+ code?: string;
320
+ detail?: string;
321
+ } | undefined;
310
322
  dispose: () => void;
311
323
  }
312
324
  export declare function openInstallOutput(verbose: boolean, isTty?: boolean): InstallOutput;
325
+ export declare function runSupervisedUpdateCommand(cmd: string, args: string[], output: InstallOutput, env?: NodeJS.ProcessEnv): UpdateResult;
313
326
  export declare function runUpdateCommand(cmd: string, args: string[], env?: NodeJS.ProcessEnv): UpdateResult;
314
327
  declare function performUpdateCommand(cmd: string, args: string[], runner?: UpdateRunner, env?: NodeJS.ProcessEnv): UpdateResult;
315
328
  declare function performUpdate(command: string, runner?: UpdateRunner): UpdateResult;
@@ -385,6 +398,9 @@ interface EnforceUpdateDeps {
385
398
  * Exit codes:
386
399
  * 0 — update succeeded; user must rerun their command
387
400
  * 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
401
+ *
402
+ * A timed-out updater is the exception: it has been forcibly reaped, so the
403
+ * gate returns and lets the user's command run on the current version.
388
404
  */
389
405
  declare function enforceUpdateRequired(decision: VersionCheckResponse, deps?: EnforceUpdateDeps): void;
390
406
  /**
@@ -418,6 +434,7 @@ export declare function shouldSkipGate(argv: readonly string[]): boolean;
418
434
  export declare const __test__: {
419
435
  CLIENT_ID: string;
420
436
  CONVERGENCE_TIMEOUT_MS: number;
437
+ DEFAULT_UPDATE_TIMEOUT_MS: number;
421
438
  ENDPOINT_PATH: string;
422
439
  FETCH_TIMEOUT_MS: number;
423
440
  checkUpdateConvergence: typeof checkUpdateConvergence;
@@ -448,5 +465,6 @@ export declare const __test__: {
448
465
  resolveRunningInstall: typeof resolveRunningInstall;
449
466
  resolveRunningManager: typeof resolveRunningManager;
450
467
  resolveRunningPrefix: typeof resolveRunningPrefix;
468
+ updateTimeoutMs: typeof updateTimeoutMs;
451
469
  };
452
470
  //# sourceMappingURL=version-gate.d.ts.map
@@ -534,16 +534,74 @@ export function inOwnProcessGroup(options) {
534
534
  }
535
535
  /** Tail of captured installer stderr kept to explain a failure. */
536
536
  const INSTALL_DETAIL_MAX_CHARS = 400;
537
+ /** Give a package-manager update ample time without letting it wedge every CLI call. */
538
+ const DEFAULT_UPDATE_TIMEOUT_MS = 30 * 60 * 1_000;
539
+ /** Node clamps longer timer delays to 1ms, so accepting them would invert the deadline. */
540
+ const MAX_UPDATE_TIMEOUT_MS = 2_147_483_647;
541
+ /** Let a well-behaved updater stop before force-killing the rest of its group. */
542
+ const UPDATE_TERMINATE_GRACE_MS = 1_000;
543
+ /** A successful group kill must still yield a close event so the supervisor reaps its child. */
544
+ const UPDATE_REAP_GRACE_MS = 1_000;
545
+ function updateTimeoutMs(env = process.env) {
546
+ const value = env.HQ_UPDATE_TIMEOUT_MS;
547
+ if (value === undefined || value.trim() === "")
548
+ return DEFAULT_UPDATE_TIMEOUT_MS;
549
+ const parsed = Number(value);
550
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= MAX_UPDATE_TIMEOUT_MS) {
551
+ return parsed;
552
+ }
553
+ // Falling back rather than clamping is deliberate: a clamp would silently
554
+ // substitute a materially different deadline. The warning tells operators
555
+ // their requested value was rejected before any update begins.
556
+ console.error(chalk.yellow(`⚠ Ignoring HQ_UPDATE_TIMEOUT_MS=${JSON.stringify(value)}: expected a positive integer no greater than ${MAX_UPDATE_TIMEOUT_MS}ms; using ${DEFAULT_UPDATE_TIMEOUT_MS}ms.`));
557
+ return DEFAULT_UPDATE_TIMEOUT_MS;
558
+ }
559
+ /**
560
+ * The actual installer runs in a small asynchronous supervisor, rather than
561
+ * directly under `spawnSync`. `spawnSync({ timeout })` only signals its direct
562
+ * child. That is unsafe here because the installer deliberately runs in its
563
+ * own process group: the leader can exit while its descendants keep the global
564
+ * store and registry connection alive. The CommonJS file is copied next to the
565
+ * compiled module for production and is directly runnable from source tests.
566
+ */
567
+ const UPDATE_COMMAND_SUPERVISOR = fileURLToPath(new URL("./update-command-supervisor.cjs", import.meta.url));
568
+ function readSupervisorSpawnError(errorMarker) {
569
+ try {
570
+ const value = JSON.parse(readFileSync(errorMarker, "utf-8"));
571
+ const code = typeof value.code === "string" ? value.code : undefined;
572
+ const detail = typeof value.detail === "string" ? value.detail : undefined;
573
+ return code === undefined && detail === undefined ? undefined : { code, detail };
574
+ }
575
+ catch {
576
+ return undefined;
577
+ }
578
+ }
537
579
  export function openInstallOutput(verbose, isTty = process.stdout.isTTY === true && process.stderr.isTTY === true) {
538
580
  if (verbose && isTty) {
581
+ const dir = mkdtempSync(path.join(os.tmpdir(), "hq-cli-install-"));
582
+ const timeoutMarker = path.join(dir, "timed-out");
583
+ const errorMarker = path.join(dir, "spawn-error.json");
539
584
  return {
540
585
  stdio: ["ignore", "inherit", "inherit"],
541
586
  stderrTail: () => "",
542
- dispose: () => { },
587
+ timeoutMarker,
588
+ errorMarker,
589
+ timedOut: () => existsSync(timeoutMarker),
590
+ spawnError: () => readSupervisorSpawnError(errorMarker),
591
+ dispose: () => {
592
+ try {
593
+ rmSync(dir, { recursive: true, force: true });
594
+ }
595
+ catch {
596
+ // Best-effort cleanup of the supervisor's timeout marker.
597
+ }
598
+ },
543
599
  };
544
600
  }
545
601
  const dir = mkdtempSync(path.join(os.tmpdir(), "hq-cli-install-"));
546
602
  const errPath = path.join(dir, "stderr.log");
603
+ const timeoutPath = path.join(dir, "timed-out");
604
+ const errorPath = path.join(dir, "spawn-error.json");
547
605
  const out = openSync(path.join(dir, "stdout.log"), "a");
548
606
  const err = openSync(errPath, "a");
549
607
  return {
@@ -556,6 +614,10 @@ export function openInstallOutput(verbose, isTty = process.stdout.isTTY === true
556
614
  return "";
557
615
  }
558
616
  },
617
+ timeoutMarker: timeoutPath,
618
+ errorMarker: errorPath,
619
+ timedOut: () => existsSync(timeoutPath),
620
+ spawnError: () => readSupervisorSpawnError(errorPath),
559
621
  dispose: () => {
560
622
  for (const fd of [out, err]) {
561
623
  try {
@@ -574,13 +636,28 @@ export function openInstallOutput(verbose, isTty = process.stdout.isTTY === true
574
636
  },
575
637
  };
576
638
  }
577
- export function runUpdateCommand(cmd, args, env) {
578
- const output = openInstallOutput(true);
639
+ export function runSupervisedUpdateCommand(cmd, args, output, env) {
579
640
  try {
580
641
  const plan = buildSpawnPlan(cmd, args);
581
- const result = spawnSync(plan.cmd, plan.args, inOwnProcessGroup({
642
+ const timeoutMs = updateTimeoutMs(env);
643
+ // The outer sync spawn waits only for our supervisor. The supervisor owns
644
+ // the wall-clock deadline, sends TERM to the updater's whole process group,
645
+ // waits briefly, sends KILL to survivors, and waits for its direct child so
646
+ // the completed result leaves no zombie behind.
647
+ const result = spawnSync(process.execPath, [
648
+ UPDATE_COMMAND_SUPERVISOR,
649
+ JSON.stringify({
650
+ cmd: plan.cmd,
651
+ args: plan.args,
652
+ shell: plan.shell,
653
+ timeoutMs,
654
+ graceMs: UPDATE_TERMINATE_GRACE_MS,
655
+ reapMs: UPDATE_REAP_GRACE_MS,
656
+ timeoutMarker: output.timeoutMarker,
657
+ errorMarker: output.errorMarker,
658
+ }),
659
+ ], inOwnProcessGroup({
582
660
  stdio: output.stdio,
583
- shell: plan.shell,
584
661
  ...(env ? { env } : {}),
585
662
  }));
586
663
  // spawnSync reports a missing executable via `error`, not a throw.
@@ -588,6 +665,26 @@ export function runUpdateCommand(cmd, args, env) {
588
665
  const code = result.error.code;
589
666
  return { ok: false, code, detail: result.error.message };
590
667
  }
668
+ if (output.timedOut()) {
669
+ const command = [cmd, ...args].join(" ");
670
+ const supervisorError = output.spawnError();
671
+ const detail = [
672
+ `timed out after ${timeoutMs}ms: ${command}`,
673
+ supervisorError?.detail,
674
+ ]
675
+ .filter(Boolean)
676
+ .join("; ");
677
+ console.error(chalk.red(`✗ Update ${detail}.`));
678
+ return { ok: false, code: "ETIMEDOUT", detail };
679
+ }
680
+ const spawnedError = output.spawnError();
681
+ if (spawnedError) {
682
+ return {
683
+ ok: false,
684
+ code: spawnedError.code,
685
+ detail: spawnedError.detail ?? "failed to start updater",
686
+ };
687
+ }
591
688
  if (result.status !== 0) {
592
689
  // When output went to a file rather than the user's terminal, its tail is
593
690
  // the only explanation anyone will ever see for this failure.
@@ -606,6 +703,12 @@ export function runUpdateCommand(cmd, args, env) {
606
703
  detail: err instanceof Error ? err.message : String(err),
607
704
  };
608
705
  }
706
+ }
707
+ export function runUpdateCommand(cmd, args, env) {
708
+ const output = openInstallOutput(true);
709
+ try {
710
+ return runSupervisedUpdateCommand(cmd, args, output, env);
711
+ }
609
712
  finally {
610
713
  output.dispose();
611
714
  }
@@ -796,6 +899,9 @@ export function checkUpdateConvergence(targetVersion, deps = {}) {
796
899
  * Exit codes:
797
900
  * 0 — update succeeded; user must rerun their command
798
901
  * 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
902
+ *
903
+ * A timed-out updater is the exception: it has been forcibly reaped, so the
904
+ * gate returns and lets the user's command run on the current version.
799
905
  */
800
906
  function enforceUpdateRequired(decision, deps = {}) {
801
907
  const banner = chalk.red.bold(`✗ hq-cli ${decision.currentVersion} is below the minimum required version (${decision.minVersion}).`);
@@ -858,6 +964,8 @@ function enforceUpdateRequired(decision, deps = {}) {
858
964
  // unexpected throw from the install attempt.
859
965
  lock.release();
860
966
  }
967
+ if (exitCode === "continue")
968
+ return;
861
969
  process.exit(exitCode);
862
970
  }
863
971
  /**
@@ -923,7 +1031,7 @@ function attemptRequiredUpdate(decision, deps, install) {
923
1031
  // artifacts and retry the install ONCE. Guarded on `cleaned.length > 0` so a
924
1032
  // plain EACCES on an otherwise-healthy prefix falls straight through to the
925
1033
  // sudo retry below without a redundant reinstall attempt.
926
- if (!result.ok && prefix) {
1034
+ if (!result.ok && result.code !== "ETIMEDOUT" && prefix) {
927
1035
  const cleaner = deps.cleanStale ?? cleanStalePartialInstall;
928
1036
  const cleaned = cleaner(prefix);
929
1037
  if (cleaned.length > 0) {
@@ -943,7 +1051,7 @@ function attemptRequiredUpdate(decision, deps, install) {
943
1051
  // Never for pnpm or Bun: elevation changes the package manager's global home,
944
1052
  // leaving the user's shim untouched while reporting success. A failed
945
1053
  // non-npm update must surface instead.
946
- if (!result.ok && primaryCmd && !isManagedOutsideNpm) {
1054
+ if (!result.ok && result.code !== "ETIMEDOUT" && primaryCmd && !isManagedOutsideNpm) {
947
1055
  console.error(chalk.dim(` Update failed unprivileged; retrying with: sudo -n ${primaryCmd} ${primaryArgs.join(" ")}`));
948
1056
  const sudoResult = performUpdateCommand("sudo", ["-n", primaryCmd, ...primaryArgs], runner);
949
1057
  if (sudoResult.ok)
@@ -973,6 +1081,15 @@ function attemptRequiredUpdate(decision, deps, install) {
973
1081
  if (isManagedOutsideNpm && result.code === "ENOENT" && command) {
974
1082
  console.error(chalk.dim(` (hq-pro suggests \`${command}\` — that is for npm-managed installs; use it only if you have switched this install to npm.)`));
975
1083
  }
1084
+ // The self-update is allowed to fail, but a deadline expiry must not turn
1085
+ // into a second, indefinite denial of the user's command. The supervisor
1086
+ // either reaped the group or recorded why the OS denied that signal; in
1087
+ // neither case did an update land. Continue on the current CLI rather than
1088
+ // pretending it did or aborting the command that caused the gate to run.
1089
+ if (result.code === "ETIMEDOUT") {
1090
+ console.error(chalk.dim(" Continuing on the current hq-cli version."));
1091
+ return "continue";
1092
+ }
976
1093
  return 75;
977
1094
  }
978
1095
  // Read-your-writes: a "successful" install into the npm prefix does not
@@ -1028,6 +1145,7 @@ export function shouldSkipGate(argv) {
1028
1145
  export const __test__ = {
1029
1146
  CLIENT_ID,
1030
1147
  CONVERGENCE_TIMEOUT_MS,
1148
+ DEFAULT_UPDATE_TIMEOUT_MS,
1031
1149
  ENDPOINT_PATH,
1032
1150
  FETCH_TIMEOUT_MS,
1033
1151
  checkUpdateConvergence,
@@ -1058,5 +1176,6 @@ export const __test__ = {
1058
1176
  resolveRunningInstall,
1059
1177
  resolveRunningManager,
1060
1178
  resolveRunningPrefix,
1179
+ updateTimeoutMs,
1061
1180
  };
1062
1181
  //# sourceMappingURL=version-gate.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.115.6",
3
+ "version": "5.117.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
36
36
  "@aws-sdk/client-s3": "^3.1049.0",
37
- "@indigoai-us/hq-cloud": "~6.16.41",
37
+ "@indigoai-us/hq-cloud": "~6.16.48",
38
38
  "@indigoai-us/hq-flags-client": "^0.1.2",
39
39
  "@sentry/node": "^10.49.0",
40
40
  "@tobilu/qmd": "2.5.3",