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.
- package/README.md +30 -0
- package/README.zh-CN.md +30 -0
- package/lib/adapters/test-helpers.d.ts +4 -2
- package/lib/adapters/test-helpers.js +11 -3
- package/lib/adapters/test-helpers.js.map +1 -1
- package/lib/cli/index.d.ts +44 -0
- package/lib/cli/index.js +230 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/client.d.ts +260 -5
- package/lib/client.js +2519 -820
- package/lib/client.js.map +1 -1
- package/lib/core/analyzer.d.ts +18 -1
- package/lib/core/analyzer.js +29 -0
- package/lib/core/analyzer.js.map +1 -1
- package/lib/core/backup.d.ts +3 -1
- package/lib/core/backup.js +70 -0
- package/lib/core/backup.js.map +1 -1
- package/lib/core/exporter.d.ts +11 -0
- package/lib/core/exporter.js +11 -0
- package/lib/core/exporter.js.map +1 -1
- package/lib/core/importer.d.ts +3 -0
- package/lib/core/importer.js +1 -0
- package/lib/core/importer.js.map +1 -1
- package/lib/core/index.d.ts +1 -0
- package/lib/core/index.js +1 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/restore.d.ts +92 -0
- package/lib/core/restore.js +505 -0
- package/lib/core/restore.js.map +1 -0
- package/lib/core/run-registry.d.ts +65 -0
- package/lib/core/run-registry.js +120 -0
- package/lib/core/run-registry.js.map +1 -0
- package/lib/core/types.d.ts +20 -0
- package/lib/core/types.js.map +1 -1
- package/lib/index.d.ts +45 -0
- package/lib/index.js +442 -8
- package/lib/index.js.map +1 -1
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +47 -0
- package/lib/sync/fs.js.map +1 -0
- package/lib/sync/git/git-transport.d.ts +87 -0
- package/lib/sync/git/git-transport.js +333 -0
- package/lib/sync/git/git-transport.js.map +1 -0
- package/lib/sync/layout.d.ts +29 -0
- package/lib/sync/layout.js +166 -0
- package/lib/sync/layout.js.map +1 -0
- package/lib/sync/sync-config.d.ts +16 -0
- package/lib/sync/sync-config.js +67 -0
- package/lib/sync/sync-config.js.map +1 -0
- package/lib/sync/sync-engine.d.ts +96 -0
- package/lib/sync/sync-engine.js +236 -0
- package/lib/sync/sync-engine.js.map +1 -0
- package/lib/sync/sync-state.d.ts +31 -0
- package/lib/sync/sync-state.js +103 -0
- package/lib/sync/sync-state.js.map +1 -0
- package/lib/sync/transport.d.ts +50 -0
- package/lib/sync/transport.js +22 -0
- package/lib/sync/transport.js.map +1 -0
- package/package.json +4 -1
- package/src/adapters/test-helpers.ts +10 -3
- package/src/cli/index.ts +278 -0
- package/src/client/ConfigManagerSection.tsx +49 -14
- package/src/client/api.ts +53 -5
- package/src/client/client-types.ts +0 -6
- package/src/client/common/ProgressBar.tsx +31 -19
- package/src/client/common/progress-view-edge.test.ts +60 -0
- package/src/client/common/progress-view.test.ts +79 -0
- package/src/client/common/progress-view.ts +94 -0
- package/src/client/config-manager.module.css +99 -20
- package/src/client/export/ExportView.tsx +63 -49
- package/src/client/import/ImportWizardView.tsx +107 -66
- package/src/client/index.ts +22 -0
- package/src/client/locales.ts +53 -0
- package/src/client/run-store.test.ts +500 -0
- package/src/client/run-store.ts +772 -0
- package/src/client/snapshots/SnapshotsPanel.tsx +240 -0
- package/src/client/sync/SyncSettingsView.tsx +261 -0
- package/src/client/sync/sync-api.test.ts +117 -0
- package/src/client/sync/sync-api.ts +132 -0
- package/src/client/sync/sync-locales.ts +77 -0
- package/src/client/sync/sync-view.test.ts +211 -0
- package/src/client/sync/sync-view.ts +202 -0
- package/src/core/analyzer.ts +50 -1
- package/src/core/backup.ts +78 -2
- package/src/core/exporter.ts +21 -0
- package/src/core/importer.ts +4 -0
- package/src/core/index.ts +5 -0
- package/src/core/restore.ts +587 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +139 -0
- package/src/core/run-registry.ts +166 -0
- package/src/core/smoke.test.ts +5 -0
- package/src/core/types.ts +22 -0
- package/src/index.ts +476 -8
- package/src/security/security.test.ts +5 -0
- package/src/sync/fs.ts +46 -0
- package/src/sync/git/git-transport.test.ts +456 -0
- package/src/sync/git/git-transport.ts +390 -0
- package/src/sync/layout.test.ts +226 -0
- package/src/sync/layout.ts +183 -0
- package/src/sync/sync-config.ts +72 -0
- package/src/sync/sync-engine.test.ts +299 -0
- package/src/sync/sync-engine.ts +318 -0
- package/src/sync/sync-state.test.ts +118 -0
- package/src/sync/sync-state.ts +123 -0
- package/src/sync/transport.test.ts +110 -0
- package/src/sync/transport.ts +73 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-git-channel:Git 私有仓库通道(SyncTransport 的 git 实现)。
|
|
3
|
+
*
|
|
4
|
+
* 设计:
|
|
5
|
+
* - 快照以「散文件目录」提交到 git 仓库:工作副本 <workDir>/snapshots/<id>/ 即 t2 layout 布局
|
|
6
|
+
* (manifest.json + 平铺 JSON 分区 + 文件类分区目录),每次 sync 一次 commit + push。
|
|
7
|
+
* - 命令执行走 node:child_process execFile(promise 封装,数组参数无 shell 注入),
|
|
8
|
+
* 默认 gitBin='git',可注入 exec 便于测试。
|
|
9
|
+
* - 认证:token 仅从注入的 credentials provider 读取(每次网络操作时 getToken()),
|
|
10
|
+
* 经 git credential helper(store --file=<临时文件>)传给 git —— token 不进入 argv、
|
|
11
|
+
* 不进入 repoUrl、不写入任何同步内容/commit message/日志;临时凭据文件用后即删。
|
|
12
|
+
* - 私有仓库判定:checkIsPrivate() 先匿名 ls-remote(成功=公开),失败再用凭据探测
|
|
13
|
+
* (成功=私有),isPrivateHint 缓存最近结果供 UI 提示(私有仓库为推荐使用场景)。
|
|
14
|
+
* - 契约:同 id 重复 upload = 覆盖(幂等友好:内容无变化时不产生 commit);download 不存在抛错;
|
|
15
|
+
* delete 不存在视为成功。
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { promisify } from 'node:util';
|
|
22
|
+
import { createSnapshotFs, joinFs } from "../fs.js";
|
|
23
|
+
import { readSnapshotFromDir, SNAPSHOT_MANIFEST_FILE, writeSnapshotToDir } from "../layout.js";
|
|
24
|
+
import { computeSnapshotMeta } from "../transport.js";
|
|
25
|
+
import { parseJsonSafe } from "../../utils/json.js";
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
27
|
+
export class GitTransportError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'GitTransportError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const SNAPSHOTS_REL = 'snapshots';
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
35
|
+
const DEFAULT_AUTHOR = { name: 'DSH Config Sync', email: 'sync@dsh.local' };
|
|
36
|
+
const DEFAULT_CREDENTIAL_USERNAME = 'oauth2';
|
|
37
|
+
/** 快照 id 安全字符集:字母数字开头,仅 . _ -;防路径穿越与 commit message 注入 */
|
|
38
|
+
const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
39
|
+
const defaultExec = async (cmd, args, opts) => {
|
|
40
|
+
try {
|
|
41
|
+
const { stdout, stderr } = await execFileAsync(cmd, args, {
|
|
42
|
+
cwd: opts.cwd,
|
|
43
|
+
timeout: opts.timeoutMs,
|
|
44
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
});
|
|
48
|
+
return { stdout: String(stdout), stderr: String(stderr), code: 0 };
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const e = err;
|
|
52
|
+
return {
|
|
53
|
+
stdout: String(e.stdout ?? ''),
|
|
54
|
+
stderr: String(e.stderr ?? (err instanceof Error ? err.message : String(err))),
|
|
55
|
+
code: typeof e.code === 'number' ? e.code : 1,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/** git config 值里的路径转义:含空白/引号时用引号包裹(Windows 路径转正斜杠) */
|
|
60
|
+
function quoteGitValue(value) {
|
|
61
|
+
return /[\s"']/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
62
|
+
}
|
|
63
|
+
/** 实现 SyncTransport 的 git 通道。所有操作前 ensureRepo() 保证工作副本就绪,网络命令带凭据。 */
|
|
64
|
+
export class GitTransport {
|
|
65
|
+
type = 'git';
|
|
66
|
+
o;
|
|
67
|
+
repoReady = false;
|
|
68
|
+
privateHint = null;
|
|
69
|
+
constructor(options) {
|
|
70
|
+
if (typeof options.repoUrl !== 'string' || options.repoUrl.length === 0) {
|
|
71
|
+
throw new GitTransportError('repoUrl 必须是非空字符串');
|
|
72
|
+
}
|
|
73
|
+
if (typeof options.workDir !== 'string' || options.workDir.length === 0) {
|
|
74
|
+
throw new GitTransportError('workDir 必须是非空字符串');
|
|
75
|
+
}
|
|
76
|
+
if (options.credentials === null || typeof options.credentials !== 'object'
|
|
77
|
+
|| typeof options.credentials.getToken !== 'function') {
|
|
78
|
+
throw new GitTransportError('credentials 必须提供 getToken()');
|
|
79
|
+
}
|
|
80
|
+
this.o = {
|
|
81
|
+
repoUrl: options.repoUrl,
|
|
82
|
+
workDir: options.workDir,
|
|
83
|
+
credentials: options.credentials,
|
|
84
|
+
gitBin: options.gitBin ?? 'git',
|
|
85
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
86
|
+
exec: options.exec ?? defaultExec,
|
|
87
|
+
author: options.author ?? DEFAULT_AUTHOR,
|
|
88
|
+
credentialUsername: options.credentialUsername ?? DEFAULT_CREDENTIAL_USERNAME,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** 最近一次 checkIsPrivate() 的结果;null = 尚未检查 */
|
|
92
|
+
get isPrivateHint() {
|
|
93
|
+
return this.privateHint;
|
|
94
|
+
}
|
|
95
|
+
/** 列出远端已有快照(按 createdAt 升序)。读取工作副本 snapshots/ 目录(先 pull 同步远端)。 */
|
|
96
|
+
async list() {
|
|
97
|
+
await this.ensureRepo();
|
|
98
|
+
await this.pullFromRemote();
|
|
99
|
+
const fsx = createSnapshotFs();
|
|
100
|
+
const snapsAbs = this.snapshotsDir();
|
|
101
|
+
const names = await fsx.readdir(snapsAbs);
|
|
102
|
+
const metas = [];
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
if (name === '' || name === '.' || name === '..')
|
|
105
|
+
continue;
|
|
106
|
+
const dirAbs = joinFs(snapsAbs, name);
|
|
107
|
+
if (!(await fsx.isDir(dirAbs)))
|
|
108
|
+
continue;
|
|
109
|
+
const m = await this.readDirManifest(dirAbs);
|
|
110
|
+
if (m === null)
|
|
111
|
+
continue; // 损坏/不完整 → 跳过(防御工作副本污染,不静默失败整体 list)
|
|
112
|
+
metas.push({
|
|
113
|
+
id: m.id,
|
|
114
|
+
createdAt: m.createdAt,
|
|
115
|
+
sections: m.sectionHashes,
|
|
116
|
+
manifest: m.manifest,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
metas.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
|
|
120
|
+
return metas;
|
|
121
|
+
}
|
|
122
|
+
/** 上传快照:写散文件目录 → add → commit → push;同 id 覆盖;内容无变化时幂等(不产生 commit)。 */
|
|
123
|
+
async upload(snapshot) {
|
|
124
|
+
this.assertSafeId(snapshot.id);
|
|
125
|
+
await this.ensureRepo();
|
|
126
|
+
await this.pullFromRemote();
|
|
127
|
+
const fsx = createSnapshotFs();
|
|
128
|
+
const dir = this.snapshotDir(snapshot.id);
|
|
129
|
+
const existed = await fsx.exists(dir);
|
|
130
|
+
if (existed)
|
|
131
|
+
await fsx.remove(dir); // 覆盖语义:先清旧目录,避免残留旧文件
|
|
132
|
+
await writeSnapshotToDir(snapshot, dir, fsx);
|
|
133
|
+
const rel = `${SNAPSHOTS_REL}/${snapshot.id}`;
|
|
134
|
+
await this.runGit(['add', '--', rel]);
|
|
135
|
+
const diff = await this.runGit(['diff', '--cached', '--quiet'], { allowNonZero: true });
|
|
136
|
+
if (diff.code !== 0) {
|
|
137
|
+
const verb = existed ? 'update' : 'add';
|
|
138
|
+
await this.runGit(['commit', '-m', `sync: ${verb} snapshot ${snapshot.id}`]);
|
|
139
|
+
await this.runGit(['push', '-u', 'origin', 'HEAD'], { withCredential: true });
|
|
140
|
+
}
|
|
141
|
+
return computeSnapshotMeta(snapshot);
|
|
142
|
+
}
|
|
143
|
+
/** 下载快照完整载荷(读回散文件目录)。不存在的 id 必须抛错(契约)。 */
|
|
144
|
+
async download(id) {
|
|
145
|
+
this.assertSafeId(id);
|
|
146
|
+
await this.ensureRepo();
|
|
147
|
+
await this.pullFromRemote();
|
|
148
|
+
const dir = this.snapshotDir(id);
|
|
149
|
+
if (!(await createSnapshotFs().isDir(dir))) {
|
|
150
|
+
throw new GitTransportError(`快照 ${id} 不存在(本地工作副本 ${dir})`);
|
|
151
|
+
}
|
|
152
|
+
return readSnapshotFromDir(dir);
|
|
153
|
+
}
|
|
154
|
+
/** 删除远端快照(不存在视为成功,契约)。 */
|
|
155
|
+
async delete(id) {
|
|
156
|
+
this.assertSafeId(id);
|
|
157
|
+
await this.ensureRepo();
|
|
158
|
+
await this.pullFromRemote();
|
|
159
|
+
const fsx = createSnapshotFs();
|
|
160
|
+
const dir = this.snapshotDir(id);
|
|
161
|
+
if (!(await fsx.exists(dir)))
|
|
162
|
+
return; // 不存在视为成功
|
|
163
|
+
await fsx.remove(dir);
|
|
164
|
+
const rel = `${SNAPSHOTS_REL}/${id}`;
|
|
165
|
+
await this.runGit(['add', '-A', '--', rel]);
|
|
166
|
+
const diff = await this.runGit(['diff', '--cached', '--quiet'], { allowNonZero: true });
|
|
167
|
+
if (diff.code !== 0) {
|
|
168
|
+
await this.runGit(['commit', '-m', `sync: delete snapshot ${id}`]);
|
|
169
|
+
await this.runGit(['push', '-u', 'origin', 'HEAD'], { withCredential: true });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 私有仓库可见性探测(结果缓存于 isPrivateHint):
|
|
174
|
+
* 匿名 ls-remote 成功 → 公开;匿名失败 + 凭据探测成功 → 私有;两者都失败 → 抛错。
|
|
175
|
+
* UI 层可用 isPrivateHint 提示「私有仓库推荐」;公开仓库也可用(内容需自行评估)。
|
|
176
|
+
*/
|
|
177
|
+
async checkIsPrivate() {
|
|
178
|
+
if (this.privateHint !== null)
|
|
179
|
+
return this.privateHint;
|
|
180
|
+
const anon = await this.runGit(['ls-remote', this.o.repoUrl], { allowNonZero: true, cwd: undefined });
|
|
181
|
+
if (anon.code === 0) {
|
|
182
|
+
this.privateHint = false;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const authed = await this.runGit(['ls-remote', this.o.repoUrl], { allowNonZero: true, cwd: undefined, withCredential: true });
|
|
186
|
+
if (authed.code === 0) {
|
|
187
|
+
this.privateHint = true;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
throw new GitTransportError(`仓库不可达或认证失败: ${this.mask(this.o.repoUrl, null)}`);
|
|
191
|
+
}
|
|
192
|
+
/* ---------------- 内部实现 ---------------- */
|
|
193
|
+
async ensureRepo() {
|
|
194
|
+
if (this.repoReady)
|
|
195
|
+
return;
|
|
196
|
+
const fsx = createSnapshotFs();
|
|
197
|
+
await fsx.mkdir(this.o.workDir); // 不存在则创建
|
|
198
|
+
if (await fsx.exists(path.join(this.o.workDir, '.git'))) {
|
|
199
|
+
this.repoReady = true;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const entries = await fsx.readdir(this.o.workDir);
|
|
203
|
+
if (entries.length > 0) {
|
|
204
|
+
throw new GitTransportError(`workDir 不是 git 仓库(缺少 .git)且目录非空,无法 clone: ${this.o.workDir}`);
|
|
205
|
+
}
|
|
206
|
+
await this.runGit(['clone', this.o.repoUrl, '.'], { cwd: this.o.workDir, withCredential: true });
|
|
207
|
+
// 仓库级提交身份(写入远端历史,可经 author 选项覆盖;不改全局配置)
|
|
208
|
+
await this.runGit(['config', 'user.name', this.o.author.name]);
|
|
209
|
+
await this.runGit(['config', 'user.email', this.o.author.email]);
|
|
210
|
+
this.repoReady = true;
|
|
211
|
+
}
|
|
212
|
+
/** pull --ff-only 同步远端;无本地提交(全新仓库)或无 upstream 时静默跳过 */
|
|
213
|
+
async pullFromRemote() {
|
|
214
|
+
const head = await this.runGit(['rev-parse', '--verify', '--quiet', 'HEAD'], { allowNonZero: true });
|
|
215
|
+
if (head.code !== 0)
|
|
216
|
+
return; // 无本地提交 → 无可 pull
|
|
217
|
+
const res = await this.runGit(['pull', '--ff-only'], { withCredential: true, allowNonZero: true });
|
|
218
|
+
if (res.code === 0)
|
|
219
|
+
return;
|
|
220
|
+
if (/no tracking information/i.test(res.stderr))
|
|
221
|
+
return; // 无 upstream(初始状态)→ 跳过
|
|
222
|
+
throw new GitTransportError(`git pull 失败: ${this.mask(res.stderr, await this.readTokenOnce())}`);
|
|
223
|
+
}
|
|
224
|
+
/** 执行 git 命令;withCredential=true 时注入 credential helper(token 不进 argv),失败时错误消息脱敏 */
|
|
225
|
+
async runGit(args, opts = {}) {
|
|
226
|
+
const cwd = opts.cwd === undefined ? this.o.workDir : opts.cwd;
|
|
227
|
+
let extra = [];
|
|
228
|
+
let token = null;
|
|
229
|
+
let cleanup = null;
|
|
230
|
+
if (opts.withCredential) {
|
|
231
|
+
const cred = await this.buildCredentialArgs();
|
|
232
|
+
extra = cred.extraArgs;
|
|
233
|
+
token = cred.token;
|
|
234
|
+
cleanup = cred.cleanup;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const result = await this.o.exec(this.o.gitBin, [...extra, ...args], { cwd, timeoutMs: this.o.timeoutMs });
|
|
238
|
+
if (result.code !== 0 && !opts.allowNonZero) {
|
|
239
|
+
throw new GitTransportError(this.mask(`git ${args.join(' ')} 失败 (exit ${result.code}): ${result.stderr}`, token));
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
if (cleanup)
|
|
245
|
+
await cleanup();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 为网络命令构造 credential helper 参数:
|
|
250
|
+
* 仅 http(s) 远端需要 token;本地路径 / ssh 走 git 原生认证(密钥),不调用 provider。
|
|
251
|
+
* token 写入 os.tmpdir() 下临时 store 文件(权限 0600),命令后立即删除 —— 永不落工作副本/远端。
|
|
252
|
+
*/
|
|
253
|
+
async buildCredentialArgs() {
|
|
254
|
+
if (!/^https?:\/\//i.test(this.o.repoUrl)) {
|
|
255
|
+
return { extraArgs: [], token: '', cleanup: async () => { } };
|
|
256
|
+
}
|
|
257
|
+
const token = await this.o.credentials.getToken();
|
|
258
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-git-cred-'));
|
|
259
|
+
const credFile = path.join(tmpDir, 'credential');
|
|
260
|
+
const host = this.repoHost();
|
|
261
|
+
const fileArg = quoteGitValue(credFile.replace(/\\/g, '/'));
|
|
262
|
+
await fs.writeFile(credFile, `https://${this.o.credentialUsername}:${token}@${host}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
263
|
+
return {
|
|
264
|
+
extraArgs: ['-c', 'credential.helper=', '-c', `credential.helper=store --file=${fileArg}`],
|
|
265
|
+
token,
|
|
266
|
+
cleanup: async () => { await fs.rm(tmpDir, { recursive: true, force: true }); },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** 从 repoUrl 提取 credential 匹配用的 host[:port] */
|
|
270
|
+
repoHost() {
|
|
271
|
+
try {
|
|
272
|
+
const u = new URL(this.o.repoUrl);
|
|
273
|
+
return u.port ? `${u.hostname}:${u.port}` : u.hostname;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
throw new GitTransportError(`无法解析 repoUrl(http(s) 仓库必须为合法 URL): ${this.o.repoUrl}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** 读取一次 token(错误消息脱敏用);非 http(s) 场景返回空 */
|
|
280
|
+
async readTokenOnce() {
|
|
281
|
+
if (!/^https?:\/\//i.test(this.o.repoUrl))
|
|
282
|
+
return null;
|
|
283
|
+
try {
|
|
284
|
+
return await this.o.credentials.getToken();
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** 错误/日志脱敏:token(原文与 URL 编码两种形态)与 repoUrl 一律替换 */
|
|
291
|
+
mask(text, token) {
|
|
292
|
+
let out = text.split(this.o.repoUrl).join('[REPO_URL]');
|
|
293
|
+
if (token && token.length >= 4) {
|
|
294
|
+
out = out.split(token).join('[REDACTED]').split(encodeURIComponent(token)).join('[REDACTED]');
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
assertSafeId(id) {
|
|
299
|
+
if (typeof id !== 'string' || !SAFE_ID_RE.test(id)) {
|
|
300
|
+
throw new GitTransportError(`非法快照 id: ${JSON.stringify(id)}(仅允许字母数字开头,字符限 . _ -)`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
snapshotDir(id) {
|
|
304
|
+
return joinFs(this.snapshotsDir(), id);
|
|
305
|
+
}
|
|
306
|
+
snapshotsDir() {
|
|
307
|
+
return joinFs(this.o.workDir, SNAPSHOTS_REL);
|
|
308
|
+
}
|
|
309
|
+
/** 读快照目录 manifest.json(结构不合法 → null,调用方跳过) */
|
|
310
|
+
async readDirManifest(dir) {
|
|
311
|
+
const fsx = createSnapshotFs();
|
|
312
|
+
try {
|
|
313
|
+
const abs = joinFs(dir, SNAPSHOT_MANIFEST_FILE);
|
|
314
|
+
if (!(await fsx.exists(abs)))
|
|
315
|
+
return null;
|
|
316
|
+
const raw = Buffer.from(await fsx.readFile(abs)).toString('utf8');
|
|
317
|
+
const parsed = parseJsonSafe(raw);
|
|
318
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
319
|
+
return null;
|
|
320
|
+
const m = parsed;
|
|
321
|
+
if (typeof m['id'] !== 'string' || typeof m['createdAt'] !== 'string'
|
|
322
|
+
|| m['manifest'] === null || typeof m['manifest'] !== 'object'
|
|
323
|
+
|| m['sectionHashes'] === null || typeof m['sectionHashes'] !== 'object') {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return parsed;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=git-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-transport.js","sourceRoot":"","sources":["../../../src/sync/git/git-transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SectionId } from '../schema/types.ts';
|
|
2
|
+
import type { SnapshotFs } from './fs.ts';
|
|
3
|
+
import type { SyncSnapshot } from './transport.ts';
|
|
4
|
+
export declare const SNAPSHOT_MANIFEST_FILE = "manifest.json";
|
|
5
|
+
/** 快照根目录下的 manifest.json 结构 */
|
|
6
|
+
export interface SnapshotDirManifest {
|
|
7
|
+
id: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
manifest: SyncSnapshot['manifest'];
|
|
10
|
+
/** 各分区内容 hash(写入时由 hashSection 计算) */
|
|
11
|
+
sectionHashes: Partial<Record<SectionId, string>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 文件相对路径安全检查:拒绝空/'.'/'..'、绝对路径、Windows 盘符、反斜杠、
|
|
15
|
+
* 含 '..' 或 '.' 片段(防穿越快照根目录)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function isSafeRelPath(rel: string): boolean;
|
|
18
|
+
/** 递归列出 dir 下的全部文件(相对 dir 的正斜杠路径,字典序);目录不存在 → [] */
|
|
19
|
+
export declare function listSnapshotFiles(fsx: SnapshotFs, dir: string): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* 将快照写入散文件目录。
|
|
22
|
+
* 非法输入(不支持的子分区 / 穿越路径)在任何写入发生前抛错,不留半成品目录。
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeSnapshotToDir(snapshot: SyncSnapshot, dir: string, fsx?: SnapshotFs): Promise<SnapshotDirManifest>;
|
|
25
|
+
/**
|
|
26
|
+
* 从散文件目录读回快照。
|
|
27
|
+
* manifest 声明的分区必须可解析:JSON 分区文件缺失 / 文件分区目录缺失 → 抛错(不静默降级)。
|
|
28
|
+
*/
|
|
29
|
+
export declare function readSnapshotFromDir(dir: string, fsx?: SnapshotFs): Promise<SyncSnapshot>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-sync-transport:散文件目录布局。
|
|
3
|
+
* 快照根目录 = manifest.json + 按 SECTION_JSON_PATHS 平铺的 JSON 分区
|
|
4
|
+
* + 按 SECTION_FILE_PREFIXES 的文件类分区目录(skills 等真实文件)。
|
|
5
|
+
* 复用 src/schema/config.ts 的路径表;核心逻辑经 SnapshotFs 注入(默认 node:fs/promises)。
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { parseJsonSafe, stringifyJsonSafe } from "../utils/json.js";
|
|
9
|
+
import { sha256Hex } from "../utils/hashing.js";
|
|
10
|
+
import { SECTION_FILE_PREFIXES, SECTION_JSON_PATHS } from "../schema/config.js";
|
|
11
|
+
import { createSnapshotFs, joinFs } from "./fs.js";
|
|
12
|
+
import { hashSection } from "./sync-state.js";
|
|
13
|
+
export const SNAPSHOT_MANIFEST_FILE = 'manifest.json';
|
|
14
|
+
/** 布局支持的分区 = SECTION_JSON_PATHS ∪ SECTION_FILE_PREFIXES(secrets 等不在内) */
|
|
15
|
+
const LAYOUT_SECTION_IDS = [
|
|
16
|
+
...Object.keys(SECTION_JSON_PATHS),
|
|
17
|
+
...Object.keys(SECTION_FILE_PREFIXES),
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* 文件相对路径安全检查:拒绝空/'.'/'..'、绝对路径、Windows 盘符、反斜杠、
|
|
21
|
+
* 含 '..' 或 '.' 片段(防穿越快照根目录)。
|
|
22
|
+
*/
|
|
23
|
+
export function isSafeRelPath(rel) {
|
|
24
|
+
if (rel === '' || rel === '.' || rel === '..')
|
|
25
|
+
return false;
|
|
26
|
+
if (rel.startsWith('/') || /^[A-Za-z]:[\\/]/.test(rel))
|
|
27
|
+
return false;
|
|
28
|
+
if (rel.includes('\\'))
|
|
29
|
+
return false;
|
|
30
|
+
const parts = rel.split('/');
|
|
31
|
+
if (parts.some((p) => p === '' || p === '.' || p === '..'))
|
|
32
|
+
return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** 递归列出 dir 下的全部文件(相对 dir 的正斜杠路径,字典序);目录不存在 → [] */
|
|
36
|
+
export async function listSnapshotFiles(fsx, dir) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const walk = async (abs, rel) => {
|
|
39
|
+
const names = await fsx.readdir(abs);
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
if (name === '' || name === '.' || name === '..')
|
|
42
|
+
continue; // 防御异常/恶意 fs
|
|
43
|
+
const childAbs = joinFs(abs, name);
|
|
44
|
+
const childRel = rel === '' ? name : `${rel}/${name}`;
|
|
45
|
+
if (await fsx.isDir(childAbs)) {
|
|
46
|
+
await walk(childAbs, childRel);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
out.push(childRel);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
await walk(dir, '');
|
|
54
|
+
return out.sort();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 将快照写入散文件目录。
|
|
58
|
+
* 非法输入(不支持的子分区 / 穿越路径)在任何写入发生前抛错,不留半成品目录。
|
|
59
|
+
*/
|
|
60
|
+
export async function writeSnapshotToDir(snapshot, dir, fsx = createSnapshotFs()) {
|
|
61
|
+
const sectionHashes = {};
|
|
62
|
+
for (const [id, data] of Object.entries(snapshot.sections)) {
|
|
63
|
+
const sid = id;
|
|
64
|
+
if (!LAYOUT_SECTION_IDS.includes(sid)) {
|
|
65
|
+
throw new Error(`分区 ${sid} 不在散文件布局支持范围(SECTION_JSON_PATHS / SECTION_FILE_PREFIXES)`);
|
|
66
|
+
}
|
|
67
|
+
sectionHashes[sid] = hashSection(data);
|
|
68
|
+
}
|
|
69
|
+
// 预校验全部文件相对路径(不合法 → 抛错且零写入)
|
|
70
|
+
for (const [sid, data] of Object.entries(snapshot.sections)) {
|
|
71
|
+
if (!(sid in SECTION_FILE_PREFIXES))
|
|
72
|
+
continue;
|
|
73
|
+
for (const file of data.files) {
|
|
74
|
+
if (!isSafeRelPath(file.relativePath)) {
|
|
75
|
+
throw new Error(`非法文件相对路径: ${file.relativePath}(分区 ${sid})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await fsx.mkdir(dir);
|
|
80
|
+
const manifest = {
|
|
81
|
+
id: snapshot.id,
|
|
82
|
+
createdAt: snapshot.createdAt,
|
|
83
|
+
manifest: snapshot.manifest,
|
|
84
|
+
sectionHashes,
|
|
85
|
+
};
|
|
86
|
+
await fsx.writeFile(joinFs(dir, SNAPSHOT_MANIFEST_FILE), new TextEncoder().encode(stringifyJsonSafe(manifest, { space: 2 })));
|
|
87
|
+
// JSON 分区:按 SECTION_JSON_PATHS 平铺
|
|
88
|
+
for (const [sid, rel] of Object.entries(SECTION_JSON_PATHS)) {
|
|
89
|
+
const data = snapshot.sections[sid];
|
|
90
|
+
if (data === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
const abs = joinFs(dir, rel);
|
|
93
|
+
await fsx.mkdir(path.dirname(abs));
|
|
94
|
+
await fsx.writeFile(abs, new TextEncoder().encode(stringifyJsonSafe(data, { space: 2 })));
|
|
95
|
+
}
|
|
96
|
+
// 文件类分区:目录前缀 + 真实文件
|
|
97
|
+
for (const [sid, prefix] of Object.entries(SECTION_FILE_PREFIXES)) {
|
|
98
|
+
const data = snapshot.sections[sid];
|
|
99
|
+
if (data === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
const baseAbs = joinFs(dir, prefix);
|
|
102
|
+
await fsx.mkdir(baseAbs); // 空文件类分区也保留目录(读回可还原空 files)
|
|
103
|
+
for (const file of data.files) {
|
|
104
|
+
const abs = joinFs(baseAbs, file.relativePath);
|
|
105
|
+
await fsx.mkdir(path.dirname(abs));
|
|
106
|
+
await fsx.writeFile(abs, file.data);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return manifest;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 从散文件目录读回快照。
|
|
113
|
+
* manifest 声明的分区必须可解析:JSON 分区文件缺失 / 文件分区目录缺失 → 抛错(不静默降级)。
|
|
114
|
+
*/
|
|
115
|
+
export async function readSnapshotFromDir(dir, fsx = createSnapshotFs()) {
|
|
116
|
+
const manifestAbs = joinFs(dir, SNAPSHOT_MANIFEST_FILE);
|
|
117
|
+
if (!(await fsx.exists(manifestAbs))) {
|
|
118
|
+
throw new Error(`快照目录缺少 ${SNAPSHOT_MANIFEST_FILE}: ${dir}`);
|
|
119
|
+
}
|
|
120
|
+
const parsed = parseJsonSafe(Buffer.from(await fsx.readFile(manifestAbs)).toString('utf8'));
|
|
121
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
122
|
+
throw new Error('manifest.json 损坏:必须是对象');
|
|
123
|
+
}
|
|
124
|
+
const m = parsed;
|
|
125
|
+
if (typeof m['id'] !== 'string' || typeof m['createdAt'] !== 'string'
|
|
126
|
+
|| m['manifest'] === null || typeof m['manifest'] !== 'object'
|
|
127
|
+
|| m['sectionHashes'] === null || typeof m['sectionHashes'] !== 'object') {
|
|
128
|
+
throw new Error('manifest.json 损坏:缺少 id/createdAt/manifest/sectionHashes');
|
|
129
|
+
}
|
|
130
|
+
const manifest = parsed;
|
|
131
|
+
const sections = {};
|
|
132
|
+
const declared = Object.keys(manifest.sectionHashes);
|
|
133
|
+
for (const sid of declared) {
|
|
134
|
+
if (!LAYOUT_SECTION_IDS.includes(sid)) {
|
|
135
|
+
throw new Error(`manifest 声明了布局不支持的分区 ${sid}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// JSON 分区
|
|
139
|
+
for (const [sid, rel] of Object.entries(SECTION_JSON_PATHS)) {
|
|
140
|
+
if (!(sid in manifest.sectionHashes))
|
|
141
|
+
continue;
|
|
142
|
+
const abs = joinFs(dir, rel);
|
|
143
|
+
if (!(await fsx.exists(abs))) {
|
|
144
|
+
throw new Error(`快照缺少 JSON 分区文件 ${rel}(${sid})`);
|
|
145
|
+
}
|
|
146
|
+
sections[sid] = parseJsonSafe(Buffer.from(await fsx.readFile(abs)).toString('utf8'));
|
|
147
|
+
}
|
|
148
|
+
// 文件类分区
|
|
149
|
+
for (const [sid, prefix] of Object.entries(SECTION_FILE_PREFIXES)) {
|
|
150
|
+
if (!(sid in manifest.sectionHashes))
|
|
151
|
+
continue;
|
|
152
|
+
const baseAbs = joinFs(dir, prefix);
|
|
153
|
+
if (!(await fsx.isDir(baseAbs))) {
|
|
154
|
+
throw new Error(`快照缺少文件分区目录 ${prefix}(${sid})`);
|
|
155
|
+
}
|
|
156
|
+
const rels = await listSnapshotFiles(fsx, baseAbs);
|
|
157
|
+
const files = [];
|
|
158
|
+
for (const rel of rels) {
|
|
159
|
+
const data = await fsx.readFile(joinFs(baseAbs, rel));
|
|
160
|
+
files.push({ relativePath: rel, data, contentHash: sha256Hex(data) });
|
|
161
|
+
}
|
|
162
|
+
sections[sid] = { version: 1, files };
|
|
163
|
+
}
|
|
164
|
+
return { id: manifest.id, createdAt: manifest.createdAt, manifest: manifest.manifest, sections };
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/sync/layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const SYNC_CONFIG_FILE = "sync-config.json";
|
|
2
|
+
/** 持久化的仓库配置(不含任何凭据) */
|
|
3
|
+
export interface SyncConfig {
|
|
4
|
+
repoUrl: string;
|
|
5
|
+
gitBin?: string;
|
|
6
|
+
}
|
|
7
|
+
/** 读取仓库配置;文件不存在/损坏 → null(视为未配置,UI 显示空表单) */
|
|
8
|
+
export declare function readSyncConfig(dir: string): Promise<SyncConfig | null>;
|
|
9
|
+
/** 保存仓库配置(自动创建目录;覆盖旧值) */
|
|
10
|
+
export declare function writeSyncConfig(dir: string, cfg: SyncConfig): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* 仓库地址合法性校验(返回错误消息;null = 合法)。
|
|
13
|
+
* 安全约束:token 永不拼入 repoUrl —— http(s) 地址带 userinfo(username[:password]@)直接拒绝,
|
|
14
|
+
* 引导用户把 token 放凭据字段(DSH credentials),避免 token 经 URL 泄漏进 git 历史/日志。
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateRepoUrl(repoUrl: string): string | null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-sync-ui:同步仓库配置的持久化(sync-config.json)。
|
|
3
|
+
*
|
|
4
|
+
* 与 sync-state.json 的分工:sync-state 记录「同步了哪些分区、何时同步」(t4 拥有),
|
|
5
|
+
* 本文件只记录「上次使用的仓库配置」(repoUrl / gitBin),供 UI 打开设置页时回填表单。
|
|
6
|
+
* - repoUrl 不含 token(token 只存 DSH credentials,见 src/index.ts SYNC_CREDENTIAL_REF);
|
|
7
|
+
* - 文件位于同步状态目录($DSH_HOME/dsh-config-manager/sync/),非敏感(可回显给浏览器);
|
|
8
|
+
* - 纯逻辑 + 可注入 fs(node:fs/promises 直用;测试走临时目录)。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { parseJsonSafe, stringifyJsonSafe } from "../utils/json.js";
|
|
13
|
+
export const SYNC_CONFIG_FILE = 'sync-config.json';
|
|
14
|
+
/** 读取仓库配置;文件不存在/损坏 → null(视为未配置,UI 显示空表单) */
|
|
15
|
+
export async function readSyncConfig(dir) {
|
|
16
|
+
const file = path.join(dir, SYNC_CONFIG_FILE);
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = await fs.readFile(file, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const parsed = parseJsonSafe(raw);
|
|
25
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
26
|
+
return null;
|
|
27
|
+
const obj = parsed;
|
|
28
|
+
if (typeof obj['repoUrl'] !== 'string' || obj['repoUrl'] === '')
|
|
29
|
+
return null;
|
|
30
|
+
const cfg = { repoUrl: obj['repoUrl'] };
|
|
31
|
+
if (typeof obj['gitBin'] === 'string' && obj['gitBin'] !== '')
|
|
32
|
+
cfg.gitBin = obj['gitBin'];
|
|
33
|
+
return cfg;
|
|
34
|
+
}
|
|
35
|
+
/** 保存仓库配置(自动创建目录;覆盖旧值) */
|
|
36
|
+
export async function writeSyncConfig(dir, cfg) {
|
|
37
|
+
await fs.mkdir(dir, { recursive: true });
|
|
38
|
+
await fs.writeFile(path.join(dir, SYNC_CONFIG_FILE), stringifyJsonSafe(cfg, { space: 2 }), 'utf8');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 仓库地址合法性校验(返回错误消息;null = 合法)。
|
|
42
|
+
* 安全约束:token 永不拼入 repoUrl —— http(s) 地址带 userinfo(username[:password]@)直接拒绝,
|
|
43
|
+
* 引导用户把 token 放凭据字段(DSH credentials),避免 token 经 URL 泄漏进 git 历史/日志。
|
|
44
|
+
*/
|
|
45
|
+
export function validateRepoUrl(repoUrl) {
|
|
46
|
+
if (typeof repoUrl !== 'string' || repoUrl.trim() === '') {
|
|
47
|
+
return 'repoUrl is required';
|
|
48
|
+
}
|
|
49
|
+
const url = repoUrl.trim();
|
|
50
|
+
if (/\s/.test(url)) {
|
|
51
|
+
return '仓库地址不能包含空白字符';
|
|
52
|
+
}
|
|
53
|
+
if (/^https?:\/\//i.test(url)) {
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = new URL(url);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return `无法解析仓库地址: ${url}`;
|
|
60
|
+
}
|
|
61
|
+
if (parsed.username !== '' || parsed.password !== '') {
|
|
62
|
+
return '请勿在仓库地址中包含用户名/密码(认证 token 请使用凭据字段,不会拼入地址)';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=sync-config.js.map
|