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