@wonseok-han/confluence-sync 1.7.0 → 2.0.0

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 (518) hide show
  1. package/README.md +44 -409
  2. package/dist/cli/args.js +1 -1
  3. package/dist/cli/help.js +214 -115
  4. package/dist/cli/init.js +44 -5
  5. package/dist/cli/main.js +42 -11
  6. package/dist/cli/status.js +83 -0
  7. package/dist/confluence/client.js +157 -86
  8. package/dist/confluence/config.js +12 -8
  9. package/dist/confluence/env-file.js +23 -0
  10. package/dist/confluence/links.js +18 -0
  11. package/dist/conversion/asset-paths.js +5 -0
  12. package/dist/conversion/attachments.js +29 -7
  13. package/dist/conversion/convert.js +10 -22
  14. package/dist/conversion/footnotes.js +114 -11
  15. package/dist/conversion/markdown.js +26 -0
  16. package/dist/conversion/pdf-pages.js +10 -8
  17. package/dist/documents/anchors.js +9 -0
  18. package/dist/documents/callout-roundtrip.js +75 -0
  19. package/dist/documents/callouts.js +74 -0
  20. package/dist/documents/catalog.js +2 -2
  21. package/dist/documents/footnote-roundtrip.js +96 -0
  22. package/dist/documents/format.js +116 -0
  23. package/dist/documents/html-tables.js +30 -0
  24. package/dist/documents/html-to-markdown.js +47 -13
  25. package/dist/documents/markdown.js +83 -12
  26. package/dist/documents/obsidian.js +6 -3
  27. package/dist/documents/tables.js +89 -0
  28. package/dist/runtime/network.js +4 -0
  29. package/dist/shared/colors.js +2 -2
  30. package/dist/shared/progress.js +164 -0
  31. package/dist/shared/version.js +12 -0
  32. package/dist/site/assets/Monaco-LICENSE.txt +21 -0
  33. package/dist/site/assets/Monaco-ThirdPartyNotices.txt +448 -0
  34. package/dist/site/assets/PDFJS-LICENSE.txt +177 -0
  35. package/dist/site/assets/app.css +1 -0
  36. package/dist/site/assets/app.js +4 -0
  37. package/dist/site/assets/chunks/chunk-TEW74QPN.js +35 -0
  38. package/dist/site/assets/chunks/markdown-S6623E66.js +1 -0
  39. package/dist/site/assets/chunks/monaco-36ZGUH4Y.js +897 -0
  40. package/dist/site/assets/chunks/monaco-HFLEBJTI.css +1 -0
  41. package/dist/site/assets/chunks/pdf-DDN7TXTP.js +55 -0
  42. package/dist/site/assets/confluence-env-example.txt +32 -0
  43. package/dist/site/assets/editor.worker.js +1 -0
  44. package/dist/site/assets/favicon.svg +3 -0
  45. package/dist/site/assets/guide/01-account-settings.png +0 -0
  46. package/dist/site/assets/guide/02-atlassian-security.png +0 -0
  47. package/dist/site/assets/guide/03-api-tokens.png +0 -0
  48. package/dist/site/assets/guide/04-scope-api-token-step1.png +0 -0
  49. package/dist/site/assets/guide/05-scope-api-token-step2.png +0 -0
  50. package/dist/site/assets/guide/06-scope-api-token-step3.png +0 -0
  51. package/dist/site/assets/guide/07-scope-api-token-step4.png +0 -0
  52. package/dist/site/assets/guide/web/document-preview.png +0 -0
  53. package/dist/site/assets/guide/web/remote-diff.png +0 -0
  54. package/dist/site/assets/guide/web/sync-workspace.png +0 -0
  55. package/dist/site/assets/pdf/cmaps/78-EUC-H.bcmap +0 -0
  56. package/dist/site/assets/pdf/cmaps/78-EUC-V.bcmap +0 -0
  57. package/dist/site/assets/pdf/cmaps/78-H.bcmap +0 -0
  58. package/dist/site/assets/pdf/cmaps/78-RKSJ-H.bcmap +0 -0
  59. package/dist/site/assets/pdf/cmaps/78-RKSJ-V.bcmap +0 -0
  60. package/dist/site/assets/pdf/cmaps/78-V.bcmap +0 -0
  61. package/dist/site/assets/pdf/cmaps/78ms-RKSJ-H.bcmap +0 -0
  62. package/dist/site/assets/pdf/cmaps/78ms-RKSJ-V.bcmap +0 -0
  63. package/dist/site/assets/pdf/cmaps/83pv-RKSJ-H.bcmap +0 -0
  64. package/dist/site/assets/pdf/cmaps/90ms-RKSJ-H.bcmap +0 -0
  65. package/dist/site/assets/pdf/cmaps/90ms-RKSJ-V.bcmap +0 -0
  66. package/dist/site/assets/pdf/cmaps/90msp-RKSJ-H.bcmap +0 -0
  67. package/dist/site/assets/pdf/cmaps/90msp-RKSJ-V.bcmap +0 -0
  68. package/dist/site/assets/pdf/cmaps/90pv-RKSJ-H.bcmap +0 -0
  69. package/dist/site/assets/pdf/cmaps/90pv-RKSJ-V.bcmap +0 -0
  70. package/dist/site/assets/pdf/cmaps/Add-H.bcmap +0 -0
  71. package/dist/site/assets/pdf/cmaps/Add-RKSJ-H.bcmap +0 -0
  72. package/dist/site/assets/pdf/cmaps/Add-RKSJ-V.bcmap +0 -0
  73. package/dist/site/assets/pdf/cmaps/Add-V.bcmap +0 -0
  74. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-0.bcmap +0 -0
  75. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-1.bcmap +0 -0
  76. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-2.bcmap +0 -0
  77. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-3.bcmap +0 -0
  78. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-4.bcmap +0 -0
  79. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-5.bcmap +0 -0
  80. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-6.bcmap +0 -0
  81. package/dist/site/assets/pdf/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  82. package/dist/site/assets/pdf/cmaps/Adobe-GB1-0.bcmap +0 -0
  83. package/dist/site/assets/pdf/cmaps/Adobe-GB1-1.bcmap +0 -0
  84. package/dist/site/assets/pdf/cmaps/Adobe-GB1-2.bcmap +0 -0
  85. package/dist/site/assets/pdf/cmaps/Adobe-GB1-3.bcmap +0 -0
  86. package/dist/site/assets/pdf/cmaps/Adobe-GB1-4.bcmap +0 -0
  87. package/dist/site/assets/pdf/cmaps/Adobe-GB1-5.bcmap +0 -0
  88. package/dist/site/assets/pdf/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  89. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-0.bcmap +0 -0
  90. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-1.bcmap +0 -0
  91. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-2.bcmap +0 -0
  92. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-3.bcmap +0 -0
  93. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-4.bcmap +0 -0
  94. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-5.bcmap +0 -0
  95. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-6.bcmap +0 -0
  96. package/dist/site/assets/pdf/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  97. package/dist/site/assets/pdf/cmaps/Adobe-Korea1-0.bcmap +0 -0
  98. package/dist/site/assets/pdf/cmaps/Adobe-Korea1-1.bcmap +0 -0
  99. package/dist/site/assets/pdf/cmaps/Adobe-Korea1-2.bcmap +0 -0
  100. package/dist/site/assets/pdf/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  101. package/dist/site/assets/pdf/cmaps/B5-H.bcmap +0 -0
  102. package/dist/site/assets/pdf/cmaps/B5-V.bcmap +0 -0
  103. package/dist/site/assets/pdf/cmaps/B5pc-H.bcmap +0 -0
  104. package/dist/site/assets/pdf/cmaps/B5pc-V.bcmap +0 -0
  105. package/dist/site/assets/pdf/cmaps/CNS-EUC-H.bcmap +0 -0
  106. package/dist/site/assets/pdf/cmaps/CNS-EUC-V.bcmap +0 -0
  107. package/dist/site/assets/pdf/cmaps/CNS1-H.bcmap +0 -0
  108. package/dist/site/assets/pdf/cmaps/CNS1-V.bcmap +0 -0
  109. package/dist/site/assets/pdf/cmaps/CNS2-H.bcmap +0 -0
  110. package/dist/site/assets/pdf/cmaps/CNS2-V.bcmap +3 -0
  111. package/dist/site/assets/pdf/cmaps/ETHK-B5-H.bcmap +0 -0
  112. package/dist/site/assets/pdf/cmaps/ETHK-B5-V.bcmap +0 -0
  113. package/dist/site/assets/pdf/cmaps/ETen-B5-H.bcmap +0 -0
  114. package/dist/site/assets/pdf/cmaps/ETen-B5-V.bcmap +0 -0
  115. package/dist/site/assets/pdf/cmaps/ETenms-B5-H.bcmap +3 -0
  116. package/dist/site/assets/pdf/cmaps/ETenms-B5-V.bcmap +0 -0
  117. package/dist/site/assets/pdf/cmaps/EUC-H.bcmap +0 -0
  118. package/dist/site/assets/pdf/cmaps/EUC-V.bcmap +0 -0
  119. package/dist/site/assets/pdf/cmaps/Ext-H.bcmap +0 -0
  120. package/dist/site/assets/pdf/cmaps/Ext-RKSJ-H.bcmap +0 -0
  121. package/dist/site/assets/pdf/cmaps/Ext-RKSJ-V.bcmap +0 -0
  122. package/dist/site/assets/pdf/cmaps/Ext-V.bcmap +0 -0
  123. package/dist/site/assets/pdf/cmaps/GB-EUC-H.bcmap +0 -0
  124. package/dist/site/assets/pdf/cmaps/GB-EUC-V.bcmap +0 -0
  125. package/dist/site/assets/pdf/cmaps/GB-H.bcmap +4 -0
  126. package/dist/site/assets/pdf/cmaps/GB-V.bcmap +0 -0
  127. package/dist/site/assets/pdf/cmaps/GBK-EUC-H.bcmap +0 -0
  128. package/dist/site/assets/pdf/cmaps/GBK-EUC-V.bcmap +0 -0
  129. package/dist/site/assets/pdf/cmaps/GBK2K-H.bcmap +0 -0
  130. package/dist/site/assets/pdf/cmaps/GBK2K-V.bcmap +0 -0
  131. package/dist/site/assets/pdf/cmaps/GBKp-EUC-H.bcmap +0 -0
  132. package/dist/site/assets/pdf/cmaps/GBKp-EUC-V.bcmap +0 -0
  133. package/dist/site/assets/pdf/cmaps/GBT-EUC-H.bcmap +0 -0
  134. package/dist/site/assets/pdf/cmaps/GBT-EUC-V.bcmap +0 -0
  135. package/dist/site/assets/pdf/cmaps/GBT-H.bcmap +0 -0
  136. package/dist/site/assets/pdf/cmaps/GBT-V.bcmap +0 -0
  137. package/dist/site/assets/pdf/cmaps/GBTpc-EUC-H.bcmap +0 -0
  138. package/dist/site/assets/pdf/cmaps/GBTpc-EUC-V.bcmap +0 -0
  139. package/dist/site/assets/pdf/cmaps/GBpc-EUC-H.bcmap +0 -0
  140. package/dist/site/assets/pdf/cmaps/GBpc-EUC-V.bcmap +0 -0
  141. package/dist/site/assets/pdf/cmaps/H.bcmap +0 -0
  142. package/dist/site/assets/pdf/cmaps/HKdla-B5-H.bcmap +0 -0
  143. package/dist/site/assets/pdf/cmaps/HKdla-B5-V.bcmap +0 -0
  144. package/dist/site/assets/pdf/cmaps/HKdlb-B5-H.bcmap +0 -0
  145. package/dist/site/assets/pdf/cmaps/HKdlb-B5-V.bcmap +0 -0
  146. package/dist/site/assets/pdf/cmaps/HKgccs-B5-H.bcmap +0 -0
  147. package/dist/site/assets/pdf/cmaps/HKgccs-B5-V.bcmap +0 -0
  148. package/dist/site/assets/pdf/cmaps/HKm314-B5-H.bcmap +0 -0
  149. package/dist/site/assets/pdf/cmaps/HKm314-B5-V.bcmap +0 -0
  150. package/dist/site/assets/pdf/cmaps/HKm471-B5-H.bcmap +0 -0
  151. package/dist/site/assets/pdf/cmaps/HKm471-B5-V.bcmap +0 -0
  152. package/dist/site/assets/pdf/cmaps/HKscs-B5-H.bcmap +0 -0
  153. package/dist/site/assets/pdf/cmaps/HKscs-B5-V.bcmap +0 -0
  154. package/dist/site/assets/pdf/cmaps/Hankaku.bcmap +0 -0
  155. package/dist/site/assets/pdf/cmaps/Hiragana.bcmap +0 -0
  156. package/dist/site/assets/pdf/cmaps/KSC-EUC-H.bcmap +0 -0
  157. package/dist/site/assets/pdf/cmaps/KSC-EUC-V.bcmap +0 -0
  158. package/dist/site/assets/pdf/cmaps/KSC-H.bcmap +0 -0
  159. package/dist/site/assets/pdf/cmaps/KSC-Johab-H.bcmap +0 -0
  160. package/dist/site/assets/pdf/cmaps/KSC-Johab-V.bcmap +0 -0
  161. package/dist/site/assets/pdf/cmaps/KSC-V.bcmap +0 -0
  162. package/dist/site/assets/pdf/cmaps/KSCms-UHC-H.bcmap +0 -0
  163. package/dist/site/assets/pdf/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  164. package/dist/site/assets/pdf/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  165. package/dist/site/assets/pdf/cmaps/KSCms-UHC-V.bcmap +0 -0
  166. package/dist/site/assets/pdf/cmaps/KSCpc-EUC-H.bcmap +0 -0
  167. package/dist/site/assets/pdf/cmaps/KSCpc-EUC-V.bcmap +0 -0
  168. package/dist/site/assets/pdf/cmaps/Katakana.bcmap +0 -0
  169. package/dist/site/assets/pdf/cmaps/LICENSE +36 -0
  170. package/dist/site/assets/pdf/cmaps/NWP-H.bcmap +0 -0
  171. package/dist/site/assets/pdf/cmaps/NWP-V.bcmap +0 -0
  172. package/dist/site/assets/pdf/cmaps/RKSJ-H.bcmap +0 -0
  173. package/dist/site/assets/pdf/cmaps/RKSJ-V.bcmap +0 -0
  174. package/dist/site/assets/pdf/cmaps/Roman.bcmap +0 -0
  175. package/dist/site/assets/pdf/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  176. package/dist/site/assets/pdf/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  177. package/dist/site/assets/pdf/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  178. package/dist/site/assets/pdf/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  179. package/dist/site/assets/pdf/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  180. package/dist/site/assets/pdf/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  181. package/dist/site/assets/pdf/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  182. package/dist/site/assets/pdf/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  183. package/dist/site/assets/pdf/cmaps/UniGB-UCS2-H.bcmap +0 -0
  184. package/dist/site/assets/pdf/cmaps/UniGB-UCS2-V.bcmap +0 -0
  185. package/dist/site/assets/pdf/cmaps/UniGB-UTF16-H.bcmap +0 -0
  186. package/dist/site/assets/pdf/cmaps/UniGB-UTF16-V.bcmap +0 -0
  187. package/dist/site/assets/pdf/cmaps/UniGB-UTF32-H.bcmap +0 -0
  188. package/dist/site/assets/pdf/cmaps/UniGB-UTF32-V.bcmap +0 -0
  189. package/dist/site/assets/pdf/cmaps/UniGB-UTF8-H.bcmap +0 -0
  190. package/dist/site/assets/pdf/cmaps/UniGB-UTF8-V.bcmap +0 -0
  191. package/dist/site/assets/pdf/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  192. package/dist/site/assets/pdf/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  193. package/dist/site/assets/pdf/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  194. package/dist/site/assets/pdf/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  195. package/dist/site/assets/pdf/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  196. package/dist/site/assets/pdf/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  197. package/dist/site/assets/pdf/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  198. package/dist/site/assets/pdf/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  199. package/dist/site/assets/pdf/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  200. package/dist/site/assets/pdf/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  201. package/dist/site/assets/pdf/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  202. package/dist/site/assets/pdf/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  203. package/dist/site/assets/pdf/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  204. package/dist/site/assets/pdf/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  205. package/dist/site/assets/pdf/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  206. package/dist/site/assets/pdf/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  207. package/dist/site/assets/pdf/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  208. package/dist/site/assets/pdf/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  209. package/dist/site/assets/pdf/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  210. package/dist/site/assets/pdf/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  211. package/dist/site/assets/pdf/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  212. package/dist/site/assets/pdf/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  213. package/dist/site/assets/pdf/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  214. package/dist/site/assets/pdf/cmaps/UniKS-UCS2-H.bcmap +0 -0
  215. package/dist/site/assets/pdf/cmaps/UniKS-UCS2-V.bcmap +0 -0
  216. package/dist/site/assets/pdf/cmaps/UniKS-UTF16-H.bcmap +0 -0
  217. package/dist/site/assets/pdf/cmaps/UniKS-UTF16-V.bcmap +0 -0
  218. package/dist/site/assets/pdf/cmaps/UniKS-UTF32-H.bcmap +0 -0
  219. package/dist/site/assets/pdf/cmaps/UniKS-UTF32-V.bcmap +0 -0
  220. package/dist/site/assets/pdf/cmaps/UniKS-UTF8-H.bcmap +0 -0
  221. package/dist/site/assets/pdf/cmaps/UniKS-UTF8-V.bcmap +0 -0
  222. package/dist/site/assets/pdf/cmaps/V.bcmap +0 -0
  223. package/dist/site/assets/pdf/cmaps/WP-Symbol.bcmap +0 -0
  224. package/dist/site/assets/pdf/standard_fonts/FoxitDingbats.pfb +0 -0
  225. package/dist/site/assets/pdf/standard_fonts/FoxitFixed.pfb +0 -0
  226. package/dist/site/assets/pdf/standard_fonts/FoxitFixedBold.pfb +0 -0
  227. package/dist/site/assets/pdf/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
  228. package/dist/site/assets/pdf/standard_fonts/FoxitFixedItalic.pfb +0 -0
  229. package/dist/site/assets/pdf/standard_fonts/FoxitSerif.pfb +0 -0
  230. package/dist/site/assets/pdf/standard_fonts/FoxitSerifBold.pfb +0 -0
  231. package/dist/site/assets/pdf/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
  232. package/dist/site/assets/pdf/standard_fonts/FoxitSerifItalic.pfb +0 -0
  233. package/dist/site/assets/pdf/standard_fonts/FoxitSymbol.pfb +0 -0
  234. package/dist/site/assets/pdf/standard_fonts/LICENSE_FOXIT +27 -0
  235. package/dist/site/assets/pdf/standard_fonts/LICENSE_LIBERATION +363 -0
  236. package/dist/site/assets/pdf/standard_fonts/LiberationSans-Bold.ttf +0 -0
  237. package/dist/site/assets/pdf/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
  238. package/dist/site/assets/pdf/standard_fonts/LiberationSans-Italic.ttf +0 -0
  239. package/dist/site/assets/pdf/standard_fonts/LiberationSans-Regular.ttf +0 -0
  240. package/dist/site/assets/pdf/wasm/LICENSE_JBIG2 +196 -0
  241. package/dist/site/assets/pdf/wasm/LICENSE_OPENJPEG +39 -0
  242. package/dist/site/assets/pdf/wasm/LICENSE_PDFJS_JBIG2 +13 -0
  243. package/dist/site/assets/pdf/wasm/LICENSE_PDFJS_OPENJPEG +22 -0
  244. package/dist/site/assets/pdf/wasm/LICENSE_PDFJS_QCMS +19 -0
  245. package/dist/site/assets/pdf/wasm/LICENSE_QCMS +21 -0
  246. package/dist/site/assets/pdf/wasm/jbig2.wasm +0 -0
  247. package/dist/site/assets/pdf/wasm/jbig2_nowasm_fallback.js +15 -0
  248. package/dist/site/assets/pdf/wasm/openjpeg.wasm +0 -0
  249. package/dist/site/assets/pdf/wasm/openjpeg_nowasm_fallback.js +17 -0
  250. package/dist/site/assets/pdf/wasm/qcms_bg.wasm +0 -0
  251. package/dist/site/assets/pdf/wasm/quickjs-eval.js +16 -0
  252. package/dist/site/assets/pdf/wasm/quickjs-eval.wasm +0 -0
  253. package/dist/site/assets/pdf.worker.min.mjs +29 -0
  254. package/dist/site/assets/workspace.worker.js +409 -0
  255. package/dist/site/index.html +253 -0
  256. package/dist/sync/baseline.js +45 -0
  257. package/dist/sync/changes.js +51 -0
  258. package/dist/sync/diff.js +80 -0
  259. package/dist/sync/fetch.js +287 -0
  260. package/dist/sync/ignore.js +4 -4
  261. package/dist/sync/local-state.js +12 -0
  262. package/dist/sync/mapping.js +31 -8
  263. package/dist/sync/metadata.js +57 -0
  264. package/dist/sync/prepare.js +52 -0
  265. package/dist/sync/pull.js +618 -141
  266. package/dist/sync/push.js +262 -207
  267. package/dist/sync/reconcile.js +51 -0
  268. package/dist/sync/references.js +2 -2
  269. package/dist/sync/remap.js +17 -5
  270. package/dist/sync/render.js +3 -2
  271. package/dist/sync/restore.js +28 -0
  272. package/dist/sync/scope.js +47 -0
  273. package/dist/sync/staging.js +193 -0
  274. package/dist/sync/workspace.js +100 -0
  275. package/dist/web/assets/PDFJS-LICENSE.txt +177 -0
  276. package/dist/web/assets/app.css +1 -1
  277. package/dist/web/assets/app.js +3 -938
  278. package/dist/web/assets/chunks/chunk-TEW74QPN.js +35 -0
  279. package/dist/web/assets/chunks/markdown-S6623E66.js +1 -0
  280. package/dist/web/assets/chunks/monaco-36ZGUH4Y.js +897 -0
  281. package/dist/web/assets/chunks/monaco-HFLEBJTI.css +1 -0
  282. package/dist/web/assets/chunks/pdf-DDN7TXTP.js +55 -0
  283. package/dist/web/assets/confluence-env-example.txt +32 -0
  284. package/dist/web/assets/editor.worker.js +1 -30
  285. package/dist/web/assets/favicon.svg +3 -0
  286. package/dist/web/assets/guide/01-account-settings.png +0 -0
  287. package/dist/web/assets/guide/02-atlassian-security.png +0 -0
  288. package/dist/web/assets/guide/03-api-tokens.png +0 -0
  289. package/dist/web/assets/guide/04-scope-api-token-step1.png +0 -0
  290. package/dist/web/assets/guide/05-scope-api-token-step2.png +0 -0
  291. package/dist/web/assets/guide/06-scope-api-token-step3.png +0 -0
  292. package/dist/web/assets/guide/07-scope-api-token-step4.png +0 -0
  293. package/dist/web/assets/guide/web/document-preview.png +0 -0
  294. package/dist/web/assets/guide/web/remote-diff.png +0 -0
  295. package/dist/web/assets/guide/web/sync-workspace.png +0 -0
  296. package/dist/web/assets/pdf/cmaps/78-EUC-H.bcmap +0 -0
  297. package/dist/web/assets/pdf/cmaps/78-EUC-V.bcmap +0 -0
  298. package/dist/web/assets/pdf/cmaps/78-H.bcmap +0 -0
  299. package/dist/web/assets/pdf/cmaps/78-RKSJ-H.bcmap +0 -0
  300. package/dist/web/assets/pdf/cmaps/78-RKSJ-V.bcmap +0 -0
  301. package/dist/web/assets/pdf/cmaps/78-V.bcmap +0 -0
  302. package/dist/web/assets/pdf/cmaps/78ms-RKSJ-H.bcmap +0 -0
  303. package/dist/web/assets/pdf/cmaps/78ms-RKSJ-V.bcmap +0 -0
  304. package/dist/web/assets/pdf/cmaps/83pv-RKSJ-H.bcmap +0 -0
  305. package/dist/web/assets/pdf/cmaps/90ms-RKSJ-H.bcmap +0 -0
  306. package/dist/web/assets/pdf/cmaps/90ms-RKSJ-V.bcmap +0 -0
  307. package/dist/web/assets/pdf/cmaps/90msp-RKSJ-H.bcmap +0 -0
  308. package/dist/web/assets/pdf/cmaps/90msp-RKSJ-V.bcmap +0 -0
  309. package/dist/web/assets/pdf/cmaps/90pv-RKSJ-H.bcmap +0 -0
  310. package/dist/web/assets/pdf/cmaps/90pv-RKSJ-V.bcmap +0 -0
  311. package/dist/web/assets/pdf/cmaps/Add-H.bcmap +0 -0
  312. package/dist/web/assets/pdf/cmaps/Add-RKSJ-H.bcmap +0 -0
  313. package/dist/web/assets/pdf/cmaps/Add-RKSJ-V.bcmap +0 -0
  314. package/dist/web/assets/pdf/cmaps/Add-V.bcmap +0 -0
  315. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-0.bcmap +0 -0
  316. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-1.bcmap +0 -0
  317. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-2.bcmap +0 -0
  318. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-3.bcmap +0 -0
  319. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-4.bcmap +0 -0
  320. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-5.bcmap +0 -0
  321. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-6.bcmap +0 -0
  322. package/dist/web/assets/pdf/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  323. package/dist/web/assets/pdf/cmaps/Adobe-GB1-0.bcmap +0 -0
  324. package/dist/web/assets/pdf/cmaps/Adobe-GB1-1.bcmap +0 -0
  325. package/dist/web/assets/pdf/cmaps/Adobe-GB1-2.bcmap +0 -0
  326. package/dist/web/assets/pdf/cmaps/Adobe-GB1-3.bcmap +0 -0
  327. package/dist/web/assets/pdf/cmaps/Adobe-GB1-4.bcmap +0 -0
  328. package/dist/web/assets/pdf/cmaps/Adobe-GB1-5.bcmap +0 -0
  329. package/dist/web/assets/pdf/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  330. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-0.bcmap +0 -0
  331. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-1.bcmap +0 -0
  332. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-2.bcmap +0 -0
  333. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-3.bcmap +0 -0
  334. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-4.bcmap +0 -0
  335. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-5.bcmap +0 -0
  336. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-6.bcmap +0 -0
  337. package/dist/web/assets/pdf/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  338. package/dist/web/assets/pdf/cmaps/Adobe-Korea1-0.bcmap +0 -0
  339. package/dist/web/assets/pdf/cmaps/Adobe-Korea1-1.bcmap +0 -0
  340. package/dist/web/assets/pdf/cmaps/Adobe-Korea1-2.bcmap +0 -0
  341. package/dist/web/assets/pdf/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  342. package/dist/web/assets/pdf/cmaps/B5-H.bcmap +0 -0
  343. package/dist/web/assets/pdf/cmaps/B5-V.bcmap +0 -0
  344. package/dist/web/assets/pdf/cmaps/B5pc-H.bcmap +0 -0
  345. package/dist/web/assets/pdf/cmaps/B5pc-V.bcmap +0 -0
  346. package/dist/web/assets/pdf/cmaps/CNS-EUC-H.bcmap +0 -0
  347. package/dist/web/assets/pdf/cmaps/CNS-EUC-V.bcmap +0 -0
  348. package/dist/web/assets/pdf/cmaps/CNS1-H.bcmap +0 -0
  349. package/dist/web/assets/pdf/cmaps/CNS1-V.bcmap +0 -0
  350. package/dist/web/assets/pdf/cmaps/CNS2-H.bcmap +0 -0
  351. package/dist/web/assets/pdf/cmaps/CNS2-V.bcmap +3 -0
  352. package/dist/web/assets/pdf/cmaps/ETHK-B5-H.bcmap +0 -0
  353. package/dist/web/assets/pdf/cmaps/ETHK-B5-V.bcmap +0 -0
  354. package/dist/web/assets/pdf/cmaps/ETen-B5-H.bcmap +0 -0
  355. package/dist/web/assets/pdf/cmaps/ETen-B5-V.bcmap +0 -0
  356. package/dist/web/assets/pdf/cmaps/ETenms-B5-H.bcmap +3 -0
  357. package/dist/web/assets/pdf/cmaps/ETenms-B5-V.bcmap +0 -0
  358. package/dist/web/assets/pdf/cmaps/EUC-H.bcmap +0 -0
  359. package/dist/web/assets/pdf/cmaps/EUC-V.bcmap +0 -0
  360. package/dist/web/assets/pdf/cmaps/Ext-H.bcmap +0 -0
  361. package/dist/web/assets/pdf/cmaps/Ext-RKSJ-H.bcmap +0 -0
  362. package/dist/web/assets/pdf/cmaps/Ext-RKSJ-V.bcmap +0 -0
  363. package/dist/web/assets/pdf/cmaps/Ext-V.bcmap +0 -0
  364. package/dist/web/assets/pdf/cmaps/GB-EUC-H.bcmap +0 -0
  365. package/dist/web/assets/pdf/cmaps/GB-EUC-V.bcmap +0 -0
  366. package/dist/web/assets/pdf/cmaps/GB-H.bcmap +4 -0
  367. package/dist/web/assets/pdf/cmaps/GB-V.bcmap +0 -0
  368. package/dist/web/assets/pdf/cmaps/GBK-EUC-H.bcmap +0 -0
  369. package/dist/web/assets/pdf/cmaps/GBK-EUC-V.bcmap +0 -0
  370. package/dist/web/assets/pdf/cmaps/GBK2K-H.bcmap +0 -0
  371. package/dist/web/assets/pdf/cmaps/GBK2K-V.bcmap +0 -0
  372. package/dist/web/assets/pdf/cmaps/GBKp-EUC-H.bcmap +0 -0
  373. package/dist/web/assets/pdf/cmaps/GBKp-EUC-V.bcmap +0 -0
  374. package/dist/web/assets/pdf/cmaps/GBT-EUC-H.bcmap +0 -0
  375. package/dist/web/assets/pdf/cmaps/GBT-EUC-V.bcmap +0 -0
  376. package/dist/web/assets/pdf/cmaps/GBT-H.bcmap +0 -0
  377. package/dist/web/assets/pdf/cmaps/GBT-V.bcmap +0 -0
  378. package/dist/web/assets/pdf/cmaps/GBTpc-EUC-H.bcmap +0 -0
  379. package/dist/web/assets/pdf/cmaps/GBTpc-EUC-V.bcmap +0 -0
  380. package/dist/web/assets/pdf/cmaps/GBpc-EUC-H.bcmap +0 -0
  381. package/dist/web/assets/pdf/cmaps/GBpc-EUC-V.bcmap +0 -0
  382. package/dist/web/assets/pdf/cmaps/H.bcmap +0 -0
  383. package/dist/web/assets/pdf/cmaps/HKdla-B5-H.bcmap +0 -0
  384. package/dist/web/assets/pdf/cmaps/HKdla-B5-V.bcmap +0 -0
  385. package/dist/web/assets/pdf/cmaps/HKdlb-B5-H.bcmap +0 -0
  386. package/dist/web/assets/pdf/cmaps/HKdlb-B5-V.bcmap +0 -0
  387. package/dist/web/assets/pdf/cmaps/HKgccs-B5-H.bcmap +0 -0
  388. package/dist/web/assets/pdf/cmaps/HKgccs-B5-V.bcmap +0 -0
  389. package/dist/web/assets/pdf/cmaps/HKm314-B5-H.bcmap +0 -0
  390. package/dist/web/assets/pdf/cmaps/HKm314-B5-V.bcmap +0 -0
  391. package/dist/web/assets/pdf/cmaps/HKm471-B5-H.bcmap +0 -0
  392. package/dist/web/assets/pdf/cmaps/HKm471-B5-V.bcmap +0 -0
  393. package/dist/web/assets/pdf/cmaps/HKscs-B5-H.bcmap +0 -0
  394. package/dist/web/assets/pdf/cmaps/HKscs-B5-V.bcmap +0 -0
  395. package/dist/web/assets/pdf/cmaps/Hankaku.bcmap +0 -0
  396. package/dist/web/assets/pdf/cmaps/Hiragana.bcmap +0 -0
  397. package/dist/web/assets/pdf/cmaps/KSC-EUC-H.bcmap +0 -0
  398. package/dist/web/assets/pdf/cmaps/KSC-EUC-V.bcmap +0 -0
  399. package/dist/web/assets/pdf/cmaps/KSC-H.bcmap +0 -0
  400. package/dist/web/assets/pdf/cmaps/KSC-Johab-H.bcmap +0 -0
  401. package/dist/web/assets/pdf/cmaps/KSC-Johab-V.bcmap +0 -0
  402. package/dist/web/assets/pdf/cmaps/KSC-V.bcmap +0 -0
  403. package/dist/web/assets/pdf/cmaps/KSCms-UHC-H.bcmap +0 -0
  404. package/dist/web/assets/pdf/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  405. package/dist/web/assets/pdf/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  406. package/dist/web/assets/pdf/cmaps/KSCms-UHC-V.bcmap +0 -0
  407. package/dist/web/assets/pdf/cmaps/KSCpc-EUC-H.bcmap +0 -0
  408. package/dist/web/assets/pdf/cmaps/KSCpc-EUC-V.bcmap +0 -0
  409. package/dist/web/assets/pdf/cmaps/Katakana.bcmap +0 -0
  410. package/dist/web/assets/pdf/cmaps/LICENSE +36 -0
  411. package/dist/web/assets/pdf/cmaps/NWP-H.bcmap +0 -0
  412. package/dist/web/assets/pdf/cmaps/NWP-V.bcmap +0 -0
  413. package/dist/web/assets/pdf/cmaps/RKSJ-H.bcmap +0 -0
  414. package/dist/web/assets/pdf/cmaps/RKSJ-V.bcmap +0 -0
  415. package/dist/web/assets/pdf/cmaps/Roman.bcmap +0 -0
  416. package/dist/web/assets/pdf/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  417. package/dist/web/assets/pdf/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  418. package/dist/web/assets/pdf/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  419. package/dist/web/assets/pdf/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  420. package/dist/web/assets/pdf/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  421. package/dist/web/assets/pdf/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  422. package/dist/web/assets/pdf/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  423. package/dist/web/assets/pdf/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  424. package/dist/web/assets/pdf/cmaps/UniGB-UCS2-H.bcmap +0 -0
  425. package/dist/web/assets/pdf/cmaps/UniGB-UCS2-V.bcmap +0 -0
  426. package/dist/web/assets/pdf/cmaps/UniGB-UTF16-H.bcmap +0 -0
  427. package/dist/web/assets/pdf/cmaps/UniGB-UTF16-V.bcmap +0 -0
  428. package/dist/web/assets/pdf/cmaps/UniGB-UTF32-H.bcmap +0 -0
  429. package/dist/web/assets/pdf/cmaps/UniGB-UTF32-V.bcmap +0 -0
  430. package/dist/web/assets/pdf/cmaps/UniGB-UTF8-H.bcmap +0 -0
  431. package/dist/web/assets/pdf/cmaps/UniGB-UTF8-V.bcmap +0 -0
  432. package/dist/web/assets/pdf/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  433. package/dist/web/assets/pdf/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  434. package/dist/web/assets/pdf/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  435. package/dist/web/assets/pdf/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  436. package/dist/web/assets/pdf/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  437. package/dist/web/assets/pdf/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  438. package/dist/web/assets/pdf/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  439. package/dist/web/assets/pdf/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  440. package/dist/web/assets/pdf/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  441. package/dist/web/assets/pdf/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  442. package/dist/web/assets/pdf/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  443. package/dist/web/assets/pdf/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  444. package/dist/web/assets/pdf/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  445. package/dist/web/assets/pdf/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  446. package/dist/web/assets/pdf/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  447. package/dist/web/assets/pdf/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  448. package/dist/web/assets/pdf/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  449. package/dist/web/assets/pdf/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  450. package/dist/web/assets/pdf/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  451. package/dist/web/assets/pdf/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  452. package/dist/web/assets/pdf/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  453. package/dist/web/assets/pdf/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  454. package/dist/web/assets/pdf/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  455. package/dist/web/assets/pdf/cmaps/UniKS-UCS2-H.bcmap +0 -0
  456. package/dist/web/assets/pdf/cmaps/UniKS-UCS2-V.bcmap +0 -0
  457. package/dist/web/assets/pdf/cmaps/UniKS-UTF16-H.bcmap +0 -0
  458. package/dist/web/assets/pdf/cmaps/UniKS-UTF16-V.bcmap +0 -0
  459. package/dist/web/assets/pdf/cmaps/UniKS-UTF32-H.bcmap +0 -0
  460. package/dist/web/assets/pdf/cmaps/UniKS-UTF32-V.bcmap +0 -0
  461. package/dist/web/assets/pdf/cmaps/UniKS-UTF8-H.bcmap +0 -0
  462. package/dist/web/assets/pdf/cmaps/UniKS-UTF8-V.bcmap +0 -0
  463. package/dist/web/assets/pdf/cmaps/V.bcmap +0 -0
  464. package/dist/web/assets/pdf/cmaps/WP-Symbol.bcmap +0 -0
  465. package/dist/web/assets/pdf/standard_fonts/FoxitDingbats.pfb +0 -0
  466. package/dist/web/assets/pdf/standard_fonts/FoxitFixed.pfb +0 -0
  467. package/dist/web/assets/pdf/standard_fonts/FoxitFixedBold.pfb +0 -0
  468. package/dist/web/assets/pdf/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
  469. package/dist/web/assets/pdf/standard_fonts/FoxitFixedItalic.pfb +0 -0
  470. package/dist/web/assets/pdf/standard_fonts/FoxitSerif.pfb +0 -0
  471. package/dist/web/assets/pdf/standard_fonts/FoxitSerifBold.pfb +0 -0
  472. package/dist/web/assets/pdf/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
  473. package/dist/web/assets/pdf/standard_fonts/FoxitSerifItalic.pfb +0 -0
  474. package/dist/web/assets/pdf/standard_fonts/FoxitSymbol.pfb +0 -0
  475. package/dist/web/assets/pdf/standard_fonts/LICENSE_FOXIT +27 -0
  476. package/dist/web/assets/pdf/standard_fonts/LICENSE_LIBERATION +363 -0
  477. package/dist/web/assets/pdf/standard_fonts/LiberationSans-Bold.ttf +0 -0
  478. package/dist/web/assets/pdf/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
  479. package/dist/web/assets/pdf/standard_fonts/LiberationSans-Italic.ttf +0 -0
  480. package/dist/web/assets/pdf/standard_fonts/LiberationSans-Regular.ttf +0 -0
  481. package/dist/web/assets/pdf/wasm/LICENSE_JBIG2 +196 -0
  482. package/dist/web/assets/pdf/wasm/LICENSE_OPENJPEG +39 -0
  483. package/dist/web/assets/pdf/wasm/LICENSE_PDFJS_JBIG2 +13 -0
  484. package/dist/web/assets/pdf/wasm/LICENSE_PDFJS_OPENJPEG +22 -0
  485. package/dist/web/assets/pdf/wasm/LICENSE_PDFJS_QCMS +19 -0
  486. package/dist/web/assets/pdf/wasm/LICENSE_QCMS +21 -0
  487. package/dist/web/assets/pdf/wasm/jbig2.wasm +0 -0
  488. package/dist/web/assets/pdf/wasm/jbig2_nowasm_fallback.js +15 -0
  489. package/dist/web/assets/pdf/wasm/openjpeg.wasm +0 -0
  490. package/dist/web/assets/pdf/wasm/openjpeg_nowasm_fallback.js +17 -0
  491. package/dist/web/assets/pdf/wasm/qcms_bg.wasm +0 -0
  492. package/dist/web/assets/pdf/wasm/quickjs-eval.js +16 -0
  493. package/dist/web/assets/pdf/wasm/quickjs-eval.wasm +0 -0
  494. package/dist/web/assets/pdf.worker.min.mjs +29 -0
  495. package/dist/web/assets/workspace.worker.js +409 -0
  496. package/dist/web/document-source.js +14 -0
  497. package/dist/web/hosted-files.js +189 -0
  498. package/dist/web/hosted.js +316 -0
  499. package/dist/web/node-runner.js +26 -0
  500. package/dist/web/preview.js +74 -0
  501. package/dist/web/server.js +85 -240
  502. package/dist/web/sync.js +365 -71
  503. package/dist/web/table-editor-script.js +99 -0
  504. package/dist/web/table-format.js +224 -0
  505. package/dist/web/views/help.js +15 -10
  506. package/dist/web/views/sync-panel.js +90 -19
  507. package/dist/web/views/token-guide.js +27 -0
  508. package/dist/web/views/usage-guide.js +12 -0
  509. package/dist/web/views/workspace.js +85 -15
  510. package/dist/web/workspace-api.js +371 -0
  511. package/docs/user/CLI.md +366 -0
  512. package/docs/user/CONFIGURATION.md +125 -0
  513. package/docs/user/OBSIDIAN.md +117 -0
  514. package/docs/user/WEB.md +90 -0
  515. package/docs/user/images/web/document-preview.png +0 -0
  516. package/docs/user/images/web/remote-diff.png +0 -0
  517. package/docs/user/images/web/sync-workspace.png +0 -0
  518. package/package.json +23 -6
package/dist/sync/pull.js CHANGED
@@ -1,5 +1,10 @@
1
+ import { progressStep } from '../shared/progress.js';
2
+ import { connectWorkspace } from './scope.js';
3
+ import { storeBaseline } from './baseline.js';
4
+ import { localStateHash } from './local-state.js';
5
+ import { buildTreeRenderer } from './render.js';
1
6
  /**
2
- * `confluence-sync pull <pageId|url> [--out <dir>] [--children]`
7
+ * `confluence-sync pull <pageId|url> [--out <dir>] (하위 항목 포함)`
3
8
  * Confluence 페이지를 읽어 Markdown(.md) 으로 생성한다(역방향).
4
9
  * 자식이 있는 페이지는 push 관례를 역으로 적용해 <slug>/README.md 폴더로 펼친다.
5
10
  * 첨부는 attachments/<문서명>/ 하위에 내려받고 이미지 링크를 그 경로로 재작성한다.
@@ -7,19 +12,29 @@
7
12
  * 함께 받은 페이지끼리의 내부 링크는 마지막에 상대 .md 경로로 이어 붙인다(Obsidian 그래프·백링크가 살아난다).
8
13
  * 각 문서 머리에 pageId 를 담은 frontmatter 를 남겨, 매핑 파일 없이도 push 가 원본 페이지를 다시 찾아간다.
9
14
  */
10
- import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
15
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, mkdtempSync, rmSync, readdirSync, copyFileSync, lstatSync, realpathSync, rmdirSync, constants } from 'node:fs';
11
16
  import { resolve, join, dirname, basename, relative } from 'node:path';
17
+ import { fetchRemote } from './fetch.js';
18
+ import { verifyScope } from './scope.js';
19
+ import { positionalArgs } from '../cli/args.js';
20
+ import { tmpdir } from 'node:os';
21
+ import { createHash } from 'node:crypto';
22
+ import { loadWorkspace, findSyncBase, resolveSyncBase, inScope, inNestedWorkspace, within, assertWorkspaceConnection, requireScope, scopePath } from './workspace.js';
12
23
  import { readEnv, requireEnv } from '../confluence/config.js';
13
- import { createClient } from '../confluence/client.js';
24
+ import { createClient, ConfluenceDownloadError } from '../confluence/client.js';
14
25
  import { htmlToMarkdown, codeLanguagesFromStorage } from '../documents/html-to-markdown.js';
15
- import { buildFrontmatter, splitFrontmatter } from '../documents/obsidian.js';
26
+ import { buildFrontmatter, splitFrontmatter, wikilinkDisplayText } from '../documents/obsidian.js';
16
27
  import { collectHeadings, matchConfluenceAnchor } from '../documents/anchors.js';
17
28
  import { collectMarkdown } from '../documents/catalog.js';
18
- import { buildTreeRenderer } from './render.js';
29
+ import { preparePushTree } from './prepare.js';
19
30
  import { docHash } from '../documents/markdown.js';
31
+ import { rewriteTableLinks } from '../documents/tables.js';
20
32
  import { buildIgnorer } from './ignore.js';
21
- import { loadMapping, saveMapping, MAPPING_FILE } from './mapping.js';
22
- import { cyan, dim, green, red, yellow } from '../shared/colors.js';
33
+ import { mappingPath as defaultMappingPath, metadataFiles, METADATA_DIR } from './metadata.js';
34
+ import { loadMapping, saveMapping } from './mapping.js';
35
+ import { cyan, dim, green } from '../shared/colors.js';
36
+ class PullError extends Error {
37
+ }
23
38
  // 첨부 이미지는 항상 <md위치>/attachments/<문서명>/ 하위에 저장한다.
24
39
  const ASSETS_DIR = 'attachments';
25
40
  /** 마크다운 링크 중 Confluence 페이지를 가리키는 것: [text](https://.../pages/<id>/...) */
@@ -28,6 +43,15 @@ const PAGE_LINK = /\[([^\]]*)\]\(<?(https?:\/\/[^\s)<>]*?\/pages\/(\d+)[^\s)<>]*
28
43
  const SELF_ANCHOR = /\[([^\]]*)\]\(#([^)\s]+)\)/g;
29
44
  /** 다른 페이지의 섹션: [text](https://.../pages/<id>/제목#앵커) — 앵커만 따로 잡는다 */
30
45
  const PAGE_ANCHOR = /(\[[^\]]*\]\(<?https?:\/\/[^\s)<>]*?\/pages\/(\d+)[^\s)<>#]*)#([^\s)<>]+?)(>?\))/g;
46
+ function claim(ctx, path, id, directory = false) {
47
+ const rel = relative(ctx.stageBase, path).split('\\').join('/');
48
+ if (!within(ctx.stageBase, path) || !inScope(rel || '.', ctx.workspace) || inNestedWorkspace(ctx.baseDir, directory ? `${rel}/.boundary` : rel))
49
+ throw new Error(`관리 범위 밖 pull 대상: ${rel}`);
50
+ const prior = ctx.claimed.get(path);
51
+ if (prior && prior !== id)
52
+ throw new Error(`같은 로컬 경로에 여러 원격 항목이 대응합니다: ${rel}`);
53
+ ctx.claimed.set(path, id);
54
+ }
31
55
  function optVal(argv, name) {
32
56
  const i = argv.indexOf(name);
33
57
  return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined;
@@ -42,48 +66,87 @@ function parseContentId(input) {
42
66
  }
43
67
  /** 제목 → 파일/폴더명(파일시스템 금지문자 치환, 한글 유지) */
44
68
  function slug(title) {
45
- return title.replace(/[\/\\:*?"<>|]/g, '-').replace(/\s+/g, ' ').trim() || 'untitled';
69
+ const value = title.replace(/[\/\\:*?"<>|]/g, '-').replace(/\s+/g, ' ').trim();
70
+ return !value || /^\.+$/.test(value) ? 'untitled' : value;
46
71
  }
47
72
  /** 공백이 있으면 <...> 로 감싼 마크다운 링크 목적지 */
48
73
  const dest = (p) => (/\s/.test(p) ? `<${p}>` : p);
49
74
  /** 페이지 1건을 .md 로 생성(자식 페이지/폴더가 있으면 <slug>/README.md 폴더로). */
50
75
  async function pullPage(ctx, node, destDir) {
51
- const { client, withChildren } = ctx;
76
+ const { client } = ctx;
52
77
  const { id, title, html, storage } = node;
53
78
  // 페이지도 하위 페이지 + 하위 폴더를 모두 가질 수 있다(폴더가 페이지 밑에 올 수 있음).
54
- const childFolders = withChildren ? await client.getChildFolders(id) : [];
55
- const childPages = withChildren ? await client.getChildPages(id) : [];
79
+ const childFolders = ctx.incremental ? [] : await client.getChildFolders(id);
80
+ const childPages = ctx.incremental ? [] : await client.getChildPages(id);
56
81
  const hasChildren = childFolders.length + childPages.length > 0;
57
- const folder = hasChildren ? join(destDir, slug(title)) : destDir;
58
- const filePath = hasChildren ? join(folder, 'README.md') : join(destDir, `${slug(title)}.md`);
82
+ const rooted = id === ctx.rootId;
83
+ const known = ctx.knownPaths.get(id);
84
+ const filePath = known ?? (rooted ? join(destDir, 'README.md') : hasChildren ? join(destDir, slug(title), 'README.md') : join(destDir, `${slug(title)}.md`));
85
+ const folder = dirname(filePath);
86
+ claim(ctx, filePath, id);
87
+ if (node.version !== undefined)
88
+ ctx.versions.set(id, node.version);
59
89
  mkdirSync(dirname(filePath), { recursive: true });
60
90
  // 첨부는 항상 <md위치>/attachments/<문서명>/이미지 에 저장
61
91
  const mdBase = basename(filePath).replace(/\.md$/, '');
62
92
  const assetPrefix = `${ASSETS_DIR}/${mdBase}`;
63
93
  const referenced = new Set();
64
- const body = htmlToMarkdown(html, {
94
+ let body = htmlToMarkdown(html, {
95
+ obsidian: ctx.obsidian, pageId: id,
65
96
  onImage: (f) => referenced.add(f),
66
97
  assetPrefix,
67
98
  codeLangs: codeLanguagesFromStorage(storage), // 코드블록 언어는 storage 가 진짜
99
+ storage,
68
100
  });
69
101
  if (referenced.size) {
70
102
  const attachments = await client.listAttachments(id);
71
103
  const byName = new Map(attachments.map((a) => [a.filename, a.downloadPath]));
72
- const saveDir = join(dirname(filePath), assetPrefix);
104
+ const saveDir = join(dirname(filePath), assetPrefix), unavailable = new Set();
105
+ const missing = (name, reason) => {
106
+ unavailable.add(name);
107
+ ctx.missingAttachments++;
108
+ console.log(`[경고] 원격 첨부 누락 #${id}: ${name} (${reason}). 원격 참조를 유지하고 계속합니다.`);
109
+ };
73
110
  for (const name of referenced) {
111
+ if (basename(name) !== name || name === '.' || name === '..' || name.includes('\\'))
112
+ throw new Error(`잘못된 첨부 파일명: ${name}`);
74
113
  const dp = byName.get(name);
75
114
  if (!dp) {
76
- console.error(yellow(` ⚠ 첨부 없음: ${name}`));
115
+ missing(name, '이 페이지 첨부 목록에 없음');
77
116
  continue;
78
117
  }
118
+ let bytes;
79
119
  try {
80
- mkdirSync(saveDir, { recursive: true });
81
- writeFileSync(join(saveDir, name), await client.downloadAttachment(dp));
120
+ bytes = await client.downloadAttachment(dp);
82
121
  }
83
- catch (e) {
84
- console.error(red(` ✗ 첨부 다운로드 실패 ${name}`) + `\n${e.message}`);
122
+ catch (error) {
123
+ if (error instanceof ConfluenceDownloadError && error.status === 404) {
124
+ missing(name, '다운로드 404');
125
+ continue;
126
+ }
127
+ throw error;
85
128
  }
129
+ mkdirSync(saveDir, { recursive: true });
130
+ const assetPath = join(saveDir, name);
131
+ ctx.assetOwners.set(assetPath, id);
132
+ writeFileSync(assetPath, bytes);
86
133
  }
134
+ if (unavailable.size)
135
+ body = htmlToMarkdown(html, {
136
+ obsidian: ctx.obsidian, pageId: id, assetPrefix, codeLangs: codeLanguagesFromStorage(storage), storage,
137
+ attachmentDestination: (name, source) => {
138
+ if (!unavailable.has(name))
139
+ return `${assetPrefix}/${name}`;
140
+ if (/^https?:\/\//i.test(source))
141
+ return source;
142
+ const base = ctx.remoteBaseUrl.replace(/\/$/, '');
143
+ if (source.startsWith('/wiki/'))
144
+ return base.replace(/\/wiki$/, '') + source;
145
+ if (source.startsWith('/'))
146
+ return base + source;
147
+ return `${base}/download/attachments/${id}/${encodeURIComponent(name)}`;
148
+ },
149
+ });
87
150
  }
88
151
  // frontmatter 의 pageId 는 push 왕복의 앵커다(매핑 파일이 없어도 원본 페이지를 찾아간다).
89
152
  // 취향 옵션이 아니라 중복 페이지 생성을 막는 장치이므로 항상 쓴다. Obsidian 은 이를 속성으로 보여준다.
@@ -100,7 +163,7 @@ async function pullPage(ctx, node, destDir) {
100
163
  ctx.written.push(filePath);
101
164
  ctx.pathById.set(id, filePath);
102
165
  const imgNote = referenced.size ? `, 이미지 ${referenced.size}` : '';
103
- console.log(` ${green('+ 생성')} ${filePath} ${dim(`(#${id}${imgNote})`)}`);
166
+ console.log(` ${cyan('↓ 다운로드 준비')} ${join(ctx.baseDir, relative(ctx.stageBase, filePath))} ${dim(`(#${id}${imgNote}, 검증 후 저장)`)}`);
104
167
  const counts = { pages: 1, folders: 0 };
105
168
  for (const f of childFolders)
106
169
  add(counts, await pullNode(ctx, f.id, folder));
@@ -109,34 +172,46 @@ async function pullPage(ctx, node, destDir) {
109
172
  return counts;
110
173
  }
111
174
  function add(acc, c) { acc.pages += c.pages; acc.folders += c.folders; }
112
- /** 노드(페이지/폴더)를 재귀적으로 가져온다. 폴더는 디렉토리(본문 없음)로 만든다. 노드 하나 실패는 스킵. */
175
+ /** 노드(페이지/폴더)를 재귀적으로 가져온다. 폴더는 디렉토리(본문 없음)로 만든다. 노드 하나라도 실패하면 임시 다운로드 전체를 폐기. */
113
176
  async function pullNode(ctx, id, destDir) {
114
- const { client, withChildren } = ctx;
177
+ const { client } = ctx;
178
+ if (ctx.visited.has(id))
179
+ return { pages: 0, folders: 0 };
180
+ const known = ctx.knownPaths.get(id);
181
+ if (known && !inScope(relative(ctx.stageBase, known).split('\\').join('/'), ctx.workspace))
182
+ return { pages: 0, folders: 0 };
183
+ ctx.visited.add(id);
184
+ progressStep(`원격 문서·폴더 #${id} 가져오기 · ${ctx.visited.size}번째 항목`);
115
185
  try {
186
+ const location = await client.assertInScope(id);
187
+ if (location?.parentId)
188
+ ctx.parents.set(id, location.parentId);
116
189
  const node = await client.getNode(id);
190
+ if (ctx.incremental && (String(node.id) !== id || node.type !== 'page'))
191
+ throw new Error(`매핑된 원격 페이지가 아닙니다: #${id}`);
192
+ const expected = ctx.expectedVersions.get(id);
193
+ if (ctx.incremental && (!Number.isInteger(node.version) || !node.version || (expected !== undefined && node.version < expected)))
194
+ throw new Error(`원격 버전을 확인할 수 없거나 Fetch보다 이전 버전입니다: #${id}. 다시 Fetch한 후 Pull하세요.`);
117
195
  if (node.type === 'folder') {
118
- const dir = join(destDir, slug(node.title));
196
+ const dir = ctx.knownPaths.get(id) ?? (id === ctx.rootId ? destDir : join(destDir, slug(node.title)));
197
+ claim(ctx, dir, id, true);
119
198
  mkdirSync(dir, { recursive: true });
120
199
  ctx.folderIds.set(dir, id);
121
- console.log(` ${cyan('📁 폴더')} ${dir} ${dim(`(#${id})`)}`);
200
+ console.log(` ${cyan('📁 폴더 준비')} ${join(ctx.baseDir, relative(ctx.stageBase, dir))} ${dim(`(#${id}, 검증 후 저장)`)}`);
122
201
  const counts = { pages: 0, folders: 1 };
123
- if (withChildren) {
124
- for (const f of await client.getChildFolders(id))
125
- add(counts, await pullNode(ctx, f.id, dir));
126
- for (const p of await client.getChildPages(id))
127
- add(counts, await pullNode(ctx, p.id, dir));
128
- }
129
- else {
130
- console.log(dim(' (하위 내용은 --children 으로 가져옵니다)'));
131
- }
202
+ for (const f of await client.getChildFolders(id))
203
+ add(counts, await pullNode(ctx, f.id, dir));
204
+ for (const p of await client.getChildPages(id))
205
+ add(counts, await pullNode(ctx, p.id, dir));
132
206
  return counts;
133
207
  }
134
208
  // page: getNode 결과(html + storage) 그대로 사용
135
209
  return await pullPage(ctx, node, destDir);
136
210
  }
137
211
  catch (e) {
138
- console.error(red(` ✗ 실패 #${id}`) + `\n${e.message}`);
139
- return { pages: 0, folders: 0 };
212
+ if (e instanceof PullError)
213
+ throw e;
214
+ throw new PullError(`pull 실패 #${id}: ${e.message}`);
140
215
  }
141
216
  }
142
217
  function readDocInfo(file) {
@@ -155,8 +230,8 @@ function readDocInfo(file) {
155
230
  * 아무 데도 가리키지 못한다. 실제 헤딩을 찾아 그 문서의 슬러그로 바꿔 줘야 링크가 산다.
156
231
  * relinkPass 보다 **먼저** 돈다 — 아직 링크가 절대 URL 이라 어느 페이지인지 id 로 알 수 있기 때문이다.
157
232
  */
158
- export function anchorPass(written, pathById) {
159
- const info = new Map();
233
+ export function anchorPass(written, pathById, references = new Map()) {
234
+ const info = new Map(references);
160
235
  for (const f of written)
161
236
  info.set(f, readDocInfo(f));
162
237
  let count = 0;
@@ -188,6 +263,17 @@ export function anchorPass(written, pathById) {
188
263
  count++;
189
264
  return `${head}#${slug}${tail}`;
190
265
  });
266
+ after = rewriteTableLinks(after, href => {
267
+ const match = href.match(/^(https?:\/\/[^\s]*?\/pages\/(\d+)[^#]*)#(.+)$/);
268
+ const target = match ? pathById.get(match[2]) : undefined;
269
+ const targetInfo = href.startsWith('#') ? self : target ? info.get(target) : undefined;
270
+ const fragment = href.startsWith('#') ? href.slice(1) : match?.[3];
271
+ const slug = targetInfo && fragment ? toSlug(fragment, targetInfo) : null;
272
+ if (!slug)
273
+ return href;
274
+ count++;
275
+ return href.startsWith('#') ? `#${slug}` : `${match[1]}#${slug}`;
276
+ });
191
277
  if (after !== before)
192
278
  writeFileSync(file, after);
193
279
  }
@@ -218,13 +304,23 @@ export function relinkPass(written, pathById, wikilinks, info) {
218
304
  if (wikilinks && nameCount.get(name) === 1) {
219
305
  // Obsidian 은 슬러그가 아니라 헤딩 텍스트로 섹션을 가리킨다: [[문서#헤딩 텍스트]]
220
306
  const heading = slug ? info?.get(target)?.textBySlug.get(slug) : undefined;
307
+ const display = wikilinkDisplayText(label);
221
308
  if (!heading)
222
- return label === name ? `[[${name}]]` : `[[${name}|${label}]]`;
223
- return `[[${name}#${heading}|${label}]]`;
309
+ return display === name ? `[[${name}]]` : `[[${name}|${display}]]`;
310
+ return `[[${name}#${heading}|${display}]]`;
224
311
  }
225
312
  const rel = relative(dirname(file), target).split('\\').join('/');
226
313
  return `[${label}](${dest(rel + (slug ? `#${slug}` : ''))})`;
227
314
  });
315
+ // HTML cells retain HTML links even with --obsidian (wikilinks do not render there).
316
+ after = rewriteTableLinks(after, href => {
317
+ const match = href.match(/^https?:\/\/[^\s]*?\/pages\/(\d+)[^#]*(#.*)?$/);
318
+ const target = match ? pathById.get(match[1]) : undefined;
319
+ if (!target || target === file)
320
+ return href;
321
+ count++;
322
+ return relative(dirname(file), target).split('\\').join('/') + (match[2] ?? '');
323
+ });
228
324
  // 같은 문서 섹션도 vault 표기로: [§4](#슬러그) → [[#헤딩 텍스트|§4]]
229
325
  if (wikilinks) {
230
326
  after = after.replace(SELF_ANCHOR, (whole, text, frag) => {
@@ -232,7 +328,7 @@ export function relinkPass(written, pathById, wikilinks, info) {
232
328
  if (!heading)
233
329
  return whole;
234
330
  count++;
235
- const label = text.trim();
331
+ const label = wikilinkDisplayText(text.trim());
236
332
  return label && label !== heading ? `[[#${heading}|${label}]]` : `[[#${heading}]]`;
237
333
  });
238
334
  }
@@ -241,19 +337,6 @@ export function relinkPass(written, pathById, wikilinks, info) {
241
337
  }
242
338
  return count;
243
339
  }
244
- /**
245
- * 매핑 파일(.confluence-sync.json)이 있는 가장 가까운 상위 디렉토리 = 동기화 루트.
246
- * pull 을 base 하위 폴더로 받아도(`--out <base>/어딘가`) 매핑 키가 base 기준으로 남아야
247
- * 이어지는 push 가 같은 문서를 알아본다.
248
- */
249
- function findSyncBase(from) {
250
- for (let d = from;; d = dirname(d)) {
251
- if (existsSync(join(d, MAPPING_FILE)))
252
- return d;
253
- if (dirname(d) === d)
254
- return null;
255
- }
256
- }
257
340
  /**
258
341
  * 받아온 문서를 매핑 파일에 기록한다.
259
342
  *
@@ -263,109 +346,503 @@ function findSyncBase(from) {
263
346
  *
264
347
  * 반환값은 기록한 항목 수.
265
348
  */
266
- export function mappingPass(baseDir, mappingPath, pathById, folderIds) {
349
+ export async function mappingPass(baseDir, mappingPath, pathById, folderIds, versions = new Map(), parents = new Map(), validate) {
267
350
  const posix = (p) => p.split('\\').join('/');
268
351
  const ignorer = buildIgnorer(baseDir, []);
269
352
  const rels = collectMarkdown(baseDir)
270
353
  .map((f) => posix(relative(baseDir, f)))
271
354
  .filter((r) => !ignorer.ignores(r));
272
- const { docs, render } = buildTreeRenderer(baseDir, rels);
355
+ progressStep('가져온 문서의 동기화 기준 계산');
356
+ const before = existsSync(mappingPath) ? readFileSync(mappingPath, 'utf8') : undefined;
273
357
  const m = loadMapping(mappingPath);
274
- // 같은 페이지가 다른 키에 남아 있으면(문서가 옮겨졌으면) 중복 항목이 된다 → 옛 키를 걷어낸다
275
- const ids = new Set([...pathById.keys(), ...folderIds.values()]);
276
- for (const [k, v] of Object.entries(m))
277
- if (ids.has(v.pageId))
278
- delete m[k];
279
- let n = 0;
280
- for (const [id, abs] of pathById) {
281
- const rel = posix(relative(baseDir, abs));
282
- const doc = rel.startsWith('..') ? undefined : docs[rel];
283
- if (!doc)
284
- continue; // base 밖으로 받았거나 제외 규칙에 걸린 문서는 기록하지 않는다
285
- m[rel] = { pageId: id, title: doc.title, hash: docHash(doc.title, render(rel, doc.body, doc.title)) };
286
- n++;
358
+ const createdObjects = [];
359
+ const prepared = await preparePushTree(baseDir, rels, [...pathById.values()].map(path => posix(relative(baseDir, path))).filter(rel => rels.includes(rel)));
360
+ try {
361
+ const { docs, render } = prepared;
362
+ const pending = [];
363
+ // 같은 페이지가 다른 키에 남아 있으면(문서가 옮겨졌으면) 중복 항목이 된다 → 옛 키를 걷어낸다
364
+ const ids = new Set([...pathById.keys(), ...folderIds.values()]);
365
+ for (const [k, v] of Object.entries(m))
366
+ if (ids.has(v.pageId))
367
+ delete m[k];
368
+ let n = 0;
369
+ for (const [id, abs] of pathById) {
370
+ const rel = posix(relative(baseDir, abs));
371
+ const doc = rel.startsWith('..') ? undefined : docs[rel];
372
+ if (!doc)
373
+ continue; // base 밖으로 받았거나 제외 규칙에 걸린 문서는 기록하지 않는다
374
+ m[rel] = { pageId: id, title: doc.title, hash: docHash(doc.title, render(rel, doc.body, doc.title)), localHash: localStateHash(baseDir, rel, render(rel, doc.body, doc.title)), ...(versions.has(id) ? { remoteVersion: versions.get(id) } : {}), ...(parents.has(id) ? { remoteParentId: parents.get(id) } : {}) };
375
+ pending.push({ rel, text: readFileSync(abs, 'utf8') });
376
+ n++;
377
+ }
378
+ for (const [dir, id] of folderIds) {
379
+ const rel = posix(relative(baseDir, dir));
380
+ if (!rel || rel.startsWith('..'))
381
+ continue;
382
+ m[`${rel}/`] = { pageId: id, title: basename(dir), type: 'folder' };
383
+ n++;
384
+ }
385
+ validate?.();
386
+ for (const { rel, text } of pending) {
387
+ const id = createHash('sha256').update(text).digest('hex');
388
+ const object = join(baseDir, METADATA_DIR, 'objects', id + '.md');
389
+ if (!existsSync(object))
390
+ createdObjects.push(object);
391
+ m[rel].sourceObject = storeBaseline(baseDir, text);
392
+ }
393
+ validate?.();
394
+ if ((existsSync(mappingPath) ? readFileSync(mappingPath, 'utf8') : undefined) !== before)
395
+ throw new Error('기준 계산 중 매핑이 변경되었습니다. 다시 가져오세요.');
396
+ saveMapping(mappingPath, m);
397
+ return n;
287
398
  }
288
- for (const [dir, id] of folderIds) {
289
- const rel = posix(relative(baseDir, dir));
290
- if (!rel || rel.startsWith('..'))
291
- continue;
292
- m[`${rel}/`] = { pageId: id, title: basename(dir), type: 'folder' };
293
- n++;
399
+ catch (error) {
400
+ let retained = new Set();
401
+ try {
402
+ retained = new Set(Object.values(loadMapping(mappingPath)).map(entry => entry.sourceObject ?? ''));
403
+ }
404
+ catch {
405
+ throw error;
406
+ }
407
+ for (const object of createdObjects)
408
+ if (!retained.has(basename(object, '.md')) && existsSync(object))
409
+ rmSync(object, { force: true });
410
+ throw error;
411
+ }
412
+ finally {
413
+ prepared.dispose();
294
414
  }
295
- saveMapping(mappingPath, m);
296
- return n;
297
415
  }
298
- export async function runPull(argv) {
299
- const withChildren = argv.includes('--children');
300
- const wholeSpace = argv.includes('--space');
301
- const outDir = resolve(optVal(argv, '--out') ?? process.cwd());
302
- // 매핑을 어디에 남길지. base 는 매핑 키의 기준이므로 push 때의 --base 와 같아야 한다.
303
- const noMapping = argv.includes('--no-mapping');
304
- const baseOpt = optVal(argv, '--base');
305
- const baseDir = resolve(baseOpt ?? findSyncBase(outDir) ?? outDir);
306
- const mappingPath = resolve(optVal(argv, '--mapping') ?? join(baseDir, MAPPING_FILE));
307
- if (!noMapping && relative(baseDir, outDir).startsWith('..')) {
308
- console.error(red(`✗ --out 이 base 밖입니다: ${outDir}`) + dim(`\n base: ${baseDir} (--base 로 맞추거나 --no-mapping 을 쓰세요)`));
309
- process.exit(1);
416
+ /** Explicit remote recovery only accepts mapped Markdown files; replacement needs opt-in. */
417
+ export function validateRestorePaths(base, paths, overwrite = false) {
418
+ if (!paths.length)
419
+ throw new Error('복구할 파일을 직접 지정하세요: csync restore README.md');
420
+ requireScope(loadWorkspace(base));
421
+ const mapping = loadMapping(defaultMappingPath(base)), ignore = buildIgnorer(base, []);
422
+ const selected = [...new Set(paths.map(path => scopePath(base, path)))];
423
+ for (const rel of selected) {
424
+ const entry = mapping[rel];
425
+ if (!/\.md$/i.test(rel) || !entry || entry.type === 'folder')
426
+ throw new Error(`매핑된 Markdown 파일만 복구할 수 있습니다: ${rel}`);
427
+ if (ignore.ignores(rel) || inNestedWorkspace(base, rel))
428
+ throw new Error(`제외되었거나 독립 작업공간에 속한 파일입니다: ${rel}`);
429
+ if (entry.pendingMove)
430
+ throw new Error(`이동 대기 중인 파일은 복구할 수 없습니다: ${rel}`);
431
+ try {
432
+ const info = lstatSync(resolve(base, rel));
433
+ if (overwrite && info.isFile() && !info.isSymbolicLink()) {
434
+ const id = splitFrontmatter(readFileSync(resolve(base, rel), 'utf8')).data.pageId;
435
+ if (id && id !== entry.pageId)
436
+ throw new Error(`매핑과 로컬 pageId가 다릅니다: ${rel}`);
437
+ continue;
438
+ }
439
+ }
440
+ catch (error) {
441
+ if (error.code === 'ENOENT')
442
+ continue;
443
+ throw error;
444
+ }
445
+ throw new Error(`이미 존재하는 로컬 파일은 복구로 덮어쓰지 않습니다: ${rel}. 일반 파일의 수정을 버리려면 --overwrite를 지정하세요.`);
310
446
  }
311
- const env = readEnv();
447
+ const requestedIds = new Set(selected.map(rel => mapping[rel].pageId));
448
+ for (const file of collectMarkdown(base)) {
449
+ const rel = relative(base, file).split('\\').join('/');
450
+ if (inNestedWorkspace(base, rel))
451
+ continue;
452
+ const id = splitFrontmatter(readFileSync(file, 'utf8')).data.pageId;
453
+ if (id && requestedIds.has(id) && !(overwrite && selected.includes(rel) && mapping[rel].pageId === id))
454
+ throw new Error(`같은 pageId의 로컬 문서가 이미 있습니다: ${rel}. 파일 이동 여부를 확인하세요.`);
455
+ }
456
+ return selected;
457
+ }
458
+ export async function runPull(argv, options = {}) {
459
+ const restoring = options.restorePaths !== undefined;
460
+ const restoreOverwrite = restoring && options.restoreOverwrite === true;
461
+ if (argv.includes('--scope'))
462
+ throw new Error('--scope는 제거되었습니다. Pull은 스페이스 작업공간을 기준으로 실행합니다.');
463
+ if (argv.includes('--no-mapping'))
464
+ throw new Error('Pull은 매핑을 유지해야 합니다.');
465
+ if (argv.includes('--root'))
466
+ throw new Error('--root는 제거되었습니다. 기준 폴더는 스페이스 루트에 연결됩니다.');
467
+ const explicitRemoteOnly = argv.includes('--remote-only');
468
+ const legacyTraversal = !restoring && (argv.includes('--full') || argv.includes('--space') || positionalArgs(argv).length > 1);
469
+ const remoteOnly = !restoring && !legacyTraversal;
470
+ if (explicitRemoteOnly && (argv.includes('--full') || argv.includes('--space') || positionalArgs(argv).length > 1))
471
+ throw new Error('--remote-only는 전체 탐색·개별 페이지 지정과 함께 사용할 수 없습니다.');
472
+ const explicitBase = optVal(argv, '--base');
473
+ const outDir = resolve(optVal(argv, '--out') ?? resolveSyncBase(explicitBase) ?? process.cwd());
474
+ const baseDir = resolve(explicitBase ?? findSyncBase(outDir) ?? outDir);
475
+ if (!within(baseDir, outDir))
476
+ throw new Error(`--out 이 base 밖입니다: ${outDir}`);
477
+ if (baseDir !== outDir)
478
+ throw new Error('Pull은 기준 폴더 안의 원래 경로에 저장합니다. --base와 --out은 같아야 합니다.');
479
+ const mappingPath = resolve(optVal(argv, '--mapping') ?? defaultMappingPath(baseDir));
480
+ const restorePaths = restoring ? validateRestorePaths(baseDir, options.restorePaths, restoreOverwrite) : undefined;
481
+ let workspace = loadWorkspace(baseDir);
482
+ const env = readEnv(baseDir);
312
483
  requireEnv(env);
313
- const client = createClient({ baseUrl: env.baseUrl, email: env.email, token: env.token }, { force: false, verify: false });
314
- const ctx = {
315
- client,
316
- withChildren,
317
- obsidian: argv.includes('--obsidian'),
318
- spaceKey: env.spaceKey,
319
- written: [],
320
- pathById: new Map(),
321
- folderIds: new Map(),
322
- };
323
- const done = (c) => {
324
- // 앵커를 먼저 슬러그로 바꾼 뒤 링크를 상대경로로 옮긴다(순서가 바뀌면 어느 페이지의 헤딩인지 알 수 없다).
325
- const { count: anchors, info } = anchorPass(ctx.written, ctx.pathById);
326
- const relinked = relinkPass(ctx.written, ctx.pathById, ctx.obsidian, info);
327
- // 본문이 다 정해진 뒤에 기록해야 해시가 맞는다
328
- const mapped = noMapping ? 0 : mappingPass(baseDir, mappingPath, ctx.pathById, ctx.folderIds);
329
- console.log(`\n${green('완료')} 페이지 ${c.pages}개${c.folders ? `, 폴더 ${c.folders}개` : ''} 생성` +
330
- (relinked ? ` ${cyan(`내부링크 ${relinked}개 연결`)}` : '') +
331
- (anchors ? ` ${cyan(`섹션링크 ${anchors}개 복원`)}` : '') +
332
- (mapped ? ` ${cyan(`매핑 ${mapped}건`)}` : ''));
333
- if (mapped)
334
- console.log(dim(` 매핑: ${mappingPath} (base: ${baseDir})`));
335
- };
336
- // --space: 스페이스 홈페이지(콘텐츠 트리 루트)부터 전체를 재귀적으로 가져옴
337
- if (wholeSpace) {
338
- const { homepageId } = await client.getSpaceInfo(env.spaceKey);
339
- if (!homepageId) {
340
- console.error(red(`✗ 스페이스 '${env.spaceKey}' 의 홈페이지를 찾을 수 없습니다.`));
341
- process.exit(1);
484
+ if (workspace.version !== 2) {
485
+ mkdirSync(baseDir, { recursive: true });
486
+ workspace = await connectWorkspace(baseDir, env);
487
+ }
488
+ const scope = requireScope(workspace);
489
+ const oldMapping = loadMapping(mappingPath);
490
+ const stamp = (file) => existsSync(file) ? createHash('sha256').update(readFileSync(file)).digest('hex') : undefined;
491
+ const metadataBefore = new Map([mappingPath, join(baseDir, '.env'), ...metadataFiles(baseDir)].map(path => [path, stamp(path)]));
492
+ const assertMetadataUnchanged = () => { for (const [path, hash] of metadataBefore)
493
+ if (stamp(path) !== hash)
494
+ throw new Error('pull 도중 설정·매핑이 변경되었습니다.'); };
495
+ assertWorkspaceConnection(workspace, env);
496
+ const client = createClient({ baseUrl: env.baseUrl, email: env.email, token: env.token }, { force: false, verify: false, scope: { id: scope.id, spaceId: workspace.connection.spaceId, spaceOnly: !legacyTraversal } });
497
+ const target = positionalArgs(argv)[1];
498
+ const knownById = new Map(Object.entries(oldMapping).map(([rel, entry]) => [entry.pageId, rel]));
499
+ let ids;
500
+ if (target) {
501
+ const id = parseContentId(target);
502
+ if (!id || !((id === scope.id) || (knownById.has(id) && inScope(knownById.get(id).replace(/\/$/, ''), workspace))))
503
+ throw new Error('가져올 페이지가 작업공간에 매핑되지 않았습니다.');
504
+ ids = [id];
505
+ }
506
+ else
507
+ ids = [scope.id];
508
+ // A complete Fetch observation selects existing pages; it never authorizes a baseline advance.
509
+ let incremental = !legacyTraversal, skipped = 0;
510
+ let discovered;
511
+ const newPaths = new Set();
512
+ const recoveringPaths = new Set();
513
+ const expectedVersions = new Map();
514
+ if (restorePaths) {
515
+ ids = restorePaths.map(rel => oldMapping[rel].pageId);
516
+ for (const rel of restorePaths)
517
+ if (oldMapping[rel].remoteVersion)
518
+ expectedVersions.set(oldMapping[rel].pageId, oldMapping[rel].remoteVersion);
519
+ console.log(`원격에서 ${restoreOverwrite ? '선택 문서 원복(로컬 수정 덮어쓰기)' : '누락 문서 복구'} · ${restorePaths.length}개 · 하위 항목 탐색 없음`);
520
+ }
521
+ else if (legacyTraversal) {
522
+ console.log('기존 전체 탐색 옵션 호환 실행 · 일반 가져오기는 csync pull을 사용하세요.');
523
+ }
524
+ else {
525
+ const { snapshot, cached, requests } = await fetchRemote(baseDir, env, { mappingFile: mappingPath });
526
+ discovered = snapshot;
527
+ const localIds = new Map();
528
+ for (const file of collectMarkdown(baseDir)) {
529
+ const rel = relative(baseDir, file).split('\\').join('/');
530
+ if (inNestedWorkspace(baseDir, rel))
531
+ continue;
532
+ const id = splitFrontmatter(readFileSync(file, 'utf8')).data.pageId;
533
+ if (id)
534
+ localIds.set(id, [...(localIds.get(id) ?? []), rel]);
342
535
  }
343
- console.log(`${dim('pull:')} space ${cyan(env.spaceKey)} ${dim('→')} ${cyan(outDir)} ${dim('(전체)')}`);
344
- ctx.withChildren = true;
345
- done(await pullNode(ctx, homepageId, outDir));
536
+ ids = snapshot.items.filter(item => {
537
+ if (target && !ids.includes(item.pageId))
538
+ return false;
539
+ if (item.state !== 'changed' && item.state !== 'new') {
540
+ skipped++;
541
+ return false;
542
+ }
543
+ if (item.remoteVersion !== undefined)
544
+ expectedVersions.set(item.pageId, item.remoteVersion);
545
+ if (item.state === 'new') {
546
+ if (localIds.get(item.pageId)?.some(path => path !== item.path)) {
547
+ console.log(`같은 pageId의 다른 로컬 문서가 있어 보존: ${item.path}`);
548
+ skipped++;
549
+ return false;
550
+ }
551
+ try {
552
+ const info = lstatSync(resolve(baseDir, item.path));
553
+ const fm = info.isFile() && !info.isSymbolicLink() ? splitFrontmatter(readFileSync(resolve(baseDir, item.path), 'utf8')).data : {};
554
+ if (fm.pageId === item.pageId && fm.spaceKey === env.spaceKey) {
555
+ recoveringPaths.add(item.path);
556
+ console.log(`받기 미완료 문서 재검증: ${item.path}`);
557
+ }
558
+ else {
559
+ console.log(`기존 로컬 경로가 있어 보존: ${item.path}`);
560
+ skipped++;
561
+ return false;
562
+ }
563
+ }
564
+ catch (error) {
565
+ if (error.code !== 'ENOENT')
566
+ throw error;
567
+ }
568
+ if (inNestedWorkspace(baseDir, item.path) || buildIgnorer(baseDir, []).ignores(item.path))
569
+ return false;
570
+ knownById.set(item.pageId, item.path);
571
+ if (!recoveringPaths.has(item.path))
572
+ newPaths.add(item.path);
573
+ }
574
+ return true;
575
+ }).map(item => item.pageId);
576
+ console.log(`Fetch 기반 Pull · 조회 시각 ${snapshot.checkedAt}${cached ? ' (캐시)' : ''} · 메타데이터 요청 ${requests}회`);
577
+ console.log(`가져올 원격 신규·변경 ${ids.length}건 · 건너뜀 ${skipped}건`);
578
+ const unavailable = snapshot.items.filter(item => item.state === 'unavailable');
579
+ if (unavailable.length)
580
+ console.log(`[경고] 원격 조회 불가 ${unavailable.length}건은 문서와 매핑을 유지합니다.`);
581
+ }
582
+ assertMetadataUnchanged();
583
+ let rootId = ids.includes(scope.id) ? scope.id : undefined;
584
+ if (rootId && knownById.has(rootId) && knownById.get(rootId) !== 'README.md')
585
+ throw new Error('스페이스 루트 매핑은 README.md여야 합니다. 기존 매핑을 확인하세요.');
586
+ if (incremental && !ids.length) {
587
+ console.log('완료 · Fetch 기준 내려받을 원격 변경이 없습니다. 문서·매핑을 유지합니다.');
346
588
  return;
347
589
  }
348
- // 위치 인자: ['pull', <pageId|url>, ...]
349
- const positionals = [];
350
- for (let i = 0; i < argv.length; i++) {
351
- if (argv[i] === '--out' || argv[i] === '--base' || argv[i] === '--mapping') {
352
- i++;
353
- continue;
590
+ await verifyScope(client, workspace);
591
+ assertMetadataUnchanged();
592
+ // Stage everything first. Any download/conversion/scope failure leaves local sources untouched.
593
+ const originals = new Map();
594
+ const remember = (dir) => {
595
+ if (!existsSync(dir))
596
+ return;
597
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
598
+ const path = join(dir, entry.name);
599
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules')
600
+ remember(path);
601
+ else if (entry.isFile())
602
+ originals.set(path, stamp(path));
354
603
  }
355
- if (argv[i].startsWith('-'))
356
- continue;
357
- positionals.push(argv[i]);
604
+ };
605
+ remember(baseDir);
606
+ for (const path of [mappingPath, join(baseDir, '.env'), ...metadataFiles(baseDir)])
607
+ originals.set(path, stamp(path));
608
+ const ignore = buildIgnorer(baseDir, []);
609
+ const rels = (existsSync(baseDir) ? collectMarkdown(baseDir) : []).map(file => relative(baseDir, file).split('\\').join('/')).filter(rel => !inNestedWorkspace(baseDir, rel) && !ignore.ignores(rel));
610
+ const requestedIds = new Set(ids);
611
+ const scoped = rels.filter(rel => inScope(rel, workspace) && (!incremental || requestedIds.has(oldMapping[rel]?.pageId)));
612
+ progressStep('덮어쓰기 전 로컬 변경·첨부 확인');
613
+ const tree = await preparePushTree(baseDir, rels, scoped);
614
+ const localClean = new Set();
615
+ try {
616
+ // Old releases stored the unprepared renderer hash. Accept only an exact match
617
+ // with that baseline, never a fresh hash inferred from the current file alone.
618
+ const legacy = buildTreeRenderer(baseDir, rels);
619
+ for (const rel of scoped) {
620
+ const doc = tree.docs[rel], rendered = tree.render(rel, doc.body, doc.title), entry = oldMapping[rel];
621
+ const clean = entry?.localHash
622
+ ? entry.localHash === localStateHash(baseDir, rel, rendered)
623
+ : !!entry?.hash && (entry.hash === docHash(doc.title, rendered)
624
+ || entry.hash === docHash(legacy.docs[rel].title, legacy.render(rel, legacy.docs[rel].body, legacy.docs[rel].title))
625
+ || entry.hash === docHash(doc.title, tree.render(rel, doc.body, doc.title, 'panels'))
626
+ || entry.hash === docHash(doc.title, tree.render(rel, doc.body, doc.title, true))
627
+ || entry.hash === docHash(legacy.docs[rel].title, legacy.render(rel, legacy.docs[rel].body, legacy.docs[rel].title, 'panels'))
628
+ || entry.hash === docHash(legacy.docs[rel].title, legacy.render(rel, legacy.docs[rel].body, legacy.docs[rel].title, true)));
629
+ if (clean)
630
+ localClean.add(rel);
631
+ }
632
+ }
633
+ finally {
634
+ tree.dispose();
358
635
  }
359
- const target = positionals[1];
360
- if (!target) {
361
- console.error(red('✗ 가져올 페이지/폴더를 지정하세요.') + `\n 예) ${cyan('confluence-sync pull <pageId|url> [--out <dir>] [--children]')}\n 스페이스 전체: ${cyan('confluence-sync pull --space')}`);
362
- process.exit(1);
636
+ if (remoteOnly && incremental) {
637
+ ids = ids.filter(id => { const rel = knownById.get(id); const clean = !!rel && (newPaths.has(rel) || recoveringPaths.has(rel) || localClean.has(rel)) && !oldMapping[rel]?.pendingMove; if (!clean) {
638
+ skipped++;
639
+ console.log(`로컬 변경·확인 필요로 보존: ${rel ?? id}`);
640
+ } return clean; });
641
+ rootId = ids.includes(scope.id) ? scope.id : undefined;
642
+ if (!ids.length) {
643
+ console.log('완료 · 안전하게 가져올 원격 전용 변경이 없습니다. 로컬 변경은 유지합니다.');
644
+ return;
645
+ }
646
+ }
647
+ const stage = mkdtempSync(join(tmpdir(), 'csync-pull-'));
648
+ const stageOut = join(stage, relative(baseDir, outDir));
649
+ const ctx = {
650
+ client, remoteBaseUrl: env.baseUrl, missingAttachments: 0, incremental, expectedVersions,
651
+ obsidian: argv.includes('--obsidian'), spaceKey: env.spaceKey, written: [], pathById: new Map(), folderIds: new Map(),
652
+ versions: new Map(), parents: new Map(), assetOwners: new Map(), knownPaths: new Map([...knownById].map(([id, rel]) => [id, resolve(stage, rel)])),
653
+ stageBase: stage, baseDir, workspace, rootId, claimed: new Map(), visited: new Set(),
654
+ };
655
+ try {
656
+ if (discovered?.folders)
657
+ for (const folder of discovered.folders) {
658
+ if (![...newPaths, ...recoveringPaths].some(path => path.startsWith(folder.path)))
659
+ continue;
660
+ const prior = oldMapping[folder.path];
661
+ if (prior && prior.pageId !== folder.pageId)
662
+ throw new Error(`원격 폴더 경로 충돌: ${folder.path}`);
663
+ const path = resolve(stage, folder.path);
664
+ claim(ctx, path, folder.pageId, true);
665
+ mkdirSync(path, { recursive: true });
666
+ ctx.folderIds.set(path, folder.pageId);
667
+ }
668
+ // Reuse page IDs and local paths even when a remote title has changed.
669
+ if (rootId)
670
+ ctx.knownPaths.delete(rootId);
671
+ const counts = { pages: 0, folders: 0 };
672
+ for (const id of [...new Set(ids)]) {
673
+ if (rootId === id && oldMapping['README.md'] && oldMapping['README.md'].pageId !== id)
674
+ throw new Error('루트 README.md가 다른 페이지에 연결되어 있습니다.');
675
+ add(counts, await pullNode(ctx, id, stageOut));
676
+ }
677
+ // Link changed pages to unchanged local pages without rewriting or re-baselining them.
678
+ const linkPaths = new Map(ctx.pathById), referenceInfo = new Map();
679
+ if (incremental)
680
+ for (const [id, rel] of knownById) {
681
+ if (linkPaths.has(id) || !/\.md$/i.test(rel) || ignore.ignores(rel) || inNestedWorkspace(baseDir, rel))
682
+ continue;
683
+ const original = resolve(baseDir, rel), staged = resolve(stage, rel);
684
+ if (!within(baseDir, original) || !within(stage, staged) || !existsSync(original) || !lstatSync(original).isFile() || lstatSync(original).isSymbolicLink() || !within(realpathSync(baseDir), realpathSync(original)))
685
+ continue;
686
+ linkPaths.set(id, staged);
687
+ referenceInfo.set(staged, readDocInfo(original));
688
+ }
689
+ const { count: anchors, info } = anchorPass(ctx.written, linkPaths, referenceInfo);
690
+ const relinked = relinkPass(ctx.written, linkPaths, ctx.obsidian, info);
691
+ const stagedFiles = [];
692
+ const stagedDirs = [];
693
+ const gather = (dir) => {
694
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
695
+ const path = join(dir, entry.name);
696
+ if (entry.isDirectory()) {
697
+ stagedDirs.push(path);
698
+ gather(path);
699
+ }
700
+ else
701
+ stagedFiles.push(path);
702
+ }
703
+ };
704
+ gather(stage);
705
+ const stagedDocuments = new Set(ctx.written);
706
+ const checkDestination = (rel, directory) => {
707
+ const dest = resolve(baseDir, rel);
708
+ for (let path = dest; within(baseDir, path); path = dirname(path)) {
709
+ if (existsSync(path)) {
710
+ const info = lstatSync(path);
711
+ if (info.isSymbolicLink() || !within(realpathSync(baseDir), realpathSync(path)))
712
+ throw new Error(`심볼릭 링크 pull 대상은 지원하지 않습니다: ${rel}`);
713
+ if ((path === dest && !directory) ? !info.isFile() : !info.isDirectory())
714
+ throw new Error(`파일·폴더 경로 충돌: ${rel}`);
715
+ }
716
+ if (path === baseDir)
717
+ break;
718
+ }
719
+ };
720
+ for (const dir of stagedDirs)
721
+ checkDestination(relative(stage, dir), true);
722
+ // Validate all destinations before the first replacement, including attachment collisions.
723
+ for (const file of stagedFiles) {
724
+ const rel = relative(stage, file).split('\\').join('/');
725
+ const dest = resolve(baseDir, rel);
726
+ if (ignore.ignores(rel))
727
+ throw new Error(`제외된 pull 대상: ${rel}`);
728
+ checkDestination(rel, false);
729
+ if (stamp(dest) !== originals.get(dest))
730
+ throw new Error(`pull 도중 로컬 파일이 변경되었습니다: ${rel}`);
731
+ if (!existsSync(dest))
732
+ continue;
733
+ if (stagedDocuments.has(file)) {
734
+ const entry = oldMapping[rel];
735
+ if (recoveringPaths.has(rel)) {
736
+ if (!readFileSync(file).equals(readFileSync(dest)))
737
+ throw new Error(`받기 미완료 문서가 원격과 다릅니다: ${rel}. 로컬 수정은 유지합니다. 원격과 비교하고 필요한 변경을 보관한 뒤 다시 시도하세요.`);
738
+ continue;
739
+ }
740
+ if (!entry || (!localClean.has(rel) && !(restoreOverwrite && restorePaths?.includes(rel))) || entry.pendingMove)
741
+ throw new Error(`로컬 변경 충돌: ${rel}. 변경을 보관하거나 비교한 뒤 다시 pull하세요.`);
742
+ }
743
+ else if (stamp(file) !== stamp(dest)) {
744
+ const owner = ctx.assetOwners.get(file);
745
+ const ownerRel = owner ? knownById.get(owner) : undefined;
746
+ if (!ownerRel || (!localClean.has(ownerRel) && !(restoreOverwrite && restorePaths?.includes(ownerRel))))
747
+ throw new Error(`첨부 파일 충돌: ${rel}. 별도 --out으로 받아 비교하세요.`);
748
+ }
749
+ }
750
+ for (const path of [mappingPath, join(baseDir, '.env'), ...metadataFiles(baseDir)])
751
+ if (stamp(path) !== originals.get(path))
752
+ throw new Error('pull 도중 설정·매핑이 변경되었습니다.');
753
+ if (restorePaths)
754
+ validateRestorePaths(baseDir, restorePaths, restoreOverwrite);
755
+ const backup = mkdtempSync(join(tmpdir(), 'csync-pull-backup-'));
756
+ const applied = [], createdDirs = [];
757
+ let mapped, keepBackup = false;
758
+ const ensureDirectory = (dir) => {
759
+ if (existsSync(dir))
760
+ return;
761
+ const absent = [];
762
+ for (let p = dir; within(baseDir, p) && !existsSync(p); p = dirname(p))
763
+ absent.push(p);
764
+ mkdirSync(dir, { recursive: true });
765
+ createdDirs.push(...absent.reverse());
766
+ };
767
+ try {
768
+ ensureDirectory(baseDir);
769
+ for (const dir of stagedDirs)
770
+ ensureDirectory(join(baseDir, relative(stage, dir)));
771
+ for (const file of stagedFiles) {
772
+ const rel = relative(stage, file).split('\\').join('/'), dest = join(baseDir, relative(stage, file));
773
+ if (recoveringPaths.has(rel) && stagedDocuments.has(file))
774
+ continue;
775
+ if (restorePaths && existsSync(dest) && !stagedDocuments.has(file) && stamp(dest) === stamp(file))
776
+ continue;
777
+ if (stamp(dest) !== originals.get(dest))
778
+ throw new Error(`pull 도중 로컬 파일이 변경되었습니다: ${rel}`);
779
+ ensureDirectory(dirname(dest));
780
+ const record = { dest, hash: stamp(file) };
781
+ if (existsSync(dest)) {
782
+ record.prior = join(backup, String(applied.length));
783
+ copyFileSync(dest, record.prior);
784
+ }
785
+ applied.push(record);
786
+ try {
787
+ copyFileSync(file, dest, (restorePaths && !restoreOverwrite || newPaths.has(rel)) ? constants.COPYFILE_EXCL : 0);
788
+ }
789
+ catch (error) {
790
+ if (error.code === 'EEXIST' && (restorePaths && !restoreOverwrite || newPaths.has(rel))) {
791
+ applied.pop();
792
+ throw error;
793
+ }
794
+ record.hash = stamp(dest);
795
+ throw error;
796
+ }
797
+ }
798
+ const paths = new Map([...ctx.pathById].map(([id, path]) => [id, join(baseDir, relative(stage, path))]));
799
+ const folders = new Map([...ctx.folderIds].map(([path, id]) => [join(baseDir, relative(stage, path)), id]));
800
+ mapped = await mappingPass(baseDir, mappingPath, paths, folders, ctx.versions, ctx.parents, () => {
801
+ for (const file of ctx.written)
802
+ if (stamp(join(baseDir, relative(stage, file))) !== stamp(file))
803
+ throw new Error('기준 계산 중 로컬 문서가 변경되었습니다. 추가 수정은 보존합니다.');
804
+ for (const record of applied)
805
+ if (stamp(record.dest) !== record.hash)
806
+ throw new Error('기준 계산 중 로컬 문서·첨부가 변경되었습니다. 추가 수정은 보존합니다.');
807
+ });
808
+ }
809
+ catch (error) {
810
+ let preserved = 0;
811
+ for (const record of applied.reverse()) {
812
+ if (stamp(record.dest) !== record.hash) {
813
+ preserved++;
814
+ continue;
815
+ }
816
+ try {
817
+ if (record.prior)
818
+ copyFileSync(record.prior, record.dest);
819
+ else
820
+ rmSync(record.dest, { force: true });
821
+ }
822
+ catch {
823
+ keepBackup = true;
824
+ }
825
+ }
826
+ for (const dir of [...new Set(createdDirs)].reverse())
827
+ try {
828
+ rmdirSync(dir);
829
+ }
830
+ catch { /* Keep non-empty directories and concurrent local additions. */ }
831
+ console.log(preserved ? `[경고] 적용 실패로 저장을 취소했습니다. 도중 추가 수정된 ${preserved}개 파일은 보존했습니다.` : '적용 실패로 문서·첨부 저장을 취소했습니다. 기존 로컬 파일은 복원했습니다.');
832
+ if (keepBackup)
833
+ throw new Error(`${error.message} · 일부 파일 복원 실패. 복구용 백업: ${backup}`);
834
+ throw error;
835
+ }
836
+ finally {
837
+ if (!keepBackup)
838
+ rmSync(backup, { recursive: true, force: true });
839
+ }
840
+ console.log(`\n${green('완료')} 페이지 ${counts.pages}개, 폴더 ${counts.folders}개${incremental ? `, 건너뜀 ${skipped}개` : ''} 매핑 ${mapped}건 내부링크 ${relinked}개, 앵커 ${anchors}개`);
841
+ console.log(dim(` base: ${baseDir}`));
842
+ if (ctx.missingAttachments)
843
+ console.log(`[경고 요약] 원격 첨부 ${ctx.missingAttachments}개를 로컬로 저장하지 못했습니다. 문서는 저장했으며 원격 참조를 유지했습니다.`);
363
844
  }
364
- const contentId = parseContentId(target);
365
- if (!contentId) {
366
- console.error(red(`✗ 콘텐츠 ID 를 인식할 수 없습니다: ${target}`) + '\n 숫자 ID 또는 .../pages/<ID>/... · .../folder/<ID> URL 을 주세요.');
367
- process.exit(1);
845
+ finally {
846
+ rmSync(stage, { recursive: true, force: true });
368
847
  }
369
- console.log(`${dim('pull:')} #${contentId} ${dim('→')} ${cyan(outDir)}${withChildren ? dim(' (+children)') : ''}`);
370
- done(await pullNode(ctx, contentId, outDir));
371
848
  }