@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,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessions 分区 adapter(默认关,设计 §3.3/§15):
|
|
3
|
+
* 数据源 = ~/.dsh/sessions/<projectKey>/<sessionId>/…(zstd jsonl,含敏感信息)。
|
|
4
|
+
* defaultIncluded=false:Quick Export 不包含,用户显式勾选才导出(v1 文件级复制)。
|
|
5
|
+
* 研究报告 §4.9:DSH 无会话批量导出 API,逐会话文件复制是唯一通道。
|
|
6
|
+
*/
|
|
7
|
+
import { FileCollectionAdapter } from './file-collection.ts';
|
|
8
|
+
|
|
9
|
+
export class SessionsAdapter extends FileCollectionAdapter {
|
|
10
|
+
readonly id = 'sessions' as const;
|
|
11
|
+
readonly displayName = 'Sessions';
|
|
12
|
+
readonly defaultIncluded = false;
|
|
13
|
+
readonly portability = 'deviceSpecific' as const;
|
|
14
|
+
readonly baseDir = 'sessions';
|
|
15
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings adapter 测试:非 UI namespace 导出(redact+revision)/ Create·Skip·Conflict 分析 /
|
|
3
|
+
* applyItem 乐观锁写回 / validate。
|
|
4
|
+
*/
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { SettingsAdapter } from './settings.ts';
|
|
8
|
+
import { makeContext, makeImportContext } from './test-helpers.ts';
|
|
9
|
+
import type { PlanItem } from '../core/types.ts';
|
|
10
|
+
|
|
11
|
+
const NS = ['general', 'theme', 'llm-deepseek'];
|
|
12
|
+
|
|
13
|
+
test('settings: 导出只含非 UI namespace(redacted + revision + secrets 标记)', async () => {
|
|
14
|
+
const ctx = makeContext('win32', 'C:\\Users\\alice');
|
|
15
|
+
ctx.settings.ns.set('general', { value: { theme: 'dark' }, revision: 3, secrets: [] });
|
|
16
|
+
ctx.settings.ns.set('theme', { value: { mode: 'dark' }, revision: 1, secrets: [] }); // UI 类 → 排除
|
|
17
|
+
ctx.settings.ns.set('llm-deepseek', {
|
|
18
|
+
value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
|
|
19
|
+
revision: 5,
|
|
20
|
+
secrets: [{ path: ['apiKey'], set: true }],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const adapter = new SettingsAdapter(NS);
|
|
24
|
+
const out = await adapter.export(ctx, { includeSecrets: false });
|
|
25
|
+
assert.equal(out.data.version, 1);
|
|
26
|
+
assert.ok(out.data.namespaces['general']);
|
|
27
|
+
assert.ok(out.data.namespaces['llm-deepseek']);
|
|
28
|
+
assert.ok(!out.data.namespaces['theme'], 'theme 属 UI 类,应被 settings 排除');
|
|
29
|
+
assert.equal(out.data.namespaces['llm-deepseek']?.revision, 5);
|
|
30
|
+
assert.deepEqual(out.data.namespaces['llm-deepseek']?.secrets, [{ path: ['apiKey'], set: true }]);
|
|
31
|
+
assert.equal(out.counts.namespaces, 2);
|
|
32
|
+
|
|
33
|
+
const v = await adapter.validate(out.data);
|
|
34
|
+
assert.equal(v.valid, true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('settings: analyzeImport 未注册→MissingDependency / 空→Create / 一致→Skip / 不同→Conflict + applyItem 写回(幂等)', async () => {
|
|
38
|
+
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
39
|
+
src.settings.ns.set('general', { value: { theme: 'dark' }, revision: 3, secrets: [] });
|
|
40
|
+
const adapter = new SettingsAdapter(NS);
|
|
41
|
+
const exported = await adapter.export(src, { includeSecrets: false });
|
|
42
|
+
const sections = new Map([['settings', exported.data]]);
|
|
43
|
+
|
|
44
|
+
// 场景 A:目标命名空间未注册(缺少提供插件)→ MissingDependency(不是 Create,写入必失败)
|
|
45
|
+
const dstA = makeContext('linux', '/home/bob');
|
|
46
|
+
let items = await adapter.analyzeImport(exported.data, makeImportContext(dstA, sections));
|
|
47
|
+
assert.equal(items.length, 1);
|
|
48
|
+
assert.equal(items[0]?.kind, 'MissingDependency');
|
|
49
|
+
assert.equal(items[0]?.target?.ref, 'general');
|
|
50
|
+
|
|
51
|
+
// 场景 B:目标已注册但为空 → Create 并写入(初始化)
|
|
52
|
+
const dst = makeContext('linux', '/home/bob');
|
|
53
|
+
dst.settings.registered.add('general'); // 注册但无值
|
|
54
|
+
items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
55
|
+
assert.equal(items[0]?.kind, 'Create');
|
|
56
|
+
assert.equal(items[0]?.target?.ref, 'general');
|
|
57
|
+
const r = await adapter.applyItem(items[0]!, makeImportContext(dst, sections));
|
|
58
|
+
assert.equal(r.ok, true);
|
|
59
|
+
assert.deepEqual(dst.settings.ns.get('general')?.value, { theme: 'dark' });
|
|
60
|
+
|
|
61
|
+
// 重复导入 → Skip(幂等)
|
|
62
|
+
items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
63
|
+
assert.equal(items[0]?.kind, 'Skip');
|
|
64
|
+
|
|
65
|
+
// 目标不同 → Conflict;useImported(Update)后覆盖
|
|
66
|
+
dst.settings.ns.set('general', { value: { theme: 'light' }, revision: 9, secrets: [] });
|
|
67
|
+
items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
68
|
+
assert.equal(items[0]?.kind, 'Conflict');
|
|
69
|
+
const useItem: PlanItem = { ...items[0]!, kind: 'Update' };
|
|
70
|
+
await adapter.applyItem(useItem, makeImportContext(dst, sections));
|
|
71
|
+
assert.deepEqual(dst.settings.ns.get('general')?.value, { theme: 'dark' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('settings: applyItem 读时锁 — 目标 revision 不同时仍按当前值提交(不覆盖并发修改)', async () => {
|
|
75
|
+
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
76
|
+
src.settings.ns.set('general', { value: { theme: 'dark' }, revision: 3, secrets: [] });
|
|
77
|
+
const adapter = new SettingsAdapter(NS);
|
|
78
|
+
const exported = await adapter.export(src, { includeSecrets: false });
|
|
79
|
+
|
|
80
|
+
const dst = makeContext('linux', '/home/bob');
|
|
81
|
+
dst.settings.ns.set('general', { value: { theme: 'light' }, revision: 9, secrets: [] });
|
|
82
|
+
const sections = new Map([['settings', exported.data]]);
|
|
83
|
+
const item: PlanItem = {
|
|
84
|
+
id: 'settings:general', kind: 'Update', adapter: 'settings',
|
|
85
|
+
description: '更新 general', severity: 'info',
|
|
86
|
+
target: { adapter: 'settings', ref: 'general' },
|
|
87
|
+
};
|
|
88
|
+
const r = await adapter.applyItem(item, makeImportContext(dst, sections));
|
|
89
|
+
assert.equal(r.ok, true);
|
|
90
|
+
assert.deepEqual(dst.settings.ns.get('general')?.value, { theme: 'dark' }, 'useImported 应覆盖目标');
|
|
91
|
+
assert.equal(dst.settings.ns.get('general')?.revision, 10, '提交后 revision 递增');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('settings: validate 拒绝非法结构', async () => {
|
|
95
|
+
const adapter = new SettingsAdapter(NS);
|
|
96
|
+
const bad = await adapter.validate({ version: 2, namespaces: {} } as never);
|
|
97
|
+
assert.equal(bad.valid, false);
|
|
98
|
+
const noNs = await adapter.validate({ version: 1, namespaces: null as never });
|
|
99
|
+
assert.equal(noNs.valid, false);
|
|
100
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings 分区 adapter(设计 §3.3/§4.1/§4.2):
|
|
3
|
+
* 数据源 = ~/.dsh/settings.yaml 中「非 UI 类」namespace(经 ctx.settings.describe({redactSecrets:true}))。
|
|
4
|
+
*
|
|
5
|
+
* 说明:DSH settings 服务没有「枚举全部 namespace」的 API(研究报告 §3.2),
|
|
6
|
+
* 因此 namespace 清单由宿主注入(真实实现可从 settings.yaml 顶层 key 解析)。
|
|
7
|
+
* 默认只导出名单内且不属于 UI 类(isUiNamespace)的 namespace,与 ui adapter 互斥并集 = 全部设置。
|
|
8
|
+
*/
|
|
9
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
10
|
+
import { msgOf, zhMsg } from '../core/messages.ts';
|
|
11
|
+
import type { MsgFunc } from '../core/messages.ts';
|
|
12
|
+
import type { NamespaceRecord, SettingsSection } from '../schema/types.ts';
|
|
13
|
+
import type {
|
|
14
|
+
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
15
|
+
ImportContext, NamespaceInfo, PlanItem, ValidationResult,
|
|
16
|
+
} from '../core/types.ts';
|
|
17
|
+
import { isUiNamespace } from './ui.ts';
|
|
18
|
+
|
|
19
|
+
/** namespace 清单提供者:宿主从 settings.yaml 顶层 key / 插件注册表获得 */
|
|
20
|
+
export type NamespaceProvider = (ctx: HostContext) => Promise<string[]> | string[];
|
|
21
|
+
|
|
22
|
+
export async function resolveNamespaces(
|
|
23
|
+
namespaces: string[] | NamespaceProvider,
|
|
24
|
+
ctx: HostContext,
|
|
25
|
+
): Promise<string[]> {
|
|
26
|
+
return typeof namespaces === 'function' ? namespaces(ctx) : namespaces;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 批量读取 namespace 的 redacted 记录(settings/ui 共用;单项失败跳过并告警) */
|
|
30
|
+
export async function collectNamespaceRecords(
|
|
31
|
+
ctx: HostContext,
|
|
32
|
+
names: string[],
|
|
33
|
+
warnings: string[],
|
|
34
|
+
): Promise<Record<string, NamespaceRecord>> {
|
|
35
|
+
const namespaces: Record<string, NamespaceRecord> = {};
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
try {
|
|
38
|
+
const info = await ctx.settings.describe(name, { redactSecrets: true });
|
|
39
|
+
namespaces[name] = {
|
|
40
|
+
value: info.value,
|
|
41
|
+
base: info.base,
|
|
42
|
+
revision: info.revision,
|
|
43
|
+
applies: info.applies,
|
|
44
|
+
secrets: info.secrets,
|
|
45
|
+
};
|
|
46
|
+
} catch (err) {
|
|
47
|
+
warnings.push(msgOf(ctx)('adapter.settingsNsReadFailed', { name, reason: err instanceof Error ? err.message : String(err) }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return namespaces;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 值是否“空”(目标已注册但从未配置 → 视为待初始化,Create 而非 Conflict) */
|
|
54
|
+
export function isEmptyValue(v: unknown): boolean {
|
|
55
|
+
if (v === undefined || v === null) return true;
|
|
56
|
+
if (typeof v === 'object') return Object.keys(v as Record<string, unknown>).length === 0;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 对比导入记录与目标当前值:不存在 → Create;一致 → Skip;不同 → Conflict;
|
|
61
|
+
* 目标端 describe 抛错(命名空间未注册:提供它的插件在目标未激活/未安装)→ MissingDependency(§15 依赖检测),
|
|
62
|
+
* 此时 Create/Update 必然失败,标记为“需注意”让导入继续,而不是整体失败。纯计算,零写入。 */
|
|
63
|
+
export async function planNamespaceItems(
|
|
64
|
+
data: Record<string, NamespaceRecord>,
|
|
65
|
+
adapter: 'settings' | 'ui',
|
|
66
|
+
ctx: ImportContext,
|
|
67
|
+
): Promise<PlanItem[]> {
|
|
68
|
+
const msg = ctx.msg;
|
|
69
|
+
const items: PlanItem[] = [];
|
|
70
|
+
for (const [name, rec] of Object.entries(data)) {
|
|
71
|
+
let current: NamespaceInfo | null = null;
|
|
72
|
+
let targetUnavailable = false;
|
|
73
|
+
try {
|
|
74
|
+
current = await ctx.target.settings.describe(name, { redactSecrets: true });
|
|
75
|
+
} catch {
|
|
76
|
+
// 目标连 describe 都失败 → 命名空间未注册(缺少提供插件)或目标不可读:
|
|
77
|
+
// 不能假定“不存在就创建”(replace 会同样失败),按依赖缺失处理。
|
|
78
|
+
targetUnavailable = true;
|
|
79
|
+
}
|
|
80
|
+
const id = `${adapter}:${name}`;
|
|
81
|
+
if (targetUnavailable) {
|
|
82
|
+
items.push({
|
|
83
|
+
id, kind: 'MissingDependency', adapter,
|
|
84
|
+
description: msg('adapter.settingsUnregistered', { ns: name }),
|
|
85
|
+
severity: 'warning', target: { adapter, ref: name },
|
|
86
|
+
});
|
|
87
|
+
} else if (current === null || isEmptyValue(current.value)) {
|
|
88
|
+
// 目标已注册但从未配置(空值)→ 初始化(Create)
|
|
89
|
+
items.push({
|
|
90
|
+
id, kind: 'Create', adapter, description: msg('adapter.settingsCreate', { name }), severity: 'info',
|
|
91
|
+
target: { adapter, ref: name },
|
|
92
|
+
});
|
|
93
|
+
} else if (isDeepStrictEqual(current.value, rec.value)) {
|
|
94
|
+
items.push({ id, kind: 'Skip', adapter, description: msg('adapter.settingsSame', { name }), severity: 'info' });
|
|
95
|
+
} else {
|
|
96
|
+
items.push({
|
|
97
|
+
id, kind: 'Conflict', adapter, description: msg('adapter.settingsDiff', { name }),
|
|
98
|
+
detail: `current=${JSON.stringify(current.value)} imported=${JSON.stringify(rec.value)}`.slice(0, 200),
|
|
99
|
+
severity: 'warning', target: { adapter, ref: name },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return items;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 写入单个 namespace(settings/ui 共用):读时锁 + expectedRevision 乐观锁,防覆盖并发修改 */
|
|
107
|
+
export async function applyNamespaceItem(
|
|
108
|
+
item: PlanItem,
|
|
109
|
+
sectionId: 'settings' | 'ui',
|
|
110
|
+
ctx: ImportContext,
|
|
111
|
+
): Promise<ApplyResult> {
|
|
112
|
+
const ref = item.target?.ref;
|
|
113
|
+
if (!ref) return { ok: false, message: ctx.msg('adapter.missingTargetRef') };
|
|
114
|
+
const data = ctx.sections.get(sectionId) as { namespaces?: Record<string, NamespaceRecord> } | undefined;
|
|
115
|
+
const rec = data?.namespaces?.[ref];
|
|
116
|
+
if (!rec) return { ok: false, message: ctx.msg('adapter.namespaceMissing', { ref }) };
|
|
117
|
+
// 写入前读当前 revision 作为 expectedRevision(读时锁):目标不存在(Create)则不带锁提交
|
|
118
|
+
let expected: number | undefined;
|
|
119
|
+
try {
|
|
120
|
+
expected = (await ctx.target.settings.describe(ref, { redactSecrets: true })).revision;
|
|
121
|
+
} catch {
|
|
122
|
+
expected = undefined;
|
|
123
|
+
}
|
|
124
|
+
await ctx.target.settings.replace(ref, rec.value, expected);
|
|
125
|
+
return { ok: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class SettingsAdapter implements ConfigAdapter<SettingsSection> {
|
|
129
|
+
readonly id = 'settings' as const;
|
|
130
|
+
readonly displayName = 'Settings';
|
|
131
|
+
readonly defaultIncluded = true;
|
|
132
|
+
readonly portability = 'portable' as const;
|
|
133
|
+
private readonly namespaces: string[] | NamespaceProvider;
|
|
134
|
+
|
|
135
|
+
constructor(namespaces: string[] | NamespaceProvider = []) {
|
|
136
|
+
this.namespaces = namespaces;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<SettingsSection>> {
|
|
140
|
+
const warnings: string[] = [];
|
|
141
|
+
const all = await resolveNamespaces(this.namespaces, ctx);
|
|
142
|
+
const nonUi = all.filter((n) => !isUiNamespace(n));
|
|
143
|
+
if (nonUi.length === 0) warnings.push(msgOf(ctx)('adapter.noSettingsNamespaces'));
|
|
144
|
+
const namespaces = await collectNamespaceRecords(ctx, nonUi, warnings);
|
|
145
|
+
return {
|
|
146
|
+
sectionId: 'settings',
|
|
147
|
+
data: { version: 1, namespaces },
|
|
148
|
+
counts: { namespaces: Object.keys(namespaces).length },
|
|
149
|
+
warnings,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async analyzeImport(data: SettingsSection, ctx: ImportContext): Promise<PlanItem[]> {
|
|
154
|
+
return planNamespaceItems(data.namespaces, 'settings', ctx);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
|
|
158
|
+
return applyNamespaceItem(item, 'settings', ctx);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async validate(data: SettingsSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
|
|
162
|
+
const issues: ValidationResult['issues'] = [];
|
|
163
|
+
if (data === null || typeof data !== 'object') {
|
|
164
|
+
return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'settings' }), severity: 'error' }] };
|
|
165
|
+
}
|
|
166
|
+
if (data.version !== 1) {
|
|
167
|
+
issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
|
|
168
|
+
}
|
|
169
|
+
if (data.namespaces === null || typeof data.namespaces !== 'object') {
|
|
170
|
+
issues.push({ path: 'namespaces', message: msg('adapter.validate.missingObject', { subject: 'namespaces' }), severity: 'error' });
|
|
171
|
+
} else {
|
|
172
|
+
for (const [name, rec] of Object.entries(data.namespaces)) {
|
|
173
|
+
if (rec === null || typeof rec !== 'object') {
|
|
174
|
+
issues.push({ path: `namespaces.${name}`, message: msg('adapter.validate.recordObject', { subject: 'namespace' }), severity: 'error' });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (typeof rec.revision !== 'number') {
|
|
178
|
+
issues.push({ path: `namespaces.${name}.revision`, message: msg('adapter.validate.number', { subject: 'revision' }), severity: 'error' });
|
|
179
|
+
}
|
|
180
|
+
if (!('value' in rec)) {
|
|
181
|
+
issues.push({ path: `namespaces.${name}.value`, message: msg('adapter.validate.missingValue'), severity: 'error' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skills 分区 adapter(设计 §3.3):
|
|
3
|
+
* 数据源 = ~/.dsh/skills/(flat .md + 目录 bundle;dsh-skill-filesystem 发现)。
|
|
4
|
+
* 仅迁移用户主目录技能;~/.agents/skills、项目级技能默认不迁(研究报告 §2.2)。
|
|
5
|
+
*/
|
|
6
|
+
import { FileCollectionAdapter } from './file-collection.ts';
|
|
7
|
+
|
|
8
|
+
export class SkillsAdapter extends FileCollectionAdapter {
|
|
9
|
+
readonly id = 'skills' as const;
|
|
10
|
+
readonly displayName = 'Skills';
|
|
11
|
+
readonly defaultIncluded = true;
|
|
12
|
+
readonly portability = 'portable' as const;
|
|
13
|
+
readonly baseDir = 'skills';
|
|
14
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 适配器测试基础设施(仿 src/core/smoke.test.ts 的内存 mock 门面)。
|
|
3
|
+
* 仅供 src/adapters/*.test.ts 使用,不参与生产构建语义。
|
|
4
|
+
*/
|
|
5
|
+
import { normalizePath } from '../utils/paths.ts';
|
|
6
|
+
import { createLogger, type Logger } from '../utils/logger.ts';
|
|
7
|
+
import { sha256Hex } from '../utils/hashing.ts';
|
|
8
|
+
import { zhMsg } from '../core/messages.ts';
|
|
9
|
+
import type {
|
|
10
|
+
CredentialsFacade, FileSystemFacade, HostContext, ImportContext, NamespaceInfo,
|
|
11
|
+
PatchFileFacade, PluginInfo, PluginsFacade, SettingsFacade, Snapshot,
|
|
12
|
+
SnapshotStore, WorkspaceFacade,
|
|
13
|
+
} from '../core/types.ts';
|
|
14
|
+
import type { Manifest, SectionId, WorkspaceRecord } from '../schema/types.ts';
|
|
15
|
+
|
|
16
|
+
export class MemFs implements FileSystemFacade {
|
|
17
|
+
files = new Map<string, Uint8Array>();
|
|
18
|
+
/** 可选(issue #37):详细的链接遍历。测试按需注入;缺省走 listRecursive 回退路径。 */
|
|
19
|
+
listRecursiveDetailed?: FileSystemFacade['listRecursiveDetailed'];
|
|
20
|
+
private readonly homeDir: string;
|
|
21
|
+
constructor(homeDir: string) {
|
|
22
|
+
this.homeDir = homeDir;
|
|
23
|
+
}
|
|
24
|
+
private key(p: string): string {
|
|
25
|
+
return normalizePath(p.startsWith(this.homeDir) ? p : `${this.homeDir}/${p}`);
|
|
26
|
+
}
|
|
27
|
+
async readFile(relPath: string): Promise<Uint8Array> {
|
|
28
|
+
const v = this.files.get(this.key(relPath));
|
|
29
|
+
if (v === undefined) throw new Error(`ENOENT: ${relPath}`);
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
async writeFile(relPath: string, data: Uint8Array): Promise<void> {
|
|
33
|
+
this.files.set(this.key(relPath), data);
|
|
34
|
+
}
|
|
35
|
+
async exists(relPath: string): Promise<boolean> {
|
|
36
|
+
return this.files.has(this.key(relPath));
|
|
37
|
+
}
|
|
38
|
+
async copy(from: string, to: string): Promise<void> {
|
|
39
|
+
const v = this.files.get(this.key(from));
|
|
40
|
+
if (v === undefined) throw new Error(`ENOENT: ${from}`);
|
|
41
|
+
this.files.set(this.key(to), v);
|
|
42
|
+
}
|
|
43
|
+
async remove(relPath: string): Promise<void> {
|
|
44
|
+
this.files.delete(this.key(relPath));
|
|
45
|
+
}
|
|
46
|
+
async listRecursive(dir: string): Promise<string[]> {
|
|
47
|
+
const base = normalizePath(this.homeDir);
|
|
48
|
+
const prefix = normalizePath(dir) === '' ? base : `${base}/${normalizePath(dir)}`;
|
|
49
|
+
const out: string[] = [];
|
|
50
|
+
for (const k of this.files.keys()) {
|
|
51
|
+
if (k === prefix || k.startsWith(prefix + '/')) {
|
|
52
|
+
out.push(k.slice(base.length).replace(/^[\\/]+/, ''));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out.sort();
|
|
56
|
+
}
|
|
57
|
+
async mkdir(): Promise<void> { /* 内存实现无需建目录 */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** ns.set 时自动注册(有值即视为对应插件已激活;对齐真实 dsh-settings 的注册语义) */
|
|
61
|
+
class AutoRegisterMap extends Map<string, { value: unknown; base?: unknown; revision: number; applies?: string[]; secrets: { path: string[]; set: boolean }[] }> {
|
|
62
|
+
private readonly registered: Set<string>;
|
|
63
|
+
constructor(registered: Set<string>) {
|
|
64
|
+
super();
|
|
65
|
+
this.registered = registered;
|
|
66
|
+
}
|
|
67
|
+
override set(key: string, value: { value: unknown; base?: unknown; revision: number; applies?: string[]; secrets: { path: string[]; set: boolean }[] }): this {
|
|
68
|
+
this.registered.add(key);
|
|
69
|
+
return super.set(key, value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class MemSettings implements SettingsFacade {
|
|
74
|
+
/** 已注册命名空间(对齐真实 dsh-settings:插件激活时注册;未注册的 describe/replace 抛错) */
|
|
75
|
+
registered = new Set<string>();
|
|
76
|
+
/** 有值的命名空间自动视为已注册 */
|
|
77
|
+
ns = new AutoRegisterMap(this.registered);
|
|
78
|
+
async describe(namespace: string, _opts?: { redactSecrets?: boolean }): Promise<NamespaceInfo> {
|
|
79
|
+
if (!this.registered.has(namespace)) throw new Error(`settings namespace "${namespace}" is not registered`);
|
|
80
|
+
const rec = this.ns.get(namespace);
|
|
81
|
+
return {
|
|
82
|
+
value: rec?.value,
|
|
83
|
+
base: rec?.base,
|
|
84
|
+
revision: rec?.revision ?? 0,
|
|
85
|
+
applies: rec?.applies,
|
|
86
|
+
secrets: rec?.secrets ?? [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async replace(namespace: string, value: unknown, expectedRevision?: number): Promise<void> {
|
|
90
|
+
if (!this.registered.has(namespace)) throw new Error(`settings namespace "${namespace}" is not registered`);
|
|
91
|
+
const rec = this.ns.get(namespace);
|
|
92
|
+
if (expectedRevision !== undefined && rec && rec.revision !== expectedRevision) {
|
|
93
|
+
throw new Error(`SETTINGS_CONFLICT: ${namespace} revision ${rec.revision} !== ${expectedRevision}`);
|
|
94
|
+
}
|
|
95
|
+
this.ns.set(namespace, {
|
|
96
|
+
value, revision: (rec?.revision ?? 0) + 1, secrets: rec?.secrets ?? [], base: rec?.base, applies: rec?.applies,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async update?(namespace: string, patch: unknown, expectedRevision?: number): Promise<void> {
|
|
100
|
+
if (!this.registered.has(namespace)) throw new Error(`settings namespace "${namespace}" is not registered`);
|
|
101
|
+
const rec = this.ns.get(namespace);
|
|
102
|
+
if (expectedRevision !== undefined && rec && rec.revision !== expectedRevision) {
|
|
103
|
+
throw new Error(`SETTINGS_CONFLICT: ${namespace} revision ${rec.revision} !== ${expectedRevision}`);
|
|
104
|
+
}
|
|
105
|
+
const merged = { ...(rec?.value as Record<string, unknown> | undefined), ...(patch as Record<string, unknown>) };
|
|
106
|
+
this.ns.set(namespace, { value: merged, revision: (rec?.revision ?? 0) + 1, secrets: rec?.secrets ?? [] });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class MemCredentials implements CredentialsFacade {
|
|
111
|
+
values = new Map<string, string>();
|
|
112
|
+
async describe(ref: string): Promise<{ configured: boolean; source?: string; writable?: boolean }> {
|
|
113
|
+
const v = this.values.get(ref);
|
|
114
|
+
return { configured: v !== undefined, source: v !== undefined ? 'file' : 'env', writable: true };
|
|
115
|
+
}
|
|
116
|
+
async set(ref: string, value: string): Promise<void> { this.values.set(ref, value); }
|
|
117
|
+
async unset(ref: string): Promise<void> { this.values.delete(ref); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class MemPlugins implements PluginsFacade {
|
|
121
|
+
installed = new Map<string, PluginInfo>();
|
|
122
|
+
/** 测试钩子:install 抛错(模拟 npm ERESOLVE / 网络失败) */
|
|
123
|
+
failInstall = false;
|
|
124
|
+
/** 测试钩子:记录最近一次 install 收到的 spec(验证非 registry spec 透传) */
|
|
125
|
+
lastSpec: string | undefined;
|
|
126
|
+
async listInstalled(): Promise<PluginInfo[]> {
|
|
127
|
+
return [...this.installed.values()];
|
|
128
|
+
}
|
|
129
|
+
async install(pkg: string, spec?: string): Promise<{ needsRestart: boolean }> {
|
|
130
|
+
if (this.failInstall) throw new Error(`npm error code ERESOLVE: could not resolve ${pkg}`);
|
|
131
|
+
if (this.installed.has(pkg)) return { needsRestart: false };
|
|
132
|
+
this.lastSpec = spec;
|
|
133
|
+
this.installed.set(pkg, { name: pkg, version: '1.0.0', enabled: true, spec });
|
|
134
|
+
return { needsRestart: true };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class MemWorkspace implements WorkspaceFacade {
|
|
139
|
+
records = new Map<string, WorkspaceRecord>();
|
|
140
|
+
async listRecords(): Promise<WorkspaceRecord[]> { return [...this.records.values()]; }
|
|
141
|
+
async writeRecord(r: WorkspaceRecord): Promise<void> { this.records.set(r.id, r); }
|
|
142
|
+
async removeRecord(id: string): Promise<void> { this.records.delete(id); }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class MemPatch implements PatchFileFacade {
|
|
146
|
+
lines = new Map<string, { lineId: string; raw: unknown }>();
|
|
147
|
+
async readPatchLines(_file: string): Promise<{ lineId: string; raw: unknown }[]> {
|
|
148
|
+
return [...this.lines.values()];
|
|
149
|
+
}
|
|
150
|
+
async applyPatchChanges(
|
|
151
|
+
_file: string,
|
|
152
|
+
changes: { lineId: string; raw: unknown; action: 'insert' | 'update' | 'remove' }[],
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
for (const c of changes) {
|
|
155
|
+
if (c.action === 'remove') this.lines.delete(c.lineId);
|
|
156
|
+
else this.lines.set(c.lineId, { lineId: c.lineId, raw: c.raw });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export class MemSnapshotStore implements SnapshotStore {
|
|
162
|
+
snapshots = new Map<string, Snapshot>();
|
|
163
|
+
blobs = new Map<string, Uint8Array>();
|
|
164
|
+
async save(snapshot: Snapshot, blobs: Map<string, Uint8Array> = new Map()): Promise<string> {
|
|
165
|
+
this.snapshots.set(snapshot.id, snapshot);
|
|
166
|
+
for (const [k, v] of blobs) this.blobs.set(`${snapshot.id}/${k}`, v);
|
|
167
|
+
return snapshot.id;
|
|
168
|
+
}
|
|
169
|
+
async load(id: string): Promise<Snapshot> {
|
|
170
|
+
const s = this.snapshots.get(id);
|
|
171
|
+
if (!s) throw new Error(`snapshot not found: ${id}`);
|
|
172
|
+
return s;
|
|
173
|
+
}
|
|
174
|
+
async readBlob(id: string, blobPath: string): Promise<Uint8Array> {
|
|
175
|
+
const v = this.blobs.get(`${id}/${blobPath}`);
|
|
176
|
+
if (!v) throw new Error(`blob not found: ${id}/${blobPath}`);
|
|
177
|
+
return v;
|
|
178
|
+
}
|
|
179
|
+
async updateStatus(id: string, status: Snapshot['status']): Promise<void> {
|
|
180
|
+
const s = this.snapshots.get(id);
|
|
181
|
+
if (!s) throw new Error(`snapshot not found: ${id}`);
|
|
182
|
+
s.status = status;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class MockHostContext implements HostContext {
|
|
187
|
+
platform: string;
|
|
188
|
+
arch = 'x64';
|
|
189
|
+
homeDir: string;
|
|
190
|
+
dshVersion = '0.1.0-rc.6';
|
|
191
|
+
log: Logger;
|
|
192
|
+
profile?: string;
|
|
193
|
+
settings = new MemSettings();
|
|
194
|
+
credentials = new MemCredentials();
|
|
195
|
+
plugins = new MemPlugins();
|
|
196
|
+
workspace = new MemWorkspace();
|
|
197
|
+
patchFile = new MemPatch();
|
|
198
|
+
fs: MemFs;
|
|
199
|
+
constructor(platform: string, homeDir: string, profile?: string) {
|
|
200
|
+
this.platform = platform;
|
|
201
|
+
this.homeDir = homeDir;
|
|
202
|
+
this.profile = profile;
|
|
203
|
+
this.fs = new MemFs(homeDir);
|
|
204
|
+
this.log = createLogger({ level: 'error', sink: () => {} });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function makeContext(platform: string, homeDir: string, profile?: string): MockHostContext {
|
|
209
|
+
return new MockHostContext(platform, homeDir, profile);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 构造最小合法 manifest(adapter 单测用;真实 manifest 由 exporter 生成) */
|
|
213
|
+
export function makeManifest(platform = 'win32'): Manifest {
|
|
214
|
+
return {
|
|
215
|
+
schemaVersion: 1,
|
|
216
|
+
exporter: { name: 'dsh-config-manager-test', version: '0.0.0' },
|
|
217
|
+
source: { dshVersion: '0.1.0-rc.6', platform: platform as Manifest['source']['platform'], arch: 'x64' },
|
|
218
|
+
exportedAt: new Date('2026-08-14T12:00:00.000Z').toISOString(),
|
|
219
|
+
sections: {
|
|
220
|
+
settings: false, ui: false, providers: false, plugins: false, mcp: false, prompts: false,
|
|
221
|
+
skills: false, agentPresets: false, agentInstructions: false, workspaces: false, pluginFiles: false,
|
|
222
|
+
credentialsStatus: false, secrets: false, sessions: false, self: false,
|
|
223
|
+
},
|
|
224
|
+
security: { containsSecrets: false, encrypted: false, encryption: null },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 构造 ImportContext(adapter 单测用;sections 键运行时即 SectionId 字符串) */
|
|
229
|
+
export function makeImportContext(
|
|
230
|
+
target: HostContext,
|
|
231
|
+
sections: ReadonlyMap<string, unknown>,
|
|
232
|
+
overrides: Partial<ImportContext> = {},
|
|
233
|
+
): ImportContext {
|
|
234
|
+
return {
|
|
235
|
+
manifest: makeManifest(target.platform),
|
|
236
|
+
targetPlatform: target.platform,
|
|
237
|
+
target,
|
|
238
|
+
sections: sections as Map<SectionId, unknown>,
|
|
239
|
+
pathMappings: [],
|
|
240
|
+
resolutions: {},
|
|
241
|
+
secretInputs: {},
|
|
242
|
+
log: target.log,
|
|
243
|
+
msg: zhMsg,
|
|
244
|
+
...overrides,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { sha256Hex };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui adapter 测试:UI namespace 名单切分(与 settings 互斥)、uiMigrationNotes 说明导出、写回。
|
|
3
|
+
*/
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { UiAdapter, isUiNamespace, UI_MIGRATION_NOTES } from './ui.ts';
|
|
7
|
+
import { makeContext, makeImportContext } from './test-helpers.ts';
|
|
8
|
+
|
|
9
|
+
const NS = ['general', 'theme', 'pet', 'dsh-better-sidebar', 'llm-deepseek'];
|
|
10
|
+
|
|
11
|
+
test('ui: 名单识别与 settings 互斥切分', () => {
|
|
12
|
+
assert.equal(isUiNamespace('theme'), true);
|
|
13
|
+
assert.equal(isUiNamespace('pet'), true);
|
|
14
|
+
assert.equal(isUiNamespace('dsh-better-sidebar'), true);
|
|
15
|
+
assert.equal(isUiNamespace('ui-onboarding'), true);
|
|
16
|
+
assert.equal(isUiNamespace('general'), false);
|
|
17
|
+
assert.equal(isUiNamespace('llm-deepseek'), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('ui: 导出只含 UI namespace + uiMigrationNotes(纯说明)', async () => {
|
|
21
|
+
const ctx = makeContext('win32', 'C:\\Users\\alice');
|
|
22
|
+
ctx.settings.ns.set('general', { value: { theme: 'dark' }, revision: 3, secrets: [] });
|
|
23
|
+
ctx.settings.ns.set('theme', { value: { mode: 'dark' }, revision: 1, secrets: [] });
|
|
24
|
+
ctx.settings.ns.set('pet', { value: { position: 'bottom-right' }, revision: 2, secrets: [] });
|
|
25
|
+
|
|
26
|
+
const adapter = new UiAdapter(NS);
|
|
27
|
+
const out = await adapter.export(ctx, { includeSecrets: false });
|
|
28
|
+
assert.equal(out.data.version, 1);
|
|
29
|
+
assert.ok(out.data.namespaces['theme']);
|
|
30
|
+
assert.ok(out.data.namespaces['pet']);
|
|
31
|
+
assert.ok(!out.data.namespaces['general'], 'general 非 UI 类,应被 ui 排除');
|
|
32
|
+
assert.equal(out.counts.namespaces, 2);
|
|
33
|
+
assert.ok(out.data.uiMigrationNotes.length >= 1);
|
|
34
|
+
assert.equal(out.data.uiMigrationNotes[0]?.migratable, false);
|
|
35
|
+
assert.equal(UI_MIGRATION_NOTES.length, out.data.uiMigrationNotes.length);
|
|
36
|
+
|
|
37
|
+
const v = await adapter.validate(out.data);
|
|
38
|
+
assert.equal(v.valid, true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('ui: analyzeImport 与 applyItem 写回', async () => {
|
|
42
|
+
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
43
|
+
src.settings.ns.set('theme', { value: { mode: 'dark' }, revision: 1, secrets: [] });
|
|
44
|
+
const adapter = new UiAdapter(NS);
|
|
45
|
+
const exported = await adapter.export(src, { includeSecrets: false });
|
|
46
|
+
const sections = new Map([['ui', exported.data]]);
|
|
47
|
+
|
|
48
|
+
const dst = makeContext('linux', '/home/bob');
|
|
49
|
+
dst.settings.registered.add('theme'); // 目标已注册该 UI 命名空间(空值 → Create)
|
|
50
|
+
const items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
51
|
+
assert.equal(items.length, 1);
|
|
52
|
+
assert.equal(items[0]?.kind, 'Create');
|
|
53
|
+
assert.equal(items[0]?.adapter, 'ui');
|
|
54
|
+
const r = await adapter.applyItem(items[0]!, makeImportContext(dst, sections));
|
|
55
|
+
assert.equal(r.ok, true);
|
|
56
|
+
assert.deepEqual(dst.settings.ns.get('theme')?.value, { mode: 'dark' });
|
|
57
|
+
});
|