@max-null/dsh-plugin-center 0.2.9 → 0.2.11

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/client.js CHANGED
@@ -408,9 +408,11 @@ var STRINGS = {
408
408
  installFailed: "\u5B89\u88C5\u5931\u8D25\uFF1A{e}",
409
409
  installNotApplied: "\u5B89\u88C5\u672A\u751F\u6548",
410
410
  updatedOne: "\u5DF2\u66F4\u65B0 {n}\uFF0C\u91CD\u542F\u540E\u751F\u6548",
411
+ updatedHot: "\u5DF2\u66F4\u65B0 {n}\uFF0C\u524D\u7AEF\u5DF2\u70ED\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F",
411
412
  updateFailed: "\u66F4\u65B0\u5931\u8D25\uFF1A{e}",
412
413
  updateNotApplied: "\u66F4\u65B0\u672A\u751F\u6548",
413
414
  updatedMany: "\u5DF2\u66F4\u65B0 {n} \u4E2A\u63D2\u4EF6\uFF0C\u91CD\u542F\u540E\u751F\u6548",
415
+ updatedManyHot: "\u5DF2\u66F4\u65B0 {n} \u4E2A\u63D2\u4EF6\uFF0C\u524D\u7AEF\u5DF2\u70ED\u751F\u6548",
414
416
  commandTitle: "\u5728\u7EC8\u7AEF\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4",
415
417
  commandHint: "\u5F53\u524D\u5E94\u7528\u6B63\u5728\u8FD0\u884C\uFF0C\u88AB\u9501\u5B9A\u7684\u6587\u4EF6\u65E0\u6CD5\u5728\u5E94\u7528\u5185\u66FF\u6362\u3002\u8BF7\u5148\u5173\u95ED\u5E94\u7528\uFF0C\u518D\u5728\u7EC8\u7AEF\u6267\u884C\uFF1A",
416
418
  commandCopy: "\u590D\u5236\u547D\u4EE4",
@@ -493,9 +495,11 @@ var STRINGS = {
493
495
  installFailed: "Install failed: {e}",
494
496
  installNotApplied: "Install did not take effect",
495
497
  updatedOne: "Updated {n}; restart to take effect",
498
+ updatedHot: "Updated {n}; hot-applied in the browser, no restart needed",
496
499
  updateFailed: "Update failed: {e}",
497
500
  updateNotApplied: "Update did not take effect",
498
501
  updatedMany: "Updated {n} plugin(s); restart to take effect",
502
+ updatedManyHot: "Updated {n} plugin(s); hot-applied, no restart needed",
499
503
  commandTitle: "Run this command in a terminal",
500
504
  commandHint: "The app is running and locked files cannot be replaced in-place. Close the app first, then run:",
501
505
  commandCopy: "Copy command",
@@ -1143,6 +1147,11 @@ function CenterPanel({ variant = "section" }) {
1143
1147
  setCopied(false);
1144
1148
  return;
1145
1149
  }
1150
+ if (value.hot === true) {
1151
+ showToast(t("updatedHot", { n: name }) + (value.durationMs !== void 0 ? `\uFF08${(value.durationMs / 1e3).toFixed(1)}s\uFF09` : ""), "ok", 5e3);
1152
+ refreshUpdates(true);
1153
+ return;
1154
+ }
1146
1155
  if (value.pending === true) {
1147
1156
  pendingInstall.add(name);
1148
1157
  readyPending.current.push(name);
@@ -1173,6 +1182,7 @@ function CenterPanel({ variant = "section" }) {
1173
1182
  readyPending.current = [];
1174
1183
  const commands = [];
1175
1184
  let okCount = 0;
1185
+ let hotCount = 0;
1176
1186
  const okNames = [];
1177
1187
  const failures = [];
1178
1188
  for (const u of updates) {
@@ -1186,6 +1196,11 @@ function CenterPanel({ variant = "section" }) {
1186
1196
  commands.push(value.command);
1187
1197
  continue;
1188
1198
  }
1199
+ if (value.hot === true) {
1200
+ hotCount++;
1201
+ okNames.push(value.durationMs !== void 0 ? `${u.name}\uFF08${(value.durationMs / 1e3).toFixed(1)}s\uFF09` : u.name);
1202
+ continue;
1203
+ }
1189
1204
  if (value.pending === true) {
1190
1205
  readyPending.current.push(u.name);
1191
1206
  okNames.push(`${u.name}\uFF08\u5F85\u91CD\u542F\uFF09`);
@@ -1214,6 +1229,8 @@ function CenterPanel({ variant = "section" }) {
1214
1229
  }
1215
1230
  if (failures.length > 0) {
1216
1231
  showToast(t("updateSummary", { a: okCount, b: failures.length, c: failures.join("\uFF1B") }), "error", 15e3);
1232
+ } else if (commands.length === 0 && readyPending.current.length === 0 && hotCount === okCount) {
1233
+ showToast(t("updatedManyHot", { n: okCount }) + `\uFF08${okNames.join("\u3001")}\uFF09`, "ok", 6e3);
1217
1234
  } else if (commands.length === 0 && readyPending.current.length === 0) {
1218
1235
  showToast(t("updatedMany", { n: okCount }) + `\uFF08${okNames.join("\u3001")}\uFF09`, "ok", 6e3);
1219
1236
  }
@@ -1400,6 +1417,7 @@ function WhatsNewDialog() {
1400
1417
  const updateNow = async () => {
1401
1418
  setBusy(true);
1402
1419
  let okCount = 0;
1420
+ let hotCount = 0;
1403
1421
  const failures = [];
1404
1422
  for (const u of whatsNewDigests) {
1405
1423
  try {
@@ -1411,6 +1429,10 @@ function WhatsNewDialog() {
1411
1429
  continue;
1412
1430
  }
1413
1431
  okCount++;
1432
+ if (value.hot === true) {
1433
+ hotCount++;
1434
+ continue;
1435
+ }
1414
1436
  if (value.pending === true || value.direct === true) {
1415
1437
  pendingInstall.add(u.name);
1416
1438
  markDoneUpdate({ name: u.name, fromVersion: u.fromVersion, toVersion: u.toVersion });
@@ -1421,7 +1443,7 @@ function WhatsNewDialog() {
1421
1443
  }
1422
1444
  setBusy(false);
1423
1445
  if (failures.length === 0) {
1424
- showToast(t("updatedMany", { n: okCount }), "ok", 6e3);
1446
+ showToast(hotCount === okCount ? t("updatedManyHot", { n: okCount }) : t("updatedMany", { n: okCount }), "ok", 6e3);
1425
1447
  closeWhatsNew();
1426
1448
  } else {
1427
1449
  showToast(t("updateSummary", { a: okCount, b: failures.length, c: failures.join("\uFF1B") }), "error", 8e3);
package/dist/rpc.js CHANGED
@@ -50,6 +50,7 @@ export class PluginCenterRpc extends Service {
50
50
  direct: result.direct === true,
51
51
  pending: result.pending === true,
52
52
  command: result.command,
53
+ hot: result.hot === true,
53
54
  },
54
55
  };
55
56
  }
package/dist/update.d.ts CHANGED
@@ -26,6 +26,10 @@ export interface PnpmResult {
26
26
  /** True when the update was installed DIRECTLY (files on disk now; a
27
27
  * restart makes the running DSH pick them up), no locks were hit. */
28
28
  direct?: boolean;
29
+ /** True when the update changed no host-side files (only the browser
30
+ * bundle, which client-hmr hot-swaps): no restart is needed. Absent when
31
+ * the package dir is missing or the snapshot could not be taken. */
32
+ hot?: boolean;
29
33
  /** True when the update was only PREPARED (downloaded to the pending dir)
30
34
  * and the real install is queued for the next SSiD/DSH startup. */
31
35
  pending?: boolean;
@@ -84,6 +88,22 @@ export declare function pnpmCommandCandidates(profileDir: string): string[];
84
88
  export declare function runPnpm(args: readonly string[], cwd: string): Promise<PnpmResult>;
85
89
  /** Install a package into the web profile, mirroring `dsh plugin add` semantics. */
86
90
  export declare function installPlugin(packageSpec: string, profileDir: string): Promise<PnpmResult>;
91
+ /** One host-side file identity: relative path + content hash. */
92
+ export interface FileIdentity {
93
+ path: string;
94
+ hash: string;
95
+ }
96
+ /** Hash every host-runtime file under a package dir, excluding the browser
97
+ * bundle (hot-swappable via client-hmr) and doc/type artifacts. The installed
98
+ * package lives inside the running profile's node_modules, so the diff tells
99
+ * whether the update touched only bundle files or also host code. The
100
+ * package.json version bump is normalized away — installed-version metadata
101
+ * changes on every update and has no runtime effect. Returns null when the
102
+ * dir is absent or unreadable.
103
+ */
104
+ export declare function snapshotHostFiles(pkgDir: string): FileIdentity[] | null;
105
+ /** True when the two snapshots disagree (host-side code changed). */
106
+ export declare function hostFilesChanged(before: FileIdentity[], after: FileIdentity[]): boolean;
87
107
  /**
88
108
  * Update a package to the given version, mirroring `dsh plugin add <pkg>`.
89
109
  * The exact version is required: with a bare package name, pnpm 11's
package/dist/update.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * first (many community repos ship no release/tag/CHANGELOG — verified §7.2).
5
5
  */
6
6
  import { spawn } from 'node:child_process';
7
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
8
+ import { createHash } from 'node:crypto';
8
9
  import { homedir } from 'node:os';
9
10
  import { join } from 'node:path';
10
11
  import { compareVersions, satisfies } from "./semver.js";
@@ -275,6 +276,15 @@ export async function runPnpm(args, cwd) {
275
276
  if (last.ok)
276
277
  break;
277
278
  }
279
+ // 自更新鸡生蛋兜底提示:所有候选都失败且是 store 版本不匹配时,把可复制
280
+ // 的手动命令放进 detail,用户按提示在 profile 目录执行即可(2026-08-23
281
+ // 另一台电脑 v10/v11 实测——旧 host 无自动兜底,需要一条显式命令)。
282
+ if (!last.ok && last.detail.includes('ERR_PNPM_UNEXPECTED_STORE')) {
283
+ const major = detectStoreMajor(cwd);
284
+ if (major !== undefined) {
285
+ last.detail += `\n\nHint: 在 profile 目录(${cwd})执行:\n npx --yes pnpm@${major} ${args.join(' ')}`;
286
+ }
287
+ }
278
288
  logPnpm(cwd, args, last);
279
289
  return last;
280
290
  }
@@ -283,6 +293,125 @@ export async function installPlugin(packageSpec, profileDir) {
283
293
  // `-w` is required: every profile ships a pnpm-workspace.yaml.
284
294
  return runPnpm(['add', '-w', packageSpec], profileDir);
285
295
  }
296
+ /** Resolve `exports["./client"]` exactly like client-modules does (string or
297
+ * { default }): the browser bundle path, relative to the package dir. */
298
+ function clientBundleRel(pkgDir) {
299
+ try {
300
+ const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
301
+ const exp = pkg.exports;
302
+ if (exp !== null && typeof exp === 'object') {
303
+ const client = exp['./client'];
304
+ if (typeof client === 'string')
305
+ return client.replace(/^\.\//, '');
306
+ if (client !== null && typeof client === 'object') {
307
+ const d = client.default;
308
+ if (typeof d === 'string')
309
+ return d.replace(/^\.\//, '');
310
+ }
311
+ }
312
+ }
313
+ catch { /* unreadable package.json: fall through to name-based rules */ }
314
+ return null;
315
+ }
316
+ /** True when the file never needs a host restart: the browser bundle (exact
317
+ * exports path or legacy name), source maps, type declarations, docs/assets,
318
+ * images. Conservative direction: a missed exclusion only adds a restart
319
+ * (false negative), never removes one (false positive). */
320
+ function snapshotExcluded(rel, isDir, clientRels) {
321
+ // join() produces backslashes on Windows; normalize for set membership.
322
+ const norm = rel.replace(/\\/g, '/');
323
+ const name = norm.split('/').pop();
324
+ if (name === 'node_modules' || name === 'docs' || name === 'assets')
325
+ return true;
326
+ if (isDir)
327
+ return false;
328
+ if (clientRels.has(norm))
329
+ return true;
330
+ if (name === 'client.js' || name === 'client.mjs')
331
+ return true;
332
+ if (name.endsWith('.map') || name.endsWith('.d.ts'))
333
+ return true;
334
+ if (/[.](png|jpe?g|gif|webp|svg|ico)$/i.test(name))
335
+ return true;
336
+ if (/^(README|LICENSE|CHANGELOG|UNLICENSE)(\..*)?$/i.test(name))
337
+ return true;
338
+ return false;
339
+ }
340
+ /** Hash every host-runtime file under a package dir, excluding the browser
341
+ * bundle (hot-swappable via client-hmr) and doc/type artifacts. The installed
342
+ * package lives inside the running profile's node_modules, so the diff tells
343
+ * whether the update touched only bundle files or also host code. The
344
+ * package.json version bump is normalized away — installed-version metadata
345
+ * changes on every update and has no runtime effect. Returns null when the
346
+ * dir is absent or unreadable.
347
+ */
348
+ export function snapshotHostFiles(pkgDir) {
349
+ try {
350
+ if (!existsSync(pkgDir) || !statSync(pkgDir).isDirectory())
351
+ return null;
352
+ }
353
+ catch {
354
+ return null;
355
+ }
356
+ const clientRels = new Set();
357
+ const clientRel = clientBundleRel(pkgDir);
358
+ if (clientRel !== null)
359
+ clientRels.add(clientRel);
360
+ const out = [];
361
+ const walk = (dir, prefix) => {
362
+ let entries = [];
363
+ try {
364
+ entries = readdirSync(dir);
365
+ }
366
+ catch {
367
+ return;
368
+ }
369
+ for (const name of entries) {
370
+ const full = join(dir, name);
371
+ const rel = prefix === '' ? name : join(prefix, name);
372
+ let st;
373
+ try {
374
+ st = statSync(full);
375
+ }
376
+ catch {
377
+ continue;
378
+ }
379
+ if (snapshotExcluded(rel, st.isDirectory(), clientRels))
380
+ continue;
381
+ if (st.isDirectory()) {
382
+ walk(full, rel);
383
+ continue;
384
+ }
385
+ try {
386
+ let data = readFileSync(full);
387
+ if (name === 'package.json') {
388
+ // Installed-version metadata changes on every update; strip it so a
389
+ // pure bundle update still compares equal.
390
+ const pkg = JSON.parse(data.toString('utf8'));
391
+ delete pkg.version;
392
+ data = Buffer.from(JSON.stringify(pkg), 'utf8');
393
+ }
394
+ const hash = createHash('sha256').update(data).digest('hex');
395
+ out.push({ path: rel, hash });
396
+ }
397
+ catch { /* unreadable file: ignore */ }
398
+ }
399
+ };
400
+ walk(pkgDir, '');
401
+ return out;
402
+ }
403
+ /** True when the two snapshots disagree (host-side code changed). */
404
+ export function hostFilesChanged(before, after) {
405
+ const a = new Map(before.map(i => [i.path, i.hash]));
406
+ const b = new Map(after.map(i => [i.path, i.hash]));
407
+ if (a.size !== b.size)
408
+ return true;
409
+ for (const [path, hash] of a) {
410
+ if (b.get(path) !== hash)
411
+ return true;
412
+ }
413
+ return false;
414
+ }
286
415
  /**
287
416
  * Update a package to the given version, mirroring `dsh plugin add <pkg>`.
288
417
  * The exact version is required: with a bare package name, pnpm 11's
@@ -292,5 +421,14 @@ export async function installPlugin(packageSpec, profileDir) {
292
421
  * `minimumReleaseAgeExclude` itself (2026-08-18, reproduced in-process).
293
422
  */
294
423
  export async function updatePlugin(packageName, version, profileDir) {
295
- return runPnpm(['add', '-w', `${packageName}@${version}`], profileDir);
424
+ const pkgDir = join(profileDir, 'node_modules', packageName);
425
+ const before = snapshotHostFiles(pkgDir);
426
+ const result = await runPnpm(['add', '-w', `${packageName}@${version}`], profileDir);
427
+ if (result.ok && before !== null && before.length > 0) {
428
+ const after = snapshotHostFiles(pkgDir);
429
+ if (after !== null && !hostFilesChanged(before, after)) {
430
+ result.hot = true;
431
+ }
432
+ }
433
+ return result;
296
434
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-plugin-center",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Plugin center for DeepSeek Harness 閳?installed metadata, community market, update detection, and What\u0027s New",
5
5
  "license": "MIT",
6
6
  "publishConfig": {