@pandablessing/dsh-user-sync 1.0.4 → 1.0.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 +124 -106
- package/lib/client.js +534 -505
- package/lib/index.js +130 -2
- package/lib/scan.js +13 -0
- package/lib/sync.js +10 -0
- package/package.json +9 -7
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -201,6 +201,117 @@ export function apply(ctx) {
|
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
203
|
let pluginManager = null;
|
|
204
|
+
/** 运行时 pnpm 的 node + pnpm.cjs 路径;找不到时返回 null。 */
|
|
205
|
+
const pnpmRuntime = () => {
|
|
206
|
+
const runtime = findRuntimeNode();
|
|
207
|
+
if (!runtime)
|
|
208
|
+
return null;
|
|
209
|
+
const pnpmCjs = join(dirname(runtime), '..', '..', 'pnpm', 'bin', 'pnpm.cjs');
|
|
210
|
+
return existsSync(pnpmCjs) ? { node: runtime, pnpmCjs } : null;
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* 按 profile 的 package.json 安装依赖。
|
|
214
|
+
* `link:` / `file:` 指向不存在的本地路径时,pnpm install 会整体失败(连带普通包也装不上),
|
|
215
|
+
* 因此安装期间临时把这些坏 link 从 package.json 摘掉,装完原样恢复。
|
|
216
|
+
*/
|
|
217
|
+
const installProfileDependencies = async () => {
|
|
218
|
+
const rt = pnpmRuntime();
|
|
219
|
+
if (!rt)
|
|
220
|
+
throw new Error('runtime node/pnpm not found');
|
|
221
|
+
const pkgPath = join(paths.profileDir, 'package.json');
|
|
222
|
+
const workspacePath = join(paths.profileDir, 'pnpm-workspace.yaml');
|
|
223
|
+
const raw = readFileSync(pkgPath, 'utf8');
|
|
224
|
+
const removedLinks = [];
|
|
225
|
+
let workspaceRaw = null;
|
|
226
|
+
const missingPatchLines = [];
|
|
227
|
+
try {
|
|
228
|
+
const pkg = JSON.parse(raw);
|
|
229
|
+
const dependencies = pkg.dependencies ?? {};
|
|
230
|
+
// pnpm-workspace.yaml 的 patchedDependencies 指向 patches/*.patch;文件缺失时 pnpm install 会整体失败
|
|
231
|
+
try {
|
|
232
|
+
workspaceRaw = readFileSync(workspacePath, 'utf8');
|
|
233
|
+
for (const line of workspaceRaw.split('\n')) {
|
|
234
|
+
const match = line.match(/^\s+'?([^':]+)'?@[^:]+:\s*patches\/(.+?)\s*$/);
|
|
235
|
+
if (match && !existsSync(join(paths.profileDir, 'patches', match[2])))
|
|
236
|
+
missingPatchLines.push(line);
|
|
237
|
+
}
|
|
238
|
+
if (missingPatchLines.length > 0) {
|
|
239
|
+
const patched = workspaceRaw
|
|
240
|
+
.split('\n')
|
|
241
|
+
.filter((line) => !missingPatchLines.includes(line))
|
|
242
|
+
.join('\n');
|
|
243
|
+
writeFileSync(workspacePath, patched, 'utf8');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
workspaceRaw = null;
|
|
248
|
+
}
|
|
249
|
+
const brokenLinks = Object.entries(dependencies).filter(([, spec]) => {
|
|
250
|
+
if (!spec.startsWith('link:') && !spec.startsWith('file:'))
|
|
251
|
+
return false;
|
|
252
|
+
const target = join(paths.profileDir, spec.slice(5));
|
|
253
|
+
return !existsSync(target);
|
|
254
|
+
});
|
|
255
|
+
if (brokenLinks.length > 0) {
|
|
256
|
+
const patched = { ...pkg, dependencies: { ...dependencies } };
|
|
257
|
+
for (const [name] of brokenLinks) {
|
|
258
|
+
delete patched.dependencies[name];
|
|
259
|
+
removedLinks.push(name);
|
|
260
|
+
}
|
|
261
|
+
writeFileSync(pkgPath, `${JSON.stringify(patched, null, 2)}\n`, 'utf8');
|
|
262
|
+
}
|
|
263
|
+
await runCommand(rt.node, [rt.pnpmCjs, 'install', '--force'], { cwd: paths.profileDir, timeout: 300_000 });
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (removedLinks.length > 0)
|
|
267
|
+
writeFileSync(pkgPath, raw, 'utf8');
|
|
268
|
+
if (workspaceRaw !== null && missingPatchLines?.length > 0)
|
|
269
|
+
writeFileSync(join(paths.profileDir, 'pnpm-workspace.yaml'), workspaceRaw, 'utf8');
|
|
270
|
+
}
|
|
271
|
+
return { removedLinks };
|
|
272
|
+
};
|
|
273
|
+
/** 缺失插件落盘校验:pnpm 的 .modules.yaml 可能残留记录,缺失时再跑一次 pnpm install。 */
|
|
274
|
+
const forceReinstallIfMissing = async (names) => {
|
|
275
|
+
const missing = names.filter((name) => !existsSync(join(paths.profileDir, 'node_modules', name)));
|
|
276
|
+
if (missing.length === 0)
|
|
277
|
+
return;
|
|
278
|
+
try {
|
|
279
|
+
await installProfileDependencies();
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
log('pnpm-force', error);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
/** 把远端包声明的依赖装上:先按 package.json 整体安装,再逐个 installBundle 兜底,link/file 依赖只提示手动处理。 */
|
|
286
|
+
const ensurePluginsInstalled = async (pm, dependencies) => {
|
|
287
|
+
const plan = planPluginRecovery({ dependencies, installed: [] });
|
|
288
|
+
let removedLinks = [];
|
|
289
|
+
try {
|
|
290
|
+
const result = await installProfileDependencies();
|
|
291
|
+
removedLinks = result.removedLinks;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
log('install-dependencies', error);
|
|
295
|
+
}
|
|
296
|
+
// 安装后重新判定:DSH 的 listBundles 只要声明在就算 installed,实际落盘要看 node_modules
|
|
297
|
+
const bundles = await pm.listBundles();
|
|
298
|
+
const installedNow = bundles
|
|
299
|
+
.filter((bundle) => bundle.installed)
|
|
300
|
+
.map((bundle) => bundle.name)
|
|
301
|
+
.filter((name) => existsSync(join(paths.profileDir, 'node_modules', name)));
|
|
302
|
+
const stillMissing = planPluginRecovery({ dependencies, installed: installedNow }).missing;
|
|
303
|
+
const results = await installMissing({
|
|
304
|
+
plan: { missing: stillMissing },
|
|
305
|
+
installBundle: (spec) => pm.installBundle(spec),
|
|
306
|
+
});
|
|
307
|
+
await forceReinstallIfMissing(stillMissing.map((item) => item.name));
|
|
308
|
+
const installed = plan.missing.map((item) => ({
|
|
309
|
+
name: item.name,
|
|
310
|
+
ok: existsSync(join(paths.profileDir, 'node_modules', item.name)),
|
|
311
|
+
error: results.find((result) => result.name === item.name)?.error,
|
|
312
|
+
}));
|
|
313
|
+
return { installed, linkDeps: plan.linkDeps.map(({ name, spec }) => ({ name, spec })) };
|
|
314
|
+
};
|
|
204
315
|
const routes = [
|
|
205
316
|
{
|
|
206
317
|
// 最小同步路由:用于区分「同步 handler 本身不可用」与「status/scan 具体逻辑抛错」
|
|
@@ -484,7 +595,24 @@ export function apply(ctx) {
|
|
|
484
595
|
const body = (await request.json());
|
|
485
596
|
if (!isPackageId(String(body?.name ?? '')))
|
|
486
597
|
return json({ error: 'E_BAD_NAME' }, 400);
|
|
487
|
-
|
|
598
|
+
const result = (await sync.applyRemote(String(body.name)));
|
|
599
|
+
// 同步只恢复声明文件;应用后必须真正把插件装上,否则「清单同步了却没装」
|
|
600
|
+
const pm = pluginManager;
|
|
601
|
+
const manifest = result.manifest;
|
|
602
|
+
const dependencies = manifest?.dependencies ?? null;
|
|
603
|
+
if (pm && result.ok && dependencies && Object.keys(dependencies).length > 0) {
|
|
604
|
+
try {
|
|
605
|
+
const { installed, linkDeps } = await ensurePluginsInstalled(pm, dependencies);
|
|
606
|
+
result.installed = installed;
|
|
607
|
+
result.linkDeps = linkDeps;
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
log('apply-install', error);
|
|
611
|
+
result.installed = [];
|
|
612
|
+
result.installError = error?.message ?? String(error);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return json(result);
|
|
488
616
|
},
|
|
489
617
|
},
|
|
490
618
|
];
|
package/lib/scan.js
CHANGED
|
@@ -155,6 +155,19 @@ function collectProfileFiles({ dshHome, profileName, modules, ctx, }) {
|
|
|
155
155
|
if (modules.includes('plugins')) {
|
|
156
156
|
for (const name of ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'cordis.yml'])
|
|
157
157
|
push(name, 'plugins');
|
|
158
|
+
// pnpm-workspace.yaml 的 patchedDependencies 指向 patches/*.patch;
|
|
159
|
+
// 少了这些文件,换机后 pnpm install 会报 ERR_PNPM_PATCH_NOT_FOUND,插件全部装不上
|
|
160
|
+
let patchFiles = [];
|
|
161
|
+
try {
|
|
162
|
+
patchFiles = readdirSync(join(profileDir, 'patches'), { withFileTypes: true })
|
|
163
|
+
.filter((dirent) => dirent.isFile() && dirent.name.endsWith('.patch'))
|
|
164
|
+
.map((dirent) => dirent.name);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
patchFiles = [];
|
|
168
|
+
}
|
|
169
|
+
for (const name of patchFiles)
|
|
170
|
+
push(`patches/${name}`, 'plugins');
|
|
158
171
|
}
|
|
159
172
|
if (modules.includes('config')) {
|
|
160
173
|
push('cordis.patch.yml', 'config');
|
package/lib/sync.js
CHANGED
|
@@ -97,6 +97,15 @@ export function createSync({ dshHome, userHome, profileName = 'desktop', transpo
|
|
|
97
97
|
}
|
|
98
98
|
if (hits.length > 0)
|
|
99
99
|
return { ok: false, code: 'E_SENSITIVE', hits };
|
|
100
|
+
// 把 profile 的依赖声明写进 manifest:目标机应用后据此自动安装缺失插件
|
|
101
|
+
let dependencies = {};
|
|
102
|
+
try {
|
|
103
|
+
const pkg = JSON.parse(readFileSync(join(dshHome, 'profiles', profileName, 'package.json'), 'utf8'));
|
|
104
|
+
dependencies = pkg.dependencies ?? {};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
dependencies = {};
|
|
108
|
+
}
|
|
100
109
|
const packageId = makePackageId({ deviceId: currentDeviceId() });
|
|
101
110
|
const manifest = buildManifest(files, {
|
|
102
111
|
packageId,
|
|
@@ -106,6 +115,7 @@ export function createSync({ dshHome, userHome, profileName = 'desktop', transpo
|
|
|
106
115
|
arch: process.arch,
|
|
107
116
|
profile: profileName,
|
|
108
117
|
modules: modulesNow(),
|
|
118
|
+
dependencies,
|
|
109
119
|
});
|
|
110
120
|
const tarEntries = [
|
|
111
121
|
{ path: MANIFEST_PATH, data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8') },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pandablessing/dsh-user-sync",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "DeepSeek Harness 用户级配置同步插件",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -31,11 +31,6 @@
|
|
|
31
31
|
"platform": "web"
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsc -p tsconfig.json",
|
|
36
|
-
"build:server": "tsc -p server/tsconfig.json",
|
|
37
|
-
"test": "node --test test/*.test.js"
|
|
38
|
-
},
|
|
39
34
|
"license": "MulanPSL-2.0",
|
|
40
35
|
"keywords": [
|
|
41
36
|
"dsh",
|
|
@@ -59,5 +54,12 @@
|
|
|
59
54
|
},
|
|
60
55
|
"publishConfig": {
|
|
61
56
|
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json",
|
|
60
|
+
"build:server": "tsc -p server/tsconfig.json",
|
|
61
|
+
"test": "node --test test/*.test.js",
|
|
62
|
+
"publish:npm": "node scripts/publish.mjs",
|
|
63
|
+
"publish:dry": "node scripts/publish.mjs --dry-run"
|
|
62
64
|
}
|
|
63
|
-
}
|
|
65
|
+
}
|