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/routes/snapshots.ts
CHANGED
|
@@ -16,6 +16,17 @@ import { buildFileDiffBody, buildRestoreBody, executeRestorePlan, makeRestoreExe
|
|
|
16
16
|
import { EnvironmentLockUnavailableError, runWithMutationLock } from '../utils/env-lock.ts'
|
|
17
17
|
import { isENOENT } from '../utils/guards.ts'
|
|
18
18
|
import { join } from 'node:path'
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 恢复计划指纹(预览-执行一致性):只取「动作类型 + 目标 + blob」三列 —— 描述文案、详情、顺序无关的
|
|
23
|
+
* 易变字段一律不进,保证同一份快照 + 同一磁盘状态下指纹稳定。内容变化不会改变指纹(同 kind/target),
|
|
24
|
+
* 只有**动作集合真的变了**(文件出现/消失、插件增删)才会不同 —— 那正是需要用户重新确认的情况。
|
|
25
|
+
*/
|
|
26
|
+
function restorePlanFingerprint(plan: Awaited<ReturnType<typeof planRestore>>): string {
|
|
27
|
+
const lines = plan.actions.map((action) => [action.kind, action.target ?? '', action.blobPath ?? ''].join('\u0000'))
|
|
28
|
+
return createHash('sha256').update(lines.join('\n')).digest('hex')
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
|
|
21
32
|
const {
|
|
@@ -32,7 +43,11 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
32
43
|
// M4:列出快照元信息(id/createdAt/sourceZip/status/计数,createdAt 倒序)
|
|
33
44
|
endpoint({ path: '/api/dsh-config-manager/snapshots', methods: ['GET'] }, async (req, res) => {
|
|
34
45
|
try {
|
|
35
|
-
|
|
46
|
+
// 「读不到」与「没有」必须分清(core 的 out.unreadable,验收 F2):快照目录被权限/占位挡住时
|
|
47
|
+
// 一律回传事实,界面据此渲染告警 —— 此前这个信号在路由层被丢掉,用户看到的是空列表。
|
|
48
|
+
const out: { unreadable?: string } = {}
|
|
49
|
+
const snapshots = await listSnapshots(snapshotsDir, out)
|
|
50
|
+
writeJson(res, 200, { snapshots, unreadable: out.unreadable ?? null })
|
|
36
51
|
} catch (error) {
|
|
37
52
|
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
38
53
|
}
|
|
@@ -78,7 +93,8 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
78
93
|
// git 风格恢复预览:逐动作的变更状态 + 行数统计(读取有上限,见 core/snapshot-diff.ts)。
|
|
79
94
|
// summarize 内部逐项兜底、绝不抛错 —— 统计失败不影响计划本身。
|
|
80
95
|
const changeSummary = await summarizeRestoreChanges({ plan, snapshotDir, homeDir: host.homeDir, msg })
|
|
81
|
-
|
|
96
|
+
// 指纹随预览一起下发:执行时带回来即可发现「确认后磁盘又变了」
|
|
97
|
+
writeJson(res, 200, { dryRun: true, plan, changeSummary, planFingerprint: restorePlanFingerprint(plan) })
|
|
82
98
|
return
|
|
83
99
|
}
|
|
84
100
|
// 真实执行(Phase 2 锁:destructive 必须先获取 GLOBAL 环境锁;被挡 → 423)
|
|
@@ -95,6 +111,16 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
95
111
|
const runId = run.runId
|
|
96
112
|
try {
|
|
97
113
|
const plan = await planRestore(restoreOpts)
|
|
114
|
+
// 预览-执行一致性:客户端带了预览指纹而当前计划与之不符 → 拒绝(409)并让用户重新预览。
|
|
115
|
+
// 为什么必须挡:用户确认的是「还原 A/B」,此刻磁盘已被改动的话,同一份快照可能多出
|
|
116
|
+
// hostFileRemove / fileRemove 这类**破坏性**动作 —— 确认内容与实际执行内容不一致且无人提示。
|
|
117
|
+
const expectedFingerprint = parsed.value.planFingerprint
|
|
118
|
+
if (expectedFingerprint !== undefined && expectedFingerprint !== restorePlanFingerprint(plan)) {
|
|
119
|
+
host.log.warn('恢复被拒:预览后计划已变化', { snapshotId })
|
|
120
|
+
runs.fail(runId, 'plan-changed')
|
|
121
|
+
writeJson(res, 409, { error: 'plan-changed', code: 'plan-changed', runId })
|
|
122
|
+
return
|
|
123
|
+
}
|
|
98
124
|
const report = await executeRestorePlan(
|
|
99
125
|
plan,
|
|
100
126
|
makeRestoreExecutor(snapshotDir, host, host.profile),
|
|
@@ -118,7 +144,11 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
118
144
|
snapshotId: snapshotId,
|
|
119
145
|
runId,
|
|
120
146
|
source: 'api',
|
|
121
|
-
summary:
|
|
147
|
+
summary: msg('route.snapshotRestoreSummary', {
|
|
148
|
+
id: snapshotId,
|
|
149
|
+
count: report.restored.length,
|
|
150
|
+
suffix: report.removedPlugins.length > 0 ? msg('route.snapshotRestoreRemovedPluginsSuffix', { count: report.removedPlugins.length }) : '',
|
|
151
|
+
}),
|
|
122
152
|
})
|
|
123
153
|
writeJson(res, 200, historyError === undefined ? { dryRun: false, report, runId } : { dryRun: false, report, runId, historyWriteError: historyError })
|
|
124
154
|
} catch (error) {
|
|
@@ -159,6 +189,23 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
159
189
|
writeJson(res, 400, { error: 'snapshotId is required and must be a valid snapshot id' })
|
|
160
190
|
return
|
|
161
191
|
}
|
|
192
|
+
// Phase 4 F3 同源:被未收敛 journal 引用的快照是 SAFE MODE 恢复的唯一证据 ——
|
|
193
|
+
// 自动保留清理早已豁免它们,手动删除也必须挡住(此前只校验了 id 形状)。
|
|
194
|
+
// 读不到引用集合时**保守拒绝**(与自动 prune 的 fail-closed 同一条口径),并让原因可辨。
|
|
195
|
+
if (host.phase3Recovery !== undefined) {
|
|
196
|
+
let referenced: Set<string>
|
|
197
|
+
try {
|
|
198
|
+
referenced = await host.phase3Recovery.store.listReferencedSnapshotIds()
|
|
199
|
+
} catch (error) {
|
|
200
|
+
host.log.warn('快照删除被拒:读取 recovery 引用集合失败', { error: error instanceof Error ? error.message : String(error) })
|
|
201
|
+
writeJson(res, 409, { error: 'recovery-reference-unreadable', code: 'recovery-reference-unreadable' })
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if (referenced.has(id)) {
|
|
205
|
+
writeJson(res, 409, { error: 'snapshot-referenced-by-recovery', code: 'snapshot-referenced-by-recovery' })
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
}
|
|
162
209
|
const removed = await deleteSnapshot(snapshotsDir, id)
|
|
163
210
|
const historyError = await tryAppendHistory({
|
|
164
211
|
kind: 'snapshot-delete',
|
|
@@ -166,7 +213,7 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
166
213
|
sections: [id],
|
|
167
214
|
snapshotId: id,
|
|
168
215
|
source: 'api',
|
|
169
|
-
summary:
|
|
216
|
+
summary: msg('route.snapshotDeleteSummary', { id }),
|
|
170
217
|
})
|
|
171
218
|
writeJson(res, 200, historyError === undefined ? { ok: true, removed } : { ok: true, removed, historyWriteError: historyError })
|
|
172
219
|
} catch (error) {
|
package/src/routes/sync.ts
CHANGED
|
@@ -118,7 +118,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
118
118
|
resolveSyncPassword,
|
|
119
119
|
scheduler,
|
|
120
120
|
selectionCache,
|
|
121
|
-
|
|
121
|
+
selectionOptInSections,
|
|
122
122
|
selectionView,
|
|
123
123
|
selectionViewByChannel,
|
|
124
124
|
syncCredentialsByChannelView,
|
|
@@ -126,6 +126,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
126
126
|
syncPasswordConfigured,
|
|
127
127
|
syncSectionCatalog,
|
|
128
128
|
syncSessions,
|
|
129
|
+
syncWorkDir,
|
|
129
130
|
tmpDir,
|
|
130
131
|
tryAppendHistory,
|
|
131
132
|
withMutationGate,
|
|
@@ -163,6 +164,11 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
163
164
|
: undefined
|
|
164
165
|
// 云端点配置视图(只回 secretStored 布尔;与当前通道无关,配置过即回填)
|
|
165
166
|
const cloud = await cloudChannelViews(full, credentials)
|
|
167
|
+
// issue #79:git 工作副本是否已跟随 repoUrl(只读自检,2 条本地只读 git 命令)。
|
|
168
|
+
// 读不出来时**如实回传错误**(workDirError),不让状态接口静默少一块信息。
|
|
169
|
+
const gitRepoUrl = full?.git?.repoUrl ?? ''
|
|
170
|
+
let workDir: Awaited<ReturnType<typeof syncWorkDir.inspect>> | null = null
|
|
171
|
+
if (gitRepoUrl !== '') workDir = await syncWorkDir.inspect(gitRepoUrl)
|
|
166
172
|
writeJson(res, 200, {
|
|
167
173
|
ok: true,
|
|
168
174
|
configured: full !== null,
|
|
@@ -194,11 +200,44 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
194
200
|
autosync: await buildAutosyncStatus(syncDir, transport),
|
|
195
201
|
// 全部通道的自动同步状态(git/webdav 各自独立;UI 按当前 tab 取对应通道)
|
|
196
202
|
autosyncByChannel: await buildAutosyncStatusByChannel(syncDir),
|
|
203
|
+
// 工作副本事实(仅 git 且配置了地址时返回;'facts' = 自检成功,'error' = 自检读不出来)
|
|
204
|
+
...(workDir === null ? {} : { workDir }),
|
|
197
205
|
})
|
|
198
206
|
} catch (error) {
|
|
199
207
|
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
200
208
|
}
|
|
201
209
|
}),
|
|
210
|
+
// ------------------------------------------------------ sync/workdir/reset
|
|
211
|
+
// issue #79:工作副本的自救入口(改造前 App 内完全没有 —— 改了仓库地址只能手工
|
|
212
|
+
// git remote set-url,或删掉 <dataDir>/sync/work 重来)。两种模式,破坏性清清楚楚:
|
|
213
|
+
// relink = 只把 origin / push URL 重指向当前配置地址(非破坏性:不碰工作树与本地提交);
|
|
214
|
+
// reclone = 删除工作副本目录后重新 clone(**丢弃未上行的本地提交**)→ 必须显式 confirm。
|
|
215
|
+
// 写动作 → 过 mutation gate(SAFE MODE / 环境锁);删除边界见 GitTransport.rebuildWorkDir
|
|
216
|
+
// (目录非空且无 .git 一律拒绝,绝不删来路不明的目录)。
|
|
217
|
+
endpoint({ path: '/api/dsh-config-manager/sync/workdir/reset', methods: ['POST'] }, withMutationGate('sync-workdir-reset', async (req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
const body = await requireJsonObject(req)
|
|
220
|
+
const mode = body['mode'] === undefined ? 'relink' : body['mode']
|
|
221
|
+
if (mode !== 'relink' && mode !== 'reclone') {
|
|
222
|
+
writeJson(res, 400, { error: msg('sync.workdirModeInvalid', { mode: JSON.stringify(body['mode']) }) })
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (mode === 'reclone' && body['confirm'] !== true) {
|
|
226
|
+
writeJson(res, 400, { error: msg('sync.workdirRecloneNeedsConfirm') })
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
const full = await readFullSyncConfig(syncDir)
|
|
230
|
+
const repoUrl = full?.git?.repoUrl ?? ''
|
|
231
|
+
if (repoUrl === '') {
|
|
232
|
+
writeJson(res, 400, { error: msg('sync.workdirNotConfigured') })
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
const rebuilt = await syncWorkDir.rebuild(repoUrl, mode)
|
|
236
|
+
writeJson(res, 200, { ok: true, mode, workDir: { facts: rebuilt } })
|
|
237
|
+
} catch (error) {
|
|
238
|
+
writeSyncRouteError(res, error)
|
|
239
|
+
}
|
|
240
|
+
})),
|
|
202
241
|
// ------------------------------------------------------ sync/config
|
|
203
242
|
// m-sync-config:保存同步通道配置(parseSyncBody 校验 + password/token 写 DSH credentials +
|
|
204
243
|
// writeSyncConfig 落盘)。UI 表单自动保存 /「保存配置」按钮调用;响应为轻量状态视图
|
|
@@ -295,7 +334,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
295
334
|
const body = await requireJsonObject(req)
|
|
296
335
|
try {
|
|
297
336
|
const syncCfg = await prepareSync(body)
|
|
298
|
-
|
|
337
|
+
// 用户驱动的推送:放行持久化选择里勾选的可选分区(mcp / sessions),否则它们按非 portable 跳过
|
|
338
|
+
const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
|
|
299
339
|
const snapshotId =
|
|
300
340
|
typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined
|
|
301
341
|
const sections = extractSyncSections(body, knownSyncSectionIds)
|
|
@@ -311,7 +351,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
311
351
|
// 因为那是用户此刻输入的覆盖值;两者都没有 → 交给 engine 报「加密需要密码」。
|
|
312
352
|
const pushChannel: SyncTransportType = channelOf(syncCfg)
|
|
313
353
|
const effectiveEncryptPassword = encryptPassword ?? await resolveSyncPassword(syncPasswordRef('ENCRYPT', pushChannel))
|
|
314
|
-
//
|
|
354
|
+
// sessions 的导出参数(「最新 N 个」/ 逐会话点名);**放行**由引擎的 optInSections 决定
|
|
315
355
|
const sessions = extractSyncSessions(body)
|
|
316
356
|
const sessionsOpt = sections !== undefined && sections.includes('sessions') && sessions !== undefined
|
|
317
357
|
? { sessions }
|
|
@@ -352,8 +392,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
352
392
|
const body = await requireJsonObject(req)
|
|
353
393
|
try {
|
|
354
394
|
const syncCfg = await prepareSync(body)
|
|
355
|
-
//
|
|
356
|
-
const engine = makeSyncEngine(syncCfg, {
|
|
395
|
+
// 用户驱动的拉取:放行持久化选择里勾选的**全部**可选分区(自动同步只放行 'auto' 档)
|
|
396
|
+
const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
|
|
357
397
|
const strategy =
|
|
358
398
|
body['strategy'] === 'replace' || body['strategy'] === 'skipExisting' ? body['strategy'] : 'merge'
|
|
359
399
|
const snapshotId =
|
|
@@ -616,7 +656,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
616
656
|
const body = await requireJsonObject(req)
|
|
617
657
|
try {
|
|
618
658
|
const syncCfg = await prepareSync(body)
|
|
619
|
-
const engine = makeSyncEngine(syncCfg, {
|
|
659
|
+
const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
|
|
620
660
|
const snapshotId = typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined
|
|
621
661
|
// 可选落地文件名(产物库「下载」要给用户一个可辨识的名字,而不是固定的 snapshot.zip)。
|
|
622
662
|
// 不安全的名字直接 400 而不是「静默换名」:它会进 path.join(dir, name),白名单是唯一防线。
|
|
@@ -708,8 +748,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
708
748
|
const body = await requireJsonObject(req)
|
|
709
749
|
try {
|
|
710
750
|
const syncCfg = await prepareSync(body)
|
|
711
|
-
//
|
|
712
|
-
const engine = makeSyncEngine(syncCfg, {
|
|
751
|
+
// 用户驱动的一键同步:放行持久化选择里勾选的**全部**可选分区(自动同步只放行 'auto' 档)
|
|
752
|
+
const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
|
|
713
753
|
const snapshotId = typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined
|
|
714
754
|
// 解密密码:请求体 > 已保存(DSH credentials)。明文快照根本不会被解密
|
|
715
755
|
// (engine.prepareSnapshot 只在 manifest.encrypted 时才用密码),因此「未加密备份
|
|
@@ -731,7 +771,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
731
771
|
msg('host.syncPullTimeout'),
|
|
732
772
|
)
|
|
733
773
|
if (!preview.ok || preview.plan === null || preview.analysis === null) {
|
|
734
|
-
writeJson(res, 200, { ok: false, syncSessionId: '', snapshotId: preview.snapshotId, items: [], needsReview: false, compatibility: 'unsupported', message: preview.message ?? '
|
|
774
|
+
writeJson(res, 200, { ok: false, syncSessionId: '', snapshotId: preview.snapshotId, items: [], needsReview: false, compatibility: 'unsupported', message: preview.message ?? msg('route.syncPreviewFailed') })
|
|
735
775
|
return
|
|
736
776
|
}
|
|
737
777
|
// T1:abort 处置在**预览阶段**就阻断(此刻仍零写入:临时 ZIP 与内存会话都还没登记)。
|
|
@@ -777,7 +817,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
777
817
|
const syncSessionId = typeof body['syncSessionId'] === 'string' ? body['syncSessionId'] : ''
|
|
778
818
|
const session = syncSessions.get(syncSessionId)
|
|
779
819
|
if (session === undefined) {
|
|
780
|
-
writeJson(res, 400, { error: '
|
|
820
|
+
writeJson(res, 400, { error: msg('route.syncSessionExpired') })
|
|
781
821
|
return
|
|
782
822
|
}
|
|
783
823
|
// T1:abort 处置在**应用前**再判一次(客户端可能绕过 preview 直接 apply,或预览后改了处置)。
|
|
@@ -804,7 +844,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
804
844
|
if (d === undefined || !d.adopt) return []
|
|
805
845
|
// Conflict 项必须有 resolution;keepLocal/skip 不写入本地 → 剔除
|
|
806
846
|
if (item.kind === 'Conflict') {
|
|
807
|
-
if (d.resolution === undefined) throw new SyncRouteError(
|
|
847
|
+
if (d.resolution === undefined) throw new SyncRouteError(msg('route.syncConflictResolutionRequired', { id: item.id }))
|
|
808
848
|
if (d.resolution === 'keepLocal' || d.resolution === 'skip') return []
|
|
809
849
|
// useRemote → 转成 Update 计划项(镜像 analyzer.applyItemResolution 的
|
|
810
850
|
// useImported 分支),applyOne 才会把远端值真正写进本地。
|
|
@@ -850,10 +890,14 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
850
890
|
source: 'api',
|
|
851
891
|
// issue #56:历史如实区分「全部成功 / 部分成功(N 项未生效)」——
|
|
852
892
|
// 此前 ok:true 一律写「N 项」+ 无 error,用户在历史里看到的就是一次干净的成功。
|
|
853
|
-
summary:
|
|
893
|
+
summary: msg('route.syncApplySummary', {
|
|
894
|
+
count: Array.isArray(report.applied) ? report.applied.length : subItems.length,
|
|
895
|
+
status: report.rolledBack === true ? (report.rollbackFull === false ? msg('route.syncRollbackIncomplete') : msg('route.syncRolledBack')) : '',
|
|
896
|
+
ineffective: report.ineffective.length > 0 ? msg('route.syncIneffectiveSuffix', { count: report.ineffective.length }) : '',
|
|
897
|
+
}),
|
|
854
898
|
error: report.ok
|
|
855
|
-
? (report.ineffective.length > 0 ? '
|
|
856
|
-
: '
|
|
899
|
+
? (report.ineffective.length > 0 ? msg('route.syncApplyPartial') : undefined)
|
|
900
|
+
: msg('route.syncApplyFailed'),
|
|
857
901
|
})
|
|
858
902
|
writeJson(res, 200, historyError === undefined ? {
|
|
859
903
|
ok: report.ok,
|
|
@@ -863,6 +907,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
863
907
|
warnings: report.warnings,
|
|
864
908
|
restoreId: report.restoreId,
|
|
865
909
|
rolledBack: report.rolledBack,
|
|
910
|
+
// 诚实性(2026-10):整体回滚是否完整(false = 有项没恢复,失败项已并入 warnings)
|
|
911
|
+
...(report.rollbackFull === undefined ? {} : { rollbackFull: report.rollbackFull }),
|
|
866
912
|
failed: report.failed,
|
|
867
913
|
// issue #56:未生效项(warning)单独回传,界面据此把「成功」降级为「部分成功」
|
|
868
914
|
ineffective: report.ineffective,
|
|
@@ -875,6 +921,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
875
921
|
warnings: report.warnings,
|
|
876
922
|
restoreId: report.restoreId,
|
|
877
923
|
rolledBack: report.rolledBack,
|
|
924
|
+
// 诚实性(2026-10):整体回滚是否完整(false = 有项没恢复,失败项已并入 warnings)
|
|
925
|
+
...(report.rollbackFull === undefined ? {} : { rollbackFull: report.rollbackFull }),
|
|
878
926
|
failed: report.failed,
|
|
879
927
|
ineffective: report.ineffective,
|
|
880
928
|
result: report.result,
|
|
@@ -1019,9 +1067,12 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
|
|
|
1019
1067
|
sections: [],
|
|
1020
1068
|
snapshotId: restoreId,
|
|
1021
1069
|
source: 'api',
|
|
1022
|
-
summary:
|
|
1070
|
+
summary: msg('route.syncRollbackSummary', { id: restoreId }),
|
|
1023
1071
|
})
|
|
1024
|
-
|
|
1072
|
+
// 诚实回传:full=false 时把 failed(含 manualHint)一并给出,界面据此列出「哪些项没回滚、
|
|
1073
|
+
// 需要人工补录」而不是无条件报「已回滚」。CLI 侧同口径见 src/cli/actions.ts 的 rollback 文案。
|
|
1074
|
+
const payload = { ok: true, full: report.full, failed: report.failed }
|
|
1075
|
+
writeJson(res, 200, historyError === undefined ? payload : { ...payload, historyWriteError: historyError })
|
|
1025
1076
|
} catch (error) {
|
|
1026
1077
|
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
1027
1078
|
}
|
package/src/schema/config.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { parseJsonSafe } from '../utils/json.ts';
|
|
10
10
|
import { isPathSafe } from '../utils/paths.ts';
|
|
11
|
-
import {
|
|
11
|
+
import { schemaMsg } from './messages.ts';
|
|
12
|
+
import { sectionMetaOf, type SectionMeta } from './section-registry.ts';
|
|
12
13
|
import { sectionDataVersionIssue } from './versions.ts';
|
|
13
14
|
import type { SectionData, SectionId } from './types.ts';
|
|
14
15
|
|
|
@@ -22,7 +23,6 @@ export {
|
|
|
22
23
|
SECTION_DATA_VERSION,
|
|
23
24
|
sectionMeta,
|
|
24
25
|
sectionMetaOf,
|
|
25
|
-
requireSectionMeta,
|
|
26
26
|
isSectionId,
|
|
27
27
|
jsonPathOf,
|
|
28
28
|
filePrefixOf,
|
|
@@ -36,13 +36,29 @@ export type {
|
|
|
36
36
|
SectionPortability,
|
|
37
37
|
} from './section-registry.ts';
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* 拿不到元数据就**显式失败**(绝不让未注册分区按其它分区语义继续)。
|
|
41
|
+
* core 侧修正「未知分区静默记成 settingsNamespace」时用它替代 default 分支。
|
|
42
|
+
*
|
|
43
|
+
* t7:本函数原在 `section-registry.ts`;该模块受「只 import 一个类型」的零依赖铁律约束
|
|
44
|
+
* (client bundle 侧 `ui/export-flow.ts` 也在用它),故把唯一的用户可见文案抛出点迁到本文件
|
|
45
|
+
* (已持有本域消息目录)。public surface 不变:`config.ts` 与 `schema/index.ts` 仍照旧导出它。
|
|
46
|
+
*/
|
|
47
|
+
export function requireSectionMeta(id: string): SectionMeta {
|
|
48
|
+
const meta = sectionMetaOf(id);
|
|
49
|
+
if (meta === null) {
|
|
50
|
+
throw new Error(schemaMsg('schema.sectionNotRegistered', { section: id }));
|
|
51
|
+
}
|
|
52
|
+
return meta;
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
/** 从 ZIP 内 JSON 解析分区数据(深度保护 + 结构校验) */
|
|
40
56
|
export function parseSectionJson<T extends SectionData>(sectionId: SectionId, raw: string): T {
|
|
41
57
|
const parsed = parseJsonSafe(raw) as T;
|
|
42
58
|
const issues = validateSectionData(sectionId, parsed);
|
|
43
59
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
44
60
|
if (errors.length > 0) {
|
|
45
|
-
throw new Error(
|
|
61
|
+
throw new Error(schemaMsg('schema.sectionDataInvalid', { section: sectionId, issues: errors.map((e) => e.message).join('; ') }));
|
|
46
62
|
}
|
|
47
63
|
return parsed;
|
|
48
64
|
}
|
|
@@ -60,11 +76,11 @@ export interface SectionIssue { path: string; message: string; severity: 'error'
|
|
|
60
76
|
export function validateSectionData(sectionId: SectionId, data: unknown): SectionIssue[] {
|
|
61
77
|
const issues: SectionIssue[] = [];
|
|
62
78
|
if (data === null || typeof data !== 'object') {
|
|
63
|
-
return [{ path: '$', message:
|
|
79
|
+
return [{ path: '$', message: schemaMsg('schema.sectionDataMustBeObject', { section: sectionId }), severity: 'error' }];
|
|
64
80
|
}
|
|
65
81
|
const meta = sectionMetaOf(sectionId);
|
|
66
82
|
if (meta === null) {
|
|
67
|
-
return [{ path: '$', message:
|
|
83
|
+
return [{ path: '$', message: schemaMsg('schema.sectionUnregistered', { section: String(sectionId) }), severity: 'error' }];
|
|
68
84
|
}
|
|
69
85
|
const obj = data as Record<string, unknown>;
|
|
70
86
|
const versionIssue = sectionDataVersionIssue(meta.id, obj['version']);
|
|
@@ -77,32 +93,32 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
|
|
|
77
93
|
case 'namespaces': {
|
|
78
94
|
const ns = obj['namespaces'];
|
|
79
95
|
if (ns === null || typeof ns !== 'object') {
|
|
80
|
-
issues.push({ path: 'namespaces', message:
|
|
96
|
+
issues.push({ path: 'namespaces', message: schemaMsg('schema.namespacesMissing', { section: meta.id }), severity: 'error' });
|
|
81
97
|
} else {
|
|
82
98
|
for (const [name, rec] of Object.entries(ns as Record<string, unknown>)) {
|
|
83
99
|
if (rec === null || typeof rec !== 'object') {
|
|
84
|
-
issues.push({ path: `namespaces.${name}`, message: '
|
|
100
|
+
issues.push({ path: `namespaces.${name}`, message: schemaMsg('schema.namespaceRecordMustBeObject'), severity: 'error' });
|
|
85
101
|
continue;
|
|
86
102
|
}
|
|
87
103
|
const r = rec as Record<string, unknown>;
|
|
88
|
-
if (typeof r['revision'] !== 'number') issues.push({ path: `namespaces.${name}.revision`, message: '
|
|
89
|
-
if (!('value' in r)) issues.push({ path: `namespaces.${name}.value`, message: '
|
|
104
|
+
if (typeof r['revision'] !== 'number') issues.push({ path: `namespaces.${name}.revision`, message: schemaMsg('schema.revisionMustBeNumber'), severity: 'error' });
|
|
105
|
+
if (!('value' in r)) issues.push({ path: `namespaces.${name}.value`, message: schemaMsg('schema.valueMissing'), severity: 'error' });
|
|
90
106
|
const secrets = r['secrets'];
|
|
91
|
-
if (secrets !== undefined && !Array.isArray(secrets)) issues.push({ path: `namespaces.${name}.secrets`, message: '
|
|
107
|
+
if (secrets !== undefined && !Array.isArray(secrets)) issues.push({ path: `namespaces.${name}.secrets`, message: schemaMsg('schema.secretsMustBeArray'), severity: 'error' });
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
110
|
break;
|
|
95
111
|
}
|
|
96
112
|
case 'object': {
|
|
97
113
|
const value = obj[payload.key];
|
|
98
|
-
if (value === null || typeof value !== 'object') issues.push({ path: payload.key, message:
|
|
114
|
+
if (value === null || typeof value !== 'object') issues.push({ path: payload.key, message: schemaMsg('schema.objectMissing', { key: payload.key }), severity: 'error' });
|
|
99
115
|
break;
|
|
100
116
|
}
|
|
101
117
|
case 'array': {
|
|
102
|
-
if (!Array.isArray(obj[payload.key])) issues.push({ path: payload.key, message:
|
|
118
|
+
if (!Array.isArray(obj[payload.key])) issues.push({ path: payload.key, message: schemaMsg('schema.keyMustBeArray', { key: payload.key }), severity: 'error' });
|
|
103
119
|
for (const extra of payload.extraArrayKeys ?? []) {
|
|
104
120
|
if (obj[extra] !== undefined && !Array.isArray(obj[extra])) {
|
|
105
|
-
issues.push({ path: extra, message:
|
|
121
|
+
issues.push({ path: extra, message: schemaMsg('schema.keyMustBeArray', { key: extra }), severity: 'error' });
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
if (payload.validatePatchFiles === true) {
|
|
@@ -111,20 +127,20 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
|
|
|
111
127
|
const patchFiles = obj['patchFiles'];
|
|
112
128
|
if (patchFiles !== undefined) {
|
|
113
129
|
if (!Array.isArray(patchFiles)) {
|
|
114
|
-
issues.push({ path: 'patchFiles', message: 'patchFiles
|
|
130
|
+
issues.push({ path: 'patchFiles', message: schemaMsg('schema.keyMustBeArray', { key: 'patchFiles' }), severity: 'error' });
|
|
115
131
|
} else {
|
|
116
132
|
patchFiles.forEach((pf, i) => {
|
|
117
133
|
const rec = (pf !== null && typeof pf === 'object') ? pf as Record<string, unknown> : null;
|
|
118
134
|
if (rec === null) {
|
|
119
|
-
issues.push({ path: `patchFiles[${i}]`, message: '
|
|
135
|
+
issues.push({ path: `patchFiles[${i}]`, message: schemaMsg('schema.patchFileEntryMustBeObject'), severity: 'error' });
|
|
120
136
|
return;
|
|
121
137
|
}
|
|
122
138
|
const rel = rec['relativePath'];
|
|
123
139
|
if (typeof rel !== 'string' || rel === '' || !isPathSafe(rel)) {
|
|
124
|
-
issues.push({ path: `patchFiles[${i}].relativePath`, message: '
|
|
140
|
+
issues.push({ path: `patchFiles[${i}].relativePath`, message: schemaMsg('schema.patchFilePathUnsafe'), severity: 'error' });
|
|
125
141
|
}
|
|
126
142
|
if (typeof rec['base64'] !== 'string') {
|
|
127
|
-
issues.push({ path: `patchFiles[${i}].base64`, message: '
|
|
143
|
+
issues.push({ path: `patchFiles[${i}].base64`, message: schemaMsg('schema.patchFileContentMustBeBase64'), severity: 'error' });
|
|
128
144
|
}
|
|
129
145
|
});
|
|
130
146
|
}
|
|
@@ -133,14 +149,31 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
|
|
|
133
149
|
break;
|
|
134
150
|
}
|
|
135
151
|
case 'files': {
|
|
136
|
-
|
|
152
|
+
const files = obj['files'];
|
|
153
|
+
if (!Array.isArray(files)) {
|
|
154
|
+
issues.push({ path: 'files', message: schemaMsg('schema.keyMustBeArray', { key: 'files' }), severity: 'error' });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
// 与 patchFiles 同级的防线(此前只判了 Array.isArray):relativePath 会被写到 <home> 之下,
|
|
158
|
+
// 不可信 bundle 的路径穿越向量必须逐项过 isPathSafe(不得为绝对路径 / 含 ..)。
|
|
159
|
+
files.forEach((entry, i) => {
|
|
160
|
+
const rec = (entry !== null && typeof entry === 'object') ? entry as Record<string, unknown> : null;
|
|
161
|
+
if (rec === null) {
|
|
162
|
+
issues.push({ path: `files[${i}]`, message: schemaMsg('schema.fileEntryMustBeObject'), severity: 'error' });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const rel = rec['relativePath'];
|
|
166
|
+
if (typeof rel !== 'string' || rel === '' || !isPathSafe(rel)) {
|
|
167
|
+
issues.push({ path: `files[${i}].relativePath`, message: schemaMsg('schema.filePathUnsafe'), severity: 'error' });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
137
170
|
break;
|
|
138
171
|
}
|
|
139
172
|
case 'none': {
|
|
140
173
|
// secrets:凭据值走独立加密容器(.credentials.yaml / secrets.enc),没有分区 JSON 可校验
|
|
141
174
|
issues.push({
|
|
142
175
|
path: '$',
|
|
143
|
-
message:
|
|
176
|
+
message: schemaMsg('schema.sectionNoJsonPayload', { section: meta.id }),
|
|
144
177
|
severity: 'error',
|
|
145
178
|
});
|
|
146
179
|
break;
|
|
@@ -148,7 +181,7 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
|
|
|
148
181
|
default: {
|
|
149
182
|
// 判别联合已穷尽:新增 payload.kind 时此赋值会编译失败(提醒补校验分支)
|
|
150
183
|
const exhaustive: never = payload;
|
|
151
|
-
issues.push({ path: '$', message:
|
|
184
|
+
issues.push({ path: '$', message: schemaMsg('schema.unhandledPayloadKind', { payload: JSON.stringify(exhaustive) }), severity: 'error' });
|
|
152
185
|
}
|
|
153
186
|
}
|
|
154
187
|
return issues;
|
package/src/schema/index.ts
CHANGED
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
* 本文件把 `./schema` 变成真实可用的运行时入口。
|
|
20
20
|
*
|
|
21
21
|
* 约束(勿破):本文件只 import 同目录模块(`./config.ts` / `./section-registry.ts` / `./types.ts` /
|
|
22
|
-
* `./versions.ts`)。`section-registry.ts`
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* `./versions.ts`)。`section-registry.ts` 自身仍然只 import 一个**类型**(`type { SectionId }`;
|
|
23
|
+
* 未注册分区的错误文案 t7 起在 `config.ts`,见那里)。
|
|
24
|
+
* 目标性质是**零 `node:` / 零 npm / 零 @deepseek-ai 依赖**(headless 消费:
|
|
25
|
+
* `docs/spec/headless-consumption.md`;client bundle 自包含)。唯一的跨层依赖是本域消息目录
|
|
26
|
+
* (`./messages.ts` → `../core/engine-locale.ts`,纯函数、零 node / 零 npm),不破坏该性质;
|
|
27
|
+
* 除此之外不得 import `../core/*`、`node:*` 或任何 UI / DSH 包。
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
/* —— 版本判定(唯一出口,业务代码零散落 `if (v === 1)`) —— */
|
|
@@ -44,6 +46,7 @@ export {
|
|
|
44
46
|
SECTION_JSON_PATHS,
|
|
45
47
|
SECTION_FILE_PREFIXES,
|
|
46
48
|
isFileSection,
|
|
49
|
+
requireSectionMeta,
|
|
47
50
|
} from './config.ts';
|
|
48
51
|
|
|
49
52
|
/*
|
|
@@ -57,7 +60,6 @@ export {
|
|
|
57
60
|
SECTION_DATA_VERSION,
|
|
58
61
|
sectionMeta,
|
|
59
62
|
sectionMetaOf,
|
|
60
|
-
requireSectionMeta,
|
|
61
63
|
isSectionId,
|
|
62
64
|
jsonPathOf,
|
|
63
65
|
filePrefixOf,
|
package/src/schema/manifest.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { parseJsonSafe } from '../utils/json.ts';
|
|
|
6
6
|
import { CURRENT_SCHEMA_VERSION } from './versions.ts';
|
|
7
7
|
import type { Manifest, Platform, SectionId } from './types.ts';
|
|
8
8
|
import { SECTION_IDS } from './config.ts';
|
|
9
|
+
import { schemaMsg } from './messages.ts';
|
|
9
10
|
|
|
10
11
|
export const MANIFEST_FILE = 'manifest.json';
|
|
11
12
|
export const CHECKSUMS_FILE = 'integrity/checksums.json';
|
|
@@ -57,7 +58,7 @@ export function parseManifest(raw: string): Manifest {
|
|
|
57
58
|
const issues = validateManifest(parsed);
|
|
58
59
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
59
60
|
if (errors.length > 0) {
|
|
60
|
-
throw new Error(
|
|
61
|
+
throw new Error(schemaMsg('schema.manifestInvalid', { issues: errors.map((e) => e.message).join('; ') }));
|
|
61
62
|
}
|
|
62
63
|
return parsed as Manifest;
|
|
63
64
|
}
|
|
@@ -68,58 +69,58 @@ export interface ManifestIssue { path: string; message: string; severity: 'error
|
|
|
68
69
|
export function validateManifest(m: unknown): ManifestIssue[] {
|
|
69
70
|
const issues: ManifestIssue[] = [];
|
|
70
71
|
if (m === null || typeof m !== 'object') {
|
|
71
|
-
return [{ path: '$', message: '
|
|
72
|
+
return [{ path: '$', message: schemaMsg('schema.manifestMustBeObject'), severity: 'error' }];
|
|
72
73
|
}
|
|
73
74
|
const obj = m as Record<string, unknown>;
|
|
74
75
|
|
|
75
76
|
if (typeof obj['schemaVersion'] !== 'number') {
|
|
76
|
-
issues.push({ path: 'schemaVersion', message: '
|
|
77
|
+
issues.push({ path: 'schemaVersion', message: schemaMsg('schema.schemaVersionMustBeNumber'), severity: 'error' });
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
const exporter = obj['exporter'] as Record<string, unknown> | undefined;
|
|
80
81
|
if (!exporter || typeof exporter !== 'object') {
|
|
81
|
-
issues.push({ path: 'exporter', message: '
|
|
82
|
+
issues.push({ path: 'exporter', message: schemaMsg('schema.exporterMissing'), severity: 'error' });
|
|
82
83
|
} else {
|
|
83
|
-
if (typeof exporter['name'] !== 'string') issues.push({ path: 'exporter.name', message: '
|
|
84
|
-
if (typeof exporter['version'] !== 'string') issues.push({ path: 'exporter.version', message: '
|
|
84
|
+
if (typeof exporter['name'] !== 'string') issues.push({ path: 'exporter.name', message: schemaMsg('schema.exporterNameMustBeString'), severity: 'error' });
|
|
85
|
+
if (typeof exporter['version'] !== 'string') issues.push({ path: 'exporter.version', message: schemaMsg('schema.exporterVersionMustBeString'), severity: 'error' });
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
const source = obj['source'] as Record<string, unknown> | undefined;
|
|
88
89
|
if (!source || typeof source !== 'object') {
|
|
89
|
-
issues.push({ path: 'source', message: '
|
|
90
|
+
issues.push({ path: 'source', message: schemaMsg('schema.sourceMissing'), severity: 'error' });
|
|
90
91
|
} else {
|
|
91
|
-
if (typeof source['dshVersion'] !== 'string') issues.push({ path: 'source.dshVersion', message: '
|
|
92
|
-
if (typeof source['platform'] !== 'string') issues.push({ path: 'source.platform', message: '
|
|
93
|
-
if (typeof source['arch'] !== 'string') issues.push({ path: 'source.arch', message: '
|
|
92
|
+
if (typeof source['dshVersion'] !== 'string') issues.push({ path: 'source.dshVersion', message: schemaMsg('schema.sourceDshVersionMustBeString'), severity: 'error' });
|
|
93
|
+
if (typeof source['platform'] !== 'string') issues.push({ path: 'source.platform', message: schemaMsg('schema.sourcePlatformMustBeString'), severity: 'error' });
|
|
94
|
+
if (typeof source['arch'] !== 'string') issues.push({ path: 'source.arch', message: schemaMsg('schema.sourceArchMustBeString'), severity: 'error' });
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
if (typeof obj['exportedAt'] !== 'string' || Number.isNaN(Date.parse(obj['exportedAt']))) {
|
|
97
|
-
issues.push({ path: 'exportedAt', message: '
|
|
98
|
+
issues.push({ path: 'exportedAt', message: schemaMsg('schema.exportedAtInvalid'), severity: 'error' });
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
const sections = obj['sections'] as Record<string, unknown> | undefined;
|
|
101
102
|
if (!sections || typeof sections !== 'object') {
|
|
102
|
-
issues.push({ path: 'sections', message: '
|
|
103
|
+
issues.push({ path: 'sections', message: schemaMsg('schema.sectionsMissing'), severity: 'error' });
|
|
103
104
|
} else {
|
|
104
105
|
for (const key of Object.keys(sections)) {
|
|
105
106
|
if (!(SECTION_IDS as readonly string[]).includes(key)) {
|
|
106
|
-
issues.push({ path: `sections.${key}`, message:
|
|
107
|
+
issues.push({ path: `sections.${key}`, message: schemaMsg('schema.unknownSection', { section: key }), severity: 'warning' });
|
|
107
108
|
}
|
|
108
109
|
if (typeof sections[key] !== 'boolean') {
|
|
109
|
-
issues.push({ path: `sections.${key}`, message:
|
|
110
|
+
issues.push({ path: `sections.${key}`, message: schemaMsg('schema.sectionFlagMustBeBoolean', { section: key }), severity: 'error' });
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
const security = obj['security'] as Record<string, unknown> | undefined;
|
|
115
116
|
if (!security || typeof security !== 'object') {
|
|
116
|
-
issues.push({ path: 'security', message: '
|
|
117
|
+
issues.push({ path: 'security', message: schemaMsg('schema.securityMissing'), severity: 'error' });
|
|
117
118
|
} else {
|
|
118
|
-
if (typeof security['containsSecrets'] !== 'boolean') issues.push({ path: 'security.containsSecrets', message: '
|
|
119
|
-
if (typeof security['encrypted'] !== 'boolean') issues.push({ path: 'security.encrypted', message: '
|
|
119
|
+
if (typeof security['containsSecrets'] !== 'boolean') issues.push({ path: 'security.containsSecrets', message: schemaMsg('schema.containsSecretsMustBeBoolean'), severity: 'error' });
|
|
120
|
+
if (typeof security['encrypted'] !== 'boolean') issues.push({ path: 'security.encrypted', message: schemaMsg('schema.encryptedMustBeBoolean'), severity: 'error' });
|
|
120
121
|
const enc = security['encryption'];
|
|
121
122
|
if (enc !== null && (typeof enc !== 'object' || enc === undefined)) {
|
|
122
|
-
issues.push({ path: 'security.encryption', message: '
|
|
123
|
+
issues.push({ path: 'security.encryption', message: schemaMsg('schema.encryptionMustBeObjectOrNull'), severity: 'error' });
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|