@logictan/dsh-config-manager 0.1.60

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 (429) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/cordis.patch.yml +21 -0
  4. package/lib/adapters/agent-instructions.d.ts +13 -0
  5. package/lib/adapters/agent-instructions.js +46 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/credentials.d.ts +37 -0
  9. package/lib/adapters/credentials.js +122 -0
  10. package/lib/adapters/file-collection.d.ts +15 -0
  11. package/lib/adapters/file-collection.js +127 -0
  12. package/lib/adapters/index.d.ts +57 -0
  13. package/lib/adapters/index.js +55 -0
  14. package/lib/adapters/link-report.d.ts +16 -0
  15. package/lib/adapters/link-report.js +47 -0
  16. package/lib/adapters/mcp.d.ts +28 -0
  17. package/lib/adapters/mcp.js +178 -0
  18. package/lib/adapters/plugin-files.d.ts +18 -0
  19. package/lib/adapters/plugin-files.js +142 -0
  20. package/lib/adapters/plugins.d.ts +41 -0
  21. package/lib/adapters/plugins.js +603 -0
  22. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  23. package/lib/adapters/pnpm-workspace.js +147 -0
  24. package/lib/adapters/prompts.d.ts +30 -0
  25. package/lib/adapters/prompts.js +231 -0
  26. package/lib/adapters/providers.d.ts +34 -0
  27. package/lib/adapters/providers.js +155 -0
  28. package/lib/adapters/self.d.ts +17 -0
  29. package/lib/adapters/self.js +71 -0
  30. package/lib/adapters/sessions.d.ts +14 -0
  31. package/lib/adapters/sessions.js +15 -0
  32. package/lib/adapters/settings.d.ts +28 -0
  33. package/lib/adapters/settings.js +165 -0
  34. package/lib/adapters/skills.d.ts +13 -0
  35. package/lib/adapters/skills.js +14 -0
  36. package/lib/adapters/test-helpers.d.ts +125 -0
  37. package/lib/adapters/test-helpers.js +236 -0
  38. package/lib/adapters/ui.d.ts +27 -0
  39. package/lib/adapters/ui.js +89 -0
  40. package/lib/adapters/workspaces.d.ts +13 -0
  41. package/lib/adapters/workspaces.js +106 -0
  42. package/lib/client.d.ts +1026 -0
  43. package/lib/client.js +10047 -0
  44. package/lib/core/analyzer.d.ts +99 -0
  45. package/lib/core/analyzer.js +934 -0
  46. package/lib/core/backup.d.ts +138 -0
  47. package/lib/core/backup.js +513 -0
  48. package/lib/core/boot-rescue.d.ts +146 -0
  49. package/lib/core/boot-rescue.js +618 -0
  50. package/lib/core/cache-cleaner.d.ts +31 -0
  51. package/lib/core/cache-cleaner.js +95 -0
  52. package/lib/core/config-lifecycle.d.ts +180 -0
  53. package/lib/core/config-lifecycle.js +481 -0
  54. package/lib/core/config-snapshot.d.ts +135 -0
  55. package/lib/core/config-snapshot.js +424 -0
  56. package/lib/core/config-state.d.ts +70 -0
  57. package/lib/core/config-state.js +177 -0
  58. package/lib/core/consult-source.d.ts +58 -0
  59. package/lib/core/consult-source.js +260 -0
  60. package/lib/core/crash-report.d.ts +84 -0
  61. package/lib/core/crash-report.js +317 -0
  62. package/lib/core/exporter.d.ts +68 -0
  63. package/lib/core/exporter.js +375 -0
  64. package/lib/core/ghost-sweep.d.ts +19 -0
  65. package/lib/core/ghost-sweep.js +52 -0
  66. package/lib/core/importer.d.ts +60 -0
  67. package/lib/core/importer.js +29 -0
  68. package/lib/core/index.d.ts +14 -0
  69. package/lib/core/index.js +14 -0
  70. package/lib/core/journal.d.ts +197 -0
  71. package/lib/core/journal.js +418 -0
  72. package/lib/core/local-plugin-host.d.ts +35 -0
  73. package/lib/core/local-plugin-host.js +120 -0
  74. package/lib/core/local-plugin-pack.d.ts +135 -0
  75. package/lib/core/local-plugin-pack.js +278 -0
  76. package/lib/core/messages.d.ts +310 -0
  77. package/lib/core/messages.js +621 -0
  78. package/lib/core/migration-consult.d.ts +117 -0
  79. package/lib/core/migration-consult.js +363 -0
  80. package/lib/core/migration-history.d.ts +153 -0
  81. package/lib/core/migration-history.js +411 -0
  82. package/lib/core/model-tools.d.ts +50 -0
  83. package/lib/core/model-tools.js +148 -0
  84. package/lib/core/msg-types.d.ts +8 -0
  85. package/lib/core/msg-types.js +6 -0
  86. package/lib/core/phase3-child-crash.d.ts +1 -0
  87. package/lib/core/phase3-child-crash.js +87 -0
  88. package/lib/core/phase3-host.d.ts +138 -0
  89. package/lib/core/phase3-host.js +359 -0
  90. package/lib/core/phase3-prod-child.d.ts +1 -0
  91. package/lib/core/phase3-prod-child.js +35 -0
  92. package/lib/core/phase4-crash-child.d.ts +1 -0
  93. package/lib/core/phase4-crash-child.js +103 -0
  94. package/lib/core/plugin-cli.d.ts +150 -0
  95. package/lib/core/plugin-cli.js +497 -0
  96. package/lib/core/reconcile.d.ts +120 -0
  97. package/lib/core/reconcile.js +389 -0
  98. package/lib/core/recovery-orchestrator.d.ts +94 -0
  99. package/lib/core/recovery-orchestrator.js +327 -0
  100. package/lib/core/restore.d.ts +162 -0
  101. package/lib/core/restore.js +703 -0
  102. package/lib/core/rollback.d.ts +12 -0
  103. package/lib/core/rollback.js +145 -0
  104. package/lib/core/run-registry.d.ts +85 -0
  105. package/lib/core/run-registry.js +157 -0
  106. package/lib/core/startup-barrier.d.ts +53 -0
  107. package/lib/core/startup-barrier.js +77 -0
  108. package/lib/core/types.d.ts +515 -0
  109. package/lib/core/types.js +27 -0
  110. package/lib/core/undo.d.ts +73 -0
  111. package/lib/core/undo.js +87 -0
  112. package/lib/core/validator.d.ts +18 -0
  113. package/lib/core/validator.js +77 -0
  114. package/lib/core/verify-recovery.d.ts +21 -0
  115. package/lib/core/verify-recovery.js +256 -0
  116. package/lib/core/watcher.d.ts +101 -0
  117. package/lib/core/watcher.js +219 -0
  118. package/lib/index.d.ts +238 -0
  119. package/lib/index.js +2764 -0
  120. package/lib/migrations/index.d.ts +29 -0
  121. package/lib/migrations/index.js +56 -0
  122. package/lib/migrations/v1-to-v2.d.ts +9 -0
  123. package/lib/migrations/v1-to-v2.js +10 -0
  124. package/lib/profiles/index.d.ts +4 -0
  125. package/lib/profiles/index.js +5 -0
  126. package/lib/profiles/profile-manager.d.ts +106 -0
  127. package/lib/profiles/profile-manager.js +543 -0
  128. package/lib/schema/config.d.ts +28 -0
  129. package/lib/schema/config.js +165 -0
  130. package/lib/schema/index.d.ts +24 -0
  131. package/lib/schema/index.js +26 -0
  132. package/lib/schema/manifest.d.ts +28 -0
  133. package/lib/schema/manifest.js +109 -0
  134. package/lib/schema/tombstones.d.ts +46 -0
  135. package/lib/schema/tombstones.js +64 -0
  136. package/lib/schema/types.d.ts +232 -0
  137. package/lib/schema/types.js +8 -0
  138. package/lib/schema/versions.d.ts +27 -0
  139. package/lib/schema/versions.js +50 -0
  140. package/lib/security/encryption.d.ts +85 -0
  141. package/lib/security/encryption.js +279 -0
  142. package/lib/security/index.d.ts +16 -0
  143. package/lib/security/index.js +17 -0
  144. package/lib/security/integrity.d.ts +30 -0
  145. package/lib/security/integrity.js +78 -0
  146. package/lib/security/redaction.d.ts +26 -0
  147. package/lib/security/redaction.js +95 -0
  148. package/lib/security/secret-scanner.d.ts +86 -0
  149. package/lib/security/secret-scanner.js +404 -0
  150. package/lib/security/vault.d.ts +59 -0
  151. package/lib/security/vault.js +151 -0
  152. package/lib/security/zip-security.d.ts +27 -0
  153. package/lib/security/zip-security.js +154 -0
  154. package/lib/sync/autosync-config.d.ts +39 -0
  155. package/lib/sync/autosync-config.js +151 -0
  156. package/lib/sync/autosync-scheduler.d.ts +139 -0
  157. package/lib/sync/autosync-scheduler.js +564 -0
  158. package/lib/sync/fs.d.ts +18 -0
  159. package/lib/sync/fs.js +49 -0
  160. package/lib/sync/git/git-transport.d.ts +100 -0
  161. package/lib/sync/git/git-transport.js +437 -0
  162. package/lib/sync/github-auth.d.ts +131 -0
  163. package/lib/sync/github-auth.js +264 -0
  164. package/lib/sync/layout.d.ts +43 -0
  165. package/lib/sync/layout.js +203 -0
  166. package/lib/sync/snapshot-json.d.ts +9 -0
  167. package/lib/sync/snapshot-json.js +107 -0
  168. package/lib/sync/sync-config.d.ts +81 -0
  169. package/lib/sync/sync-config.js +281 -0
  170. package/lib/sync/sync-engine.d.ts +269 -0
  171. package/lib/sync/sync-engine.js +591 -0
  172. package/lib/sync/sync-history.d.ts +42 -0
  173. package/lib/sync/sync-history.js +78 -0
  174. package/lib/sync/sync-selection.d.ts +29 -0
  175. package/lib/sync/sync-selection.js +127 -0
  176. package/lib/sync/sync-session.d.ts +40 -0
  177. package/lib/sync/sync-session.js +54 -0
  178. package/lib/sync/sync-state.d.ts +37 -0
  179. package/lib/sync/sync-state.js +108 -0
  180. package/lib/sync/transport.d.ts +75 -0
  181. package/lib/sync/transport.js +55 -0
  182. package/lib/sync/ui-prefs.d.ts +34 -0
  183. package/lib/sync/ui-prefs.js +109 -0
  184. package/lib/sync/webdav/webdav-transport.d.ts +77 -0
  185. package/lib/sync/webdav/webdav-transport.js +451 -0
  186. package/lib/ui/errors.d.ts +19 -0
  187. package/lib/ui/errors.js +81 -0
  188. package/lib/ui/export-flow.d.ts +82 -0
  189. package/lib/ui/export-flow.js +98 -0
  190. package/lib/ui/i18n.d.ts +289 -0
  191. package/lib/ui/i18n.js +581 -0
  192. package/lib/ui/migration-consult-view.d.ts +91 -0
  193. package/lib/ui/migration-consult-view.js +115 -0
  194. package/lib/ui/progress.d.ts +38 -0
  195. package/lib/ui/progress.js +87 -0
  196. package/lib/ui/report.d.ts +23 -0
  197. package/lib/ui/report.js +153 -0
  198. package/lib/ui/test-helpers.d.ts +69 -0
  199. package/lib/ui/test-helpers.js +154 -0
  200. package/lib/ui/types.d.ts +257 -0
  201. package/lib/ui/types.js +21 -0
  202. package/lib/utils/atomic-write.d.ts +88 -0
  203. package/lib/utils/atomic-write.js +399 -0
  204. package/lib/utils/env-lock.d.ts +393 -0
  205. package/lib/utils/env-lock.js +930 -0
  206. package/lib/utils/hashing.d.ts +17 -0
  207. package/lib/utils/hashing.js +41 -0
  208. package/lib/utils/json.d.ts +24 -0
  209. package/lib/utils/json.js +76 -0
  210. package/lib/utils/logger.d.ts +39 -0
  211. package/lib/utils/logger.js +82 -0
  212. package/lib/utils/paths.d.ts +42 -0
  213. package/lib/utils/paths.js +189 -0
  214. package/lib/utils/proxy.d.ts +69 -0
  215. package/lib/utils/proxy.js +455 -0
  216. package/lib/utils/recursive-walk.d.ts +24 -0
  217. package/lib/utils/recursive-walk.js +134 -0
  218. package/lib/utils/zip.d.ts +54 -0
  219. package/lib/utils/zip.js +285 -0
  220. package/package.json +188 -0
  221. package/src/adapters/agent-instructions.ts +48 -0
  222. package/src/adapters/agent-presets.ts +14 -0
  223. package/src/adapters/credentials.test.ts +90 -0
  224. package/src/adapters/credentials.ts +143 -0
  225. package/src/adapters/file-collection.ts +137 -0
  226. package/src/adapters/files.test.ts +353 -0
  227. package/src/adapters/index.ts +94 -0
  228. package/src/adapters/link-report.test.ts +65 -0
  229. package/src/adapters/link-report.ts +61 -0
  230. package/src/adapters/mcp.test.ts +96 -0
  231. package/src/adapters/mcp.ts +185 -0
  232. package/src/adapters/plugin-files.ts +146 -0
  233. package/src/adapters/plugins.test.ts +326 -0
  234. package/src/adapters/plugins.ts +615 -0
  235. package/src/adapters/pnpm-workspace.test.ts +87 -0
  236. package/src/adapters/pnpm-workspace.ts +150 -0
  237. package/src/adapters/prompts.test.ts +110 -0
  238. package/src/adapters/prompts.ts +230 -0
  239. package/src/adapters/providers.test.ts +126 -0
  240. package/src/adapters/providers.ts +173 -0
  241. package/src/adapters/roundtrip.test.ts +190 -0
  242. package/src/adapters/self.test.ts +92 -0
  243. package/src/adapters/self.ts +75 -0
  244. package/src/adapters/sessions.ts +15 -0
  245. package/src/adapters/settings.test.ts +100 -0
  246. package/src/adapters/settings.ts +187 -0
  247. package/src/adapters/skills.ts +14 -0
  248. package/src/adapters/test-helpers.ts +248 -0
  249. package/src/adapters/ui.test.ts +57 -0
  250. package/src/adapters/ui.ts +108 -0
  251. package/src/adapters/workspaces.test.ts +106 -0
  252. package/src/adapters/workspaces.ts +109 -0
  253. package/src/client/ConfigManagerSection.tsx +90 -0
  254. package/src/client/api.ts +66 -0
  255. package/src/client/client-types.ts +18 -0
  256. package/src/client/common/ErrorBanner.tsx +73 -0
  257. package/src/client/common/Icon.tsx +129 -0
  258. package/src/client/common/Modal.tsx +173 -0
  259. package/src/client/common/ToastViewport.tsx +73 -0
  260. package/src/client/common/toast-store.test.ts +192 -0
  261. package/src/client/common/toast-store.ts +181 -0
  262. package/src/client/common/toast-types.ts +7 -0
  263. package/src/client/common/ui.tsx +342 -0
  264. package/src/client/config-manager.module.css +2971 -0
  265. package/src/client/css-modules.d.ts +8 -0
  266. package/src/client/index.ts +65 -0
  267. package/src/client/locales.ts +56 -0
  268. package/src/client/lucide-icons.d.ts +38 -0
  269. package/src/client/run-store.test.ts +223 -0
  270. package/src/client/run-store.ts +372 -0
  271. package/src/client/sync/SyncConfirmView.tsx +379 -0
  272. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  273. package/src/client/sync/SyncHistoryView.tsx +220 -0
  274. package/src/client/sync/SyncSettingsView.tsx +1535 -0
  275. package/src/client/sync/history-model.test.ts +194 -0
  276. package/src/client/sync/history-model.ts +195 -0
  277. package/src/client/sync/sync-api.test.ts +585 -0
  278. package/src/client/sync/sync-api.ts +543 -0
  279. package/src/client/sync/sync-locales.ts +416 -0
  280. package/src/client/sync/sync-push-preview.test.ts +52 -0
  281. package/src/client/sync/sync-view-v2.test.ts +213 -0
  282. package/src/client/sync/sync-view.test.ts +487 -0
  283. package/src/client/sync/sync-view.ts +759 -0
  284. package/src/core/analyzer.ts +1068 -0
  285. package/src/core/backup.ts +605 -0
  286. package/src/core/boot-rescue.test.ts +847 -0
  287. package/src/core/boot-rescue.ts +737 -0
  288. package/src/core/cache-cleaner.test.ts +169 -0
  289. package/src/core/cache-cleaner.ts +121 -0
  290. package/src/core/config-lifecycle.test.ts +727 -0
  291. package/src/core/config-lifecycle.ts +573 -0
  292. package/src/core/config-snapshot.test.ts +369 -0
  293. package/src/core/config-snapshot.ts +522 -0
  294. package/src/core/config-state.test.ts +381 -0
  295. package/src/core/config-state.ts +207 -0
  296. package/src/core/consult-source.test.ts +199 -0
  297. package/src/core/consult-source.ts +293 -0
  298. package/src/core/crash-report.test.ts +380 -0
  299. package/src/core/crash-report.ts +369 -0
  300. package/src/core/exporter.ts +417 -0
  301. package/src/core/ghost-sweep.test.ts +94 -0
  302. package/src/core/ghost-sweep.ts +67 -0
  303. package/src/core/importer.ts +83 -0
  304. package/src/core/index.ts +56 -0
  305. package/src/core/journal.test.ts +257 -0
  306. package/src/core/journal.ts +518 -0
  307. package/src/core/local-plugin-host.ts +152 -0
  308. package/src/core/local-plugin-pack.test.ts +359 -0
  309. package/src/core/local-plugin-pack.ts +363 -0
  310. package/src/core/messages.test.ts +54 -0
  311. package/src/core/messages.ts +664 -0
  312. package/src/core/migration-consult.test.ts +315 -0
  313. package/src/core/migration-consult.ts +494 -0
  314. package/src/core/migration-history.test.ts +319 -0
  315. package/src/core/migration-history.ts +484 -0
  316. package/src/core/model-tools.test.ts +190 -0
  317. package/src/core/model-tools.ts +208 -0
  318. package/src/core/msg-types.ts +10 -0
  319. package/src/core/phase3-child-crash.ts +95 -0
  320. package/src/core/phase3-host.ts +411 -0
  321. package/src/core/phase3-p1.test.ts +212 -0
  322. package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
  323. package/src/core/phase3-prod-child.ts +36 -0
  324. package/src/core/phase3-production-integration.test.ts +286 -0
  325. package/src/core/phase4-crash-child.ts +101 -0
  326. package/src/core/phase4-crash-injection.test.ts +166 -0
  327. package/src/core/plugin-cli.fs.test.ts +184 -0
  328. package/src/core/plugin-cli.test.ts +137 -0
  329. package/src/core/plugin-cli.ts +563 -0
  330. package/src/core/reconcile.test.ts +392 -0
  331. package/src/core/reconcile.ts +498 -0
  332. package/src/core/recovery-orchestrator.test.ts +145 -0
  333. package/src/core/recovery-orchestrator.ts +366 -0
  334. package/src/core/restore-manage.test.ts +94 -0
  335. package/src/core/restore.ts +849 -0
  336. package/src/core/rollback.ts +151 -0
  337. package/src/core/run-progress.test.ts +237 -0
  338. package/src/core/run-registry.test.ts +232 -0
  339. package/src/core/run-registry.ts +209 -0
  340. package/src/core/smoke.test.ts +842 -0
  341. package/src/core/startup-barrier.ts +99 -0
  342. package/src/core/types.ts +542 -0
  343. package/src/core/undo.ts +116 -0
  344. package/src/core/validator.ts +84 -0
  345. package/src/core/verify-recovery.ts +265 -0
  346. package/src/core/watcher.test.ts +326 -0
  347. package/src/core/watcher.ts +263 -0
  348. package/src/index.facade.test.ts +265 -0
  349. package/src/index.sync.test.ts +217 -0
  350. package/src/index.ts +2975 -0
  351. package/src/migrations/index.ts +71 -0
  352. package/src/migrations/v1-to-v2.ts +17 -0
  353. package/src/profiles/index.ts +8 -0
  354. package/src/profiles/profile-manager.test.ts +282 -0
  355. package/src/profiles/profile-manager.ts +630 -0
  356. package/src/schema/config.ts +165 -0
  357. package/src/schema/index.ts +71 -0
  358. package/src/schema/manifest-schema.test.ts +519 -0
  359. package/src/schema/manifest.ts +124 -0
  360. package/src/schema/tombstones.test.ts +131 -0
  361. package/src/schema/tombstones.ts +89 -0
  362. package/src/schema/types.ts +245 -0
  363. package/src/schema/versions.ts +55 -0
  364. package/src/security/encryption.ts +335 -0
  365. package/src/security/index.ts +16 -0
  366. package/src/security/integrity.ts +102 -0
  367. package/src/security/redaction.ts +101 -0
  368. package/src/security/secret-scanner.ts +445 -0
  369. package/src/security/security.test.ts +1215 -0
  370. package/src/security/vault.test.ts +164 -0
  371. package/src/security/vault.ts +198 -0
  372. package/src/security/zip-security.ts +180 -0
  373. package/src/sync/autosync-config.test.ts +147 -0
  374. package/src/sync/autosync-config.ts +179 -0
  375. package/src/sync/autosync-scheduler.test.ts +490 -0
  376. package/src/sync/autosync-scheduler.ts +646 -0
  377. package/src/sync/fs.ts +48 -0
  378. package/src/sync/git/git-transport.test.ts +673 -0
  379. package/src/sync/git/git-transport.ts +491 -0
  380. package/src/sync/github-auth.test.ts +280 -0
  381. package/src/sync/github-auth.ts +356 -0
  382. package/src/sync/layout.test.ts +270 -0
  383. package/src/sync/layout.ts +222 -0
  384. package/src/sync/snapshot-json.test.ts +119 -0
  385. package/src/sync/snapshot-json.ts +128 -0
  386. package/src/sync/sync-config.test.ts +310 -0
  387. package/src/sync/sync-config.ts +299 -0
  388. package/src/sync/sync-engine.test.ts +1074 -0
  389. package/src/sync/sync-engine.ts +754 -0
  390. package/src/sync/sync-history.test.ts +116 -0
  391. package/src/sync/sync-history.ts +120 -0
  392. package/src/sync/sync-selection.test.ts +186 -0
  393. package/src/sync/sync-selection.ts +143 -0
  394. package/src/sync/sync-session.test.ts +143 -0
  395. package/src/sync/sync-session.ts +77 -0
  396. package/src/sync/sync-state.test.ts +164 -0
  397. package/src/sync/sync-state.ts +131 -0
  398. package/src/sync/transport.test.ts +111 -0
  399. package/src/sync/transport.ts +118 -0
  400. package/src/sync/ui-prefs.test.ts +152 -0
  401. package/src/sync/ui-prefs.ts +135 -0
  402. package/src/sync/webdav/webdav-transport.test.ts +871 -0
  403. package/src/sync/webdav/webdav-transport.ts +535 -0
  404. package/src/ui/errors.test.ts +47 -0
  405. package/src/ui/errors.ts +100 -0
  406. package/src/ui/export-flow.test.ts +87 -0
  407. package/src/ui/export-flow.ts +156 -0
  408. package/src/ui/i18n.ts +590 -0
  409. package/src/ui/migration-consult-view.test.ts +151 -0
  410. package/src/ui/migration-consult-view.ts +178 -0
  411. package/src/ui/progress.test.ts +47 -0
  412. package/src/ui/progress.ts +102 -0
  413. package/src/ui/report.test.ts +101 -0
  414. package/src/ui/report.ts +146 -0
  415. package/src/ui/test-helpers.ts +184 -0
  416. package/src/ui/types.ts +325 -0
  417. package/src/utils/atomic-write.test.ts +312 -0
  418. package/src/utils/atomic-write.ts +422 -0
  419. package/src/utils/env-lock.test.ts +999 -0
  420. package/src/utils/env-lock.ts +1086 -0
  421. package/src/utils/hashing.ts +53 -0
  422. package/src/utils/json.ts +77 -0
  423. package/src/utils/logger.ts +114 -0
  424. package/src/utils/paths.ts +176 -0
  425. package/src/utils/proxy.test.ts +465 -0
  426. package/src/utils/proxy.ts +493 -0
  427. package/src/utils/recursive-walk.test.ts +148 -0
  428. package/src/utils/recursive-walk.ts +145 -0
  429. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * m-github-oauth:GitHub OAuth device flow(设备码流程)认证模块。
3
+ *
4
+ * 插件场景选择 device flow 而非授权码+回调:无需注册回调 URL,天然适合本地 GUI
5
+ * 插件 —— 用户点「使用 GitHub 登录」→ 宿主向 GitHub 请求设备码 → UI 展示一次性
6
+ * 用户码 + 授权页 URL → 用户在浏览器确认 → 宿主轮询换取 access token → 存入
7
+ * DSH credentials(SYNC_CREDENTIAL_REF,与手动 token 同一凭据槽位)→ git 通道
8
+ * 的 getToken provider 从那里读取,token 永不进 argv / repoUrl / 日志 / 浏览器。
9
+ *
10
+ * 安全不变量:
11
+ * - 本模块不接触任何秘密的持久化:startDeviceFlow 产出设备码(宿主侧内存登记),
12
+ * pollForToken 只返回 access token(由宿主立即写入 credentials,绝不回传 UI);
13
+ * - 错误消息只携带 GitHub 返回的 error/error_description,绝不内嵌 token 值;
14
+ * - fetcher / now 可注入:测试全程 mock,不碰真实网络。
15
+ *
16
+ * 流程(GitHub 官方 device flow):
17
+ * 1. POST https://github.com/login/device/code { client_id, scope }
18
+ * → { device_code, user_code, verification_uri, expires_in, interval }
19
+ * 2. 用户访问 verification_uri 输入 user_code 授权
20
+ * 3. POST https://github.com/login/oauth/access_token
21
+ * { client_id, device_code, grant_type=urn:ietf:params:oauth:grant-type:device_code
22
+ * [, client_secret] } → { access_token } 或 { error, error_description }
23
+ * error ∈ authorization_pending | slow_down | expired_token | access_denied | ...
24
+ */
25
+ import { randomUUID } from 'node:crypto';
26
+ import { defaultFetcher } from '../utils/proxy.js';
27
+ /** GitHub device flow 第一步:请求设备码 */
28
+ export const GITHUB_DEVICE_CODE_URL = 'https://github.com/login/device/code';
29
+ /** GitHub device flow 第三步:轮询换取 access token */
30
+ export const GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
31
+ /** 缺省 scope:repo(读写私有仓库,同步通道需要) */
32
+ export const GITHUB_DEVICE_FLOW_SCOPE = 'repo';
33
+ /** GitHub 缺省轮询间隔(秒) */
34
+ export const GITHUB_DEFAULT_INTERVAL = 5;
35
+ /** slow_down 时按 RFC 8628 额外增加的等待秒数 */
36
+ export const SLOW_DOWN_EXTRA_SECONDS = 5;
37
+ /** 设备码过期秒数上限兜底(GitHub 默认 900s) */
38
+ const DEVICE_CODE_MAX_EXPIRES_IN = 3600;
39
+ /** GitHub OAuth 可预期错误:code = GitHub error / 固定内部码,status = HTTP 状态(网络层无) */
40
+ export class GitHubAuthError extends Error {
41
+ code;
42
+ status;
43
+ constructor(message, code, status) {
44
+ super(message);
45
+ this.name = 'GitHubAuthError';
46
+ this.code = code;
47
+ this.status = status;
48
+ }
49
+ }
50
+ /** 解析 GitHub 表单/JSON 混合响应:优先 JSON(Accept: application/json 时 GitHub 返回 JSON) */
51
+ async function parseGitHubJson(response, context) {
52
+ const text = await response.text();
53
+ if (text.trim() === '') {
54
+ throw new GitHubAuthError(`${context}: GitHub 返回空响应(HTTP ${response.status})`, 'empty_response', response.status);
55
+ }
56
+ try {
57
+ const parsed = JSON.parse(text);
58
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
59
+ throw new Error('not a JSON object');
60
+ }
61
+ return parsed;
62
+ }
63
+ catch {
64
+ throw new GitHubAuthError(`${context}: GitHub 返回了无法解析的响应(HTTP ${response.status}),请稍后重试`, 'invalid_json', response.status);
65
+ }
66
+ }
67
+ /**
68
+ * GitHub OAuth device flow 客户端。
69
+ * 无状态(设备码登记由宿主侧的 DeviceFlowStore 负责),fetch 可注入。
70
+ */
71
+ export class GitHubAuthClient {
72
+ fetcher;
73
+ now;
74
+ constructor(options = {}) {
75
+ this.fetcher = options.fetcher ?? defaultFetcher();
76
+ this.now = options.now ?? (() => Date.now());
77
+ }
78
+ /**
79
+ * 用给定 token 验证身份并取回当前用户(GET /user)。
80
+ *
81
+ * 供 /sync/github/validate 判定「已存 token 是否仍有效」:401 → `unauthorized`,
82
+ * 其余错误按 `http_error` 抛出(调用方据此区分「未登录」与「真实故障」,不误判登出)。
83
+ * token 只作为本次请求的 Authorization 头,绝不回传、不落盘、不进日志。
84
+ */
85
+ async getUser(token) {
86
+ if (typeof token !== 'string' || token === '') {
87
+ throw new GitHubAuthError('token 必须是非空字符串', 'no_token');
88
+ }
89
+ let response;
90
+ try {
91
+ response = await this.fetcher('https://api.github.com/user', {
92
+ method: 'GET',
93
+ headers: {
94
+ accept: 'application/vnd.github+json',
95
+ authorization: `Bearer ${token}`,
96
+ 'user-agent': 'dsh-config-manager',
97
+ },
98
+ });
99
+ }
100
+ catch (err) {
101
+ throw new GitHubAuthError(`校验 GitHub token 失败:${err instanceof Error ? err.message : String(err)}`, 'network_error');
102
+ }
103
+ if (response.status === 401) {
104
+ throw new GitHubAuthError('GitHub token 无效或已过期', 'unauthorized', 401);
105
+ }
106
+ if (!response.ok) {
107
+ throw new GitHubAuthError(`GitHub 返回 HTTP ${response.status}`, 'http_error', response.status);
108
+ }
109
+ const parsed = await parseGitHubJson(response, '校验 token');
110
+ const login = parsed['login'];
111
+ const id = parsed['id'];
112
+ if (typeof login !== 'string' || login === '' || typeof id !== 'number') {
113
+ throw new GitHubAuthError('GitHub /user 响应缺少 login/id', 'invalid_response', response.status);
114
+ }
115
+ return { login, id };
116
+ }
117
+ /**
118
+ * 第一步:请求设备码。产出一次性 user_code + 授权页 URL + device_code
119
+ * (device_code 由宿主登记,只在本模块与宿主间流转,绝不发给浏览器)。
120
+ */
121
+ async startDeviceFlow(clientId, scope = GITHUB_DEVICE_FLOW_SCOPE) {
122
+ if (typeof clientId !== 'string' || clientId === '') {
123
+ throw new GitHubAuthError('client_id 必须是非空字符串', 'invalid_client_id');
124
+ }
125
+ const body = new URLSearchParams();
126
+ body.set('client_id', clientId);
127
+ body.set('scope', scope);
128
+ let response;
129
+ try {
130
+ response = await this.fetcher(GITHUB_DEVICE_CODE_URL, {
131
+ method: 'POST',
132
+ headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
133
+ body: body.toString(),
134
+ });
135
+ }
136
+ catch (err) {
137
+ throw new GitHubAuthError(`请求 GitHub 设备码失败:${err instanceof Error ? err.message : String(err)}`, 'network_error');
138
+ }
139
+ const parsed = await parseGitHubJson(response, '请求设备码');
140
+ const ghError = parsed['error'];
141
+ if (typeof ghError === 'string' && ghError !== '') {
142
+ const desc = parsed['error_description'];
143
+ throw new GitHubAuthError(`GitHub 拒绝了设备码请求:${typeof desc === 'string' && desc !== '' ? desc : ghError}`, ghError, response.status);
144
+ }
145
+ const deviceCode = parsed['device_code'];
146
+ const userCode = parsed['user_code'];
147
+ const verificationUri = parsed['verification_uri'];
148
+ if (typeof deviceCode !== 'string' || deviceCode === ''
149
+ || typeof userCode !== 'string' || userCode === ''
150
+ || typeof verificationUri !== 'string' || verificationUri === '') {
151
+ throw new GitHubAuthError('GitHub 设备码响应缺少必要字段(device_code/user_code/verification_uri)', 'invalid_response', response.status);
152
+ }
153
+ const expiresIn = typeof parsed['expires_in'] === 'number' && Number.isFinite(parsed['expires_in'])
154
+ ? Math.min(Math.max(Math.floor(parsed['expires_in']), 1), DEVICE_CODE_MAX_EXPIRES_IN)
155
+ : 900;
156
+ const interval = typeof parsed['interval'] === 'number' && Number.isFinite(parsed['interval'])
157
+ ? Math.max(Math.floor(parsed['interval']), 1)
158
+ : GITHUB_DEFAULT_INTERVAL;
159
+ return { deviceCode, userCode, verificationUri, expiresIn, interval };
160
+ }
161
+ /**
162
+ * 单次 token 交换(device flow 第三步的原子操作)。
163
+ * 返回类型化结果:成功 = access token;失败 = GitHub error code(pending/slow_down
164
+ * 等由 pollForToken 归并为轮询状态)。
165
+ */
166
+ async exchangeDeviceCode(params) {
167
+ const body = new URLSearchParams();
168
+ body.set('client_id', params.clientId);
169
+ body.set('device_code', params.deviceCode);
170
+ body.set('grant_type', 'urn:ietf:params:oauth:grant-type:device_code');
171
+ if (params.clientSecret !== undefined && params.clientSecret !== '') {
172
+ body.set('client_secret', params.clientSecret);
173
+ }
174
+ let response;
175
+ try {
176
+ response = await this.fetcher(GITHUB_ACCESS_TOKEN_URL, {
177
+ method: 'POST',
178
+ headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
179
+ body: body.toString(),
180
+ });
181
+ }
182
+ catch (err) {
183
+ throw new GitHubAuthError(`交换 GitHub token 失败:${err instanceof Error ? err.message : String(err)}`, 'network_error');
184
+ }
185
+ const parsed = await parseGitHubJson(response, '交换 token');
186
+ const accessToken = parsed['access_token'];
187
+ if (typeof accessToken === 'string' && accessToken !== '') {
188
+ return { ok: true, accessToken };
189
+ }
190
+ const ghError = parsed['error'];
191
+ const desc = parsed['error_description'];
192
+ return {
193
+ ok: false,
194
+ error: typeof ghError === 'string' && ghError !== '' ? ghError : 'unknown_error',
195
+ errorDescription: typeof desc === 'string' && desc !== '' ? desc : undefined,
196
+ };
197
+ }
198
+ /**
199
+ * 轮询换取 access token,把 GitHub 错误码归并为 UI 可消费的轮询状态:
200
+ * - authorization_pending → pending(按 interval 继续等)
201
+ * - slow_down → pending(interval + 5s,RFC 8628)
202
+ * - expired_token → expired(终止,重新开始)
203
+ * - access_denied → denied(终止)
204
+ * - 其他 error → error(终止,携带 errorCode + 描述)
205
+ */
206
+ async pollForToken(params) {
207
+ const result = await this.exchangeDeviceCode(params);
208
+ if (result.ok) {
209
+ return { status: 'success', accessToken: result.accessToken };
210
+ }
211
+ const intervalMs = Math.max(params.interval ?? GITHUB_DEFAULT_INTERVAL, 1) * 1000;
212
+ switch (result.error) {
213
+ case 'authorization_pending':
214
+ return { status: 'pending', pollDelayMs: intervalMs };
215
+ case 'slow_down':
216
+ return { status: 'pending', pollDelayMs: intervalMs + SLOW_DOWN_EXTRA_SECONDS * 1000 };
217
+ case 'expired_token':
218
+ return { status: 'expired' };
219
+ case 'access_denied':
220
+ return { status: 'denied' };
221
+ default:
222
+ return { status: 'error', errorCode: result.error, message: result.errorDescription };
223
+ }
224
+ }
225
+ }
226
+ /**
227
+ * 宿主内存设备码登记表(进程生命周期,跨请求共享 —— 与 RunRegistry 同模式)。
228
+ * device_code 的持有者是宿主:UI 只拿 flowId(随机 id),轮询时宿主凭 flowId 取回
229
+ * device_code 去 GitHub 换 token。过期条目惰性清理,防止无限增长。
230
+ */
231
+ export class DeviceFlowStore {
232
+ flows = new Map();
233
+ now;
234
+ constructor(now) {
235
+ this.now = now ?? (() => Date.now());
236
+ }
237
+ get size() {
238
+ return this.flows.size;
239
+ }
240
+ /** 登记一个设备码流;flowId 由调用方生成(随机,不可猜) */
241
+ set(flowId, entry) {
242
+ this.flows.set(flowId, entry);
243
+ }
244
+ /** 取回设备码流;过期条目视为不存在并立即清理 */
245
+ get(flowId) {
246
+ const entry = this.flows.get(flowId);
247
+ if (entry === undefined)
248
+ return undefined;
249
+ if (this.now() > entry.expiresAt) {
250
+ this.flows.delete(flowId);
251
+ return undefined;
252
+ }
253
+ return entry;
254
+ }
255
+ /** 主动移除(取消/完成后清理) */
256
+ delete(flowId) {
257
+ this.flows.delete(flowId);
258
+ }
259
+ /** 生成不可猜的 flowId(路由层用) */
260
+ static newFlowId() {
261
+ return randomUUID();
262
+ }
263
+ }
264
+ //# sourceMappingURL=github-auth.js.map
@@ -0,0 +1,43 @@
1
+ import type { SectionId } from '../schema/types.ts';
2
+ import type { SnapshotFs } from './fs.ts';
3
+ import type { SyncSnapshot } from './transport.ts';
4
+ export declare const SNAPSHOT_MANIFEST_FILE = "manifest.json";
5
+ /**
6
+ * 空文件类分区的 git 占位文件(git 不跟踪空目录):
7
+ * 上传方(GitTransport.upload)给 files 为空的文件类分区目录写入本占位文件,
8
+ * 保证远端仓库保留该目录;读回时仅当「文件名 + 内容」同时匹配才过滤,
9
+ * 避免吞掉用户真实同名文件。占位文件不参与 sectionHashes(基于传入数据计算)。
10
+ * 注意:占位内容不得含换行 —— Windows core.autocrlf 会把 LF 转 CRLF,内容校验会失败。
11
+ */
12
+ export declare const SNAPSHOT_KEEP_FILE = ".gitkeep";
13
+ export declare const SNAPSHOT_KEEP_CONTENT = "DSH Config Manager sync placeholder (keep empty section dir)";
14
+ /** 快照根目录下的 manifest.json 结构 */
15
+ export interface SnapshotDirManifest {
16
+ id: string;
17
+ createdAt: string;
18
+ manifest: SyncSnapshot['manifest'];
19
+ /** 各分区内容 hash(写入时由 hashSection 计算) */
20
+ sectionHashes: Partial<Record<SectionId, string>>;
21
+ }
22
+ /**
23
+ * 文件相对路径安全检查:拒绝空/'.'/'..'、绝对路径、Windows 盘符、反斜杠、
24
+ * 含 '..' 或 '.' 片段(防穿越快照根目录)。
25
+ */
26
+ export declare function isSafeRelPath(rel: string): boolean;
27
+ /** 递归列出 dir 下的全部文件(相对 dir 的正斜杠路径,字典序);目录不存在 → [] */
28
+ export declare function listSnapshotFiles(fsx: SnapshotFs, dir: string): Promise<string[]>;
29
+ /**
30
+ * 将快照写入散文件目录。
31
+ * 非法输入(不支持的子分区 / 穿越路径)在任何写入发生前抛错,不留半成品目录。
32
+ */
33
+ export declare function writeSnapshotToDir(snapshot: SyncSnapshot, dir: string, fsx?: SnapshotFs): Promise<SnapshotDirManifest>;
34
+ /**
35
+ * 从散文件目录读回快照。
36
+ * manifest 声明的分区必须可解析:JSON 分区文件缺失 / 文件分区目录缺失 → 抛错(不静默降级),
37
+ * 但 opts.missingFileDir='empty' 时文件分区目录缺失降级为空分区
38
+ * (git 通道专用:git 不跟踪空目录,目录缺失 = 空文件分区;提交原子性保证非空目录不会缺失)。
39
+ * 读回时过滤 git 占位文件(SNAPSHOT_KEEP_FILE),使 files 与上传方数据一致。
40
+ */
41
+ export declare function readSnapshotFromDir(dir: string, fsx?: SnapshotFs, opts?: {
42
+ missingFileDir?: 'throw' | 'empty';
43
+ }): Promise<SyncSnapshot>;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * m-sync-transport:散文件目录布局。
3
+ * 快照根目录 = manifest.json + 按 SECTION_JSON_PATHS 平铺的 JSON 分区
4
+ * + 按 SECTION_FILE_PREFIXES 的文件类分区目录(skills 等真实文件)。
5
+ * 复用 src/schema/config.ts 的路径表;核心逻辑经 SnapshotFs 注入(默认 node:fs/promises)。
6
+ */
7
+ import path from 'node:path';
8
+ import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.js';
9
+ import { sha256Hex } from '../utils/hashing.js';
10
+ import { SECTION_FILE_PREFIXES, SECTION_JSON_PATHS } from '../schema/config.js';
11
+ import { createSnapshotFs, joinFs } from './fs.js';
12
+ import { hashSection } from './sync-state.js';
13
+ import { isEncryptedSections } from './transport.js';
14
+ export const SNAPSHOT_MANIFEST_FILE = 'manifest.json';
15
+ /**
16
+ * 空文件类分区的 git 占位文件(git 不跟踪空目录):
17
+ * 上传方(GitTransport.upload)给 files 为空的文件类分区目录写入本占位文件,
18
+ * 保证远端仓库保留该目录;读回时仅当「文件名 + 内容」同时匹配才过滤,
19
+ * 避免吞掉用户真实同名文件。占位文件不参与 sectionHashes(基于传入数据计算)。
20
+ * 注意:占位内容不得含换行 —— Windows core.autocrlf 会把 LF 转 CRLF,内容校验会失败。
21
+ */
22
+ export const SNAPSHOT_KEEP_FILE = '.gitkeep';
23
+ export const SNAPSHOT_KEEP_CONTENT = 'DSH Config Manager sync placeholder (keep empty section dir)';
24
+ const SNAPSHOT_KEEP_BYTES = new TextEncoder().encode(SNAPSHOT_KEEP_CONTENT);
25
+ function isKeepFile(rel, data) {
26
+ if (rel !== SNAPSHOT_KEEP_FILE)
27
+ return false;
28
+ if (data.length !== SNAPSHOT_KEEP_BYTES.length)
29
+ return false;
30
+ for (let i = 0; i < data.length; i++) {
31
+ if (data[i] !== SNAPSHOT_KEEP_BYTES[i])
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+ /** 布局支持的分区 = SECTION_JSON_PATHS ∪ SECTION_FILE_PREFIXES(secrets 等不在内) */
37
+ const LAYOUT_SECTION_IDS = [
38
+ ...Object.keys(SECTION_JSON_PATHS),
39
+ ...Object.keys(SECTION_FILE_PREFIXES),
40
+ ];
41
+ /**
42
+ * 文件相对路径安全检查:拒绝空/'.'/'..'、绝对路径、Windows 盘符、反斜杠、
43
+ * 含 '..' 或 '.' 片段(防穿越快照根目录)。
44
+ */
45
+ export function isSafeRelPath(rel) {
46
+ if (rel === '' || rel === '.' || rel === '..')
47
+ return false;
48
+ if (rel.startsWith('/') || /^[A-Za-z]:[\\/]/.test(rel))
49
+ return false;
50
+ if (rel.includes('\\'))
51
+ return false;
52
+ const parts = rel.split('/');
53
+ if (parts.some((p) => p === '' || p === '.' || p === '..'))
54
+ return false;
55
+ return true;
56
+ }
57
+ /** 递归列出 dir 下的全部文件(相对 dir 的正斜杠路径,字典序);目录不存在 → [] */
58
+ export async function listSnapshotFiles(fsx, dir) {
59
+ const out = [];
60
+ const walk = async (abs, rel) => {
61
+ const names = await fsx.readdir(abs);
62
+ for (const name of names) {
63
+ if (name === '' || name === '.' || name === '..')
64
+ continue; // 防御异常/恶意 fs
65
+ const childAbs = joinFs(abs, name);
66
+ const childRel = rel === '' ? name : `${rel}/${name}`;
67
+ if (await fsx.isDir(childAbs)) {
68
+ await walk(childAbs, childRel);
69
+ }
70
+ else {
71
+ out.push(childRel);
72
+ }
73
+ }
74
+ };
75
+ await walk(dir, '');
76
+ return out.sort();
77
+ }
78
+ /**
79
+ * 将快照写入散文件目录。
80
+ * 非法输入(不支持的子分区 / 穿越路径)在任何写入发生前抛错,不留半成品目录。
81
+ */
82
+ export async function writeSnapshotToDir(snapshot, dir, fsx = createSnapshotFs()) {
83
+ // 加密快照不写散文件目录(本地不落盘:密文/明文都不落;远端已存密文)
84
+ if (isEncryptedSections(snapshot.sections)) {
85
+ throw new Error('加密快照不写散文件目录(本地不落盘;请直接使用密文传输)');
86
+ }
87
+ const sectionHashes = {};
88
+ for (const [id, data] of Object.entries(snapshot.sections)) {
89
+ const sid = id;
90
+ if (!LAYOUT_SECTION_IDS.includes(sid)) {
91
+ throw new Error(`分区 ${sid} 不在散文件布局支持范围(SECTION_JSON_PATHS / SECTION_FILE_PREFIXES)`);
92
+ }
93
+ sectionHashes[sid] = hashSection(data);
94
+ }
95
+ // 预校验全部文件相对路径(不合法 → 抛错且零写入)
96
+ for (const [sid, data] of Object.entries(snapshot.sections)) {
97
+ if (!(sid in SECTION_FILE_PREFIXES))
98
+ continue;
99
+ for (const file of data.files) {
100
+ if (!isSafeRelPath(file.relativePath)) {
101
+ throw new Error(`非法文件相对路径: ${file.relativePath}(分区 ${sid})`);
102
+ }
103
+ }
104
+ }
105
+ await fsx.mkdir(dir);
106
+ const manifest = {
107
+ id: snapshot.id,
108
+ createdAt: snapshot.createdAt,
109
+ manifest: snapshot.manifest,
110
+ sectionHashes,
111
+ };
112
+ await fsx.writeFile(joinFs(dir, SNAPSHOT_MANIFEST_FILE), new TextEncoder().encode(stringifyJsonSafe(manifest, { space: 2 })));
113
+ // JSON 分区:按 SECTION_JSON_PATHS 平铺
114
+ for (const [sid, rel] of Object.entries(SECTION_JSON_PATHS)) {
115
+ const data = snapshot.sections[sid];
116
+ if (data === undefined)
117
+ continue;
118
+ const abs = joinFs(dir, rel);
119
+ await fsx.mkdir(path.dirname(abs));
120
+ await fsx.writeFile(abs, new TextEncoder().encode(stringifyJsonSafe(data, { space: 2 })));
121
+ }
122
+ // 文件类分区:目录前缀 + 真实文件
123
+ for (const [sid, prefix] of Object.entries(SECTION_FILE_PREFIXES)) {
124
+ const data = snapshot.sections[sid];
125
+ if (data === undefined)
126
+ continue;
127
+ const baseAbs = joinFs(dir, prefix);
128
+ await fsx.mkdir(baseAbs); // 空文件类分区也保留目录(读回可还原空 files)
129
+ for (const file of data.files) {
130
+ const abs = joinFs(baseAbs, file.relativePath);
131
+ await fsx.mkdir(path.dirname(abs));
132
+ await fsx.writeFile(abs, file.data);
133
+ }
134
+ }
135
+ return manifest;
136
+ }
137
+ /**
138
+ * 从散文件目录读回快照。
139
+ * manifest 声明的分区必须可解析:JSON 分区文件缺失 / 文件分区目录缺失 → 抛错(不静默降级),
140
+ * 但 opts.missingFileDir='empty' 时文件分区目录缺失降级为空分区
141
+ * (git 通道专用:git 不跟踪空目录,目录缺失 = 空文件分区;提交原子性保证非空目录不会缺失)。
142
+ * 读回时过滤 git 占位文件(SNAPSHOT_KEEP_FILE),使 files 与上传方数据一致。
143
+ */
144
+ export async function readSnapshotFromDir(dir, fsx = createSnapshotFs(), opts = {}) {
145
+ const manifestAbs = joinFs(dir, SNAPSHOT_MANIFEST_FILE);
146
+ if (!(await fsx.exists(manifestAbs))) {
147
+ throw new Error(`快照目录缺少 ${SNAPSHOT_MANIFEST_FILE}: ${dir}`);
148
+ }
149
+ const parsed = parseJsonSafe(Buffer.from(await fsx.readFile(manifestAbs)).toString('utf8'));
150
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
151
+ throw new Error('manifest.json 损坏:必须是对象');
152
+ }
153
+ const m = parsed;
154
+ if (typeof m['id'] !== 'string' || typeof m['createdAt'] !== 'string'
155
+ || m['manifest'] === null || typeof m['manifest'] !== 'object'
156
+ || m['sectionHashes'] === null || typeof m['sectionHashes'] !== 'object') {
157
+ throw new Error('manifest.json 损坏:缺少 id/createdAt/manifest/sectionHashes');
158
+ }
159
+ const manifest = parsed;
160
+ const sections = {};
161
+ const declared = Object.keys(manifest.sectionHashes);
162
+ for (const sid of declared) {
163
+ if (!LAYOUT_SECTION_IDS.includes(sid)) {
164
+ throw new Error(`manifest 声明了布局不支持的分区 ${sid}`);
165
+ }
166
+ }
167
+ // JSON 分区
168
+ for (const [sid, rel] of Object.entries(SECTION_JSON_PATHS)) {
169
+ if (!(sid in manifest.sectionHashes))
170
+ continue;
171
+ const abs = joinFs(dir, rel);
172
+ if (!(await fsx.exists(abs))) {
173
+ throw new Error(`快照缺少 JSON 分区文件 ${rel}(${sid})`);
174
+ }
175
+ sections[sid] = parseJsonSafe(Buffer.from(await fsx.readFile(abs)).toString('utf8'));
176
+ }
177
+ // 文件类分区
178
+ for (const [sid, prefix] of Object.entries(SECTION_FILE_PREFIXES)) {
179
+ if (!(sid in manifest.sectionHashes))
180
+ continue;
181
+ const baseAbs = joinFs(dir, prefix);
182
+ if (!(await fsx.isDir(baseAbs))) {
183
+ if (opts.missingFileDir === 'empty') {
184
+ // git 通道降级:目录缺失 = 空文件分区(git 不跟踪空目录,详见函数注释)
185
+ sections[sid] = { version: 1, files: [] };
186
+ continue;
187
+ }
188
+ throw new Error(`快照缺少文件分区目录 ${prefix}(${sid})`);
189
+ }
190
+ const rels = await listSnapshotFiles(fsx, baseAbs);
191
+ const files = [];
192
+ for (const rel of rels) {
193
+ const data = await fsx.readFile(joinFs(baseAbs, rel));
194
+ // 过滤 git 占位文件(名 + 内容同时匹配才过滤,避免吞用户真实同名文件)
195
+ if (isKeepFile(rel, data))
196
+ continue;
197
+ files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
198
+ }
199
+ sections[sid] = { version: 1, files };
200
+ }
201
+ return { id: manifest.id, createdAt: manifest.createdAt, manifest: manifest.manifest, sections };
202
+ }
203
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1,9 @@
1
+ import type { SyncSnapshot } from './transport.ts';
2
+ /** sections 整体 JSON 化(加密密文载荷原样透传)。 */
3
+ export declare function sectionsToJsonSafe(sections: SyncSnapshot['sections']): SyncSnapshot['sections'];
4
+ /** sections 从 JSON 还原(加密密文载荷原样透传)。 */
5
+ export declare function sectionsFromJsonSafe(sections: unknown): SyncSnapshot['sections'];
6
+ /** 整份快照序列化为 JSON 字符串(本地不落盘,仅用于传输/加密载荷)。 */
7
+ export declare function serializeSnapshot(snapshot: SyncSnapshot): string;
8
+ /** 从 JSON 字符串还原快照(形状非法抛错;调用方负责包装成通道错误)。 */
9
+ export declare function deserializeSnapshot(raw: string): SyncSnapshot;
@@ -0,0 +1,107 @@
1
+ import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.js';
2
+ import { isEncryptedSections } from './transport.js';
3
+ /** 二进制数据在 JSON 中的标记对象键(base64 载荷)。 */
4
+ const BIN_KEY = '$bin';
5
+ /** duck-typing:文件类分区(version===1 + files 数组) */
6
+ function isFilesSectionLike(v) {
7
+ if (v === null || typeof v !== 'object')
8
+ return false;
9
+ const o = v;
10
+ return o.version === 1 && Array.isArray(o.files);
11
+ }
12
+ /** 单个文件分区:Uint8Array → { $bin: base64 }(非二进制原样保留)。 */
13
+ function encodeFileData(data) {
14
+ if (data instanceof Uint8Array) {
15
+ return { [BIN_KEY]: Buffer.from(data).toString('base64') };
16
+ }
17
+ return data;
18
+ }
19
+ /** 单个文件分区:{ $bin: base64 } → Uint8Array(非标记对象原样保留)。 */
20
+ function decodeFileData(data) {
21
+ if (data !== null && typeof data === 'object') {
22
+ const bin = data[BIN_KEY];
23
+ if (typeof bin === 'string') {
24
+ // 普通 Uint8Array(非 Buffer 子类):与内存中文件分区形态一致,深比较等价
25
+ return new Uint8Array(Buffer.from(bin, 'base64'));
26
+ }
27
+ }
28
+ return data;
29
+ }
30
+ /** 单个分区 JSON 化:文件类分区逐文件转 base64;JSON 分区原样。 */
31
+ function sectionToJsonSafe(section) {
32
+ if (!isFilesSectionLike(section))
33
+ return section;
34
+ return {
35
+ version: 1,
36
+ files: section.files.map((f) => ({
37
+ relativePath: f.relativePath,
38
+ data: encodeFileData(f.data),
39
+ ...(f.contentHash !== undefined ? { contentHash: f.contentHash } : {}),
40
+ })),
41
+ };
42
+ }
43
+ /** 单个分区从 JSON 还原:文件类分区逐文件还原 Uint8Array。 */
44
+ function sectionFromJsonSafe(section) {
45
+ if (!isFilesSectionLike(section))
46
+ return section;
47
+ return {
48
+ version: 1,
49
+ files: section.files.map((f) => ({
50
+ relativePath: f.relativePath,
51
+ data: decodeFileData(f.data),
52
+ ...(f.contentHash !== undefined ? { contentHash: f.contentHash } : {}),
53
+ })),
54
+ };
55
+ }
56
+ /** sections 整体 JSON 化(加密密文载荷原样透传)。 */
57
+ export function sectionsToJsonSafe(sections) {
58
+ if (isEncryptedSections(sections))
59
+ return sections;
60
+ const out = {};
61
+ for (const [id, data] of Object.entries(sections)) {
62
+ out[id] = sectionToJsonSafe(data);
63
+ }
64
+ return out;
65
+ }
66
+ /** sections 从 JSON 还原(加密密文载荷原样透传)。 */
67
+ export function sectionsFromJsonSafe(sections) {
68
+ if (isEncryptedSections(sections))
69
+ return sections;
70
+ if (sections === null || typeof sections !== 'object' || Array.isArray(sections)) {
71
+ throw new Error('快照 sections 必须是对象');
72
+ }
73
+ const out = {};
74
+ for (const [id, data] of Object.entries(sections)) {
75
+ out[id] = sectionFromJsonSafe(data);
76
+ }
77
+ return out;
78
+ }
79
+ /** 整份快照序列化为 JSON 字符串(本地不落盘,仅用于传输/加密载荷)。 */
80
+ export function serializeSnapshot(snapshot) {
81
+ return stringifyJsonSafe({
82
+ id: snapshot.id,
83
+ createdAt: snapshot.createdAt,
84
+ manifest: snapshot.manifest,
85
+ sections: sectionsToJsonSafe(snapshot.sections),
86
+ });
87
+ }
88
+ /** 从 JSON 字符串还原快照(形状非法抛错;调用方负责包装成通道错误)。 */
89
+ export function deserializeSnapshot(raw) {
90
+ const parsed = parseJsonSafe(raw);
91
+ const snap = parsed;
92
+ const okShape = snap !== null && typeof snap === 'object' && !Array.isArray(snap)
93
+ && typeof snap.id === 'string'
94
+ && typeof snap.createdAt === 'string'
95
+ && typeof snap.manifest === 'object' && snap.manifest !== null
96
+ && typeof snap.sections === 'object' && snap.sections !== null;
97
+ if (!okShape) {
98
+ throw new Error('快照 JSON 形状非法(缺少 id/createdAt/manifest/sections)');
99
+ }
100
+ return {
101
+ id: snap.id,
102
+ createdAt: snap.createdAt,
103
+ manifest: snap.manifest,
104
+ sections: sectionsFromJsonSafe(snap.sections),
105
+ };
106
+ }
107
+ //# sourceMappingURL=snapshot-json.js.map