@linxin666/dsh-remote-web-ui 0.3.23 → 0.4.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.
Files changed (116) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +13 -10
  3. package/README.zh.md +13 -10
  4. package/icon.svg +3 -0
  5. package/lib/client.js +396 -132
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +604 -262
  8. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  9. package/lib/types/client/FenceNotice.js +0 -3
  10. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.js +3 -2
  13. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +14 -9
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +13 -4
  17. package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +14 -8
  20. package/lib/types/client/UpdateEntry.js +2 -2
  21. package/lib/types/client/UpdatePanel.js +2 -2
  22. package/lib/types/client/deep-link.js +2 -2
  23. package/lib/types/client/index.d.ts +16 -8
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +56 -32
  26. package/lib/types/client/locales.d.ts +0 -2
  27. package/lib/types/client/locales.d.ts.map +1 -1
  28. package/lib/types/client/locales.js +6 -8
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  30. package/lib/types/client/mobile-adapt.js +143 -28
  31. package/lib/types/client/pair-api.d.ts +32 -3
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +44 -6
  34. package/lib/types/client/plugin-card-seat.d.ts +104 -0
  35. package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
  36. package/lib/types/client/plugin-card-seat.js +143 -0
  37. package/lib/types/client/remote-channel.d.ts.map +1 -1
  38. package/lib/types/client/remote-channel.js +18 -5
  39. package/lib/types/client/settings-form.d.ts +17 -17
  40. package/lib/types/client/settings-form.d.ts.map +1 -1
  41. package/lib/types/client/settings-form.js +30 -27
  42. package/lib/types/console-output.d.ts +41 -0
  43. package/lib/types/console-output.d.ts.map +1 -0
  44. package/lib/types/console-output.js +89 -0
  45. package/lib/types/http.d.ts +18 -1
  46. package/lib/types/http.d.ts.map +1 -1
  47. package/lib/types/http.js +22 -1
  48. package/lib/types/index.d.ts +19 -11
  49. package/lib/types/index.d.ts.map +1 -1
  50. package/lib/types/index.js +91 -76
  51. package/lib/types/inner-auth.d.ts +3 -2
  52. package/lib/types/inner-auth.d.ts.map +1 -1
  53. package/lib/types/inner-auth.js +3 -2
  54. package/lib/types/pair-grant.d.ts +66 -0
  55. package/lib/types/pair-grant.d.ts.map +1 -0
  56. package/lib/types/pair-grant.js +95 -0
  57. package/lib/types/pairing.d.ts +44 -12
  58. package/lib/types/pairing.d.ts.map +1 -1
  59. package/lib/types/pairing.js +69 -20
  60. package/lib/types/pairing.persist.test.js +26 -0
  61. package/lib/types/posture.d.ts +11 -3
  62. package/lib/types/posture.d.ts.map +1 -1
  63. package/lib/types/posture.js +8 -3
  64. package/lib/types/remote-api.d.ts +10 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +34 -34
  67. package/lib/types/remote-channel-boot.d.ts +6 -1
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +16 -8
  70. package/lib/types/remote-channel-rules.d.ts +14 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +8 -0
  73. package/lib/types/remote-methods.d.ts +1 -1
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +1 -0
  76. package/lib/types/routes.d.ts +47 -5
  77. package/lib/types/routes.d.ts.map +1 -1
  78. package/lib/types/routes.js +160 -58
  79. package/lib/types/tunnel.d.ts +21 -6
  80. package/lib/types/tunnel.d.ts.map +1 -1
  81. package/lib/types/tunnel.js +58 -11
  82. package/lib/types/update.d.ts +4 -0
  83. package/lib/types/update.d.ts.map +1 -1
  84. package/lib/types/update.js +74 -33
  85. package/package.json +23 -20
  86. package/src/client/FenceNotice.tsx +0 -2
  87. package/src/client/PluginSettingsCard.tsx +7 -2
  88. package/src/client/RemoteEntry.tsx +14 -9
  89. package/src/client/RemotePanel.tsx +17 -7
  90. package/src/client/RemoteSettingsCard.tsx +16 -9
  91. package/src/client/UpdateEntry.tsx +2 -2
  92. package/src/client/UpdatePanel.tsx +3 -3
  93. package/src/client/deep-link.ts +2 -2
  94. package/src/client/index.ts +70 -40
  95. package/src/client/locales.ts +6 -8
  96. package/src/client/mobile-adapt.ts +136 -25
  97. package/src/client/pair-api.ts +47 -7
  98. package/src/client/plugin-card-seat.ts +192 -0
  99. package/src/client/remote-channel.ts +19 -4
  100. package/src/client/remote.module.css +38 -0
  101. package/src/client/settings-form.ts +34 -31
  102. package/src/console-output.ts +97 -0
  103. package/src/http.ts +23 -1
  104. package/src/index.ts +102 -79
  105. package/src/inner-auth.ts +3 -2
  106. package/src/pair-grant.ts +131 -0
  107. package/src/pairing.persist.test.ts +36 -0
  108. package/src/pairing.ts +75 -24
  109. package/src/posture.ts +12 -4
  110. package/src/remote-api.ts +34 -41
  111. package/src/remote-channel-boot.ts +16 -8
  112. package/src/remote-channel-rules.ts +17 -0
  113. package/src/remote-methods.ts +1 -0
  114. package/src/routes.ts +165 -57
  115. package/src/tunnel.ts +56 -11
  116. package/src/update.ts +74 -29
package/src/update.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  import { readFileSync } from 'node:fs'
13
13
  import { dirname, join } from 'node:path'
14
14
  import { spawn } from 'node:child_process'
15
+ import { createOutputCapture } from './console-output.ts'
16
+ import { withIdentityEncoding } from './http.ts'
15
17
 
16
18
  /** npm registry base used for version probes. */
17
19
  export const NPM_REGISTRY = 'https://registry.npmjs.org'
@@ -321,7 +323,7 @@ export async function fetchLatestVersion(
321
323
  const controller = new AbortController()
322
324
  const timer = setTimeout(() => { controller.abort() }, timeoutMs)
323
325
  try {
324
- const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', { signal: controller.signal })
326
+ const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', withIdentityEncoding({ signal: controller.signal }))
325
327
  if (!response.ok) return undefined
326
328
  const body = await response.json()
327
329
  if (typeof body !== 'object' || body === null) return undefined
@@ -404,10 +406,10 @@ export async function fetchGitHubReleaseNotes(
404
406
  try {
405
407
  const response = await fetchImpl(
406
408
  `https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`,
407
- {
409
+ withIdentityEncoding({
408
410
  headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-web-update' },
409
411
  signal: controller.signal,
410
- },
412
+ }),
411
413
  )
412
414
  if (!response.ok) return undefined
413
415
  const body = await response.json()
@@ -557,11 +559,24 @@ export interface UpdateRunDeps {
557
559
  timeoutMs?: number
558
560
  /** Platform override (defaults to process.platform; test seam). */
559
561
  platform?: NodeJS.Platform
562
+ /** Process-group kill seam (defaults to process.kill; test seam). */
563
+ killImpl?: KillImpl
560
564
  }
561
565
 
562
566
  /** Cap on captured pnpm output (keeps error payloads bounded). */
563
567
  const OUTPUT_CAP = 16 * 1024
564
568
 
569
+ /**
570
+ * Characters a package name may contain before it is handed to the update
571
+ * command line. Deliberately looser than the strict family shape (the tests
572
+ * and future callers use short names) but it admits no whitespace, quotes,
573
+ * shell metacharacters, or path separators beyond the scope slash.
574
+ */
575
+ const SHELL_SAFE_PACKAGE_RE = /^@?[A-Za-z0-9][A-Za-z0-9._/-]*$/
576
+
577
+ /** Seam for the process-group kill (tests inject a recorder). */
578
+ export type KillImpl = (pid: number, signal: NodeJS.Signals) => void
579
+
565
580
  /**
566
581
  * Windows cmd command-not-found stderr. With shell:true a missing shim
567
582
  * exits with code 1 or 9009 (cmd cannot report ENOENT), so the fallback chain
@@ -571,20 +586,14 @@ const OUTPUT_CAP = 16 * 1024
571
586
  const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|不是内部或外部命令/i
572
587
 
573
588
  /**
574
- * Decode process output with tolerant decoding:
575
- * Defaults to UTF-8; if invalid byte sequences are encountered (common on
576
- * Windows consoles using code page 936/GBK), falls back to GBK via TextDecoder.
589
+ * Fold one decoded candidate output into the running total under the display
590
+ * cap (the tail carries the failure reason).
591
+ * @param output - the accumulated text.
592
+ * @param text - the candidate's decoded text.
577
593
  */
578
- function decodeProcessChunk(chunk: Buffer): string {
579
- try {
580
- return new TextDecoder('utf-8', { fatal: true }).decode(chunk)
581
- } catch {
582
- try {
583
- return new TextDecoder('gbk').decode(chunk)
584
- } catch {
585
- return chunk.toString('utf8')
586
- }
587
- }
594
+ function capOutput(output: string, text: string): string {
595
+ const combined = output + text
596
+ return combined.length > OUTPUT_CAP ? combined.slice(combined.length - OUTPUT_CAP) : combined
588
597
  }
589
598
 
590
599
  /**
@@ -608,16 +617,24 @@ const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0'
608
617
  export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
609
618
  return new Promise((resolve) => {
610
619
  const spawnImpl = deps.spawnImpl ?? spawn
611
- const packages = deps.packages
612
620
  const platform = deps.platform ?? process.platform
621
+ // The names come from the profile/anchor manifests and the win32 candidate
622
+ // runs through a shell, so anything outside the npm name shape is dropped
623
+ // instead of being concatenated into a command line (a dependency key
624
+ // containing shell metacharacters would otherwise run as a second command).
625
+ const packages = deps.packages.filter(name => SHELL_SAFE_PACKAGE_RE.test(name))
613
626
  // Windows ships pnpm/corepack/npx as .cmd shims; Node's spawn cannot
614
627
  // start .cmd files directly (ENOENT even when installed), so route
615
628
  // them through cmd.exe there. POSIX spawns stay shell-free.
629
+ // POSIX: the child leads its own process group (detached) so the timeout
630
+ // can signal pnpm's whole tree — pnpm spawns lifecycle-script children that
631
+ // a direct SIGTERM to the wrapper leaves running against the profile.
616
632
  const spawnOptions: Record<string, unknown> = {
617
633
  cwd: deps.profileDir,
618
634
  stdio: ['ignore', 'pipe', 'pipe'],
619
- ...(platform === 'win32' ? { shell: true } : {}),
635
+ ...(platform === 'win32' ? { shell: true } : { detached: true }),
620
636
  }
637
+ const killImpl: KillImpl = deps.killImpl ?? process.kill
621
638
  // Ordered fallback chain: each is tried only when the previous one is
622
639
  // missing on PATH (ENOENT on the spawn error event, never on close).
623
640
  // `--latest` is required: dsh plugin writes exact-version specs (e.g.
@@ -638,14 +655,21 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
638
655
  // (its tail is subject to OUTPUT_CAP). The win32 missing-command test must
639
656
  // run against `currentOutput` so a previous candidate's 'not recognized'
640
657
  // stderr cannot misclassify a real failure in the next one.
658
+ // Each candidate accumulates raw bytes and is decoded exactly once
659
+ // (console-output.ts): decoding per `data` event splits a multi-byte
660
+ // character that straddles two reads into replacement characters, which is
661
+ // what the panel then shows as garbled pnpm diagnostics.
641
662
  let output = ''
642
- let currentOutput = ''
643
- const append = (chunk: Buffer): void => {
644
- const text = decodeProcessChunk(chunk)
645
- output += text
646
- if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP)
647
- currentOutput += text
648
- if (currentOutput.length > OUTPUT_CAP) currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP)
663
+ let capture = createOutputCapture(OUTPUT_CAP)
664
+ let candidateFolded = false
665
+ const append = (chunk: Buffer): void => { capture.push(chunk) }
666
+ /** This candidate's decoded text (idempotent). */
667
+ const candidateText = (): string => capture.read()
668
+ /** Fold this candidate's text into the running output exactly once. */
669
+ const foldCandidate = (): void => {
670
+ if (candidateFolded) return
671
+ candidateFolded = true
672
+ output = capOutput(output, capture.read())
649
673
  }
650
674
  let currentChild: ReturnType<typeof spawnImpl> | undefined
651
675
  // Terminal guard: once a result is produced the promise is settled and no
@@ -666,14 +690,31 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
666
690
  const pid = (currentChild as { pid?: number } | undefined)?.pid
667
691
  if (pid !== undefined && pid > 0) {
668
692
  try {
669
- spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' })
693
+ // A spawn failure arrives as an asynchronous 'error' event, which
694
+ // the try/catch cannot see; an emitter with no error listener throws
695
+ // and would take the host process down on this path.
696
+ const killer = spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' })
697
+ killer.on?.('error', () => {})
670
698
  } catch {
671
699
  // Best-effort kill; fall through to the timeout result.
672
700
  }
673
701
  }
674
702
  } else {
675
- currentChild?.kill('SIGTERM')
703
+ // Detached child: its process-group id is its pid, so the negative pid
704
+ // reaches pnpm's grandchildren too. A group kill throws when the child
705
+ // already exited; fall back to the direct kill.
706
+ const pid = (currentChild as { pid?: number } | undefined)?.pid
707
+ if (pid !== undefined && pid > 0) {
708
+ try {
709
+ killImpl(-pid, 'SIGTERM')
710
+ } catch {
711
+ try { currentChild?.kill('SIGTERM') } catch { /* already gone */ }
712
+ }
713
+ } else {
714
+ currentChild?.kill('SIGTERM')
715
+ }
676
716
  }
717
+ foldCandidate()
677
718
  finish({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' })
678
719
  }, deps.timeoutMs ?? 10 * 60_000)
679
720
  const runCandidate = (index: number): void => {
@@ -689,7 +730,8 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
689
730
  return
690
731
  }
691
732
  const candidate = candidates[index]
692
- currentOutput = ''
733
+ capture = createOutputCapture(OUTPUT_CAP)
734
+ candidateFolded = false
693
735
  const child = spawnImpl(candidate.command, candidate.args, spawnOptions)
694
736
  currentChild = child
695
737
  let settled = false
@@ -702,6 +744,7 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
702
744
  child.stderr?.on('data', append)
703
745
  child.on('error', (error: Error) => {
704
746
  once(() => {
747
+ foldCandidate()
705
748
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
706
749
  // Command missing: try the next candidate in the chain.
707
750
  runCandidate(index + 1)
@@ -716,11 +759,13 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
716
759
  // 'not recognized' or localized message instead of ENOENT — keep the
717
760
  // chain going. Checked against this candidate's own output so a prior
718
761
  // fallback's stderr cannot misclassify a real failure here.
719
- if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)))) {
762
+ if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(candidateText())))) {
763
+ foldCandidate()
720
764
  runCandidate(index + 1)
721
765
  return
722
766
  }
723
767
  settled = true
768
+ foldCandidate()
724
769
  finish({
725
770
  ok: code === 0,
726
771
  exitCode: code,