@indigoai-us/hq-cli 5.101.7 → 5.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -167,6 +167,51 @@ export function resolveRunningManager() {
167
167
  export function resolveRunningPrefix() {
168
168
  return resolveRunningInstall().prefix;
169
169
  }
170
+ /**
171
+ * Derive `PNPM_HOME` from a pnpm-managed install's own path. pnpm resolves its
172
+ * global bin directory from `PNPM_HOME` (or an explicit `global-bin-dir`), and
173
+ * `PNPM_HOME` is the path segment immediately preceding pnpm's global root, e.g.
174
+ *
175
+ * /home/u/.local/share/pnpm/global/5/… -> PNPM_HOME=/home/u/.local/share/pnpm
176
+ *
177
+ * Returns null for non-pnpm installs or a `packageRoot` with no `/global/`
178
+ * segment (nothing to derive from). Reading it from the running install is more
179
+ * accurate than trusting the inherited environment: it names the exact copy
180
+ * that is actually on PATH.
181
+ */
182
+ export function derivePnpmHome(install) {
183
+ if (install.manager !== "pnpm" || !install.packageRoot)
184
+ return null;
185
+ const normalized = install.packageRoot.replace(/\\/g, "/");
186
+ const marker = normalized.indexOf("/global/");
187
+ if (marker === -1)
188
+ return null;
189
+ const home = normalized.slice(0, marker);
190
+ return home || null;
191
+ }
192
+ /**
193
+ * Environment for the pnpm self-update spawn. `pnpm add -g` aborts with
194
+ * `ERR_PNPM_NO_GLOBAL_BIN_DIR` (exit 1) when neither `PNPM_HOME` nor a
195
+ * `global-bin-dir` setting is present — the exact failure seen when `hq`
196
+ * self-updates from a minimal-environment parent (a systemd `--user` service,
197
+ * cron, or any non-login shell that never sourced the profile exporting
198
+ * `PNPM_HOME`). When the running install is pnpm-managed and the parent lacks
199
+ * `PNPM_HOME`, inject one derived from the install's own path so the update can
200
+ * find the global bin dir it is about to rewrite.
201
+ *
202
+ * A no-op for npm/bun installs, and for any environment that already sets
203
+ * `PNPM_HOME` (never overridden — the caller's value wins). Returns `undefined`
204
+ * when nothing needs injecting, so callers can spawn with the inherited
205
+ * environment unchanged (and pass no `env` at all).
206
+ */
207
+ export function pnpmUpdateEnv(install, base = process.env) {
208
+ if (install.manager !== "pnpm" || base.PNPM_HOME)
209
+ return undefined;
210
+ const home = derivePnpmHome(install);
211
+ if (!home)
212
+ return undefined;
213
+ return { ...base, PNPM_HOME: home };
214
+ }
170
215
  export function buildPrefixedInstallArgv(prefix) {
171
216
  return ["install", "-g", "--prefix", prefix, LATEST_PACKAGE_SPEC];
172
217
  }
@@ -307,12 +352,13 @@ async function fetchVersionDecision() {
307
352
  // public surface (and the `__test__` block below) stable for existing callers
308
353
  // and tests.
309
354
  export { buildSpawnPlan, quoteForWindowsShell };
310
- export function runUpdateCommand(cmd, args) {
355
+ export function runUpdateCommand(cmd, args, env) {
311
356
  try {
312
357
  const plan = buildSpawnPlan(cmd, args);
313
358
  const result = spawnSync(plan.cmd, plan.args, {
314
359
  stdio: "inherit",
315
360
  shell: plan.shell,
361
+ ...(env ? { env } : {}),
316
362
  });
317
363
  // spawnSync reports a missing executable via `error`, not a throw.
318
364
  if (result.error) {
@@ -335,8 +381,11 @@ export function runUpdateCommand(cmd, args) {
335
381
  };
336
382
  }
337
383
  }
338
- function performUpdateCommand(cmd, args, runner = runUpdateCommand) {
339
- return runner(cmd, args);
384
+ function performUpdateCommand(cmd, args, runner = runUpdateCommand, env) {
385
+ // Forward env only when there is one to forward, so the common path keeps the
386
+ // two-argument runner call it has always made (and existing runner spies see
387
+ // no phantom `undefined` third argument).
388
+ return env === undefined ? runner(cmd, args) : runner(cmd, args, env);
340
389
  }
341
390
  function performUpdate(command, runner = runUpdateCommand) {
342
391
  const parts = command.split(/\s+/).filter(Boolean);
@@ -442,11 +491,17 @@ function enforceUpdateRequired(decision, deps = {}) {
442
491
  primaryCmd = parts[0] ?? "";
443
492
  primaryArgs = parts.slice(1);
444
493
  }
494
+ // A pnpm global install needs PNPM_HOME to locate its global bin dir; a
495
+ // minimal-environment parent (systemd, cron, non-login shell) lacks it and
496
+ // `pnpm add -g` would abort with ERR_PNPM_NO_GLOBAL_BIN_DIR. Derive one from
497
+ // the running install so the update is not environment-dependent. No-op for
498
+ // npm/bun and when PNPM_HOME is already set.
499
+ const updateEnv = pnpmUpdateEnv(install);
445
500
  let result;
446
501
  if (isManagedOutsideNpm) {
447
502
  console.error(chalk.dim(` Detected a ${install.manager}-managed global install; updating with ${install.manager}`));
448
503
  console.error(chalk.dim(` Running: ${primaryCmd} ${primaryArgs.join(" ")}`));
449
- result = performUpdateCommand(primaryCmd, primaryArgs, runner);
504
+ result = performUpdateCommand(primaryCmd, primaryArgs, runner, updateEnv);
450
505
  }
451
506
  else if (prefix) {
452
507
  console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
@@ -558,8 +613,10 @@ export const __test__ = {
558
613
  buildPrefixedInstallArgv,
559
614
  buildSpawnPlan,
560
615
  cleanStalePartialInstall,
616
+ derivePnpmHome,
561
617
  enforceUpdateRequired,
562
618
  isBunManagedPackageDir,
619
+ pnpmUpdateEnv,
563
620
  isPnpmManagedPackageDir,
564
621
  npmPrefixFromPackageDir,
565
622
  nudgeUpdateRecommended,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.101.7",
3
+ "version": "5.103.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {