@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,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh plugin CLI 通道(方案A 主机侧实现,M1)。
|
|
3
|
+
*
|
|
4
|
+
* 本模块是唯一的子进程启动点:把「安装 / 更新 profile 插件」转成官方
|
|
5
|
+
* `dsh plugin --profile <name> <args…>` 调用(dsh CLI 内部转发 pnpm,并在
|
|
6
|
+
* 成功后 reconcile dsh.profile.bundles),以及从 profile 文件实时读取已装
|
|
7
|
+
* 插件清单(不依赖 web 专用 pluginMarketplace / pluginInventory 服务)。
|
|
8
|
+
*
|
|
9
|
+
* 设计对齐:
|
|
10
|
+
* - dsh 官方实现 @deepseek-ai/dsh/lib/plugin-*.js(runPlugin / reconcilePlugins /
|
|
11
|
+
* exportsPatch 的 bundle 判定语义);
|
|
12
|
+
* - dshmarket src/dsh-cli.ts(dshArgv / spawnShim / killTree / spawnEnv)与
|
|
13
|
+
* src/profile.ts(readInstalled / readInstalledVersion / hasDshManifest)。
|
|
14
|
+
*
|
|
15
|
+
* 只依赖 node 内置模块,零 DSH 运行时依赖(M5 的 TDD 可直接单测本模块)。
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
import { atomicWriteFileSync } from '../utils/atomic-write.js';
|
|
21
|
+
/* ------------------------------------------------------------ profile 文件 */
|
|
22
|
+
/**
|
|
23
|
+
* dsh 出厂自带的 in-box bundle(profile 模板自身安装的层):唯一从「已装
|
|
24
|
+
* 插件清单」中隐藏的名字。社区插件可以合法地发布在官方 scope 下,因此不能
|
|
25
|
+
* 按 scope 整段过滤(dshmarket #28 的结论)。
|
|
26
|
+
*/
|
|
27
|
+
export const INBOX_BUNDLES = new Set([
|
|
28
|
+
'@deepseek-ai/dsh-base',
|
|
29
|
+
'@deepseek-ai/dsh-web-app',
|
|
30
|
+
'@deepseek-ai/dsh-headless',
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* profile 名合法性校验,与 dsh-app-boot 的 resolveProfileDir 一致:拒绝空名、
|
|
34
|
+
* 路径分隔符与保留名,防止 join(home, 'profiles', name) 越界。
|
|
35
|
+
*/
|
|
36
|
+
export function validateProfileName(profile) {
|
|
37
|
+
if (profile === ''
|
|
38
|
+
|| profile.includes('/') || profile.includes('\\')
|
|
39
|
+
|| profile === '.' || profile === '..' || profile === 'node_modules') {
|
|
40
|
+
throw new Error(`invalid profile name ${JSON.stringify(profile)}`);
|
|
41
|
+
}
|
|
42
|
+
return profile;
|
|
43
|
+
}
|
|
44
|
+
/** Profile 目录:$DSH_HOME/profiles/<name>(homeDir 由宿主 resolveDshHome() 解析)。 */
|
|
45
|
+
export function resolveProfileDir(homeDir, profile) {
|
|
46
|
+
return join(homeDir, 'profiles', validateProfileName(profile));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 从启动参数解析 profile 名:`--profile <name>` → 缺省 'web'。与宿主 apply 时的
|
|
50
|
+
* resolveProfileName 同源(config 覆盖仅 apply 处有;报告/适配器场景用本函数即可)。
|
|
51
|
+
* argv 参数化便于测试;缺省读当前进程 argv。
|
|
52
|
+
* 宽容语义:缺值 / 值以 '-' 开头 / 校验非法 → 一律回退 'web'。消息构建路径
|
|
53
|
+
* (applyItem 失败分支、补行等)绝不允许因 profile 名抛错破坏非致命 warning;
|
|
54
|
+
* 且 launcher 在启动时已拒绝非法 profile,此分支实际不可达。
|
|
55
|
+
*/
|
|
56
|
+
export function resolveProfileNameFromArgv(argv = process.argv.slice(2)) {
|
|
57
|
+
const at = argv.indexOf('--profile');
|
|
58
|
+
if (at === -1)
|
|
59
|
+
return 'web';
|
|
60
|
+
const value = argv[at + 1];
|
|
61
|
+
if (value === undefined || value === '' || value.startsWith('-'))
|
|
62
|
+
return 'web';
|
|
63
|
+
try {
|
|
64
|
+
return validateProfileName(value);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 'web';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** 读 profile package.json;不可读(未初始化 / 损坏)返回 null。 */
|
|
71
|
+
export function readProfileManifest(profileDir) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
74
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** 写回 profile package.json(2 空格缩进 + 尾换行,与 dsh-app-boot 一致;原子写防半装损坏)。 */
|
|
81
|
+
export function writeProfileManifest(profileDir, manifest) {
|
|
82
|
+
atomicWriteFileSync(join(profileDir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o644 });
|
|
83
|
+
}
|
|
84
|
+
/** 真实已装的依赖(in-box bundles 过滤掉):name → 声明的版本 spec。 */
|
|
85
|
+
export function readInstalled(profileDir) {
|
|
86
|
+
const manifest = readProfileManifest(profileDir);
|
|
87
|
+
const installed = {};
|
|
88
|
+
for (const [name, spec] of Object.entries(manifest?.dependencies ?? {})) {
|
|
89
|
+
if (!INBOX_BUNDLES.has(name))
|
|
90
|
+
installed[name] = spec;
|
|
91
|
+
}
|
|
92
|
+
return installed;
|
|
93
|
+
}
|
|
94
|
+
/** node_modules/<name>/package.json 里实际落盘的版本;未装 / 不可读为 null。 */
|
|
95
|
+
export function readInstalledVersion(profileDir, name) {
|
|
96
|
+
try {
|
|
97
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'node_modules', name, 'package.json'), 'utf8'));
|
|
98
|
+
return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** 包的 package.json 是否声明 dsh.bundle.patch(即它是一个 bundle / profile 层)。 */
|
|
105
|
+
export function hasDshBundlePatch(pkgDir) {
|
|
106
|
+
try {
|
|
107
|
+
const manifest = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
108
|
+
return manifest.dsh?.bundle?.patch !== undefined;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 维护 dsh.profile.bundles 与已装依赖一致(语义对齐官方 plugin 命令的
|
|
116
|
+
* reconcilePlugins,按「已装状态」而非依赖 diff 判定):
|
|
117
|
+
* - 声明 dsh.bundle.patch 的依赖必须出现在 bundles(追加在依赖顺序尾部);
|
|
118
|
+
* - bundles 中「曾是依赖、但不再是(移除或版本不再声明 patch)」的条目移出;
|
|
119
|
+
* - in-box bundles 不是依赖,永远不碰。
|
|
120
|
+
* 仅在确有变化时写回 package.json。返回是否写回。
|
|
121
|
+
*/
|
|
122
|
+
export function reconcileBundles(profileDir) {
|
|
123
|
+
const manifest = readProfileManifest(profileDir);
|
|
124
|
+
if (manifest === null)
|
|
125
|
+
return false;
|
|
126
|
+
const dependencies = Object.keys(manifest.dependencies ?? {});
|
|
127
|
+
const dependencySet = new Set(dependencies);
|
|
128
|
+
const bundles = manifest.dsh?.profile?.bundles;
|
|
129
|
+
const plugins = Array.isArray(bundles) ? [...bundles] : [];
|
|
130
|
+
let changed = false;
|
|
131
|
+
for (const name of dependencies) {
|
|
132
|
+
if (INBOX_BUNDLES.has(name))
|
|
133
|
+
continue;
|
|
134
|
+
const isBundle = hasDshBundlePatch(join(profileDir, 'node_modules', name));
|
|
135
|
+
if (isBundle && !plugins.includes(name)) {
|
|
136
|
+
plugins.push(name);
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const name of [...plugins]) {
|
|
141
|
+
const wasDependency = dependencySet.has(name);
|
|
142
|
+
const stillBundle = dependencySet.has(name) && hasDshBundlePatch(join(profileDir, 'node_modules', name));
|
|
143
|
+
if (wasDependency && !stillBundle) {
|
|
144
|
+
plugins.splice(plugins.indexOf(name), 1);
|
|
145
|
+
changed = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!changed)
|
|
149
|
+
return false;
|
|
150
|
+
manifest.dsh = {
|
|
151
|
+
...manifest.dsh,
|
|
152
|
+
profile: { ...manifest.dsh?.profile, bundles: plugins },
|
|
153
|
+
};
|
|
154
|
+
writeProfileManifest(profileDir, manifest);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 中止安装后清理半装状态(用户「跳过当前插件」):移除 profile package.json 中的
|
|
159
|
+
* 依赖行 + reconcile bundles + 删 node_modules/<pkg>。
|
|
160
|
+
*
|
|
161
|
+
* 目的:pnpm add 被中途 kill 后,package.json 可能已声明依赖但 node_modules 不完整,
|
|
162
|
+
* 直接启动 DSH 会因缺包而失败。本函数把 manifest 恢复为「该插件未安装」的一致状态,
|
|
163
|
+
* 消除「声明了依赖但没装全」的启动风险。pnpm-lock.yaml 不触碰(运行时不被读取,
|
|
164
|
+
* 下次安装 pnpm 自行对齐)。尽力而为:任何失败不抛错,由调用方记录告警。
|
|
165
|
+
*/
|
|
166
|
+
export function cleanupAbortedInstall(profileDir, pkg) {
|
|
167
|
+
const manifest = readProfileManifest(profileDir);
|
|
168
|
+
if (manifest !== null) {
|
|
169
|
+
const deps = manifest.dependencies ?? {};
|
|
170
|
+
if (Object.prototype.hasOwnProperty.call(deps, pkg)) {
|
|
171
|
+
delete deps[pkg];
|
|
172
|
+
manifest.dependencies = deps;
|
|
173
|
+
writeProfileManifest(profileDir, manifest);
|
|
174
|
+
}
|
|
175
|
+
// 若 pkg 曾是 bundle 成员且已非依赖 → 从 bundles 移除
|
|
176
|
+
reconcileBundles(profileDir);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
rmSync(join(profileDir, 'node_modules', pkg), { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// 尽力而为:目录可能不存在或正被占用,忽略
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** 实时读 profile 已装插件清单(先 reconcile bundles,再逐依赖取版本/bundle 属性)。 */
|
|
186
|
+
export function listInstalledPlugins(homeDir, profile) {
|
|
187
|
+
const dir = resolveProfileDir(homeDir, profile);
|
|
188
|
+
reconcileBundles(dir);
|
|
189
|
+
const out = [];
|
|
190
|
+
const installed = readInstalled(dir);
|
|
191
|
+
for (const name of Object.keys(installed)) {
|
|
192
|
+
const isBundle = hasDshBundlePatch(join(dir, 'node_modules', name));
|
|
193
|
+
out.push({
|
|
194
|
+
name,
|
|
195
|
+
version: readInstalledVersion(dir, name) ?? '',
|
|
196
|
+
// 文件视图无法区分 patch 行禁用:依赖即视为启用(与旧 marketplace 视图一致)。
|
|
197
|
+
enabled: true,
|
|
198
|
+
isBundle,
|
|
199
|
+
// 直接依赖的 bundle 自己就是 profile 层;普通依赖不属于任何聚合 bundle。
|
|
200
|
+
inBundles: isBundle ? [name] : [],
|
|
201
|
+
// 声明依赖 spec 原样保留:github:/file:/link: 等非 registry 来源导入时按此安装。
|
|
202
|
+
spec: installed[name],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// issue #28:仅通过 `dsh.profile.bundles` 声明的层(不在 dependencies 里)此前**完全不可见**,
|
|
206
|
+
// 于是「明明装了、备份里却是空的」。reconcileBundles 对这类条目是**保留**的(它的移除条件
|
|
207
|
+
// 要求该名字曾是依赖),所以它们确实会被 DSH 挂载,清单来源必须与之一致。
|
|
208
|
+
const manifest = readProfileManifest(dir);
|
|
209
|
+
const declaredBundles = manifest?.dsh?.profile?.bundles;
|
|
210
|
+
for (const name of Array.isArray(declaredBundles) ? declaredBundles : []) {
|
|
211
|
+
if (INBOX_BUNDLES.has(name))
|
|
212
|
+
continue;
|
|
213
|
+
if (Object.prototype.hasOwnProperty.call(installed, name))
|
|
214
|
+
continue; // 已在依赖清单里
|
|
215
|
+
out.push({
|
|
216
|
+
name,
|
|
217
|
+
version: readInstalledVersion(dir, name) ?? '',
|
|
218
|
+
enabled: true,
|
|
219
|
+
isBundle: true,
|
|
220
|
+
// 直接声明的层自己就是 profile 层
|
|
221
|
+
inBundles: [name],
|
|
222
|
+
// 无依赖声明 → 无 spec:导入时按裸包名安装(与 registry 依赖同语义)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
/* --------------------------------------------------------------- CLI 通道 */
|
|
228
|
+
/** 15 分钟默认上限(慢网络 + git 安装),可用环境变量覆盖(CI/测试)。 */
|
|
229
|
+
const INSTALL_TIMEOUT_MS = Number(process.env.DSH_CONFIG_MANAGER_INSTALL_TIMEOUT_MS) || 15 * 60 * 1000;
|
|
230
|
+
/**
|
|
231
|
+
* 非 registry 来源的依赖 spec 前缀:这些来源必须把 spec 原样交给 pnpm add,
|
|
232
|
+
* 裸包名在 registry 上查不到(fetch-404 → 幽灵依赖拖垮整个 profile 的安装)。
|
|
233
|
+
* 版本区间(^x / x.y.z)与裸包名走默认语义:npm 最新版(官方机制,见 §34.17 设计决策)。
|
|
234
|
+
*/
|
|
235
|
+
const NON_REGISTRY_SPEC = /^(github:|gitlab:|bitbucket:|git\+|file:|link:|workspace:|https?:)/i;
|
|
236
|
+
/** 安装目标:非 registry spec 按 spec 安装;其余按裸包名(npm 最新版)。 */
|
|
237
|
+
export function installSpecFor(pkg, spec) {
|
|
238
|
+
if (spec !== undefined && spec !== '' && NON_REGISTRY_SPEC.test(spec))
|
|
239
|
+
return spec;
|
|
240
|
+
return pkg;
|
|
241
|
+
}
|
|
242
|
+
/** Windows 的 npm/corepack/pnpm/dsh 都是 .cmd 垫片:Node 直接 spawn 无法启动(ENOENT/EINVAL)。 */
|
|
243
|
+
const winCmdShim = process.platform === 'win32';
|
|
244
|
+
/**
|
|
245
|
+
* 子进程环境:CI=true 让 pnpm v10+ 在无 TTY 时立即行动或失败,而不是卡在
|
|
246
|
+
* 静默交互提示;macOS GUI 启动不继承 shell PATH,补上常见 bin 目录(dshmarket #32)。
|
|
247
|
+
*/
|
|
248
|
+
function spawnEnv() {
|
|
249
|
+
if (process.platform === 'win32')
|
|
250
|
+
return { ...process.env, CI: 'true' };
|
|
251
|
+
const parts = (process.env.PATH ?? '').split(':').filter((part) => part !== '');
|
|
252
|
+
for (const bin of ['/opt/homebrew/bin', '/usr/local/bin', `${process.env.HOME ?? ''}/.local/bin`]) {
|
|
253
|
+
if (bin !== '' && !parts.includes(bin))
|
|
254
|
+
parts.push(bin);
|
|
255
|
+
}
|
|
256
|
+
return { ...process.env, CI: 'true', PATH: parts.join(':') };
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 重放启动当前宿主进程的 dsh CLI:
|
|
260
|
+
* - process.argv[1] 是 dsh 入口(全局 bin / 源码启动的 bin.ts / bin.js)→ 用
|
|
261
|
+
* 当前 Node 重新执行该入口(execArgv 原样保留,cwd 靠近入口保持相对导入可解析);
|
|
262
|
+
* - 否则退回 PATH 上的 `dsh`(Windows 是 .cmd,需经 shell)。
|
|
263
|
+
*/
|
|
264
|
+
export function dshArgv() {
|
|
265
|
+
const entry = process.argv[1];
|
|
266
|
+
if (entry !== undefined && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
|
|
267
|
+
const abs = resolve(entry);
|
|
268
|
+
return { file: process.execPath, args: [...process.execArgv, abs], cwd: dirname(abs), viaShell: false };
|
|
269
|
+
}
|
|
270
|
+
return { file: 'dsh', args: [], cwd: undefined, viaShell: winCmdShim };
|
|
271
|
+
}
|
|
272
|
+
/** cmd.exe 视为语法字符(即使在一个 token 内)。 */
|
|
273
|
+
const CMD_METACHARS = /[\s"&|<>^()%!]/;
|
|
274
|
+
/** 为一个 argv token 构造 cmd.exe 命令行引用(需要引用的 token 用双引号包裹并加倍内部引号)。 */
|
|
275
|
+
export function quoteCmdArg(arg) {
|
|
276
|
+
if (!CMD_METACHARS.test(arg))
|
|
277
|
+
return arg;
|
|
278
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
279
|
+
}
|
|
280
|
+
/** 由 argv 构造 cmd.exe 命令行(仅 Windows 垫片路径使用)。 */
|
|
281
|
+
export function cmdCommandLine(argv) {
|
|
282
|
+
return argv.map(quoteCmdArg).join(' ');
|
|
283
|
+
}
|
|
284
|
+
/** cmd.exe 解析结果(仅 Windows 垫片路径使用)。 */
|
|
285
|
+
const COMSPEC = process.env.ComSpec ?? 'cmd.exe';
|
|
286
|
+
/**
|
|
287
|
+
* 启动命令:Windows .cmd 垫片经 cmd.exe /d /s /c 显式构造的命令行(避开 Node
|
|
288
|
+
* 弃用的 shell:true + argv 组合,DEP0190);其余直接 shell:false 启动。
|
|
289
|
+
*/
|
|
290
|
+
function spawnShim(file, args, options) {
|
|
291
|
+
const { viaShell = false, ...spawnOptions } = options;
|
|
292
|
+
if (!viaShell || process.platform !== 'win32') {
|
|
293
|
+
return spawn(file, [...args], { ...spawnOptions, shell: false });
|
|
294
|
+
}
|
|
295
|
+
return spawn(COMSPEC, ['/d', '/s', '/c', `"${cmdCommandLine([file, ...args])}"`], {
|
|
296
|
+
...spawnOptions,
|
|
297
|
+
shell: false,
|
|
298
|
+
windowsVerbatimArguments: true,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* 杀掉子进程整棵进程树:Windows 的 kill() 只终止包装进程、pnpm 子进程还活着,
|
|
303
|
+
* 用 taskkill /T /F;POSIX 子进程独立进程组启动(detached),先 SIGTERM 组、
|
|
304
|
+
* 5 秒后 SIGKILL 兜底。
|
|
305
|
+
*/
|
|
306
|
+
export function killTree(child) {
|
|
307
|
+
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
308
|
+
try {
|
|
309
|
+
spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
/* fall through to plain kill */
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const signalTree = (signal) => {
|
|
317
|
+
if (child.pid === undefined)
|
|
318
|
+
return;
|
|
319
|
+
try {
|
|
320
|
+
process.kill(-child.pid, signal);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
try {
|
|
324
|
+
child.kill(signal);
|
|
325
|
+
}
|
|
326
|
+
catch { /* already gone */ }
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
signalTree('SIGTERM');
|
|
330
|
+
const escalate = setTimeout(() => signalTree('SIGKILL'), 5000);
|
|
331
|
+
escalate.unref?.();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* 运行 `dsh plugin --profile <profile> <pluginArgs…>`:
|
|
335
|
+
* cwd=profile 目录、CI 环境、windowsHide、pipe 输出、15 分钟超时杀进程树。
|
|
336
|
+
* 永远 resolve(不 throw),由调用方判定成败。
|
|
337
|
+
*
|
|
338
|
+
* signal:可选 AbortSignal。中止时立即 killTree 杀掉整棵进程树(dsh 包装 + pnpm 孙进程),
|
|
339
|
+
* 结果标记 aborted=true(调用方据此判定「用户跳过/取消」,而非失败)。
|
|
340
|
+
*/
|
|
341
|
+
export function runDshPlugin(profileDir, profile, pluginArgs, timeoutMs = INSTALL_TIMEOUT_MS, signal) {
|
|
342
|
+
const { file, args, viaShell } = dshArgv();
|
|
343
|
+
return new Promise((resolvePromise) => {
|
|
344
|
+
const child = spawnShim(file, [...args, 'plugin', '--profile', profile, ...pluginArgs], {
|
|
345
|
+
cwd: profileDir,
|
|
346
|
+
env: spawnEnv(),
|
|
347
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
348
|
+
windowsHide: true,
|
|
349
|
+
viaShell,
|
|
350
|
+
// POSIX 下独立进程组,超时/取消可一次杀掉 dsh 包装进程 + pnpm 孙进程。
|
|
351
|
+
detached: process.platform !== 'win32',
|
|
352
|
+
});
|
|
353
|
+
let stdout = '';
|
|
354
|
+
let stderr = '';
|
|
355
|
+
let timedOut = false;
|
|
356
|
+
let aborted = false;
|
|
357
|
+
const timer = setTimeout(() => {
|
|
358
|
+
timedOut = true;
|
|
359
|
+
killTree(child);
|
|
360
|
+
}, timeoutMs);
|
|
361
|
+
const onAbort = () => {
|
|
362
|
+
aborted = true;
|
|
363
|
+
clearTimeout(timer);
|
|
364
|
+
killTree(child);
|
|
365
|
+
};
|
|
366
|
+
if (signal !== undefined) {
|
|
367
|
+
if (signal.aborted)
|
|
368
|
+
onAbort();
|
|
369
|
+
else
|
|
370
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
371
|
+
}
|
|
372
|
+
child.stdout?.on('data', (chunk) => {
|
|
373
|
+
stdout = (stdout + chunk.toString()).slice(-256 * 1024);
|
|
374
|
+
});
|
|
375
|
+
child.stderr?.on('data', (chunk) => {
|
|
376
|
+
stderr = (stderr + chunk.toString()).slice(-64 * 1024);
|
|
377
|
+
});
|
|
378
|
+
child.on('error', (error) => {
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
signal?.removeEventListener('abort', onAbort);
|
|
381
|
+
resolvePromise({ exitCode: 127, timedOut: false, aborted, stdout, stderr, spawnError: error.message });
|
|
382
|
+
});
|
|
383
|
+
child.on('close', (code) => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
signal?.removeEventListener('abort', onAbort);
|
|
386
|
+
resolvePromise({ exitCode: code, timedOut, aborted, stdout, stderr });
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 瞬时网络故障判定:pnpm 的 5xx 拉取码、meta 拉取放弃、以及穿透 dsh 包装的
|
|
392
|
+
* 原始 socket 错误。永久的形态(404、鉴权)刻意排除——重试只会加倍等坏消息。
|
|
393
|
+
*/
|
|
394
|
+
export function isTransientDshPluginFailure(output) {
|
|
395
|
+
return /ERR_PNPM_FETCH_5\d\d|ERR_PNPM_META_FETCH_FAIL|FetchError|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|socket hang up|network timeout/i.test(output);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 把一次失败的 dsh plugin 调用的合并输出映射到已知失败模式。
|
|
399
|
+
* @param output - stdout+stderr 合并文本。
|
|
400
|
+
* @returns 分类结果;未识别返回 null(调用方展示原始摘要)。
|
|
401
|
+
*/
|
|
402
|
+
export function classifyDshPluginFailure(output) {
|
|
403
|
+
if (output.includes('ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF')) {
|
|
404
|
+
return {
|
|
405
|
+
code: 'hoist-pattern-diff',
|
|
406
|
+
recoverable: true,
|
|
407
|
+
message: 'profile 的 node_modules 由旧版 pnpm 创建,与当前 pnpm 的默认配置不兼容;在 profile 目录执行一次 pnpm install 重建后重试 / this profile\'s node_modules was created by a different pnpm major; rebuild it (pnpm install) before retrying',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (output.includes('ERR_PNPM_ADDING_TO_ROOT')) {
|
|
411
|
+
return {
|
|
412
|
+
code: 'adding-to-root',
|
|
413
|
+
recoverable: true,
|
|
414
|
+
message: 'profile 是 pnpm workspace,当前 dsh CLI 转发未带 -w 被 pnpm 拒绝;可手动执行 dsh plugin --profile <name> add -w <pkg> / pnpm refused to add at a workspace root (missing -w); run dsh plugin --profile <name> add -w <pkg> by hand',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (/--workspace-root may only be used inside a workspace/i.test(output)) {
|
|
418
|
+
return {
|
|
419
|
+
code: 'not-a-workspace',
|
|
420
|
+
recoverable: false,
|
|
421
|
+
message: 'profile 目录不是 pnpm workspace,却传入了 -w,因此被 pnpm 拒绝 / -w was passed but the profile is not a pnpm workspace, so pnpm refused the operation',
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (output.includes('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION') || output.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')) {
|
|
425
|
+
return {
|
|
426
|
+
code: 'release-age-violation',
|
|
427
|
+
recoverable: true,
|
|
428
|
+
message: 'profile 里有刚发布不久的插件版本,pnpm 的安全等待期检查拒绝了本次改动(即使改的是别的插件);等待发布期满,或临时以 --config.minimumReleaseAge=0 重试 / a recently-published plugin version in this profile trips pnpm\'s fresh-release safety check, blocking any change (even to other plugins); wait for the release to mature, or retry once with --config.minimumReleaseAge=0',
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED') || /git-hosted package "[^"]+" needs to execute build scripts/i.test(output)) {
|
|
432
|
+
return {
|
|
433
|
+
code: 'git-build-blocked',
|
|
434
|
+
recoverable: false,
|
|
435
|
+
message: '该 git 插件需要在安装时执行构建脚本,被 pnpm 的 allowBuilds 白名单拦截;把 pnpm 提示的包名加入 profile 的 pnpm-workspace.yaml 的 allowBuilds 后重试 / this git-hosted plugin needs to run its build script at install time, which pnpm blocks until allowed — add the exact package pnpm printed to allowBuilds in the profile\'s pnpm-workspace.yaml, then re-run',
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// issue #35:pnpm-workspace.yaml 的 patchedDependencies 指向不存在的 patch 文件时,
|
|
439
|
+
// pnpm 会在**任何** add 上失败(含与补丁无关的插件)。此时逐条报「插件装不上」是误导——
|
|
440
|
+
// 真正要修的是缺失的 patch 文件或那条声明。
|
|
441
|
+
if (/Failed to read patch file|patch file .*does not exist/i.test(output)) {
|
|
442
|
+
// 路径可能含盘符冒号(C:\...),故按整行取、再切掉尾随的 ':' 或 ': <错误文本>'
|
|
443
|
+
const raw = /Failed to read patch file\s+([^\r\n]+)/i.exec(output)?.[1]?.trim();
|
|
444
|
+
const file = raw === undefined ? undefined : raw.split(/:\s/)[0]?.replace(/:\s*$/, '').trim();
|
|
445
|
+
const zh = file === undefined ? '' : `(${file})`;
|
|
446
|
+
const en = file === undefined ? '' : ` (${file})`;
|
|
447
|
+
return {
|
|
448
|
+
code: 'patch-file-missing',
|
|
449
|
+
recoverable: false,
|
|
450
|
+
message: `profile 的 pnpm-workspace.yaml 声明了 patchedDependencies,但对应的 patch 文件不存在${zh};pnpm 因此拒绝任何安装(含与补丁无关的插件)。把缺失的 patch 文件放回 profile 的 patches/ 目录,或从 pnpm-workspace.yaml 删除该条目后重试 / the profile's pnpm-workspace.yaml declares patchedDependencies but the patch file is missing${en}; pnpm then refuses every install (even unrelated plugins). Put the patch file back under the profile's patches/ directory, or remove that entry from pnpm-workspace.yaml and retry`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (output.includes('ERR_PNPM_FETCH_404')) {
|
|
454
|
+
const pkg = /GET\s+\S*\/([^/\s]+):/.exec(output)?.[1]?.replace(/%2[Ff]/g, '/');
|
|
455
|
+
const zh = pkg === undefined ? '' : `(${pkg})`;
|
|
456
|
+
const en = pkg === undefined ? '' : ` (${pkg})`;
|
|
457
|
+
return {
|
|
458
|
+
code: 'fetch-404',
|
|
459
|
+
recoverable: false,
|
|
460
|
+
message: `有一个依赖在 registry 上不存在${zh},pnpm 拒绝任何安装操作;它可能是之前失败操作残留在 profile package.json 里的幽灵依赖(手动删除该行),也可能是需要登录的私有包 / a dependency cannot be resolved from the registry${en}; pnpm refuses every install while it is present — it may be a ghost entry left in the profile's package.json by an earlier failed operation (remove that line by hand), or a private package needing registry credentials`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (isTransientDshPluginFailure(output)) {
|
|
464
|
+
return {
|
|
465
|
+
code: 'transient-network',
|
|
466
|
+
recoverable: true,
|
|
467
|
+
message: '拉取依赖时网络临时失败(安装会重放整个依赖树,任何一个既有依赖抖动都会中断本次操作);稍后直接重试通常即可 / a transient network failure while fetching dependencies (installs replay the whole dependency tree, so any existing dependency can hiccup); a plain retry usually succeeds',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (output.includes('pnpm not found on PATH')) {
|
|
471
|
+
return {
|
|
472
|
+
code: 'pnpm-missing',
|
|
473
|
+
recoverable: false,
|
|
474
|
+
message: '找不到 pnpm(dsh 转发 pnpm 失败):请先安装 pnpm(npm install -g pnpm 或 corepack enable pnpm),然后重试 / pnpm is not on PATH (dsh forwards to pnpm): install pnpm (npm install -g pnpm or corepack enable pnpm) and retry',
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
/** 安装失败 → 分类后的双语可读 Error;未识别则保留 stderr 尾部摘要。 */
|
|
480
|
+
export function installErrorFor(pkg, result) {
|
|
481
|
+
if (result.spawnError !== undefined && /ENOENT|EINVAL/.test(result.spawnError)) {
|
|
482
|
+
return new Error(`无法启动 dsh CLI(${result.spawnError}):请确认 dsh 已安装且在 PATH 上,然后重试安装 ${pkg} / cannot start dsh CLI (${result.spawnError}): make sure dsh is installed and on PATH, then retry installing ${pkg}`);
|
|
483
|
+
}
|
|
484
|
+
if (result.timedOut) {
|
|
485
|
+
return new Error(`安装 ${pkg} 超时(进程已终止,默认 15 分钟):请检查网络或稍后重试 / install ${pkg} timed out (process killed, default 15 min): check your network and retry later`);
|
|
486
|
+
}
|
|
487
|
+
const output = `${result.stderr}\n${result.stdout}`;
|
|
488
|
+
const failure = classifyDshPluginFailure(output);
|
|
489
|
+
if (failure !== null) {
|
|
490
|
+
return new Error(`安装 ${pkg} 失败(${failure.code}):${failure.message}`);
|
|
491
|
+
}
|
|
492
|
+
const tail = (result.stderr.trim().split('\n').slice(-8).join('\n')
|
|
493
|
+
|| result.stdout.trim().split('\n').slice(-8).join('\n')
|
|
494
|
+
|| '无输出');
|
|
495
|
+
return new Error(`dsh plugin 安装 ${pkg} 失败(exit ${String(result.exitCode)}):${tail}`);
|
|
496
|
+
}
|
|
497
|
+
//# sourceMappingURL=plugin-cli.js.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciler(Phase 3:startup reconciliation)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:扫描 active journals → parse/schema 校验(corrupt 隔离)→ 验 environment fingerprint
|
|
5
|
+
* → 验 Journal→Lock 绑定 → 验快照 → 验 step 指纹 → 外部探测 → 选择推荐 recovery outcome
|
|
6
|
+
* → 执行(需用户确认的破坏性动作才执行)→ recovery-history → 幂等。
|
|
7
|
+
*
|
|
8
|
+
* **Reconciler 不拥有 Environment Lock primitive 实现** —— 通过 Coordinator/port 使用
|
|
9
|
+
* (release、active≤1、recover 显式流程由宿主/Coordinator 编排)。
|
|
10
|
+
*
|
|
11
|
+
* **Startup(Rev 3 P1-NEW-2)不自动 recover stale lock**:
|
|
12
|
+
* - 若发现 stale lock + incomplete journal → RECOVERY_REQUIRED / SAFE MODE,不自动 recoverStaleLock;
|
|
13
|
+
* - 用户显式确认后才 prove stale → recover → acquire → reconcile。
|
|
14
|
+
*/
|
|
15
|
+
import { JournalStore, type OperationJournal, type JournalStep } from './journal.ts';
|
|
16
|
+
export type StepFingerprintVerdict = 'before-match' | 'after-match' | 'none' | 'unable';
|
|
17
|
+
export type ExternalProbe = 'installed' | 'half-installed' | 'reverse-residue' | 'not-installed' | 'unknown';
|
|
18
|
+
export interface ReconcileProbeHooks {
|
|
19
|
+
/** 对某 step 的目标做磁盘指纹判定。 */
|
|
20
|
+
verifyStepFingerprint(step: JournalStep): Promise<StepFingerprintVerdict>;
|
|
21
|
+
/** 外部步骤(插件/Git/WebDAV)状态探测。 */
|
|
22
|
+
probeExternal(step: JournalStep): Promise<ExternalProbe>;
|
|
23
|
+
/**
|
|
24
|
+
* 快照完整性(id 合法 + snapshot.json 存在 + 必要 blobs 存在 + READY + 属于本 op)。
|
|
25
|
+
* binding 为 journal 的绑定字段(operationId/ownerInstanceId/environmentFingerprint),
|
|
26
|
+
* 实现应校验 snapshot 的 manifest binding 与 journal 一致(P1-1:防伪造快照过 snapshotExists)。
|
|
27
|
+
*/
|
|
28
|
+
snapshotExists(snapshotId: string | null, binding?: {
|
|
29
|
+
operationId?: string;
|
|
30
|
+
ownerInstanceId?: string;
|
|
31
|
+
environmentFingerprint?: string;
|
|
32
|
+
}): Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
export interface ReconcileEnv {
|
|
35
|
+
environmentFingerprint: string;
|
|
36
|
+
/** 判定某 journal 是否属于仍存活的 live owner(live op 不可 reconcile/quarantine/move)。 */
|
|
37
|
+
isLiveOwner(journal: OperationJournal): boolean | Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* P1-A:期望的 crashed-ownership instanceId(= stale environment.lock owner.instanceId)。
|
|
40
|
+
* 显式 recovery 捕获 stale ownership 证据后传入:active journal 的 ownerInstanceId/lockId
|
|
41
|
+
* 必须匹配该 crashed ownership,才可作为 trusted transaction 参与 reconcile/rollback/resume;
|
|
42
|
+
* 否则 → needs-attention(SAFE MODE,不 trusted),防跨 transaction/伪造 journal。
|
|
43
|
+
*/
|
|
44
|
+
expectedOwnershipInstanceId?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ReconcileOptions {
|
|
47
|
+
/** 是否自动隔离 corrupt journal(move 到 quarantine + attention sidecar)。默认 true。 */
|
|
48
|
+
autoQuarantine?: boolean;
|
|
49
|
+
/** 是否对 terminal-in-active journal 自动 move 到 completed。默认 true。 */
|
|
50
|
+
organizeTerminal?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export type ReconcileDecisionKind = 'live' | 'corrupt' | 'env-mismatch' | 'organized' | 'recovered' | 'noop' | 'rollback-recommended' | 'rollback-continue' | 'needs-attention';
|
|
53
|
+
export interface ReconcileDecision {
|
|
54
|
+
operationId: string;
|
|
55
|
+
kind: ReconcileDecisionKind;
|
|
56
|
+
reason: string;
|
|
57
|
+
snapshotId: string | null;
|
|
58
|
+
}
|
|
59
|
+
/** 汇总结果:是否该进入 SAFE MODE(任一 needs-attention / env-mismatch / corrupt / rollback-recommended)。 */
|
|
60
|
+
export interface ReconcileOutcome {
|
|
61
|
+
decisions: ReconcileDecision[];
|
|
62
|
+
safeModeRequired: boolean;
|
|
63
|
+
/** unresolved(未到 terminal)的 incomplete op id(RECOVERY_REQUIRED 候选)。 */
|
|
64
|
+
unresolved: string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 分析 active/ 下的所有 journal,返回每个 op 的决策 + 是否需 SAFE MODE。
|
|
68
|
+
* 只读(除 corrupt quarantine 与 terminal organize / recovery-history 外,不做 data mutation)。
|
|
69
|
+
*/
|
|
70
|
+
export declare function reconcileActive(store: JournalStore, hooks: ReconcileProbeHooks, env: ReconcileEnv, opts?: ReconcileOptions): Promise<ReconcileOutcome>;
|
|
71
|
+
export interface RecoveryExecutorInput {
|
|
72
|
+
operationId: string;
|
|
73
|
+
action: 'rollback' | 'resume' | 'dismiss';
|
|
74
|
+
snapshotId: string | null;
|
|
75
|
+
}
|
|
76
|
+
/** 执行恢复(破坏性动作必须 userConfirm=true 才执行)。 */
|
|
77
|
+
export interface RecoveryExecutorInput {
|
|
78
|
+
operationId: string;
|
|
79
|
+
action: 'rollback' | 'resume' | 'dismiss';
|
|
80
|
+
snapshotId: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* 恢复决策(§5.2 引擎映射):rollback-recommended → restore executor(恢复到 trusted snapshot);
|
|
83
|
+
* rollback-continue → rollback executor(续跑中断回滚)。缺省 = rollback-continue(兼容旧调用)。
|
|
84
|
+
*/
|
|
85
|
+
decision?: 'rollback-recommended' | 'rollback-continue';
|
|
86
|
+
/** 宿主注入的实际回滚执行器(rollback-continue:续跑中断回滚;破坏性副作用由宿主/Coordinator 执行)。 */
|
|
87
|
+
performRollback?: (snapshotId: string) => Promise<{
|
|
88
|
+
full: boolean;
|
|
89
|
+
failed: string[];
|
|
90
|
+
}>;
|
|
91
|
+
/** 宿主注入的 restore 执行器(rollback-recommended:恢复到 trusted snapshot)。 */
|
|
92
|
+
performRestore?: (snapshotId: string) => Promise<{
|
|
93
|
+
full: boolean;
|
|
94
|
+
failed: string[];
|
|
95
|
+
}>;
|
|
96
|
+
}
|
|
97
|
+
export declare function executeRecovery(store: JournalStore, input: RecoveryExecutorInput, userConfirmed: boolean): Promise<'done' | 'needs-confirmation' | 'failed'>;
|
|
98
|
+
/**
|
|
99
|
+
* 对 terminal `NEEDS_ATTENTION` journal 重算 recovery 决策(§5.4,Reviewer A 第二轮 P1-2)。
|
|
100
|
+
*
|
|
101
|
+
* `reconcileActive` 对 terminal `NEEDS_ATTENTION` journal 恒返回 `needs-attention`
|
|
102
|
+
* (`NEEDS_ATTENTION` 是 terminal,`rollback-recommended`/`rollback-continue` 只在转换进
|
|
103
|
+
* NEEDS_ATTENTION 的当次 pass 出现一次)。`GET /recovery/status` 用本函数重算,区分
|
|
104
|
+
* `rollback-recommended` / `rollback-continue` / `needs-attention`。
|
|
105
|
+
*
|
|
106
|
+
* **只读**:不修改 journal,不写 SAFE MODE,不写 recovery-history。
|
|
107
|
+
*/
|
|
108
|
+
export declare function recomputeRecoveryDecision(j: OperationJournal, hooks: Pick<ReconcileProbeHooks, 'snapshotExists'>): Promise<'rollback-continue' | 'rollback-recommended' | 'needs-attention'>;
|
|
109
|
+
export interface StartupInspection {
|
|
110
|
+
safeModeRequired: boolean;
|
|
111
|
+
recoveryRequired: boolean;
|
|
112
|
+
decisions: ReconcileDecision[];
|
|
113
|
+
unresolved: string[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Startup 只读事务状态 + 锁状态扫描(Rev 3 P1-NEW-2):
|
|
117
|
+
* 只检测、只读;**不自动 recoverStaleLock、不自动 create journal**。
|
|
118
|
+
* lockState: 'LOCKED' | 'STALE_LOCK_DETECTED' | 'UNKNOWN_STATE' | 'FREE'
|
|
119
|
+
*/
|
|
120
|
+
export declare function inspectStartup(store: JournalStore, hooks: ReconcileProbeHooks, env: ReconcileEnv, opts: ReconcileOptions, lockState: 'LOCKED' | 'STALE_LOCK_DETECTED' | 'UNKNOWN_STATE' | 'FREE'): Promise<StartupInspection>;
|