dsh-config-manager 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/lib/adapters/providers.js +13 -3
- package/lib/adapters/providers.js.map +1 -1
- package/lib/adapters/settings.d.ts +5 -1
- package/lib/adapters/settings.js +24 -3
- package/lib/adapters/settings.js.map +1 -1
- package/lib/adapters/test-helpers.d.ts +21 -3
- package/lib/adapters/test-helpers.js +29 -4
- package/lib/adapters/test-helpers.js.map +1 -1
- package/lib/adapters/workspaces.js +14 -2
- package/lib/adapters/workspaces.js.map +1 -1
- package/lib/client.d.ts +2 -1
- package/lib/client.js +81 -78
- package/lib/client.js.map +1 -1
- package/lib/core/analyzer.js +9 -2
- package/lib/core/analyzer.js.map +1 -1
- package/lib/core/rollback.js +4 -2
- package/lib/core/rollback.js.map +1 -1
- package/lib/core/types.d.ts +5 -1
- package/lib/core/types.js.map +1 -1
- package/lib/profiles/profile-manager.js +3 -1
- package/lib/profiles/profile-manager.js.map +1 -1
- package/lib/ui/report.js +8 -4
- package/lib/ui/report.js.map +1 -1
- package/lib/ui/types.d.ts +2 -1
- package/lib/ui/types.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/providers.test.ts +1 -0
- package/src/adapters/providers.ts +12 -3
- package/src/adapters/roundtrip.test.ts +2 -0
- package/src/adapters/settings.test.ts +11 -4
- package/src/adapters/settings.ts +22 -3
- package/src/adapters/test-helpers.ts +27 -3
- package/src/adapters/ui.test.ts +1 -0
- package/src/adapters/workspaces.test.ts +27 -0
- package/src/adapters/workspaces.ts +13 -2
- package/src/client/import/ImportWizardView.tsx +7 -4
- package/src/core/analyzer.ts +11 -2
- package/src/core/rollback.ts +4 -2
- package/src/core/types.ts +5 -2
- package/src/profiles/profile-manager.test.ts +2 -0
- package/src/profiles/profile-manager.ts +3 -1
- package/src/ui/report.ts +6 -4
- package/src/ui/types.ts +2 -1
|
@@ -77,3 +77,30 @@ test('workspaces: applyItem 写入映射后数据(PathMapper 先行)', async
|
|
|
77
77
|
const items2 = await adapter.analyzeImport(mappedData, makeImportContext(dst, sections));
|
|
78
78
|
assert.equal(items2.find((i) => i.id === 'workspace:ws-ops')?.kind, 'Skip');
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
test('workspaces: applyItem 写入失败(路径 realpath ENOENT)→ warning 非致命,不抛错', async () => {
|
|
82
|
+
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
83
|
+
src.workspace.records.set('ws-ops', {
|
|
84
|
+
id: 'ws-ops', path: 'C:\\Users\\alice\\projects\\ops', title: 'OpsFlow', sessionIds: [],
|
|
85
|
+
});
|
|
86
|
+
const adapter = new WorkspacesAdapter();
|
|
87
|
+
const exported = await adapter.export(src, { includeSecrets: false });
|
|
88
|
+
const sections = new Map([['workspaces', exported.data]]);
|
|
89
|
+
const dst = makeContext('linux', '/home/bob');
|
|
90
|
+
// 模拟目标端工作区服务对不存在路径 realpath 失败(dsh-workspace 真实行为)
|
|
91
|
+
const original = dst.workspace.writeRecord.bind(dst.workspace);
|
|
92
|
+
dst.workspace.writeRecord = async () => {
|
|
93
|
+
throw new Error("ENOENT: no such file or directory, realpath '/home/bob/projects/ops'");
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
|
|
97
|
+
const create = items.find((i) => i.id === 'workspace:ws-ops') as PlanItem;
|
|
98
|
+
const r = await adapter.applyItem(create, makeImportContext(dst, sections));
|
|
99
|
+
assert.equal(r.ok, false, '写入失败');
|
|
100
|
+
assert.equal(r.warning, true, '应为非致命警告(§34.17:不拖垮整体导入)');
|
|
101
|
+
assert.match(r.message ?? '', /未能写入|realpath/);
|
|
102
|
+
} finally {
|
|
103
|
+
dst.workspace.writeRecord = original;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
@@ -69,8 +69,19 @@ export class WorkspacesAdapter implements ConfigAdapter<WorkspacesSection> {
|
|
|
69
69
|
const data = ctx.sections.get('workspaces') as WorkspacesSection | undefined;
|
|
70
70
|
const rec = data?.workspaces.find((r) => r.id === ref);
|
|
71
71
|
if (!rec) return { ok: false, message: `导入数据缺少工作区 ${ref}` };
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
try {
|
|
73
|
+
await ctx.target.workspace.writeRecord(rec);
|
|
74
|
+
return { ok: true };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// 目标端无法写入(如路径 realpath 失败/目录不存在)→ 非致命警告(§34.17),
|
|
77
|
+
// 不触发整体回滚——否则一个失效路径会拖垮已成功导入的其余配置。
|
|
78
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
warning: true,
|
|
82
|
+
message: `工作区 ${ref} 未能写入:${msg}(需映射路径或先在目标创建目录)`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
async validate(data: WorkspacesSection): Promise<ValidationResult> {
|
|
@@ -312,7 +312,10 @@ export function ImportWizardView({ api, t }: ImportWizardViewProps) {
|
|
|
312
312
|
)
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
|
|
315
|
+
// 流程阶段页只在 wizard.step === 'preview' 时渲染:execute 开始后 step 变
|
|
316
|
+
// importing/result,若 phase 仍是 confirm,必须让位给导入中/结果页(否则点「导入」
|
|
317
|
+
// 无反应——confirm 页一直挡着,要回退一步才露出结果)。
|
|
318
|
+
if (phase === 'conflicts' && step === 'preview') {
|
|
316
319
|
if (conflictCollector === null || snapshot.plan === null) return null
|
|
317
320
|
return (
|
|
318
321
|
<div className={css.viewBody}>
|
|
@@ -328,7 +331,7 @@ export function ImportWizardView({ api, t }: ImportWizardViewProps) {
|
|
|
328
331
|
)
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
if (phase === 'path-mapping') {
|
|
334
|
+
if (phase === 'path-mapping' && step === 'preview') {
|
|
332
335
|
const issues = snapshot.analysis?.pathIssues ?? []
|
|
333
336
|
return (
|
|
334
337
|
<div className={css.viewBody}>
|
|
@@ -342,7 +345,7 @@ export function ImportWizardView({ api, t }: ImportWizardViewProps) {
|
|
|
342
345
|
)
|
|
343
346
|
}
|
|
344
347
|
|
|
345
|
-
if (phase === 'secrets') {
|
|
348
|
+
if (phase === 'secrets' && step === 'preview') {
|
|
346
349
|
const missing = snapshot.plan?.missingSecrets ?? []
|
|
347
350
|
return (
|
|
348
351
|
<div className={css.viewBody}>
|
|
@@ -356,7 +359,7 @@ export function ImportWizardView({ api, t }: ImportWizardViewProps) {
|
|
|
356
359
|
)
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
if (phase === 'confirm') {
|
|
362
|
+
if (phase === 'confirm' && step === 'preview') {
|
|
360
363
|
return (
|
|
361
364
|
<div className={css.viewBody}>
|
|
362
365
|
<Card className={css.optionsCard}>
|
package/src/core/analyzer.ts
CHANGED
|
@@ -412,6 +412,7 @@ export class Analyzer {
|
|
|
412
412
|
for (const item of byAdapter.get(adapterId) ?? []) {
|
|
413
413
|
const outcome = await this.applyOne(adapter, item, importCtx);
|
|
414
414
|
executed.push(outcome.executed);
|
|
415
|
+
// 仅硬失败计入 anyFailed(warning 属非致命:目标不可达等,不触发回滚,§34.17)
|
|
415
416
|
if (outcome.executed.status === 'failed') anyFailed = true;
|
|
416
417
|
if (outcome.needsRestart) needsRestart = true;
|
|
417
418
|
if (outcome.warning) warnings.push(outcome.warning);
|
|
@@ -477,7 +478,13 @@ export class Analyzer {
|
|
|
477
478
|
item: PlanItem,
|
|
478
479
|
ctx: ImportContext,
|
|
479
480
|
): Promise<{ executed: ExecutedItem; needsRestart: boolean; warning?: string }> {
|
|
480
|
-
if (
|
|
481
|
+
if (
|
|
482
|
+
item.kind === 'Skip' ||
|
|
483
|
+
item.kind === 'Warning' ||
|
|
484
|
+
item.kind === 'MissingDependency' ||
|
|
485
|
+
(item.kind === 'Conflict' && item.conflict?.resolution !== 'useImported')
|
|
486
|
+
) {
|
|
487
|
+
// Warning / MissingDependency 是信息项(依赖缺失、需人工注意),不调用 applyItem。
|
|
481
488
|
return { executed: { itemId: item.id, status: 'skipped' }, needsRestart: false };
|
|
482
489
|
}
|
|
483
490
|
if (item.kind === 'Error') {
|
|
@@ -493,9 +500,11 @@ export class Analyzer {
|
|
|
493
500
|
}
|
|
494
501
|
try {
|
|
495
502
|
const result: ApplyResult = await adapter.applyItem(item, ctx);
|
|
503
|
+
const status = result.ok ? 'ok' : (result.warning === true ? 'warning' : 'failed');
|
|
496
504
|
return {
|
|
497
|
-
executed: { itemId: item.id, status
|
|
505
|
+
executed: { itemId: item.id, status, message: result.message },
|
|
498
506
|
needsRestart: result.needsRestart === true,
|
|
507
|
+
warning: result.warning === true ? `${item.id}: ${result.message ?? item.description}` : undefined,
|
|
499
508
|
};
|
|
500
509
|
} catch (err) {
|
|
501
510
|
this.ctx.log.error(`应用计划项失败 ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
package/src/core/rollback.ts
CHANGED
|
@@ -67,8 +67,10 @@ async function compensateOne(
|
|
|
67
67
|
}
|
|
68
68
|
case 'patchLine': {
|
|
69
69
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// 引擎只管理 profile 的 cordis.patch.yml(backup.ts 的 patchLine 快照只记 lineId 作 ref,
|
|
71
|
+
// 不含文件编码)——回滚固定写回该文件;切勿把 lineId 当文件名(否则 patchPath 抛「仅支持管理」)。
|
|
72
|
+
const file = 'cordis.patch.yml';
|
|
73
|
+
const lineId = entry.ref;
|
|
72
74
|
await ctx.patchFile.applyPatchChanges(file, [
|
|
73
75
|
{ lineId, raw: entry.before, action: entry.before === null ? 'remove' : 'update' },
|
|
74
76
|
]);
|
package/src/core/types.ts
CHANGED
|
@@ -154,7 +154,9 @@ export interface PlanItem {
|
|
|
154
154
|
target?: SnapshotTarget; // 该项将修改的目标(快照登记用)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
/** applyItem 结果。warning=true 表示“未应用但属非致命”(如目标路径不可达/需人工映射),
|
|
158
|
+
* 引擎记为警告、不触发失败回滚(§34.17 单项失败不拖垮整体)。 */
|
|
159
|
+
export interface ApplyResult { ok: boolean; message?: string; needsRestart?: boolean; warning?: boolean; }
|
|
158
160
|
|
|
159
161
|
/* ---------------- 三段式输入输出 ---------------- */
|
|
160
162
|
|
|
@@ -187,7 +189,8 @@ export interface ImportPlan {
|
|
|
187
189
|
|
|
188
190
|
export interface ExecutedItem {
|
|
189
191
|
itemId: string;
|
|
190
|
-
|
|
192
|
+
/** warning = 未应用但非致命(目标不可达等),不计入失败、不触发回滚 */
|
|
193
|
+
status: 'ok' | 'skipped' | 'warning' | 'failed';
|
|
191
194
|
message?: string;
|
|
192
195
|
}
|
|
193
196
|
|
|
@@ -134,6 +134,7 @@ test('analyzeSwitch Preview + executeSwitch(confirm 安全阀 + Create 应用
|
|
|
134
134
|
await srcMgr.saveCurrent('work');
|
|
135
135
|
|
|
136
136
|
const dst = makeContext('linux', '/home/bob');
|
|
137
|
+
for (const n of NS) dst.settings.registered.add(n); // 目标已装同插件(注册但为空 → Create 初始化)
|
|
137
138
|
const dstMgr = new ProfileManager({ dataDir, ctx: dst, adapters, snapshotStore: new MemSnapshotStore() });
|
|
138
139
|
|
|
139
140
|
// 未确认 → 拒绝且零写入
|
|
@@ -250,6 +251,7 @@ test('exportProfile / importProfile 往返(完整验证)', async () => {
|
|
|
250
251
|
|
|
251
252
|
// 导入后的 profile 可切换到空目标
|
|
252
253
|
const dst = makeContext('linux', '/home/bob');
|
|
254
|
+
for (const n of NS) dst.settings.registered.add(n); // 目标已装同插件
|
|
253
255
|
const dstMgr = new ProfileManager({ dataDir, ctx: dst, adapters, snapshotStore: new MemSnapshotStore() });
|
|
254
256
|
const r = await dstMgr.executeSwitch('work-copy', { confirm: true });
|
|
255
257
|
assert.equal(r.ok, true);
|
|
@@ -510,9 +510,11 @@ export class ProfileManager {
|
|
|
510
510
|
}
|
|
511
511
|
try {
|
|
512
512
|
const result: ApplyResult = await adapter.applyItem(item, ctx);
|
|
513
|
+
const status = result.ok ? 'ok' : (result.warning === true ? 'warning' : 'failed');
|
|
513
514
|
return {
|
|
514
|
-
executed: { itemId: item.id, status
|
|
515
|
+
executed: { itemId: item.id, status, message: result.message },
|
|
515
516
|
needsRestart: result.needsRestart === true,
|
|
517
|
+
warning: result.warning === true ? `${item.id}: ${result.message ?? item.description}` : undefined,
|
|
516
518
|
};
|
|
517
519
|
} catch (err) {
|
|
518
520
|
this.ctx.log.error(`切换 Profile 应用计划项失败 ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
package/src/ui/report.ts
CHANGED
|
@@ -64,11 +64,12 @@ export function importSectionStats(executed: readonly ExecutedItem[]): ImportSec
|
|
|
64
64
|
const section = sectionFromItemId(e.itemId);
|
|
65
65
|
let stat = bySection.get(section);
|
|
66
66
|
if (!stat) {
|
|
67
|
-
stat = { section: section as SectionId, ok: 0, skipped: 0, failed: 0, items: [] };
|
|
67
|
+
stat = { section: section as SectionId, ok: 0, skipped: 0, warned: 0, failed: 0, items: [] };
|
|
68
68
|
bySection.set(section, stat);
|
|
69
69
|
}
|
|
70
70
|
if (e.status === 'ok') stat.ok += 1;
|
|
71
71
|
else if (e.status === 'skipped') stat.skipped += 1;
|
|
72
|
+
else if (e.status === 'warning') stat.warned += 1;
|
|
72
73
|
else stat.failed += 1;
|
|
73
74
|
stat.items.push(e);
|
|
74
75
|
}
|
|
@@ -83,12 +84,13 @@ export function renderImportReport(result: ImportResult): string {
|
|
|
83
84
|
const parts: string[] = [];
|
|
84
85
|
if (s.ok > 0) parts.push(`✓ ${s.ok} imported/restored`);
|
|
85
86
|
if (s.skipped > 0) parts.push(`- ${s.skipped} skipped`);
|
|
87
|
+
if (s.warned > 0) parts.push(`⚠ ${s.warned} need attention`);
|
|
86
88
|
if (s.failed > 0) parts.push(`✗ ${s.failed} failed`);
|
|
87
89
|
lines.push(`${s.section}: ${parts.join(' ') || 'no changes'}`);
|
|
88
|
-
//
|
|
90
|
+
// 失败/警告项给出可操作原因(§23)
|
|
89
91
|
for (const it of s.items) {
|
|
90
|
-
if (it.status === 'failed') {
|
|
91
|
-
lines.push(` Reason: ${it.message ?? '未知原因'}`);
|
|
92
|
+
if (it.status === 'failed' || it.status === 'warning') {
|
|
93
|
+
lines.push(` ${it.status === 'failed' ? 'Reason' : 'Note'}: ${it.message ?? '未知原因'}`);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
}
|
package/src/ui/types.ts
CHANGED
|
@@ -156,8 +156,9 @@ export interface ImportSectionStat {
|
|
|
156
156
|
section: SectionId;
|
|
157
157
|
ok: number;
|
|
158
158
|
skipped: number;
|
|
159
|
+
warned: number;
|
|
159
160
|
failed: number;
|
|
160
|
-
items: { itemId: string; status: 'ok' | 'skipped' | 'failed'; message?: string }[];
|
|
161
|
+
items: { itemId: string; status: 'ok' | 'skipped' | 'warning' | 'failed'; message?: string }[];
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
export type ImportResultAction = 'fixIssues' | 'viewDetails' | 'done';
|