@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 +4 -0
- package/dist/commands/core-resolve-company-native.d.ts +2 -0
- package/dist/commands/core-resolve-company-native.js +54 -17
- package/dist/commands/install-global.d.ts +1 -1
- package/dist/commands/install-global.js +5 -4
- package/dist/commands/pkg-install.js +5 -3
- package/dist/utils/self-update.d.ts +4 -3
- package/dist/utils/self-update.js +12 -5
- package/dist/utils/version-gate.d.ts +20 -4
- package/dist/utils/version-gate.js +112 -11
- package/package.json +1 -1
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
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
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
|
-
|
|
354
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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 ===
|
|
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(
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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("--
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|