@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/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
- return json(await sync.applyRemote(String(body.name)));
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.4",
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
+ }