@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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue #35 回归:patchedDependencies 的解析与「目标机没有 patch 文件」条目的剔除。
|
|
3
|
+
* 目标:绝不把「声明存在、文件不存在」的组合写到目标机(否则 pnpm 拒绝一切 add)。
|
|
4
|
+
*/
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { parsePnpmPatchedDependencies, sanitizePnpmWorkspacePatches } from './pnpm-workspace.ts';
|
|
8
|
+
|
|
9
|
+
const SOURCE = [
|
|
10
|
+
'packages:',
|
|
11
|
+
" - 'plugins/*'",
|
|
12
|
+
'allowBuilds:',
|
|
13
|
+
' ssh2: true',
|
|
14
|
+
'patchedDependencies:',
|
|
15
|
+
' dsh-approval-gate: patches/dsh-approval-gate.patch',
|
|
16
|
+
' dsh-whale-galgame: patches/dsh-whale-galgame.patch',
|
|
17
|
+
'minimumReleaseAgeExclude:',
|
|
18
|
+
' - dsh-config-manager',
|
|
19
|
+
'',
|
|
20
|
+
].join('\n');
|
|
21
|
+
|
|
22
|
+
test('parse:读出全部声明(含引号/行尾注释)', () => {
|
|
23
|
+
const r = parsePnpmPatchedDependencies(SOURCE);
|
|
24
|
+
assert.equal(r.unsupported, null);
|
|
25
|
+
assert.deepEqual(r.declared, [
|
|
26
|
+
{ name: 'dsh-approval-gate', path: 'patches/dsh-approval-gate.patch' },
|
|
27
|
+
{ name: 'dsh-whale-galgame', path: 'patches/dsh-whale-galgame.patch' },
|
|
28
|
+
]);
|
|
29
|
+
const quoted = parsePnpmPatchedDependencies("patchedDependencies:\n a: 'p/a.patch' # 说明\n");
|
|
30
|
+
assert.deepEqual(quoted.declared, [{ name: 'a', path: 'p/a.patch' }]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('sanitize:patch 文件都在 → 逐字节不变(保留注释与格式)', () => {
|
|
34
|
+
const r = sanitizePnpmWorkspacePatches(SOURCE, () => true);
|
|
35
|
+
assert.equal(r.text, SOURCE, '无需修改时必须原样返回');
|
|
36
|
+
assert.deepEqual(r.dropped, []);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('sanitize:目标机缺文件 → 只删那一条(issue #35 的复现路径)', () => {
|
|
40
|
+
const r = sanitizePnpmWorkspacePatches(SOURCE, (rel) => rel === 'patches/dsh-approval-gate.patch');
|
|
41
|
+
assert.deepEqual(r.dropped.map((d) => d.name), ['dsh-whale-galgame']);
|
|
42
|
+
assert.ok(r.text.includes('dsh-approval-gate: patches/dsh-approval-gate.patch'), '可用的条目必须保留');
|
|
43
|
+
assert.ok(!r.text.includes('dsh-whale-galgame'), '缺文件的条目必须整行消失');
|
|
44
|
+
// 其余配置一字不动
|
|
45
|
+
assert.ok(r.text.includes('allowBuilds:\n ssh2: true'));
|
|
46
|
+
assert.ok(r.text.includes('minimumReleaseAgeExclude:\n - dsh-config-manager'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('sanitize:全部条目都缺 → 连 patchedDependencies 键一起删(不留空键)', () => {
|
|
50
|
+
const r = sanitizePnpmWorkspacePatches(SOURCE, () => false);
|
|
51
|
+
assert.equal(r.dropped.length, 2);
|
|
52
|
+
assert.ok(!r.text.includes('patchedDependencies'), '空键也必须删掉');
|
|
53
|
+
assert.ok(r.text.includes('packages:'), '其它键不受影响');
|
|
54
|
+
assert.ok(r.text.includes('minimumReleaseAgeExclude:'), '键之后的块不得被误删');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('sanitize:越界/绝对路径的声明同样移除(目标机不可读,pnpm 亦拒绝)', () => {
|
|
58
|
+
const text = "patchedDependencies:\n a: ../outside/a.patch\n b: C:/abs/b.patch\n c: p/c.patch\n";
|
|
59
|
+
const r = sanitizePnpmWorkspacePatches(text, () => true);
|
|
60
|
+
assert.deepEqual(r.dropped.map((d) => d.name).sort(), ['a', 'b']);
|
|
61
|
+
assert.ok(r.text.includes('c: p/c.patch'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('sanitize:单行 flow 形态不猜着改,返回 unsupported 供上层告警', () => {
|
|
65
|
+
const text = 'patchedDependencies: {a: p/a.patch}\nallowBuilds:\n ssh2: true\n';
|
|
66
|
+
const r = sanitizePnpmWorkspacePatches(text, () => false);
|
|
67
|
+
assert.equal(r.text, text, '不支持的形态必须原样返回,绝不半改');
|
|
68
|
+
assert.ok(r.unsupported !== null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('sanitize:CRLF 文本保留 CRLF;块内注释随被删条目一起消失', () => {
|
|
72
|
+
const text = 'patchedDependencies:\r\n # 旧补丁\r\n a: p/a.patch\r\n b: p/b.patch\r\nother: 1\r\n';
|
|
73
|
+
const r = sanitizePnpmWorkspacePatches(text, (rel) => rel === 'p/b.patch');
|
|
74
|
+
assert.equal(r.text.includes('\r\n'), true, 'CRLF 不得被改成 LF');
|
|
75
|
+
assert.ok(!r.text.includes('a: p/a.patch'));
|
|
76
|
+
assert.ok(!r.text.includes('旧补丁'), '被删条目的注释一并移除');
|
|
77
|
+
assert.ok(r.text.includes('b: p/b.patch'));
|
|
78
|
+
assert.ok(r.text.includes('other: 1'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('sanitize:没有 patchedDependencies 时不产生任何改动', () => {
|
|
82
|
+
const text = 'allowBuilds:\n ssh2: true\n';
|
|
83
|
+
const r = sanitizePnpmWorkspacePatches(text, () => false);
|
|
84
|
+
assert.equal(r.text, text);
|
|
85
|
+
assert.deepEqual(r.declared, []);
|
|
86
|
+
assert.equal(r.unsupported, null);
|
|
87
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pnpm-workspace.yaml 的 `patchedDependencies` 处理(issue #35)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:plugins 分区会**整段搬运** pnpm-workspace.yaml,但 `patchedDependencies` 引用的
|
|
5
|
+
* `patches/*.patch` 文件不在任何被同步的分区里。目标机因此拿到「声明存在、文件不存在」的
|
|
6
|
+
* 组合,此后**任何** `pnpm add`(含 dsh plugin add)都失败于
|
|
7
|
+
* `Failed to read patch file ... (os error 2)` —— 用户实测 13/13 插件安装全灭。
|
|
8
|
+
*
|
|
9
|
+
* 本模块做两件事,且**不做整文件重写**(保留用户注释与格式):
|
|
10
|
+
* 1. `parsePnpmPatchedDependencies`:按行解析声明(供导出侧收集 patch 文件);
|
|
11
|
+
* 2. `sanitizePnpmWorkspacePatches`:把「目标机没有对应文件」的条目整行删掉
|
|
12
|
+
* (含其上方注释),全部被删时连键一起删 —— 宁可少迁移一条 patch 声明,
|
|
13
|
+
* 也不让目标机的 pnpm 从此拒绝一切安装。
|
|
14
|
+
*/
|
|
15
|
+
import { isPathSafe, normalizePath } from '../utils/paths.ts';
|
|
16
|
+
|
|
17
|
+
export interface PatchedDependency {
|
|
18
|
+
/** 依赖名(patchedDependencies 的键) */
|
|
19
|
+
name: string;
|
|
20
|
+
/** 声明的 patch 路径(相对 profile 目录,已去引号;原始写法保留) */
|
|
21
|
+
path: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ParsePatchedResult {
|
|
25
|
+
declared: PatchedDependency[];
|
|
26
|
+
/** 无法安全改写的形态(如单行 flow 映射)→ 调用方据此告警,绝不猜着改 */
|
|
27
|
+
unsupported: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const KEY_RE = /^patchedDependencies\s*:(.*)$/;
|
|
31
|
+
const ENTRY_RE = /^([ \t]+)([^\s#][^:]*?)\s*:\s*(.*)$/;
|
|
32
|
+
|
|
33
|
+
/** 去引号 + 去行尾注释(保守:只在引号外找 ` #`) */
|
|
34
|
+
function cleanScalar(raw: string): string {
|
|
35
|
+
let v = raw.trim();
|
|
36
|
+
if (v.startsWith("'") || v.startsWith('"')) {
|
|
37
|
+
const quote = v[0] as string;
|
|
38
|
+
const end = v.indexOf(quote, 1);
|
|
39
|
+
if (end > 0) return v.slice(1, end);
|
|
40
|
+
}
|
|
41
|
+
const hash = v.indexOf(' #');
|
|
42
|
+
if (hash >= 0) v = v.slice(0, hash);
|
|
43
|
+
return v.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 解析 patchedDependencies 的声明(纯函数;不校验文件是否存在)。 */
|
|
47
|
+
export function parsePnpmPatchedDependencies(text: string): ParsePatchedResult {
|
|
48
|
+
const declared: PatchedDependency[] = [];
|
|
49
|
+
if (text === '') return { declared, unsupported: null };
|
|
50
|
+
const lines = text.split(/\r?\n/);
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
const m = KEY_RE.exec(lines[i] ?? '');
|
|
53
|
+
if (m === null) continue;
|
|
54
|
+
const inline = (m[1] ?? '').trim();
|
|
55
|
+
// 单行 flow 形态(patchedDependencies: {a: b})无法按行安全删条目 → 不改写,交给调用方告警
|
|
56
|
+
if (inline !== '' && !inline.startsWith('#')) {
|
|
57
|
+
return { declared, unsupported: lines[i] ?? '' };
|
|
58
|
+
}
|
|
59
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
60
|
+
const line = lines[j] ?? '';
|
|
61
|
+
if (line.trim() === '' || /^[ \t]*#/.test(line)) continue;
|
|
62
|
+
const e = ENTRY_RE.exec(line);
|
|
63
|
+
if (e === null) break; // 缩进块结束(回到顶层键)
|
|
64
|
+
declared.push({ name: (e[2] ?? '').trim(), path: cleanScalar(e[3] ?? '') });
|
|
65
|
+
}
|
|
66
|
+
break; // patchedDependencies 只处理第一处(YAML 重复键本就非法)
|
|
67
|
+
}
|
|
68
|
+
return { declared, unsupported: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SanitizeResult {
|
|
72
|
+
/** 清理后的文本(无需修改时与入参逐字节相同) */
|
|
73
|
+
text: string;
|
|
74
|
+
declared: PatchedDependency[];
|
|
75
|
+
/** 被移除的条目(目标机没有对应 patch 文件) */
|
|
76
|
+
dropped: PatchedDependency[];
|
|
77
|
+
unsupported: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 删除 patchedDependencies 中「目标机不存在对应 patch 文件」的条目。
|
|
82
|
+
* @param available 判断某相对路径(相对 profile 目录)在**导入后**是否存在
|
|
83
|
+
*/
|
|
84
|
+
export function sanitizePnpmWorkspacePatches(
|
|
85
|
+
text: string,
|
|
86
|
+
available: (relPath: string) => boolean,
|
|
87
|
+
): SanitizeResult {
|
|
88
|
+
const parsed = parsePnpmPatchedDependencies(text);
|
|
89
|
+
if (parsed.unsupported !== null) {
|
|
90
|
+
return { text, declared: parsed.declared, dropped: [], unsupported: parsed.unsupported };
|
|
91
|
+
}
|
|
92
|
+
if (parsed.declared.length === 0) return { text, declared: [], dropped: [], unsupported: null };
|
|
93
|
+
|
|
94
|
+
const droppedNames = new Set<string>();
|
|
95
|
+
for (const d of parsed.declared) {
|
|
96
|
+
const rel = normalizePath(d.path);
|
|
97
|
+
// 越界/绝对路径的声明在目标机同样不可读(pnpm 也会拒绝)→ 一并移除
|
|
98
|
+
if (rel === '' || !isPathSafe(rel) || !available(rel)) droppedNames.add(d.name);
|
|
99
|
+
}
|
|
100
|
+
if (droppedNames.size === 0) return { text, declared: parsed.declared, dropped: [], unsupported: null };
|
|
101
|
+
|
|
102
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
103
|
+
const lines = text.split(/\r?\n/);
|
|
104
|
+
const out: string[] = [];
|
|
105
|
+
let removed = 0;
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
const line = lines[i] ?? '';
|
|
108
|
+
const m = KEY_RE.exec(line);
|
|
109
|
+
if (m === null) { out.push(line); continue; }
|
|
110
|
+
const inline = (m[1] ?? '').trim();
|
|
111
|
+
if (inline !== '' && !inline.startsWith('#')) { out.push(line); continue; }
|
|
112
|
+
// 收集整块(键 + 缩进条目 + 其上的注释)
|
|
113
|
+
const block: string[] = [];
|
|
114
|
+
let j = i + 1;
|
|
115
|
+
for (; j < lines.length; j++) {
|
|
116
|
+
const l = lines[j] ?? '';
|
|
117
|
+
if (l.trim() === '' || /^[ \t]*#/.test(l)) { block.push(l); continue; }
|
|
118
|
+
if (ENTRY_RE.exec(l) === null) break;
|
|
119
|
+
block.push(l);
|
|
120
|
+
}
|
|
121
|
+
// 逐条判定:保留/删除(注释与空行随其**下一条**条目一起处置;尾部孤立注释一律保留)
|
|
122
|
+
const kept: string[] = [];
|
|
123
|
+
let pending: string[] = [];
|
|
124
|
+
let keptEntries = 0;
|
|
125
|
+
for (const bl of block) {
|
|
126
|
+
const e = ENTRY_RE.exec(bl);
|
|
127
|
+
if (e === null) { pending.push(bl); continue; }
|
|
128
|
+
const name = (e[2] ?? '').trim();
|
|
129
|
+
if (droppedNames.has(name)) { removed += 1; pending = []; continue; }
|
|
130
|
+
kept.push(...pending, bl);
|
|
131
|
+
pending = [];
|
|
132
|
+
keptEntries += 1;
|
|
133
|
+
}
|
|
134
|
+
kept.push(...pending);
|
|
135
|
+
if (keptEntries > 0) {
|
|
136
|
+
out.push(line); // 键保留
|
|
137
|
+
out.push(...kept);
|
|
138
|
+
} else {
|
|
139
|
+
removed += 1; // 键本身也删(没有任何条目留下)
|
|
140
|
+
}
|
|
141
|
+
i = j - 1;
|
|
142
|
+
}
|
|
143
|
+
// 全部条目都没了 → 键与块一起消失;否则写回(保留原 EOL)
|
|
144
|
+
return {
|
|
145
|
+
text: out.join(eol),
|
|
146
|
+
declared: parsed.declared,
|
|
147
|
+
dropped: parsed.declared.filter((d) => droppedNames.has(d.name)),
|
|
148
|
+
unsupported: null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompts adapter 测试:patch config 提取(systemPrompt persona / planMode sections)、
|
|
3
|
+
* 同行 Skip/Conflict、目标无来源行 → Warning(诚实降级)、applyItem 合并写回。
|
|
4
|
+
*/
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { PromptsAdapter, extractPrompts, mergePromptIntoLine } from './prompts.ts';
|
|
8
|
+
import { makeContext, makeImportContext } from './test-helpers.ts';
|
|
9
|
+
import type { PromptsExportSection } from './prompts.ts';
|
|
10
|
+
import type { PlanItem } from '../core/types.ts';
|
|
11
|
+
|
|
12
|
+
const LINES = [
|
|
13
|
+
{
|
|
14
|
+
lineId: 'persona-line',
|
|
15
|
+
raw: { id: 'persona-line', name: '@deepseek-ai/dsh-web', config: { systemPrompt: { persona: 'You are a helpful coding assistant.' } } },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
lineId: 'plan-line',
|
|
19
|
+
raw: { id: 'plan-line', name: 'plan-mode', config: { planMode: { sections: [{ name: 'plan', order: 1, text: 'Plan first, then code.' }] } } },
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
test('prompts: extractPrompts 提取 persona 与 planMode sections', () => {
|
|
24
|
+
const prompts = extractPrompts(LINES);
|
|
25
|
+
assert.equal(prompts.length, 2);
|
|
26
|
+
const persona = prompts.find((p) => p.kind === 'systemPrompt');
|
|
27
|
+
assert.equal(persona?.name, 'persona-line:persona');
|
|
28
|
+
assert.equal(persona?.text, 'You are a helpful coding assistant.');
|
|
29
|
+
assert.equal(persona?.sourceLineId, 'persona-line');
|
|
30
|
+
const plan = prompts.find((p) => p.kind === 'planMode');
|
|
31
|
+
assert.equal(plan?.name, 'plan');
|
|
32
|
+
assert.equal(plan?.text, 'Plan first, then code.');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('prompts: mergePromptIntoLine 合并 persona / planMode 文本', () => {
|
|
36
|
+
// persona 更新(原为对象形态)
|
|
37
|
+
const merged = mergePromptIntoLine(LINES[0]!.raw, {
|
|
38
|
+
id: 'prompt:persona-line:persona', name: 'persona-line:persona', kind: 'systemPrompt',
|
|
39
|
+
text: 'New persona.', sourceLineId: 'persona-line',
|
|
40
|
+
});
|
|
41
|
+
const config = (merged as Record<string, unknown>)['config'] as Record<string, unknown>;
|
|
42
|
+
assert.equal((config['systemPrompt'] as Record<string, unknown>)['persona'], 'New persona.');
|
|
43
|
+
|
|
44
|
+
// planMode 新增 section
|
|
45
|
+
const planMerged = mergePromptIntoLine(LINES[1]!.raw, {
|
|
46
|
+
id: 'prompt:plan-line:step2', name: 'step2', kind: 'planMode',
|
|
47
|
+
text: 'Step two text.', sourceLineId: 'plan-line',
|
|
48
|
+
});
|
|
49
|
+
const planConfig = (planMerged as Record<string, unknown>)['config'] as Record<string, unknown>;
|
|
50
|
+
const sections = (planConfig['planMode'] as Record<string, unknown>)['sections'] as Record<string, unknown>[];
|
|
51
|
+
assert.equal(sections.length, 2);
|
|
52
|
+
assert.equal(sections[1]?.['text'], 'Step two text.');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('prompts: 同行同内容 Skip / 不同 Conflict(update) / 目标无 → Warning', async () => {
|
|
56
|
+
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
57
|
+
for (const l of LINES) src.patchFile.lines.set(l.lineId, { lineId: l.lineId, raw: l.raw });
|
|
58
|
+
const adapter = new PromptsAdapter();
|
|
59
|
+
const exported = await adapter.export(src, { includeSecrets: false });
|
|
60
|
+
assert.equal(exported.data.prompts.length, 2);
|
|
61
|
+
const sections = new Map([['prompts', exported.data]]);
|
|
62
|
+
|
|
63
|
+
const dst = makeContext('linux', '/home/bob');
|
|
64
|
+
for (const l of LINES) dst.patchFile.lines.set(l.lineId, { lineId: l.lineId, raw: l.raw });
|
|
65
|
+
let items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
66
|
+
assert.ok(items.every((i) => i.kind === 'Skip'), '目标一致 → 全部 Skip');
|
|
67
|
+
|
|
68
|
+
// 目标 persona 不同 → Conflict(target.ref = 来源行)
|
|
69
|
+
dst.patchFile.lines.set('persona-line', {
|
|
70
|
+
lineId: 'persona-line',
|
|
71
|
+
raw: { id: 'persona-line', name: '@deepseek-ai/dsh-web', config: { systemPrompt: { persona: 'Old persona.' } } },
|
|
72
|
+
});
|
|
73
|
+
items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
74
|
+
const conflict = items.find((i) => i.id === 'prompt:persona-line:persona');
|
|
75
|
+
assert.equal(conflict?.kind, 'Conflict');
|
|
76
|
+
assert.equal(conflict?.target?.ref, 'persona-line');
|
|
77
|
+
const useItem: PlanItem = { ...conflict!, kind: 'Update' };
|
|
78
|
+
const r = await adapter.applyItem(useItem, makeImportContext(dst, sections));
|
|
79
|
+
assert.equal(r.ok, true);
|
|
80
|
+
assert.equal(r.needsRestart, true);
|
|
81
|
+
const raw = dst.patchFile.lines.get('persona-line')?.raw as Record<string, unknown>;
|
|
82
|
+
const sp = (raw['config'] as Record<string, unknown>)['systemPrompt'] as Record<string, unknown>;
|
|
83
|
+
assert.equal(sp['persona'], 'You are a helpful coding assistant.', 'Conflict useImported 应合并更新 persona');
|
|
84
|
+
|
|
85
|
+
// 目标无来源行但有行名 → Create(重建行)
|
|
86
|
+
const dst2 = makeContext('darwin', '/Users/bob');
|
|
87
|
+
items = await adapter.analyzeImport(exported.data, makeImportContext(dst2, sections));
|
|
88
|
+
assert.ok(items.every((i) => i.kind === 'Create'), '目标无来源行但有行名 → Create 重建');
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
const rc = await adapter.applyItem(item, makeImportContext(dst2, sections));
|
|
91
|
+
assert.equal(rc.ok, true);
|
|
92
|
+
}
|
|
93
|
+
const created = dst2.patchFile.lines.get('persona-line')?.raw as Record<string, unknown>;
|
|
94
|
+
assert.equal(((created['config'] as Record<string, unknown>)['systemPrompt'] as Record<string, unknown>)['persona'], 'You are a helpful coding assistant.');
|
|
95
|
+
|
|
96
|
+
// 无行名的 prompt(来源信息缺失)→ Warning,不编造自动创建
|
|
97
|
+
const orphan: PromptsExportSection = { version: 1, prompts: [{ id: 'p1', name: 'orphan', kind: 'systemPrompt', text: 'x' }] };
|
|
98
|
+
const itemsNoName = await adapter.analyzeImport(orphan, makeImportContext(dst2, new Map([['prompts', orphan]])));
|
|
99
|
+
assert.ok(itemsNoName.every((i) => i.kind === 'Warning'), '来源行名缺失 → Warning 提示手动配置');
|
|
100
|
+
const w = await adapter.applyItem(itemsNoName[0]!, makeImportContext(dst2, sections));
|
|
101
|
+
assert.equal(w.ok, true, 'Warning 项 applyItem 无副作用');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('prompts: validate', async () => {
|
|
105
|
+
const adapter = new PromptsAdapter();
|
|
106
|
+
const ok = await adapter.validate({ version: 1, prompts: [{ id: 'a', name: 'n', kind: 'systemPrompt', text: 't' }] });
|
|
107
|
+
assert.equal(ok.valid, true);
|
|
108
|
+
const bad = await adapter.validate({ version: 1, prompts: [{ id: 'a', name: 1 as never, kind: 'systemPrompt', text: 't' }] });
|
|
109
|
+
assert.equal(bad.valid, false);
|
|
110
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompts 分区 adapter(设计 §3.3/§1.2):
|
|
3
|
+
* 数据源 = 组合 patch config 中的 systemPrompt persona / plan-mode section(研究报告 §2.2:
|
|
4
|
+
* DSH 无独立 prompt/rules 文件,persona 与 plan-mode 均经 patch config 注入)。
|
|
5
|
+
*
|
|
6
|
+
* v1 策略(诚实降级):
|
|
7
|
+
* - 目标存在同 lineId 行 → 同内容 Skip / 不同 Conflict(更新该行 config);
|
|
8
|
+
* - 目标无该行但存在同名 prompt → Conflict(更新其所在行);
|
|
9
|
+
* - 均不存在 → Warning(不编造行名自动创建,提示手动配置)。
|
|
10
|
+
* rules/commands 无独立存储(研究报告 §2.2),不实现。
|
|
11
|
+
*/
|
|
12
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
13
|
+
import { msgOf, zhMsg } from '../core/messages.ts';
|
|
14
|
+
import type { MsgFunc } from '../core/messages.ts';
|
|
15
|
+
import type { PromptEntry, PromptsSection } from '../schema/types.ts';
|
|
16
|
+
import type {
|
|
17
|
+
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
18
|
+
ImportContext, PlanItem, ValidationResult,
|
|
19
|
+
} from '../core/types.ts';
|
|
20
|
+
import { USER_PATCH_FILE } from './plugins.ts';
|
|
21
|
+
|
|
22
|
+
/** 导出记录:PromptEntry 之外记录来源行名(导入需要重建行时使用) */
|
|
23
|
+
export interface PromptExportEntry extends PromptEntry {
|
|
24
|
+
sourceLineName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PromptsExportSection { version: 1; prompts: PromptExportEntry[]; }
|
|
28
|
+
|
|
29
|
+
/** 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text) */
|
|
30
|
+
export function extractPrompts(lines: { lineId: string; raw: unknown }[]): PromptExportEntry[] {
|
|
31
|
+
const prompts: PromptExportEntry[] = [];
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
for (const entry of entriesOf(line.raw)) {
|
|
34
|
+
const config = entry.config;
|
|
35
|
+
if (config === null || typeof config !== 'object') continue;
|
|
36
|
+
const c = config as Record<string, unknown>;
|
|
37
|
+
const sourceLineName = typeof entry.name === 'string' ? entry.name : undefined;
|
|
38
|
+
|
|
39
|
+
const sp = c['systemPrompt'];
|
|
40
|
+
if (typeof sp === 'string' && sp.trim() !== '') {
|
|
41
|
+
prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: sp, sourceLineId: line.lineId, sourceLineName });
|
|
42
|
+
} else if (sp !== null && typeof sp === 'object' && typeof (sp as Record<string, unknown>)['persona'] === 'string') {
|
|
43
|
+
const persona = (sp as Record<string, unknown>)['persona'] as string;
|
|
44
|
+
if (persona.trim() !== '') {
|
|
45
|
+
prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: persona, sourceLineId: line.lineId, sourceLineName });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const pm = c['planMode'];
|
|
50
|
+
if (pm !== null && typeof pm === 'object' && Array.isArray((pm as Record<string, unknown>)['sections'])) {
|
|
51
|
+
const sections = (pm as Record<string, unknown>)['sections'] as Record<string, unknown>[];
|
|
52
|
+
for (const [i, s] of sections.entries()) {
|
|
53
|
+
if (s === null || typeof s !== 'object' || typeof s['text'] !== 'string' || (s['text'] as string).trim() === '') continue;
|
|
54
|
+
const name = typeof s['name'] === 'string' && s['name'] !== '' ? s['name'] : `${line.lineId}:section${i}`;
|
|
55
|
+
prompts.push({ id: `prompt:${line.lineId}:${name}`, name, kind: 'planMode', text: s['text'] as string, sourceLineId: line.lineId, sourceLineName });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return prompts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** patch 行 → 可枚举 entry(兼容单行与 insert 块;与 mcp.ts 共用形态) */
|
|
64
|
+
function entriesOf(raw: unknown): { id?: unknown; name?: unknown; config?: unknown }[] {
|
|
65
|
+
if (raw === null || typeof raw !== 'object') return [];
|
|
66
|
+
const obj = raw as Record<string, unknown>;
|
|
67
|
+
if (Array.isArray(obj['insert'])) {
|
|
68
|
+
return obj['insert']
|
|
69
|
+
.filter((e): e is Record<string, unknown> => e !== null && typeof e === 'object')
|
|
70
|
+
.map((e) => e as { id?: unknown; name?: unknown; config?: unknown });
|
|
71
|
+
}
|
|
72
|
+
if (obj['id'] !== undefined || obj['name'] !== undefined) {
|
|
73
|
+
return [obj as { id?: unknown; name?: unknown; config?: unknown }];
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 把 prompt 文本合并进目标行 config(systemPrompt.persona 或 planMode.sections) */
|
|
79
|
+
export function mergePromptIntoLine(raw: unknown, prompt: PromptExportEntry): Record<string, unknown> {
|
|
80
|
+
const cloned = structuredClone(raw) as Record<string, unknown>;
|
|
81
|
+
if (cloned['config'] === null || typeof cloned['config'] !== 'object') cloned['config'] = {};
|
|
82
|
+
const config = cloned['config'] as Record<string, unknown>;
|
|
83
|
+
if (prompt.kind === 'systemPrompt') {
|
|
84
|
+
const sp = config['systemPrompt'];
|
|
85
|
+
if (typeof sp === 'string') config['systemPrompt'] = { persona: sp };
|
|
86
|
+
else if (sp === null || typeof sp !== 'object') config['systemPrompt'] = { persona: prompt.text };
|
|
87
|
+
else (config['systemPrompt'] as Record<string, unknown>)['persona'] = prompt.text;
|
|
88
|
+
} else {
|
|
89
|
+
if (config['planMode'] === null || typeof config['planMode'] !== 'object') config['planMode'] = {};
|
|
90
|
+
const pm = config['planMode'] as Record<string, unknown>;
|
|
91
|
+
if (!Array.isArray(pm['sections'])) pm['sections'] = [];
|
|
92
|
+
const sections = pm['sections'] as Record<string, unknown>[];
|
|
93
|
+
const sec = sections.find((s) => s !== null && typeof s === 'object' && s['name'] === prompt.name);
|
|
94
|
+
if (sec) sec['text'] = prompt.text;
|
|
95
|
+
else sections.push({ name: prompt.name, text: prompt.text });
|
|
96
|
+
}
|
|
97
|
+
return cloned;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 由导出条目构造「重建行」raw(Create 导入用;行名取自来源行,缺失时用占位名) */
|
|
101
|
+
export function buildPromptLine(lineId: string, prompt: PromptExportEntry): Record<string, unknown> {
|
|
102
|
+
const name = prompt.sourceLineName ?? 'dsh-config-manager';
|
|
103
|
+
if (prompt.kind === 'planMode') {
|
|
104
|
+
return { id: lineId, name, config: { planMode: { sections: [{ name: prompt.name, text: prompt.text }] } } };
|
|
105
|
+
}
|
|
106
|
+
return { id: lineId, name, config: { systemPrompt: { persona: prompt.text } } };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
|
|
110
|
+
readonly id = 'prompts' as const;
|
|
111
|
+
readonly displayName = 'Prompts';
|
|
112
|
+
readonly defaultIncluded = true;
|
|
113
|
+
readonly portability = 'portable' as const;
|
|
114
|
+
|
|
115
|
+
async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<PromptsExportSection>> {
|
|
116
|
+
const warnings: string[] = [];
|
|
117
|
+
let lines: { lineId: string; raw: unknown }[] = [];
|
|
118
|
+
try {
|
|
119
|
+
lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
warnings.push(msgOf(ctx)('adapter.patchReadFailedPrompts', { reason: err instanceof Error ? err.message : String(err) }));
|
|
122
|
+
}
|
|
123
|
+
const prompts = extractPrompts(lines);
|
|
124
|
+
return {
|
|
125
|
+
sectionId: 'prompts',
|
|
126
|
+
data: { version: 1, prompts },
|
|
127
|
+
counts: { prompts: prompts.length },
|
|
128
|
+
warnings,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async analyzeImport(data: PromptsExportSection, ctx: ImportContext): Promise<PlanItem[]> {
|
|
133
|
+
const msg = ctx.msg;
|
|
134
|
+
const items: PlanItem[] = [];
|
|
135
|
+
const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
|
|
136
|
+
const targetPrompts = extractPrompts(targetLines);
|
|
137
|
+
for (const p of data.prompts) {
|
|
138
|
+
const id = p.id;
|
|
139
|
+
const sameName = targetPrompts.find((t) => t.name === p.name);
|
|
140
|
+
const targetLine = targetLines.find((l) => l.lineId === p.sourceLineId);
|
|
141
|
+
if (targetLine && sameName && sameName.sourceLineId === p.sourceLineId) {
|
|
142
|
+
if (sameName.text === p.text && sameName.kind === p.kind) {
|
|
143
|
+
items.push({ id, kind: 'Skip', adapter: 'prompts', description: msg('adapter.promptSame', { name: p.name }), severity: 'info' });
|
|
144
|
+
} else {
|
|
145
|
+
items.push({
|
|
146
|
+
id, kind: 'Conflict', adapter: 'prompts',
|
|
147
|
+
description: msg('adapter.promptDiff', { name: p.name }),
|
|
148
|
+
detail: `行 ${p.sourceLineId} current=${JSON.stringify(sameName.text).slice(0, 80)} imported=${JSON.stringify(p.text).slice(0, 80)}`,
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
target: { adapter: 'prompts', ref: p.sourceLineId ?? '' },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
} else if (sameName) {
|
|
154
|
+
// 目标存在同名 prompt(不同来源行)→ 更新其所在行
|
|
155
|
+
if (sameName.text === p.text && sameName.kind === p.kind) {
|
|
156
|
+
items.push({ id, kind: 'Skip', adapter: 'prompts', description: msg('adapter.promptSame', { name: p.name }), severity: 'info' });
|
|
157
|
+
} else {
|
|
158
|
+
items.push({
|
|
159
|
+
id, kind: 'Conflict', adapter: 'prompts',
|
|
160
|
+
description: msg('adapter.promptDiffWithLine', { name: p.name, line: sameName.sourceLineId ?? '' }),
|
|
161
|
+
severity: 'warning',
|
|
162
|
+
target: { adapter: 'prompts', ref: sameName.sourceLineId ?? '' },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
} else if (p.sourceLineName) {
|
|
166
|
+
// 目标无来源行但记录到行名 → 重建行(Create);幂等键 = 行 id
|
|
167
|
+
const lineId = p.sourceLineId ?? `prompt-${p.name}`;
|
|
168
|
+
items.push({
|
|
169
|
+
id, kind: 'Create', adapter: 'prompts',
|
|
170
|
+
description: msg('adapter.promptCreate', { name: p.name, line: lineId }), severity: 'info',
|
|
171
|
+
target: { adapter: 'prompts', ref: lineId },
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
items.push({
|
|
175
|
+
id, kind: 'Warning', adapter: 'prompts',
|
|
176
|
+
description: msg('adapter.promptManual', { name: p.name }),
|
|
177
|
+
severity: 'info',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return items;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
|
|
185
|
+
const msg = ctx.msg;
|
|
186
|
+
if (item.kind === 'Warning') return { ok: true, message: msg('adapter.promptWarningNoWrite') };
|
|
187
|
+
const ref = item.target?.ref;
|
|
188
|
+
if (!ref) return { ok: false, message: msg('adapter.missingTargetRef') };
|
|
189
|
+
const data = ctx.sections.get('prompts') as PromptsExportSection | undefined;
|
|
190
|
+
const prompt = data?.prompts.find((p) => p.id === item.id);
|
|
191
|
+
if (!prompt) return { ok: false, message: msg('adapter.promptMissing', { id: item.id }) };
|
|
192
|
+
// Create:目标无来源行 → 用记录的行名重建 patch 行(insert)
|
|
193
|
+
if (item.kind === 'Create') {
|
|
194
|
+
const raw = buildPromptLine(ref, prompt);
|
|
195
|
+
await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
|
|
196
|
+
{ lineId: ref, raw, action: 'insert' },
|
|
197
|
+
]);
|
|
198
|
+
return { ok: true, needsRestart: true, message: msg('adapter.promptCreated', { name: prompt.name, ref }) };
|
|
199
|
+
}
|
|
200
|
+
// Update / Conflict(useImported):合并进目标行 config
|
|
201
|
+
const lines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
|
|
202
|
+
const line = lines.find((l) => l.lineId === ref);
|
|
203
|
+
if (!line) return { ok: false, message: msg('adapter.patchLineMissing', { ref }) };
|
|
204
|
+
const newRaw = mergePromptIntoLine(line.raw, prompt);
|
|
205
|
+
await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
|
|
206
|
+
{ lineId: ref, raw: newRaw, action: 'update' },
|
|
207
|
+
]);
|
|
208
|
+
return { ok: true, needsRestart: true, message: msg('adapter.promptWritten', { name: prompt.name, ref }) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async validate(data: PromptsExportSection, msg: MsgFunc = zhMsg): Promise<ValidationResult> {
|
|
212
|
+
const issues: ValidationResult['issues'] = [];
|
|
213
|
+
if (data === null || typeof data !== 'object') {
|
|
214
|
+
return { valid: false, issues: [{ path: '$', message: msg('adapter.validate.object', { subject: 'prompts' }), severity: 'error' }] };
|
|
215
|
+
}
|
|
216
|
+
if (data.version !== 1) {
|
|
217
|
+
issues.push({ path: 'version', message: msg('adapter.validate.version', { value: String(data.version) }), severity: 'error' });
|
|
218
|
+
}
|
|
219
|
+
if (!Array.isArray(data.prompts)) {
|
|
220
|
+
issues.push({ path: 'prompts', message: msg('adapter.validate.array', { subject: 'prompts' }), severity: 'error' });
|
|
221
|
+
} else {
|
|
222
|
+
for (const p of data.prompts) {
|
|
223
|
+
if (p === null || typeof p !== 'object' || typeof p.name !== 'string' || typeof p.text !== 'string') {
|
|
224
|
+
issues.push({ path: 'prompts[]', message: msg('adapter.validate.promptIdentity'), severity: 'error' });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
|
|
229
|
+
}
|
|
230
|
+
}
|