@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,564 @@
1
+ /**
2
+ * AutoSyncScheduler:宿主后台自动同步调度器(按同步通道独立调度)。
3
+ *
4
+ * 生命周期:
5
+ * - start():读两个通道(git/webdav)的 autosync 配置;每个 enabled 通道启动独立定时器
6
+ * (按各自 interval);无条件对每个 enabled 通道执行一次「启动触发下载合并」
7
+ * (受各自 startupMinIntervalMs 阈值约束)。
8
+ * - stop():清全部定时器、标记不再调度。
9
+ * - runOnce(channel):对指定通道执行一次完整双向自动同步(§6.1 流程)。
10
+ *
11
+ * 双通道语义:git 与 webdav 的自动同步配置(enabled/interval/运行状态)各自独立,
12
+ * 互不干扰;同一时刻至多执行一个通道的 runOnce(runs.register('autosync') 全局防重,
13
+ * 避免两个引擎并发写本地配置),另一通道的定时触发在本轮结束后自然补跑。
14
+ *
15
+ * 核心逻辑(runOnce):
16
+ * - 读该通道配置;若 !enabled → return
17
+ * - runs.register('autosync') 防重复;同 kind running → 跳过
18
+ * - readSyncConfig(该通道) → 按通道判定未配置(git 无 git.repoUrl / webdav 无 webdav.url)
19
+ * → 记 skipped(未配置) → return
20
+ * - Phase A: engine.pull() 差异预览 → 判定 needsReview(冲突/缺失依赖/路径问题)
21
+ * - 需人工决策 → 跳过 + 写历史 skipped + conflictedSections[] → return
22
+ * - Phase B: 无冲突 → engine.preview() + engine.applyItems() 覆盖写入本地
23
+ * - Phase C: 完整双向 → engine.push() 上传
24
+ * - 收尾:写该通道 autosync-config(lastRunAt, lastRunStatus, consecutiveFailures, lastRunHistoryId)
25
+ *
26
+ * 连续失败计数:只对网络/传输/apply 真实失败计数;skipped(未配置/冲突跳过/无远端)不计。
27
+ * 连续失败 ≥ 3 → host.log.warn 通知 + 记 notifiedAt。
28
+ */
29
+ import crypto from 'node:crypto';
30
+ import fs from 'node:fs/promises';
31
+ import path from 'node:path';
32
+ import { withMutationLock, LOCK_BLOCK_MESSAGE } from '../utils/env-lock.js';
33
+ import { readAutosyncConfig, writeAutosyncConfig } from './autosync-config.js';
34
+ import { readSyncConfigFor, isGitConfig, isWebDavConfig } from './sync-config.js';
35
+ import { readSyncHistory, appendAutosyncEntry } from './sync-history.js';
36
+ /** 间隔 → ms 换算(§4.3) */
37
+ export function intervalToMs(interval) {
38
+ const table = {
39
+ '5m': 5 * 60 * 1000,
40
+ '15m': 15 * 60 * 1000,
41
+ '30m': 30 * 60 * 1000,
42
+ '60m': 60 * 60 * 1000,
43
+ '6h': 6 * 60 * 60 * 1000,
44
+ '12h': 12 * 60 * 60 * 1000,
45
+ '24h': 24 * 60 * 60 * 1000,
46
+ };
47
+ return table[interval];
48
+ }
49
+ /**
50
+ * 启动触发下载合并且满足阈值(now - lastRunAt >= startupMinIntervalMs)?
51
+ * lastRunAt 为 undefined(从未运行)→ true。
52
+ */
53
+ export function shouldTriggerStartupRun(lastRunAt, startupMinIntervalMs, nowMs = Date.now()) {
54
+ if (lastRunAt === undefined || lastRunAt === '')
55
+ return true;
56
+ const lastMs = Date.parse(lastRunAt);
57
+ if (Number.isNaN(lastMs))
58
+ return true;
59
+ return nowMs - lastMs >= startupMinIntervalMs;
60
+ }
61
+ export class AutoSyncScheduler {
62
+ syncDir;
63
+ host;
64
+ makeSyncEngine;
65
+ msg;
66
+ runs;
67
+ now;
68
+ readConfig;
69
+ writeConfig;
70
+ readSyncConfigFn;
71
+ readHistoryFn;
72
+ appendHistoryFn;
73
+ setTimer;
74
+ clearTimer;
75
+ detectRemoteNew;
76
+ detectLocalChange;
77
+ mutationLock;
78
+ isBlocked;
79
+ phase3Recovery;
80
+ /** 本次 runOnce 由 withMutationLock 取得的下游锁上下文(供 apply/push journal-wrap),非线程级。 */
81
+ lockCtxForJournal = null;
82
+ /** 每通道一个定时器(git/webdav 各自 enabled 时独立排期)。 */
83
+ timers = new Map();
84
+ stopped = false;
85
+ running = false;
86
+ constructor(opts) {
87
+ this.syncDir = opts.syncDir;
88
+ this.host = opts.host;
89
+ this.makeSyncEngine = opts.makeSyncEngine;
90
+ this.msg = opts.msg;
91
+ this.runs = opts.runs;
92
+ this.now = opts.now ?? (() => new Date());
93
+ this.readConfig = opts.readConfig ?? ((channel) => readAutosyncConfig(this.syncDir, channel));
94
+ this.writeConfig = opts.writeConfig ?? ((channel, cfg) => writeAutosyncConfig(this.syncDir, channel, cfg));
95
+ this.readSyncConfigFn = opts.readSyncConfigFn ?? ((channel) => readSyncConfigFor(this.syncDir, channel));
96
+ this.readHistoryFn = opts.readHistoryFn ?? (() => readSyncHistory(this.syncDir));
97
+ this.appendHistoryFn = opts.appendHistoryFn ?? ((entry) => appendAutosyncEntry(this.syncDir, entry));
98
+ this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
99
+ this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
100
+ this.mutationLock = opts.mutationLock;
101
+ this.isBlocked = opts.isBlocked;
102
+ this.phase3Recovery = opts.phase3Recovery;
103
+ this.detectRemoteNew = opts.detectRemoteNew ?? defaultDetectRemoteNew;
104
+ this.detectLocalChange = opts.detectLocalChange ?? defaultDetectLocalChange;
105
+ }
106
+ /** 追加一条自动同步历史(自动带上触发通道;0x 安全:绝不覆盖调用方显式 transport)。 */
107
+ async appendHistory(channel, entry) {
108
+ await this.appendHistoryFn({ ...entry, transport: channel });
109
+ }
110
+ /** 启动:读两个通道配置 → enabled 通道各自排定时器 → 对每个 enabled 通道执行一次启动触发。 */
111
+ start() {
112
+ if (this.stopped)
113
+ return;
114
+ this.refreshTimers();
115
+ void this.startupRuns();
116
+ }
117
+ /** 停止:清全部定时器、标记不再调度;正在执行的任务允许自然结束。 */
118
+ stop() {
119
+ this.stopped = true;
120
+ for (const timer of this.timers.values())
121
+ this.clearTimer(timer);
122
+ this.timers.clear();
123
+ }
124
+ /** 重新加载配置(路由 POST /sync/autosync 后调用;重排所有通道定时器)。 */
125
+ async reload() {
126
+ if (this.stopped)
127
+ return;
128
+ this.refreshTimers();
129
+ }
130
+ refreshTimers() {
131
+ for (const timer of this.timers.values())
132
+ this.clearTimer(timer);
133
+ this.timers.clear();
134
+ const channels = ['git', 'webdav'];
135
+ for (const channel of channels) {
136
+ void this.readConfig(channel).then((cfg) => {
137
+ if (this.stopped || !cfg.enabled)
138
+ return;
139
+ const ms = intervalToMs(cfg.interval);
140
+ const timer = this.setTimer(() => {
141
+ // 一次性定时器:触发后立即从表移除,避免 reload() 清理到已失效的句柄
142
+ this.timers.delete(channel);
143
+ if (this.stopped)
144
+ return;
145
+ void this.runOnce(channel)
146
+ .catch((err) => {
147
+ this.host.log.error(`自动同步定时触发失败(${channel})`, { error: err instanceof Error ? err.message : String(err) });
148
+ })
149
+ .then(() => {
150
+ // 本轮结束(成功 / 跳过 / 失败)后重新排定下一次;refreshTimers
151
+ // 内部重读配置,若期间被关闭(enabled=false)则不再排期。
152
+ if (!this.stopped)
153
+ this.refreshTimers();
154
+ });
155
+ }, ms);
156
+ this.timers.set(channel, timer);
157
+ }).catch(() => { });
158
+ }
159
+ }
160
+ /** 启动触发下载合并(每个 enabled 通道;受各自 startupMinIntervalMs 阈值约束)。 */
161
+ async startupRuns() {
162
+ const channels = ['git', 'webdav'];
163
+ for (const channel of channels) {
164
+ try {
165
+ const cfg = await this.readConfig(channel);
166
+ if (!cfg.enabled)
167
+ continue; // 该通道总开关关闭 → 启动触发不执行
168
+ if (!shouldTriggerStartupRun(cfg.lastRunAt, cfg.startupMinIntervalMs, this.now().getTime())) {
169
+ this.host.log.info(`自动同步启动触发跳过(${channel}):距上次运行未达阈值`);
170
+ continue;
171
+ }
172
+ await this.runOnce(channel, { startup: true });
173
+ }
174
+ catch (err) {
175
+ this.host.log.error(`启动触发下载合并失败(${channel})`, { error: err instanceof Error ? err.message : String(err) });
176
+ }
177
+ }
178
+ }
179
+ /**
180
+ * 执行一次指定通道的自动同步(§6.1)。
181
+ * @param channel - 同步通道(git / webdav;各自独立的配置与运行状态)
182
+ * @param opts.startup - true 表示启动触发变体(只做 Phase A+B,不做 Phase C push)
183
+ */
184
+ async runOnce(channel, opts = {}) {
185
+ if (this.running)
186
+ return { status: 'skipped', direction: 'none', skipReason: 'running', historyId: '', consecutiveFailures: 0 };
187
+ const cfg = await this.readConfig(channel);
188
+ if (!cfg.enabled) {
189
+ return { status: 'skipped', direction: 'none', skipReason: 'disabled', historyId: '', consecutiveFailures: cfg.consecutiveFailures };
190
+ }
191
+ this.running = true;
192
+ const nowIso = this.now().toISOString();
193
+ const historyId = `autosync-${crypto.randomUUID()}`;
194
+ // runs 防重复:同 kind running → 跳过(内部语义,不打搅用户)
195
+ let runId = null;
196
+ try {
197
+ const run = this.runs.register('autosync');
198
+ runId = run.runId;
199
+ }
200
+ catch {
201
+ this.running = false;
202
+ return { status: 'skipped', direction: 'none', skipReason: 'conflict', historyId, consecutiveFailures: cfg.consecutiveFailures };
203
+ }
204
+ // Phase 2 锁:autosync 的 apply(写本地配置)与 push(写远端+本地散文件)属 GLOBAL mutation。
205
+ // 获取失败(另一项 DSH 任务进行中)→ skipped(mutation-locked);成功则在整个 runOnce 持锁并在 finally 释放。
206
+ // 无锁环境(测试未注入 mutationLock)→ 不锁定,直接执行(与旧行为一致)。
207
+ let releaseLock = null;
208
+ if (this.mutationLock !== undefined) {
209
+ try {
210
+ const lk = await withMutationLock(this.mutationLock, { op: 'autosync', target: channel, isBlocked: this.isBlocked });
211
+ if (lk.context === null) {
212
+ this.running = false;
213
+ // issue #27:被挡时按分类告知用户。stale 残留锁「重试不会自愈」,必须显式回收——
214
+ // 这里把与 423 响应同文案的指引写进日志,否则自动同步只会静默跳过,用户无从下手。
215
+ // issue #31:与 backup-scheduler 对齐——非 stale 分类也留一行(不再只对 stale 说话);
216
+ // 更重要的是**补写历史**:此前该分支 return 前不写 sync-history,用户看历史面板
217
+ // 只能发现「自动同步不再更新」,却看不到任何一条「为什么跳过」(实测 9 天 0 条记录)。
218
+ if (lk.reason === 'stale') {
219
+ this.host.log.warn(`自动同步已跳过(${channel}):${LOCK_BLOCK_MESSAGE.stale}`);
220
+ }
221
+ else if (lk.reason !== undefined) {
222
+ this.host.log.info(`自动同步已跳过(${channel}):${LOCK_BLOCK_MESSAGE[lk.reason]}`);
223
+ }
224
+ await this.appendHistory(channel, {
225
+ direction: 'both',
226
+ status: 'skipped',
227
+ skipReason: 'mutation-locked',
228
+ createdAt: nowIso,
229
+ failureCountAtRun: cfg.consecutiveFailures,
230
+ });
231
+ return {
232
+ status: 'skipped', direction: 'none', skipReason: 'mutation-locked', historyId,
233
+ consecutiveFailures: cfg.consecutiveFailures,
234
+ };
235
+ }
236
+ releaseLock = lk.release;
237
+ this.lockCtxForJournal = lk.context;
238
+ }
239
+ catch {
240
+ // issue #31 D:acquire 本身抛错(锁目录 IO/权限)也是「被挡」,同样必须留历史 ——
241
+ // 否则用户在历史面板只会看到「自动同步不再更新」,而没有任何一条解释。
242
+ this.running = false;
243
+ await this.appendHistory(channel, {
244
+ direction: 'both',
245
+ status: 'skipped',
246
+ skipReason: 'mutation-locked',
247
+ createdAt: nowIso,
248
+ failureCountAtRun: cfg.consecutiveFailures,
249
+ });
250
+ return { status: 'skipped', direction: 'none', skipReason: 'mutation-locked', historyId, consecutiveFailures: cfg.consecutiveFailures };
251
+ }
252
+ }
253
+ try {
254
+ // 读该通道 sync-config:按通道判定「已配置」——git 看 git.repoUrl、webdav 看 webdav.url。
255
+ const syncCfg = await this.readSyncConfigFn(channel);
256
+ if (!syncIsConfigured(syncCfg)) {
257
+ const result = {
258
+ status: 'skipped', direction: 'none', skipReason: 'unconfigured', historyId,
259
+ consecutiveFailures: cfg.consecutiveFailures,
260
+ };
261
+ await this.appendHistory(channel, {
262
+ direction: 'both',
263
+ status: 'skipped',
264
+ skipReason: 'unconfigured',
265
+ createdAt: nowIso,
266
+ failureCountAtRun: cfg.consecutiveFailures,
267
+ });
268
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
269
+ return result;
270
+ }
271
+ const engine = this.makeSyncEngine(syncCfg);
272
+ // 事件驱动触发(§3.1/§3.2/§3.3 看变化不看时间):
273
+ // - remoteNew:远端是否出现比本地祖先更新的快照 → 决定是否做下载合并(Phase A);
274
+ // - localDirty:本地 portable 配置相对基线是否真的变了 → 决定是否上传(Phase C)。
275
+ // 定时器只是兜底轮询;真正驱动是这两个「变化」信号。
276
+ const remoteNew = await this.detectRemoteNew(engine);
277
+ const localDirty = await this.detectLocalChange(engine);
278
+ // 两端都无变化 → 什么都不做,记为 upToDate(不重复拉取/不空转)。
279
+ if (!remoteNew && !localDirty) {
280
+ const result = {
281
+ status: 'success', direction: 'none', skipReason: 'upToDate', historyId,
282
+ consecutiveFailures: cfg.consecutiveFailures,
283
+ };
284
+ await this.appendHistory(channel, {
285
+ direction: 'both', status: 'success', skipReason: 'upToDate',
286
+ createdAt: nowIso, failureCountAtRun: cfg.consecutiveFailures,
287
+ });
288
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
289
+ return result;
290
+ }
291
+ let appliedSections = [];
292
+ // Phase A: pull 覆盖(下载)—— 仅当远端有新快照才拉取(§3.2)。
293
+ // 产品语义:勾选即同步,不做 diff/合并;远端最新快照按选择覆盖写入本地。
294
+ if (remoteNew) {
295
+ let pullReport;
296
+ try {
297
+ pullReport = await engine.pull();
298
+ }
299
+ catch (err) {
300
+ const error = err instanceof Error ? err.message : String(err);
301
+ const result = {
302
+ status: 'failed', direction: 'pull', error, historyId,
303
+ consecutiveFailures: cfg.consecutiveFailures + 1,
304
+ };
305
+ await this.appendHistory(channel, {
306
+ direction: 'pull', status: 'failed', error, createdAt: nowIso,
307
+ failureCountAtRun: cfg.consecutiveFailures + 1,
308
+ });
309
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
310
+ this.maybeNotify(cfg.consecutiveFailures + 1, historyId, nowIso, channel);
311
+ return result;
312
+ }
313
+ // 有需要人工决策的项(冲突/缺失依赖/路径问题)→ 自动同步不擅自覆盖,交手动同步处理。
314
+ if (pullReport.needsReview) {
315
+ const conflictedSections = [...new Set(pullReport.changes
316
+ .filter((c) => c.kind === 'Conflict' || c.kind === 'MissingSecret'
317
+ || c.kind === 'MissingDependency' || c.kind === 'Error')
318
+ .map((c) => c.adapter))];
319
+ const result = {
320
+ status: 'skipped', direction: 'pull', skipReason: 'conflict',
321
+ conflictedSections, historyId,
322
+ consecutiveFailures: cfg.consecutiveFailures,
323
+ };
324
+ await this.appendHistory(channel, {
325
+ direction: 'pull', status: 'skipped', skipReason: 'conflict',
326
+ conflictedSections, createdAt: nowIso,
327
+ failureCountAtRun: cfg.consecutiveFailures,
328
+ });
329
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
330
+ return result;
331
+ }
332
+ const pullSections = [...new Set(pullReport.changes.map((c) => c.adapter))];
333
+ if (pullSections.length === 0) {
334
+ // 远端快照无物可应用(无变化)→ 无远端产出;若本地有改动则仅走上传。
335
+ // 此处不立即返回,让 Phase C 依据 localDirty 决定是否上传本地改动。
336
+ }
337
+ else {
338
+ // Phase B: 写入本地(preview 会话 → 全部采纳 → applyItems)。P0-A:包 intent journal。
339
+ const rawApply = async () => {
340
+ const preview = await engine.preview();
341
+ if (preview.plan === null || preview.zipPath === '') {
342
+ throw new Error(preview.message ?? '同步预览失败:远端无快照');
343
+ }
344
+ const subPlan = {
345
+ ...preview.plan,
346
+ items: preview.plan.items.filter((i) => i.kind !== 'Skip'),
347
+ };
348
+ try {
349
+ // snapshotId:基线必须对齐「刚应用的远端快照」,否则 hasNewRemoteSnapshot()
350
+ // 会把这个快照一直当成新的 → 每轮空转重复拉取。
351
+ return await engine.applyItems(preview.zipPath, subPlan, { snapshotId: preview.snapshotId });
352
+ }
353
+ finally {
354
+ // preview() 的临时 ZIP 由调用方负责清理(SyncEngine.preview 契约)。
355
+ try {
356
+ await fs.rm(path.dirname(preview.zipPath), { recursive: true, force: true });
357
+ }
358
+ catch { /* 尽力清理 */ }
359
+ }
360
+ };
361
+ const applyReport = (this.phase3Recovery !== undefined && this.lockCtxForJournal !== null)
362
+ ? (await this.phase3Recovery.runExternalIntent({
363
+ operationType: 'autosync-apply', lockCtx: this.lockCtxForJournal,
364
+ intent: { adapter: 'sync', ref: channel, kind: 'Apply' }, fn: rawApply,
365
+ })).result
366
+ : await rawApply();
367
+ appliedSections = applyReport.applied;
368
+ if (!applyReport.ok) {
369
+ const error = applyReport.warnings.join('; ') || 'applyItems 执行失败';
370
+ const result = {
371
+ status: 'failed', direction: 'pull', error, historyId,
372
+ consecutiveFailures: cfg.consecutiveFailures + 1,
373
+ };
374
+ await this.appendHistory(channel, {
375
+ direction: 'pull', status: 'failed', error, createdAt: nowIso,
376
+ failureCountAtRun: cfg.consecutiveFailures + 1,
377
+ });
378
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
379
+ this.maybeNotify(cfg.consecutiveFailures + 1, historyId, nowIso, channel);
380
+ return result;
381
+ }
382
+ }
383
+ }
384
+ // Phase C: push 上传(完整双向)—— 仅当本地真有改动才上传(§3.1);startup 变体不上传。
385
+ if (!opts.startup && localDirty) {
386
+ try {
387
+ // Phase C: 完整双向 → engine.push() 上传。P0-A:外部 push 包 intent journal。
388
+ const rawPush = async () => engine.push();
389
+ const pushReport = (this.phase3Recovery !== undefined && this.lockCtxForJournal !== null)
390
+ ? (await this.phase3Recovery.runExternalIntent({
391
+ operationType: 'autosync-push', lockCtx: this.lockCtxForJournal,
392
+ intent: { adapter: 'sync', ref: channel, kind: 'Push' }, fn: rawPush,
393
+ })).result
394
+ : await rawPush();
395
+ if (!pushReport.ok) {
396
+ const error = pushReport.message ?? 'push 失败';
397
+ const result = {
398
+ status: 'failed', direction: appliedSections.length ? 'both' : 'push',
399
+ appliedSections, error, historyId,
400
+ consecutiveFailures: cfg.consecutiveFailures + 1,
401
+ };
402
+ await this.appendHistory(channel, {
403
+ direction: appliedSections.length ? 'both' : 'push', status: 'failed',
404
+ appliedSections, error,
405
+ createdAt: nowIso, failureCountAtRun: cfg.consecutiveFailures + 1,
406
+ });
407
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
408
+ this.maybeNotify(cfg.consecutiveFailures + 1, historyId, nowIso, channel);
409
+ return result;
410
+ }
411
+ const result = {
412
+ status: 'success', direction: appliedSections.length ? 'both' : 'push',
413
+ appliedSections, pushedSnapshotId: pushReport.snapshotId, historyId,
414
+ consecutiveFailures: 0,
415
+ };
416
+ await this.appendHistory(channel, {
417
+ direction: appliedSections.length ? 'both' : 'push', status: 'success',
418
+ appliedSections, pushedSnapshotId: pushReport.snapshotId,
419
+ createdAt: nowIso, failureCountAtRun: 0,
420
+ });
421
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
422
+ return result;
423
+ }
424
+ catch (err) {
425
+ const error = err instanceof Error ? err.message : String(err);
426
+ const result = {
427
+ status: 'failed', direction: appliedSections.length ? 'both' : 'push',
428
+ appliedSections, error, historyId,
429
+ consecutiveFailures: cfg.consecutiveFailures + 1,
430
+ };
431
+ await this.appendHistory(channel, {
432
+ direction: appliedSections.length ? 'both' : 'push', status: 'failed',
433
+ appliedSections, error,
434
+ createdAt: nowIso, failureCountAtRun: cfg.consecutiveFailures + 1,
435
+ });
436
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
437
+ this.maybeNotify(cfg.consecutiveFailures + 1, historyId, nowIso, channel);
438
+ return result;
439
+ }
440
+ }
441
+ // startup 变体 / 仅远端合并:只做 pull 合并(不上传),或远端无新生且本地无改动(已在上方 upToDate 短路上)。
442
+ const result = {
443
+ status: 'success', direction: 'pull', appliedSections,
444
+ ...(appliedSections.length === 0 ? { skipReason: 'unchanged' } : {}),
445
+ historyId, consecutiveFailures: 0,
446
+ };
447
+ await this.appendHistory(channel, {
448
+ direction: 'pull', status: 'success', appliedSections,
449
+ ...(appliedSections.length === 0 ? { skipReason: 'unchanged' } : {}),
450
+ createdAt: nowIso, failureCountAtRun: 0,
451
+ });
452
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
453
+ return result;
454
+ }
455
+ catch (err) {
456
+ const error = err instanceof Error ? err.message : String(err);
457
+ const result = {
458
+ status: 'failed', direction: 'none', error, historyId,
459
+ consecutiveFailures: cfg.consecutiveFailures + 1,
460
+ };
461
+ await this.appendHistory(channel, {
462
+ direction: 'both', status: 'failed', error,
463
+ createdAt: nowIso, failureCountAtRun: cfg.consecutiveFailures + 1,
464
+ });
465
+ await this.writeFinalConfig(cfg, result, nowIso, historyId, channel);
466
+ this.maybeNotify(cfg.consecutiveFailures + 1, historyId, nowIso, channel);
467
+ return result;
468
+ }
469
+ finally {
470
+ this.running = false;
471
+ // 收尾 RunRegistry:不 finish 会让 autosync 的 running 记录滞留
472
+ // (保留期 30 分钟),期间任何再次 runOnce 都会 register('autosync')
473
+ // → RunConflictError → 永远 skip(conflict),后台同步就此停摆。
474
+ if (runId !== null) {
475
+ try {
476
+ this.runs.finish(runId, { kind: 'autosync' });
477
+ }
478
+ catch {
479
+ /* 尽力而为:收尾失败不影响同步结果 */
480
+ }
481
+ }
482
+ // Phase 2 锁:始终释放(若本次成功获取)
483
+ if (releaseLock !== null) {
484
+ await releaseLock().catch(() => { });
485
+ }
486
+ }
487
+ }
488
+ /** 收尾:写该通道 autosync-config(lastRunAt, lastRunStatus, consecutiveFailures, lastRunHistoryId)。 */
489
+ async writeFinalConfig(base, result, nowIso, historyId, channel) {
490
+ await this.writeConfig(channel, {
491
+ ...base,
492
+ lastRunAt: nowIso,
493
+ lastRunStatus: result.status,
494
+ consecutiveFailures: result.consecutiveFailures,
495
+ lastRunHistoryId: historyId,
496
+ ...(result.error !== undefined ? { lastRunMessage: result.error } : {}),
497
+ });
498
+ }
499
+ /** 连续失败 ≥ 3 → 通知(host.log.warn)。 */
500
+ maybeNotify(failures, historyId, nowIso, channel) {
501
+ if (failures >= 3) {
502
+ this.host.log.warn(`自动同步连续失败 ${failures} 次,请检查仓库配置/凭据(${channel})`);
503
+ // 记录 notifiedAt(更新历史 entry)
504
+ void this.readHistoryFn().then(async (hist) => {
505
+ const entry = hist.autosyncEntries.find((e) => e.createdAt === nowIso);
506
+ if (entry) {
507
+ entry.notifiedAt = nowIso;
508
+ await this.writeConfig(channel, {
509
+ ...(await this.readConfig(channel)),
510
+ lastRunMessage: `连续失败 ${failures} 次,已通知`,
511
+ });
512
+ }
513
+ }).catch(() => { });
514
+ }
515
+ }
516
+ }
517
+ /**
518
+ * 同步通道是否「已配置」:git 看 git.repoUrl 非空;webdav 看 webdav.url 非空;
519
+ * 未配置(null / 缺字段)→ false。作为 autosync 未配置跳过的判定依据。
520
+ */
521
+ export function syncIsConfigured(cfg) {
522
+ if (cfg === null || typeof cfg !== 'object')
523
+ return false;
524
+ if (isWebDavConfig(cfg)) {
525
+ return typeof cfg.webdav.url === 'string' && cfg.webdav.url !== '';
526
+ }
527
+ if (isGitConfig(cfg)) {
528
+ return typeof cfg.git.repoUrl === 'string' && cfg.git.repoUrl !== '';
529
+ }
530
+ return false;
531
+ }
532
+ /**
533
+ * 缺省远端新快照检测(§3.2):engine 实现了 hasNewRemoteSnapshot() → 调用;
534
+ * 否则(测试 mock 未实现)保守返回 true(假设有新生,保持旧行为=每次都尝试拉取)。
535
+ */
536
+ async function defaultDetectRemoteNew(engine) {
537
+ const fn = engine.hasNewRemoteSnapshot;
538
+ if (typeof fn === 'function') {
539
+ try {
540
+ return await fn.call(engine);
541
+ }
542
+ catch {
543
+ return true; // 检测失败保守视为有新生,避免漏拉
544
+ }
545
+ }
546
+ return true;
547
+ }
548
+ /**
549
+ * 缺省本地变化检测(§3.1):engine 实现了 hasLocalChanges() → 调用;
550
+ * 否则(测试 mock 未实现)保守返回 true(假设有本地改动,保持旧行为=每次都尝试上传)。
551
+ */
552
+ async function defaultDetectLocalChange(engine) {
553
+ const fn = engine.hasLocalChanges;
554
+ if (typeof fn === 'function') {
555
+ try {
556
+ return await fn.call(engine);
557
+ }
558
+ catch {
559
+ return true; // 检测失败保守视为有改动,避免漏传
560
+ }
561
+ }
562
+ return true;
563
+ }
564
+ //# sourceMappingURL=autosync-scheduler.js.map
@@ -0,0 +1,18 @@
1
+ /** 同步层所需的文件系统操作最小集(路径均为宿主文件系统绝对路径) */
2
+ export interface SnapshotFs {
3
+ readFile(p: string): Promise<Uint8Array>;
4
+ writeFile(p: string, data: Uint8Array): Promise<void>;
5
+ /** 递归创建目录(已存在则成功) */
6
+ mkdir(p: string): Promise<void>;
7
+ /** 直接子项名字(含目录名);目录不存在 → [] */
8
+ readdir(p: string): Promise<string[]>;
9
+ isDir(p: string): Promise<boolean>;
10
+ exists(p: string): Promise<boolean>;
11
+ /** 递归删除(不存在则成功) */
12
+ remove(p: string): Promise<void>;
13
+ }
14
+ /** 目录 + 正斜杠相对路径 → 宿主路径(正斜杠连接,Windows 下 Node fs 同样接受;
15
+ * 两侧多余斜杠一律清除,避免产生 '//' 段与空路径段) */
16
+ export declare function joinFs(dir: string, rel: string): string;
17
+ /** 默认 node:fs/promises 适配器 */
18
+ export declare function createSnapshotFs(): SnapshotFs;
package/lib/sync/fs.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 可注入的最小 fs 门面(同步层核心逻辑经此访问磁盘)。
3
+ * 默认实现包 node:fs/promises;测试/内存场景可注入自定义实现(零副作用、纯接口)。
4
+ */
5
+ import fs from 'node:fs/promises';
6
+ import { atomicWriteFile } from '../utils/atomic-write.js';
7
+ /** 目录 + 正斜杠相对路径 → 宿主路径(正斜杠连接,Windows 下 Node fs 同样接受;
8
+ * 两侧多余斜杠一律清除,避免产生 '//' 段与空路径段) */
9
+ export function joinFs(dir, rel) {
10
+ const base = dir.replace(/[\\/]+$/, '');
11
+ const cleanRel = rel.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '');
12
+ return cleanRel === '' ? base : `${base}/${cleanRel}`;
13
+ }
14
+ /** 默认 node:fs/promises 适配器 */
15
+ export function createSnapshotFs() {
16
+ return {
17
+ async readFile(p) { return fs.readFile(p); },
18
+ /** 原子写:同目录 tmp + fsync + rename(同步快照文件不半写;注入的 mem-fs 天然原子) */
19
+ async writeFile(p, data) { await atomicWriteFile(p, data); },
20
+ async mkdir(p) { await fs.mkdir(p, { recursive: true }); },
21
+ async readdir(p) {
22
+ try {
23
+ return (await fs.readdir(p)).sort();
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ },
29
+ async isDir(p) {
30
+ try {
31
+ return (await fs.stat(p)).isDirectory();
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ },
37
+ async exists(p) {
38
+ try {
39
+ await fs.access(p);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ },
46
+ async remove(p) { await fs.rm(p, { recursive: true, force: true }); },
47
+ };
48
+ }
49
+ //# sourceMappingURL=fs.js.map