dsh-config-manager 0.1.86 → 0.2.0
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/cli/web/page.js +2 -2
- package/lib/client.d.ts +86 -1
- package/lib/client.js +2371 -778
- package/lib/core/native-path-picker.d.ts +49 -0
- package/lib/core/native-path-picker.js +475 -0
- package/lib/core/path-browse.d.ts +42 -0
- package/lib/core/path-browse.js +134 -0
- package/lib/foreign/claude-sessions.d.ts +1 -1
- package/lib/foreign/claude-sessions.js +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -3
- package/lib/market/security.js +1 -1
- package/lib/market/types.d.ts +1 -1
- package/lib/market/types.js +1 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/index.js +2 -0
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/path-browse.d.ts +3 -0
- package/lib/routes/path-browse.js +63 -0
- package/lib/security/secret-scanner.js +1 -1
- package/lib/ui/sync-settings-view.d.ts +18 -0
- package/lib/ui/sync-settings-view.js +20 -0
- package/package.json +2 -2
- package/src/cli/web/page.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +20 -17
- package/src/client/api.ts +60 -1
- package/src/client/bundle-selfcontained.test.ts +9 -0
- package/src/client/chat/chat-entry-view.test.ts +1 -1
- package/src/client/chat/rollback-entry-view.test.ts +14 -0
- package/src/client/chat/rollback-entry-view.ts +2 -1
- package/src/client/common/CopyButton.tsx +21 -2
- package/src/client/common/Icon.tsx +7 -0
- package/src/client/common/routes.ts +5 -0
- package/src/client/config-manager.module.css +70 -0
- package/src/client/import/ImportWizardView.tsx +1 -0
- package/src/client/import/PathMappingForm.tsx +62 -8
- package/src/client/import/PathPickerDialog.tsx +172 -0
- package/src/client/import/dsh-directory-pick.test.ts +55 -0
- package/src/client/import/dsh-directory-pick.ts +55 -0
- package/src/client/import/import-wizard-steps.tsx +5 -2
- package/src/client/index.ts +12 -1
- package/src/client/locales.ts +25 -0
- package/src/client/lucide-icons.d.ts +2 -0
- package/src/client/sync/SyncPanel.tsx +4 -4
- package/src/client/sync/sync-view.ts +1 -1
- package/src/core/native-path-picker.test.ts +118 -0
- package/src/core/native-path-picker.ts +517 -0
- package/src/core/path-browse.test.ts +88 -0
- package/src/core/path-browse.ts +166 -0
- package/src/foreign/claude-sessions.ts +3 -3
- package/src/index.ts +3 -3
- package/src/market/security.ts +1 -1
- package/src/market/types.ts +1 -1
- package/src/routes/index.ts +3 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/path-browse.ts +70 -0
- package/src/security/secret-scanner.test.ts +1 -1
- package/src/security/secret-scanner.ts +1 -1
- package/src/ui/session-issue-labels.ts +1 -1
- package/src/ui/sync-settings-view.test.ts +18 -1
- package/src/ui/sync-settings-view.ts +21 -0
package/src/client/locales.ts
CHANGED
|
@@ -289,6 +289,19 @@ export const zh = {
|
|
|
289
289
|
'import.paths.kind.homeMismatch': '基础路径不同',
|
|
290
290
|
'import.paths.unresolved': '还有 {count} 条路径未映射(留空将跳过该路径,可以继续)',
|
|
291
291
|
'import.paths.empty': '无需映射路径',
|
|
292
|
+
// 「选择本机路径」弹窗(第③项):浏览器拿不到本机真实路径,目录列举走宿主只读端点
|
|
293
|
+
'import.paths.browse': '选择本机路径',
|
|
294
|
+
'import.paths.picker.title': '选择本机路径',
|
|
295
|
+
'import.paths.picker.up': '上一层',
|
|
296
|
+
'import.paths.picker.home': '主目录',
|
|
297
|
+
'import.paths.picker.thisPc': '此电脑',
|
|
298
|
+
'import.paths.picker.useCurrent': '使用当前目录',
|
|
299
|
+
'import.paths.picker.empty': '这个目录里没有可进入的项',
|
|
300
|
+
'import.paths.picker.truncated': '只显示前 {count} 项',
|
|
301
|
+
'import.paths.picker.notFound': '这个路径不存在(已为你打开上一级)',
|
|
302
|
+
'import.paths.picker.denied': '没有读取这个目录的权限',
|
|
303
|
+
'import.paths.picker.notAbsolute': '请填绝对路径(如 D:\\ 或 /)',
|
|
304
|
+
'import.paths.picker.failed': '读取这个目录失败',
|
|
292
305
|
// —— 快照恢复 ——
|
|
293
306
|
'home.schedule.title': '定时备份',
|
|
294
307
|
'snapshots.selectHint': '选择快照以预览恢复计划(只读预览,不会改动任何设置):',
|
|
@@ -1160,6 +1173,18 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1160
1173
|
'import.paths.kind.homeMismatch': 'Different base path',
|
|
1161
1174
|
'import.paths.unresolved': '{count} path(s) unmapped (blank = skip that path, you can continue)',
|
|
1162
1175
|
'import.paths.empty': 'No paths to map',
|
|
1176
|
+
'import.paths.browse': 'Choose a local path',
|
|
1177
|
+
'import.paths.picker.title': 'Choose a local path',
|
|
1178
|
+
'import.paths.picker.up': 'Up one level',
|
|
1179
|
+
'import.paths.picker.home': 'Home folder',
|
|
1180
|
+
'import.paths.picker.thisPc': 'This PC',
|
|
1181
|
+
'import.paths.picker.useCurrent': 'Use this folder',
|
|
1182
|
+
'import.paths.picker.empty': 'Nothing to open in this folder',
|
|
1183
|
+
'import.paths.picker.truncated': 'Showing the first {count} entries',
|
|
1184
|
+
'import.paths.picker.notFound': 'That path does not exist (opened its parent instead)',
|
|
1185
|
+
'import.paths.picker.denied': 'No permission to read this folder',
|
|
1186
|
+
'import.paths.picker.notAbsolute': 'Enter an absolute path (e.g. D:\\ or /)',
|
|
1187
|
+
'import.paths.picker.failed': 'Could not read this folder',
|
|
1163
1188
|
'home.schedule.title': 'Schedule',
|
|
1164
1189
|
'snapshots.selectHint': 'Select a snapshot to preview its restore plan (read-only preview, changes nothing):',
|
|
1165
1190
|
'snapshots.status': 'Status',
|
|
@@ -29,6 +29,8 @@ declare module 'lucide-react/dist/esm/icons/download.mjs' { const i: LucideIconC
|
|
|
29
29
|
declare module 'lucide-react/dist/esm/icons/eye.mjs' { const i: LucideIconComponent; export default i }
|
|
30
30
|
declare module 'lucide-react/dist/esm/icons/file.mjs' { const i: LucideIconComponent; export default i }
|
|
31
31
|
declare module 'lucide-react/dist/esm/icons/folder-input.mjs' { const i: LucideIconComponent; export default i }
|
|
32
|
+
declare module 'lucide-react/dist/esm/icons/folder.mjs' { const i: LucideIconComponent; export default i }
|
|
33
|
+
declare module 'lucide-react/dist/esm/icons/folder-open.mjs' { const i: LucideIconComponent; export default i }
|
|
32
34
|
declare module 'lucide-react/dist/esm/icons/hard-drive-download.mjs' { const i: LucideIconComponent; export default i }
|
|
33
35
|
declare module 'lucide-react/dist/esm/icons/history.mjs' { const i: LucideIconComponent; export default i }
|
|
34
36
|
declare module 'lucide-react/dist/esm/icons/info.mjs' { const i: LucideIconComponent; export default i }
|
|
@@ -74,7 +74,7 @@ import type {
|
|
|
74
74
|
import {
|
|
75
75
|
buildSelectionRequest, buildSyncChannelBody, buildSyncConfigBody, buildSyncPushBody,
|
|
76
76
|
channelBackfillFromStatus, channelRemoteReady, cloudFormFromStatus, computeEncryptInvalid, encryptToggle,
|
|
77
|
-
githubPollDecision, hasSelectedSections, includeSecretsToggle, isGithubFlowInFlight, resolveInitialChannel,
|
|
77
|
+
githubPollDecision, hasSelectedSections, includeSecretsToggle, isGithubFlowInFlight, pickSyncPrimary, resolveInitialChannel,
|
|
78
78
|
needsSessionInventory, saveQueueOnFlush, saveQueueOnRequest, saveQueueOnSettled, toggleSectionSelection,
|
|
79
79
|
validateCloudForm, DEFAULT_S3_PROVIDER,
|
|
80
80
|
SYNC_CONFIG_SAVE_DEBOUNCE_MS,
|
|
@@ -1261,10 +1261,10 @@ export function SyncPanel({ api, t, cmT }: SyncPanelProps) {
|
|
|
1261
1261
|
{ready && (
|
|
1262
1262
|
<Card>
|
|
1263
1263
|
{/* 动作行(方案 A 定案):用户只面对两个方向 —— 下行「一键同步」/ 上行「推送到远端」。
|
|
1264
|
-
|
|
1264
|
+
合并裁决并入一键同步(有基线走三方裁决视图、无基线走老导入确认);并排两个按钮看不出该按哪个(issue 115)⇒ 按已确证远端事实只让一个落在 primary。 */}
|
|
1265
1265
|
<div className={css.actionRow}>
|
|
1266
1266
|
<Button
|
|
1267
|
-
variant=
|
|
1267
|
+
variant={pickSyncPrimary(state.statusInfo, channel) === 'sync' ? 'primary' : 'ghost'}
|
|
1268
1268
|
disabled={busy}
|
|
1269
1269
|
loading={state.busy === 'sync'}
|
|
1270
1270
|
onClick={() => { void runSyncSmart(channel) }}
|
|
@@ -1273,7 +1273,7 @@ export function SyncPanel({ api, t, cmT }: SyncPanelProps) {
|
|
|
1273
1273
|
? (syncPhase === 'preview' ? t('syncflow.checkingRemote') : t('syncflow.syncing'))
|
|
1274
1274
|
: t('syncflow.button')}
|
|
1275
1275
|
</Button>
|
|
1276
|
-
<Button disabled={!computeSyncButtons(state.busy, ready, uiT).canPush || githubBusy || !hasSelectedSections(cs) || computeEncryptInvalid(cs)} onClick={() => { switchChannel(channel); void runPushPreview() }}>
|
|
1276
|
+
<Button variant={pickSyncPrimary(state.statusInfo, channel) === 'push' ? 'primary' : 'ghost'} disabled={!computeSyncButtons(state.busy, ready, uiT).canPush || githubBusy || !hasSelectedSections(cs) || computeEncryptInvalid(cs)} onClick={() => { switchChannel(channel); void runPushPreview() }}>
|
|
1277
1277
|
{computeSyncButtons(state.busy, ready, uiT).pushLabel}
|
|
1278
1278
|
</Button>
|
|
1279
1279
|
{/* 测试连接(只读自检):与两个主操作同一行、排在最后;结果走 toast(不再占页面高度) */}
|
|
@@ -294,7 +294,7 @@ export function cloudIssueKey(code: string): SyncKey | null {
|
|
|
294
294
|
/* ------------------------------------------------ G-34:gist 通道不适合大分区 */
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
* G-34(docs/
|
|
297
|
+
* G-34(docs/maintenance/known-gaps.md):gist 通道**刻意不做内容寻址外置** —— 一次 GET 把全部
|
|
298
298
|
* 文件内容取回、单文件内容超限会被截断、文件数还有上限,因此会话这类大分区不适合走 gist。
|
|
299
299
|
*
|
|
300
300
|
* 本常量 = 「gist 表单」常驻提示的字典键(组件按它取 sync 字典文案渲染 hint)。
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原生目录选择器的调度逻辑单测(不弹窗、不 spawn 真进程:全部走注入的 spawner)。
|
|
3
|
+
*
|
|
4
|
+
* 真机那半(PowerShell 的 IFileOpenDialog 到底能不能弹出来)无法在无头测试里断言,
|
|
5
|
+
* 由方案的 `Probe` 自检 + 人工目测覆盖;本文件只钉住**我们自己的**判断逻辑:
|
|
6
|
+
* 三态区分(ok / canceled / unsupported·failed)、取消不当错误、能力缺失必须回落、
|
|
7
|
+
* 起始路径与标题经环境变量传递、以及路径归一化不毁掉盘符根。
|
|
8
|
+
*/
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import {
|
|
12
|
+
PICK_ENV_START,
|
|
13
|
+
PICK_ENV_TITLE,
|
|
14
|
+
defaultSpawner,
|
|
15
|
+
pickNativeDirectory,
|
|
16
|
+
} from './native-path-picker.ts';
|
|
17
|
+
import type { NativePickExec, NativePickRun } from './native-path-picker.ts';
|
|
18
|
+
|
|
19
|
+
/** 造一个 spawner:按调用顺序吐出预设结果,并记录每次 run(便于断言 argv/env)。 */
|
|
20
|
+
function scripted(results: Partial<NativePickExec>[]): { spawner: (run: NativePickRun) => Promise<NativePickExec>; runs: NativePickRun[] } {
|
|
21
|
+
const runs: NativePickRun[] = [];
|
|
22
|
+
let index = 0;
|
|
23
|
+
return {
|
|
24
|
+
runs,
|
|
25
|
+
spawner: async (run) => {
|
|
26
|
+
runs.push(run);
|
|
27
|
+
const preset = results[index] ?? {};
|
|
28
|
+
index += 1;
|
|
29
|
+
return { code: 0, stdout: '', stderr: '', missing: false, ...preset };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('win32:选中路径 → ok,且起始路径/标题走环境变量而不是命令行', async () => {
|
|
35
|
+
const { spawner, runs } = scripted([{ code: 0, stdout: 'D:\\Projects\\picked\\\r\n' }]);
|
|
36
|
+
const result = await pickNativeDirectory({ platform: 'win32', startPath: 'D:\\Projects', title: '选择本机路径', spawner });
|
|
37
|
+
assert.deepEqual(result, { ok: true, path: 'D:\\Projects\\picked' });
|
|
38
|
+
const run = runs[0];
|
|
39
|
+
assert.ok(run !== undefined);
|
|
40
|
+
assert.equal(run.file, 'powershell');
|
|
41
|
+
// -STA 必须在:PowerShell 7 默认 MTA,COM 模态窗口会直接失败。
|
|
42
|
+
assert.ok(run.args.includes('-STA'));
|
|
43
|
+
assert.equal(run.env[PICK_ENV_START], 'D:\\Projects');
|
|
44
|
+
assert.equal(run.env[PICK_ENV_TITLE], '选择本机路径');
|
|
45
|
+
// 用户给的字符串绝不进 argv(否则就等于拼进脚本了)。
|
|
46
|
+
assert.ok(!run.args.some((arg) => arg.includes('picked') || arg.includes('选择本机路径')));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('win32:退出码 2(用户取消)→ canceled,不是失败', async () => {
|
|
50
|
+
const { spawner } = scripted([{ code: 2 }]);
|
|
51
|
+
assert.deepEqual(await pickNativeDirectory({ platform: 'win32', spawner }), { ok: false, reason: 'canceled' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('win32:HRESULT 错误 → failed 并带回 detail', async () => {
|
|
55
|
+
const { spawner } = scripted([{ code: 3, stderr: 'ERR:-2147024809\r\n' }]);
|
|
56
|
+
const result = await pickNativeDirectory({ platform: 'win32', spawner });
|
|
57
|
+
assert.deepEqual(result, { ok: false, reason: 'failed', detail: 'ERR:-2147024809' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('win32:powershell 不存在 → unsupported(客户端回落内置浏览器)', async () => {
|
|
61
|
+
const { spawner } = scripted([{ missing: true, code: null }]);
|
|
62
|
+
const result = await pickNativeDirectory({ platform: 'win32', spawner });
|
|
63
|
+
assert.equal(result.ok, false);
|
|
64
|
+
assert.equal(result.ok === false ? result.reason : '', 'unsupported');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('win32:空 stdout 当作取消处理(窗口关了但没给路径)', async () => {
|
|
68
|
+
const { spawner } = scripted([{ code: 0, stdout: ' \r\n' }]);
|
|
69
|
+
assert.deepEqual(await pickNativeDirectory({ platform: 'win32', spawner }), { ok: false, reason: 'canceled' });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('win32:盘符根与 POSIX 根的原样保留 / 常规尾部反斜杠剥离', async () => {
|
|
73
|
+
for (const [stdout, expected] of [
|
|
74
|
+
['D:\\\r\n', 'D:\\'],
|
|
75
|
+
['D:\\Apps\\nodejs\\\r\n', 'D:\\Apps\\nodejs'],
|
|
76
|
+
['/home/me/docs/\r\n', '/home/me/docs'],
|
|
77
|
+
['/\r\n', '/'],
|
|
78
|
+
] as const) {
|
|
79
|
+
const { spawner } = scripted([{ code: 0, stdout }]);
|
|
80
|
+
const result = await pickNativeDirectory({ platform: 'win32', spawner });
|
|
81
|
+
assert.deepEqual(result, { ok: true, path: expected }, `归一化 ${JSON.stringify(stdout)} 期望 ${expected}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('darwin:osascript 选中 → ok;取消(-128 / 退出码 1)→ canceled', async () => {
|
|
86
|
+
const ok = scripted([{ code: 0, stdout: '/Users/me/Documents\n' }]);
|
|
87
|
+
assert.deepEqual(await pickNativeDirectory({ platform: 'darwin', spawner: ok.spawner }), { ok: true, path: '/Users/me/Documents' });
|
|
88
|
+
assert.equal(ok.runs[0]?.file, 'osascript');
|
|
89
|
+
|
|
90
|
+
const canceled = scripted([{ code: 0, stderr: 'execution error: User canceled. (-128)\n' }]);
|
|
91
|
+
assert.deepEqual(await pickNativeDirectory({ platform: 'darwin', spawner: canceled.spawner }), { ok: false, reason: 'canceled' });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('linux:zenity 缺失时自动换 kdialog;两个都没有 → unsupported', async () => {
|
|
95
|
+
const fallback = scripted([{ missing: true, code: null }, { code: 0, stdout: '/home/me\n' }]);
|
|
96
|
+
assert.deepEqual(await pickNativeDirectory({ platform: 'linux', startPath: '/home', spawner: fallback.spawner }), { ok: true, path: '/home/me' });
|
|
97
|
+
assert.equal(fallback.runs[0]?.file, 'zenity');
|
|
98
|
+
assert.equal(fallback.runs[1]?.file, 'kdialog');
|
|
99
|
+
|
|
100
|
+
const none = scripted([{ missing: true, code: null }, { missing: true, code: null }]);
|
|
101
|
+
const result = await pickNativeDirectory({ platform: 'linux', spawner: none.spawner });
|
|
102
|
+
assert.equal(result.ok === false ? result.reason : '', 'unsupported');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('其他平台:直接 unsupported,且根本不 spawn', async () => {
|
|
106
|
+
const { spawner, runs } = scripted([]);
|
|
107
|
+
const result = await pickNativeDirectory({ platform: 'freebsd', spawner });
|
|
108
|
+
assert.equal(result.ok === false ? result.reason : '', 'unsupported');
|
|
109
|
+
assert.equal(runs.length, 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('defaultSpawner:超时也保留子进程已经吐出的 stderr(排障线索不能丢)', async () => {
|
|
113
|
+
const script = 'process.stderr.write("boom\\n"); setTimeout(() => {}, 10000)';
|
|
114
|
+
const exec = await defaultSpawner({ file: process.execPath, args: ['-e', script], env: {}, timeoutMs: 400 });
|
|
115
|
+
assert.equal(exec.code, null);
|
|
116
|
+
assert.match(exec.stderr, /timed out/);
|
|
117
|
+
assert.match(exec.stderr, /boom/);
|
|
118
|
+
});
|