@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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-autosync:自动同步执行记录持久化(sync-history.json)。
|
|
3
|
+
*
|
|
4
|
+
* 与 sync-config/autosync-config 并列独立文件。schemaVersion:1,
|
|
5
|
+
* 结构 { schemaVersion, autosyncEntries: AutosyncHistoryEntry[], updatedAt }。
|
|
6
|
+
*
|
|
7
|
+
* - append 追加(升序)并裁剪保留最近 AUTOSYNC_HISTORY_KEEP 条;
|
|
8
|
+
* - 原子写(临时文件 + rename);
|
|
9
|
+
* - 损坏 JSON 严格拒绝(不静默降级到空列表)。
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.js';
|
|
15
|
+
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
16
|
+
export const SYNC_HISTORY_FILE = 'sync-history.json';
|
|
17
|
+
export const SYNC_HISTORY_SCHEMA_VERSION = 1;
|
|
18
|
+
/** 自动同步历史保留上限 */
|
|
19
|
+
export const AUTOSYNC_HISTORY_KEEP = 200;
|
|
20
|
+
/** 读取同步历史;文件不存在 → 空列表(schemaVersion=1)。损坏 JSON 抛错。 */
|
|
21
|
+
export async function readSyncHistory(dir) {
|
|
22
|
+
const file = path.join(dir, SYNC_HISTORY_FILE);
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = await fs.readFile(file, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { schemaVersion: SYNC_HISTORY_SCHEMA_VERSION, autosyncEntries: [], updatedAt: '' };
|
|
29
|
+
}
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = parseJsonSafe(raw);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:JSON 解析失败`);
|
|
36
|
+
}
|
|
37
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
38
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:必须是对象`);
|
|
39
|
+
}
|
|
40
|
+
const obj = parsed;
|
|
41
|
+
if (obj['schemaVersion'] !== SYNC_HISTORY_SCHEMA_VERSION) {
|
|
42
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:schemaVersion 必须是 ${SYNC_HISTORY_SCHEMA_VERSION}`);
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(obj['autosyncEntries'])) {
|
|
45
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:autosyncEntries 必须是数组`);
|
|
46
|
+
}
|
|
47
|
+
const entries = [];
|
|
48
|
+
for (const it of obj['autosyncEntries']) {
|
|
49
|
+
if (it === null || typeof it !== 'object' || Array.isArray(it)) {
|
|
50
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:每个 entry 必须是对象`);
|
|
51
|
+
}
|
|
52
|
+
const e = it;
|
|
53
|
+
if (typeof e['direction'] !== 'string' || typeof e['status'] !== 'string' || typeof e['createdAt'] !== 'string') {
|
|
54
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:entry 必须含字符串 direction/status/createdAt`);
|
|
55
|
+
}
|
|
56
|
+
entries.push(it);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
schemaVersion: SYNC_HISTORY_SCHEMA_VERSION,
|
|
60
|
+
autosyncEntries: entries,
|
|
61
|
+
updatedAt: typeof obj['updatedAt'] === 'string' ? obj['updatedAt'] : '',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** 追加一条自动同步执行记录(升序),裁剪到 AUTOSYNC_HISTORY_KEEP 条,原子写。 */
|
|
65
|
+
export async function appendAutosyncEntry(dir, entry, now = () => new Date()) {
|
|
66
|
+
const current = await readSyncHistory(dir);
|
|
67
|
+
current.autosyncEntries.push(entry);
|
|
68
|
+
// 裁剪:保留最近 AUTOSYNC_HISTORY_KEEP 条(升序 → 去掉最前的超限条)
|
|
69
|
+
if (current.autosyncEntries.length > AUTOSYNC_HISTORY_KEEP) {
|
|
70
|
+
current.autosyncEntries = current.autosyncEntries.slice(current.autosyncEntries.length - AUTOSYNC_HISTORY_KEEP);
|
|
71
|
+
}
|
|
72
|
+
current.updatedAt = now().toISOString();
|
|
73
|
+
await fs.mkdir(dir, { recursive: true });
|
|
74
|
+
const target = path.join(dir, SYNC_HISTORY_FILE);
|
|
75
|
+
const data = stringifyJsonSafe(current, { space: 2 });
|
|
76
|
+
await atomicWriteFile(target, data, { mode: 0o600 });
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=sync-history.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SectionId } from '../schema/types.ts';
|
|
2
|
+
import type { SyncTransportType } from './sync-config.ts';
|
|
3
|
+
export declare const SYNC_SELECTION_FILE = "sync-selection.json";
|
|
4
|
+
export declare const SYNC_SELECTION_SCHEMA_VERSION = 2;
|
|
5
|
+
/** 可读的版本:1 = 上游顶层单通道(或早期 channels 信封);2 = 本信封(含上游 0.1.60)。 */
|
|
6
|
+
export declare const SYNC_SELECTION_SUPPORTED_VERSIONS: readonly number[];
|
|
7
|
+
/** 远程同步分区选择模式:default = 全部推荐分区;advanced = 自定义勾选。 */
|
|
8
|
+
export type SyncSelectionMode = 'default' | 'advanced';
|
|
9
|
+
/** 远程同步分区选择(持久化面;单通道)。 */
|
|
10
|
+
export interface SyncSelection {
|
|
11
|
+
schemaVersion: number;
|
|
12
|
+
mode: SyncSelectionMode;
|
|
13
|
+
/** 高级模式勾选分区;default 模式可为空数组 */
|
|
14
|
+
sections: SectionId[];
|
|
15
|
+
}
|
|
16
|
+
/** 缺省配置(首次无文件 / 损坏 / 不支持 schema 时回退) */
|
|
17
|
+
export declare function defaultSyncSelection(): SyncSelection;
|
|
18
|
+
/**
|
|
19
|
+
* 生效的同步分区范围:
|
|
20
|
+
* - mode='advanced' 且 sections 非空 → sections(自定义同步);
|
|
21
|
+
* - 其余(default / advanced 但未勾选)→ undefined(= 全部推荐分区)。
|
|
22
|
+
*/
|
|
23
|
+
export declare function effectiveSections(sel: SyncSelection): SectionId[] | undefined;
|
|
24
|
+
/** 读取全部通道的分区选择配置;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
|
|
25
|
+
export declare function readAllSyncSelections(dir: string): Promise<Record<SyncTransportType, SyncSelection>>;
|
|
26
|
+
/** 读取指定通道的分区选择配置;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
|
|
27
|
+
export declare function readSyncSelection(dir: string, channel: SyncTransportType): Promise<SyncSelection>;
|
|
28
|
+
/** 写入指定通道的分区选择配置(原子写:临时文件 + rename;保留另一通道;自动创建目录)。 */
|
|
29
|
+
export declare function writeSyncSelection(dir: string, channel: SyncTransportType, sel: SyncSelection): Promise<void>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-sync-selection:远程同步分区选择持久化(sync-selection.json)。
|
|
3
|
+
*
|
|
4
|
+
* 与 sync-config.json / sync-autosync.json 并列独立文件:语义清楚、schema 演进独立。
|
|
5
|
+
* schemaVersion:2 —— 按同步通道拆分(git / webdav 各自独立的分区勾选):
|
|
6
|
+
* ```
|
|
7
|
+
* { "schemaVersion": 2,
|
|
8
|
+
* "channels": {
|
|
9
|
+
* "git": { mode: 'default'|'advanced', sections: SectionId[] },
|
|
10
|
+
* "webdav": { ... } } }
|
|
11
|
+
* ```
|
|
12
|
+
* - mode='default'(快速同步):推送/自动同步使用全部推荐分区(sections 可空);
|
|
13
|
+
* - mode='advanced'(自定义同步):推送/自动同步只处理勾选的 sections(空 sections 回退全量,
|
|
14
|
+
* 避免自动同步卡死)。
|
|
15
|
+
*
|
|
16
|
+
* 快照恒为明文:同步通道是用户自有的私有通道,勾选即同步,不加密、不脱敏。
|
|
17
|
+
*
|
|
18
|
+
* **版本号与上游同号是刻意的**:上游 0.1.60 的 `schemaVersion:2` 就是本信封
|
|
19
|
+
* (多出 `encrypt` / `includeSecrets` 两个已废弃字段),上游的 v1 则是**顶层单通道**
|
|
20
|
+
* 形状(`{mode, sections}`,无 channels)。本文件必须能读回上游 v2,否则升级即静默
|
|
21
|
+
* 丢失用户勾选;写出的 v2 上游也仍能读(它接受 ver===2),故回滚到上游不会重置。
|
|
22
|
+
* 若把本信封编号写成 1,就会与上游 v1 的顶层形状撞号:既读不回上游 v2,写出的文件
|
|
23
|
+
* 上游按顶层解析又只得到缺省。
|
|
24
|
+
*
|
|
25
|
+
* 原子写(临时文件 + rename),损坏/不支持 schema 回退缺省(mode='default', sections=[])。
|
|
26
|
+
* 持久化原因:自动同步调度器运行于 Host 进程(浏览器关闭也在跑),必须从磁盘读
|
|
27
|
+
* 用户选择,而不是依赖浏览器 localStorage。
|
|
28
|
+
*/
|
|
29
|
+
import fs from 'node:fs/promises';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.js';
|
|
32
|
+
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
33
|
+
export const SYNC_SELECTION_FILE = 'sync-selection.json';
|
|
34
|
+
export const SYNC_SELECTION_SCHEMA_VERSION = 2;
|
|
35
|
+
/** 可读的版本:1 = 上游顶层单通道(或早期 channels 信封);2 = 本信封(含上游 0.1.60)。 */
|
|
36
|
+
export const SYNC_SELECTION_SUPPORTED_VERSIONS = [1, 2];
|
|
37
|
+
/** 缺省配置(首次无文件 / 损坏 / 不支持 schema 时回退) */
|
|
38
|
+
export function defaultSyncSelection() {
|
|
39
|
+
return { schemaVersion: SYNC_SELECTION_SCHEMA_VERSION, mode: 'default', sections: [] };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 生效的同步分区范围:
|
|
43
|
+
* - mode='advanced' 且 sections 非空 → sections(自定义同步);
|
|
44
|
+
* - 其余(default / advanced 但未勾选)→ undefined(= 全部推荐分区)。
|
|
45
|
+
*/
|
|
46
|
+
export function effectiveSections(sel) {
|
|
47
|
+
if (sel.mode === 'advanced' && sel.sections.length > 0)
|
|
48
|
+
return [...sel.sections];
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** 从单通道对象解析(非法字段回退缺省)。 */
|
|
52
|
+
function parseChannelSelection(obj) {
|
|
53
|
+
const sel = defaultSyncSelection();
|
|
54
|
+
if (obj['mode'] === 'advanced' || obj['mode'] === 'default')
|
|
55
|
+
sel.mode = obj['mode'];
|
|
56
|
+
if (Array.isArray(obj['sections'])) {
|
|
57
|
+
sel.sections = obj['sections'].filter((s) => typeof s === 'string' && s !== '');
|
|
58
|
+
}
|
|
59
|
+
return sel;
|
|
60
|
+
}
|
|
61
|
+
/** 读取全部通道的分区选择配置;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
|
|
62
|
+
export async function readAllSyncSelections(dir) {
|
|
63
|
+
const fallback = () => ({
|
|
64
|
+
git: defaultSyncSelection(),
|
|
65
|
+
webdav: defaultSyncSelection(),
|
|
66
|
+
});
|
|
67
|
+
const file = path.join(dir, SYNC_SELECTION_FILE);
|
|
68
|
+
let raw;
|
|
69
|
+
try {
|
|
70
|
+
raw = await fs.readFile(file, 'utf8');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return fallback();
|
|
74
|
+
}
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = parseJsonSafe(raw);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return fallback();
|
|
81
|
+
}
|
|
82
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
83
|
+
return fallback();
|
|
84
|
+
const obj = parsed;
|
|
85
|
+
// 缺 schemaVersion 视为 v1(与上游一致);不支持的版本回退缺省。
|
|
86
|
+
const ver = typeof obj['schemaVersion'] === 'number' ? obj['schemaVersion'] : 1;
|
|
87
|
+
if (!SYNC_SELECTION_SUPPORTED_VERSIONS.includes(ver))
|
|
88
|
+
return fallback();
|
|
89
|
+
const channels = obj['channels'];
|
|
90
|
+
if (channels === null || typeof channels !== 'object' || Array.isArray(channels)) {
|
|
91
|
+
// v1 顶层单通道形状:整个对象就是一个通道配置(归 git),与上游迁移语义一致。
|
|
92
|
+
// v2 缺 channels 信封属于损坏 → 回退缺省。
|
|
93
|
+
if (ver === 1)
|
|
94
|
+
return { git: parseChannelSelection(obj), webdav: defaultSyncSelection() };
|
|
95
|
+
return fallback();
|
|
96
|
+
}
|
|
97
|
+
const ch = channels;
|
|
98
|
+
const pick = (ns) => ns !== null && typeof ns === 'object' && !Array.isArray(ns)
|
|
99
|
+
? parseChannelSelection(ns)
|
|
100
|
+
: defaultSyncSelection();
|
|
101
|
+
return { git: pick(ch['git']), webdav: pick(ch['webdav']) };
|
|
102
|
+
}
|
|
103
|
+
/** 读取指定通道的分区选择配置;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
|
|
104
|
+
export async function readSyncSelection(dir, channel) {
|
|
105
|
+
const all = await readAllSyncSelections(dir);
|
|
106
|
+
return all[channel];
|
|
107
|
+
}
|
|
108
|
+
/** 写入指定通道的分区选择配置(原子写:临时文件 + rename;保留另一通道;自动创建目录)。 */
|
|
109
|
+
export async function writeSyncSelection(dir, channel, sel) {
|
|
110
|
+
await fs.mkdir(dir, { recursive: true });
|
|
111
|
+
const existing = await readAllSyncSelections(dir);
|
|
112
|
+
const channels = {
|
|
113
|
+
git: channel === 'git' ? sel : existing.git,
|
|
114
|
+
webdav: channel === 'webdav' ? sel : existing.webdav,
|
|
115
|
+
};
|
|
116
|
+
const payload = {
|
|
117
|
+
schemaVersion: SYNC_SELECTION_SCHEMA_VERSION,
|
|
118
|
+
channels: {
|
|
119
|
+
git: { mode: channels.git.mode, sections: channels.git.sections },
|
|
120
|
+
webdav: { mode: channels.webdav.mode, sections: channels.webdav.sections },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const target = path.join(dir, SYNC_SELECTION_FILE);
|
|
124
|
+
const data = stringifyJsonSafe(payload, { space: 2 });
|
|
125
|
+
await atomicWriteFile(target, data, { mode: 0o600 });
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=sync-selection.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ImportAnalysis, ImportPlan } from '../core/types.ts';
|
|
2
|
+
import type { SyncConfig } from './sync-config.ts';
|
|
3
|
+
/** 默认会话 TTL(30 分钟) */
|
|
4
|
+
export declare const DEFAULT_SESSION_TTL_MS: number;
|
|
5
|
+
/** 一次差异确认会话的完整数据 */
|
|
6
|
+
export interface SyncSession {
|
|
7
|
+
id: string;
|
|
8
|
+
/** 临时标准 ZIP(apply-items 执行 executeImportPlan 需要) */
|
|
9
|
+
zipPath: string;
|
|
10
|
+
plan: ImportPlan;
|
|
11
|
+
analysis: ImportAnalysis;
|
|
12
|
+
/** 被拉取的远端快照 id */
|
|
13
|
+
snapshotId: string;
|
|
14
|
+
/** 该次同步使用的通道配置(git/webdav 通用;apply-items 据此重建引擎) */
|
|
15
|
+
config: SyncConfig;
|
|
16
|
+
/** 创建时间(epoch ms) */
|
|
17
|
+
createdAt: number;
|
|
18
|
+
/** 过期时间(epoch ms;过期条目惰性清理) */
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
}
|
|
21
|
+
/** 内存会话存储;同 id 覆盖;过期条目视为不存在。 */
|
|
22
|
+
export declare class SyncSessionStore {
|
|
23
|
+
private readonly sessions;
|
|
24
|
+
private readonly ttlMs;
|
|
25
|
+
private readonly now;
|
|
26
|
+
constructor(opts?: {
|
|
27
|
+
ttlMs?: number;
|
|
28
|
+
now?: () => number;
|
|
29
|
+
});
|
|
30
|
+
/** 登记(或覆盖)会话;id 缺省时自动生成。createdAt/expiresAt 缺省自动填充。返回会话 id。 */
|
|
31
|
+
set(session: Omit<SyncSession, 'id' | 'createdAt' | 'expiresAt'> & {
|
|
32
|
+
id?: string;
|
|
33
|
+
createdAt?: number;
|
|
34
|
+
expiresAt?: number;
|
|
35
|
+
}): string;
|
|
36
|
+
/** 读取会话;过期条目返回 undefined(惰性清理)。 */
|
|
37
|
+
get(id: string): SyncSession | undefined;
|
|
38
|
+
/** 删除会话(cancel / apply-items 消费后调用)。 */
|
|
39
|
+
delete(id: string): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncSessionStore:一键同步差异确认会话的内存登记。
|
|
3
|
+
*
|
|
4
|
+
* 会话把「拉取预览」与「逐项执行导入」解耦:一次 /sync/sync 在内存持有一份
|
|
5
|
+
* 临时 ZIP 路径 + ImportPlan + ImportAnalysis,配一个 syncSessionId;
|
|
6
|
+
* /sync/apply-items 凭 id 复用这些数据,避免重复拉取/重复 build ZIP。
|
|
7
|
+
*
|
|
8
|
+
* 生命周期:
|
|
9
|
+
* - 进程内存(不落盘),默认 TTL 30 分钟,过期条目惰性清理(get 时检查);
|
|
10
|
+
* - 同 id 覆盖(set 后旧 session 被新 session 替换,旧 ZIP 由上层负责清理);
|
|
11
|
+
* - delete 显式移除(/sync/cancel 与 apply-items 消费后调用)。
|
|
12
|
+
*/
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
/** 默认会话 TTL(30 分钟) */
|
|
15
|
+
export const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;
|
|
16
|
+
/** 内存会话存储;同 id 覆盖;过期条目视为不存在。 */
|
|
17
|
+
export class SyncSessionStore {
|
|
18
|
+
sessions = new Map();
|
|
19
|
+
ttlMs;
|
|
20
|
+
now;
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_SESSION_TTL_MS;
|
|
23
|
+
this.now = opts.now ?? (() => Date.now());
|
|
24
|
+
}
|
|
25
|
+
/** 登记(或覆盖)会话;id 缺省时自动生成。createdAt/expiresAt 缺省自动填充。返回会话 id。 */
|
|
26
|
+
set(session) {
|
|
27
|
+
const id = session.id ?? `sync-session-${crypto.randomUUID()}`;
|
|
28
|
+
const createdAt = session.createdAt ?? this.now();
|
|
29
|
+
const expiresAt = session.expiresAt ?? createdAt + this.ttlMs;
|
|
30
|
+
this.sessions.set(id, {
|
|
31
|
+
...session,
|
|
32
|
+
id,
|
|
33
|
+
createdAt,
|
|
34
|
+
expiresAt,
|
|
35
|
+
});
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
/** 读取会话;过期条目返回 undefined(惰性清理)。 */
|
|
39
|
+
get(id) {
|
|
40
|
+
const s = this.sessions.get(id);
|
|
41
|
+
if (s === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (this.now() >= s.expiresAt) {
|
|
44
|
+
this.sessions.delete(id);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return { ...s };
|
|
48
|
+
}
|
|
49
|
+
/** 删除会话(cancel / apply-items 消费后调用)。 */
|
|
50
|
+
delete(id) {
|
|
51
|
+
this.sessions.delete(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=sync-session.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MsgFunc } from '../core/messages.ts';
|
|
2
|
+
import type { SectionData, SectionId } from '../schema/types.ts';
|
|
3
|
+
import type { SnapshotFs } from './fs.ts';
|
|
4
|
+
export declare const SYNC_STATE_SCHEMA_VERSION = 2;
|
|
5
|
+
/** 历史可读取版本:v1 缺 lastSnapshotId 时做内存迁移到 v2(lastSnapshotId='')。 */
|
|
6
|
+
export declare const SYNC_STATE_SUPPORTED_VERSIONS: readonly number[];
|
|
7
|
+
export declare const SYNC_STATE_FILE = "sync-state.json";
|
|
8
|
+
/** 单个分区的同步状态:内容 hash + 最近变更时间 */
|
|
9
|
+
export interface SyncSectionState {
|
|
10
|
+
hash: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
/** sync-state.json 结构 */
|
|
14
|
+
export interface SyncState {
|
|
15
|
+
schemaVersion: number;
|
|
16
|
+
lastSyncAt: string;
|
|
17
|
+
sections: Partial<Record<SectionId, SyncSectionState>>;
|
|
18
|
+
/** 当前绑定的传输通道(type = SyncTransport.type,ref = 通道内引用,如 git 分支) */
|
|
19
|
+
transport?: {
|
|
20
|
+
type: string;
|
|
21
|
+
ref: string;
|
|
22
|
+
};
|
|
23
|
+
/** 最近一次同步的快照 ID(= 共同祖先指针);'' = 从未同步或无祖先 */
|
|
24
|
+
lastSnapshotId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 分区内容 hash:
|
|
28
|
+
* - JSON 分区:键序规范化的 JSON 序列化 → SHA-256(同一内容不同键序 → 相同 hash)
|
|
29
|
+
* - 文件类分区:文件相对路径 + 文件字节 SHA-256 的有序清单 → SHA-256
|
|
30
|
+
* (文件数组顺序无关;contentHash 字段不参与,以字节为准)
|
|
31
|
+
*/
|
|
32
|
+
export declare function hashSection(data: SectionData): string;
|
|
33
|
+
/** 读取同步状态;文件不存在 → 返回缺省空状态(从未同步)。
|
|
34
|
+
* v1 文件 → 内存迁移到 v2(lastSnapshotId=''),不在磁盘上就地升级。 */
|
|
35
|
+
export declare function loadSyncState(dir: string, fsx?: SnapshotFs, msg?: MsgFunc): Promise<SyncState>;
|
|
36
|
+
/** 保存同步状态(自动创建目录) */
|
|
37
|
+
export declare function saveSyncState(dir: string, state: SyncState, fsx?: SnapshotFs): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-sync-transport:sync-state.json 模型。
|
|
3
|
+
* 记录上次同步时间与各分区内容 hash,供变更检测(与远端快照对比决定是否重传)。
|
|
4
|
+
* 纯逻辑 + 可注入 fs;不触碰真实 ~/.dsh。
|
|
5
|
+
*/
|
|
6
|
+
import { sha256Hex } from '../utils/hashing.js';
|
|
7
|
+
import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.js';
|
|
8
|
+
import { zhMsg } from '../core/messages.js';
|
|
9
|
+
import { createSnapshotFs, joinFs } from './fs.js';
|
|
10
|
+
export const SYNC_STATE_SCHEMA_VERSION = 2;
|
|
11
|
+
/** 历史可读取版本:v1 缺 lastSnapshotId 时做内存迁移到 v2(lastSnapshotId='')。 */
|
|
12
|
+
export const SYNC_STATE_SUPPORTED_VERSIONS = [1, 2];
|
|
13
|
+
export const SYNC_STATE_FILE = 'sync-state.json';
|
|
14
|
+
/** 文件类分区判定:{ version: 1, files: [...] }(duck-typing,与 JSON 分区区分) */
|
|
15
|
+
function isFilesSection(data) {
|
|
16
|
+
const obj = data;
|
|
17
|
+
return data !== null && typeof data === 'object' && obj.version === 1 && Array.isArray(obj.files);
|
|
18
|
+
}
|
|
19
|
+
/** 键序规范化的 JSON 序列化(跨机器/跨解析稳定,用于内容 hash) */
|
|
20
|
+
function canonicalJson(value) {
|
|
21
|
+
if (value === null || typeof value !== 'object') {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return 'null'; // 防御:不产生 undefined 字面量
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return `[${value.map((v) => canonicalJson(v)).join(',')}]`;
|
|
28
|
+
}
|
|
29
|
+
const obj = value;
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (const key of Object.keys(obj).sort()) {
|
|
32
|
+
const v = obj[key];
|
|
33
|
+
if (v === undefined)
|
|
34
|
+
continue; // 与 JSON.stringify 一致:跳过 undefined 键
|
|
35
|
+
parts.push(`${JSON.stringify(key)}:${canonicalJson(v)}`);
|
|
36
|
+
}
|
|
37
|
+
return `{${parts.join(',')}}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 分区内容 hash:
|
|
41
|
+
* - JSON 分区:键序规范化的 JSON 序列化 → SHA-256(同一内容不同键序 → 相同 hash)
|
|
42
|
+
* - 文件类分区:文件相对路径 + 文件字节 SHA-256 的有序清单 → SHA-256
|
|
43
|
+
* (文件数组顺序无关;contentHash 字段不参与,以字节为准)
|
|
44
|
+
*/
|
|
45
|
+
export function hashSection(data) {
|
|
46
|
+
if (isFilesSection(data)) {
|
|
47
|
+
const files = [...data.files]
|
|
48
|
+
.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0))
|
|
49
|
+
.map((f) => `${f.relativePath}\u0000${sha256Hex(f.data)}`)
|
|
50
|
+
.join('\u0001');
|
|
51
|
+
return sha256Hex(`files:v1\u0000${files}`);
|
|
52
|
+
}
|
|
53
|
+
return sha256Hex(canonicalJson(data));
|
|
54
|
+
}
|
|
55
|
+
/** 读取同步状态;文件不存在 → 返回缺省空状态(从未同步)。
|
|
56
|
+
* v1 文件 → 内存迁移到 v2(lastSnapshotId=''),不在磁盘上就地升级。 */
|
|
57
|
+
export async function loadSyncState(dir, fsx = createSnapshotFs(), msg = zhMsg) {
|
|
58
|
+
const file = joinFs(dir, SYNC_STATE_FILE);
|
|
59
|
+
if (!(await fsx.exists(file))) {
|
|
60
|
+
return { schemaVersion: SYNC_STATE_SCHEMA_VERSION, lastSyncAt: '', sections: {}, lastSnapshotId: '' };
|
|
61
|
+
}
|
|
62
|
+
const raw = Buffer.from(await fsx.readFile(file)).toString('utf8');
|
|
63
|
+
const parsed = parseJsonSafe(raw);
|
|
64
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
65
|
+
throw new Error(msg('sync.state.notObject'));
|
|
66
|
+
}
|
|
67
|
+
const obj = parsed;
|
|
68
|
+
if (typeof obj['schemaVersion'] !== 'number' || !SYNC_STATE_SUPPORTED_VERSIONS.includes(obj['schemaVersion'])) {
|
|
69
|
+
throw new Error(msg('sync.state.schemaUnsupported', { version: String(obj['schemaVersion']), expected: String(SYNC_STATE_SUPPORTED_VERSIONS.join('/')) }));
|
|
70
|
+
}
|
|
71
|
+
if (typeof obj['lastSyncAt'] !== 'string') {
|
|
72
|
+
throw new Error(msg('sync.state.lastSyncAt'));
|
|
73
|
+
}
|
|
74
|
+
if (obj['sections'] === null || typeof obj['sections'] !== 'object' || Array.isArray(obj['sections'])) {
|
|
75
|
+
throw new Error(msg('sync.state.sections'));
|
|
76
|
+
}
|
|
77
|
+
const sections = {};
|
|
78
|
+
for (const [sid, rec] of Object.entries(obj['sections'])) {
|
|
79
|
+
if (rec === null || typeof rec !== 'object' || Array.isArray(rec)) {
|
|
80
|
+
throw new Error(msg('sync.state.sectionRecord', { section: sid }));
|
|
81
|
+
}
|
|
82
|
+
const r = rec;
|
|
83
|
+
if (typeof r['hash'] !== 'string' || typeof r['updatedAt'] !== 'string') {
|
|
84
|
+
throw new Error(msg('sync.state.sectionFields', { section: sid }));
|
|
85
|
+
}
|
|
86
|
+
sections[sid] = { hash: r['hash'], updatedAt: r['updatedAt'] };
|
|
87
|
+
}
|
|
88
|
+
const state = {
|
|
89
|
+
schemaVersion: SYNC_STATE_SCHEMA_VERSION,
|
|
90
|
+
lastSyncAt: obj['lastSyncAt'],
|
|
91
|
+
sections,
|
|
92
|
+
lastSnapshotId: typeof obj['lastSnapshotId'] === 'string' ? obj['lastSnapshotId'] : '',
|
|
93
|
+
};
|
|
94
|
+
const t = obj['transport'];
|
|
95
|
+
if (t !== undefined) {
|
|
96
|
+
if (t === null || typeof t !== 'object' || typeof t['type'] !== 'string' || typeof t['ref'] !== 'string') {
|
|
97
|
+
throw new Error(msg('sync.state.transport'));
|
|
98
|
+
}
|
|
99
|
+
state.transport = { type: t.type, ref: t.ref };
|
|
100
|
+
}
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
/** 保存同步状态(自动创建目录) */
|
|
104
|
+
export async function saveSyncState(dir, state, fsx = createSnapshotFs()) {
|
|
105
|
+
await fsx.mkdir(dir);
|
|
106
|
+
await fsx.writeFile(joinFs(dir, SYNC_STATE_FILE), new TextEncoder().encode(stringifyJsonSafe(state, { space: 2 })));
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=sync-state.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-sync-transport:SyncTransport 抽象层。
|
|
3
|
+
* 纯类型 + 纯 helper,零副作用、零 fs —— 传输通道实现(git 等)由后续波次提供。
|
|
4
|
+
* 与核心引擎同原则:只依赖 src/schema/types.ts 的纯类型,不 import 任何 DSH 运行时包。
|
|
5
|
+
*/
|
|
6
|
+
import type { EncryptionInfo, Manifest, SectionData, SectionId } from '../schema/types.ts';
|
|
7
|
+
/** manifest 摘要:快照列表展示 / 变更判断所需的轻量来源信息 */
|
|
8
|
+
export interface ManifestSummary {
|
|
9
|
+
schemaVersion: number;
|
|
10
|
+
dshVersion: string;
|
|
11
|
+
platform: string;
|
|
12
|
+
/** 快照包含的分区(按 manifest.sections 中为 true 的键) */
|
|
13
|
+
sectionIds: SectionId[];
|
|
14
|
+
containsSecrets: boolean;
|
|
15
|
+
/** 快照是否加密(sections 为 EncryptedSections 密文载荷);缺省 false(旧快照兼容) */
|
|
16
|
+
encrypted?: boolean;
|
|
17
|
+
/** 产生该快照的同步通道(git / webdav;供同步历史展示触发来源;旧快照缺省 undefined) */
|
|
18
|
+
transport?: string;
|
|
19
|
+
}
|
|
20
|
+
/** 快照元信息:list() 条目 / upload() 返回值 */
|
|
21
|
+
export interface SyncSnapshotMeta {
|
|
22
|
+
id: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
/** 各分区内容 hash(sectionId → hashSection 结果),用于变更检测。
|
|
25
|
+
* 加密快照的 sections 为密文载荷,不参与明文 hash 比较 → 空对象。 */
|
|
26
|
+
sections: Partial<Record<SectionId, string>>;
|
|
27
|
+
manifest: ManifestSummary;
|
|
28
|
+
}
|
|
29
|
+
/** 加密快照的 sections 载荷:整个明文 sections 对象序列化后整体加密(AES-256-GCM)。 */
|
|
30
|
+
export interface EncryptedSections {
|
|
31
|
+
encrypted: {
|
|
32
|
+
/** 加密参数(salt/iv/authTag base64;与 security/encryption.ts 的 EncryptionInfo 对齐) */
|
|
33
|
+
info: EncryptionInfo;
|
|
34
|
+
/** base64:带 DSC1 头的密文(明文 = 序列化的 sections Record) */
|
|
35
|
+
data: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** 快照载荷:upload() 入参 / download() 返回 */
|
|
39
|
+
export interface SyncSnapshot {
|
|
40
|
+
id: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
manifest: ManifestSummary;
|
|
43
|
+
/** JSON 分区数据 + 文件类分区(FilesSection);加密快照为 EncryptedSections 密文载荷 */
|
|
44
|
+
sections: Partial<Record<SectionId, SectionData>> | EncryptedSections;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 远端快照传输通道契约。
|
|
48
|
+
* 实现约定:同 id 重复 upload 视为覆盖(幂等友好);download 对不存在的 id 必须抛错。
|
|
49
|
+
*/
|
|
50
|
+
export interface SyncTransport {
|
|
51
|
+
readonly type: string;
|
|
52
|
+
/** 列出远端已有快照(按 createdAt 升序) */
|
|
53
|
+
list(): Promise<SyncSnapshotMeta[]>;
|
|
54
|
+
/** 上传快照,返回其元信息(含各分区 hash) */
|
|
55
|
+
upload(snapshot: SyncSnapshot): Promise<SyncSnapshotMeta>;
|
|
56
|
+
/** 下载快照完整载荷 */
|
|
57
|
+
download(id: string): Promise<SyncSnapshot>;
|
|
58
|
+
/** 删除远端快照(不存在视为成功) */
|
|
59
|
+
delete(id: string): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
/** 由快照计算元信息:sections hash 记录 + manifest 摘要透传。
|
|
62
|
+
* 加密快照(sections 为密文载荷)→ sections hash 记录为空(密文无法与本地明文比较)。 */
|
|
63
|
+
export declare function computeSnapshotMeta(snapshot: SyncSnapshot): SyncSnapshotMeta;
|
|
64
|
+
/** 判定远端索引条目与本地快照「内容相同」(快照级跳过判定,供 webdav 通道增量上传用):
|
|
65
|
+
* 仅比较各分区内容 hash(sections 全等),不比较 createdAt / manifest 摘要
|
|
66
|
+
* (同 id 重复上传但内容未变 → 视为幂等覆盖,无需重新 PUT 载荷)。
|
|
67
|
+
* - 本地 sections 为空对象(加密快照:密文无法与远端明文 hash 比较)→ 返回 false,
|
|
68
|
+
* 「无法比较」必须照常上传,绝不跳过;
|
|
69
|
+
* - 键集合与每个键的 hash 值全部相等 → true;否则 false。
|
|
70
|
+
*/
|
|
71
|
+
export declare function sectionsEqual(remote: SyncSnapshotMeta, local: SyncSnapshotMeta): boolean;
|
|
72
|
+
/** 判定 sections 是否为加密密文载荷(duck-typing:含 encrypted.info + encrypted.data 字符串)。 */
|
|
73
|
+
export declare function isEncryptedSections(sections: unknown): sections is EncryptedSections;
|
|
74
|
+
/** 从导出 Manifest 提取摘要 */
|
|
75
|
+
export declare function manifestSummaryFrom(manifest: Manifest): ManifestSummary;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { hashSection } from './sync-state.js';
|
|
2
|
+
/** 由快照计算元信息:sections hash 记录 + manifest 摘要透传。
|
|
3
|
+
* 加密快照(sections 为密文载荷)→ sections hash 记录为空(密文无法与本地明文比较)。 */
|
|
4
|
+
export function computeSnapshotMeta(snapshot) {
|
|
5
|
+
const sections = {};
|
|
6
|
+
if (!isEncryptedSections(snapshot.sections)) {
|
|
7
|
+
for (const [id, data] of Object.entries(snapshot.sections)) {
|
|
8
|
+
sections[id] = hashSection(data);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return { id: snapshot.id, createdAt: snapshot.createdAt, sections, manifest: snapshot.manifest };
|
|
12
|
+
}
|
|
13
|
+
/** 判定远端索引条目与本地快照「内容相同」(快照级跳过判定,供 webdav 通道增量上传用):
|
|
14
|
+
* 仅比较各分区内容 hash(sections 全等),不比较 createdAt / manifest 摘要
|
|
15
|
+
* (同 id 重复上传但内容未变 → 视为幂等覆盖,无需重新 PUT 载荷)。
|
|
16
|
+
* - 本地 sections 为空对象(加密快照:密文无法与远端明文 hash 比较)→ 返回 false,
|
|
17
|
+
* 「无法比较」必须照常上传,绝不跳过;
|
|
18
|
+
* - 键集合与每个键的 hash 值全部相等 → true;否则 false。
|
|
19
|
+
*/
|
|
20
|
+
export function sectionsEqual(remote, local) {
|
|
21
|
+
const r = remote.sections;
|
|
22
|
+
const l = local.sections;
|
|
23
|
+
if (Object.keys(l).length === 0)
|
|
24
|
+
return false; // 本地为空(加密快照)→ 无法比较
|
|
25
|
+
if (Object.keys(r).length !== Object.keys(l).length)
|
|
26
|
+
return false;
|
|
27
|
+
for (const key of Object.keys(r)) {
|
|
28
|
+
if (r[key] !== l[key])
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** 判定 sections 是否为加密密文载荷(duck-typing:含 encrypted.info + encrypted.data 字符串)。 */
|
|
34
|
+
export function isEncryptedSections(sections) {
|
|
35
|
+
if (sections === null || typeof sections !== 'object')
|
|
36
|
+
return false;
|
|
37
|
+
const enc = sections.encrypted;
|
|
38
|
+
if (enc === null || typeof enc !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
const e = enc;
|
|
41
|
+
return typeof e.data === 'string' && e.data !== '' && e.info !== null && typeof e.info === 'object';
|
|
42
|
+
}
|
|
43
|
+
/** 从导出 Manifest 提取摘要 */
|
|
44
|
+
export function manifestSummaryFrom(manifest) {
|
|
45
|
+
return {
|
|
46
|
+
schemaVersion: manifest.schemaVersion,
|
|
47
|
+
dshVersion: manifest.source.dshVersion,
|
|
48
|
+
platform: manifest.source.platform,
|
|
49
|
+
sectionIds: Object.entries(manifest.sections)
|
|
50
|
+
.filter(([, included]) => included)
|
|
51
|
+
.map(([id]) => id),
|
|
52
|
+
containsSecrets: manifest.security.containsSecrets,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const UI_PREFS_FILE = "ui-prefs.json";
|
|
2
|
+
export declare const UI_PREFS_SCHEMA_VERSION = 1;
|
|
3
|
+
/** 同步通道类型(与 sync-selection / sync-config 共享语义;避免循环 import 自行声明) */
|
|
4
|
+
export type UiPrefsChannel = 'git' | 'webdav';
|
|
5
|
+
/** 插件自身 UI 偏好(持久化面)。 */
|
|
6
|
+
export interface UiPrefs {
|
|
7
|
+
schemaVersion: number;
|
|
8
|
+
/** 用户上次选择的同步通道;未配置为 undefined */
|
|
9
|
+
lastSyncChannel?: UiPrefsChannel;
|
|
10
|
+
/** Star 引导弹窗:首次进入页面时间(ms 时间戳);未配置 = 尚未进入过页面 */
|
|
11
|
+
starPromptFirstSeenAt?: number;
|
|
12
|
+
/** Star 引导弹窗:用户点过「不再提示」(永久不再弹) */
|
|
13
|
+
starPromptDismissed?: boolean;
|
|
14
|
+
/** Star 引导弹窗:用户点过「去点 Star」(方案 A:引导完成,不再弹) */
|
|
15
|
+
starPromptClicked?: boolean;
|
|
16
|
+
/** 更新内容弹窗:上次已记录/展示过的插件版本号(如 '0.1.54') */
|
|
17
|
+
releaseNotesLastSeenVersion?: string;
|
|
18
|
+
/** 更新内容弹窗:用户点过「永不提示」(永久不再自动弹出) */
|
|
19
|
+
releaseNotesDismissed?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** 缺省配置(首次无文件 / 损坏 / 不支持 schema 时回退) */
|
|
22
|
+
export declare function defaultUiPrefs(): UiPrefs;
|
|
23
|
+
/** 读取 UI 偏好;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
|
|
24
|
+
export declare function readUiPrefs(dir: string): Promise<UiPrefs>;
|
|
25
|
+
/** 写入 UI 偏好(原子写:临时文件 + rename;自动创建目录)。 */
|
|
26
|
+
export declare function writeUiPrefs(dir: string, prefs: UiPrefs): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* 局部原子更新:read → merge → write。
|
|
29
|
+
* 多端点共写 ui-prefs.json 时(sync/ui-prefs 与 star-prompt),任一端点写全量对象
|
|
30
|
+
* 都会丢掉另一端点刚写入的字段;统一走本函数按补丁合并,杜绝互相覆盖。
|
|
31
|
+
*/
|
|
32
|
+
export declare function updateUiPrefs(dir: string, patch: UiPrefsPatch): Promise<UiPrefs>;
|
|
33
|
+
/** updateUiPrefs 的补丁类型(UiPrefs 全部可选字段,schemaVersion 不可补丁)。 */
|
|
34
|
+
export type UiPrefsPatch = Partial<Omit<UiPrefs, 'schemaVersion'>>;
|