@indigoai-us/hq-cli 5.345.28 → 5.345.29

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.
@@ -20,6 +20,7 @@ export function registerCoreResolveCompanyNativeCommand(core) {
20
20
  args: operands,
21
21
  cwd: root,
22
22
  hqRoot: root,
23
+ env: { HQ_CALLER_CWD: process.cwd() },
23
24
  });
24
25
  if (code !== 0)
25
26
  process.exit(code);
@@ -10,7 +10,7 @@ type HqResult = {
10
10
  code: number;
11
11
  stdout: string;
12
12
  };
13
- type ResolveCompanyFs = Pick<typeof fs, "readFileSync" | "statSync" | "accessSync">;
13
+ type ResolveCompanyFs = Pick<typeof fs, "readFileSync" | "statSync" | "lstatSync" | "accessSync">;
14
14
  export type ResolveCompanyDependencies = {
15
15
  cwd: string;
16
16
  hqRoot?: string;
@@ -18,7 +18,7 @@ export type ResolveCompanyDependencies = {
18
18
  fs?: ResolveCompanyFs;
19
19
  stdin?: string;
20
20
  stdinIsTty?: boolean;
21
- spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string) => Promise<HqResult>;
21
+ spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string, command?: string) => Promise<HqResult>;
22
22
  pid?: number;
23
23
  random?: () => number;
24
24
  };
@@ -1,8 +1,9 @@
1
1
  /** Native port of resolve-company.sh. This module stays on the fast import path. */
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
6
+ import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
6
7
  export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
7
8
  export const RESOLVE_COMPANY_HELP = [
8
9
  "hq-core: public",
@@ -15,6 +16,13 @@ export const RESOLVE_COMPANY_HELP = [
15
16
  "Output: {\"company\":\"<slug>\",\"source\":\"prompt|session|device_default|none\"}",
16
17
  "Always exits 0 and never writes state.",
17
18
  "",
19
+ "Folder mode (--path <dir>): ask the repo-to-company registry through",
20
+ "`$HQ_CLI_BIN resolve-company --path <dir> --json` (HQ_CLI_BIN defaults to",
21
+ "`hq`). Sources: registry (linked folder), registry_miss (not linked), or",
22
+ "unavailable (CLI missing, failed, timed out, or returned bad output). A",
23
+ "registry answer is accepted only when it names a real companies/<slug>",
24
+ "directory. Folder mode never falls back to prompt, session, or device default.",
25
+ "",
18
26
  "set -uo pipefail",
19
27
  "",
20
28
  "ROOT=\"\"",
@@ -222,21 +230,49 @@ function validPreflight(text, slug, session, operation) {
222
230
  value.companySlug === slug &&
223
231
  typeof value.companyUid === "string" && value.companyUid.length > 0);
224
232
  }
225
- function commandOnPath(name, env) {
226
- const pathValue = env.PATH ?? "";
227
- for (const directory of pathValue.split(path.delimiter)) {
228
- const candidate = path.join(directory || ".", name);
233
+ function commandPath(name, env) {
234
+ const hasPathSeparator = name.includes("/") || name.includes("\\");
235
+ const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH");
236
+ const candidates = hasPathSeparator
237
+ ? [name]
238
+ : (pathKey ? env[pathKey] ?? "" : "").split(path.delimiter).flatMap((directory) => {
239
+ const base = path.join(directory || ".", name);
240
+ return process.platform === "win32" && !path.extname(name)
241
+ ? [`${base}.com`, `${base}.exe`, `${base}.bat`, `${base}.cmd`]
242
+ : [base];
243
+ });
244
+ for (const candidate of candidates) {
229
245
  try {
230
246
  fs.accessSync(candidate, fs.constants.X_OK);
231
- return true;
247
+ return candidate;
232
248
  }
233
249
  catch {
234
250
  // `command -v` simply advances to the next PATH entry.
235
251
  }
236
252
  }
253
+ return undefined;
254
+ }
255
+ function commandOnPath(name, env) {
256
+ if (commandPath(name, env))
257
+ return true;
258
+ if (process.platform !== "win32" || name.includes("/") || name.includes("\\") || path.extname(name))
259
+ return false;
260
+ const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH");
261
+ for (const directory of (pathKey ? env[pathKey] ?? "" : "").split(path.delimiter)) {
262
+ try {
263
+ // Git Bash considers an executable extensionless script a command. Keep
264
+ // the presence check faithful to that shell contract; actual Windows
265
+ // process launch still selects a PATHEXT executable or batch shim.
266
+ fs.accessSync(path.join(directory || ".", name), fs.constants.X_OK);
267
+ return true;
268
+ }
269
+ catch {
270
+ // `command -v` advances through each PATH entry.
271
+ }
272
+ }
237
273
  return false;
238
274
  }
239
- function runHq(args, env, timeoutMs, cwd) {
275
+ function runHq(args, env, timeoutMs, cwd, command = "hq") {
240
276
  return new Promise((resolve) => {
241
277
  let settled = false;
242
278
  let stdout = "";
@@ -247,7 +283,15 @@ function runHq(args, env, timeoutMs, cwd) {
247
283
  if (!child?.pid)
248
284
  return;
249
285
  try {
250
- process.kill(-child.pid, signal);
286
+ if (process.platform === "win32") {
287
+ const taskkill = path.join(env.SystemRoot ?? env.WINDIR ?? "C:\\Windows", "System32", "taskkill.exe");
288
+ const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true });
289
+ if (result.error || result.status !== 0)
290
+ child.kill(signal);
291
+ }
292
+ else {
293
+ process.kill(-child.pid, signal);
294
+ }
251
295
  }
252
296
  catch (error) {
253
297
  if (errorCode(error) !== "ESRCH")
@@ -279,7 +323,24 @@ function runHq(args, env, timeoutMs, cwd) {
279
323
  }
280
324
  }, timeoutMs);
281
325
  try {
282
- child = spawn("hq", args, { cwd, detached: true, stdio: ["ignore", "pipe", "ignore"], env });
326
+ const resolved = commandPath(command, env);
327
+ if (!resolved) {
328
+ finish({ code: 127, stdout });
329
+ return;
330
+ }
331
+ let executable = resolved;
332
+ let executableArgs = args;
333
+ let windowsVerbatimArguments;
334
+ if (process.platform === "win32" && isWindowsBatchFile(resolved)) {
335
+ const plan = buildCmdExeSpawnPlan(resolved, args, env);
336
+ executable = plan.cmd;
337
+ executableArgs = plan.args;
338
+ windowsVerbatimArguments = plan.windowsVerbatimArguments;
339
+ }
340
+ child = spawn(executable, executableArgs, {
341
+ cwd, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "ignore"], env,
342
+ windowsVerbatimArguments,
343
+ });
283
344
  }
284
345
  catch {
285
346
  finish({ code: 127, stdout });
@@ -301,6 +362,8 @@ function parseArgs(args) {
301
362
  let root = "";
302
363
  let prompt = "";
303
364
  let havePrompt = false;
365
+ let folder = "";
366
+ let haveFolder = false;
304
367
  for (let index = 0; index < args.length;) {
305
368
  const arg = args[index];
306
369
  if (arg === "--root") {
@@ -312,14 +375,84 @@ function parseArgs(args) {
312
375
  havePrompt = true;
313
376
  index += Math.min(2, args.length - index);
314
377
  }
378
+ else if (arg === "--path") {
379
+ folder = args[index + 1] ?? "";
380
+ haveFolder = true;
381
+ index += Math.min(2, args.length - index);
382
+ }
315
383
  else if (arg === "--help" || arg === "-h") {
316
- return { root, prompt, havePrompt, help: true };
384
+ return { root, prompt, havePrompt, folder, haveFolder, help: true };
317
385
  }
318
386
  else {
319
387
  index += 1;
320
388
  }
321
389
  }
322
- return { root, prompt, havePrompt, help: false };
390
+ return { root, prompt, havePrompt, folder, haveFolder, help: false };
391
+ }
392
+ function unavailable(reason) {
393
+ return {
394
+ ...output("", "unavailable"),
395
+ stderr: Buffer.from(`resolve-company: WARNING company registry unavailable (${reason}); binding personal, not guessing a company\n`),
396
+ };
397
+ }
398
+ async function resolveFolder(folder, root, cwd, env, fileSystem, invoke) {
399
+ if (!folder)
400
+ return unavailable("empty --path");
401
+ const cli = env.HQ_CLI_BIN || "hq";
402
+ if (!commandPath(cli, env))
403
+ return unavailable(`'${cli}' not found`);
404
+ if (!commandPath("jq", env))
405
+ return unavailable("jq not found");
406
+ // The registry command is the contract boundary. Calling it preserves its
407
+ // own hq-anywhere gate and avoids importing the full command graph here.
408
+ const hqEnv = {
409
+ ...EMPTY_ENV,
410
+ ...env,
411
+ HQ_NO_UPDATE_CHECK: "1",
412
+ HQ_RESOLVE_TIMEOUT_MS: env.HQ_RESOLVE_TIMEOUT_MS || "5000",
413
+ };
414
+ const parsedTimeout = Number(hqEnv.HQ_RESOLVE_TIMEOUT_MS);
415
+ const timeoutMs = parsedTimeout || 2_000;
416
+ const response = await invoke(["resolve-company", "--path", folder, "--json"], hqEnv, timeoutMs, cwd, cli);
417
+ const value = record(parseJson(response.stdout));
418
+ if (response.code !== 0 && !(response.code === 1 && value?.company === null)) {
419
+ return unavailable(`'${cli} resolve-company' exited ${response.code}`);
420
+ }
421
+ if (!value)
422
+ return unavailable(`'${cli} resolve-company' returned unreadable output`);
423
+ if (value.company === null || value.company === undefined)
424
+ return output("", "registry_miss");
425
+ if (typeof value.company !== "string")
426
+ return unavailable(`'${cli} resolve-company' returned unreadable output`);
427
+ // jq -r plus command substitution removes trailing newlines before the
428
+ // shell validates the returned slug.
429
+ const slug = value.company.replace(/\n+$/, "");
430
+ if (!slug || slug === "personal" || slug.startsWith("_") || !/^[A-Za-z0-9_-]+$/.test(slug)) {
431
+ return unavailable(`registry returned invalid company '${slug}'`);
432
+ }
433
+ const companyPath = path.join(root, "companies", slug);
434
+ let isDirectory = false;
435
+ let isSymlink = false;
436
+ try {
437
+ isDirectory = fileSystem.statSync(companyPath).isDirectory();
438
+ }
439
+ catch (error) {
440
+ const code = errorCode(error);
441
+ if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES" && code !== "ELOOP")
442
+ throw error;
443
+ }
444
+ try {
445
+ isSymlink = fileSystem.lstatSync(companyPath).isSymbolicLink();
446
+ }
447
+ catch (error) {
448
+ const code = errorCode(error);
449
+ if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES" && code !== "ELOOP")
450
+ throw error;
451
+ }
452
+ if (!isDirectory || isSymlink) {
453
+ return unavailable(`registry company '${slug}' has no companies/${slug} directory in ${root}`);
454
+ }
455
+ return output(slug, "registry");
323
456
  }
324
457
  async function readStdin() {
325
458
  const chunks = [];
@@ -333,11 +466,14 @@ export async function resolveCompany(args, dependencies) {
333
466
  const parsed = parseArgs(args);
334
467
  if (parsed.help)
335
468
  return { code: 0, stdout: Buffer.from(RESOLVE_COMPANY_HELP), stderr: Buffer.alloc(0) };
469
+ const root = parsed.root || dependencies.hqRoot || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || dependencies.cwd;
470
+ if (parsed.haveFolder) {
471
+ return await resolveFolder(parsed.folder, root, dependencies.cwd, env, fileSystem, dependencies.spawnHq ?? runHq);
472
+ }
336
473
  let prompt = parsed.prompt;
337
474
  if (!parsed.havePrompt && dependencies.stdinIsTty !== true) {
338
475
  prompt = dependencies.stdin ?? await readStdin();
339
476
  }
340
- const root = parsed.root || dependencies.hqRoot || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || dependencies.cwd;
341
477
  const manifest = path.join(root, "companies/manifest.yaml");
342
478
  if (!isFile(manifest, fileSystem))
343
479
  return output("", "none");
@@ -393,6 +529,7 @@ export async function resolveCompany(args, dependencies) {
393
529
  export async function runResolveCompanyFast(args, stdin, cwd, hqRoot, _commandName, _hqRootArg, stdinReader = readStdin) {
394
530
  const parsed = parseArgs(args);
395
531
  const actualStdin = parsed.havePrompt || process.stdin.isTTY === true
532
+ || parsed.haveFolder
396
533
  ? stdin
397
534
  : stdin || await stdinReader();
398
535
  const result = await resolveCompany(args, {
@@ -184,6 +184,16 @@ export const FAST_SCAFFOLD_SCRIPTS = [
184
184
  shellHqRoot: "live",
185
185
  readsStdin: true,
186
186
  },
187
+ {
188
+ name: "checkpoint",
189
+ kind: "native",
190
+ modulePath: "./commands/core-checkpoint-fast.js",
191
+ exportName: "runCheckpointFast",
192
+ flagKey: "hq-core-native-fast-route",
193
+ familyFlagKey: "core-native-session",
194
+ fallbackToCommandGraph: true,
195
+ root: "live",
196
+ },
187
197
  {
188
198
  name: "hq-session",
189
199
  kind: "native",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.28",
3
+ "version": "5.345.29",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {