@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync-history 测试:自动同步执行记录读写(sync-history.json)、append 升序、
|
|
3
|
+
* 读不存在 → 空、损坏拒绝、裁剪 N 条。
|
|
4
|
+
*/
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readSyncHistory, appendAutosyncEntry, SYNC_HISTORY_FILE,
|
|
13
|
+
AUTOSYNC_HISTORY_KEEP,
|
|
14
|
+
} from './sync-history.ts';
|
|
15
|
+
import type { AutosyncHistoryEntry } from './sync-history.ts';
|
|
16
|
+
import type { SectionId } from '../schema/types.ts';
|
|
17
|
+
|
|
18
|
+
function makeEntry(seed: number, status: AutosyncHistoryEntry['status']): AutosyncHistoryEntry {
|
|
19
|
+
return {
|
|
20
|
+
direction: 'both',
|
|
21
|
+
status,
|
|
22
|
+
appliedSections: (seed % 2 === 0 ? ['settings'] : []) as SectionId[],
|
|
23
|
+
createdAt: new Date(2026, 7, 16, 12, seed).toISOString(),
|
|
24
|
+
failureCountAtRun: 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('appendAutosyncEntry:追加记录 → readSyncHistory 读回升序列表', async () => {
|
|
29
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-history-append-'));
|
|
30
|
+
try {
|
|
31
|
+
await appendAutosyncEntry(dir, makeEntry(0, 'success'));
|
|
32
|
+
await appendAutosyncEntry(dir, makeEntry(1, 'skipped'));
|
|
33
|
+
const hist = await readSyncHistory(dir);
|
|
34
|
+
assert.equal(hist.schemaVersion, 1);
|
|
35
|
+
assert.equal(hist.autosyncEntries.length, 2);
|
|
36
|
+
assert.equal(hist.autosyncEntries[0]!.status, 'success');
|
|
37
|
+
assert.equal(hist.autosyncEntries[1]!.status, 'skipped');
|
|
38
|
+
// updatedAt 非空
|
|
39
|
+
assert.ok(hist.updatedAt !== '', 'updatedAt 已写入');
|
|
40
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('readSyncHistory:文件不存在 → 返回空列表(schemaVersion=1)', async () => {
|
|
44
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-history-missing-'));
|
|
45
|
+
try {
|
|
46
|
+
const hist = await readSyncHistory(dir);
|
|
47
|
+
assert.equal(hist.schemaVersion, 1);
|
|
48
|
+
assert.deepEqual(hist.autosyncEntries, []);
|
|
49
|
+
assert.equal(hist.updatedAt, '');
|
|
50
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('readSyncHistory:损坏 JSON → 抛错', async () => {
|
|
54
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-history-corrupt-'));
|
|
55
|
+
try {
|
|
56
|
+
await fs.writeFile(path.join(dir, SYNC_HISTORY_FILE), '{not-json', 'utf8');
|
|
57
|
+
await assert.rejects(() => readSyncHistory(dir), /损坏/);
|
|
58
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('appendAutosyncEntry:记录触发通道(transport git/webdav)→ 读回保留', async () => {
|
|
62
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-history-channel-'));
|
|
63
|
+
try {
|
|
64
|
+
await appendAutosyncEntry(dir, { ...makeEntry(0, 'success'), transport: 'git' });
|
|
65
|
+
await appendAutosyncEntry(dir, { ...makeEntry(1, 'skipped'), transport: 'webdav' });
|
|
66
|
+
const hist = await readSyncHistory(dir);
|
|
67
|
+
assert.equal(hist.autosyncEntries[0]!.transport, 'git');
|
|
68
|
+
assert.equal(hist.autosyncEntries[1]!.transport, 'webdav');
|
|
69
|
+
// 旧记录(无字段)读回 → undefined(向后兼容)
|
|
70
|
+
const raw = await fs.readFile(path.join(dir, SYNC_HISTORY_FILE), 'utf8');
|
|
71
|
+
void raw;
|
|
72
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('readSyncHistory:旧记录无 transport 字段 → 读回 undefined(向后兼容)', async () => {
|
|
76
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-history-legacy-'));
|
|
77
|
+
try {
|
|
78
|
+
await fs.writeFile(
|
|
79
|
+
path.join(dir, SYNC_HISTORY_FILE),
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
autosyncEntries: [{ direction: 'push', status: 'success', createdAt: '2026-08-16T12:00:00.000Z', failureCountAtRun: 0 }],
|
|
83
|
+
updatedAt: '2026-08-16T12:00:00.000Z',
|
|
84
|
+
}),
|
|
85
|
+
'utf8',
|
|
86
|
+
);
|
|
87
|
+
const hist = await readSyncHistory(dir);
|
|
88
|
+
assert.equal(hist.autosyncEntries[0]!.transport, undefined);
|
|
89
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('appendAutosyncEntry:裁剪到 AUTOSYNC_HISTORY_KEEP 条', async () => {
|
|
93
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-history-prune-'));
|
|
94
|
+
try {
|
|
95
|
+
// 预置 250 条
|
|
96
|
+
for (let i = 0; i < AUTOSYNC_HISTORY_KEEP + 50; i++) {
|
|
97
|
+
await appendAutosyncEntry(dir, makeEntry(i, 'success'));
|
|
98
|
+
}
|
|
99
|
+
const hist = await readSyncHistory(dir);
|
|
100
|
+
assert.ok(hist.autosyncEntries.length <= AUTOSYNC_HISTORY_KEEP, `裁剪后不超过 ${AUTOSYNC_HISTORY_KEEP} 条`);
|
|
101
|
+
assert.equal(hist.autosyncEntries.length, AUTOSYNC_HISTORY_KEEP);
|
|
102
|
+
// 保留的是最新的(末尾的 createdAt 最大)
|
|
103
|
+
const first = hist.autosyncEntries[0]!;
|
|
104
|
+
const last = hist.autosyncEntries[hist.autosyncEntries.length - 1]!;
|
|
105
|
+
assert.ok(first.createdAt <= last.createdAt, '升序排列,最新的在末尾');
|
|
106
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('appendAutosyncEntry:原子写(不残留 .tmp 文件)', async () => {
|
|
110
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-history-atomic-'));
|
|
111
|
+
try {
|
|
112
|
+
await appendAutosyncEntry(dir, makeEntry(0, 'failed'));
|
|
113
|
+
const files = await fs.readdir(dir);
|
|
114
|
+
assert.ok(!files.some((f) => f.includes('.tmp')), '不应残留 .tmp 临时文件');
|
|
115
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
116
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
|
|
15
|
+
import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.ts';
|
|
16
|
+
import { atomicWriteFile } from '../utils/atomic-write.ts';
|
|
17
|
+
import type { SectionId } from '../schema/types.ts';
|
|
18
|
+
import type { SyncTransportType } from './sync-config.ts';
|
|
19
|
+
|
|
20
|
+
export const SYNC_HISTORY_FILE = 'sync-history.json';
|
|
21
|
+
export const SYNC_HISTORY_SCHEMA_VERSION = 1;
|
|
22
|
+
/** 自动同步历史保留上限 */
|
|
23
|
+
export const AUTOSYNC_HISTORY_KEEP = 200;
|
|
24
|
+
|
|
25
|
+
/** 自动同步执行记录(§3.7 AutosyncHistoryEntry) */
|
|
26
|
+
export interface AutosyncHistoryEntry {
|
|
27
|
+
/** 触发该次运行的同步通道(git / webdav;旧记录缺省 undefined) */
|
|
28
|
+
transport?: SyncTransportType;
|
|
29
|
+
/** 执行方向 */
|
|
30
|
+
direction: 'pull' | 'push' | 'both';
|
|
31
|
+
status: 'success' | 'skipped' | 'failed' | 'partial';
|
|
32
|
+
/** 跳过原因(冲突项 / 缺失依赖 / Install / 错误 / 无远端 / 网络) */
|
|
33
|
+
skipReason?: string;
|
|
34
|
+
/** 被跳过的冲突分区 id(冲突跳过时列出) */
|
|
35
|
+
conflictedSections?: SectionId[];
|
|
36
|
+
/** 本次自动合并实际写入的分区 */
|
|
37
|
+
appliedSections?: SectionId[];
|
|
38
|
+
/** 本次 push 产生的快照 id(direction 含 push 时) */
|
|
39
|
+
pushedSnapshotId?: string;
|
|
40
|
+
/** 本次 pull 来源快照 id */
|
|
41
|
+
pulledSnapshotId?: string;
|
|
42
|
+
/** failed 时的错误摘要(脱敏) */
|
|
43
|
+
error?: string;
|
|
44
|
+
/** 连续失败 3 次通知时间(ISO-8601 UTC) */
|
|
45
|
+
notifiedAt?: string;
|
|
46
|
+
/** 本次触发时的连续失败计数 */
|
|
47
|
+
failureCountAtRun: number;
|
|
48
|
+
/** 记录创建时间(ISO-8601 UTC) */
|
|
49
|
+
createdAt: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** sync-history.json 文件结构 */
|
|
53
|
+
export interface SyncHistoryFile {
|
|
54
|
+
schemaVersion: number;
|
|
55
|
+
autosyncEntries: AutosyncHistoryEntry[];
|
|
56
|
+
updatedAt: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 读取同步历史;文件不存在 → 空列表(schemaVersion=1)。损坏 JSON 抛错。 */
|
|
60
|
+
export async function readSyncHistory(dir: string): Promise<SyncHistoryFile> {
|
|
61
|
+
const file = path.join(dir, SYNC_HISTORY_FILE);
|
|
62
|
+
let raw: string;
|
|
63
|
+
try {
|
|
64
|
+
raw = await fs.readFile(file, 'utf8');
|
|
65
|
+
} catch {
|
|
66
|
+
return { schemaVersion: SYNC_HISTORY_SCHEMA_VERSION, autosyncEntries: [], updatedAt: '' };
|
|
67
|
+
}
|
|
68
|
+
let parsed: unknown;
|
|
69
|
+
try {
|
|
70
|
+
parsed = parseJsonSafe(raw);
|
|
71
|
+
} catch {
|
|
72
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:JSON 解析失败`);
|
|
73
|
+
}
|
|
74
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
75
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:必须是对象`);
|
|
76
|
+
}
|
|
77
|
+
const obj = parsed as Record<string, unknown>;
|
|
78
|
+
if (obj['schemaVersion'] !== SYNC_HISTORY_SCHEMA_VERSION) {
|
|
79
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:schemaVersion 必须是 ${SYNC_HISTORY_SCHEMA_VERSION}`);
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(obj['autosyncEntries'])) {
|
|
82
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:autosyncEntries 必须是数组`);
|
|
83
|
+
}
|
|
84
|
+
const entries: AutosyncHistoryEntry[] = [];
|
|
85
|
+
for (const it of obj['autosyncEntries']) {
|
|
86
|
+
if (it === null || typeof it !== 'object' || Array.isArray(it)) {
|
|
87
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:每个 entry 必须是对象`);
|
|
88
|
+
}
|
|
89
|
+
const e = it as Record<string, unknown>;
|
|
90
|
+
if (typeof e['direction'] !== 'string' || typeof e['status'] !== 'string' || typeof e['createdAt'] !== 'string') {
|
|
91
|
+
throw new Error(`${SYNC_HISTORY_FILE} 损坏:entry 必须含字符串 direction/status/createdAt`);
|
|
92
|
+
}
|
|
93
|
+
entries.push(it as unknown as AutosyncHistoryEntry);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: SYNC_HISTORY_SCHEMA_VERSION,
|
|
97
|
+
autosyncEntries: entries,
|
|
98
|
+
updatedAt: typeof obj['updatedAt'] === 'string' ? obj['updatedAt'] : '',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 追加一条自动同步执行记录(升序),裁剪到 AUTOSYNC_HISTORY_KEEP 条,原子写。 */
|
|
103
|
+
export async function appendAutosyncEntry(
|
|
104
|
+
dir: string,
|
|
105
|
+
entry: AutosyncHistoryEntry,
|
|
106
|
+
now: () => Date = () => new Date(),
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
const current = await readSyncHistory(dir);
|
|
109
|
+
current.autosyncEntries.push(entry);
|
|
110
|
+
// 裁剪:保留最近 AUTOSYNC_HISTORY_KEEP 条(升序 → 去掉最前的超限条)
|
|
111
|
+
if (current.autosyncEntries.length > AUTOSYNC_HISTORY_KEEP) {
|
|
112
|
+
current.autosyncEntries = current.autosyncEntries.slice(current.autosyncEntries.length - AUTOSYNC_HISTORY_KEEP);
|
|
113
|
+
}
|
|
114
|
+
current.updatedAt = now().toISOString();
|
|
115
|
+
|
|
116
|
+
await fs.mkdir(dir, { recursive: true });
|
|
117
|
+
const target = path.join(dir, SYNC_HISTORY_FILE);
|
|
118
|
+
const data = stringifyJsonSafe(current, { space: 2 });
|
|
119
|
+
await atomicWriteFile(target, data, { mode: 0o600 });
|
|
120
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync-selection 测试:sync-selection.json 读写往返、缺省值、损坏 JSON 回退缺省、
|
|
3
|
+
* 非字符串 sections 过滤、effectiveSections(高级模式 → 勾选分区;其余 → undefined)、
|
|
4
|
+
* 按通道独立(git/webdav 互不干扰)、v1(顶层单通道)迁移为 git。
|
|
5
|
+
*/
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
defaultSyncSelection, effectiveSections, readSyncSelection, writeSyncSelection,
|
|
14
|
+
readAllSyncSelections, SYNC_SELECTION_FILE, SYNC_SELECTION_SCHEMA_VERSION,
|
|
15
|
+
} from './sync-selection.ts';
|
|
16
|
+
|
|
17
|
+
test('writeSyncSelection + readSyncSelection:advanced 模式写入 → 读回字段一致(git 通道)', async () => {
|
|
18
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-rt-'));
|
|
19
|
+
try {
|
|
20
|
+
await writeSyncSelection(dir, 'git', { schemaVersion: SYNC_SELECTION_SCHEMA_VERSION, mode: 'advanced', sections: ['settings', 'skills'] });
|
|
21
|
+
const sel = await readSyncSelection(dir, 'git');
|
|
22
|
+
assert.equal(sel.mode, 'advanced');
|
|
23
|
+
assert.deepEqual(sel.sections, ['settings', 'skills']);
|
|
24
|
+
// 原始文件校验(v2 按通道)
|
|
25
|
+
const raw = JSON.parse(await fs.readFile(path.join(dir, SYNC_SELECTION_FILE), 'utf8'));
|
|
26
|
+
assert.equal(raw.schemaVersion, SYNC_SELECTION_SCHEMA_VERSION);
|
|
27
|
+
assert.equal(raw.channels.git.mode, 'advanced');
|
|
28
|
+
assert.deepEqual(raw.channels.git.sections, ['settings', 'skills']);
|
|
29
|
+
assert.equal(raw.channels.webdav.mode, 'default', '未配置的 webdav 通道落盘为缺省');
|
|
30
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('readSyncSelection:文件不存在 → 返回缺省(default 模式 + 空 sections)', async () => {
|
|
34
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-default-'));
|
|
35
|
+
try {
|
|
36
|
+
const sel = await readSyncSelection(dir, 'git');
|
|
37
|
+
assert.equal(sel.mode, 'default');
|
|
38
|
+
assert.deepEqual(sel.sections, []);
|
|
39
|
+
const webdav = await readSyncSelection(dir, 'webdav');
|
|
40
|
+
assert.equal(webdav.mode, 'default');
|
|
41
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('readSyncSelection:损坏 JSON → 回退缺省', async () => {
|
|
45
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-corrupt-'));
|
|
46
|
+
try {
|
|
47
|
+
await fs.writeFile(path.join(dir, SYNC_SELECTION_FILE), '{not-json', 'utf8');
|
|
48
|
+
const sel = await readSyncSelection(dir, 'git');
|
|
49
|
+
assert.equal(sel.mode, 'default');
|
|
50
|
+
assert.deepEqual(sel.sections, []);
|
|
51
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('readSyncSelection:不支持的 schemaVersion → 回退缺省', async () => {
|
|
55
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-schema-'));
|
|
56
|
+
try {
|
|
57
|
+
await fs.writeFile(
|
|
58
|
+
path.join(dir, SYNC_SELECTION_FILE),
|
|
59
|
+
JSON.stringify({ schemaVersion: 99, mode: 'advanced', sections: ['settings'] }),
|
|
60
|
+
'utf8',
|
|
61
|
+
);
|
|
62
|
+
const sel = await readSyncSelection(dir, 'git');
|
|
63
|
+
assert.equal(sel.mode, 'default');
|
|
64
|
+
assert.deepEqual(sel.sections, []);
|
|
65
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('readSyncSelection:非法 mode / 非字符串 sections 元素 → 过滤回退', async () => {
|
|
69
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-filter-'));
|
|
70
|
+
try {
|
|
71
|
+
await fs.writeFile(
|
|
72
|
+
path.join(dir, SYNC_SELECTION_FILE),
|
|
73
|
+
JSON.stringify({ schemaVersion: 1, channels: { git: { mode: 'bogus', sections: ['settings', 42, '', 'skills'] } } }),
|
|
74
|
+
'utf8',
|
|
75
|
+
);
|
|
76
|
+
const sel = await readSyncSelection(dir, 'git');
|
|
77
|
+
assert.equal(sel.mode, 'default', '非法 mode 回退 default');
|
|
78
|
+
assert.deepEqual(sel.sections, ['settings', 'skills'], '非字符串/空串元素被过滤');
|
|
79
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('effectiveSections:advanced + 非空 → 勾选分区;default / advanced 空勾选 → undefined(全量)', () => {
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
effectiveSections({ schemaVersion: SYNC_SELECTION_SCHEMA_VERSION, mode: 'advanced', sections: ['settings', 'skills'] }),
|
|
85
|
+
['settings', 'skills'],
|
|
86
|
+
);
|
|
87
|
+
assert.equal(effectiveSections(defaultSyncSelection()), undefined, 'default 模式 = 全量推荐分区');
|
|
88
|
+
assert.equal(
|
|
89
|
+
effectiveSections({ schemaVersion: SYNC_SELECTION_SCHEMA_VERSION, mode: 'advanced', sections: [] }),
|
|
90
|
+
undefined,
|
|
91
|
+
'advanced 但未勾选 → 回退全量(避免自动同步卡死)',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('readSyncSelection:旧字段(encrypt/includeSecrets)被忽略,只读 mode + sections', async () => {
|
|
96
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-safe-'));
|
|
97
|
+
try {
|
|
98
|
+
await fs.writeFile(
|
|
99
|
+
path.join(dir, SYNC_SELECTION_FILE),
|
|
100
|
+
JSON.stringify({ schemaVersion: 1, channels: { git: { mode: 'advanced', sections: ['settings'], encrypt: true, includeSecrets: true } } }),
|
|
101
|
+
'utf8',
|
|
102
|
+
);
|
|
103
|
+
const sel = await readSyncSelection(dir, 'git');
|
|
104
|
+
assert.equal(sel.mode, 'advanced');
|
|
105
|
+
assert.deepEqual(sel.sections, ['settings']);
|
|
106
|
+
assert.equal('encrypt' in sel, false, '旧字段不再出现在读取结果');
|
|
107
|
+
assert.equal('includeSecrets' in sel, false);
|
|
108
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('readSyncSelection:升级路径 —— 上游 v2 文件(含 encrypt/includeSecrets)→ 保留用户勾选', async () => {
|
|
112
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-upstream-v2-'));
|
|
113
|
+
try {
|
|
114
|
+
// 逐字取自上游 dsh-config-manager@0.1.60 实际落盘的文件形状:
|
|
115
|
+
// 同一 channels 信封,多出 encrypt / includeSecrets 两个已废弃字段。
|
|
116
|
+
await fs.writeFile(
|
|
117
|
+
path.join(dir, SYNC_SELECTION_FILE),
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
schemaVersion: 2,
|
|
120
|
+
channels: {
|
|
121
|
+
git: { mode: 'advanced', sections: ['settings', 'providers', 'prompts'], encrypt: true, includeSecrets: true },
|
|
122
|
+
webdav: { mode: 'advanced', sections: ['settings', 'skills'], encrypt: true, includeSecrets: true },
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
'utf8',
|
|
126
|
+
);
|
|
127
|
+
const git = await readSyncSelection(dir, 'git');
|
|
128
|
+
assert.equal(git.mode, 'advanced', '上游 v2 的 git 勾选必须被保留,不得回退缺省');
|
|
129
|
+
assert.deepEqual(git.sections, ['settings', 'providers', 'prompts']);
|
|
130
|
+
assert.equal('encrypt' in git, false, '已废弃字段不进入读取结果');
|
|
131
|
+
assert.equal('includeSecrets' in git, false);
|
|
132
|
+
const webdav = await readSyncSelection(dir, 'webdav');
|
|
133
|
+
assert.equal(webdav.mode, 'advanced', '上游 v2 的 webdav 勾选必须被保留');
|
|
134
|
+
assert.deepEqual(webdav.sections, ['settings', 'skills']);
|
|
135
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('按通道独立:写 webdav 不影响 git,反之亦然', async () => {
|
|
139
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-perchannel-'));
|
|
140
|
+
try {
|
|
141
|
+
await writeSyncSelection(dir, 'git', { schemaVersion: SYNC_SELECTION_SCHEMA_VERSION, mode: 'default', sections: [] });
|
|
142
|
+
await writeSyncSelection(dir, 'webdav', { schemaVersion: SYNC_SELECTION_SCHEMA_VERSION, mode: 'advanced', sections: ['settings', 'skills'] });
|
|
143
|
+
const git = await readSyncSelection(dir, 'git');
|
|
144
|
+
assert.equal(git.mode, 'default', 'git 通道保持 default');
|
|
145
|
+
const webdav = await readSyncSelection(dir, 'webdav');
|
|
146
|
+
assert.equal(webdav.mode, 'advanced');
|
|
147
|
+
assert.deepEqual(webdav.sections, ['settings', 'skills']);
|
|
148
|
+
// 全量读取视图
|
|
149
|
+
const all = await readAllSyncSelections(dir);
|
|
150
|
+
assert.equal(all.git.mode, 'default');
|
|
151
|
+
assert.equal(all.webdav.mode, 'advanced');
|
|
152
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('readSyncSelection:上游 v1 顶层单通道形状 → 归 git 通道,webdav 缺省', async () => {
|
|
156
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-upstream-v1-'));
|
|
157
|
+
try {
|
|
158
|
+
// 上游 v1:整个文件就是一个通道配置,没有 channels 信封。
|
|
159
|
+
await fs.writeFile(
|
|
160
|
+
path.join(dir, SYNC_SELECTION_FILE),
|
|
161
|
+
JSON.stringify({ schemaVersion: 1, mode: 'advanced', sections: ['settings', 'mcp'] }),
|
|
162
|
+
'utf8',
|
|
163
|
+
);
|
|
164
|
+
const git = await readSyncSelection(dir, 'git');
|
|
165
|
+
assert.equal(git.mode, 'advanced');
|
|
166
|
+
assert.deepEqual(git.sections, ['settings', 'mcp']);
|
|
167
|
+
const webdav = await readSyncSelection(dir, 'webdav');
|
|
168
|
+
assert.equal(webdav.mode, 'default', 'v1 无 webdav 信息 → 缺省');
|
|
169
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('非 channels 形状(顶层单通道)→ 回退缺省(schemaVersion 不匹配)', async () => {
|
|
173
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-sync-selection-v1-'));
|
|
174
|
+
try {
|
|
175
|
+
await fs.writeFile(
|
|
176
|
+
path.join(dir, SYNC_SELECTION_FILE),
|
|
177
|
+
JSON.stringify({ schemaVersion: 0, mode: 'advanced', sections: ['settings'] }),
|
|
178
|
+
'utf8',
|
|
179
|
+
);
|
|
180
|
+
const git = await readSyncSelection(dir, 'git');
|
|
181
|
+
assert.equal(git.mode, 'default', '不支持 schema → 回退缺省');
|
|
182
|
+
assert.deepEqual(git.sections, []);
|
|
183
|
+
const webdav = await readSyncSelection(dir, 'webdav');
|
|
184
|
+
assert.equal(webdav.mode, 'default', 'webdav 通道回退缺省');
|
|
185
|
+
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
186
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
|
|
32
|
+
import type { SectionId } from '../schema/types.ts';
|
|
33
|
+
import type { SyncTransportType } from './sync-config.ts';
|
|
34
|
+
import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.ts';
|
|
35
|
+
import { atomicWriteFile } from '../utils/atomic-write.ts';
|
|
36
|
+
|
|
37
|
+
export const SYNC_SELECTION_FILE = 'sync-selection.json';
|
|
38
|
+
export const SYNC_SELECTION_SCHEMA_VERSION = 2;
|
|
39
|
+
/** 可读的版本:1 = 上游顶层单通道(或早期 channels 信封);2 = 本信封(含上游 0.1.60)。 */
|
|
40
|
+
export const SYNC_SELECTION_SUPPORTED_VERSIONS: readonly number[] = [1, 2];
|
|
41
|
+
|
|
42
|
+
/** 远程同步分区选择模式:default = 全部推荐分区;advanced = 自定义勾选。 */
|
|
43
|
+
export type SyncSelectionMode = 'default' | 'advanced';
|
|
44
|
+
|
|
45
|
+
/** 远程同步分区选择(持久化面;单通道)。 */
|
|
46
|
+
export interface SyncSelection {
|
|
47
|
+
schemaVersion: number;
|
|
48
|
+
mode: SyncSelectionMode;
|
|
49
|
+
/** 高级模式勾选分区;default 模式可为空数组 */
|
|
50
|
+
sections: SectionId[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 缺省配置(首次无文件 / 损坏 / 不支持 schema 时回退) */
|
|
54
|
+
export function defaultSyncSelection(): SyncSelection {
|
|
55
|
+
return { schemaVersion: SYNC_SELECTION_SCHEMA_VERSION, mode: 'default', sections: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 生效的同步分区范围:
|
|
60
|
+
* - mode='advanced' 且 sections 非空 → sections(自定义同步);
|
|
61
|
+
* - 其余(default / advanced 但未勾选)→ undefined(= 全部推荐分区)。
|
|
62
|
+
*/
|
|
63
|
+
export function effectiveSections(sel: SyncSelection): SectionId[] | undefined {
|
|
64
|
+
if (sel.mode === 'advanced' && sel.sections.length > 0) return [...sel.sections];
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 从单通道对象解析(非法字段回退缺省)。 */
|
|
69
|
+
function parseChannelSelection(obj: Record<string, unknown>): SyncSelection {
|
|
70
|
+
const sel = defaultSyncSelection();
|
|
71
|
+
if (obj['mode'] === 'advanced' || obj['mode'] === 'default') sel.mode = obj['mode'];
|
|
72
|
+
if (Array.isArray(obj['sections'])) {
|
|
73
|
+
sel.sections = obj['sections'].filter(
|
|
74
|
+
(s): s is SectionId => typeof s === 'string' && s !== '',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return sel;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 读取全部通道的分区选择配置;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
|
|
81
|
+
export async function readAllSyncSelections(dir: string): Promise<Record<SyncTransportType, SyncSelection>> {
|
|
82
|
+
const fallback = (): Record<SyncTransportType, SyncSelection> => ({
|
|
83
|
+
git: defaultSyncSelection(),
|
|
84
|
+
webdav: defaultSyncSelection(),
|
|
85
|
+
});
|
|
86
|
+
const file = path.join(dir, SYNC_SELECTION_FILE);
|
|
87
|
+
let raw: string;
|
|
88
|
+
try {
|
|
89
|
+
raw = await fs.readFile(file, 'utf8');
|
|
90
|
+
} catch {
|
|
91
|
+
return fallback();
|
|
92
|
+
}
|
|
93
|
+
let parsed: unknown;
|
|
94
|
+
try {
|
|
95
|
+
parsed = parseJsonSafe(raw);
|
|
96
|
+
} catch {
|
|
97
|
+
return fallback();
|
|
98
|
+
}
|
|
99
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return fallback();
|
|
100
|
+
const obj = parsed as Record<string, unknown>;
|
|
101
|
+
// 缺 schemaVersion 视为 v1(与上游一致);不支持的版本回退缺省。
|
|
102
|
+
const ver = typeof obj['schemaVersion'] === 'number' ? obj['schemaVersion'] : 1;
|
|
103
|
+
if (!SYNC_SELECTION_SUPPORTED_VERSIONS.includes(ver)) return fallback();
|
|
104
|
+
const channels = obj['channels'];
|
|
105
|
+
if (channels === null || typeof channels !== 'object' || Array.isArray(channels)) {
|
|
106
|
+
// v1 顶层单通道形状:整个对象就是一个通道配置(归 git),与上游迁移语义一致。
|
|
107
|
+
// v2 缺 channels 信封属于损坏 → 回退缺省。
|
|
108
|
+
if (ver === 1) return { git: parseChannelSelection(obj), webdav: defaultSyncSelection() };
|
|
109
|
+
return fallback();
|
|
110
|
+
}
|
|
111
|
+
const ch = channels as Record<string, unknown>;
|
|
112
|
+
const pick = (ns: unknown): SyncSelection =>
|
|
113
|
+
ns !== null && typeof ns === 'object' && !Array.isArray(ns)
|
|
114
|
+
? parseChannelSelection(ns as Record<string, unknown>)
|
|
115
|
+
: defaultSyncSelection();
|
|
116
|
+
return { git: pick(ch['git']), webdav: pick(ch['webdav']) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 读取指定通道的分区选择配置;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
|
|
120
|
+
export async function readSyncSelection(dir: string, channel: SyncTransportType): Promise<SyncSelection> {
|
|
121
|
+
const all = await readAllSyncSelections(dir);
|
|
122
|
+
return all[channel];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 写入指定通道的分区选择配置(原子写:临时文件 + rename;保留另一通道;自动创建目录)。 */
|
|
126
|
+
export async function writeSyncSelection(dir: string, channel: SyncTransportType, sel: SyncSelection): Promise<void> {
|
|
127
|
+
await fs.mkdir(dir, { recursive: true });
|
|
128
|
+
const existing = await readAllSyncSelections(dir);
|
|
129
|
+
const channels: Record<SyncTransportType, SyncSelection> = {
|
|
130
|
+
git: channel === 'git' ? sel : existing.git,
|
|
131
|
+
webdav: channel === 'webdav' ? sel : existing.webdav,
|
|
132
|
+
};
|
|
133
|
+
const payload: Record<string, unknown> = {
|
|
134
|
+
schemaVersion: SYNC_SELECTION_SCHEMA_VERSION,
|
|
135
|
+
channels: {
|
|
136
|
+
git: { mode: channels.git.mode, sections: channels.git.sections },
|
|
137
|
+
webdav: { mode: channels.webdav.mode, sections: channels.webdav.sections },
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const target = path.join(dir, SYNC_SELECTION_FILE);
|
|
141
|
+
const data = stringifyJsonSafe(payload, { space: 2 });
|
|
142
|
+
await atomicWriteFile(target, data, { mode: 0o600 });
|
|
143
|
+
}
|