@logictan/dsh-config-manager 0.1.60
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/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 远程同步区块的纯渲染模型(m-sync-ui)。
|
|
3
|
+
*
|
|
4
|
+
* 与 src/ui/progress.ts → progress-view.ts 同模式:把「报告怎么渲染 / 按钮什么状态 /
|
|
5
|
+
* 状态行写什么 / 私有仓库提示怎么展示」做成无副作用纯函数,node --test 直接测,
|
|
6
|
+
* React 组件只做装配。文案直接用中文(项目源语言),组件不重复造。
|
|
7
|
+
*/
|
|
8
|
+
import type { PlanItem, PlanItemKind } from '../../core/types.ts';
|
|
9
|
+
import type { SectionId } from '../../schema/types.ts';
|
|
10
|
+
import type { PullChange, SyncPullReport, SyncPushPreview, SyncPushReport } from '../../sync/sync-engine.ts';
|
|
11
|
+
import { DEFAULT_CATEGORIES } from '../../ui/export-flow.ts';
|
|
12
|
+
import { EXPORT_GROUPS, type ExportGroup } from '../../ui/types.ts';
|
|
13
|
+
import type {
|
|
14
|
+
ApplyItemsResponse, AutosyncInterval, AutosyncStatusResponse, GithubPollResponse, SyncConfirmItem,
|
|
15
|
+
SyncItemAdoption, SyncSectionInfo, SyncSnapshotLite, SyncStatusResponse,
|
|
16
|
+
} from './sync-api.ts';
|
|
17
|
+
import { zhUiT, type UiT } from '../../ui/i18n.ts';
|
|
18
|
+
|
|
19
|
+
/* ---------------------------------------------------------------- 私有仓库提示 */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 私有仓库强制提示文案(Settings 区块常驻警示横幅)。
|
|
23
|
+
* 安全约束:同步内容为可移植配置,public 仓库会公开配置 → 必须私有;
|
|
24
|
+
* token 仅用于认证,绝不写入同步文件/提交内容/日志。
|
|
25
|
+
*/
|
|
26
|
+
export function privateRepoHint(t: UiT = zhUiT): string {
|
|
27
|
+
return t('sync.privateRepoHint');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ---------------------------------------------------------------- 同步分区模式 */
|
|
31
|
+
|
|
32
|
+
/** 远程同步模式:默认(快速导出) / 高级(自定义导出)。 */
|
|
33
|
+
export type SyncMode = 'default' | 'advanced';
|
|
34
|
+
|
|
35
|
+
/** 可同步分区选项(status.syncSections 投影 + 导出目录补充分组/描述;高级模式勾选目录单选项)。 */
|
|
36
|
+
export interface SyncSectionOption {
|
|
37
|
+
id: SectionId;
|
|
38
|
+
label: string;
|
|
39
|
+
/** 一句话描述(来自导出目录;未知 id 为空串) */
|
|
40
|
+
description: string;
|
|
41
|
+
/** 所属导出分组(General / AI / Extensions / …;未知 id 兜底 'general') */
|
|
42
|
+
group: ExportGroup;
|
|
43
|
+
/** 是否为推荐分区(defaultIncluded=true;默认模式全选、高级模式初始勾选) */
|
|
44
|
+
defaultIncluded: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** host 目录(SyncSectionInfo[])→ UI 勾选项(保留 id 顺序)。
|
|
48
|
+
* 同步分区必为导出目录(DEFAULT_CATEGORIES)的可移植子集:分组/描述从导出目录
|
|
49
|
+
* 补充(单一事实源,与「导出备份·自定义模式」的目录保持一致),未命中 id 兜底。 */
|
|
50
|
+
export function syncSectionOptions(info: readonly SyncSectionInfo[]): SyncSectionOption[] {
|
|
51
|
+
const meta = new Map(DEFAULT_CATEGORIES.map((c) => [c.id, c]));
|
|
52
|
+
return info.map((s) => {
|
|
53
|
+
const cat = meta.get(s.id);
|
|
54
|
+
return {
|
|
55
|
+
id: s.id,
|
|
56
|
+
label: s.displayName,
|
|
57
|
+
description: cat?.description ?? '',
|
|
58
|
+
group: cat?.group ?? 'general',
|
|
59
|
+
defaultIncluded: s.defaultIncluded,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 高级模式勾选目录 → 按导出分组(EXPORT_GROUPS)投影:与「导出备份·自定义模式」同构,
|
|
65
|
+
* 空分组省略;UI 直接渲染 groupCard。 */
|
|
66
|
+
export function syncSectionGroups(options: readonly SyncSectionOption[]): {
|
|
67
|
+
group: ExportGroup;
|
|
68
|
+
label: string;
|
|
69
|
+
note?: string;
|
|
70
|
+
items: SyncSectionOption[];
|
|
71
|
+
}[] {
|
|
72
|
+
return EXPORT_GROUPS
|
|
73
|
+
.map((g) => ({
|
|
74
|
+
group: g.id,
|
|
75
|
+
label: g.label,
|
|
76
|
+
...(g.note !== undefined ? { note: g.note } : {}),
|
|
77
|
+
items: options.filter((o) => o.group === g.id),
|
|
78
|
+
}))
|
|
79
|
+
.filter((g) => g.items.length > 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 默认模式的推荐同步分区:默认包含的分区。 */
|
|
83
|
+
export function recommendedSyncSections(info: readonly SyncSectionInfo[]): SectionId[] {
|
|
84
|
+
return info.filter((s) => s.defaultIncluded).map((s) => s.id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ---------------------------------------------------------------- 变更摘要 */
|
|
88
|
+
|
|
89
|
+
/** 需要人工决策的 PlanItem 类型(与 SyncEngine.pull 的 needsReview 判定一致)。
|
|
90
|
+
* 注意:'Install' 不在此列 —— 插件安装随同步自动采用(product requirement)。 */
|
|
91
|
+
const REVIEW_KINDS: ReadonlySet<PlanItemKind> = new Set([
|
|
92
|
+
'Conflict', 'MissingSecret', 'MissingDependency', 'Error',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
export interface PullChangeSummary {
|
|
96
|
+
total: number;
|
|
97
|
+
info: number;
|
|
98
|
+
warning: number;
|
|
99
|
+
error: number;
|
|
100
|
+
/** 是否包含需要人工决策的项(冲突/密钥/依赖/安装/错误) */
|
|
101
|
+
needsReview: boolean;
|
|
102
|
+
items: PullChange[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 差异摘要:按 severity 计数 + 需人工决策标记(UI 统计徽章与警示横幅的数据源) */
|
|
106
|
+
export function summarizePullChanges(changes: readonly PullChange[]): PullChangeSummary {
|
|
107
|
+
let info = 0;
|
|
108
|
+
let warning = 0;
|
|
109
|
+
let error = 0;
|
|
110
|
+
let needsReview = false;
|
|
111
|
+
for (const c of changes) {
|
|
112
|
+
if (c.severity === 'error') error += 1;
|
|
113
|
+
else if (c.severity === 'warning') warning += 1;
|
|
114
|
+
else info += 1;
|
|
115
|
+
if (REVIEW_KINDS.has(c.kind)) needsReview = true;
|
|
116
|
+
}
|
|
117
|
+
return { total: changes.length, info, warning, error, needsReview, items: [...changes] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** PlanItemKind → 短标签(列表徽章) */
|
|
121
|
+
export function kindLabel(kind: PlanItemKind, t: UiT = zhUiT): string {
|
|
122
|
+
switch (kind) {
|
|
123
|
+
case 'Create': return t('sync.kind.create');
|
|
124
|
+
case 'Update': return t('sync.kind.update');
|
|
125
|
+
case 'Skip': return t('sync.kind.skip');
|
|
126
|
+
case 'Conflict': return t('sync.kind.conflict');
|
|
127
|
+
case 'Install': return t('sync.kind.install');
|
|
128
|
+
case 'MissingSecret': return t('sync.kind.missingSecret');
|
|
129
|
+
case 'MissingDependency': return t('sync.kind.missingDependency');
|
|
130
|
+
case 'PathMapping': return t('sync.kind.pathMapping');
|
|
131
|
+
case 'Warning': return t('sync.kind.warning');
|
|
132
|
+
case 'Error': return t('sync.kind.error');
|
|
133
|
+
default: return kind;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** severity → 短标签 */
|
|
138
|
+
export function severityLabel(severity: PlanItem['severity'], t: UiT = zhUiT): string {
|
|
139
|
+
switch (severity) {
|
|
140
|
+
case 'error': return t('sync.severity.error');
|
|
141
|
+
case 'warning': return t('sync.severity.warning');
|
|
142
|
+
default: return t('sync.severity.info');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ---------------------------------------------------------------- 按钮状态 */
|
|
147
|
+
|
|
148
|
+
/** 远程同步通道类型:git(默认)或 webdav */
|
|
149
|
+
export type SyncChannel = 'git' | 'webdav';
|
|
150
|
+
|
|
151
|
+
/* ---------------------------------------------------------------- 每通道独立状态 */
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 每个同步通道(git/webdav)各自独立的设置状态:
|
|
155
|
+
* 自动同步、同步模式(默认/高级 + 分区勾选)、远端快照互不共享。
|
|
156
|
+
*/
|
|
157
|
+
export interface ChannelSyncState {
|
|
158
|
+
/** 同步模式:默认(快速导出推荐分区) / 高级(自定义勾选分区) */
|
|
159
|
+
syncMode: SyncMode
|
|
160
|
+
/** 高级模式勾选的同步分区(初始 = 推荐分区;空 = 未勾选任何分区) */
|
|
161
|
+
syncSections: SectionId[]
|
|
162
|
+
/** 当前选中的历史快照 id('' = 最新) */
|
|
163
|
+
selectedSnapshotId: string
|
|
164
|
+
/** 该通道远端历史快照列表(「选择历史快照」下拉数据源) */
|
|
165
|
+
snapshots: SyncSnapshotLite[]
|
|
166
|
+
/** 该通道是否正在拉取远端快照列表 */
|
|
167
|
+
loadingSnapshots?: boolean
|
|
168
|
+
/** 该通道自动同步状态 */
|
|
169
|
+
autosync: AutosyncStatusResponse | null
|
|
170
|
+
/** 该通道自动同步开关(回填自 autosync) */
|
|
171
|
+
autosyncEnabled: boolean
|
|
172
|
+
/** 该通道自动同步间隔(回填自 autosync) */
|
|
173
|
+
autosyncInterval: AutosyncInterval
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 缺省每通道状态(未配置时各字段默认值)。 */
|
|
177
|
+
export function defaultChannelSyncState(): ChannelSyncState {
|
|
178
|
+
return {
|
|
179
|
+
syncMode: 'default',
|
|
180
|
+
syncSections: [],
|
|
181
|
+
selectedSnapshotId: '',
|
|
182
|
+
snapshots: [],
|
|
183
|
+
loadingSnapshots: false,
|
|
184
|
+
autosync: null,
|
|
185
|
+
autosyncEnabled: false,
|
|
186
|
+
autosyncInterval: '30m',
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 通道子 tab 的渲染模型(active/disabled 由组件据此装配 modeTabs)。 */
|
|
191
|
+
export interface ChannelTabModel {
|
|
192
|
+
channel: SyncChannel
|
|
193
|
+
active: boolean
|
|
194
|
+
disabled: boolean
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 通道子 tab 列表:git/webdav 两个 tab;busy 时全部禁用(防并发操作切换)。 */
|
|
198
|
+
export function channelTabModels(active: SyncChannel, busy: boolean): ChannelTabModel[] {
|
|
199
|
+
return (['git', 'webdav'] as const).map((channel) => ({
|
|
200
|
+
channel,
|
|
201
|
+
active: channel === active,
|
|
202
|
+
disabled: busy,
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ---------------------------------------------------------------- 通道选择持久化 */
|
|
207
|
+
|
|
208
|
+
/** 记住用户最近选择的通道(localStorage key;跨会话保持在用户上次所在栏)。
|
|
209
|
+
* m-self:磁盘持久化(ui-prefs.json)为权威来源(Host 可读、随 self 分区进备份),
|
|
210
|
+
* localStorage 仅保留为 status 响应未带回填时的同步降级通道(升级前遗留数据兼容)。 */
|
|
211
|
+
export const SYNC_CHANNEL_STORAGE_KEY = 'dsh.configManager.syncChannel';
|
|
212
|
+
|
|
213
|
+
/** 从 localStorage 读用户记住的通道;无/非法 → null(缺省 git,交由配置回填)。
|
|
214
|
+
* 浏览器环境走 globalThis.localStorage;node 测试注入 mock storage 或返回 null。 */
|
|
215
|
+
export function readStoredChannel(storage?: Pick<Storage, 'getItem'> | null): SyncChannel | null {
|
|
216
|
+
const s = storage ?? browserStorage();
|
|
217
|
+
if (s === null) return null;
|
|
218
|
+
try {
|
|
219
|
+
const v = s.getItem(SYNC_CHANNEL_STORAGE_KEY);
|
|
220
|
+
return v === 'webdav' || v === 'git' ? v : null;
|
|
221
|
+
} catch {
|
|
222
|
+
return null; // localStorage 不可用(隐私模式等)静默降级
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 把用户选择的通道写入 localStorage(记住,跨进入保持)。 */
|
|
227
|
+
export function writeStoredChannel(channel: SyncChannel, storage?: Pick<Storage, 'setItem'> | null): void {
|
|
228
|
+
const s = storage ?? browserStorage();
|
|
229
|
+
if (s === null) return;
|
|
230
|
+
try {
|
|
231
|
+
s.setItem(SYNC_CHANNEL_STORAGE_KEY, channel);
|
|
232
|
+
} catch {
|
|
233
|
+
// 静默;记住失败不阻断功能
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 浏览器 localStorage;非浏览器(node 测试)→ null */
|
|
238
|
+
function browserStorage(): Pick<Storage, 'getItem' | 'setItem'> | null {
|
|
239
|
+
const g = globalThis as { localStorage?: Storage } | undefined;
|
|
240
|
+
return g?.localStorage ?? null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ---------------------------------------------------------------- WebDAV 预设 */
|
|
244
|
+
|
|
245
|
+
/** 常见 WebDAV 服务器预设:label 展示名 + url 模板(含 <占位> 待用户替换)。 */
|
|
246
|
+
export interface WebDavPreset {
|
|
247
|
+
id: string;
|
|
248
|
+
label: string;
|
|
249
|
+
/** url 模板;可能含 <server>/<user> 占位符,用户需替换为真实地址 */
|
|
250
|
+
url: string;
|
|
251
|
+
/** 是否需要用户替换占位符 */
|
|
252
|
+
hasPlaceholder: boolean;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 内置常见 WebDAV 服务器(预设下拉数据源;第一项为自定义)。 */
|
|
256
|
+
export const WEBDAV_PRESETS: readonly WebDavPreset[] = [
|
|
257
|
+
{ id: 'custom', label: 'Custom URL', url: '', hasPlaceholder: false },
|
|
258
|
+
{ id: 'jianguoyun', label: '坚果云 (Jianguoyun)', url: 'https://dav.jianguoyun.com/dav/', hasPlaceholder: false },
|
|
259
|
+
{ id: 'nextcloud', label: 'Nextcloud', url: 'https://<server>/remote.php/dav/files/<user>/', hasPlaceholder: true },
|
|
260
|
+
{ id: 'owncloud', label: 'ownCloud', url: 'https://<server>/remote.php/dav/files/<user>/', hasPlaceholder: true },
|
|
261
|
+
{ id: 'seafile', label: 'Seafile', url: 'https://<server>/seafdav/', hasPlaceholder: true },
|
|
262
|
+
{ id: 'synology', label: 'Synology NAS (WebDAV)', url: 'https://<nas-ip>:5006/', hasPlaceholder: true },
|
|
263
|
+
{ id: 'box', label: 'Box', url: 'https://dav.box.com/dav/', hasPlaceholder: false },
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
/** 默认预设(自定义)对应的 id。 */
|
|
267
|
+
export const WEBDAV_CUSTOM_PRESET_ID = 'custom';
|
|
268
|
+
|
|
269
|
+
/** 根据预设 id 取 preset;未知 id → 自定义(缺省)。 */
|
|
270
|
+
export function presetById(id: string): WebDavPreset {
|
|
271
|
+
return WEBDAV_PRESETS.find((p) => p.id === id) ?? WEBDAV_PRESETS[0]!;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 从已填 url 反推最接近的预设 id(用于下拉回显;无匹配 → 自定义)。 */
|
|
275
|
+
export function presetIdForUrl(url: string): string {
|
|
276
|
+
const trimmed = url.trim();
|
|
277
|
+
if (trimmed === '') return WEBDAV_CUSTOM_PRESET_ID;
|
|
278
|
+
for (const p of WEBDAV_PRESETS) {
|
|
279
|
+
if (!p.hasPlaceholder && p.url !== '' && trimmed.toLowerCase().startsWith(p.url.toLowerCase())) {
|
|
280
|
+
return p.id;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return WEBDAV_CUSTOM_PRESET_ID;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface SyncButtons {
|
|
287
|
+
canPush: boolean;
|
|
288
|
+
canPull: boolean;
|
|
289
|
+
pushLabel: string;
|
|
290
|
+
pullLabel: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 活动通道的远端地址是否就绪(git=repoUrl,webdav=webdavUrl)。 */
|
|
294
|
+
export function computeRemoteReady(channel: SyncChannel, gitUrl: string, webdavUrl: string): boolean {
|
|
295
|
+
const url = channel === 'webdav' ? webdavUrl : gitUrl;
|
|
296
|
+
return url.trim() !== '';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 按钮可用性与文案:
|
|
301
|
+
* - 任一操作进行中(busy)→ 两个按钮都禁用(防并发 push/pull);
|
|
302
|
+
* - 活动通道远端地址未就绪(remoteReady=false)→ 禁用(无从同步);
|
|
303
|
+
* - busy 时按钮文案切换为「正在推送/拉取…」(配 Spinner)。
|
|
304
|
+
*/
|
|
305
|
+
export function computeSyncButtons(busy: 'sync' | 'push' | 'pull' | 'apply' | 'rollback' | null, remoteReady: boolean, t: UiT = zhUiT): SyncButtons {
|
|
306
|
+
const idle = busy === null;
|
|
307
|
+
const enabled = idle && remoteReady;
|
|
308
|
+
return {
|
|
309
|
+
canPush: enabled,
|
|
310
|
+
canPull: enabled,
|
|
311
|
+
pushLabel: busy === 'push' ? t('sync.pushing') : busy === 'sync' ? t('sync.syncing') : t('sync.pushLabel'),
|
|
312
|
+
pullLabel: busy === 'pull' ? t('sync.pulling') : busy === 'sync' ? t('sync.syncing') : t('sync.pullLabel'),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* ---------------------------------------------------------------- 状态行 */
|
|
317
|
+
|
|
318
|
+
export type SyncStatusKind = 'loading' | 'unconfigured' | 'ready' | 'error';
|
|
319
|
+
|
|
320
|
+
export interface SyncStatusSummary {
|
|
321
|
+
kind: SyncStatusKind;
|
|
322
|
+
text: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 状态行渲染模型:加载 / 未配置 / 就绪(凭据 + 上次同步 + 通道)/ 错误 */
|
|
326
|
+
export function computeSyncStatus(
|
|
327
|
+
statusInfo: SyncStatusResponse | null,
|
|
328
|
+
loading: boolean,
|
|
329
|
+
error: string | null,
|
|
330
|
+
t: UiT = zhUiT,
|
|
331
|
+
): SyncStatusSummary {
|
|
332
|
+
if (loading) return { kind: 'loading', text: t('sync.statusLoading') };
|
|
333
|
+
if (error !== null) return { kind: 'error', text: error };
|
|
334
|
+
if (statusInfo === null || !statusInfo.configured) {
|
|
335
|
+
return { kind: 'unconfigured', text: t('sync.statusUnconfigured') };
|
|
336
|
+
}
|
|
337
|
+
const isWebdav = statusInfo.transport?.type === 'webdav';
|
|
338
|
+
const credOk = isWebdav
|
|
339
|
+
? (statusInfo.webdav?.passwordConfigured ?? false)
|
|
340
|
+
: statusInfo.credentialConfigured;
|
|
341
|
+
const cred = credOk
|
|
342
|
+
? t('sync.credConfigured')
|
|
343
|
+
: t('sync.credMissing');
|
|
344
|
+
const last =
|
|
345
|
+
statusInfo.lastSyncAt !== undefined && statusInfo.lastSyncAt !== ''
|
|
346
|
+
? t('sync.lastSync', { time: formatDateTime(statusInfo.lastSyncAt) })
|
|
347
|
+
: t('sync.neverSynced');
|
|
348
|
+
const transport =
|
|
349
|
+
statusInfo.transport !== undefined
|
|
350
|
+
? ` · ${statusInfo.transport.type}${statusInfo.transport.ref !== '' ? `/${statusInfo.transport.ref}` : ''}`
|
|
351
|
+
: '';
|
|
352
|
+
return { kind: 'ready', text: `${cred} · ${last}${transport}` };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** ISO-8601 → 本地可读时间(YYYY-MM-DD HH:mm;非法输入原样返回) */
|
|
356
|
+
export function formatDateTime(iso: string): string {
|
|
357
|
+
const d = new Date(iso);
|
|
358
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
359
|
+
const pad = (n: number): string => String(n).padStart(2, '0');
|
|
360
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** 上次同步时间的展示文本('' / undefined = 从未同步) */
|
|
364
|
+
export function formatLastSync(iso: string | undefined, t: UiT = zhUiT): string {
|
|
365
|
+
if (iso === undefined || iso === '') return t('sync.neverSyncedShort');
|
|
366
|
+
return formatDateTime(iso);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* ---------------------------------------------------------------- 报告渲染模型 */
|
|
370
|
+
|
|
371
|
+
export interface PushReportView {
|
|
372
|
+
kind: 'ok' | 'error';
|
|
373
|
+
headline: string;
|
|
374
|
+
sections: string[];
|
|
375
|
+
warnings: string[];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** push 报告 → 渲染模型(ok 头部带快照 id;失败显示引擎 message;分区与告警透传) */
|
|
379
|
+
export function pushReportView(report: SyncPushReport | null, t: UiT = zhUiT): PushReportView | null {
|
|
380
|
+
if (report === null) return null;
|
|
381
|
+
if (!report.ok) {
|
|
382
|
+
return { kind: 'error', headline: report.message ?? t('sync.pushFailed'), sections: report.sections, warnings: report.warnings };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
kind: 'ok',
|
|
386
|
+
headline: t('sync.pushOk', { id: report.snapshotId }),
|
|
387
|
+
sections: report.sections,
|
|
388
|
+
warnings: report.warnings,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ------------------------------------------------ P0-② push 前只读预览渲染模型 */
|
|
393
|
+
|
|
394
|
+
export interface PushPreviewView {
|
|
395
|
+
ok: boolean;
|
|
396
|
+
/** 将推送的分区行(含计数 + 是否相对基线有变化) */
|
|
397
|
+
rows: { section: string; count: number; changed: boolean }[];
|
|
398
|
+
/** 变更分区数(要展示「新增/更新 N 个分区」) */
|
|
399
|
+
changedCount: number;
|
|
400
|
+
/** 远端现有快照数(0 = 首次推送创建首个基线) */
|
|
401
|
+
remoteSnapshotCount: number;
|
|
402
|
+
/** 只读提示(预览不写远端) */
|
|
403
|
+
previewHint: string;
|
|
404
|
+
headline: string;
|
|
405
|
+
error: string | null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** push 预览 → 渲染模型(P0-②):列分区 + 变更计数 + 远端基线提示。 */
|
|
409
|
+
export function pushPreviewView(preview: SyncPushPreview | null, t: UiT = zhUiT): PushPreviewView | null {
|
|
410
|
+
if (preview === null) return null;
|
|
411
|
+
if (!preview.ok) {
|
|
412
|
+
return {
|
|
413
|
+
ok: false, rows: [], changedCount: 0, remoteSnapshotCount: preview.remoteSnapshotCount,
|
|
414
|
+
previewHint: '', headline: '', error: preview.message ?? t('sync.pushFailed'),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const rows = preview.sections.map((s) => ({ section: s.section, count: s.count, changed: s.changed }));
|
|
418
|
+
const changedCount = preview.sections.filter((s) => s.changed).length;
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
rows,
|
|
422
|
+
changedCount,
|
|
423
|
+
remoteSnapshotCount: preview.remoteSnapshotCount,
|
|
424
|
+
previewHint: t('sync.pushPreviewHint'),
|
|
425
|
+
headline: t('sync.pushPreviewHeadline', {
|
|
426
|
+
total: String(preview.sections.length),
|
|
427
|
+
changed: String(changedCount),
|
|
428
|
+
}),
|
|
429
|
+
error: null,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface PullReportView {
|
|
434
|
+
kind: 'ok' | 'empty' | 'error';
|
|
435
|
+
headline: string;
|
|
436
|
+
summary: PullChangeSummary | null;
|
|
437
|
+
/** 只读预览提示(ok 时非空:明确「预览不执行导入」) */
|
|
438
|
+
previewHint: string;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** pull 报告 → 渲染模型(差异预览;empty = 无变更;error = 拉取失败) */
|
|
442
|
+
export function pullReportView(report: SyncPullReport | null, t: UiT = zhUiT): PullReportView | null {
|
|
443
|
+
if (report === null) return null;
|
|
444
|
+
if (!report.ok) {
|
|
445
|
+
return { kind: 'error', headline: report.message ?? t('sync.pullFailed'), summary: null, previewHint: '' };
|
|
446
|
+
}
|
|
447
|
+
if (report.changes.length === 0) {
|
|
448
|
+
return { kind: 'empty', headline: report.message ?? t('sync.pullEmpty'), summary: null, previewHint: '' };
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
kind: 'ok',
|
|
452
|
+
headline: t('sync.pullOk', { id: report.snapshotId, count: String(report.changes.length) }),
|
|
453
|
+
summary: summarizePullChanges(report.changes),
|
|
454
|
+
previewHint: t('sync.previewHint'),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* ---------------------------------------------------------------- GitHub 登录视图模型 */
|
|
459
|
+
|
|
460
|
+
export type GithubLoginPhase = 'idle' | 'starting' | 'waiting' | 'polling' | 'success' | 'error';
|
|
461
|
+
|
|
462
|
+
export interface GithubLoginView {
|
|
463
|
+
phase: GithubLoginPhase;
|
|
464
|
+
/** 一次性用户码(waiting/polling 展示,用户到 GitHub 授权页输入) */
|
|
465
|
+
userCode: string;
|
|
466
|
+
/** GitHub 授权页 URL */
|
|
467
|
+
verificationUri: string;
|
|
468
|
+
/** 状态行文案(中文,项目源语言;与 computeSyncStatus 同策略,不依赖 locale 注入) */
|
|
469
|
+
statusText: string;
|
|
470
|
+
/** 主按钮文案:发起 / 重新登录 */
|
|
471
|
+
startLabel: string;
|
|
472
|
+
/** 是否可发起/重试登录 */
|
|
473
|
+
canStart: boolean;
|
|
474
|
+
/** 流程进行中是否展示「取消」按钮 */
|
|
475
|
+
canCancel: boolean;
|
|
476
|
+
/** 是否展示设备码 + 授权链接区块 */
|
|
477
|
+
showCode: boolean;
|
|
478
|
+
/** 错误消息(phase=error;来自轮询终止态或请求失败) */
|
|
479
|
+
error: string | null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* GitHub 登录区块渲染模型(纯函数,node 可测):
|
|
484
|
+
* - idle → 可发起;starting → 请求设备码中;waiting → 展示设备码等待用户在浏览器授权;
|
|
485
|
+
* - polling → 轮询 GitHub 中(仍展示代码区块);success → 完成;error → 可重试。
|
|
486
|
+
*/
|
|
487
|
+
export function computeGithubLoginView(
|
|
488
|
+
phase: GithubLoginPhase,
|
|
489
|
+
userCode: string,
|
|
490
|
+
verificationUri: string,
|
|
491
|
+
error: string | null,
|
|
492
|
+
t: UiT = zhUiT,
|
|
493
|
+
): GithubLoginView {
|
|
494
|
+
const inFlight = phase === 'starting' || phase === 'waiting' || phase === 'polling';
|
|
495
|
+
let statusText: string;
|
|
496
|
+
switch (phase) {
|
|
497
|
+
case 'starting':
|
|
498
|
+
statusText = t('sync.github.starting');
|
|
499
|
+
break;
|
|
500
|
+
case 'waiting':
|
|
501
|
+
statusText = userCode === ''
|
|
502
|
+
? t('sync.github.waitingNoCode')
|
|
503
|
+
: t('sync.github.waiting', { code: userCode });
|
|
504
|
+
break;
|
|
505
|
+
case 'polling':
|
|
506
|
+
statusText = t('sync.github.polling');
|
|
507
|
+
break;
|
|
508
|
+
case 'success':
|
|
509
|
+
statusText = t('sync.github.success');
|
|
510
|
+
break;
|
|
511
|
+
case 'error':
|
|
512
|
+
statusText = error ?? t('sync.github.failed');
|
|
513
|
+
break;
|
|
514
|
+
default:
|
|
515
|
+
statusText = t('sync.github.defaultStatus');
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
phase,
|
|
519
|
+
userCode,
|
|
520
|
+
verificationUri,
|
|
521
|
+
statusText,
|
|
522
|
+
startLabel: phase === 'error' ? t('sync.github.relogin') : t('sync.github.login'),
|
|
523
|
+
canStart: phase === 'idle' || phase === 'error',
|
|
524
|
+
canCancel: inFlight,
|
|
525
|
+
showCode: phase === 'waiting' || phase === 'polling',
|
|
526
|
+
error,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** 轮询终止态 → 用户可读消息(pending 不是终止态,返回空串;成功/拒绝/过期/错误给出明确文案) */
|
|
531
|
+
export function githubPollMessage(poll: GithubPollResponse, t: UiT = zhUiT): string {
|
|
532
|
+
switch (poll.status) {
|
|
533
|
+
case 'success':
|
|
534
|
+
return t('sync.github.pollSuccess');
|
|
535
|
+
case 'denied':
|
|
536
|
+
return t('sync.github.pollDenied');
|
|
537
|
+
case 'expired':
|
|
538
|
+
return t('sync.github.pollExpired');
|
|
539
|
+
case 'error':
|
|
540
|
+
return t('sync.github.pollError', { detail: poll.message ?? poll.errorCode ?? t('sync.github.unknownError') });
|
|
541
|
+
default:
|
|
542
|
+
return '';
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* ---------------------------------------------------------------- 一键同步差异确认(方案 A) */
|
|
547
|
+
|
|
548
|
+
/** 需要人工决策的 PlanItemKind(与 Host /sync/sync 的 needsReview 判定对齐)。
|
|
549
|
+
* 注意:'Install'(安装插件)不在其中 —— 插件安装默认自动采用(defaultAdopt=true)、
|
|
550
|
+
* 不逐项展示、无需手动选择(product requirement)。 */
|
|
551
|
+
const CONFIRM_REVIEW_KINDS: ReadonlySet<PlanItemKind> = new Set([
|
|
552
|
+
'Conflict', 'MissingSecret', 'MissingDependency', 'Error', 'PathMapping',
|
|
553
|
+
// issue #35:Warning 也进确认列表 —— 它承载「本次同步会剔除哪些无法满足的
|
|
554
|
+
// patchedDependencies 声明」这类改变配置语义的信息,不能默默自动采用。
|
|
555
|
+
// 与宿主 REVIEW_KINDS 必须保持一致(两处同源,勿单改一处)。
|
|
556
|
+
'Warning',
|
|
557
|
+
]);
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 是否需要人工决策(是否进入差异确认列表)。
|
|
561
|
+
* 非决策项(Create / Update / Skip 等)默认自动采用(defaultAdopt=true),
|
|
562
|
+
* 不逐项展示但 apply-items 时照常导入;Warning 例外 —— 见 CONFIRM_REVIEW_KINDS。
|
|
563
|
+
*/
|
|
564
|
+
export function isReviewItem(kind: PlanItemKind): boolean {
|
|
565
|
+
return CONFIRM_REVIEW_KINDS.has(kind);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* issue #35:会**改变工具链行为**的项 —— pnpm-workspace.yaml 本次移除了无法满足的
|
|
570
|
+
* patchedDependencies 声明时(宿主会带上 detail)。这类项此前默认自动采用且不展示,
|
|
571
|
+
* 用户即使已知风险也无法否决;现在进确认列表(可取消),默认仍采用(sanitize 结果更安全,
|
|
572
|
+
* 默认不采用会静默丢掉 allowBuilds / 冷静期配置)。
|
|
573
|
+
* 注意:与宿主 src/index.ts 的同名判定必须保持一致(两侧刻意重复,避免跨端 import)。
|
|
574
|
+
*/
|
|
575
|
+
export function isToolchainChangeItem(item: { itemId: string; detail?: string | undefined }): boolean {
|
|
576
|
+
return item.itemId === 'plugins:pnpm-workspace' && item.detail !== undefined && item.detail !== '';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* 仅保留需人工决策的项(差异确认列表只渲染这些)。
|
|
581
|
+
* 统计(summarizeConfirmItems)仍基于全量 items,不受影响。
|
|
582
|
+
* issue #35:除 kind 命中外,**改变工具链行为**的项(pnpm-workspace 剔除声明)也进列表。
|
|
583
|
+
*/
|
|
584
|
+
export function reviewItems(items: readonly SyncConfirmItem[]): SyncConfirmItem[] {
|
|
585
|
+
return items.filter((it) => CONFIRM_REVIEW_KINDS.has(it.kind) || isToolchainChangeItem(it));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** 冲突解决方式:与导入恢复向导(ConflictList)完全一致的两项(保留当前 / 使用导入)。
|
|
589
|
+
* - keepLocal = keepCurrent(保留本地现有值,不写入);
|
|
590
|
+
* - useRemote = useImported(采用远端快照值,写入本地)。
|
|
591
|
+
*/
|
|
592
|
+
export type SyncConflictResolution = 'keepLocal' | 'useRemote';
|
|
593
|
+
|
|
594
|
+
/** 单条 Conflict 项的批量决策(resolution + adopt)。 */
|
|
595
|
+
export interface ConflictDecision {
|
|
596
|
+
itemId: string;
|
|
597
|
+
resolution: SyncConflictResolution;
|
|
598
|
+
adopt: boolean;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 「全部保留本地」:所有 Conflict 项 → resolution=keepLocal、adopt=false。
|
|
603
|
+
* 仅作用于 Conflict 项,非 Conflict 项的 adopt 保持默认。
|
|
604
|
+
*/
|
|
605
|
+
export function keepLocalAll(items: readonly SyncConfirmItem[]): ConflictDecision[] {
|
|
606
|
+
return items
|
|
607
|
+
.filter((it) => it.kind === 'Conflict')
|
|
608
|
+
.map((it) => ({ itemId: it.itemId, resolution: 'keepLocal', adopt: false }));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 「全部采用远端」:所有 Conflict 项 → resolution=useRemote、adopt=true。
|
|
613
|
+
* 仅作用于 Conflict 项,非 Conflict 项的 adopt 保持默认。
|
|
614
|
+
*/
|
|
615
|
+
export function useRemoteAll(items: readonly SyncConfirmItem[]): ConflictDecision[] {
|
|
616
|
+
return items
|
|
617
|
+
.filter((it) => it.kind === 'Conflict')
|
|
618
|
+
.map((it) => ({ itemId: it.itemId, resolution: 'useRemote', adopt: true }));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export interface SyncConfirmSummary {
|
|
622
|
+
total: number;
|
|
623
|
+
info: number;
|
|
624
|
+
warning: number;
|
|
625
|
+
error: number;
|
|
626
|
+
/** 默认/当前采用数(adopt=true 的项数)。 */
|
|
627
|
+
adopted: number;
|
|
628
|
+
/** 是否包含任何需人工决策项。 */
|
|
629
|
+
needsReview: boolean;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** 差异确认列表摘要(按 severity 计数 + 采用数 + needsReview 徽章数据源)。 */
|
|
633
|
+
export function summarizeConfirmItems(items: readonly SyncConfirmItem[]): SyncConfirmSummary {
|
|
634
|
+
let info = 0;
|
|
635
|
+
let warning = 0;
|
|
636
|
+
let error = 0;
|
|
637
|
+
let adopted = 0;
|
|
638
|
+
let needsReview = false;
|
|
639
|
+
for (const it of items) {
|
|
640
|
+
if (it.severity === 'error') error += 1;
|
|
641
|
+
else if (it.severity === 'warning') warning += 1;
|
|
642
|
+
else info += 1;
|
|
643
|
+
if (it.adopt) adopted += 1;
|
|
644
|
+
if (CONFIRM_REVIEW_KINDS.has(it.kind)) needsReview = true;
|
|
645
|
+
}
|
|
646
|
+
return { total: items.length, info, warning, error, adopted, needsReview };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* 收集用户逐项决策 → apply-items 请求体 adoptions[]。
|
|
651
|
+
* 仅包含 adopt=true 的项;Conflict 项 adopt=true 且未给 resolution → 抛错(强制先解决)。
|
|
652
|
+
* 与导入恢复向导一致:只提供「保留当前 / 使用导入」两项,跳过 = 取消勾选(adopt=false)。
|
|
653
|
+
*/
|
|
654
|
+
export function buildAdoptions(
|
|
655
|
+
items: readonly SyncConfirmItem[],
|
|
656
|
+
adopted: ReadonlyMap<string, boolean>,
|
|
657
|
+
resolutions: ReadonlyMap<string, SyncConflictResolution>,
|
|
658
|
+
): SyncItemAdoption[] {
|
|
659
|
+
const out: SyncItemAdoption[] = [];
|
|
660
|
+
for (const it of items) {
|
|
661
|
+
if (adopted.get(it.itemId) !== true) continue; // adopt=false / 未列出 → 跳过
|
|
662
|
+
const adoption: SyncItemAdoption = { itemId: it.itemId, adopt: true };
|
|
663
|
+
if (it.kind === 'Conflict') {
|
|
664
|
+
const resolution = resolutions.get(it.itemId);
|
|
665
|
+
if (resolution === undefined) {
|
|
666
|
+
throw new Error(`冲突项 ${it.itemId} 必须先选择解决方式(保留当前 / 使用导入)`);
|
|
667
|
+
}
|
|
668
|
+
adoption.resolution = resolution;
|
|
669
|
+
}
|
|
670
|
+
out.push(adoption);
|
|
671
|
+
}
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export type ApplyItemsViewKind = 'ok' | 'failed' | 'rolledBack';
|
|
676
|
+
|
|
677
|
+
export interface ApplyItemsView {
|
|
678
|
+
kind: ApplyItemsViewKind;
|
|
679
|
+
headline: string;
|
|
680
|
+
sections: string[];
|
|
681
|
+
warnings: string[];
|
|
682
|
+
restoreId: string;
|
|
683
|
+
needsRestart: boolean;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** apply-items 执行结果 → 渲染模型(ok / failed / 整体回滚)。 */
|
|
687
|
+
export function applyItemsReportView(
|
|
688
|
+
report: ApplyItemsResponse | null,
|
|
689
|
+
t: UiT = zhUiT,
|
|
690
|
+
): ApplyItemsView | null {
|
|
691
|
+
if (report === null) return null;
|
|
692
|
+
const failedOnly = report.failed.length > 0 && !report.ok;
|
|
693
|
+
const kind: ApplyItemsViewKind = !report.ok && report.rolledBack ? 'rolledBack' : failedOnly ? 'failed' : 'ok';
|
|
694
|
+
const headline = kind === 'ok'
|
|
695
|
+
? t('sync.importDone', { n: String(report.applied.length) })
|
|
696
|
+
: kind === 'rolledBack'
|
|
697
|
+
? t('sync.importFailed')
|
|
698
|
+
: t('sync.importFailed');
|
|
699
|
+
return {
|
|
700
|
+
kind,
|
|
701
|
+
headline,
|
|
702
|
+
sections: report.applied,
|
|
703
|
+
warnings: report.warnings,
|
|
704
|
+
restoreId: report.restoreId,
|
|
705
|
+
needsRestart: report.needsRestart,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* ---------------------------------------------------------------- 自动同步(方案 A) */
|
|
710
|
+
|
|
711
|
+
/** AutosyncInterval → ms。 */
|
|
712
|
+
export function autosyncIntervalMs(interval: AutosyncInterval): number {
|
|
713
|
+
switch (interval) {
|
|
714
|
+
case '5m': return 5 * 60 * 1000;
|
|
715
|
+
case '15m': return 15 * 60 * 1000;
|
|
716
|
+
case '60m': return 60 * 60 * 1000;
|
|
717
|
+
case '6h': return 6 * 60 * 60 * 1000;
|
|
718
|
+
case '12h': return 12 * 60 * 60 * 1000;
|
|
719
|
+
case '24h': return 24 * 60 * 60 * 1000;
|
|
720
|
+
default: return 30 * 60 * 1000;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** 距下次自动同步剩余 ms(已到期 → 0)。elapsedMs 为 host 计算的「距上次执行已过 ms」。 */
|
|
725
|
+
export function computeAutosyncCountdown(elapsedMs: number, intervalMs: number): number {
|
|
726
|
+
if (elapsedMs < 0) return -1; // 从未运行
|
|
727
|
+
return Math.max(0, intervalMs - elapsedMs);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* 剩余时长 → 可读文案(向上取整,避免出现「0 分钟」;≤0 视为 1 分钟兜底)。
|
|
732
|
+
* 例:4 分钟 →「4 分钟」;90 分钟 →「2 小时」;30 小时 →「2 天」。
|
|
733
|
+
*/
|
|
734
|
+
export function formatIntervalDuration(ms: number, t: UiT = zhUiT): string {
|
|
735
|
+
const totalMinutes = Math.max(1, Math.ceil(ms / 60000));
|
|
736
|
+
if (totalMinutes < 60) return t('sync.duration.min', { n: totalMinutes });
|
|
737
|
+
const totalHours = Math.ceil(totalMinutes / 60);
|
|
738
|
+
if (totalHours < 24) return t('sync.duration.hour', { n: totalHours });
|
|
739
|
+
const totalDays = Math.ceil(totalHours / 24);
|
|
740
|
+
return t('sync.duration.day', { n: totalDays });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** 自动同步状态行的可读文案(未运行 / 上次状态 / 连续失败计数)。 */
|
|
744
|
+
export function autosyncStatusText(status: AutosyncStatusResponse, t: UiT = zhUiT): string {
|
|
745
|
+
if (status.lastRunAt === undefined || status.lastRunAt === '' || status.lastRunStatus === undefined) {
|
|
746
|
+
return t('sync.autosyncNever');
|
|
747
|
+
}
|
|
748
|
+
const statusText = status.lastRunStatus === 'success'
|
|
749
|
+
? t('sync.autosyncSuccess')
|
|
750
|
+
: status.lastRunStatus === 'skipped'
|
|
751
|
+
? t('sync.autosyncSkipped')
|
|
752
|
+
: status.lastRunStatus === 'partial'
|
|
753
|
+
? t('sync.autosyncPartial')
|
|
754
|
+
: t('sync.autosyncFailed');
|
|
755
|
+
const time = formatDateTime(status.lastRunAt);
|
|
756
|
+
const base = t('sync.autosyncLastRun', { time });
|
|
757
|
+
const fail = status.consecutiveFailures > 0 ? ` · ${t('sync.autosyncFailCount', { n: String(status.consecutiveFailures) })}` : '';
|
|
758
|
+
return `${statusText} · ${base}${fail}`;
|
|
759
|
+
}
|