@indigoai-us/hq-cli 5.342.10 → 5.342.12

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.342.12] — 2026-10-04
6
+
7
+ - Record every lane worker resume with its launch time and trigger, and classify watcher, monitor-event, and pending-work wakes separately from keep-alive timer accuracy.
8
+
9
+ - Hand off the package-use lease through a non-JSON marker after a re-executed CLI publishes its matching lease, and forward parent-only stop signals to the child.
10
+
11
+ ## [5.342.11] — 2026-10-04
12
+ - Release the CLI package-use lease before interactive login waits after resident modules preload.
13
+ - Show the cached version update nudge after interactive commands when a newer cached release remains and its feature flag is enabled; avoid waiting for registry readiness when there is no nudge to show or repeating guidance after the updater already reported its result.
14
+
5
15
  ## [5.342.10] — 2026-10-04
6
16
  - Attach bounded status, gateway error-class, and integrations subcommand tags to captured gateway faults.
7
17
 
@@ -11147,7 +11147,7 @@ export declare const COMMAND_CATALOG: readonly [{
11147
11147
  readonly subcommands: readonly [];
11148
11148
  }, {
11149
11149
  readonly name: "keep-alive-audit";
11150
- readonly description: "Audit lead lane keep-alive timing. A later envelope proves another turn ran and may reclassify an expired stop as restarted_untimed when no launch record remains; late_restarts_after_window counts known launches after the grace window.";
11150
+ readonly description: "Audit lead lane keep-alive timing. Durable worker-resume events retain launch time and trigger after bounded lane history rolls over; late_restarts_after_window counts known launches after the grace window.";
11151
11151
  readonly aliases: readonly [];
11152
11152
  readonly hidden: false;
11153
11153
  readonly helpOption: true;
@@ -13537,7 +13537,7 @@ export const COMMAND_CATALOG = [
13537
13537
  },
13538
13538
  {
13539
13539
  "name": "keep-alive-audit",
13540
- "description": "Audit lead lane keep-alive timing. A later envelope proves another turn ran and may reclassify an expired stop as restarted_untimed when no launch record remains; late_restarts_after_window counts known launches after the grace window.",
13540
+ "description": "Audit lead lane keep-alive timing. Durable worker-resume events retain launch time and trigger after bounded lane history rolls over; late_restarts_after_window counts known launches after the grace window.",
13541
13541
  "aliases": [],
13542
13542
  "hidden": false,
13543
13543
  "helpOption": true,
@@ -14,6 +14,8 @@
14
14
  * session is kept valid by `hq auth refresh` / `hq-auth-refresh` and consumed
15
15
  * by the deploy + sync skills.
16
16
  */
17
+ import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
18
+ import { preloadResidentPackageModules, reportResidentPackagePreloadFailure } from "../utils/resident-package-preload.js";
17
19
  import * as fs from "node:fs";
18
20
  import { open, realpath } from "node:fs/promises";
19
21
  import path from "node:path";
@@ -231,6 +233,7 @@ export function registerAuthCommands(program) {
231
233
  }
232
234
  }
233
235
  const selection = resolveLoginProviderSelection(options.provider, process.env, rememberedProvider);
236
+ await releasePackageUseLeaseAfterResidentPreload(() => preloadResidentPackageModules("auth-login"), reportResidentPackagePreloadFailure, releaseCurrentPackageUseLease);
234
237
  const tokens = await browserLogin(selection.config);
235
238
  try {
236
239
  storeLastLoginProvider(providerFromIdToken(tokens.idToken) ?? selection.providerToRemember);
@@ -2,7 +2,7 @@ import { buildKeepAliveAudit, formatKeepAliveAuditText } from "../lib/lanes/keep
2
2
  export function registerLaneKeepAliveAuditCommand(parent, deps) {
3
3
  parent
4
4
  .command("keep-alive-audit")
5
- .description("Audit lead lane keep-alive timing. A later envelope proves another turn ran and may reclassify an expired stop as restarted_untimed when no launch record remains; late_restarts_after_window counts known launches after the grace window.")
5
+ .description("Audit lead lane keep-alive timing. Durable worker-resume events retain launch time and trigger after bounded lane history rolls over; late_restarts_after_window counts known launches after the grace window.")
6
6
  .option("--since <iso>", "Include stopped turns at or after this ISO date-time")
7
7
  .option("--lane <id>", "Audit one exact lane id")
8
8
  .option("--json", "Print the report as JSON")
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * hq login — opens browser for Cognito auth.
3
3
  */
4
+ import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
5
+ import { preloadResidentPackageModules, reportResidentPackagePreloadFailure } from "../utils/resident-package-preload.js";
4
6
  import chalk from 'chalk';
5
7
  import { browserLogin, loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
6
8
  import { cognitoConfigForLoginProvider } from '../utils/login-provider.js';
@@ -30,6 +32,15 @@ export function registerLoginCommand(program) {
30
32
  .option('--provider <provider>', 'OAuth provider to use: google, microsoft, or picker')
31
33
  .action(async (options) => {
32
34
  let accessToken;
35
+ let packageUseLeaseReleased = false;
36
+ const releaseForInteractiveWait = async () => {
37
+ if (packageUseLeaseReleased)
38
+ return;
39
+ await releasePackageUseLeaseAfterResidentPreload(async () => {
40
+ await preloadResidentPackageModules("login");
41
+ packageUseLeaseReleased = true;
42
+ }, reportResidentPackagePreloadFailure, releaseCurrentPackageUseLease);
43
+ };
33
44
  try {
34
45
  const existing = loadCachedTokens();
35
46
  if (existing && !isExpiring(existing, 120)) {
@@ -38,6 +49,7 @@ export function registerLoginCommand(program) {
38
49
  accessToken = existing.accessToken;
39
50
  }
40
51
  else {
52
+ await releaseForInteractiveWait();
41
53
  console.log('Opening browser for authentication...');
42
54
  const tokens = await browserLogin(cognitoConfigForLoginProvider(options.provider));
43
55
  const who = peekIdToken(tokens.idToken).email ?? 'HQ';
@@ -63,6 +75,7 @@ export function registerLoginCommand(program) {
63
75
  .join("\n");
64
76
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
65
77
  try {
78
+ await releaseForInteractiveWait();
66
79
  const answer = await rl.question(`Choose this device's default company:\n${choices}\nSelection: `);
67
80
  const selected = activation.memberships[Number.parseInt(answer, 10) - 1];
68
81
  if (selected) {
@@ -47,6 +47,7 @@ let refreshCompletion = Promise.resolve();
47
47
  let completeRefresh = null;
48
48
  let refreshWorker = null;
49
49
  let refreshCompanyUid;
50
+ let refreshHasCompanyContext = false;
50
51
  let refreshPendingSnapshot = null;
51
52
  let refreshSentry;
52
53
  /** Wait briefly for the current company refresh to persist or report completion. */
@@ -144,13 +145,12 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
144
145
  completeRefresh = null;
145
146
  refreshWorker = null;
146
147
  refreshCompanyUid = companyUid;
148
+ refreshHasCompanyContext = Boolean(companyUid || company.companySlug);
147
149
  refreshPendingSnapshot = null;
148
150
  refreshSentry = sentry;
149
- if (companyUid || company.companySlug) {
150
- refreshCompletion = new Promise((resolve) => {
151
- completeRefresh = resolve;
152
- });
153
- }
151
+ refreshCompletion = new Promise((resolve) => {
152
+ completeRefresh = resolve;
153
+ });
154
154
  const finishRefresh = () => {
155
155
  completeRefresh?.();
156
156
  completeRefresh = null;
@@ -220,6 +220,10 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
220
220
  refreshPendingSnapshot = null;
221
221
  finishRefresh();
222
222
  }
223
+ else if (!refreshHasCompanyContext) {
224
+ refreshPendingSnapshot = null;
225
+ finishRefresh();
226
+ }
223
227
  }
224
228
  else {
225
229
  finishRefresh();
@@ -316,6 +320,7 @@ export function _resetFlagRegistryForTests() {
316
320
  completeRefresh = null;
317
321
  refreshWorker = null;
318
322
  refreshCompanyUid = undefined;
323
+ refreshHasCompanyContext = false;
319
324
  refreshPendingSnapshot = null;
320
325
  refreshSentry = undefined;
321
326
  }
@@ -1,8 +1,9 @@
1
- type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "restarted_untimed" | "not_restarted" | "keep_alive_off" | "waiting";
1
+ type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "work_triggered" | "self_wake" | "early_by_keep_alive" | "restarted_untimed" | "not_restarted" | "keep_alive_off" | "waiting";
2
2
  interface KeepAliveAuditRestart {
3
3
  at: string | null;
4
4
  trigger: string;
5
5
  caller: string | null;
6
+ workerEvent?: boolean;
6
7
  }
7
8
  interface KeepAliveAuditRow {
8
9
  lane: string;
@@ -55,6 +55,7 @@ const VERDICTS = [
55
55
  "late",
56
56
  "early_wake",
57
57
  "message_wake",
58
+ "work_triggered",
58
59
  "self_wake",
59
60
  "early_by_keep_alive",
60
61
  "restarted_untimed",
@@ -68,6 +69,50 @@ function parseTime(value) {
68
69
  const parsed = Date.parse(value);
69
70
  return Number.isFinite(parsed) ? parsed : undefined;
70
71
  }
72
+ function readLaneWorkerResumeEvents(hqRoot, laneId) {
73
+ const root = fs.realpathSync(hqRoot);
74
+ const eventsPath = path.join(laneRunDir(root, laneId), "watcher", "events.jsonl");
75
+ if (!withinRoot(root, eventsPath)) {
76
+ throw new LanesError("keep_alive_audit_path_invalid", `Lane watcher event path escapes the HQ root for ${laneId}`, { lane_id: laneId });
77
+ }
78
+ let text;
79
+ try {
80
+ const stat = fs.lstatSync(eventsPath);
81
+ if (!stat.isFile() || stat.isSymbolicLink()) {
82
+ throw new LanesError("keep_alive_audit_event_invalid", `Lane watcher events must be a regular file: ${eventsPath}`, { lane_id: laneId, path: eventsPath });
83
+ }
84
+ const realPath = fs.realpathSync(eventsPath);
85
+ if (!withinRoot(root, realPath)) {
86
+ throw new LanesError("keep_alive_audit_path_invalid", `Lane watcher event file escapes the HQ root for ${laneId}`, { lane_id: laneId, path: eventsPath });
87
+ }
88
+ text = fs.readFileSync(realPath, "utf8");
89
+ }
90
+ catch (err) {
91
+ if (err.code === "ENOENT")
92
+ return [];
93
+ throw err;
94
+ }
95
+ const events = [];
96
+ for (const [index, line] of text.split(/\r?\n/).entries()) {
97
+ if (!line.trim())
98
+ continue;
99
+ let value;
100
+ try {
101
+ value = JSON.parse(line);
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ if (!isRecord(value) || value.event !== "lane_worker_resumed")
107
+ continue;
108
+ const at = parseTime(value.resumed_at);
109
+ if (at === undefined || typeof value.reason !== "string" || !value.reason.trim()) {
110
+ throw new LanesError("keep_alive_audit_event_invalid", `Lane worker resume event has invalid timing or reason: ${eventsPath}:${index + 1}`, { lane_id: laneId, path: eventsPath, line: index + 1 });
111
+ }
112
+ events.push({ at, reason: value.reason, caller: typeof value.caller === "string" ? value.caller : null });
113
+ }
114
+ return events.sort((a, b) => a.at - b.at);
115
+ }
71
116
  function iso(ms) {
72
117
  return new Date(ms).toISOString();
73
118
  }
@@ -176,9 +221,7 @@ function readKeepAliveResumeEvents(hqRoot, laneId) {
176
221
  if (nextEligibleAt === undefined) {
177
222
  throw new LanesError("keep_alive_audit_event_invalid", `Keep-alive resume event has invalid timing fields: ${eventsPath}:${index + 1}`, { lane_id: laneId, path: eventsPath, line: index + 1 });
178
223
  }
179
- const reason = value.reason === "pending_work" || value.reason === "next_check_at_due" || value.reason === "backoff"
180
- ? value.reason
181
- : undefined;
224
+ const reason = typeof value.reason === "string" ? value.reason : undefined;
182
225
  events.push({ at, nextEligibleAt, ...(reason === undefined ? {} : { reason }) });
183
226
  }
184
227
  return events.sort((a, b) => a.at - b.at);
@@ -286,7 +329,18 @@ function laneWasKeepAliveOffAtStop(lane, stoppedAt) {
286
329
  const optedInAt = parseTime(lane.keep_alive_opted_in_at);
287
330
  return optedInAt === undefined || optedInAt > stoppedAt;
288
331
  }
289
- function firstRestartAfter(lane, stop, nextEnvelopeMs) {
332
+ function firstRestartAfter(lane, stop, nextEnvelopeMs, workerResumeEvents) {
333
+ const recordedWorkerResume = workerResumeEvents
334
+ .filter((event) => event.at > stop.mtimeMs && (nextEnvelopeMs === undefined || event.at < nextEnvelopeMs))
335
+ .sort((a, b) => a.at - b.at)[0];
336
+ if (recordedWorkerResume !== undefined) {
337
+ return {
338
+ at: iso(recordedWorkerResume.at),
339
+ trigger: recordedWorkerResume.reason,
340
+ caller: recordedWorkerResume.caller,
341
+ workerEvent: true,
342
+ };
343
+ }
290
344
  const restart = (lane.resume_history ?? [])
291
345
  .map((entry) => ({ entry, at: parseTime(entry.at) }))
292
346
  .filter(({ entry, at }) => at !== undefined && at > stop.mtimeMs && (nextEnvelopeMs === undefined || at < nextEnvelopeMs) && typeof entry.trigger === "string")
@@ -329,6 +383,14 @@ function classifyVerdict(input) {
329
383
  if (restart?.trigger === "unknown" && laneWasKeepAliveOffAtStop(lane, input.stoppedAt))
330
384
  return "keep_alive_off";
331
385
  if (restart) {
386
+ if (restart.trigger === "pending_work" || restart.trigger === "watcher" || restart.trigger === "monitor_event")
387
+ return "work_triggered";
388
+ const workEvidenceEnd = input.nextEnvelopeAt ?? Number.POSITIVE_INFINITY;
389
+ const triggeredWork = restart.workerEvent !== true && input.resumeEvents
390
+ .filter((event) => event.at > input.stoppedAt && event.at < workEvidenceEnd && event.at <= (restart.at === null ? Number.POSITIVE_INFINITY : Date.parse(restart.at)))
391
+ .some((event) => event.reason === "pending_work" || event.reason === "watcher" || event.reason === "monitor_event");
392
+ if (triggeredWork)
393
+ return "work_triggered";
332
394
  if (restart.at === null)
333
395
  return "restarted_untimed";
334
396
  const restartAt = Date.parse(restart.at);
@@ -336,12 +398,14 @@ function classifyVerdict(input) {
336
398
  return "self_wake";
337
399
  if (restart.trigger === "message" || restart.trigger === "answered_questions")
338
400
  return "message_wake";
339
- if (restart.trigger === "keep_alive") {
401
+ if (restart.trigger === "keep_alive" || restart.trigger === "next_check_at_due" || restart.trigger === "backoff") {
340
402
  const event = input.resumeEvents
341
403
  .filter((candidate) => candidate.at > input.stoppedAt && candidate.at <= restartAt + 2 * 60_000)
342
404
  .sort((left, right) => Math.abs(left.at - restartAt) - Math.abs(right.at - restartAt))[0];
343
405
  if (event?.reason === "pending_work")
344
- return "message_wake";
406
+ return "work_triggered";
407
+ if (event?.reason === "watcher" || event?.reason === "monitor_event")
408
+ return "work_triggered";
345
409
  if (event?.reason === undefined &&
346
410
  (hasDeliveredDropboxEvidence(input.hqRoot, lane.lane_id, input.stoppedAt, restartAt) ||
347
411
  hasMessageResumeEvidence(lane, input.stoppedAt, restartAt) ||
@@ -384,6 +448,7 @@ export function buildKeepAliveAudit(input) {
384
448
  try {
385
449
  const { allMtimes, stops } = readStopEnvelopes(input.hqRoot, lane.lane_id);
386
450
  const keepAliveResumeEvents = readKeepAliveResumeEvents(input.hqRoot, lane.lane_id);
451
+ const laneWorkerResumeEvents = readLaneWorkerResumeEvents(input.hqRoot, lane.lane_id);
387
452
  const doneClassifications = readDoneClassifications(input.hqRoot, lane.lane_id);
388
453
  for (const stop of stops) {
389
454
  if (stop.mtimeMs < sinceMs || stop.mtimeMs > nowMs)
@@ -391,7 +456,7 @@ export function buildKeepAliveAudit(input) {
391
456
  if (!doneClassifications.some((at) => at >= stop.mtimeMs && at - stop.mtimeMs <= 10 * 60_000))
392
457
  continue;
393
458
  const nextEnvelopeMs = allMtimes.find((mtime) => mtime > stop.mtimeMs);
394
- const restart = firstRestartAfter(lane, stop, nextEnvelopeMs);
459
+ const restart = firstRestartAfter(lane, stop, nextEnvelopeMs, laneWorkerResumeEvents);
395
460
  const due = dueTime(stop, keepAliveResumeEvents);
396
461
  rows.push({
397
462
  lane: lane.lane_id,
@@ -402,7 +467,7 @@ export function buildKeepAliveAudit(input) {
402
467
  due_at: iso(due.at),
403
468
  due_reason: due.reason,
404
469
  restart,
405
- verdict: classifyVerdict({ dueAt: due.at, stoppedAt: stop.mtimeMs, nextCheckAt: stop.nextCheckAt, restart, lane, nowMs, resumeEvents: keepAliveResumeEvents, hqRoot: input.hqRoot }),
470
+ verdict: classifyVerdict({ dueAt: due.at, stoppedAt: stop.mtimeMs, nextCheckAt: stop.nextCheckAt, restart, lane, nowMs, resumeEvents: keepAliveResumeEvents, ...(nextEnvelopeMs === undefined ? {} : { nextEnvelopeAt: nextEnvelopeMs }), hqRoot: input.hqRoot }),
406
471
  });
407
472
  }
408
473
  }
@@ -56,6 +56,70 @@ const PROVIDER_COMMAND = {
56
56
  codex: "codex",
57
57
  grok: "grok",
58
58
  };
59
+ function keepAliveResumeReason(runDir, pendingResume) {
60
+ if (pendingResume?.keep_alive !== true || typeof pendingResume.at !== "string")
61
+ return undefined;
62
+ const eventsPath = path.join(runDir, "watcher", "events.jsonl");
63
+ let content;
64
+ try {
65
+ content = fs.readFileSync(eventsPath, "utf8");
66
+ }
67
+ catch (err) {
68
+ if (err.code === "ENOENT")
69
+ return "keep_alive";
70
+ throw err;
71
+ }
72
+ for (const line of content.split(/\r?\n/).reverse()) {
73
+ if (!line.trim())
74
+ continue;
75
+ let value;
76
+ try {
77
+ value = JSON.parse(line);
78
+ }
79
+ catch {
80
+ continue;
81
+ }
82
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
83
+ const event = value;
84
+ if (event.event === "keep_alive_resume" && event.at === pendingResume.at && typeof event.reason === "string") {
85
+ return event.reason;
86
+ }
87
+ }
88
+ }
89
+ return "keep_alive";
90
+ }
91
+ function appendLaneWorkerResumeEvent(input) {
92
+ const eventsPath = path.join(laneRunDir(input.hqRoot, input.laneId), "watcher", "events.jsonl");
93
+ fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
94
+ let separator = "";
95
+ try {
96
+ const fd = fs.openSync(eventsPath, "r");
97
+ try {
98
+ const size = fs.fstatSync(fd).size;
99
+ if (size > 0) {
100
+ const lastByte = Buffer.alloc(1);
101
+ fs.readSync(fd, lastByte, 0, 1, size - 1);
102
+ if (lastByte[0] !== 0x0a)
103
+ separator = "\n";
104
+ }
105
+ }
106
+ finally {
107
+ fs.closeSync(fd);
108
+ }
109
+ }
110
+ catch (err) {
111
+ if (err.code !== "ENOENT")
112
+ throw err;
113
+ }
114
+ fs.appendFileSync(eventsPath, `${separator}${JSON.stringify({
115
+ event: "lane_worker_resumed",
116
+ lane_id: input.laneId,
117
+ resumed_at: input.resumedAt,
118
+ reason: input.reason,
119
+ ...(input.caller === undefined ? {} : { caller: input.caller }),
120
+ ...(input.turnId === undefined ? {} : { turn_id: input.turnId }),
121
+ })}\n`, "utf8");
122
+ }
59
123
  export function providerCommandOnPath(command, pathEnv) {
60
124
  // Test and embedded callers can intentionally omit PATH and delegate lookup
61
125
  // to their spawn implementation. A present PATH is authoritative.
@@ -2081,6 +2145,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2081
2145
  trigger.kind === "resume" || trigger.kind === "message"
2082
2146
  ? "user"
2083
2147
  : undefined);
2148
+ const resumeReason = keepAliveResumeReason(runDir, pendingResume) ?? resumeHistoryTrigger;
2084
2149
  let statusGateHeartbeat;
2085
2150
  try {
2086
2151
  statusGateHeartbeat = managerStatusGateHeartbeat(hqRoot, existing, laneTurnId, nowIso);
@@ -2356,6 +2421,14 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2356
2421
  ...(resumeHistoryCaller ? { caller: resumeHistoryCaller } : {}),
2357
2422
  }),
2358
2423
  }));
2424
+ appendLaneWorkerResumeEvent({
2425
+ hqRoot,
2426
+ laneId: lane.lane_id,
2427
+ resumedAt: nowIso,
2428
+ reason: resumeReason,
2429
+ ...(resumeHistoryCaller === undefined ? {} : { caller: resumeHistoryCaller }),
2430
+ turnId: laneTurnId,
2431
+ });
2359
2432
  }
2360
2433
  return laneLaunchResult(readLane(hqRoot, lane.lane_id));
2361
2434
  }
@@ -2670,6 +2743,12 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2670
2743
  ...(callerLaneId ? { caller: callerLaneId } : {}),
2671
2744
  }),
2672
2745
  }));
2746
+ appendLaneWorkerResumeEvent({
2747
+ hqRoot,
2748
+ laneId: lane.lane_id,
2749
+ resumedAt: startedAt,
2750
+ reason: "envelope_repair",
2751
+ });
2673
2752
  return workerPid;
2674
2753
  }
2675
2754
  export { PROVIDER_HOME_ENV, PROVIDER_COMMAND };
package/dist/main.js CHANGED
@@ -41,16 +41,17 @@ import { DEFAULT_COGNITO, isMachineIdentity, loadMachineCachedTokens, } from "./
41
41
  import { isCompanySelectionError } from "./utils/company-selection-error.js";
42
42
  import { canOfferTeamUpgrade, formatPlanGateError, offerTeamUpgrade, offerUpgradeLink, planGateErrorFromVaultError, } from "./utils/plan-gate-error.js";
43
43
  import { upgradeToTeam } from "./utils/team-upgrade.js";
44
- import { printVersionAndNudge, refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
45
- import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
44
+ import { printCachedVersionNudge, printVersionAndNudge, refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
45
+ import { enforceVersionGate, isLocalDependencyInstall, resolveRunningInstall, shouldSkipGate, } from "./utils/version-gate.js";
46
46
  import { autoUpdateAndReexec } from "./utils/self-update.js";
47
47
  import { CLI_VERSION } from "./cli-version.js";
48
48
  import { findLazyCommand, registerCommandCatalog } from "./lazy-commands.js";
49
49
  import { emitCliFirstSyncNextStepOutcome, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliInviteNudgeEvent, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takeCliInviteNudgeEvents, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
50
50
  import { reportCliClientHealthInvocation } from "./utils/client-health.js";
51
51
  import { settleWithin } from "./utils/settle-with-timeout.js";
52
+ import { isVersionRequest } from "./version-request.js";
52
53
  import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
53
- import { kickFlagRegistryReadiness } from "./lib/flag-registry.js";
54
+ import { kickFlagRegistryReadiness, waitForFlagRegistryRefresh, } from "./lib/flag-registry.js";
54
55
  import { isPackageRootResolutionError, packageRootCaptureContext, } from "./utils/package-root-diagnostics.js";
55
56
  import { registerCommandsWithRecovery } from "./startup-registration.js";
56
57
  import { releasePackageUseLeaseAfterCommandLoad } from "./utils/package-use-lease.js";
@@ -185,6 +186,8 @@ export async function runCli() {
185
186
  // out (after the finally block's telemetry, hence not process.exit here).
186
187
  let reexecStatus = null;
187
188
  let versionNudgeHandled = false;
189
+ let versionUpdateHandled = false;
190
+ let commandParsed = false;
188
191
  try {
189
192
  runGlobalNetworkLifecycle(process.argv, () => Sentry.addBreadcrumb({
190
193
  category: "command",
@@ -209,6 +212,9 @@ export async function runCli() {
209
212
  return "continue";
210
213
  const gate = (await foregroundNetworkBudget.waitFor(VERSION_GATE_FOREGROUND_WAIT_MS, FINALIZATION_FOREGROUND_WAIT_MS, (timeoutMs) => enforceVersionGate(async (decision) => {
211
214
  const outcome = await autoUpdateAndReexec(process.argv, decision.latestVersion);
215
+ if (outcome.action === "update-failed" || outcome.action === "updated-no-reexec") {
216
+ versionUpdateHandled = true;
217
+ }
212
218
  if (outcome.action === "reexec")
213
219
  reexecStatus = outcome.reexecStatus ?? 0;
214
220
  return outcome.action === "reexec";
@@ -218,6 +224,9 @@ export async function runCli() {
218
224
  const cachedLatest = staleAgainstCachedLatest();
219
225
  if (cachedLatest) {
220
226
  const outcome = await autoUpdateAndReexec(process.argv, cachedLatest);
227
+ if (outcome.action === "update-failed" || outcome.action === "updated-no-reexec") {
228
+ versionUpdateHandled = true;
229
+ }
221
230
  if (outcome.action === "reexec") {
222
231
  reexecStatus = outcome.reexecStatus ?? 0;
223
232
  return "reexec";
@@ -264,6 +273,7 @@ export async function runCli() {
264
273
  }
265
274
  await releasePackageUseLeaseAfterCommandLoad(process.argv);
266
275
  await program.parseAsync();
276
+ commandParsed = true;
267
277
  maybeShowFirstSyncNextStep();
268
278
  }
269
279
  catch (err) {
@@ -287,10 +297,26 @@ export async function runCli() {
287
297
  const inviteTeammateExposureFlushes = takePlanLimitInviteTeammateExposures().map((companyUid) => emitPlanLimitInviteTeammateExposure(companyUid, releaseHealthTimeoutMs));
288
298
  const limitNoticeExposureFlushes = takeCliLimitNoticeExposures().map((exposure) => emitCliLimitNoticeExposure(exposure, releaseHealthTimeoutMs));
289
299
  const limitNoticeEngagementFlushes = takeCliLimitNoticeEngagements().map((engagement) => emitCliLimitNoticeEngagement(engagement, releaseHealthTimeoutMs));
300
+ const commandArgs = process.argv.slice(2);
301
+ const canShowCachedVersionNudge = commandParsed &&
302
+ !versionNudgeHandled &&
303
+ !versionUpdateHandled &&
304
+ process.stdout.isTTY === true &&
305
+ !commandArgs.includes("--json") &&
306
+ !isVersionRequest(process.argv) &&
307
+ !shouldSkipGate(process.argv) &&
308
+ !isLocalDependencyInstall(resolveRunningInstall());
309
+ const cachedVersionNudge = canShowCachedVersionNudge && staleAgainstCachedLatest() !== null
310
+ ? (async () => {
311
+ await waitForFlagRegistryRefresh(releaseHealthTimeoutMs);
312
+ await printCachedVersionNudge(process.stderr);
313
+ })()
314
+ : undefined;
290
315
  await settleWithin([
291
316
  ...(versionNudgeHandled
292
317
  ? []
293
318
  : [refreshVersionCache({ timeoutMs: releaseHealthTimeoutMs })]),
319
+ ...(cachedVersionNudge ? [cachedVersionNudge] : []),
294
320
  Sentry.flush(releaseHealthTimeoutMs),
295
321
  ...firstSyncOutcomeFlushes,
296
322
  ...inviteNudgeOutcomeFlushes,
@@ -1,3 +1,4 @@
1
+ export declare const PACKAGE_USE_LEASE_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_LEASE_HANDOFF";
1
2
  export declare class PackageUseUpdatePendingError extends Error {
2
3
  constructor(message: string);
3
4
  }
@@ -10,11 +11,24 @@ export interface PackageUseLeaseRecord {
10
11
  start_time_ms: number;
11
12
  hq_version: string;
12
13
  }
14
+ export interface PackageUseLeaseHandoff {
15
+ readonly envValue: string;
16
+ waitForChild(pid: number, childAlive: () => boolean): Promise<boolean>;
17
+ cleanup(): void;
18
+ }
13
19
  export declare function npmPrefix(root: string, platform?: NodeJS.Platform): string | null;
14
20
  export declare function packageUseLeaseDirectory(prefix: string, stateDirectory?: string): string;
15
21
  export declare function packageUseUpdateRequestPath(prefix: string, stateDirectory?: string): string;
16
22
  declare function waitForUpdateRequest(requestPath: string, timeoutMs: number): void;
17
23
  export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string): PackageUseLease;
24
+ /**
25
+ * Create a bounded, per-reexec rendezvous in the current lease directory.
26
+ * The child confirms readiness only after publishing its own atomic lease.
27
+ */
28
+ export declare function createPackageUseLeaseHandoff(lease: PackageUseLease, timeoutMs?: number): PackageUseLeaseHandoff;
29
+ export declare function createCurrentPackageUseLeaseHandoff(timeoutMs?: number): PackageUseLeaseHandoff | null;
30
+ /** Publish this process's lease identity to a parent waiting in the same prefix. */
31
+ export declare function publishPackageUseLeaseHandoff(lease: PackageUseLease, env?: NodeJS.ProcessEnv): boolean;
18
32
  export declare function acquireCurrentPackageUseLease(): PackageUseLease | null;
19
33
  /** Release this process's package-use lease after a long-running command has loaded its package modules. */
20
34
  export declare function releaseCurrentPackageUseLease(): void;
@@ -30,6 +44,7 @@ export declare function currentPackageUseUpdatePending(): boolean;
30
44
  export declare function currentPackageUseUpdateRequested(): boolean;
31
45
  export declare const __test__: {
32
46
  waitForUpdateRequest: typeof waitForUpdateRequest;
47
+ setCurrentPackageUseLease(lease: PackageUseLease | null): void;
33
48
  };
34
49
  export {};
35
50
  //# sourceMappingURL=package-use-lease.d.ts.map
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
6
6
  import { performance } from "node:perf_hooks";
7
7
  const CLI_NAME = "@indigoai-us/hq-cli";
8
8
  const UPDATE_REQUEST = "update.pending.json";
9
+ export const PACKAGE_USE_LEASE_HANDOFF_ENV = "HQ_CLI_PACKAGE_USE_LEASE_HANDOFF";
10
+ const HANDOFF_TIMEOUT_MS = 2_000;
9
11
  export class PackageUseUpdatePendingError extends Error {
10
12
  constructor(message) {
11
13
  super(message);
@@ -172,6 +174,93 @@ export function acquirePackageUseLease(prefix, hqVersion, stateDirectory) {
172
174
  }
173
175
  throw new PackageUseUpdatePendingError("An HQ CLI update is waiting for the package lease; retry when it finishes");
174
176
  }
177
+ /**
178
+ * Create a bounded, per-reexec rendezvous in the current lease directory.
179
+ * The child confirms readiness only after publishing its own atomic lease.
180
+ */
181
+ export function createPackageUseLeaseHandoff(lease, timeoutMs = HANDOFF_TIMEOUT_MS) {
182
+ const directory = path.dirname(lease.leasePath);
183
+ const markerPath = path.join(directory, `.reexec-${process.pid}-${randomBytes(12).toString("hex")}.handoff`);
184
+ let cleaned = false;
185
+ const cleanup = () => {
186
+ if (cleaned)
187
+ return;
188
+ cleaned = true;
189
+ process.removeListener("exit", cleanup);
190
+ try {
191
+ rmSync(markerPath, { force: true });
192
+ rmSync(`${markerPath}.tmp`, { force: true });
193
+ }
194
+ catch (error) {
195
+ process.stderr.write(`hq-cli package-use lease handoff cleanup failed (${errorLabel(error)})\n`);
196
+ }
197
+ };
198
+ process.once("exit", cleanup);
199
+ return {
200
+ envValue: markerPath,
201
+ async waitForChild(pid, childAlive) {
202
+ const deadline = Date.now() + timeoutMs;
203
+ while (childAlive() && Date.now() <= deadline) {
204
+ try {
205
+ const marker = JSON.parse(readFileSync(markerPath, "utf8"));
206
+ if (marker.pid === pid && Number.isSafeInteger(marker.start_time_ms) && typeof marker.lease_name === "string") {
207
+ const leasePath = path.join(directory, marker.lease_name);
208
+ if (path.basename(leasePath) === marker.lease_name) {
209
+ const record = JSON.parse(readFileSync(leasePath, "utf8"));
210
+ if (record.pid === pid && record.start_time_ms === marker.start_time_ms)
211
+ return true;
212
+ }
213
+ }
214
+ }
215
+ catch (error) {
216
+ if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) {
217
+ process.stderr.write(`hq-cli package-use lease handoff check failed (${errorLabel(error)})\n`);
218
+ }
219
+ }
220
+ await new Promise((resolve) => setTimeout(resolve, 25));
221
+ }
222
+ return false;
223
+ },
224
+ cleanup,
225
+ };
226
+ }
227
+ export function createCurrentPackageUseLeaseHandoff(timeoutMs = HANDOFF_TIMEOUT_MS) {
228
+ return currentPackageUseLease ? createPackageUseLeaseHandoff(currentPackageUseLease, timeoutMs) : null;
229
+ }
230
+ /** Publish this process's lease identity to a parent waiting in the same prefix. */
231
+ export function publishPackageUseLeaseHandoff(lease, env = process.env) {
232
+ const markerPath = env[PACKAGE_USE_LEASE_HANDOFF_ENV];
233
+ if (!markerPath)
234
+ return false;
235
+ const directory = path.dirname(lease.leasePath);
236
+ if (path.dirname(path.resolve(markerPath)) !== path.resolve(directory)) {
237
+ delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
238
+ return false;
239
+ }
240
+ const match = /^(\d+)-(\d+)\.json$/.exec(path.basename(lease.leasePath));
241
+ if (!match || Number(match[1]) !== process.pid) {
242
+ delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
243
+ return false;
244
+ }
245
+ const record = {
246
+ pid: process.pid,
247
+ start_time_ms: Number(match[2]),
248
+ lease_name: path.basename(lease.leasePath),
249
+ };
250
+ const tempPath = `${markerPath}.tmp`;
251
+ try {
252
+ writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { flag: "wx", mode: 0o600 });
253
+ renameSync(tempPath, markerPath);
254
+ delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
255
+ return true;
256
+ }
257
+ catch (error) {
258
+ rmSync(tempPath, { force: true });
259
+ delete env[PACKAGE_USE_LEASE_HANDOFF_ENV];
260
+ process.stderr.write(`hq-cli package-use lease handoff publish failed (${errorLabel(error)})\n`);
261
+ return false;
262
+ }
263
+ }
175
264
  function exists(file) {
176
265
  try {
177
266
  readFileSync(file);
@@ -198,6 +287,7 @@ export function acquireCurrentPackageUseLease() {
198
287
  }
199
288
  const lease = acquirePackageUseLease(prefix, pkg.version);
200
289
  currentPackageUseLease = lease;
290
+ publishPackageUseLeaseHandoff(lease);
201
291
  return lease;
202
292
  }
203
293
  /** Release this process's package-use lease after a long-running command has loaded its package modules. */
@@ -260,5 +350,8 @@ export function currentPackageUseUpdateRequested() {
260
350
  }
261
351
  export const __test__ = {
262
352
  waitForUpdateRequest,
353
+ setCurrentPackageUseLease(lease) {
354
+ currentPackageUseLease = lease;
355
+ },
263
356
  };
264
357
  //# sourceMappingURL=package-use-lease.js.map
@@ -1,4 +1,4 @@
1
- export type ResidentPackagePath = "bot" | "mcp" | "daemon" | "childWrapper" | "coreJobRun";
1
+ export type ResidentPackagePath = "auth-login" | "bot" | "childWrapper" | "coreJobRun" | "login" | "mcp" | "daemon";
2
2
  /** Package imports reachable lazily from each resident command's runtime graph. */
3
3
  export declare const RESIDENT_PACKAGE_MODULES: Record<ResidentPackagePath, readonly string[]>;
4
4
  export declare class ResidentPackagePreloadError extends Error {
@@ -1,5 +1,23 @@
1
1
  /** Package imports reachable lazily from each resident command's runtime graph. */
2
2
  export const RESIDENT_PACKAGE_MODULES = {
3
+ login: [
4
+ "../lib/mesh/api.js",
5
+ "../lib/work-context/config.js",
6
+ "../lib/work-context/default-company.js",
7
+ "../lib/work-context/paths.js",
8
+ "../lib/plan-limit-nag.js",
9
+ "../utils/plan-gate-error.js",
10
+ "../sentry.js",
11
+ "open",
12
+ ],
13
+ "auth-login": [
14
+ "../lib/auth-login-provider-state.js",
15
+ "../lib/daemon/mcp-oauth.js",
16
+ "../lib/plan-limit-nag.js",
17
+ "../utils/plan-gate-error.js",
18
+ "../sentry.js",
19
+ "open",
20
+ ],
3
21
  bot: [
4
22
  "../lib/flag-registry.js",
5
23
  "../commands/access.js",
@@ -24,6 +42,30 @@ export const RESIDENT_PACKAGE_MODULES = {
24
42
  ],
25
43
  };
26
44
  const LOADERS = {
45
+ login: [
46
+ () => import("../lib/mesh/api.js"),
47
+ () => import("../lib/work-context/config.js"),
48
+ () => import("../lib/work-context/default-company.js"),
49
+ () => import("../lib/work-context/paths.js"),
50
+ () => import("../lib/plan-limit-nag.js"),
51
+ () => import("../utils/plan-gate-error.js"),
52
+ async () => {
53
+ const { preloadSentrySdk } = await import("../sentry.js");
54
+ preloadSentrySdk();
55
+ },
56
+ () => import("open"),
57
+ ],
58
+ "auth-login": [
59
+ () => import("../lib/auth-login-provider-state.js"),
60
+ () => import("../lib/daemon/mcp-oauth.js"),
61
+ () => import("../lib/plan-limit-nag.js"),
62
+ () => import("../utils/plan-gate-error.js"),
63
+ async () => {
64
+ const { preloadSentrySdk } = await import("../sentry.js");
65
+ preloadSentrySdk();
66
+ },
67
+ () => import("open"),
68
+ ],
27
69
  bot: [
28
70
  () => import("../lib/flag-registry.js"),
29
71
  () => import("../commands/access.js"),
@@ -56,6 +56,7 @@
56
56
  * Opt-outs: `HQ_NO_UPDATE_CHECK=1` (the shared knob that also silences
57
57
  * version-check), `hq rescue --no-self-update`, and the re-exec guard env.
58
58
  */
59
+ import { spawn } from "node:child_process";
59
60
  import * as fs from "node:fs";
60
61
  import { type RunningInstall, type UpdateResult } from "./version-gate.js";
61
62
  import { type RestartDaemonResult } from "../lib/mesh/live/daemon/install.js";
@@ -145,6 +146,10 @@ export interface SelfUpdateDeps {
145
146
  renamePath?: typeof fs.renameSync;
146
147
  runner?: (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => UpdateResult;
147
148
  reexec?: (argv: string[], env: NodeJS.ProcessEnv) => number | null;
149
+ /** Async process-spawn seam for lease handoff tests. */
150
+ spawnReexec?: typeof spawn;
151
+ /** stdin TTY probe for re-exec signal forwarding tests. */
152
+ stdinIsTTY?: () => boolean;
148
153
  acquireLock?: () => (() => void) | null;
149
154
  /**
150
155
  * Read-your-writes convergence check run after a "successful" install:
@@ -56,7 +56,7 @@
56
56
  * Opt-outs: `HQ_NO_UPDATE_CHECK=1` (the shared knob that also silences
57
57
  * version-check), `hq rescue --no-self-update`, and the re-exec guard env.
58
58
  */
59
- import { spawnSync } from "node:child_process";
59
+ import { spawn } from "node:child_process";
60
60
  import { randomBytes } from "node:crypto";
61
61
  import * as fs from "node:fs";
62
62
  import * as path from "node:path";
@@ -70,6 +70,7 @@ 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";
72
72
  import { restartDaemonServiceIfInstalled } from "../lib/mesh/live/daemon/install.js";
73
+ import { createCurrentPackageUseLeaseHandoff, PACKAGE_USE_LEASE_HANDOFF_ENV, releaseCurrentPackageUseLease, } from "./package-use-lease.js";
73
74
  /**
74
75
  * Set on the re-exec'd child so it can never self-update (and re-exec) again.
75
76
  * One update + one re-exec per user invocation, ever.
@@ -596,18 +597,101 @@ export function acquireUpdateLock(now = Date.now()) {
596
597
  * command. Returns the child's exit status, or null when the child could not be
597
598
  * started at all (e.g. `hq` not on PATH in a minimal-PATH parent).
598
599
  */
599
- function reexecHq(argv, env) {
600
+ async function reexecHq(argv, env, spawnProcess = spawn, stdinIsTTY = () => process.stdin.isTTY === true) {
600
601
  const plan = buildSpawnPlan("hq", argv);
601
- const result = spawnSync(plan.cmd, plan.args, {
602
- stdio: "inherit",
603
- shell: plan.shell,
604
- env,
605
- });
606
- if (result.error)
602
+ const handoff = createCurrentPackageUseLeaseHandoff();
603
+ const childEnv = handoff
604
+ ? { ...env, [PACKAGE_USE_LEASE_HANDOFF_ENV]: handoff.envValue }
605
+ : env;
606
+ let childForSignals;
607
+ let childSpawned = false;
608
+ let childClosed = false;
609
+ let spawnedChild;
610
+ const pendingSignals = [];
611
+ const reexecSignals = process.platform === "win32"
612
+ ? ["SIGINT", "SIGTERM"]
613
+ : ["SIGINT", "SIGTERM", "SIGHUP"];
614
+ const forwardSignal = (signal) => {
615
+ if (signal === "SIGINT" && stdinIsTTY())
616
+ return;
617
+ if (childClosed)
618
+ return;
619
+ if (!childForSignals || !childSpawned) {
620
+ pendingSignals.push(signal);
621
+ return;
622
+ }
623
+ if (childForSignals.exitCode === null && childForSignals.signalCode === null) {
624
+ childForSignals.kill(signal);
625
+ }
626
+ };
627
+ const signalHandlers = new Map(reexecSignals.map((signal) => [signal, () => forwardSignal(signal)]));
628
+ for (const [signal, handler] of signalHandlers)
629
+ process.on(signal, handler);
630
+ const removeSignalHandlers = () => {
631
+ for (const [signal, handler] of signalHandlers)
632
+ process.removeListener(signal, handler);
633
+ };
634
+ // spawnSync left default parent signal handling in place, so a parent-only
635
+ // SIGTERM or SIGHUP could terminate it. Forward stop signals to the async
636
+ // child while keeping this process waiting for the child's exit status.
637
+ try {
638
+ spawnedChild = spawnProcess(plan.cmd, plan.args, {
639
+ stdio: "inherit",
640
+ shell: plan.shell,
641
+ env: childEnv,
642
+ });
643
+ childForSignals = spawnedChild;
644
+ }
645
+ catch (error) {
646
+ removeSignalHandlers();
647
+ handoff?.cleanup();
648
+ process.stderr.write(`hq-cli re-exec spawn failed (${reexecErrorClass(error)})\n`);
607
649
  return null;
608
- // A signal-killed child has status null; surface it as a failure exit rather
609
- // than pretending the command completed.
610
- return result.status ?? 1;
650
+ }
651
+ return new Promise((resolve) => {
652
+ let alive = true;
653
+ let spawnFailed = false;
654
+ spawnedChild.once("error", (error) => {
655
+ spawnFailed = true;
656
+ process.stderr.write(`hq-cli re-exec spawn failed (${reexecErrorClass(error)})\n`);
657
+ });
658
+ spawnedChild.once("spawn", () => {
659
+ childSpawned = true;
660
+ for (const signal of pendingSignals.splice(0)) {
661
+ if (spawnedChild.exitCode === null && spawnedChild.signalCode === null)
662
+ spawnedChild.kill(signal);
663
+ }
664
+ if (!handoff || spawnedChild.pid === undefined)
665
+ return;
666
+ void handoff.waitForChild(spawnedChild.pid, () => alive).then((matched) => {
667
+ if (matched && alive) {
668
+ releaseCurrentPackageUseLease();
669
+ handoff.cleanup();
670
+ }
671
+ }).catch((error) => {
672
+ process.stderr.write(`hq-cli package-use lease handoff wait failed (${error instanceof Error ? error.name : "unknown"})\n`);
673
+ });
674
+ });
675
+ spawnedChild.once("close", (code, _signal) => {
676
+ alive = false;
677
+ childClosed = true;
678
+ pendingSignals.length = 0;
679
+ removeSignalHandlers();
680
+ handoff?.cleanup();
681
+ // The former spawnSync path returned null on spawn failure and 1 for a signal.
682
+ resolve(spawnFailed ? null : code ?? 1);
683
+ });
684
+ });
685
+ }
686
+ function reexecErrorClass(error) {
687
+ if (error && typeof error === "object") {
688
+ const code = error.code;
689
+ if (typeof code === "string" && /^[A-Z0-9_]{1,32}$/.test(code))
690
+ return code;
691
+ if (error instanceof Error)
692
+ return error.name;
693
+ }
694
+ return "unknown";
611
695
  }
612
696
  async function updateAndReexec(argv, flavor, known, deps) {
613
697
  const env = deps.env ?? process.env;
@@ -829,7 +913,11 @@ async function updateAndReexec(argv, flavor, known, deps) {
829
913
  return { action: "updated", latest };
830
914
  }
831
915
  const childEnv = { ...env, [REEXEC_GUARD_ENV]: "1" };
832
- const status = (deps.reexec ?? reexecHq)([...argv.slice(2)], childEnv);
916
+ const status = deps.spawnReexec
917
+ ? await reexecHq([...argv.slice(2)], childEnv, deps.spawnReexec, deps.stdinIsTTY)
918
+ : deps.reexec
919
+ ? deps.reexec([...argv.slice(2)], childEnv)
920
+ : await reexecHq([...argv.slice(2)], childEnv, spawn, deps.stdinIsTTY);
833
921
  if (status === null) {
834
922
  console.error(chalk.yellow(`⚠ Updated hq-cli to ${latest}, but couldn't re-launch \`hq\` from PATH; ` +
835
923
  `continuing the ${flavor.noun} on ${current}.`));
@@ -37,6 +37,7 @@ import { fileURLToPath } from "node:url";
37
37
  import chalk from "chalk";
38
38
  import semver from "semver";
39
39
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
40
+ import { isVersionRequest } from "../version-request.js";
40
41
  import { Sentry } from "../sentry.js";
41
42
  import { isCliTelemetryDisabled } from "./cli-telemetry.js";
42
43
  import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
@@ -315,11 +316,6 @@ export function isLocalDependencyPackageDir(pkgDir, platform = process.platform)
315
316
  // `<dir>/lib/node_modules` while the `.bin` shim keeps resolving
316
317
  // `<dir>/node_modules`.
317
318
  //
318
- // Windows is the exception and must keep the old behaviour: its global
319
- // layout is `<prefix>\node_modules` with no `lib` segment, so the same test
320
- // would misread a genuine global install as local.
321
- if (platform === "win32")
322
- return false;
323
319
  const segments = pkgDir
324
320
  .replace(/\\/g, "/")
325
321
  .replace(/\/+$/, "")
@@ -328,6 +324,23 @@ export function isLocalDependencyPackageDir(pkgDir, platform = process.platform)
328
324
  const nodeModulesIndex = segments.lastIndexOf("node_modules");
329
325
  if (nodeModulesIndex <= 0)
330
326
  return false;
327
+ if (platform === "win32") {
328
+ const prefix = segments.slice(0, nodeModulesIndex).join("/");
329
+ const normalize = (value) => value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
330
+ const configuredPrefix = process.env.npm_config_prefix ?? process.env.NPM_CONFIG_PREFIX;
331
+ if (configuredPrefix && normalize(configuredPrefix) === normalize(prefix))
332
+ return false;
333
+ const appData = process.env.APPDATA;
334
+ const defaultPrefix = appData
335
+ ? path.join(appData, "npm")
336
+ : path.join(os.homedir(), "AppData", "Roaming", "npm");
337
+ if (normalize(defaultPrefix) === normalize(prefix))
338
+ return false;
339
+ const pathEntries = (process.env.PATH ?? "").split(";");
340
+ if (pathEntries.some((entry) => normalize(entry) === normalize(prefix)))
341
+ return false;
342
+ return true;
343
+ }
331
344
  return segments[nodeModulesIndex - 1] !== "lib";
332
345
  }
333
346
  export function isLocalDependencyInstall(install, platform = process.platform) {
@@ -1588,12 +1601,11 @@ export async function enforceVersionGate(onUpdateRecommended, options = {}) {
1588
1601
  * force-upgraded.
1589
1602
  */
1590
1603
  export function shouldSkipGate(argv) {
1591
- return argv.some((a) => a === "--version" ||
1604
+ return isVersionRequest(argv) || argv.some((a) => a === "--version" ||
1592
1605
  a === "-V" ||
1593
1606
  a === "-v" ||
1594
1607
  a === "--help" ||
1595
- a === "-h" ||
1596
- a === "version");
1608
+ a === "-h");
1597
1609
  }
1598
1610
  export const __test__ = {
1599
1611
  CLIENT_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.342.10",
3
+ "version": "5.342.12",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {