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