@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,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置状态快照(Phase 1 P0-1 / P0-2 的持久化与回放层)。
|
|
3
|
+
*
|
|
4
|
+
* 与既有 `core/backup.ts` 的 `Snapshot` 的分工:
|
|
5
|
+
* - `Snapshot`(backup.ts):**导入计划驱动**,只登记「本次导入将写入的目标」的原值,
|
|
6
|
+
* 用于导入失败回滚。它不描述「整体配置在某时刻是什么样」。
|
|
7
|
+
* - `ConfigSnapshot`(本模块):**状态驱动**,记录整份配置在某一时刻的分区数据 + 分区指纹,
|
|
8
|
+
* 用于自动快照、撤销/重做、最后正常状态定位。恢复方式是**回放 adapter**
|
|
9
|
+
* (validate → analyzeImport → applyItem,与导入/Profile 切换同一条管线),
|
|
10
|
+
* 因此天然覆盖 settings 命名空间、patch 行、文件类分区与插件安装。
|
|
11
|
+
*
|
|
12
|
+
* 存储布局:`<dir>/<id>/config-snapshot.json` = `{ meta, data }`。
|
|
13
|
+
* `data` 为各分区的 ExportSection.data;其中的 Uint8Array(文件类分区的字节)
|
|
14
|
+
* 经 `__u8` base64 包封做可逆 JSON 编码(JSON.stringify 会把字节变成 {0:..,1:..})。
|
|
15
|
+
*
|
|
16
|
+
* 安全:快照 id 严格白名单校验(防穿越);写入走 atomicWriteFile;分区数据只含
|
|
17
|
+
* `includeSecrets: false` 的导出结果,密钥值从不进入本模块。
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { atomicWriteFile } from '../utils/atomic-write.ts';
|
|
22
|
+
import { parseJsonSafe } from '../utils/json.ts';
|
|
23
|
+
import { SECTION_IDS } from '../schema/config.ts';
|
|
24
|
+
import { CURRENT_SCHEMA_VERSION } from '../schema/versions.ts';
|
|
25
|
+
import { msgOf, zhMsg } from './messages.ts';
|
|
26
|
+
import type { MsgFunc } from './messages.ts';
|
|
27
|
+
import { isValidSnapshotId } from './restore.ts';
|
|
28
|
+
import { statesEqual, type ConfigState } from './config-state.ts';
|
|
29
|
+
import type { ConfigSnapshotKind, UndoCandidate } from './undo.ts';
|
|
30
|
+
import type { ConfigAdapter, ExportSection, HostContext, ImportContext } from './types.ts';
|
|
31
|
+
import type { Manifest, SectionId } from '../schema/types.ts';
|
|
32
|
+
|
|
33
|
+
/** 快照文件名(每个快照一个目录) */
|
|
34
|
+
export const CONFIG_SNAPSHOT_FILE = 'config-snapshot.json';
|
|
35
|
+
|
|
36
|
+
/** 可逆 JSON 编码:Uint8Array → { __u8: base64 } */
|
|
37
|
+
const U8_KEY = '__u8';
|
|
38
|
+
|
|
39
|
+
function encodeForJson(value: unknown): unknown {
|
|
40
|
+
if (value instanceof Uint8Array) return { [U8_KEY]: Buffer.from(value).toString('base64') };
|
|
41
|
+
if (Array.isArray(value)) return value.map(encodeForJson);
|
|
42
|
+
if (value !== null && typeof value === 'object') {
|
|
43
|
+
const out: Record<string, unknown> = {};
|
|
44
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[k] = encodeForJson(v);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function decodeFromJson(value: unknown): unknown {
|
|
51
|
+
if (Array.isArray(value)) return value.map(decodeFromJson);
|
|
52
|
+
if (value !== null && typeof value === 'object') {
|
|
53
|
+
const rec = value as Record<string, unknown>;
|
|
54
|
+
const keys = Object.keys(rec);
|
|
55
|
+
if (keys.length === 1 && keys[0] === U8_KEY && typeof rec[U8_KEY] === 'string') {
|
|
56
|
+
return new Uint8Array(Buffer.from(rec[U8_KEY] as string, 'base64'));
|
|
57
|
+
}
|
|
58
|
+
const out: Record<string, unknown> = {};
|
|
59
|
+
for (const [k, v] of Object.entries(rec)) out[k] = decodeFromJson(v);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 快照元数据(落盘部分) */
|
|
66
|
+
export interface ConfigSnapshotMeta {
|
|
67
|
+
id: string;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
kind: ConfigSnapshotKind;
|
|
70
|
+
/** 人类可读原因(如 'plugin-change' / 'before-restore:<id>' / 'manual') */
|
|
71
|
+
reason: string;
|
|
72
|
+
/** 触发源(watcher / route / tool / schedule / undo / baseline) */
|
|
73
|
+
trigger?: string;
|
|
74
|
+
/** 参与采集的分区(升序) */
|
|
75
|
+
sections: SectionId[];
|
|
76
|
+
/** 采集到的配置状态(分区指纹;撤销/重做的内容比对基准) */
|
|
77
|
+
state: ConfigState;
|
|
78
|
+
note?: string;
|
|
79
|
+
tags?: string[];
|
|
80
|
+
pinned?: boolean;
|
|
81
|
+
/** pre-restore:是否已被重做消费 */
|
|
82
|
+
consumed?: boolean;
|
|
83
|
+
/** 被撤销跨过(见 undo.ts steppedIds) */
|
|
84
|
+
stepped?: boolean;
|
|
85
|
+
/** pre-restore:本次撤销的目标快照 id */
|
|
86
|
+
undoOf?: string;
|
|
87
|
+
/** 落盘字节数(快照体积,供 UI 展示与容量告警) */
|
|
88
|
+
totalBytes: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 完整快照(元数据 + 分区数据) */
|
|
92
|
+
export interface ConfigSnapshot extends ConfigSnapshotMeta {
|
|
93
|
+
data: Partial<Record<SectionId, unknown>>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 供 undo.ts 使用的最小候选视图 */
|
|
97
|
+
export function toUndoCandidate(meta: ConfigSnapshotMeta): UndoCandidate {
|
|
98
|
+
return {
|
|
99
|
+
id: meta.id,
|
|
100
|
+
createdAt: meta.createdAt,
|
|
101
|
+
kind: meta.kind,
|
|
102
|
+
state: meta.state,
|
|
103
|
+
consumed: meta.consumed,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ------------------------------------------------------------ 读写 */
|
|
108
|
+
|
|
109
|
+
function snapshotDir(dir: string, id: string): string {
|
|
110
|
+
if (!isValidSnapshotId(id)) throw new Error(`非法配置快照 id: ${JSON.stringify(id)}`);
|
|
111
|
+
const target = path.join(dir, id);
|
|
112
|
+
if (!target.startsWith(path.resolve(dir) + path.sep)) throw new Error(`配置快照路径越界: ${id}`);
|
|
113
|
+
return target;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SaveConfigSnapshotOptions {
|
|
117
|
+
dir: string;
|
|
118
|
+
kind: ConfigSnapshotKind;
|
|
119
|
+
reason: string;
|
|
120
|
+
trigger?: string;
|
|
121
|
+
/** 本次采集的各分区导出结果(adapter.export 产出) */
|
|
122
|
+
sections: Map<SectionId, ExportSection>;
|
|
123
|
+
state: ConfigState;
|
|
124
|
+
note?: string;
|
|
125
|
+
tags?: string[];
|
|
126
|
+
now?: () => Date;
|
|
127
|
+
idFactory?: () => string;
|
|
128
|
+
/** 落盘上限(字节;缺省 64 MiB,超出抛错而不是静默截断) */
|
|
129
|
+
maxBytes?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 生成快照 id:时间戳 + 随机短串(可读 + 唯一;与既有 uuid 风格区分开) */
|
|
133
|
+
export function makeConfigSnapshotId(now: () => Date = () => new Date()): string {
|
|
134
|
+
const d = now();
|
|
135
|
+
const p = (n: number, w = 2): string => String(n).padStart(w, '0');
|
|
136
|
+
const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
137
|
+
const rand = Math.random().toString(36).slice(2, 6).padEnd(4, '0');
|
|
138
|
+
return `${stamp}-${rand}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 写入一个配置快照。返回落盘后的元数据。
|
|
143
|
+
* id 冲突时自动追加后缀重试(同一秒内多次快照)。
|
|
144
|
+
*/
|
|
145
|
+
export async function saveConfigSnapshot(opts: SaveConfigSnapshotOptions): Promise<ConfigSnapshotMeta> {
|
|
146
|
+
const { dir, kind, reason, sections, state } = opts;
|
|
147
|
+
const now = opts.now ?? ((): Date => new Date());
|
|
148
|
+
const idFactory = opts.idFactory ?? ((): string => makeConfigSnapshotId(now));
|
|
149
|
+
const maxBytes = opts.maxBytes ?? 64 * 1024 * 1024;
|
|
150
|
+
|
|
151
|
+
await fs.mkdir(dir, { recursive: true });
|
|
152
|
+
let id = idFactory();
|
|
153
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
await fs.access(path.join(dir, id));
|
|
156
|
+
id = `${idFactory()}-${Math.random().toString(36).slice(2, 5)}`;
|
|
157
|
+
} catch {
|
|
158
|
+
break; // 不存在 → 可用
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const data: Record<string, unknown> = {};
|
|
163
|
+
for (const [sectionId, exported] of sections) {
|
|
164
|
+
data[sectionId] = encodeForJson(exported.data);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sectionsList = [...sections.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
168
|
+
const payload = { meta: null as unknown, data };
|
|
169
|
+
const metaBase: Omit<ConfigSnapshotMeta, 'totalBytes'> = {
|
|
170
|
+
id,
|
|
171
|
+
createdAt: now().toISOString(),
|
|
172
|
+
kind,
|
|
173
|
+
reason,
|
|
174
|
+
...(opts.trigger !== undefined ? { trigger: opts.trigger } : {}),
|
|
175
|
+
sections: sectionsList,
|
|
176
|
+
state,
|
|
177
|
+
...(opts.note !== undefined ? { note: opts.note } : {}),
|
|
178
|
+
...(opts.tags !== undefined ? { tags: opts.tags } : {}),
|
|
179
|
+
};
|
|
180
|
+
payload.meta = metaBase;
|
|
181
|
+
const serialized = JSON.stringify(payload, null, 2);
|
|
182
|
+
const bytes = Buffer.byteLength(serialized);
|
|
183
|
+
if (bytes > maxBytes) {
|
|
184
|
+
throw new Error(`配置快照超出上限(${bytes} > ${maxBytes} 字节);请缩小分区范围后重试`);
|
|
185
|
+
}
|
|
186
|
+
const meta: ConfigSnapshotMeta = { ...metaBase, totalBytes: bytes };
|
|
187
|
+
payload.meta = meta;
|
|
188
|
+
|
|
189
|
+
const finalText = JSON.stringify(payload, null, 2);
|
|
190
|
+
const target = snapshotDir(dir, id);
|
|
191
|
+
await fs.mkdir(target, { recursive: true });
|
|
192
|
+
await atomicWriteFile(path.join(target, CONFIG_SNAPSHOT_FILE), finalText);
|
|
193
|
+
return meta;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* meta.state 形状校验。
|
|
198
|
+
* 语义:
|
|
199
|
+
* - 缺失 / undefined → **放行**(旧格式快照,撤销侧会按「无法比对」剔除,
|
|
200
|
+
* 但它仍应出现在列表里可见/可恢复);
|
|
201
|
+
* - 存在但不是 `{ sections: [...] }` → 判为损坏并整条丢弃:继续信任它会让
|
|
202
|
+
* statesEqual/planUndo 拿到形状不对的对象(历史上会直接抛 TypeError,
|
|
203
|
+
* 一条坏文件就能让整个灾备面板 500)。
|
|
204
|
+
*/
|
|
205
|
+
function isValidStateField(value: unknown): boolean {
|
|
206
|
+
if (value === undefined) return true;
|
|
207
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
208
|
+
return Array.isArray((value as { sections?: unknown }).sections);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 读单个快照的元数据;缺失/损坏/非法 id → null(绝不抛)。 */
|
|
212
|
+
export async function readConfigSnapshotMeta(dir: string, id: string): Promise<ConfigSnapshotMeta | null> {
|
|
213
|
+
if (!isValidSnapshotId(id)) return null;
|
|
214
|
+
try {
|
|
215
|
+
const raw = await fs.readFile(path.join(dir, id, CONFIG_SNAPSHOT_FILE), 'utf8');
|
|
216
|
+
const parsed = parseJsonSafe(raw) as { meta?: ConfigSnapshotMeta } | null;
|
|
217
|
+
const meta = parsed?.meta;
|
|
218
|
+
if (meta === null || meta === undefined || typeof meta !== 'object') return null;
|
|
219
|
+
if (meta.id !== id || typeof meta.createdAt !== 'string' || typeof meta.kind !== 'string') return null;
|
|
220
|
+
if (!isValidStateField((meta as { state?: unknown }).state)) return null;
|
|
221
|
+
return meta;
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 列出全部快照元数据(按 createdAt 倒序;损坏条目跳过,不阻断其余)。 */
|
|
228
|
+
export async function listConfigSnapshots(dir: string): Promise<ConfigSnapshotMeta[]> {
|
|
229
|
+
let entries;
|
|
230
|
+
try {
|
|
231
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
232
|
+
} catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const metas: ConfigSnapshotMeta[] = [];
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
if (!entry.isDirectory()) continue;
|
|
238
|
+
const meta = await readConfigSnapshotMeta(dir, entry.name);
|
|
239
|
+
if (meta !== null) metas.push(meta);
|
|
240
|
+
}
|
|
241
|
+
metas.sort((a, b) => {
|
|
242
|
+
if (a.createdAt !== b.createdAt) return a.createdAt < b.createdAt ? 1 : -1;
|
|
243
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
244
|
+
});
|
|
245
|
+
return metas;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 读完整快照(含分区数据)。缺失/损坏抛出。 */
|
|
249
|
+
export async function loadConfigSnapshot(dir: string, id: string): Promise<ConfigSnapshot> {
|
|
250
|
+
const target = snapshotDir(dir, id);
|
|
251
|
+
const raw = await fs.readFile(path.join(target, CONFIG_SNAPSHOT_FILE), 'utf8');
|
|
252
|
+
const parsed = parseJsonSafe(raw) as { meta?: ConfigSnapshotMeta; data?: Record<string, unknown> } | null;
|
|
253
|
+
const meta = parsed?.meta;
|
|
254
|
+
if (meta === null || meta === undefined || typeof meta !== 'object' || meta.id !== id) {
|
|
255
|
+
throw new Error(`配置快照 ${id} 的 ${CONFIG_SNAPSHOT_FILE} 不是合法对象`);
|
|
256
|
+
}
|
|
257
|
+
const data: Partial<Record<SectionId, unknown>> = {};
|
|
258
|
+
for (const [k, v] of Object.entries(parsed?.data ?? {})) {
|
|
259
|
+
data[k as SectionId] = decodeFromJson(v);
|
|
260
|
+
}
|
|
261
|
+
return { ...meta, data };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 更新的元数据字段(写入时保留分区数据不变)。 */
|
|
265
|
+
export type ConfigSnapshotMetaPatch = Partial<
|
|
266
|
+
Pick<ConfigSnapshotMeta, 'note' | 'tags' | 'pinned' | 'consumed' | 'stepped' | 'undoOf'>
|
|
267
|
+
>;
|
|
268
|
+
|
|
269
|
+
/** 原地更新元数据字段;快照不存在返回 null。 */
|
|
270
|
+
export async function updateConfigSnapshotMeta(
|
|
271
|
+
dir: string, id: string, patch: ConfigSnapshotMetaPatch,
|
|
272
|
+
): Promise<ConfigSnapshotMeta | null> {
|
|
273
|
+
if (!isValidSnapshotId(id)) return null;
|
|
274
|
+
const target = path.join(dir, id, CONFIG_SNAPSHOT_FILE);
|
|
275
|
+
let raw: string;
|
|
276
|
+
try {
|
|
277
|
+
raw = await fs.readFile(target, 'utf8');
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const parsed = parseJsonSafe(raw) as { meta?: ConfigSnapshotMeta; data?: unknown } | null;
|
|
282
|
+
if (parsed?.meta === null || parsed?.meta === undefined) return null;
|
|
283
|
+
const meta: ConfigSnapshotMeta = { ...parsed.meta };
|
|
284
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
285
|
+
if (v === undefined) continue;
|
|
286
|
+
(meta as unknown as Record<string, unknown>)[k] = v;
|
|
287
|
+
}
|
|
288
|
+
await atomicWriteFile(target, JSON.stringify({ meta, data: parsed.data ?? {} }, null, 2));
|
|
289
|
+
return meta;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 删除单个快照(幂等:不存在视为成功)。 */
|
|
293
|
+
export async function deleteConfigSnapshot(dir: string, id: string): Promise<boolean> {
|
|
294
|
+
if (!isValidSnapshotId(id)) throw new Error(`非法配置快照 id: ${JSON.stringify(id)}`);
|
|
295
|
+
const target = snapshotDir(dir, id);
|
|
296
|
+
try {
|
|
297
|
+
await fs.stat(target);
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ------------------------------------------------------------ 保留策略 */
|
|
306
|
+
|
|
307
|
+
export interface ConfigSnapshotPruneOptions {
|
|
308
|
+
/** auto / baseline / undo 三类共用的保留份数(缺省 20) */
|
|
309
|
+
keepAuto?: number;
|
|
310
|
+
/** pre-restore 保留份数(缺省 10) */
|
|
311
|
+
keepPreRestore?: number;
|
|
312
|
+
/** manual 保留份数(缺省 Infinity = 永不自动清理) */
|
|
313
|
+
keepManual?: number;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 保留清理:按 kind 分桶,各自保留最新 N 份,删除更旧的。
|
|
318
|
+
* pinned 快照**一律豁免**(用户显式保留的回滚点不得被自动淘汰)。
|
|
319
|
+
* 返回被删除的快照 id 列表。
|
|
320
|
+
*/
|
|
321
|
+
export async function pruneConfigSnapshots(
|
|
322
|
+
dir: string, opts: ConfigSnapshotPruneOptions = {},
|
|
323
|
+
): Promise<string[]> {
|
|
324
|
+
const keepAuto = opts.keepAuto ?? 20;
|
|
325
|
+
const keepPre = opts.keepPreRestore ?? 10;
|
|
326
|
+
const keepManual = opts.keepManual ?? Number.POSITIVE_INFINITY;
|
|
327
|
+
const metas = (await listConfigSnapshots(dir)).filter((m) => m.pinned !== true);
|
|
328
|
+
const bucket = (kind: ConfigSnapshotKind): number => {
|
|
329
|
+
if (kind === 'pre-restore') return keepPre;
|
|
330
|
+
if (kind === 'manual') return keepManual;
|
|
331
|
+
return keepAuto;
|
|
332
|
+
};
|
|
333
|
+
const grouped = new Map<number, ConfigSnapshotMeta[]>();
|
|
334
|
+
for (const meta of metas) {
|
|
335
|
+
const limit = bucket(meta.kind);
|
|
336
|
+
const list = grouped.get(limit) ?? [];
|
|
337
|
+
list.push(meta);
|
|
338
|
+
grouped.set(limit, list);
|
|
339
|
+
}
|
|
340
|
+
// metas 已按 createdAt 倒序 → 每桶内前 limit 个保留,其余删除
|
|
341
|
+
const removed: string[] = [];
|
|
342
|
+
for (const [limit, list] of grouped) {
|
|
343
|
+
if (!Number.isFinite(limit)) continue;
|
|
344
|
+
for (const meta of list.slice(Math.max(0, limit))) {
|
|
345
|
+
await fs.rm(path.join(dir, meta.id), { recursive: true, force: true });
|
|
346
|
+
removed.push(meta.id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return removed;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ------------------------------------------------------------ 回放恢复 */
|
|
353
|
+
|
|
354
|
+
/** 构建回放用 manifest(与 profiles/profile-manager.ts 的 buildProfileManifest 同构) */
|
|
355
|
+
function buildReplayManifest(ctx: HostContext): Manifest {
|
|
356
|
+
const sections: Record<string, boolean> = {};
|
|
357
|
+
for (const id of SECTION_IDS) sections[id] = false;
|
|
358
|
+
return {
|
|
359
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
360
|
+
exporter: { name: 'DSH Config Manager', version: '0.1.0' },
|
|
361
|
+
source: {
|
|
362
|
+
dshVersion: ctx.dshVersion,
|
|
363
|
+
platform: ctx.platform as Manifest['source']['platform'],
|
|
364
|
+
arch: ctx.arch,
|
|
365
|
+
},
|
|
366
|
+
exportedAt: new Date().toISOString(),
|
|
367
|
+
sections: sections as Manifest['sections'],
|
|
368
|
+
security: { containsSecrets: false, encrypted: false, encryption: null },
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface RestoreConfigSnapshotOptions {
|
|
373
|
+
dir: string;
|
|
374
|
+
id: string;
|
|
375
|
+
adapters: readonly ConfigAdapter[];
|
|
376
|
+
ctx: HostContext;
|
|
377
|
+
/**
|
|
378
|
+
* 分区应用顺序(缺省 = adapters 传入顺序)。宿主应传入与导入管线一致的顺序
|
|
379
|
+
* (副作用大的 patch/安装最后),避免半途失败留下不一致的中间态。
|
|
380
|
+
*/
|
|
381
|
+
applyOrder?: readonly SectionId[];
|
|
382
|
+
/** 只回放这些分区(缺省 = 快照内的全部分区) */
|
|
383
|
+
only?: readonly SectionId[];
|
|
384
|
+
msg?: MsgFunc;
|
|
385
|
+
/** 逐项进度回调(UI 进度条) */
|
|
386
|
+
onItem?: (info: { index: number; total: number; detail: string }) => void;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface ConfigSnapshotRestoreReport {
|
|
390
|
+
ok: boolean;
|
|
391
|
+
snapshotId: string;
|
|
392
|
+
/** 成功应用的计划项 `${section}:${itemId}` */
|
|
393
|
+
applied: string[];
|
|
394
|
+
/** 跳过(Skip/Warning/MissingSecret 等非破坏性项) */
|
|
395
|
+
skipped: string[];
|
|
396
|
+
failed: { item: string; reason: string }[];
|
|
397
|
+
/** 校验不通过而整体跳过的分区 */
|
|
398
|
+
invalidSections: { section: SectionId; reason: string }[];
|
|
399
|
+
needsRestart: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** 非破坏性计划项:不调用 applyItem(与导入管线的「可执行 kinds」口径一致) */
|
|
403
|
+
const NON_EXECUTABLE_KINDS = new Set(['Skip', 'Warning', 'MissingSecret', 'MissingDependency', 'PathMapping']);
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* 回放一个配置快照:对每个分区 validate → analyzeImport → applyItem。
|
|
407
|
+
* 与导入/Profile 切换共用 adapter 管线,因此行为一致、无第二套写入逻辑。
|
|
408
|
+
* 单项失败不拖垮其余(如实计入 failed),与 rollback.ts 的尽力语义一致。
|
|
409
|
+
*/
|
|
410
|
+
export async function restoreConfigSnapshot(
|
|
411
|
+
opts: RestoreConfigSnapshotOptions,
|
|
412
|
+
): Promise<ConfigSnapshotRestoreReport> {
|
|
413
|
+
const { dir, id, adapters, ctx } = opts;
|
|
414
|
+
const msg = opts.msg ?? ctx.msg ?? zhMsg;
|
|
415
|
+
const snapshot = await loadConfigSnapshot(dir, id);
|
|
416
|
+
const only = opts.only !== undefined ? new Set(opts.only) : null;
|
|
417
|
+
|
|
418
|
+
const sections = new Map<SectionId, unknown>();
|
|
419
|
+
for (const [k, v] of Object.entries(snapshot.data)) {
|
|
420
|
+
if (only !== null && !only.has(k as SectionId)) continue;
|
|
421
|
+
sections.set(k as SectionId, v);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const importCtx: ImportContext = {
|
|
425
|
+
manifest: buildReplayManifest(ctx),
|
|
426
|
+
targetPlatform: ctx.platform,
|
|
427
|
+
target: ctx,
|
|
428
|
+
sections,
|
|
429
|
+
pathMappings: [],
|
|
430
|
+
resolutions: {},
|
|
431
|
+
secretInputs: {},
|
|
432
|
+
log: ctx.log,
|
|
433
|
+
msg,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const ordered = opts.applyOrder !== undefined
|
|
437
|
+
? [...adapters].sort((a, b) => {
|
|
438
|
+
const ia = opts.applyOrder!.indexOf(a.id);
|
|
439
|
+
const ib = opts.applyOrder!.indexOf(b.id);
|
|
440
|
+
return (ia < 0 ? Number.MAX_SAFE_INTEGER : ia) - (ib < 0 ? Number.MAX_SAFE_INTEGER : ib);
|
|
441
|
+
})
|
|
442
|
+
: [...adapters];
|
|
443
|
+
|
|
444
|
+
const report: ConfigSnapshotRestoreReport = {
|
|
445
|
+
ok: true,
|
|
446
|
+
snapshotId: id,
|
|
447
|
+
applied: [],
|
|
448
|
+
skipped: [],
|
|
449
|
+
failed: [],
|
|
450
|
+
invalidSections: [],
|
|
451
|
+
needsRestart: false,
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
for (const adapter of ordered) {
|
|
455
|
+
const data = sections.get(adapter.id);
|
|
456
|
+
if (data === undefined) continue;
|
|
457
|
+
let items;
|
|
458
|
+
try {
|
|
459
|
+
const validation = await adapter.validate(data, msg);
|
|
460
|
+
if (!validation.valid) {
|
|
461
|
+
const reason = validation.issues
|
|
462
|
+
.filter((i) => i.severity === 'error')
|
|
463
|
+
.map((i) => `${i.path}: ${i.message}`)
|
|
464
|
+
.join('; ');
|
|
465
|
+
report.invalidSections.push({ section: adapter.id, reason: reason === '' ? 'validation failed' : reason });
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
items = await adapter.analyzeImport(data, importCtx);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
report.invalidSections.push({
|
|
471
|
+
section: adapter.id,
|
|
472
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
473
|
+
});
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
let index = 0;
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
index += 1;
|
|
479
|
+
opts.onItem?.({ index, total: items.length, detail: `${adapter.id}:${item.id}` });
|
|
480
|
+
if (item.kind === 'Error') {
|
|
481
|
+
report.failed.push({ item: `${adapter.id}:${item.id}`, reason: item.description });
|
|
482
|
+
report.ok = false;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (NON_EXECUTABLE_KINDS.has(item.kind)) {
|
|
486
|
+
report.skipped.push(`${adapter.id}:${item.id}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const result = await adapter.applyItem(item, importCtx);
|
|
491
|
+
if (result.ok) {
|
|
492
|
+
report.applied.push(`${adapter.id}:${item.id}`);
|
|
493
|
+
if (result.needsRestart === true) report.needsRestart = true;
|
|
494
|
+
} else if (result.warning === true) {
|
|
495
|
+
report.skipped.push(`${adapter.id}:${item.id}`);
|
|
496
|
+
} else {
|
|
497
|
+
report.failed.push({
|
|
498
|
+
item: `${adapter.id}:${item.id}`,
|
|
499
|
+
reason: result.message ?? 'applyItem 返回失败',
|
|
500
|
+
});
|
|
501
|
+
report.ok = false;
|
|
502
|
+
}
|
|
503
|
+
} catch (err) {
|
|
504
|
+
report.failed.push({
|
|
505
|
+
item: `${adapter.id}:${item.id}`,
|
|
506
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
507
|
+
});
|
|
508
|
+
report.ok = false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return report;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** 便捷:两个快照的分区指纹是否等价(供 UI「与当前一致」提示) */
|
|
517
|
+
export function snapshotsEquivalent(a: ConfigSnapshotMeta, b: ConfigSnapshotMeta): boolean {
|
|
518
|
+
return statesEqual(a.state, b.state);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** 供调用方取翻译器(保持与 core 其他模块一致的导出面) */
|
|
522
|
+
export const configSnapshotMsgOf = msgOf;
|