@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,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置生命周期编排(Phase 1 P0-1 + P0-2 的可用能力层)。
|
|
3
|
+
*
|
|
4
|
+
* 把地基三件套组装成宿主可用的能力:
|
|
5
|
+
* - `config-state` 采集分区指纹 → 判断「有没有变、变了哪些分区」
|
|
6
|
+
* - `config-snapshot` 持久化与回放
|
|
7
|
+
* - `watcher` 防抖监听 + 回声抑制
|
|
8
|
+
*
|
|
9
|
+
* 提供:自动快照(watch → 防抖 → 回声判定 → 落盘 → 保留清理)、
|
|
10
|
+
* 撤销/重做(按内容差异选目标,撤销前落 pre-restore 供重做)、状态查询。
|
|
11
|
+
*
|
|
12
|
+
* 全部 IO 依赖注入(adapters / ctx / 目录 / watch 工厂),因此可在测试中
|
|
13
|
+
* 用内存门面 + 假定时器完整驱动,不需要真文件系统与 sleep。
|
|
14
|
+
*
|
|
15
|
+
* 回声抑制的两条防线(缺一不可,竞品的真实事故):
|
|
16
|
+
* 1. 写操作窗口:`watcher.beginSuppress/endSuppress` 直接丢弃事件;
|
|
17
|
+
* 2. 窗口之后:`EchoRegistry` 按内容指纹识别「延迟投递的恢复自写事件」。
|
|
18
|
+
* 二者都在本类内统一管理,调用方无需关心。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs/promises';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { sha256Hex } from '../utils/hashing.js';
|
|
24
|
+
import { normalizePath } from '../utils/paths.js';
|
|
25
|
+
import { stateFromExports, statesEqual } from './config-state.js';
|
|
26
|
+
import { listConfigSnapshots, pruneConfigSnapshots, readConfigSnapshotMeta, restoreConfigSnapshot, saveConfigSnapshot, toUndoCandidate, } from './config-snapshot.js';
|
|
27
|
+
import { canRedo, canUndo, planRedo, planUndo, steppedIds } from './undo.js';
|
|
28
|
+
import { DebouncedWatcher, EchoRegistry } from './watcher.js';
|
|
29
|
+
import { updateConfigSnapshotMeta } from './config-snapshot.js';
|
|
30
|
+
/** 启动关键配置文件(相对 homeDir)——自动快照最需要覆盖的那一批 */
|
|
31
|
+
export const BOOT_CRITICAL_RELS = [
|
|
32
|
+
'settings.yaml',
|
|
33
|
+
'settings.json',
|
|
34
|
+
'cordis.patch.yml',
|
|
35
|
+
'.env',
|
|
36
|
+
'AGENTS.md',
|
|
37
|
+
];
|
|
38
|
+
/** profile 下启动关键配置文件(相对 homeDir) */
|
|
39
|
+
export function profileCriticalRels(profile) {
|
|
40
|
+
return [
|
|
41
|
+
`profiles/${profile}/cordis.patch.yml`,
|
|
42
|
+
`profiles/${profile}/package.json`,
|
|
43
|
+
`profiles/${profile}/cordis.yml`,
|
|
44
|
+
`profiles/${profile}/pnpm-workspace.yaml`,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
/** 需要监听变更的目录(绝对路径;去重) */
|
|
48
|
+
export function watchDirsFor(homeDir, profile) {
|
|
49
|
+
const dirs = [
|
|
50
|
+
homeDir,
|
|
51
|
+
path.join(homeDir, 'profiles', profile),
|
|
52
|
+
path.join(homeDir, 'skills'),
|
|
53
|
+
path.join(homeDir, '.agent-presets'),
|
|
54
|
+
];
|
|
55
|
+
return [...new Set(dirs.map((d) => path.resolve(d)))];
|
|
56
|
+
}
|
|
57
|
+
/** 回声候选文件(绝对路径):我们恢复时会写的配置面文件 */
|
|
58
|
+
export function echoCandidatePaths(homeDir, profile) {
|
|
59
|
+
return [...BOOT_CRITICAL_RELS, ...profileCriticalRels(profile)]
|
|
60
|
+
.map((rel) => path.resolve(homeDir, rel));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 配置生命周期服务(宿主单例)。
|
|
64
|
+
* 所有公开方法都不抛:错误转为 `ok:false + reason`,避免拖垮宿主路由。
|
|
65
|
+
*/
|
|
66
|
+
export class ConfigLifecycle {
|
|
67
|
+
opts;
|
|
68
|
+
echo = new EchoRegistry();
|
|
69
|
+
watcher = null;
|
|
70
|
+
lastAutoAt = null;
|
|
71
|
+
flushRunning = false;
|
|
72
|
+
disposed = false;
|
|
73
|
+
/**
|
|
74
|
+
* flush 进行中到达的事件批(见 onAutoFlush)。
|
|
75
|
+
* watcher 在回调前已把事件批从 pending 摘除,若直接丢弃就再也没有定时器补发,
|
|
76
|
+
* 那一次变更将永远不会被快照 —— 撤销会直接跳过它。
|
|
77
|
+
*/
|
|
78
|
+
deferred = [];
|
|
79
|
+
/**
|
|
80
|
+
* 最近一次回放(撤销/重做)把配置写成的目标状态。
|
|
81
|
+
* 第二层回声防线的兜底:抑制窗口只能挡住**窗口内**的事件,窗口之后才投递的事件
|
|
82
|
+
* (macOS / 网络盘 / 杀毒扫描后重写)只能靠内容判定 —— 若此刻采集到的状态仍等于
|
|
83
|
+
* 刚写回的目标状态,那批事件就是恢复自写的回声,不得产生新快照(否则它比
|
|
84
|
+
* pre-restore 更新,会把重做通道堵死)。真实变更一旦落盘即清空。
|
|
85
|
+
*/
|
|
86
|
+
lastReplayState = null;
|
|
87
|
+
/** 当前实际建立监听的目录(供 reconcileWatchSet 比对,避免无谓重建) */
|
|
88
|
+
watchedDirs = [];
|
|
89
|
+
constructor(options) {
|
|
90
|
+
this.opts = options;
|
|
91
|
+
}
|
|
92
|
+
warn(message, detail) {
|
|
93
|
+
this.opts.onWarn?.(message, detail);
|
|
94
|
+
}
|
|
95
|
+
get dir() {
|
|
96
|
+
return this.opts.dir;
|
|
97
|
+
}
|
|
98
|
+
get isWatching() {
|
|
99
|
+
return this.watcher?.isRunning === true;
|
|
100
|
+
}
|
|
101
|
+
/* ---------------------------------------------------------- 采集与快照 */
|
|
102
|
+
/**
|
|
103
|
+
* 采集各分区导出结果 + 分区指纹:**单次遍历**,不重复 export。
|
|
104
|
+
* 单分区失败 → 记入 failed 并跳过(不抛),使调用方能判断这次状态是否可信。
|
|
105
|
+
*/
|
|
106
|
+
async captureSections(only) {
|
|
107
|
+
const sections = new Map();
|
|
108
|
+
const failed = [];
|
|
109
|
+
for (const adapter of this.opts.adapters) {
|
|
110
|
+
if (only !== undefined && !only.includes(adapter.id))
|
|
111
|
+
continue;
|
|
112
|
+
try {
|
|
113
|
+
sections.set(adapter.id, await adapter.export(this.opts.ctx, { includeSecrets: false, only: [adapter.id] }));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
failed.push(adapter.id);
|
|
117
|
+
this.opts.onSectionError?.(adapter.id, error);
|
|
118
|
+
this.warn(`采集分区失败,已跳过: ${adapter.id}`, error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { sections, state: stateFromExports(sections), failed };
|
|
122
|
+
}
|
|
123
|
+
/** 采集当前配置状态(单分区失败跳过,不抛) */
|
|
124
|
+
async capture(only) {
|
|
125
|
+
return (await this.captureSections(only)).state;
|
|
126
|
+
}
|
|
127
|
+
/** 采集并落盘一个配置快照 */
|
|
128
|
+
async snapshot(req) {
|
|
129
|
+
return this.saveSnapshot(req, await this.captureSections(req.only));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 用一个**已采集**的产物落盘快照。
|
|
133
|
+
* 为什么必须支持传入已采集结果:撤销的 pre-restore 快照必须与「据以挑选目标的
|
|
134
|
+
* 那次采集」是同一份观测 —— 否则二次采集之间用户又改了配置,pre-restore 记下的
|
|
135
|
+
* 就不是「撤销前状态」,重做会把用户没见过的内容写回去。
|
|
136
|
+
*/
|
|
137
|
+
async saveSnapshot(req, captured) {
|
|
138
|
+
const meta = await saveConfigSnapshot({
|
|
139
|
+
dir: this.opts.dir,
|
|
140
|
+
kind: req.kind,
|
|
141
|
+
reason: req.reason,
|
|
142
|
+
sections: captured.sections,
|
|
143
|
+
state: captured.state,
|
|
144
|
+
...(req.trigger !== undefined ? { trigger: req.trigger } : {}),
|
|
145
|
+
...(req.note !== undefined ? { note: req.note } : {}),
|
|
146
|
+
...(req.tags !== undefined ? { tags: req.tags } : {}),
|
|
147
|
+
});
|
|
148
|
+
if (req.kind !== 'manual') {
|
|
149
|
+
await this.prune();
|
|
150
|
+
}
|
|
151
|
+
return meta;
|
|
152
|
+
}
|
|
153
|
+
async list() {
|
|
154
|
+
return listConfigSnapshots(this.opts.dir);
|
|
155
|
+
}
|
|
156
|
+
async prune() {
|
|
157
|
+
return pruneConfigSnapshots(this.opts.dir, {
|
|
158
|
+
...(this.opts.keepAuto !== undefined ? { keepAuto: this.opts.keepAuto } : {}),
|
|
159
|
+
...(this.opts.keepPreRestore !== undefined ? { keepPreRestore: this.opts.keepPreRestore } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/* ---------------------------------------------------------- 状态 */
|
|
163
|
+
async status() {
|
|
164
|
+
const metas = await this.list();
|
|
165
|
+
const captured = await this.captureSections();
|
|
166
|
+
const candidates = metas.map(toUndoCandidate);
|
|
167
|
+
// 采集不完整(有分区导出失败)→ 状态不可信:宁可报「不可撤销」,也不能拿一个
|
|
168
|
+
// 缺分区的状态去挑目标 —— 它会与内容其实相同的快照判为「不同」,做成一次假成功的空操作。
|
|
169
|
+
const trustworthy = captured.failed.length === 0;
|
|
170
|
+
const lastAuto = metas.find((m) => m.kind === 'auto' || m.kind === 'baseline');
|
|
171
|
+
return {
|
|
172
|
+
canUndo: trustworthy && canUndo(captured.state, candidates),
|
|
173
|
+
canRedo: canRedo(candidates),
|
|
174
|
+
total: metas.length,
|
|
175
|
+
lastAutoAt: lastAuto?.createdAt ?? null,
|
|
176
|
+
watching: this.isWatching,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/* ---------------------------------------------------------- 撤销 / 重做 */
|
|
180
|
+
/**
|
|
181
|
+
* 撤销:回退到与当前状态**内容不同**的最新快照。
|
|
182
|
+
* 撤销前先落 `pre-restore` 快照记录当前状态,使重做可逆。
|
|
183
|
+
*/
|
|
184
|
+
async undo() {
|
|
185
|
+
if (this.disposed)
|
|
186
|
+
return { ok: false, reason: 'disposed' };
|
|
187
|
+
let captured;
|
|
188
|
+
let metas;
|
|
189
|
+
try {
|
|
190
|
+
captured = await this.captureSections();
|
|
191
|
+
metas = await this.list();
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
195
|
+
}
|
|
196
|
+
// 采集不完整时状态不可信:拒绝执行,而不是做一次「回放内容其实相同」的假成功空操作。
|
|
197
|
+
if (captured.failed.length > 0) {
|
|
198
|
+
return { ok: false, reason: `capture-incomplete:${captured.failed.join(',')}` };
|
|
199
|
+
}
|
|
200
|
+
const current = captured.state;
|
|
201
|
+
const plan = planUndo(current, metas.map(toUndoCandidate));
|
|
202
|
+
if (plan.kind === 'none')
|
|
203
|
+
return { ok: false, reason: plan.reason };
|
|
204
|
+
let pre;
|
|
205
|
+
try {
|
|
206
|
+
// 复用上面那次采集:pre-restore 记录的必须正是「据以挑选目标的那个状态」,
|
|
207
|
+
// 二次采集会让两者之间用户的新改动溜进 pre-restore(重做时写回没见过的内容)。
|
|
208
|
+
pre = await this.saveSnapshot({ kind: 'pre-restore', reason: `before-undo:${plan.targetId}`, trigger: 'undo' }, captured);
|
|
209
|
+
await updateConfigSnapshotMeta(this.opts.dir, pre.id, { undoOf: plan.targetId });
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return { ok: false, reason: `撤销前快照失败: ${error instanceof Error ? error.message : String(error)}` };
|
|
213
|
+
}
|
|
214
|
+
const report = await this.replay(plan.targetId);
|
|
215
|
+
// 标记被跨过的快照(重做回来时要清掉)
|
|
216
|
+
const stepped = steppedIds(metas.map(toUndoCandidate), plan.targetId, current);
|
|
217
|
+
for (const id of stepped) {
|
|
218
|
+
try {
|
|
219
|
+
await updateConfigSnapshotMeta(this.opts.dir, id, { stepped: true });
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
this.warn(`标记 stepped 失败: ${id}`, error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
ok: report.ok,
|
|
227
|
+
targetId: plan.targetId,
|
|
228
|
+
preSnapshotId: pre.id,
|
|
229
|
+
report,
|
|
230
|
+
stepped,
|
|
231
|
+
...(report.ok ? {} : { reason: '回放失败,详见 report.failed' }),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** 重做:回到最近一次未消费的 pre-restore(其后若有新快照则拒绝)。 */
|
|
235
|
+
async redo() {
|
|
236
|
+
if (this.disposed)
|
|
237
|
+
return { ok: false, reason: 'disposed' };
|
|
238
|
+
let metas;
|
|
239
|
+
try {
|
|
240
|
+
metas = await this.list();
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
244
|
+
}
|
|
245
|
+
const plan = planRedo(metas.map(toUndoCandidate));
|
|
246
|
+
if (plan.kind === 'none')
|
|
247
|
+
return { ok: false, reason: plan.reason };
|
|
248
|
+
const report = await this.replay(plan.targetId);
|
|
249
|
+
// 只有回放真的成功才消费 pre-restore。失败时若照样标记 consumed,planRedo 立刻
|
|
250
|
+
// 变成 no-pre-restore,用户就再没有回到「撤销前状态」的通道了 —— 而配置此刻正停在
|
|
251
|
+
// 半应用的中间态(这正是最需要重做/回退的时候)。
|
|
252
|
+
if (report.ok) {
|
|
253
|
+
try {
|
|
254
|
+
await updateConfigSnapshotMeta(this.opts.dir, plan.targetId, { consumed: true });
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
this.warn(`标记 consumed 失败: ${plan.targetId}`, error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// 重做后:内容已回到撤销前状态的那些「被跨过」快照,清除标记
|
|
261
|
+
const unstepped = [];
|
|
262
|
+
try {
|
|
263
|
+
const restored = await this.capture();
|
|
264
|
+
for (const meta of metas) {
|
|
265
|
+
if (meta.stepped !== true)
|
|
266
|
+
continue;
|
|
267
|
+
if (statesEqual(meta.state, restored)) {
|
|
268
|
+
await updateConfigSnapshotMeta(this.opts.dir, meta.id, { stepped: false });
|
|
269
|
+
unstepped.push(meta.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
this.warn('重做后清理 stepped 标记失败', error);
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
ok: report.ok,
|
|
278
|
+
targetId: plan.targetId,
|
|
279
|
+
report,
|
|
280
|
+
unstepped,
|
|
281
|
+
...(report.ok ? {} : { reason: '回放失败,详见 report.failed' }),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 回放指定快照(撤销/重做共用)。
|
|
286
|
+
* 全程在监听抑制窗口内,并在结束后登记回声指纹——两道防线都在这里落地。
|
|
287
|
+
*/
|
|
288
|
+
async replay(id) {
|
|
289
|
+
// 先取目标快照的状态:回放后用它做「延迟回声」的内容判定(见 lastReplayState)。
|
|
290
|
+
const targetMeta = await readConfigSnapshotMeta(this.opts.dir, id);
|
|
291
|
+
const run = async () => restoreConfigSnapshot({
|
|
292
|
+
dir: this.opts.dir,
|
|
293
|
+
id,
|
|
294
|
+
adapters: this.opts.adapters,
|
|
295
|
+
ctx: this.opts.ctx,
|
|
296
|
+
...(this.opts.applyOrder !== undefined ? { applyOrder: this.opts.applyOrder } : {}),
|
|
297
|
+
});
|
|
298
|
+
const report = this.watcher !== null
|
|
299
|
+
? await this.watcher.suppressWhileAsync(run)
|
|
300
|
+
: await run();
|
|
301
|
+
this.lastReplayState = targetMeta?.state ?? null;
|
|
302
|
+
await this.recordEcho();
|
|
303
|
+
return report;
|
|
304
|
+
}
|
|
305
|
+
/* ---------------------------------------------------------- 回声抑制 */
|
|
306
|
+
/**
|
|
307
|
+
* 登记「刚写回的内容指纹」:恢复动作写完文件后调用。
|
|
308
|
+
* 只登记我们确实会写的配置面文件(存在才登记),避免把无关文件误判为回声而漏拍。
|
|
309
|
+
*/
|
|
310
|
+
async recordEcho() {
|
|
311
|
+
this.echo.clear();
|
|
312
|
+
for (const abs of echoCandidatePaths(this.opts.ctx.homeDir, this.opts.profile)) {
|
|
313
|
+
try {
|
|
314
|
+
const data = await fs.readFile(abs);
|
|
315
|
+
this.echo.record(normalizePath(abs), sha256Hex(data));
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// 文件不存在(恢复时被删除)→ 不登记;后续出现即视为真实变更
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 事件批是否为「恢复自写的回声」。
|
|
324
|
+
* 判定:批内每个文件都必须已登记**且**内容仍等于登记值;任一文件内容变了、
|
|
325
|
+
* 未登记、或读不到(新增/删除)→ 真实变更。
|
|
326
|
+
*/
|
|
327
|
+
async isEchoBatch(events) {
|
|
328
|
+
if (this.echo.size === 0 || events.length === 0)
|
|
329
|
+
return false;
|
|
330
|
+
let checked = false;
|
|
331
|
+
for (const event of events) {
|
|
332
|
+
const abs = normalizePath(path.resolve(event.dir, event.filename));
|
|
333
|
+
if (!this.echo.has(abs))
|
|
334
|
+
return false;
|
|
335
|
+
let data;
|
|
336
|
+
try {
|
|
337
|
+
data = await fs.readFile(abs);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return false; // 读不到 = 新增或删除 → 真实变更
|
|
341
|
+
}
|
|
342
|
+
if (!this.echo.isEcho(abs, sha256Hex(data)))
|
|
343
|
+
return false;
|
|
344
|
+
checked = true;
|
|
345
|
+
}
|
|
346
|
+
return checked;
|
|
347
|
+
}
|
|
348
|
+
/* ---------------------------------------------------------- 自动快照 */
|
|
349
|
+
/** 启动自动快照监听(幂等;未注入 watchFactory 或已关闭则空操作) */
|
|
350
|
+
startAutoSnapshot() {
|
|
351
|
+
if (this.disposed)
|
|
352
|
+
return;
|
|
353
|
+
if (this.opts.autoEnabled === false)
|
|
354
|
+
return;
|
|
355
|
+
if (this.opts.watchFactory === undefined)
|
|
356
|
+
return;
|
|
357
|
+
this.stopAutoSnapshot();
|
|
358
|
+
// 只监听**已存在**的目录:skills / .agent-presets 等在 DSH 首次启动时常常不存在,
|
|
359
|
+
// 直接 watch 会为每个缺失目录刷一条告警(真实 QA 启动实测 2 条),既吵又无意义。
|
|
360
|
+
// 自动快照的主战场是 settings.yaml / cordis.patch.yml / profile 目录 —— 它们在任何
|
|
361
|
+
// 可用安装里都存在;仅在运行期才出现的目录由 reconcileWatchSet() 在下次快照后接管。
|
|
362
|
+
const dirs = watchDirsFor(this.opts.ctx.homeDir, this.opts.profile).filter((d) => existsSync(d));
|
|
363
|
+
if (dirs.length === 0) {
|
|
364
|
+
this.warn('没有可监听的目录,自动快照未启动');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.watcher = new DebouncedWatcher({
|
|
368
|
+
dirs,
|
|
369
|
+
debounceMs: this.opts.debounceMs ?? 1500,
|
|
370
|
+
watchFactory: this.opts.watchFactory,
|
|
371
|
+
onFlush: (events) => { void this.onAutoFlush(events); },
|
|
372
|
+
onError: (dir, error) => this.warn(`监听失败: ${dir}`, error),
|
|
373
|
+
...(this.opts.timers !== undefined ? { timers: this.opts.timers } : {}),
|
|
374
|
+
});
|
|
375
|
+
this.watchedDirs = [...dirs];
|
|
376
|
+
this.watcher.start();
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* 监听集自愈:启动之后才出现的目录(skills / .agent-presets 在全新安装里通常不存在)
|
|
380
|
+
* 在**下一次快照之后**自动纳入监听。
|
|
381
|
+
*
|
|
382
|
+
* 为什么需要它:startAutoSnapshot 每次进程只被调用一次(启动闸门),若只在那里过滤
|
|
383
|
+
* 一次,那两个目录在整个进程生命周期里都不会被监听 —— 与注释里的承诺不符(「注释承诺 >
|
|
384
|
+
* 实际防线」正是本仓库踩过的坑)。放在 flush 之后调用,既不打断进行中的采集,也不会
|
|
385
|
+
* 丢掉排队中的事件批。
|
|
386
|
+
*/
|
|
387
|
+
reconcileWatchSet() {
|
|
388
|
+
if (this.disposed || this.watcher === null)
|
|
389
|
+
return;
|
|
390
|
+
const desired = watchDirsFor(this.opts.ctx.homeDir, this.opts.profile).filter((d) => existsSync(d));
|
|
391
|
+
const current = this.watchedDirs;
|
|
392
|
+
if (desired.length === current.length && desired.every((d, i) => d === current[i]))
|
|
393
|
+
return;
|
|
394
|
+
this.startAutoSnapshot(); // 内含 stop + 重建,幂等
|
|
395
|
+
}
|
|
396
|
+
stopAutoSnapshot() {
|
|
397
|
+
this.watcher?.stop();
|
|
398
|
+
this.watcher = null;
|
|
399
|
+
this.watchedDirs = [];
|
|
400
|
+
// 停监听即作废排队中的事件批:它们对应的变更已经错过防抖窗口,
|
|
401
|
+
// 且 dispose 之后不应再产生快照。
|
|
402
|
+
this.deferred = [];
|
|
403
|
+
}
|
|
404
|
+
/** 释放:停止监听(宿主 dispose 调用) */
|
|
405
|
+
dispose() {
|
|
406
|
+
this.disposed = true;
|
|
407
|
+
this.stopAutoSnapshot();
|
|
408
|
+
}
|
|
409
|
+
/** 供宿主在路由层复用的抑制包装(例如 Profile 切换时) */
|
|
410
|
+
async suppressWhileAsync(fn) {
|
|
411
|
+
if (this.watcher === null)
|
|
412
|
+
return fn();
|
|
413
|
+
return this.watcher.suppressWhileAsync(fn);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 防抖到期后的自动快照。
|
|
417
|
+
*
|
|
418
|
+
* 并发保护是「排队」而不是「丢弃」:watcher 在回调前已经把事件批从 pending 摘除,
|
|
419
|
+
* 若这里直接 return,那批事件就再也没有定时器补发 —— 那次变更永远不会被快照,
|
|
420
|
+
* 撤销会直接跳过它(实测:连写 v2、v3 只落了 1 份快照,且 undo 无从回到 v2)。
|
|
421
|
+
*/
|
|
422
|
+
async onAutoFlush(events) {
|
|
423
|
+
if (this.disposed)
|
|
424
|
+
return;
|
|
425
|
+
if (this.flushRunning) {
|
|
426
|
+
this.deferred.push(...events);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.flushRunning = true;
|
|
430
|
+
try {
|
|
431
|
+
let batch = events;
|
|
432
|
+
for (;;) {
|
|
433
|
+
await this.flushOnce(batch);
|
|
434
|
+
batch = this.deferred.splice(0);
|
|
435
|
+
if (batch.length === 0)
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
this.warn('自动快照失败', error);
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
this.flushRunning = false;
|
|
444
|
+
// 异常路径上仍积压的事件不得滞留(否则那次变更同样永远不会被快照)
|
|
445
|
+
if (this.deferred.length > 0 && !this.disposed) {
|
|
446
|
+
const rest = this.deferred.splice(0);
|
|
447
|
+
void this.onAutoFlush(rest);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
// 队列已排空 → 顺带让新增目录(skills / .agent-presets)进入监听集
|
|
451
|
+
this.reconcileWatchSet();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** 处理一批事件:回声判定 → 采集 → 落盘。 */
|
|
455
|
+
async flushOnce(events) {
|
|
456
|
+
if (await this.isEchoBatch(events))
|
|
457
|
+
return; // 恢复自写的回声 → 不产生快照
|
|
458
|
+
const captured = await this.captureSections();
|
|
459
|
+
// 第二层回声兜底(见 lastReplayState):恢复动作刚把配置写成 X,此刻采集到的仍是 X
|
|
460
|
+
// → 这批事件是窗口之后才投递的恢复自写回声,不得产生快照(否则挡住重做)。
|
|
461
|
+
if (this.lastReplayState !== null && statesEqual(captured.state, this.lastReplayState))
|
|
462
|
+
return;
|
|
463
|
+
// 第三层兜底:与最新快照内容完全相同 → 这份快照不携带任何新信息。
|
|
464
|
+
// 覆盖面比前两层广:**任何**恢复通道(导入、备份恢复、Profile 切换、同步应用)写完
|
|
465
|
+
// 配置都会触发事件,而它们不会经过 replay(),因此拿不到 lastReplayState。
|
|
466
|
+
// 若不跳过,恢复完立刻多出一份「等于刚恢复内容」的快照,它会比 pre-restore 更新,
|
|
467
|
+
// 把重做通道永久堵死(正是竞品记录过的那起事故)。
|
|
468
|
+
const newest = (await this.list()).find((m) => m.kind !== 'pre-restore');
|
|
469
|
+
if (newest !== undefined && statesEqual(newest.state, captured.state)) {
|
|
470
|
+
this.echo.clear();
|
|
471
|
+
this.lastReplayState = captured.state;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.echo.clear(); // 真实变更发生 → 清空登记,避免后续合法修改被误判为回声
|
|
475
|
+
this.lastReplayState = null;
|
|
476
|
+
const meta = await this.saveSnapshot({ kind: 'auto', reason: 'config-change', trigger: 'watcher' }, captured);
|
|
477
|
+
this.lastAutoAt = meta.createdAt;
|
|
478
|
+
this.opts.onAutoSnapshot?.(meta);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
//# sourceMappingURL=config-lifecycle.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { msgOf } from './messages.ts';
|
|
2
|
+
import type { MsgFunc } from './messages.ts';
|
|
3
|
+
import { type ConfigState } from './config-state.ts';
|
|
4
|
+
import type { ConfigSnapshotKind, UndoCandidate } from './undo.ts';
|
|
5
|
+
import type { ConfigAdapter, ExportSection, HostContext } from './types.ts';
|
|
6
|
+
import type { SectionId } from '../schema/types.ts';
|
|
7
|
+
/** 快照文件名(每个快照一个目录) */
|
|
8
|
+
export declare const CONFIG_SNAPSHOT_FILE = "config-snapshot.json";
|
|
9
|
+
/** 快照元数据(落盘部分) */
|
|
10
|
+
export interface ConfigSnapshotMeta {
|
|
11
|
+
id: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
kind: ConfigSnapshotKind;
|
|
14
|
+
/** 人类可读原因(如 'plugin-change' / 'before-restore:<id>' / 'manual') */
|
|
15
|
+
reason: string;
|
|
16
|
+
/** 触发源(watcher / route / tool / schedule / undo / baseline) */
|
|
17
|
+
trigger?: string;
|
|
18
|
+
/** 参与采集的分区(升序) */
|
|
19
|
+
sections: SectionId[];
|
|
20
|
+
/** 采集到的配置状态(分区指纹;撤销/重做的内容比对基准) */
|
|
21
|
+
state: ConfigState;
|
|
22
|
+
note?: string;
|
|
23
|
+
tags?: string[];
|
|
24
|
+
pinned?: boolean;
|
|
25
|
+
/** pre-restore:是否已被重做消费 */
|
|
26
|
+
consumed?: boolean;
|
|
27
|
+
/** 被撤销跨过(见 undo.ts steppedIds) */
|
|
28
|
+
stepped?: boolean;
|
|
29
|
+
/** pre-restore:本次撤销的目标快照 id */
|
|
30
|
+
undoOf?: string;
|
|
31
|
+
/** 落盘字节数(快照体积,供 UI 展示与容量告警) */
|
|
32
|
+
totalBytes: number;
|
|
33
|
+
}
|
|
34
|
+
/** 完整快照(元数据 + 分区数据) */
|
|
35
|
+
export interface ConfigSnapshot extends ConfigSnapshotMeta {
|
|
36
|
+
data: Partial<Record<SectionId, unknown>>;
|
|
37
|
+
}
|
|
38
|
+
/** 供 undo.ts 使用的最小候选视图 */
|
|
39
|
+
export declare function toUndoCandidate(meta: ConfigSnapshotMeta): UndoCandidate;
|
|
40
|
+
export interface SaveConfigSnapshotOptions {
|
|
41
|
+
dir: string;
|
|
42
|
+
kind: ConfigSnapshotKind;
|
|
43
|
+
reason: string;
|
|
44
|
+
trigger?: string;
|
|
45
|
+
/** 本次采集的各分区导出结果(adapter.export 产出) */
|
|
46
|
+
sections: Map<SectionId, ExportSection>;
|
|
47
|
+
state: ConfigState;
|
|
48
|
+
note?: string;
|
|
49
|
+
tags?: string[];
|
|
50
|
+
now?: () => Date;
|
|
51
|
+
idFactory?: () => string;
|
|
52
|
+
/** 落盘上限(字节;缺省 64 MiB,超出抛错而不是静默截断) */
|
|
53
|
+
maxBytes?: number;
|
|
54
|
+
}
|
|
55
|
+
/** 生成快照 id:时间戳 + 随机短串(可读 + 唯一;与既有 uuid 风格区分开) */
|
|
56
|
+
export declare function makeConfigSnapshotId(now?: () => Date): string;
|
|
57
|
+
/**
|
|
58
|
+
* 写入一个配置快照。返回落盘后的元数据。
|
|
59
|
+
* id 冲突时自动追加后缀重试(同一秒内多次快照)。
|
|
60
|
+
*/
|
|
61
|
+
export declare function saveConfigSnapshot(opts: SaveConfigSnapshotOptions): Promise<ConfigSnapshotMeta>;
|
|
62
|
+
/** 读单个快照的元数据;缺失/损坏/非法 id → null(绝不抛)。 */
|
|
63
|
+
export declare function readConfigSnapshotMeta(dir: string, id: string): Promise<ConfigSnapshotMeta | null>;
|
|
64
|
+
/** 列出全部快照元数据(按 createdAt 倒序;损坏条目跳过,不阻断其余)。 */
|
|
65
|
+
export declare function listConfigSnapshots(dir: string): Promise<ConfigSnapshotMeta[]>;
|
|
66
|
+
/** 读完整快照(含分区数据)。缺失/损坏抛出。 */
|
|
67
|
+
export declare function loadConfigSnapshot(dir: string, id: string): Promise<ConfigSnapshot>;
|
|
68
|
+
/** 更新的元数据字段(写入时保留分区数据不变)。 */
|
|
69
|
+
export type ConfigSnapshotMetaPatch = Partial<Pick<ConfigSnapshotMeta, 'note' | 'tags' | 'pinned' | 'consumed' | 'stepped' | 'undoOf'>>;
|
|
70
|
+
/** 原地更新元数据字段;快照不存在返回 null。 */
|
|
71
|
+
export declare function updateConfigSnapshotMeta(dir: string, id: string, patch: ConfigSnapshotMetaPatch): Promise<ConfigSnapshotMeta | null>;
|
|
72
|
+
/** 删除单个快照(幂等:不存在视为成功)。 */
|
|
73
|
+
export declare function deleteConfigSnapshot(dir: string, id: string): Promise<boolean>;
|
|
74
|
+
export interface ConfigSnapshotPruneOptions {
|
|
75
|
+
/** auto / baseline / undo 三类共用的保留份数(缺省 20) */
|
|
76
|
+
keepAuto?: number;
|
|
77
|
+
/** pre-restore 保留份数(缺省 10) */
|
|
78
|
+
keepPreRestore?: number;
|
|
79
|
+
/** manual 保留份数(缺省 Infinity = 永不自动清理) */
|
|
80
|
+
keepManual?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 保留清理:按 kind 分桶,各自保留最新 N 份,删除更旧的。
|
|
84
|
+
* pinned 快照**一律豁免**(用户显式保留的回滚点不得被自动淘汰)。
|
|
85
|
+
* 返回被删除的快照 id 列表。
|
|
86
|
+
*/
|
|
87
|
+
export declare function pruneConfigSnapshots(dir: string, opts?: ConfigSnapshotPruneOptions): Promise<string[]>;
|
|
88
|
+
export interface RestoreConfigSnapshotOptions {
|
|
89
|
+
dir: string;
|
|
90
|
+
id: string;
|
|
91
|
+
adapters: readonly ConfigAdapter[];
|
|
92
|
+
ctx: HostContext;
|
|
93
|
+
/**
|
|
94
|
+
* 分区应用顺序(缺省 = adapters 传入顺序)。宿主应传入与导入管线一致的顺序
|
|
95
|
+
* (副作用大的 patch/安装最后),避免半途失败留下不一致的中间态。
|
|
96
|
+
*/
|
|
97
|
+
applyOrder?: readonly SectionId[];
|
|
98
|
+
/** 只回放这些分区(缺省 = 快照内的全部分区) */
|
|
99
|
+
only?: readonly SectionId[];
|
|
100
|
+
msg?: MsgFunc;
|
|
101
|
+
/** 逐项进度回调(UI 进度条) */
|
|
102
|
+
onItem?: (info: {
|
|
103
|
+
index: number;
|
|
104
|
+
total: number;
|
|
105
|
+
detail: string;
|
|
106
|
+
}) => void;
|
|
107
|
+
}
|
|
108
|
+
export interface ConfigSnapshotRestoreReport {
|
|
109
|
+
ok: boolean;
|
|
110
|
+
snapshotId: string;
|
|
111
|
+
/** 成功应用的计划项 `${section}:${itemId}` */
|
|
112
|
+
applied: string[];
|
|
113
|
+
/** 跳过(Skip/Warning/MissingSecret 等非破坏性项) */
|
|
114
|
+
skipped: string[];
|
|
115
|
+
failed: {
|
|
116
|
+
item: string;
|
|
117
|
+
reason: string;
|
|
118
|
+
}[];
|
|
119
|
+
/** 校验不通过而整体跳过的分区 */
|
|
120
|
+
invalidSections: {
|
|
121
|
+
section: SectionId;
|
|
122
|
+
reason: string;
|
|
123
|
+
}[];
|
|
124
|
+
needsRestart: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 回放一个配置快照:对每个分区 validate → analyzeImport → applyItem。
|
|
128
|
+
* 与导入/Profile 切换共用 adapter 管线,因此行为一致、无第二套写入逻辑。
|
|
129
|
+
* 单项失败不拖垮其余(如实计入 failed),与 rollback.ts 的尽力语义一致。
|
|
130
|
+
*/
|
|
131
|
+
export declare function restoreConfigSnapshot(opts: RestoreConfigSnapshotOptions): Promise<ConfigSnapshotRestoreReport>;
|
|
132
|
+
/** 便捷:两个快照的分区指纹是否等价(供 UI「与当前一致」提示) */
|
|
133
|
+
export declare function snapshotsEquivalent(a: ConfigSnapshotMeta, b: ConfigSnapshotMeta): boolean;
|
|
134
|
+
/** 供调用方取翻译器(保持与 core 其他模块一致的导出面) */
|
|
135
|
+
export declare const configSnapshotMsgOf: typeof msgOf;
|