dsh-config-manager 0.1.0 → 0.1.2
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 +38 -4
- package/README.zh-CN.md +38 -4
- package/lib/client.js +118 -95
- package/lib/client.js.map +1 -1
- package/lib/ui/flow.d.ts +17 -0
- package/lib/ui/flow.js +22 -0
- package/lib/ui/flow.js.map +1 -0
- package/package.json +5 -1
- package/src/client/import/ImportWizardView.tsx +18 -8
- package/src/core/smoke.test.ts +8 -5
- package/src/ui/flow.test.ts +37 -0
- package/src/ui/flow.ts +24 -0
package/lib/ui/flow.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入向导的 UI 流程阶段(覆盖在 wizard.step === 'preview' 之上)与导航。
|
|
3
|
+
*
|
|
4
|
+
* 注意:hasConflicts / hasPathIssues / hasSecrets 来自 Dry Run 的原始
|
|
5
|
+
* analysis/plan,流程中不会因用户已解决而重算——因此导航必须是「只前进」
|
|
6
|
+
* (按适用阶段有序列表推进),绝不能靠“当前阶段 != X 且 hasX”判定:
|
|
7
|
+
* 那会让已经过的阶段被重新命中,点「下一步」反而跳回上一步。
|
|
8
|
+
*/
|
|
9
|
+
/** 中间流程阶段(wizard.step 之外的 UI 层页面) */
|
|
10
|
+
export type FlowPhase = 'preview' | 'conflicts' | 'path-mapping' | 'secrets' | 'confirm';
|
|
11
|
+
/**
|
|
12
|
+
* 计算下一阶段:在 `list`(适用阶段的有序列表)中取 `from` 的下一项。
|
|
13
|
+
* - `from` 不在列表中(如来自 preview 页)→ 取第一项;
|
|
14
|
+
* - `from` 已是最后一项(confirm)→ 原地返回;
|
|
15
|
+
* - 绝不回退:已完成阶段不会因仍适用而被再次命中。
|
|
16
|
+
*/
|
|
17
|
+
export declare function nextFlowPhase(list: FlowPhase[], from: FlowPhase): FlowPhase;
|
package/lib/ui/flow.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入向导的 UI 流程阶段(覆盖在 wizard.step === 'preview' 之上)与导航。
|
|
3
|
+
*
|
|
4
|
+
* 注意:hasConflicts / hasPathIssues / hasSecrets 来自 Dry Run 的原始
|
|
5
|
+
* analysis/plan,流程中不会因用户已解决而重算——因此导航必须是「只前进」
|
|
6
|
+
* (按适用阶段有序列表推进),绝不能靠“当前阶段 != X 且 hasX”判定:
|
|
7
|
+
* 那会让已经过的阶段被重新命中,点「下一步」反而跳回上一步。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 计算下一阶段:在 `list`(适用阶段的有序列表)中取 `from` 的下一项。
|
|
11
|
+
* - `from` 不在列表中(如来自 preview 页)→ 取第一项;
|
|
12
|
+
* - `from` 已是最后一项(confirm)→ 原地返回;
|
|
13
|
+
* - 绝不回退:已完成阶段不会因仍适用而被再次命中。
|
|
14
|
+
*/
|
|
15
|
+
export function nextFlowPhase(list, from) {
|
|
16
|
+
if (list.length === 0)
|
|
17
|
+
return 'confirm';
|
|
18
|
+
const idx = list.indexOf(from);
|
|
19
|
+
const next = list[Math.min(idx + 1, list.length - 1)];
|
|
20
|
+
return next ?? 'confirm';
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../../src/ui/flow.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAiB,EAAE,IAAe;IAC9D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACvC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;IACrD,OAAO,IAAI,IAAI,SAAS,CAAA;AAC1B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-config-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "DSH Config Manager — backup / export / import / migrate DSH configuration (dual-face Cordis plugin: host engine + web UI). Manifest shape mirrors @linxin666/dsh-ssh@0.1.12.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/xiajiajun516/dsh-config-manager.git"
|
|
9
|
+
},
|
|
6
10
|
"engines": {
|
|
7
11
|
"node": "^22.19.0 || >=24.0.0"
|
|
8
12
|
},
|
|
@@ -21,6 +21,7 @@ import { useRef, useState } from 'react'
|
|
|
21
21
|
import type { ChangeEvent } from 'react'
|
|
22
22
|
import { ImportWizard } from '../../ui/import-wizard.ts'
|
|
23
23
|
import { ConflictCollector } from '../../ui/conflict-view.ts'
|
|
24
|
+
import { nextFlowPhase, type FlowPhase } from '../../ui/flow.ts'
|
|
24
25
|
import type { ImportPreviewSummary, ProgressEvent, WizardSnapshot } from '../../ui/types.ts'
|
|
25
26
|
import type { PathMapping } from '../../core/types.ts'
|
|
26
27
|
import type { ConfigManagerApi, UploadResponse } from '../api.ts'
|
|
@@ -38,8 +39,7 @@ export interface ImportWizardViewProps {
|
|
|
38
39
|
t: TranslateNS<'config-manager'>
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
/** 中间流程阶段(wizard.step 之外的 UI
|
|
42
|
-
type FlowPhase = 'preview' | 'conflicts' | 'path-mapping' | 'secrets' | 'confirm'
|
|
42
|
+
/** 中间流程阶段(wizard.step 之外的 UI 层页面)——定义见 src/ui/flow.ts */
|
|
43
43
|
|
|
44
44
|
const SCORE_LABEL: Record<string, string> = {
|
|
45
45
|
excellent: 'Excellent',
|
|
@@ -118,14 +118,24 @@ export function ImportWizardView({ api, t }: ImportWizardViewProps) {
|
|
|
118
118
|
const hasPathIssues = (snapshot.analysis?.pathIssues.length ?? 0) > 0
|
|
119
119
|
const hasSecrets = (snapshot.plan?.missingSecrets.length ?? 0) > 0
|
|
120
120
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
/**
|
|
122
|
+
* 适用阶段的有序列表(仅含需要用户处理 + 确认页)。
|
|
123
|
+
* hasConflicts/hasPathIssues/hasSecrets 基于原始 analysis/plan(Dry Run 产物),
|
|
124
|
+
* 在流程中不会因已解决而重算——所以导航必须只前进(见 nextFlowPhase),
|
|
125
|
+
* 而不是靠"当前阶段 != X"判定(那会让已完成阶段被重新命中、跳回上一步)。
|
|
126
|
+
*/
|
|
127
|
+
const applicablePhases = (): FlowPhase[] => {
|
|
128
|
+
const list: FlowPhase[] = []
|
|
129
|
+
if (hasConflicts) list.push('conflicts')
|
|
130
|
+
if (hasPathIssues) list.push('path-mapping')
|
|
131
|
+
if (hasSecrets) list.push('secrets')
|
|
132
|
+
list.push('confirm')
|
|
133
|
+
return list
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
/** 从某阶段完成后进入的下一个阶段:只前进(from 不在列表时取第一项) */
|
|
137
|
+
const nextPhase = (from: FlowPhase): FlowPhase => nextFlowPhase(applicablePhases(), from)
|
|
138
|
+
|
|
129
139
|
/* ---------- 动作 ---------- */
|
|
130
140
|
|
|
131
141
|
/** 选择并上传 ZIP → wizard.selectZip(analyzing → compatibility) */
|
package/src/core/smoke.test.ts
CHANGED
|
@@ -37,12 +37,15 @@ import type {
|
|
|
37
37
|
|
|
38
38
|
class MemFs implements FileSystemFacade {
|
|
39
39
|
files = new Map<string, Uint8Array>();
|
|
40
|
-
private readonly homeDir: string;
|
|
40
|
+
private readonly homeDir: string; // 规范化后的 home(纯字符串前缀,不依赖宿主 path 语义)
|
|
41
41
|
constructor(homeDir: string) {
|
|
42
|
-
this.homeDir = homeDir;
|
|
42
|
+
this.homeDir = normalizePath(homeDir);
|
|
43
43
|
}
|
|
44
|
+
/** 内存 fs 的 key:与宿主平台解耦——避免 POSIX 上 path.resolve('C:\\...', p) 注入 cwd 前缀,
|
|
45
|
+
* 也避免 win32 上 path.resolve('/home/...', p) 补盘符,导致 listRecursive 切片错位。 */
|
|
44
46
|
private key(p: string): string {
|
|
45
|
-
|
|
47
|
+
const rel = normalizePath(p).replace(/^\/+/, '');
|
|
48
|
+
return rel === '' ? this.homeDir : `${this.homeDir}/${rel}`;
|
|
46
49
|
}
|
|
47
50
|
async readFile(relPath: string): Promise<Uint8Array> {
|
|
48
51
|
const v = this.files.get(this.key(relPath));
|
|
@@ -64,10 +67,10 @@ class MemFs implements FileSystemFacade {
|
|
|
64
67
|
this.files.delete(this.key(relPath));
|
|
65
68
|
}
|
|
66
69
|
async listRecursive(dir: string): Promise<string[]> {
|
|
67
|
-
const prefix =
|
|
70
|
+
const prefix = this.key(dir) + '/';
|
|
68
71
|
const out: string[] = [];
|
|
69
72
|
for (const k of this.files.keys()) {
|
|
70
|
-
if (k.startsWith(prefix)) out.push(k.slice(this.homeDir.length)
|
|
73
|
+
if (k.startsWith(prefix)) out.push(k.slice(this.homeDir.length + 1)); // 去掉 home + '/'
|
|
71
74
|
}
|
|
72
75
|
return out.sort();
|
|
73
76
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { nextFlowPhase, type FlowPhase } from './flow.ts'
|
|
4
|
+
|
|
5
|
+
const ALL: FlowPhase[] = ['conflicts', 'path-mapping', 'secrets', 'confirm']
|
|
6
|
+
|
|
7
|
+
test('来自 preview(不在列表中)→ 取第一项', () => {
|
|
8
|
+
assert.equal(nextFlowPhase(['conflicts', 'path-mapping', 'confirm'], 'preview'), 'conflicts')
|
|
9
|
+
assert.equal(nextFlowPhase(['secrets', 'confirm'], 'preview'), 'secrets')
|
|
10
|
+
assert.equal(nextFlowPhase(['confirm'], 'preview'), 'confirm')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('顺序前进:conflicts → path-mapping → secrets → confirm', () => {
|
|
14
|
+
assert.equal(nextFlowPhase(ALL, 'conflicts'), 'path-mapping')
|
|
15
|
+
assert.equal(nextFlowPhase(ALL, 'path-mapping'), 'secrets')
|
|
16
|
+
assert.equal(nextFlowPhase(ALL, 'secrets'), 'confirm')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('回归:path-mapping 完成后即使 conflicts 仍适用(hasX 不变)也只前进,不回跳', () => {
|
|
20
|
+
// 模拟“既有冲突又有路径问题”:list 含 conflicts,但从 path-mapping 出发必须到 secrets/confirm
|
|
21
|
+
const list: FlowPhase[] = ['conflicts', 'path-mapping', 'confirm']
|
|
22
|
+
assert.equal(nextFlowPhase(list, 'path-mapping'), 'confirm')
|
|
23
|
+
assert.equal(nextFlowPhase(list, 'conflicts'), 'path-mapping')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('跳过不适用的阶段', () => {
|
|
27
|
+
assert.equal(nextFlowPhase(['path-mapping', 'confirm'], 'preview'), 'path-mapping')
|
|
28
|
+
assert.equal(nextFlowPhase(['confirm'], 'conflicts'), 'confirm')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('最后一项原地返回(confirm 无下一步)', () => {
|
|
32
|
+
assert.equal(nextFlowPhase(ALL, 'confirm'), 'confirm')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('空列表兜底返回 confirm', () => {
|
|
36
|
+
assert.equal(nextFlowPhase([], 'preview'), 'confirm')
|
|
37
|
+
})
|
package/src/ui/flow.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入向导的 UI 流程阶段(覆盖在 wizard.step === 'preview' 之上)与导航。
|
|
3
|
+
*
|
|
4
|
+
* 注意:hasConflicts / hasPathIssues / hasSecrets 来自 Dry Run 的原始
|
|
5
|
+
* analysis/plan,流程中不会因用户已解决而重算——因此导航必须是「只前进」
|
|
6
|
+
* (按适用阶段有序列表推进),绝不能靠“当前阶段 != X 且 hasX”判定:
|
|
7
|
+
* 那会让已经过的阶段被重新命中,点「下一步」反而跳回上一步。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 中间流程阶段(wizard.step 之外的 UI 层页面) */
|
|
11
|
+
export type FlowPhase = 'preview' | 'conflicts' | 'path-mapping' | 'secrets' | 'confirm'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 计算下一阶段:在 `list`(适用阶段的有序列表)中取 `from` 的下一项。
|
|
15
|
+
* - `from` 不在列表中(如来自 preview 页)→ 取第一项;
|
|
16
|
+
* - `from` 已是最后一项(confirm)→ 原地返回;
|
|
17
|
+
* - 绝不回退:已完成阶段不会因仍适用而被再次命中。
|
|
18
|
+
*/
|
|
19
|
+
export function nextFlowPhase(list: FlowPhase[], from: FlowPhase): FlowPhase {
|
|
20
|
+
if (list.length === 0) return 'confirm'
|
|
21
|
+
const idx = list.indexOf(from)
|
|
22
|
+
const next = list[Math.min(idx + 1, list.length - 1)]
|
|
23
|
+
return next ?? 'confirm'
|
|
24
|
+
}
|