dsh-config-manager 0.1.80 → 0.1.82

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.
Files changed (146) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/lib/cli/actions.d.ts +5 -0
  4. package/lib/cli/actions.js +5 -0
  5. package/lib/cli/index.js +33 -17
  6. package/lib/cli/restore-resume.d.ts +25 -0
  7. package/lib/cli/restore-resume.js +49 -0
  8. package/lib/client.d.ts +6 -0
  9. package/lib/client.js +521 -519
  10. package/lib/core/analyzer.d.ts +3 -41
  11. package/lib/core/analyzer.js +9 -292
  12. package/lib/core/import-planning.d.ts +110 -0
  13. package/lib/core/import-planning.js +305 -0
  14. package/lib/core/journal.d.ts +12 -0
  15. package/lib/core/journal.js +12 -0
  16. package/lib/core/messages.d.ts +17 -0
  17. package/lib/core/messages.js +37 -0
  18. package/lib/core/model-tools.js +17 -5
  19. package/lib/core/phase3-host.d.ts +21 -0
  20. package/lib/core/phase3-host.js +63 -14
  21. package/lib/core/plugin-cli.d.ts +24 -2
  22. package/lib/core/plugin-cli.js +36 -3
  23. package/lib/core/recovery-orchestrator.d.ts +17 -1
  24. package/lib/core/recovery-orchestrator.js +39 -29
  25. package/lib/core/restore-crash-child.d.ts +1 -0
  26. package/lib/core/restore-crash-child.js +77 -0
  27. package/lib/core/restore-resume.d.ts +161 -0
  28. package/lib/core/restore-resume.js +274 -0
  29. package/lib/core/restore-wal.d.ts +88 -0
  30. package/lib/core/restore-wal.js +164 -0
  31. package/lib/core/restore.d.ts +39 -1
  32. package/lib/core/restore.js +229 -13
  33. package/lib/core/rollback-ignore-rules.d.ts +14 -0
  34. package/lib/core/rollback-ignore-rules.js +54 -1
  35. package/lib/core/rollback-point.d.ts +3 -1
  36. package/lib/core/rollback-point.js +10 -1
  37. package/lib/index.d.ts +6 -252
  38. package/lib/index.js +17 -561
  39. package/lib/market/git-file-writer.d.ts +12 -3
  40. package/lib/market/git-file-writer.js +42 -6
  41. package/lib/market/messages-market.d.ts +1 -0
  42. package/lib/market/messages-market.js +2 -0
  43. package/lib/market/reader.d.ts +14 -2
  44. package/lib/market/reader.js +57 -18
  45. package/lib/routes/consult.js +1 -1
  46. package/lib/routes/import.js +1 -1
  47. package/lib/routes/market.js +1 -1
  48. package/lib/routes/me.js +1 -1
  49. package/lib/routes/prefs.js +1 -2
  50. package/lib/routes/profiles.js +1 -1
  51. package/lib/routes/recovery.js +1 -1
  52. package/lib/routes/route-support.d.ts +275 -0
  53. package/lib/routes/route-support.js +637 -0
  54. package/lib/routes/snapshots.js +40 -8
  55. package/lib/routes/sync.js +17 -1
  56. package/lib/sync/autosync-scheduler.d.ts +8 -3
  57. package/lib/sync/autosync-scheduler.js +22 -9
  58. package/lib/sync/gist/gist-transport.d.ts +44 -2
  59. package/lib/sync/gist/gist-transport.js +133 -39
  60. package/lib/sync/gist/messages.d.ts +1 -0
  61. package/lib/sync/gist/messages.js +3 -0
  62. package/lib/sync/git/git-transport.d.ts +33 -0
  63. package/lib/sync/git/git-transport.js +111 -13
  64. package/lib/sync/s3/oss4-verify.d.ts +47 -0
  65. package/lib/sync/s3/oss4-verify.js +156 -0
  66. package/lib/sync/s3/s3-providers.d.ts +10 -0
  67. package/lib/sync/s3/s3-providers.js +29 -6
  68. package/lib/sync/s3/s3-transport.js +6 -1
  69. package/lib/sync/s3/sigv4.d.ts +23 -2
  70. package/lib/sync/s3/sigv4.js +19 -8
  71. package/lib/sync/sync-engine.d.ts +13 -0
  72. package/lib/sync/sync-engine.js +9 -1
  73. package/lib/sync/sync-session.d.ts +5 -0
  74. package/lib/utils/git-fast-forward.d.ts +18 -0
  75. package/lib/utils/git-fast-forward.js +28 -0
  76. package/lib/utils/workdir-lock.d.ts +58 -0
  77. package/lib/utils/workdir-lock.js +147 -0
  78. package/package.json +2 -1
  79. package/src/cli/actions.ts +6 -0
  80. package/src/cli/index-guards.test.ts +62 -0
  81. package/src/cli/index.ts +30 -20
  82. package/src/cli/restore-resume.test.ts +120 -0
  83. package/src/cli/restore-resume.ts +76 -0
  84. package/src/client/CLIENT_DEPENDENCIES.md +8 -29
  85. package/src/client/sync/merge-view.ts +2 -0
  86. package/src/core/analyzer.ts +16 -340
  87. package/src/core/import-planning.ts +356 -0
  88. package/src/core/journal.ts +13 -0
  89. package/src/core/messages.ts +37 -0
  90. package/src/core/model-tools.ts +19 -5
  91. package/src/core/phase3-host.ts +73 -14
  92. package/src/core/plugin-cli.fs.test.ts +72 -2
  93. package/src/core/plugin-cli.ts +35 -3
  94. package/src/core/recovery-orchestrator.ts +41 -19
  95. package/src/core/restore-crash-child.ts +85 -0
  96. package/src/core/restore-crash-injection.test.ts +329 -0
  97. package/src/core/restore-resume.test.ts +376 -0
  98. package/src/core/restore-resume.ts +414 -0
  99. package/src/core/restore-target-symlink.test.ts +232 -0
  100. package/src/core/restore-wal.test.ts +170 -0
  101. package/src/core/restore-wal.ts +222 -0
  102. package/src/core/restore.ts +251 -13
  103. package/src/core/rollback-ignore-rules.ts +53 -1
  104. package/src/core/rollback-point.ts +19 -2
  105. package/src/core/rollback-sensitive-exclusion.test.ts +226 -0
  106. package/src/index.facade.test.ts +49 -0
  107. package/src/index.ts +23 -647
  108. package/src/market/git-file-writer.test.ts +39 -3
  109. package/src/market/git-file-writer.ts +43 -7
  110. package/src/market/market.test.ts +31 -3
  111. package/src/market/messages-market.ts +2 -0
  112. package/src/market/reader.ts +55 -18
  113. package/src/routes/consult.ts +1 -1
  114. package/src/routes/import.ts +1 -1
  115. package/src/routes/market.ts +1 -1
  116. package/src/routes/me.ts +1 -1
  117. package/src/routes/prefs.ts +3 -2
  118. package/src/routes/profiles.ts +1 -1
  119. package/src/routes/recovery.ts +1 -1
  120. package/src/routes/route-support.ts +735 -0
  121. package/src/routes/snapshots.ts +47 -7
  122. package/src/routes/sync.ts +17 -1
  123. package/src/security/secret-scanner.test.ts +1 -1
  124. package/src/sync/autosync-scheduler.test.ts +62 -0
  125. package/src/sync/autosync-scheduler.ts +21 -8
  126. package/src/sync/gist/gist-conditional-write.test.ts +34 -15
  127. package/src/sync/gist/gist-delete-concurrency.test.ts +244 -0
  128. package/src/sync/gist/gist-overwrite-semantics.test.ts +258 -0
  129. package/src/sync/gist/gist-transport.ts +154 -44
  130. package/src/sync/gist/messages.ts +3 -0
  131. package/src/sync/git/git-transport.test.ts +68 -6
  132. package/src/sync/git/git-transport.ts +117 -14
  133. package/src/sync/s3/oss4-verify.ts +201 -0
  134. package/src/sync/s3/s3-providers.ts +32 -6
  135. package/src/sync/s3/s3-transport.oss4-server.test.ts +117 -0
  136. package/src/sync/s3/s3-transport.test.ts +44 -1
  137. package/src/sync/s3/s3-transport.ts +8 -1
  138. package/src/sync/s3/sigv4.test.ts +97 -0
  139. package/src/sync/s3/sigv4.ts +38 -9
  140. package/src/sync/sync-engine.test.ts +64 -0
  141. package/src/sync/sync-engine.ts +22 -1
  142. package/src/sync/sync-session.ts +5 -0
  143. package/src/utils/git-fast-forward.test.ts +44 -0
  144. package/src/utils/git-fast-forward.ts +31 -0
  145. package/src/utils/workdir-lock.test.ts +111 -0
  146. package/src/utils/workdir-lock.ts +162 -0
@@ -1,8 +1,8 @@
1
1
  import { type ZipArchive, type ZipSafetyLimits } from '../utils/zip.ts';
2
2
  import type { BootSafetyReport } from './boot-safety.ts';
3
- import type { Manifest, SectionId } from '../schema/types.ts';
3
+ import type { SectionId } from '../schema/types.ts';
4
4
  import type { MsgFunc } from './messages.ts';
5
- import { type ConfigAdapter, type ExecutedItem, type HostContext, type ImportAnalysis, type ImportDecisions, type ImportPlan, type ImportResult, type PathMapping, type SessionFormatProbeResult, type SnapshotStore, type TransactionSnapshotContext } from './types.ts';
5
+ import { type ConfigAdapter, type ExecutedItem, type HostContext, type ImportAnalysis, type ImportDecisions, type ImportPlan, type ImportResult, type SessionFormatProbeResult, type TransactionSnapshotContext, type SnapshotStore } from './types.ts';
6
6
  /**
7
7
  * 执行阶段顺序(设计 §5.4:副作用大的 patch/安装最后)。导出供宿主生命周期等复用同一顺序。
8
8
  *
@@ -188,42 +188,4 @@ export declare class Analyzer {
188
188
  private fileFp;
189
189
  private applyOne;
190
190
  }
191
- /**
192
- * G-06:执行 schema 迁移链并把迁移结果重新校验为合法 manifest。
193
- *
194
- * `migrateToCurrent` 此前在 `src/` 内零引用(迁移链有定义、有单测、从不执行);
195
- * `loadBundle` 只用 isSupported 做判定就直接把旧文档当新格式使用。本函数是迁移链的
196
- * **真实接线点**:沿链迁移 → 重新校验结果是合法 manifest(不合法则抛明确错误,
197
- * 绝不把半迁移文档当合法 manifest 继续)→ 每个已应用步骤翻译成用户可见告警。
198
- *
199
- * 显式传入 from/target 使「链式迁移真的被执行」可被直接单测:当前
200
- * `MIN_SUPPORTED_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION = 1`,`needsMigration` 恒假,
201
- * 因此该分支在真实导入中结构上不可达(loadBundle 仍按 needsMigration 调用本函数)。
202
- */
203
- export declare function runSchemaMigration(doc: unknown, fromVersion: number, targetVersion: number, msg: MsgFunc): {
204
- manifest: Manifest;
205
- warnings: string[];
206
- };
207
- /**
208
- * 冲突决策(applyItemResolution)已抽到 `./conflict-decisions.ts`:
209
- * 与恢复/回放路径共用同一份语义(回放侧随灾备快照线下线,语义仍是同一条)。
210
- */
211
- /**
212
- * 基础路径重定基规则:导出机 DSH home → 本机 DSH home(issue #45 用户补充)。
213
- *
214
- * 只在「两边的规范化路径都是绝对路径、且不相同」时生成;两侧都去掉尾部分隔符后比较,
215
- * 避免 /opt/.dsh/ 与 /opt/.dsh 被判成不同而白跑一次全量替换。前缀匹配仍由 applyPrefixMappings
216
- * 负责(必须落在段边界),所以 /opt/.dsh 不会误伤 /opt/.dsh-extra。
217
- *
218
- * @returns 映射规则;不需要重定基(缺 sourceHome / 相同 / 相对路径)→ undefined
219
- */
220
- export declare function rebaseMapping(sourceHome: string | undefined, localHome: string): PathMapping | undefined;
221
- /**
222
- * 把映射应用到分区数据(PathMapper 先行:只替换匹配前缀的字符串叶值)。
223
- *
224
- * `fileCollectionIds` 里的分区一律跳过:`FileCollectionAdapter` 把 `relativePath` 当落盘路径用,
225
- * 它必须逐字保留(issue #45 ④:改写会话文件的相对路径会让目标机下次启动直接报
226
- * `corrupt session log`;pluginFiles/skills 等同理——丢位置等于丢归属)。
227
- * 映射在这些分区上**没有任何合法作用**:文件内容存的是字节(`Uint8Array` 叶子),不会被改。
228
- */
229
- export declare function applyMappingsToSections(sections: Map<SectionId, unknown>, mappings: PathMapping[], fileCollectionIds?: ReadonlySet<string>): void;
191
+ export { applyMappingsToSections, rebaseMapping, runSchemaMigration } from './import-planning.ts';
@@ -12,12 +12,10 @@ import path from 'node:path';
12
12
  import { sha256Hex } from '../utils/hashing.js';
13
13
  import { parseJsonSafe } from '../utils/json.js';
14
14
  import { SECTION_FILE_PREFIXES, SECTION_IDS, SECTION_JSON_PATHS, isFileSection, validateSectionData, } from '../schema/config.js';
15
- import { CHECKSUMS_FILE, MANIFEST_FILE, parseManifest, validateManifest } from '../schema/manifest.js';
15
+ import { CHECKSUMS_FILE, MANIFEST_FILE, parseManifest } from '../schema/manifest.js';
16
16
  import { CURRENT_SCHEMA_VERSION, canImport, describeVersion, isSupported, isTooNew, needsMigration, } from '../schema/versions.js';
17
- import { migrateToCurrent } from '../migrations/index.js';
18
- import { isAbsolutePath, applyPrefixMappings } from '../utils/paths.js';
19
17
  import { parseZip } from '../utils/zip.js';
20
- import { loadTombstones, isTombstoned } from '../schema/tombstones.js';
18
+ import { loadTombstones } from '../schema/tombstones.js';
21
19
  import { DEFAULT_SENSITIVE_RELS, restoreVaultFiles } from '../security/vault.js';
22
20
  import { createSnapshot, fsForSection, planItemWritesTarget, resolveFileTarget, resolveFileTargetRel } from './backup.js';
23
21
  import { isCredentialConfigured } from './credential-status.js';
@@ -1332,292 +1330,11 @@ export class Analyzer {
1332
1330
  }
1333
1331
  }
1334
1332
  }
1335
- async function reportRollbackToJournal(binding, report) {
1336
- const reporter = binding;
1337
- await reporter?.recordRollback?.(report);
1338
- }
1339
- /* ---------------- 纯函数辅助 ---------------- */
1340
- /**
1341
- * G-06:执行 schema 迁移链并把迁移结果重新校验为合法 manifest。
1342
- *
1343
- * `migrateToCurrent` 此前在 `src/` 内零引用(迁移链有定义、有单测、从不执行);
1344
- * `loadBundle` 只用 isSupported 做判定就直接把旧文档当新格式使用。本函数是迁移链的
1345
- * **真实接线点**:沿链迁移 → 重新校验结果是合法 manifest(不合法则抛明确错误,
1346
- * 绝不把半迁移文档当合法 manifest 继续)→ 每个已应用步骤翻译成用户可见告警。
1347
- *
1348
- * 显式传入 from/target 使「链式迁移真的被执行」可被直接单测:当前
1349
- * `MIN_SUPPORTED_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION = 1`,`needsMigration` 恒假,
1350
- * 因此该分支在真实导入中结构上不可达(loadBundle 仍按 needsMigration 调用本函数)。
1351
- */
1352
- export function runSchemaMigration(doc, fromVersion, targetVersion, msg) {
1353
- const result = migrateToCurrent(doc, fromVersion, targetVersion);
1354
- const issues = validateManifest(result.doc).filter((i) => i.severity === 'error');
1355
- if (issues.length > 0) {
1356
- throw new Error(msg('import.migrateInvalidManifest', { issues: issues.map((i) => i.message).join('; ') }));
1357
- }
1358
- return {
1359
- manifest: result.doc,
1360
- warnings: result.applied.map((step) => msg('import.migrated', { from: String(step.from), to: String(step.to) })),
1361
- };
1362
- }
1363
- async function verifyAgainstTable(entries, table) {
1364
- const mismatches = [];
1365
- const missing = [];
1366
- for (const [relPath, expected] of Object.entries(table)) {
1367
- const data = entries.get(relPath);
1368
- if (data === undefined) {
1369
- missing.push(relPath);
1370
- continue;
1371
- }
1372
- if (sha256Hex(data) !== expected)
1373
- mismatches.push(relPath);
1374
- }
1375
- return { ok: mismatches.length === 0 && missing.length === 0, mismatches, missing };
1376
- }
1377
- /**
1378
- * 冲突决策(applyItemResolution)已抽到 `./conflict-decisions.ts`:
1379
- * 与恢复/回放路径共用同一份语义(回放侧随灾备快照线下线,语义仍是同一条)。
1380
- */
1381
- /**
1382
- * 基础路径重定基规则:导出机 DSH home → 本机 DSH home(issue #45 用户补充)。
1383
- *
1384
- * 只在「两边的规范化路径都是绝对路径、且不相同」时生成;两侧都去掉尾部分隔符后比较,
1385
- * 避免 /opt/.dsh/ 与 /opt/.dsh 被判成不同而白跑一次全量替换。前缀匹配仍由 applyPrefixMappings
1386
- * 负责(必须落在段边界),所以 /opt/.dsh 不会误伤 /opt/.dsh-extra。
1387
- *
1388
- * @returns 映射规则;不需要重定基(缺 sourceHome / 相同 / 相对路径)→ undefined
1389
- */
1390
- export function rebaseMapping(sourceHome, localHome) {
1391
- if (sourceHome === undefined || sourceHome === '')
1392
- return undefined;
1393
- const strip = (value) => value.replace(/\\/g, '/').replace(/\/+$/, '');
1394
- const from = strip(sourceHome);
1395
- const to = strip(localHome ?? '');
1396
- if (from === '' || to === '' || from === to)
1397
- return undefined;
1398
- const isAbsolute = (value) => value.startsWith('/') || /^[a-zA-Z]:[\/]/.test(value);
1399
- if (!isAbsolute(from) || !isAbsolute(to))
1400
- return undefined;
1401
- return { oldPrefix: from, newPrefix: to, appliesTo: [] };
1402
- }
1403
- /**
1404
- * 会话单元版本查询:探针的单元键(`projectKey/会话目录`)与计划项单元 id
1405
- * (`sessions:projectKey/会话目录`)是两个键空间,必须两种形态都试。
1406
- *
1407
- * 为什么不在探针里直接带上适配器前缀:探针只认「分区内的相对路径」,它不知道也不该知道
1408
- * 哪个适配器在消费它(core 也不得 import 会话字节工具)。键空间对齐因此落在计划生成期。
1409
- */
1410
- function sessionUnitVersion(byUnit, unitId) {
1411
- const direct = byUnit.get(unitId);
1412
- if (direct !== undefined)
1413
- return direct;
1414
- const prefix = 'sessions:';
1415
- const bare = unitId.startsWith(prefix) ? unitId.slice(prefix.length) : prefix + unitId;
1416
- return byUnit.get(bare);
1417
- }
1418
- /** 路径映射合并:只保留「已解析」(newPrefix 非空)的映射用于执行期数据改写;
1419
- * 未解决项(newPrefix 空)保留在 plan.items 里供 UI 提示,但不参与数据映射。 */
1420
- function mergePathMappings(items, userMappings) {
1421
- const merged = new Map();
1422
- for (const m of userMappings) {
1423
- if (m.newPrefix !== '')
1424
- merged.set(m.oldPrefix, m);
1425
- }
1426
- return [...merged.values()];
1427
- }
1428
- /**
1429
- * 把映射应用到分区数据(PathMapper 先行:只替换匹配前缀的字符串叶值)。
1430
- *
1431
- * `fileCollectionIds` 里的分区一律跳过:`FileCollectionAdapter` 把 `relativePath` 当落盘路径用,
1432
- * 它必须逐字保留(issue #45 ④:改写会话文件的相对路径会让目标机下次启动直接报
1433
- * `corrupt session log`;pluginFiles/skills 等同理——丢位置等于丢归属)。
1434
- * 映射在这些分区上**没有任何合法作用**:文件内容存的是字节(`Uint8Array` 叶子),不会被改。
1435
- */
1436
- export function applyMappingsToSections(sections, mappings, fileCollectionIds = new Set()) {
1437
- if (mappings.length === 0)
1438
- return;
1439
- for (const [sectionId, data] of sections) {
1440
- if (fileCollectionIds.has(sectionId))
1441
- continue;
1442
- const appliesTo = sectionId === 'workspaces' ? ['workspaces']
1443
- : sectionId === 'mcp' ? ['mcp']
1444
- : sectionId === 'skills' ? ['skills']
1445
- : sectionId === 'pluginFiles' ? ['pluginConfig']
1446
- : [];
1447
- const relevant = mappings.filter((m) => m.appliesTo.some((a) => appliesTo.includes(a)) || m.appliesTo.length === 0);
1448
- if (relevant.length === 0)
1449
- continue;
1450
- sections.set(sectionId, applyPrefixMappings(data, relevant));
1451
- }
1452
- }
1453
- /** 路径形态判定:跨平台盘符/UNIX 冲突 → platformMismatch;其余绝对路径 → missing(需映射) */
1454
- function detectPathIssues(sourcePlatform, targetPlatform, sections) {
1455
- const issues = [];
1456
- const workspaces = sections.get('workspaces');
1457
- for (const w of workspaces?.workspaces ?? []) {
1458
- const p = w.path;
1459
- // section:如实标注来源分区,供导入向导按「本次会写盘的分区」过滤(见 PathIssue.section)
1460
- if (p && isAbsolutePath(p))
1461
- issues.push({ ...judgePath(p, sourcePlatform, targetPlatform), section: 'workspaces' });
1462
- }
1463
- const mcp = sections.get('mcp');
1464
- for (const s of mcp?.servers ?? []) {
1465
- for (const p of [s.cwd, s.command]) {
1466
- if (p && isAbsolutePath(p))
1467
- issues.push({ ...judgePath(p, sourcePlatform, targetPlatform), section: 'mcp' });
1468
- }
1469
- }
1470
- // 去重
1471
- const seen = new Set();
1472
- return issues.filter((i) => {
1473
- const key = `${i.kind}:${i.value}`;
1474
- if (seen.has(key))
1475
- return false;
1476
- seen.add(key);
1477
- return true;
1478
- });
1479
- }
1480
- function judgePath(p, sourcePlatform, targetPlatform) {
1481
- const isWinStyle = /^[a-zA-Z]:[\\/]/.test(p);
1482
- const isUnixStyle = p.startsWith('/');
1483
- if (sourcePlatform !== targetPlatform && ((isWinStyle && targetPlatform !== 'win32') || (isUnixStyle && targetPlatform === 'win32'))) {
1484
- return { kind: 'platformMismatch', value: p };
1485
- }
1486
- return { kind: 'missing', value: p };
1487
- }
1488
- /**
1489
- * 凭据计划项(MissingSecret / Skip)在**导入路径**上的唯一生成点。
1490
- *
1491
- * ref 来源取**并集**:① credentialsStatus 里 configured=true 的 ref(源机声明「有」,
1492
- * 但普通备份不带值);② 宿主解密出的归档 ref(`opts.decryptedCredentials`)——
1493
- * 加密备份携带的是 `.credentials.yaml` **原文**,可能含未被任何 settings namespace 引用的 ref
1494
- * (如其它插件自用的 key),只认 ① 会让这些值静默丢掉(真机反馈:导入密钥没生效)。
1495
- *
1496
- * 规则(与同步侧 sync-engine.appendCredentialPlanItems 同口径:**值的有无**优先于本机状态):
1497
- * - **有值** → MissingSecret「随加密备份恢复」:用户显式勾了「导出密钥」,导入即写回;
1498
- * **不因本机已有而跳过**(跳过 = 用户以为密钥导入了、其实没写)。
1499
- * - **无值**(只有 ref 名)→ 本机已配置 → Skip(保留本机值、不再索要补录,用户报告
1500
- * 「已有的重复密钥也会提示」);本机没有 → MissingSecret「需要补录」。
1501
- *
1502
- * 凭据值不可回读,因此无值分支一律不覆盖本机已有值。
1503
- *
1504
- * @param decrypted 归档里解出的 ref→值(仅内存;undefined = 未提供密码/无凭据载荷)
1505
- * @param isConfiguredLocally 目标机是否已配置该 ref(读不到 → false,保守按需补录处理)
1506
- */
1507
- /**
1508
- * 凭据值「可用」的**唯一判据**(t14 / core-F2 修):必须是非空字符串。
1509
- *
1510
- * 此前三处各判各的 —— 计划期用 `Map.has()`(空串也算「有值」)、执行期用 `value !== ''`、
1511
- * 结果期又用 `Map.has()`。于是「归档里该 ref 的值为空串」时:计划项说「随加密备份恢复」,
1512
- * 执行静默跳过,结果既不报缺失还把该 ref 算成「已从归档写回」—— 用户看到「导入完成」,
1513
- * 密钥其实没写进去(静默 no-op)。三处必须同源,否则该缺陷会再次以另一种形态复发。
1514
- */
1515
- function usableSecretValue(decrypted, inputs, ref) {
1516
- const fromArchive = decrypted?.get(ref);
1517
- if (typeof fromArchive === 'string' && fromArchive !== '')
1518
- return true;
1519
- const fromInput = inputs?.[ref];
1520
- return typeof fromInput === 'string' && fromInput !== '';
1521
- }
1522
- async function buildCredentialPlanItems(items, sections, decrypted, msg, isConfiguredLocally) {
1523
- const creds = sections.get('credentialsStatus');
1524
- const declared = [];
1525
- for (const c of creds?.credentials ?? []) {
1526
- if (typeof c.ref === 'string' && c.ref !== '' && c.configured === true)
1527
- declared.push(c.ref);
1528
- }
1529
- const refs = [...new Set([...declared, ...(decrypted?.keys() ?? [])])];
1530
- const existing = new Set(items.map((i) => i.id));
1531
- for (const ref of refs) {
1532
- const id = `secret:${ref}`;
1533
- if (existing.has(id))
1534
- continue;
1535
- existing.add(id);
1536
- const target = { adapter: 'credentialsStatus', ref };
1537
- if (usableSecretValue(decrypted, undefined, ref)) {
1538
- items.push({
1539
- id,
1540
- kind: 'MissingSecret',
1541
- adapter: 'credentialsStatus',
1542
- description: msg('import.secretFromArchive', { ref }),
1543
- severity: 'warning',
1544
- target,
1545
- });
1546
- continue;
1547
- }
1548
- if (await isConfiguredLocally(ref)) {
1549
- items.push({
1550
- id,
1551
- kind: 'Skip',
1552
- adapter: 'credentialsStatus',
1553
- description: msg('import.secretAlreadyConfigured', { ref }),
1554
- severity: 'info',
1555
- target,
1556
- });
1557
- continue;
1558
- }
1559
- items.push({
1560
- id,
1561
- kind: 'MissingSecret',
1562
- adapter: 'credentialsStatus',
1563
- description: msg('import.secretMissingDesc', { ref }),
1564
- severity: 'warning',
1565
- target,
1566
- });
1567
- }
1568
- }
1569
- /* ---------------- F4 删除墓碑过滤(纯函数,可独立测试) ---------------- */
1570
- /**
1571
- * 按删除墓碑过滤计划项(F4):
1572
- * - 条目级:插件(Install/Update/Conflict)、技能(skills 文件)、文件类条目(agentPresets 等)命中墓碑 → 剔除;
1573
- * - 分区级:墓碑 kind='section' 且 id=分区 id → 整分区跳过(仅记一条 skipped)。
1574
- * 返回过滤后的 items 与被跳过清单(纯函数:不读文件、不改入参)。
1575
- */
1576
- function applyTombstoneFilter(items, tombstones) {
1577
- if (tombstones.length === 0)
1578
- return { items, skipped: [] };
1579
- const tombstonedSections = new Set(tombstones.filter((t) => t.kind === 'section').map((t) => t.id));
1580
- const skipped = [];
1581
- const out = [];
1582
- const reportedSections = new Set();
1583
- for (const item of items) {
1584
- // 分区级墓碑优先:整个分区不出现(含 Skip 等非写入项,UI 提示「该分区已删除」)
1585
- if (tombstonedSections.has(item.adapter)) {
1586
- if (!reportedSections.has(item.adapter)) {
1587
- skipped.push({ kind: 'section', id: item.adapter, adapter: item.adapter });
1588
- reportedSections.add(item.adapter);
1589
- }
1590
- continue;
1591
- }
1592
- const key = tombstoneKeyForItem(item);
1593
- if (key !== null && isTombstoned(key.kind, key.id, tombstones)) {
1594
- skipped.push({ kind: key.kind, id: key.id, adapter: item.adapter });
1595
- continue;
1596
- }
1597
- out.push(item);
1598
- }
1599
- return { items: out, skipped };
1600
- }
1601
- /** PlanItem → 墓碑键({kind,id})映射;非「将写入」的条目(Skip 等)与配置类项返回 null 不过滤 */
1602
- function tombstoneKeyForItem(item) {
1603
- // 插件本体(plugin:<pkg>):Install / Update / Conflict 均视为「将写入该插件」→ 命中墓碑剔除;
1604
- // issue #95 的 Create = 「把 host 自带的层声明进 dsh.profile.bundles」,同样是写入该插件,
1605
- // 必须一起被墓碑剔除(用户删过的插件不得被一份旧备份重新声明回来)。
1606
- // patch: 行与 plugins:pnpm-workspace 是配置类项,不按插件墓碑过滤(分区级墓碑可覆盖)。
1607
- if (item.adapter === 'plugins' && item.id.startsWith('plugin:')) {
1608
- if (item.kind !== 'Install' && item.kind !== 'Update' && item.kind !== 'Conflict' && item.kind !== 'Create')
1609
- return null;
1610
- return { kind: 'plugin', id: item.id.slice('plugin:'.length) };
1611
- }
1612
- // 文件类分区条目(id = <分区>:<相对路径>):skills → skill 墓碑;其余文件类(agentPresets/
1613
- // agentInstructions/pluginFiles/sessions/self)→ 通用 file 墓碑(id 为相对路径)。
1614
- if (item.kind === 'Create' || item.kind === 'Update' || item.kind === 'Conflict') {
1615
- const prefix = `${item.adapter}:`;
1616
- if (item.adapter === 'skills')
1617
- return { kind: 'skill', id: item.id.slice(prefix.length) };
1618
- if (isFileSection(item.adapter))
1619
- return { kind: 'file', id: item.id.slice(prefix.length) };
1620
- }
1621
- return null;
1622
- }
1333
+ /* ---------------- 计划期纯函数(P4 拆分,已外提到 ./import-planning.ts) ---------------- */
1334
+ // analyzer 只保留三段式引擎本体;路径重定基/映射、schema 迁移校验、逐路径 issue、凭据计划项、
1335
+ // 墓碑过滤、journal 上报这些**纯计算**都搬去 `./import-planning.ts`(本文件 1824 → 约 1490 行)。
1336
+ // 依赖方向单行:analyzer → import-planning,后者绝不 import 本文件(避免第二个入口环)。
1337
+ import { applyMappingsToSections, applyTombstoneFilter, buildCredentialPlanItems, detectPathIssues, mergePathMappings, rebaseMapping, reportRollbackToJournal, runSchemaMigration, sessionUnitVersion, usableSecretValue, verifyAgainstTable, } from './import-planning.js';
1338
+ // 对外导入面保持不变(core/index.ts 与既有测试按 './analyzer.ts' 取这三个名字)。
1339
+ export { applyMappingsToSections, rebaseMapping, runSchemaMigration } from './import-planning.js';
1623
1340
  //# sourceMappingURL=analyzer.js.map
@@ -0,0 +1,110 @@
1
+ import type { Manifest, SectionId } from '../schema/types.ts';
2
+ import type { Tombstone, TombstoneKind } from '../schema/tombstones.ts';
3
+ import type { MsgFunc } from './messages.ts';
4
+ import type { PathIssue, PathMapping, PlanItem, SkippedTombstone, TransactionSnapshotContext } from './types.ts';
5
+ export declare function reportRollbackToJournal(binding: TransactionSnapshotContext | undefined, report: {
6
+ full: boolean;
7
+ failed: readonly string[];
8
+ }): Promise<void>;
9
+ /**
10
+ * G-06:执行 schema 迁移链并把迁移结果重新校验为合法 manifest。
11
+ *
12
+ * `migrateToCurrent` 此前在 `src/` 内零引用(迁移链有定义、有单测、从不执行);
13
+ * `loadBundle` 只用 isSupported 做判定就直接把旧文档当新格式使用。本函数是迁移链的
14
+ * **真实接线点**:沿链迁移 → 重新校验结果是合法 manifest(不合法则抛明确错误,
15
+ * 绝不把半迁移文档当合法 manifest 继续)→ 每个已应用步骤翻译成用户可见告警。
16
+ *
17
+ * 显式传入 from/target 使「链式迁移真的被执行」可被直接单测:当前
18
+ * `MIN_SUPPORTED_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION = 1`,`needsMigration` 恒假,
19
+ * 因此该分支在真实导入中结构上不可达(loadBundle 仍按 needsMigration 调用本函数)。
20
+ */
21
+ export declare function runSchemaMigration(doc: unknown, fromVersion: number, targetVersion: number, msg: MsgFunc): {
22
+ manifest: Manifest;
23
+ warnings: string[];
24
+ };
25
+ export declare function verifyAgainstTable(entries: Map<string, Uint8Array>, table: Record<string, string>): Promise<{
26
+ ok: boolean;
27
+ mismatches: string[];
28
+ missing: string[];
29
+ }>;
30
+ /**
31
+ * 冲突决策(applyItemResolution)已抽到 `./conflict-decisions.ts`:
32
+ * 与恢复/回放路径共用同一份语义(回放侧随灾备快照线下线,语义仍是同一条)。
33
+ */
34
+ /**
35
+ * 基础路径重定基规则:导出机 DSH home → 本机 DSH home(issue #45 用户补充)。
36
+ *
37
+ * 只在「两边的规范化路径都是绝对路径、且不相同」时生成;两侧都去掉尾部分隔符后比较,
38
+ * 避免 /opt/.dsh/ 与 /opt/.dsh 被判成不同而白跑一次全量替换。前缀匹配仍由 applyPrefixMappings
39
+ * 负责(必须落在段边界),所以 /opt/.dsh 不会误伤 /opt/.dsh-extra。
40
+ *
41
+ * @returns 映射规则;不需要重定基(缺 sourceHome / 相同 / 相对路径)→ undefined
42
+ */
43
+ export declare function rebaseMapping(sourceHome: string | undefined, localHome: string): PathMapping | undefined;
44
+ /**
45
+ * 会话单元版本查询:探针的单元键(`projectKey/会话目录`)与计划项单元 id
46
+ * (`sessions:projectKey/会话目录`)是两个键空间,必须两种形态都试。
47
+ *
48
+ * 为什么不在探针里直接带上适配器前缀:探针只认「分区内的相对路径」,它不知道也不该知道
49
+ * 哪个适配器在消费它(core 也不得 import 会话字节工具)。键空间对齐因此落在计划生成期。
50
+ */
51
+ export declare function sessionUnitVersion(byUnit: ReadonlyMap<string, number>, unitId: string): number | undefined;
52
+ /** 路径映射合并:只保留「已解析」(newPrefix 非空)的映射用于执行期数据改写;
53
+ * 未解决项(newPrefix 空)保留在 plan.items 里供 UI 提示,但不参与数据映射。 */
54
+ export declare function mergePathMappings(items: PlanItem[], userMappings: PathMapping[]): PathMapping[];
55
+ /**
56
+ * 把映射应用到分区数据(PathMapper 先行:只替换匹配前缀的字符串叶值)。
57
+ *
58
+ * `fileCollectionIds` 里的分区一律跳过:`FileCollectionAdapter` 把 `relativePath` 当落盘路径用,
59
+ * 它必须逐字保留(issue #45 ④:改写会话文件的相对路径会让目标机下次启动直接报
60
+ * `corrupt session log`;pluginFiles/skills 等同理——丢位置等于丢归属)。
61
+ * 映射在这些分区上**没有任何合法作用**:文件内容存的是字节(`Uint8Array` 叶子),不会被改。
62
+ */
63
+ export declare function applyMappingsToSections(sections: Map<SectionId, unknown>, mappings: PathMapping[], fileCollectionIds?: ReadonlySet<string>): void;
64
+ /** 路径形态判定:跨平台盘符/UNIX 冲突 → platformMismatch;其余绝对路径 → missing(需映射) */
65
+ export declare function detectPathIssues(sourcePlatform: string, targetPlatform: string, sections: Map<SectionId, unknown>): PathIssue[];
66
+ export declare function judgePath(p: string, sourcePlatform: string, targetPlatform: string): PathIssue;
67
+ /**
68
+ * 凭据计划项(MissingSecret / Skip)在**导入路径**上的唯一生成点。
69
+ *
70
+ * ref 来源取**并集**:① credentialsStatus 里 configured=true 的 ref(源机声明「有」,
71
+ * 但普通备份不带值);② 宿主解密出的归档 ref(`opts.decryptedCredentials`)——
72
+ * 加密备份携带的是 `.credentials.yaml` **原文**,可能含未被任何 settings namespace 引用的 ref
73
+ * (如其它插件自用的 key),只认 ① 会让这些值静默丢掉(真机反馈:导入密钥没生效)。
74
+ *
75
+ * 规则(与同步侧 sync-engine.appendCredentialPlanItems 同口径:**值的有无**优先于本机状态):
76
+ * - **有值** → MissingSecret「随加密备份恢复」:用户显式勾了「导出密钥」,导入即写回;
77
+ * **不因本机已有而跳过**(跳过 = 用户以为密钥导入了、其实没写)。
78
+ * - **无值**(只有 ref 名)→ 本机已配置 → Skip(保留本机值、不再索要补录,用户报告
79
+ * 「已有的重复密钥也会提示」);本机没有 → MissingSecret「需要补录」。
80
+ *
81
+ * 凭据值不可回读,因此无值分支一律不覆盖本机已有值。
82
+ *
83
+ * @param decrypted 归档里解出的 ref→值(仅内存;undefined = 未提供密码/无凭据载荷)
84
+ * @param isConfiguredLocally 目标机是否已配置该 ref(读不到 → false,保守按需补录处理)
85
+ */
86
+ /**
87
+ * 凭据值「可用」的**唯一判据**(t14 / core-F2 修):必须是非空字符串。
88
+ *
89
+ * 此前三处各判各的 —— 计划期用 `Map.has()`(空串也算「有值」)、执行期用 `value !== ''`、
90
+ * 结果期又用 `Map.has()`。于是「归档里该 ref 的值为空串」时:计划项说「随加密备份恢复」,
91
+ * 执行静默跳过,结果既不报缺失还把该 ref 算成「已从归档写回」—— 用户看到「导入完成」,
92
+ * 密钥其实没写进去(静默 no-op)。三处必须同源,否则该缺陷会再次以另一种形态复发。
93
+ */
94
+ export declare function usableSecretValue(decrypted: ReadonlyMap<string, string> | undefined, inputs: Record<string, string> | undefined, ref: string): boolean;
95
+ export declare function buildCredentialPlanItems(items: PlanItem[], sections: Map<SectionId, unknown>, decrypted: Map<string, string> | undefined, msg: MsgFunc, isConfiguredLocally: (ref: string) => Promise<boolean>): Promise<void>;
96
+ /**
97
+ * 按删除墓碑过滤计划项(F4):
98
+ * - 条目级:插件(Install/Update/Conflict)、技能(skills 文件)、文件类条目(agentPresets 等)命中墓碑 → 剔除;
99
+ * - 分区级:墓碑 kind='section' 且 id=分区 id → 整分区跳过(仅记一条 skipped)。
100
+ * 返回过滤后的 items 与被跳过清单(纯函数:不读文件、不改入参)。
101
+ */
102
+ export declare function applyTombstoneFilter(items: PlanItem[], tombstones: Tombstone[]): {
103
+ items: PlanItem[];
104
+ skipped: SkippedTombstone[];
105
+ };
106
+ /** PlanItem → 墓碑键({kind,id})映射;非「将写入」的条目(Skip 等)与配置类项返回 null 不过滤 */
107
+ export declare function tombstoneKeyForItem(item: PlanItem): {
108
+ kind: TombstoneKind;
109
+ id: string;
110
+ } | null;