dsh-config-manager 0.1.71 → 0.1.73
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 +149 -98
- package/README.zh-CN.md +245 -88
- package/lib/adapters/agent-instructions.js +1 -1
- package/lib/adapters/file-collection.js +1 -1
- package/lib/adapters/index.d.ts +2 -1
- package/lib/adapters/index.js +6 -1
- package/lib/adapters/link-report.d.ts +1 -0
- package/lib/adapters/link-report.js +27 -3
- package/lib/adapters/messages.d.ts +21 -0
- package/lib/adapters/messages.js +57 -0
- package/lib/adapters/plugin-data.d.ts +114 -0
- package/lib/adapters/plugin-data.js +309 -0
- package/lib/adapters/plugin-files.js +3 -2
- package/lib/adapters/plugins.js +4 -3
- package/lib/adapters/prompts.d.ts +1 -0
- package/lib/adapters/prompts.js +7 -1
- package/lib/adapters/self.d.ts +8 -2
- package/lib/adapters/self.js +3 -2
- package/lib/adapters/sessions.d.ts +1 -0
- package/lib/adapters/sessions.js +4 -2
- package/lib/adapters/test-helpers.d.ts +9 -0
- package/lib/adapters/test-helpers.js +14 -0
- package/lib/adapters/ui.d.ts +2 -2
- package/lib/adapters/ui.js +25 -25
- package/lib/adapters/workspaces.d.ts +1 -0
- package/lib/adapters/workspaces.js +7 -3
- package/lib/cli/actions.d.ts +9 -1
- package/lib/cli/actions.js +5 -2
- package/lib/cli/help.js +23 -10
- package/lib/cli/index.d.ts +23 -0
- package/lib/cli/index.js +90 -32
- package/lib/cli/sessions-inspect.js +3 -1
- package/lib/cli/sessions-repair.js +25 -6
- package/lib/cli/web/page.d.ts +12 -0
- package/lib/cli/web/page.js +21 -12
- package/lib/cli/web/routes.js +2 -2
- package/lib/cli/web/server.js +4 -4
- package/lib/client.d.ts +1442 -494
- package/lib/client.js +15326 -8520
- package/lib/core/analyzer.d.ts +11 -2
- package/lib/core/analyzer.js +31 -8
- package/lib/core/backup-plan.d.ts +10 -0
- package/lib/core/backup-plan.js +51 -20
- package/lib/core/backup-verify.js +24 -23
- package/lib/core/backup.d.ts +25 -1
- package/lib/core/backup.js +99 -28
- package/lib/core/boot-rescue.js +4 -3
- package/lib/core/boot-safety.js +2 -1
- package/lib/core/command-rollback.d.ts +182 -0
- package/lib/core/command-rollback.js +293 -0
- package/lib/core/consult-source.js +7 -6
- package/lib/core/disk-usage.d.ts +74 -10
- package/lib/core/disk-usage.js +67 -4
- package/lib/core/engine-locale.d.ts +87 -0
- package/lib/core/engine-locale.js +125 -0
- package/lib/core/exporter.js +3 -3
- package/lib/core/fork-reconcile.d.ts +128 -0
- package/lib/core/fork-reconcile.js +150 -0
- package/lib/core/journal.js +6 -5
- package/lib/core/local-plugin-host.js +3 -2
- package/lib/core/local-plugin-pack.js +11 -6
- package/lib/core/messages-core.d.ts +294 -0
- package/lib/core/messages-core.js +619 -0
- package/lib/core/messages.d.ts +61 -2
- package/lib/core/messages.js +118 -13
- package/lib/core/migration-consult.js +21 -20
- package/lib/core/migration-history.js +19 -5
- package/lib/core/model-tools.d.ts +0 -22
- package/lib/core/model-tools.js +50 -27
- package/lib/core/phase3-host.js +30 -5
- package/lib/core/plugin-cli.js +18 -19
- package/lib/core/plugin-data-registry.d.ts +124 -0
- package/lib/core/plugin-data-registry.js +198 -0
- package/lib/core/reconcile.d.ts +2 -14
- package/lib/core/reconcile.js +87 -17
- package/lib/core/recovery-orchestrator.js +19 -18
- package/lib/core/reinstall.d.ts +4 -0
- package/lib/core/reinstall.js +26 -21
- package/lib/core/restore.js +20 -17
- package/lib/core/rollback-gate.d.ts +33 -0
- package/lib/core/rollback-gate.js +57 -0
- package/lib/core/rollback-ignore-rules.d.ts +31 -0
- package/lib/core/rollback-ignore-rules.js +140 -0
- package/lib/core/rollback-ledger.d.ts +248 -0
- package/lib/core/rollback-ledger.js +1282 -0
- package/lib/core/rollback-point.d.ts +1391 -0
- package/lib/core/rollback-point.js +3503 -0
- package/lib/core/rollback.js +6 -4
- package/lib/core/run-registry.js +6 -5
- package/lib/core/session-health.d.ts +15 -0
- package/lib/core/session-meta.d.ts +42 -2
- package/lib/core/session-meta.js +140 -1
- package/lib/core/session-title.d.ts +28 -0
- package/lib/core/session-title.js +185 -0
- package/lib/core/startup-barrier.d.ts +24 -0
- package/lib/core/startup-barrier.js +22 -0
- package/lib/core/transaction-coordinator.js +4 -3
- package/lib/core/types.d.ts +37 -1
- package/lib/core/validator.js +5 -4
- package/lib/core/verify-recovery.js +31 -30
- package/lib/foreign/bundle.js +3 -2
- package/lib/foreign/continue.js +2 -1
- package/lib/foreign/crush.js +2 -1
- package/lib/foreign/gemini.js +2 -1
- package/lib/foreign/goose.js +2 -1
- package/lib/foreign/kilocode.js +2 -1
- package/lib/foreign/kimi.js +2 -1
- package/lib/foreign/messages.d.ts +15 -0
- package/lib/foreign/messages.js +40 -0
- package/lib/foreign/mimocode.js +2 -1
- package/lib/foreign/openclaw.js +2 -1
- package/lib/foreign/opencode.js +2 -1
- package/lib/foreign/pi.js +2 -1
- package/lib/foreign/qoder.js +2 -1
- package/lib/foreign/qwen.js +2 -1
- package/lib/foreign/registry.d.ts +1 -1
- package/lib/foreign/registry.js +8 -7
- package/lib/foreign/teleagent.js +2 -1
- package/lib/foreign/workbuddy.js +2 -1
- package/lib/foreign/zcode.js +2 -1
- package/lib/foreign/zed.js +2 -1
- package/lib/index.d.ts +227 -7
- package/lib/index.js +799 -54
- package/lib/market/git-file-writer.js +20 -19
- package/lib/market/github-repos.js +35 -19
- package/lib/market/index-parser.js +31 -30
- package/lib/market/messages-market.d.ts +138 -0
- package/lib/market/messages-market.js +305 -0
- package/lib/market/my-repo.d.ts +2 -2
- package/lib/market/my-repo.js +32 -29
- package/lib/market/prepare.js +18 -15
- package/lib/market/security.js +20 -19
- package/lib/market/types.js +2 -1
- package/lib/market/url.js +2 -1
- package/lib/migrations/index.js +6 -5
- package/lib/migrations/messages-migrations.d.ts +32 -0
- package/lib/migrations/messages-migrations.js +58 -0
- package/lib/profiles/dsh-profile-launcher.js +2 -1
- package/lib/profiles/dsh-profile-runtime.js +2 -1
- package/lib/profiles/messages.d.ts +7 -0
- package/lib/profiles/messages.js +27 -0
- package/lib/routes/backup.js +18 -2
- package/lib/routes/import.js +3 -3
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +3 -2
- package/lib/routes/kit.js +6 -1
- package/lib/routes/market.js +1 -1
- package/lib/routes/messages.d.ts +43 -0
- package/lib/routes/messages.js +102 -0
- package/lib/routes/prefs.js +26 -3
- package/lib/routes/profiles.js +12 -6
- package/lib/routes/recovery.d.ts +21 -0
- package/lib/routes/recovery.js +478 -94
- package/lib/routes/snapshots.js +51 -4
- package/lib/routes/sync.js +79 -17
- package/lib/schema/config.d.ts +11 -1
- package/lib/schema/config.js +52 -20
- package/lib/schema/index.d.ts +8 -6
- package/lib/schema/index.js +8 -6
- package/lib/schema/manifest.js +19 -18
- package/lib/schema/messages.d.ts +47 -0
- package/lib/schema/messages.js +110 -0
- package/lib/schema/section-registry.d.ts +42 -15
- package/lib/schema/section-registry.js +67 -32
- package/lib/schema/types.d.ts +1 -1
- package/lib/schema/versions.js +6 -5
- package/lib/security/encryption.js +16 -15
- package/lib/security/integrity.js +8 -7
- package/lib/security/messages.d.ts +30 -0
- package/lib/security/messages.js +77 -0
- package/lib/security/redaction.js +12 -0
- package/lib/security/secret-scanner.js +16 -13
- package/lib/security/vault.js +2 -1
- package/lib/security/zip-security.js +4 -3
- package/lib/sync/ancestor.js +2 -1
- package/lib/sync/autosync-config.d.ts +11 -0
- package/lib/sync/autosync-config.js +12 -0
- package/lib/sync/autosync-scheduler.d.ts +11 -1
- package/lib/sync/autosync-scheduler.js +115 -76
- package/lib/sync/backup-files.js +3 -2
- package/lib/sync/backup-scheduler.js +6 -5
- package/lib/sync/blob-store.js +2 -1
- package/lib/sync/git/git-transport.d.ts +55 -0
- package/lib/sync/git/git-transport.js +152 -2
- package/lib/sync/github-auth.js +10 -9
- package/lib/sync/layout.js +13 -12
- package/lib/sync/messages.d.ts +63 -0
- package/lib/sync/messages.js +149 -0
- package/lib/sync/review-queue.js +6 -5
- package/lib/sync/s3/messages.d.ts +7 -0
- package/lib/sync/s3/messages.js +16 -0
- package/lib/sync/s3/s3-providers.d.ts +4 -1
- package/lib/sync/s3/s3-providers.js +5 -5
- package/lib/sync/s3/s3-transport.js +1 -1
- package/lib/sync/s3/sigv4.js +2 -1
- package/lib/sync/snapshot-crypto.js +6 -5
- package/lib/sync/snapshot-json.js +3 -2
- package/lib/sync/sync-engine.d.ts +58 -22
- package/lib/sync/sync-engine.js +119 -48
- package/lib/sync/sync-history.js +7 -6
- package/lib/sync/sync-selection.d.ts +15 -4
- package/lib/sync/sync-selection.js +17 -5
- package/lib/sync/ui-prefs.d.ts +22 -0
- package/lib/sync/ui-prefs.js +28 -0
- package/lib/sync/webdav/webdav-transport.d.ts +1 -0
- package/lib/sync/webdav/webdav-transport.js +4 -1
- package/lib/ui/artifact-view.js +4 -3
- package/lib/ui/commands.d.ts +1 -1
- package/lib/ui/commands.js +10 -0
- package/lib/ui/diff-view.d.ts +10 -0
- package/lib/ui/diff-view.js +17 -0
- package/lib/ui/disk-usage-view.d.ts +33 -4
- package/lib/ui/disk-usage-view.js +29 -1
- package/lib/ui/errors.d.ts +1 -1
- package/lib/ui/errors.js +4 -3
- package/lib/ui/export-flow.js +26 -20
- package/lib/ui/foreign-view.d.ts +5 -1
- package/lib/ui/foreign-view.js +26 -2
- package/lib/ui/i18n.d.ts +68 -1
- package/lib/ui/i18n.js +120 -2
- package/lib/ui/import-wizard.d.ts +7 -0
- package/lib/ui/import-wizard.js +13 -10
- package/lib/ui/progress.d.ts +2 -2
- package/lib/ui/progress.js +1 -2
- package/lib/ui/report.d.ts +5 -5
- package/lib/ui/report.js +6 -6
- package/lib/ui/rollback-view.d.ts +815 -0
- package/lib/ui/rollback-view.js +738 -0
- package/lib/ui/selection-model.d.ts +41 -0
- package/lib/ui/selection-model.js +30 -0
- package/lib/ui/session-inventory-view.d.ts +97 -3
- package/lib/ui/session-inventory-view.js +76 -2
- package/lib/ui/session-issue-labels.d.ts +43 -0
- package/lib/ui/session-issue-labels.js +48 -0
- package/lib/ui/sync-settings-view.d.ts +33 -0
- package/lib/ui/sync-settings-view.js +40 -0
- package/lib/ui/test-helpers.js +2 -1
- package/lib/ui/types.d.ts +8 -3
- package/lib/ui/types.js +1 -1
- package/lib/utils/atomic-write.js +6 -5
- package/lib/utils/bundle-scan.d.ts +11 -1
- package/lib/utils/bundle-scan.js +12 -1
- package/lib/utils/env-lock.d.ts +10 -0
- package/lib/utils/env-lock.js +73 -46
- package/lib/utils/json.js +6 -5
- package/lib/utils/messages-utils.d.ts +96 -0
- package/lib/utils/messages-utils.js +216 -0
- package/lib/utils/proxy.js +18 -5
- package/lib/utils/recursive-walk.d.ts +22 -0
- package/lib/utils/recursive-walk.js +75 -1
- package/lib/utils/session-log-repair.js +10 -9
- package/lib/utils/zip.js +27 -26
- package/package.json +12 -2
- package/src/adapters/agent-instructions.ts +1 -1
- package/src/adapters/file-collection.test.ts +1 -0
- package/src/adapters/file-collection.ts +1 -1
- package/src/adapters/files.test.ts +27 -0
- package/src/adapters/index.ts +6 -1
- package/src/adapters/link-report.test.ts +22 -2
- package/src/adapters/link-report.ts +29 -4
- package/src/adapters/messages.ts +61 -0
- package/src/adapters/plugin-data.test.ts +225 -0
- package/src/adapters/plugin-data.ts +351 -0
- package/src/adapters/plugin-files.ts +3 -2
- package/src/adapters/plugins.ts +4 -3
- package/src/adapters/prompts.ts +7 -1
- package/src/adapters/self.test.ts +26 -0
- package/src/adapters/self.ts +10 -3
- package/src/adapters/sessions.ts +4 -2
- package/src/adapters/settings.test.ts +38 -1
- package/src/adapters/test-helpers.ts +15 -0
- package/src/adapters/ui.test.ts +2 -2
- package/src/adapters/ui.ts +27 -26
- package/src/adapters/workspaces.ts +7 -3
- package/src/cli/actions.test.ts +28 -0
- package/src/cli/actions.ts +11 -2
- package/src/cli/help.ts +23 -10
- package/src/cli/index-guards.test.ts +193 -0
- package/src/cli/index.ts +106 -31
- package/src/cli/sessions-inspect.test.ts +15 -1
- package/src/cli/sessions-inspect.ts +3 -1
- package/src/cli/sessions-repair.test.ts +84 -0
- package/src/cli/sessions-repair.ts +33 -7
- package/src/cli/web/console-route-parity.test.ts +115 -0
- package/src/cli/web/page.ts +23 -12
- package/src/cli/web/routes.ts +2 -1
- package/src/cli/web/server.ts +4 -4
- package/src/cli/web/web.test.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +30 -2
- package/src/client/about/AboutPanel.tsx +2 -2
- package/src/client/about/PluginUpdateDialog.tsx +1 -1
- package/src/client/about/ReleaseNotesDialog.tsx +2 -2
- package/src/client/about/about-view.test.ts +13 -3
- package/src/client/about/about-view.ts +10 -6
- package/src/client/about/release-notes-view.test.ts +14 -4
- package/src/client/about/release-notes-view.ts +4 -2
- package/src/client/api.ts +18 -6
- package/src/client/chat/ChatEntryView.tsx +411 -0
- package/src/client/chat/RollbackDiffTab.tsx +108 -0
- package/src/client/chat/RollbackFileIcon.tsx +96 -0
- package/src/client/chat/RollbackReviewDialog.tsx +607 -0
- package/src/client/chat/chat-entry-view.test.ts +1174 -0
- package/src/client/chat/chat-entry-view.ts +758 -0
- package/src/client/chat/chat-locales.ts +152 -0
- package/src/client/chat/chat-node-registration.ts +64 -0
- package/src/client/chat/chat-point-window.ts +183 -0
- package/src/client/chat/chat-slots.ts +107 -0
- package/src/client/chat/rollback-diff-tab.test.ts +105 -0
- package/src/client/chat/rollback-diff-tab.ts +174 -0
- package/src/client/chat/rollback-entry-view.test.ts +1469 -0
- package/src/client/chat/rollback-entry-view.ts +946 -0
- package/src/client/chat/sidebar-slots.ts +59 -0
- package/src/client/common/ConfirmDialog.tsx +7 -3
- package/src/client/common/ContentPicker.tsx +48 -10
- package/src/client/common/DiffTable.tsx +133 -0
- package/src/client/common/ErrorBanner.tsx +4 -4
- package/src/client/common/Icon.tsx +2 -0
- package/src/client/common/Modal.tsx +8 -1
- package/src/client/common/ProgressBar.tsx +8 -2
- package/src/client/common/ReportView.tsx +4 -4
- package/src/client/common/RunsCenter.tsx +1 -0
- package/src/client/common/client-route-parity.test.ts +84 -0
- package/src/client/common/info-hint-guard.test.ts +46 -13
- package/src/client/common/picker-lazy-units.test.ts +42 -0
- package/src/client/common/plan-text-redaction.test.ts +17 -6
- package/src/client/common/progress-view-edge.test.ts +8 -7
- package/src/client/common/progress-view.test.ts +9 -9
- package/src/client/common/progress-view.ts +2 -2
- package/src/client/common/routes.ts +38 -0
- package/src/client/common/section-labels.test.ts +1 -0
- package/src/client/common/section-labels.ts +2 -0
- package/src/client/common/ui.tsx +73 -10
- package/src/client/config-manager.module.css +1109 -31
- package/src/client/css-scale-guard.test.ts +30 -0
- package/src/client/environment/EnvironmentPanel.tsx +11 -4
- package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
- package/src/client/export/ExportView.tsx +13 -8
- package/src/client/history/HistoryPanel.tsx +4 -3
- package/src/client/history/history-api.ts +2 -2
- package/src/client/history/history-locales.ts +5 -1
- package/src/client/home/HomePanel.tsx +29 -8
- package/src/client/import/ForeignImportView.tsx +4 -5
- package/src/client/import/ImportWizardView.tsx +5 -3
- package/src/client/import/import-wizard-split.test.ts +3 -1
- package/src/client/import/import-wizard-steps.tsx +9 -9
- package/src/client/index.ts +232 -3
- package/src/client/library/LibraryActions.tsx +32 -7
- package/src/client/library/LibraryPanel.tsx +23 -3
- package/src/client/library/RestorePlanView.tsx +9 -66
- package/src/client/locales.ts +62 -9
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +13 -2
- package/src/client/market/MyConfigsView.tsx +5 -4
- package/src/client/market/MyConfigsWizard.tsx +10 -8
- package/src/client/market/market-api.test.ts +9 -8
- package/src/client/market/market-api.ts +2 -2
- package/src/client/market/market-locales.ts +3 -0
- package/src/client/market/market-view.test.ts +6 -6
- package/src/client/market/market-view.ts +3 -3
- package/src/client/market/my-configs-api.ts +2 -2
- package/src/client/market/my-configs-view.test.ts +23 -22
- package/src/client/market/my-configs-view.ts +8 -8
- package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
- package/src/client/recovery/RecoveryPanel.tsx +442 -283
- package/src/client/recovery/RollbackCard.tsx +859 -0
- package/src/client/recovery/RollbackDiffPane.tsx +69 -0
- package/src/client/recovery/RollbackFileChanges.tsx +163 -0
- package/src/client/recovery/incident-api.ts +2 -2
- package/src/client/recovery/recovery-api.ts +144 -6
- package/src/client/recovery/recovery-locales.test.ts +299 -0
- package/src/client/recovery/recovery-locales.ts +520 -149
- package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
- package/src/client/recovery/rollback-enabled.ts +41 -0
- package/src/client/recovery/rollback-file-changes.test.ts +114 -0
- package/src/client/recovery/session-dialog-mount.test.ts +54 -0
- package/src/client/run-store.ts +84 -21
- package/src/client/sync/AutosyncCard.tsx +25 -1
- package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
- package/src/client/sync/ChannelConfigDialog.tsx +10 -10
- package/src/client/sync/DecryptPasswordCard.tsx +2 -2
- package/src/client/sync/SecurityOptionsCard.tsx +27 -17
- package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
- package/src/client/sync/SyncConfirmView.tsx +46 -8
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +31 -23
- package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
- package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
- package/src/client/sync/SyncSettingsView.test.ts +20 -1
- package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
- package/src/client/sync/WorkDirCard.tsx +112 -0
- package/src/client/sync/history-model.test.ts +42 -9
- package/src/client/sync/history-model.ts +30 -15
- package/src/client/sync/sync-api.test.ts +57 -33
- package/src/client/sync/sync-api.ts +80 -6
- package/src/client/sync/sync-locales.ts +104 -3
- package/src/client/sync/sync-view-v2.test.ts +65 -13
- package/src/client/sync/sync-view.test.ts +107 -52
- package/src/client/sync/sync-view.ts +124 -41
- package/src/core/analyzer.ts +31 -9
- package/src/core/backup-plan.test.ts +48 -5
- package/src/core/backup-plan.ts +51 -20
- package/src/core/backup-verify.ts +24 -23
- package/src/core/backup.test.ts +76 -0
- package/src/core/backup.ts +105 -29
- package/src/core/boot-rescue.ts +4 -3
- package/src/core/boot-safety.ts +2 -1
- package/src/core/command-rollback.test.ts +536 -0
- package/src/core/command-rollback.ts +406 -0
- package/src/core/consult-source.ts +7 -6
- package/src/core/disk-usage.test.ts +257 -1
- package/src/core/disk-usage.ts +134 -14
- package/src/core/engine-locale.test.ts +593 -0
- package/src/core/engine-locale.ts +175 -0
- package/src/core/exporter.ts +3 -3
- package/src/core/fork-reconcile.test.ts +279 -0
- package/src/core/fork-reconcile.ts +234 -0
- package/src/core/journal.ts +6 -5
- package/src/core/local-plugin-host.ts +3 -2
- package/src/core/local-plugin-pack.ts +11 -6
- package/src/core/messages-core.ts +661 -0
- package/src/core/messages.ts +125 -17
- package/src/core/migration-consult.ts +21 -20
- package/src/core/migration-history.ts +19 -5
- package/src/core/model-tools.ts +28 -27
- package/src/core/phase3-host.ts +21 -5
- package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
- package/src/core/plugin-cli.ts +18 -19
- package/src/core/plugin-data-registry.test.ts +113 -0
- package/src/core/plugin-data-registry.ts +261 -0
- package/src/core/reconcile.test.ts +117 -0
- package/src/core/reconcile.ts +85 -17
- package/src/core/recovery-orchestrator.ts +19 -18
- package/src/core/reinstall.ts +26 -21
- package/src/core/restore.ts +20 -17
- package/src/core/rollback-file-diff.test.ts +159 -0
- package/src/core/rollback-gate.test.ts +66 -0
- package/src/core/rollback-gate.ts +61 -0
- package/src/core/rollback-ignore-rules.ts +126 -0
- package/src/core/rollback-ledger.test.ts +655 -0
- package/src/core/rollback-ledger.ts +1314 -0
- package/src/core/rollback-point.test.ts +2853 -0
- package/src/core/rollback-point.ts +4340 -0
- package/src/core/rollback.ts +5 -3
- package/src/core/run-registry.ts +6 -5
- package/src/core/session-health.ts +15 -0
- package/src/core/session-meta.test.ts +117 -1
- package/src/core/session-meta.ts +149 -2
- package/src/core/session-title.test.ts +127 -0
- package/src/core/session-title.ts +189 -0
- package/src/core/startup-barrier.test.ts +69 -0
- package/src/core/startup-barrier.ts +27 -0
- package/src/core/transaction-coordinator.ts +4 -3
- package/src/core/types.ts +37 -1
- package/src/core/validator.ts +5 -4
- package/src/core/verify-recovery.ts +31 -30
- package/src/foreign/bundle.ts +3 -2
- package/src/foreign/continue.ts +2 -1
- package/src/foreign/crush.ts +2 -1
- package/src/foreign/file-budget.test.ts +2 -0
- package/src/foreign/gemini.ts +2 -1
- package/src/foreign/goose.ts +2 -1
- package/src/foreign/kilocode.ts +2 -1
- package/src/foreign/kimi.ts +2 -1
- package/src/foreign/messages.ts +44 -0
- package/src/foreign/mimocode.ts +2 -1
- package/src/foreign/openclaw.ts +2 -1
- package/src/foreign/opencode.ts +2 -1
- package/src/foreign/pi.ts +2 -1
- package/src/foreign/qoder.ts +2 -1
- package/src/foreign/qwen.ts +2 -1
- package/src/foreign/registry.test.ts +1 -1
- package/src/foreign/registry.ts +8 -7
- package/src/foreign/teleagent.ts +2 -1
- package/src/foreign/workbuddy.ts +2 -1
- package/src/foreign/zcode.ts +2 -1
- package/src/foreign/zed.ts +2 -1
- package/src/index.facade.test.ts +340 -1
- package/src/index.ts +876 -57
- package/src/market/git-file-writer.ts +20 -19
- package/src/market/github-repos.test.ts +23 -0
- package/src/market/github-repos.ts +37 -19
- package/src/market/index-parser.ts +31 -30
- package/src/market/messages-market.ts +325 -0
- package/src/market/my-repo.ts +32 -29
- package/src/market/prepare.ts +18 -15
- package/src/market/security.ts +20 -19
- package/src/market/types.ts +2 -1
- package/src/market/url.ts +2 -1
- package/src/migrations/index.ts +6 -5
- package/src/migrations/messages-migrations.ts +59 -0
- package/src/profiles/dsh-profile-launcher.ts +2 -1
- package/src/profiles/dsh-profile-runtime.ts +2 -1
- package/src/profiles/messages.ts +31 -0
- package/src/routes/backup.ts +18 -2
- package/src/routes/import.ts +3 -3
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +9 -3
- package/src/routes/market.ts +1 -1
- package/src/routes/messages.ts +106 -0
- package/src/routes/prefs.ts +31 -2
- package/src/routes/profiles.ts +12 -6
- package/src/routes/recovery.test.ts +405 -1
- package/src/routes/recovery.ts +477 -94
- package/src/routes/snapshots.ts +51 -4
- package/src/routes/sync.ts +82 -17
- package/src/schema/config.ts +53 -20
- package/src/schema/index.ts +7 -5
- package/src/schema/manifest-schema.test.ts +4 -4
- package/src/schema/manifest.ts +19 -18
- package/src/schema/messages.ts +120 -0
- package/src/schema/registry.test.ts +28 -7
- package/src/schema/section-registry.ts +96 -42
- package/src/schema/types.ts +2 -1
- package/src/schema/versions.ts +6 -5
- package/src/security/encryption.ts +16 -15
- package/src/security/integrity.ts +8 -7
- package/src/security/messages.ts +87 -0
- package/src/security/redaction.test.ts +17 -1
- package/src/security/redaction.ts +13 -0
- package/src/security/secret-scanner.ts +19 -12
- package/src/security/security.test.ts +47 -0
- package/src/security/vault.ts +2 -1
- package/src/security/zip-security.ts +4 -3
- package/src/sync/ancestor.ts +2 -1
- package/src/sync/autosync-config.ts +21 -0
- package/src/sync/autosync-scheduler.test.ts +63 -7
- package/src/sync/autosync-scheduler.ts +118 -78
- package/src/sync/backup-files.ts +3 -2
- package/src/sync/backup-scheduler.ts +6 -5
- package/src/sync/blob-store.ts +2 -1
- package/src/sync/git/git-transport.test.ts +170 -8
- package/src/sync/git/git-transport.ts +176 -2
- package/src/sync/github-auth.ts +10 -9
- package/src/sync/layout.ts +13 -12
- package/src/sync/messages.ts +153 -0
- package/src/sync/review-queue.ts +6 -5
- package/src/sync/s3/messages.ts +16 -0
- package/src/sync/s3/s3-providers.ts +9 -6
- package/src/sync/s3/s3-transport.ts +1 -1
- package/src/sync/s3/sigv4.ts +3 -1
- package/src/sync/snapshot-crypto.ts +6 -5
- package/src/sync/snapshot-json.ts +3 -2
- package/src/sync/sync-credentials.test.ts +119 -0
- package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
- package/src/sync/sync-engine.test.ts +226 -8
- package/src/sync/sync-engine.ts +154 -52
- package/src/sync/sync-history.ts +7 -6
- package/src/sync/sync-selection.test.ts +12 -2
- package/src/sync/sync-selection.ts +18 -5
- package/src/sync/transport.test.ts +1 -0
- package/src/sync/ui-prefs.test.ts +44 -1
- package/src/sync/ui-prefs.ts +39 -0
- package/src/sync/webdav/webdav-transport.ts +4 -1
- package/src/ui/artifact-view.ts +4 -3
- package/src/ui/commands.test.ts +1 -0
- package/src/ui/commands.ts +11 -1
- package/src/ui/diff-view.test.ts +9 -1
- package/src/ui/diff-view.ts +22 -0
- package/src/ui/disk-usage-view.test.ts +113 -6
- package/src/ui/disk-usage-view.ts +63 -7
- package/src/ui/errors.ts +4 -3
- package/src/ui/export-flow.ts +26 -20
- package/src/ui/foreign-view.test.ts +43 -0
- package/src/ui/foreign-view.ts +30 -3
- package/src/ui/i18n.ts +131 -2
- package/src/ui/import-wizard.ts +18 -10
- package/src/ui/progress.ts +2 -2
- package/src/ui/report.ts +7 -6
- package/src/ui/rollback-view.test.ts +836 -0
- package/src/ui/rollback-view.ts +1405 -0
- package/src/ui/selection-model.test.ts +33 -1
- package/src/ui/selection-model.ts +53 -0
- package/src/ui/session-inventory-view.test.ts +109 -0
- package/src/ui/session-inventory-view.ts +138 -3
- package/src/ui/session-issue-labels.test.ts +51 -0
- package/src/ui/session-issue-labels.ts +70 -0
- package/src/ui/sync-settings-view.test.ts +36 -0
- package/src/ui/sync-settings-view.ts +49 -0
- package/src/ui/test-helpers.ts +2 -1
- package/src/ui/types.ts +10 -4
- package/src/utils/atomic-write.ts +6 -5
- package/src/utils/bundle-scan.test.ts +2 -2
- package/src/utils/bundle-scan.ts +12 -1
- package/src/utils/env-lock.ts +83 -49
- package/src/utils/json.ts +7 -5
- package/src/utils/messages-utils.ts +232 -0
- package/src/utils/proxy.test.ts +25 -0
- package/src/utils/proxy.ts +19 -5
- package/src/utils/recursive-walk.test.ts +43 -0
- package/src/utils/recursive-walk.ts +75 -1
- package/src/utils/session-log-repair.ts +10 -9
- package/src/utils/zip.ts +27 -26
- package/lib/core/checkpoint.d.ts +0 -362
- package/lib/core/checkpoint.js +0 -1309
- package/src/core/checkpoint.test.ts +0 -665
- package/src/core/checkpoint.ts +0 -1614
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginDataAdapter 单测(pluginData / pluginAssets 两个分区)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖四件必须成立的事:
|
|
5
|
+
* ① 收录口径:配置 / 大资产各进各的分区;**可再生成缓存永不出现**;
|
|
6
|
+
* ② 单元 = 区域(选择器按区域勾选;预览与导出体积同口径);
|
|
7
|
+
* ③ 环境变量覆盖:报码跳过、不猜新位置;「设成同一个值」不算覆盖;
|
|
8
|
+
* ④ 导入白名单:只有本机内置来源 / 已安装插件自述声明之下的路径才允许写,
|
|
9
|
+
* 其余(含 .credentials.yaml 这类敏感文件)判 Error 且 applyItem 也拒绝。
|
|
10
|
+
*/
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
|
|
14
|
+
import { PluginDataAdapter } from './plugin-data.ts';
|
|
15
|
+
import { makeContext, makeImportContext, sha256Hex } from './test-helpers.ts';
|
|
16
|
+
import type { PluginDataSource } from '../core/plugin-data-registry.ts';
|
|
17
|
+
import type { FilesSection } from '../schema/types.ts';
|
|
18
|
+
|
|
19
|
+
const HOME = 'C:\\Users\\alice';
|
|
20
|
+
const bytes = (s: string): Uint8Array => Buffer.from(s, 'utf8');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 测试用的宿主上下文:**把系统 home 与 $DSH_HOME 指向同一个内存根**。
|
|
24
|
+
*
|
|
25
|
+
* 真机上两者不同($DSH_HOME = ~/.dsh),而这两个分区的根是**系统 home** —— 单测里合并成一个根
|
|
26
|
+
* 只是让夹具短;两者不同的真机形态由 tests/e2e/plugin-data-migration.test.ts 覆盖。
|
|
27
|
+
*/
|
|
28
|
+
function ctxFor(home: string, profile = 'web') {
|
|
29
|
+
const ctx = makeContext('win32', home, profile);
|
|
30
|
+
ctx.osHomeDir = home;
|
|
31
|
+
ctx.homeFs = ctx.fs;
|
|
32
|
+
return ctx;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 真机形态的假数据:wallpaper-engine 的配置 + 缓存 + 上传的壁纸;mnemon 的运行时与数据库 */
|
|
36
|
+
async function seedHome(ctx: ReturnType<typeof makeContext>): Promise<void> {
|
|
37
|
+
await ctx.fs.writeFile('.dsh-wallpaper-engine/config.json', bytes('{"wallpaper":"a"}'));
|
|
38
|
+
await ctx.fs.writeFile('.dsh-wallpaper-engine/cache/frame.bin', bytes('cache'));
|
|
39
|
+
await ctx.fs.writeFile('.dsh-wallpaper-engine/uploads/wall.mp4', bytes('video'));
|
|
40
|
+
await ctx.fs.writeFile('.mnemon/runtime/MEMORY.md', bytes('memory'));
|
|
41
|
+
await ctx.fs.writeFile('.mnemon/data/body.db', bytes('db'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('收录口径:配置分区只带配置,资产分区只带大资产,缓存永不出现', async () => {
|
|
45
|
+
const ctx = ctxFor(HOME);
|
|
46
|
+
await seedHome(ctx);
|
|
47
|
+
|
|
48
|
+
const config = new PluginDataAdapter('pluginData', { env: {} });
|
|
49
|
+
const configOut = await config.export(ctx, { includeSecrets: false });
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
configOut.data.files.map((f) => f.relativePath).sort(),
|
|
52
|
+
['.dsh-wallpaper-engine/config.json', '.mnemon/runtime/MEMORY.md'],
|
|
53
|
+
);
|
|
54
|
+
assert.equal(configOut.data.files[0]?.contentHash, sha256Hex(bytes('{"wallpaper":"a"}')));
|
|
55
|
+
|
|
56
|
+
const assets = new PluginDataAdapter('pluginAssets', { env: {} });
|
|
57
|
+
const assetOut = await assets.export(ctx, { includeSecrets: false });
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
assetOut.data.files.map((f) => f.relativePath).sort(),
|
|
60
|
+
['.dsh-wallpaper-engine/uploads/wall.mp4', '.mnemon/data/body.db'],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// 缓存绝不出现(两个分区都不许)
|
|
64
|
+
for (const out of [configOut, assetOut]) {
|
|
65
|
+
assert.ok(!out.data.files.some((f) => f.relativePath.includes('/cache/')), '缓存必须被排除');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('单元 = 区域:单元 id 就是区域路径,且预览与导出的体积同口径', async () => {
|
|
70
|
+
const ctx = ctxFor(HOME);
|
|
71
|
+
await seedHome(ctx);
|
|
72
|
+
const adapter = new PluginDataAdapter('pluginData', { env: {} });
|
|
73
|
+
|
|
74
|
+
const out = await adapter.export(ctx, { includeSecrets: false });
|
|
75
|
+
const units = adapter.listUnits(out);
|
|
76
|
+
assert.deepEqual(units.map((u) => u.id).sort(), ['pluginData:.dsh-wallpaper-engine', 'pluginData:.mnemon/runtime']);
|
|
77
|
+
assert.equal(units.find((u) => u.id === 'pluginData:.mnemon/runtime')?.fileCount, 1);
|
|
78
|
+
|
|
79
|
+
const preview = await adapter.preview(ctx, { includeSecrets: false });
|
|
80
|
+
assert.deepEqual(preview.items, units, '预览单元必须与导出单元逐项一致');
|
|
81
|
+
assert.equal(
|
|
82
|
+
preview.sizeBytes,
|
|
83
|
+
out.data.files.reduce((n, f) => n + f.data.byteLength, 0),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 条目级选择:只勾 mnemon 运行时
|
|
87
|
+
const only = await adapter.export(ctx, { includeSecrets: false, includeItems: { pluginData: ['pluginData:.mnemon/runtime'] } });
|
|
88
|
+
assert.deepEqual(only.data.files.map((f) => f.relativePath), ['.mnemon/runtime/MEMORY.md']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('环境变量覆盖:报码跳过、不猜位置;设成与默认相同不算覆盖', async () => {
|
|
92
|
+
const ctx = ctxFor(HOME);
|
|
93
|
+
await seedHome(ctx);
|
|
94
|
+
|
|
95
|
+
const moved = new PluginDataAdapter('pluginData', { env: { DSH_WE_DATA_DIR: 'D:\\wallpapers' } });
|
|
96
|
+
const movedOut = await moved.export(ctx, { includeSecrets: false });
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
movedOut.data.files.map((f) => f.relativePath),
|
|
99
|
+
['.mnemon/runtime/MEMORY.md'],
|
|
100
|
+
'被覆盖的区域不得被读成「默认位置」,未被覆盖的来源照常收录',
|
|
101
|
+
);
|
|
102
|
+
assert.ok(movedOut.warnings.some((w) => w.includes('DSH_WE_DATA_DIR')), '必须如实报出是哪个变量覆盖了它');
|
|
103
|
+
|
|
104
|
+
const assetsMoved = new PluginDataAdapter('pluginAssets', { env: { DSH_WE_UPLOAD_DIR: '~/pics' } });
|
|
105
|
+
const assetsOut = await assetsMoved.export(ctx, { includeSecrets: false });
|
|
106
|
+
assert.deepEqual(assetsOut.data.files.map((f) => f.relativePath), ['.mnemon/data/body.db'], '只跳过被覆盖的那一个区域');
|
|
107
|
+
|
|
108
|
+
const same = new PluginDataAdapter('pluginData', { env: { DSH_WE_DATA_DIR: '~/.dsh-wallpaper-engine' } });
|
|
109
|
+
const sameOut = await same.export(ctx, { includeSecrets: false });
|
|
110
|
+
assert.equal(sameOut.data.files.length, 2, '值等于默认位置 → 不算覆盖');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('导入白名单:包外路径一律 Error,applyItem 也拒绝;包内路径正常写回', async () => {
|
|
114
|
+
const src = ctxFor(HOME);
|
|
115
|
+
await seedHome(src);
|
|
116
|
+
const adapter = new PluginDataAdapter('pluginData', { env: {} });
|
|
117
|
+
const out = await adapter.export(src, { includeSecrets: false });
|
|
118
|
+
|
|
119
|
+
const evil: FilesSection = {
|
|
120
|
+
version: 1,
|
|
121
|
+
files: [
|
|
122
|
+
...out.data.files,
|
|
123
|
+
{ relativePath: '.credentials.yaml', data: bytes('token: x'), contentHash: sha256Hex(bytes('token: x')) },
|
|
124
|
+
{ relativePath: '.dsh-wallpaper-engine/uploads/wall.mp4', data: bytes('video'), contentHash: sha256Hex(bytes('video')) },
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const dst = ctxFor('/home/bob');
|
|
129
|
+
const sections = new Map<string, unknown>([['pluginData', evil]]);
|
|
130
|
+
const items = await adapter.analyzeImport(evil, makeImportContext(dst, sections));
|
|
131
|
+
|
|
132
|
+
const denied = items.filter((i) => i.kind === 'Error');
|
|
133
|
+
assert.equal(denied.length, 2, '包外路径 + 资产路径(走配置分区)都必须被拒');
|
|
134
|
+
assert.ok(denied.every((i) => i.description.includes('拒绝')), '拒绝理由必须可见');
|
|
135
|
+
|
|
136
|
+
const allowed = items.filter((i) => i.kind === 'Create');
|
|
137
|
+
assert.equal(allowed.length, 2);
|
|
138
|
+
for (const item of allowed) {
|
|
139
|
+
const r = await adapter.applyItem(item, makeImportContext(dst, sections));
|
|
140
|
+
assert.equal(r.ok, true);
|
|
141
|
+
}
|
|
142
|
+
assert.equal(Buffer.from(await dst.fs.readFile('.mnemon/runtime/MEMORY.md')).toString(), 'memory');
|
|
143
|
+
|
|
144
|
+
// 纵深防御:手工构造一个指向包外的计划项,applyItem 也必须拒绝
|
|
145
|
+
const forged = await adapter.applyItem(
|
|
146
|
+
{ id: 'x', kind: 'Create', adapter: 'pluginData', description: '', severity: 'info', target: { adapter: 'pluginData', ref: 'AGENTS.md' } },
|
|
147
|
+
makeImportContext(dst, sections),
|
|
148
|
+
);
|
|
149
|
+
assert.equal(forged.ok, false);
|
|
150
|
+
assert.equal(await dst.fs.exists('AGENTS.md'), false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('自述声明(dsh.dataDirs):已安装插件的区域可导出、可导入;未声明的插件目录仍被拒', async () => {
|
|
154
|
+
const ctx = ctxFor(HOME);
|
|
155
|
+
await ctx.fs.writeFile('profiles/web/package.json', bytes(JSON.stringify({ dependencies: { 'dsh-fake': '^1.0.0' } })));
|
|
156
|
+
await ctx.fs.writeFile(
|
|
157
|
+
'profiles/web/node_modules/dsh-fake/package.json',
|
|
158
|
+
bytes(JSON.stringify({ name: 'dsh-fake', dsh: { dataDirs: [{ path: '.fake-plugin', role: 'config', env: ['FAKE_DIR'] }] } })),
|
|
159
|
+
);
|
|
160
|
+
await ctx.fs.writeFile('.fake-plugin/state.json', bytes('{"a":1}'));
|
|
161
|
+
|
|
162
|
+
const adapter = new PluginDataAdapter('pluginData', { env: {} });
|
|
163
|
+
const out = await adapter.export(ctx, { includeSecrets: false });
|
|
164
|
+
assert.deepEqual(out.data.files.map((f) => f.relativePath), ['.fake-plugin/state.json']);
|
|
165
|
+
assert.deepEqual(adapter.listUnits(out).map((u) => u.id), ['pluginData:.fake-plugin']);
|
|
166
|
+
|
|
167
|
+
// 目标机也装了同一个插件 → 声明可解析 → 允许写
|
|
168
|
+
const dst = ctxFor('/home/bob');
|
|
169
|
+
await dst.fs.writeFile('profiles/web/package.json', bytes(JSON.stringify({ dependencies: { 'dsh-fake': '^1.0.0' } })));
|
|
170
|
+
await dst.fs.writeFile(
|
|
171
|
+
'profiles/web/node_modules/dsh-fake/package.json',
|
|
172
|
+
bytes(JSON.stringify({ name: 'dsh-fake', dsh: { dataDirs: [{ path: '.fake-plugin', role: 'config' }] } })),
|
|
173
|
+
);
|
|
174
|
+
const sections = new Map<string, unknown>([['pluginData', out.data]]);
|
|
175
|
+
const items = await adapter.analyzeImport(out.data, makeImportContext(dst, sections));
|
|
176
|
+
assert.deepEqual(items.map((i) => i.kind), ['Create']);
|
|
177
|
+
|
|
178
|
+
// 换一台机器(没装该插件、也没声明)→ 同一份包必须被拒,绝不静默写到未知位置
|
|
179
|
+
const fresh = ctxFor('/home/carol');
|
|
180
|
+
const freshItems = await adapter.analyzeImport(out.data, makeImportContext(fresh, sections));
|
|
181
|
+
assert.deepEqual(freshItems.map((i) => i.kind), ['Error']);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('空形态:一个区域都不存在时不报错、不加噪音文件(只有 dirEmpty 告警)', async () => {
|
|
185
|
+
const ctx = ctxFor(HOME);
|
|
186
|
+
const adapter = new PluginDataAdapter('pluginData', { env: {} });
|
|
187
|
+
const out = await adapter.export(ctx, { includeSecrets: false });
|
|
188
|
+
assert.deepEqual(out.data.files, []);
|
|
189
|
+
assert.equal(out.counts.files, 0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('旧宿主(没有系统 home 门面):不收集也不导入,并如实报码 —— 绝不退回 $DSH_HOME 读错位置', async () => {
|
|
193
|
+
// 注意:这里**故意**用未接线的上下文(homeFs 缺省 undefined)
|
|
194
|
+
const ctx = makeContext('win32', HOME, 'web');
|
|
195
|
+
await ctx.fs.writeFile('.dsh-wallpaper-engine/config.json', bytes('{"a":1}'));
|
|
196
|
+
const adapter = new PluginDataAdapter('pluginData', { env: {} });
|
|
197
|
+
|
|
198
|
+
const out = await adapter.export(ctx, { includeSecrets: false });
|
|
199
|
+
assert.deepEqual(out.data.files, [], '缺门面时不得在 $DSH_HOME 下按同名相对路径去读');
|
|
200
|
+
assert.ok(out.warnings.some((w) => w.includes('系统主目录')), '必须如实说明为什么什么都没收集');
|
|
201
|
+
|
|
202
|
+
const sections = new Map<string, unknown>([['pluginData', {
|
|
203
|
+
version: 1,
|
|
204
|
+
files: [{ relativePath: '.dsh-wallpaper-engine/config.json', data: bytes('{"a":1}'), contentHash: sha256Hex(bytes('{"a":1}')) }],
|
|
205
|
+
}]]);
|
|
206
|
+
const target = makeContext('linux', '/home/bob', 'web');
|
|
207
|
+
const items = await adapter.analyzeImport(
|
|
208
|
+
{ version: 1, files: [{ relativePath: '.dsh-wallpaper-engine/config.json', data: bytes('{"a":1}'), contentHash: sha256Hex(bytes('{"a":1}')) }] },
|
|
209
|
+
makeImportContext(target, sections),
|
|
210
|
+
);
|
|
211
|
+
assert.deepEqual(items.map((i) => i.kind), ['Error']);
|
|
212
|
+
assert.equal(await target.fs.exists('.dsh-wallpaper-engine/config.json'), false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('注入假来源:来源表可替换(测试与未来扩展都走同一条路)', async () => {
|
|
216
|
+
const ctx = ctxFor(HOME);
|
|
217
|
+
await ctx.fs.writeFile('.my-plugin/a.json', bytes('{}'));
|
|
218
|
+
const fake: PluginDataSource[] = [{
|
|
219
|
+
id: 'fake', packages: ['x'], evidence: 'fixture',
|
|
220
|
+
areas: [{ relPath: '.my-plugin', role: 'config' }],
|
|
221
|
+
}];
|
|
222
|
+
const adapter = new PluginDataAdapter('pluginData', { sources: fake, env: {} });
|
|
223
|
+
const out = await adapter.export(ctx, { includeSecrets: false });
|
|
224
|
+
assert.deepEqual(out.data.files.map((f) => f.relativePath), ['.my-plugin/a.json']);
|
|
225
|
+
});
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pluginData / pluginAssets 分区 adapter(第三方插件数据迁移)。
|
|
3
|
+
*
|
|
4
|
+
* 数据来源 = \`src/core/plugin-data-registry.ts\` 的来源注册表:内置已取证来源 + 已安装插件在
|
|
5
|
+
* 自己 package.json 里声明的 \`dsh.dataDirs\`。每个「区域(area)」在备份里是**一个可勾选单元**,
|
|
6
|
+
* 单元 id = 区域的 home 相对 POSIX 路径(如 \`.dsh-wallpaper-engine/uploads\`)——
|
|
7
|
+
* 用户能在导出选择器里逐区域看到「几个文件 / 多大」,也能只带走配置、不带几百 MB 的上传壁纸。
|
|
8
|
+
*
|
|
9
|
+
* 为什么两个分区而不是一个:
|
|
10
|
+
* - \`pluginData\` = 配置与轻量状态;\`pluginAssets\` = 大体积用户数据(上传的壁纸、本地数据库);
|
|
11
|
+
* - 「默认不勾选大资产」在既有 UI 选择模型里**只有分区级开关**这一条可靠通道(单元级只有
|
|
12
|
+
* 「默认全选 + 稀疏排除」,没有「默认排除」的概念)—— 拆成两个分区后,「可见但默认不勾选」
|
|
13
|
+
* 就是既有的 \`defaultIncluded=false\` 语义,不需要动共享选择模型,也不会与导入/市场两处选择语义打架。
|
|
14
|
+
*
|
|
15
|
+
* 实现:继承 FileCollectionAdapter(baseDir = '',即 home 根;与 agentInstructions 同一形态),
|
|
16
|
+
* 复用它的单元分组 / 单分区字节闸门 / 预览-导出一致性 / 幂等哈希比对;只覆写三处:
|
|
17
|
+
* ① \`listRelPaths()\` —— 清单来自来源注册表(并如实记录被环境变量覆盖而跳过的区域);
|
|
18
|
+
* ② \`unitIdOf()\` —— 单元 = 区域(最长前缀归属);
|
|
19
|
+
* ③ \`analyzeImport() / applyItem()\` —— **写入路径白名单**(见下)。
|
|
20
|
+
*
|
|
21
|
+
* 安全不变量(本分区是全仓唯一「按 home 相对路径任意写」的分区,必须自己守住):
|
|
22
|
+
* - **导入白名单**:只有「本机内置来源的区域」与「本机已安装插件声明的区域」之下的路径才允许写。
|
|
23
|
+
* 恶意/损坏的包想把 \`.credentials.yaml\` 或 \`AGENTS.md\` 塞进来,在这里被判 Error(计划里可见,
|
|
24
|
+
* 执行期不写);applyItem 再查一遍(纵深防御),内层还有基类的 control-plane namespace 拒绝。
|
|
25
|
+
* - **缓存永不收录**:role === 'cache' 的区域不属于任何分区,导出时根本不进清单。
|
|
26
|
+
* - **环境变量覆盖即跳过**:区域声明的 env 存在且解析结果与默认位置不同 → 报码跳过,绝不猜新位置。
|
|
27
|
+
* - **不产生用户可见散文**:告警与计划项文案一律走消息目录(i18n 铁律)。
|
|
28
|
+
*/
|
|
29
|
+
import { normalizePath } from '../utils/paths.ts';
|
|
30
|
+
import { engineMsg } from './messages.ts';
|
|
31
|
+
import { listFilesDetailed } from './link-report.ts';
|
|
32
|
+
import type { RecursiveListing } from '../utils/recursive-walk.ts';
|
|
33
|
+
import { sectionMeta } from '../schema/section-registry.ts';
|
|
34
|
+
import {
|
|
35
|
+
BUILTIN_PLUGIN_DATA_SOURCES, areaOfPath, areasForSection, isExcludedRel, normalizeHomeRel,
|
|
36
|
+
parseDeclaredDataDirs,
|
|
37
|
+
} from '../core/plugin-data-registry.ts';
|
|
38
|
+
import type { PluginDataArea, PluginDataSource, PluginDataSection } from '../core/plugin-data-registry.ts';
|
|
39
|
+
import { osHomeDirOf } from '../core/backup.ts';
|
|
40
|
+
import { FileCollectionAdapter } from './file-collection.ts';
|
|
41
|
+
import type { FilesSection } from '../schema/types.ts';
|
|
42
|
+
import type {
|
|
43
|
+
ApplyResult, ExportOptions, ExportSection, FileSystemFacade, HostContext, ImportContext, PlanItem,
|
|
44
|
+
SectionPreview,
|
|
45
|
+
} from '../core/types.ts';
|
|
46
|
+
|
|
47
|
+
/** 区域条目(来源 + 区域),编译期与运行期唯一的区域表示 */
|
|
48
|
+
export type PluginDataEntry = { source: PluginDataSource; area: PluginDataArea };
|
|
49
|
+
|
|
50
|
+
/** 声明来源缓存的 TTL(毫秒):一次导出/导入/执行里会多次解析,逐次读 15+ 个 package.json 是净开销 */
|
|
51
|
+
const DECLARED_TTL_MS = 5_000;
|
|
52
|
+
|
|
53
|
+
/** 单次解析里最多读取多少个已安装插件的 manifest(防御性上限:profile 依赖数量异常时不无限读) */
|
|
54
|
+
const MAX_DECLARED_PACKAGES = 300;
|
|
55
|
+
|
|
56
|
+
export interface PluginDataAdapterOptions {
|
|
57
|
+
/** 内置来源(缺省 = BUILTIN_PLUGIN_DATA_SOURCES;测试注入假来源) */
|
|
58
|
+
readonly sources?: readonly PluginDataSource[];
|
|
59
|
+
/** 自述声明缓存 TTL;测试可传 0 关闭缓存 */
|
|
60
|
+
readonly declaredTtlMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* 环境变量快照(缺省 process.env)。
|
|
63
|
+
*
|
|
64
|
+
* 为什么走构造参数而不是 HostContext:HostContext 上没有 env(它是宿主门面的集合),
|
|
65
|
+
* 而「区域被环境变量搬走」这件事必须在**同一次调用**里可判定、可注入测试值 ——
|
|
66
|
+
* 直接读 process.env 会让单测无法造「被覆盖」的场景(本机若恰好设了该变量,测试还会 flaky)。
|
|
67
|
+
*/
|
|
68
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class PluginDataAdapter extends FileCollectionAdapter {
|
|
72
|
+
readonly id: PluginDataSection;
|
|
73
|
+
readonly displayName: string;
|
|
74
|
+
readonly defaultIncluded = false;
|
|
75
|
+
readonly portability = 'deviceSpecific' as const;
|
|
76
|
+
/** home 根(相对路径 = home 相对路径,与 agentInstructions 同形) */
|
|
77
|
+
readonly baseDir = '';
|
|
78
|
+
|
|
79
|
+
private readonly builtin: readonly PluginDataSource[];
|
|
80
|
+
private readonly declaredTtlMs: number;
|
|
81
|
+
private readonly env: Readonly<Record<string, string | undefined>>;
|
|
82
|
+
/**
|
|
83
|
+
* 自述声明缓存。**必须带 ctx 键**(homeDir + profile):这台机器装了某插件,不代表另一台装了 ——
|
|
84
|
+
* 缓存若按实例共享,导入到「没装该插件」的目标机时会拿源机的声明去放行写入,
|
|
85
|
+
* 白名单就变成了「谁先导入谁说了算」。
|
|
86
|
+
*/
|
|
87
|
+
private declaredMemo: { key: string; at: number; sources: PluginDataSource[] } | undefined;
|
|
88
|
+
/** 本轮清单(listRelPaths 写入;unitIdOf 是同步纯函数,只能从这里拿区域表) */
|
|
89
|
+
private active: readonly PluginDataEntry[] | undefined;
|
|
90
|
+
/** 本轮告警(环境变量覆盖跳过等;export/preview 收尾合并进返回的 warnings) */
|
|
91
|
+
private pendingWarnings: string[] = [];
|
|
92
|
+
|
|
93
|
+
constructor(section: PluginDataSection, options: PluginDataAdapterOptions = {}) {
|
|
94
|
+
super();
|
|
95
|
+
this.id = section;
|
|
96
|
+
this.displayName = sectionMeta(section).displayName;
|
|
97
|
+
this.builtin = options.sources ?? BUILTIN_PLUGIN_DATA_SOURCES;
|
|
98
|
+
this.declaredTtlMs = options.declaredTtlMs ?? DECLARED_TTL_MS;
|
|
99
|
+
this.env = options.env ?? process.env;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 本分区可收集区域(内置 + 自述声明) */
|
|
103
|
+
private async allowedEntries(ctx: HostContext): Promise<readonly PluginDataEntry[]> {
|
|
104
|
+
const sources = await this.allSources(ctx);
|
|
105
|
+
return areasForSection(sources, this.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async allSources(ctx: HostContext): Promise<readonly PluginDataSource[]> {
|
|
109
|
+
const declared = await this.declaredSources(ctx);
|
|
110
|
+
return [...this.builtin, ...declared];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 已安装插件的自述声明(dsh.dataDirs)。
|
|
115
|
+
*
|
|
116
|
+
* 为什么读 profile 的 node_modules 而不是全局目录:DSH 的插件装在 \`profiles/<profile>/node_modules\`
|
|
117
|
+
* 下(pnpm 隔离布局),且同一个包在不同 profile 可以是不同版本 —— 声明必须取**本 profile 实际装的**那份。
|
|
118
|
+
*
|
|
119
|
+
* 读不到(未安装 / 无声明 / 形状非法)一律跳过:本机制是**附加**信息,绝不因为一个包读不出来
|
|
120
|
+
* 就影响内置来源或整次导出。
|
|
121
|
+
*/
|
|
122
|
+
private async declaredSources(ctx: HostContext): Promise<PluginDataSource[]> {
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const profile = ctx.profile !== undefined && ctx.profile !== '' ? ctx.profile : 'web';
|
|
125
|
+
const memoKey = ctx.homeDir + '|' + profile;
|
|
126
|
+
if (
|
|
127
|
+
this.declaredMemo !== undefined
|
|
128
|
+
&& this.declaredMemo.key === memoKey
|
|
129
|
+
&& now - this.declaredMemo.at < this.declaredTtlMs
|
|
130
|
+
) {
|
|
131
|
+
return this.declaredMemo.sources;
|
|
132
|
+
}
|
|
133
|
+
const base = 'profiles/' + profile;
|
|
134
|
+
const out: PluginDataSource[] = [];
|
|
135
|
+
let deps: string[] = [];
|
|
136
|
+
try {
|
|
137
|
+
const raw = await ctx.fs.readFile(base + '/package.json');
|
|
138
|
+
const parsed: unknown = JSON.parse(new TextDecoder().decode(raw));
|
|
139
|
+
const dep = (parsed as { dependencies?: unknown } | null)?.dependencies;
|
|
140
|
+
if (dep !== null && typeof dep === 'object') deps = Object.keys(dep).slice(0, MAX_DECLARED_PACKAGES);
|
|
141
|
+
} catch {
|
|
142
|
+
deps = [];
|
|
143
|
+
}
|
|
144
|
+
for (const name of deps) {
|
|
145
|
+
try {
|
|
146
|
+
const raw = await ctx.fs.readFile(base + '/node_modules/' + name + '/package.json');
|
|
147
|
+
const manifest: unknown = JSON.parse(new TextDecoder().decode(raw));
|
|
148
|
+
const source = parseDeclaredDataDirs(name, manifest);
|
|
149
|
+
if (source !== null) out.push(source);
|
|
150
|
+
} catch {
|
|
151
|
+
// 未安装 / 无 manifest → 无声明,跳过
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
this.declaredMemo = { key: memoKey, at: now, sources: out };
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 区域是否被环境变量覆盖(覆盖 → 返回变量名;未覆盖 / 未声明 → null)。
|
|
160
|
+
*
|
|
161
|
+
* 判定口径:变量**存在且解析结果与默认位置不同**才算覆盖 —— 仅仅 \`DSH_WE_DATA_DIR=~/.dsh-wallpaper-engine\`
|
|
162
|
+
* 这种「设成同一个值」的写法不该让区域凭空消失(那会让用户以为「没有数据」)。
|
|
163
|
+
* 解析只做 \`~\` 展开与分隔符归一(不做 realpath:不读盘、不猜)。
|
|
164
|
+
*/
|
|
165
|
+
private envOverrideOf(ctx: HostContext, area: PluginDataArea): string | null {
|
|
166
|
+
for (const name of area.envOverrides ?? []) {
|
|
167
|
+
const raw = this.env[name];
|
|
168
|
+
if (raw === undefined || raw.trim() === '') continue;
|
|
169
|
+
const normalized = raw.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
170
|
+
// 默认位置以**系统 home** 为根(本分区的根;见 core/backup.ts 的 OS_HOME_FILE_SECTIONS)
|
|
171
|
+
const home = osHomeDirOf(ctx).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
172
|
+
const expanded = normalized.startsWith('~/') ? home + normalized.slice(1) : normalized;
|
|
173
|
+
const defaultAbs = home + '/' + area.relPath;
|
|
174
|
+
if (expanded !== defaultAbs) return name;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 区域是否是**普通文件**(而不是目录 / 不存在)。
|
|
181
|
+
*
|
|
182
|
+
* 判据顺序为什么是这样:`statSize` 对「非普通文件」与「不存在」都返回 null,无法单独区分
|
|
183
|
+
* 目录与缺失;所以先判「是文件」,其余一律按目录走遍历 —— 目录不存在时遍历返回空,
|
|
184
|
+
* 结果同样是「没有内容」,不会把缺失误报成文件,也不会把目录误读成空内容而静默丢数据。
|
|
185
|
+
* 门面未实现 statSize(旧宿主)时退回读一次内容:读得出来就是文件。
|
|
186
|
+
*/
|
|
187
|
+
private async isRegularFile(fsFacade: FileSystemFacade, rel: string): Promise<boolean> {
|
|
188
|
+
if (fsFacade.statSize !== undefined) {
|
|
189
|
+
try {
|
|
190
|
+
return (await fsFacade.statSize(rel)) !== null;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await fsFacade.readFile(rel);
|
|
197
|
+
return true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static emptyListing(): RecursiveListing {
|
|
204
|
+
return { paths: [], skippedLinks: [], skippedDepDirs: [], followedLinks: 0, unreadableDirs: [] };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 清单枚举 = 来源注册表的区域(覆写基类的目录递归;baseDir='' 时基类实现会递归**整个 home**)。
|
|
209
|
+
*
|
|
210
|
+
* **必须覆写**:home 根上递归会遍历 profiles/ sessions/ attachments/ 等全部内容(本机实测
|
|
211
|
+
* 4000+ 文件 / 数百 MB),既慢又会把无关目录当成该分区的「可勾选单元」。
|
|
212
|
+
*/
|
|
213
|
+
protected override async listRelPaths(ctx: HostContext): Promise<RecursiveListing> {
|
|
214
|
+
// 本分区的根是**系统 home**(~/.dsh-wallpaper-engine、~/.mnemon …),不是 $DSH_HOME ——
|
|
215
|
+
// 内容 I/O 一律走 homeFs;宿主没提供就什么都不收集,并如实报码(绝不退回 $DSH_HOME 去读错位置)。
|
|
216
|
+
const homeFs = ctx.homeFs;
|
|
217
|
+
if (homeFs === undefined) {
|
|
218
|
+
this.active = [];
|
|
219
|
+
this.pendingWarnings = [engineMsg('adapter.pluginDataNoOsHome')];
|
|
220
|
+
return PluginDataAdapter.emptyListing();
|
|
221
|
+
}
|
|
222
|
+
const entries = await this.allowedEntries(ctx);
|
|
223
|
+
this.active = entries;
|
|
224
|
+
const warnings: string[] = [];
|
|
225
|
+
const paths: string[] = [];
|
|
226
|
+
const listing = PluginDataAdapter.emptyListing();
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const rel = entry.area.relPath;
|
|
229
|
+
const env = this.envOverrideOf(ctx, entry.area);
|
|
230
|
+
if (env !== null) {
|
|
231
|
+
warnings.push(engineMsg('adapter.pluginDataEnvOverridden', { path: rel, env }));
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (await this.isRegularFile(homeFs, rel)) {
|
|
235
|
+
paths.push(rel);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
let sub: RecursiveListing;
|
|
239
|
+
try {
|
|
240
|
+
sub = await listFilesDetailed(homeFs, rel);
|
|
241
|
+
} catch {
|
|
242
|
+
// 目录不存在 / 读不到 → 视为无内容(存在的空目录与缺失在这里同义:都没有文件可带)
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
listing.skippedLinks.push(...sub.skippedLinks);
|
|
246
|
+
listing.skippedDepDirs.push(...sub.skippedDepDirs);
|
|
247
|
+
listing.unreadableDirs.push(...sub.unreadableDirs);
|
|
248
|
+
listing.followedLinks += sub.followedLinks;
|
|
249
|
+
for (const p of sub.paths) {
|
|
250
|
+
if (isExcludedRel(entry.area, p)) continue;
|
|
251
|
+
paths.push(p);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
listing.paths = paths;
|
|
255
|
+
this.pendingWarnings = warnings;
|
|
256
|
+
return listing;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 单元 = 区域(最长前缀归属)。
|
|
261
|
+
*
|
|
262
|
+
* 用「本轮清单写入的区域表」而不是重新解析:listRelPaths 与本函数在同一次 collect 里成对调用,
|
|
263
|
+
* 重算会让两次解析结果不一致时出现「单元 id 不在清单里」(选择器勾不上)。测试/异常路径下
|
|
264
|
+
* \`active\` 可能是 undefined(例如直接调 listUnits),此时退回**内置来源**的最长前缀 —— 与清单口径同源。
|
|
265
|
+
*/
|
|
266
|
+
protected override unitIdOf(relativePath: string): string {
|
|
267
|
+
const entries = this.active ?? areasForSection(this.builtin, this.id);
|
|
268
|
+
const hit = areaOfPath(entries, normalizePath(relativePath));
|
|
269
|
+
return hit === null ? normalizePath(relativePath) : hit.area.relPath;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 读一个成员文件:**换根到系统 home 门面**(基类的其余逻辑 —— 体积、哈希、字节闸门 —— 一字不动)。
|
|
274
|
+
*
|
|
275
|
+
* 能走到这里说明 listRelPaths 产出过条目(缺门面时它不产出任何路径),因此缺门面即**抛错**,
|
|
276
|
+
* 不再造一个「空文件」出来(那会把缺通道伪装成空内容)。
|
|
277
|
+
*/
|
|
278
|
+
protected override async readMember(
|
|
279
|
+
ctx: HostContext, rel: string, mode: 'content' | 'size', statTimes?: Map<string, number>,
|
|
280
|
+
): Promise<{ data: Uint8Array; sizeBytes: number; contentHash: string }> {
|
|
281
|
+
const homeFs = ctx.homeFs;
|
|
282
|
+
if (homeFs === undefined) throw new Error(engineMsg('adapter.pluginDataNoOsHome'));
|
|
283
|
+
return super.readMember({ ...ctx, fs: homeFs }, rel, mode, statTimes);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override async export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<FilesSection>> {
|
|
287
|
+
this.pendingWarnings = [];
|
|
288
|
+
const out = await super.export(ctx, options);
|
|
289
|
+
return { ...out, warnings: [...out.warnings, ...this.pendingWarnings] };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
override async preview(ctx: HostContext, options: ExportOptions): Promise<SectionPreview<FilesSection>> {
|
|
293
|
+
this.pendingWarnings = [];
|
|
294
|
+
const out = await super.preview(ctx, options);
|
|
295
|
+
return {
|
|
296
|
+
...out,
|
|
297
|
+
section: { ...out.section, warnings: [...out.section.warnings, ...this.pendingWarnings] },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 导入分析 + **写入路径白名单**(本分区唯一的越界防线)。
|
|
303
|
+
*
|
|
304
|
+
* 判据:路径必须是本机可解析的某个区域(内置来源,或已安装插件的自述声明)之下,且未被该区域
|
|
305
|
+
* 的 excludeRel 排除。不满足 → Error(计划里可见、执行期不写),绝不「尽力而为」地写出去。
|
|
306
|
+
*/
|
|
307
|
+
override async analyzeImport(data: FilesSection, ctx: ImportContext): Promise<PlanItem[]> {
|
|
308
|
+
// 宿主没有系统 home 门面(旧宿主)→ 白名单为空 + 目标读也换不了根:**全部拒绝**并说明原因。
|
|
309
|
+
// 绝不退回 $DSH_HOME 的 ctx.fs 去比对/写入(那会落到错误位置)。
|
|
310
|
+
const homeFs = ctx.target.homeFs;
|
|
311
|
+
const entries = homeFs === undefined ? [] : await this.allowedEntries(ctx.target);
|
|
312
|
+
const items = await super.analyzeImport(
|
|
313
|
+
data,
|
|
314
|
+
homeFs === undefined ? ctx : { ...ctx, target: { ...ctx.target, fs: homeFs } },
|
|
315
|
+
);
|
|
316
|
+
return items.map((item) => {
|
|
317
|
+
const ref = item.target?.ref;
|
|
318
|
+
if (ref === undefined) return item;
|
|
319
|
+
const rel = normalizeHomeRel(ref);
|
|
320
|
+
if (rel !== null && areaOfPath(entries, rel) !== null) return item;
|
|
321
|
+
return {
|
|
322
|
+
id: item.id,
|
|
323
|
+
...(item.unitId !== undefined ? { unitId: item.unitId } : {}),
|
|
324
|
+
kind: 'Error',
|
|
325
|
+
adapter: this.id,
|
|
326
|
+
description: engineMsg(
|
|
327
|
+
homeFs === undefined ? 'adapter.pluginDataNoOsHome' : 'adapter.pluginDataPathNotAllowed',
|
|
328
|
+
{ path: ref },
|
|
329
|
+
),
|
|
330
|
+
severity: 'error',
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** 应用前再查一遍白名单(纵深防御:plan 可能来自消费者手工构造/缓存反序列化) */
|
|
336
|
+
override async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
|
|
337
|
+
const homeFs = ctx.target.homeFs;
|
|
338
|
+
if (homeFs === undefined) {
|
|
339
|
+
return { ok: false, message: engineMsg('adapter.pluginDataNoOsHome') };
|
|
340
|
+
}
|
|
341
|
+
const ref = item.target?.ref;
|
|
342
|
+
if (ref !== undefined) {
|
|
343
|
+
const rel = normalizeHomeRel(ref);
|
|
344
|
+
const entries = await this.allowedEntries(ctx.target);
|
|
345
|
+
if (rel === null || areaOfPath(entries, rel) === null) {
|
|
346
|
+
return { ok: false, message: engineMsg('adapter.pluginDataPathNotAllowed', { path: ref }) };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return super.applyItem(item, { ...ctx, target: { ...ctx.target, fs: homeFs } });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { sha256Hex } from '../utils/hashing.ts';
|
|
10
10
|
import { msgOf, zhMsg } from '../core/messages.ts';
|
|
11
|
+
import { engineMsg } from './messages.ts';
|
|
11
12
|
import { linkWarnings, listFilesDetailed } from './link-report.ts';
|
|
12
13
|
import type { RecursiveListing } from '../utils/recursive-walk.ts';
|
|
13
14
|
import { isPathSafe, isReservedInternalRel } from '../utils/paths.ts';
|
|
@@ -36,7 +37,7 @@ export class PluginFilesAdapter implements ConfigAdapter<FilesSection> {
|
|
|
36
37
|
constructor(whitelist: string[] = [...DEFAULT_PLUGIN_FILE_WHITELIST], collectDir?: string) {
|
|
37
38
|
this.whitelist = whitelist;
|
|
38
39
|
if (collectDir !== undefined && collectDir !== '' && !isPathSafe(collectDir)) {
|
|
39
|
-
throw new Error(
|
|
40
|
+
throw new Error(engineMsg('adapter.pluginFilesCollectDirInvalid', { dir: collectDir }));
|
|
40
41
|
}
|
|
41
42
|
this.collectDir = collectDir === '' ? undefined : collectDir;
|
|
42
43
|
}
|
|
@@ -63,7 +64,7 @@ export class PluginFilesAdapter implements ConfigAdapter<FilesSection> {
|
|
|
63
64
|
}
|
|
64
65
|
// 2) 约定配置目录递归收集(相对 ~/.dsh 根的完整路径;与白名单文件去重)
|
|
65
66
|
// issue #37:与 skills 等同一条遍历(跟随 junction/符号链接 + 跳过留痕)
|
|
66
|
-
let listing: RecursiveListing = { paths: [], skippedLinks: [], followedLinks: 0, unreadableDirs: [] };
|
|
67
|
+
let listing: RecursiveListing = { paths: [], skippedLinks: [], skippedDepDirs: [], followedLinks: 0, unreadableDirs: [] };
|
|
67
68
|
if (this.collectDir !== undefined) {
|
|
68
69
|
try {
|
|
69
70
|
listing = await listFilesDetailed(ctx.fs, this.collectDir);
|
package/src/adapters/plugins.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { classifyPluginSpec, LOCAL_PLUGIN_DIR, resolveLocalPluginPath } from '..
|
|
|
18
18
|
import type { PackLocalPluginsResult } from '../core/local-plugin-pack.ts';
|
|
19
19
|
import { msgOf, zhMsg } from '../core/messages.ts';
|
|
20
20
|
import type { MsgFunc } from '../core/messages.ts';
|
|
21
|
+
import { engineMsg } from './messages.ts';
|
|
21
22
|
import { stringifyJsonSafe } from '../utils/json.ts';
|
|
22
23
|
import { isPathSafe, normalizePath } from '../utils/paths.ts';
|
|
23
24
|
import { isENOENT } from '../utils/guards.ts';
|
|
@@ -77,7 +78,7 @@ function tarballOf(data: PluginsSection, pkgName: string): LocalPluginTarball |
|
|
|
77
78
|
export function localTarballCacheName(relativePath: string): string {
|
|
78
79
|
const base = relativePath.split('/').pop() ?? '';
|
|
79
80
|
if (base === '' || base.includes('\\') || base === '.' || base === '..') {
|
|
80
|
-
throw new Error(
|
|
81
|
+
throw new Error(engineMsg('adapter.tarballRelPathInvalid', { path: relativePath }));
|
|
81
82
|
}
|
|
82
83
|
return base;
|
|
83
84
|
}
|
|
@@ -697,7 +698,7 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
697
698
|
names: clean.dropped.map((d) => d.name).join(', '),
|
|
698
699
|
}));
|
|
699
700
|
}
|
|
700
|
-
return { ok: true, needsRestart: true, message: notes.join('
|
|
701
|
+
return { ok: true, needsRestart: true, message: notes.join(msg('adapter.listSeparator')) };
|
|
701
702
|
} catch (err) {
|
|
702
703
|
const reason = err instanceof Error ? err.message : String(err);
|
|
703
704
|
return {
|
|
@@ -730,7 +731,7 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
730
731
|
spec = `file:${abs}`;
|
|
731
732
|
} catch (err) {
|
|
732
733
|
// 解包失败不阻断安装尝试:退回原 spec(可能失败,但会如实报错而非静默丢失)
|
|
733
|
-
ctx.onLog?.(
|
|
734
|
+
ctx.onLog?.(msg('adapter.localTarballUnpackFailed', { name, err: err instanceof Error ? err.message : String(err) }));
|
|
734
735
|
}
|
|
735
736
|
}
|
|
736
737
|
// 执行日志:记录实际将发起的子进程命令行(与宿主 DshPluginsFacade 的
|
package/src/adapters/prompts.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { msgOf, zhMsg } from '../core/messages.ts';
|
|
16
16
|
import type { MsgFunc } from '../core/messages.ts';
|
|
17
|
+
// t6:注册 adapters 域追加目录(本模块只消费注入的 msg,故只引它的注册副作用)。
|
|
18
|
+
import './messages.ts';
|
|
17
19
|
import type { PromptEntry } from '../schema/types.ts';
|
|
18
20
|
import type {
|
|
19
21
|
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
@@ -159,7 +161,11 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
|
|
|
159
161
|
items.push({
|
|
160
162
|
id, kind: 'Conflict', adapter: 'prompts',
|
|
161
163
|
description: msg('adapter.promptDiff', { name: p.name }),
|
|
162
|
-
detail:
|
|
164
|
+
detail: msg('adapter.promptDiffDetail', {
|
|
165
|
+
line: String(p.sourceLineId),
|
|
166
|
+
current: JSON.stringify(sameName.text).slice(0, 80),
|
|
167
|
+
imported: JSON.stringify(p.text).slice(0, 80),
|
|
168
|
+
}),
|
|
163
169
|
severity: 'warning',
|
|
164
170
|
// 层身份随计划项走:快照按这一层记原行、回滚才能写回原文件(issue #71)
|
|
165
171
|
target: { adapter: 'prompts', ref: p.sourceLineId ?? '', file: targetLine.file },
|