dshmarket 1.48.0 → 1.49.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.
package/client/client.js CHANGED
@@ -105,7 +105,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
105
105
  installing: "安装中…",
106
106
  installedBadge: "✓ 已安装",
107
107
  alreadyInstalled: "✓ 已安装",
108
- restartBanner: "项变更完成,重启 DeepSeek Harness 后生效",
108
+ restartBanner: "项变更需重启 DeepSeek Harness 后生效",
109
109
  uninstall: "卸载",
110
110
  confirmRemove: "确认卸载?",
111
111
  uninstalling: "卸载中…",
@@ -638,7 +638,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
638
638
  installing: "Installing…",
639
639
  installedBadge: "✓ Installed",
640
640
  alreadyInstalled: "✓ Installed",
641
- restartBanner: "change(s) done — restart DeepSeek Harness to apply",
641
+ restartBanner: "change(s) pending restart — restart DeepSeek Harness to apply",
642
642
  uninstall: "Uninstall",
643
643
  confirmRemove: "Uninstall?",
644
644
  uninstalling: "Uninstalling…",
@@ -6348,6 +6348,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6348
6348
  const [buildsSkipped, setBuildsSkipped] = (0, react.useState)(null);
6349
6349
  const [updatingAll, setUpdatingAll] = (0, react.useState)(false);
6350
6350
  const [updatedNames, setUpdatedNames] = (0, react.useState)([]);
6351
+ const [restartNames, setRestartNames] = (0, react.useState)([]);
6351
6352
  const [hotUrls, setHotUrls] = (0, react.useState)([]);
6352
6353
  const [hotNames, setHotNames] = (0, react.useState)([]);
6353
6354
  const [progressLine, setProgressLine] = (0, react.useState)(null);
@@ -6667,12 +6668,13 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6667
6668
  }
6668
6669
  if (Array.isArray(saved.doneUrls) && saved.doneUrls.length > 0) setDoneUrls(saved.doneUrls);
6669
6670
  if (Array.isArray(saved.updated) && saved.updated.length > 0) setUpdatedNames(saved.updated);
6671
+ if (Array.isArray(saved.restartNames) && saved.restartNames.length > 0) setRestartNames(saved.restartNames);
6670
6672
  if (typeof saved.removed === "number" && saved.removed > 0) setRemovedCount(saved.removed);
6671
6673
  if (typeof saved.toggled === "number" && saved.toggled > 0) setToggleRestart(saved.toggled);
6672
6674
  }, [bootId]);
6673
6675
  (0, react.useEffect)(() => {
6674
6676
  if (bootId === null) return;
6675
- if (doneUrls.length === 0 && updatedNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
6677
+ if (doneUrls.length === 0 && updatedNames.length === 0 && restartNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
6676
6678
  sessionStorage.removeItem("dshm-restart");
6677
6679
  return;
6678
6680
  }
@@ -6680,6 +6682,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6680
6682
  boot: bootId,
6681
6683
  doneUrls,
6682
6684
  updated: updatedNames,
6685
+ restartNames,
6683
6686
  removed: removedCount,
6684
6687
  toggled: toggleRestart
6685
6688
  }));
@@ -6687,6 +6690,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6687
6690
  bootId,
6688
6691
  doneUrls,
6689
6692
  updatedNames,
6693
+ restartNames,
6690
6694
  removedCount,
6691
6695
  toggleRestart
6692
6696
  ]);
@@ -7307,6 +7311,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
7307
7311
  if (status === 200 && body.ok) {
7308
7312
  setRecords((list) => patch(list, updateRecordId, { state: "done" }));
7309
7313
  setUpdatedNames((names) => names.concat(name));
7314
+ const activation = body.activation && typeof body.activation === "object" ? body.activation[name] : void 0;
7315
+ if (!activation || activation.state === "restart") setRestartNames((names) => names.includes(name) ? names : names.concat(name));
7310
7316
  if (body.activation && typeof body.activation === "object") setActivations((prev) => ({
7311
7317
  ...prev,
7312
7318
  ...body.activation
@@ -7854,7 +7860,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
7854
7860
  });
7855
7861
  }, [doGroupAction, groups]);
7856
7862
  const selfName = installed["dshmarket"] !== void 0 ? "dshmarket" : "dsh-market";
7857
- const updatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable);
7863
+ const updatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && !effectiveDisabledSet.has(name) && updates[name] && updates[name].updateAvailable);
7858
7864
  const batchUpdatableNames = updatableNames.filter((name) => updates[name]?.restoreRequired !== true);
7859
7865
  const ignoredUpdateSet = (0, react.useMemo)(() => new Set(ignoredUpdateNames), [ignoredUpdateNames]);
7860
7866
  const reminderUpdatableNames = updatableNames.filter((name) => !ignoredUpdateSet.has(name));
@@ -8102,7 +8108,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
8102
8108
  webdavUrl,
8103
8109
  webdavUser
8104
8110
  ]);
8105
- const sessionPendingRestart = doneUrls.length + updatedNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0);
8111
+ const sessionPendingRestart = doneUrls.length + restartNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0);
8106
8112
  /**
8107
8113
  * Plugins the HOST reports as restart-pending, independent of what this
8108
8114
  * browser session happens to remember. Installing and then reloading the
package/lib/dsh-cli.js CHANGED
@@ -230,6 +230,33 @@ export function gitEnvForPnpm(env = process.env) {
230
230
  return {};
231
231
  return { GIT_TERMINAL_PROMPT: '0' };
232
232
  }
233
+ /**
234
+ * Every `--config.<key>=<value>` override in the argv, repeated as the
235
+ * `PNPM_CONFIG_<KEY>` environment variable that pnpm 12 still reads.
236
+ *
237
+ * pnpm 12 ignores some `--config.<key>` overrides on the command line, in
238
+ * either spelling, without a word: `fetchTimeout` (#615; measured on
239
+ * 12.2.1, 12.3.0 and 12.4.1) and `auto-install-peers` (12.4.1 auto-installs
240
+ * the peer with the flag present and not with the variable), so the retries
241
+ * that carry them ran exactly like the first attempt. `PNPM_CONFIG_*` is
242
+ * honoured by 11.8, 11.21 and 12.4 alike, so the argument stays for the
243
+ * versions that read it and the variable carries the same value for the
244
+ * ones that do not. Scoped to the run that carries the flag; nothing is set
245
+ * otherwise, so a user's own values are untouched on every other run. Keys
246
+ * are accepted in either spelling, so respelling a constant (as #600 did
247
+ * for the release-age one) cannot silently drop the variable.
248
+ */
249
+ export function pnpmConfigEnvForArgs(pluginArgs) {
250
+ const env = {};
251
+ for (const arg of pluginArgs) {
252
+ const match = /^--config\.([A-Za-z][A-Za-z0-9-]*)=(\S+)$/.exec(arg);
253
+ if (match === null)
254
+ continue;
255
+ const key = match[1].replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toUpperCase();
256
+ env[`PNPM_CONFIG_${key}`] = match[2];
257
+ }
258
+ return env;
259
+ }
233
260
  function spawnEnv() {
234
261
  // pnpm v10+ blocks forever on a silent interactive prompt without a TTY;
235
262
  // CI mode forces it to act or fail instead of asking.
@@ -739,7 +766,7 @@ export function runDshPlugin(profile, pluginArgs) {
739
766
  // pnpm v10 blocks forever on a silent interactive prompt without a TTY
740
767
  // (observed on re-add over a pinned git spec); CI mode forces it to act
741
768
  // or fail instead of asking.
742
- env: spawnEnv(),
769
+ env: { ...spawnEnv(), ...pnpmConfigEnvForArgs(pluginArgs) },
743
770
  stdio: ['ignore', 'pipe', 'pipe'],
744
771
  viaShell,
745
772
  // Own process group on POSIX so cancel/timeout can kill the whole
package/lib/install.js CHANGED
@@ -29,6 +29,14 @@ export const RELEASE_AGE_OVERRIDE = '--config.minimum-release-age=0';
29
29
  * WHOLE repo even for a `#path:` subdirectory plugin) on slow networks; a
30
30
  * plain retry fails again at the same limit, so the recovery re-runs with
31
31
  * this override once. Scoped to a single command like RELEASE_AGE_OVERRIDE.
32
+ *
33
+ * pnpm 12 ignores this flag on the command line in either spelling (#615).
34
+ * On the CLI runner, runDshPlugin repeats every `--config.<key>` override
35
+ * as PNPM_CONFIG_<KEY>, which pnpm 11 and 12 both read, so the retried
36
+ * command really does get the longer limit there. The Desktop runtime hands
37
+ * its host argv only, so on a Desktop host the retry still depends on that
38
+ * host's pnpm reading the flag. The flag stays: pnpm 11 and earlier read
39
+ * it, and it costs nothing on the versions that do not.
32
40
  */
33
41
  export const FETCH_TIMEOUT_OVERRIDE = '--config.fetchTimeout=600000';
34
42
  /**
@@ -47,6 +55,10 @@ export const FETCH_TIMEOUT_OVERRIDE = '--config.fetchTimeout=600000';
47
55
  * Verified against pnpm 10.29.3: `peerDependencyRules.ignoreMissing` does
48
56
  * NOT prevent the fetch (it only silences the warning), so this flag is the
49
57
  * only lever that actually works.
58
+ *
59
+ * pnpm 12 ignores this flag on the command line too (12.4.1 auto-installs
60
+ * the peer regardless); runDshPlugin repeats it as
61
+ * PNPM_CONFIG_AUTO_INSTALL_PEERS, which 12 reads (#615).
50
62
  */
51
63
  export const AUTO_INSTALL_PEERS_OFF = '--config.auto-install-peers=false';
52
64
  /**
package/lib/restart.js CHANGED
@@ -241,6 +241,10 @@ export function restartLaunch() {
241
241
  * DSH sandbox tool runners) pops a visible node window. Wrapping the launch
242
242
  * in `powershell -WindowStyle Hidden` gives the host a HIDDEN console that
243
243
  * children inherit instead. POSIX keeps the plain detached spawn.
244
+ *
245
+ * The helper that runs this invocation spawns it with `windowsHide`, because
246
+ * the helper has no console of its own to hand down and Windows would
247
+ * otherwise create a visible one for PowerShell (#624).
244
248
  */
245
249
  export function respawnInvocation(launch, platform = process.platform) {
246
250
  if (platform !== 'win32') {
@@ -325,7 +329,17 @@ export function restartHelperSource(spawned, launch, logs, port) {
325
329
  ' try {',
326
330
  ' const out = fs.openSync(logOut, "a")',
327
331
  ' const err = fs.openSync(logErr, "a")',
328
- ' child = spawn(file, args, { cwd, detached, stdio: ["ignore", out, err], env: process.env, shell: viaShell })',
332
+ // windowsHide (#624 by @davidekingsss): this helper is itself detached,
333
+ // so on Windows it has no console, and a console program spawned from a
334
+ // console-less parent is given a NEW, visible one — the "Windows
335
+ // PowerShell" window that owns the replacement and takes it down when
336
+ // closed. `-WindowStyle Hidden` cannot hide it: that flag governs the
337
+ // window PowerShell would create, not the console the spawn handed it.
338
+ // CREATE_NO_WINDOW keeps the console but never shows it, and the host's
339
+ // own console children inherit that hidden console rather than popping
340
+ // windows of their own (measured by the reporter with a console-less
341
+ // launcher: one PseudoConsoleWindow without the flag, none with it).
342
+ ' child = spawn(file, args, { cwd, detached, stdio: ["ignore", out, err], env: process.env, shell: viaShell, windowsHide: true })',
329
343
  // spawn reports a missing or unexecutable file ASYNCHRONOUSLY; the
330
344
  // try/catch below only covers the synchronous throw, so without this
331
345
  // listener that failure is exactly as silent as the bug being fixed.
@@ -112,6 +112,23 @@ export declare function toolSearchDirs(platform?: string, env?: NodeJS.ProcessEn
112
112
  * reports needs Windows, where the spawn is not detached.
113
113
  */
114
114
  export declare function gitEnvForPnpm(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
115
+ /**
116
+ * Every `--config.<key>=<value>` override in the argv, repeated as the
117
+ * `PNPM_CONFIG_<KEY>` environment variable that pnpm 12 still reads.
118
+ *
119
+ * pnpm 12 ignores some `--config.<key>` overrides on the command line, in
120
+ * either spelling, without a word: `fetchTimeout` (#615; measured on
121
+ * 12.2.1, 12.3.0 and 12.4.1) and `auto-install-peers` (12.4.1 auto-installs
122
+ * the peer with the flag present and not with the variable), so the retries
123
+ * that carry them ran exactly like the first attempt. `PNPM_CONFIG_*` is
124
+ * honoured by 11.8, 11.21 and 12.4 alike, so the argument stays for the
125
+ * versions that read it and the variable carries the same value for the
126
+ * ones that do not. Scoped to the run that carries the flag; nothing is set
127
+ * otherwise, so a user's own values are untouched on every other run. Keys
128
+ * are accepted in either spelling, so respelling a constant (as #600 did
129
+ * for the release-age one) cannot silently drop the variable.
130
+ */
131
+ export declare function pnpmConfigEnvForArgs(pluginArgs: readonly string[]): NodeJS.ProcessEnv;
115
132
  /**
116
133
  * Windows npm/corepack/pnpm are `.cmd` shims. Node's `spawn` without a shell
117
134
  * cannot start them (ENOENT / EINVAL). Same pattern as dsh's `plugin` forwarder.
@@ -24,6 +24,14 @@ export declare const RELEASE_AGE_OVERRIDE = "--config.minimum-release-age=0";
24
24
  * WHOLE repo even for a `#path:` subdirectory plugin) on slow networks; a
25
25
  * plain retry fails again at the same limit, so the recovery re-runs with
26
26
  * this override once. Scoped to a single command like RELEASE_AGE_OVERRIDE.
27
+ *
28
+ * pnpm 12 ignores this flag on the command line in either spelling (#615).
29
+ * On the CLI runner, runDshPlugin repeats every `--config.<key>` override
30
+ * as PNPM_CONFIG_<KEY>, which pnpm 11 and 12 both read, so the retried
31
+ * command really does get the longer limit there. The Desktop runtime hands
32
+ * its host argv only, so on a Desktop host the retry still depends on that
33
+ * host's pnpm reading the flag. The flag stays: pnpm 11 and earlier read
34
+ * it, and it costs nothing on the versions that do not.
27
35
  */
28
36
  export declare const FETCH_TIMEOUT_OVERRIDE = "--config.fetchTimeout=600000";
29
37
  /**
@@ -42,6 +50,10 @@ export declare const FETCH_TIMEOUT_OVERRIDE = "--config.fetchTimeout=600000";
42
50
  * Verified against pnpm 10.29.3: `peerDependencyRules.ignoreMissing` does
43
51
  * NOT prevent the fetch (it only silences the warning), so this flag is the
44
52
  * only lever that actually works.
53
+ *
54
+ * pnpm 12 ignores this flag on the command line too (12.4.1 auto-installs
55
+ * the peer regardless); runDshPlugin repeats it as
56
+ * PNPM_CONFIG_AUTO_INSTALL_PEERS, which 12 reads (#615).
45
57
  */
46
58
  export declare const AUTO_INSTALL_PEERS_OFF = "--config.auto-install-peers=false";
47
59
  /**
@@ -132,6 +132,10 @@ export declare function restartLaunch(): {
132
132
  * DSH sandbox tool runners) pops a visible node window. Wrapping the launch
133
133
  * in `powershell -WindowStyle Hidden` gives the host a HIDDEN console that
134
134
  * children inherit instead. POSIX keeps the plain detached spawn.
135
+ *
136
+ * The helper that runs this invocation spawns it with `windowsHide`, because
137
+ * the helper has no console of its own to hand down and Windows would
138
+ * otherwise create a visible one for PowerShell (#624).
135
139
  */
136
140
  export declare function respawnInvocation(launch: {
137
141
  file: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dshmarket",
3
3
  "description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
4
- "version": "1.48.0",
4
+ "version": "1.49.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -1375,6 +1375,11 @@ export function MarketSection(props: MarketSectionProps) {
1375
1375
  const [buildsSkipped, setBuildsSkipped] = useState<{ plugin?: RegistryPlugin; updateName?: string; names: string[]; restore?: boolean } | null>(null)
1376
1376
  const [updatingAll, setUpdatingAll] = useState(false)
1377
1377
  const [updatedNames, setUpdatedNames] = useState<string[]>([])
1378
+ // #558: session-tracked updates the host actually parked behind a restart.
1379
+ // Kept separate from `updatedNames`, which doubles as the row-level
1380
+ // "updated" marker and therefore also records client-only updates that go
1381
+ // live without a restart.
1382
+ const [restartNames, setRestartNames] = useState<string[]>([])
1378
1383
  const [hotUrls, setHotUrls] = useState<string[]>([])
1379
1384
  const [hotNames, setHotNames] = useState<string[]>([])
1380
1385
  const [progressLine, setProgressLine] = useState<string | null>(null)
@@ -1748,13 +1753,14 @@ export function MarketSection(props: MarketSectionProps) {
1748
1753
  }
1749
1754
  if (Array.isArray(saved.doneUrls) && saved.doneUrls.length > 0) setDoneUrls(saved.doneUrls)
1750
1755
  if (Array.isArray(saved.updated) && saved.updated.length > 0) setUpdatedNames(saved.updated)
1756
+ if (Array.isArray(saved.restartNames) && saved.restartNames.length > 0) setRestartNames(saved.restartNames)
1751
1757
  if (typeof saved.removed === 'number' && saved.removed > 0) setRemovedCount(saved.removed)
1752
1758
  if (typeof saved.toggled === 'number' && saved.toggled > 0) setToggleRestart(saved.toggled)
1753
1759
  }, [bootId])
1754
1760
 
1755
1761
  useEffect(() => {
1756
1762
  if (bootId === null) return
1757
- if (doneUrls.length === 0 && updatedNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
1763
+ if (doneUrls.length === 0 && updatedNames.length === 0 && restartNames.length === 0 && removedCount === 0 && toggleRestart === 0) {
1758
1764
  // Nothing pending: drop any stale entry (e.g. a hot mount cleared the
1759
1765
  // only doneUrl) so a same-boot remount cannot resurrect the banner (#73).
1760
1766
  sessionStorage.removeItem('dshm-restart')
@@ -1764,10 +1770,11 @@ export function MarketSection(props: MarketSectionProps) {
1764
1770
  boot: bootId,
1765
1771
  doneUrls,
1766
1772
  updated: updatedNames,
1773
+ restartNames,
1767
1774
  removed: removedCount,
1768
1775
  toggled: toggleRestart,
1769
1776
  }))
1770
- }, [bootId, doneUrls, updatedNames, removedCount, toggleRestart])
1777
+ }, [bootId, doneUrls, updatedNames, restartNames, removedCount, toggleRestart])
1771
1778
 
1772
1779
  const fixEnv = useCallback(() => {
1773
1780
  setEnvFixing(true)
@@ -2412,6 +2419,14 @@ export function MarketSection(props: MarketSectionProps) {
2412
2419
  if (status === 200 && body.ok) {
2413
2420
  setRecords(list => patchRecord(list, updateRecordId, { state: 'done' }))
2414
2421
  setUpdatedNames(names => names.concat(name))
2422
+ // #558: the restart banner counts plugins the host parked behind a
2423
+ // restart, not every completed change. A client-only plugin comes
2424
+ // back 'inert'/'live' and goes live on refresh; when the host
2425
+ // reports no activation at all, count it to stay on the safe side.
2426
+ const activation = body.activation && typeof body.activation === 'object' ? body.activation[name] : undefined
2427
+ if (!activation || activation.state === 'restart') {
2428
+ setRestartNames(names => names.includes(name) ? names : names.concat(name))
2429
+ }
2415
2430
  if (body.activation && typeof body.activation === 'object') {
2416
2431
  setActivations(prev => ({ ...prev, ...body.activation }))
2417
2432
  }
@@ -2972,7 +2987,11 @@ export function MarketSection(props: MarketSectionProps) {
2972
2987
  // mid-run, which would strand the remaining items.
2973
2988
  const selfName = installed['dshmarket'] !== undefined ? 'dshmarket' : 'dsh-market'
2974
2989
  const updatableNames = Object.keys(installed).filter(
2975
- name => name !== selfName && !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable,
2990
+ name => name !== selfName
2991
+ && !updatedNames.includes(name)
2992
+ && !effectiveDisabledSet.has(name)
2993
+ && updates[name]
2994
+ && updates[name].updateAvailable,
2976
2995
  )
2977
2996
  // Replacing a local source with its catalog source is deliberately not a
2978
2997
  // batch update: every such plugin has an existing, explicit confirmation
@@ -3233,7 +3252,10 @@ export function MarketSection(props: MarketSectionProps) {
3233
3252
  if (Date.now() - last >= 24 * 60 * 60 * 1000) runWebdav('backup')
3234
3253
  }, [autoBackup, runWebdav, webdavUrl, webdavUser])
3235
3254
 
3236
- const sessionPendingRestart = doneUrls.length + updatedNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0)
3255
+ // #558: count restart-pending session changes (restartNames), not every
3256
+ // completed `updatedNames` entry (which would double-count a batch update
3257
+ // that reports both a done URL and a name for the same plugin).
3258
+ const sessionPendingRestart = doneUrls.length + restartNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0)
3237
3259
  /**
3238
3260
  * Plugins the HOST reports as restart-pending, independent of what this
3239
3261
  * browser session happens to remember. Installing and then reloading the
@@ -72,7 +72,7 @@ export const zh = {
72
72
  installing: '安装中…',
73
73
  installedBadge: '✓ 已安装',
74
74
  alreadyInstalled: '✓ 已安装',
75
- restartBanner: '项变更完成,重启 DeepSeek Harness 后生效',
75
+ restartBanner: '项变更需重启 DeepSeek Harness 后生效',
76
76
  uninstall: '卸载',
77
77
  confirmRemove: '确认卸载?',
78
78
  uninstalling: '卸载中…',
@@ -613,7 +613,7 @@ export const en: Record<MarketKey, string> = {
613
613
  installing: 'Installing…',
614
614
  installedBadge: '✓ Installed',
615
615
  alreadyInstalled: '✓ Installed',
616
- restartBanner: 'change(s) done — restart DeepSeek Harness to apply',
616
+ restartBanner: 'change(s) pending restart — restart DeepSeek Harness to apply',
617
617
  uninstall: 'Uninstall',
618
618
  confirmRemove: 'Uninstall?',
619
619
  uninstalling: 'Uninstalling…',
package/src/dsh-cli.ts CHANGED
@@ -232,6 +232,33 @@ export function gitEnvForPnpm(env: NodeJS.ProcessEnv = process.env): NodeJS.Proc
232
232
  return { GIT_TERMINAL_PROMPT: '0' }
233
233
  }
234
234
 
235
+ /**
236
+ * Every `--config.<key>=<value>` override in the argv, repeated as the
237
+ * `PNPM_CONFIG_<KEY>` environment variable that pnpm 12 still reads.
238
+ *
239
+ * pnpm 12 ignores some `--config.<key>` overrides on the command line, in
240
+ * either spelling, without a word: `fetchTimeout` (#615; measured on
241
+ * 12.2.1, 12.3.0 and 12.4.1) and `auto-install-peers` (12.4.1 auto-installs
242
+ * the peer with the flag present and not with the variable), so the retries
243
+ * that carry them ran exactly like the first attempt. `PNPM_CONFIG_*` is
244
+ * honoured by 11.8, 11.21 and 12.4 alike, so the argument stays for the
245
+ * versions that read it and the variable carries the same value for the
246
+ * ones that do not. Scoped to the run that carries the flag; nothing is set
247
+ * otherwise, so a user's own values are untouched on every other run. Keys
248
+ * are accepted in either spelling, so respelling a constant (as #600 did
249
+ * for the release-age one) cannot silently drop the variable.
250
+ */
251
+ export function pnpmConfigEnvForArgs(pluginArgs: readonly string[]): NodeJS.ProcessEnv {
252
+ const env: NodeJS.ProcessEnv = {}
253
+ for (const arg of pluginArgs) {
254
+ const match = /^--config\.([A-Za-z][A-Za-z0-9-]*)=(\S+)$/.exec(arg)
255
+ if (match === null) continue
256
+ const key = match[1]!.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toUpperCase()
257
+ env[`PNPM_CONFIG_${key}`] = match[2]!
258
+ }
259
+ return env
260
+ }
261
+
235
262
  function spawnEnv(): NodeJS.ProcessEnv {
236
263
  // pnpm v10+ blocks forever on a silent interactive prompt without a TTY;
237
264
  // CI mode forces it to act or fail instead of asking.
@@ -913,7 +940,7 @@ export function runDshPlugin(profile: string, pluginArgs: string[]): Promise<Ins
913
940
  // pnpm v10 blocks forever on a silent interactive prompt without a TTY
914
941
  // (observed on re-add over a pinned git spec); CI mode forces it to act
915
942
  // or fail instead of asking.
916
- env: spawnEnv(),
943
+ env: { ...spawnEnv(), ...pnpmConfigEnvForArgs(pluginArgs) },
917
944
  stdio: ['ignore', 'pipe', 'pipe'],
918
945
  viaShell,
919
946
  // Own process group on POSIX so cancel/timeout can kill the whole
package/src/install.ts CHANGED
@@ -33,6 +33,14 @@ export const RELEASE_AGE_OVERRIDE = '--config.minimum-release-age=0'
33
33
  * WHOLE repo even for a `#path:` subdirectory plugin) on slow networks; a
34
34
  * plain retry fails again at the same limit, so the recovery re-runs with
35
35
  * this override once. Scoped to a single command like RELEASE_AGE_OVERRIDE.
36
+ *
37
+ * pnpm 12 ignores this flag on the command line in either spelling (#615).
38
+ * On the CLI runner, runDshPlugin repeats every `--config.<key>` override
39
+ * as PNPM_CONFIG_<KEY>, which pnpm 11 and 12 both read, so the retried
40
+ * command really does get the longer limit there. The Desktop runtime hands
41
+ * its host argv only, so on a Desktop host the retry still depends on that
42
+ * host's pnpm reading the flag. The flag stays: pnpm 11 and earlier read
43
+ * it, and it costs nothing on the versions that do not.
36
44
  */
37
45
  export const FETCH_TIMEOUT_OVERRIDE = '--config.fetchTimeout=600000'
38
46
 
@@ -52,6 +60,10 @@ export const FETCH_TIMEOUT_OVERRIDE = '--config.fetchTimeout=600000'
52
60
  * Verified against pnpm 10.29.3: `peerDependencyRules.ignoreMissing` does
53
61
  * NOT prevent the fetch (it only silences the warning), so this flag is the
54
62
  * only lever that actually works.
63
+ *
64
+ * pnpm 12 ignores this flag on the command line too (12.4.1 auto-installs
65
+ * the peer regardless); runDshPlugin repeats it as
66
+ * PNPM_CONFIG_AUTO_INSTALL_PEERS, which 12 reads (#615).
55
67
  */
56
68
  export const AUTO_INSTALL_PEERS_OFF = '--config.auto-install-peers=false'
57
69
 
package/src/restart.ts CHANGED
@@ -246,6 +246,10 @@ export function restartLaunch(): { file: string; args: string[]; cwd: string; vi
246
246
  * DSH sandbox tool runners) pops a visible node window. Wrapping the launch
247
247
  * in `powershell -WindowStyle Hidden` gives the host a HIDDEN console that
248
248
  * children inherit instead. POSIX keeps the plain detached spawn.
249
+ *
250
+ * The helper that runs this invocation spawns it with `windowsHide`, because
251
+ * the helper has no console of its own to hand down and Windows would
252
+ * otherwise create a visible one for PowerShell (#624).
249
253
  */
250
254
  export function respawnInvocation(
251
255
  launch: { file: string; args: string[]; viaShell: boolean },
@@ -347,7 +351,17 @@ export function restartHelperSource(
347
351
  ' try {',
348
352
  ' const out = fs.openSync(logOut, "a")',
349
353
  ' const err = fs.openSync(logErr, "a")',
350
- ' child = spawn(file, args, { cwd, detached, stdio: ["ignore", out, err], env: process.env, shell: viaShell })',
354
+ // windowsHide (#624 by @davidekingsss): this helper is itself detached,
355
+ // so on Windows it has no console, and a console program spawned from a
356
+ // console-less parent is given a NEW, visible one — the "Windows
357
+ // PowerShell" window that owns the replacement and takes it down when
358
+ // closed. `-WindowStyle Hidden` cannot hide it: that flag governs the
359
+ // window PowerShell would create, not the console the spawn handed it.
360
+ // CREATE_NO_WINDOW keeps the console but never shows it, and the host's
361
+ // own console children inherit that hidden console rather than popping
362
+ // windows of their own (measured by the reporter with a console-less
363
+ // launcher: one PseudoConsoleWindow without the flag, none with it).
364
+ ' child = spawn(file, args, { cwd, detached, stdio: ["ignore", out, err], env: process.env, shell: viaShell, windowsHide: true })',
351
365
  // spawn reports a missing or unexecutable file ASYNCHRONOUSLY; the
352
366
  // try/catch below only covers the synchronous throw, so without this
353
367
  // listener that failure is exactly as silent as the bug being fixed.