dsh-config-manager 0.1.71 → 0.1.72
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 +111 -96
- package/README.zh-CN.md +207 -86
- package/lib/adapters/agent-instructions.js +1 -1
- package/lib/adapters/file-collection.js +1 -1
- package/lib/adapters/index.d.ts +1 -1
- package/lib/adapters/index.js +1 -1
- package/lib/adapters/link-report.d.ts +1 -0
- package/lib/adapters/link-report.js +27 -3
- package/lib/adapters/messages.d.ts +18 -0
- package/lib/adapters/messages.js +50 -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 +7 -1
- package/lib/adapters/self.js +1 -1
- package/lib/adapters/sessions.d.ts +1 -0
- package/lib/adapters/sessions.js +4 -2
- 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 +1349 -490
- package/lib/client.js +43047 -36738
- package/lib/core/backup-plan.js +30 -17
- package/lib/core/backup-verify.js +24 -23
- package/lib/core/backup.d.ts +5 -0
- package/lib/core/backup.js +51 -25
- package/lib/core/boot-rescue.js +4 -3
- package/lib/core/boot-safety.js +2 -1
- package/lib/core/command-rollback.d.ts +178 -0
- package/lib/core/command-rollback.js +289 -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 +292 -0
- package/lib/core/messages-core.js +615 -0
- package/lib/core/messages.d.ts +60 -2
- package/lib/core/messages.js +116 -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/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-ignore-rules.d.ts +31 -0
- package/lib/core/rollback-ignore-rules.js +140 -0
- package/lib/core/rollback-point.d.ts +1208 -0
- package/lib/core/rollback-point.js +2993 -0
- 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 +18 -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.js +7 -6
- 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 +223 -7
- package/lib/index.js +729 -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 +42 -0
- package/lib/routes/messages.js +100 -0
- package/lib/routes/profiles.js +12 -6
- package/lib/routes/recovery.js +378 -114
- package/lib/routes/snapshots.js +51 -4
- package/lib/routes/sync.js +68 -16
- 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 +41 -14
- package/lib/schema/section-registry.js +35 -29
- 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 +37 -15
- package/lib/sync/sync-engine.js +67 -37
- 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/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/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 +24 -20
- package/lib/ui/foreign-view.d.ts +5 -1
- package/lib/ui/foreign-view.js +26 -2
- package/lib/ui/i18n.d.ts +65 -0
- package/lib/ui/i18n.js +114 -0
- 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 +756 -0
- package/lib/ui/rollback-view.js +724 -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/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 +1 -1
- package/src/adapters/link-report.test.ts +22 -2
- package/src/adapters/link-report.ts +29 -4
- package/src/adapters/messages.ts +54 -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 +8 -2
- package/src/adapters/sessions.ts +4 -2
- package/src/adapters/settings.test.ts +38 -1
- 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 +391 -0
- package/src/client/chat/RollbackDiffTab.tsx +61 -0
- package/src/client/chat/RollbackFileIcon.tsx +96 -0
- package/src/client/chat/RollbackReviewDialog.tsx +591 -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 +173 -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 +164 -0
- package/src/client/chat/rollback-entry-view.test.ts +1469 -0
- package/src/client/chat/rollback-entry-view.ts +941 -0
- package/src/client/chat/sidebar-slots.ts +37 -0
- package/src/client/common/ConfirmDialog.tsx +7 -3
- package/src/client/common/ContentPicker.tsx +48 -10
- package/src/client/common/DiffTable.tsx +97 -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 +78 -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 +28 -0
- package/src/client/common/ui.tsx +73 -10
- package/src/client/config-manager.module.css +996 -21
- 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 +186 -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 +54 -5
- 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 +665 -0
- package/src/client/recovery/RollbackDiffPane.tsx +58 -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 +116 -6
- package/src/client/recovery/recovery-locales.test.ts +299 -0
- package/src/client/recovery/recovery-locales.ts +488 -149
- package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
- package/src/client/recovery/rollback-file-changes.test.ts +73 -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/backup-plan.test.ts +28 -0
- package/src/core/backup-plan.ts +29 -17
- package/src/core/backup-verify.ts +24 -23
- package/src/core/backup.test.ts +76 -0
- package/src/core/backup.ts +55 -25
- 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 +402 -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 +657 -0
- package/src/core/messages.ts +123 -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/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-ignore-rules.ts +126 -0
- package/src/core/rollback-point.test.ts +2603 -0
- package/src/core/rollback-point.ts +3755 -0
- 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 +18 -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.ts +7 -6
- 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 +813 -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 +104 -0
- package/src/routes/profiles.ts +12 -6
- package/src/routes/recovery.test.ts +405 -1
- package/src/routes/recovery.ts +377 -109
- package/src/routes/snapshots.ts +51 -4
- package/src/routes/sync.ts +67 -16
- package/src/schema/config.ts +53 -20
- package/src/schema/index.ts +7 -5
- package/src/schema/manifest.ts +19 -18
- package/src/schema/messages.ts +120 -0
- package/src/schema/registry.test.ts +19 -5
- package/src/schema/section-registry.ts +64 -39
- 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-engine-download-tombstone.test.ts +1 -1
- package/src/sync/sync-engine.test.ts +226 -8
- package/src/sync/sync-engine.ts +103 -41
- 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/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/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 +24 -20
- package/src/ui/foreign-view.test.ts +43 -0
- package/src/ui/foreign-view.ts +30 -3
- package/src/ui/i18n.ts +125 -0
- 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 +819 -0
- package/src/ui/rollback-view.ts +1339 -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/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
package/src/adapters/sessions.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { FileCollectionAdapter } from './file-collection.ts';
|
|
11
11
|
import { listFilesDetailed } from './link-report.ts';
|
|
12
12
|
import { msgOf } from '../core/messages.ts';
|
|
13
|
+
// t6:注册 adapters 域追加目录(本模块只消费注入的 msg,故只引它的注册副作用)。
|
|
14
|
+
import './messages.ts';
|
|
13
15
|
import { groupSessionUnits, isSessionLogFileName, normalizeSessionLimit, pickLatestSessionUnits, sessionIdKey } from '../core/session-select.ts';
|
|
14
16
|
import { projectKeyOf } from '../core/session-meta.ts';
|
|
15
17
|
import { applyPathMapping } from '../core/path-mapping.ts';
|
|
@@ -367,7 +369,7 @@ export class SessionsAdapter extends FileCollectionAdapter {
|
|
|
367
369
|
if (present.has(sessionIdKey(next.sessionId))) continue;
|
|
368
370
|
const dirRel = await this.findSessionDir(ctx, next.sessionId, next.key, allKeys);
|
|
369
371
|
if (dirRel === null) {
|
|
370
|
-
uncoupled.push(next.sessionId
|
|
372
|
+
uncoupled.push(msg('adapter.sessionUncoupledNote', { id: next.sessionId, reason: msg('export.sessionRelationNotFound') }));
|
|
371
373
|
continue;
|
|
372
374
|
}
|
|
373
375
|
const listing = await listFilesDetailed(ctx.fs, dirRel);
|
|
@@ -381,7 +383,7 @@ export class SessionsAdapter extends FileCollectionAdapter {
|
|
|
381
383
|
loaded.push({ relativePath: this.relPathOf(rel), data, contentHash: sha256Hex(data) });
|
|
382
384
|
}
|
|
383
385
|
if (tooLarge || loaded.length === 0) {
|
|
384
|
-
uncoupled.push(next.sessionId
|
|
386
|
+
uncoupled.push(msg('adapter.sessionUncoupledNote', { id: next.sessionId, reason: msg('export.sessionParentTooLarge') }));
|
|
385
387
|
continue;
|
|
386
388
|
}
|
|
387
389
|
usedBytes += unitBytes;
|
|
@@ -6,7 +6,8 @@ import test from 'node:test';
|
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
7
|
import { SettingsAdapter } from './settings.ts';
|
|
8
8
|
import { makeContext, makeImportContext } from './test-helpers.ts';
|
|
9
|
-
import type { PlanItem } from '../core/types.ts';
|
|
9
|
+
import type { PlanItem, SensitiveHit } from '../core/types.ts';
|
|
10
|
+
import { createSecretScanner } from '../security/secret-scanner.ts';
|
|
10
11
|
|
|
11
12
|
const NS = ['general', 'theme', 'llm-deepseek'];
|
|
12
13
|
|
|
@@ -176,3 +177,39 @@ test('settings: describeAll 抛错时退回逐名路径(快路径失败不得
|
|
|
176
177
|
assert.deepEqual(out.warnings, []);
|
|
177
178
|
});
|
|
178
179
|
|
|
180
|
+
test('settings: value/base 共享同一嵌套对象不得触发强化 scanner 的循环误报(用户上报 2026-10-09)', async () => {
|
|
181
|
+
const ctx = makeContext('win32', 'C:\\Users\\alice');
|
|
182
|
+
// 真实 DSH 的 describe() 同时回传 value(生效值)与 base(组合层):未被用户覆盖的嵌套分组在两处是
|
|
183
|
+
// 同一个对象 → 导出结构里出现 DAG 共享引用。旧版全局 visited 判环会把它读成
|
|
184
|
+
// 「检测到循环引用,无法扫描(路径 namespaces.<ns>.value.<key>)」,整次备份失败。
|
|
185
|
+
const dream = { enabled: true, idleMinutes: 180, apiKey: 'sk-live-abcdefgh12345' };
|
|
186
|
+
ctx.settings.ns.set('meow-memory', {
|
|
187
|
+
value: { promptLang: 'zh', dream },
|
|
188
|
+
base: { promptLang: 'zh', dream },
|
|
189
|
+
revision: 3,
|
|
190
|
+
secrets: [],
|
|
191
|
+
});
|
|
192
|
+
const adapter = new SettingsAdapter(['meow-memory']);
|
|
193
|
+
const exported = await adapter.export(ctx, { includeSecrets: false });
|
|
194
|
+
const rec = exported.data.namespaces['meow-memory'];
|
|
195
|
+
// 前置条件:导出的 value/base 确实共享同一个对象(否则这条测试没覆盖到目标形态)
|
|
196
|
+
assert.strictEqual(
|
|
197
|
+
(rec?.value as { dream?: unknown } | undefined)?.dream,
|
|
198
|
+
(rec?.base as { dream?: unknown } | undefined)?.dream,
|
|
199
|
+
'value/base 必须共享同一 dream 对象',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const scanner = createSecretScanner();
|
|
203
|
+
let scanned: { sanitized: unknown; hits: SensitiveHit[] } | undefined;
|
|
204
|
+
assert.doesNotThrow(() => { scanned = scanner.scanAndRedact(exported.data); }, '共享引用不得被误判为循环');
|
|
205
|
+
const outRec = (scanned!.sanitized as {
|
|
206
|
+
namespaces: Record<string, { value: { dream: Record<string, unknown> }; base: { dream: Record<string, unknown> } }>;
|
|
207
|
+
}).namespaces['meow-memory']!;
|
|
208
|
+
// 两个分支都要在,且各自完成脱敏(不能因为共享就丢分支或漏剥)
|
|
209
|
+
assert.equal(outRec.value.dream['apiKey'], '', 'value 分支脱敏');
|
|
210
|
+
assert.equal(outRec.base.dream['apiKey'], '', 'base 分支脱敏');
|
|
211
|
+
assert.equal(outRec.value.dream['idleMinutes'], 180, '非敏感字段保留');
|
|
212
|
+
assert.ok(scanned!.hits.some((h) => h.path === 'namespaces.meow-memory.value.dream.apiKey'));
|
|
213
|
+
assert.ok(scanned!.hits.some((h) => h.path === 'namespaces.meow-memory.base.dream.apiKey'));
|
|
214
|
+
});
|
|
215
|
+
|
package/src/adapters/ui.test.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import test from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
|
-
import { UiAdapter, isUiNamespace,
|
|
6
|
+
import { UiAdapter, isUiNamespace, uiMigrationNotes } from './ui.ts';
|
|
7
7
|
import { makeContext, makeImportContext } from './test-helpers.ts';
|
|
8
8
|
|
|
9
9
|
const NS = ['general', 'theme', 'pet', 'dsh-better-sidebar', 'llm-deepseek'];
|
|
@@ -32,7 +32,7 @@ test('ui: 导出只含 UI namespace + uiMigrationNotes(纯说明)', async ()
|
|
|
32
32
|
assert.equal(out.counts.namespaces, 2);
|
|
33
33
|
assert.ok(out.data.uiMigrationNotes.length >= 1);
|
|
34
34
|
assert.equal(out.data.uiMigrationNotes[0]?.migratable, false);
|
|
35
|
-
assert.equal(
|
|
35
|
+
assert.equal(uiMigrationNotes().length, out.data.uiMigrationNotes.length);
|
|
36
36
|
|
|
37
37
|
const v = await adapter.validate(out.data);
|
|
38
38
|
assert.equal(v.valid, true);
|
package/src/adapters/ui.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* 数据源 = settings.yaml 中「UI 类」namespace(pet / dsh-better-sidebar / remote-web-ui / ui-onboarding / skin 等)。
|
|
4
4
|
* localStorage 等 Host 不可迁移的 UI 状态以 uiMigrationNotes 纯说明形式导出(不含任何值)。
|
|
5
5
|
*/
|
|
6
|
-
import type { NamespaceRecord, UiSection } from '../schema/types.ts';
|
|
6
|
+
import type { NamespaceRecord, UiMigrationNote, UiSection } from '../schema/types.ts';
|
|
7
7
|
import { zhMsg } from '../core/messages.ts';
|
|
8
8
|
import type { MsgFunc } from '../core/messages.ts';
|
|
9
|
+
import { engineMsg } from './messages.ts';
|
|
9
10
|
import type {
|
|
10
11
|
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
11
12
|
ImportContext, PlanItem, ValidationResult,
|
|
@@ -34,31 +35,30 @@ export function isUiNamespace(name: string): boolean {
|
|
|
34
35
|
return KNOWN_UI_NAMESPACE_PREFIXES.some((p) => lower.startsWith(p.toLowerCase()));
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
storage: 'localStorage',
|
|
49
|
-
key: 'aionui-panel.*',
|
|
50
|
-
migratable: false,
|
|
51
|
-
reason: '面板宽度/折叠状态存于浏览器 localStorage,Host 侧无访问通道,需在目标机器重新配置',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
plugin: 'dsh-client-ui-*',
|
|
55
|
-
storage: 'localStorage',
|
|
56
|
-
key: '*',
|
|
57
|
-
migratable: false,
|
|
58
|
-
reason: '浏览器端 UI 状态无法经 Host API 迁移',
|
|
59
|
-
},
|
|
38
|
+
/**
|
|
39
|
+
* localStorage 等 Host 无通道的 UI 状态说明(纯说明,不含值;研究报告 §2.2/§4.4)。
|
|
40
|
+
*
|
|
41
|
+
* t6(英文界面零中文):说明行的 `reason` 是用户可见文案 ⇒ 表里只放**目录键**,取文案在导出期现取
|
|
42
|
+
* (`uiMigrationNotes(msg)`)—— 这样导出的说明行跟随界面语言,而不是冻结在装配期语言。
|
|
43
|
+
* (表本身是数据;中文只存在于 adapters 域的追加目录 `src/adapters/messages.ts`。)
|
|
44
|
+
*/
|
|
45
|
+
const UI_MIGRATION_NOTE_ROWS: ReadonlyArray<Omit<UiMigrationNote, 'reason'> & { reasonKey: string }> = [
|
|
46
|
+
{ plugin: 'dsh-task-board', storage: 'localStorage', key: 'dsh.taskBoard.v1', migratable: false, reasonKey: 'adapter.uiLocalStorageTaskBoard' },
|
|
47
|
+
{ plugin: 'dsh-aionui-panel', storage: 'localStorage', key: 'aionui-panel.*', migratable: false, reasonKey: 'adapter.uiLocalStoragePanel' },
|
|
48
|
+
{ plugin: 'dsh-client-ui-*', storage: 'localStorage', key: '*', migratable: false, reasonKey: 'adapter.uiLocalStorageGeneric' },
|
|
60
49
|
];
|
|
61
50
|
|
|
51
|
+
/** UI 迁移说明行(取文案在调用期现取;缺省 = 当前引擎语言的 engineMsg)。 */
|
|
52
|
+
export function uiMigrationNotes(msg: MsgFunc = engineMsg): UiSection['uiMigrationNotes'] {
|
|
53
|
+
return UI_MIGRATION_NOTE_ROWS.map((row) => ({
|
|
54
|
+
plugin: row.plugin,
|
|
55
|
+
storage: row.storage,
|
|
56
|
+
key: row.key,
|
|
57
|
+
migratable: row.migratable,
|
|
58
|
+
reason: msg(row.reasonKey),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
62
|
export class UiAdapter implements ConfigAdapter<UiSection> {
|
|
63
63
|
readonly id = 'ui' as const;
|
|
64
64
|
// 元数据唯一来源 = 注册表(t31):不再与 ui/export-flow.ts 的导出目录各写一份
|
|
@@ -76,10 +76,11 @@ export class UiAdapter implements ConfigAdapter<UiSection> {
|
|
|
76
76
|
const all = await resolveNamespaces(this.namespaces, ctx);
|
|
77
77
|
const uiNames = all.filter((n) => isUiNamespace(n));
|
|
78
78
|
const namespaces: Record<string, NamespaceRecord> = await collectNamespaceRecords(ctx, uiNames, warnings);
|
|
79
|
+
const notes = uiMigrationNotes(ctx.msg);
|
|
79
80
|
return {
|
|
80
81
|
sectionId: 'ui',
|
|
81
|
-
data: { version: 1, namespaces, uiMigrationNotes:
|
|
82
|
-
counts: { namespaces: Object.keys(namespaces).length, notes:
|
|
82
|
+
data: { version: 1, namespaces, uiMigrationNotes: notes },
|
|
83
|
+
counts: { namespaces: Object.keys(namespaces).length, notes: notes.length },
|
|
83
84
|
warnings,
|
|
84
85
|
};
|
|
85
86
|
}
|
|
@@ -8,6 +8,8 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
8
8
|
import { normalizePath } from '../utils/paths.ts';
|
|
9
9
|
import { zhMsg } from '../core/messages.ts';
|
|
10
10
|
import type { MsgFunc } from '../core/messages.ts';
|
|
11
|
+
// t6:注册 adapters 域追加目录(本模块只消费注入的 msg,故只引它的注册副作用)。
|
|
12
|
+
import './messages.ts';
|
|
11
13
|
import type { WorkspaceRecord, WorkspacesSection } from '../schema/types.ts';
|
|
12
14
|
import type {
|
|
13
15
|
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext,
|
|
@@ -214,14 +216,16 @@ export class WorkspacesAdapter implements ConfigAdapter<WorkspacesSection> {
|
|
|
214
216
|
// 目标端无法写入(如路径 realpath 失败/目录不存在)→ 非致命警告(§34.17),
|
|
215
217
|
// 不触发整体回滚——否则一个失效路径会拖垮已成功导入的其余配置。
|
|
216
218
|
const reason = err instanceof Error ? err.message : String(err);
|
|
217
|
-
const detail = notes.length > 0
|
|
219
|
+
const detail = notes.length > 0
|
|
220
|
+
? ctx.msg('adapter.workspaceNotesParen', { notes: notes.join(ctx.msg('adapter.listSeparator')) })
|
|
221
|
+
: '';
|
|
218
222
|
return {
|
|
219
223
|
ok: false,
|
|
220
224
|
warning: true,
|
|
221
225
|
message: ctx.msg('adapter.workspaceWriteFailed', { ref, msg: reason }) + detail,
|
|
222
226
|
};
|
|
223
227
|
}
|
|
224
|
-
return { ok: true, ...(notes.length > 0 ? { message: notes.join('
|
|
228
|
+
return { ok: true, ...(notes.length > 0 ? { message: notes.join(ctx.msg('adapter.listSeparator')) } : {}) };
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
/**
|
|
@@ -431,7 +435,7 @@ async function registerImportedSessions(
|
|
|
431
435
|
const outsideNote = outsideFailed > 0
|
|
432
436
|
? ctx.msg('adapter.workspaceSessionsOutsideBundle', { count: String(outsideFailed) })
|
|
433
437
|
: '';
|
|
434
|
-
const compose = (head: string): string => (outsideNote === '' || head === '' ? head + outsideNote : head + '
|
|
438
|
+
const compose = (head: string): string => (outsideNote === '' || head === '' ? head + outsideNote : head + ctx.msg('adapter.listSeparator') + outsideNote);
|
|
435
439
|
if (bundledFailed > 0) {
|
|
436
440
|
return {
|
|
437
441
|
message: compose(ctx.msg('adapter.workspaceSessionsPending', {
|
package/src/cli/actions.test.ts
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
sessionRepairRowMessage, sessionVerifyNoteOf, stopProfile, unlockEncryptedBackup, type SessionLogRepairRow,
|
|
21
21
|
} from './actions.ts'
|
|
22
22
|
import type { SessionVerifyResult } from '../utils/session-verify.ts'
|
|
23
|
+
import { parseZip } from '../utils/zip.ts'
|
|
24
|
+
import { MANIFEST_FILE } from '../schema/manifest.ts'
|
|
23
25
|
|
|
24
26
|
async function withTmp<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
25
27
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dcm-actions-'))
|
|
@@ -400,6 +402,32 @@ test('V4-01 离线导出:真的产出 ZIP 并自检通过;不可收集的分
|
|
|
400
402
|
})
|
|
401
403
|
})
|
|
402
404
|
|
|
405
|
+
test('V4-01b 离线导出的 manifest.exporter.version 用真实版本(不再写死 rescue-console)', async () => {
|
|
406
|
+
await withTmp(async (tmp) => {
|
|
407
|
+
const home = path.join(tmp, 'home')
|
|
408
|
+
await fs.mkdir(path.join(home, 'skills', 'demo'), { recursive: true })
|
|
409
|
+
await fs.writeFile(path.join(home, 'skills', 'demo', 'SKILL.md'), '# demo\n')
|
|
410
|
+
const dataDir = path.join(home, 'dsh-config-manager')
|
|
411
|
+
const paths = {
|
|
412
|
+
homeDir: home, dataDir,
|
|
413
|
+
snapshotsDir: path.join(dataDir, 'snapshots'),
|
|
414
|
+
exportsDir: path.join(dataDir, 'exports'),
|
|
415
|
+
locksDir: path.join(dataDir, 'locks'),
|
|
416
|
+
controlRoots: [dataDir],
|
|
417
|
+
profile: 'web',
|
|
418
|
+
exporterVersion: '9.9.9-test',
|
|
419
|
+
}
|
|
420
|
+
const result = await exportOfflineBackup(paths, ['skills'])
|
|
421
|
+
assert.equal(result.ok, true, result.message)
|
|
422
|
+
const buf = await fs.readFile(result.outPath!)
|
|
423
|
+
const manifest = JSON.parse(
|
|
424
|
+
Buffer.from(parseZip(buf).readEntry(MANIFEST_FILE)).toString('utf8'),
|
|
425
|
+
) as { exporter?: { name?: string; version?: string } }
|
|
426
|
+
assert.equal(manifest.exporter?.version, '9.9.9-test', 'manifest 必须写调用方注入的真实版本')
|
|
427
|
+
assert.notEqual(manifest.exporter?.version, 'rescue-console', '不得再写死假的导出器版本')
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
|
|
403
431
|
test('V4-02 解锁:非加密文件如实报 not-encrypted(绝不谎报解锁成功)', async () => {
|
|
404
432
|
await withTmp(async (tmp) => {
|
|
405
433
|
const exportsDir = path.join(tmp, 'exports')
|
package/src/cli/actions.ts
CHANGED
|
@@ -306,6 +306,12 @@ export interface RescuePaths {
|
|
|
306
306
|
locksDir: string
|
|
307
307
|
controlRoots: readonly string[]
|
|
308
308
|
profile: string
|
|
309
|
+
/**
|
|
310
|
+
* 离线导出的 manifest.exporter.version(CLI 传真实版本;缺省 = 中性占位)。
|
|
311
|
+
* 为什么不在这里再算一次版本:actions.ts 不得 import ./index.ts(会成环),
|
|
312
|
+
* 而版本号只有 CLI 侧从 package.json 读得到 —— 由 runWeb 经本结构注入。
|
|
313
|
+
*/
|
|
314
|
+
exporterVersion?: string
|
|
309
315
|
}
|
|
310
316
|
|
|
311
317
|
export interface RescueStatus {
|
|
@@ -419,7 +425,9 @@ export type WriteGateOutcome = { ok: true } | WriteGateRefusal
|
|
|
419
425
|
/**
|
|
420
426
|
* 写动作的前置门(与 CLI `sessions repair --fix` 同一判据)。
|
|
421
427
|
*
|
|
422
|
-
* `needsDshStopped
|
|
428
|
+
* `needsDshStopped`:**改写/搬动会话字节**的动作(会话布局归位 repairSessions、字节级就地修复
|
|
429
|
+
* repairSessionLogInline)与**快照恢复**(runSnapshotRestore 会覆盖/删除 `$DSH_HOME` 下正在使用的文件)
|
|
430
|
+
* 都要「DSH 已停」这一条;磁盘清理不需要(只碰可重建缓存)。
|
|
423
431
|
*/
|
|
424
432
|
export async function checkWriteGates(
|
|
425
433
|
controlRoots: readonly string[],
|
|
@@ -1356,7 +1364,8 @@ export async function exportOfflineBackup(
|
|
|
1356
1364
|
}
|
|
1357
1365
|
const checksums = buildChecksums(collection.entries)
|
|
1358
1366
|
const manifest = buildManifest({
|
|
1359
|
-
|
|
1367
|
+
// 真实版本(runWeb 经 RescuePaths 注入);拿不到时给中性占位,绝不谎报某个具体版本。
|
|
1368
|
+
exporterVersion: paths.exporterVersion ?? '0.0.0-unknown',
|
|
1360
1369
|
// 离线探测不到真实 DSH 版本:如实标注,不谎报
|
|
1361
1370
|
dshVersion: 'cli-offline',
|
|
1362
1371
|
platform: process.platform as never,
|
package/src/cli/help.ts
CHANGED
|
@@ -155,7 +155,7 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
155
155
|
title: '离线救急台(本机网页)',
|
|
156
156
|
usage: 'web',
|
|
157
157
|
summary: '离线救急台:只读诊断 + 逐项确认的修复',
|
|
158
|
-
usageLines: ['web [--port <n>] [--no-open] [--home <dir>] [--data-root <dir>] [--idle-timeout <min>]'],
|
|
158
|
+
usageLines: ['web [--port <n>] [--no-open] [--home <dir>] [--data-dir <dir>] [--data-root <dir>] [--profile <name>] [--idle-timeout <min>]'],
|
|
159
159
|
description: [
|
|
160
160
|
'启动一个只绑 127.0.0.1 的本机网页,把诊断与修复动作摆成能点的界面:',
|
|
161
161
|
'实例心跳 / SAFE MODE / 残留锁 / 快照 / 备份自检 / 磁盘占用 / 会话体检 / 档案实例。',
|
|
@@ -171,7 +171,9 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
171
171
|
{ flag: '--no-open', desc: '启动后不打开浏览器(SSH / 无桌面场景)' },
|
|
172
172
|
{ flag: '--idle-timeout <min>', desc: '空闲多少分钟自动退出(0 = 不自动退出;缺省 30)' },
|
|
173
173
|
{ flag: '--home <dir>', desc: '要查看的 DSH home(缺省 $DSH_HOME,即 ~/.dsh)' },
|
|
174
|
-
{ flag: '--data-
|
|
174
|
+
{ flag: '--data-dir <dir>', desc: '快照 / 导出目录(历史语义):尾段是 snapshots / exports 时按它的父目录当数据根,再派生 快照/导出/锁 三个目录;否则整条按数据根处理(缺省 $DSH_HOME/dsh-config-manager)' },
|
|
175
|
+
{ flag: '--data-root <dir>', desc: '插件数据根 dataDir(自定义时必须显式传;给了就以它为准)' },
|
|
176
|
+
{ flag: '--profile <name>', desc: '救急台操作的 DSH profile(缺省 web;影响「恢复」页的目标档案)' },
|
|
175
177
|
{ flag: '-h, --help', desc: '显示本页' },
|
|
176
178
|
],
|
|
177
179
|
},
|
|
@@ -195,6 +197,7 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
195
197
|
'快照是每次导入前自动留下的整机配置副本;本命令只读,按新→旧列出:',
|
|
196
198
|
'ID / 创建时间 / 来源 ZIP / 状态 / 条目数 / 宿主文件数 / 插件数。',
|
|
197
199
|
'没有任何快照时打印「无快照」并照常返回 0。',
|
|
200
|
+
'目录存在却读不出来(权限 / 路径被文件占住)时,把事实打到 stderr 并以非 0 退出 —— 读不到 ≠ 没有快照。',
|
|
198
201
|
],
|
|
199
202
|
sections: [
|
|
200
203
|
{
|
|
@@ -204,7 +207,13 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
204
207
|
{ flag: '-h, --help', desc: '显示本页' },
|
|
205
208
|
],
|
|
206
209
|
},
|
|
207
|
-
{
|
|
210
|
+
{
|
|
211
|
+
title: '退出码',
|
|
212
|
+
entries: [
|
|
213
|
+
{ flag: '0', desc: '列出成功(目录不存在、没有快照也是 0)' },
|
|
214
|
+
{ flag: '1', desc: '快照目录存在但读不出来(≠ 没有快照;原因打到 stderr)' },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
208
217
|
],
|
|
209
218
|
examples: ['dcm snapshots', 'dcm snapshots --data-dir D:/rescue/snapshots'],
|
|
210
219
|
},
|
|
@@ -213,7 +222,7 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
213
222
|
title: '只读自检备份 ZIP',
|
|
214
223
|
usage: 'verify [<file|path>]',
|
|
215
224
|
summary: '只读自检备份 ZIP(无参 = 导出目录全部)',
|
|
216
|
-
usageLines: ['verify [<file|path>] [--json] [--data-dir <dir>]'],
|
|
225
|
+
usageLines: ['verify [<file|path> | --id <file|path>] [--json] [--data-dir <dir>]'],
|
|
217
226
|
description: [
|
|
218
227
|
'重新读取备份 ZIP,核对结构与 integrity/checksums.json 的逐条 SHA-256;全程零写入。',
|
|
219
228
|
'无参数时校验导出目录下全部 *.zip;也可以给一个文件名或完整路径。',
|
|
@@ -234,6 +243,7 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
234
243
|
title: '选项',
|
|
235
244
|
entries: [
|
|
236
245
|
{ flag: '--json', desc: '机器可读输出(退出码仍为 0/1;出错的 JSON 也走 stdout)' },
|
|
246
|
+
{ flag: '--id <file|path>', desc: '待校验目标(文件名或路径);与位置参数同义,同时给出时以 --id 为准' },
|
|
237
247
|
{ flag: '--data-dir <dir>', desc: '导出目录(缺省 $DSH_HOME/dsh-config-manager/exports)' },
|
|
238
248
|
{ flag: '-h, --help', desc: '显示本页' },
|
|
239
249
|
],
|
|
@@ -330,8 +340,9 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
330
340
|
description: [
|
|
331
341
|
'把配置恢复到该快照的状态:每个被覆盖或被删除的文件先拷到 <快照目录>/pre-restore/,以便反悔。',
|
|
332
342
|
'不带 --id 时挑最近一个非 rolled-back 的快照;--dry-run 只打印计划,零写入。',
|
|
333
|
-
'
|
|
334
|
-
' ① SAFE MODE 存在未恢复的 transaction;②
|
|
343
|
+
'真正执行前有三道门,任一不过就拒绝(没有 --force):',
|
|
344
|
+
' ① SAFE MODE 存在未恢复的 transaction;② 环境锁被别的任务持有;',
|
|
345
|
+
' ③ 本机有 DSH 实例在跑(恢复会覆盖/删除它正在使用的文件)——关掉 DSH,或改用 dcm web 救急台。',
|
|
335
346
|
],
|
|
336
347
|
sections: [
|
|
337
348
|
{
|
|
@@ -350,7 +361,7 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
350
361
|
title: '退出码',
|
|
351
362
|
entries: [
|
|
352
363
|
{ flag: '0', desc: '全部动作成功(--dry-run 恒 0)' },
|
|
353
|
-
{ flag: '1', desc: '有失败步骤 / 没有可用快照 / id 非法 / SAFE MODE
|
|
364
|
+
{ flag: '1', desc: '有失败步骤 / 没有可用快照 / 快照目录读不出来 / id 非法 / SAFE MODE、环境锁或「DSH 在跑」拒绝' },
|
|
354
365
|
],
|
|
355
366
|
},
|
|
356
367
|
],
|
|
@@ -374,6 +385,8 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
374
385
|
'--map old=new 用于跨机恢复:命中前缀会先改写首帧 cwd,其余帧逐字节保留。',
|
|
375
386
|
'--keep <dir> 处理重复 id:点名保留哪一份,其余移进 sessions/.cm-repair-quarantine-<时间戳>/(只搬不删)。',
|
|
376
387
|
'写盘前会检查 DSH 是否在跑;在跑则拒绝,避免边读边改。',
|
|
388
|
+
'--json 的 ok 与退出码同源:0(无阻断级问题)时为 true,否则 false —— 两者不会互相矛盾。',
|
|
389
|
+
'会话根不存在或存在却读不出来时,repair 都如实报错并非 0 退出(读不到 ≠ 没有会话)。',
|
|
377
390
|
],
|
|
378
391
|
sections: [
|
|
379
392
|
{
|
|
@@ -398,9 +411,9 @@ export const COMMAND_HELP: readonly CommandHelp[] = [
|
|
|
398
411
|
{
|
|
399
412
|
title: '退出码',
|
|
400
413
|
entries: [
|
|
401
|
-
{ flag: '0', desc: '无阻断问题;repair 的 dry-run 恒 0' },
|
|
402
|
-
{ flag: '1', desc: '
|
|
403
|
-
{ flag: '2', desc: 'list / doctor
|
|
414
|
+
{ flag: '0', desc: '无阻断问题;repair 的 dry-run 恒 0(--json 的 ok 同时为 true)' },
|
|
415
|
+
{ flag: '1', desc: '有阻断问题(--json 的 ok 为 false);repair 的 --fix 有失败/冲突/回滚;repair 的会话根不存在或读不出来;参数错误' },
|
|
416
|
+
{ flag: '2', desc: 'list / doctor 找不到会话根目录(repair 的同一情形是 1)' },
|
|
404
417
|
],
|
|
405
418
|
},
|
|
406
419
|
],
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 守卫单测(本轮修复的四条纪律,全部是「不能悄悄放过」的行为):
|
|
3
|
+
* ① 参数白名单:verify 不再悄悄接受 --dry-run / --profile / --settings;
|
|
4
|
+
* ② snapshots / restore 的「快照目录读不出来」必须区别于「没有快照」(非 0 退出 + stderr);
|
|
5
|
+
* ③ restore 的第三道写入门:本机有 DSH 在跑 → 拒绝(且 dry-run 不受影响);
|
|
6
|
+
* ④ web 的四个目录从**同一个 root** 派生(--data-dir 尾段命中 snapshots/exports 时上溯一层)。
|
|
7
|
+
*/
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { parseCli, resolveWebPaths, runCli, type CliIo } from './index.ts';
|
|
15
|
+
|
|
16
|
+
async function withTmp<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
17
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-cm-cli-guards-'));
|
|
18
|
+
try {
|
|
19
|
+
return await fn(dir);
|
|
20
|
+
} finally {
|
|
21
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function captureIo(): { io: CliIo; out: string[] } {
|
|
26
|
+
const out: string[] = [];
|
|
27
|
+
const io: CliIo = { log: (s) => out.push(s), error: (s) => out.push(s) };
|
|
28
|
+
return { io, out };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ---------------------------------------------------------------- ① 参数白名单 */
|
|
32
|
+
|
|
33
|
+
test('G-01 verify 必须拒绝 --dry-run / --profile / --settings(不得悄悄接受并忽略)', () => {
|
|
34
|
+
for (const args of [
|
|
35
|
+
['verify', '--dry-run'],
|
|
36
|
+
['verify', '--profile', 'dev'],
|
|
37
|
+
['verify', '--settings', '/s/settings.yaml'],
|
|
38
|
+
]) {
|
|
39
|
+
assert.equal(parseCli(args).ok, false, '应拒绝: ' + args.join(' '));
|
|
40
|
+
}
|
|
41
|
+
// --id 是 verify 位置参数的等价写法(见 help 的 verify 段),必须继续可用。
|
|
42
|
+
const byId = parseCli(['verify', '--id', 'backup.zip']);
|
|
43
|
+
assert.ok(byId.ok);
|
|
44
|
+
assert.equal(byId.options.id, 'backup.zip');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('G-02 其它命令也按「真正用到才放行」拒绝 restore 系参数', () => {
|
|
48
|
+
assert.equal(parseCli(['snapshots', '--profile', 'dev']).ok, false, 'snapshots 不用 --profile');
|
|
49
|
+
assert.equal(parseCli(['snapshots', '--settings', '/s']).ok, false, 'snapshots 不用 --settings');
|
|
50
|
+
assert.equal(parseCli(['snapshots', '--dry-run']).ok, false, 'snapshots 不用 --dry-run');
|
|
51
|
+
// 反向:真正用到这些参数的命令必须继续放行(不能为了「统一拒绝」把它们一起打死)。
|
|
52
|
+
assert.ok(parseCli(['reinstall', '--dry-run']).ok, 'reinstall 真的用 --dry-run');
|
|
53
|
+
assert.ok(parseCli(['backup', '--dry-run']).ok, 'backup 真的用 --dry-run');
|
|
54
|
+
assert.ok(parseCli(['import', '--from', 'claude-code', '--dry-run']).ok, 'import 真的用 --dry-run');
|
|
55
|
+
const web = parseCli(['web', '--profile', 'dev']);
|
|
56
|
+
assert.ok(web.ok, 'web 真的用 --profile(救急台「恢复」页的目标档案)');
|
|
57
|
+
assert.equal(web.options.profile, 'dev');
|
|
58
|
+
assert.ok(parseCli(['restore', '--id', 'x', '--dry-run', '--profile', 'dev', '--settings', '/s']).ok, 'restore 全都要');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/* ---------------------------------------------------------------- ② 读不到 ≠ 没有 */
|
|
62
|
+
|
|
63
|
+
test('G-03 snapshots:快照目录读不出来 → stderr 说明 + 非 0 退出(绝不显示成「无快照」)', async () => {
|
|
64
|
+
await withTmp(async (tmp) => {
|
|
65
|
+
// 用一个**普通文件**占住快照目录路径:readdir 必失败(ENOTDIR),且不是「目录不存在」。
|
|
66
|
+
const dataDir = path.join(tmp, 'snapshots-as-file');
|
|
67
|
+
await fs.writeFile(dataDir, 'not a directory', 'utf8');
|
|
68
|
+
const { io, out } = captureIo();
|
|
69
|
+
const code = await runCli(['snapshots', '--data-dir', dataDir], io, { DSH_HOME: path.join(tmp, 'home') });
|
|
70
|
+
assert.equal(code, 1, '读不出来必须非 0 退出');
|
|
71
|
+
const text = out.join('\n');
|
|
72
|
+
assert.match(text, /快照目录读不出来/, '必须把事实打到 stderr');
|
|
73
|
+
assert.doesNotMatch(text, /无快照/, '不得显示成「没有快照」');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('G-04 restore:默认挑快照时目录读不出来 → 如实打印 + 非 0 退出', async () => {
|
|
78
|
+
await withTmp(async (tmp) => {
|
|
79
|
+
const dataDir = path.join(tmp, 'snapshots-as-file');
|
|
80
|
+
await fs.writeFile(dataDir, 'not a directory', 'utf8');
|
|
81
|
+
const { io, out } = captureIo();
|
|
82
|
+
const code = await runCli(['restore', '--data-dir', dataDir], io, { DSH_HOME: path.join(tmp, 'home') });
|
|
83
|
+
assert.equal(code, 1);
|
|
84
|
+
assert.match(out.join('\n'), /快照目录读不出来/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('G-05 没有快照(目录不存在)仍照旧:snapshots 返回 0 并打印「无快照」', async () => {
|
|
89
|
+
await withTmp(async (tmp) => {
|
|
90
|
+
const { io, out } = captureIo();
|
|
91
|
+
const code = await runCli(['snapshots', '--data-dir', path.join(tmp, 'missing')], io, { DSH_HOME: path.join(tmp, 'home') });
|
|
92
|
+
assert.equal(code, 0, '目录不存在 ≠ 读不出来(core 的 listSnapshots 只有 ENOENT 才算没有)');
|
|
93
|
+
assert.match(out.join('\n'), /无快照/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/* ---------------------------------------------------------------- ③ restore 的第三道门 */
|
|
98
|
+
|
|
99
|
+
/** 造一个「可恢复」的最小快照 + 当前 home 文件。 */
|
|
100
|
+
async function seedRestorableSnapshot(tmp: string): Promise<{ homeDir: string; dataDir: string; id: string }> {
|
|
101
|
+
const homeDir = path.join(tmp, 'home');
|
|
102
|
+
const dataDir = path.join(tmp, 'snapshots');
|
|
103
|
+
const id = 'snap-guard-1';
|
|
104
|
+
const dir = path.join(dataDir, id);
|
|
105
|
+
await fs.mkdir(path.join(dir, 'blobs', 'host'), { recursive: true });
|
|
106
|
+
await fs.writeFile(path.join(dir, 'blobs', 'host', 'settings'), 'SNAPSHOT settings\n', 'utf8');
|
|
107
|
+
await fs.writeFile(path.join(dir, 'snapshot.json'), JSON.stringify({
|
|
108
|
+
id, createdAt: '2026-08-14T12:00:00.000Z', sourceZip: 'backup.zip', status: 'done',
|
|
109
|
+
entries: [],
|
|
110
|
+
hostFileBackups: [{ relPath: 'settings.yaml', blobPath: 'blobs/host/settings', existed: true }],
|
|
111
|
+
beforePlugins: [],
|
|
112
|
+
}), 'utf8');
|
|
113
|
+
await fs.mkdir(homeDir, { recursive: true });
|
|
114
|
+
await fs.writeFile(path.join(homeDir, 'settings.yaml'), 'CURRENT settings\n', 'utf8');
|
|
115
|
+
// 心跳落在候选控制根之一(--data-dir 本身)下:pid 用本进程 → 判定为「正在跑」
|
|
116
|
+
await fs.mkdir(path.join(dataDir, 'running'), { recursive: true });
|
|
117
|
+
return { homeDir, dataDir, id };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test('G-06 restore:本机有 DSH 在跑 → 拒绝执行(与救急台 checkWriteGates 同源)', async () => {
|
|
121
|
+
await withTmp(async (tmp) => {
|
|
122
|
+
const { homeDir, dataDir, id } = await seedRestorableSnapshot(tmp);
|
|
123
|
+
await fs.writeFile(path.join(dataDir, 'running', 'web.json'), JSON.stringify({
|
|
124
|
+
schemaVersion: 1, name: 'web', pid: process.pid, port: 3099,
|
|
125
|
+
startedAt: new Date().toISOString(), updatedAt: Date.now(),
|
|
126
|
+
}), 'utf8');
|
|
127
|
+
|
|
128
|
+
const { io, out } = captureIo();
|
|
129
|
+
const code = await runCli(['restore', '--id', id, '--data-dir', dataDir], io, { DSH_HOME: homeDir });
|
|
130
|
+
assert.equal(code, 1, 'DSH 在跑时恢复必须被拒绝');
|
|
131
|
+
const text = out.join('\n');
|
|
132
|
+
assert.match(text, /检测到 DSH 正在运行/);
|
|
133
|
+
assert.match(text, /dcm web/, '必须给出「起不来时」的出口');
|
|
134
|
+
assert.equal(
|
|
135
|
+
await fs.readFile(path.join(homeDir, 'settings.yaml'), 'utf8'),
|
|
136
|
+
'CURRENT settings\n',
|
|
137
|
+
'被拒绝时一个字节都不许改',
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('G-07 restore --dry-run:DSH 在跑也照旧只打印计划(门只在真实执行前)', async () => {
|
|
143
|
+
await withTmp(async (tmp) => {
|
|
144
|
+
const { homeDir, dataDir, id } = await seedRestorableSnapshot(tmp);
|
|
145
|
+
await fs.writeFile(path.join(dataDir, 'running', 'web.json'), JSON.stringify({
|
|
146
|
+
schemaVersion: 1, name: 'web', pid: process.pid, port: 3099,
|
|
147
|
+
startedAt: new Date().toISOString(), updatedAt: Date.now(),
|
|
148
|
+
}), 'utf8');
|
|
149
|
+
|
|
150
|
+
const { io, out } = captureIo();
|
|
151
|
+
const code = await runCli(['restore', '--dry-run', '--id', id, '--data-dir', dataDir], io, { DSH_HOME: homeDir });
|
|
152
|
+
assert.equal(code, 0, 'dry-run 零写入,不受写入门影响');
|
|
153
|
+
assert.match(out.join('\n'), /动作计划/);
|
|
154
|
+
assert.equal(await fs.readFile(path.join(homeDir, 'settings.yaml'), 'utf8'), 'CURRENT settings\n');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/* ---------------------------------------------------------------- ④ web 目录派生 */
|
|
159
|
+
|
|
160
|
+
test('G-08 resolveWebPaths:快照/导出/锁/数据根四处必须同源(--data-dir 只上溯一次)', () => {
|
|
161
|
+
// 统一先 resolve:Windows 下 '\\root' 是「当前盘符根」,函数内部也会 resolve,避免断言口径不一致。
|
|
162
|
+
const homeDsh = path.resolve(path.sep, 'home', 'dsh');
|
|
163
|
+
const root = path.resolve(path.sep, 'root');
|
|
164
|
+
const env = { DSH_HOME: homeDsh } as Record<string, string | undefined>;
|
|
165
|
+
|
|
166
|
+
// 尾段命中 snapshots:上溯一层当作 root,四个目录都挂在它下面。
|
|
167
|
+
const fromSnapshots = resolveWebPaths({ dataDir: path.join(root, 'snapshots') }, env);
|
|
168
|
+
assert.equal(fromSnapshots.root, root);
|
|
169
|
+
assert.equal(fromSnapshots.dataDir, root);
|
|
170
|
+
assert.equal(fromSnapshots.snapshotsDir, path.join(root, 'snapshots'));
|
|
171
|
+
assert.equal(fromSnapshots.exportsDir, path.join(root, 'exports'), '导出目录也必须上溯(不再挂到 leaf 下)');
|
|
172
|
+
assert.equal(fromSnapshots.locksDir, path.join(root, 'locks'));
|
|
173
|
+
|
|
174
|
+
// exports 同理
|
|
175
|
+
const fromExports = resolveWebPaths({ dataDir: path.join(root, 'exports') }, env);
|
|
176
|
+
assert.equal(fromExports.snapshotsDir, path.join(root, 'snapshots'));
|
|
177
|
+
assert.equal(fromExports.exportsDir, path.join(root, 'exports'));
|
|
178
|
+
|
|
179
|
+
// 直接给数据根
|
|
180
|
+
const fromRoot = resolveWebPaths({ dataDir: root }, env);
|
|
181
|
+
assert.equal(fromRoot.snapshotsDir, path.join(root, 'snapshots'));
|
|
182
|
+
assert.equal(fromRoot.exportsDir, path.join(root, 'exports'));
|
|
183
|
+
|
|
184
|
+
// --data-root 恒为准(即使同时给了 leaf 形态的 --data-dir)
|
|
185
|
+
const other = path.resolve(path.sep, 'other');
|
|
186
|
+
const explicitRoot = resolveWebPaths({ dataRoot: other, dataDir: path.join(root, 'snapshots') }, env);
|
|
187
|
+
assert.equal(explicitRoot.root, other);
|
|
188
|
+
assert.equal(explicitRoot.snapshotsDir, path.join(other, 'snapshots'));
|
|
189
|
+
|
|
190
|
+
// 都没给:缺省根 = $DSH_HOME/dsh-config-manager
|
|
191
|
+
const defaults = resolveWebPaths({}, env);
|
|
192
|
+
assert.equal(defaults.root, path.join(homeDsh, 'dsh-config-manager'));
|
|
193
|
+
});
|