dsh-config-manager 0.1.12 → 0.1.13

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 (107) hide show
  1. package/README.md +30 -0
  2. package/README.zh-CN.md +30 -0
  3. package/lib/adapters/test-helpers.d.ts +4 -2
  4. package/lib/adapters/test-helpers.js +11 -3
  5. package/lib/adapters/test-helpers.js.map +1 -1
  6. package/lib/cli/index.d.ts +44 -0
  7. package/lib/cli/index.js +230 -0
  8. package/lib/cli/index.js.map +1 -0
  9. package/lib/client.d.ts +260 -5
  10. package/lib/client.js +2519 -820
  11. package/lib/client.js.map +1 -1
  12. package/lib/core/analyzer.d.ts +18 -1
  13. package/lib/core/analyzer.js +29 -0
  14. package/lib/core/analyzer.js.map +1 -1
  15. package/lib/core/backup.d.ts +3 -1
  16. package/lib/core/backup.js +70 -0
  17. package/lib/core/backup.js.map +1 -1
  18. package/lib/core/exporter.d.ts +11 -0
  19. package/lib/core/exporter.js +11 -0
  20. package/lib/core/exporter.js.map +1 -1
  21. package/lib/core/importer.d.ts +3 -0
  22. package/lib/core/importer.js +1 -0
  23. package/lib/core/importer.js.map +1 -1
  24. package/lib/core/index.d.ts +1 -0
  25. package/lib/core/index.js +1 -0
  26. package/lib/core/index.js.map +1 -1
  27. package/lib/core/restore.d.ts +92 -0
  28. package/lib/core/restore.js +505 -0
  29. package/lib/core/restore.js.map +1 -0
  30. package/lib/core/run-registry.d.ts +65 -0
  31. package/lib/core/run-registry.js +120 -0
  32. package/lib/core/run-registry.js.map +1 -0
  33. package/lib/core/types.d.ts +20 -0
  34. package/lib/core/types.js.map +1 -1
  35. package/lib/index.d.ts +45 -0
  36. package/lib/index.js +442 -8
  37. package/lib/index.js.map +1 -1
  38. package/lib/sync/fs.d.ts +18 -0
  39. package/lib/sync/fs.js +47 -0
  40. package/lib/sync/fs.js.map +1 -0
  41. package/lib/sync/git/git-transport.d.ts +87 -0
  42. package/lib/sync/git/git-transport.js +333 -0
  43. package/lib/sync/git/git-transport.js.map +1 -0
  44. package/lib/sync/layout.d.ts +29 -0
  45. package/lib/sync/layout.js +166 -0
  46. package/lib/sync/layout.js.map +1 -0
  47. package/lib/sync/sync-config.d.ts +16 -0
  48. package/lib/sync/sync-config.js +67 -0
  49. package/lib/sync/sync-config.js.map +1 -0
  50. package/lib/sync/sync-engine.d.ts +96 -0
  51. package/lib/sync/sync-engine.js +236 -0
  52. package/lib/sync/sync-engine.js.map +1 -0
  53. package/lib/sync/sync-state.d.ts +31 -0
  54. package/lib/sync/sync-state.js +103 -0
  55. package/lib/sync/sync-state.js.map +1 -0
  56. package/lib/sync/transport.d.ts +50 -0
  57. package/lib/sync/transport.js +22 -0
  58. package/lib/sync/transport.js.map +1 -0
  59. package/package.json +4 -1
  60. package/src/adapters/test-helpers.ts +10 -3
  61. package/src/cli/index.ts +278 -0
  62. package/src/client/ConfigManagerSection.tsx +49 -14
  63. package/src/client/api.ts +53 -5
  64. package/src/client/client-types.ts +0 -6
  65. package/src/client/common/ProgressBar.tsx +31 -19
  66. package/src/client/common/progress-view-edge.test.ts +60 -0
  67. package/src/client/common/progress-view.test.ts +79 -0
  68. package/src/client/common/progress-view.ts +94 -0
  69. package/src/client/config-manager.module.css +99 -20
  70. package/src/client/export/ExportView.tsx +63 -49
  71. package/src/client/import/ImportWizardView.tsx +107 -66
  72. package/src/client/index.ts +22 -0
  73. package/src/client/locales.ts +53 -0
  74. package/src/client/run-store.test.ts +500 -0
  75. package/src/client/run-store.ts +772 -0
  76. package/src/client/snapshots/SnapshotsPanel.tsx +240 -0
  77. package/src/client/sync/SyncSettingsView.tsx +261 -0
  78. package/src/client/sync/sync-api.test.ts +117 -0
  79. package/src/client/sync/sync-api.ts +132 -0
  80. package/src/client/sync/sync-locales.ts +77 -0
  81. package/src/client/sync/sync-view.test.ts +211 -0
  82. package/src/client/sync/sync-view.ts +202 -0
  83. package/src/core/analyzer.ts +50 -1
  84. package/src/core/backup.ts +78 -2
  85. package/src/core/exporter.ts +21 -0
  86. package/src/core/importer.ts +4 -0
  87. package/src/core/index.ts +5 -0
  88. package/src/core/restore.ts +587 -0
  89. package/src/core/run-progress.test.ts +237 -0
  90. package/src/core/run-registry.test.ts +139 -0
  91. package/src/core/run-registry.ts +166 -0
  92. package/src/core/smoke.test.ts +5 -0
  93. package/src/core/types.ts +22 -0
  94. package/src/index.ts +476 -8
  95. package/src/security/security.test.ts +5 -0
  96. package/src/sync/fs.ts +46 -0
  97. package/src/sync/git/git-transport.test.ts +456 -0
  98. package/src/sync/git/git-transport.ts +390 -0
  99. package/src/sync/layout.test.ts +226 -0
  100. package/src/sync/layout.ts +183 -0
  101. package/src/sync/sync-config.ts +72 -0
  102. package/src/sync/sync-engine.test.ts +299 -0
  103. package/src/sync/sync-engine.ts +318 -0
  104. package/src/sync/sync-state.test.ts +118 -0
  105. package/src/sync/sync-state.ts +123 -0
  106. package/src/sync/transport.test.ts +110 -0
  107. package/src/sync/transport.ts +73 -0
@@ -67,6 +67,19 @@ interface AnalyzedBundle extends Bundle {
67
67
  adapterIssues: string[];
68
68
  }
69
69
 
70
+ /** m1:每完成一个计划项的进度回调信息(Host 侧 run 状态更新用) */
71
+ export interface PlanItemProgress {
72
+ adapter: SectionId;
73
+ /** 已处理计划项序号(1 起,含 skip/warning 信息项) */
74
+ index: number;
75
+ /** 将实际执行的计划项总数(APPLY_ORDER 内各项合计) */
76
+ total: number;
77
+ /** 该项最终状态(ok/skipped/warning/failed) */
78
+ status?: ExecutedItem['status'];
79
+ /** 当前计划项 id(非敏感) */
80
+ detail?: string;
81
+ }
82
+
70
83
  export class Analyzer {
71
84
  private readonly ctx: HostContext;
72
85
  private readonly adapters: ConfigAdapter[];
@@ -361,7 +374,14 @@ export class Analyzer {
361
374
  async executeImportPlan(
362
375
  zipPath: string,
363
376
  plan: ImportPlan,
364
- opts: { confirm?: boolean; secretInputs?: Record<string, string>; decryptedCredentials?: Map<string, string>; rollbackOnError?: boolean } = {},
377
+ opts: {
378
+ confirm?: boolean;
379
+ secretInputs?: Record<string, string>;
380
+ decryptedCredentials?: Map<string, string>;
381
+ rollbackOnError?: boolean;
382
+ /** m1:每完成一个计划项调用(真实进度埋点;不传则无埋点) */
383
+ onItem?: (info: PlanItemProgress) => void;
384
+ } = {},
365
385
  ): Promise<ImportResult> {
366
386
  const bundle = await this.loadBundle(zipPath);
367
387
  const analyzed = await this.analyzeBundle(bundle);
@@ -405,12 +425,27 @@ export class Analyzer {
405
425
  list.push(item);
406
426
  byAdapter.set(item.adapter, list);
407
427
  }
428
+ const totalItems = APPLY_ORDER.reduce((sum, id) => sum + (byAdapter.get(id)?.length ?? 0), 0);
429
+ let itemIndex = 0;
408
430
 
409
431
  for (const adapterId of APPLY_ORDER) {
410
432
  const adapter = this.adapters.find((a) => a.id === adapterId);
411
433
  if (!adapter) continue;
412
434
  for (const item of byAdapter.get(adapterId) ?? []) {
413
435
  const outcome = await this.applyOne(adapter, item, importCtx);
436
+ // m1 埋点:每完成一个计划项上报(真实进度;onItem 抛错不得中断导入)
437
+ itemIndex += 1;
438
+ try {
439
+ opts.onItem?.({
440
+ adapter: item.adapter,
441
+ index: itemIndex,
442
+ total: totalItems,
443
+ status: outcome.executed.status,
444
+ detail: item.id,
445
+ });
446
+ } catch {
447
+ // 埋点回调失败不影响导入执行(进度是尽力而为)
448
+ }
414
449
  executed.push(outcome.executed);
415
450
  // 仅硬失败计入 anyFailed(warning 属非致命:目标不可达等,不触发回滚,§34.17)
416
451
  if (outcome.executed.status === 'failed') anyFailed = true;
@@ -429,6 +464,8 @@ export class Analyzer {
429
464
  store: this.snapshotStore,
430
465
  adapters: this.adapters,
431
466
  });
467
+ // M1:回滚完成 → 快照标记 rolled-back(元数据写失败只告警,不影响回滚结论)
468
+ await this.markSnapshotStatus(snapshot.id, 'rolled-back');
432
469
  this.ctx.log.warn(`导入失败,已回滚(${rollbackReport.full ? '完整' : '部分'})`, {
433
470
  failed: executed.filter((e) => e.status === 'failed').map((e) => e.itemId),
434
471
  });
@@ -462,6 +499,9 @@ export class Analyzer {
462
499
  .filter((s) => !importCtx.decryptedCredentials?.has(s.ref) && !importCtx.secretInputs[s.ref])
463
500
  .map((s) => s.ref);
464
501
 
502
+ // M1:导入成功 → 快照标记 done(元数据写失败只告警,不改变导入结论)
503
+ await this.markSnapshotStatus(snapshot.id, 'done');
504
+
465
505
  return {
466
506
  ok: true, // 单项失败已如实记录在 executed;无未捕获异常即完成
467
507
  executed,
@@ -473,6 +513,15 @@ export class Analyzer {
473
513
  };
474
514
  }
475
515
 
516
+ /** 快照状态标记(M1):成功→done / 失败回滚→rolled-back。元数据写失败只告警不抛错。 */
517
+ private async markSnapshotStatus(id: string, status: 'done' | 'rolled-back'): Promise<void> {
518
+ try {
519
+ await this.snapshotStore.updateStatus(id, status);
520
+ } catch (err) {
521
+ this.ctx.log.warn(`快照 ${id} 状态标记 ${status} 失败: ${err instanceof Error ? err.message : String(err)}`);
522
+ }
523
+ }
524
+
476
525
  private async applyOne(
477
526
  adapter: ConfigAdapter,
478
527
  item: PlanItem,
@@ -12,13 +12,67 @@ import { parseJsonSafe } from '../utils/json.ts';
12
12
  import { sha256Hex } from '../utils/hashing.ts';
13
13
  import type { SectionId } from '../schema/types.ts';
14
14
  import type {
15
- ConfigAdapter, HostContext, ImportPlan, PlanItem, Snapshot,
16
- SnapshotEntry, SnapshotStore, SnapshotTarget,
15
+ ConfigAdapter, HostContext, HostFileBackup, ImportPlan, PlanItem, Snapshot,
16
+ SnapshotEntry, SnapshotStatus, SnapshotStore, SnapshotTarget,
17
17
  } from './types.ts';
18
18
 
19
19
  /** 导入将实际写入的目标 kinds(这些项才需要快照) */
20
20
  const EXECUTABLE_KINDS = new Set(['Create', 'Update', 'Install', 'MissingSecret', 'MissingDependency']);
21
21
 
22
+ /**
23
+ * 需要整文件备份的宿主关键文件(相对 $DSH_HOME):
24
+ * - settings.yaml(DSH 配置主存储;不存在时探测 settings.json)
25
+ * - cordis.patch.yml(用户 patch 层)
26
+ * - profiles/<profile>/cordis.patch.yml(profile patch 层,宿主暴露 profile 时)
27
+ */
28
+ const HOST_FILE_CANDIDATES: ReadonlyArray<{ relPath: string }> = [
29
+ { relPath: 'settings.yaml' },
30
+ { relPath: 'settings.json' },
31
+ { relPath: 'cordis.patch.yml' },
32
+ ];
33
+
34
+ /**
35
+ * 宿主整文件备份(M1):探测存在性,存在的文件字节进 blobs Map 由 store.save 落盘,
36
+ * 全部候选(含 existed:false)登记进 hostFileBackups,供 M2 restore 整文件还原。
37
+ */
38
+ async function backupHostFiles(
39
+ ctx: HostContext,
40
+ blobs: Map<string, Uint8Array>,
41
+ ): Promise<HostFileBackup[]> {
42
+ const candidates = [...HOST_FILE_CANDIDATES];
43
+ if (ctx.profile !== undefined && ctx.profile !== '') {
44
+ candidates.push({ relPath: `profiles/${ctx.profile}/cordis.patch.yml` });
45
+ }
46
+
47
+ const backups: HostFileBackup[] = [];
48
+ for (const { relPath } of candidates) {
49
+ // settings.yaml 与 settings.json 互斥:主存储存在时不再探测 json 备选
50
+ if (relPath === 'settings.json' && backups.some((b) => b.relPath === 'settings.yaml' && b.existed)) {
51
+ continue;
52
+ }
53
+ let existed = false;
54
+ try {
55
+ existed = await ctx.fs.exists(relPath);
56
+ } catch (err) {
57
+ ctx.log.warn(`快照探测宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
58
+ existed = false;
59
+ }
60
+ if (!existed) {
61
+ backups.push({ relPath, blobPath: '', existed: false });
62
+ continue;
63
+ }
64
+ const blobPath = `blobs/host/${crypto.randomUUID()}`;
65
+ try {
66
+ blobs.set(blobPath, await ctx.fs.readFile(relPath));
67
+ backups.push({ relPath, blobPath, existed: true });
68
+ } catch (err) {
69
+ ctx.log.warn(`快照读取宿主文件失败 ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
70
+ backups.push({ relPath, blobPath: '', existed: false });
71
+ }
72
+ }
73
+ return backups;
74
+ }
75
+
22
76
  /** 文件类分区的目标基准目录(相对 homeDir;pluginFiles 的 ref 已是完整相对路径) */
23
77
  const FILE_BASES: Partial<Record<SectionId, string>> = {
24
78
  skills: 'skills',
@@ -152,11 +206,25 @@ export async function createSnapshot(opts: CreateSnapshotOptions): Promise<Snaps
152
206
  }
153
207
  }
154
208
 
209
+ // 3) 宿主整文件备份(M1):settings.yaml/settings.json + 用户/ profile 层 cordis.patch.yml
210
+ const hostFileBackups = await backupHostFiles(ctx, blobs);
211
+
212
+ // 4) 导入前插件清单(M2 restore 撤销插件对比基准;读取失败不阻断快照)
213
+ let beforePlugins: Snapshot['beforePlugins'] = [];
214
+ try {
215
+ beforePlugins = await ctx.plugins.listInstalled();
216
+ } catch (err) {
217
+ ctx.log.warn(`快照登记插件清单失败: ${err instanceof Error ? err.message : String(err)}`);
218
+ }
219
+
155
220
  const snapshot: Snapshot = {
156
221
  id: crypto.randomUUID(),
157
222
  createdAt: new Date().toISOString(),
158
223
  sourceZip,
159
224
  entries,
225
+ status: 'pending',
226
+ beforePlugins,
227
+ hostFileBackups,
160
228
  };
161
229
  // file 条目登记 snapshotId,回滚读 blob 时定位快照目录
162
230
  for (const entry of snapshot.entries) {
@@ -209,4 +277,12 @@ export class FileSnapshotStore implements SnapshotStore {
209
277
  if (!target.startsWith(dir)) throw new Error(`快照 blob 路径越界: ${blobPath}`);
210
278
  return fs.readFile(target);
211
279
  }
280
+
281
+ /** 标记快照生命周期状态:重写 <dir>/<id>/snapshot.json(保留其余字段)。 */
282
+ async updateStatus(id: string, status: SnapshotStatus): Promise<void> {
283
+ const file = path.join(this.snapshotDir(id), 'snapshot.json');
284
+ const snapshot = parseJsonSafe(await fs.readFile(file, 'utf8')) as Snapshot;
285
+ snapshot.status = status;
286
+ await fs.writeFile(file, JSON.stringify(snapshot, null, 2));
287
+ }
212
288
  }
@@ -20,6 +20,15 @@ import type {
20
20
  ExportSection, HostContext, SecretScanner, SensitiveHit,
21
21
  } from './types.ts';
22
22
 
23
+ /** m1:每导出一个分区前的进度回调信息(Host 侧 run 状态更新用;section = adapter id) */
24
+ export interface SectionProgress {
25
+ section: string;
26
+ /** 当前分区序号(1 起) */
27
+ index: number;
28
+ /** 选中分区总数 */
29
+ total: number;
30
+ }
31
+
23
32
  export interface ExporterOptions {
24
33
  ctx: HostContext;
25
34
  adapters: ConfigAdapter[];
@@ -30,6 +39,8 @@ export interface ExporterOptions {
30
39
  /** 插件自身版本(manifest.exporter.version) */
31
40
  exporterVersion?: string;
32
41
  now?: () => Date;
42
+ /** m1:每导出一个分区前调用(真实进度埋点;不传则无埋点) */
43
+ onSection?: (info: SectionProgress) => void;
33
44
  }
34
45
 
35
46
  /** 缺省 SecretScanner:递归黑名单字段剥离(字段名大小写不敏感;二进制/Uint8Array 原样跳过) */
@@ -76,6 +87,7 @@ export class Exporter {
76
87
  private readonly encryption: EncryptionProvider | null;
77
88
  private readonly exporterVersion: string;
78
89
  private readonly now: () => Date;
90
+ private readonly onSection: ((info: SectionProgress) => void) | undefined;
79
91
 
80
92
  constructor(opts: ExporterOptions) {
81
93
  this.ctx = opts.ctx;
@@ -84,6 +96,7 @@ export class Exporter {
84
96
  this.encryption = opts.encryption ?? null;
85
97
  this.exporterVersion = opts.exporterVersion ?? '0.1.0';
86
98
  this.now = opts.now ?? (() => new Date());
99
+ this.onSection = opts.onSection;
87
100
  }
88
101
 
89
102
  /**
@@ -108,8 +121,16 @@ export class Exporter {
108
121
  const included: ExportReport['included'] = [];
109
122
  const excluded: SectionId[] = this.adapters.filter((a) => !selected.includes(a.id)).map((a) => a.id);
110
123
 
124
+ // m1 埋点:每导出一个分区前上报真实进度(onSection 抛错不得中断导出)
125
+ let sectionIndex = 0;
111
126
  for (const adapter of this.adapters) {
112
127
  if (!selected.includes(adapter.id)) continue;
128
+ sectionIndex += 1;
129
+ try {
130
+ this.onSection?.({ section: adapter.id, index: sectionIndex, total: selected.length });
131
+ } catch {
132
+ // 埋点回调失败不影响导出本身(进度是尽力而为)
133
+ }
113
134
  let section: ExportSection;
114
135
  try {
115
136
  section = await adapter.export(this.ctx, options);
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { ZipArchive, ZipSafetyLimits } from '../utils/zip.ts';
12
12
  import { Analyzer } from './analyzer.ts';
13
+ import type { PlanItemProgress } from './analyzer.ts';
13
14
  import type {
14
15
  ConfigAdapter, HostContext, ImportAnalysis, ImportDecisions, ImportPlan,
15
16
  ImportResult, SnapshotStore,
@@ -35,6 +36,8 @@ export interface ExecuteOptions {
35
36
  decryptedCredentials?: Map<string, string>;
36
37
  /** 任一项失败立即整体回滚(默认 false:单项失败如实记录并继续其余项) */
37
38
  rollbackOnError?: boolean;
39
+ /** m1:每完成一个计划项的进度回调(透传给 Analyzer;不传则无埋点) */
40
+ onItem?: (info: PlanItemProgress) => void;
38
41
  }
39
42
 
40
43
  export class Importer {
@@ -61,6 +64,7 @@ export class Importer {
61
64
  secretInputs: opts.secretInputs,
62
65
  decryptedCredentials: opts.decryptedCredentials,
63
66
  rollbackOnError: opts.rollbackOnError,
67
+ onItem: opts.onItem,
64
68
  });
65
69
  }
66
70
  }
package/src/core/index.ts CHANGED
@@ -16,6 +16,11 @@ export {
16
16
  type CreateSnapshotOptions, type FileSnapshotStoreOptions,
17
17
  } from './backup.ts';
18
18
  export { rollback, type RollbackOptions } from './rollback.ts';
19
+ export {
20
+ planRestore, restore, listSnapshots,
21
+ type RestoreAction, type RestoreActionKind, type RestoreOptions,
22
+ type RestorePlan, type RestoreReport, type SnapshotMeta,
23
+ } from './restore.ts';
19
24
  export {
20
25
  computeCompatibility, describeCompatibility, describeSchemaStatus,
21
26
  validateSections,