@linxin666/dsh-remote-web-ui 0.1.12 → 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.
Files changed (68) hide show
  1. package/README.i18n.yaml +6 -0
  2. package/README.md +17 -3
  3. package/README.zh.md +171 -0
  4. package/lib/client.js +171 -100
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +338 -31
  7. package/lib/mobile.js +109 -33
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  10. package/lib/types/client/PluginSettingsCard.js +3 -2
  11. package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
  12. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  13. package/lib/types/client/RemoteSettingsCard.js +3 -1
  14. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  15. package/lib/types/client/UpdateEntry.js +10 -2
  16. package/lib/types/client/deep-link.js +10 -5
  17. package/lib/types/client/locales.d.ts +8 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales.js +10 -2
  20. package/lib/types/client/update-api.d.ts +10 -0
  21. package/lib/types/client/update-api.d.ts.map +1 -1
  22. package/lib/types/client/update-api.js +22 -2
  23. package/lib/types/index.d.ts +7 -0
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/lib/types/index.js +7 -1
  26. package/lib/types/mobile/api.d.ts +9 -2
  27. package/lib/types/mobile/api.d.ts.map +1 -1
  28. package/lib/types/mobile/api.js +7 -3
  29. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  30. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  31. package/lib/types/mobile/mobile-styles.js +3 -0
  32. package/lib/types/mobile/views/App.d.ts +3 -1
  33. package/lib/types/mobile/views/App.d.ts.map +1 -1
  34. package/lib/types/mobile/views/App.js +18 -3
  35. package/lib/types/mobile/views/ChatView.d.ts +6 -0
  36. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  37. package/lib/types/mobile/views/ChatView.js +95 -7
  38. package/lib/types/mobile/views/ChatView.test.js +161 -3
  39. package/lib/types/mobile/views/WorkspaceView.d.ts +3 -1
  40. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  41. package/lib/types/mobile/views/WorkspaceView.js +9 -2
  42. package/lib/types/mobile/views/WorkspaceView.test.d.ts +2 -0
  43. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +1 -0
  44. package/lib/types/mobile/views/WorkspaceView.test.js +56 -0
  45. package/lib/types/mobile-api.d.ts +2 -0
  46. package/lib/types/mobile-api.d.ts.map +1 -1
  47. package/lib/types/mobile-api.js +17 -2
  48. package/lib/types/update.d.ts +5 -1
  49. package/lib/types/update.d.ts.map +1 -1
  50. package/lib/types/update.js +122 -26
  51. package/package.json +1 -11
  52. package/src/client/PluginSettingsCard.tsx +30 -5
  53. package/src/client/RemoteSettingsCard.tsx +18 -0
  54. package/src/client/UpdateEntry.tsx +10 -2
  55. package/src/client/deep-link.ts +10 -4
  56. package/src/client/locales.ts +10 -2
  57. package/src/client/settings-card.module.css +139 -144
  58. package/src/client/update-api.ts +22 -2
  59. package/src/index.ts +14 -1
  60. package/src/mobile/api.ts +14 -2
  61. package/src/mobile/mobile-styles.ts +3 -0
  62. package/src/mobile/views/App.tsx +22 -3
  63. package/src/mobile/views/ChatView.test.tsx +185 -3
  64. package/src/mobile/views/ChatView.tsx +102 -8
  65. package/src/mobile/views/WorkspaceView.test.tsx +66 -0
  66. package/src/mobile/views/WorkspaceView.tsx +11 -2
  67. package/src/mobile-api.ts +20 -2
  68. package/src/update.ts +116 -26
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
  }