@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,491 @@
1
+ /**
2
+ * m-git-channel:Git 私有仓库通道(SyncTransport 的 git 实现)。
3
+ *
4
+ * 设计:
5
+ * - 明文快照以「散文件目录」提交到 git 仓库:工作副本 <workDir>/snapshots/<id>/ 即 t2 layout 布局
6
+ * (manifest.json + 平铺 JSON 分区 + 文件类分区目录),每次 sync 一次 commit + push;
7
+ * - 加密快照(EncryptedSections 密文载荷,无法平铺为明文 JSON 分区)走「密文单文件」布局:
8
+ * 整个快照 JSON 提交到 <workDir>/snapshots-encrypted/<id>.json(远端已存密文;
9
+ * 本地工作副本即远端镜像,不产生额外明文审计副本)。
10
+ * - 命令执行走 node:child_process execFile(promise 封装,数组参数无 shell 注入),
11
+ * 始终使用系统 PATH 中的 git(固定命令 'git'),可注入 exec 便于测试。
12
+ * - 认证:token 仅从注入的 credentials provider 读取(每次网络操作时 getToken()),
13
+ * 经 git credential helper(store --file=<临时文件>)传给 git —— token 不进入 argv、
14
+ * 不进入 repoUrl、不写入任何同步内容/commit message/日志;临时凭据文件用后即删。
15
+ * - 私有仓库判定:checkIsPrivate() 先匿名 ls-remote(成功=公开),失败再用凭据探测
16
+ * (成功=私有),isPrivateHint 缓存最近结果供 UI 提示(私有仓库为推荐使用场景)。
17
+ * - 契约:同 id 重复 upload = 覆盖(幂等友好:内容无变化时不产生 commit);download 不存在抛错;
18
+ * delete 不存在视为成功。
19
+ */
20
+ import { execFile } from 'node:child_process';
21
+ import fs from 'node:fs/promises';
22
+ import os from 'node:os';
23
+ import path from 'node:path';
24
+ import { promisify } from 'node:util';
25
+
26
+ import { createSnapshotFs, joinFs } from '../fs.ts';
27
+ import type { SnapshotFs } from '../fs.ts';
28
+ import { readSnapshotFromDir, SNAPSHOT_KEEP_CONTENT, SNAPSHOT_KEEP_FILE, SNAPSHOT_MANIFEST_FILE, writeSnapshotToDir } from '../layout.ts';
29
+ import type { SnapshotDirManifest } from '../layout.ts';
30
+ import { deserializeSnapshot, serializeSnapshot } from '../snapshot-json.ts';
31
+ import { computeSnapshotMeta, isEncryptedSections } from '../transport.ts';
32
+ import type { SyncSnapshot, SyncSnapshotMeta, SyncTransport } from '../transport.ts';
33
+ import { parseJsonSafe } from '../../utils/json.ts';
34
+ import { atomicWriteFile } from '../../utils/atomic-write.ts';
35
+ import { SECTION_FILE_PREFIXES } from '../../schema/config.ts';
36
+ import type { FilesSection, SectionData, SectionId } from '../../schema/types.ts';
37
+ import { zhMsg } from '../../core/messages.ts';
38
+ import type { MsgFunc } from '../../core/messages.ts';
39
+
40
+ const execFileAsync = promisify(execFile);
41
+
42
+ /** git 命令执行结果(code ≠ 0 = git 命令失败/退出非零) */
43
+ export interface GitExecResult {
44
+ stdout: string;
45
+ stderr: string;
46
+ code: number;
47
+ }
48
+
49
+ /** 可注入的 git 执行器(测试 mock 用);默认实现 = child_process.execFile promise 封装 */
50
+ export type GitExecFn = (
51
+ cmd: string,
52
+ args: string[],
53
+ opts: { cwd?: string; timeoutMs?: number },
54
+ ) => Promise<GitExecResult>;
55
+
56
+ /** 凭据提供者:token 只从这里读取,绝不在任何地方持久化 */
57
+ export interface GitCredentialProvider {
58
+ getToken(): Promise<string>;
59
+ }
60
+
61
+ export interface GitAuthor { name: string; email: string; }
62
+
63
+ export interface GitTransportOptions {
64
+ /** 远端私有仓库地址(https/ssh/本地路径);token 绝不拼入此 URL */
65
+ repoUrl: string;
66
+ /** 本地 git 工作副本目录(不存在则创建;已 clone 则复用) */
67
+ workDir: string;
68
+ /** token 提供者(http(s) 远端必填;本地/ssh 远端不会被调用) */
69
+ credentials: GitCredentialProvider;
70
+ /** 单条 git 命令超时 ms,默认 60000 */
71
+ timeoutMs?: number;
72
+ /** 注入 exec(测试 mock 用);缺省 = execFile 封装 */
73
+ exec?: GitExecFn;
74
+ /** 提交作者(写入远端历史),默认 DSH Config Sync <sync@dsh.local> */
75
+ author?: GitAuthor;
76
+ /** credential 用户名(GitHub PAT 用 oauth2),默认 'oauth2' */
77
+ credentialUsername?: string;
78
+ /** 消息翻译器(缺省 zh) */
79
+ msg?: MsgFunc;
80
+ }
81
+
82
+ export class GitTransportError extends Error {
83
+ constructor(message: string) {
84
+ super(message);
85
+ this.name = 'GitTransportError';
86
+ }
87
+ }
88
+
89
+ const SNAPSHOTS_REL = 'snapshots';
90
+ /** 加密快照的「密文单文件」目录:整个快照 JSON(含密文载荷)以 <id>.json 提交。
91
+ * 加密快照不写散文件目录(密文无法平铺为明文 JSON 分区;远端已存密文)。 */
92
+ const SNAPSHOTS_ENCRYPTED_REL = 'snapshots-encrypted';
93
+ const DEFAULT_TIMEOUT_MS = 60_000;
94
+ const DEFAULT_AUTHOR: GitAuthor = { name: 'DSH Config Sync', email: 'sync@dsh.local' };
95
+ const DEFAULT_CREDENTIAL_USERNAME = 'oauth2';
96
+ /** 快照 id 安全字符集:字母数字开头,仅 . _ -;防路径穿越与 commit message 注入 */
97
+ const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
98
+
99
+ const defaultExec: GitExecFn = async (cmd, args, opts) => {
100
+ try {
101
+ const { stdout, stderr } = await execFileAsync(cmd, args, {
102
+ cwd: opts.cwd,
103
+ timeout: opts.timeoutMs,
104
+ maxBuffer: 16 * 1024 * 1024,
105
+ windowsHide: true,
106
+ encoding: 'utf8',
107
+ });
108
+ return { stdout: String(stdout), stderr: String(stderr), code: 0 };
109
+ } catch (err) {
110
+ const e = err as { code?: number; stdout?: string; stderr?: string };
111
+ return {
112
+ stdout: String(e.stdout ?? ''),
113
+ stderr: String(e.stderr ?? (err instanceof Error ? err.message : String(err))),
114
+ code: typeof e.code === 'number' ? e.code : 1,
115
+ };
116
+ }
117
+ };
118
+
119
+ /** git config 值里的路径转义:含空白/引号时用引号包裹(Windows 路径转正斜杠) */
120
+ function quoteGitValue(value: string): string {
121
+ return /[\s"']/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
122
+ }
123
+
124
+ /** 实现 SyncTransport 的 git 通道。所有操作前 ensureRepo() 保证工作副本就绪,网络命令带凭据。 */
125
+ export class GitTransport implements SyncTransport {
126
+ readonly type = 'git';
127
+
128
+ private readonly o: GitTransportOptions & {
129
+ gitBin: string;
130
+ timeoutMs: number;
131
+ author: GitAuthor;
132
+ credentialUsername: string;
133
+ exec: GitExecFn;
134
+ };
135
+ private repoReady = false;
136
+ private privateHint: boolean | null = null;
137
+ private readonly msg: MsgFunc;
138
+
139
+ constructor(options: GitTransportOptions) {
140
+ if (typeof options.repoUrl !== 'string' || options.repoUrl.length === 0) {
141
+ throw new GitTransportError(zhMsg('sync.git.repoUrlRequired'));
142
+ }
143
+ if (typeof options.workDir !== 'string' || options.workDir.length === 0) {
144
+ throw new GitTransportError(zhMsg('sync.git.workDirRequired'));
145
+ }
146
+ if (options.credentials === null || typeof options.credentials !== 'object'
147
+ || typeof options.credentials.getToken !== 'function') {
148
+ throw new GitTransportError(zhMsg('sync.git.credentialsRequired'));
149
+ }
150
+ this.msg = options.msg ?? zhMsg;
151
+ this.o = {
152
+ repoUrl: options.repoUrl,
153
+ workDir: options.workDir,
154
+ credentials: options.credentials,
155
+ gitBin: 'git',
156
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
157
+ exec: options.exec ?? defaultExec,
158
+ author: options.author ?? DEFAULT_AUTHOR,
159
+ credentialUsername: options.credentialUsername ?? DEFAULT_CREDENTIAL_USERNAME,
160
+ };
161
+ }
162
+
163
+ /** 最近一次 checkIsPrivate() 的结果;null = 尚未检查 */
164
+ get isPrivateHint(): boolean | null {
165
+ return this.privateHint;
166
+ }
167
+
168
+ /** 列出远端已有快照(按 createdAt 升序)。读取工作副本 snapshots/(明文散文件)
169
+ * 与 snapshots-encrypted/(密文单文件)两个目录(先 pull 同步远端)。 */
170
+ async list(): Promise<SyncSnapshotMeta[]> {
171
+ await this.ensureRepo();
172
+ await this.pullFromRemote();
173
+ const fsx = createSnapshotFs();
174
+ const metas: SyncSnapshotMeta[] = [];
175
+ // 明文散文件目录
176
+ const snapsAbs = this.snapshotsDir();
177
+ const names = await fsx.readdir(snapsAbs);
178
+ for (const name of names) {
179
+ if (name === '' || name === '.' || name === '..') continue;
180
+ const dirAbs = joinFs(snapsAbs, name);
181
+ if (!(await fsx.isDir(dirAbs))) continue;
182
+ const m = await this.readDirManifest(dirAbs);
183
+ if (m === null) continue; // 损坏/不完整 → 跳过(防御工作副本污染,不静默失败整体 list)
184
+ metas.push({
185
+ id: m.id,
186
+ createdAt: m.createdAt,
187
+ sections: m.sectionHashes,
188
+ manifest: m.manifest,
189
+ });
190
+ }
191
+ // 密文单文件目录(snapshots-encrypted/<id>.json):整体 JSON 快照
192
+ const encAbs = this.encryptedSnapshotsDir();
193
+ const encNames = await fsx.readdir(encAbs);
194
+ for (const name of encNames) {
195
+ if (!name.endsWith('.json')) continue;
196
+ const id = name.slice(0, -'.json'.length);
197
+ if (id === '' || id === '.' || id === '..' || id.includes('/') || id.includes('\\')) continue;
198
+ const snap = await this.readEncryptedSnapshotFile(joinFs(encAbs, name));
199
+ if (snap === null) continue; // 损坏/解析失败 → 跳过
200
+ metas.push({ id: snap.id, createdAt: snap.createdAt, sections: {}, manifest: snap.manifest });
201
+ }
202
+ metas.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
203
+ return metas;
204
+ }
205
+
206
+ /**
207
+ * 上传快照:写散文件目录(明文)或密文单文件(加密)→ add → commit → push;
208
+ * 同 id 覆盖;内容无变化时幂等(不产生 commit)。
209
+ * 双形态互斥:同 id 从一种形态切到另一种时先清掉旧形态残留,保证工作副本/远端布局自洽。
210
+ */
211
+ async upload(snapshot: SyncSnapshot): Promise<SyncSnapshotMeta> {
212
+ this.assertSafeId(snapshot.id);
213
+ await this.ensureRepo();
214
+ await this.pullFromRemote();
215
+ const fsx = createSnapshotFs();
216
+ const encrypted = isEncryptedSections(snapshot.sections);
217
+ // 覆盖判定:任一种旧形态(散文件目录 / 密文单文件)已存在 → update
218
+ const existed = await fsx.exists(this.snapshotDir(snapshot.id))
219
+ || await fsx.exists(this.encryptedSnapshotFile(snapshot.id));
220
+ let rel: string;
221
+ if (encrypted) {
222
+ // 密文单文件:整个快照 JSON(sections 为 EncryptedSections,纯字符串 JSON 安全)
223
+ const file = this.encryptedSnapshotFile(snapshot.id);
224
+ await fsx.remove(file); // 覆盖语义:先删旧文件
225
+ await fsx.mkdir(this.encryptedSnapshotsDir());
226
+ await fsx.writeFile(file, new TextEncoder().encode(serializeSnapshot(snapshot)));
227
+ // 同 id 旧散文件目录残留 → 一并清理(形态切换不残留)
228
+ await fsx.remove(this.snapshotDir(snapshot.id));
229
+ rel = `${SNAPSHOTS_ENCRYPTED_REL}/${snapshot.id}.json`;
230
+ } else {
231
+ const dir = this.snapshotDir(snapshot.id);
232
+ if (await fsx.exists(dir)) await fsx.remove(dir); // 覆盖语义:先清旧目录,避免残留旧文件
233
+ await writeSnapshotToDir(snapshot, dir, fsx);
234
+ // git 不跟踪空目录:为空文件类分区目录写占位文件,保证远端保留目录。
235
+ // 占位文件不参与 manifest.sectionHashes(基于传入数据计算);读回时按名+内容过滤。
236
+ // 否则空分区(如未安装 skills)上传后目录在远端丢失,B 机全新 clone 下载即失败。
237
+ const plain = snapshot.sections as Partial<Record<SectionId, SectionData>>;
238
+ for (const [sid, prefix] of Object.entries(SECTION_FILE_PREFIXES)) {
239
+ const data = plain[sid as SectionId] as FilesSection | undefined;
240
+ if (data === undefined || data.files.length > 0) continue;
241
+ // joinFs 只接受两个参数,占位路径拼进 prefix 一起传
242
+ await fsx.writeFile(joinFs(dir, `${prefix}${SNAPSHOT_KEEP_FILE}`), new TextEncoder().encode(SNAPSHOT_KEEP_CONTENT));
243
+ }
244
+ // 同 id 旧密文单文件残留 → 一并清理(形态切换不残留)
245
+ await fsx.remove(this.encryptedSnapshotFile(snapshot.id));
246
+ rel = `${SNAPSHOTS_REL}/${snapshot.id}`;
247
+ }
248
+ await this.runGit(['add', '--', rel]);
249
+ const diff = await this.runGit(['diff', '--cached', '--quiet'], { allowNonZero: true });
250
+ if (diff.code !== 0) {
251
+ const verb = existed ? 'update' : 'add';
252
+ await this.runGit(['commit', '-m', `sync: ${verb} snapshot ${snapshot.id}`]);
253
+ await this.runGit(['push', '-u', 'origin', 'HEAD'], { withCredential: true });
254
+ }
255
+ return computeSnapshotMeta(snapshot);
256
+ }
257
+
258
+ /** 下载快照完整载荷。明文 → 读回散文件目录;加密 → 读回密文单文件。不存在的 id 必须抛错(契约)。 */
259
+ async download(id: string): Promise<SyncSnapshot> {
260
+ this.assertSafeId(id);
261
+ await this.ensureRepo();
262
+ await this.pullFromRemote();
263
+ const fsx = createSnapshotFs();
264
+ // 优先散文件目录(明文布局);其次密文单文件(加密布局)
265
+ // missingFileDir='empty':git 不跟踪空目录 → 目录缺失 = 空文件分区(非损坏;
266
+ // 提交原子性保证非空目录不会缺失),兼容旧版插件上传的无占位快照
267
+ if (await fsx.isDir(this.snapshotDir(id))) {
268
+ return readSnapshotFromDir(this.snapshotDir(id), fsx, { missingFileDir: 'empty' });
269
+ }
270
+ const encFile = this.encryptedSnapshotFile(id);
271
+ if (await fsx.exists(encFile)) {
272
+ const raw = Buffer.from(await fsx.readFile(encFile)).toString('utf8');
273
+ try {
274
+ return deserializeSnapshot(raw);
275
+ } catch (err) {
276
+ throw new GitTransportError(this.msg('sync.git.snapshotCorrupt', {
277
+ id, dir: encFile, err: this.mask(String((err as Error)?.message ?? ''), null),
278
+ }));
279
+ }
280
+ }
281
+ throw new GitTransportError(this.msg('sync.git.snapshotMissing', { id, dir: this.snapshotDir(id) }));
282
+ }
283
+
284
+ /** 删除远端快照(明文散文件目录或密文单文件;不存在视为成功,契约)。 */
285
+ async delete(id: string): Promise<void> {
286
+ this.assertSafeId(id);
287
+ await this.ensureRepo();
288
+ await this.pullFromRemote();
289
+ const fsx = createSnapshotFs();
290
+ const plainDir = this.snapshotDir(id);
291
+ const encFile = this.encryptedSnapshotFile(id);
292
+ const plainExists = await fsx.exists(plainDir);
293
+ const encExists = await fsx.exists(encFile);
294
+ if (!plainExists && !encExists) return; // 不存在视为成功
295
+ let rel: string;
296
+ if (plainExists) {
297
+ await fsx.remove(plainDir);
298
+ rel = `${SNAPSHOTS_REL}/${id}`;
299
+ } else {
300
+ await fsx.remove(encFile);
301
+ rel = `${SNAPSHOTS_ENCRYPTED_REL}/${id}.json`;
302
+ }
303
+ await this.runGit(['add', '-A', '--', rel]);
304
+ const diff = await this.runGit(['diff', '--cached', '--quiet'], { allowNonZero: true });
305
+ if (diff.code !== 0) {
306
+ await this.runGit(['commit', '-m', `sync: delete snapshot ${id}`]);
307
+ await this.runGit(['push', '-u', 'origin', 'HEAD'], { withCredential: true });
308
+ }
309
+ }
310
+
311
+ /**
312
+ * 私有仓库可见性探测(结果缓存于 isPrivateHint):
313
+ * 匿名 ls-remote 成功 → 公开;匿名失败 + 凭据探测成功 → 私有;两者都失败 → 抛错。
314
+ * UI 层可用 isPrivateHint 提示「私有仓库推荐」;公开仓库也可用(内容需自行评估)。
315
+ */
316
+ async checkIsPrivate(): Promise<boolean> {
317
+ if (this.privateHint !== null) return this.privateHint;
318
+ const anon = await this.runGit(['ls-remote', this.o.repoUrl], { allowNonZero: true, cwd: undefined });
319
+ if (anon.code === 0) {
320
+ this.privateHint = false;
321
+ return false;
322
+ }
323
+ const authed = await this.runGit(['ls-remote', this.o.repoUrl], { allowNonZero: true, cwd: undefined, withCredential: true });
324
+ if (authed.code === 0) {
325
+ this.privateHint = true;
326
+ return true;
327
+ }
328
+ throw new GitTransportError(this.msg('sync.git.repoUnreachable', { url: this.mask(this.o.repoUrl, null) }));
329
+ }
330
+
331
+ /* ---------------- 内部实现 ---------------- */
332
+
333
+ private async ensureRepo(): Promise<void> {
334
+ if (this.repoReady) return;
335
+ const fsx = createSnapshotFs();
336
+ await fsx.mkdir(this.o.workDir); // 不存在则创建
337
+ if (await fsx.exists(path.join(this.o.workDir, '.git'))) {
338
+ this.repoReady = true;
339
+ return;
340
+ }
341
+ const entries = await fsx.readdir(this.o.workDir);
342
+ if (entries.length > 0) {
343
+ throw new GitTransportError(this.msg('sync.git.workDirNotRepo', { dir: this.o.workDir }));
344
+ }
345
+ await this.runGit(['clone', this.o.repoUrl, '.'], { cwd: this.o.workDir, withCredential: true });
346
+ // 仓库级提交身份(写入远端历史,可经 author 选项覆盖;不改全局配置)
347
+ await this.runGit(['config', 'user.name', this.o.author.name]);
348
+ await this.runGit(['config', 'user.email', this.o.author.email]);
349
+ this.repoReady = true;
350
+ }
351
+
352
+ /** pull --ff-only 同步远端;无本地提交(全新仓库)或无 upstream 时静默跳过 */
353
+ private async pullFromRemote(): Promise<void> {
354
+ const head = await this.runGit(['rev-parse', '--verify', '--quiet', 'HEAD'], { allowNonZero: true });
355
+ if (head.code !== 0) return; // 无本地提交 → 无可 pull
356
+ const res = await this.runGit(['pull', '--ff-only'], { withCredential: true, allowNonZero: true });
357
+ if (res.code === 0) return;
358
+ if (/no tracking information/i.test(res.stderr)) return; // 无 upstream(初始状态)→ 跳过
359
+ throw new GitTransportError(this.msg('sync.git.pullFailed', { err: this.mask(res.stderr, await this.readTokenOnce()) }));
360
+ }
361
+
362
+ /** 执行 git 命令;withCredential=true 时注入 credential helper(token 不进 argv),失败时错误消息脱敏 */
363
+ private async runGit(
364
+ args: string[],
365
+ opts: { cwd?: string; withCredential?: boolean; allowNonZero?: boolean } = {},
366
+ ): Promise<GitExecResult> {
367
+ const cwd = opts.cwd === undefined ? this.o.workDir : opts.cwd;
368
+ let extra: string[] = [];
369
+ let token: string | null = null;
370
+ let cleanup: (() => Promise<void>) | null = null;
371
+ if (opts.withCredential) {
372
+ const cred = await this.buildCredentialArgs();
373
+ extra = cred.extraArgs;
374
+ token = cred.token;
375
+ cleanup = cred.cleanup;
376
+ }
377
+ try {
378
+ const result = await this.o.exec(this.o.gitBin, [...extra, ...args], { cwd, timeoutMs: this.o.timeoutMs });
379
+ if (result.code !== 0 && !opts.allowNonZero) {
380
+ throw new GitTransportError(
381
+ this.msg('sync.git.cmdFailed', { args: args.join(' '), code: String(result.code), err: this.mask(result.stderr, token) }),
382
+ );
383
+ }
384
+ return result;
385
+ } finally {
386
+ if (cleanup) await cleanup();
387
+ }
388
+ }
389
+
390
+ /**
391
+ * 为网络命令构造 credential helper 参数:
392
+ * 仅 http(s) 远端需要 token;本地路径 / ssh 走 git 原生认证(密钥),不调用 provider。
393
+ * token 写入 os.tmpdir() 下临时 store 文件(权限 0600),命令后立即删除 —— 永不落工作副本/远端。
394
+ */
395
+ private async buildCredentialArgs(): Promise<{ extraArgs: string[]; token: string; cleanup: () => Promise<void> }> {
396
+ if (!/^https?:\/\//i.test(this.o.repoUrl)) {
397
+ return { extraArgs: [], token: '', cleanup: async () => {} };
398
+ }
399
+ const token = await this.o.credentials.getToken();
400
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-git-cred-'));
401
+ const credFile = path.join(tmpDir, 'credential');
402
+ const host = this.repoHost();
403
+ const fileArg = quoteGitValue(credFile.replace(/\\/g, '/'));
404
+ await atomicWriteFile(credFile, `https://${this.o.credentialUsername}:${token}@${host}\n`, { mode: 0o600, symlink: 'reject' });
405
+ return {
406
+ extraArgs: ['-c', 'credential.helper=', '-c', `credential.helper=store --file=${fileArg}`],
407
+ token,
408
+ cleanup: async () => { await fs.rm(tmpDir, { recursive: true, force: true }); },
409
+ };
410
+ }
411
+
412
+ /** 从 repoUrl 提取 credential 匹配用的 host[:port] */
413
+ private repoHost(): string {
414
+ try {
415
+ const u = new URL(this.o.repoUrl);
416
+ return u.port ? `${u.hostname}:${u.port}` : u.hostname;
417
+ } catch {
418
+ throw new GitTransportError(this.msg('sync.git.repoUrlInvalid', { url: this.o.repoUrl }));
419
+ }
420
+ }
421
+
422
+ /** 读取一次 token(错误消息脱敏用);非 http(s) 场景返回空 */
423
+ private async readTokenOnce(): Promise<string | null> {
424
+ if (!/^https?:\/\//i.test(this.o.repoUrl)) return null;
425
+ try { return await this.o.credentials.getToken(); } catch { return null; }
426
+ }
427
+
428
+ /** 错误/日志脱敏:token(原文与 URL 编码两种形态)与 repoUrl 一律替换 */
429
+ private mask(text: string, token: string | null): string {
430
+ let out = text.split(this.o.repoUrl).join('[REPO_URL]');
431
+ if (token && token.length >= 4) {
432
+ out = out.split(token).join('[REDACTED]').split(encodeURIComponent(token)).join('[REDACTED]');
433
+ }
434
+ return out;
435
+ }
436
+
437
+ private assertSafeId(id: string): void {
438
+ if (typeof id !== 'string' || !SAFE_ID_RE.test(id)) {
439
+ throw new GitTransportError(this.msg('sync.git.invalidSnapshotId', { id: JSON.stringify(id) }));
440
+ }
441
+ }
442
+
443
+ private snapshotDir(id: string): string {
444
+ return joinFs(this.snapshotsDir(), id);
445
+ }
446
+
447
+ private snapshotsDir(): string {
448
+ return joinFs(this.o.workDir, SNAPSHOTS_REL);
449
+ }
450
+
451
+ /** 加密快照密文单文件目录(<workDir>/snapshots-encrypted/) */
452
+ private encryptedSnapshotsDir(): string {
453
+ return joinFs(this.o.workDir, SNAPSHOTS_ENCRYPTED_REL);
454
+ }
455
+
456
+ /** 加密快照密文单文件路径(<id>.json) */
457
+ private encryptedSnapshotFile(id: string): string {
458
+ return joinFs(this.encryptedSnapshotsDir(), `${id}.json`);
459
+ }
460
+
461
+ /** 读密文单文件快照(解析失败 → null,调用方跳过,不静默失败整体 list)。 */
462
+ private async readEncryptedSnapshotFile(file: string): Promise<SyncSnapshot | null> {
463
+ try {
464
+ const raw = Buffer.from(await createSnapshotFs().readFile(file)).toString('utf8');
465
+ return deserializeSnapshot(raw);
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+
471
+ /** 读快照目录 manifest.json(结构不合法 → null,调用方跳过) */
472
+ private async readDirManifest(dir: string): Promise<SnapshotDirManifest | null> {
473
+ const fsx: SnapshotFs = createSnapshotFs();
474
+ try {
475
+ const abs = joinFs(dir, SNAPSHOT_MANIFEST_FILE);
476
+ if (!(await fsx.exists(abs))) return null;
477
+ const raw = Buffer.from(await fsx.readFile(abs)).toString('utf8');
478
+ const parsed = parseJsonSafe(raw);
479
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
480
+ const m = parsed as Record<string, unknown>;
481
+ if (typeof m['id'] !== 'string' || typeof m['createdAt'] !== 'string'
482
+ || m['manifest'] === null || typeof m['manifest'] !== 'object'
483
+ || m['sectionHashes'] === null || typeof m['sectionHashes'] !== 'object') {
484
+ return null;
485
+ }
486
+ return parsed as SnapshotDirManifest;
487
+ } catch {
488
+ return null;
489
+ }
490
+ }
491
+ }