dsh-config-manager 0.1.63 → 0.1.65

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 (547) hide show
  1. package/README.md +66 -14
  2. package/README.zh-CN.md +43 -13
  3. package/lib/adapters/agent-instructions.d.ts +3 -3
  4. package/lib/adapters/agent-instructions.js +5 -3
  5. package/lib/adapters/agent-presets.d.ts +3 -3
  6. package/lib/adapters/agent-presets.js +5 -3
  7. package/lib/adapters/credentials.d.ts +3 -3
  8. package/lib/adapters/credentials.js +20 -23
  9. package/lib/adapters/file-collection.d.ts +43 -0
  10. package/lib/adapters/file-collection.js +130 -18
  11. package/lib/adapters/index.d.ts +1 -0
  12. package/lib/adapters/index.js +11 -0
  13. package/lib/adapters/json-section.d.ts +32 -0
  14. package/lib/adapters/json-section.js +61 -0
  15. package/lib/adapters/mcp.d.ts +3 -3
  16. package/lib/adapters/mcp.js +16 -19
  17. package/lib/adapters/plugin-files.d.ts +3 -3
  18. package/lib/adapters/plugin-files.js +11 -14
  19. package/lib/adapters/plugins.d.ts +3 -3
  20. package/lib/adapters/plugins.js +39 -42
  21. package/lib/adapters/prompts.d.ts +3 -3
  22. package/lib/adapters/prompts.js +16 -20
  23. package/lib/adapters/providers.d.ts +3 -3
  24. package/lib/adapters/providers.js +23 -26
  25. package/lib/adapters/sessions.d.ts +97 -1
  26. package/lib/adapters/sessions.js +455 -15
  27. package/lib/adapters/settings.d.ts +3 -3
  28. package/lib/adapters/settings.js +23 -26
  29. package/lib/adapters/skills.d.ts +3 -3
  30. package/lib/adapters/skills.js +5 -3
  31. package/lib/adapters/test-helpers.d.ts +8 -0
  32. package/lib/adapters/test-helpers.js +8 -0
  33. package/lib/adapters/ui.d.ts +3 -3
  34. package/lib/adapters/ui.js +14 -17
  35. package/lib/adapters/workspaces.d.ts +22 -0
  36. package/lib/adapters/workspaces.js +237 -16
  37. package/lib/cli/index.d.ts +46 -4
  38. package/lib/cli/index.js +170 -34
  39. package/lib/cli/sessions-repair.d.ts +28 -0
  40. package/lib/cli/sessions-repair.js +206 -0
  41. package/lib/client.d.ts +464 -173
  42. package/lib/client.js +14414 -8888
  43. package/lib/core/analyzer.d.ts +71 -5
  44. package/lib/core/analyzer.js +409 -112
  45. package/lib/core/backup-plan.d.ts +7 -1
  46. package/lib/core/backup-plan.js +15 -8
  47. package/lib/core/backup.d.ts +53 -2
  48. package/lib/core/backup.js +120 -24
  49. package/lib/core/boot-paths.d.ts +14 -0
  50. package/lib/core/boot-paths.js +28 -0
  51. package/lib/core/boot-rescue.d.ts +21 -0
  52. package/lib/core/boot-rescue.js +57 -1
  53. package/lib/core/boot-safety.d.ts +50 -0
  54. package/lib/core/boot-safety.js +211 -0
  55. package/lib/core/conflict-decisions.d.ts +16 -0
  56. package/lib/core/conflict-decisions.js +21 -0
  57. package/lib/core/crash-report.d.ts +15 -12
  58. package/lib/core/crash-report.js +33 -40
  59. package/lib/core/credential-status.d.ts +15 -0
  60. package/lib/core/credential-status.js +13 -0
  61. package/lib/core/exporter.d.ts +18 -0
  62. package/lib/core/exporter.js +115 -3
  63. package/lib/core/importer.d.ts +15 -2
  64. package/lib/core/importer.js +10 -3
  65. package/lib/core/index.d.ts +2 -0
  66. package/lib/core/index.js +3 -0
  67. package/lib/core/journal.d.ts +74 -5
  68. package/lib/core/journal.js +127 -12
  69. package/lib/core/messages.d.ts +54 -0
  70. package/lib/core/messages.js +110 -0
  71. package/lib/core/migration-consult.js +0 -4
  72. package/lib/core/model-tools.js +6 -9
  73. package/lib/core/path-mapping.d.ts +11 -0
  74. package/lib/core/path-mapping.js +28 -0
  75. package/lib/core/phase3-host.d.ts +79 -6
  76. package/lib/core/phase3-host.js +223 -73
  77. package/lib/core/phase4-crash-child.js +0 -1
  78. package/lib/core/plugin-cli.js +22 -2
  79. package/lib/core/reconcile.d.ts +8 -1
  80. package/lib/core/reconcile.js +12 -4
  81. package/lib/core/recovery-orchestrator.js +1 -1
  82. package/lib/core/restore.js +10 -4
  83. package/lib/core/rollback.js +1 -1
  84. package/lib/core/run-registry.d.ts +63 -3
  85. package/lib/core/run-registry.js +74 -4
  86. package/lib/core/session-meta.d.ts +27 -8
  87. package/lib/core/session-meta.js +59 -32
  88. package/lib/core/session-repair.d.ts +89 -0
  89. package/lib/core/session-repair.js +118 -0
  90. package/lib/core/session-select.d.ts +50 -0
  91. package/lib/core/session-select.js +137 -0
  92. package/lib/core/startup-barrier.d.ts +15 -0
  93. package/lib/core/startup-barrier.js +24 -2
  94. package/lib/core/transaction-coordinator.js +3 -3
  95. package/lib/core/types.d.ts +250 -0
  96. package/lib/core/validator.js +0 -1
  97. package/lib/index.d.ts +368 -6
  98. package/lib/index.js +884 -3365
  99. package/lib/market/index-parser.d.ts +2 -1
  100. package/lib/market/index-parser.js +27 -6
  101. package/lib/market/my-repo.d.ts +0 -1
  102. package/lib/market/my-repo.js +0 -6
  103. package/lib/market/prepare.d.ts +9 -0
  104. package/lib/market/prepare.js +5 -31
  105. package/lib/market/reader.js +0 -1
  106. package/lib/market/security.js +3 -9
  107. package/lib/profiles/dsh-profile-launcher.d.ts +141 -0
  108. package/lib/profiles/dsh-profile-launcher.js +396 -0
  109. package/lib/profiles/dsh-profile-manager.d.ts +49 -15
  110. package/lib/profiles/dsh-profile-manager.js +151 -48
  111. package/lib/profiles/dsh-profile-runtime.d.ts +83 -0
  112. package/lib/profiles/dsh-profile-runtime.js +194 -0
  113. package/lib/profiles/dsh-profile-shared.d.ts +117 -10
  114. package/lib/profiles/dsh-profile-shared.js +12 -0
  115. package/lib/profiles/index.d.ts +10 -3
  116. package/lib/profiles/index.js +10 -3
  117. package/lib/profiles/process-control.d.ts +37 -0
  118. package/lib/profiles/process-control.js +75 -0
  119. package/lib/routes/backup.d.ts +9 -0
  120. package/lib/routes/backup.js +99 -0
  121. package/lib/routes/consult.d.ts +9 -0
  122. package/lib/routes/consult.js +134 -0
  123. package/lib/routes/context.d.ts +9 -0
  124. package/lib/routes/context.js +2 -0
  125. package/lib/routes/history.d.ts +9 -0
  126. package/lib/routes/history.js +53 -0
  127. package/lib/routes/import.d.ts +9 -0
  128. package/lib/routes/import.js +390 -0
  129. package/lib/routes/index.d.ts +10 -0
  130. package/lib/routes/index.js +27 -0
  131. package/lib/routes/kit.d.ts +99 -0
  132. package/lib/routes/kit.js +161 -0
  133. package/lib/routes/market.d.ts +9 -0
  134. package/lib/routes/market.js +256 -0
  135. package/lib/routes/me.d.ts +9 -0
  136. package/lib/routes/me.js +170 -0
  137. package/lib/routes/prefs.d.ts +9 -0
  138. package/lib/routes/prefs.js +103 -0
  139. package/lib/routes/profiles.d.ts +9 -0
  140. package/lib/routes/profiles.js +250 -0
  141. package/lib/routes/recovery.d.ts +9 -0
  142. package/lib/routes/recovery.js +137 -0
  143. package/lib/routes/snapshots.d.ts +9 -0
  144. package/lib/routes/snapshots.js +214 -0
  145. package/lib/routes/sync.d.ts +9 -0
  146. package/lib/routes/sync.js +783 -0
  147. package/lib/schema/config.d.ts +10 -9
  148. package/lib/schema/config.js +78 -96
  149. package/lib/schema/index.d.ts +8 -1
  150. package/lib/schema/index.js +13 -1
  151. package/lib/schema/manifest.d.ts +2 -0
  152. package/lib/schema/manifest.js +1 -0
  153. package/lib/schema/section-registry.d.ts +211 -0
  154. package/lib/schema/section-registry.js +285 -0
  155. package/lib/schema/types.d.ts +13 -3
  156. package/lib/schema/versions.d.ts +11 -6
  157. package/lib/schema/versions.js +27 -1
  158. package/lib/security/secret-scanner.js +1 -1
  159. package/lib/security/zip-security.d.ts +8 -5
  160. package/lib/security/zip-security.js +20 -90
  161. package/lib/sync/ancestor.d.ts +11 -0
  162. package/lib/sync/ancestor.js +20 -0
  163. package/lib/sync/autosync-config.js +9 -18
  164. package/lib/sync/autosync-scheduler.d.ts +7 -2
  165. package/lib/sync/autosync-scheduler.js +35 -17
  166. package/lib/sync/backup-schedule-config.js +0 -1
  167. package/lib/sync/blob-store.d.ts +85 -0
  168. package/lib/sync/blob-store.js +85 -0
  169. package/lib/sync/git/git-transport.d.ts +28 -5
  170. package/lib/sync/git/git-transport.js +170 -24
  171. package/lib/sync/github-auth.d.ts +0 -1
  172. package/lib/sync/github-auth.js +0 -2
  173. package/lib/sync/layout.d.ts +16 -2
  174. package/lib/sync/layout.js +48 -7
  175. package/lib/sync/risk.d.ts +18 -12
  176. package/lib/sync/risk.js +17 -28
  177. package/lib/sync/session-tombstones.d.ts +39 -0
  178. package/lib/sync/session-tombstones.js +125 -0
  179. package/lib/sync/sync-config.d.ts +26 -3
  180. package/lib/sync/sync-config.js +40 -3
  181. package/lib/sync/sync-engine.d.ts +140 -12
  182. package/lib/sync/sync-engine.js +266 -49
  183. package/lib/sync/sync-history.js +0 -1
  184. package/lib/sync/sync-selection.d.ts +18 -0
  185. package/lib/sync/sync-selection.js +50 -24
  186. package/lib/sync/sync-state.d.ts +31 -4
  187. package/lib/sync/sync-state.js +38 -6
  188. package/lib/sync/transport.d.ts +103 -0
  189. package/lib/sync/transport.js +121 -0
  190. package/lib/sync/ui-prefs.d.ts +4 -2
  191. package/lib/sync/ui-prefs.js +6 -4
  192. package/lib/sync/webdav/webdav-transport.d.ts +36 -4
  193. package/lib/sync/webdav/webdav-transport.js +242 -43
  194. package/lib/ui/dsh-profiles-view.d.ts +52 -9
  195. package/lib/ui/dsh-profiles-view.js +80 -13
  196. package/lib/ui/export-flow.d.ts +2 -2
  197. package/lib/ui/export-flow.js +63 -17
  198. package/lib/ui/i18n.d.ts +12 -2
  199. package/lib/ui/i18n.js +25 -4
  200. package/lib/ui/import-log.d.ts +72 -0
  201. package/lib/ui/import-log.js +110 -0
  202. package/lib/ui/import-stepper.d.ts +14 -0
  203. package/lib/ui/import-stepper.js +18 -0
  204. package/lib/ui/import-wizard.d.ts +106 -3
  205. package/lib/ui/import-wizard.js +125 -7
  206. package/lib/ui/mid-ellipsis.d.ts +31 -0
  207. package/lib/ui/mid-ellipsis.js +39 -0
  208. package/lib/ui/my-configs-view.d.ts +136 -0
  209. package/lib/ui/my-configs-view.js +127 -0
  210. package/lib/ui/progress.js +6 -0
  211. package/lib/ui/report.d.ts +26 -0
  212. package/lib/ui/report.js +37 -0
  213. package/lib/ui/runs-view.d.ts +128 -0
  214. package/lib/ui/runs-view.js +123 -0
  215. package/lib/ui/select-model.d.ts +51 -0
  216. package/lib/ui/select-model.js +84 -0
  217. package/lib/ui/selection-model.d.ts +118 -2
  218. package/lib/ui/selection-model.js +291 -4
  219. package/lib/ui/snapshots-view.d.ts +116 -0
  220. package/lib/ui/snapshots-view.js +141 -0
  221. package/lib/ui/sync-settings-view.d.ts +295 -0
  222. package/lib/ui/sync-settings-view.js +280 -0
  223. package/lib/ui/test-helpers.d.ts +7 -1
  224. package/lib/ui/test-helpers.js +4 -1
  225. package/lib/ui/types.d.ts +8 -1
  226. package/lib/utils/env-lock.d.ts +77 -5
  227. package/lib/utils/env-lock.js +171 -57
  228. package/lib/utils/logger.d.ts +17 -13
  229. package/lib/utils/logger.js +79 -41
  230. package/lib/utils/paths.d.ts +1 -1
  231. package/lib/utils/paths.js +2 -2
  232. package/lib/utils/session-log.d.ts +70 -0
  233. package/lib/utils/session-log.js +347 -0
  234. package/lib/utils/zip.d.ts +49 -5
  235. package/lib/utils/zip.js +240 -51
  236. package/lib/utils/zstd-frame.d.ts +44 -0
  237. package/lib/utils/zstd-frame.js +115 -0
  238. package/package.json +4 -2
  239. package/src/adapters/agent-instructions.ts +5 -3
  240. package/src/adapters/agent-presets.ts +5 -3
  241. package/src/adapters/credentials.ts +19 -22
  242. package/src/adapters/file-collection.test.ts +256 -0
  243. package/src/adapters/file-collection.ts +129 -17
  244. package/src/adapters/index.ts +12 -1
  245. package/src/adapters/json-section.ts +73 -0
  246. package/src/adapters/mcp.ts +16 -19
  247. package/src/adapters/plugin-files.ts +11 -14
  248. package/src/adapters/plugins.test.ts +1 -1
  249. package/src/adapters/plugins.ts +39 -42
  250. package/src/adapters/prompts.ts +16 -20
  251. package/src/adapters/providers.ts +22 -25
  252. package/src/adapters/roundtrip.test.ts +44 -0
  253. package/src/adapters/sessions.test.ts +400 -1
  254. package/src/adapters/sessions.ts +435 -16
  255. package/src/adapters/settings.ts +22 -25
  256. package/src/adapters/skills.ts +5 -3
  257. package/src/adapters/test-helpers.ts +8 -0
  258. package/src/adapters/ui.ts +14 -17
  259. package/src/adapters/workspaces.test.ts +226 -2
  260. package/src/adapters/workspaces.ts +247 -16
  261. package/src/cli/index.ts +180 -37
  262. package/src/cli/sessions-repair.ts +229 -0
  263. package/src/client/CLIENT_DEPENDENCIES.md +3 -1
  264. package/src/client/ConfigManagerSection.tsx +50 -46
  265. package/src/client/about/AboutPanel.tsx +22 -8
  266. package/src/client/about/about-view.test.ts +53 -6
  267. package/src/client/about/about-view.ts +26 -2
  268. package/src/client/api.ts +195 -242
  269. package/src/client/client-types.ts +2 -2
  270. package/src/client/common/ConfirmDialog.tsx +3 -2
  271. package/src/client/common/ContentPicker.tsx +53 -24
  272. package/src/client/common/CopyButton.tsx +74 -0
  273. package/src/client/common/ErrorBanner.tsx +3 -2
  274. package/src/client/common/Icon.tsx +116 -0
  275. package/src/client/common/Modal.tsx +29 -8
  276. package/src/client/common/Motion.tsx +70 -0
  277. package/src/client/common/ProgressBar.tsx +18 -9
  278. package/src/client/common/ReportView.tsx +128 -54
  279. package/src/client/common/RunsCenter.tsx +378 -0
  280. package/src/client/common/Select.tsx +223 -0
  281. package/src/client/common/Skeleton.tsx +128 -0
  282. package/src/client/common/client-route-parity.test.ts +171 -0
  283. package/src/client/common/http-usage-guard.test.ts +163 -0
  284. package/src/client/common/http.test.ts +225 -0
  285. package/src/client/common/http.ts +238 -0
  286. package/src/client/common/morph-icons.test.ts +206 -0
  287. package/src/client/common/morph-icons.ts +89 -0
  288. package/src/client/common/plan-text-redaction.test.ts +29 -7
  289. package/src/client/common/progress-view.test.ts +33 -1
  290. package/src/client/common/progress-view.ts +17 -0
  291. package/src/client/common/routes.ts +130 -0
  292. package/src/client/common/runs-center-guard.test.ts +44 -0
  293. package/src/client/common/ui.tsx +48 -5
  294. package/src/client/config-manager.module.css +877 -35
  295. package/src/client/export/ExportView.tsx +27 -3
  296. package/src/client/history/HistoryPanel.tsx +30 -30
  297. package/src/client/history/history-api.ts +34 -61
  298. package/src/client/icon-layer-guard.test.ts +137 -0
  299. package/src/client/import/ImportLogPanel.tsx +136 -0
  300. package/src/client/import/ImportWizardView.tsx +455 -838
  301. package/src/client/import/PathMappingForm.tsx +12 -1
  302. package/src/client/import/import-wizard-split.test.ts +101 -0
  303. package/src/client/import/import-wizard-steps.tsx +352 -0
  304. package/src/client/import/use-import-wizard-controller.ts +476 -0
  305. package/src/client/index.ts +4 -6
  306. package/src/client/locales.ts +258 -123
  307. package/src/client/lucide-icons.d.ts +11 -0
  308. package/src/client/market/MarketPanel.tsx +39 -37
  309. package/src/client/market/MyConfigsInstall.tsx +93 -0
  310. package/src/client/market/MyConfigsList.tsx +121 -0
  311. package/src/client/market/MyConfigsLoginCard.tsx +100 -0
  312. package/src/client/market/MyConfigsView.tsx +148 -476
  313. package/src/client/market/MyConfigsWizard.tsx +252 -0
  314. package/src/client/market/market-api.test.ts +36 -0
  315. package/src/client/market/market-api.ts +13 -70
  316. package/src/client/market/market-view.ts +1 -1
  317. package/src/client/market/my-configs-api.ts +17 -71
  318. package/src/client/market/my-configs-split.test.ts +58 -0
  319. package/src/client/overview/OverviewPanel.tsx +5 -48
  320. package/src/client/profiles/ProfilesPanel.tsx +422 -215
  321. package/src/client/recovery/RecoveryPanel.tsx +121 -5
  322. package/src/client/recovery/incident-api.ts +91 -0
  323. package/src/client/recovery/recovery-api.ts +15 -65
  324. package/src/client/recovery/recovery-locales.ts +56 -0
  325. package/src/client/recovery/recovery-view.ts +71 -0
  326. package/src/client/run-store.test.ts +593 -3
  327. package/src/client/run-store.ts +430 -80
  328. package/src/client/snapshots/BackupScheduleCard.tsx +434 -0
  329. package/src/client/snapshots/RestorePlanView.tsx +8 -5
  330. package/src/client/snapshots/SnapshotsEmptyState.tsx +31 -0
  331. package/src/client/snapshots/SnapshotsListTable.tsx +97 -0
  332. package/src/client/snapshots/SnapshotsPanel.tsx +55 -630
  333. package/src/client/sync/AutosyncCard.tsx +84 -0
  334. package/src/client/sync/ChannelConfigDialog.tsx +266 -0
  335. package/src/client/sync/DecryptPasswordCard.tsx +48 -0
  336. package/src/client/sync/SecurityOptionsCard.tsx +83 -0
  337. package/src/client/sync/SyncChannelEntryCard.tsx +78 -0
  338. package/src/client/sync/SyncConfirmView.tsx +10 -8
  339. package/src/client/sync/SyncHistoryView.tsx +3 -2
  340. package/src/client/sync/SyncSectionPickerDialog.tsx +182 -0
  341. package/src/client/sync/SyncSettingsView.test.ts +85 -0
  342. package/src/client/sync/SyncSettingsView.tsx +247 -667
  343. package/src/client/sync/history-model.ts +4 -10
  344. package/src/client/sync/sync-api.ts +59 -100
  345. package/src/client/sync/sync-locales.ts +16 -2
  346. package/src/client/sync/sync-view-v2.test.ts +30 -4
  347. package/src/client/sync/sync-view.ts +67 -17
  348. package/src/core/analyzer-mapping.test.ts +55 -0
  349. package/src/core/analyzer.ts +458 -111
  350. package/src/core/apply-order.test.ts +182 -0
  351. package/src/core/backup-plan.test.ts +141 -0
  352. package/src/core/backup-plan.ts +16 -8
  353. package/src/core/backup.test.ts +310 -0
  354. package/src/core/backup.ts +174 -23
  355. package/src/core/base-path-rebase.test.ts +97 -0
  356. package/src/core/boot-paths.test.ts +35 -0
  357. package/src/core/boot-paths.ts +29 -0
  358. package/src/core/boot-rescue.test.ts +108 -0
  359. package/src/core/boot-rescue.ts +57 -1
  360. package/src/core/boot-safety.test.ts +138 -0
  361. package/src/core/boot-safety.ts +248 -0
  362. package/src/core/conflict-decisions.ts +34 -0
  363. package/src/core/crash-report.test.ts +25 -41
  364. package/src/core/crash-report.ts +34 -41
  365. package/src/core/credential-status.test.ts +35 -0
  366. package/src/core/credential-status.ts +25 -0
  367. package/src/core/export-coupling.test.ts +255 -0
  368. package/src/core/exporter.ts +110 -3
  369. package/src/core/finalize-order.test.ts +82 -0
  370. package/src/core/import-sessions-visibility.test.ts +93 -0
  371. package/src/core/importer.ts +21 -3
  372. package/src/core/incident-wiring.test.ts +96 -0
  373. package/src/core/index.ts +10 -0
  374. package/src/core/journal.test.ts +59 -2
  375. package/src/core/journal.ts +152 -10
  376. package/src/core/messages.ts +110 -0
  377. package/src/core/migration-consult.ts +0 -5
  378. package/src/core/migration-history.test.ts +3 -5
  379. package/src/core/model-tools.test.ts +0 -14
  380. package/src/core/model-tools.ts +6 -8
  381. package/src/core/path-mapping.ts +33 -0
  382. package/src/core/phase3-consistency.test.ts +1 -1
  383. package/src/core/phase3-host.ts +248 -70
  384. package/src/core/phase3-p2b-fingerprint.test.ts +0 -1
  385. package/src/core/phase3-production-integration.test.ts +1 -1
  386. package/src/core/phase4-crash-child.ts +0 -1
  387. package/src/core/phase4-crash-injection.test.ts +1 -3
  388. package/src/core/plugin-cli.fs.test.ts +54 -2
  389. package/src/core/plugin-cli.ts +21 -2
  390. package/src/core/reconcile.test.ts +62 -0
  391. package/src/core/reconcile.ts +20 -5
  392. package/src/core/recovery-orchestrator.test.ts +13 -1
  393. package/src/core/recovery-orchestrator.ts +1 -1
  394. package/src/core/restore.ts +10 -4
  395. package/src/core/rollback.ts +1 -1
  396. package/src/core/run-registry-runs.test.ts +71 -0
  397. package/src/core/run-registry.test.ts +58 -7
  398. package/src/core/run-registry.ts +96 -5
  399. package/src/core/session-meta.test.ts +59 -1
  400. package/src/core/session-meta.ts +62 -29
  401. package/src/core/session-repair.test.ts +91 -0
  402. package/src/core/session-repair.ts +203 -0
  403. package/src/core/session-select.test.ts +73 -0
  404. package/src/core/session-select.ts +131 -0
  405. package/src/core/smoke.test.ts +5 -3
  406. package/src/core/startup-barrier.ts +29 -2
  407. package/src/core/transaction-coordinator.ts +3 -3
  408. package/src/core/types.ts +253 -0
  409. package/src/core/validator.ts +0 -1
  410. package/src/index.sync.test.ts +8 -0
  411. package/src/index.ts +611 -2922
  412. package/src/market/git-file-writer.test.ts +1 -1
  413. package/src/market/index-parser.ts +29 -6
  414. package/src/market/market.test.ts +146 -1
  415. package/src/market/my-repo.test.ts +57 -4
  416. package/src/market/my-repo.ts +0 -7
  417. package/src/market/prepare.ts +12 -32
  418. package/src/market/reader.ts +0 -1
  419. package/src/market/security.ts +3 -11
  420. package/src/profiles/dsh-profile-launcher.test.ts +359 -0
  421. package/src/profiles/dsh-profile-launcher.ts +473 -0
  422. package/src/profiles/dsh-profile-manager.test.ts +123 -59
  423. package/src/profiles/dsh-profile-manager.ts +148 -51
  424. package/src/profiles/dsh-profile-runtime.test.ts +196 -0
  425. package/src/profiles/dsh-profile-runtime.ts +228 -0
  426. package/src/profiles/dsh-profile-shared.ts +126 -9
  427. package/src/profiles/index.ts +30 -4
  428. package/src/profiles/process-control.ts +99 -0
  429. package/src/routes/backup.ts +103 -0
  430. package/src/routes/consult.ts +139 -0
  431. package/src/routes/context.ts +10 -0
  432. package/src/routes/history.ts +56 -0
  433. package/src/routes/import.ts +399 -0
  434. package/src/routes/index.ts +36 -0
  435. package/src/routes/kit.ts +223 -0
  436. package/src/routes/market.ts +275 -0
  437. package/src/routes/me.ts +168 -0
  438. package/src/routes/prefs.ts +104 -0
  439. package/src/routes/profiles.ts +252 -0
  440. package/src/routes/recovery.ts +113 -0
  441. package/src/routes/snapshots.ts +219 -0
  442. package/src/routes/sync.ts +810 -0
  443. package/src/schema/config.ts +96 -91
  444. package/src/schema/index.ts +33 -1
  445. package/src/schema/manifest.ts +3 -0
  446. package/src/schema/registry.test.ts +293 -0
  447. package/src/schema/section-registry.ts +463 -0
  448. package/src/schema/types.ts +13 -3
  449. package/src/schema/versions.ts +31 -1
  450. package/src/security/secret-scanner.ts +1 -1
  451. package/src/security/security.test.ts +145 -2
  452. package/src/security/zip-security.ts +20 -92
  453. package/src/sync/ancestor.test.ts +0 -1
  454. package/src/sync/ancestor.ts +23 -0
  455. package/src/sync/autosync-config.ts +9 -18
  456. package/src/sync/autosync-scheduler.test.ts +60 -3
  457. package/src/sync/autosync-scheduler.ts +45 -19
  458. package/src/sync/backup-schedule-config.ts +0 -1
  459. package/src/sync/backup-scheduler.test.ts +15 -2
  460. package/src/sync/blob-store.test.ts +191 -0
  461. package/src/sync/blob-store.ts +139 -0
  462. package/src/sync/git/git-transport.test.ts +203 -1
  463. package/src/sync/git/git-transport.ts +173 -24
  464. package/src/sync/github-auth.ts +0 -2
  465. package/src/sync/layout.ts +62 -8
  466. package/src/sync/risk.test.ts +32 -1
  467. package/src/sync/risk.ts +26 -31
  468. package/src/sync/session-tombstones.test.ts +87 -0
  469. package/src/sync/session-tombstones.ts +123 -0
  470. package/src/sync/sync-config.test.ts +132 -0
  471. package/src/sync/sync-config.ts +47 -6
  472. package/src/sync/sync-credentials.test.ts +77 -0
  473. package/src/sync/sync-engine.test.ts +799 -8
  474. package/src/sync/sync-engine.ts +322 -51
  475. package/src/sync/sync-history.ts +0 -1
  476. package/src/sync/sync-selection.test.ts +43 -7
  477. package/src/sync/sync-selection.ts +56 -24
  478. package/src/sync/sync-state.test.ts +83 -0
  479. package/src/sync/sync-state.ts +58 -7
  480. package/src/sync/transport.ts +191 -0
  481. package/src/sync/ui-prefs.ts +10 -6
  482. package/src/sync/webdav/webdav-transport.test.ts +122 -2
  483. package/src/sync/webdav/webdav-transport.ts +256 -37
  484. package/src/ui/dsh-profiles-view.test.ts +87 -12
  485. package/src/ui/dsh-profiles-view.ts +106 -17
  486. package/src/ui/export-flow.ts +65 -18
  487. package/src/ui/i18n.test.ts +147 -0
  488. package/src/ui/i18n.ts +25 -4
  489. package/src/ui/import-log.test.ts +106 -0
  490. package/src/ui/import-log.ts +158 -0
  491. package/src/ui/import-stepper.test.ts +25 -1
  492. package/src/ui/import-stepper.ts +18 -0
  493. package/src/ui/import-wizard.test.ts +168 -2
  494. package/src/ui/import-wizard.ts +175 -8
  495. package/src/ui/market-import.test.ts +0 -1
  496. package/src/ui/mid-ellipsis.test.ts +160 -0
  497. package/src/ui/mid-ellipsis.ts +37 -0
  498. package/src/ui/my-configs-view.test.ts +222 -0
  499. package/src/ui/my-configs-view.ts +206 -0
  500. package/src/ui/progress.ts +6 -0
  501. package/src/ui/report.test.ts +32 -2
  502. package/src/ui/report.ts +52 -0
  503. package/src/ui/runs-view.test.ts +152 -0
  504. package/src/ui/runs-view.ts +207 -0
  505. package/src/ui/select-model.test.ts +75 -0
  506. package/src/ui/select-model.ts +92 -0
  507. package/src/ui/selection-model.test.ts +315 -12
  508. package/src/ui/selection-model.ts +327 -4
  509. package/src/ui/snapshots-view.test.ts +228 -0
  510. package/src/ui/snapshots-view.ts +233 -0
  511. package/src/ui/sync-settings-view.test.ts +348 -0
  512. package/src/ui/sync-settings-view.ts +516 -0
  513. package/src/ui/test-helpers.ts +8 -1
  514. package/src/ui/types.ts +10 -1
  515. package/src/utils/env-lock.test.ts +290 -107
  516. package/src/utils/env-lock.ts +209 -54
  517. package/src/utils/logger.test.ts +178 -1
  518. package/src/utils/logger.ts +81 -44
  519. package/src/utils/paths.ts +2 -2
  520. package/src/utils/session-log.test.ts +136 -0
  521. package/src/utils/session-log.ts +344 -0
  522. package/src/utils/zip.test.ts +284 -0
  523. package/src/utils/zip.ts +226 -52
  524. package/src/utils/zstd-frame.test.ts +80 -0
  525. package/src/utils/zstd-frame.ts +131 -0
  526. package/lib/core/config-lifecycle.d.ts +0 -180
  527. package/lib/core/config-lifecycle.js +0 -481
  528. package/lib/core/config-snapshot.d.ts +0 -135
  529. package/lib/core/config-snapshot.js +0 -424
  530. package/lib/core/config-state.d.ts +0 -70
  531. package/lib/core/config-state.js +0 -177
  532. package/lib/core/undo.d.ts +0 -73
  533. package/lib/core/undo.js +0 -87
  534. package/lib/core/watcher.d.ts +0 -101
  535. package/lib/core/watcher.js +0 -219
  536. package/src/client/lifecycle/LifecyclePanel.tsx +0 -525
  537. package/src/client/lifecycle/lifecycle-api.ts +0 -249
  538. package/src/core/config-lifecycle.test.ts +0 -727
  539. package/src/core/config-lifecycle.ts +0 -573
  540. package/src/core/config-snapshot.test.ts +0 -369
  541. package/src/core/config-snapshot.ts +0 -522
  542. package/src/core/config-state.test.ts +0 -381
  543. package/src/core/config-state.ts +0 -207
  544. package/src/core/phase1-wiring.test.ts +0 -139
  545. package/src/core/undo.ts +0 -116
  546. package/src/core/watcher.test.ts +0 -326
  547. package/src/core/watcher.ts +0 -263
@@ -4,8 +4,8 @@
4
4
  import test from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
  import {
7
- exportCountsText, formatBytes, importSectionStats, renderExportReport, renderImportReport,
8
- renderRollbackReport, sectionFromItemId, suggestedActions,
7
+ exportCountsText, formatBytes, importProblems, importSectionStats, importTotals,
8
+ renderExportReport, renderImportReport, renderRollbackReport, sectionFromItemId, suggestedActions,
9
9
  } from './report.ts';
10
10
  import { makeUiT } from './i18n.ts';
11
11
  import { makeExportReport, makeImportResult, makeRollbackReport } from './test-helpers.ts';
@@ -103,6 +103,36 @@ test('report: suggestedActions 仅「完成」(报告已内联全部详情,
103
103
  assert.deepEqual(suggestedActions(failed), ['done']);
104
104
  });
105
105
 
106
+ test('report: importTotals 汇总(ok/跳过/警告/失败 + problems 不含跳过)', () => {
107
+ const totals = importTotals(makeImportResult().executed);
108
+ assert.deepEqual(totals, { ok: 5, skipped: 2, warned: 0, failed: 0, problems: 0 });
109
+
110
+ const withIssues = importTotals([
111
+ { itemId: 'settings:a', status: 'ok' },
112
+ { itemId: 'plugin:x', status: 'warning', message: '装不上' },
113
+ { itemId: 'mcp:m', status: 'failed', message: 'npx 不存在' },
114
+ { itemId: 'plugin:y', status: 'skipped' },
115
+ ]);
116
+ assert.deepEqual(withIssues, { ok: 1, skipped: 1, warned: 1, failed: 1, problems: 2 });
117
+ });
118
+
119
+ test('report: importProblems 抽失败/警告项(带分区与原因,保持执行顺序)', () => {
120
+ const problems = importProblems([
121
+ { itemId: 'settings:a', status: 'ok' },
122
+ { itemId: 'plugin:x', status: 'warning', message: '插件 x 安装失败: npm 不可达' },
123
+ { itemId: 'plugin:y', status: 'skipped' },
124
+ { itemId: 'mcp:m', status: 'failed', message: 'npx 不存在' },
125
+ { itemId: 'weird', status: 'warning' },
126
+ ]);
127
+ assert.deepEqual(problems.map((p) => p.itemId), ['plugin:x', 'mcp:m', 'weird'], '只留失败/警告,顺序不变');
128
+ assert.equal(problems[0]!.section, 'plugins', '分区由 itemId 前缀推断(与分区统计同源)');
129
+ assert.equal(problems[0]!.status, 'warning');
130
+ assert.equal(problems[0]!.message, '插件 x 安装失败: npm 不可达');
131
+ assert.equal(problems[1]!.section, 'mcp');
132
+ assert.equal(problems[2]!.section, 'other', '未知前缀归 other');
133
+ assert.equal(problems[2]!.message, undefined, '无原因时不留 undefined 字段');
134
+ });
135
+
106
136
  test('report: formatBytes', () => {
107
137
  assert.equal(formatBytes(512), '512 B');
108
138
  assert.equal(formatBytes(20480), '20.0 KB');
package/src/ui/report.ts CHANGED
@@ -115,6 +115,58 @@ export function importSectionStats(executed: readonly ExecutedItem[]): ImportSec
115
115
  return [...bySection.values()];
116
116
  }
117
117
 
118
+ /** 导入结果总数(跨分区合计口径;结果页顶部「总览」徽章用,替代逐分区 `settings: 6✓ 12≈` 天书)。 */
119
+ export interface ImportTotals {
120
+ ok: number;
121
+ skipped: number;
122
+ warned: number;
123
+ failed: number;
124
+ /** 需要用户关注的项数 = 警告 + 失败(跳过是「按预期未写」,不算问题)。 */
125
+ problems: number;
126
+ }
127
+
128
+ /** 汇总 executed 的总数(与 importSectionStats 同源口径:ok / skipped / warning / failed)。 */
129
+ export function importTotals(executed: readonly ExecutedItem[]): ImportTotals {
130
+ const totals: ImportTotals = { ok: 0, skipped: 0, warned: 0, failed: 0, problems: 0 };
131
+ for (const e of executed) {
132
+ if (e.status === 'ok') totals.ok += 1;
133
+ else if (e.status === 'skipped') totals.skipped += 1;
134
+ else if (e.status === 'warning') totals.warned += 1;
135
+ else totals.failed += 1;
136
+ }
137
+ totals.problems = totals.warned + totals.failed;
138
+ return totals;
139
+ }
140
+
141
+ /** 结果页「需要你关注」清单条目(失败 / 警告项 + 所属分区)。 */
142
+ export interface ImportProblem {
143
+ section: SectionId | 'other';
144
+ itemId: string;
145
+ status: 'failed' | 'warning';
146
+ message?: string;
147
+ }
148
+
149
+ /**
150
+ * 跨分区抽出失败 / 警告项(保持执行顺序)。
151
+ *
152
+ * 为什么需要:原先这些项的原因只以纯文本行埋在 `renderImportReport` 的等宽块里
153
+ * (` 说明: 插件 x 安装失败…`),用户看到「⚠ 5 需要注意」却不知道是哪 5 个;结构化
154
+ * 清单直接把「哪一项 + 什么原因」摆出来。
155
+ */
156
+ export function importProblems(executed: readonly ExecutedItem[]): ImportProblem[] {
157
+ const out: ImportProblem[] = [];
158
+ for (const e of executed) {
159
+ if (e.status !== 'failed' && e.status !== 'warning') continue;
160
+ out.push({
161
+ section: sectionFromItemId(e.itemId),
162
+ itemId: e.itemId,
163
+ status: e.status,
164
+ ...(e.message !== undefined ? { message: e.message } : {}),
165
+ });
166
+ }
167
+ return out;
168
+ }
169
+
118
170
  /** 导入报告渲染(含回滚状态;§22 动作按钮由 suggestedActions 给出) */
119
171
  export function renderImportReport(result: ImportResult, t: UiT = zhUiT): string {
120
172
  const lines: string[] = [result.ok ? t('report.importComplete') : t('report.importFailed'), ''];
@@ -0,0 +1,152 @@
1
+ /**
2
+ * 运行中心模型单测:卡片可终止性 / 待决策态 / 日志聚合 / 决策框默认值。
3
+ */
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+
7
+ import { cancelDialogModel, relativeTimeLabel, runCards, runSummary, RUN_CARD_LOG_TAIL } from './runs-view.ts';
8
+ import type { RunState } from '../core/run-registry.ts';
9
+
10
+ function run(partial: Partial<RunState> & { runId: string }): RunState {
11
+ return {
12
+ kind: 'import',
13
+ status: 'running',
14
+ section: null,
15
+ sectionTotal: null,
16
+ item: null,
17
+ itemTotal: null,
18
+ detail: null,
19
+ log: [],
20
+ pendingDecision: null,
21
+ createdAt: 0,
22
+ updatedAt: 0,
23
+ ...partial,
24
+ };
25
+ }
26
+
27
+ test('runCards:按 updatedAt 倒序,且不修改入参数组', () => {
28
+ const list = [run({ runId: 'a', updatedAt: 100 }), run({ runId: 'b', updatedAt: 300 })];
29
+ const cards = runCards(list);
30
+ assert.deepEqual(cards.map((c) => c.runId), ['b', 'a']);
31
+ assert.deepEqual(list.map((r) => r.runId), ['a', 'b'], '入参数组顺序不得被就地改写');
32
+ });
33
+
34
+ test('runCards:只有「导入 + running」可终止(其余 run 类型没有安全点,绝不放假按钮)', () => {
35
+ const cards = runCards([
36
+ run({ runId: 'imp', kind: 'import' }),
37
+ run({ runId: 'sync', kind: 'sync-apply' }),
38
+ run({ runId: 'done', status: 'done' }),
39
+ run({ runId: 'auto', kind: 'autosync' }),
40
+ ]);
41
+ assert.deepEqual(cards.filter((c) => c.canCancel).map((c) => c.runId), ['imp']);
42
+ });
43
+
44
+ test('runCards:待决策态优先(在等人 ≠ 卡住):awaitingDecision 为真时不得同时给出终止按钮', () => {
45
+ const cards = runCards([run({ runId: 'waiting', pendingDecision: 'cancel' })]);
46
+ assert.equal(cards[0]?.awaitingDecision, true);
47
+ assert.equal(cards[0]?.canCancel, false, '已在等决策时再点终止没有意义');
48
+ assert.equal(cards[0]?.canSkip, false, '等待决策期间不得再发跳过');
49
+ });
50
+
51
+ test('runCards:跳过当前插件的判据复用 isSkippablePluginInstall(detail 带 plugin: 前缀)', () => {
52
+ const cards = runCards([
53
+ run({ runId: 'installing', section: 'plugins', detail: 'plugin:@linxin666/dsh-ssh' }),
54
+ run({ runId: 'patching', section: 'plugins', detail: 'plugins:pnpm-workspace.yaml' }),
55
+ run({ runId: 'settings', section: 'settings', detail: 'settings:general' }),
56
+ ]);
57
+ assert.deepEqual(cards.filter((c) => c.canSkip).map((c) => c.runId), ['installing'], '只有正在安装的插件项可跳过');
58
+ });
59
+
60
+ test('runCards:日志聚合出计数与问题数,尾部行按上限截断', () => {
61
+ const log = [
62
+ '▶ plugin:a',
63
+ '✓ plugin:a',
64
+ '▶ plugin:b',
65
+ '⚠ plugin:b',
66
+ '▶ plugin:c',
67
+ '✗ plugin:c',
68
+ ];
69
+ const cards = runCards([run({ runId: 'x', log, item: 2, itemTotal: 5 })]);
70
+ const card = cards[0];
71
+ assert.equal(card?.counts?.ok, 1);
72
+ assert.equal(card?.counts?.warn, 1);
73
+ assert.equal(card?.counts?.fail, 1);
74
+ assert.equal(card?.problems, 2, '问题数 = 警告 + 失败');
75
+ assert.equal(card?.progress.percent, 40);
76
+ const many = Array.from({ length: 30 }, (_v, i) => `line-${i}`);
77
+ assert.equal(runCards([run({ runId: 'y', log: many })])[0]?.logTail.length, RUN_CARD_LOG_TAIL);
78
+ assert.equal(runCards([run({ runId: 'y', log: many })])[0]?.logTail.at(-1), 'line-29');
79
+ });
80
+
81
+ test('runCards:无 item/itemTotal → percent=null(不编造 0% 或 100%)', () => {
82
+ const card = runCards([run({ runId: 'n', item: null, itemTotal: null })])[0];
83
+ assert.equal(card?.progress.percent, null);
84
+ assert.equal(card?.counts, null, '无日志时不产出空计数对象');
85
+ });
86
+
87
+ test('runSummary:只统计 running,awaiting 是其中待决策的那部分', () => {
88
+ const summary = runSummary([
89
+ run({ runId: 'a' }),
90
+ run({ runId: 'b', pendingDecision: 'cancel' }),
91
+ run({ runId: 'c', status: 'done' }),
92
+ ]);
93
+ assert.deepEqual(summary, { running: 2, awaiting: 1, total: 3 });
94
+ });
95
+
96
+ test('cancelDialogModel:默认选项跟随用户既有的「失败不回滚」偏好', () => {
97
+ const conservative = cancelDialogModel(run({ runId: 'a', item: 1, itemTotal: 4 }), { defaultRollbackOnError: true });
98
+ assert.equal(conservative.defaultDecision, 'rollback');
99
+ const keepPreferred = cancelDialogModel(run({ runId: 'a', item: 1, itemTotal: 4 }), { defaultRollbackOnError: false });
100
+ assert.equal(keepPreferred.defaultDecision, 'keep');
101
+ assert.deepEqual(keepPreferred.options.map((o) => o.id), ['rollback', 'keep']);
102
+ });
103
+
104
+ test('cancelDialogModel:代价数字可算则算、算不出给 null(绝不编造)', () => {
105
+ const known = cancelDialogModel(run({ runId: 'a', item: 3, itemTotal: 10 }), { defaultRollbackOnError: true });
106
+ assert.equal(known.appliedCount, 3);
107
+ assert.equal(known.pendingCount, 7);
108
+ const unknown = cancelDialogModel(run({ runId: 'a' }), { defaultRollbackOnError: true });
109
+ assert.equal(unknown.appliedCount, null);
110
+ assert.equal(unknown.pendingCount, null);
111
+ assert.equal(unknown.totalCount, null);
112
+ });
113
+ test('relativeTimeLabel:刚刚 / 分钟 / 小时 / 天(时钟漂移不得产出负数)', () => {
114
+ const now = 10_000_000_000
115
+ assert.deepEqual(relativeTimeLabel(now - 5_000, now), { key: 'overview.time.now' })
116
+ assert.deepEqual(relativeTimeLabel(now - 3 * 60_000, now), { key: 'overview.time.min', params: { n: '3' } })
117
+ assert.deepEqual(relativeTimeLabel(now - 5 * 3_600_000, now), { key: 'overview.time.hour', params: { n: '5' } })
118
+ assert.deepEqual(relativeTimeLabel(now - 50 * 3_600_000, now), { key: 'overview.time.day', params: { n: '2' } })
119
+ assert.deepEqual(relativeTimeLabel(now + 60_000, now), { key: 'overview.time.now' })
120
+ });
121
+
122
+ test('runCards:每张卡片带相对时间(没有它「正在跑」和「几小时前就结束」看起来一样)', () => {
123
+ const now = 10_000_000_000
124
+ const cards = runCards([
125
+ run({ runId: 'old', status: 'done', updatedAt: now - 2 * 3_600_000 }),
126
+ run({ runId: 'fresh', updatedAt: now - 10_000 }),
127
+ ], { now })
128
+ assert.deepEqual(cards.find((c) => c.runId === 'old')?.time, { key: 'overview.time.hour', params: { n: '2' } })
129
+ assert.deepEqual(cards.find((c) => c.runId === 'fresh')?.time, { key: 'overview.time.now' })
130
+ });
131
+ /**
132
+ * 用户报告的场景:点了「终止」以后界面只说「完成当前任务后终止」,然后就没了 ——
133
+ * 等多久、是不是卡住、还能做什么,全都没有。这三条用例钉住「等待可见 + 超阈值给出路」。
134
+ */
135
+ test('cancelWait:已请求终止但未到安全点 → 显示已等待分钟数;超过阈值升级为 stuck', () => {
136
+ const now = 10_000_000
137
+ const fresh = runCards([run({ runId: 'a', cancelRequestedAt: now - 30_000 })], { now })[0]
138
+ assert.deepEqual(fresh?.cancelWait, { minutes: 0, stuck: false }, '刚请求:不显示分钟,也不算卡住');
139
+ const waiting = runCards([run({ runId: 'b', cancelRequestedAt: now - 3 * 60_000 })], { now })[0]
140
+ assert.deepEqual(waiting?.cancelWait, { minutes: 3, stuck: true }, '超过阈值必须标记 stuck(界面要给「先跳过/否则重启」的出路)');
141
+ assert.equal(waiting?.canCancel, false, '已请求过就不再给终止按钮(再点没有任何新效果)');
142
+ });
143
+
144
+ test('cancelWait:未请求终止 → null;已到安全点(待决策)→ 由 awaitingDecision 接管,不再显示等待', () => {
145
+ const now = 10_000_000
146
+ const idle = runCards([run({ runId: 'a' })], { now })[0]
147
+ assert.equal(idle?.cancelWait, null)
148
+ assert.equal(idle?.canCancel, true, '没请求过终止 → 可以终止');
149
+ const atSafePoint = runCards([run({ runId: 'b', cancelRequestedAt: now - 60_000, pendingDecision: 'cancel' })], { now })[0]
150
+ assert.equal(atSafePoint?.cancelWait, null, '已到安全点:界面切到决策框,不再显示「等待」')
151
+ assert.equal(atSafePoint?.awaitingDecision, true)
152
+ });
@@ -0,0 +1,207 @@
1
+ /**
2
+ * 运行中心(Run Center)纯渲染模型:把宿主 /runs 的 RunState 列表变成「一张卡片 + 一次决策」。
3
+ *
4
+ * 为什么单独一层:卡片要区分「在跑 / 在等人 / 已结束」,决策框要算「已应用多少 / 剩下多少」
5
+ * 并给出默认选项 —— 这些判据都必须可单测(UI 分层铁律:逻辑在 src/ui,React 壳只装配)。
6
+ *
7
+ * 三条设计约束(读码得出,不是偏好):
8
+ * 1. **可终止 ≠ 所有 run**:只有导入引擎实现了「计划项边界协作式取消」(/runs/cancel);
9
+ * 其余 run 类型(同步/恢复/备份/自动同步)没有安全点,UI 必须如实禁用而不是给个假按钮。
10
+ * 2. **待决策态优先于一切**:pendingDecision 非空时 run 并没有卡住,它在等人 —— 卡片必须显性提示,
11
+ * 否则用户会以为任务死了(这正是「终止需弹窗」的功能入口)。
12
+ * 3. **默认选项跟随既有偏好**:用户此前勾过「失败不回滚」(rollbackOnError=false)时,决策框默认落在
13
+ * 「保留已应用项」;没勾过则默认「回滚」。不推翻用户已有的心智,也不替他做更危险的选择。
14
+ */
15
+ import type { RunKind, RunState } from '../core/run-registry.ts'
16
+ import { buildImportLogModel } from './import-log.ts'
17
+ import { isSkippablePluginInstall } from './import-wizard.ts'
18
+ import type { ImportLogCounts } from './import-log.ts'
19
+
20
+ /** 终止决策(与宿主 /runs/cancel/decision 的取值一一对应)。 */
21
+ export type CancelDecision = 'rollback' | 'keep'
22
+
23
+ /** 一张运行卡片的纯数据模型(文案一律给 key,由壳层翻译)。 */
24
+ export interface RunCardModel {
25
+ runId: string
26
+ kind: RunKind
27
+ /** 卡片标题 key(`runs.kind.<kind>`)。 */
28
+ kindLabelKey: string
29
+ status: 'running' | 'done' | 'failed'
30
+ /** 状态徽章 key(`runs.status.<status>`;待决策时另由 awaitingDecision 覆盖显示)。 */
31
+ statusLabelKey: string
32
+ /** 已到安全点、等用户选「回滚 / 保留」。 */
33
+ awaitingDecision: boolean
34
+ /** 可手动终止(只有导入支持计划项边界协作式取消;已请求过就不再显示按钮)。 */
35
+ canCancel: boolean
36
+ /**
37
+ * 已请求终止、正等当前计划项结束(null = 未请求)。
38
+ * `stuck` = 等待超过阈值 → 界面必须从「请稍候」升级为「给出路」。
39
+ */
40
+ cancelWait: { minutes: number; stuck: boolean } | null
41
+ /** 可「跳过当前插件」(仅导入的插件安装阶段有意义)。 */
42
+ canSkip: boolean
43
+ /** 进度(item/itemTotal 优先;导出用 section 计数)。 */
44
+ progress: {
45
+ labelKey: string
46
+ section: string | null
47
+ item: number | null
48
+ itemTotal: number | null
49
+ detail: string | null
50
+ percent: number | null
51
+ }
52
+ /** 日志聚合计数(无日志时为 null)。 */
53
+ counts: ImportLogCounts | null
54
+ /** 需要用户关注的项数(警告 + 失败)。 */
55
+ problems: number
56
+ /** 最近日志行(已截断;供折叠查看,不含敏感内容:宿主侧落账前已 redact)。 */
57
+ logTail: string[]
58
+ updatedAt: number
59
+ /**
60
+ * 「最近一次更新」的相对时间(复用 overview.time.* 字典)。
61
+ * 为什么需要:没有它,「进行中」和「三小时前就结束的僵尸卡片」在界面上长得一模一样,
62
+ * 用户只能得出「一直在加载」的结论(真机报告)。
63
+ */
64
+ time: { key: string; params?: { n: string } }
65
+ }
66
+
67
+ /** 日志尾部保留行数(卡片里只给「最近发生了什么」,完整日志在所属页面)。 */
68
+ export const RUN_CARD_LOG_TAIL = 8
69
+
70
+ /**
71
+ * 终止请求等多久算「可能卡住」(2 分钟)。
72
+ * 为什么要有阈值:协作式取消只能等到**项边界**,而插件安装可以跑几十分钟。
73
+ * 2 分钟内大概率是在装插件(正常),超过就该把出路摆到用户面前(跳过当前项 / 重启),
74
+ * 否则用户只能一遍遍点「终止…」——而那个按钮此刻已经没有任何作用了。
75
+ */
76
+ export const CANCEL_STUCK_AFTER_MS = 2 * 60 * 1000
77
+
78
+ /**
79
+ * 相对时间(纯函数,now 显式传入 → 可单测):刚刚 / N 分钟前 / N 小时前 / N 天前。
80
+ * 键复用 config-manager 字典里既有的 overview.time.*(同字典,不新造文案)。
81
+ */
82
+ export function relativeTimeLabel(updatedAt: number, now: number): { key: string; params?: { n: string } } {
83
+ const diffMs = Math.max(0, now - updatedAt)
84
+ const minutes = Math.floor(diffMs / 60_000)
85
+ if (minutes < 1) return { key: 'overview.time.now' }
86
+ if (minutes < 60) return { key: 'overview.time.min', params: { n: String(minutes) } }
87
+ const hours = Math.floor(minutes / 60)
88
+ if (hours < 24) return { key: 'overview.time.hour', params: { n: String(hours) } }
89
+ return { key: 'overview.time.day', params: { n: String(Math.floor(hours / 24)) } }
90
+ }
91
+
92
+ function percentOf(item: number | null, itemTotal: number | null): number | null {
93
+ if (item === null || itemTotal === null || itemTotal <= 0) return null
94
+ return Math.min(100, Math.max(0, Math.round((item / itemTotal) * 100)))
95
+ }
96
+
97
+ /** RunState[] → 卡片模型(按 updatedAt 倒序;终态与进行中同列,由 status 区分)。 */
98
+ export function runCards(
99
+ runs: readonly RunState[],
100
+ opts: { logTail?: number; now?: number } = {},
101
+ ): RunCardModel[] {
102
+ const tail = opts.logTail ?? RUN_CARD_LOG_TAIL
103
+ const now = opts.now ?? Date.now()
104
+ return [...runs]
105
+ .sort((a, b) => b.updatedAt - a.updatedAt)
106
+ .map((run) => {
107
+ const log = run.log ?? []
108
+ const model = log.length > 0 ? buildImportLogModel(log) : null
109
+ const running = run.status === 'running'
110
+ const awaitingDecision = running && run.pendingDecision === 'cancel'
111
+ // 终止请求已到达、但还没走到安全点:把「等了多久」算出来给界面用(见 CANCEL_STUCK_AFTER_MS)
112
+ const requestedAt = typeof run.cancelRequestedAt === 'number' ? run.cancelRequestedAt : null
113
+ const waitedMs = requestedAt === null ? 0 : Math.max(0, now - requestedAt)
114
+ const cancelWait = requestedAt !== null && running && !awaitingDecision
115
+ ? { minutes: Math.floor(waitedMs / 60_000), stuck: waitedMs >= CANCEL_STUCK_AFTER_MS }
116
+ : null
117
+ return {
118
+ runId: run.runId,
119
+ kind: run.kind,
120
+ kindLabelKey: `runs.kind.${run.kind}`,
121
+ status: run.status,
122
+ statusLabelKey: `runs.status.${run.status}`,
123
+ awaitingDecision,
124
+ // 只有导入实现了协作式取消;其余类型给禁用态(不给假按钮)。
125
+ // 已请求过终止就不再给按钮:再点也不会有新效果(run 级信号是幂等的),
126
+ // 留着一个亮着的主按钮只会让用户以为「多点几次就能好」。
127
+ canCancel: running && run.kind === 'import' && !awaitingDecision && cancelWait === null,
128
+ cancelWait,
129
+ // 「跳过当前插件」复用向导的**同一判据**(isSkippablePluginInstall:detail 带 plugin: 前缀),
130
+ // 绝不在这里另写一份「是不是插件安装项」的判断 —— 两份判据漂移会让两个页面对同一个 run
131
+ // 给出不同的可操作性。skipRequested 由运行中心自己的在途状态抑制(见 RunsCenter)。
132
+ canSkip: running && run.kind === 'import' && run.pendingDecision !== 'cancel'
133
+ && isSkippablePluginInstall(run.detail ?? undefined, running, false),
134
+ progress: {
135
+ labelKey: `runs.progress.${running ? 'running' : run.status === 'done' ? 'done' : 'failed'}`,
136
+ section: run.section,
137
+ item: run.item,
138
+ itemTotal: run.itemTotal,
139
+ detail: run.detail,
140
+ percent: percentOf(run.item, run.itemTotal),
141
+ },
142
+ counts: model === null ? null : model.counts,
143
+ problems: model === null ? 0 : model.problems,
144
+ logTail: log.slice(-Math.max(0, tail)),
145
+ updatedAt: run.updatedAt,
146
+ time: relativeTimeLabel(run.updatedAt, now),
147
+ }
148
+ })
149
+ }
150
+
151
+ /** 状态栏 / 抽屉徽章用的计数。 */
152
+ export function runSummary(runs: readonly RunState[]): { running: number; awaiting: number; total: number } {
153
+ let running = 0
154
+ let awaiting = 0
155
+ for (const run of runs) {
156
+ if (run.status !== 'running') continue
157
+ running += 1
158
+ if (run.pendingDecision === 'cancel') awaiting += 1
159
+ }
160
+ return { running, awaiting, total: runs.length }
161
+ }
162
+
163
+ /** 终止决策框模型。 */
164
+ export interface CancelDialogModel {
165
+ runId: string
166
+ kindLabelKey: string
167
+ /** 已经应用的计划项数(引擎回传的 executed 长度不可得,故用进度计数近似并如实标注)。 */
168
+ appliedCount: number | null
169
+ /** 计划项总数(null = 宿主未上报)。 */
170
+ totalCount: number | null
171
+ /** 未执行项数(total - applied;无法计算时 null)。 */
172
+ pendingCount: number | null
173
+ defaultDecision: CancelDecision
174
+ options: { id: CancelDecision; labelKey: string; descKey: string }[]
175
+ }
176
+
177
+ /**
178
+ * 终止决策框模型。默认选项跟随用户既有的 rollbackOnError 偏好:
179
+ * 勾过「失败不回滚」→ 默认保留(与他的心智一致);否则默认回滚(安全侧)。
180
+ */
181
+ export function cancelDialogModel(
182
+ run: Pick<RunState, 'runId' | 'kind' | 'item' | 'itemTotal'>,
183
+ opts: { defaultRollbackOnError: boolean },
184
+ ): CancelDialogModel {
185
+ const applied = run.item
186
+ const total = run.itemTotal
187
+ return {
188
+ runId: run.runId,
189
+ kindLabelKey: `runs.kind.${run.kind}`,
190
+ appliedCount: applied,
191
+ totalCount: total,
192
+ pendingCount: applied !== null && total !== null ? Math.max(0, total - applied) : null,
193
+ defaultDecision: opts.defaultRollbackOnError ? 'rollback' : 'keep',
194
+ options: [
195
+ { id: 'rollback', labelKey: 'runs.cancel.rollback', descKey: 'runs.cancel.rollbackDesc' },
196
+ { id: 'keep', labelKey: 'runs.cancel.keep', descKey: 'runs.cancel.keepDesc' },
197
+ ],
198
+ }
199
+ }
200
+
201
+ /** 「已应用/未执行」的用户可读摘要参数(null → 壳层显示「未知」而不是编个数字)。 */
202
+ export function cancelCostParams(model: CancelDialogModel): { applied: string; pending: string } {
203
+ return {
204
+ applied: model.appliedCount === null ? '' : String(model.appliedCount),
205
+ pending: model.pendingCount === null ? '' : String(model.pendingCount),
206
+ }
207
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 自定义下拉交互模型测试(node:test,零依赖)。
3
+ */
4
+ import test from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+
7
+ import {
8
+ edgeActiveIndex, firstEnabledIndex, initialActiveIndex, lastEnabledIndex,
9
+ selectDisplayLabel, selectedOption, stepActiveIndex, type SelectOption,
10
+ } from './select-model.ts'
11
+
12
+ const OPTS: SelectOption[] = [
13
+ { value: 'a', label: 'A' },
14
+ { value: 'b', label: 'B', disabled: true },
15
+ { value: 'c', label: 'C' },
16
+ ]
17
+
18
+ test('selectedOption / selectDisplayLabel:命中、未命中与回退链', () => {
19
+ assert.equal(selectedOption(OPTS, 'c')?.label, 'C')
20
+ assert.equal(selectedOption(OPTS, 'zzz'), undefined)
21
+ assert.equal(selectDisplayLabel(OPTS, 'a'), 'A')
22
+ // 未命中:先用 fallback(调用点的占位文案),没有再退回原值(绝不显示空白)
23
+ assert.equal(selectDisplayLabel(OPTS, 'zzz', '(已失效)'), '(已失效)')
24
+ assert.equal(selectDisplayLabel(OPTS, 'zzz'), 'zzz')
25
+ })
26
+
27
+ test('initialActiveIndex:选中项优先,选中项不可用则落到第一个可用项', () => {
28
+ assert.equal(initialActiveIndex(OPTS, 'c'), 2)
29
+ // 选中项被禁用 → 不把高亮停在禁用项上
30
+ assert.equal(initialActiveIndex(OPTS, 'b'), 0)
31
+ assert.equal(initialActiveIndex(OPTS, 'nope'), 0)
32
+ assert.equal(initialActiveIndex([], 'a'), -1)
33
+ })
34
+
35
+ test('firstEnabledIndex / lastEnabledIndex:跳过禁用项', () => {
36
+ assert.equal(firstEnabledIndex(OPTS), 0)
37
+ assert.equal(lastEnabledIndex(OPTS), 2)
38
+ assert.equal(firstEnabledIndex([{ value: 'x', label: 'X', disabled: true }]), -1)
39
+ assert.equal(lastEnabledIndex([{ value: 'x', label: 'X', disabled: true }]), -1)
40
+ assert.equal(firstEnabledIndex([]), -1)
41
+ })
42
+
43
+ test('stepActiveIndex:禁用项跳过 + 环绕 + 无高亮时落到首/尾', () => {
44
+ // 0 → 跳过被禁用的 1 → 2
45
+ assert.equal(stepActiveIndex(OPTS, 0, 1), 2)
46
+ // 2 → 环绕回 0(中间禁用项跳过)
47
+ assert.equal(stepActiveIndex(OPTS, 2, 1), 0)
48
+ // 0 → 反向环绕 → 2
49
+ assert.equal(stepActiveIndex(OPTS, 0, -1), 2)
50
+ // 无高亮(-1):正向 → 首项,反向 → 尾项
51
+ assert.equal(stepActiveIndex(OPTS, -1, 1), 0)
52
+ assert.equal(stepActiveIndex(OPTS, -1, -1), 2)
53
+ })
54
+
55
+ test('stepActiveIndex:全部禁用 / 空清单恒 -1;单项时原地返回', () => {
56
+ const allDisabled: SelectOption[] = [
57
+ { value: 'a', label: 'A', disabled: true },
58
+ { value: 'b', label: 'B', disabled: true },
59
+ ]
60
+ assert.equal(stepActiveIndex(allDisabled, 0, 1), -1)
61
+ assert.equal(stepActiveIndex([], -1, 1), -1)
62
+ const single: SelectOption[] = [{ value: 'only', label: 'Only' }]
63
+ assert.equal(stepActiveIndex(single, 0, 1), 0)
64
+ assert.equal(stepActiveIndex(single, 0, -1), 0)
65
+ })
66
+
67
+ test('edgeActiveIndex:Home/End 跳过禁用项', () => {
68
+ assert.equal(edgeActiveIndex(OPTS, 'first'), 0)
69
+ assert.equal(edgeActiveIndex(OPTS, 'last'), 2)
70
+ const tailDisabled: SelectOption[] = [
71
+ { value: 'a', label: 'A' },
72
+ { value: 'b', label: 'B', disabled: true },
73
+ ]
74
+ assert.equal(edgeActiveIndex(tailDisabled, 'last'), 0)
75
+ })
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 自定义下拉(Select)的纯交互模型 —— src/client/common/Select.tsx 只做装配。
3
+ *
4
+ * 为什么需要它:原生 <select> 的外观由浏览器渲染(无法跟随 `--dsw-*` token,
5
+ * 深色主题下是白底系统控件、与工作台其余控件格格不入),故改为 div/button 自绘。
6
+ * 自绘换来的是**无障碍与键盘语义要自己扛**(ARIA combobox + listbox 模式),
7
+ * 所以可测的那部分(高亮项推导 / 上下移动 / 首尾跳转)全部落在这里,node 可测。
8
+ *
9
+ * 分层铁律:本文件零依赖(不 import react / 浏览器 API),client 与 node 单测都能用。
10
+ */
11
+
12
+ /** 下拉选项(value 一律字符串:调用点的数字枚举在边界处 String/Number 转换)。 */
13
+ export interface SelectOption {
14
+ /** 回传给 onChange 的值(唯一键) */
15
+ value: string
16
+ /** 展示文本(已本地化;不在本层做 i18n) */
17
+ label: string
18
+ /** 禁用项:键盘移动跳过、不可提交(灰显) */
19
+ disabled?: boolean
20
+ }
21
+
22
+ /** 选中值对应的选项;值不在清单里(如远端快照已被清理)→ undefined。 */
23
+ export function selectedOption(options: SelectOption[], value: string): SelectOption | undefined {
24
+ for (const option of options) {
25
+ if (option.value === value) return option
26
+ }
27
+ return undefined
28
+ }
29
+
30
+ /**
31
+ * 触发器上的显示文本。
32
+ *
33
+ * 值不在清单里时**不显示空**:先用 fallback(调用点给的占位文案),
34
+ * 仍为空则显示原值本身 —— 显示「一个不存在的 id」也比显示空白强
35
+ * (用户至少能看出当前选中的是什么;空白会被读成「没选中」)。
36
+ */
37
+ export function selectDisplayLabel(options: SelectOption[], value: string, fallback = ''): string {
38
+ const hit = selectedOption(options, value)
39
+ if (hit !== undefined) return hit.label
40
+ return fallback !== '' ? fallback : value
41
+ }
42
+
43
+ /**
44
+ * 打开菜单时的初始高亮项:
45
+ * 选中项优先(它是用户下次最可能改动的锚点);选中项不存在或已禁用 → 第一个可用项。
46
+ * 全部禁用 / 空清单 → -1(表示没有高亮项)。
47
+ */
48
+ export function initialActiveIndex(options: SelectOption[], value: string): number {
49
+ const selected = options.findIndex((option) => option.value === value)
50
+ if (selected >= 0 && options[selected]?.disabled !== true) return selected
51
+ return firstEnabledIndex(options)
52
+ }
53
+
54
+ /** 第一个可用项下标;没有 → -1。 */
55
+ export function firstEnabledIndex(options: SelectOption[]): number {
56
+ return options.findIndex((option) => option.disabled !== true)
57
+ }
58
+
59
+ /** 最后一个可用项下标;没有 → -1。 */
60
+ export function lastEnabledIndex(options: SelectOption[]): number {
61
+ for (let i = options.length - 1; i >= 0; i--) {
62
+ if (options[i]?.disabled !== true) return i
63
+ }
64
+ return -1
65
+ }
66
+
67
+ /**
68
+ * 从 from 出发按 delta(±1)移动高亮项。
69
+ *
70
+ * 三条语义(与原生 <select> 的键盘行为对齐):
71
+ * ① 禁用项跳过,不停留;
72
+ * ② 到达末尾**环绕**(原生 select 也环绕),环绕一整圈回到原位;
73
+ * ③ 当前无高亮(from < 0)时:delta > 0 → 第一个可用项,delta < 0 → 最后一个可用项。
74
+ * 全部项禁用 / 空清单 → -1。
75
+ */
76
+ export function stepActiveIndex(options: SelectOption[], from: number, delta: number): number {
77
+ const total = options.length
78
+ if (total === 0) return -1
79
+ if (firstEnabledIndex(options) < 0) return -1
80
+ if (from < 0) return delta >= 0 ? firstEnabledIndex(options) : lastEnabledIndex(options)
81
+ let index = from
82
+ for (let hop = 0; hop < total; hop++) {
83
+ index = (index + delta + total) % total
84
+ if (options[index]?.disabled !== true) return index
85
+ }
86
+ return from
87
+ }
88
+
89
+ /** Home/End:跳到第一个/最后一个可用项。 */
90
+ export function edgeActiveIndex(options: SelectOption[], edge: 'first' | 'last'): number {
91
+ return edge === 'first' ? firstEnabledIndex(options) : lastEnabledIndex(options)
92
+ }