@indigoai-us/hq-cli 5.345.33 → 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.
@@ -55,6 +55,8 @@ export interface InstallPaths {
55
55
  * host has no conhost (the task then runs node directly).
56
56
  */
57
57
  windowsHeadlessLauncher?: string;
58
+ /** Windows command processor used to execute the stable npm .cmd shim. */
59
+ windowsCommandProcessor?: string;
58
60
  /** When true, render the root/ec2-user system unit instead of user unit. */
59
61
  systemUser?: string;
60
62
  /** Verified HQ root to persist and pass to the installed service. */
@@ -102,7 +104,6 @@ export declare function restartDaemonServiceIfInstalled(opts?: {
102
104
  env?: NodeJS.ProcessEnv;
103
105
  existsSync?: typeof fs.existsSync;
104
106
  readFileSync?: typeof fs.readFileSync;
105
- writeFileSync?: typeof fs.writeFileSync;
106
107
  run?: (command: string, args: string[]) => {
107
108
  status: number | null;
108
109
  stdout?: string;
@@ -119,7 +120,7 @@ export declare function detectPlatform(platform?: NodeJS.Platform): DaemonInstal
119
120
  * dies with MODULE_NOT_FOUND on the next restart. Prefer the live npm-global
120
121
  * binary when both exist.
121
122
  */
122
- export declare function preferStableHqBinary(entry: string): string;
123
+ export declare function preferStableHqBinary(entry: string, platform?: DaemonInstallPlatform, existsSync?: typeof fs.existsSync): string;
123
124
  /**
124
125
  * Absolute path of the CLI entry that is executing this process.
125
126
  * realpath of argv[1] only — never a PATH lookup of `hq`.
@@ -153,7 +154,6 @@ export declare function installDaemonIfStale(opts: {
153
154
  run?: RunCommand;
154
155
  uid?: number;
155
156
  sleep?: SleepFn;
156
- writeFileSync?: typeof fs.writeFileSync;
157
157
  mkdirSync?: typeof fs.mkdirSync;
158
158
  reloadSystemd?: () => {
159
159
  status: number | null;
@@ -193,6 +193,10 @@ export declare function buildInstallPaths(opts: {
193
193
  windowsUser?: string;
194
194
  /** Windows headless launcher override (tests); defaults to %SystemRoot%\System32\conhost.exe when present. */
195
195
  windowsHeadlessLauncher?: string | null;
196
+ /** Platform override for unit path and command rendering tests. */
197
+ platform?: DaemonInstallPlatform;
198
+ /** Filesystem seam for stable hq shim resolution tests. */
199
+ existsSync?: typeof fs.existsSync;
196
200
  }): InstallPaths;
197
201
  /** `DOMAIN\user` for the current account, as Task Scheduler expects. */
198
202
  export declare function resolveWindowsUser(env?: NodeJS.ProcessEnv): string;
@@ -277,7 +281,6 @@ export declare function installDaemonService(opts: {
277
281
  paths?: InstallPaths;
278
282
  /** When set, write the system unit template (does not sudo-install). */
279
283
  systemUser?: string;
280
- writeFileSync?: typeof fs.writeFileSync;
281
284
  mkdirSync?: typeof fs.mkdirSync;
282
285
  /** Optional: actually load the service (disabled in tests). */
283
286
  activate?: (platform: DaemonInstallPlatform, dest: string) => void;
@@ -16,6 +16,7 @@ import * as os from "node:os";
16
16
  import * as path from "node:path";
17
17
  import { spawnSync } from "node:child_process";
18
18
  import semver from "semver";
19
+ import { atomicWrite } from "../../../index-render/shared.js";
19
20
  import { resolveDaemonAssetDir } from "./log.js";
20
21
  import { LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, WINDOWS_TASK_NAME, daemonDir, daemonLogPath, daemonPidPath, } from "./paths.js";
21
22
  import { isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, verifyHqRoot } from "./config.js";
@@ -123,17 +124,25 @@ function migrateLegacyDaemonUnit(opts) {
123
124
  // The rendered command only has to name a binary; install already resolved one.
124
125
  }
125
126
  if (platform === "darwin") {
126
- opts.writeFileSync(unitPath, renderLaunchdPlist(paths), { mode: 0o600 });
127
+ replaceUnitFile(unitPath, renderLaunchdPlist(paths), 0o600, () => {
128
+ activateLaunchd(unitPath, asRunCommand(opts.run), opts.uid, { sleep: opts.sleep });
129
+ });
127
130
  return;
128
131
  }
129
132
  if (platform === "linux") {
130
- opts.writeFileSync(unitPath, renderSystemdUserUnit(paths), { mode: 0o600 });
131
- opts.run("systemctl", ["--user", "daemon-reload"]);
133
+ replaceUnitFile(unitPath, renderSystemdUserUnit(paths), 0o600, () => {
134
+ const result = opts.run("systemctl", ["--user", "daemon-reload"]);
135
+ if (result.status !== 0)
136
+ throw new Error("systemctl --user daemon-reload failed while repairing the mesh unit");
137
+ });
132
138
  return;
133
139
  }
134
140
  const rendered = renderWindowsTaskXml(paths);
135
- opts.writeFileSync(unitPath, encodeWindowsTaskXml(rendered));
136
- opts.run("schtasks.exe", ["/Create", "/TN", WINDOWS_TASK_NAME, "/XML", unitPath, "/F"]);
141
+ replaceUnitFile(unitPath, encodeWindowsTaskXml(rendered), 0o600, () => {
142
+ const result = opts.run("schtasks.exe", ["/Create", "/TN", WINDOWS_TASK_NAME, "/XML", unitPath, "/F"]);
143
+ if (result.status !== 0)
144
+ throw new Error("schtasks /Create failed while repairing the mesh unit");
145
+ });
137
146
  }
138
147
  /**
139
148
  * Restart the installed user daemon after a CLI upgrade. This is deliberately
@@ -155,7 +164,6 @@ export function restartDaemonServiceIfInstalled(opts = {}) {
155
164
  const home = opts.home ?? os.homedir();
156
165
  const existsSync = opts.existsSync ?? fs.existsSync;
157
166
  const readFileSync = opts.readFileSync ?? fs.readFileSync;
158
- const writeFileSync = opts.writeFileSync ?? fs.writeFileSync;
159
167
  const run = opts.run ?? ((command, args) => {
160
168
  const result = spawnSync(command, args, { encoding: "utf8" });
161
169
  return {
@@ -165,7 +173,17 @@ export function restartDaemonServiceIfInstalled(opts = {}) {
165
173
  };
166
174
  });
167
175
  try {
168
- migrateLegacyDaemonUnit({ platform, home, hqRoot: opts.hqRoot, env: opts.env, existsSync, readFileSync, writeFileSync, run });
176
+ migrateLegacyDaemonUnit({
177
+ platform,
178
+ home,
179
+ hqRoot: opts.hqRoot,
180
+ env: opts.env,
181
+ existsSync,
182
+ readFileSync,
183
+ run,
184
+ uid: opts.uid,
185
+ sleep: opts.sleep,
186
+ });
169
187
  if (platform === "darwin") {
170
188
  const plist = path.join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
171
189
  if (!existsSync(plist))
@@ -239,14 +257,46 @@ export function detectPlatform(platform = process.platform) {
239
257
  * dies with MODULE_NOT_FOUND on the next restart. Prefer the live npm-global
240
258
  * binary when both exist.
241
259
  */
242
- export function preferStableHqBinary(entry) {
243
- const stale = `${path.sep}toolchain${path.sep}node${path.sep}bin${path.sep}`;
244
- const stable = `${path.sep}toolchain${path.sep}npm-global${path.sep}bin${path.sep}`;
245
- if (!entry.includes(stale))
246
- return entry;
247
- const candidate = entry.replace(stale, stable);
248
- if (candidate !== entry && fs.existsSync(candidate))
249
- return path.resolve(candidate);
260
+ export function preferStableHqBinary(entry, platform = detectPlatform(), existsSync = fs.existsSync) {
261
+ const api = platform === "win32" ? path.win32 : path;
262
+ const sep = api.sep;
263
+ const stale = `${sep}toolchain${sep}node${sep}bin${sep}`;
264
+ const stable = `${sep}toolchain${sep}npm-global${sep}bin${sep}`;
265
+ if (entry.includes(stale)) {
266
+ const candidate = entry.replace(stale, stable);
267
+ if (candidate !== entry && existsSync(candidate))
268
+ return api.resolve(candidate);
269
+ }
270
+ const normalized = entry.replace(/\\/g, "/");
271
+ const parts = normalized.split("/").filter(Boolean);
272
+ let packageAt = -1;
273
+ for (let i = 0; i < parts.length - 2; i += 1) {
274
+ if (parts[i]?.toLowerCase() === "node_modules"
275
+ && parts[i + 1]?.toLowerCase() === "@indigoai-us"
276
+ && parts[i + 2]?.toLowerCase() === "hq-cli") {
277
+ packageAt = i;
278
+ break;
279
+ }
280
+ }
281
+ if (packageAt >= 0) {
282
+ const prefixParts = parts.slice(0, packageAt);
283
+ let prefix;
284
+ if (platform === "win32") {
285
+ const root = path.win32.parse(entry).root;
286
+ const rootParts = root.replace(/\\+$/, "").split(/[\\/]+/).filter(Boolean);
287
+ prefix = path.win32.resolve(root, ...prefixParts.slice(rootParts.length));
288
+ }
289
+ else {
290
+ if (prefixParts.at(-1) === "lib")
291
+ prefixParts.pop();
292
+ prefix = path.posix.resolve(`/${prefixParts.join("/")}`);
293
+ }
294
+ const shim = platform === "win32"
295
+ ? path.win32.join(prefix, "hq.cmd")
296
+ : path.posix.join(prefix, "bin", "hq");
297
+ if (existsSync(shim))
298
+ return api.resolve(shim);
299
+ }
250
300
  return entry;
251
301
  }
252
302
  /**
@@ -346,7 +396,6 @@ export function installDaemonIfStale(opts) {
346
396
  platform,
347
397
  paths: opts.paths,
348
398
  systemUser: opts.systemUser,
349
- writeFileSync: opts.writeFileSync,
350
399
  mkdirSync: opts.mkdirSync,
351
400
  reloadSystemd: opts.reloadSystemd,
352
401
  schtasks: opts.schtasks,
@@ -357,9 +406,6 @@ export function installDaemonIfStale(opts) {
357
406
  uid: opts.uid,
358
407
  sleep: opts.sleep,
359
408
  });
360
- if (platform === "linux" && !opts.systemUser && !opts.paths.systemUser) {
361
- activateSystemd(SYSTEMD_UNIT_NAME, run);
362
- }
363
409
  return {
364
410
  reinstalled: true,
365
411
  message: `${installed.message} Restarted with ${opts.paths.nodeBinary} ${opts.paths.hqBinary}.`,
@@ -413,7 +459,8 @@ export function buildInstallPaths(opts) {
413
459
  const home = opts.home ?? os.homedir();
414
460
  const env = opts.env ?? process.env;
415
461
  const ddir = daemonDir(opts.meshRoot, home, env);
416
- const hqBinary = opts.hqBinary ?? resolveHqBinary(opts.argv, env);
462
+ const platform = opts.platform ?? detectPlatform();
463
+ const hqBinary = preferStableHqBinary(opts.hqBinary ?? resolveHqBinary(opts.argv, env), platform, opts.existsSync ?? fs.existsSync);
417
464
  const nodeBinary = opts.nodeBinary ?? resolveNodeBinary(opts.execPath, env);
418
465
  return {
419
466
  home,
@@ -430,6 +477,7 @@ export function buildInstallPaths(opts) {
430
477
  windowsHeadlessLauncher: opts.windowsHeadlessLauncher === undefined
431
478
  ? resolveWindowsHeadlessLauncher(env)
432
479
  : (opts.windowsHeadlessLauncher ?? undefined),
480
+ windowsCommandProcessor: path.win32.join(env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"),
433
481
  systemUser: opts.systemUser,
434
482
  hqRoot: opts.hqRoot,
435
483
  hqRootSource: opts.hqRootSource,
@@ -480,7 +528,12 @@ export function windowsLogonNotice(paths) {
480
528
  function describeWindowsLaunchMode(paths) {
481
529
  return windowsLaunchMode(paths) === "conhost-headless"
482
530
  ? `mode: conhost --headless via ${paths.windowsHeadlessLauncher}`
483
- : "mode: direct node (conhost --headless unavailable on this host; a console window may appear at logon)";
531
+ : isWindowsCommandShim(paths.hqBinary)
532
+ ? "mode: direct command processor (conhost --headless unavailable on this host; a console window may appear at logon)"
533
+ : "mode: direct node (conhost --headless unavailable on this host; a console window may appear at logon)";
534
+ }
535
+ function isWindowsCommandShim(file) {
536
+ return path.win32.extname(file).toLowerCase() === ".cmd";
484
537
  }
485
538
  function safeUsername() {
486
539
  try {
@@ -601,13 +654,19 @@ WantedBy=multi-user.target
601
654
  */
602
655
  export function renderWindowsTaskXml(paths) {
603
656
  const user = escapeXml(paths.windowsUser);
604
- const nodeArg = windowsArgQuote(paths.nodeBinary);
657
+ const stableShim = isWindowsCommandShim(paths.hqBinary);
605
658
  const hqArg = windowsArgQuote(paths.hqBinary);
606
- const command = paths.windowsHeadlessLauncher ?? paths.nodeBinary;
659
+ const commandProcessor = paths.windowsCommandProcessor ?? path.win32.join("C:\\Windows", "System32", "cmd.exe");
660
+ const command = stableShim ? (paths.windowsHeadlessLauncher ?? commandProcessor) : (paths.windowsHeadlessLauncher ?? paths.nodeBinary);
607
661
  const rootArgs = paths.hqRoot ? ` --hq-root ${windowsArgQuote(paths.hqRoot)}` : "";
608
- const args = paths.windowsHeadlessLauncher
609
- ? `--headless ${nodeArg} ${hqArg} mesh daemon run${rootArgs}`
610
- : `${hqArg} mesh daemon run${rootArgs}`;
662
+ const commandLine = `${hqArg} mesh daemon run${rootArgs}`;
663
+ const args = stableShim
664
+ ? paths.windowsHeadlessLauncher
665
+ ? `--headless ${windowsArgQuote(commandProcessor)} /d /s /c "${commandLine}"`
666
+ : `/d /s /c "${commandLine}"`
667
+ : paths.windowsHeadlessLauncher
668
+ ? `--headless ${windowsArgQuote(paths.nodeBinary)} ${hqArg} mesh daemon run${rootArgs}`
669
+ : `${hqArg} mesh daemon run${rootArgs}`;
611
670
  return `<?xml version="1.0" encoding="UTF-16"?>
612
671
  <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
613
672
  <RegistrationInfo>
@@ -769,7 +828,7 @@ export function parseWindowsTaskHqRoot(xml) {
769
828
  if (quoted)
770
829
  return quoted[1].replace(/\\"/g, '"');
771
830
  const bare = /--hq-root (\S+)/.exec(args);
772
- return bare ? bare[1] : null;
831
+ return bare ? bare[1].replace(/"+$/, "") : null;
773
832
  }
774
833
  /**
775
834
  * The HQ root the installed Windows task actually carries: the task XML on
@@ -812,11 +871,40 @@ function shellQuote(value) {
812
871
  return value;
813
872
  return `'${value.replace(/'/g, `'\\''`)}'`;
814
873
  }
874
+ function replaceUnitFile(file, content, mode, reload) {
875
+ let previous = null;
876
+ let previousMode = mode;
877
+ try {
878
+ previous = fs.readFileSync(file);
879
+ previousMode = fs.statSync(file).mode & 0o777;
880
+ }
881
+ catch (error) {
882
+ if (error.code !== "ENOENT")
883
+ throw error;
884
+ }
885
+ try {
886
+ atomicWrite(file, content, { mode });
887
+ reload?.();
888
+ }
889
+ catch (error) {
890
+ try {
891
+ if (previous) {
892
+ atomicWrite(file, previous, { mode: previousMode });
893
+ }
894
+ else {
895
+ fs.rmSync(file, { force: true });
896
+ }
897
+ }
898
+ catch (rollbackError) {
899
+ throw new AggregateError([error, rollbackError], `Unit replacement and rollback failed: ${file}`);
900
+ }
901
+ throw error;
902
+ }
903
+ }
815
904
  export function installDaemonService(opts) {
816
905
  const platform = opts.platform ?? detectPlatform();
817
906
  const paths = opts.paths ??
818
907
  buildInstallPaths({ systemUser: opts.systemUser });
819
- const writeFileSync = opts.writeFileSync ?? fs.writeFileSync;
820
908
  const mkdirSync = opts.mkdirSync ?? fs.mkdirSync;
821
909
  mkdirSync(paths.daemonDir, { recursive: true, mode: 0o700 });
822
910
  registerTrustedHqRoot(paths.daemonDir, paths.hqRoot);
@@ -832,13 +920,12 @@ export function installDaemonService(opts) {
832
920
  if (platform === "darwin") {
833
921
  const rendered = renderLaunchdPlist(paths);
834
922
  mkdirSync(paths.launchAgentsDir, { recursive: true });
835
- writeFileSync(paths.plistPath, rendered, { mode: 0o600 });
836
- if (opts.run) {
837
- activateLaunchd(paths.plistPath, opts.run, opts.uid, { sleep: opts.sleep });
838
- }
839
- else {
840
- opts.activate?.(platform, paths.plistPath);
841
- }
923
+ replaceUnitFile(paths.plistPath, rendered, 0o600, () => {
924
+ if (opts.run)
925
+ activateLaunchd(paths.plistPath, opts.run, opts.uid, { sleep: opts.sleep });
926
+ else
927
+ opts.activate?.(platform, paths.plistPath);
928
+ });
842
929
  return {
843
930
  platform,
844
931
  action: "install",
@@ -856,7 +943,7 @@ export function installDaemonService(opts) {
856
943
  });
857
944
  // Write under daemon dir as a template the operator can sudo-install.
858
945
  const dest = path.join(paths.daemonDir, SYSTEMD_UNIT_NAME);
859
- writeFileSync(dest, rendered, { mode: 0o644 });
946
+ replaceUnitFile(dest, rendered, 0o644);
860
947
  return {
861
948
  platform,
862
949
  action: "install",
@@ -870,9 +957,18 @@ export function installDaemonService(opts) {
870
957
  }
871
958
  const rendered = renderSystemdUserUnit(paths);
872
959
  mkdirSync(paths.systemdUserDir, { recursive: true });
873
- writeFileSync(paths.systemdUnitPath, rendered, { mode: 0o600 });
874
- (opts.reloadSystemd ?? (() => spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" })))();
875
- opts.activate?.(platform, paths.systemdUnitPath);
960
+ replaceUnitFile(paths.systemdUnitPath, rendered, 0o600, () => {
961
+ const result = (opts.reloadSystemd ?? (() => {
962
+ const reload = spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
963
+ return { status: reload.status };
964
+ }))();
965
+ if (result.status !== 0)
966
+ throw new Error("systemctl --user daemon-reload failed while installing the mesh unit");
967
+ if (opts.run)
968
+ activateSystemd(SYSTEMD_UNIT_NAME, opts.run);
969
+ else
970
+ opts.activate?.(platform, paths.systemdUnitPath);
971
+ });
876
972
  return {
877
973
  platform,
878
974
  action: "install",
@@ -885,14 +981,15 @@ export function installDaemonService(opts) {
885
981
  if (platform === "win32") {
886
982
  const schtasks = opts.schtasks ?? defaultSchtasks;
887
983
  const rendered = renderWindowsTaskXml(paths);
888
- writeFileSync(paths.windowsTaskXmlPath, encodeWindowsTaskXml(rendered), { mode: 0o600 });
889
- const created = schtasks([
890
- "/Create", "/TN", WINDOWS_TASK_NAME, "/XML", paths.windowsTaskXmlPath, "/F",
891
- ]);
892
- if (created.status !== 0) {
893
- throw new Error(`schtasks /Create failed for ${WINDOWS_TASK_NAME} (exit ${created.status ?? "signal"}): ` +
894
- `${(created.stderr || created.stdout).trim()}`);
895
- }
984
+ replaceUnitFile(paths.windowsTaskXmlPath, encodeWindowsTaskXml(rendered), 0o600, () => {
985
+ const created = schtasks([
986
+ "/Create", "/TN", WINDOWS_TASK_NAME, "/XML", paths.windowsTaskXmlPath, "/F",
987
+ ]);
988
+ if (created.status !== 0) {
989
+ throw new Error(`schtasks /Create failed for ${WINDOWS_TASK_NAME} (exit ${created.status ?? "signal"}): ` +
990
+ `${(created.stderr || created.stdout).trim()}`);
991
+ }
992
+ });
896
993
  // /F replaced any previous registration without ending its instance; stop
897
994
  // the recorded daemon so the fresh task instance can take the pid lock.
898
995
  schtasks(["/End", "/TN", WINDOWS_TASK_NAME]);
@@ -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;