@indigoai-us/hq-cli 5.109.8 → 5.109.10

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,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.109.10] — 2026-09-12
6
+
7
+ ### Fixed
8
+
9
+ - A global reinstall that tears the install tree out mid-startup now recovers
10
+ even when the module that vanished is a RELATIVE sibling (for example
11
+ `@babel/runtime`'s `./typeof.js`, reached through the mqtt → worker-timers
12
+ chain). The torn-install classifier reduced a relative CJS specifier to the
13
+ package name `.`, producing a readiness probe that could never turn true — so
14
+ an affected command burned the entire 90-second settle budget, never re-ran on
15
+ the settled tree, and reported an error instead of recovering. Relative
16
+ specifiers are now re-resolved the way Node's CJS resolver does — from the
17
+ importer's directory, trying the file, its `.js`/`.json`/`.node` forms, and the
18
+ directory forms — so the command waits only as long as the reinstall actually
19
+ takes and then re-execs once on the healed tree. A genuinely missing sibling
20
+ still surfaces exactly one error naming the resolved path (Sentry HQ-CLI-1R).
21
+
5
22
  ## [5.109.8] — 2026-09-11
6
23
 
7
24
  ### Fixed
@@ -37,6 +37,28 @@ export declare const VALID_PROVIDERS: Set<string>;
37
37
  export declare const VALID_AUTH_MODES: Set<string>;
38
38
  /** Customer-facing agent size keys served by hq-pro's authoritative catalog. */
39
39
  export declare const VALID_AGENT_SIZE_KEYS: Set<string>;
40
+ /**
41
+ * `hq agents create` exits with this distinct status when payment is required.
42
+ * Scripts can distinguish the expected upgrade state from an ordinary command
43
+ * failure without parsing human output.
44
+ */
45
+ export declare const AGENT_CREATE_PAYMENT_REQUIRED_EXIT_CODE = 3;
46
+ export type AgentCreatePlanLimit = Readonly<{
47
+ requiredPlan: "agents-500";
48
+ amountMinor: number;
49
+ currency: string;
50
+ checkoutUrl?: string;
51
+ }>;
52
+ /**
53
+ * Decode only the priced response defined by the agent-create contract. A
54
+ * lookalike 403 must stay an ordinary API error rather than opening a browser
55
+ * or guessing at a price.
56
+ */
57
+ export declare function agentCreatePlanLimitFromPayload(status: number, body: unknown): AgentCreatePlanLimit | null;
58
+ /** Print a server-priced plan block without minting or rewriting its URL. */
59
+ export declare function surfaceAgentCreatePlanLimit(planLimit: AgentCreatePlanLimit, opts?: {
60
+ json?: boolean;
61
+ }): Promise<void>;
40
62
  /**
41
63
  * Resolve a closed-set option value, or exit(1) with a message naming the
42
64
  * offending input and the legal set.
@@ -82,7 +104,9 @@ export declare class AgentsHttpError extends Error {
82
104
  code?: string;
83
105
  /** hq-pro's billing envelope on a `402 billing_required` provision block. */
84
106
  billing?: BillingErrorPayload;
85
- constructor(status: number, message: string, code?: string, billing?: BillingErrorPayload);
107
+ /** Strictly decoded priced `403 AGENT_PLAN_LIMIT` create response. */
108
+ agentCreatePlanLimit?: AgentCreatePlanLimit;
109
+ constructor(status: number, message: string, code?: string, billing?: BillingErrorPayload, agentCreatePlanLimit?: AgentCreatePlanLimit);
86
110
  }
87
111
  /** Roster row from `GET /v1/agents` — a superset is returned; we keep what we render. */
88
112
  export interface CompanyAgentView {
@@ -194,7 +218,7 @@ export declare function formatAgentCreateSize(option: AgentCreateSizeOption): st
194
218
  /** Ask a TTY user to choose one of hq-pro's currently selectable quotes. */
195
219
  export declare function promptForAgentCreateSize(view: AgentCreateOptionsView): Promise<QuotedAgentCreateSizeOption>;
196
220
  /** Confirm creation using the server quote, preserving $0 as a real answer. */
197
- export declare function confirmAgentCreateQuoteOrExit(option: QuotedAgentCreateSizeOption, yes?: boolean): void;
221
+ export declare function confirmAgentCreateQuoteOrExit(option: QuotedAgentCreateSizeOption, yes?: boolean, quiet?: boolean): void;
198
222
  export declare function provisionAgent(token: string, input: ProvisionAgentInput): Promise<{
199
223
  uid?: string;
200
224
  slug?: string;
@@ -25,6 +25,7 @@
25
25
  * the caller's single active membership (same as `members.ts`).
26
26
  */
27
27
  import chalk from "chalk";
28
+ import open from "open";
28
29
  import { randomUUID } from "node:crypto";
29
30
  import * as readline from "node:readline";
30
31
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
@@ -50,6 +51,96 @@ export const VALID_PROVIDERS = new Set(["codex", "grok", "claude", "agents-v2"])
50
51
  export const VALID_AUTH_MODES = new Set(["subscription", "apiKey"]);
51
52
  /** Customer-facing agent size keys served by hq-pro's authoritative catalog. */
52
53
  export const VALID_AGENT_SIZE_KEYS = new Set(["basic", "power", "dev"]);
54
+ /**
55
+ * `hq agents create` exits with this distinct status when payment is required.
56
+ * Scripts can distinguish the expected upgrade state from an ordinary command
57
+ * failure without parsing human output.
58
+ */
59
+ export const AGENT_CREATE_PAYMENT_REQUIRED_EXIT_CODE = 3;
60
+ function isCheckoutUrl(value) {
61
+ if (typeof value !== "string" || !value.trim())
62
+ return false;
63
+ try {
64
+ const url = new URL(value);
65
+ return url.protocol === "https:" && !url.username && !url.password;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ /**
72
+ * Decode only the priced response defined by the agent-create contract. A
73
+ * lookalike 403 must stay an ordinary API error rather than opening a browser
74
+ * or guessing at a price.
75
+ */
76
+ export function agentCreatePlanLimitFromPayload(status, body) {
77
+ if (status !== 403 || !body || typeof body !== "object")
78
+ return null;
79
+ const payload = body;
80
+ if (payload.code !== "AGENT_PLAN_LIMIT" ||
81
+ payload.reasonCode !== "plan_limit" ||
82
+ payload.requiredPlan !== "agents-500" ||
83
+ typeof payload.amountMinor !== "number" ||
84
+ !Number.isSafeInteger(payload.amountMinor) ||
85
+ payload.amountMinor < 0 ||
86
+ typeof payload.currency !== "string" ||
87
+ !/^[A-Za-z]{3}$/.test(payload.currency)) {
88
+ return null;
89
+ }
90
+ if (payload.checkoutUrl !== undefined && !isCheckoutUrl(payload.checkoutUrl)) {
91
+ return null;
92
+ }
93
+ return {
94
+ requiredPlan: "agents-500",
95
+ amountMinor: payload.amountMinor,
96
+ currency: payload.currency.toUpperCase(),
97
+ ...(typeof payload.checkoutUrl === "string"
98
+ ? { checkoutUrl: payload.checkoutUrl }
99
+ : {}),
100
+ };
101
+ }
102
+ function formatServerPrice(amountMinor, currency) {
103
+ return new Intl.NumberFormat("en-US", {
104
+ style: "currency",
105
+ currency,
106
+ }).format(amountMinor / 100);
107
+ }
108
+ function canOfferAgentCheckout(json) {
109
+ return json !== true &&
110
+ process.stdin.isTTY === true &&
111
+ process.stdout.isTTY === true &&
112
+ !process.env.CI;
113
+ }
114
+ async function offerAgentCheckout(url) {
115
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
116
+ try {
117
+ const answer = await new Promise((resolve) => rl.question("Open the payment page now? [y/N] ", resolve));
118
+ if (!/^y(?:es)?$/i.test(answer.trim()))
119
+ return;
120
+ }
121
+ finally {
122
+ rl.close();
123
+ }
124
+ try {
125
+ await open(url);
126
+ }
127
+ catch {
128
+ console.error("Could not open the payment page automatically. Use the URL above.");
129
+ }
130
+ }
131
+ /** Print a server-priced plan block without minting or rewriting its URL. */
132
+ export async function surfaceAgentCreatePlanLimit(planLimit, opts = {}) {
133
+ const price = `${formatServerPrice(planLimit.amountMinor, planLimit.currency)}/month`;
134
+ console.error(`Payment required: your plan does not include agents. Upgrade to ${planLimit.requiredPlan} costs ${price}.`);
135
+ if (!planLimit.checkoutUrl) {
136
+ console.error("Ask a company owner to upgrade before creating an agent.");
137
+ return;
138
+ }
139
+ // Deliberately plain and on its own line so terminals make this clickable.
140
+ console.log(planLimit.checkoutUrl);
141
+ if (canOfferAgentCheckout(opts.json))
142
+ await offerAgentCheckout(planLimit.checkoutUrl);
143
+ }
53
144
  /**
54
145
  * Resolve a closed-set option value, or exit(1) with a message naming the
55
146
  * offending input and the legal set.
@@ -112,12 +203,15 @@ export class AgentsHttpError extends Error {
112
203
  code;
113
204
  /** hq-pro's billing envelope on a `402 billing_required` provision block. */
114
205
  billing;
115
- constructor(status, message, code, billing) {
206
+ /** Strictly decoded priced `403 AGENT_PLAN_LIMIT` create response. */
207
+ agentCreatePlanLimit;
208
+ constructor(status, message, code, billing, agentCreatePlanLimit) {
116
209
  super(message);
117
210
  this.name = "AgentsHttpError";
118
211
  this.status = status;
119
212
  this.code = code;
120
213
  this.billing = billing;
214
+ this.agentCreatePlanLimit = agentCreatePlanLimit;
121
215
  }
122
216
  }
123
217
  /**
@@ -134,7 +228,7 @@ export async function agentsRequest(opts) {
134
228
  // decline copy ("Your card was declined…") while `error` is the generic
135
229
  // "payment required" — error-first would feed surfaceBillingBlocked the
136
230
  // generic string and lose the decline reason (mirrors outpostRequest).
137
- body.message ?? body.error ?? res.statusText, body.code, parseBillingPayload(body));
231
+ body.message ?? body.error ?? res.statusText, body.code, parseBillingPayload(body), agentCreatePlanLimitFromPayload(res.status, body) ?? undefined);
138
232
  }
139
233
  return (await res.json());
140
234
  }
@@ -242,7 +336,7 @@ export async function promptForAgentCreateSize(view) {
242
336
  }
243
337
  }
244
338
  /** Confirm creation using the server quote, preserving $0 as a real answer. */
245
- export function confirmAgentCreateQuoteOrExit(option, yes) {
339
+ export function confirmAgentCreateQuoteOrExit(option, yes, quiet = false) {
246
340
  if (option.notBilled || option.netMonthlyCents === 0) {
247
341
  const message = `${option.productName} is ${formatUsd(0)}/month for this company — ` +
248
342
  "there is no per-agent charge.";
@@ -250,14 +344,26 @@ export function confirmAgentCreateQuoteOrExit(option, yes) {
250
344
  console.error(chalk.yellow(`${message}\nRe-run with --yes to confirm agent provisioning.`));
251
345
  process.exit(1);
252
346
  }
253
- console.log(chalk.dim(`${message} Provisioning…`));
347
+ if (!quiet) {
348
+ console.log(chalk.dim(`${message} Provisioning…`));
349
+ }
254
350
  return;
255
351
  }
256
- confirmChargeOrExit({
257
- resource: "agent",
258
- unitCents: option.netMonthlyCents,
259
- yes,
260
- });
352
+ if (!quiet) {
353
+ confirmChargeOrExit({
354
+ resource: "agent",
355
+ unitCents: option.netMonthlyCents,
356
+ yes,
357
+ });
358
+ return;
359
+ }
360
+ if (!yes) {
361
+ confirmChargeOrExit({
362
+ resource: "agent",
363
+ unitCents: option.netMonthlyCents,
364
+ yes,
365
+ });
366
+ }
261
367
  }
262
368
  export async function provisionAgent(token, input) {
263
369
  return agentsRequest({
@@ -764,7 +870,7 @@ export function registerAgentsCommand(program) {
764
870
  });
765
871
  agents
766
872
  .command("provision <name>")
767
- .alias("new")
873
+ .aliases(["new", "create"])
768
874
  .description("Provision a new cloud agent (company-specific monthly price shown before creation)")
769
875
  .option("--company <slug>", "Company slug (resolves to companyUid)")
770
876
  .option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
@@ -775,6 +881,7 @@ export function registerAgentsCommand(program) {
775
881
  .option("--description <text>", "Short description / bio")
776
882
  .option("--size <size>", "Agent box size: basic | power | dev (omitted keeps the current default)")
777
883
  .option("--yes", "Confirm the quoted monthly cost and provision the agent")
884
+ .option("--json", "Emit machine-readable output and never open a browser")
778
885
  .action(async function (name, opts) {
779
886
  try {
780
887
  // Both of these previously fell back to their default on an
@@ -818,12 +925,12 @@ export function registerAgentsCommand(program) {
818
925
  : shouldChooseInteractively
819
926
  ? await promptForAgentCreateSize(createOptions)
820
927
  : requireDefaultAgentCreateSize(createOptions);
821
- if (!shouldChooseInteractively) {
928
+ if (!shouldChooseInteractively && !opts.json) {
822
929
  console.log(chalk.dim(formatAgentCreateSize(quotedSize)));
823
930
  }
824
931
  // Every path confirms hq-pro's company-specific quote. Omission still
825
932
  // leaves the POST size implicit, preserving the server-side default.
826
- confirmAgentCreateQuoteOrExit(quotedSize, opts.yes);
933
+ confirmAgentCreateQuoteOrExit(quotedSize, opts.yes, opts.json);
827
934
  const slug = opts.slug ?? slugifyAgentName(name);
828
935
  try {
829
936
  const result = await provisionAgent(token, {
@@ -849,10 +956,28 @@ export function registerAgentsCommand(program) {
849
956
  surface: CLI_AGENT_CREATE_SURFACE,
850
957
  });
851
958
  const uid = typeof result.uid === "string" ? result.uid : slug;
852
- console.log(chalk.green(`Provisioning started for agent "${name}".`));
853
- console.log(chalk.dim(`Track setup: hq agents status ${uid} --company <slug>`));
959
+ if (opts.json) {
960
+ console.log(JSON.stringify(result));
961
+ }
962
+ else {
963
+ console.log(chalk.green(`Provisioning started for agent "${name}".`));
964
+ console.log(chalk.dim(`Track setup: hq agents status ${uid} --company <slug>`));
965
+ }
854
966
  }
855
967
  catch (err) {
968
+ if (err instanceof AgentsHttpError &&
969
+ err.status === 403 &&
970
+ err.code === "AGENT_PLAN_LIMIT") {
971
+ if (!err.agentCreatePlanLimit) {
972
+ throw new Error("HQ Pro returned an unrecognized agent payment-required response.");
973
+ }
974
+ await surfaceAgentCreatePlanLimit(err.agentCreatePlanLimit, opts);
975
+ // Keep the payment-required status through normal CLI teardown.
976
+ // Unlike process.exit(), this is testable and cannot be caught by
977
+ // this action's outer error boundary as a generic failure.
978
+ process.exitCode = AGENT_CREATE_PAYMENT_REQUIRED_EXIT_CODE;
979
+ return;
980
+ }
856
981
  // No card on file → surface the shareable payment link instead of an
857
982
  // opaque 402, then exit non-zero so scripts can react.
858
983
  if (err instanceof AgentsHttpError &&
@@ -124,6 +124,32 @@ function registryText(now, entries) {
124
124
  function withoutTimestamp(content) {
125
125
  return content.split('\n').filter((line) => !line.startsWith('generated_at:')).join('\n');
126
126
  }
127
+ /**
128
+ * Worker roots hold two kinds of `worker.yaml` that must never reach the
129
+ * registry.
130
+ *
131
+ * The `_template` company and any `_overrides` directory are scaffolding —
132
+ * copies kept so a new tenant or a pack override can be stamped out from them.
133
+ *
134
+ * A checkout nested under a tenant's `repos/` is a source repository that tenant
135
+ * happens to keep inside HQ, and when that repository is HQ itself, its own
136
+ * `core/workers` and template trees are full of worker.yaml files. Indexing them
137
+ * treats product source as deployed workers: they claim ids real workers already
138
+ * own, and because duplicates resolve by path order — a tenant path sorts before
139
+ * both `core` and `personal` — the checkout wins. The operator's actual worker
140
+ * then silently vanishes from the registry every skill reads.
141
+ */
142
+ function isExcludedWorkerPath(relativeFile) {
143
+ const normalized = relativeFile.replaceAll('\\', '/');
144
+ const [root, tenant, nested] = normalized.split('/');
145
+ if (root !== 'companies')
146
+ return /(?:^|\/)_overrides(?:\/|$)/.test(normalized);
147
+ if (tenant === '_template')
148
+ return true;
149
+ if (nested === 'repos')
150
+ return true;
151
+ return /(?:^|\/)_overrides(?:\/|$)/.test(normalized);
152
+ }
127
153
  /**
128
154
  * Generate core/workers/registry.yaml from worker.yaml files. Invalid workers
129
155
  * are quarantined but do not prevent all valid workers from being registered.
@@ -138,7 +164,7 @@ export function generateWorkersRegistry(hqRoot, options = {}) {
138
164
  ...workerYamlFiles(hqRoot, 'personal/workers'),
139
165
  ].sort();
140
166
  for (const relativeFile of files) {
141
- if (relativeFile.startsWith('companies/_template/') || /(?:^|[/\\])_overrides(?:[/\\]|$)/.test(relativeFile))
167
+ if (isExcludedWorkerPath(relativeFile))
142
168
  continue;
143
169
  const fields = readWorkerFields(path.join(hqRoot, relativeFile));
144
170
  const missing = ['id', 'type', 'description'].filter((key) => !fields[key]);
@@ -46,19 +46,28 @@ export type ModuleNotFoundCode = "ERR_MODULE_NOT_FOUND" | "MODULE_NOT_FOUND" | "
46
46
  * '<abs>'` + `requireStack`.
47
47
  * - `cjs-package`: CJS require of a bare specifier — `Cannot find module
48
48
  * '<name>'` + `requireStack`.
49
+ * - `cjs-relative`: CJS require of a RELATIVE specifier — `Cannot find module
50
+ * './x'` (or `../x`) + `requireStack`. Node reports the RAW
51
+ * relative token, never a resolved path, so it is re-resolved
52
+ * from the importer's directory rather than reduced by
53
+ * packageNameOf (which would yield the un-probeable name `.`).
49
54
  * - `esm-enoent`: ESM loader ENOENT (HQ-CLI-1M) — a module present at RESOLVE
50
55
  * and gone at READ, so getSourceSync/openSync raises ENOENT
51
56
  * (not ERR_MODULE_NOT_FOUND). `err.path` is the vanished file.
52
57
  * - `unknown`: a module-not-found whose message did not parse; recovery
53
58
  * still waits on the lock / retired-dir / quiet signals.
54
59
  */
55
- export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-package" | "esm-enoent" | "unknown";
60
+ export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-package" | "cjs-relative" | "esm-enoent" | "unknown";
56
61
  /**
57
62
  * The missing thing, re-resolvable by the readiness probe:
58
- * - `path`: an absolute filesystem path (a `.js` file, or a package dir).
59
- * - `package`: a bare package `name` resolvable from directory `from` upward.
60
- * - `unknown`: the message did not parse; treated as "present" by the probe so
61
- * readiness turns only on the lock / retired-dir / quiet signals.
63
+ * - `path`: an absolute filesystem path (a `.js` file, or a package dir).
64
+ * - `package`: a bare package `name` resolvable from directory `from` upward.
65
+ * - `relative`: a RELATIVE `specifier` (`./x`, `../x`) re-resolved the way
66
+ * Node's CJS resolver does from directory `from`
67
+ * `path.resolve(from, specifier)` plus the `.js`/`.json`/`.node`
68
+ * and directory forms.
69
+ * - `unknown`: the message did not parse; treated as "present" by the probe so
70
+ * readiness turns only on the lock / retired-dir / quiet signals.
62
71
  */
63
72
  export type ModuleErrorTarget = {
64
73
  kind: "path";
@@ -67,6 +76,10 @@ export type ModuleErrorTarget = {
67
76
  kind: "package";
68
77
  name: string;
69
78
  from: string;
79
+ } | {
80
+ kind: "relative";
81
+ specifier: string;
82
+ from: string;
70
83
  } | {
71
84
  kind: "unknown";
72
85
  };
@@ -115,6 +128,11 @@ export interface InstallTreeFs {
115
128
  * exact ancestor walk `getPackageJSONURL` performs, done WITHOUT
116
129
  * `createRequire` so an ESM-only / export-conditioned package
117
130
  * cannot false-negative.
131
+ * - `relative`: re-resolve `path.resolve(from, specifier)` as Node's CJS
132
+ * require would — a real file (exact / `.js` / `.json` / `.node`),
133
+ * or a directory whose package.json `main` (or that main's index)
134
+ * or own `index.*` is a real file. A bare or partially-extracted
135
+ * directory is NOT loadable and reads as not-present.
118
136
  * - `unknown`: true (readiness turns on the other signals).
119
137
  * Any filesystem error reads as "not present" rather than throwing.
120
138
  */
@@ -52,6 +52,13 @@ const ESM_MODULE_IMPORTED_RE = /^Cannot find module '([^']+)' imported from (.+)
52
52
  * the settle wait. `load\b` also excludes the sibling `esm/loader` module.
53
53
  */
54
54
  const ESM_LOADER_FRAME = /node:internal[\\/]modules[\\/]esm[\\/]load\b/;
55
+ /**
56
+ * A relative module specifier — `./x`, `../x`, `.\x`, `..\x`, or a bare `.`/`..`.
57
+ * Mirrors the shape src/utils/incomplete-install-error.ts uses, extended with the
58
+ * bare-`.`/`..` case so a token that packageNameOf would reduce to the
59
+ * un-probeable name `.` is caught here and re-resolved instead.
60
+ */
61
+ const RELATIVE_CJS_SPECIFIER = /^\.\.?([\\/]|$)/;
55
62
  /**
56
63
  * Reduce a bare specifier to its PACKAGE name: `@scope/name/sub` → `@scope/name`,
57
64
  * `name/sub` → `name`, `name` → `name`. This is the unit the readiness probe
@@ -64,6 +71,20 @@ export function packageNameOf(specifier) {
64
71
  return parts.slice(0, 2).join("/");
65
72
  return parts[0] ?? specifier;
66
73
  }
74
+ /**
75
+ * Build a `package` target from an already-reduced name, UNLESS that name is not
76
+ * a plausible bare package: empty, a relative token reduced to `.`/`..`, or a
77
+ * subpath-imports token (`#internal`). Each of those reduces to a name whose
78
+ * `node_modules/<name>/package.json` ancestor probe can NEVER be true, so it
79
+ * falls back to `unknown` — readiness then turns on the lock / retired-dir /
80
+ * quiet signals rather than a probe that stays permanently false.
81
+ */
82
+ function barePackageTarget(name, from) {
83
+ if (name.length === 0 || name.startsWith(".") || name.startsWith("#")) {
84
+ return { kind: "unknown" };
85
+ }
86
+ return { kind: "package", name, from };
87
+ }
67
88
  /**
68
89
  * Classify a thrown value as a module-resolution failure and extract the missing
69
90
  * target, or return `null` for anything that is not one. The decision is
@@ -128,7 +149,7 @@ export function classifyModuleNotFound(err) {
128
149
  dialect: "esm-package",
129
150
  specifier: spec,
130
151
  importer,
131
- target: { kind: "package", name: spec, from: path.dirname(importer) },
152
+ target: barePackageTarget(spec, path.dirname(importer)),
132
153
  };
133
154
  }
134
155
  // (c) CJS `Cannot find module '<spec>'` (+ Require stack). Absolute → a path
@@ -142,12 +163,32 @@ export function classifyModuleNotFound(err) {
142
163
  if (path.isAbsolute(spec)) {
143
164
  return { code, dialect: "cjs-path", specifier: spec, importer, target: { kind: "path", path: spec } };
144
165
  }
166
+ // Relative specifier (`./x`, `../x`): Node reports the RAW token, so
167
+ // packageNameOf would reduce it to `.`/`..` and build a package target
168
+ // whose ancestor probe can never be true. Re-resolve it from the importer's
169
+ // directory, and report the RESOLVED path as the diagnostic specifier
170
+ // (actionable — the missing sibling's real location), mirroring esm-path.
171
+ if (RELATIVE_CJS_SPECIFIER.test(spec)) {
172
+ if (importer && path.isAbsolute(importer)) {
173
+ const from = path.dirname(importer);
174
+ return {
175
+ code,
176
+ dialect: "cjs-relative",
177
+ specifier: path.resolve(from, spec),
178
+ importer,
179
+ target: { kind: "relative", specifier: spec, from },
180
+ };
181
+ }
182
+ // No usable (absolute) importer survived — fall back to the lock /
183
+ // retired-dir / quiet signals rather than a probe that cannot be true.
184
+ return { code, dialect: "cjs-relative", specifier: spec, importer, target: { kind: "unknown" } };
185
+ }
145
186
  return {
146
187
  code,
147
188
  dialect: "cjs-package",
148
189
  specifier: spec,
149
190
  importer,
150
- target: { kind: "package", name: packageNameOf(spec), from: path.dirname(importer) },
191
+ target: barePackageTarget(packageNameOf(spec), path.dirname(importer)),
151
192
  };
152
193
  }
153
194
  }
@@ -179,6 +220,58 @@ function defaultIsPidAlive(pid) {
179
220
  return err.code === "EPERM";
180
221
  }
181
222
  }
223
+ /** CJS extensions Node's LOAD_AS_FILE / LOAD_INDEX try, in order. */
224
+ const CJS_FILE_EXTENSIONS = [".js", ".json", ".node"];
225
+ /** Whether `p` exists AND is a regular file (not a directory); any fs error → false. */
226
+ function isResolvableFile(p, fs) {
227
+ try {
228
+ return fs.existsSync(p) && !fs.statSync(p).isDirectory();
229
+ }
230
+ catch {
231
+ return false;
232
+ }
233
+ }
234
+ /** Node's LOAD_AS_FILE(X): X itself, then X + each CJS extension — each must be a file. */
235
+ function loadAsFile(base, fs) {
236
+ if (isResolvableFile(base, fs))
237
+ return true;
238
+ return CJS_FILE_EXTENSIONS.some((ext) => isResolvableFile(base + ext, fs));
239
+ }
240
+ /** Node's LOAD_INDEX(X): X/index.{js,json,node} — each must be a file. */
241
+ function loadIndex(dir, fs) {
242
+ return CJS_FILE_EXTENSIONS.some((ext) => isResolvableFile(path.join(dir, `index${ext}`), fs));
243
+ }
244
+ /**
245
+ * Whether a relative target's resolved `base` is loadable exactly as Node's CJS
246
+ * require would load it: LOAD_AS_FILE(base), else LOAD_AS_DIRECTORY(base) — its
247
+ * package.json `main` resolved as a file (or that main's index), else
248
+ * base/index.*. A bare or partially-extracted directory — one that holds neither
249
+ * a resolvable `main` nor an index file — is deliberately NOT loadable, so it
250
+ * reads as not-present and the single re-exec cannot fire on a still-torn tree
251
+ * (mirrors the MODULE_NOT_FOUND Node would still throw for that state).
252
+ */
253
+ function relativeTargetResolvable(base, fs) {
254
+ if (loadAsFile(base, fs))
255
+ return true;
256
+ const manifest = path.join(base, "package.json");
257
+ if (isResolvableFile(manifest, fs)) {
258
+ let main = null;
259
+ try {
260
+ const parsed = JSON.parse(fs.readFileSync(manifest, "utf-8"));
261
+ if (typeof parsed.main === "string" && parsed.main.trim() !== "")
262
+ main = parsed.main;
263
+ }
264
+ catch {
265
+ main = null; // unreadable / unparseable manifest — fall back to index.*
266
+ }
267
+ if (main !== null) {
268
+ const mainBase = path.resolve(base, main);
269
+ if (loadAsFile(mainBase, fs) || loadIndex(mainBase, fs))
270
+ return true;
271
+ }
272
+ }
273
+ return loadIndex(base, fs);
274
+ }
182
275
  /**
183
276
  * Whether the missing target is now present on disk — the readiness signal the
184
277
  * settle wait polls. It re-resolves the SAME step Node's resolver took:
@@ -189,6 +282,11 @@ function defaultIsPidAlive(pid) {
189
282
  * exact ancestor walk `getPackageJSONURL` performs, done WITHOUT
190
283
  * `createRequire` so an ESM-only / export-conditioned package
191
284
  * cannot false-negative.
285
+ * - `relative`: re-resolve `path.resolve(from, specifier)` as Node's CJS
286
+ * require would — a real file (exact / `.js` / `.json` / `.node`),
287
+ * or a directory whose package.json `main` (or that main's index)
288
+ * or own `index.*` is a real file. A bare or partially-extracted
289
+ * directory is NOT loadable and reads as not-present.
192
290
  * - `unknown`: true (readiness turns on the other signals).
193
291
  * Any filesystem error reads as "not present" rather than throwing.
194
292
  */
@@ -209,6 +307,15 @@ export function installTargetPresent(target, fs = nodeInstallTreeFs) {
209
307
  }
210
308
  return true;
211
309
  }
310
+ if (target.kind === "relative") {
311
+ // Re-resolve exactly as Node's CJS require would from `from`, so the probe
312
+ // turns true only when the missing module is ACTUALLY loadable again — a
313
+ // bare or partially-extracted directory must not read as present, or the
314
+ // single re-exec would fire on a still-torn tree. The extensionless case
315
+ // matters — a production sibling shape (`./Base/TreeIterator`, HQ-CLI-1N)
316
+ // carries no extension, so an exact-path-only probe would stay false for it.
317
+ return relativeTargetResolvable(path.resolve(target.from, target.specifier), fs);
318
+ }
212
319
  // package: ancestor walk from `from`.
213
320
  let dir = path.resolve(target.from);
214
321
  for (;;) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.109.8",
3
+ "version": "5.109.10",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {