@indigoai-us/hq-cli 5.345.34 → 5.345.35

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,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.35] — 2026-10-07
6
+ - Claude plugin installs now use the HQ packages marketplace by default; local marketplace directory and environment overrides remain available.
7
+ - Pin self-updates to the pnpm version recorded by the global install; preserve store dirs on pnpm 11+ and omit pnpm 10 virtual-store flags.
8
+
5
9
  ## [5.345.34] — 2026-10-07
6
10
  - Protect live remote-control tmux sessions with socket-scoped creator identity before cleanup.
7
11
 
@@ -19,6 +19,8 @@ export type ResolveCompanyDependencies = {
19
19
  stdin?: string;
20
20
  stdinIsTty?: boolean;
21
21
  spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string, command?: string) => Promise<HqResult>;
22
+ onHqSpawn?: (args: readonly string[]) => void;
23
+ onHqExit?: (args: readonly string[]) => void;
22
24
  pid?: number;
23
25
  random?: () => number;
24
26
  };
@@ -1,5 +1,5 @@
1
1
  /** Native port of resolve-company.sh. This module stays on the fast import path. */
2
- import { spawn, spawnSync } from "node:child_process";
2
+ import { spawn } 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";
@@ -272,30 +272,46 @@ function commandOnPath(name, env) {
272
272
  }
273
273
  return false;
274
274
  }
275
- function runHq(args, env, timeoutMs, cwd, command = "hq") {
275
+ function runHq(args, env, timeoutMs, cwd, command = "hq", onExitObserved) {
276
276
  return new Promise((resolve) => {
277
277
  let settled = false;
278
278
  let stdout = "";
279
279
  let child;
280
280
  let timeoutKill;
281
281
  let timedOut = false;
282
- const signalGroup = (signal) => {
283
- if (!child?.pid)
282
+ const signalGroup = (signal, onComplete) => {
283
+ if (!child?.pid) {
284
+ onComplete?.();
284
285
  return;
286
+ }
285
287
  try {
286
288
  if (process.platform === "win32") {
287
289
  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);
290
+ let completed = false;
291
+ const complete = (code) => {
292
+ if (completed)
293
+ return;
294
+ completed = true;
295
+ if (code !== 0)
296
+ child?.kill(signal);
297
+ onComplete?.();
298
+ };
299
+ const killer = spawn(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
300
+ stdio: "ignore",
301
+ windowsHide: true,
302
+ });
303
+ killer.once("error", () => complete(1));
304
+ killer.once("exit", (code) => complete(code));
291
305
  }
292
306
  else {
293
307
  process.kill(-child.pid, signal);
308
+ onComplete?.();
294
309
  }
295
310
  }
296
311
  catch (error) {
297
312
  if (errorCode(error) !== "ESRCH")
298
313
  finish({ code: 1, stdout });
314
+ onComplete?.();
299
315
  }
300
316
  };
301
317
  const finish = (result) => {
@@ -310,13 +326,13 @@ function runHq(args, env, timeoutMs, cwd, command = "hq") {
310
326
  const timer = setTimeout(() => {
311
327
  timedOut = true;
312
328
  if (child?.pid) {
313
- signalGroup("SIGTERM");
314
- // The shell wrapper returns shortly after its 2 second deadline. Do
315
- // not wait for inherited stdio handles after killing the process group.
316
- timeoutKill = setTimeout(() => {
317
- signalGroup("SIGKILL");
318
- finish({ code: 124, stdout });
319
- }, 250);
329
+ signalGroup("SIGTERM", () => {
330
+ // The shell wrapper returns shortly after its 2 second deadline. Give
331
+ // the tree time to exit, then force-kill and wait for Windows cleanup.
332
+ timeoutKill = setTimeout(() => {
333
+ signalGroup("SIGKILL", () => finish({ code: 124, stdout }));
334
+ }, 250);
335
+ });
320
336
  }
321
337
  else {
322
338
  finish({ code: 124, stdout });
@@ -349,9 +365,24 @@ function runHq(args, env, timeoutMs, cwd, command = "hq") {
349
365
  child.stdout?.setEncoding("utf8");
350
366
  child.stdout?.on("data", (chunk) => { stdout += chunk; });
351
367
  child.once("error", () => finish({ code: 127, stdout }));
368
+ if (onExitObserved)
369
+ child.once("exit", onExitObserved);
352
370
  child.once("exit", (code, signal) => {
353
- signalGroup("SIGTERM");
354
- finish({ code: timedOut ? 124 : typeof code === "number" ? code : signal ? 128 : 1, stdout });
371
+ if (timedOut)
372
+ return;
373
+ clearTimeout(timer);
374
+ const result = { code: typeof code === "number" ? code : signal ? 128 : 1, stdout };
375
+ if (process.platform === "win32") {
376
+ // Tree cleanup is asynchronous on Windows. Start it after the direct
377
+ // child exits, but let the resolver advance without waiting for
378
+ // taskkill; detached descendants must not hold up the next lookup.
379
+ signalGroup("SIGTERM");
380
+ finish(result);
381
+ return;
382
+ }
383
+ signalGroup("SIGTERM", () => {
384
+ finish(result);
385
+ });
355
386
  });
356
387
  });
357
388
  }
@@ -502,7 +533,13 @@ export async function resolveCompany(args, dependencies) {
502
533
  hqEnv.HQ_ROOT = dependencies.hqRoot;
503
534
  hqEnv.CLAUDE_PROJECT_DIR = dependencies.hqRoot;
504
535
  }
505
- const invoke = dependencies.spawnHq ?? runHq;
536
+ const invokeImplementation = dependencies.spawnHq
537
+ ? dependencies.spawnHq
538
+ : (args, childEnv, timeoutMs, cwd, command) => runHq(args, childEnv, timeoutMs, cwd, command, () => dependencies.onHqExit?.(args));
539
+ const invoke = (args, env, timeoutMs, cwd, command) => {
540
+ dependencies.onHqSpawn?.(args);
541
+ return invokeImplementation(args, env, timeoutMs, cwd, command);
542
+ };
506
543
  const defaultResponse = await invoke(["mesh", "context", "default", "get", "--json"], hqEnv, 2_000, root);
507
544
  const defaultSlug = defaultSlugFromJson(defaultResponse.stdout, knownSlugs);
508
545
  if (!defaultSlug)
@@ -39,7 +39,7 @@ export interface ClaudePluginSource {
39
39
  pluginName: string;
40
40
  pluginId: string;
41
41
  }
42
- /** Resolve the local marketplace source; published hosting remains an operator decision. */
42
+ /** Resolve local overrides first, then the published HQ marketplace. */
43
43
  export declare function resolveClaudePluginSource(explicitDir?: string, env?: NodeJS.ProcessEnv, repository?: string): ClaudePluginSource;
44
44
  export { AnywhereRuntimeDisabledError as AnywhereDisabledError };
45
45
  export declare const isAnywhereEnabled: (_home: string, env?: NodeJS.ProcessEnv) => boolean;
@@ -46,14 +46,15 @@ export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env)
46
46
  const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
47
47
  return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
48
48
  }
49
- /** Resolve the local marketplace source; published hosting remains an operator decision. */
50
- export function resolveClaudePluginSource(explicitDir, env = process.env, repository) {
49
+ const HQ_PACKAGES_MARKETPLACE_REPOSITORY = 'indigoai-us/hq-packages';
50
+ /** Resolve local overrides first, then the published HQ marketplace. */
51
+ export function resolveClaudePluginSource(explicitDir, env = process.env, repository = HQ_PACKAGES_MARKETPLACE_REPOSITORY) {
51
52
  const configured = explicitDir?.trim() || env.HQ_CLAUDE_PLUGIN_MARKETPLACE?.trim();
52
53
  if (!configured) {
53
- if (repository === 'indigoai-us/hq-packages') {
54
+ if (repository === HQ_PACKAGES_MARKETPLACE_REPOSITORY) {
54
55
  return { sourceDir: repository, marketplaceName: 'hq-packages', pluginName: 'hq', pluginId: 'hq@hq-packages' };
55
56
  }
56
- throw Object.assign(new Error('Published HQ Claude plugin source is not configured; its repository/host awaits an operator decision. Pass --marketplace-dir <dir> or set HQ_CLAUDE_PLUGIN_MARKETPLACE.'), { expected: true });
57
+ throw Object.assign(new Error(`Unsupported published HQ Claude plugin repository "${repository}". Pass --marketplace-dir <dir> or set HQ_CLAUDE_PLUGIN_MARKETPLACE for a local marketplace.`), { expected: true });
57
58
  }
58
59
  const sourceDir = path.resolve(configured);
59
60
  const manifestPath = path.join(sourceDir, '.claude-plugin', 'marketplace.json');
@@ -72,10 +72,12 @@ export function registerPackageInstallCommand(parent) {
72
72
  if (opts.via === 'plugin' &&
73
73
  !marketplaceDir?.trim() &&
74
74
  !process.env.HQ_CLAUDE_PLUGIN_MARKETPLACE?.trim()) {
75
- if (await anywhereRuntimeAllowed(process.env)) {
76
- await runInstallGlobal({ ...opts, marketplaceRepository: 'indigoai-us/hq-packages' });
77
- return;
75
+ if (!(await anywhereRuntimeAllowed(process.env))) {
76
+ console.error(chalk.red('Install failed:'), 'HQ Anywhere plugin installation requires both the hq-anywhere-runtime flag and your person setting.');
77
+ process.exit(1);
78
78
  }
79
+ await runInstallGlobal({ ...opts, marketplaceRepository: 'indigoai-us/hq-packages' });
80
+ return;
79
81
  }
80
82
  await runInstallGlobal({ ...opts, marketplaceDir });
81
83
  return;
@@ -58,7 +58,7 @@
58
58
  */
59
59
  import { spawn } from "node:child_process";
60
60
  import * as fs from "node:fs";
61
- import { type RunningInstall, type UpdateResult } from "./version-gate.js";
61
+ import { probePnpmVersion, type RunningInstall, type UpdateResult } from "./version-gate.js";
62
62
  import { type RestartDaemonResult } from "../lib/mesh/live/daemon/install.js";
63
63
  /**
64
64
  * Set on the re-exec'd child so it can never self-update (and re-exec) again.
@@ -116,7 +116,7 @@ export declare function buildSelfUpdatePlan(install: RunningInstall): {
116
116
  * to stdout. Progress is summarised on stderr by the caller instead; captured
117
117
  * stderr is kept only to explain a failure.
118
118
  */
119
- export declare function runUpdateQuiet(cmd: string, args: string[], env?: NodeJS.ProcessEnv): UpdateResult;
119
+ export declare function runUpdateQuiet(cmd: string, args: string[], env?: NodeJS.ProcessEnv, cwd?: string): UpdateResult;
120
120
  /**
121
121
  * Serialize self-updates across concurrent `hq` processes AND the hq-sync
122
122
  * menubar app. Without this, a machine running several HQ agents can fire many
@@ -144,7 +144,8 @@ export interface SelfUpdateDeps {
144
144
  homeDir?: string;
145
145
  /** Same-filesystem install rename, injectable for rollback tests. */
146
146
  renamePath?: typeof fs.renameSync;
147
- runner?: (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => UpdateResult;
147
+ runner?: (cmd: string, args: string[], env?: NodeJS.ProcessEnv, cwd?: string) => UpdateResult;
148
+ probePnpmVersion?: typeof probePnpmVersion;
148
149
  /** Delay between bounded npm retries when the just-published target is not visible yet. */
149
150
  sleep?: (ms: number) => Promise<void>;
150
151
  reexec?: (argv: string[], env: NodeJS.ProcessEnv) => number | null;
@@ -65,7 +65,7 @@ import chalk from "chalk";
65
65
  import { Sentry } from "../sentry.js";
66
66
  import { isCliTelemetryDisabled } from "./cli-telemetry.js";
67
67
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
68
- import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
68
+ import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
69
69
  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";
@@ -548,8 +548,12 @@ async function fetchLatestVersion() {
548
548
  * through the resolved prefix so the copy that is actually running is replaced.
549
549
  */
550
550
  export function buildSelfUpdatePlan(install) {
551
- if (install.manager === "pnpm")
552
- return { cmd: "pnpm", args: buildPnpmInstallArgv(install) };
551
+ if (install.manager === "pnpm") {
552
+ const version = pnpmInstalledVersion(install);
553
+ return version
554
+ ? { cmd: "corepack", args: [`pnpm@${version}`, ...buildPnpmInstallArgv(install)] }
555
+ : { cmd: "pnpm", args: buildPnpmInstallArgv(install) };
556
+ }
553
557
  if (install.manager === "bun")
554
558
  return { cmd: "bun", args: buildBunInstallArgv() };
555
559
  if (install.prefix)
@@ -563,12 +567,12 @@ export function buildSelfUpdatePlan(install) {
563
567
  * to stdout. Progress is summarised on stderr by the caller instead; captured
564
568
  * stderr is kept only to explain a failure.
565
569
  */
566
- export function runUpdateQuiet(cmd, args, env) {
570
+ export function runUpdateQuiet(cmd, args, env, cwd) {
567
571
  // File sinks rather than pipes: a pipe dies with this process and would take
568
572
  // the installer down with it mid-write. See `openInstallOutput`.
569
573
  const output = openInstallOutput(false);
570
574
  try {
571
- return runSupervisedUpdateCommand(cmd, args, output, env);
575
+ return runSupervisedUpdateCommand(cmd, args, output, env, cwd);
572
576
  }
573
577
  finally {
574
578
  output.dispose();
@@ -826,6 +830,9 @@ async function updateAndReexec(argv, flavor, known, deps) {
826
830
  if (staged) {
827
831
  return stagedNpmInstall(install, latestValid, { run: runner, env, rename: renamePath });
828
832
  }
833
+ if (install.manager === "pnpm") {
834
+ return performPnpmUpdate(install, runner, updateEnv ?? env, deps.probePnpmVersion);
835
+ }
829
836
  return updateEnv === undefined
830
837
  ? runner(plan.cmd, plan.args)
831
838
  : runner(plan.cmd, plan.args, updateEnv);
@@ -229,9 +229,19 @@ export declare function buildPnpmInstallArgv(install?: RunningInstall): string[]
229
229
  * forward the paths recorded for the install it is replacing.
230
230
  */
231
231
  export declare function pnpmInstallDirs(install: RunningInstall): {
232
+ globalDir: string;
232
233
  storeDir: string;
233
234
  virtualStoreDir: string;
234
235
  } | null;
236
+ /** Version recorded by pnpm for the global install that owns this package. */
237
+ export declare function pnpmInstalledVersion(install: RunningInstall): string | null;
238
+ /**
239
+ * Resolve a bounded pnpm executable probe. pnpm 10 and older reject
240
+ * --virtual-store-dir for global installs; pnpm 11+ accepts the flag.
241
+ */
242
+ export declare function probePnpmVersion(cmd: string, args: string[], env: NodeJS.ProcessEnv, cwd: string): string | null;
243
+ /** Run the install-owned pnpm first; use PATH only if that version cannot run. */
244
+ export declare function performPnpmUpdate(install: RunningInstall, runner?: UpdateRunner, env?: NodeJS.ProcessEnv, probe?: typeof probePnpmVersion): UpdateResult;
235
245
  /** Argv for updating a Bun-managed global install. */
236
246
  export declare function buildBunInstallArgv(): string[];
237
247
  /**
@@ -278,11 +288,12 @@ export type UpdateResult = {
278
288
  ok: boolean;
279
289
  detail?: string;
280
290
  code?: string;
291
+ stdout?: string;
281
292
  /** Sanitized installer error code for diagnostics; never drives retry or exit behavior. */
282
293
  diagnosticCode?: string;
283
294
  failureKind?: "package_manager_exit";
284
295
  };
285
- type UpdateRunner = (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => UpdateResult;
296
+ type UpdateRunner = (cmd: string, args: string[], env?: NodeJS.ProcessEnv, cwd?: string) => UpdateResult;
286
297
  export { buildSpawnPlan, quoteForWindowsShell };
287
298
  /**
288
299
  * Add `detached` to a **synchronous** spawn's options.
@@ -366,9 +377,9 @@ export interface InstallOutput {
366
377
  dispose: () => void;
367
378
  }
368
379
  export declare function openInstallOutput(verbose: boolean, isTty?: boolean): InstallOutput;
369
- export declare function runSupervisedUpdateCommand(cmd: string, args: string[], output: InstallOutput, env?: NodeJS.ProcessEnv): UpdateResult;
370
- export declare function runUpdateCommand(cmd: string, args: string[], env?: NodeJS.ProcessEnv): UpdateResult;
371
- declare function performUpdateCommand(cmd: string, args: string[], runner?: UpdateRunner, env?: NodeJS.ProcessEnv): UpdateResult;
380
+ export declare function runSupervisedUpdateCommand(cmd: string, args: string[], output: InstallOutput, env?: NodeJS.ProcessEnv, cwd?: string): UpdateResult;
381
+ export declare function runUpdateCommand(cmd: string, args: string[], env?: NodeJS.ProcessEnv, cwd?: string): UpdateResult;
382
+ declare function performUpdateCommand(cmd: string, args: string[], runner?: UpdateRunner, env?: NodeJS.ProcessEnv, cwd?: string): UpdateResult;
372
383
  declare function performUpdate(command: string, runner?: UpdateRunner): UpdateResult;
373
384
  /**
374
385
  * Soft notify when the server says we're below `latestVersion` but still ≥
@@ -434,6 +445,7 @@ export declare function checkUpdateConvergence(targetVersion: string, deps?: {
434
445
  /** Injectable surface for {@link enforceUpdateRequired} (unit tests). */
435
446
  interface EnforceUpdateDeps {
436
447
  performUpdateString?: (command: string) => UpdateResult;
448
+ probePnpmVersion?: typeof probePnpmVersion;
437
449
  resolveInstall?: () => RunningInstall;
438
450
  canWriteNpmInstall?: (install: RunningInstall) => boolean;
439
451
  homeDir?: string;
@@ -518,6 +530,10 @@ export declare const __test__: {
518
530
  enforceUpdateRequired: typeof enforceUpdateRequired;
519
531
  isBunManagedPackageDir: typeof isBunManagedPackageDir;
520
532
  pnpmUpdateEnv: typeof pnpmUpdateEnv;
533
+ pnpmInstallDirs: typeof pnpmInstallDirs;
534
+ pnpmInstalledVersion: typeof pnpmInstalledVersion;
535
+ performPnpmUpdate: typeof performPnpmUpdate;
536
+ probePnpmVersion: typeof probePnpmVersion;
521
537
  isLocalDependencyInstall: typeof isLocalDependencyInstall;
522
538
  isLocalDependencyPackageDir: typeof isLocalDependencyPackageDir;
523
539
  isNewerVersion: typeof isNewerVersion;
@@ -626,8 +626,10 @@ export function buildPrefixedInstallArgv(prefix) {
626
626
  export function buildPnpmInstallArgv(install) {
627
627
  const args = ["add", "-g", LATEST_PACKAGE_SPEC];
628
628
  const dirs = install ? pnpmInstallDirs(install) : null;
629
- if (dirs)
630
- args.push("--store-dir", dirs.storeDir, "--virtual-store-dir", dirs.virtualStoreDir);
629
+ if (dirs) {
630
+ args.push("--global-dir", dirs.globalDir, "--store-dir", dirs.storeDir);
631
+ args.push("--virtual-store-dir", dirs.virtualStoreDir);
632
+ }
631
633
  return args;
632
634
  }
633
635
  /**
@@ -655,7 +657,10 @@ export function pnpmInstallDirs(install) {
655
657
  const virtualStoreDir = path.isAbsolute(configuredVirtualStore)
656
658
  ? configuredVirtualStore
657
659
  : path.resolve(globalNodeModules, configuredVirtualStore);
658
- return { storeDir, virtualStoreDir };
660
+ const pnpmHome = derivePnpmHome(install);
661
+ if (!pnpmHome)
662
+ return null;
663
+ return { globalDir: path.join(pnpmHome, "global"), storeDir, virtualStoreDir };
659
664
  }
660
665
  catch {
661
666
  // The metadata file is absent in older pnpm global layouts. The global
@@ -664,11 +669,93 @@ export function pnpmInstallDirs(install) {
664
669
  if (!pnpmHome)
665
670
  return null;
666
671
  return {
672
+ globalDir: path.join(pnpmHome, "global"),
667
673
  storeDir: path.join(pnpmHome, "store", "v11"),
668
674
  virtualStoreDir: path.join(globalNodeModules, ".pnpm"),
669
675
  };
670
676
  }
671
677
  }
678
+ /** Version recorded by pnpm for the global install that owns this package. */
679
+ export function pnpmInstalledVersion(install) {
680
+ if (install.manager !== "pnpm" || !install.packageRoot)
681
+ return null;
682
+ const normalized = install.packageRoot.replace(/\\/g, "/");
683
+ const match = normalized.match(/^(.*\/global\/\d+\/node_modules)(?:\/|$)|^(.*\/global\/v\d+\/[^/]+\/node_modules)(?:\/|$)/);
684
+ const globalNodeModules = match?.[1] ?? match?.[2];
685
+ if (!globalNodeModules)
686
+ return null;
687
+ try {
688
+ const metadata = yaml.load(readFileSync(path.join(globalNodeModules, ".modules.yaml"), "utf8"));
689
+ const value = typeof metadata?.packageManager === "string" ? metadata.packageManager : "";
690
+ const version = value.match(/^pnpm@(.+)$/)?.[1];
691
+ return version && semver.valid(version) ? version : null;
692
+ }
693
+ catch {
694
+ return null;
695
+ }
696
+ }
697
+ /**
698
+ * Resolve a bounded pnpm executable probe. pnpm 10 and older reject
699
+ * --virtual-store-dir for global installs; pnpm 11+ accepts the flag.
700
+ */
701
+ export function probePnpmVersion(cmd, args, env, cwd) {
702
+ try {
703
+ const plan = buildSpawnPlan(cmd, args);
704
+ const result = spawnSync(plan.cmd, plan.args, {
705
+ encoding: "utf8",
706
+ timeout: 10_000,
707
+ cwd,
708
+ env,
709
+ shell: plan.shell,
710
+ windowsHide: true,
711
+ });
712
+ if (result.error || result.status !== 0)
713
+ return null;
714
+ const match = (result.stdout ?? "").trim().match(/(?:^|\s)(\d+\.\d+\.\d+(?:-[\w.-]+)?)(?:\s|$)/);
715
+ return match?.[1] && semver.valid(match[1]) ? match[1] : null;
716
+ }
717
+ catch (error) {
718
+ console.error(chalk.dim(`hq: pnpm version probe failed (${error instanceof Error ? error.name : "unknown"}).`));
719
+ return null;
720
+ }
721
+ }
722
+ /** Run the install-owned pnpm first; use PATH only if that version cannot run. */
723
+ export function performPnpmUpdate(install, runner = runUpdateCommand, env = process.env, probe = probePnpmVersion) {
724
+ const cwd = mkdtempSync(path.join(os.tmpdir(), "hq-cli-pnpm-update-"));
725
+ const updateEnv = {
726
+ ...env,
727
+ COREPACK_ENABLE_PROJECT_SPEC: "0",
728
+ COREPACK_ENABLE_STRICT: "0",
729
+ COREPACK_DEFAULT_TO_LATEST: "0",
730
+ npm_config_manage_package_manager_versions: "false",
731
+ };
732
+ try {
733
+ const installedVersion = pnpmInstalledVersion(install);
734
+ if (installedVersion) {
735
+ const command = `pnpm@${installedVersion}`;
736
+ if (probe("corepack", [command, "--version"], updateEnv, cwd) === installedVersion) {
737
+ return runner("corepack", [command, ...buildPnpmInstallArgv(install)], updateEnv, cwd);
738
+ }
739
+ }
740
+ const fallbackVersion = probe("pnpm", ["--version"], updateEnv, cwd);
741
+ if (!fallbackVersion) {
742
+ return { ok: false, code: "PNPM_UNAVAILABLE", detail: "pnpm not found on PATH; install-owned Corepack pnpm could not run" };
743
+ }
744
+ // pnpm 10.28.1 and earlier reject --virtual-store-dir with -g. Keep the
745
+ // existing global directory and store, but omit only that unsupported flag.
746
+ const supportsGlobalVirtualStoreDir = semver.gte(fallbackVersion, "11.0.0");
747
+ const args = buildPnpmInstallArgv(install);
748
+ if (!supportsGlobalVirtualStoreDir) {
749
+ const index = args.indexOf("--virtual-store-dir");
750
+ if (index !== -1)
751
+ args.splice(index, 2);
752
+ }
753
+ return runner("pnpm", args, updateEnv, cwd);
754
+ }
755
+ finally {
756
+ rmSync(cwd, { recursive: true, force: true });
757
+ }
758
+ }
672
759
  /** Argv for updating a Bun-managed global install. */
673
760
  export function buildBunInstallArgv() {
674
761
  return ["add", "-g", LATEST_PACKAGE_SPEC];
@@ -1037,7 +1124,7 @@ export function openInstallOutput(verbose, isTty = process.stdout.isTTY === true
1037
1124
  },
1038
1125
  };
1039
1126
  }
1040
- export function runSupervisedUpdateCommand(cmd, args, output, env) {
1127
+ export function runSupervisedUpdateCommand(cmd, args, output, env, cwd) {
1041
1128
  try {
1042
1129
  const plan = buildSpawnPlan(cmd, args);
1043
1130
  const timeoutMs = updateTimeoutMs(env);
@@ -1061,6 +1148,7 @@ export function runSupervisedUpdateCommand(cmd, args, output, env) {
1061
1148
  ], inOwnProcessGroup({
1062
1149
  stdio: output.stdio,
1063
1150
  ...(env ? { env } : {}),
1151
+ ...(cwd ? { cwd } : {}),
1064
1152
  }));
1065
1153
  // spawnSync reports a missing executable via `error`, not a throw.
1066
1154
  if (result.error) {
@@ -1108,20 +1196,22 @@ export function runSupervisedUpdateCommand(cmd, args, output, env) {
1108
1196
  };
1109
1197
  }
1110
1198
  }
1111
- export function runUpdateCommand(cmd, args, env) {
1199
+ export function runUpdateCommand(cmd, args, env, cwd) {
1112
1200
  const output = openInstallOutput(true);
1113
1201
  try {
1114
- return runSupervisedUpdateCommand(cmd, args, output, env);
1202
+ return runSupervisedUpdateCommand(cmd, args, output, env, cwd);
1115
1203
  }
1116
1204
  finally {
1117
1205
  output.dispose();
1118
1206
  }
1119
1207
  }
1120
- function performUpdateCommand(cmd, args, runner = runUpdateCommand, env) {
1208
+ function performUpdateCommand(cmd, args, runner = runUpdateCommand, env, cwd) {
1121
1209
  // Forward env only when there is one to forward, so the common path keeps the
1122
1210
  // two-argument runner call it has always made (and existing runner spies see
1123
1211
  // no phantom `undefined` third argument).
1124
- return env === undefined ? runner(cmd, args) : runner(cmd, args, env);
1212
+ return env === undefined && cwd === undefined
1213
+ ? runner(cmd, args)
1214
+ : runner(cmd, args, env, cwd);
1125
1215
  }
1126
1216
  function performUpdate(command, runner = runUpdateCommand) {
1127
1217
  const parts = command.split(/\s+/).filter(Boolean);
@@ -1141,8 +1231,13 @@ function performUpdate(command, runner = runUpdateCommand) {
1141
1231
  * speak the running manager's language.
1142
1232
  */
1143
1233
  function manualUpdateCommand(install, decision) {
1144
- if (install.manager === "pnpm")
1145
- return `pnpm ${buildPnpmInstallArgv(install).join(" ")}`;
1234
+ if (install.manager === "pnpm") {
1235
+ const version = pnpmInstalledVersion(install);
1236
+ const plan = version
1237
+ ? `corepack pnpm@${version} ${buildPnpmInstallArgv(install).join(" ")}`
1238
+ : `pnpm ${buildPnpmInstallArgv(install).join(" ")}`;
1239
+ return plan;
1240
+ }
1146
1241
  if (install.manager === "bun")
1147
1242
  return `bun ${buildBunInstallArgv().join(" ")}`;
1148
1243
  if (install.prefix)
@@ -1522,7 +1617,9 @@ function attemptRequiredUpdate(decision, deps, install) {
1522
1617
  if (isManagedOutsideNpm) {
1523
1618
  console.error(chalk.dim(` Detected a ${install.manager}-managed global install; updating with ${install.manager}`));
1524
1619
  console.error(chalk.dim(` Running: ${primaryCmd} ${primaryArgs.join(" ")}`));
1525
- result = performUpdateCommand(primaryCmd, primaryArgs, runner, updateEnv);
1620
+ result = install.manager === "pnpm"
1621
+ ? performPnpmUpdate(install, runner, updateEnv ?? process.env, deps.probePnpmVersion)
1622
+ : performUpdateCommand(primaryCmd, primaryArgs, runner, updateEnv);
1526
1623
  }
1527
1624
  else if (prefix) {
1528
1625
  console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
@@ -1719,6 +1816,10 @@ export const __test__ = {
1719
1816
  enforceUpdateRequired,
1720
1817
  isBunManagedPackageDir,
1721
1818
  pnpmUpdateEnv,
1819
+ pnpmInstallDirs,
1820
+ pnpmInstalledVersion,
1821
+ performPnpmUpdate,
1822
+ probePnpmVersion,
1722
1823
  isLocalDependencyInstall,
1723
1824
  isLocalDependencyPackageDir,
1724
1825
  isNewerVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.34",
3
+ "version": "5.345.35",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {