@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,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugins 分区 adapter(设计 §3.3/§8):
|
|
3
|
+
* 数据源 = ctx.plugins.listInstalled()(插件清单)+ 用户 patch 层(profile cordis.patch.yml)。
|
|
4
|
+
*
|
|
5
|
+
* 安全不变量:绝不打包插件二进制;导入走 DSH 官方机制(dsh plugin CLI → needsRestart 提示)。
|
|
6
|
+
* patch 行导入(用户自定义行:启用/禁用/插入)写回 cordis.patch.yml,同样 needsRestart。
|
|
7
|
+
*
|
|
8
|
+
* T1(本地源插件迁移):`link:` / `file:` 来源的插件指向本机路径,换机后必然不可达
|
|
9
|
+
* (曾导致插件被静默丢失)。导出时经注入的 `localPack` 执行 `npm pack`,把 tarball 作为
|
|
10
|
+
* 文件条目随分区进入 ZIP(落在 `plugin-files/local-plugins/` 前缀下,复用既有文件类分区通道,
|
|
11
|
+
* **不新增分区 id**);导入时把 spec 重写为 `file:<解包后的绝对路径>` 再交给官方安装通道。
|
|
12
|
+
*/
|
|
13
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
14
|
+
import { sha256Hex } from '../utils/hashing.js';
|
|
15
|
+
import { installSpecFor, resolveProfileNameFromArgv } from '../core/plugin-cli.js';
|
|
16
|
+
import { isLocalPluginSpec, isPackedLocalSpec, LOCAL_PLUGIN_DIR } from '../core/local-plugin-pack.js';
|
|
17
|
+
import { msgOf, zhMsg } from '../core/messages.js';
|
|
18
|
+
import { isPathSafe, normalizePath } from '../utils/paths.js';
|
|
19
|
+
import { PLUGIN_PATCH_REF_PREFIX } from '../core/backup.js';
|
|
20
|
+
import { parsePnpmPatchedDependencies, sanitizePnpmWorkspacePatches } from './pnpm-workspace.js';
|
|
21
|
+
export const USER_PATCH_FILE = 'cordis.patch.yml';
|
|
22
|
+
/** tarball 字节 → base64(零依赖,避免 Buffer 在浏览器侧类型问题) */
|
|
23
|
+
function bytesToBase64(data) {
|
|
24
|
+
if (typeof Buffer !== 'undefined')
|
|
25
|
+
return Buffer.from(data).toString('base64');
|
|
26
|
+
let binary = '';
|
|
27
|
+
for (const b of data)
|
|
28
|
+
binary += String.fromCharCode(b);
|
|
29
|
+
return btoa(binary);
|
|
30
|
+
}
|
|
31
|
+
/** base64 → 字节(导入端解包用) */
|
|
32
|
+
function base64ToBytes(b64) {
|
|
33
|
+
if (typeof Buffer !== 'undefined')
|
|
34
|
+
return new Uint8Array(Buffer.from(b64, 'base64'));
|
|
35
|
+
const binary = atob(b64);
|
|
36
|
+
const out = new Uint8Array(binary.length);
|
|
37
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
38
|
+
out[i] = binary.charCodeAt(i);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** 本地插件 tarball 解包到 homeDir 内的固定缓存目录(相对 $DSH_HOME)。 */
|
|
42
|
+
export const LOCAL_TARBALL_CACHE_REL = 'dsh-config-manager/local-plugins';
|
|
43
|
+
/** 按包名取出备份内的 tarball 条目(不存在返回 undefined)。 */
|
|
44
|
+
function tarballOf(data, pkgName) {
|
|
45
|
+
return data.localTarballs?.find((t) => t.packageName === pkgName);
|
|
46
|
+
}
|
|
47
|
+
/** 归档内相对路径 → 缓存目录下的安全文件名(拒绝任何路径穿越)。 */
|
|
48
|
+
export function localTarballCacheName(relativePath) {
|
|
49
|
+
const base = relativePath.split('/').pop() ?? '';
|
|
50
|
+
if (base === '' || base.includes('\\') || base === '.' || base === '..') {
|
|
51
|
+
throw new Error(`非法 tarball 相对路径: ${relativePath}`);
|
|
52
|
+
}
|
|
53
|
+
return base;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 把备份内的 tarball 写入 `$DSH_HOME/dsh-config-manager/local-plugins/<name>`,
|
|
57
|
+
* 返回**绝对路径**(供 `file:` spec 使用)。
|
|
58
|
+
*
|
|
59
|
+
* 为什么写进 $DSH_HOME 内:`ctx.target.fs` 是「限定在 home 根内」的门面
|
|
60
|
+
* (`DshFileSystemFacade.abs()` 对根外路径抛 fsPathEscape),写 homeDir 外会被拒。
|
|
61
|
+
* 该目录同时落在既有保留区内(`dsh-config-manager/`),随 self 分区语义一致。
|
|
62
|
+
*/
|
|
63
|
+
async function writeLocalTarball(ctx, tarball) {
|
|
64
|
+
const name = localTarballCacheName(tarball.relativePath);
|
|
65
|
+
const rel = `${LOCAL_TARBALL_CACHE_REL}/${name}`;
|
|
66
|
+
await ctx.target.fs.writeFile(rel, base64ToBytes(tarball.base64));
|
|
67
|
+
// `file:` spec 一律用正斜杠(pnpm 跨平台接受正斜杠;反斜杠在部分版本需转义)
|
|
68
|
+
const abs = `${ctx.target.homeDir.replace(/[\\/]+$/, '')}/${rel}`;
|
|
69
|
+
return abs.replace(/\\/g, '/');
|
|
70
|
+
}
|
|
71
|
+
/** profile 目录相对 $DSH_HOME 的路径(patches/ 与 pnpm-workspace.yaml 都挂在这里)。 */
|
|
72
|
+
export const PNPM_PROFILE_DIR = (profile) => `profiles/${profile !== undefined && profile !== '' ? profile : 'web'}`;
|
|
73
|
+
/** pnpm-workspace.yaml 相对 $DSH_HOME 的路径(plugins 分区内按「插件安装配置」管理)。 */
|
|
74
|
+
export const PNPM_WORKSPACE_REL = (profile) => `${PNPM_PROFILE_DIR(profile)}/pnpm-workspace.yaml`;
|
|
75
|
+
/** 单个 patch 文件随备份迁移的体积上限(issue #35;patch 是纯文本,超过说明放错了东西)。 */
|
|
76
|
+
export const MAX_PATCH_FILE_BYTES = 2 * 1024 * 1024;
|
|
77
|
+
/** patch 行 raw 是否由其他 adapter 管理(mcp-client 行 / systemPrompt / planMode 行)。
|
|
78
|
+
* 这些行的导入归 mcp.ts / prompts.ts,plugins 分区只负责普通用户行(启用/禁用/插入插件等)。 */
|
|
79
|
+
function isManagedElsewhere(raw) {
|
|
80
|
+
for (const entry of entriesOfRaw(raw)) {
|
|
81
|
+
const config = entry.config;
|
|
82
|
+
if (config === null || typeof config !== 'object')
|
|
83
|
+
continue;
|
|
84
|
+
const c = config;
|
|
85
|
+
if (typeof c['serverName'] === 'string' && c['serverName'] !== '')
|
|
86
|
+
return true;
|
|
87
|
+
if (c['systemPrompt'] !== undefined)
|
|
88
|
+
return true;
|
|
89
|
+
if (c['planMode'] !== undefined)
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/** patch 行 → entry 列表(兼容单行与 insert 块;与 mcp/prompts 共用形态) */
|
|
95
|
+
function entriesOfRaw(raw) {
|
|
96
|
+
if (raw === null || typeof raw !== 'object')
|
|
97
|
+
return [];
|
|
98
|
+
const obj = raw;
|
|
99
|
+
if (Array.isArray(obj['insert'])) {
|
|
100
|
+
return obj['insert']
|
|
101
|
+
.filter((e) => e !== null && typeof e === 'object')
|
|
102
|
+
.map((e) => e);
|
|
103
|
+
}
|
|
104
|
+
if (obj['id'] !== undefined || obj['name'] !== undefined) {
|
|
105
|
+
return [obj];
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
export class PluginsAdapter {
|
|
110
|
+
id = 'plugins';
|
|
111
|
+
displayName = 'Plugins';
|
|
112
|
+
defaultIncluded = true;
|
|
113
|
+
portability = 'portable';
|
|
114
|
+
/** 插件自身包名:导出 plugins 分区时不列自己(避免备份里出现「当前正在生成备份的插件」的自引用条目) */
|
|
115
|
+
selfName;
|
|
116
|
+
/** T1:本地源(link:/file:)插件打包钩子;未注入 = 不打包(保持改造前行为) */
|
|
117
|
+
localPack;
|
|
118
|
+
constructor(selfName = 'dsh-config-manager', localPack) {
|
|
119
|
+
this.selfName = selfName;
|
|
120
|
+
this.localPack = localPack;
|
|
121
|
+
}
|
|
122
|
+
async export(ctx, _options) {
|
|
123
|
+
const plugins = [];
|
|
124
|
+
const warnings = [];
|
|
125
|
+
try {
|
|
126
|
+
const installed = await ctx.plugins.listInstalled();
|
|
127
|
+
for (const p of installed) {
|
|
128
|
+
// 不导出自身:本插件即正在生成该备份的插件,备份不应包含指向自己的清单条目
|
|
129
|
+
if (this.selfName !== '' && p.name === this.selfName)
|
|
130
|
+
continue;
|
|
131
|
+
plugins.push({
|
|
132
|
+
name: p.name,
|
|
133
|
+
version: p.version,
|
|
134
|
+
// 声明依赖 spec(github:/file:/link: 等非 registry 来源导入时按此重装)
|
|
135
|
+
spec: p.spec,
|
|
136
|
+
isBundle: p.isBundle ?? false,
|
|
137
|
+
inBundles: p.inBundles ?? [],
|
|
138
|
+
enabled: p.enabled,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
warnings.push(msgOf(ctx)('adapter.pluginListReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
|
|
144
|
+
}
|
|
145
|
+
const patch = [];
|
|
146
|
+
try {
|
|
147
|
+
const lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
|
|
148
|
+
for (const l of lines)
|
|
149
|
+
patch.push({ file: USER_PATCH_FILE, lineId: l.lineId, raw: l.raw });
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
warnings.push(msgOf(ctx)('adapter.patchReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
|
|
153
|
+
}
|
|
154
|
+
// pnpm-workspace.yaml(allowBuilds / minimumReleaseAgeExclude 等):随插件分区迁移,
|
|
155
|
+
// 否则目标 profile 的 pnpm 可能因构建白名单/冷静期拒绝安装插件(§34.17 同款语义)。
|
|
156
|
+
let pnpmWorkspace = null;
|
|
157
|
+
try {
|
|
158
|
+
const rel = PNPM_WORKSPACE_REL(ctx.profile);
|
|
159
|
+
if (await ctx.fs.exists(rel)) {
|
|
160
|
+
pnpmWorkspace = new TextDecoder().decode(await ctx.fs.readFile(rel));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
warnings.push(msgOf(ctx)('adapter.pnpmReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
|
|
165
|
+
}
|
|
166
|
+
// issue #35:`patchedDependencies` 引用的 patches/** 必须与声明同进同出。
|
|
167
|
+
// 只搬 pnpm-workspace.yaml 文本会让目标机拿到「声明在、文件不在」的组合,
|
|
168
|
+
// 此后 pnpm 拒绝**一切** add(含插件安装)——实测 13/13 插件安装全灭。
|
|
169
|
+
let patchFiles;
|
|
170
|
+
if (pnpmWorkspace !== null && pnpmWorkspace !== '') {
|
|
171
|
+
const parsed = parsePnpmPatchedDependencies(pnpmWorkspace);
|
|
172
|
+
if (parsed.unsupported !== null) {
|
|
173
|
+
warnings.push(msgOf(ctx)('adapter.pwPatchesUnsupported', { line: parsed.unsupported }));
|
|
174
|
+
}
|
|
175
|
+
const collected = [];
|
|
176
|
+
for (const decl of parsed.declared) {
|
|
177
|
+
const rel = normalizePath(decl.path);
|
|
178
|
+
if (rel === '' || !isPathSafe(rel)) {
|
|
179
|
+
warnings.push(msgOf(ctx)('adapter.patchFileUnsafe', { name: decl.name, path: decl.path }));
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const homeRel = `${PNPM_PROFILE_DIR(ctx.profile)}/${rel}`;
|
|
183
|
+
try {
|
|
184
|
+
if (!(await ctx.fs.exists(homeRel))) {
|
|
185
|
+
warnings.push(msgOf(ctx)('adapter.patchFileMissing', { name: decl.name, path: rel }));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const data = await ctx.fs.readFile(homeRel);
|
|
189
|
+
if (data.byteLength > MAX_PATCH_FILE_BYTES) {
|
|
190
|
+
warnings.push(msgOf(ctx)('adapter.patchFileTooLarge', {
|
|
191
|
+
name: decl.name, path: rel, limit: `${Math.floor(MAX_PATCH_FILE_BYTES / 1024)} KiB`,
|
|
192
|
+
}));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
collected.push({ relativePath: rel, base64: bytesToBase64(data) });
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
warnings.push(msgOf(ctx)('adapter.patchFileReadFailed', {
|
|
199
|
+
name: decl.name, path: rel, reason: err instanceof Error ? err.message : String(err),
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (collected.length > 0)
|
|
204
|
+
patchFiles = collected;
|
|
205
|
+
}
|
|
206
|
+
// T1:本地源(link:/file:)插件打包。这些 spec 指向本机路径,换机后必然不可达
|
|
207
|
+
// (曾导致插件被静默丢失)。钩子由宿主注入;未注入 / 无本地源 / 单个失败一律不中断导出。
|
|
208
|
+
let localTarballs;
|
|
209
|
+
let effectivePlugins = plugins;
|
|
210
|
+
if (this.localPack !== undefined) {
|
|
211
|
+
try {
|
|
212
|
+
const packed = await this.localPack(plugins, ctx);
|
|
213
|
+
warnings.push(...packed.warnings);
|
|
214
|
+
if (packed.packed.length > 0) {
|
|
215
|
+
localTarballs = packed.packed.map((p) => ({
|
|
216
|
+
packageName: p.packageName,
|
|
217
|
+
version: p.version,
|
|
218
|
+
relativePath: p.relativePath,
|
|
219
|
+
base64: bytesToBase64(p.data),
|
|
220
|
+
}));
|
|
221
|
+
// 清单里的 spec 同步改写为可移植形式:即便导入端不拆 tarball,
|
|
222
|
+
// 也不会再把本机绝对路径写进目标 profile package.json。
|
|
223
|
+
const rewritten = packed.rewritten;
|
|
224
|
+
effectivePlugins = plugins.map((p) => rewritten[p.name] !== undefined ? { ...p, spec: rewritten[p.name] } : p);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
warnings.push(msgOf(ctx)('adapter.localPackFailed', { reason: err instanceof Error ? err.message : String(err) }));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
sectionId: 'plugins',
|
|
233
|
+
data: {
|
|
234
|
+
version: 1,
|
|
235
|
+
plugins: effectivePlugins,
|
|
236
|
+
patch,
|
|
237
|
+
pnpmWorkspace,
|
|
238
|
+
...(localTarballs !== undefined ? { localTarballs } : {}),
|
|
239
|
+
...(patchFiles !== undefined ? { patchFiles } : {}),
|
|
240
|
+
},
|
|
241
|
+
counts: {
|
|
242
|
+
plugins: effectivePlugins.length,
|
|
243
|
+
patchLines: patch.length,
|
|
244
|
+
...(localTarballs !== undefined ? { localTarballs: localTarballs.length } : {}),
|
|
245
|
+
...(patchFiles !== undefined ? { patchFiles: patchFiles.length } : {}),
|
|
246
|
+
},
|
|
247
|
+
warnings,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 导入后目标机**可能**存在的 patch 文件集合(相对 profile 目录)——issue #35。
|
|
252
|
+
* = 备份携带的 patchFiles ∪ 目标机本来就有的文件。只用于判定「声明能否被满足」,
|
|
253
|
+
* 不做任何写入(写入在 applyItem 内、且先于 pnpm-workspace.yaml)。
|
|
254
|
+
*/
|
|
255
|
+
async patchAvailability(data, ctx) {
|
|
256
|
+
const set = new Set();
|
|
257
|
+
for (const pf of data.patchFiles ?? []) {
|
|
258
|
+
const rel = normalizePath(pf.relativePath);
|
|
259
|
+
if (rel !== '' && isPathSafe(rel))
|
|
260
|
+
set.add(rel);
|
|
261
|
+
}
|
|
262
|
+
const profileDir = PNPM_PROFILE_DIR(ctx.target.profile);
|
|
263
|
+
for (const decl of parsePnpmPatchedDependencies(data.pnpmWorkspace ?? '').declared) {
|
|
264
|
+
const rel = normalizePath(decl.path);
|
|
265
|
+
if (rel === '' || !isPathSafe(rel) || set.has(rel))
|
|
266
|
+
continue;
|
|
267
|
+
try {
|
|
268
|
+
if (await ctx.target.fs.exists(`${profileDir}/${rel}`))
|
|
269
|
+
set.add(rel);
|
|
270
|
+
}
|
|
271
|
+
catch { /* 读不到 = 视为不存在 */ }
|
|
272
|
+
}
|
|
273
|
+
return set;
|
|
274
|
+
}
|
|
275
|
+
async analyzeImport(data, ctx) {
|
|
276
|
+
const msg = ctx.msg;
|
|
277
|
+
const items = [];
|
|
278
|
+
// pnpm-workspace.yaml:先于插件安装写入(allowBuilds / minimumReleaseAgeExclude 需在
|
|
279
|
+
// pnpm add 时生效)。与目标不同 → Create/Update;无文件/内容一致 → Skip。
|
|
280
|
+
if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && data.pnpmWorkspace !== '') {
|
|
281
|
+
// issue #35:patch 文件项必须**先于** pnpm-workspace.yaml。
|
|
282
|
+
// 原因有二:① 配置里的 patchedDependencies 只有文件已就位才可用,先写配置会留下
|
|
283
|
+
// 「声明在、文件未到」的窗口(中途中断 → 目标机 pnpm 从此拒绝一切 add);
|
|
284
|
+
// ② 它们是独立计划项(可被用户 keepCurrent 否决),配置项的写入不得越权替它们落盘。
|
|
285
|
+
// 作为计划项还有第三个作用:进入导入前快照(回滚可还原被覆盖的原文件)。
|
|
286
|
+
for (const pf of data.patchFiles ?? []) {
|
|
287
|
+
const rel = normalizePath(pf.relativePath);
|
|
288
|
+
if (rel === '' || !isPathSafe(rel))
|
|
289
|
+
continue;
|
|
290
|
+
const id = `plugins:patch:${rel}`;
|
|
291
|
+
const abs = `${PNPM_PROFILE_DIR(ctx.target.profile)}/${rel}`;
|
|
292
|
+
let current = null;
|
|
293
|
+
try {
|
|
294
|
+
current = await ctx.target.fs.exists(abs) ? await ctx.target.fs.readFile(abs) : null;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
current = null;
|
|
298
|
+
}
|
|
299
|
+
const target = { adapter: 'plugins', ref: `${PLUGIN_PATCH_REF_PREFIX}${rel}` };
|
|
300
|
+
if (current === null) {
|
|
301
|
+
items.push({
|
|
302
|
+
id, kind: 'Create', adapter: 'plugins',
|
|
303
|
+
description: msg('adapter.patchFileCreate', { path: rel }), severity: 'info', target,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
else if (sha256Hex(current) === sha256Hex(base64ToBytes(pf.base64))) {
|
|
307
|
+
items.push({ id, kind: 'Skip', adapter: 'plugins', description: msg('adapter.fileSame', { path: rel }), severity: 'info' });
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
items.push({
|
|
311
|
+
id, kind: 'Conflict', adapter: 'plugins',
|
|
312
|
+
description: msg('adapter.fileDiff', { path: rel }), severity: 'warning', target,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// issue #35:目标机满足不了的 patchedDependencies 声明必须剔除——否则写进去之后,
|
|
317
|
+
// 目标机 pnpm 会因为「patch 文件读不到」拒绝**一切** add(含本次要装的插件)。
|
|
318
|
+
const available = await this.patchAvailability(data, ctx);
|
|
319
|
+
const clean = sanitizePnpmWorkspacePatches(data.pnpmWorkspace, (rel) => available.has(rel));
|
|
320
|
+
const droppedNames = clean.dropped.map((d) => d.name).join(', ');
|
|
321
|
+
let current = null;
|
|
322
|
+
try {
|
|
323
|
+
const rel = PNPM_WORKSPACE_REL(ctx.target.profile);
|
|
324
|
+
current = await ctx.target.fs.exists(rel)
|
|
325
|
+
? new TextDecoder().decode(await ctx.target.fs.readFile(rel))
|
|
326
|
+
: null;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
current = null;
|
|
330
|
+
}
|
|
331
|
+
if (current !== clean.text) {
|
|
332
|
+
items.push({
|
|
333
|
+
id: 'plugins:pnpm-workspace',
|
|
334
|
+
kind: current === null ? 'Create' : 'Update',
|
|
335
|
+
adapter: 'plugins',
|
|
336
|
+
description: current === null ? msg('adapter.pwCreate') : msg('adapter.pwUpdate'),
|
|
337
|
+
...(clean.dropped.length > 0
|
|
338
|
+
? { detail: msg('adapter.pwPatchesDroppedDetail', { count: String(clean.dropped.length), names: droppedNames }) }
|
|
339
|
+
: {}),
|
|
340
|
+
severity: 'info',
|
|
341
|
+
target: { adapter: 'plugins', ref: 'pnpm-workspace.yaml' },
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// 剔除是**可见**的信息项(不静默改变用户配置语义)
|
|
345
|
+
if (clean.dropped.length > 0) {
|
|
346
|
+
items.push({
|
|
347
|
+
id: 'plugins:pnpm-workspace-dropped',
|
|
348
|
+
kind: 'Warning',
|
|
349
|
+
adapter: 'plugins',
|
|
350
|
+
description: msg('adapter.pwPatchesDropped', { count: String(clean.dropped.length), names: droppedNames }),
|
|
351
|
+
severity: 'warning',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (clean.unsupported !== null) {
|
|
355
|
+
items.push({
|
|
356
|
+
id: 'plugins:pnpm-workspace-flow',
|
|
357
|
+
kind: 'Warning',
|
|
358
|
+
adapter: 'plugins',
|
|
359
|
+
description: msg('adapter.pwPatchesUnsupported', { line: clean.unsupported }),
|
|
360
|
+
severity: 'warning',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// 插件:包名唯一键;同版本 Skip / 未装 Install / 版本不同 Conflict
|
|
365
|
+
const targetInstalled = await ctx.target.plugins.listInstalled();
|
|
366
|
+
for (const p of data.plugins) {
|
|
367
|
+
const id = `plugin:${p.name}`;
|
|
368
|
+
const tp = targetInstalled.find((t) => t.name === p.name);
|
|
369
|
+
// T1:该插件在备份内自带 tarball(本地源 link:/file:)→ 目标机无需原始路径即可安装。
|
|
370
|
+
const hasTarball = tarballOf(data, p.name) !== undefined;
|
|
371
|
+
const tarballHint = hasTarball ? msg('adapter.pluginLocalTarballHint') : undefined;
|
|
372
|
+
if (!tp) {
|
|
373
|
+
items.push({
|
|
374
|
+
id, kind: 'Install', adapter: 'plugins',
|
|
375
|
+
description: msg('adapter.pluginInstall', { name: p.name, version: p.version }),
|
|
376
|
+
detail: p.isBundle ? msg('adapter.pluginBundleMember') : tarballHint,
|
|
377
|
+
severity: 'info',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
else if (tp.version === p.version) {
|
|
381
|
+
items.push({ id, kind: 'Skip', adapter: 'plugins', description: msg('adapter.pluginSame', { name: p.name }), severity: 'info' });
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
items.push({
|
|
385
|
+
id, kind: 'Conflict', adapter: 'plugins',
|
|
386
|
+
description: msg('adapter.pluginDiff', { name: p.name }),
|
|
387
|
+
detail: msg('adapter.pluginVersionDetail', { current: tp.version, imported: p.version }),
|
|
388
|
+
severity: 'warning',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// 用户 patch 行:lineId 唯一键;存在且同 → Skip;存在不同 → Conflict;不存在 → Create。
|
|
393
|
+
// mcp-client 行与 systemPrompt/planMode 行由 mcp/prompts adapter 管理,此处跳过(避免重复写入覆盖)。
|
|
394
|
+
const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
|
|
395
|
+
for (const pl of data.patch) {
|
|
396
|
+
if (isManagedElsewhere(pl.raw))
|
|
397
|
+
continue;
|
|
398
|
+
const id = `patch:${pl.lineId}`;
|
|
399
|
+
const tl = targetLines.find((l) => l.lineId === pl.lineId);
|
|
400
|
+
if (!tl) {
|
|
401
|
+
items.push({
|
|
402
|
+
id, kind: 'Create', adapter: 'plugins',
|
|
403
|
+
description: msg('adapter.patchLineCreate', { lineId: pl.lineId }), severity: 'info',
|
|
404
|
+
target: { adapter: 'plugins', ref: pl.lineId },
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else if (isDeepStrictEqual(tl.raw, pl.raw)) {
|
|
408
|
+
items.push({ id, kind: 'Skip', adapter: 'plugins', description: msg('adapter.patchLineSame', { lineId: pl.lineId }), severity: 'info' });
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
items.push({
|
|
412
|
+
id, kind: 'Conflict', adapter: 'plugins',
|
|
413
|
+
description: msg('adapter.patchLineDiff', { lineId: pl.lineId }), severity: 'warning',
|
|
414
|
+
target: { adapter: 'plugins', ref: pl.lineId },
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return items;
|
|
419
|
+
}
|
|
420
|
+
async applyItem(item, ctx) {
|
|
421
|
+
const msg = ctx.msg;
|
|
422
|
+
// pnpm-workspace.yaml:插件安装的 pnpm 配置(allowBuilds / minimumReleaseAgeExclude),
|
|
423
|
+
// 必须先于任何插件安装写入,pnpm add 时才能生效。失败为非致命 warning。
|
|
424
|
+
// issue #35:patch 文件项(写入 profile 的 patches/ 目录;同时进入导入前快照,可回滚)
|
|
425
|
+
if (item.id.startsWith('plugins:patch:')) {
|
|
426
|
+
const ref = item.target?.ref;
|
|
427
|
+
if (ref === undefined || !ref.startsWith(PLUGIN_PATCH_REF_PREFIX)) {
|
|
428
|
+
return { ok: false, message: msg('adapter.missingTargetRef') };
|
|
429
|
+
}
|
|
430
|
+
const rel = normalizePath(ref.slice(PLUGIN_PATCH_REF_PREFIX.length));
|
|
431
|
+
if (rel === '' || !isPathSafe(rel)) {
|
|
432
|
+
return { ok: false, message: msg('adapter.patchFileUnsafe', { name: item.id, path: rel }) };
|
|
433
|
+
}
|
|
434
|
+
const data = ctx.sections.get('plugins');
|
|
435
|
+
const pf = data?.patchFiles?.find((p) => normalizePath(p.relativePath) === rel);
|
|
436
|
+
if (pf === undefined)
|
|
437
|
+
return { ok: false, message: msg('adapter.dataMissingFile', { ref: rel }) };
|
|
438
|
+
try {
|
|
439
|
+
await ctx.target.fs.writeFile(`${PNPM_PROFILE_DIR(ctx.target.profile)}/${rel}`, base64ToBytes(pf.base64));
|
|
440
|
+
return { ok: true };
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
warning: true,
|
|
446
|
+
message: msg('adapter.patchFileWriteFailed', { path: rel, reason: err instanceof Error ? err.message : String(err) }),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (item.id === 'plugins:pnpm-workspace') {
|
|
451
|
+
const data = ctx.sections.get('plugins');
|
|
452
|
+
const text = data?.pnpmWorkspace;
|
|
453
|
+
if (text === undefined || text === null || text === '') {
|
|
454
|
+
return { ok: false, message: msg('adapter.pwMissing') };
|
|
455
|
+
}
|
|
456
|
+
// issue #35:判定「声明能否被满足」只看**此刻磁盘上的真实状态**——patch 文件项
|
|
457
|
+
// (`plugins:patch:*`)在计划里排在本项之前,已按用户决策落盘或保留原样。
|
|
458
|
+
// 这里**绝不**替它们写文件:那样会绕过用户在冲突项上的 keepCurrent 选择,
|
|
459
|
+
// 正是 issue #35 要消除的「静默覆盖」。
|
|
460
|
+
const profileDir = PNPM_PROFILE_DIR(ctx.target.profile);
|
|
461
|
+
const available = new Set();
|
|
462
|
+
for (const decl of parsePnpmPatchedDependencies(text).declared) {
|
|
463
|
+
const rel = normalizePath(decl.path);
|
|
464
|
+
if (rel === '' || !isPathSafe(rel) || available.has(rel))
|
|
465
|
+
continue;
|
|
466
|
+
try {
|
|
467
|
+
if (await ctx.target.fs.exists(`${profileDir}/${rel}`))
|
|
468
|
+
available.add(rel);
|
|
469
|
+
}
|
|
470
|
+
catch { /* 读不到 = 视为不存在 */ }
|
|
471
|
+
}
|
|
472
|
+
const clean = sanitizePnpmWorkspacePatches(text, (rel) => available.has(rel));
|
|
473
|
+
try {
|
|
474
|
+
await ctx.target.fs.writeFile(PNPM_WORKSPACE_REL(ctx.target.profile), new TextEncoder().encode(clean.text));
|
|
475
|
+
const notes = [msg('adapter.pwWritten')];
|
|
476
|
+
if (clean.dropped.length > 0) {
|
|
477
|
+
notes.push(msg('adapter.pwWrittenPatchesDropped', {
|
|
478
|
+
count: String(clean.dropped.length),
|
|
479
|
+
names: clean.dropped.map((d) => d.name).join(', '),
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
return { ok: true, needsRestart: true, message: notes.join(';') };
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
warning: true,
|
|
489
|
+
message: msg('adapter.pwWriteFailed', { msg: reason }),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// 插件安装/更新:官方机制 dsh plugin CLI(dsh plugin --profile <p> add <pkg>);
|
|
494
|
+
// needsRestart 提示(设计 §8:不打包二进制)。
|
|
495
|
+
// 版本冲突(useImported)会解析成 Update,同样走这条安装通道(官方机制只能装到 npm
|
|
496
|
+
// 最新版,无法精确锁定备份版本,故如实提示)。
|
|
497
|
+
// 失败为非致命 warning(§34.17):一个装不上的插件(npm 依赖冲突/网络不可达等)不得
|
|
498
|
+
// 拖垮已成功导入的其余配置——与 workspaces 的「目标不可达 → warning」同款语义。
|
|
499
|
+
if (item.id.startsWith('plugin:')) {
|
|
500
|
+
const name = item.id.replace(/^plugin:/, '');
|
|
501
|
+
try {
|
|
502
|
+
// 非 registry 来源(github:/file: 等)按来源 spec 安装,registry 包按裸包名装 npm 最新版
|
|
503
|
+
const data = ctx.sections.get('plugins');
|
|
504
|
+
let spec = data?.plugins.find((p) => p.name === name)?.spec;
|
|
505
|
+
// T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,把 spec 重写为绝对路径。
|
|
506
|
+
// 这是「换机后本地插件不再丢失」的关键:原始绝对路径在目标机不存在,解包出的 tgz 一定存在。
|
|
507
|
+
const tarball = data !== undefined ? tarballOf(data, name) : undefined;
|
|
508
|
+
if (tarball !== undefined) {
|
|
509
|
+
try {
|
|
510
|
+
const abs = await writeLocalTarball(ctx, tarball);
|
|
511
|
+
spec = `file:${abs}`;
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
// 解包失败不阻断安装尝试:退回原 spec(可能失败,但会如实报错而非静默丢失)
|
|
515
|
+
ctx.onLog?.(`本地插件 ${name} 的 tarball 解包失败,回退原始 spec:${err instanceof Error ? err.message : String(err)}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// 执行日志:记录实际将发起的子进程命令行(与宿主 DshPluginsFacade 的
|
|
519
|
+
// dsh plugin --profile <p> add <spec> 一致);仅非敏感文本,渲染前 UI 再 redact 兜底
|
|
520
|
+
ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProfileNameFromArgv()} add ${installSpecFor(name, spec)}`);
|
|
521
|
+
// 透传中止信号:用户「跳过当前插件」→ 宿主 kill 子进程 + 清半装状态 → 抛 ImportUserSkippedError
|
|
522
|
+
const result = await ctx.target.plugins.install(name, spec, ctx.signal);
|
|
523
|
+
const suffix = result.needsRestart ? msg('adapter.pluginRestartSuffix') : '';
|
|
524
|
+
return {
|
|
525
|
+
ok: true,
|
|
526
|
+
needsRestart: true,
|
|
527
|
+
message: item.kind === 'Update'
|
|
528
|
+
? msg('adapter.pluginUpdateTriggered', { name, suffix })
|
|
529
|
+
: msg('adapter.pluginInstallOk', { name, suffix }),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
// 用户跳过:原样上抛(引擎 applyOne 捕获 → skipped + skippedByUser,不触发回滚)
|
|
534
|
+
if (ctx.signal?.aborted)
|
|
535
|
+
throw err;
|
|
536
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
warning: true,
|
|
540
|
+
// 保留 warning(§34.17 非致命):一个装不上的插件不得拖垮已成功导入的其余配置;
|
|
541
|
+
// message 附可复制的手动安装命令(profile 解析与 M1 宿主一致)。
|
|
542
|
+
message: (item.kind === 'Update' ? msg('adapter.pluginUpdateFailed', { name, msg: reason, profile: resolveProfileNameFromArgv() }) : msg('adapter.pluginInstallFailed', { name, msg: reason, profile: resolveProfileNameFromArgv() })),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// patch 行:Create → insert,Update/Conflict(useImported) → update
|
|
547
|
+
const ref = item.target?.ref;
|
|
548
|
+
if (!ref)
|
|
549
|
+
return { ok: false, message: msg('adapter.missingTargetRef') };
|
|
550
|
+
const data = ctx.sections.get('plugins');
|
|
551
|
+
const pl = data?.patch.find((p) => p.lineId === ref);
|
|
552
|
+
if (!pl)
|
|
553
|
+
return { ok: false, message: msg('adapter.patchMissing', { ref }) };
|
|
554
|
+
await ctx.target.patchFile.applyPatchChanges(pl.file, [
|
|
555
|
+
{ lineId: ref, raw: pl.raw, action: item.kind === 'Create' ? 'insert' : 'update' },
|
|
556
|
+
]);
|
|
557
|
+
return { ok: true, needsRestart: true, message: msg('adapter.patchWritten', { ref }) };
|
|
558
|
+
}
|
|
559
|
+
async validate(data, msg = zhMsg) {
|
|
560
|
+
const issues = [];
|
|
561
|
+
if (data === null || typeof data !== 'object') {
|
|
562
|
+
return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'plugins' }), severity: 'error' }] };
|
|
563
|
+
}
|
|
564
|
+
if (data.version !== 1) {
|
|
565
|
+
issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
|
|
566
|
+
}
|
|
567
|
+
if (!Array.isArray(data.plugins)) {
|
|
568
|
+
issues.push({ path: 'plugins', message: msg('adapter.validate.array', { subject: 'plugins' }), severity: 'error' });
|
|
569
|
+
}
|
|
570
|
+
if (data.patch !== undefined && !Array.isArray(data.patch)) {
|
|
571
|
+
issues.push({ path: 'patch', message: msg('adapter.validate.array', { subject: 'patch' }), severity: 'error' });
|
|
572
|
+
}
|
|
573
|
+
if (data.pnpmWorkspace !== undefined && data.pnpmWorkspace !== null && typeof data.pnpmWorkspace !== 'string') {
|
|
574
|
+
issues.push({ path: 'pnpmWorkspace', message: msg('adapter.validate.string', { subject: 'pnpmWorkspace' }), severity: 'error' });
|
|
575
|
+
}
|
|
576
|
+
// T1:本地插件 tarball 载荷(缺省合法;存在时逐条校验形状与路径安全)
|
|
577
|
+
if (data.localTarballs !== undefined) {
|
|
578
|
+
if (!Array.isArray(data.localTarballs)) {
|
|
579
|
+
issues.push({ path: 'localTarballs', message: msg('adapter.validate.array', { subject: 'localTarballs' }), severity: 'error' });
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
data.localTarballs.forEach((t, i) => {
|
|
583
|
+
if (t === null || typeof t !== 'object') {
|
|
584
|
+
issues.push({ path: `localTarballs[${i}]`, message: msg('adapter.validate.object', { subject: 'localTarballs[]' }), severity: 'error' });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
for (const field of ['packageName', 'version', 'relativePath', 'base64']) {
|
|
588
|
+
if (typeof t[field] !== 'string' || t[field] === '') {
|
|
589
|
+
issues.push({ path: `localTarballs[${i}].${field}`, message: msg('adapter.validate.string', { subject: field }), severity: 'error' });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// 相对路径必须落在 LOCAL_PLUGIN_DIR 之下且不含穿越段(防写 homeDir 外)
|
|
593
|
+
const rel = typeof t.relativePath === 'string' ? t.relativePath.replace(/\\/g, '/') : '';
|
|
594
|
+
if (rel !== '' && (!rel.startsWith(`${LOCAL_PLUGIN_DIR}/`) || rel.includes('..'))) {
|
|
595
|
+
issues.push({ path: `localTarballs[${i}].relativePath`, message: msg('adapter.validate.localTarballPath', { path: rel }), severity: 'error' });
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
//# sourceMappingURL=plugins.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface PatchedDependency {
|
|
2
|
+
/** 依赖名(patchedDependencies 的键) */
|
|
3
|
+
name: string;
|
|
4
|
+
/** 声明的 patch 路径(相对 profile 目录,已去引号;原始写法保留) */
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsePatchedResult {
|
|
8
|
+
declared: PatchedDependency[];
|
|
9
|
+
/** 无法安全改写的形态(如单行 flow 映射)→ 调用方据此告警,绝不猜着改 */
|
|
10
|
+
unsupported: string | null;
|
|
11
|
+
}
|
|
12
|
+
/** 解析 patchedDependencies 的声明(纯函数;不校验文件是否存在)。 */
|
|
13
|
+
export declare function parsePnpmPatchedDependencies(text: string): ParsePatchedResult;
|
|
14
|
+
export interface SanitizeResult {
|
|
15
|
+
/** 清理后的文本(无需修改时与入参逐字节相同) */
|
|
16
|
+
text: string;
|
|
17
|
+
declared: PatchedDependency[];
|
|
18
|
+
/** 被移除的条目(目标机没有对应 patch 文件) */
|
|
19
|
+
dropped: PatchedDependency[];
|
|
20
|
+
unsupported: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 删除 patchedDependencies 中「目标机不存在对应 patch 文件」的条目。
|
|
24
|
+
* @param available 判断某相对路径(相对 profile 目录)在**导入后**是否存在
|
|
25
|
+
*/
|
|
26
|
+
export declare function sanitizePnpmWorkspacePatches(text: string, available: (relPath: string) => boolean): SanitizeResult;
|