@linxin666/dsh-remote-web-ui 0.1.13 → 0.1.14

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/lib/index.js CHANGED
@@ -1715,30 +1715,85 @@ async function checkUpdates(deps) {
1715
1715
  /** Cap on captured pnpm output (keeps error payloads bounded). */
1716
1716
  const OUTPUT_CAP = 16 * 1024;
1717
1717
  /**
1718
- * Run the update: `pnpm update <packages>` inside the profile directory.
1718
+ * Windows cmd command-not-found stderr. With shell:true a missing shim
1719
+ * exits with code 1 (cmd cannot report ENOENT), so the fallback chain
1720
+ * detects this message instead of the spawn error event.
1721
+ */
1722
+ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
1723
+ /**
1724
+ * Run the update inside the profile directory. Tries pnpm first, falls back
1725
+ * to corepack and then npx when the previous command is missing (ENOENT);
1726
+ * all candidates share one hard timeout and keep accumulating output.
1719
1727
  * @param deps - profile dir, package list, and spawn/timeout seams.
1720
1728
  * @returns the outcome with captured output.
1721
1729
  */
1722
1730
  function runUpdate(deps) {
1723
1731
  return new Promise((resolve) => {
1724
- const child = (deps.spawnImpl ?? spawn)("pnpm", ["update", ...deps.packages], {
1732
+ const spawnImpl = deps.spawnImpl ?? spawn;
1733
+ const packages = deps.packages;
1734
+ const platform = deps.platform ?? process.platform;
1735
+ const spawnOptions = {
1725
1736
  cwd: deps.profileDir,
1726
1737
  stdio: [
1727
1738
  "ignore",
1728
1739
  "pipe",
1729
1740
  "pipe"
1730
- ]
1731
- });
1741
+ ],
1742
+ ...platform === "win32" ? { shell: true } : {}
1743
+ };
1744
+ const candidates = [
1745
+ {
1746
+ command: "pnpm",
1747
+ args: ["update", ...packages]
1748
+ },
1749
+ {
1750
+ command: "corepack",
1751
+ args: [
1752
+ "pnpm",
1753
+ "update",
1754
+ ...packages
1755
+ ]
1756
+ },
1757
+ {
1758
+ command: "npx",
1759
+ args: [
1760
+ "--yes",
1761
+ "pnpm",
1762
+ "update",
1763
+ ...packages
1764
+ ]
1765
+ }
1766
+ ];
1732
1767
  let output = "";
1768
+ let currentOutput = "";
1733
1769
  const append = (chunk) => {
1734
1770
  output += chunk.toString("utf8");
1735
1771
  if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP);
1772
+ currentOutput += chunk.toString("utf8");
1773
+ if (currentOutput.length > OUTPUT_CAP) currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP);
1774
+ };
1775
+ let currentChild;
1776
+ let finished = false;
1777
+ const finish = (result) => {
1778
+ if (finished) return;
1779
+ finished = true;
1780
+ clearTimeout(timer);
1781
+ resolve(result);
1736
1782
  };
1737
- child.stdout?.on("data", append);
1738
- child.stderr?.on("data", append);
1739
1783
  const timer = setTimeout(() => {
1740
- child.kill("SIGTERM");
1741
- resolve({
1784
+ if (finished) return;
1785
+ if (platform === "win32") {
1786
+ const pid = currentChild?.pid;
1787
+ if (pid !== void 0 && pid > 0) try {
1788
+ spawnImpl("taskkill", [
1789
+ "/pid",
1790
+ String(pid),
1791
+ "/t",
1792
+ "/f"
1793
+ ], { stdio: "ignore" });
1794
+ } catch {}
1795
+ } else currentChild?.kill("SIGTERM");
1796
+ finish({
1742
1797
  ok: false,
1743
1798
  exitCode: null,
1744
1799
  output,
@@ -1746,27 +1801,59 @@ function runUpdate(deps) {
1746
1801
  errorCode: "timeout"
1747
1802
  });
1748
1803
  }, deps.timeoutMs ?? 10 * 6e4);
1749
- child.on("error", (error) => {
1750
- clearTimeout(timer);
1751
- const missing = error.code === "ENOENT";
1752
- resolve({
1753
- ok: false,
1754
- exitCode: null,
1755
- output,
1756
- error: missing ? "pnpm not found on PATH" : error.message,
1757
- errorCode: missing ? "pnpm-missing" : void 0
1804
+ const runCandidate = (index) => {
1805
+ if (finished) return;
1806
+ if (index >= candidates.length) {
1807
+ finish({
1808
+ ok: false,
1809
+ exitCode: null,
1810
+ output,
1811
+ error: "pnpm not found on PATH (tried pnpm, corepack, npx); install pnpm and restart the app",
1812
+ errorCode: "pnpm-missing"
1813
+ });
1814
+ return;
1815
+ }
1816
+ const candidate = candidates[index];
1817
+ currentOutput = "";
1818
+ const child = spawnImpl(candidate.command, candidate.args, spawnOptions);
1819
+ currentChild = child;
1820
+ let settled = false;
1821
+ const once = (fn) => {
1822
+ if (settled) return;
1823
+ settled = true;
1824
+ fn();
1825
+ };
1826
+ child.stdout?.on("data", append);
1827
+ child.stderr?.on("data", append);
1828
+ child.on("error", (error) => {
1829
+ once(() => {
1830
+ if (error.code === "ENOENT") runCandidate(index + 1);
1831
+ else finish({
1832
+ ok: false,
1833
+ exitCode: null,
1834
+ output,
1835
+ error: error.message,
1836
+ errorCode: void 0
1837
+ });
1838
+ });
1758
1839
  });
1759
- });
1760
- child.on("close", (code) => {
1761
- clearTimeout(timer);
1762
- resolve({
1763
- ok: code === 0,
1764
- exitCode: code,
1765
- output,
1766
- error: code === 0 ? void 0 : "pnpm exited with code " + String(code),
1767
- ...code === 0 ? {} : { errorCode: "pnpm-failed" }
1840
+ child.on("close", (code) => {
1841
+ if (settled || finished) return;
1842
+ if (platform === "win32" && code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)) {
1843
+ runCandidate(index + 1);
1844
+ return;
1845
+ }
1846
+ settled = true;
1847
+ finish({
1848
+ ok: code === 0,
1849
+ exitCode: code,
1850
+ output,
1851
+ error: code === 0 ? void 0 : "pnpm exited with code " + String(code),
1852
+ ...code === 0 ? {} : { errorCode: "pnpm-failed" }
1853
+ });
1768
1854
  });
1769
- });
1855
+ };
1856
+ runCandidate(0);
1770
1857
  });
1771
1858
  }
1772
1859
  //#endregion
@@ -96,7 +96,7 @@ export const zh = {
96
96
  'update.packages': '组件版本',
97
97
  'update.retry': '重新检查',
98
98
  'update.close': '关闭更新面板',
99
- 'update.error.pnpmMissing': '未找到 pnpm,请先安装 pnpm(brew install pnpm 或 corepack enable)。',
99
+ 'update.error.pnpmMissing': '未找到 pnpm,已尝试 pnpm / corepack / npx 仍不可用。请安装 pnpm 后重启应用(Windows: npm install -g pnpm 或 corepack enable;macOS: brew install pnpm 或 corepack enable)。',
100
100
  'update.error.timeout': '更新超时,已终止安装进程,请重试。',
101
101
  'update.error.notFound': '未找到 dsh-web-ui 聚合包安装,无法更新。',
102
102
  'update.error.link': '当前为本地开发模式(链接安装),无法自动更新。',
@@ -200,7 +200,7 @@ export const en = {
200
200
  'update.packages': 'Component versions',
201
201
  'update.retry': 'Check again',
202
202
  'update.close': 'Close update panel',
203
- 'update.error.pnpmMissing': 'pnpm was not found. Install it first (brew install pnpm or corepack enable).',
203
+ 'update.error.pnpmMissing': 'pnpm was not found (tried pnpm / corepack / npx). Install pnpm and restart the app (Windows: npm install -g pnpm or corepack enable; macOS: brew install pnpm or corepack enable).',
204
204
  'update.error.timeout': 'The update timed out and the install process was killed; try again.',
205
205
  'update.error.notFound': 'The dsh-web-ui aggregate package is not installed; nothing to update.',
206
206
  'update.error.link': 'Local development mode (link install) — automatic update is unavailable.',
@@ -180,9 +180,13 @@ export interface UpdateRunDeps {
180
180
  spawnImpl?: typeof spawn;
181
181
  /** Hard timeout; the child is killed on expiry. */
182
182
  timeoutMs?: number;
183
+ /** Platform override (defaults to process.platform; test seam). */
184
+ platform?: NodeJS.Platform;
183
185
  }
184
186
  /**
185
- * Run the update: `pnpm update <packages>` inside the profile directory.
187
+ * Run the update inside the profile directory. Tries pnpm first, falls back
188
+ * to corepack and then npx when the previous command is missing (ENOENT);
189
+ * all candidates share one hard timeout and keep accumulating output.
186
190
  * @param deps - profile dir, package list, and spawn/timeout seams.
187
191
  * @returns the outcome with captured output.
188
192
  */
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,YAAY,+BAA+B,CAAA;AAExD,oEAAoE;AACpE,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAEzC,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,8BAA8B,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,YAAY,iCAAiC,CAAA;AAQ1D,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CASlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5D;AAYD,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAahF;AAED,6DAA6D;AAC7D,KAAK,cAAc,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAE9D,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAG1D;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,iGAAiG;IACjG,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;IAChC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAA;IACjB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC9C,oEAAoE;IACpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GACrC,MAAM,GAAG,SAAS,CASpB;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,GACpC,YAAY,GAAG;IAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAAA;CAAE,CAiBhD;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAQhF;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,EAC9E,SAAS,SAAS,GACjB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiB7B;AAaD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAgD/E;AAED,4DAA4D;AAC5D,MAAM,MAAM,eAAe;AACzB,2BAA2B;AACzB,cAAc;AAChB,6CAA6C;GAC3C,SAAS;AACX,2CAA2C;GACzC,WAAW;AACb,iEAAiE;GAC/D,MAAM;AACR,4BAA4B;GAC1B,aAAa,CAAA;AAEjB,gCAAgC;AAChC,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAA;IACX,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAA;IAClB,sCAAsC;IACtC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;CAC3B;AAYD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAsHvE"}
@@ -286,48 +286,144 @@ export async function checkUpdates(deps) {
286
286
  /** Cap on captured pnpm output (keeps error payloads bounded). */
287
287
  const OUTPUT_CAP = 16 * 1024;
288
288
  /**
289
- * Run the update: `pnpm update <packages>` inside the profile directory.
289
+ * Windows cmd command-not-found stderr. With shell:true a missing shim
290
+ * exits with code 1 (cmd cannot report ENOENT), so the fallback chain
291
+ * detects this message instead of the spawn error event.
292
+ */
293
+ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
294
+ /**
295
+ * Run the update inside the profile directory. Tries pnpm first, falls back
296
+ * to corepack and then npx when the previous command is missing (ENOENT);
297
+ * all candidates share one hard timeout and keep accumulating output.
290
298
  * @param deps - profile dir, package list, and spawn/timeout seams.
291
299
  * @returns the outcome with captured output.
292
300
  */
293
301
  export function runUpdate(deps) {
294
302
  return new Promise((resolve) => {
295
- const child = (deps.spawnImpl ?? spawn)('pnpm', ['update', ...deps.packages], {
303
+ const spawnImpl = deps.spawnImpl ?? spawn;
304
+ const packages = deps.packages;
305
+ const platform = deps.platform ?? process.platform;
306
+ // Windows ships pnpm/corepack/npx as .cmd shims; Node's spawn cannot
307
+ // start .cmd files directly (ENOENT even when installed), so route
308
+ // them through cmd.exe there. POSIX spawns stay shell-free.
309
+ const spawnOptions = {
296
310
  cwd: deps.profileDir,
297
311
  stdio: ['ignore', 'pipe', 'pipe'],
298
- });
312
+ ...(platform === 'win32' ? { shell: true } : {}),
313
+ };
314
+ // Ordered fallback chain: each is tried only when the previous one is
315
+ // missing on PATH (ENOENT on the spawn error event, never on close).
316
+ const candidates = [
317
+ { command: 'pnpm', args: ['update', ...packages] },
318
+ { command: 'corepack', args: ['pnpm', 'update', ...packages] },
319
+ { command: 'npx', args: ['--yes', 'pnpm', 'update', ...packages] },
320
+ ];
321
+ // `output` accumulates across candidates for UI display; `currentOutput`
322
+ // is reset per candidate and carries only that candidate's own diagnostics
323
+ // (its tail is subject to OUTPUT_CAP). The win32 missing-command test must
324
+ // run against `currentOutput` so a previous candidate's 'not recognized'
325
+ // stderr cannot misclassify a real failure in the next one.
299
326
  let output = '';
327
+ let currentOutput = '';
300
328
  const append = (chunk) => {
301
329
  output += chunk.toString('utf8');
302
330
  if (output.length > OUTPUT_CAP)
303
331
  output = output.slice(output.length - OUTPUT_CAP);
332
+ currentOutput += chunk.toString('utf8');
333
+ if (currentOutput.length > OUTPUT_CAP)
334
+ currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP);
335
+ };
336
+ let currentChild;
337
+ // Terminal guard: once a result is produced the promise is settled and no
338
+ // further candidate is started (a killed child's late close must not
339
+ // respawn anything after a timeout resolution).
340
+ let finished = false;
341
+ const finish = (result) => {
342
+ if (finished)
343
+ return;
344
+ finished = true;
345
+ clearTimeout(timer);
346
+ resolve(result);
304
347
  };
305
- child.stdout?.on('data', append);
306
- child.stderr?.on('data', append);
307
348
  const timer = setTimeout(() => {
308
- child.kill('SIGTERM');
309
- resolve({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' });
349
+ if (finished)
350
+ return;
351
+ if (platform === 'win32') {
352
+ // cmd.exe under shell:true only kills the shell wrapper; kill the whole
353
+ // process tree best-effort so pnpm/npx do not keep running.
354
+ const pid = currentChild?.pid;
355
+ if (pid !== undefined && pid > 0) {
356
+ try {
357
+ spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' });
358
+ }
359
+ catch {
360
+ // Best-effort kill; fall through to the timeout result.
361
+ }
362
+ }
363
+ }
364
+ else {
365
+ currentChild?.kill('SIGTERM');
366
+ }
367
+ finish({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' });
310
368
  }, deps.timeoutMs ?? 10 * 60_000);
311
- child.on('error', (error) => {
312
- clearTimeout(timer);
313
- const missing = error.code === 'ENOENT';
314
- resolve({
315
- ok: false,
316
- exitCode: null,
317
- output,
318
- error: missing ? 'pnpm not found on PATH' : error.message,
319
- errorCode: missing ? 'pnpm-missing' : undefined,
369
+ const runCandidate = (index) => {
370
+ if (finished)
371
+ return;
372
+ if (index >= candidates.length) {
373
+ finish({
374
+ ok: false,
375
+ exitCode: null,
376
+ output,
377
+ error: 'pnpm not found on PATH (tried pnpm, corepack, npx); install pnpm and restart the app',
378
+ errorCode: 'pnpm-missing',
379
+ });
380
+ return;
381
+ }
382
+ const candidate = candidates[index];
383
+ currentOutput = '';
384
+ const child = spawnImpl(candidate.command, candidate.args, spawnOptions);
385
+ currentChild = child;
386
+ let settled = false;
387
+ const once = (fn) => {
388
+ if (settled)
389
+ return;
390
+ settled = true;
391
+ fn();
392
+ };
393
+ child.stdout?.on('data', append);
394
+ child.stderr?.on('data', append);
395
+ child.on('error', (error) => {
396
+ once(() => {
397
+ if (error.code === 'ENOENT') {
398
+ // Command missing: try the next candidate in the chain.
399
+ runCandidate(index + 1);
400
+ }
401
+ else {
402
+ finish({ ok: false, exitCode: null, output, error: error.message, errorCode: undefined });
403
+ }
404
+ });
320
405
  });
321
- });
322
- child.on('close', (code) => {
323
- clearTimeout(timer);
324
- resolve({
325
- ok: code === 0,
326
- exitCode: code,
327
- output,
328
- error: code === 0 ? undefined : 'pnpm exited with code ' + String(code),
329
- ...(code === 0 ? {} : { errorCode: 'pnpm-failed' }),
406
+ child.on('close', (code) => {
407
+ if (settled || finished)
408
+ return;
409
+ // Windows + shell: a missing command reports exit 1 with
410
+ // 'not recognized' instead of ENOENT — keep the chain going. Checked
411
+ // against this candidate's own output so a prior fallback's stderr
412
+ // cannot misclassify a real failure here.
413
+ if (platform === 'win32' && code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)) {
414
+ runCandidate(index + 1);
415
+ return;
416
+ }
417
+ settled = true;
418
+ finish({
419
+ ok: code === 0,
420
+ exitCode: code,
421
+ output,
422
+ error: code === 0 ? undefined : 'pnpm exited with code ' + String(code),
423
+ ...(code === 0 ? {} : { errorCode: 'pnpm-failed' }),
424
+ });
330
425
  });
331
- });
426
+ };
427
+ runCandidate(0);
332
428
  });
333
429
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-remote-web-ui",
3
3
  "description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
4
- "version": "0.1.13",
4
+ "version": "0.1.14",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -97,7 +97,7 @@ export const zh = {
97
97
  'update.packages': '组件版本',
98
98
  'update.retry': '重新检查',
99
99
  'update.close': '关闭更新面板',
100
- 'update.error.pnpmMissing': '未找到 pnpm,请先安装 pnpm(brew install pnpm 或 corepack enable)。',
100
+ 'update.error.pnpmMissing': '未找到 pnpm,已尝试 pnpm / corepack / npx 仍不可用。请安装 pnpm 后重启应用(Windows: npm install -g pnpm 或 corepack enable;macOS: brew install pnpm 或 corepack enable)。',
101
101
  'update.error.timeout': '更新超时,已终止安装进程,请重试。',
102
102
  'update.error.notFound': '未找到 dsh-web-ui 聚合包安装,无法更新。',
103
103
  'update.error.link': '当前为本地开发模式(链接安装),无法自动更新。',
@@ -208,7 +208,7 @@ export const en = {
208
208
  'update.packages': 'Component versions',
209
209
  'update.retry': 'Check again',
210
210
  'update.close': 'Close update panel',
211
- 'update.error.pnpmMissing': 'pnpm was not found. Install it first (brew install pnpm or corepack enable).',
211
+ 'update.error.pnpmMissing': 'pnpm was not found (tried pnpm / corepack / npx). Install pnpm and restart the app (Windows: npm install -g pnpm or corepack enable; macOS: brew install pnpm or corepack enable).',
212
212
  'update.error.timeout': 'The update timed out and the install process was killed; try again.',
213
213
  'update.error.notFound': 'The dsh-web-ui aggregate package is not installed; nothing to update.',
214
214
  'update.error.link': 'Local development mode (link install) — automatic update is unavailable.',
package/src/update.ts CHANGED
@@ -383,53 +383,143 @@ export interface UpdateRunDeps {
383
383
  spawnImpl?: typeof spawn
384
384
  /** Hard timeout; the child is killed on expiry. */
385
385
  timeoutMs?: number
386
+ /** Platform override (defaults to process.platform; test seam). */
387
+ platform?: NodeJS.Platform
386
388
  }
387
389
 
388
390
  /** Cap on captured pnpm output (keeps error payloads bounded). */
389
391
  const OUTPUT_CAP = 16 * 1024
390
392
 
391
393
  /**
392
- * Run the update: `pnpm update <packages>` inside the profile directory.
394
+ * Windows cmd command-not-found stderr. With shell:true a missing shim
395
+ * exits with code 1 (cmd cannot report ENOENT), so the fallback chain
396
+ * detects this message instead of the spawn error event.
397
+ */
398
+ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i
399
+
400
+ /**
401
+ * Run the update inside the profile directory. Tries pnpm first, falls back
402
+ * to corepack and then npx when the previous command is missing (ENOENT);
403
+ * all candidates share one hard timeout and keep accumulating output.
393
404
  * @param deps - profile dir, package list, and spawn/timeout seams.
394
405
  * @returns the outcome with captured output.
395
406
  */
396
407
  export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
397
408
  return new Promise((resolve) => {
398
- const child = (deps.spawnImpl ?? spawn)('pnpm', ['update', ...deps.packages], {
409
+ const spawnImpl = deps.spawnImpl ?? spawn
410
+ const packages = deps.packages
411
+ const platform = deps.platform ?? process.platform
412
+ // Windows ships pnpm/corepack/npx as .cmd shims; Node's spawn cannot
413
+ // start .cmd files directly (ENOENT even when installed), so route
414
+ // them through cmd.exe there. POSIX spawns stay shell-free.
415
+ const spawnOptions: Record<string, unknown> = {
399
416
  cwd: deps.profileDir,
400
417
  stdio: ['ignore', 'pipe', 'pipe'],
401
- })
418
+ ...(platform === 'win32' ? { shell: true } : {}),
419
+ }
420
+ // Ordered fallback chain: each is tried only when the previous one is
421
+ // missing on PATH (ENOENT on the spawn error event, never on close).
422
+ const candidates: ReadonlyArray<{ command: string; args: string[] }> = [
423
+ { command: 'pnpm', args: ['update', ...packages] },
424
+ { command: 'corepack', args: ['pnpm', 'update', ...packages] },
425
+ { command: 'npx', args: ['--yes', 'pnpm', 'update', ...packages] },
426
+ ]
427
+ // `output` accumulates across candidates for UI display; `currentOutput`
428
+ // is reset per candidate and carries only that candidate's own diagnostics
429
+ // (its tail is subject to OUTPUT_CAP). The win32 missing-command test must
430
+ // run against `currentOutput` so a previous candidate's 'not recognized'
431
+ // stderr cannot misclassify a real failure in the next one.
402
432
  let output = ''
433
+ let currentOutput = ''
403
434
  const append = (chunk: Buffer): void => {
404
435
  output += chunk.toString('utf8')
405
436
  if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP)
437
+ currentOutput += chunk.toString('utf8')
438
+ if (currentOutput.length > OUTPUT_CAP) currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP)
439
+ }
440
+ let currentChild: ReturnType<typeof spawnImpl> | undefined
441
+ // Terminal guard: once a result is produced the promise is settled and no
442
+ // further candidate is started (a killed child's late close must not
443
+ // respawn anything after a timeout resolution).
444
+ let finished = false
445
+ const finish = (result: UpdateRunResult): void => {
446
+ if (finished) return
447
+ finished = true
448
+ clearTimeout(timer)
449
+ resolve(result)
406
450
  }
407
- child.stdout?.on('data', append)
408
- child.stderr?.on('data', append)
409
451
  const timer = setTimeout(() => {
410
- child.kill('SIGTERM')
411
- resolve({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' })
452
+ if (finished) return
453
+ if (platform === 'win32') {
454
+ // cmd.exe under shell:true only kills the shell wrapper; kill the whole
455
+ // process tree best-effort so pnpm/npx do not keep running.
456
+ const pid = (currentChild as { pid?: number } | undefined)?.pid
457
+ if (pid !== undefined && pid > 0) {
458
+ try {
459
+ spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' })
460
+ } catch {
461
+ // Best-effort kill; fall through to the timeout result.
462
+ }
463
+ }
464
+ } else {
465
+ currentChild?.kill('SIGTERM')
466
+ }
467
+ finish({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' })
412
468
  }, deps.timeoutMs ?? 10 * 60_000)
413
- child.on('error', (error: Error) => {
414
- clearTimeout(timer)
415
- const missing = (error as NodeJS.ErrnoException).code === 'ENOENT'
416
- resolve({
417
- ok: false,
418
- exitCode: null,
419
- output,
420
- error: missing ? 'pnpm not found on PATH' : error.message,
421
- errorCode: missing ? 'pnpm-missing' : undefined,
469
+ const runCandidate = (index: number): void => {
470
+ if (finished) return
471
+ if (index >= candidates.length) {
472
+ finish({
473
+ ok: false,
474
+ exitCode: null,
475
+ output,
476
+ error: 'pnpm not found on PATH (tried pnpm, corepack, npx); install pnpm and restart the app',
477
+ errorCode: 'pnpm-missing',
478
+ })
479
+ return
480
+ }
481
+ const candidate = candidates[index]
482
+ currentOutput = ''
483
+ const child = spawnImpl(candidate.command, candidate.args, spawnOptions)
484
+ currentChild = child
485
+ let settled = false
486
+ const once = (fn: () => void): void => {
487
+ if (settled) return
488
+ settled = true
489
+ fn()
490
+ }
491
+ child.stdout?.on('data', append)
492
+ child.stderr?.on('data', append)
493
+ child.on('error', (error: Error) => {
494
+ once(() => {
495
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
496
+ // Command missing: try the next candidate in the chain.
497
+ runCandidate(index + 1)
498
+ } else {
499
+ finish({ ok: false, exitCode: null, output, error: error.message, errorCode: undefined })
500
+ }
501
+ })
422
502
  })
423
- })
424
- child.on('close', (code) => {
425
- clearTimeout(timer)
426
- resolve({
427
- ok: code === 0,
428
- exitCode: code,
429
- output,
430
- error: code === 0 ? undefined : 'pnpm exited with code ' + String(code),
431
- ...(code === 0 ? {} : { errorCode: 'pnpm-failed' as const }),
503
+ child.on('close', (code: number | null) => {
504
+ if (settled || finished) return
505
+ // Windows + shell: a missing command reports exit 1 with
506
+ // 'not recognized' instead of ENOENT — keep the chain going. Checked
507
+ // against this candidate's own output so a prior fallback's stderr
508
+ // cannot misclassify a real failure here.
509
+ if (platform === 'win32' && code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)) {
510
+ runCandidate(index + 1)
511
+ return
512
+ }
513
+ settled = true
514
+ finish({
515
+ ok: code === 0,
516
+ exitCode: code,
517
+ output,
518
+ error: code === 0 ? undefined : 'pnpm exited with code ' + String(code),
519
+ ...(code === 0 ? {} : { errorCode: 'pnpm-failed' as const }),
520
+ })
432
521
  })
433
- })
522
+ }
523
+ runCandidate(0)
434
524
  })
435
525
  }