dsh-config-manager 0.1.78 → 0.1.80
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/lib/adapters/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +401 -112
- package/lib/client.js +2899 -1525
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/ancestor.d.ts +22 -0
- package/lib/sync/ancestor.js +66 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +18 -1
- package/lib/sync/gist/gist-transport.js +42 -3
- package/lib/sync/gist/messages.d.ts +2 -0
- package/lib/sync/gist/messages.js +5 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/s3/sigv4.js +11 -2
- package/lib/sync/sync-engine.d.ts +212 -8
- package/lib/sync/sync-engine.js +514 -58
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +9 -0
- package/lib/ui/i18n.js +22 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/api-base.test.ts +36 -0
- package/src/client/common/http.test.ts +12 -1
- package/src/client/common/http.ts +8 -2
- package/src/client/common/routes.ts +46 -3
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/market/market-api.test.ts +3 -2
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +220 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +248 -0
- package/src/client/sync/merge-view.ts +398 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.test.ts +5 -3
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/ancestor.ts +76 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +203 -0
- package/src/sync/gist/gist-transport.ts +53 -5
- package/src/sync/gist/messages.ts +5 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.test.ts +2 -1
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/s3/sigv4.test.ts +4 -1
- package/src/sync/s3/sigv4.ts +11 -2
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +410 -19
- package/src/sync/sync-engine.ts +602 -57
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +22 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
package/lib/routes/sync.js
CHANGED
|
@@ -40,11 +40,12 @@ function extractPathMappings(body) {
|
|
|
40
40
|
import { REVIEW_KINDS, ROUTE_TIMEOUT_MS, SYNC_CREDENTIAL_REF, SYNC_WEBDAV_CREDENTIAL_REF, SyncRouteError, buildAutosyncStatus, buildAutosyncStatusByChannel, credentialRef, extractSyncSections, extractSyncSessions, isAutosyncInterval, isGitHubAuthMissing, isToolchainChangeItem, planToConfirmItems, syncPasswordRef, withTimeout, writeSyncRouteError } from '../index.js';
|
|
41
41
|
import { GitHubApiError } from '../market/github-repos.js';
|
|
42
42
|
import { redact } from '../security/redaction.js';
|
|
43
|
-
import { readAutosyncConfig, writeAutosyncConfig } from '../sync/autosync-config.js';
|
|
43
|
+
import { shouldRunOnEnable, readAutosyncConfig, writeAutosyncConfig, clampRemoteCheckMs, clearLastRunResult } from '../sync/autosync-config.js';
|
|
44
44
|
import { isValidExportFileName } from '../sync/backup-files.js';
|
|
45
45
|
import { DeviceFlowStore } from '../sync/github-auth.js';
|
|
46
46
|
import { channelOf, clearSyncChannel, isWebDavConfig, listCloudConfiguredProviders, parseSyncChannel, readFullSyncConfig, writeSyncConfig, gistChannelView, s3ChannelView, cloudSecretRef, S3_COMPAT_PROVIDERS, GIST_PROVIDER } from '../sync/sync-config.js';
|
|
47
47
|
import { SyncEngine } from '../sync/sync-engine.js';
|
|
48
|
+
import { isProbeUnsupported } from '../sync/transport.js';
|
|
48
49
|
import { readSyncHistory } from '../sync/sync-history.js';
|
|
49
50
|
import { SYNC_SELECTION_SCHEMA_VERSION, normalizeSessionsInclude, normalizeSessionsLimit, writeSyncSelection } from '../sync/sync-selection.js';
|
|
50
51
|
import { loadSyncState } from '../sync/sync-state.js';
|
|
@@ -83,6 +84,51 @@ export function syncRoutes(env) {
|
|
|
83
84
|
set: async (ref, value) => { await credentials.set(credentialRef(ref), value); },
|
|
84
85
|
has: async (ref) => (await credentials.describe(credentialRef(ref))).configured,
|
|
85
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* T-ui-merge:合并预览 / 合并执行共用的引擎选项。
|
|
89
|
+
*
|
|
90
|
+
* 解密密码口径与 /sync/sync 完全一致:请求体优先,其次已保存的 DECRYPT 凭据
|
|
91
|
+
* (syncPasswordRef('DECRYPT', channel))。密码只存在于内存,绝不写进响应 / 日志 / 同步文件;
|
|
92
|
+
* 明文快照根本不会被解密(引擎只在 manifest.encrypted 时才用密码)。
|
|
93
|
+
*/
|
|
94
|
+
const mergeEngineOptions = async (body, syncCfg) => {
|
|
95
|
+
const snapshotId = typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined;
|
|
96
|
+
const savedDecryptPassword = await resolveSyncPassword(syncPasswordRef('DECRYPT', channelOf(syncCfg)));
|
|
97
|
+
const decryptPassword = typeof body['decryptPassword'] === 'string' && body['decryptPassword'] !== ''
|
|
98
|
+
? body['decryptPassword']
|
|
99
|
+
: savedDecryptPassword;
|
|
100
|
+
const pathMappings = extractPathMappings(body);
|
|
101
|
+
return {
|
|
102
|
+
...(snapshotId === undefined ? {} : { snapshotId }),
|
|
103
|
+
...(decryptPassword === undefined ? {} : { password: decryptPassword }),
|
|
104
|
+
...(pathMappings.length === 0 ? {} : { pathMappings }),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* 合并裁决清单解析(T-ui-merge)。**粒度 = 分区 id**(冻结口径,不做 'id::path' 复合 id)。
|
|
109
|
+
*
|
|
110
|
+
* 非法条目直接丢弃 —— 引擎按「未裁决」处理,该分区进 pendingConflicts,界面会如实显示;
|
|
111
|
+
* 这样既不需要新增宿主文案键,也不会因为客户端字段拼错而静默写坏别的分区。
|
|
112
|
+
*/
|
|
113
|
+
const extractMergeResolutions = (body) => {
|
|
114
|
+
const raw = body['resolutions'];
|
|
115
|
+
if (!Array.isArray(raw))
|
|
116
|
+
return [];
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const entry of raw) {
|
|
119
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
120
|
+
continue;
|
|
121
|
+
const rec = entry;
|
|
122
|
+
const id = rec['id'];
|
|
123
|
+
const action = rec['action'];
|
|
124
|
+
if (typeof id !== 'string' || id === '')
|
|
125
|
+
continue;
|
|
126
|
+
if (action !== 'keepLocal' && action !== 'useRemote' && action !== 'useMerged' && action !== 'skip')
|
|
127
|
+
continue;
|
|
128
|
+
out.push({ id: id, action });
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
86
132
|
return [
|
|
87
133
|
// ------------------------------------------------------ sync/status
|
|
88
134
|
// m-sync-ui:同步状态(通道配置 / 凭据状态 / 上次同步 / 分区数)。只读,无 secret 值。
|
|
@@ -111,7 +157,8 @@ export function syncRoutes(env) {
|
|
|
111
157
|
: undefined;
|
|
112
158
|
// 云端点配置视图(只回 secretStored 布尔;与当前通道无关,配置过即回填)
|
|
113
159
|
const cloud = await cloudChannelViews(full, credentials);
|
|
114
|
-
// issue #79:git 工作副本是否已跟随 repoUrl
|
|
160
|
+
// issue #79 / #104 建议 3:git 工作副本是否已跟随 repoUrl,外加「领先远端提交数 /
|
|
161
|
+
// 远端快照数」(只读自检;后者是「本地一直没真正备份」的预警依据)。
|
|
115
162
|
// 读不出来时**如实回传错误**(workDirError),不让状态接口静默少一块信息。
|
|
116
163
|
const gitRepoUrl = full?.git?.repoUrl ?? '';
|
|
117
164
|
let workDir = null;
|
|
@@ -132,6 +179,9 @@ export function syncRoutes(env) {
|
|
|
132
179
|
// 已保存的云端点 provider 清单(切换 provider 时回填用;只有名字,无任何值)
|
|
133
180
|
cloudSavedProviders: await listCloudConfiguredProviders(syncDir),
|
|
134
181
|
lastSyncAt: state.lastSyncAt === '' ? undefined : state.lastSyncAt,
|
|
182
|
+
// 方案 A(同步页按钮瘦身):是否存在同步基线(本地祖先指针)。**只回布尔,绝不回快照 id** ——
|
|
183
|
+
// 界面据此在「有基线 → 三方裁决视图 / 无基线 → 导入确认」之间分派;缺省 undefined 按无基线处理。
|
|
184
|
+
hasBaseline: state.lastSnapshotId !== '',
|
|
135
185
|
sectionCount: Object.keys(state.sections).length,
|
|
136
186
|
lastTransport: state.transport,
|
|
137
187
|
// 上次选择的同步通道(磁盘 ui-prefs;UI 回填优先于此,localStorage 仅兜底)
|
|
@@ -157,9 +207,11 @@ export function syncRoutes(env) {
|
|
|
157
207
|
}
|
|
158
208
|
}),
|
|
159
209
|
// ------------------------------------------------------ sync/workdir/reset
|
|
160
|
-
// issue #79:工作副本的自救入口(改造前 App 内完全没有 —— 改了仓库地址只能手工
|
|
161
|
-
// git remote set-url,或删掉 <dataDir>/sync/work
|
|
210
|
+
// issue #79 / #104:工作副本的自救入口(改造前 App 内完全没有 —— 改了仓库地址只能手工
|
|
211
|
+
// git remote set-url,或删掉 <dataDir>/sync/work 重来)。三种模式,破坏性清清楚楚:
|
|
162
212
|
// relink = 只把 origin / push URL 重指向当前配置地址(非破坏性:不碰工作树与本地提交);
|
|
213
|
+
// adopt = 把远端分支合并进现有工作副本(issue #104:换成另一个仓库、或远端历史被重写后
|
|
214
|
+
// 两条历史不相交,pull --ff-only 永远失败);不改写任何本地提交,可 revert;
|
|
163
215
|
// reclone = 删除工作副本目录后重新 clone(**丢弃未上行的本地提交**)→ 必须显式 confirm。
|
|
164
216
|
// 写动作 → 过 mutation gate(SAFE MODE / 环境锁);删除边界见 GitTransport.rebuildWorkDir
|
|
165
217
|
// (目录非空且无 .git 一律拒绝,绝不删来路不明的目录)。
|
|
@@ -167,8 +219,10 @@ export function syncRoutes(env) {
|
|
|
167
219
|
try {
|
|
168
220
|
const body = await requireJsonObject(req);
|
|
169
221
|
const mode = body['mode'] === undefined ? 'relink' : body['mode'];
|
|
170
|
-
if (mode !== 'relink' && mode !== 'reclone') {
|
|
171
|
-
|
|
222
|
+
if (mode !== 'relink' && mode !== 'adopt' && mode !== 'reclone') {
|
|
223
|
+
// 用追加键(含三种模式)而不是既有的 sync.workdirModeInvalid:后者有冻结指纹,
|
|
224
|
+
// 只列出了两种模式,继续用会把「adopt 也合法」说反。
|
|
225
|
+
writeJson(res, 400, { error: msg('sync.workdirModeInvalidFull', { mode: JSON.stringify(body['mode']) }) });
|
|
172
226
|
return;
|
|
173
227
|
}
|
|
174
228
|
if (mode === 'reclone' && body['confirm'] !== true) {
|
|
@@ -605,6 +659,40 @@ export function syncRoutes(env) {
|
|
|
605
659
|
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
606
660
|
}
|
|
607
661
|
}),
|
|
662
|
+
// ------------------------------------------------------ sync/probe
|
|
663
|
+
// cloud-sync 借鉴项 ②:**只读**连通性自检(凭据 / 端点 / 目标容器 / 目录可达),
|
|
664
|
+
// 供 UI「测试连接」按钮拿到可直接展示的结论(例如 S3 兼容端点该把 region 填成哪个值)。
|
|
665
|
+
// 只探**已保存**的通道配置(走与 push/pull 同一条配置路径,绝不拿表单里未保存的值去发请求)。
|
|
666
|
+
// 远端唯一写副作用 = webdav 幂等 MKCOL 补齐上级目录(那正是它要修的故障:目录不存在)。
|
|
667
|
+
// 失败一律 200 + ok:false —— 「测试连接」是**预期会失败**的诊断动作,用 5xx 表达会被 UI
|
|
668
|
+
// 当成基础设施故障;文案由通道层本地化并脱敏后原样带回。
|
|
669
|
+
endpoint({ path: '/api/dsh-config-manager/sync/probe', methods: ['POST'] }, async (req, res) => {
|
|
670
|
+
const body = await requireJsonObject(req);
|
|
671
|
+
try {
|
|
672
|
+
const syncCfg = await prepareSync(body);
|
|
673
|
+
const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) });
|
|
674
|
+
const result = await engine.probe();
|
|
675
|
+
writeJson(res, 200, {
|
|
676
|
+
ok: true,
|
|
677
|
+
transport: channelOf(syncCfg),
|
|
678
|
+
message: result.message,
|
|
679
|
+
...(result.hint !== undefined ? { hint: result.hint } : {}),
|
|
680
|
+
...(result.snapshotCount !== undefined ? { snapshotCount: result.snapshotCount } : {}),
|
|
681
|
+
...(result.createdCollections !== undefined && result.createdCollections.length > 0
|
|
682
|
+
? { createdCollections: result.createdCollections }
|
|
683
|
+
: {}),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
// 「该通道不提供自检」是**能力边界**(git 通道按 fast-forward 语义自证),不是故障:
|
|
688
|
+
// 用稳定错误码标记,界面据此按中性提示渲染,绝不显示成「操作失败」。
|
|
689
|
+
writeJson(res, 200, {
|
|
690
|
+
ok: false,
|
|
691
|
+
error: error instanceof Error ? error.message : String(error),
|
|
692
|
+
...(isProbeUnsupported(error) ? { unsupported: true } : {}),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}),
|
|
608
696
|
// ------------------------------------------------------ sync/download
|
|
609
697
|
// 「拉取即导入」:把远端快照**落地成本机 ZIP** 并返回路径,供导入向导消费。
|
|
610
698
|
// 与 /sync/pull 的分工:pull 只做只读差异预览(临时 ZIP 用完即删);
|
|
@@ -682,6 +770,15 @@ export function syncRoutes(env) {
|
|
|
682
770
|
sectionCount: m.manifest.sectionIds.length,
|
|
683
771
|
platform: m.manifest.platform,
|
|
684
772
|
dshVersion: m.manifest.dshVersion,
|
|
773
|
+
// cloud-sync 借鉴项 ⑥:**推送方设备身份**(写在快照 manifest 里;旧快照没有 → 字段不出现)。
|
|
774
|
+
// 只回名字与稳定 id(都是本机自己写上去的展示信息,非秘密);缺字段时绝不回空串占位,
|
|
775
|
+
// 让界面能如实区分「旧快照没有这个字段」与「设备名是空的」。
|
|
776
|
+
...(typeof m.manifest.deviceName === 'string' && m.manifest.deviceName !== ''
|
|
777
|
+
? { deviceName: m.manifest.deviceName }
|
|
778
|
+
: {}),
|
|
779
|
+
...(typeof m.manifest.deviceId === 'string' && m.manifest.deviceId !== ''
|
|
780
|
+
? { deviceId: m.manifest.deviceId }
|
|
781
|
+
: {}),
|
|
685
782
|
}));
|
|
686
783
|
const state = await loadSyncState(syncDir);
|
|
687
784
|
// issue #59:把「远端有但读不出来」的快照如实回传(绝不静默跳过)——
|
|
@@ -910,7 +1007,8 @@ export function syncRoutes(env) {
|
|
|
910
1007
|
}
|
|
911
1008
|
}),
|
|
912
1009
|
// ------------------------------------------------------ sync/autosync
|
|
913
|
-
// m-sync-v2
|
|
1010
|
+
// m-sync-v2:自动同步配置读写(按通道:开关 + 兜底间隔 + 启动阈值 + realtime 监听/远端轮询
|
|
1011
|
+
// + 自动应用等级 + 冲突策略 + 运行状态)。
|
|
914
1012
|
// GET = 读全部通道状态({ git, webdav });POST = 写指定通道(body.transport,缺省 git)。
|
|
915
1013
|
// 同一路径注册为一个 exact 路由(方法内部分发),避免 webserver 对重复 exact 路径报错。
|
|
916
1014
|
endpoint({ path: '/api/dsh-config-manager/sync/autosync', methods: ['GET', 'POST'] }, async (req, res) => {
|
|
@@ -928,6 +1026,8 @@ export function syncRoutes(env) {
|
|
|
928
1026
|
// 按通道读写:git/webdav 各自的自动同步配置与运行状态独立(缺省 git 兜底)
|
|
929
1027
|
const channel = parseSyncChannel(body['transport']) ?? 'git';
|
|
930
1028
|
const cfg = await readAutosyncConfig(syncDir, channel);
|
|
1029
|
+
// 「这次 POST 是不是把开关从关拨到开」必须在覆盖字段之前拍下来(issue #105 建议 4)
|
|
1030
|
+
const wasEnabled = cfg.enabled;
|
|
931
1031
|
if (typeof body['enabled'] === 'boolean')
|
|
932
1032
|
cfg.enabled = body['enabled'];
|
|
933
1033
|
if (typeof body['interval'] === 'string' && isAutosyncInterval(body['interval']))
|
|
@@ -935,9 +1035,36 @@ export function syncRoutes(env) {
|
|
|
935
1035
|
if (typeof body['startupMinIntervalMs'] === 'number' && Number.isFinite(body['startupMinIntervalMs']) && body['startupMinIntervalMs'] > 0) {
|
|
936
1036
|
cfg.startupMinIntervalMs = body['startupMinIntervalMs'];
|
|
937
1037
|
}
|
|
938
|
-
|
|
939
|
-
if (
|
|
1038
|
+
// 真·自动同步:本地改动即时触发开关 + 远端轮询节奏(非法值一律忽略,绝不写 NaN)
|
|
1039
|
+
if (typeof body['realtime'] === 'boolean')
|
|
1040
|
+
cfg.realtime = body['realtime'];
|
|
1041
|
+
if (typeof body['remoteCheckMs'] === 'number' && Number.isFinite(body['remoteCheckMs'])) {
|
|
1042
|
+
cfg.remoteCheckMs = clampRemoteCheckMs(body['remoteCheckMs']);
|
|
1043
|
+
}
|
|
1044
|
+
// 自动应用等级:low = 只自动落低风险分区;medium = plugins/mcp 也自动落盘
|
|
1045
|
+
if (body['autoApplyTier'] === 'low' || body['autoApplyTier'] === 'medium') {
|
|
1046
|
+
cfg.autoApplyTier = body['autoApplyTier'];
|
|
1047
|
+
}
|
|
1048
|
+
// 冲突策略:ask = 保留冲突待人工裁决;local = 保留本机并推上去(缺省)
|
|
1049
|
+
if (body['conflictPolicy'] === 'ask' || body['conflictPolicy'] === 'local') {
|
|
1050
|
+
cfg.conflictPolicy = body['conflictPolicy'];
|
|
1051
|
+
}
|
|
1052
|
+
// issue #105:这里写的是**配置**,不是一轮运行结果。此前 readAutosyncConfig 读到的
|
|
1053
|
+
// 上一轮 lastRunMessage/lastRunSkipReason 会被原样写回("坐实"),于是十天前那次失败的
|
|
1054
|
+
// 错误原文会被一次纯改间隔的保存续命,卡片上继续挂着早已不成立的故障。
|
|
1055
|
+
// 现在:配置落盘前清空上一轮结论 —— 配置变更 ≠ 本轮结论。
|
|
1056
|
+
// issue #105 建议 4:勾选开关是「用户刚刚要求它开始工作」,不该只重排定时器、让卡片
|
|
1057
|
+
// 继续显示上一轮(甚至是十天前)的结论。若距上次运行已超过启动阈值,立刻跑一轮。
|
|
1058
|
+
const runNow = shouldRunOnEnable({ enabled: wasEnabled }, cfg);
|
|
1059
|
+
await writeAutosyncConfig(syncDir, channel, clearLastRunResult(cfg));
|
|
1060
|
+
if (scheduler) {
|
|
940
1061
|
scheduler.reload().catch(() => { });
|
|
1062
|
+
// 不 await:POST 是配置保存,不能让页面等一次网络同步;结果由调度器自己写进
|
|
1063
|
+
// sync-autosync.json / sync-history.json。并发由 runOnce 的 running 与 runs 全局防重
|
|
1064
|
+
// 挡住(在跑时返回 skipped 而不是排队),所以这里不需要额外门控。
|
|
1065
|
+
if (runNow)
|
|
1066
|
+
void scheduler.runOnce(channel, { reason: 'enable-toggle' }).catch(() => { });
|
|
1067
|
+
}
|
|
941
1068
|
writeJson(res, 200, await buildAutosyncStatus(syncDir, channel));
|
|
942
1069
|
}
|
|
943
1070
|
catch (error) {
|
|
@@ -1045,6 +1172,43 @@ export function syncRoutes(env) {
|
|
|
1045
1172
|
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1046
1173
|
}
|
|
1047
1174
|
})),
|
|
1175
|
+
// ------------------------------------------------------ sync/merge-preview
|
|
1176
|
+
// T-ui-merge:远程合并**只读**预览(三方合并 + 冲突清单 + 摘要)。
|
|
1177
|
+
// 零写入 ⇒ 不过 mutation gate、也不登记同步会话(临时 ZIP 由引擎内部用完即清);
|
|
1178
|
+
// 解密密码只走内存,响应体不含任何凭据值。
|
|
1179
|
+
endpoint({ path: '/api/dsh-config-manager/sync/merge-preview', methods: ['POST'] }, async (req, res) => {
|
|
1180
|
+
const body = await requireJsonObject(req);
|
|
1181
|
+
try {
|
|
1182
|
+
const syncCfg = await prepareSync(body);
|
|
1183
|
+
const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) });
|
|
1184
|
+
const opts = await mergeEngineOptions(body, syncCfg);
|
|
1185
|
+
const preview = await withTimeout(engine.previewMerge(opts), ROUTE_TIMEOUT_MS, msg('host.syncPullTimeout'));
|
|
1186
|
+
writeJson(res, 200, preview);
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
writeSyncRouteError(res, error);
|
|
1190
|
+
}
|
|
1191
|
+
}),
|
|
1192
|
+
// ------------------------------------------------------ sync/apply-merge
|
|
1193
|
+
// T-ui-merge:按**分区级**裁决执行合并(写动作 ⇒ mutation gate 'sync-merge-apply')。
|
|
1194
|
+
// 响应直接透传引擎报告(字段与 /sync/apply-items 同风格):其中 pendingConflicts =
|
|
1195
|
+
// 执行后仍待裁决的分区 —— skip 不推进同步基线,界面必须如实说明「下次同步仍会出现」。
|
|
1196
|
+
endpoint({ path: '/api/dsh-config-manager/sync/apply-merge', methods: ['POST'] }, withMutationGate('sync-merge-apply', async (req, res, _lockCtx, journalCtx) => {
|
|
1197
|
+
const body = await requireJsonObject(req);
|
|
1198
|
+
try {
|
|
1199
|
+
const syncCfg = await prepareSync(body);
|
|
1200
|
+
const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) });
|
|
1201
|
+
const resolutions = extractMergeResolutions(body);
|
|
1202
|
+
const opts = await mergeEngineOptions(body, syncCfg);
|
|
1203
|
+
// snapshotBinding:与 /sync/apply-items 同口径把 journal 绑定面交给引擎(审计 P0-23),
|
|
1204
|
+
// restoreId 与事务日志因此同源,异常路径下回滚入口拿到的也是同一份快照。
|
|
1205
|
+
const report = await withTimeout(engine.applyMergeResolutions(resolutions, { ...opts, ...(journalCtx !== undefined ? { snapshotBinding: journalCtx } : {}) }), ROUTE_TIMEOUT_MS, msg('host.syncPullTimeout'));
|
|
1206
|
+
writeJson(res, 200, report);
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
writeSyncRouteError(res, error);
|
|
1210
|
+
}
|
|
1211
|
+
})),
|
|
1048
1212
|
];
|
|
1049
1213
|
}
|
|
1050
1214
|
//# sourceMappingURL=sync.js.map
|
package/lib/schema/manifest.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Manifest, Platform, SectionId } from './types.ts';
|
|
1
|
+
import type { Manifest, ManifestForeignOrigin, Platform, SectionId } from './types.ts';
|
|
2
2
|
export declare const MANIFEST_FILE = "manifest.json";
|
|
3
3
|
export declare const CHECKSUMS_FILE = "integrity/checksums.json";
|
|
4
4
|
export declare const EXPORTER_NAME = "DSH Config Manager";
|
|
@@ -14,6 +14,11 @@ export interface BuildManifestInput {
|
|
|
14
14
|
exportedAt?: string;
|
|
15
15
|
/** 导出机的 DSH home(可选;见 Manifest.sourceHome,用于跨机基础路径重定基) */
|
|
16
16
|
sourceHome?: string;
|
|
17
|
+
/**
|
|
18
|
+
* 外部来源包的溯源块(可选;见 Manifest.foreign)。
|
|
19
|
+
* **只有显式传入时才写入** —— 缺省不产生该键,DSH 自产备份与旧包逐字不变。
|
|
20
|
+
*/
|
|
21
|
+
foreign?: ManifestForeignOrigin;
|
|
17
22
|
}
|
|
18
23
|
/** 构造 manifest(schemaVersion 恒为当前版本,集中于此) */
|
|
19
24
|
export declare function buildManifest(input: BuildManifestInput): Manifest;
|
package/lib/schema/manifest.js
CHANGED
|
@@ -21,6 +21,7 @@ export function buildManifest(input) {
|
|
|
21
21
|
},
|
|
22
22
|
exportedAt: input.exportedAt ?? new Date().toISOString(),
|
|
23
23
|
...(input.sourceHome !== undefined && input.sourceHome !== '' ? { sourceHome: input.sourceHome } : {}),
|
|
24
|
+
...(input.foreign !== undefined ? { foreign: input.foreign } : {}),
|
|
24
25
|
sections: input.sections,
|
|
25
26
|
security: {
|
|
26
27
|
containsSecrets: input.containsSecrets,
|
|
@@ -43,6 +44,57 @@ export function parseManifest(raw) {
|
|
|
43
44
|
}
|
|
44
45
|
return parsed;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* `manifest.foreign`(外部来源溯源块)的**宽容校验**。
|
|
49
|
+
*
|
|
50
|
+
* 铁律:本字段**绝不允许**成为拒绝一个包的理由 —— 旧包没有它、新包可能带畸形值,
|
|
51
|
+
* 两者都必须能继续导入。因此这里只产出 `warning`,且**只落一条**汇总(路径恒为 `foreign`),
|
|
52
|
+
* 出错字段名以机器码形式列在文案参数里(不逐字段刷屏、不新增用户可见文案)。
|
|
53
|
+
*/
|
|
54
|
+
function validateForeignOrigin(value) {
|
|
55
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
56
|
+
return [{
|
|
57
|
+
path: 'foreign',
|
|
58
|
+
message: schemaMsg('schema.sectionDataInvalid', { section: 'manifest.foreign', issues: 'not-an-object' }),
|
|
59
|
+
severity: 'warning',
|
|
60
|
+
}];
|
|
61
|
+
}
|
|
62
|
+
const f = value;
|
|
63
|
+
const bad = [];
|
|
64
|
+
if (typeof f['agent'] !== 'string')
|
|
65
|
+
bad.push('agent');
|
|
66
|
+
if (f['locations'] !== undefined && !isStringArray(f['locations']))
|
|
67
|
+
bad.push('locations');
|
|
68
|
+
if (f['sourceVersion'] !== undefined && typeof f['sourceVersion'] !== 'string')
|
|
69
|
+
bad.push('sourceVersion');
|
|
70
|
+
if (f['projects'] !== undefined && !isStringArray(f['projects']))
|
|
71
|
+
bad.push('projects');
|
|
72
|
+
if (f['credentialRefs'] !== undefined && !isStringArray(f['credentialRefs']))
|
|
73
|
+
bad.push('credentialRefs');
|
|
74
|
+
if (f['skipped'] !== undefined && !isSkippedAggregates(f['skipped']))
|
|
75
|
+
bad.push('skipped');
|
|
76
|
+
if (f['plugins'] !== undefined && !isPluginSummaries(f['plugins']))
|
|
77
|
+
bad.push('plugins');
|
|
78
|
+
if (bad.length === 0)
|
|
79
|
+
return [];
|
|
80
|
+
return [{
|
|
81
|
+
path: 'foreign',
|
|
82
|
+
message: schemaMsg('schema.sectionDataInvalid', { section: 'manifest.foreign', issues: bad.join(',') }),
|
|
83
|
+
severity: 'warning',
|
|
84
|
+
}];
|
|
85
|
+
}
|
|
86
|
+
function isStringArray(value) {
|
|
87
|
+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
88
|
+
}
|
|
89
|
+
function isSkippedAggregates(value) {
|
|
90
|
+
return Array.isArray(value) && value.every((v) => v !== null && typeof v === 'object'
|
|
91
|
+
&& typeof v['code'] === 'string'
|
|
92
|
+
&& typeof v['count'] === 'number');
|
|
93
|
+
}
|
|
94
|
+
function isPluginSummaries(value) {
|
|
95
|
+
return Array.isArray(value) && value.every((v) => v !== null && typeof v === 'object'
|
|
96
|
+
&& typeof v['name'] === 'string');
|
|
97
|
+
}
|
|
46
98
|
/** 结构校验:字段类型 / sections 键集合 / security 形状 */
|
|
47
99
|
export function validateManifest(m) {
|
|
48
100
|
const issues = [];
|
|
@@ -106,6 +158,9 @@ export function validateManifest(m) {
|
|
|
106
158
|
issues.push({ path: 'security.encryption', message: schemaMsg('schema.encryptionMustBeObjectOrNull'), severity: 'error' });
|
|
107
159
|
}
|
|
108
160
|
}
|
|
161
|
+
// foreign 是**外部来源包**的溯源块:缺省 = 旧包(合法);形状/字段类型不对 = 仅 warning(绝不拒绝)。
|
|
162
|
+
if (obj['foreign'] !== undefined)
|
|
163
|
+
issues.push(...validateForeignOrigin(obj['foreign']));
|
|
109
164
|
return issues;
|
|
110
165
|
}
|
|
111
166
|
//# sourceMappingURL=manifest.js.map
|
package/lib/schema/types.d.ts
CHANGED
|
@@ -27,6 +27,44 @@ export interface EncryptionInfo {
|
|
|
27
27
|
authTag: string;
|
|
28
28
|
version: number;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* **外部来源包**的溯源块(manifest.foreign,2026-10-18 新增,纯 additive)。
|
|
32
|
+
*
|
|
33
|
+
* 为什么需要它:外部来源(Claude Code / Hermes / …)合成的包是**标准 bundle v1**,
|
|
34
|
+
* 落盘后与 DSH 自产备份**逐字同构** —— 事后无法复核「这包从哪来」。本块把来源标识、
|
|
35
|
+
* 读取位置、项目根、插件元数据与跳过聚合写进 manifest,供用户与界面核对。
|
|
36
|
+
*
|
|
37
|
+
* 三条纪律(与 src/foreign/types.ts 的 ForeignOrigin 同口径):
|
|
38
|
+
* ① **只放标识与计数,绝不放值**(凭据只有引用名,诊断只有机器码,无一字配置内容);
|
|
39
|
+
* ② 向后兼容:旧包没有该字段 ⇒ 缺省即合法;本字段的存在**绝不能**让旧/新包被拒;
|
|
40
|
+
* ③ 缺字段即「不知道」,绝不填默认值假装知道。
|
|
41
|
+
*/
|
|
42
|
+
export interface ManifestForeignOrigin {
|
|
43
|
+
/** 来源 agent id(稳定机器标识,如 claude-code / hermes;不随文案变化) */
|
|
44
|
+
agent: string;
|
|
45
|
+
/** 实际读取的位置(相对 home 的 POSIX 路径;不含任何值) */
|
|
46
|
+
locations?: string[];
|
|
47
|
+
/** 来源侧版本标记(读不到即省略) */
|
|
48
|
+
sourceVersion?: string;
|
|
49
|
+
/** 被扫描的项目根绝对路径(仅溯源,供用户核对;绝不进任何分区载荷) */
|
|
50
|
+
projects?: string[];
|
|
51
|
+
/** 插件清单(名称 / 版本 / 市场 / 来源仓库 / ref / 组件种类;**不含凭据**) */
|
|
52
|
+
plugins?: {
|
|
53
|
+
name: string;
|
|
54
|
+
version?: string;
|
|
55
|
+
marketplace?: string;
|
|
56
|
+
repo?: string;
|
|
57
|
+
ref?: string;
|
|
58
|
+
components?: string[];
|
|
59
|
+
}[];
|
|
60
|
+
/** 跳过与诊断汇总(机器码 + 条数,**不含任何值**) */
|
|
61
|
+
skipped?: {
|
|
62
|
+
code: string;
|
|
63
|
+
count: number;
|
|
64
|
+
}[];
|
|
65
|
+
/** 需要用户在目标机补录的凭据引用名(只有名字,**没有任何值**) */
|
|
66
|
+
credentialRefs?: string[];
|
|
67
|
+
}
|
|
30
68
|
/** 每个导出 ZIP 根部的 manifest.json */
|
|
31
69
|
export interface Manifest {
|
|
32
70
|
schemaVersion: number;
|
|
@@ -50,6 +88,12 @@ export interface Manifest {
|
|
|
50
88
|
* 宽容读取:非字符串/缺失一律按「未知」处理(不重定基,不报错)。
|
|
51
89
|
*/
|
|
52
90
|
sourceHome?: string;
|
|
91
|
+
/**
|
|
92
|
+
* **外部来源包的溯源块**(见 ManifestForeignOrigin);DSH 自产备份没有该字段。
|
|
93
|
+
*
|
|
94
|
+
* 宽容读取:缺省 = 旧包(合法);存在但形状/字段类型不对 = 仅 warning(绝不因此拒绝导入)。
|
|
95
|
+
*/
|
|
96
|
+
foreign?: ManifestForeignOrigin;
|
|
53
97
|
sections: Record<SectionId, boolean>;
|
|
54
98
|
security: {
|
|
55
99
|
containsSecrets: boolean;
|
|
@@ -107,8 +151,9 @@ export interface PluginEntry {
|
|
|
107
151
|
/**
|
|
108
152
|
* 声明依赖 spec(profile package.json dependencies 原样),如 ^0.3.6、
|
|
109
153
|
* github:csyangwen/dsh-memory-evolve、file:/link: 等。
|
|
110
|
-
*
|
|
111
|
-
*
|
|
154
|
+
* 导入时的安装目标由 `core/plugin-cli.ts` 的 `pluginInstallTarget()` 统一推导(issue #101):
|
|
155
|
+
* 非 registry 来源按此 spec 原样安装;`version` 是具体 semver 时按 `<name>@<version>` 精确安装;
|
|
156
|
+
* 其余 spec 按 `<name>@<spec>`;都没有才退回裸包名(npm 最新版)。
|
|
112
157
|
*/
|
|
113
158
|
spec?: string;
|
|
114
159
|
/**
|
package/lib/sync/ancestor.d.ts
CHANGED
|
@@ -15,6 +15,28 @@ export declare function loadAncestor(localSnapshotsDir: string, snapshotId: stri
|
|
|
15
15
|
* 用户可见且可裁决。loadAncestor 的「不存在即抛」契约保持不变(显式读取祖先仍应大声失败)。
|
|
16
16
|
*/
|
|
17
17
|
export declare function tryLoadAncestor(localSnapshotsDir: string, snapshotId: string, fsx?: SnapshotFs): Promise<SyncSnapshot | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* 加密祖先副本的密文载荷文件名(issue #107)。
|
|
20
|
+
* 加密快照无法平铺成明文 JSON 分区,因此加密祖先目录 = manifest.json + 这一个密文载荷文件。
|
|
21
|
+
*/
|
|
22
|
+
export declare const ENCRYPTED_ANCESTOR_FILE = "sections.encrypted.json";
|
|
23
|
+
/**
|
|
24
|
+
* 读祖先副本目录:manifest.encrypted=true → 读密文单载荷;否则按散文件布局读回明文。
|
|
25
|
+
*
|
|
26
|
+
* 为什么要在这里分派(而不是改 layout.readSnapshotFromDir):散文件布局对加密快照是**结构性拒绝**
|
|
27
|
+
* (layoutEncryptedDirUnsupported,git 通道的密文单文件布局依赖它);祖先副本是本地缓存目录,
|
|
28
|
+
* 允许有自己的加密形态,且不该让「本地缓存」把明文布局的契约搞松。
|
|
29
|
+
* 读不出 / 形态不符一律抛错 —— 是否降级为 undefined 由 tryLoadAncestor 决定。
|
|
30
|
+
*/
|
|
31
|
+
export declare function readAncestorSnapshot(dir: string, fsx?: SnapshotFs): Promise<SyncSnapshot>;
|
|
32
|
+
/**
|
|
33
|
+
* 把**加密**祖先副本写入本地目录(issue #107)。
|
|
34
|
+
*
|
|
35
|
+
* 磁盘上只有 manifest.json(manifest.encrypted=true,sectionHashes 为空)+ 单个 AES-GCM 密文载荷;
|
|
36
|
+
* 密码绝不落盘(载荷里只有 salt/iv/authTag 与密文,与远端加密快照同口径)。
|
|
37
|
+
* 目录里有 manifest.json ⇒ 仍被 listAncestors / pruneAncestors 当成普通祖先副本,保留策略不变。
|
|
38
|
+
*/
|
|
39
|
+
export declare function writeEncryptedAncestor(localSnapshotsDir: string, snapshot: SyncSnapshot, fsx?: SnapshotFs): Promise<void>;
|
|
18
40
|
/** 把合并后的快照写入本地祖先副本目录(覆盖同名 id)。 */
|
|
19
41
|
export declare function writeAncestor(localSnapshotsDir: string, snapshot: SyncSnapshot, fsx?: SnapshotFs): Promise<void>;
|
|
20
42
|
/** 列出 localSnapshotsDir 下全部祖先快照目录的 (id, createdAt) 对,按 createdAt 升序。 */
|
package/lib/sync/ancestor.js
CHANGED
|
@@ -7,6 +7,8 @@ import { createSnapshotFs, joinFs } from './fs.js';
|
|
|
7
7
|
import { engineMsg } from './messages.js';
|
|
8
8
|
import { readSnapshotFromDir, writeSnapshotToDir } from './layout.js';
|
|
9
9
|
import { SNAPSHOT_MANIFEST_FILE } from './layout.js';
|
|
10
|
+
import { isEncryptedSections } from './transport.js';
|
|
11
|
+
import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.js';
|
|
10
12
|
/** 默认保留的祖先快照数量(最旧超出则被裁剪)。 */
|
|
11
13
|
export const DEFAULT_ANCESTOR_KEEP = 10;
|
|
12
14
|
/** 按 snapshotId 读取祖先快照目录;不存在抛错,不静默降级。 */
|
|
@@ -15,7 +17,7 @@ export async function loadAncestor(localSnapshotsDir, snapshotId, fsx = createSn
|
|
|
15
17
|
throw new Error(engineMsg('sync.ancestorSnapshotIdRequired'));
|
|
16
18
|
}
|
|
17
19
|
const dir = joinFs(localSnapshotsDir, snapshotId);
|
|
18
|
-
return await
|
|
20
|
+
return await readAncestorSnapshot(dir, fsx);
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* 同 loadAncestor,但「目录不存在 / 不可读 / 损坏」→ undefined(P0-7 的降级路径,供三方合并用)。
|
|
@@ -31,12 +33,74 @@ export async function tryLoadAncestor(localSnapshotsDir, snapshotId, fsx = creat
|
|
|
31
33
|
if (typeof snapshotId !== 'string' || snapshotId === '')
|
|
32
34
|
return undefined;
|
|
33
35
|
try {
|
|
34
|
-
return await
|
|
36
|
+
return await readAncestorSnapshot(joinFs(localSnapshotsDir, snapshotId), fsx);
|
|
35
37
|
}
|
|
36
38
|
catch {
|
|
37
39
|
return undefined;
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* 加密祖先副本的密文载荷文件名(issue #107)。
|
|
44
|
+
* 加密快照无法平铺成明文 JSON 分区,因此加密祖先目录 = manifest.json + 这一个密文载荷文件。
|
|
45
|
+
*/
|
|
46
|
+
export const ENCRYPTED_ANCESTOR_FILE = 'sections.encrypted.json';
|
|
47
|
+
/**
|
|
48
|
+
* 读祖先副本目录:manifest.encrypted=true → 读密文单载荷;否则按散文件布局读回明文。
|
|
49
|
+
*
|
|
50
|
+
* 为什么要在这里分派(而不是改 layout.readSnapshotFromDir):散文件布局对加密快照是**结构性拒绝**
|
|
51
|
+
* (layoutEncryptedDirUnsupported,git 通道的密文单文件布局依赖它);祖先副本是本地缓存目录,
|
|
52
|
+
* 允许有自己的加密形态,且不该让「本地缓存」把明文布局的契约搞松。
|
|
53
|
+
* 读不出 / 形态不符一律抛错 —— 是否降级为 undefined 由 tryLoadAncestor 决定。
|
|
54
|
+
*/
|
|
55
|
+
export async function readAncestorSnapshot(dir, fsx = createSnapshotFs()) {
|
|
56
|
+
const manifestAbs = joinFs(dir, SNAPSHOT_MANIFEST_FILE);
|
|
57
|
+
if (!(await fsx.exists(manifestAbs))) {
|
|
58
|
+
throw new Error(engineMsg('sync.layoutManifestMissing', { file: SNAPSHOT_MANIFEST_FILE, dir }));
|
|
59
|
+
}
|
|
60
|
+
const parsed = parseJsonSafe(Buffer.from(await fsx.readFile(manifestAbs)).toString('utf8'));
|
|
61
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
62
|
+
throw new Error(engineMsg('sync.layoutManifestNotObject'));
|
|
63
|
+
}
|
|
64
|
+
const m = parsed;
|
|
65
|
+
const manifest = m['manifest'];
|
|
66
|
+
if (manifest !== null && typeof manifest === 'object' && !Array.isArray(manifest)
|
|
67
|
+
&& manifest['encrypted'] === true) {
|
|
68
|
+
const payloadAbs = joinFs(dir, ENCRYPTED_ANCESTOR_FILE);
|
|
69
|
+
if (!(await fsx.exists(payloadAbs))) {
|
|
70
|
+
throw new Error(engineMsg('sync.layoutManifestMissing', { file: ENCRYPTED_ANCESTOR_FILE, dir }));
|
|
71
|
+
}
|
|
72
|
+
const payload = parseJsonSafe(Buffer.from(await fsx.readFile(payloadAbs)).toString('utf8'));
|
|
73
|
+
if (!isEncryptedSections(payload)) {
|
|
74
|
+
throw new Error(engineMsg('sync.layoutManifestMissingFields'));
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: typeof m['id'] === 'string' ? m['id'] : '',
|
|
78
|
+
createdAt: typeof m['createdAt'] === 'string' ? m['createdAt'] : '',
|
|
79
|
+
manifest: manifest,
|
|
80
|
+
sections: payload,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return await readSnapshotFromDir(dir, fsx);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 把**加密**祖先副本写入本地目录(issue #107)。
|
|
87
|
+
*
|
|
88
|
+
* 磁盘上只有 manifest.json(manifest.encrypted=true,sectionHashes 为空)+ 单个 AES-GCM 密文载荷;
|
|
89
|
+
* 密码绝不落盘(载荷里只有 salt/iv/authTag 与密文,与远端加密快照同口径)。
|
|
90
|
+
* 目录里有 manifest.json ⇒ 仍被 listAncestors / pruneAncestors 当成普通祖先副本,保留策略不变。
|
|
91
|
+
*/
|
|
92
|
+
export async function writeEncryptedAncestor(localSnapshotsDir, snapshot, fsx = createSnapshotFs()) {
|
|
93
|
+
if (!isEncryptedSections(snapshot.sections)) {
|
|
94
|
+
// 结构性断言:明文快照必须走 writeAncestor(散文件布局),不得落进密文单载荷形态
|
|
95
|
+
throw new Error(engineMsg('sync.layoutEncryptedDirUnsupported'));
|
|
96
|
+
}
|
|
97
|
+
const dir = joinFs(localSnapshotsDir, snapshot.id);
|
|
98
|
+
await fsx.mkdir(dir);
|
|
99
|
+
const manifest = { id: snapshot.id, createdAt: snapshot.createdAt, manifest: snapshot.manifest, sectionHashes: {} };
|
|
100
|
+
const enc = new TextEncoder();
|
|
101
|
+
await fsx.writeFile(joinFs(dir, SNAPSHOT_MANIFEST_FILE), enc.encode(stringifyJsonSafe(manifest, { space: 2 })));
|
|
102
|
+
await fsx.writeFile(joinFs(dir, ENCRYPTED_ANCESTOR_FILE), enc.encode(stringifyJsonSafe(snapshot.sections, { space: 2 })));
|
|
103
|
+
}
|
|
40
104
|
/** 把合并后的快照写入本地祖先副本目录(覆盖同名 id)。 */
|
|
41
105
|
export async function writeAncestor(localSnapshotsDir, snapshot, fsx = createSnapshotFs()) {
|
|
42
106
|
const dir = joinFs(localSnapshotsDir, snapshot.id);
|