dsh-config-manager 0.1.69 → 0.1.71
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/README.md +59 -18
- package/README.zh-CN.md +29 -9
- package/lib/adapters/credentials.js +6 -3
- package/lib/adapters/file-collection.d.ts +21 -1
- package/lib/adapters/file-collection.js +32 -8
- package/lib/adapters/mcp.d.ts +12 -2
- package/lib/adapters/mcp.js +30 -13
- package/lib/adapters/plugin-files.d.ts +8 -0
- package/lib/adapters/plugin-files.js +36 -4
- package/lib/adapters/plugins.d.ts +28 -2
- package/lib/adapters/plugins.js +204 -55
- package/lib/adapters/prompts.d.ts +12 -2
- package/lib/adapters/prompts.js +34 -19
- package/lib/adapters/providers.js +5 -4
- package/lib/adapters/self.d.ts +20 -0
- package/lib/adapters/self.js +73 -0
- package/lib/adapters/sessions.d.ts +8 -0
- package/lib/adapters/sessions.js +47 -1
- package/lib/adapters/settings.d.ts +21 -3
- package/lib/adapters/settings.js +44 -7
- package/lib/adapters/skills.d.ts +36 -4
- package/lib/adapters/skills.js +222 -3
- package/lib/adapters/test-helpers.d.ts +31 -2
- package/lib/adapters/test-helpers.js +42 -5
- package/lib/adapters/workspaces.js +97 -5
- package/lib/cli/actions.d.ts +50 -3
- package/lib/cli/actions.js +103 -11
- package/lib/cli/help.d.ts +65 -0
- package/lib/cli/help.js +517 -0
- package/lib/cli/import-source.d.ts +32 -0
- package/lib/cli/import-source.js +178 -0
- package/lib/cli/index.d.ts +18 -2
- package/lib/cli/index.js +118 -90
- package/lib/cli/web/client-script.d.ts +8 -0
- package/lib/cli/web/client-script.js +11 -0
- package/lib/cli/web/http.d.ts +5 -0
- package/lib/cli/web/http.js +2 -1
- package/lib/cli/web/page.d.ts +16 -6
- package/lib/cli/web/page.js +102 -25
- package/lib/cli/web/routes.js +24 -5
- package/lib/cli/web/server.js +23 -2
- package/lib/client.d.ts +1019 -36
- package/lib/client.js +5522 -1564
- package/lib/core/analyzer.d.ts +15 -1
- package/lib/core/analyzer.js +114 -13
- package/lib/core/backup-plan.js +17 -1
- package/lib/core/backup.d.ts +8 -0
- package/lib/core/backup.js +28 -6
- package/lib/core/cache-cleaner.d.ts +10 -0
- package/lib/core/cache-cleaner.js +63 -22
- package/lib/core/checkpoint.d.ts +362 -0
- package/lib/core/checkpoint.js +1309 -0
- package/lib/core/journal.d.ts +37 -0
- package/lib/core/journal.js +69 -0
- package/lib/core/messages.d.ts +13 -0
- package/lib/core/messages.js +26 -0
- package/lib/core/model-tools.js +3 -0
- package/lib/core/patch-layers.d.ts +83 -0
- package/lib/core/patch-layers.js +110 -0
- package/lib/core/restore.d.ts +21 -0
- package/lib/core/restore.js +37 -3
- package/lib/core/rollback.js +6 -3
- package/lib/core/self-update.d.ts +52 -0
- package/lib/core/self-update.js +70 -0
- package/lib/core/session-health.d.ts +29 -5
- package/lib/core/session-health.js +28 -3
- package/lib/core/types.d.ts +126 -0
- package/lib/foreign/antigravity.d.ts +28 -0
- package/lib/foreign/antigravity.js +157 -0
- package/lib/foreign/bundle.d.ts +25 -0
- package/lib/foreign/bundle.js +79 -0
- package/lib/foreign/chatgpt.d.ts +6 -0
- package/lib/foreign/chatgpt.js +25 -0
- package/lib/foreign/claude-code.d.ts +3 -0
- package/lib/foreign/claude-code.js +85 -0
- package/lib/foreign/claude-sessions.d.ts +108 -0
- package/lib/foreign/claude-sessions.js +299 -0
- package/lib/foreign/cline.d.ts +6 -0
- package/lib/foreign/cline.js +27 -0
- package/lib/foreign/codex.d.ts +83 -0
- package/lib/foreign/codex.js +348 -0
- package/lib/foreign/continue.d.ts +9 -0
- package/lib/foreign/continue.js +65 -0
- package/lib/foreign/copilot.d.ts +39 -0
- package/lib/foreign/copilot.js +95 -0
- package/lib/foreign/crush.d.ts +11 -0
- package/lib/foreign/crush.js +57 -0
- package/lib/foreign/cursor.d.ts +87 -0
- package/lib/foreign/cursor.js +171 -0
- package/lib/foreign/dsh.d.ts +30 -0
- package/lib/foreign/dsh.js +101 -0
- package/lib/foreign/dsh4.d.ts +3 -0
- package/lib/foreign/dsh4.js +13 -0
- package/lib/foreign/gemini.d.ts +7 -0
- package/lib/foreign/gemini.js +44 -0
- package/lib/foreign/goose.d.ts +11 -0
- package/lib/foreign/goose.js +53 -0
- package/lib/foreign/grokbuild.d.ts +8 -0
- package/lib/foreign/grokbuild.js +25 -0
- package/lib/foreign/hermes.d.ts +2 -0
- package/lib/foreign/hermes.js +78 -0
- package/lib/foreign/kernel.d.ts +132 -0
- package/lib/foreign/kernel.js +569 -0
- package/lib/foreign/kilocode.d.ts +11 -0
- package/lib/foreign/kilocode.js +51 -0
- package/lib/foreign/kimi.d.ts +7 -0
- package/lib/foreign/kimi.js +47 -0
- package/lib/foreign/mimocode.d.ts +11 -0
- package/lib/foreign/mimocode.js +51 -0
- package/lib/foreign/openclaw.d.ts +7 -0
- package/lib/foreign/openclaw.js +46 -0
- package/lib/foreign/opencode.d.ts +11 -0
- package/lib/foreign/opencode.js +52 -0
- package/lib/foreign/pi.d.ts +7 -0
- package/lib/foreign/pi.js +48 -0
- package/lib/foreign/platform-paths.d.ts +66 -0
- package/lib/foreign/platform-paths.js +141 -0
- package/lib/foreign/qoder.d.ts +10 -0
- package/lib/foreign/qoder.js +49 -0
- package/lib/foreign/qwen.d.ts +7 -0
- package/lib/foreign/qwen.js +45 -0
- package/lib/foreign/read-antigravity.d.ts +57 -0
- package/lib/foreign/read-antigravity.js +504 -0
- package/lib/foreign/read-chatgpt.d.ts +20 -0
- package/lib/foreign/read-chatgpt.js +509 -0
- package/lib/foreign/read-claude-code.d.ts +24 -0
- package/lib/foreign/read-claude-code.js +234 -0
- package/lib/foreign/read-cline.d.ts +53 -0
- package/lib/foreign/read-cline.js +479 -0
- package/lib/foreign/read-codex.d.ts +75 -0
- package/lib/foreign/read-codex.js +842 -0
- package/lib/foreign/read-continue.d.ts +87 -0
- package/lib/foreign/read-continue.js +489 -0
- package/lib/foreign/read-copilot.d.ts +42 -0
- package/lib/foreign/read-copilot.js +267 -0
- package/lib/foreign/read-crush.d.ts +54 -0
- package/lib/foreign/read-crush.js +478 -0
- package/lib/foreign/read-cursor.d.ts +141 -0
- package/lib/foreign/read-cursor.js +705 -0
- package/lib/foreign/read-dsh.d.ts +43 -0
- package/lib/foreign/read-dsh.js +205 -0
- package/lib/foreign/read-dsh4.d.ts +14 -0
- package/lib/foreign/read-dsh4.js +27 -0
- package/lib/foreign/read-gemini.d.ts +55 -0
- package/lib/foreign/read-gemini.js +273 -0
- package/lib/foreign/read-goose.d.ts +25 -0
- package/lib/foreign/read-goose.js +322 -0
- package/lib/foreign/read-grokbuild.d.ts +34 -0
- package/lib/foreign/read-grokbuild.js +455 -0
- package/lib/foreign/read-hermes.d.ts +74 -0
- package/lib/foreign/read-hermes.js +690 -0
- package/lib/foreign/read-kilocode.d.ts +17 -0
- package/lib/foreign/read-kilocode.js +46 -0
- package/lib/foreign/read-kimi.d.ts +63 -0
- package/lib/foreign/read-kimi.js +719 -0
- package/lib/foreign/read-mimocode.d.ts +12 -0
- package/lib/foreign/read-mimocode.js +56 -0
- package/lib/foreign/read-openclaw.d.ts +51 -0
- package/lib/foreign/read-openclaw.js +445 -0
- package/lib/foreign/read-opencode.d.ts +150 -0
- package/lib/foreign/read-opencode.js +896 -0
- package/lib/foreign/read-pi.d.ts +56 -0
- package/lib/foreign/read-pi.js +411 -0
- package/lib/foreign/read-qoder.d.ts +54 -0
- package/lib/foreign/read-qoder.js +317 -0
- package/lib/foreign/read-qwen.d.ts +40 -0
- package/lib/foreign/read-qwen.js +326 -0
- package/lib/foreign/read-reasonix.d.ts +66 -0
- package/lib/foreign/read-reasonix.js +537 -0
- package/lib/foreign/read-teleagent.d.ts +15 -0
- package/lib/foreign/read-teleagent.js +68 -0
- package/lib/foreign/read-trae.d.ts +67 -0
- package/lib/foreign/read-trae.js +492 -0
- package/lib/foreign/read-vibe.d.ts +28 -0
- package/lib/foreign/read-vibe.js +364 -0
- package/lib/foreign/read-workbuddy.d.ts +49 -0
- package/lib/foreign/read-workbuddy.js +346 -0
- package/lib/foreign/read-zcode.d.ts +12 -0
- package/lib/foreign/read-zcode.js +38 -0
- package/lib/foreign/read-zed.d.ts +30 -0
- package/lib/foreign/read-zed.js +473 -0
- package/lib/foreign/reasonix.d.ts +6 -0
- package/lib/foreign/reasonix.js +20 -0
- package/lib/foreign/registry.d.ts +196 -0
- package/lib/foreign/registry.js +573 -0
- package/lib/foreign/session-ir.d.ts +186 -0
- package/lib/foreign/session-ir.js +427 -0
- package/lib/foreign/session-read.d.ts +102 -0
- package/lib/foreign/session-read.js +254 -0
- package/lib/foreign/session-source.d.ts +216 -0
- package/lib/foreign/session-source.js +453 -0
- package/lib/foreign/source-modules.d.ts +45 -0
- package/lib/foreign/source-modules.js +52 -0
- package/lib/foreign/sqlite.d.ts +61 -0
- package/lib/foreign/sqlite.js +404 -0
- package/lib/foreign/teleagent.d.ts +11 -0
- package/lib/foreign/teleagent.js +53 -0
- package/lib/foreign/trae.d.ts +6 -0
- package/lib/foreign/trae.js +20 -0
- package/lib/foreign/truth-table.d.ts +62 -0
- package/lib/foreign/truth-table.js +333 -0
- package/lib/foreign/types.d.ts +272 -0
- package/lib/foreign/types.js +2 -0
- package/lib/foreign/vibe.d.ts +6 -0
- package/lib/foreign/vibe.js +23 -0
- package/lib/foreign/workbuddy.d.ts +7 -0
- package/lib/foreign/workbuddy.js +45 -0
- package/lib/foreign/zcode.d.ts +11 -0
- package/lib/foreign/zcode.js +51 -0
- package/lib/foreign/zed.d.ts +11 -0
- package/lib/foreign/zed.js +53 -0
- package/lib/index.d.ts +154 -13
- package/lib/index.js +661 -84
- package/lib/market/github-repos.d.ts +28 -0
- package/lib/market/github-repos.js +43 -1
- package/lib/market/types.d.ts +1 -3
- package/lib/profiles/dsh-profile-manager.d.ts +54 -2
- package/lib/profiles/dsh-profile-manager.js +182 -11
- package/lib/profiles/dsh-profile-shared.d.ts +47 -0
- package/lib/profiles/dsh-profile-shared.js +15 -1
- package/lib/profiles/index.d.ts +1 -1
- package/lib/profiles/index.js +1 -1
- package/lib/routes/consult.js +51 -5
- package/lib/routes/foreign-context.d.ts +43 -0
- package/lib/routes/foreign-context.js +65 -0
- package/lib/routes/foreign.d.ts +11 -0
- package/lib/routes/foreign.js +208 -0
- package/lib/routes/import.d.ts +8 -0
- package/lib/routes/import.js +22 -1
- package/lib/routes/index.d.ts +2 -2
- package/lib/routes/index.js +4 -0
- package/lib/routes/kit.d.ts +4 -3
- package/lib/routes/market.js +0 -1
- package/lib/routes/prefs.d.ts +2 -1
- package/lib/routes/prefs.js +14 -2
- package/lib/routes/recovery.js +363 -3
- package/lib/routes/session-export.d.ts +117 -0
- package/lib/routes/session-export.js +270 -0
- package/lib/routes/snapshots.js +12 -2
- package/lib/routes/sync.js +82 -10
- package/lib/security/redaction.d.ts +14 -0
- package/lib/security/redaction.js +24 -9
- package/lib/security/secret-scanner.d.ts +27 -0
- package/lib/security/secret-scanner.js +54 -1
- package/lib/sync/autosync-scheduler.d.ts +13 -1
- package/lib/sync/autosync-scheduler.js +49 -26
- package/lib/sync/backup-files.js +6 -0
- package/lib/sync/gist/gist-transport.d.ts +91 -0
- package/lib/sync/gist/gist-transport.js +406 -0
- package/lib/sync/gist/messages.d.ts +22 -0
- package/lib/sync/gist/messages.js +46 -0
- package/lib/sync/s3/messages.d.ts +53 -0
- package/lib/sync/s3/messages.js +83 -0
- package/lib/sync/s3/s3-providers.d.ts +78 -0
- package/lib/sync/s3/s3-providers.js +156 -0
- package/lib/sync/s3/s3-transport.d.ts +126 -0
- package/lib/sync/s3/s3-transport.js +551 -0
- package/lib/sync/s3/sigv4.d.ts +107 -0
- package/lib/sync/s3/sigv4.js +196 -0
- package/lib/sync/sync-config.d.ts +250 -4
- package/lib/sync/sync-config.js +658 -57
- package/lib/sync/sync-engine.d.ts +1 -0
- package/lib/sync/sync-engine.js +4 -2
- package/lib/sync/sync-history.d.ts +1 -1
- package/lib/sync/webdav/webdav-transport.d.ts +31 -3
- package/lib/sync/webdav/webdav-transport.js +102 -18
- package/lib/ui/commands.d.ts +4 -3
- package/lib/ui/commands.js +41 -9
- package/lib/ui/disk-usage-view.d.ts +44 -0
- package/lib/ui/disk-usage-view.js +55 -1
- package/lib/ui/dsh-profiles-view.d.ts +53 -0
- package/lib/ui/dsh-profiles-view.js +56 -0
- package/lib/ui/foreign-view.d.ts +162 -0
- package/lib/ui/foreign-view.js +225 -0
- package/lib/ui/i18n.d.ts +77 -0
- package/lib/ui/i18n.js +164 -0
- package/lib/ui/session-inventory-view.d.ts +106 -0
- package/lib/ui/session-inventory-view.js +58 -0
- package/lib/ui/session-layout-view.d.ts +265 -0
- package/lib/ui/session-layout-view.js +271 -0
- package/lib/ui/sync-repository-picker.d.ts +98 -0
- package/lib/ui/sync-repository-picker.js +145 -0
- package/lib/ui/sync-settings-view.d.ts +143 -4
- package/lib/ui/sync-settings-view.js +231 -7
- package/lib/ui/types.d.ts +47 -0
- package/lib/utils/asar-read.d.ts +113 -0
- package/lib/utils/asar-read.js +523 -0
- package/lib/utils/env-lock.js +7 -2
- package/lib/utils/patch-yaml.d.ts +25 -0
- package/lib/utils/patch-yaml.js +62 -0
- package/lib/utils/paths.d.ts +10 -1
- package/lib/utils/paths.js +24 -3
- package/lib/utils/recursive-walk.js +12 -13
- package/lib/utils/session-format.d.ts +35 -2
- package/lib/utils/session-format.js +131 -5
- package/lib/utils/session-health-scan.d.ts +7 -1
- package/lib/utils/session-health-scan.js +369 -12
- package/lib/utils/session-layout-repair-service.d.ts +98 -0
- package/lib/utils/session-layout-repair-service.js +457 -0
- package/lib/utils/session-log-repair.d.ts +27 -0
- package/lib/utils/session-log-repair.js +233 -62
- package/lib/utils/session-repair-service.d.ts +32 -3
- package/lib/utils/session-repair-service.js +148 -9
- package/lib/utils/session-row-facts.d.ts +73 -0
- package/lib/utils/session-row-facts.js +182 -0
- package/lib/utils/session-verify.d.ts +118 -0
- package/lib/utils/session-verify.js +705 -0
- package/lib/utils/shared-constants.d.ts +59 -0
- package/lib/utils/shared-constants.js +71 -0
- package/package.json +1 -1
- package/src/adapters/credentials.test.ts +27 -0
- package/src/adapters/credentials.ts +6 -3
- package/src/adapters/file-collection.ts +33 -8
- package/src/adapters/files.test.ts +222 -0
- package/src/adapters/mcp.test.ts +43 -0
- package/src/adapters/mcp.ts +37 -14
- package/src/adapters/plugin-files.ts +34 -4
- package/src/adapters/plugins.test.ts +241 -0
- package/src/adapters/plugins.ts +201 -53
- package/src/adapters/prompts.test.ts +49 -0
- package/src/adapters/prompts.ts +41 -20
- package/src/adapters/providers.test.ts +16 -0
- package/src/adapters/providers.ts +4 -3
- package/src/adapters/self.test.ts +77 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.test.ts +77 -0
- package/src/adapters/sessions.ts +47 -1
- package/src/adapters/settings.test.ts +78 -0
- package/src/adapters/settings.ts +42 -6
- package/src/adapters/skills.ts +221 -3
- package/src/adapters/test-helpers.ts +43 -5
- package/src/adapters/workspaces-dup-id.test.ts +144 -0
- package/src/adapters/workspaces.test.ts +10 -0
- package/src/adapters/workspaces.ts +97 -5
- package/src/cli/actions.test.ts +117 -2
- package/src/cli/actions.ts +132 -12
- package/src/cli/help.ts +570 -0
- package/src/cli/import-source.test.ts +193 -0
- package/src/cli/import-source.ts +229 -0
- package/src/cli/index.ts +129 -94
- package/src/cli/web/client-script.ts +12 -0
- package/src/cli/web/console-fixes.test.ts +36 -0
- package/src/cli/web/http.ts +7 -1
- package/src/cli/web/page.ts +108 -25
- package/src/cli/web/routes.ts +23 -4
- package/src/cli/web/script-anchor.test.ts +33 -0
- package/src/cli/web/server.ts +28 -2
- package/src/cli/web/web.test.ts +447 -4
- package/src/client/ConfigManagerSection.tsx +118 -63
- package/src/client/about/AboutPanel.tsx +42 -3
- package/src/client/about/PluginUpdateDialog.tsx +185 -0
- package/src/client/about/ReleaseBody.tsx +144 -0
- package/src/client/about/ReleaseNotesDialog.tsx +3 -130
- package/src/client/about/about-view.ts +7 -5
- package/src/client/about/plugin-update-view.test.ts +25 -0
- package/src/client/about/plugin-update-view.ts +26 -0
- package/src/client/about/release-notes-view.test.ts +33 -0
- package/src/client/about/release-notes-view.ts +34 -0
- package/src/client/api.ts +145 -1
- package/src/client/common/Icon.tsx +3 -0
- package/src/client/common/RunsCenter.tsx +8 -2
- package/src/client/common/Select.tsx +9 -2
- package/src/client/common/info-hint-guard.test.ts +63 -18
- package/src/client/common/plan-text-redaction.test.ts +193 -8
- package/src/client/common/routes.ts +15 -0
- package/src/client/config-manager.module.css +506 -354
- package/src/client/css-scale-guard.test.ts +122 -0
- package/src/client/css-token-guard.test.ts +82 -0
- package/src/client/environment/EnvironmentPanel.tsx +45 -16
- package/src/client/{snapshots → environment/maintenance}/DiskUsageCard.tsx +21 -17
- package/src/client/environment/profiles-incomplete-ui.test.ts +103 -0
- package/src/client/export/ExportView.tsx +9 -2
- package/src/client/{snapshots → home}/BackupScheduleCard.tsx +13 -4
- package/src/client/home/HomePanel.tsx +69 -157
- package/src/client/icon-layer-guard.test.ts +1 -1
- package/src/client/import/ForeignImportView.tsx +318 -0
- package/src/client/import/ImportWizardView.tsx +3 -1
- package/src/client/import/foreign-import-view-guard.test.ts +195 -0
- package/src/client/import/import-file-select.test.ts +6 -6
- package/src/client/import/import-wizard-steps.tsx +17 -1
- package/src/client/import/use-import-wizard-controller.ts +3 -3
- package/src/client/index.ts +1 -1
- package/src/client/library/LibraryActions.tsx +31 -34
- package/src/client/library/LibraryPanel.tsx +4 -1
- package/src/client/{snapshots → library}/RestorePlanView.tsx +1 -1
- package/src/client/{snapshots → library}/files-layout.test.ts +2 -2
- package/src/client/{snapshots → library}/report-dismiss.test.ts +26 -3
- package/src/client/library/task-panel-visibility.test.ts +25 -17
- package/src/client/locales.ts +278 -36
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +11 -2
- package/src/client/recovery/IncompleteCopiesSection.tsx +132 -0
- package/src/client/recovery/RecoveryPanel.tsx +486 -59
- package/src/client/recovery/recovery-api.ts +30 -0
- package/src/client/recovery/recovery-incomplete-copies.test.ts +151 -0
- package/src/client/recovery/recovery-locales.test.ts +88 -0
- package/src/client/recovery/recovery-locales.ts +321 -6
- package/src/client/recovery/recovery-panel-layout.test.ts +52 -0
- package/src/client/recovery/recovery-view.ts +92 -2
- package/src/client/run-store.test.ts +201 -89
- package/src/client/run-store.ts +114 -143
- package/src/client/session-export/session-export-view.test.ts +103 -0
- package/src/client/session-export/session-export-view.ts +118 -0
- package/src/client/sync/ChannelConfigDialog.tsx +219 -14
- package/src/client/sync/SyncChannelEntryCard.tsx +52 -4
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +166 -102
- package/src/client/sync/SyncRepositoryPicker.tsx +216 -0
- package/src/client/sync/SyncSettingsView.test.ts +6 -1
- package/src/client/sync/history-model.test.ts +94 -57
- package/src/client/sync/history-model.ts +44 -25
- package/src/client/sync/sync-api.test.ts +56 -0
- package/src/client/sync/sync-api.ts +92 -0
- package/src/client/sync/sync-locales.ts +162 -2
- package/src/client/sync/sync-view.test.ts +3 -8
- package/src/client/sync/sync-view.ts +111 -21
- package/src/core/analyzer-cache.test.ts +47 -0
- package/src/core/analyzer.ts +115 -14
- package/src/core/backup-plan.test.ts +35 -0
- package/src/core/backup-plan.ts +19 -1
- package/src/core/backup.test.ts +37 -1
- package/src/core/backup.ts +27 -6
- package/src/core/cache-cleaner.test.ts +120 -1
- package/src/core/cache-cleaner.ts +59 -22
- package/src/core/checkpoint.test.ts +665 -0
- package/src/core/checkpoint.ts +1614 -0
- package/src/core/credential-plan-consistency.test.ts +93 -0
- package/src/core/incident-wiring.test.ts +6 -2
- package/src/core/journal.test.ts +70 -0
- package/src/core/journal.ts +68 -0
- package/src/core/messages.ts +26 -0
- package/src/core/model-tools.ts +3 -0
- package/src/core/patch-layers.test.ts +94 -0
- package/src/core/patch-layers.ts +150 -0
- package/src/core/restore.ts +59 -3
- package/src/core/rollback.ts +6 -3
- package/src/core/section-content-consistency.test.ts +185 -0
- package/src/core/self-update.test.ts +110 -0
- package/src/core/self-update.ts +118 -0
- package/src/core/session-health.test.ts +91 -1
- package/src/core/session-health.ts +56 -9
- package/src/core/types.ts +127 -1
- package/src/foreign/antigravity.test.ts +271 -0
- package/src/foreign/antigravity.ts +188 -0
- package/src/foreign/bundle.ts +108 -0
- package/src/foreign/chatgpt.test.ts +279 -0
- package/src/foreign/chatgpt.ts +30 -0
- package/src/foreign/claude-code.test.ts +234 -0
- package/src/foreign/claude-code.ts +106 -0
- package/src/foreign/claude-sessions-bytes.test.ts +227 -0
- package/src/foreign/claude-sessions.test.ts +180 -0
- package/src/foreign/claude-sessions.ts +461 -0
- package/src/foreign/cline.test.ts +274 -0
- package/src/foreign/cline.ts +32 -0
- package/src/foreign/codex.test.ts +585 -0
- package/src/foreign/codex.ts +409 -0
- package/src/foreign/continue.test.ts +365 -0
- package/src/foreign/continue.ts +73 -0
- package/src/foreign/copilot.test.ts +273 -0
- package/src/foreign/copilot.ts +133 -0
- package/src/foreign/crush.test.ts +234 -0
- package/src/foreign/crush.ts +66 -0
- package/src/foreign/cursor.test.ts +509 -0
- package/src/foreign/cursor.ts +259 -0
- package/src/foreign/dsh.test.ts +198 -0
- package/src/foreign/dsh.ts +117 -0
- package/src/foreign/dsh4.test.ts +127 -0
- package/src/foreign/dsh4.ts +15 -0
- package/src/foreign/file-budget.test.ts +256 -0
- package/src/foreign/fixtures/antigravity/README.md +18 -0
- package/src/foreign/fixtures/antigravity/basic/gemini/antigravity/mcp_config.json +13 -0
- package/src/foreign/fixtures/antigravity/basic/gemini/config/mcp_config.json +24 -0
- package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_config.json +0 -0
- package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_oauth_tokens.json +0 -0
- package/src/foreign/fixtures/antigravity/empty/gemini/config/mcp_config.json +0 -0
- package/src/foreign/fixtures/codex/README.md +31 -0
- package/src/foreign/fixtures/codex/basic/.agents/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/codex/basic/.agents/skills/wrapper/nested/SKILL.md +6 -0
- package/src/foreign/fixtures/codex/basic/.codex/AGENTS.md +4 -0
- package/src/foreign/fixtures/codex/basic/.codex/config.toml +28 -0
- package/src/foreign/fixtures/codex/malformed/.codex/config.toml +3 -0
- package/src/foreign/fixtures/codex/override/.codex/AGENTS.md +1 -0
- package/src/foreign/fixtures/codex/override/.codex/AGENTS.override.md +3 -0
- package/src/foreign/fixtures/codex/sessions/.codex/archived_sessions/rollout-2026-10-05T09-00-00-22222222-2222-4222-8222-222222222222.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T10-00-00-11111111-1111-4111-8111-111111111111.jsonl +15 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-00-00-33333333-3333-4333-8333-333333333333.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-30-00-55555555-5555-4555-8555-555555555555.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T12-00-00-44444444-4444-4444-8444-444444444444.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/notes.jsonl +1 -0
- package/src/foreign/fixtures/copilot/README.md +22 -0
- package/src/foreign/fixtures/copilot/basic/copilot-instructions.md +3 -0
- package/src/foreign/fixtures/copilot/basic/instructions/empty.instructions.md +1 -0
- package/src/foreign/fixtures/copilot/basic/instructions/repo.instructions.md +5 -0
- package/src/foreign/fixtures/copilot/basic/mcp-config.json +24 -0
- package/src/foreign/fixtures/copilot/basic/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/copilot/overridden/mcp-config.json +10 -0
- package/src/foreign/fixtures/cursor/README.md +17 -0
- package/src/foreign/fixtures/cursor/alt/.cursor/mcp.json +5 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/mcp.json +18 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/agent.mdc +5 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/always.mdc +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/auto.mdc +7 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/manual.mdc +3 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/.hidden/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/notes.md +1 -0
- package/src/foreign/fixtures/cursor/basic/.cursorrules +1 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursor/mcp.json +9 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursor/rules/project-always.mdc +5 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursor/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursorrules +1 -0
- package/src/foreign/fixtures/cursor/empty/.cursor/mcp.json +0 -0
- package/src/foreign/fixtures/cursor/malformed/.cursor/mcp.json +3 -0
- package/src/foreign/fixtures/cursor/malformed/.cursor/rules/broken.mdc +6 -0
- package/src/foreign/fixtures/cursor/shape/.cursor/mcp.json +3 -0
- package/src/foreign/fixtures/hermes/README.md +14 -0
- package/src/foreign/fixtures/hermes/basic/SOUL.md +3 -0
- package/src/foreign/fixtures/hermes/basic/config.yaml +48 -0
- package/src/foreign/fixtures/hermes/basic/dotenv.fixture +1 -0
- package/src/foreign/fixtures/hermes/basic/memories/MEMORY.md +1 -0
- package/src/foreign/fixtures/hermes/basic/memories/USER.md +1 -0
- package/src/foreign/fixtures/hermes/basic/skills/se-team-design/SKILL.md +6 -0
- package/src/foreign/fixtures/hermes/basic/skills/se-team-design/examples/usage.md +1 -0
- package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/SKILL.md +6 -0
- package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/notes.md +1 -0
- package/src/foreign/fixtures/hermes/basic/state.db +1 -0
- package/src/foreign/fixtures/hermes/empty/config.yaml +0 -0
- package/src/foreign/gemini.test.ts +291 -0
- package/src/foreign/gemini.ts +52 -0
- package/src/foreign/goose.test.ts +196 -0
- package/src/foreign/goose.ts +62 -0
- package/src/foreign/grokbuild.test.ts +246 -0
- package/src/foreign/grokbuild.ts +30 -0
- package/src/foreign/hermes.test.ts +545 -0
- package/src/foreign/hermes.ts +86 -0
- package/src/foreign/kernel-credentials.test.ts +88 -0
- package/src/foreign/kernel-workspaces.test.ts +88 -0
- package/src/foreign/kernel.ts +584 -0
- package/src/foreign/kilocode.test.ts +186 -0
- package/src/foreign/kilocode.ts +60 -0
- package/src/foreign/kimi.test.ts +259 -0
- package/src/foreign/kimi.ts +55 -0
- package/src/foreign/mcp-value-shape.test.ts +286 -0
- package/src/foreign/mimocode.test.ts +178 -0
- package/src/foreign/mimocode.ts +60 -0
- package/src/foreign/openclaw.test.ts +248 -0
- package/src/foreign/openclaw.ts +54 -0
- package/src/foreign/opencode.test.ts +376 -0
- package/src/foreign/opencode.ts +61 -0
- package/src/foreign/pi.test.ts +275 -0
- package/src/foreign/pi.ts +56 -0
- package/src/foreign/platform-paths.ts +156 -0
- package/src/foreign/qoder.test.ts +266 -0
- package/src/foreign/qoder.ts +57 -0
- package/src/foreign/qwen.test.ts +203 -0
- package/src/foreign/qwen.ts +53 -0
- package/src/foreign/read-antigravity.ts +569 -0
- package/src/foreign/read-chatgpt.ts +506 -0
- package/src/foreign/read-claude-code.ts +264 -0
- package/src/foreign/read-cline.ts +517 -0
- package/src/foreign/read-codex.ts +823 -0
- package/src/foreign/read-continue.ts +557 -0
- package/src/foreign/read-copilot.ts +304 -0
- package/src/foreign/read-crush.ts +503 -0
- package/src/foreign/read-cursor.ts +778 -0
- package/src/foreign/read-dsh.ts +215 -0
- package/src/foreign/read-dsh4.ts +35 -0
- package/src/foreign/read-gemini.ts +306 -0
- package/src/foreign/read-goose.ts +333 -0
- package/src/foreign/read-grokbuild.ts +475 -0
- package/src/foreign/read-hermes.ts +724 -0
- package/src/foreign/read-kilocode.ts +55 -0
- package/src/foreign/read-kimi.ts +768 -0
- package/src/foreign/read-mimocode.ts +66 -0
- package/src/foreign/read-openclaw.ts +505 -0
- package/src/foreign/read-opencode.ts +994 -0
- package/src/foreign/read-pi.ts +459 -0
- package/src/foreign/read-qoder.ts +351 -0
- package/src/foreign/read-qwen.ts +342 -0
- package/src/foreign/read-reasonix.ts +590 -0
- package/src/foreign/read-teleagent.ts +82 -0
- package/src/foreign/read-trae.ts +488 -0
- package/src/foreign/read-vibe.ts +353 -0
- package/src/foreign/read-workbuddy.ts +374 -0
- package/src/foreign/read-zcode.ts +48 -0
- package/src/foreign/read-zed.ts +479 -0
- package/src/foreign/reasonix.test.ts +336 -0
- package/src/foreign/reasonix.ts +25 -0
- package/src/foreign/registry.test.ts +377 -0
- package/src/foreign/registry.ts +750 -0
- package/src/foreign/session-ir.test.ts +189 -0
- package/src/foreign/session-ir.ts +614 -0
- package/src/foreign/session-read.ts +295 -0
- package/src/foreign/session-source.ts +625 -0
- package/src/foreign/skill-file-caps.test.ts +171 -0
- package/src/foreign/source-modules.ts +86 -0
- package/src/foreign/source-registry.test.ts +190 -0
- package/src/foreign/sqlite.test.ts +313 -0
- package/src/foreign/sqlite.ts +446 -0
- package/src/foreign/teleagent.test.ts +147 -0
- package/src/foreign/teleagent.ts +62 -0
- package/src/foreign/trae.test.ts +316 -0
- package/src/foreign/trae.ts +25 -0
- package/src/foreign/truncation-report.test.ts +238 -0
- package/src/foreign/truth-table.ts +453 -0
- package/src/foreign/types.ts +312 -0
- package/src/foreign/vibe.test.ts +209 -0
- package/src/foreign/vibe.ts +28 -0
- package/src/foreign/workbuddy.test.ts +207 -0
- package/src/foreign/workbuddy.ts +53 -0
- package/src/foreign/zcode.test.ts +224 -0
- package/src/foreign/zcode.ts +60 -0
- package/src/foreign/zed.test.ts +240 -0
- package/src/foreign/zed.ts +62 -0
- package/src/index.facade.test.ts +254 -16
- package/src/index.ts +704 -82
- package/src/market/github-repos.test.ts +98 -1
- package/src/market/github-repos.ts +63 -1
- package/src/market/types.ts +3 -3
- package/src/profiles/dsh-profile-incomplete-copy.test.ts +177 -0
- package/src/profiles/dsh-profile-manager.test.ts +37 -0
- package/src/profiles/dsh-profile-manager.ts +188 -13
- package/src/profiles/dsh-profile-shared.ts +55 -1
- package/src/profiles/index.ts +2 -1
- package/src/routes/consult.ts +53 -4
- package/src/routes/foreign-context.ts +89 -0
- package/src/routes/foreign.test.ts +443 -0
- package/src/routes/foreign.ts +226 -0
- package/src/routes/import.ts +23 -1
- package/src/routes/index.ts +6 -2
- package/src/routes/kit.ts +4 -3
- package/src/routes/market.ts +0 -1
- package/src/routes/prefs.ts +15 -1
- package/src/routes/recovery.test.ts +167 -0
- package/src/routes/recovery.ts +331 -1
- package/src/routes/session-export.ts +308 -0
- package/src/routes/snapshots.ts +12 -2
- package/src/routes/sync.ts +91 -7
- package/src/security/redaction.test.ts +149 -0
- package/src/security/redaction.ts +25 -9
- package/src/security/secret-scanner.test.ts +186 -0
- package/src/security/secret-scanner.ts +57 -1
- package/src/sync/autosync-risk-tier.test.ts +61 -0
- package/src/sync/autosync-scheduler.test.ts +48 -1
- package/src/sync/autosync-scheduler.ts +50 -26
- package/src/sync/backup-files.ts +5 -0
- package/src/sync/gist/gist-msgkeys-catalog.test.ts +33 -0
- package/src/sync/gist/gist-msgkeys.test.ts +71 -0
- package/src/sync/gist/gist-transport.test.ts +308 -0
- package/src/sync/gist/gist-transport.ts +550 -0
- package/src/sync/gist/messages.ts +49 -0
- package/src/sync/retention-prune-all-off.test.ts +50 -0
- package/src/sync/s3/cloud-config.test.ts +283 -0
- package/src/sync/s3/messages.ts +102 -0
- package/src/sync/s3/s3-delete-index-404.test.ts +170 -0
- package/src/sync/s3/s3-delete-index-unreadable.test.ts +141 -0
- package/src/sync/s3/s3-msgkeys.test.ts +101 -0
- package/src/sync/s3/s3-providers.ts +213 -0
- package/src/sync/s3/s3-transport.test.ts +394 -0
- package/src/sync/s3/s3-transport.ts +651 -0
- package/src/sync/s3/s3-upload-index-404.test.ts +171 -0
- package/src/sync/s3/sigv4.test.ts +219 -0
- package/src/sync/s3/sigv4.ts +283 -0
- package/src/sync/sync-config.test.ts +237 -5
- package/src/sync/sync-config.ts +841 -52
- package/src/sync/sync-engine-download-tombstone.test.ts +73 -0
- package/src/sync/sync-engine.ts +5 -3
- package/src/sync/sync-history.ts +1 -1
- package/src/sync/webdav/webdav-delete-index-404.test.ts +178 -0
- package/src/sync/webdav/webdav-delete-index-unreadable.test.ts +143 -0
- package/src/sync/webdav/webdav-msgkeys.test.ts +53 -0
- package/src/sync/webdav/webdav-transport.test.ts +9 -5
- package/src/sync/webdav/webdav-transport.ts +99 -17
- package/src/sync/webdav/webdav-upload-index-404.test.ts +177 -0
- package/src/ui/commands.test.ts +4 -1
- package/src/ui/commands.ts +46 -12
- package/src/ui/disk-usage-view.test.ts +63 -1
- package/src/ui/disk-usage-view.ts +93 -1
- package/src/ui/dsh-profiles-incomplete-view.test.ts +102 -0
- package/src/ui/dsh-profiles-view.ts +94 -0
- package/src/ui/foreign-view.test.ts +241 -0
- package/src/ui/foreign-view.ts +362 -0
- package/src/ui/i18n.ts +164 -0
- package/src/ui/mid-ellipsis.test.ts +5 -10
- package/src/ui/session-inventory-view.test.ts +142 -2
- package/src/ui/session-inventory-view.ts +127 -0
- package/src/ui/session-layout-view.test.ts +361 -0
- package/src/ui/session-layout-view.ts +444 -0
- package/src/ui/sync-repository-picker.test.ts +130 -0
- package/src/ui/sync-repository-picker.ts +180 -0
- package/src/ui/sync-settings-view.test.ts +166 -0
- package/src/ui/sync-settings-view.ts +319 -9
- package/src/ui/types.ts +42 -0
- package/src/utils/asar-read.test.ts +330 -0
- package/src/utils/asar-read.ts +606 -0
- package/src/utils/env-lock.test.ts +55 -0
- package/src/utils/env-lock.ts +6 -1
- package/src/utils/patch-yaml.test.ts +112 -0
- package/src/utils/patch-yaml.ts +72 -0
- package/src/utils/paths.test.ts +63 -0
- package/src/utils/paths.ts +25 -3
- package/src/utils/recursive-walk.test.ts +20 -0
- package/src/utils/recursive-walk.ts +12 -13
- package/src/utils/session-format.test.ts +113 -2
- package/src/utils/session-format.ts +127 -5
- package/src/utils/session-health-scan.test.ts +364 -1
- package/src/utils/session-health-scan.ts +434 -15
- package/src/utils/session-layout-repair-service.test.ts +362 -0
- package/src/utils/session-layout-repair-service.ts +568 -0
- package/src/utils/session-log-repair.test.ts +260 -1
- package/src/utils/session-log-repair.ts +294 -55
- package/src/utils/session-repair-service.test.ts +148 -1
- package/src/utils/session-repair-service.ts +165 -10
- package/src/utils/session-row-facts.test.ts +159 -0
- package/src/utils/session-row-facts.ts +187 -0
- package/src/utils/session-verify.test.ts +762 -0
- package/src/utils/session-verify.ts +784 -0
- package/src/utils/shared-constants.ts +82 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/dsh-config-manager/foreign-sources` 的行为护栏(t22)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一条测试:这条路由是 GUI 里「本机装了哪几个外部 agent」的唯一数据源,
|
|
5
|
+
* 它的失败模式是**静默的**——少一条来源、回传绝对路径、把 projectDir 丢掉,界面上都看不出来。
|
|
6
|
+
*/
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
13
|
+
|
|
14
|
+
import { foreignRoutes } from './foreign.ts';
|
|
15
|
+
import { routeSpecOf } from './kit.ts';
|
|
16
|
+
import {
|
|
17
|
+
foreignSourceContext,
|
|
18
|
+
isInsideStaging,
|
|
19
|
+
} from './foreign-context.ts';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 最小宿主 env 桩:只提供 foreign 路由真正消费的四个字段。
|
|
23
|
+
*
|
|
24
|
+
* 为什么显式给:真实 `RoutesEnv` 由 `src/index.ts` 的 `makeRouteEnv()` 推断,
|
|
25
|
+
* 全量构造需要一个完整 DSH 宿主。这里只放本组用到的字段 —— 组文件在构建期只解构 env,
|
|
26
|
+
* 其余依赖都在 handler 内,所以桩足够驱动真实行为(与 route-fence 的 stubEnv 同思路)。
|
|
27
|
+
*/
|
|
28
|
+
async function stubEnv(sessionFormatVersion?: number): Promise<Record<string, unknown>> {
|
|
29
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-foreign-route-'));
|
|
30
|
+
return {
|
|
31
|
+
tmpDir,
|
|
32
|
+
roots: [tmpDir],
|
|
33
|
+
/**
|
|
34
|
+
* **host.homeDir 刻意设成一个「像 \$DSH_HOME 但不是用户 home」的路径**。
|
|
35
|
+
*
|
|
36
|
+
* 这是防回归的关键:宿主 context 的 homeDir 是 \$DSH_HOME(≈ ~/.dsh),而来源真值表
|
|
37
|
+
* 全部相对**用户 home**。此前 POST 用 host.homeDir 拼路径 → 去找 ~/.dsh/.claude(全读不到)→
|
|
38
|
+
* 稳定回 400 nothing-to-import,而 GET 用 os.homedir() 显示 found=true —— **两个接口自相矛盾**。
|
|
39
|
+
* 若测试桩把 host.homeDir 设成 os.homedir(),这个缺陷就会被**掩盖**(两边恰好同值)。
|
|
40
|
+
* 故此处故意给一个不可能存在的目录:任何误用 host.homeDir 的实现都会立刻红。
|
|
41
|
+
*/
|
|
42
|
+
host: {
|
|
43
|
+
homeDir: path.join(tmpDir, 'dsh-home-NOT-user-home'),
|
|
44
|
+
dshVersion: 'test',
|
|
45
|
+
// 缺省 = 读不到目标版本(内核据此逐条报 session-format-version-unknown,绝不猜)
|
|
46
|
+
sessionFormatVersion,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fakeRequest(url: string): IncomingMessage {
|
|
52
|
+
const req = {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
url,
|
|
55
|
+
socket: { remoteAddress: '127.0.0.1' },
|
|
56
|
+
headers: { host: '127.0.0.1:3080' },
|
|
57
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<Buffer> {
|
|
58
|
+
// GET 无体
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
return req as unknown as IncomingMessage;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface FakeResponse {
|
|
65
|
+
res: ServerResponse;
|
|
66
|
+
status: number;
|
|
67
|
+
body: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fakeResponse(): FakeResponse {
|
|
71
|
+
const state = { status: 0, body: '' };
|
|
72
|
+
const res = {
|
|
73
|
+
headersSent: false,
|
|
74
|
+
writeHead(status: number) {
|
|
75
|
+
state.status = status;
|
|
76
|
+
this.headersSent = true;
|
|
77
|
+
return this;
|
|
78
|
+
},
|
|
79
|
+
end(payload?: string) {
|
|
80
|
+
state.body = payload ?? '';
|
|
81
|
+
return this;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
res: res as unknown as ServerResponse,
|
|
86
|
+
get status() { return state.status; },
|
|
87
|
+
get body() { return state.body; },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ForeignSourcesBody {
|
|
92
|
+
conflictPolicy: string;
|
|
93
|
+
projectScoped: boolean;
|
|
94
|
+
sources: { id: string; labelKey: string; found: boolean; paths: string[]; skipped: unknown[] }[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function callRoute(url: string): Promise<{ status: number; body: ForeignSourcesBody; raw: string }> {
|
|
98
|
+
const routes = foreignRoutes({} as never);
|
|
99
|
+
assert.equal(routes.length, 2, '本组应恰好两条路由(发现 + 产包)');
|
|
100
|
+
const route = routes[0]!;
|
|
101
|
+
assert.notEqual(routeSpecOf(route), undefined, '必须经 kit 的 endpoint() 声明(不得裸注册)');
|
|
102
|
+
const res = fakeResponse();
|
|
103
|
+
await route.handler(fakeRequest(url), res.res);
|
|
104
|
+
return { status: res.status, body: JSON.parse(res.body) as ForeignSourcesBody, raw: res.body };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test('foreign-sources:30 条来源的检测结果齐全、冲突策略可见、绝不回传绝对路径', async () => {
|
|
108
|
+
const { status, body, raw } = await callRoute('/api/dsh-config-manager/foreign-sources');
|
|
109
|
+
assert.equal(status, 200);
|
|
110
|
+
assert.equal(body.conflictPolicy, 'skip-no-overwrite');
|
|
111
|
+
assert.equal(body.projectScoped, false, '未显式给 projectDir 时绝不猜「项目 = 宿主 cwd」');
|
|
112
|
+
assert.deepEqual(
|
|
113
|
+
body.sources.map((s) => s.id),
|
|
114
|
+
[
|
|
115
|
+
'claude-code', 'hermes', 'cursor', 'codex', 'copilot', 'antigravity',
|
|
116
|
+
'gemini', 'reasonix', 'opencode', 'mimocode', 'zcode', 'grokbuild', 'openclaw', 'pi',
|
|
117
|
+
'kimi', 'kilocode', 'qoder', 'chatgpt', 'workbuddy', 'qwen', 'continue', 'cline',
|
|
118
|
+
'goose', 'dsh4', 'zed', 'crush', 'teleagent', 'trae', 'vibe', 'dsh',
|
|
119
|
+
],
|
|
120
|
+
'30 个来源一个都不能少、顺序稳定',
|
|
121
|
+
);
|
|
122
|
+
for (const source of body.sources) {
|
|
123
|
+
assert.equal(typeof source.found, 'boolean', source.id + ' 的 found 必须是布尔');
|
|
124
|
+
assert.ok(Array.isArray(source.paths), source.id + ' 的 paths 必须是数组');
|
|
125
|
+
assert.ok(Array.isArray(source.skipped), source.id + ' 的 skipped 必须是数组');
|
|
126
|
+
for (const p of source.paths) {
|
|
127
|
+
assert.ok(!path.isAbsolute(p), source.id + ' 的路径必须是相对位置,实际: ' + p);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
assert.ok(!raw.includes(os.homedir().replace(/\\/g, '\\\\')), '响应体绝不包含用户 home 绝对路径');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('foreign-sources:?projectDir= 被真正传给来源(项目级真值位置进入检测面)', async () => {
|
|
134
|
+
const project = path.join(import.meta.dirname, '..', 'foreign', 'fixtures', 'cursor', 'basic', 'project');
|
|
135
|
+
const { status, body } = await callRoute(
|
|
136
|
+
'/api/dsh-config-manager/foreign-sources?projectDir=' + encodeURIComponent(project),
|
|
137
|
+
);
|
|
138
|
+
assert.equal(status, 200);
|
|
139
|
+
assert.equal(body.projectScoped, true, '显式给了 projectDir 必须如实标注');
|
|
140
|
+
const cursor = body.sources.find((s) => s.id === 'cursor');
|
|
141
|
+
assert.ok(cursor !== undefined);
|
|
142
|
+
assert.ok(cursor.paths.includes('project/.cursor/mcp.json'), '项目级 mcp.json 必须出现在检测面里');
|
|
143
|
+
assert.ok(cursor.paths.includes('project/.cursor/rules'), '项目级 rules 必须出现在检测面里');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/* ---------------- foreign-context(纯函数:三处调用点共用同一份构造) ----------------
|
|
147
|
+
|
|
148
|
+
两个缺陷的防回归护栏(都由 captain 复验确认):
|
|
149
|
+
① 缺陷 1(严重):POST 曾用 \`host.homeDir\`(= \$DSH_HOME ≈ ~/.dsh)当来源基准,
|
|
150
|
+
而来源真值表相对**用户 home** → POST 恒读不到 → 400 nothing-to-import,
|
|
151
|
+
与 GET(用 os.homedir())显示 found=true **自相矛盾**;
|
|
152
|
+
② 缺陷 2:路由模块曾 \`import { PLUGIN_VERSION } from '../index.ts'\` → 任何直接
|
|
153
|
+
import 本模块的单测都会连带加载 @deepseek-ai/* 而 ERR_MODULE_NOT_FOUND。
|
|
154
|
+
本文件现在能跑起来,本身就是缺陷 2 已修的证明(import 链里已无 src/index.ts)。 */
|
|
155
|
+
|
|
156
|
+
test('foreign-context:来源上下文恒以**用户 home** 为基准,绝不取宿主 homeDir(缺陷 1 防回归)', () => {
|
|
157
|
+
const ctx = foreignSourceContext()
|
|
158
|
+
// 不传 userHome → 回退 os.homedir()(= 来源真值表的基准)
|
|
159
|
+
assert.equal(ctx.homeDir, os.homedir(), '缺省必须是用户 home,不是 $DSH_HOME')
|
|
160
|
+
// 显式传入优先
|
|
161
|
+
assert.equal(foreignSourceContext({ userHome: 'C:/users/x' }).homeDir, 'C:/users/x')
|
|
162
|
+
// 空串视为未给(不产生空 home → 不拼出相对路径)
|
|
163
|
+
assert.equal(foreignSourceContext({ userHome: '' }).homeDir, os.homedir(), '空串必须回退,不得产出空 homeDir')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('foreign-context:projectDir / targetSessionFormatVersion 缺省时**该键不出现**(绝不猜)', () => {
|
|
167
|
+
const bare = foreignSourceContext()
|
|
168
|
+
assert.equal('projectDir' in bare, false, '未给 projectDir → 键不出现(内核据此不扫项目级,而不是拿空串拼路径)')
|
|
169
|
+
assert.equal('targetSessionFormatVersion' in bare, false, '未给版本 → 键不出现(内核逐条报码,绝不擅自填默认版本)')
|
|
170
|
+
assert.equal(foreignSourceContext({ projectDir: '' }).projectDir, undefined, '空串 = 未给')
|
|
171
|
+
const full = foreignSourceContext({ projectDir: '/proj', targetSessionFormatVersion: 4 })
|
|
172
|
+
assert.equal(full.projectDir, '/proj')
|
|
173
|
+
assert.equal(full.targetSessionFormatVersion, 4)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('foreign-context:受控目录自检用段边界,/tmp-evil 不得通过 /tmp(防前缀误判)', () => {
|
|
177
|
+
const root = path.resolve('/tmp/dcm-staging')
|
|
178
|
+
assert.equal(isInsideStaging(path.join(root, 'a.zip'), [root]), true)
|
|
179
|
+
assert.equal(isInsideStaging(root, [root]), true, '根目录自身算在内')
|
|
180
|
+
assert.equal(isInsideStaging(root + '-evil/a.zip', [root]), false, '同前缀的另一目录不得通过')
|
|
181
|
+
assert.equal(isInsideStaging(path.resolve('/elsewhere/a.zip'), [root]), false)
|
|
182
|
+
assert.equal(isInsideStaging(path.join(root, '..', 'escape.zip'), [root]), false, '上跳后不在根内')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('foreign-context:模块 import 链里不得出现 src/index.ts(缺陷 2 防回归)', async () => {
|
|
186
|
+
// 直接断言「本测试文件能 import 成功」还不够 —— 要钉住**成因**:
|
|
187
|
+
// 路由实现模块不得把宿主入口拉进 import 链。
|
|
188
|
+
const src = await fs.readFile(path.join(import.meta.dirname, 'foreign.ts'), 'utf8')
|
|
189
|
+
assert.ok(
|
|
190
|
+
!/from '\.\.\/index\.ts'/.test(src),
|
|
191
|
+
'src/routes/foreign.ts 不得 import ../index.ts(会把 @deepseek-ai/* 拉进纯单测环境)',
|
|
192
|
+
)
|
|
193
|
+
assert.ok(
|
|
194
|
+
src.includes("from './foreign-context.ts'"),
|
|
195
|
+
'必须经纯函数模块构造来源上下文(三处调用点同源的唯一实现)',
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
/* ---------------- POST /foreign-import(t17:GUI 侧的产包端点) ----------------
|
|
200
|
+
|
|
201
|
+
为什么必须真跑:这条端点是「GUI 从选来源到拿到 zipPath」之间**唯一**的一环,
|
|
202
|
+
它坏了的表现是「点导入没反应 / 报一句看不懂的错」。以下断言各自对应一种真实失败模式。 */
|
|
203
|
+
|
|
204
|
+
interface ForeignImportBody {
|
|
205
|
+
zipPath?: string
|
|
206
|
+
name?: string
|
|
207
|
+
source?: string
|
|
208
|
+
sections?: string[]
|
|
209
|
+
counts?: Record<string, number>
|
|
210
|
+
skipped?: { code: string }[]
|
|
211
|
+
credentialRefs?: string[]
|
|
212
|
+
detectedPaths?: string[]
|
|
213
|
+
conflictPolicy?: string
|
|
214
|
+
code?: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 构造 POST 请求(带 JSON 体)。 */
|
|
218
|
+
function fakePost(url: string, payload: unknown): IncomingMessage {
|
|
219
|
+
const body = Buffer.from(JSON.stringify(payload), 'utf8')
|
|
220
|
+
const req = {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
url,
|
|
223
|
+
socket: { remoteAddress: '127.0.0.1' },
|
|
224
|
+
headers: { host: '127.0.0.1:3080', 'content-length': String(body.length) },
|
|
225
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<Buffer> {
|
|
226
|
+
yield body
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
return req as unknown as IncomingMessage
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 取本组的 foreign-import 路由。 */
|
|
233
|
+
function importRoute(env: Record<string, unknown>) {
|
|
234
|
+
const routes = foreignRoutes(env as never)
|
|
235
|
+
const route = routes.find((r) => r.path === '/api/dsh-config-manager/foreign-import')
|
|
236
|
+
assert.ok(route !== undefined, '必须存在 POST /foreign-import(否则 GUI 拿不到 zipPath)')
|
|
237
|
+
assert.notEqual(routeSpecOf(route), undefined, '必须经 kit 的 endpoint() 声明(不得裸注册)')
|
|
238
|
+
return route
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 调一次 POST /foreign-import。
|
|
243
|
+
*
|
|
244
|
+
* `opts.userHome` 通过覆盖 `USERPROFILE`/`HOME` 生效 —— 路由走 `foreignSourceContext()` 的缺省
|
|
245
|
+
* 分支(`os.homedir()`),而 `os.homedir()` 正是读这两个环境变量。这样测试能用**受控的临时 userHome**
|
|
246
|
+
* 驱动真实代码路径,而不必在实现里开测试专用口子(也就不必为测试放宽生产语义)。
|
|
247
|
+
*/
|
|
248
|
+
async function callImport(
|
|
249
|
+
payload: unknown,
|
|
250
|
+
opts: { userHome?: string; sessionFormatVersion?: number } = {},
|
|
251
|
+
): Promise<{ status: number; body: ForeignImportBody; raw: string }> {
|
|
252
|
+
const env = await stubEnv(opts.sessionFormatVersion)
|
|
253
|
+
const prevProfile = process.env['USERPROFILE']
|
|
254
|
+
const prevHome = process.env['HOME']
|
|
255
|
+
if (opts.userHome !== undefined) {
|
|
256
|
+
process.env['USERPROFILE'] = opts.userHome
|
|
257
|
+
process.env['HOME'] = opts.userHome
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const res = fakeResponse()
|
|
261
|
+
await importRoute(env).handler(fakePost('/api/dsh-config-manager/foreign-import', payload), res.res)
|
|
262
|
+
return { status: res.status, body: JSON.parse(res.body) as ForeignImportBody, raw: res.body }
|
|
263
|
+
} finally {
|
|
264
|
+
if (prevProfile === undefined) delete process.env['USERPROFILE']
|
|
265
|
+
else process.env['USERPROFILE'] = prevProfile
|
|
266
|
+
if (prevHome === undefined) delete process.env['HOME']
|
|
267
|
+
else process.env['HOME'] = prevHome
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
test('GET 与 POST 对同一来源必须给同一结论(缺陷 1 的另一面:两接口自相矛盾)', async () => {
|
|
272
|
+
/**
|
|
273
|
+
* 缺陷 1 的可见症状不是「读不到」,而是 **GET 说 found=true、POST 说 nothing-to-import**。
|
|
274
|
+
* 根因是两处用了不同的 home(GET os.homedir() / POST host.homeDir)。
|
|
275
|
+
* 这里直接把两个接口对同一受控 userHome 的结论钉在一起 —— 任何一处改回错 home,本用例立刻红。
|
|
276
|
+
*/
|
|
277
|
+
const fixtureHome = path.join(import.meta.dirname, '..', 'foreign', 'fixtures', 'hermes', 'basic')
|
|
278
|
+
const userHome = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-foreign-consistency-'))
|
|
279
|
+
const target = process.platform === 'win32'
|
|
280
|
+
? path.join(userHome, 'AppData', 'Local', 'Hermes')
|
|
281
|
+
: path.join(userHome, '.hermes')
|
|
282
|
+
await fs.mkdir(target, { recursive: true })
|
|
283
|
+
await fs.cp(fixtureHome, target, { recursive: true })
|
|
284
|
+
const prev = { profile: process.env['USERPROFILE'], home: process.env['HOME'], local: process.env['LOCALAPPDATA'], hermes: process.env['HERMES_HOME'] }
|
|
285
|
+
process.env['USERPROFILE'] = userHome
|
|
286
|
+
process.env['HOME'] = userHome
|
|
287
|
+
delete process.env['LOCALAPPDATA']
|
|
288
|
+
delete process.env['HERMES_HOME']
|
|
289
|
+
const env2 = await stubEnv()
|
|
290
|
+
try {
|
|
291
|
+
// GET:检测面
|
|
292
|
+
const getRes = fakeResponse()
|
|
293
|
+
await foreignRoutes(env2 as never)[0]!.handler(fakeRequest('/api/dsh-config-manager/foreign-sources'), getRes.res)
|
|
294
|
+
const detected = (JSON.parse(getRes.body) as { sources: { id: string; found: boolean }[] })
|
|
295
|
+
.sources.find((s) => s.id === 'hermes')
|
|
296
|
+
assert.equal(detected?.found, true, 'GET 必须检测到受控 userHome 下的 Hermes')
|
|
297
|
+
|
|
298
|
+
// POST:产包面
|
|
299
|
+
const post = await callImport({ source: 'hermes' }, { userHome })
|
|
300
|
+
assert.equal(
|
|
301
|
+
post.status,
|
|
302
|
+
200,
|
|
303
|
+
'GET 说 found=true 而 POST 说读不到 —— 两个接口自相矛盾(正是缺陷 1 的症状):' + post.raw,
|
|
304
|
+
)
|
|
305
|
+
} finally {
|
|
306
|
+
if (prev.profile === undefined) delete process.env['USERPROFILE']; else process.env['USERPROFILE'] = prev.profile
|
|
307
|
+
if (prev.home === undefined) delete process.env['HOME']; else process.env['HOME'] = prev.home
|
|
308
|
+
if (prev.local === undefined) delete process.env['LOCALAPPDATA']; else process.env['LOCALAPPDATA'] = prev.local
|
|
309
|
+
if (prev.hermes === undefined) delete process.env['HERMES_HOME']; else process.env['HERMES_HOME'] = prev.hermes
|
|
310
|
+
await fs.rm(userHome, { recursive: true, force: true })
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('foreign-import:未知来源 → 400 + 可用清单,绝不回退到默认来源', async () => {
|
|
315
|
+
const { status, body, raw } = await callImport({ source: 'nonexistent-source' })
|
|
316
|
+
assert.equal(status, 400)
|
|
317
|
+
assert.ok(raw.includes('nonexistent-source'), '错误里必须点名用户给的那个 id')
|
|
318
|
+
assert.ok(raw.includes('hermes'), '必须给出可用来源清单(否则用户不知道能填什么)')
|
|
319
|
+
assert.equal(body.code, 'unknown-source', '稳定机器码供客户端判别')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('foreign-import:缺 source → 400 + 稳定码(不是 500)', async () => {
|
|
323
|
+
const { status, body } = await callImport({})
|
|
324
|
+
assert.equal(status, 400)
|
|
325
|
+
assert.equal(body.code, 'missing-source')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('foreign-import:空来源 → 400 + nothing-to-import(绝不静默产空包)', async () => {
|
|
329
|
+
// 本机没有 Antigravity 真盘 → 走「一个分区都产不出来」的分支;
|
|
330
|
+
// 若开发机恰好装了,则走 200 —— 两种都是正确行为,这里只钉住「不许静默空包」。
|
|
331
|
+
const { status, body, raw } = await callImport({ source: 'antigravity' })
|
|
332
|
+
if (status === 400) {
|
|
333
|
+
assert.ok(raw.includes('nothing-to-import'), '空结果必须给稳定码,而不是 200 + 空分区')
|
|
334
|
+
} else {
|
|
335
|
+
assert.ok((body.sections ?? []).length > 0, '200 必须伴随非空分区')
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('foreign-import:真实 fixture 产包 → 分区非空、路径受控、响应不含凭据值(缺陷 1 的端到端护栏)', async () => {
|
|
340
|
+
/**
|
|
341
|
+
* 用仓库内的跨平台 fixture 驱动 Hermes。
|
|
342
|
+
*
|
|
343
|
+
* **为什么这里不再用「HERMES_HOME 环境覆盖」**:那条路径会**掩盖缺陷 1** ——
|
|
344
|
+
* HERMES_HOME 优先于任何 home 拼接,所以即使路由把 homeDir 传成了 \$DSH_HOME 也照样能读到 fixture,
|
|
345
|
+
* 测试于是恒绿(这正是本机「看起来是好的」的原因,也是我第一版护栏的假绿来源)。
|
|
346
|
+
*
|
|
347
|
+
* 现在改为把它当**用户 home** 使用(~/.hermes 形态):路由必须用用户 home 才能找到它,
|
|
348
|
+
* 一旦退回 host.homeDir(stub 里是一个不存在的路径)→ 立刻 400 nothing-to-import → 本用例红。
|
|
349
|
+
* 因此这条用例同时是「产包正确」与「homeDir 用对了」的端到端证据。
|
|
350
|
+
*/
|
|
351
|
+
const fixtureHome = path.join(import.meta.dirname, '..', 'foreign', 'fixtures', 'hermes', 'basic')
|
|
352
|
+
/**
|
|
353
|
+
* 把 fixture 摆到「受控 userHome 之下的真实 Hermes 位置」——**按平台**:
|
|
354
|
+
* - win32:\`%LOCALAPPDATA%\Hermes\`(LOCALAPPDATA 缺省 = <userHome>/AppData/Local,见 resolveHermesHome)
|
|
355
|
+
* - 其它:\`~/.hermes\`
|
|
356
|
+
* 刻意**不设 HERMES_HOME**:它会短路掉 homeDir 拼接,从而掩盖缺陷 1(本机就是这样「看起来是好的」)。
|
|
357
|
+
*/
|
|
358
|
+
const userHome = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-foreign-userhome-'))
|
|
359
|
+
const target = process.platform === 'win32'
|
|
360
|
+
? path.join(userHome, 'AppData', 'Local', 'Hermes')
|
|
361
|
+
: path.join(userHome, '.hermes')
|
|
362
|
+
await fs.mkdir(target, { recursive: true })
|
|
363
|
+
await fs.cp(fixtureHome, target, { recursive: true })
|
|
364
|
+
const prev = { local: process.env['LOCALAPPDATA'], hermes: process.env['HERMES_HOME'] }
|
|
365
|
+
// 清掉这两个:让解析真正走 userHome(而不是被环境变量短路)
|
|
366
|
+
delete process.env['LOCALAPPDATA']
|
|
367
|
+
delete process.env['HERMES_HOME']
|
|
368
|
+
try {
|
|
369
|
+
const { status, body, raw } = await callImport({ source: 'hermes' }, { userHome })
|
|
370
|
+
// **不再静默跳过**:fixture 就在这个 userHome 里,读不到就是真失败(不得写成 return 变成假绿)
|
|
371
|
+
assert.equal(status, 200, 'fixture 位于传入的 userHome 下,必须能读到;若为 400 说明 homeDir 用错了:' + raw)
|
|
372
|
+
assert.ok((body.sections ?? []).length > 0, '必须产出至少一个分区')
|
|
373
|
+
assert.equal(body.conflictPolicy, 'skip-no-overwrite')
|
|
374
|
+
assert.ok((body.zipPath ?? '').endsWith('.zip'), 'zipPath 必须指向 ZIP')
|
|
375
|
+
assert.ok(!raw.includes('_DO_NOT_SHIP'), '响应体泄露了 fixture 哨兵值(凭据铁律)')
|
|
376
|
+
for (const p of body.detectedPaths ?? []) {
|
|
377
|
+
assert.ok(!path.isAbsolute(p), 'detectedPaths 必须是相对位置,实际: ' + p)
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
if (prev.local === undefined) delete process.env['LOCALAPPDATA']
|
|
381
|
+
else process.env['LOCALAPPDATA'] = prev.local
|
|
382
|
+
if (prev.hermes === undefined) delete process.env['HERMES_HOME']
|
|
383
|
+
else process.env['HERMES_HOME'] = prev.hermes
|
|
384
|
+
await fs.rm(userHome, { recursive: true, force: true })
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
/** 在受控 userHome 下造一条最小 Claude Code 会话(cwd 写在记录里,DSH 按它归位目录键)。 */
|
|
389
|
+
async function writeClaudeSession(userHome: string, id: string, cwd: string): Promise<void> {
|
|
390
|
+
const dir = path.join(userHome, '.claude', 'projects', 'D--proj-app')
|
|
391
|
+
await fs.mkdir(dir, { recursive: true })
|
|
392
|
+
const lines = [
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
type: 'user',
|
|
395
|
+
uuid: 'aaaaaaaa-0000-4000-8000-000000000001',
|
|
396
|
+
timestamp: '2026-10-01T10:00:00.000Z',
|
|
397
|
+
cwd,
|
|
398
|
+
message: { role: 'user', content: '帮我看看这个 bug' },
|
|
399
|
+
}),
|
|
400
|
+
JSON.stringify({
|
|
401
|
+
type: 'assistant',
|
|
402
|
+
uuid: 'aaaaaaaa-0000-4000-8000-000000000002',
|
|
403
|
+
timestamp: '2026-10-01T10:00:05.000Z',
|
|
404
|
+
cwd,
|
|
405
|
+
message: { model: 'claude-sonnet-4', content: [{ type: 'text', text: '修好了' }] },
|
|
406
|
+
}),
|
|
407
|
+
]
|
|
408
|
+
await fs.writeFile(path.join(dir, id + '.jsonl'), lines.join('\n') + '\n', 'utf8')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
test('foreign-import:目标机是 v4 时必须照 v4 产出会话(2026-10-08 桌面端整批被跳过的回归护栏)', async () => {
|
|
412
|
+
/**
|
|
413
|
+
* 真机事故:DSH 桌面端(0.2.0-rc.2)的会话格式是 v4,而转码器只认 v3 ⇒ 每一条会话都被
|
|
414
|
+
* `session-format-unsupported` 跳过、产出的包里**没有 sessions 分区**,用户看到「历史对话不见了」。
|
|
415
|
+
* 本用例把「宿主给的是 v4 就照 v4 产出」钉在**路由层**(GUI 走的正是这条端点)。
|
|
416
|
+
*/
|
|
417
|
+
const userHome = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-foreign-v4-'))
|
|
418
|
+
try {
|
|
419
|
+
await writeClaudeSession(userHome, '11111111-1111-4111-8111-111111111111', 'D:/proj/app')
|
|
420
|
+
|
|
421
|
+
const v4 = await callImport({ source: 'claude-code' }, { userHome, sessionFormatVersion: 4 })
|
|
422
|
+
assert.equal(v4.status, 200, 'v4 目标下必须产包:' + v4.raw)
|
|
423
|
+
assert.ok((v4.body.sections ?? []).includes('sessions'), 'v4 目标必须产出 sessions 分区:' + v4.raw)
|
|
424
|
+
assert.equal(v4.body.counts?.['sessions.transcoded'], 1)
|
|
425
|
+
assert.equal(
|
|
426
|
+
(v4.body.skipped ?? []).some((s) => s.code === 'session-format-unsupported'),
|
|
427
|
+
false,
|
|
428
|
+
'不得再整批报 session-format-unsupported',
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
// 读不到目标版本时**不猜**:内核如实报码、一个会话都不转 → 整包没有分区(400 nothing-to-import)。
|
|
432
|
+
// 这是「绝不猜版本」的既有语义,与上面那条并排钉住,避免修复被误读成「以后可以直接硬写 v4」。
|
|
433
|
+
const unknown = await callImport({ source: 'claude-code' }, { userHome })
|
|
434
|
+
assert.equal(unknown.status, 400, unknown.raw)
|
|
435
|
+
assert.equal(unknown.body.code, 'nothing-to-import')
|
|
436
|
+
assert.ok(
|
|
437
|
+
(unknown.body.skipped ?? []).some((s) => s.code === 'session-format-version-unknown'),
|
|
438
|
+
'读不到版本必须如实报码:' + unknown.raw,
|
|
439
|
+
)
|
|
440
|
+
} finally {
|
|
441
|
+
await fs.rm(userHome, { recursive: true, force: true })
|
|
442
|
+
}
|
|
443
|
+
})
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由组:外部 agent 来源(foreign)。
|
|
3
|
+
*
|
|
4
|
+
* 两条路由,职责严格分开:
|
|
5
|
+
* ① `GET /foreign-sources` —— **只读**的本机来源发现(本机装了哪几个来源;没装的也列出来,
|
|
6
|
+
* 只是 found=false)。检测与导入共用 src/foreign/registry.ts 的同一份装配,绝不各写一套路径表。
|
|
7
|
+
* ② `POST /foreign-import` —— 把一个外部来源**翻译成标准 bundle v1 ZIP**,落到受控临时目录并
|
|
8
|
+
* 返回 zipPath,供既有导入向导(analyze → plan → execute)消费。
|
|
9
|
+
*
|
|
10
|
+
* 为什么必须由宿主做②:`source.build()` 与 `writeForeignBundle()` 要读本机文件系统、
|
|
11
|
+
* 写 ZIP —— 浏览器半做不了。此前这条能力**只存在于 CLI**(`dcm import --from`),
|
|
12
|
+
* 于是 GUI 能选来源、能看检测结果,却在「点导入」之后拿不到 zipPath 而断链。
|
|
13
|
+
*
|
|
14
|
+
* 四条硬边界:
|
|
15
|
+
* ① **只写受控临时目录**:产物落在 `env.tmpDir`(与 /upload、/decrypt-archive 同一套暂存约定),
|
|
16
|
+
* 响应里的 zipPath 必须过 `isControlledPath` 自检 —— 绝不写到任何配置位置;
|
|
17
|
+
* ② **不做导入**:本路由只产包,**绝不**触碰 $DSH_HOME 下的配置。导入仍由既有
|
|
18
|
+
* /analyze → /plan → /execute 完成(导入前快照、冲突判定、dry-run 全部自动生效);
|
|
19
|
+
* ③ **凭据值绝不进响应**:只回 skipped 机器码 + 凭据**引用名**(值在转换层就已被剥离,
|
|
20
|
+
* 见 src/foreign/types.ts 的不变量①);
|
|
21
|
+
* ④ **空结果不静默成功**:一个分区都产不出来 → 400 + 稳定码(与 CLI 的退出码 1 同口径),
|
|
22
|
+
* 否则用户会拿到一个自检通过却什么都没有的包。
|
|
23
|
+
*
|
|
24
|
+
* W1(host-entry#F-02/#F-03):围栏 / 方法白名单 / 顶层异常处理由 src/routes/kit.ts 的 endpoint() 统一提供。
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs/promises'
|
|
27
|
+
import { readFileSync } from 'node:fs'
|
|
28
|
+
import path from 'node:path'
|
|
29
|
+
import { randomBytes } from 'node:crypto'
|
|
30
|
+
|
|
31
|
+
import { writeForeignBundle } from '../foreign/bundle.ts'
|
|
32
|
+
import {
|
|
33
|
+
FOREIGN_CONFLICT_POLICY,
|
|
34
|
+
ForeignSourceError,
|
|
35
|
+
createBuiltinForeignSourceRegistry,
|
|
36
|
+
detectForeignSources,
|
|
37
|
+
} from '../foreign/registry.ts'
|
|
38
|
+
import { redact } from '../security/redaction.ts'
|
|
39
|
+
import type { Platform } from '../schema/types.ts'
|
|
40
|
+
import { endpoint, requireJsonObject, writeJson } from './kit.ts'
|
|
41
|
+
import type { WebRoute } from './kit.ts'
|
|
42
|
+
import type { RoutesEnv } from './context.ts'
|
|
43
|
+
import {
|
|
44
|
+
FOREIGN_IMPORT_EMPTY_CODE,
|
|
45
|
+
FOREIGN_IMPORT_MISSING_SOURCE_CODE,
|
|
46
|
+
foreignSourceContext,
|
|
47
|
+
isInsideStaging,
|
|
48
|
+
} from './foreign-context.ts'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 写进 `manifest.exporter.version` 的插件版本。
|
|
52
|
+
*
|
|
53
|
+
* **运行时从 package.json 读取,不 import `../index.ts`** —— 这是本文件的关键约束:
|
|
54
|
+
* `src/index.ts` 顶部 import 了 `@deepseek-ai/*` 运行时包,一旦本模块(路由实现)把它拉进来,
|
|
55
|
+
* 任何**直接 import 本模块的单测**都会在纯单测环境 ERR_MODULE_NOT_FOUND 而根本跑不起来。
|
|
56
|
+
* 既有先例:`src/cli/index.ts` 的 resolveCliVersion 用同一套解法(只依赖 node:fs)。
|
|
57
|
+
*
|
|
58
|
+
* 也不硬编码版本:本仓库铁律是「版本必须同步多处」,再加一个常量就等于多一个会漂移的同步点
|
|
59
|
+
* (CLI 那边实测漂移过:插件升到 0.1.59 后常量仍停在 0.1.58)。读不到时回退中性占位,绝不谎报。
|
|
60
|
+
*/
|
|
61
|
+
function resolvePluginVersion(): string {
|
|
62
|
+
try {
|
|
63
|
+
const pkgUrl = new URL('../../package.json', import.meta.url)
|
|
64
|
+
const parsed = JSON.parse(readFileSync(pkgUrl, 'utf8')) as { version?: unknown }
|
|
65
|
+
if (typeof parsed.version === 'string' && parsed.version !== '') return parsed.version
|
|
66
|
+
} catch {
|
|
67
|
+
// 包结构不可解析(被裁剪 / 单文件打包):回退中性占位
|
|
68
|
+
}
|
|
69
|
+
return '0.0.0-unknown'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 进程内缓存(每个宿主进程只读一次 package.json) */
|
|
73
|
+
const PLUGIN_VERSION = resolvePluginVersion()
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 该来源当前版本读不了会话(不猜版本)时的稳定提示。
|
|
77
|
+
*
|
|
78
|
+
* 为什么宿主侧要显式传 `targetSessionFormatVersion`:会话转码必须知道目标机的格式版本,
|
|
79
|
+
* 缺省时内核逐条报 session-format-version-unknown 且**不产出 sessions 分区**(绝不猜)。
|
|
80
|
+
* 宿主能读到真实版本(`host.sessionFormatVersion`),所以 GUI 路径用它 —— 这与离线 CLI
|
|
81
|
+
* 不同:CLI 拿不到版本,只能如实报码。
|
|
82
|
+
*/
|
|
83
|
+
export function foreignRoutes(env: RoutesEnv): WebRoute[] {
|
|
84
|
+
const { host, roots, tmpDir } = env
|
|
85
|
+
return [
|
|
86
|
+
// ---------------------------------------------------- foreign-sources
|
|
87
|
+
// 六个来源的本机检测结果(found / 命中的相对位置 / 机器码);未安装是正常状态,不是错误。
|
|
88
|
+
endpoint({ path: '/api/dsh-config-manager/foreign-sources', methods: ['GET'] }, async (req, res) => {
|
|
89
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
90
|
+
const projectDir = url.searchParams.get('projectDir')
|
|
91
|
+
const projectScoped = projectDir !== null && projectDir !== ''
|
|
92
|
+
// 与 POST / CLI 共用同一份构造(见 foreign-context.ts 的缺陷记录:三处必须同源)
|
|
93
|
+
const ctx = foreignSourceContext({ projectDir: projectScoped ? (projectDir as string) : undefined })
|
|
94
|
+
const sources = await detectForeignSources(createBuiltinForeignSourceRegistry(), ctx)
|
|
95
|
+
writeJson(res, 200, { conflictPolicy: FOREIGN_CONFLICT_POLICY, projectScoped, sources })
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
// ----------------------------------------------------- foreign-import
|
|
99
|
+
// 外部来源 → 标准 bundle v1 ZIP(受控临时目录)→ zipPath。见文件头的四条硬边界。
|
|
100
|
+
endpoint({ path: '/api/dsh-config-manager/foreign-import', methods: ['POST'] }, async (req, res) => {
|
|
101
|
+
const body = await requireJsonObject(req)
|
|
102
|
+
const sourceId = typeof body?.['source'] === 'string' ? body['source'] : ''
|
|
103
|
+
if (sourceId === '') {
|
|
104
|
+
writeJson(res, 400, { error: 'source is required', code: FOREIGN_IMPORT_MISSING_SOURCE_CODE })
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 未知来源**最先判定**(在接触任何宿主依赖之前)。
|
|
109
|
+
*
|
|
110
|
+
* 为什么顺序要紧:来源 id 非法是**与运行环境无关**的请求错误,必须稳定回 400。
|
|
111
|
+
* 若把它排在读取宿主环境之后,宿主状态异常时会把它淹没成 500 —— 用户从
|
|
112
|
+
* 「你填错了来源」变成「插件坏了」,排查方向整个跑偏。
|
|
113
|
+
*/
|
|
114
|
+
const registry = createBuiltinForeignSourceRegistry()
|
|
115
|
+
let source
|
|
116
|
+
try {
|
|
117
|
+
source = registry.get(sourceId)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof ForeignSourceError) {
|
|
120
|
+
// 未知来源**绝不回退**到某个默认来源(回退会让用户以为导的是 A、实际导的是 B)
|
|
121
|
+
writeJson(res, 400, {
|
|
122
|
+
error: error.message,
|
|
123
|
+
code: error.code,
|
|
124
|
+
available: error.available,
|
|
125
|
+
})
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
throw error
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* projectDir 语义(与 GET 一致):**只在请求显式给出时传入**。
|
|
133
|
+
* 宿主进程的 cwd 不是用户的项目 —— 绝不猜(猜错会把 A 项目的规则导成 B 项目的)。
|
|
134
|
+
*/
|
|
135
|
+
const rawProject = body?.['projectDir']
|
|
136
|
+
const projectDir = typeof rawProject === 'string' && rawProject !== '' ? rawProject : undefined
|
|
137
|
+
/**
|
|
138
|
+
* 与 GET / CLI **共用同一份构造**(缺陷修复,见 foreign-context.ts 文件头)。
|
|
139
|
+
*
|
|
140
|
+
* 这里的 `homeDir` 必须是**用户 home**,不能用 `host.homeDir` —— 后者是 \$DSH_HOME(≈ ~/.dsh),
|
|
141
|
+
* 而六来源真值表全部相对用户 home。此前写错会让 POST 去找 ~/.dsh/.claude(全部读不到 →
|
|
142
|
+
* 稳定回 400 nothing-to-import),而 GET 显示 found=true,两个接口自相矛盾。
|
|
143
|
+
*/
|
|
144
|
+
const ctx = foreignSourceContext({
|
|
145
|
+
projectDir,
|
|
146
|
+
// 本机能读到会话格式版本时必须传(否则会话整批不转码);读不到就不传,内核如实报码。
|
|
147
|
+
targetSessionFormatVersion: host.sessionFormatVersion,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
let result
|
|
151
|
+
let detection
|
|
152
|
+
try {
|
|
153
|
+
detection = await source.detect(ctx)
|
|
154
|
+
result = await source.build(ctx)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
writeJson(res, 500, { error: redact(error instanceof Error ? error.message : String(error)) })
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 空结果**绝不静默成功**:自检能过,但用户拿到的是一个什么都没有的包(与 CLI 同口径)。
|
|
161
|
+
if (result.sections.length === 0) {
|
|
162
|
+
writeJson(res, 400, {
|
|
163
|
+
error: 'nothing to import: the source has no convertible configuration on this machine',
|
|
164
|
+
code: FOREIGN_IMPORT_EMPTY_CODE,
|
|
165
|
+
skipped: result.skipped,
|
|
166
|
+
})
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const outPath = path.join(tmpDir, `foreign-${source.id}-${randomBytes(6).toString('hex')}.zip`)
|
|
171
|
+
try {
|
|
172
|
+
await fs.mkdir(tmpDir, { recursive: true })
|
|
173
|
+
await writeForeignBundle({
|
|
174
|
+
result,
|
|
175
|
+
outPath,
|
|
176
|
+
exporterVersion: PLUGIN_VERSION,
|
|
177
|
+
// 包是为「现在导入到本机」合成的 → 目标机 DSH 版本;读不到就如实标注,绝不谎报
|
|
178
|
+
dshVersion: host.dshVersion ?? 'unknown',
|
|
179
|
+
platform: process.platform as Platform,
|
|
180
|
+
arch: process.arch,
|
|
181
|
+
})
|
|
182
|
+
} catch (error) {
|
|
183
|
+
// 失败即清理半成品(不留孤儿 ZIP 占着 tmp)
|
|
184
|
+
await fs.rm(outPath, { force: true }).catch(() => undefined)
|
|
185
|
+
writeJson(res, 500, { error: redact(error instanceof Error ? error.message : String(error)) })
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 自检:产物必须落在受控暂存根内。**写出去之前就多一道判定** ——
|
|
191
|
+
* 这条断言的价值不是「防注入」(tmpDir 是我们自己拼的),而是把
|
|
192
|
+
* 「受控目录约定」变成**运行期不变量**:将来有人改成别的目录,这里立刻红。
|
|
193
|
+
*/
|
|
194
|
+
if (!isInsideStaging(outPath, roots)) {
|
|
195
|
+
await fs.rm(outPath, { force: true }).catch(() => undefined)
|
|
196
|
+
writeJson(res, 500, { error: 'bundle path escaped the controlled staging area' })
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let sizeBytes = 0
|
|
201
|
+
try {
|
|
202
|
+
sizeBytes = (await fs.stat(outPath)).size
|
|
203
|
+
} catch {
|
|
204
|
+
// 体积只是展示信息,读不到不影响导入(不谎报 0:留给客户端按「未知」处理)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
writeJson(res, 200, {
|
|
208
|
+
zipPath: outPath,
|
|
209
|
+
name: path.basename(outPath),
|
|
210
|
+
source: source.id,
|
|
211
|
+
labelKey: source.labelKey,
|
|
212
|
+
sizeBytes,
|
|
213
|
+
// 分区与计数:GUI 据此显示「将导入什么」(全部是机器可读的计数,不含任何配置值)
|
|
214
|
+
sections: result.sections.map((s) => s.sectionId),
|
|
215
|
+
counts: result.counts,
|
|
216
|
+
// 未迁移项:只有机器码 + 位置名 + 条数(**没有任何配置值**)
|
|
217
|
+
skipped: result.skipped,
|
|
218
|
+
// 凭据只给**引用名**(形如 mcp:gitnexus:GITHUB_TOKEN):导入后由用户在 DSH 里补录
|
|
219
|
+
credentialRefs: result.credentialRefs,
|
|
220
|
+
/** 检测到的本机位置(相对 home),供界面显示「从哪儿读的」 */
|
|
221
|
+
detectedPaths: detection.paths,
|
|
222
|
+
conflictPolicy: FOREIGN_CONFLICT_POLICY,
|
|
223
|
+
})
|
|
224
|
+
}),
|
|
225
|
+
]
|
|
226
|
+
}
|