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
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* (实测 `verifyChecksums` 只调用 `map.get()`,不会整体物化 Map)。
|
|
29
29
|
* 对 MB 级的 DSH 配置备份完全可接受。
|
|
30
30
|
*/
|
|
31
|
+
import { engineMsg } from './messages-core.ts';
|
|
31
32
|
import fs from 'node:fs/promises';
|
|
32
33
|
import { verifyChecksums } from '../utils/hashing.ts';
|
|
33
34
|
import { isENOENT } from '../utils/guards.ts';
|
|
@@ -146,18 +147,18 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
146
147
|
try {
|
|
147
148
|
const stat = await fs.stat(zipPath);
|
|
148
149
|
if (!stat.isFile()) {
|
|
149
|
-
errors.push(
|
|
150
|
+
errors.push(engineMsg('core.backupVerify.notRegularFile', { zip: zipPath }));
|
|
150
151
|
return makeResult(zipPath, 'MISSING', errors, warnings);
|
|
151
152
|
}
|
|
152
153
|
sizeBytes = stat.size;
|
|
153
154
|
buf = await fs.readFile(zipPath);
|
|
154
155
|
} catch (err) {
|
|
155
156
|
if (isENOENT(err)) {
|
|
156
|
-
errors.push(
|
|
157
|
+
errors.push(engineMsg('core.backupVerify.fileNotFound', { zip: zipPath }));
|
|
157
158
|
return makeResult(zipPath, 'MISSING', errors, warnings);
|
|
158
159
|
}
|
|
159
160
|
// 文件在但读不动(EACCES / EBUSY / IO 错):这是自检自身的失败,不是「备份坏了」
|
|
160
|
-
errors.push(
|
|
161
|
+
errors.push(engineMsg('core.backupVerify.fileUnreadable', { reason: msgOfError(err) }));
|
|
161
162
|
return makeResult(zipPath, 'VERIFY_ERROR', errors, warnings);
|
|
162
163
|
}
|
|
163
164
|
|
|
@@ -170,7 +171,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
170
171
|
/* ---------------- 2. 打开为 ZIP ---------------- */
|
|
171
172
|
// 整体加密容器不是 ZIP(先识别,给出比「ZIP 损坏」准确得多的结论)
|
|
172
173
|
if (isArchiveBlob(buf)) {
|
|
173
|
-
errors.push('
|
|
174
|
+
errors.push(engineMsg('core.backupVerify.encryptedContainer'));
|
|
174
175
|
return withSize('UNSUPPORTED');
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -178,7 +179,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
178
179
|
try {
|
|
179
180
|
archive = createHardenedZipParser()(buf);
|
|
180
181
|
} catch (err) {
|
|
181
|
-
errors.push(
|
|
182
|
+
errors.push(engineMsg('core.backupVerify.cannotOpenZip', { reason: msgOfError(err) }));
|
|
182
183
|
return withSize('CORRUPT');
|
|
183
184
|
}
|
|
184
185
|
const metas = archive.entries();
|
|
@@ -187,13 +188,13 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
187
188
|
file: zipPath, verdict, sizeBytes, entryCount, errors, warnings,
|
|
188
189
|
});
|
|
189
190
|
if (entryCount === 0) {
|
|
190
|
-
errors.push('
|
|
191
|
+
errors.push(engineMsg('core.backupVerify.emptyArchive'));
|
|
191
192
|
return withEntries('CORRUPT');
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
/* ---------------- 3. manifest.json ---------------- */
|
|
195
196
|
if (!archive.has(MANIFEST_FILE)) {
|
|
196
|
-
errors.push(
|
|
197
|
+
errors.push(engineMsg('core.backupVerify.missingManifest', { file: MANIFEST_FILE }));
|
|
197
198
|
return withEntries('CORRUPT');
|
|
198
199
|
}
|
|
199
200
|
let manifestRaw: string;
|
|
@@ -201,14 +202,14 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
201
202
|
manifestRaw = archive.readEntryText(MANIFEST_FILE);
|
|
202
203
|
} catch (err) {
|
|
203
204
|
// 条目本身读不出来(CRC 失败/解压失败)→ 归档损坏,而非 manifest 语义非法
|
|
204
|
-
errors.push(
|
|
205
|
+
errors.push(engineMsg('core.backupVerify.manifestUnreadable', { file: MANIFEST_FILE, reason: msgOfError(err) }));
|
|
205
206
|
return withEntries('CORRUPT');
|
|
206
207
|
}
|
|
207
208
|
let manifest: Manifest;
|
|
208
209
|
try {
|
|
209
210
|
manifest = parseManifest(manifestRaw);
|
|
210
211
|
} catch (err) {
|
|
211
|
-
errors.push(
|
|
212
|
+
errors.push(engineMsg('core.backupVerify.invalidManifest', { file: MANIFEST_FILE, reason: msgOfError(err) }));
|
|
212
213
|
return withEntries('CORRUPT');
|
|
213
214
|
}
|
|
214
215
|
const sections = Object.entries(manifest.sections ?? {})
|
|
@@ -220,13 +221,13 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
220
221
|
|
|
221
222
|
if (!isSupported(manifest.schemaVersion)) {
|
|
222
223
|
// 版本超范围是「本插件读不了」,不是「备份坏了」——明确区分,避免误导用户重导
|
|
223
|
-
errors.push(
|
|
224
|
+
errors.push(engineMsg('core.backupVerify.unsupportedSchema', { version: describeVersion(manifest.schemaVersion) }));
|
|
224
225
|
return withManifest('UNSUPPORTED');
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
/* ---------------- 4. integrity/checksums.json ---------------- */
|
|
228
229
|
if (!archive.has(CHECKSUMS_FILE)) {
|
|
229
|
-
errors.push(
|
|
230
|
+
errors.push(engineMsg('core.backupVerify.missingChecksums', { file: CHECKSUMS_FILE }));
|
|
230
231
|
return withManifest('CORRUPT');
|
|
231
232
|
}
|
|
232
233
|
let table: Record<string, string>;
|
|
@@ -234,7 +235,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
234
235
|
table = parseChecksumsTable(archive.readEntryText(CHECKSUMS_FILE));
|
|
235
236
|
} catch (err) {
|
|
236
237
|
// readEntryText 的 CRC/解压失败与「表结构非法」都归 CORRUPT(两者都说明备份不可信)
|
|
237
|
-
errors.push(
|
|
238
|
+
errors.push(engineMsg('core.backupVerify.invalidChecksums', { file: CHECKSUMS_FILE, reason: msgOfError(err) }));
|
|
238
239
|
return withManifest('CORRUPT');
|
|
239
240
|
}
|
|
240
241
|
|
|
@@ -243,7 +244,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
243
244
|
const unreadableNames = new Set<string>();
|
|
244
245
|
const unreadable: string[] = [];
|
|
245
246
|
const lazyEntries = new LazyEntryMap(archive, unreadableNames, (name, reason) => {
|
|
246
|
-
unreadable.push(
|
|
247
|
+
unreadable.push(engineMsg('core.backupVerify.unreadableEntry', { name, reason }));
|
|
247
248
|
});
|
|
248
249
|
const tableKeys = Object.keys(table);
|
|
249
250
|
const check = verifyChecksums(lazyEntries, table);
|
|
@@ -254,10 +255,10 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
254
255
|
// 4b. 结构提示:未纳入校验表的条目(导出器只把 manifest 与校验表自身排除在外)
|
|
255
256
|
if (tableKeys.length === 0) {
|
|
256
257
|
if (entryCount > NON_DATA_ENTRIES.size) {
|
|
257
|
-
errors.push('
|
|
258
|
+
errors.push(engineMsg('core.backupVerify.emptyTableWithData'));
|
|
258
259
|
return withManifest('CORRUPT');
|
|
259
260
|
}
|
|
260
|
-
warnings.push('
|
|
261
|
+
warnings.push(engineMsg('core.backupVerify.emptyTable'));
|
|
261
262
|
}
|
|
262
263
|
const covered = new Set(tableKeys);
|
|
263
264
|
const uncovered = metas
|
|
@@ -265,7 +266,7 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
265
266
|
.map((m) => m.name);
|
|
266
267
|
if (uncovered.length > 0) {
|
|
267
268
|
const shown = uncovered.slice(0, 5).join(', ');
|
|
268
|
-
warnings.push(
|
|
269
|
+
warnings.push(engineMsg('core.backupVerify.uncoveredEntries', { count: uncovered.length, shown, suffix: uncovered.length > 5 ? ', …' : '' }));
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
// 4c. 结构提示:manifest 声明包含的分区在归档内应有对应条目
|
|
@@ -274,30 +275,30 @@ export async function verifyBackupZip(zipPath: string): Promise<BackupVerifyResu
|
|
|
274
275
|
const jsonPath = SECTION_JSON_PATHS[id as SectionId];
|
|
275
276
|
if (prefix !== undefined) {
|
|
276
277
|
if (!metas.some((m) => m.name.startsWith(prefix) && m.name !== prefix)) {
|
|
277
|
-
warnings.push(
|
|
278
|
+
warnings.push(engineMsg('core.backupVerify.declaredSectionNoEntries', { id }));
|
|
278
279
|
}
|
|
279
280
|
} else if (jsonPath !== undefined && !archive.has(jsonPath)) {
|
|
280
|
-
warnings.push(
|
|
281
|
+
warnings.push(engineMsg('core.backupVerify.declaredSectionPayloadMissing', { id, path: jsonPath }));
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
if (manifest.security?.encrypted === true && !metas.some((m) => SECRETS_ENC_RE.test(m.name))) {
|
|
284
|
-
warnings.push('
|
|
285
|
+
warnings.push(engineMsg('core.backupVerify.encryptedFlagNoSecrets'));
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
/* ---------------- 裁决 ---------------- */
|
|
288
289
|
if (missing.length > 0) {
|
|
289
|
-
errors.push(
|
|
290
|
+
errors.push(engineMsg('core.backupVerify.missingEntries', { count: missing.length, list: missing.join(', ') }));
|
|
290
291
|
}
|
|
291
292
|
if (mismatched.length > 0) {
|
|
292
|
-
errors.push(
|
|
293
|
+
errors.push(engineMsg('core.backupVerify.hashMismatch', { count: mismatched.length, list: mismatched.join(', ') }));
|
|
293
294
|
}
|
|
294
295
|
if (unreadable.length > 0) {
|
|
295
|
-
errors.push(
|
|
296
|
+
errors.push(engineMsg('core.backupVerify.unreadableEntries', { count: unreadable.length, list: unreadable.join(', ') }));
|
|
296
297
|
}
|
|
297
298
|
return withManifest(missing.length > 0 || mismatched.length > 0 || unreadable.length > 0 ? 'CORRUPT' : 'OK');
|
|
298
299
|
} catch (err) {
|
|
299
300
|
// 5. 自检自身异常:如实报 VERIFY_ERROR,绝不降级成 OK
|
|
300
|
-
errors.push(
|
|
301
|
+
errors.push(engineMsg('core.backupVerify.verificationError', { reason: msgOfError(err) }));
|
|
301
302
|
return withSize('VERIFY_ERROR');
|
|
302
303
|
}
|
|
303
304
|
}
|
package/src/core/backup.test.ts
CHANGED
|
@@ -344,3 +344,79 @@ test('F-4: normalizeDshVersion 归一(去空白 / unknown 丢弃 / 非字符
|
|
|
344
344
|
assert.equal(normalizeDshVersion(undefined), undefined);
|
|
345
345
|
assert.equal(normalizeDshVersion(42), undefined);
|
|
346
346
|
});
|
|
347
|
+
|
|
348
|
+
/* ---------------- 宿主文件「存在但读不出来」不得记成 existed:false(数据安全) ---------------- */
|
|
349
|
+
|
|
350
|
+
test('备份:宿主文件存在但读不出来 → existed=true + unreadable=true(绝不写 existed:false)', async (t) => {
|
|
351
|
+
const dir = await tmpDir(t, 'unreadable');
|
|
352
|
+
const { ctx } = countingCtx();
|
|
353
|
+
await seedHostFiles(ctx);
|
|
354
|
+
// 让 settings.yaml 这一次读不出来(占用 / 权限 / IO 错的等价物)
|
|
355
|
+
const fsFacade = ctx.fs as { readFile: (rel: string) => Promise<Uint8Array> };
|
|
356
|
+
const original = fsFacade.readFile.bind(ctx.fs);
|
|
357
|
+
fsFacade.readFile = async (rel: string) => {
|
|
358
|
+
if (rel === 'settings.yaml') throw new Error('EBUSY: resource busy or locked');
|
|
359
|
+
return original(rel);
|
|
360
|
+
};
|
|
361
|
+
const store = new FileSnapshotStore({ dir });
|
|
362
|
+
const snapshot = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
|
|
363
|
+
|
|
364
|
+
const settings = (snapshot.hostFileBackups ?? []).find((b) => b.relPath === 'settings.yaml');
|
|
365
|
+
assert.ok(settings, 'settings.yaml 必须仍被登记(绝不因读失败而消失)');
|
|
366
|
+
assert.equal(settings.existed, true, '存在但读不到 → existed 必须为 true(写成 false 会在恢复时删掉用户真实文件)');
|
|
367
|
+
assert.equal(settings.unreadable, true, '必须显式标注 unreadable');
|
|
368
|
+
assert.equal(settings.blobPath, '', '读不到 → 没有 blob');
|
|
369
|
+
const patch = (snapshot.hostFileBackups ?? []).find((b) => b.relPath === 'cordis.patch.yml');
|
|
370
|
+
assert.equal(patch?.existed, true, '其余宿主文件照常读');
|
|
371
|
+
assert.equal(patch?.unreadable, undefined, '读得到的条目不得被标 unreadable');
|
|
372
|
+
assert.equal((await verifySnapshot(dir, snapshot.id)).ok, true, 'unreadable 条目不得破坏 durable+verified');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
/* ---------------- 保留清理 fail-closed:引用集合读不出来时本轮绝不删(数据安全) ---------------- */
|
|
376
|
+
|
|
377
|
+
test('保留清理:recovery 引用集合读不出来 → 本轮放弃清理,绝不按空集合删(F3 不可回退)', async (t) => {
|
|
378
|
+
const dir = await tmpDir(t, 'prune-ref');
|
|
379
|
+
const { ctx } = countingCtx();
|
|
380
|
+
await seedHostFiles(ctx);
|
|
381
|
+
let scans = 0;
|
|
382
|
+
const warned: string[] = [];
|
|
383
|
+
const store = new FileSnapshotStore({
|
|
384
|
+
dir,
|
|
385
|
+
// 引用扫描一次瞬时失败(EACCES / IO 错)—— 此前会被 .catch 成空集合,随即按「无人引用」继续删
|
|
386
|
+
referencedSnapshotIds: async () => { scans += 1; throw new Error('EACCES: permission denied'); },
|
|
387
|
+
retentionPolicy: () => ({ keepLast: 1, keepMonthly: 0, keepYearly: 0 }),
|
|
388
|
+
onWarn: (message) => { warned.push(message) },
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const first = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
|
|
392
|
+
const second = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
|
|
393
|
+
|
|
394
|
+
assert.ok(scans >= 1, 'prune 必须真的问过 recovery 引用集合(否则本用例不成立)');
|
|
395
|
+
const remaining = (await fs.readdir(dir)).sort();
|
|
396
|
+
assert.deepEqual(remaining, [first.id, second.id].sort(), '读不到引用集合时本轮必须放弃清理 —— 两个快照都得留着');
|
|
397
|
+
assert.ok(warned.some((m) => m.includes('放弃')), '放弃必须留痕(绝不静默):' + JSON.stringify(warned));
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
/* ---------------- 保留清理:目录名 ≠ 内部 id 时跳过并留痕(数据安全) ---------------- */
|
|
401
|
+
|
|
402
|
+
test('保留清理:快照目录名与内部 id 不一致时跳过并留痕(绝不按内部 id 删另一个目录)', async (t) => {
|
|
403
|
+
const dir = await tmpDir(t, 'prune-name');
|
|
404
|
+
const { ctx } = countingCtx();
|
|
405
|
+
await seedHostFiles(ctx);
|
|
406
|
+
const warned: string[] = [];
|
|
407
|
+
const store = new FileSnapshotStore({
|
|
408
|
+
dir,
|
|
409
|
+
retentionPolicy: () => ({ keepLast: 1, keepMonthly: 0, keepYearly: 0 }),
|
|
410
|
+
onWarn: (message) => { warned.push(message) },
|
|
411
|
+
});
|
|
412
|
+
const first = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
|
|
413
|
+
// 手工制造「目录名 ≠ 内部 id」(改名目录 / 半截拷贝残留的真机形态)
|
|
414
|
+
const renamed = path.join(dir, 'zzz-renamed-dir');
|
|
415
|
+
await fs.rename(path.join(dir, first.id), renamed);
|
|
416
|
+
const second = await createSnapshot({ ctx, plan: emptyPlan(), sourceZip: 'unit.zip', store, adapters: [] });
|
|
417
|
+
|
|
418
|
+
const remaining = (await fs.readdir(dir)).sort();
|
|
419
|
+
assert.ok(remaining.includes('zzz-renamed-dir'), '目录名与内部 id 不一致的目录绝不能被当成候选删掉');
|
|
420
|
+
assert.ok(remaining.includes(second.id), '新建的快照当然还在');
|
|
421
|
+
assert.ok(warned.some((m) => m.includes('不一致')), '跳过必须留痕(绝不静默):' + JSON.stringify(warned));
|
|
422
|
+
});
|
package/src/core/backup.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 快照不保存 credential 值(DSH 永不回读值)——credential 条目只记 existed 标志,
|
|
6
6
|
* 回滚时 existed=true 的条目如实标记「值需人工补录」。
|
|
7
7
|
*/
|
|
8
|
+
import { engineMsg } from './messages-core.ts';
|
|
8
9
|
import fs from 'node:fs/promises';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
import crypto from 'node:crypto';
|
|
@@ -103,8 +104,11 @@ async function backupHostFiles(
|
|
|
103
104
|
try {
|
|
104
105
|
existed = await ctx.fs.exists(relPath);
|
|
105
106
|
} catch (err) {
|
|
107
|
+
// 探测自身失败 = 存在性判不出来:按「存在但不可读」登记。**绝不写 existed:false** ——
|
|
108
|
+
// 那会被恢复侧读成「当时确实不存在」,进而删掉用户真实的文件。
|
|
106
109
|
ctx.log.warn(`快照探测宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
-
existed
|
|
110
|
+
backups.push({ relPath, blobPath: '', existed: true, unreadable: true });
|
|
111
|
+
continue;
|
|
108
112
|
}
|
|
109
113
|
if (!existed) {
|
|
110
114
|
backups.push({ relPath, blobPath: '', existed: false });
|
|
@@ -115,8 +119,10 @@ async function backupHostFiles(
|
|
|
115
119
|
blobs.set(blobPath, await ctx.fs.readFile(relPath));
|
|
116
120
|
backups.push({ relPath, blobPath, existed: true });
|
|
117
121
|
} catch (err) {
|
|
122
|
+
// 文件确实存在、只是这次读不出来(占用/权限/IO):登记 existed=true + unreadable,
|
|
123
|
+
// 恢复侧据此只跳过并告警,绝不进入「删除」分支(见 HostFileBackup.unreadable)。
|
|
118
124
|
ctx.log.warn(`快照读取宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
-
backups.push({ relPath, blobPath: '', existed:
|
|
125
|
+
backups.push({ relPath, blobPath: '', existed: true, unreadable: true });
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
return backups;
|
|
@@ -142,7 +148,7 @@ const FILE_BASES: Partial<Record<SectionId, string>> = {
|
|
|
142
148
|
// 其它分区语义处理」同类缺陷)。注册表新增文件类分区时若忘了在这里补,进程启动即失败。
|
|
143
149
|
for (const id of SECTION_IDS) {
|
|
144
150
|
if (isFileSection(id) && FILE_BASES[id] === undefined) {
|
|
145
|
-
throw new Error(
|
|
151
|
+
throw new Error(engineMsg('core.backup.fileSectionBaseMissing', { section: id }));
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -316,7 +322,7 @@ async function engineSnapshotEntry(ctx: HostContext, target: SnapshotTarget): Pr
|
|
|
316
322
|
// secrets 已注册但**没有 adapter、也没有 ZIP 内分区 JSON**(凭据值走独立加密容器 secrets.enc)。
|
|
317
323
|
// 任何计划项都不应以它为快照目标 —— 显式报错,而不是落到 default 分支。
|
|
318
324
|
throw new Error(
|
|
319
|
-
'
|
|
325
|
+
engineMsg('core.backup.secretsSectionNotSnapshotTarget'),
|
|
320
326
|
);
|
|
321
327
|
default: {
|
|
322
328
|
// 编译期穷举断言:SectionId 的每一项都已被上面的 case 处理 → 此处 target.adapter 收窄为 never。
|
|
@@ -326,8 +332,7 @@ async function engineSnapshotEntry(ctx: HostContext, target: SnapshotTarget): Pr
|
|
|
326
332
|
// 旧实现在这里静默返回 settingsNamespace 条目(连 existed 都没有)→ 回滚把该目标当「原本不存在」
|
|
327
333
|
// 而不做任何补偿:该分区的写入永远撤不掉,报告却仍说「已快照、可回滚」(审计 core-flow#F-10)。
|
|
328
334
|
throw new Error(
|
|
329
|
-
|
|
330
|
-
+ '拒绝按其它分区语义静默记录(那会让该分区的写入无法回滚)',
|
|
335
|
+
engineMsg('core.backup.sectionUnsupported', { section: String(unhandled) }),
|
|
331
336
|
);
|
|
332
337
|
}
|
|
333
338
|
}
|
|
@@ -534,6 +539,11 @@ export interface FileSnapshotStoreOptions {
|
|
|
534
539
|
* 未注入 → `fallbackPruneSelector`(只按 keepLast,等价既有 FIFO;分层字段被忽略 = 保守少删)。
|
|
535
540
|
*/
|
|
536
541
|
pruneSelector?: PruneSelector;
|
|
542
|
+
/**
|
|
543
|
+
* 诊断回调(可选):保留清理**被放弃**或失败时的可见线索。
|
|
544
|
+
* 缺省 = 不记录(不改变行为);宿主接 `host.log.warn` —— 「这轮为什么没清理」必须能查。
|
|
545
|
+
*/
|
|
546
|
+
onWarn?: (message: string) => void;
|
|
537
547
|
}
|
|
538
548
|
|
|
539
549
|
/** 文件快照存储:<dir>/<id>/snapshot.json + <dir>/<id>/blobs/* */
|
|
@@ -554,7 +564,7 @@ export class FileSnapshotStore implements SnapshotStore {
|
|
|
554
564
|
*/
|
|
555
565
|
private async writeBlob(dir: string, blobPath: string, data: Uint8Array, hashes: Record<string, string>): Promise<void> {
|
|
556
566
|
const target = path.join(dir, blobPath);
|
|
557
|
-
if (!target.startsWith(dir)) throw new Error(
|
|
567
|
+
if (!target.startsWith(dir)) throw new Error(engineMsg('core.backup.blobPathEscaped', { path: blobPath }));
|
|
558
568
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
559
569
|
await atomicWriteFile(target, data);
|
|
560
570
|
hashes[blobPath] = sha256Hex(data);
|
|
@@ -579,7 +589,7 @@ export class FileSnapshotStore implements SnapshotStore {
|
|
|
579
589
|
// 4) 验证(从磁盘重读,不信任内存对象)
|
|
580
590
|
const verified = await verifySnapshot(this.options.dir, snapshot.id);
|
|
581
591
|
if (!verified.ok) {
|
|
582
|
-
throw new Error(
|
|
592
|
+
throw new Error(engineMsg('core.backup.snapshotVerifyFailed', { id: snapshot.id, reason: verified.reason ?? '' }));
|
|
583
593
|
}
|
|
584
594
|
// 5) 原子发布 READY(重写 snapshot.json readiness='READY';metadataHash 稳定不受影响)
|
|
585
595
|
const ready = { ...snapshot, readiness: 'READY' as const };
|
|
@@ -588,7 +598,9 @@ export class FileSnapshotStore implements SnapshotStore {
|
|
|
588
598
|
try {
|
|
589
599
|
await this.prune();
|
|
590
600
|
} catch (err) {
|
|
591
|
-
// 快照已 READY + verified;prune 失败(EBUSY/EPERM)不应把刚成功的快照判为 unusable
|
|
601
|
+
// 快照已 READY + verified;prune 失败(EBUSY/EPERM)不应把刚成功的快照判为 unusable ——
|
|
602
|
+
// 但必须留痕:保留清理长期失败会让快照无限堆积,用户与日志都看不到线索。
|
|
603
|
+
this.options.onWarn?.(engineMsg('core.backup.pruneFailed', { reason: err instanceof Error ? err.message : String(err) }));
|
|
592
604
|
}
|
|
593
605
|
return snapshot.id;
|
|
594
606
|
}
|
|
@@ -632,7 +644,18 @@ export class FileSnapshotStore implements SnapshotStore {
|
|
|
632
644
|
private async prune(): Promise<void> {
|
|
633
645
|
const dir = this.options.dir;
|
|
634
646
|
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
635
|
-
|
|
647
|
+
// F3:被未收敛 journal 引用的快照**绝不自动清理**。判据是「读不到 ≠ 没有引用」——
|
|
648
|
+
// 引用提供者抛错时**放弃本轮清理**,绝不按空集合继续删:一次瞬时 IO 错就可能删掉
|
|
649
|
+
// 事故恢复唯一的回滚点(那是 SAFE MODE 的唯一证据)。此前这里是 `.catch(() => new Set())`。
|
|
650
|
+
let referenced = new Set<string>();
|
|
651
|
+
if (this.options.referencedSnapshotIds !== undefined) {
|
|
652
|
+
try {
|
|
653
|
+
referenced = await this.options.referencedSnapshotIds();
|
|
654
|
+
} catch (err) {
|
|
655
|
+
this.options.onWarn?.(engineMsg('core.backup.pruneAbandoned', { reason: err instanceof Error ? err.message : String(err) }));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
636
659
|
// m-retention:策略读取失败 → 回退缺省(绝不因策略提供者异常而误删/崩溃)
|
|
637
660
|
const policy = await Promise.resolve()
|
|
638
661
|
.then(() => this.options.retentionPolicy?.() ?? defaultRetentionPolicyLike())
|
|
@@ -644,9 +667,16 @@ export class FileSnapshotStore implements SnapshotStore {
|
|
|
644
667
|
try {
|
|
645
668
|
const parsed = parseJsonSafe(await fs.readFile(path.join(dir, entry.name, 'snapshot.json'), 'utf8')) as Snapshot;
|
|
646
669
|
if (typeof parsed.id !== 'string' || parsed.id === '' || typeof parsed.createdAt !== 'string') continue;
|
|
670
|
+
// 删除目标只能是**目录名**(下面 path.join(dir, id) 就是按它删的)。目录名与内部 id 不一致时
|
|
671
|
+
// **绝不猜**:pinned/被引用的判断是对着这份 snapshot.json 做的,删除却会打在另一个目录上 ——
|
|
672
|
+
// 跳过并留痕(手工改名/半截拷贝残留会出现这种形态)。
|
|
673
|
+
if (parsed.id !== entry.name) {
|
|
674
|
+
this.options.onWarn?.(engineMsg('core.backup.dirNameIdMismatch', { dir: entry.name, id: parsed.id }));
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
647
677
|
if (parsed.pinned === true) continue; // 置顶快照豁免自动清理
|
|
648
|
-
if (referenced.has(
|
|
649
|
-
metas.push({ id:
|
|
678
|
+
if (referenced.has(entry.name)) continue; // Phase 4 F3:recovery 引用豁免
|
|
679
|
+
metas.push({ id: entry.name, createdAt: parsed.createdAt });
|
|
650
680
|
} catch {
|
|
651
681
|
// 损坏 / 非快照目录:跳过(与 listSnapshots 语义一致)
|
|
652
682
|
}
|
|
@@ -672,7 +702,7 @@ export class FileSnapshotStore implements SnapshotStore {
|
|
|
672
702
|
async readBlob(id: string, blobPath: string): Promise<Uint8Array> {
|
|
673
703
|
const target = path.join(this.snapshotDir(id), blobPath);
|
|
674
704
|
const dir = this.options.dir;
|
|
675
|
-
if (!target.startsWith(dir)) throw new Error(
|
|
705
|
+
if (!target.startsWith(dir)) throw new Error(engineMsg('core.backup.blobPathEscaped', { path: blobPath }));
|
|
676
706
|
return fs.readFile(target);
|
|
677
707
|
}
|
|
678
708
|
|
|
@@ -712,51 +742,51 @@ export interface SnapshotVerifyResult {
|
|
|
712
742
|
export async function verifySnapshot(snapshotsDir: string, id: string): Promise<SnapshotVerifyResult> {
|
|
713
743
|
const dir = path.join(snapshotsDir, id);
|
|
714
744
|
if (!dir.startsWith(path.resolve(snapshotsDir) + path.sep)) {
|
|
715
|
-
return { ok: false, reason: '
|
|
745
|
+
return { ok: false, reason: engineMsg('core.backup.snapshotPathEscaped') };
|
|
716
746
|
}
|
|
717
747
|
let raw: string;
|
|
718
748
|
try {
|
|
719
749
|
raw = await fs.readFile(path.join(dir, 'snapshot.json'), 'utf8');
|
|
720
750
|
} catch {
|
|
721
|
-
return { ok: false, reason: '
|
|
751
|
+
return { ok: false, reason: engineMsg('core.backup.snapshotJsonMissing') };
|
|
722
752
|
}
|
|
723
753
|
const snapshot = parseJsonSafe(raw) as Snapshot;
|
|
724
754
|
if (snapshot === null || typeof snapshot !== 'object' || typeof snapshot.id !== 'string' || snapshot.id === '') {
|
|
725
|
-
return { ok: false, reason: '
|
|
755
|
+
return { ok: false, reason: engineMsg('core.backup.snapshotJsonInvalid') };
|
|
726
756
|
}
|
|
727
757
|
if (snapshot.id !== id) {
|
|
728
|
-
return { ok: false, reason: '
|
|
758
|
+
return { ok: false, reason: engineMsg('core.backup.snapshotIdDirMismatch') };
|
|
729
759
|
}
|
|
730
760
|
let manifestRaw: string;
|
|
731
761
|
try {
|
|
732
762
|
manifestRaw = await fs.readFile(path.join(dir, 'manifest.json'), 'utf8');
|
|
733
763
|
} catch {
|
|
734
|
-
return { ok: false, reason: '
|
|
764
|
+
return { ok: false, reason: engineMsg('core.backup.manifestMissing') };
|
|
735
765
|
}
|
|
736
766
|
const manifest = parseJsonSafe(manifestRaw) as SnapshotManifest;
|
|
737
767
|
if (manifest === null || typeof manifest !== 'object' || manifest.snapshotId !== id) {
|
|
738
|
-
return { ok: false, reason: '
|
|
768
|
+
return { ok: false, reason: engineMsg('core.backup.manifestInvalid') };
|
|
739
769
|
}
|
|
740
770
|
// entryCount 匹配(manifest 声明条目数与实际一致,防计数篡改)
|
|
741
771
|
if (typeof manifest.entryCount !== 'number' || manifest.entryCount !== snapshot.entries.length) {
|
|
742
|
-
return { ok: false, reason: '
|
|
772
|
+
return { ok: false, reason: engineMsg('core.backup.entryCountMismatch') };
|
|
743
773
|
}
|
|
744
774
|
// metadataHash 匹配(破坏性内容)
|
|
745
775
|
if (computeMetadataHash(snapshot) !== manifest.metadataHash) {
|
|
746
|
-
return { ok: false, reason: '
|
|
776
|
+
return { ok: false, reason: engineMsg('core.backup.metadataHashMismatch') };
|
|
747
777
|
}
|
|
748
778
|
// blob hashes 匹配 + 存在
|
|
749
779
|
for (const [blobPath, expectedHash] of Object.entries(manifest.blobHashes)) {
|
|
750
780
|
const target = path.join(dir, blobPath);
|
|
751
|
-
if (!target.startsWith(dir)) return { ok: false, reason:
|
|
781
|
+
if (!target.startsWith(dir)) return { ok: false, reason: engineMsg('core.backup.blobPathEscaped', { path: blobPath }) };
|
|
752
782
|
let data: Uint8Array;
|
|
753
783
|
try {
|
|
754
784
|
data = await fs.readFile(target);
|
|
755
785
|
} catch {
|
|
756
|
-
return { ok: false, reason:
|
|
786
|
+
return { ok: false, reason: engineMsg('core.backup.blobMissing', { path: blobPath }) };
|
|
757
787
|
}
|
|
758
788
|
if (sha256Hex(data) !== expectedHash) {
|
|
759
|
-
return { ok: false, reason:
|
|
789
|
+
return { ok: false, reason: engineMsg('core.backup.blobHashMismatch', { path: blobPath }) };
|
|
760
790
|
}
|
|
761
791
|
}
|
|
762
792
|
// Review C P2:snapshot.json 引用的每个 blob(entries.copiedTo / hostFileBackups.blobPath)都必须被 manifest 覆盖,
|
|
@@ -770,7 +800,7 @@ export async function verifySnapshot(snapshotsDir: string, id: string): Promise<
|
|
|
770
800
|
}
|
|
771
801
|
for (const relBlob of referencedBlobs) {
|
|
772
802
|
if (!(relBlob in manifest.blobHashes)) {
|
|
773
|
-
return { ok: false, reason:
|
|
803
|
+
return { ok: false, reason: engineMsg('core.backup.blobNotInManifest', { blob: relBlob }) };
|
|
774
804
|
}
|
|
775
805
|
}
|
|
776
806
|
return { ok: true };
|
package/src/core/boot-rescue.ts
CHANGED
|
@@ -35,6 +35,7 @@ import path from 'node:path';
|
|
|
35
35
|
|
|
36
36
|
import { atomicWriteFile } from '../utils/atomic-write.ts';
|
|
37
37
|
import { sha256Hex } from '../utils/hashing.ts';
|
|
38
|
+
import { engineMsg } from './messages-core.ts';
|
|
38
39
|
|
|
39
40
|
/* ---------------------------------------------------------------- 常量 */
|
|
40
41
|
|
|
@@ -414,14 +415,14 @@ export interface RescueSelfMount {
|
|
|
414
415
|
/** 救援模式 profile patch 的头注释(说明本文件被谁接管、bundle 层的实际状态)。 */
|
|
415
416
|
const RESCUE_PATCH_HEADER_LINES = [
|
|
416
417
|
'# dsh-config-manager RESCUE MODE',
|
|
417
|
-
'
|
|
418
|
+
engineMsg('core.bootRescue.patchTakenOver'),
|
|
418
419
|
];
|
|
419
420
|
|
|
420
421
|
/** 救援模式 profile patch 的头注释(按 bundle 处理方式给出准确说明,不得夸大能力)。 */
|
|
421
422
|
function rescuePatchHeader(bundlesDisabled: boolean): string {
|
|
422
423
|
const note = bundlesDisabled
|
|
423
|
-
? '
|
|
424
|
-
: '
|
|
424
|
+
? engineMsg('core.bootRescue.bundlesNarrowed')
|
|
425
|
+
: engineMsg('core.bootRescue.bundlesNote');
|
|
425
426
|
return [...RESCUE_PATCH_HEADER_LINES, note, ''].join('\n');
|
|
426
427
|
}
|
|
427
428
|
|
package/src/core/boot-safety.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { BundleResolver } from './boot-rescue.ts';
|
|
|
23
23
|
import { BOOT_CRITICAL_RELS, profileCriticalRels } from './boot-paths.ts';
|
|
24
24
|
import type { MsgFunc } from './messages.ts';
|
|
25
25
|
import { zhMsg } from './messages.ts';
|
|
26
|
+
import { engineMsg } from './messages-core.ts';
|
|
26
27
|
|
|
27
28
|
/** 审计问题 id(稳定;测试与 UI 判定用,展示文本一律走 msg)。 */
|
|
28
29
|
export type BootSafetyIssueId =
|
|
@@ -167,7 +168,7 @@ export async function auditBootSafety(deps: BootSafetyDeps): Promise<BootSafetyR
|
|
|
167
168
|
// 只剔除「不可解析且非核心」的条目:其余条目(含解析不出的核心项)原样保留
|
|
168
169
|
const drop = new Set(prunable.map((p) => p.name))
|
|
169
170
|
const next = (Array.isArray(bundles) ? bundles : []).filter((b) => !(typeof b === 'string' && drop.has(b))) as string[]
|
|
170
|
-
if (!writeBundles(pkg, next)) throw new Error('
|
|
171
|
+
if (!writeBundles(pkg, next)) throw new Error(engineMsg('core.bootSafety.bundlesNotArray'))
|
|
171
172
|
await deps.writeText(pkgRel, JSON.stringify(pkg, null, 2) + '\n')
|
|
172
173
|
written = true
|
|
173
174
|
} catch (err) {
|