@logictan/dsh-config-manager 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-sync-flow:push/pull 编排(SyncEngine)。
|
|
3
|
+
*
|
|
4
|
+
* push:createAdapters 逐个 export(includeSecrets=true,真实值)→ 组装 SyncSnapshot →
|
|
5
|
+
* 本地散文件副本(复用 t2 layout,不写 ZIP)→ transport.upload → 更新 sync-state
|
|
6
|
+
* (每分区 hash + updatedAt + lastSyncAt + transport 绑定)。
|
|
7
|
+
* pull:transport.list/download 取远端快照 → 转临时标准 ZIP(buildManifest + checksums,
|
|
8
|
+
* 喂给 Importer)→ analyzeImport/createImportPlan 预览差异。
|
|
9
|
+
* 绝不直接写配置、绝不执行导入(executeImportPlan 由上层按用户确认驱动)。
|
|
10
|
+
*
|
|
11
|
+
* 明文同步(产品语义):同步通道是用户自有的私有通道,勾选即同步——
|
|
12
|
+
* 不加密、不脱敏、不做 diff/合并。快照按实际内容如实标注 manifest.security.containsSecrets。
|
|
13
|
+
*/
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs/promises';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { createAdapters } from '../adapters/index.js';
|
|
19
|
+
import { defaultSecretScanner } from '../core/exporter.js';
|
|
20
|
+
import { Importer } from '../core/importer.js';
|
|
21
|
+
import { isFileSection, SECTION_FILE_PREFIXES, SECTION_JSON_PATHS } from '../schema/config.js';
|
|
22
|
+
import { buildManifest, CHECKSUMS_FILE, MANIFEST_FILE } from '../schema/manifest.js';
|
|
23
|
+
import { CURRENT_SCHEMA_VERSION } from '../schema/versions.js';
|
|
24
|
+
import { buildChecksums } from '../utils/hashing.js';
|
|
25
|
+
import { stringifyJsonSafe } from '../utils/json.js';
|
|
26
|
+
import { writeZip } from '../utils/zip.js';
|
|
27
|
+
import { createSnapshotFs, joinFs } from './fs.js';
|
|
28
|
+
import { writeSnapshotToDir } from './layout.js';
|
|
29
|
+
import { hashSection, loadSyncState, saveSyncState } from './sync-state.js';
|
|
30
|
+
import { isEncryptedSections } from './transport.js';
|
|
31
|
+
import { msgOf, zhMsg } from '../core/messages.js';
|
|
32
|
+
import { createSnapshot } from '../core/backup.js';
|
|
33
|
+
import { rollback } from '../core/rollback.js';
|
|
34
|
+
import { FileSnapshotStore } from '../core/backup.js';
|
|
35
|
+
/**
|
|
36
|
+
* 远端快照保留数量上限:每次 push 上传成功后对远端裁剪,
|
|
37
|
+
* 保留最新 N 个(按 createdAt 升序的最末 N 个,含刚 push 的),更旧的逐个删除。
|
|
38
|
+
* 只按数量裁剪,不按时间窗口。删除失败只告警(进 push 的 warnings),不上抛阻断主流程。
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_REMOTE_SNAPSHOTS = 10;
|
|
41
|
+
export class SyncEngine {
|
|
42
|
+
ctx;
|
|
43
|
+
transport;
|
|
44
|
+
stateDir;
|
|
45
|
+
adapters;
|
|
46
|
+
importer;
|
|
47
|
+
now;
|
|
48
|
+
snapshotIdFn;
|
|
49
|
+
transportRef;
|
|
50
|
+
localSnapshotsDir;
|
|
51
|
+
rollbackSnapshotsDir;
|
|
52
|
+
zipDir;
|
|
53
|
+
exporterVersion;
|
|
54
|
+
fsx;
|
|
55
|
+
msg;
|
|
56
|
+
sections;
|
|
57
|
+
constructor(opts) {
|
|
58
|
+
if (opts.ctx === null || typeof opts.ctx !== 'object')
|
|
59
|
+
throw new Error(zhMsg('sync.missingCtx'));
|
|
60
|
+
if (opts.transport === null || typeof opts.transport !== 'object'
|
|
61
|
+
|| typeof opts.transport.upload !== 'function' || typeof opts.transport.list !== 'function'
|
|
62
|
+
|| typeof opts.transport.download !== 'function' || typeof opts.transport.delete !== 'function') {
|
|
63
|
+
throw new Error(zhMsg('sync.missingTransport'));
|
|
64
|
+
}
|
|
65
|
+
if (typeof opts.stateDir !== 'string' || opts.stateDir.length === 0) {
|
|
66
|
+
throw new Error(zhMsg('sync.missingStateDir'));
|
|
67
|
+
}
|
|
68
|
+
this.ctx = opts.ctx;
|
|
69
|
+
this.transport = opts.transport;
|
|
70
|
+
this.stateDir = opts.stateDir;
|
|
71
|
+
this.adapters = opts.adapters ?? createAdapters();
|
|
72
|
+
this.importer = opts.importer;
|
|
73
|
+
this.now = opts.now ?? (() => new Date());
|
|
74
|
+
this.snapshotIdFn = opts.snapshotId ?? (() => `sync-${crypto.randomUUID()}`);
|
|
75
|
+
this.transportRef = opts.transportRef ?? '';
|
|
76
|
+
this.localSnapshotsDir = opts.localSnapshotsDir;
|
|
77
|
+
this.rollbackSnapshotsDir = opts.rollbackSnapshotsDir ?? path.join(opts.stateDir, 'snapshots');
|
|
78
|
+
this.zipDir = opts.zipDir ?? os.tmpdir();
|
|
79
|
+
this.exporterVersion = opts.exporterVersion ?? '0.1.0';
|
|
80
|
+
this.fsx = opts.fsx ?? createSnapshotFs();
|
|
81
|
+
this.msg = opts.msg ?? msgOf(opts.ctx);
|
|
82
|
+
this.sections = opts.sections !== undefined && opts.sections.length > 0 ? [...opts.sections] : undefined;
|
|
83
|
+
}
|
|
84
|
+
/** 参与同步的分区全集:构造注入 sections(同步范围)时按注入范围过滤 ——
|
|
85
|
+
* 自动同步等后台流程 push/pull 全链路复用用户选择;手动请求仍可用 push(opts.sections) 覆盖。 */
|
|
86
|
+
syncAdapters() {
|
|
87
|
+
if (this.sections === undefined)
|
|
88
|
+
return this.adapters;
|
|
89
|
+
const wanted = new Set(this.sections);
|
|
90
|
+
return this.adapters.filter((a) => wanted.has(a.id));
|
|
91
|
+
}
|
|
92
|
+
/** 未显式传 sections 时的默认范围。
|
|
93
|
+
*
|
|
94
|
+
* - 构造注入 sections(高级模式:用户已在 UI 勾选并持久化)→ 该选择**就是**默认范围,
|
|
95
|
+
* 不再按 defaultIncluded 收窄(否则高级模式勾选的 sessions/pluginFiles 会被悄悄剔除);
|
|
96
|
+
* - 未注入(默认模式「快速导出」)→ 只取推荐分区 defaultIncluded。
|
|
97
|
+
*
|
|
98
|
+
* defaultIncluded=false 的分区(sessions 含历史会话明文、pluginFiles)语义是「用户显式
|
|
99
|
+
* 勾选才同步」;把它们并进默认模式的范围会绕过 UI 勾选与「将同步 N 个推荐分区」计数,
|
|
100
|
+
* 把敏感内容静默推到远端通道。 */
|
|
101
|
+
defaultTargets() {
|
|
102
|
+
if (this.sections !== undefined)
|
|
103
|
+
return this.syncAdapters();
|
|
104
|
+
return this.adapters.filter((a) => a.defaultIncluded);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* push 候选 adapter:
|
|
108
|
+
* - sections 缺省/空 → 全部推荐分区(defaultIncluded);
|
|
109
|
+
* - sections 显式给出 → 从**全部已挂载分区**取命中项(显式勾选可触达 defaultIncluded=false
|
|
110
|
+
* 的分区,这正是「默认关闭」的含义);未知分区 → 警告跳过(不静默,用户能看见自己勾了哪个无效项)。
|
|
111
|
+
*/
|
|
112
|
+
pushTargets(sections, warnings) {
|
|
113
|
+
if (sections === undefined || sections.length === 0)
|
|
114
|
+
return this.defaultTargets();
|
|
115
|
+
// 显式 sections 从**全部已挂载分区**解析,而非 syncAdapters():后者受构造注入范围限制,
|
|
116
|
+
// 会让「手动 push(opts.sections) 覆盖持久化勾选」失效(未在注入范围内的合法分区被误判为未知)。
|
|
117
|
+
// 与路由侧 extractSyncSections(knownSyncSectionIds=全部 adapter) 的校验口径一致。
|
|
118
|
+
const available = this.adapters;
|
|
119
|
+
const byId = new Map(available.map((a) => [a.id, a]));
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const id of sections) {
|
|
122
|
+
const adapter = byId.get(id);
|
|
123
|
+
if (adapter === undefined) {
|
|
124
|
+
warnings.push(this.msg('sync.unknownSection', { section: id }));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
out.push(adapter);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 快照读取准备(download 后、使用前):
|
|
133
|
+
* 加密快照(manifest.encrypted)→ 明确拒绝:同步通道已不再产生加密快照,
|
|
134
|
+
* 旧加密快照无法用当前引擎读取(拒绝静默当明文处理)。
|
|
135
|
+
*/
|
|
136
|
+
async prepareSnapshot(snapshot) {
|
|
137
|
+
if (snapshot.manifest.encrypted === true || isEncryptedSections(snapshot.sections)) {
|
|
138
|
+
throw new Error(this.msg('sync.legacyEncryptedSnapshot', { id: snapshot.id }));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* push:导出分区(真实值)→ 组装快照 → 本地散文件副本 → transport.upload → 更新 sync-state。
|
|
143
|
+
* 单项分区导出失败只告警跳过(§34.17),全部失败才整体失败。
|
|
144
|
+
* opts.sections:指定仅同步这些分区(自定义模式);缺省 = 全部推荐分区。
|
|
145
|
+
*/
|
|
146
|
+
async push(opts = {}) {
|
|
147
|
+
const warnings = [];
|
|
148
|
+
const plainSections = {};
|
|
149
|
+
const targets = this.pushTargets(opts.sections, warnings);
|
|
150
|
+
for (const adapter of targets) {
|
|
151
|
+
let section;
|
|
152
|
+
try {
|
|
153
|
+
section = await adapter.export(this.ctx, { includeSecrets: true });
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
warnings.push(this.msg('sync.sectionFailed', { adapter: adapter.id, reason: err instanceof Error ? err.message : String(err) }));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
warnings.push(...section.warnings);
|
|
160
|
+
plainSections[adapter.id] = section.data;
|
|
161
|
+
}
|
|
162
|
+
if (Object.keys(plainSections).length === 0) {
|
|
163
|
+
return { ok: false, snapshotId: '', sections: [], warnings, message: this.msg('sync.noSections') };
|
|
164
|
+
}
|
|
165
|
+
const nowIso = this.now().toISOString();
|
|
166
|
+
const id = opts.snapshotId ?? this.snapshotIdFn();
|
|
167
|
+
const containsSecrets = sectionsCarrySecrets(plainSections);
|
|
168
|
+
const snapshot = {
|
|
169
|
+
id,
|
|
170
|
+
createdAt: nowIso,
|
|
171
|
+
manifest: {
|
|
172
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
173
|
+
dshVersion: this.ctx.dshVersion,
|
|
174
|
+
platform: this.ctx.platform,
|
|
175
|
+
sectionIds: Object.keys(plainSections),
|
|
176
|
+
containsSecrets,
|
|
177
|
+
// 记录触发通道(git/webdav),供同步历史展示「由哪个通道触发」
|
|
178
|
+
transport: this.transport.type,
|
|
179
|
+
},
|
|
180
|
+
sections: plainSections,
|
|
181
|
+
};
|
|
182
|
+
// ① 本地散文件快照副本(审计;复用 t2 layout,不写 ZIP)
|
|
183
|
+
if (this.localSnapshotsDir !== undefined) {
|
|
184
|
+
await writeSnapshotToDir(snapshot, joinFs(this.localSnapshotsDir, id), this.fsx);
|
|
185
|
+
}
|
|
186
|
+
// ② 上传远端(传输通道负责散文件落盘 + 提交推送)
|
|
187
|
+
await this.transport.upload(snapshot);
|
|
188
|
+
// ②·1 裁剪远端快照:保留最新 MAX_REMOTE_SNAPSHOTS 个(含刚 push 的)。
|
|
189
|
+
// 删除失败只告警(进 warnings),不上抛 —— 不阻断 push 主流程。
|
|
190
|
+
await this.pruneRemoteSnapshots(id, warnings);
|
|
191
|
+
// ③ 记录基线:写 sync-state(lastSnapshotId + 每分区 hash/updatedAt + lastSyncAt + transport)
|
|
192
|
+
await this.recordBaseline(id, plainSections, nowIso);
|
|
193
|
+
return { ok: true, snapshotId: id, sections: Object.keys(plainSections), warnings };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* push 前只读预览「将推送什么」—— 零写入、零远端变更:
|
|
197
|
+
* - 导出目标分区(与 push 同口径:sections 过滤);
|
|
198
|
+
* - 逐分区相对上次基线(sync-state.sections hash)的 changed 标记;
|
|
199
|
+
* - 远端现有快照数(list 只读;首次推送 = 0)。
|
|
200
|
+
* 任何失败都不写任何内容;预览只是 push 的「确认前说明书」。
|
|
201
|
+
*/
|
|
202
|
+
async previewPush(opts = {}) {
|
|
203
|
+
const warnings = [];
|
|
204
|
+
const targets = this.pushTargets(opts.sections, warnings);
|
|
205
|
+
const plainSections = {};
|
|
206
|
+
const counts = {};
|
|
207
|
+
for (const adapter of targets) {
|
|
208
|
+
let section;
|
|
209
|
+
try {
|
|
210
|
+
section = await adapter.export(this.ctx, { includeSecrets: true });
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
warnings.push(this.msg('sync.sectionFailed', { adapter: adapter.id, reason: err instanceof Error ? err.message : String(err) }));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
plainSections[adapter.id] = section.data;
|
|
217
|
+
counts[adapter.id] = section.counts ? Object.values(section.counts).reduce((a, b) => a + b, 0) : 0;
|
|
218
|
+
}
|
|
219
|
+
if (Object.keys(plainSections).length === 0) {
|
|
220
|
+
return { ok: false, sections: [], remoteSnapshotCount: 0, message: this.msg('sync.noSections') };
|
|
221
|
+
}
|
|
222
|
+
// 基线对比:sync-state.sections 存每分区 hash;缺基线分区 → 视为新增
|
|
223
|
+
let baselineHashes = {};
|
|
224
|
+
try {
|
|
225
|
+
const state = await loadSyncState(this.stateDir, this.fsx, this.msg);
|
|
226
|
+
baselineHashes = state.sections;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
baselineHashes = {};
|
|
230
|
+
}
|
|
231
|
+
const sections = Object.keys(plainSections).map((sid) => {
|
|
232
|
+
const id = sid;
|
|
233
|
+
const changed = baselineHashes[id] === undefined || baselineHashes[id] !== hashSection(plainSections[id]);
|
|
234
|
+
return { section: id, count: counts[id] ?? 0, changed };
|
|
235
|
+
});
|
|
236
|
+
let remoteSnapshotCount = 0;
|
|
237
|
+
try {
|
|
238
|
+
const metas = await this.transport.list();
|
|
239
|
+
remoteSnapshotCount = metas.length;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// list 失败只影响展示(首次推送提示),不阻断预览
|
|
243
|
+
}
|
|
244
|
+
return { ok: true, sections, remoteSnapshotCount, message: warnings.length > 0 ? warnings.join('; ') : undefined };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 裁剪远端快照:调用 transport.list() 获取全部快照(按 createdAt 升序),
|
|
248
|
+
* 保留最新 MAX_REMOTE_SNAPSHOTS 个(含刚 push 的 pushedId),对更旧的逐个调用 transport.delete()。
|
|
249
|
+
* 只按数量裁剪,不按时间窗口。
|
|
250
|
+
*
|
|
251
|
+
* 失败语义:list/delete 失败均只 push 进 warnings,不上抛 —— 裁剪是「尽力而为」的后台整理,
|
|
252
|
+
* 绝不影响 push 主流程的成功与否(§34.17 分区级告警同款语义)。
|
|
253
|
+
*/
|
|
254
|
+
async pruneRemoteSnapshots(pushedId, warnings) {
|
|
255
|
+
let metas;
|
|
256
|
+
try {
|
|
257
|
+
metas = await this.transport.list();
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
261
|
+
warnings.push(this.msg('sync.pruneListFailed', { reason }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (metas.length <= MAX_REMOTE_SNAPSHOTS)
|
|
265
|
+
return;
|
|
266
|
+
// 升序最末 N 个保留;刚 push 的快照(createdAt 最新)必在其中,防御性也把它计入保留集
|
|
267
|
+
const keep = new Set(metas.slice(-MAX_REMOTE_SNAPSHOTS).map((m) => m.id));
|
|
268
|
+
keep.add(pushedId);
|
|
269
|
+
for (const m of metas) {
|
|
270
|
+
if (keep.has(m.id))
|
|
271
|
+
continue;
|
|
272
|
+
try {
|
|
273
|
+
await this.transport.delete(m.id);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
277
|
+
warnings.push(this.msg('sync.pruneDeleteFailed', { id: m.id, reason }));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* pull:拉取远端最新(或指定)快照 → 转临时标准 ZIP →
|
|
283
|
+
* 复用 Importer 预览流程(analyzeImport/createImportPlan)产出差异报告。
|
|
284
|
+
* 绝不直接写配置、绝不执行导入;执行由上层按用户确认后走 Importer.executeImportPlan。
|
|
285
|
+
*/
|
|
286
|
+
async pull(opts = {}) {
|
|
287
|
+
if (!this.importer) {
|
|
288
|
+
throw new Error(this.msg('sync.missingImporter'));
|
|
289
|
+
}
|
|
290
|
+
const metas = await this.transport.list();
|
|
291
|
+
if (metas.length === 0) {
|
|
292
|
+
return { ok: true, snapshotId: '', changes: [], needsReview: false, message: this.msg('sync.remoteEmpty') };
|
|
293
|
+
}
|
|
294
|
+
const targetId = opts.snapshotId ?? metas[metas.length - 1].id; // list 按 createdAt 升序 → 最新
|
|
295
|
+
const snapshot = await this.transport.download(targetId);
|
|
296
|
+
await this.prepareSnapshot(snapshot);
|
|
297
|
+
const knownIds = new Set(this.syncAdapters().map((a) => a.id));
|
|
298
|
+
const zipPath = await this.snapshotToZip(snapshot, knownIds);
|
|
299
|
+
try {
|
|
300
|
+
const analysis = await this.importer.analyzeImport(zipPath);
|
|
301
|
+
const plan = await this.importer.createImportPlan(zipPath, {
|
|
302
|
+
strategy: opts.strategy ?? 'replace',
|
|
303
|
+
resolutions: {},
|
|
304
|
+
pathMappings: [],
|
|
305
|
+
});
|
|
306
|
+
const changes = plan.items.map((i) => ({
|
|
307
|
+
id: i.id,
|
|
308
|
+
adapter: i.adapter,
|
|
309
|
+
kind: i.kind,
|
|
310
|
+
description: i.description,
|
|
311
|
+
severity: i.severity,
|
|
312
|
+
}));
|
|
313
|
+
const needsReview = plan.items.some((i) => i.kind === 'Conflict' || i.kind === 'MissingSecret' || i.kind === 'MissingDependency'
|
|
314
|
+
|| i.kind === 'Error')
|
|
315
|
+
|| analysis.pathIssues.length > 0;
|
|
316
|
+
const message = changes.length === 0
|
|
317
|
+
? this.msg('sync.unchanged')
|
|
318
|
+
: this.msg('sync.changesSummary', { compatibility: analysis.compatibility, count: String(changes.length) });
|
|
319
|
+
return { ok: analysis.valid, snapshotId: targetId, changes, needsReview, message };
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
await fs.rm(path.dirname(zipPath), { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** 列出远端已有快照(按 createdAt 升序)—— 供「选择历史快照」下拉。 */
|
|
326
|
+
async listSnapshots() {
|
|
327
|
+
return this.transport.list();
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 远端是否出现比本地共同祖先(sync-state.lastSnapshotId)更新的快照(§3.2「检测到远端新快照」)。
|
|
331
|
+
* - 远端为空 → false(无物可拉);
|
|
332
|
+
* - 本地从未同步(lastSnapshotId='')且远端非空 → true(首次可拉);
|
|
333
|
+
* - 否则比较远端最新快照 id 与 lastSnapshotId。
|
|
334
|
+
* 只读远端列表(transport.list),不做下载/合并。
|
|
335
|
+
*/
|
|
336
|
+
async hasNewRemoteSnapshot() {
|
|
337
|
+
const metas = await this.transport.list();
|
|
338
|
+
if (metas.length === 0)
|
|
339
|
+
return false;
|
|
340
|
+
const latestId = metas[metas.length - 1].id; // list 按 createdAt 升序 → 最新在末
|
|
341
|
+
const state = await loadSyncState(this.stateDir, this.fsx, this.msg);
|
|
342
|
+
if (state.lastSnapshotId === '')
|
|
343
|
+
return true;
|
|
344
|
+
return latestId !== state.lastSnapshotId;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 本地配置当前内容与上次基线(sync-state.sections hash)相比是否有变化(§3.1 上传「看变化」)。
|
|
348
|
+
* - 从未同步(sync-state.sections 为空)→ true;
|
|
349
|
+
* - 任一分区当前导出 hash ≠ 基线 hash → true;
|
|
350
|
+
* - 全部一致 → false(无本地改动,不上传)。
|
|
351
|
+
* 只读本地导出 + sync-state,不写任何东西、不碰远端。
|
|
352
|
+
*/
|
|
353
|
+
async hasLocalChanges() {
|
|
354
|
+
const state = await loadSyncState(this.stateDir, this.fsx, this.msg);
|
|
355
|
+
if (Object.keys(state.sections).length === 0)
|
|
356
|
+
return true;
|
|
357
|
+
// 必须与 push 的默认范围同源:基线只记录 push 实际上传的分区,
|
|
358
|
+
// 若此处遍历更大的集合,未上传分区会因「基线缺该分区」恒判为有改动。
|
|
359
|
+
for (const adapter of this.defaultTargets()) {
|
|
360
|
+
let section;
|
|
361
|
+
try {
|
|
362
|
+
section = await adapter.export(this.ctx, { includeSecrets: true });
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
continue; // 单项导出失败不影响判定(与 push 单项跳过语义一致)
|
|
366
|
+
}
|
|
367
|
+
const recorded = state.sections[adapter.id];
|
|
368
|
+
if (recorded === undefined)
|
|
369
|
+
return true; // 基线缺该分区 → 视为有变化
|
|
370
|
+
if (recorded.hash !== hashSection(section.data))
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 一键同步预览:拉取远端(最新或指定历史快照)→ 转临时 ZIP → Importer 分析出计划。
|
|
377
|
+
* 与 pull 的区别:临时 ZIP **不清理**(由调用方 / 会话持有,供 apply-items 复用),
|
|
378
|
+
* 并返回完整 plan/analysis/snapshotId 供会话登记。
|
|
379
|
+
* 调用方负责在会话消费或取消后清理 zipPath 所在目录。
|
|
380
|
+
*/
|
|
381
|
+
async preview(opts = {}) {
|
|
382
|
+
if (!this.importer) {
|
|
383
|
+
throw new Error(this.msg('sync.missingImporter'));
|
|
384
|
+
}
|
|
385
|
+
const metas = await this.transport.list();
|
|
386
|
+
if (metas.length === 0) {
|
|
387
|
+
return { ok: false, zipPath: '', plan: null, analysis: null, snapshotId: '', message: this.msg('sync.remoteEmpty') };
|
|
388
|
+
}
|
|
389
|
+
const targetId = opts.snapshotId ?? metas[metas.length - 1].id;
|
|
390
|
+
const snapshot = await this.transport.download(targetId);
|
|
391
|
+
await this.prepareSnapshot(snapshot);
|
|
392
|
+
const knownIds = new Set(this.syncAdapters().map((a) => a.id));
|
|
393
|
+
const zipPath = await this.snapshotToZip(snapshot, knownIds);
|
|
394
|
+
const analysis = await this.importer.analyzeImport(zipPath);
|
|
395
|
+
const plan = await this.importer.createImportPlan(zipPath, {
|
|
396
|
+
strategy: opts.strategy ?? 'replace',
|
|
397
|
+
resolutions: {},
|
|
398
|
+
pathMappings: [],
|
|
399
|
+
});
|
|
400
|
+
return { ok: analysis.valid, zipPath, plan, analysis, snapshotId: targetId, message: undefined };
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* 记录基线:写本地快照副本 + 更新 sync-state
|
|
404
|
+
* (lastSnapshotId、每分区 hash/updatedAt、lastSyncAt、transport)。
|
|
405
|
+
* 通常由 push() 在上传成功后调用,也可被上层(apply-items 完成后)显式调用。
|
|
406
|
+
*/
|
|
407
|
+
async recordBaseline(snapshotId, sections, nowIso) {
|
|
408
|
+
const ts = nowIso ?? this.now().toISOString();
|
|
409
|
+
if (this.localSnapshotsDir !== undefined && !isEncryptedSections(sections)) {
|
|
410
|
+
const plain = sections;
|
|
411
|
+
const snapshot = {
|
|
412
|
+
id: snapshotId,
|
|
413
|
+
createdAt: ts,
|
|
414
|
+
manifest: {
|
|
415
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
416
|
+
dshVersion: this.ctx.dshVersion,
|
|
417
|
+
platform: this.ctx.platform,
|
|
418
|
+
sectionIds: Object.keys(plain),
|
|
419
|
+
containsSecrets: sectionsCarrySecrets(plain),
|
|
420
|
+
// 记录触发通道(git/webdav),供同步历史展示「由哪个通道触发」
|
|
421
|
+
transport: this.transport.type,
|
|
422
|
+
},
|
|
423
|
+
sections: plain,
|
|
424
|
+
};
|
|
425
|
+
await writeSnapshotToDir(snapshot, joinFs(this.localSnapshotsDir, snapshotId), this.fsx);
|
|
426
|
+
}
|
|
427
|
+
const state = await loadSyncState(this.stateDir, this.fsx, this.msg);
|
|
428
|
+
state.lastSnapshotId = snapshotId;
|
|
429
|
+
state.lastSyncAt = ts;
|
|
430
|
+
state.transport = { type: this.transport.type, ref: this.transportRef };
|
|
431
|
+
for (const [sid, data] of Object.entries(sections)) {
|
|
432
|
+
state.sections[sid] = { hash: hashSection(data), updatedAt: ts };
|
|
433
|
+
}
|
|
434
|
+
await saveSyncState(this.stateDir, state, this.fsx);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* applyItems:按用户对差异项的逐项决策执行导入(§3.4/§5.3)。
|
|
438
|
+
*
|
|
439
|
+
* 接收「会话级临时 ZIP + 子计划」,直接执行
|
|
440
|
+
* (backup.createSnapshot 兜底 → importer.executeImportPlan → 成功 recordBaseline / 失败 rollback)。
|
|
441
|
+
*
|
|
442
|
+
* @param zipPath 会话级临时标准 ZIP(由 sync 预览生成,包含采纳项的 payload)
|
|
443
|
+
* @param subPlan 子计划(仅含采纳项的 ImportPlan;globalStrategy/pathMappings/needsRestart 沿用会话 plan)
|
|
444
|
+
* @param opts 执行选项(onItem 进度回调)
|
|
445
|
+
* @returns ApplyItemsReport(ok/applied/restoreId/rolledBack/warnings/result)
|
|
446
|
+
*/
|
|
447
|
+
async applyItems(zipPath, subPlan, opts = {}) {
|
|
448
|
+
if (!this.importer) {
|
|
449
|
+
throw new Error('applyItems: SyncEngine 缺少 importer(需在 options 中注入)');
|
|
450
|
+
}
|
|
451
|
+
if (subPlan.items.length === 0) {
|
|
452
|
+
return { ok: true, applied: [], restoreId: '', rolledBack: false, warnings: [], failed: [], result: null };
|
|
453
|
+
}
|
|
454
|
+
// 兜底快照(拿到 restoreId 给 UI 一键回滚用)
|
|
455
|
+
const store = new FileSnapshotStore({ dir: this.rollbackSnapshotsDir });
|
|
456
|
+
let snapshot;
|
|
457
|
+
try {
|
|
458
|
+
snapshot = await createSnapshot({
|
|
459
|
+
ctx: this.ctx,
|
|
460
|
+
plan: subPlan,
|
|
461
|
+
sourceZip: zipPath,
|
|
462
|
+
store,
|
|
463
|
+
adapters: this.adapters,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
catch (backupErr) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
applied: [],
|
|
470
|
+
restoreId: '',
|
|
471
|
+
rolledBack: false,
|
|
472
|
+
warnings: [`应用前快照失败:${backupErr instanceof Error ? backupErr.message : String(backupErr)}`],
|
|
473
|
+
failed: subPlan.items.map((i) => ({ itemId: i.id })),
|
|
474
|
+
result: null,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// 真正执行:Importer.executeImportPlan(confirm:true + rollbackOnError:true → 任一失败整体回滚)
|
|
478
|
+
const result = await this.importer.executeImportPlan(zipPath, subPlan, {
|
|
479
|
+
confirm: true,
|
|
480
|
+
rollbackOnError: true,
|
|
481
|
+
secretInputs: undefined,
|
|
482
|
+
decryptedCredentials: undefined,
|
|
483
|
+
onItem: opts.onItem,
|
|
484
|
+
snapshotBinding: opts.snapshotBinding,
|
|
485
|
+
});
|
|
486
|
+
if (!result.ok) {
|
|
487
|
+
// executeImportPlan 已内部回滚(rollbackOnError);这里再显式 rollback 兜底(幂等)
|
|
488
|
+
try {
|
|
489
|
+
await rollback({ ctx: this.ctx, snapshot, store, adapters: this.adapters });
|
|
490
|
+
}
|
|
491
|
+
catch { /* noop */ }
|
|
492
|
+
return {
|
|
493
|
+
ok: false,
|
|
494
|
+
applied: [],
|
|
495
|
+
restoreId: snapshot.id,
|
|
496
|
+
rolledBack: true,
|
|
497
|
+
warnings: result.warnings ?? [],
|
|
498
|
+
failed: result.executed.filter((e) => e.status === 'failed').map((e) => ({ itemId: e.itemId, message: e.message })),
|
|
499
|
+
result,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// 成功:recordBaseline 更新基线(应用后的快照)
|
|
503
|
+
const appliedIds = [...new Set(subPlan.items.map((i) => i.adapter))];
|
|
504
|
+
const mergedSections = {};
|
|
505
|
+
for (const adapter of this.syncAdapters()) {
|
|
506
|
+
if (!appliedIds.includes(adapter.id))
|
|
507
|
+
continue;
|
|
508
|
+
try {
|
|
509
|
+
const section = await adapter.export(this.ctx, { includeSecrets: true });
|
|
510
|
+
mergedSections[adapter.id] = section.data;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
// 单个分区导出失败不拖垮 recordBaseline(已应用的分区数据从 subPlan 兜底)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// 基线指向「刚应用的远端快照」;缺省才回退本地新 id(见 opts.snapshotId 注释)
|
|
517
|
+
const snapshotId = opts.snapshotId !== undefined && opts.snapshotId !== '' ? opts.snapshotId : this.snapshotIdFn();
|
|
518
|
+
await this.recordBaseline(snapshotId, mergedSections);
|
|
519
|
+
return {
|
|
520
|
+
ok: true,
|
|
521
|
+
applied: appliedIds,
|
|
522
|
+
skipped: subPlan.items.filter((i) => i.kind === 'Skip').map((i) => i.id),
|
|
523
|
+
restoreId: snapshot.id,
|
|
524
|
+
rolledBack: false,
|
|
525
|
+
warnings: result.warnings ?? [],
|
|
526
|
+
failed: [],
|
|
527
|
+
needsRestart: result.needsRestart,
|
|
528
|
+
result,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
/** 散文件快照 → 标准导出 ZIP(临时目录,用完即删):buildManifest + checksums + 平铺分区 */
|
|
532
|
+
async snapshotToZip(snapshot, knownIds) {
|
|
533
|
+
// 防御:加密快照必须已由调用方拒绝(prepareSnapshot);此处不处理密文载荷
|
|
534
|
+
if (isEncryptedSections(snapshot.sections)) {
|
|
535
|
+
throw new Error('快照仍为加密载荷,无法转 ZIP');
|
|
536
|
+
}
|
|
537
|
+
const entries = [];
|
|
538
|
+
const sectionFlags = {};
|
|
539
|
+
for (const sid of knownIds) {
|
|
540
|
+
const data = snapshot.sections[sid];
|
|
541
|
+
if (data === undefined)
|
|
542
|
+
continue;
|
|
543
|
+
sectionFlags[sid] = true;
|
|
544
|
+
if (isFileSection(sid)) {
|
|
545
|
+
const prefix = SECTION_FILE_PREFIXES[sid];
|
|
546
|
+
const files = data.files ?? [];
|
|
547
|
+
for (const f of files) {
|
|
548
|
+
entries.push({ name: `${prefix}${f.relativePath}`, data: f.data });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
const jsonPath = SECTION_JSON_PATHS[sid];
|
|
553
|
+
if (jsonPath === undefined)
|
|
554
|
+
continue;
|
|
555
|
+
entries.push({ name: jsonPath, data: Buffer.from(stringifyJsonSafe(data, { space: 2 }), 'utf8') });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const manifest = buildManifest({
|
|
559
|
+
exporterVersion: this.exporterVersion,
|
|
560
|
+
dshVersion: snapshot.manifest.dshVersion,
|
|
561
|
+
platform: snapshot.manifest.platform,
|
|
562
|
+
arch: 'unknown', // ManifestSummary 不含 arch;仅影响展示,不影响导入决策
|
|
563
|
+
sections: sectionFlags,
|
|
564
|
+
containsSecrets: snapshot.manifest.containsSecrets === true,
|
|
565
|
+
encrypted: false,
|
|
566
|
+
encryption: null,
|
|
567
|
+
exportedAt: snapshot.createdAt,
|
|
568
|
+
});
|
|
569
|
+
// checksums:覆盖除 manifest/checksums 外全部条目(与 exporter 一致)
|
|
570
|
+
const contentEntries = entries.filter((e) => e.name !== MANIFEST_FILE && e.name !== CHECKSUMS_FILE);
|
|
571
|
+
entries.push({
|
|
572
|
+
name: CHECKSUMS_FILE,
|
|
573
|
+
data: Buffer.from(stringifyJsonSafe(buildChecksums(contentEntries), { space: 2 }), 'utf8'),
|
|
574
|
+
});
|
|
575
|
+
entries.push({ name: MANIFEST_FILE, data: Buffer.from(stringifyJsonSafe(manifest, { space: 2 }), 'utf8') });
|
|
576
|
+
const dir = await fs.mkdtemp(path.join(this.zipDir, 'dsh-sync-pull-'));
|
|
577
|
+
const zipPath = path.join(dir, 'snapshot.zip');
|
|
578
|
+
await writeZip(zipPath, entries);
|
|
579
|
+
return zipPath;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* 快照是否实际携带凭据值:复用 exporter 的敏感字段扫描器(单一真源,不另造字段表)。
|
|
584
|
+
* 用于如实标注 manifest.security.containsSecrets —— 标注必须与内容一致,
|
|
585
|
+
* 否则「含明文却标注 false」会让下游按「无秘密」处理。
|
|
586
|
+
*/
|
|
587
|
+
export function sectionsCarrySecrets(sections) {
|
|
588
|
+
const scanner = defaultSecretScanner();
|
|
589
|
+
return Object.values(sections).some((data) => scanner.scanAndRedact(data).hits.length > 0);
|
|
590
|
+
}
|
|
591
|
+
//# sourceMappingURL=sync-engine.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SectionId } from '../schema/types.ts';
|
|
2
|
+
import type { SyncTransportType } from './sync-config.ts';
|
|
3
|
+
export declare const SYNC_HISTORY_FILE = "sync-history.json";
|
|
4
|
+
export declare const SYNC_HISTORY_SCHEMA_VERSION = 1;
|
|
5
|
+
/** 自动同步历史保留上限 */
|
|
6
|
+
export declare const AUTOSYNC_HISTORY_KEEP = 200;
|
|
7
|
+
/** 自动同步执行记录(§3.7 AutosyncHistoryEntry) */
|
|
8
|
+
export interface AutosyncHistoryEntry {
|
|
9
|
+
/** 触发该次运行的同步通道(git / webdav;旧记录缺省 undefined) */
|
|
10
|
+
transport?: SyncTransportType;
|
|
11
|
+
/** 执行方向 */
|
|
12
|
+
direction: 'pull' | 'push' | 'both';
|
|
13
|
+
status: 'success' | 'skipped' | 'failed' | 'partial';
|
|
14
|
+
/** 跳过原因(冲突项 / 缺失依赖 / Install / 错误 / 无远端 / 网络) */
|
|
15
|
+
skipReason?: string;
|
|
16
|
+
/** 被跳过的冲突分区 id(冲突跳过时列出) */
|
|
17
|
+
conflictedSections?: SectionId[];
|
|
18
|
+
/** 本次自动合并实际写入的分区 */
|
|
19
|
+
appliedSections?: SectionId[];
|
|
20
|
+
/** 本次 push 产生的快照 id(direction 含 push 时) */
|
|
21
|
+
pushedSnapshotId?: string;
|
|
22
|
+
/** 本次 pull 来源快照 id */
|
|
23
|
+
pulledSnapshotId?: string;
|
|
24
|
+
/** failed 时的错误摘要(脱敏) */
|
|
25
|
+
error?: string;
|
|
26
|
+
/** 连续失败 3 次通知时间(ISO-8601 UTC) */
|
|
27
|
+
notifiedAt?: string;
|
|
28
|
+
/** 本次触发时的连续失败计数 */
|
|
29
|
+
failureCountAtRun: number;
|
|
30
|
+
/** 记录创建时间(ISO-8601 UTC) */
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
/** sync-history.json 文件结构 */
|
|
34
|
+
export interface SyncHistoryFile {
|
|
35
|
+
schemaVersion: number;
|
|
36
|
+
autosyncEntries: AutosyncHistoryEntry[];
|
|
37
|
+
updatedAt: string;
|
|
38
|
+
}
|
|
39
|
+
/** 读取同步历史;文件不存在 → 空列表(schemaVersion=1)。损坏 JSON 抛错。 */
|
|
40
|
+
export declare function readSyncHistory(dir: string): Promise<SyncHistoryFile>;
|
|
41
|
+
/** 追加一条自动同步执行记录(升序),裁剪到 AUTOSYNC_HISTORY_KEEP 条,原子写。 */
|
|
42
|
+
export declare function appendAutosyncEntry(dir: string, entry: AutosyncHistoryEntry, now?: () => Date): Promise<void>;
|