@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 适配器 registry(设计 §12.1 / §3.3):
|
|
3
|
+
* 组装全部 ConfigAdapter(导出顺序参考 APPLY_ORDER:settings → ui → providers → prompts
|
|
4
|
+
* → skills → agentPresets → agentInstructions → workspaces → pluginFiles → mcp → plugins → credentialsStatus)。
|
|
5
|
+
*
|
|
6
|
+
* 明确不实现的 adapter(研究报告 §2.2 确认 DSH 无对应概念):keybindings / workflows / commands ——
|
|
7
|
+
* manifest 中不出现这些 section,产品 UI 说明「DSH 当前无此配置」;
|
|
8
|
+
* rules 概念已由 agentInstructions(~/.dsh/AGENTS.md)承接。
|
|
9
|
+
*/
|
|
10
|
+
import type { ConfigAdapter, HostContext } from '../core/types.ts';
|
|
11
|
+
import { SettingsAdapter, type NamespaceProvider } from './settings.ts';
|
|
12
|
+
import { UiAdapter } from './ui.ts';
|
|
13
|
+
import { ProvidersAdapter } from './providers.ts';
|
|
14
|
+
import { PluginsAdapter, type LocalPluginPackHook } from './plugins.ts';
|
|
15
|
+
import { McpAdapter } from './mcp.ts';
|
|
16
|
+
import { PromptsAdapter } from './prompts.ts';
|
|
17
|
+
import { SkillsAdapter } from './skills.ts';
|
|
18
|
+
import { AgentPresetsAdapter } from './agent-presets.ts';
|
|
19
|
+
import { AgentInstructionsAdapter } from './agent-instructions.ts';
|
|
20
|
+
import { WorkspacesAdapter } from './workspaces.ts';
|
|
21
|
+
import { CredentialsAdapter, type CredentialRefsProvider } from './credentials.ts';
|
|
22
|
+
import { PluginFilesAdapter } from './plugin-files.ts';
|
|
23
|
+
import { SessionsAdapter } from './sessions.ts';
|
|
24
|
+
import { SelfAdapter } from './self.ts';
|
|
25
|
+
|
|
26
|
+
export interface AdapterRegistryOptions {
|
|
27
|
+
/** settings namespace 清单(宿主从 settings.yaml 顶层 key 解析注入;缺省 [] → settings/ui/credentials 为空) */
|
|
28
|
+
namespaces?: string[] | NamespaceProvider;
|
|
29
|
+
/** 凭据 ref 收集器(缺省从 settings secrets 标记 + llm apiKeyEnv 推断) */
|
|
30
|
+
credentialsRefs?: CredentialRefsProvider;
|
|
31
|
+
/** pluginFiles 白名单(相对 ~/.dsh 根;缺省 dsh-ssh.json + pet.json) */
|
|
32
|
+
pluginFiles?: string[];
|
|
33
|
+
/** pluginFiles 约定的配置目录(相对 ~/.dsh 根),递归收集其下所有插件配置文件 */
|
|
34
|
+
pluginFilesDir?: string;
|
|
35
|
+
/** 是否包含 sessions 分区(默认关,研究报告 §4.9) */
|
|
36
|
+
includeSessions?: boolean;
|
|
37
|
+
/** 插件自身包名:导出 plugins 分区时不列自己(避免自引用;缺省 dsh-config-manager) */
|
|
38
|
+
selfPluginName?: string;
|
|
39
|
+
/**
|
|
40
|
+
* T1:本地源(link:/file:)插件打包钩子。注入后,导出 plugins 分区时会为这类插件
|
|
41
|
+
* 执行 `npm pack` 并把 tarball 随备份带走;不注入 = 保持改造前行为(本地插件换机丢失)。
|
|
42
|
+
*/
|
|
43
|
+
localPluginPack?: LocalPluginPackHook;
|
|
44
|
+
/**
|
|
45
|
+
* self 分区:插件自身配置目录(相对 ~/.dsh 根,如 'dsh-config-manager')。
|
|
46
|
+
* 缺省 'dsh-config-manager';传入 '' 表示不挂载(宿主自定义 dataDir 在 homeDir 外时)。
|
|
47
|
+
*/
|
|
48
|
+
selfDir?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 组装默认 adapter 列表(pluginFiles/sessions 恒挂载但 defaultIncluded=false,用户勾选才导出) */
|
|
52
|
+
export function createAdapters(options: AdapterRegistryOptions = {}): ConfigAdapter[] {
|
|
53
|
+
const namespaces = options.namespaces ?? [];
|
|
54
|
+
const adapters: ConfigAdapter[] = [
|
|
55
|
+
new SettingsAdapter(namespaces),
|
|
56
|
+
new UiAdapter(namespaces),
|
|
57
|
+
new ProvidersAdapter(),
|
|
58
|
+
new PluginsAdapter(options.selfPluginName, options.localPluginPack),
|
|
59
|
+
new McpAdapter(),
|
|
60
|
+
new PromptsAdapter(),
|
|
61
|
+
new SkillsAdapter(),
|
|
62
|
+
new AgentPresetsAdapter(),
|
|
63
|
+
new AgentInstructionsAdapter(),
|
|
64
|
+
new WorkspacesAdapter(),
|
|
65
|
+
new CredentialsAdapter({ namespaces, refs: options.credentialsRefs }),
|
|
66
|
+
new PluginFilesAdapter(options.pluginFiles, options.pluginFilesDir),
|
|
67
|
+
];
|
|
68
|
+
// self 分区:插件自身配置(同步/市场/偏好),portable 默认包含;'' = 不挂载
|
|
69
|
+
if (options.selfDir !== '') {
|
|
70
|
+
adapters.push(new SelfAdapter(options.selfDir ?? 'dsh-config-manager'));
|
|
71
|
+
}
|
|
72
|
+
if (options.includeSessions) adapters.push(new SessionsAdapter());
|
|
73
|
+
return adapters;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type { NamespaceProvider } from './settings.ts';
|
|
77
|
+
export type { CredentialRefsProvider } from './credentials.ts';
|
|
78
|
+
|
|
79
|
+
export { SettingsAdapter } from './settings.ts';
|
|
80
|
+
export { UiAdapter, isUiNamespace, KNOWN_UI_NAMESPACE_PREFIXES, UI_MIGRATION_NOTES } from './ui.ts';
|
|
81
|
+
export { ProvidersAdapter, DEFAULT_PROVIDER_NAMESPACES, type ProviderExportEntry, type ProviderExportSection } from './providers.ts';
|
|
82
|
+
export { PluginsAdapter, USER_PATCH_FILE } from './plugins.ts';
|
|
83
|
+
export type { LocalPluginPackHook } from './plugins.ts';
|
|
84
|
+
export { McpAdapter, extractMcpServers, buildMcpPatchLine, type McpExportEntry, type McpExportSection } from './mcp.ts';
|
|
85
|
+
export { PromptsAdapter, extractPrompts, mergePromptIntoLine, buildPromptLine, type PromptExportEntry, type PromptsExportSection } from './prompts.ts';
|
|
86
|
+
export { SkillsAdapter } from './skills.ts';
|
|
87
|
+
export { AgentPresetsAdapter } from './agent-presets.ts';
|
|
88
|
+
export { AgentInstructionsAdapter } from './agent-instructions.ts';
|
|
89
|
+
export { WorkspacesAdapter } from './workspaces.ts';
|
|
90
|
+
export { CredentialsAdapter, defaultCredentialRefs } from './credentials.ts';
|
|
91
|
+
export { PluginFilesAdapter, DEFAULT_PLUGIN_FILE_WHITELIST } from './plugin-files.ts';
|
|
92
|
+
export { SessionsAdapter } from './sessions.ts';
|
|
93
|
+
export { SelfAdapter, SELF_CONFIG_FILES } from './self.ts';
|
|
94
|
+
export { FileCollectionAdapter } from './file-collection.ts';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue #37:链接目录的告警构造(备份必须能说明「跟随了什么 / 跳过了什么」)。
|
|
3
|
+
*/
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { linkWarnings, listFilesDetailed } from './link-report.ts';
|
|
7
|
+
import { zhMsg } from '../core/messages.ts';
|
|
8
|
+
import type { FileSystemFacade } from '../core/types.ts';
|
|
9
|
+
import type { RecursiveListing } from '../utils/recursive-walk.ts';
|
|
10
|
+
|
|
11
|
+
const clean: RecursiveListing = { paths: ['skills/a.md'], skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
|
|
12
|
+
|
|
13
|
+
test('linkWarnings:无链接 → 零告警(不制造噪音)', () => {
|
|
14
|
+
assert.deepEqual(linkWarnings(zhMsg, 'Skills', clean), []);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('linkWarnings:跟随的链接必须提示「结构不会重建」', () => {
|
|
18
|
+
const out = linkWarnings(zhMsg, 'Skills', { ...clean, followedLinks: 3 });
|
|
19
|
+
assert.equal(out.length, 1);
|
|
20
|
+
assert.match(out[0]!, /3/);
|
|
21
|
+
assert.match(out[0]!, /链接目录/);
|
|
22
|
+
assert.match(out[0]!, /链接结构不会重建/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('linkWarnings:跳过项按原因归并,给出数量、原因与路径', () => {
|
|
26
|
+
const out = linkWarnings(zhMsg, 'Skills', {
|
|
27
|
+
paths: [],
|
|
28
|
+
followedLinks: 1,
|
|
29
|
+
unreadableDirs: [],
|
|
30
|
+
skippedLinks: [
|
|
31
|
+
{ path: 'skills/self', reason: 'loop' },
|
|
32
|
+
{ path: 'skills/broken', reason: 'broken' },
|
|
33
|
+
{ path: 'skills/outside', reason: 'outside-home' },
|
|
34
|
+
{ path: 'skills/fifo', reason: 'unreadable' },
|
|
35
|
+
{ path: 'skills/deep', reason: 'too-deep' },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
assert.equal(out.length, 2, '跟随 + 跳过各一条');
|
|
39
|
+
const skipped = out[1]!;
|
|
40
|
+
assert.match(skipped, /5 个链接被跳过/);
|
|
41
|
+
assert.match(skipped, /自引用\/重复链接/);
|
|
42
|
+
assert.match(skipped, /断链/);
|
|
43
|
+
assert.match(skipped, /目标在 DSH home 之外/);
|
|
44
|
+
assert.match(skipped, /skills\/self/);
|
|
45
|
+
// 未进备份必须说清楚,而不是「已跳过」这类中性的措辞
|
|
46
|
+
assert.match(skipped, /未进备份/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('linkWarnings:目录读取失败也必须告警(其内容同样未进备份)', () => {
|
|
50
|
+
const out = linkWarnings(zhMsg, 'Skills', {
|
|
51
|
+
paths: [], followedLinks: 0, skippedLinks: [], unreadableDirs: ['skills/locked', 'skills/denied'],
|
|
52
|
+
});
|
|
53
|
+
assert.equal(out.length, 1);
|
|
54
|
+
assert.match(out[0]!, /2 个目录读取失败/);
|
|
55
|
+
assert.match(out[0]!, /未进备份/);
|
|
56
|
+
assert.match(out[0]!, /skills\/locked/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('listFilesDetailed:宿主未实现 detailed 时回退到 listRecursive(旧行为不变)', async () => {
|
|
60
|
+
const fs = { listRecursive: async () => ['skills/x.md'] } as unknown as FileSystemFacade;
|
|
61
|
+
const listing = await listFilesDetailed(fs, 'skills');
|
|
62
|
+
assert.deepEqual(listing.paths, ['skills/x.md']);
|
|
63
|
+
assert.deepEqual(listing.skippedLinks, []);
|
|
64
|
+
assert.equal(listing.followedLinks, 0);
|
|
65
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件类分区的「链接目录」报告(issue #37)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:备份静默跳过 junction/符号链接,用户拿到的仍是「成功」,却少了整块内容。
|
|
5
|
+
* 修复分两步:① `utils/recursive-walk.ts` 跟随链接收集内容;② 本模块把「跟随了什么、
|
|
6
|
+
* 跳过了什么、为什么跳过」变成备份报告里可见的告警 —— 缺了 ②,用户依然无从察觉缺失。
|
|
7
|
+
*
|
|
8
|
+
* 纯函数(除 listFilesDetailed 的 IO 委托外),两个 adapter 共用,避免文案再次漂移。
|
|
9
|
+
*/
|
|
10
|
+
import type { FileSystemFacade } from '../core/types.ts';
|
|
11
|
+
import type { MsgFunc } from '../core/messages.ts';
|
|
12
|
+
import type { RecursiveListing, SkippedLink } from '../utils/recursive-walk.ts';
|
|
13
|
+
|
|
14
|
+
/** 跳过原因 → 消息 key(与 SkippedLink.reason 一一对应,穷尽映射) */
|
|
15
|
+
const REASON_KEY: Record<SkippedLink['reason'], string> = {
|
|
16
|
+
loop: 'adapter.linkReason.loop',
|
|
17
|
+
'outside-home': 'adapter.linkReason.outsideHome',
|
|
18
|
+
broken: 'adapter.linkReason.broken',
|
|
19
|
+
unreadable: 'adapter.linkReason.unreadable',
|
|
20
|
+
'too-deep': 'adapter.linkReason.tooDeep',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 单条原因下最多列出的路径数(其余折叠为计数,避免把报告撑爆) */
|
|
24
|
+
const MAX_PATHS_PER_REASON = 5
|
|
25
|
+
|
|
26
|
+
/** 取遍历结果:宿主未实现 listRecursiveDetailed(旧版/测试 mock)时回退,行为与旧版一致。 */
|
|
27
|
+
export async function listFilesDetailed(fs: FileSystemFacade, dir: string): Promise<RecursiveListing> {
|
|
28
|
+
if (fs.listRecursiveDetailed !== undefined) return fs.listRecursiveDetailed(dir)
|
|
29
|
+
return { paths: await fs.listRecursive(dir), skippedLinks: [], followedLinks: 0, unreadableDirs: [] }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 把遍历结果转成告警行(无链接时返回空数组 —— 不制造噪音)。 */
|
|
33
|
+
export function linkWarnings(msg: MsgFunc, type: string, listing: RecursiveListing): string[] {
|
|
34
|
+
const out: string[] = []
|
|
35
|
+
if (listing.followedLinks > 0) {
|
|
36
|
+
out.push(msg('adapter.linksFollowed', { type, count: String(listing.followedLinks) }))
|
|
37
|
+
}
|
|
38
|
+
if (listing.skippedLinks.length > 0) {
|
|
39
|
+
const byReason = new Map<SkippedLink['reason'], string[]>()
|
|
40
|
+
for (const s of listing.skippedLinks) {
|
|
41
|
+
const arr = byReason.get(s.reason) ?? []
|
|
42
|
+
arr.push(s.path)
|
|
43
|
+
byReason.set(s.reason, arr)
|
|
44
|
+
}
|
|
45
|
+
const detail = [...byReason.entries()].map(([reason, paths]) => {
|
|
46
|
+
const shown = paths.slice(0, MAX_PATHS_PER_REASON).join(', ')
|
|
47
|
+
const more = paths.length > MAX_PATHS_PER_REASON ? ` (+ ${paths.length - MAX_PATHS_PER_REASON})` : ''
|
|
48
|
+
return `${msg(REASON_KEY[reason])} ${paths.length} 个: ${shown}${more}`
|
|
49
|
+
}).join(';')
|
|
50
|
+
out.push(msg('adapter.linksSkipped', { type, count: String(listing.skippedLinks.length), detail }))
|
|
51
|
+
}
|
|
52
|
+
if (listing.unreadableDirs.length > 0) {
|
|
53
|
+
const shown = listing.unreadableDirs.slice(0, MAX_PATHS_PER_REASON).join(', ')
|
|
54
|
+
const more = listing.unreadableDirs.length > MAX_PATHS_PER_REASON
|
|
55
|
+
? ` (+ ${listing.unreadableDirs.length - MAX_PATHS_PER_REASON})` : ''
|
|
56
|
+
out.push(msg('adapter.dirsUnreadable', {
|
|
57
|
+
type, count: String(listing.unreadableDirs.length), detail: `${shown}${more}`,
|
|
58
|
+
}))
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp adapter 测试:组合 patch 提取(单行 + insert 块)、Create/Skip/Conflict、
|
|
3
|
+
* applyItem 写 patch 行(needsRestart)、validate。
|
|
4
|
+
*/
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { McpAdapter, extractMcpServers, buildMcpPatchLine } from './mcp.ts';
|
|
8
|
+
import { makeContext, makeImportContext } from './test-helpers.ts';
|
|
9
|
+
import type { PlanItem } from '../core/types.ts';
|
|
10
|
+
|
|
11
|
+
test('mcp: extractMcpServers 兼容单行与 insert 块', () => {
|
|
12
|
+
const lines = [
|
|
13
|
+
{
|
|
14
|
+
lineId: 'mcp-fs',
|
|
15
|
+
raw: {
|
|
16
|
+
id: 'mcp-fs',
|
|
17
|
+
name: 'dsh-mcp-client',
|
|
18
|
+
config: { serverName: 'filesystem', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'], cwd: 'C:\\Users\\alice\\projects' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
lineId: 'mcp-http',
|
|
23
|
+
raw: {
|
|
24
|
+
insert: [
|
|
25
|
+
{ id: 'mcp-http', name: 'dsh-mcp-client', config: { serverName: 'remote', url: 'https://mcp.example.com/sse', headers: { Authorization: 'Bearer abc' } } },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ lineId: 'other', raw: { id: 'other', name: '@deepseek-ai/dsh-web', config: { searchProvider: 'deepseek-official' } } },
|
|
30
|
+
];
|
|
31
|
+
const servers = extractMcpServers(lines);
|
|
32
|
+
assert.equal(servers.length, 2);
|
|
33
|
+
const fs = servers.find((s) => s.serverName === 'filesystem');
|
|
34
|
+
assert.equal(fs?.type, 'stdio');
|
|
35
|
+
assert.equal(fs?.command, 'npx');
|
|
36
|
+
assert.equal(fs?.cwd, 'C:\\Users\\alice\\projects');
|
|
37
|
+
assert.equal(fs?.sourceLineId, 'mcp-fs');
|
|
38
|
+
const http = servers.find((s) => s.serverName === 'remote');
|
|
39
|
+
assert.equal(http?.type, 'streamable-http');
|
|
40
|
+
assert.equal(http?.url, 'https://mcp.example.com/sse');
|
|
41
|
+
assert.equal(http?.sourceLineId, 'mcp-http');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('mcp: buildMcpPatchLine 构造写回行', () => {
|
|
45
|
+
const line = buildMcpPatchLine('mcp-fs', { serverName: 'filesystem', type: 'stdio', command: 'npx', args: ['-y', 'x'] });
|
|
46
|
+
assert.equal(line.id, 'mcp-fs');
|
|
47
|
+
assert.equal(line.name, 'dsh-mcp-client');
|
|
48
|
+
assert.equal((line.config as Record<string, unknown>)['command'], 'npx');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('mcp: export 提取 + analyzeImport Create/Skip/Conflict + applyItem 写 patch(needsRestart)', async () => {
|
|
52
|
+
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
53
|
+
src.patchFile.lines.set('mcp-fs', {
|
|
54
|
+
lineId: 'mcp-fs',
|
|
55
|
+
raw: { id: 'mcp-fs', name: 'dsh-mcp-client', config: { serverName: 'filesystem', command: 'npx', args: ['-y', 'x'] } },
|
|
56
|
+
});
|
|
57
|
+
const adapter = new McpAdapter();
|
|
58
|
+
const exported = await adapter.export(src, { includeSecrets: false });
|
|
59
|
+
assert.equal(exported.data.servers.length, 1);
|
|
60
|
+
assert.equal(exported.data.servers[0]?.serverName, 'filesystem');
|
|
61
|
+
const sections = new Map([['mcp', exported.data]]);
|
|
62
|
+
|
|
63
|
+
const dst = makeContext('linux', '/home/bob');
|
|
64
|
+
let items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
65
|
+
assert.equal(items.length, 1);
|
|
66
|
+
assert.equal(items[0]?.kind, 'Create');
|
|
67
|
+
assert.equal(items[0]?.target?.ref, 'mcp-fs');
|
|
68
|
+
let r = await adapter.applyItem(items[0]!, makeImportContext(dst, sections));
|
|
69
|
+
assert.equal(r.ok, true);
|
|
70
|
+
assert.equal(r.needsRestart, true, 'MCP 写 patch 必须重启生效');
|
|
71
|
+
assert.ok(dst.patchFile.lines.has('mcp-fs'));
|
|
72
|
+
assert.equal((dst.patchFile.lines.get('mcp-fs')?.raw as Record<string, unknown>)['name'], 'dsh-mcp-client');
|
|
73
|
+
|
|
74
|
+
// 幂等:同 serverName 同内容 → Skip
|
|
75
|
+
items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
76
|
+
assert.equal(items[0]?.kind, 'Skip');
|
|
77
|
+
|
|
78
|
+
// 目标同 serverName 不同内容 → Conflict(target.ref = 目标行 id)
|
|
79
|
+
dst.patchFile.lines.set('mcp-fs', {
|
|
80
|
+
lineId: 'mcp-fs',
|
|
81
|
+
raw: { id: 'mcp-fs', name: 'dsh-mcp-client', config: { serverName: 'filesystem', command: 'npx', args: ['-y', 'other'] } },
|
|
82
|
+
});
|
|
83
|
+
items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
84
|
+
assert.equal(items[0]?.kind, 'Conflict');
|
|
85
|
+
const useItem: PlanItem = { ...items[0]!, kind: 'Update' };
|
|
86
|
+
r = await adapter.applyItem(useItem, makeImportContext(dst, sections));
|
|
87
|
+
assert.equal(r.ok, true);
|
|
88
|
+
const raw = dst.patchFile.lines.get('mcp-fs')?.raw as Record<string, unknown>;
|
|
89
|
+
assert.deepEqual((raw['config'] as Record<string, unknown>)['args'], ['-y', 'x'], 'Conflict useImported 应更新目标行');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('mcp: validate 拒绝无 serverName 的条目', async () => {
|
|
93
|
+
const adapter = new McpAdapter();
|
|
94
|
+
const bad = await adapter.validate({ version: 1, servers: [{ serverName: '' } as never] });
|
|
95
|
+
assert.equal(bad.valid, false);
|
|
96
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp 分区 adapter(设计 §3.3/§1.2/§15):
|
|
3
|
+
* 数据源 = 组合 patch 中 dsh-mcp-client 行的 config(研究报告 §2.2:MCP 无 settings 面/无管理 API)。
|
|
4
|
+
* 导入 = 写 profile cordis.patch.yml 插行(needsRestart 生效);依赖检测(npx 等)由 analyzer 的
|
|
5
|
+
* dependencyChecker 执行(§15:缺失不阻塞,标记 Requires Attention)。
|
|
6
|
+
*
|
|
7
|
+
* patch 行 raw 支持两种形态:单行 { id, name, config } 与块 { insert: [{ id, name, config }] }。
|
|
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 { McpServerEntry, McpSection } from '../schema/types.ts';
|
|
13
|
+
import type {
|
|
14
|
+
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
15
|
+
ImportContext, PlanItem, ValidationResult,
|
|
16
|
+
} from '../core/types.ts';
|
|
17
|
+
import { USER_PATCH_FILE } from './plugins.ts';
|
|
18
|
+
|
|
19
|
+
/** 导出记录:McpServerEntry 之外附加来源 patch 行 id(导入写回定位用) */
|
|
20
|
+
export interface McpExportEntry extends McpServerEntry {
|
|
21
|
+
sourceLineId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface McpExportSection { version: 1; servers: McpExportEntry[]; }
|
|
25
|
+
|
|
26
|
+
/** 从 patch 行数组中提取 MCP server 条目(以 config.serverName 存在为判定) */
|
|
27
|
+
export function extractMcpServers(lines: { lineId: string; raw: unknown }[]): McpExportEntry[] {
|
|
28
|
+
const servers: McpExportEntry[] = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
for (const entry of entriesOf(line.raw)) {
|
|
31
|
+
const config = entry.config;
|
|
32
|
+
if (config === null || typeof config !== 'object') continue;
|
|
33
|
+
const c = config as Record<string, unknown>;
|
|
34
|
+
const serverName = c['serverName'];
|
|
35
|
+
if (typeof serverName !== 'string' || serverName === '') continue;
|
|
36
|
+
const type = typeof c['url'] === 'string' && c['url'] !== '' ? 'streamable-http' : 'stdio';
|
|
37
|
+
servers.push({
|
|
38
|
+
serverName,
|
|
39
|
+
type,
|
|
40
|
+
command: typeof c['command'] === 'string' ? c['command'] : undefined,
|
|
41
|
+
args: Array.isArray(c['args']) ? (c['args'] as string[]) : undefined,
|
|
42
|
+
env: c['env'] !== null && typeof c['env'] === 'object' ? (c['env'] as Record<string, string>) : undefined,
|
|
43
|
+
cwd: typeof c['cwd'] === 'string' ? c['cwd'] : undefined,
|
|
44
|
+
url: typeof c['url'] === 'string' ? c['url'] : undefined,
|
|
45
|
+
headers: c['headers'] !== null && typeof c['headers'] === 'object' ? (c['headers'] as Record<string, string>) : undefined,
|
|
46
|
+
sourceLineId: line.lineId,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return servers;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** patch 行 → 可枚举的 entry 列表(兼容单行与 insert 块) */
|
|
54
|
+
function entriesOf(raw: unknown): { id?: unknown; name?: unknown; config?: unknown }[] {
|
|
55
|
+
if (raw === null || typeof raw !== 'object') return [];
|
|
56
|
+
const obj = raw as Record<string, unknown>;
|
|
57
|
+
if (Array.isArray(obj['insert'])) {
|
|
58
|
+
return obj['insert']
|
|
59
|
+
.filter((e): e is Record<string, unknown> => e !== null && typeof e === 'object')
|
|
60
|
+
.map((e) => e as { id?: unknown; name?: unknown; config?: unknown });
|
|
61
|
+
}
|
|
62
|
+
if (obj['id'] !== undefined || obj['name'] !== undefined) {
|
|
63
|
+
return [obj as { id?: unknown; name?: unknown; config?: unknown }];
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 由导出条目构造写回 patch 行 raw(单行形态,与 dsh-mcp-client 组合 config 语义一致) */
|
|
69
|
+
export function buildMcpPatchLine(lineId: string, server: McpServerEntry): Record<string, unknown> {
|
|
70
|
+
const config: Record<string, unknown> = { serverName: server.serverName };
|
|
71
|
+
if (server.type === 'streamable-http') {
|
|
72
|
+
config['url'] = server.url;
|
|
73
|
+
if (server.headers !== undefined) config['headers'] = server.headers;
|
|
74
|
+
} else {
|
|
75
|
+
config['command'] = server.command;
|
|
76
|
+
if (server.args !== undefined) config['args'] = server.args;
|
|
77
|
+
if (server.env !== undefined) config['env'] = server.env;
|
|
78
|
+
if (server.cwd !== undefined) config['cwd'] = server.cwd;
|
|
79
|
+
}
|
|
80
|
+
return { id: lineId, name: 'dsh-mcp-client', config };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function newLineId(serverName: string): string {
|
|
84
|
+
const slug = serverName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
85
|
+
return `mcp-${slug || 'server'}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class McpAdapter implements ConfigAdapter<McpExportSection> {
|
|
89
|
+
readonly id = 'mcp' as const;
|
|
90
|
+
readonly displayName = 'MCP Servers';
|
|
91
|
+
readonly defaultIncluded = true;
|
|
92
|
+
readonly portability = 'platformSpecific' as const;
|
|
93
|
+
|
|
94
|
+
async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<McpExportSection>> {
|
|
95
|
+
const warnings: string[] = [];
|
|
96
|
+
let lines: { lineId: string; raw: unknown }[] = [];
|
|
97
|
+
try {
|
|
98
|
+
lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
warnings.push(msgOf(ctx)('adapter.patchReadFailedMCP', { reason: err instanceof Error ? err.message : String(err) }));
|
|
101
|
+
}
|
|
102
|
+
const servers = extractMcpServers(lines);
|
|
103
|
+
return {
|
|
104
|
+
sectionId: 'mcp',
|
|
105
|
+
data: { version: 1, servers },
|
|
106
|
+
counts: { servers: servers.length },
|
|
107
|
+
warnings,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async analyzeImport(data: McpExportSection, ctx: ImportContext): Promise<PlanItem[]> {
|
|
112
|
+
const msg = ctx.msg;
|
|
113
|
+
const items: PlanItem[] = [];
|
|
114
|
+
const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
|
|
115
|
+
const targetServers = extractMcpServers(targetLines);
|
|
116
|
+
for (const server of data.servers) {
|
|
117
|
+
const id = `mcp:${server.serverName}`;
|
|
118
|
+
const existing = targetServers.find((s) => s.serverName === server.serverName);
|
|
119
|
+
const comparable = { ...server } as Record<string, unknown>;
|
|
120
|
+
delete comparable.sourceLineId;
|
|
121
|
+
if (!existing) {
|
|
122
|
+
const lineId = server.sourceLineId && targetLines.some((l) => l.lineId === server.sourceLineId)
|
|
123
|
+
? `${server.sourceLineId}-imported`
|
|
124
|
+
: (server.sourceLineId ?? newLineId(server.serverName));
|
|
125
|
+
items.push({
|
|
126
|
+
id, kind: 'Create', adapter: 'mcp',
|
|
127
|
+
description: msg('adapter.mcpCreate', { serverName: server.serverName }),
|
|
128
|
+
detail: `${server.type === 'stdio' ? `${server.command} ${(server.args ?? []).join(' ')}` : server.url}`,
|
|
129
|
+
severity: 'info',
|
|
130
|
+
target: { adapter: 'mcp', ref: lineId },
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
const existingComparable = { ...existing } as Record<string, unknown>;
|
|
134
|
+
delete existingComparable.sourceLineId;
|
|
135
|
+
if (isDeepStrictEqual(existingComparable, comparable)) {
|
|
136
|
+
items.push({ id, kind: 'Skip', adapter: 'mcp', description: msg('adapter.mcpSame', { serverName: server.serverName }), severity: 'info' });
|
|
137
|
+
} else {
|
|
138
|
+
items.push({
|
|
139
|
+
id, kind: 'Conflict', adapter: 'mcp',
|
|
140
|
+
description: msg('adapter.mcpDiff', { serverName: server.serverName }),
|
|
141
|
+
detail: `current=${JSON.stringify(existingComparable)} imported=${JSON.stringify(comparable)}`.slice(0, 200),
|
|
142
|
+
severity: 'warning',
|
|
143
|
+
target: { adapter: 'mcp', ref: existing.sourceLineId },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return items;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
|
|
152
|
+
const msg = ctx.msg;
|
|
153
|
+
const serverName = item.id.replace(/^mcp:/, '');
|
|
154
|
+
const data = ctx.sections.get('mcp') as McpExportSection | undefined;
|
|
155
|
+
const server = data?.servers.find((s) => s.serverName === serverName);
|
|
156
|
+
if (!server) return { ok: false, message: msg('adapter.mcpMissing', { serverName }) };
|
|
157
|
+
const ref = item.target?.ref;
|
|
158
|
+
if (!ref) return { ok: false, message: msg('adapter.missingTargetRef') };
|
|
159
|
+
const raw = buildMcpPatchLine(ref, server);
|
|
160
|
+
await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
|
|
161
|
+
{ lineId: ref, raw, action: item.kind === 'Create' ? 'insert' : 'update' },
|
|
162
|
+
]);
|
|
163
|
+
return { ok: true, needsRestart: true, message: msg('adapter.mcpWritten', { serverName }) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async validate(data: McpExportSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
|
|
167
|
+
const issues: ValidationResult['issues'] = [];
|
|
168
|
+
if (data === null || typeof data !== 'object') {
|
|
169
|
+
return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'mcp' }), severity: 'error' }] };
|
|
170
|
+
}
|
|
171
|
+
if (data.version !== 1) {
|
|
172
|
+
issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
|
|
173
|
+
}
|
|
174
|
+
if (!Array.isArray(data.servers)) {
|
|
175
|
+
issues.push({ path: 'servers', message: msg('adapter.validate.array', { subject: 'servers' }), severity: 'error' });
|
|
176
|
+
} else {
|
|
177
|
+
for (const s of data.servers) {
|
|
178
|
+
if (s === null || typeof s !== 'object' || typeof s.serverName !== 'string' || s.serverName === '') {
|
|
179
|
+
issues.push({ path: 'servers[]', message: msg('adapter.validate.serverName'), severity: 'error' });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pluginFiles 分区 adapter(可选,设计 §3.3/§1.2):
|
|
3
|
+
* 数据源 = 插件自有配置文件(相对 ~/.dsh 根):
|
|
4
|
+
* 1. 白名单固定文件(dsh-ssh.json、pet.json 等,避免扫描整个主目录);
|
|
5
|
+
* 2. 约定的插件配置目录(collectDir,如 plugin-config/)递归收集其下所有文件。
|
|
6
|
+
* relativePath 一律是「相对 ~/.dsh 根」的完整路径(与白名单文件一致),
|
|
7
|
+
* 导入时按同一相对路径写回原位置。默认关闭(defaultIncluded=false,用户显式勾选才导出)。
|
|
8
|
+
*/
|
|
9
|
+
import { sha256Hex } from '../utils/hashing.ts';
|
|
10
|
+
import { msgOf, zhMsg } from '../core/messages.ts';
|
|
11
|
+
import { linkWarnings, listFilesDetailed } from './link-report.ts';
|
|
12
|
+
import type { RecursiveListing } from '../utils/recursive-walk.ts';
|
|
13
|
+
import { isPathSafe, isReservedInternalRel } from '../utils/paths.ts';
|
|
14
|
+
import type { MsgFunc } from '../core/messages.ts';
|
|
15
|
+
import type { FilesSection } from '../schema/types.ts';
|
|
16
|
+
import type {
|
|
17
|
+
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
18
|
+
ImportContext, PlanItem, ValidationResult,
|
|
19
|
+
} from '../core/types.ts';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_PLUGIN_FILE_WHITELIST: readonly string[] = ['dsh-ssh.json', 'pet.json'];
|
|
22
|
+
|
|
23
|
+
export class PluginFilesAdapter implements ConfigAdapter<FilesSection> {
|
|
24
|
+
readonly id = 'pluginFiles' as const;
|
|
25
|
+
readonly displayName = 'Plugin Files';
|
|
26
|
+
readonly defaultIncluded = false;
|
|
27
|
+
readonly portability = 'deviceSpecific' as const;
|
|
28
|
+
private readonly whitelist: string[];
|
|
29
|
+
/** 约定配置目录(相对 ~/.dsh 根,如 'plugin-config');递归收集其下所有文件。undefined = 不收集。 */
|
|
30
|
+
private readonly collectDir?: string;
|
|
31
|
+
|
|
32
|
+
constructor(whitelist: string[] = [...DEFAULT_PLUGIN_FILE_WHITELIST], collectDir?: string) {
|
|
33
|
+
this.whitelist = whitelist;
|
|
34
|
+
if (collectDir !== undefined && collectDir !== '' && !isPathSafe(collectDir)) {
|
|
35
|
+
throw new Error(`pluginFiles collectDir 非法(须为相对 ~/.dsh 根的安全路径): ${collectDir}`);
|
|
36
|
+
}
|
|
37
|
+
this.collectDir = collectDir === '' ? undefined : collectDir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<FilesSection>> {
|
|
41
|
+
const files: FilesSection['files'] = [];
|
|
42
|
+
const seen = new Set<string>();
|
|
43
|
+
// 1) 白名单固定文件(不存在则跳过,dsh-ssh.json 等为按需创建)
|
|
44
|
+
for (const rel of this.whitelist) {
|
|
45
|
+
try {
|
|
46
|
+
const data = await ctx.fs.readFile(rel);
|
|
47
|
+
files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
|
|
48
|
+
seen.add(rel);
|
|
49
|
+
} catch {
|
|
50
|
+
/* 白名单文件不存在则跳过 */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 2) 约定配置目录递归收集(相对 ~/.dsh 根的完整路径;与白名单文件去重)
|
|
54
|
+
// issue #37:与 skills 等同一条遍历(跟随 junction/符号链接 + 跳过留痕)
|
|
55
|
+
let listing: RecursiveListing = { paths: [], skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
|
|
56
|
+
if (this.collectDir !== undefined) {
|
|
57
|
+
try {
|
|
58
|
+
listing = await listFilesDetailed(ctx.fs, this.collectDir);
|
|
59
|
+
} catch {
|
|
60
|
+
// 目录不存在视为空
|
|
61
|
+
}
|
|
62
|
+
const rels = listing.paths;
|
|
63
|
+
for (const rel of rels) {
|
|
64
|
+
if (seen.has(rel)) continue;
|
|
65
|
+
try {
|
|
66
|
+
const data = await ctx.fs.readFile(rel);
|
|
67
|
+
files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
sectionId: 'pluginFiles',
|
|
75
|
+
data: { version: 1, files },
|
|
76
|
+
counts: { files: files.length },
|
|
77
|
+
warnings: linkWarnings(msgOf(ctx), this.displayName, listing),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async analyzeImport(data: FilesSection, ctx: ImportContext): Promise<PlanItem[]> {
|
|
82
|
+
const msg = ctx.msg;
|
|
83
|
+
const items: PlanItem[] = [];
|
|
84
|
+
for (const file of data.files) {
|
|
85
|
+
const id = `pluginFile:${file.relativePath}`;
|
|
86
|
+
// F23 修复:不可信 import 不得写内部 control-plane namespace(snapshots/transactions/locks/safe-mode)
|
|
87
|
+
if (isReservedInternalRel(file.relativePath)) {
|
|
88
|
+
items.push({
|
|
89
|
+
id, kind: 'Error', adapter: 'pluginFiles',
|
|
90
|
+
description: msg('adapter.pluginFileReserved', { path: file.relativePath }), severity: 'error',
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let current: Uint8Array | null = null;
|
|
95
|
+
try {
|
|
96
|
+
current = await ctx.target.fs.readFile(file.relativePath);
|
|
97
|
+
} catch {
|
|
98
|
+
current = null;
|
|
99
|
+
}
|
|
100
|
+
if (current === null) {
|
|
101
|
+
items.push({
|
|
102
|
+
id, kind: 'Create', adapter: 'pluginFiles',
|
|
103
|
+
description: msg('adapter.pluginFileCreate', { path: file.relativePath }), severity: 'info',
|
|
104
|
+
target: { adapter: 'pluginFiles', ref: file.relativePath },
|
|
105
|
+
});
|
|
106
|
+
} else if (sha256Hex(current) === file.contentHash) {
|
|
107
|
+
items.push({ id, kind: 'Skip', adapter: 'pluginFiles', description: msg('adapter.fileSame', { path: file.relativePath }), severity: 'info' });
|
|
108
|
+
} else {
|
|
109
|
+
items.push({
|
|
110
|
+
id, kind: 'Conflict', adapter: 'pluginFiles',
|
|
111
|
+
description: msg('adapter.fileDiff', { path: file.relativePath }), severity: 'warning',
|
|
112
|
+
target: { adapter: 'pluginFiles', ref: file.relativePath },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return items;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
|
|
120
|
+
const ref = item.target?.ref;
|
|
121
|
+
if (!ref) return { ok: false, message: ctx.msg('adapter.missingTargetRef') };
|
|
122
|
+
// F23 修复:apply 前拒绝写内部 control-plane namespace(纵深防御,analyzeImport 已标 Error)
|
|
123
|
+
if (isReservedInternalRel(ref)) {
|
|
124
|
+
return { ok: false, message: ctx.msg('adapter.pluginFileReserved', { path: ref }) };
|
|
125
|
+
}
|
|
126
|
+
const data = ctx.sections.get('pluginFiles') as FilesSection | undefined;
|
|
127
|
+
const file = data?.files.find((f) => f.relativePath === ref);
|
|
128
|
+
if (!file) return { ok: false, message: ctx.msg('adapter.dataMissingFile', { ref }) };
|
|
129
|
+
await ctx.target.fs.writeFile(ref, file.data);
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async validate(data: FilesSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
|
|
134
|
+
const issues: ValidationResult['issues'] = [];
|
|
135
|
+
if (data === null || typeof data !== 'object') {
|
|
136
|
+
return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'pluginFiles' }), severity: 'error' }] };
|
|
137
|
+
}
|
|
138
|
+
if (data.version !== 1) {
|
|
139
|
+
issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(data.files)) {
|
|
142
|
+
issues.push({ path: 'files', message: msg('adapter.validate.array', { subject: 'files' }), severity: 'error' });
|
|
143
|
+
}
|
|
144
|
+
return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
|
|
145
|
+
}
|
|
146
|
+
}
|