dshmarket 1.21.1 → 1.21.2

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/routes.js CHANGED
@@ -31,7 +31,7 @@ import { checkUpdates, fetchNpmLatest, invalidateUpdates, isUpgrade, latestPubli
31
31
  import { createThemeManager } from './themes.js';
32
32
  import { readJsonBody, sameOrigin, sendJson } from './http.js';
33
33
  import { detectedSupervisor, restartAllowed, scheduleRestart, servingPort, trustedRestartRequest, trustedDownloadRequest } from './restart.js';
34
- import { activationAfterReplace, checkClientBundle, hasHostHalf, verifyActivation } from './verify.js';
34
+ import { activationAfterReplace, brokenClientBundles, checkClientBundle, hasHostHalf, newlyBrokenBundles, verifyActivation } from './verify.js';
35
35
  import { carrierDisableIds, disableRow, enableRow, findUserPatchPath, isProtectedModule, packagePatchFlags, readUserPatchState, removeRowBlocks, rowIdsForPackage, } from './patch.js';
36
36
  import { createProfileBackup, downloadWebdav, MAX_BACKUP_BYTES, mergeRestoreManifest, restoreProfileBackup, unportableDeps, uploadWebdav, } from './backup.js';
37
37
  import { createGist, fitsGistLimit, GistError, gistErrorCode, parseGistId, readGist, resolveGistTokenSource, updateGist, verifyGistToken, } from './gist.js';
@@ -1376,6 +1376,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
1376
1376
  // ghost/bumped entries that break every later pnpm run.
1377
1377
  pendingRollbacks.clear();
1378
1378
  const compatibilityBefore = assessProfile(config.profile, activeProfileDir);
1379
+ // pnpm re-extracts the whole tree on any operation, so a plugin
1380
+ // nobody touched can come back pristine-and-broken, or lose a
1381
+ // patch that was holding it together (#222). Only what THIS run
1382
+ // broke is attributable to it, so the profile is swept before as
1383
+ // well as after.
1384
+ const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir);
1379
1385
  const manifestBefore = readManifestDeps(config.profile, activeProfileDir);
1380
1386
  const result = await runPlugin(config.profile, addArgs);
1381
1387
  const cancelled = result.cancelled;
@@ -1458,7 +1464,10 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
1458
1464
  // See the install route: an update is the operation the #222
1459
1465
  // report actually hit.
1460
1466
  const bundleCheck = checkClientBundle(config.profile, name, activeProfileDir);
1461
- const brokenBundles = bundleCheck.ok ? [] : [{ name, reason: bundleCheck.reason ?? 'parse failed' }];
1467
+ const brokenBundles = newlyBrokenBundles(bundlesBefore, [
1468
+ ...(bundleCheck.ok ? [] : [{ name, reason: bundleCheck.reason ?? 'parse failed' }]),
1469
+ ...brokenClientBundles(config.profile, activeProfileDir),
1470
+ ].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index));
1462
1471
  if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
1463
1472
  compatibility = {
1464
1473
  code: 'soft-incompatible',
@@ -2240,6 +2249,12 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2240
2249
  before.delete(retryAlias);
2241
2250
  pendingRollbacks.clear();
2242
2251
  const compatibilityBefore = assessProfile(config.profile, activeProfileDir);
2252
+ // pnpm re-extracts the whole tree on any operation, so a plugin
2253
+ // nobody touched can come back pristine-and-broken, or lose a
2254
+ // patch that was holding it together (#222). Only what THIS run
2255
+ // broke is attributable to it, so the profile is swept before as
2256
+ // well as after.
2257
+ const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir);
2243
2258
  // RAW manifest snapshot for failure rollback (#65): pnpm writes
2244
2259
  // package.json before the build-script check / registry fetches
2245
2260
  // run, so a hard-failed add leaves ghost dependencies that break
@@ -2337,10 +2352,13 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2337
2352
  // one half-written or patch-mangled, and today that surfaces
2338
2353
  // as a blank settings page long after the install reported
2339
2354
  // success, with nothing connecting the two.
2340
- const brokenBundles = addedPackages
2341
- .map(pkg => ({ name: pkg, check: checkClientBundle(config.profile, pkg, activeProfileDir) }))
2342
- .filter(entry => !entry.check.ok)
2343
- .map(entry => ({ name: entry.name, reason: entry.check.reason ?? 'parse failed' }));
2355
+ const brokenBundles = newlyBrokenBundles(bundlesBefore, [
2356
+ ...addedPackages
2357
+ .map(pkg => ({ name: pkg, check: checkClientBundle(config.profile, pkg, activeProfileDir) }))
2358
+ .filter(entry => !entry.check.ok)
2359
+ .map(entry => ({ name: entry.name, reason: entry.check.reason ?? 'parse failed' })),
2360
+ ...brokenClientBundles(config.profile, activeProfileDir),
2361
+ ].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index));
2344
2362
  if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
2345
2363
  compatibility = {
2346
2364
  code: 'soft-incompatible',
@@ -102,19 +102,43 @@ export interface BundleCheck {
102
102
  reason: string | null;
103
103
  }
104
104
  /**
105
- * Whether a package's client bundle still parses as JavaScript (#222).
105
+ * Every installed plugin whose client bundle will not parse (#222 by
106
+ * @MicroMilo).
106
107
  *
107
- * pnpm can leave a half-written or patch-mangled bundle behind — the report
108
- * describes a profile whose client bundle was broken after an update. The
109
- * browser is where that surfaces today, as a blank settings page long after
110
- * the operation reported success, with nothing connecting the two.
108
+ * The per-package check above only ever looked at what an operation added,
109
+ * which misses the failure that was actually reported: pnpm re-extracts the
110
+ * WHOLE tree on any install, so updating one plugin can restore another
111
+ * plugin's pristine — and broken — bundle, or fail to re-apply a patch that
112
+ * was holding it together. The damage then surfaces at the next boot as
113
+ * "failed to load plugins", with nothing connecting it to the install that
114
+ * caused it.
111
115
  *
112
- * `vm.Script` COMPILES without executing: it catches the syntax damage this
113
- * is looking for and never runs plugin code, so a hostile bundle gains
114
- * nothing. A missing `dsh.client`, an unresolvable exports field, or a file
115
- * that is simply absent all return ok — this check only ever fires on a file
116
- * it actually read and actually failed to parse. Everything ambiguous stays
117
- * silent, because a false "your plugin is corrupt" is the one outcome worse
118
- * than not checking.
116
+ * Cheap enough to run on every operation: 0.40ms per plugin including the
117
+ * read (measured on a 385KB bundle), so a 30-plugin profile costs ~12ms
118
+ * against an install that takes seconds.
119
+ *
120
+ * Silent on ESM bundles, like the per-package check it calls — see there for
121
+ * why. Widening the sweep is exactly what would have turned that one false
122
+ * "corrupt" into one per ESM plugin in the profile.
123
+ *
124
+ * Callers compare a before-list with an after-list rather than reporting
125
+ * this one directly — a profile can carry a broken bundle indefinitely, and
126
+ * re-reporting a problem the user already had would put them in front of
127
+ * something this operation did not cause and cannot undo.
119
128
  */
129
+ export declare function brokenClientBundles(profile: string, explicitDir?: string): {
130
+ name: string;
131
+ reason: string;
132
+ }[];
133
+ /** Bundles broken after an operation that were intact before it. */
134
+ export declare function newlyBrokenBundles(before: readonly {
135
+ name: string;
136
+ reason: string;
137
+ }[], after: readonly {
138
+ name: string;
139
+ reason: string;
140
+ }[]): {
141
+ name: string;
142
+ reason: string;
143
+ }[];
120
144
  export declare function checkClientBundle(profile: string, name: string, explicitDir?: string): BundleCheck;
package/lib/verify.js CHANGED
@@ -27,7 +27,7 @@ import { readFileSync } from 'node:fs';
27
27
  import { Script } from 'node:vm';
28
28
  import { join } from 'node:path';
29
29
  import { listHotMounts, parseSimplePatch } from './hot.js';
30
- import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir } from './profile.js';
30
+ import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.js';
31
31
  /** The profile manifest's `dsh.profile.bundles` — what the CLI reconciled. */
32
32
  function readBundles(profile, explicitDir) {
33
33
  try {
@@ -328,6 +328,55 @@ export function clientBundlePath(exportsField, depth = 0) {
328
328
  * silent, because a false "your plugin is corrupt" is the one outcome worse
329
329
  * than not checking.
330
330
  */
331
+ /**
332
+ * V8's wording when a classic-script parse trips over module syntax.
333
+ *
334
+ * Matched on the message because there is no flag-free way to compile a
335
+ * module here (`SourceTextModule` needs --experimental-vm-modules, which the
336
+ * host process does not set). If V8 ever rewords these, the failure mode is
337
+ * the false positive we had before rather than a missed real break — the
338
+ * safer direction of the two.
339
+ */
340
+ const MODULE_SYNTAX_ERROR = /Unexpected token 'export'|Cannot use import statement outside a module|await is only valid in async functions and the top level bodies of modules/;
341
+ /**
342
+ * Every installed plugin whose client bundle will not parse (#222 by
343
+ * @MicroMilo).
344
+ *
345
+ * The per-package check above only ever looked at what an operation added,
346
+ * which misses the failure that was actually reported: pnpm re-extracts the
347
+ * WHOLE tree on any install, so updating one plugin can restore another
348
+ * plugin's pristine — and broken — bundle, or fail to re-apply a patch that
349
+ * was holding it together. The damage then surfaces at the next boot as
350
+ * "failed to load plugins", with nothing connecting it to the install that
351
+ * caused it.
352
+ *
353
+ * Cheap enough to run on every operation: 0.40ms per plugin including the
354
+ * read (measured on a 385KB bundle), so a 30-plugin profile costs ~12ms
355
+ * against an install that takes seconds.
356
+ *
357
+ * Silent on ESM bundles, like the per-package check it calls — see there for
358
+ * why. Widening the sweep is exactly what would have turned that one false
359
+ * "corrupt" into one per ESM plugin in the profile.
360
+ *
361
+ * Callers compare a before-list with an after-list rather than reporting
362
+ * this one directly — a profile can carry a broken bundle indefinitely, and
363
+ * re-reporting a problem the user already had would put them in front of
364
+ * something this operation did not cause and cannot undo.
365
+ */
366
+ export function brokenClientBundles(profile, explicitDir) {
367
+ const broken = [];
368
+ for (const name of Object.keys(readInstalled(profile, explicitDir))) {
369
+ const check = checkClientBundle(profile, name, explicitDir);
370
+ if (!check.ok)
371
+ broken.push({ name, reason: check.reason ?? 'parse failed' });
372
+ }
373
+ return broken;
374
+ }
375
+ /** Bundles broken after an operation that were intact before it. */
376
+ export function newlyBrokenBundles(before, after) {
377
+ const seen = new Set(before.map(entry => entry.name));
378
+ return after.filter(entry => !seen.has(entry.name));
379
+ }
331
380
  export function checkClientBundle(profile, name, explicitDir) {
332
381
  const root = join(profileDir(profile, explicitDir), 'node_modules', name);
333
382
  let manifest;
@@ -360,6 +409,18 @@ export function checkClientBundle(profile, name, explicitDir) {
360
409
  return { ok: true, reason: null };
361
410
  }
362
411
  catch (error) {
363
- return { ok: false, reason: error instanceof Error ? error.message : String(error) };
412
+ const message = error instanceof Error ? error.message : String(error);
413
+ // `new Script` compiles a CLASSIC script, so perfectly valid module
414
+ // syntax is a SyntaxError to it: `export`, `import`, and top-level
415
+ // `await` all throw. This package ships CJS, which is why nobody noticed
416
+ // — but a plugin whose client bundle is ESM was being told its file was
417
+ // corrupt, and offered a rollback for a file that is fine.
418
+ //
419
+ // That is the exact outcome this check's own contract forbids: a false
420
+ // "your plugin is corrupt" is worse than not checking. Module syntax
421
+ // means "cannot judge with this parser", not "broken".
422
+ if (MODULE_SYNTAX_ERROR.test(message))
423
+ return { ok: true, reason: null };
424
+ return { ok: false, reason: message };
364
425
  }
365
426
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dshmarket",
3
3
  "description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
4
- "version": "1.21.1",
4
+ "version": "1.21.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
package/src/routes.ts CHANGED
@@ -39,7 +39,7 @@ import { checkUpdates, fetchNpmLatest, invalidateUpdates, isUpgrade, latestPubli
39
39
  import { createThemeManager, type LoaderEntry } from './themes.ts'
40
40
  import { readJsonBody, sameOrigin, sendJson } from './http.ts'
41
41
  import { detectedSupervisor, restartAllowed, scheduleRestart, servingPort, trustedRestartRequest, trustedDownloadRequest } from './restart.ts'
42
- import { activationAfterReplace, checkClientBundle, hasHostHalf, verifyActivation } from './verify.ts'
42
+ import { activationAfterReplace, brokenClientBundles, checkClientBundle, hasHostHalf, newlyBrokenBundles, verifyActivation } from './verify.ts'
43
43
  import {
44
44
  carrierDisableIds, disableRow, enableRow, findUserPatchPath, isProtectedModule, packagePatchFlags,
45
45
  readUserPatchState, removeRowBlocks, rowIdsForPackage,
@@ -1423,6 +1423,12 @@ export function mountMarketRoutes(
1423
1423
  // ghost/bumped entries that break every later pnpm run.
1424
1424
  pendingRollbacks.clear()
1425
1425
  const compatibilityBefore = assessProfile(config.profile, activeProfileDir)
1426
+ // pnpm re-extracts the whole tree on any operation, so a plugin
1427
+ // nobody touched can come back pristine-and-broken, or lose a
1428
+ // patch that was holding it together (#222). Only what THIS run
1429
+ // broke is attributable to it, so the profile is swept before as
1430
+ // well as after.
1431
+ const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir)
1426
1432
  const manifestBefore = readManifestDeps(config.profile, activeProfileDir)
1427
1433
  const result = await runPlugin(config.profile, addArgs)
1428
1434
  const cancelled = result.cancelled
@@ -1508,7 +1514,13 @@ export function mountMarketRoutes(
1508
1514
  // See the install route: an update is the operation the #222
1509
1515
  // report actually hit.
1510
1516
  const bundleCheck = checkClientBundle(config.profile, name, activeProfileDir)
1511
- const brokenBundles = bundleCheck.ok ? [] : [{ name, reason: bundleCheck.reason ?? 'parse failed' }]
1517
+ const brokenBundles = newlyBrokenBundles(
1518
+ bundlesBefore,
1519
+ [
1520
+ ...(bundleCheck.ok ? [] : [{ name, reason: bundleCheck.reason ?? 'parse failed' }]),
1521
+ ...brokenClientBundles(config.profile, activeProfileDir),
1522
+ ].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index),
1523
+ )
1512
1524
  if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
1513
1525
  compatibility = {
1514
1526
  code: 'soft-incompatible',
@@ -2294,6 +2306,12 @@ export function mountMarketRoutes(
2294
2306
  if (retryAlias !== null) before.delete(retryAlias)
2295
2307
  pendingRollbacks.clear()
2296
2308
  const compatibilityBefore = assessProfile(config.profile, activeProfileDir)
2309
+ // pnpm re-extracts the whole tree on any operation, so a plugin
2310
+ // nobody touched can come back pristine-and-broken, or lose a
2311
+ // patch that was holding it together (#222). Only what THIS run
2312
+ // broke is attributable to it, so the profile is swept before as
2313
+ // well as after.
2314
+ const bundlesBefore = brokenClientBundles(config.profile, activeProfileDir)
2297
2315
  // RAW manifest snapshot for failure rollback (#65): pnpm writes
2298
2316
  // package.json before the build-script check / registry fetches
2299
2317
  // run, so a hard-failed add leaves ghost dependencies that break
@@ -2386,10 +2404,16 @@ export function mountMarketRoutes(
2386
2404
  // one half-written or patch-mangled, and today that surfaces
2387
2405
  // as a blank settings page long after the install reported
2388
2406
  // success, with nothing connecting the two.
2389
- const brokenBundles = addedPackages
2390
- .map(pkg => ({ name: pkg, check: checkClientBundle(config.profile, pkg, activeProfileDir) }))
2391
- .filter(entry => !entry.check.ok)
2392
- .map(entry => ({ name: entry.name, reason: entry.check.reason ?? 'parse failed' }))
2407
+ const brokenBundles = newlyBrokenBundles(
2408
+ bundlesBefore,
2409
+ [
2410
+ ...addedPackages
2411
+ .map(pkg => ({ name: pkg, check: checkClientBundle(config.profile, pkg, activeProfileDir) }))
2412
+ .filter(entry => !entry.check.ok)
2413
+ .map(entry => ({ name: entry.name, reason: entry.check.reason ?? 'parse failed' })),
2414
+ ...brokenClientBundles(config.profile, activeProfileDir),
2415
+ ].filter((entry, index, all) => all.findIndex(other => other.name === entry.name) === index),
2416
+ )
2393
2417
  if (risks.length > 0 || shadowed.length > 0 || brokenBundles.length > 0) {
2394
2418
  compatibility = {
2395
2419
  code: 'soft-incompatible',
package/src/verify.ts CHANGED
@@ -28,7 +28,7 @@ import { readFileSync } from 'node:fs'
28
28
  import { Script } from 'node:vm'
29
29
  import { join } from 'node:path'
30
30
  import { listHotMounts, parseSimplePatch } from './hot.ts'
31
- import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir } from './profile.ts'
31
+ import { bundlePatchInsertedIds, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.ts'
32
32
 
33
33
  export type ActivationState = 'live' | 'restart' | 'inert' | 'broken' | 'missing' | 'disabled'
34
34
 
@@ -367,6 +367,60 @@ export interface BundleCheck {
367
367
  * silent, because a false "your plugin is corrupt" is the one outcome worse
368
368
  * than not checking.
369
369
  */
370
+ /**
371
+ * V8's wording when a classic-script parse trips over module syntax.
372
+ *
373
+ * Matched on the message because there is no flag-free way to compile a
374
+ * module here (`SourceTextModule` needs --experimental-vm-modules, which the
375
+ * host process does not set). If V8 ever rewords these, the failure mode is
376
+ * the false positive we had before rather than a missed real break — the
377
+ * safer direction of the two.
378
+ */
379
+ const MODULE_SYNTAX_ERROR = /Unexpected token 'export'|Cannot use import statement outside a module|await is only valid in async functions and the top level bodies of modules/
380
+
381
+ /**
382
+ * Every installed plugin whose client bundle will not parse (#222 by
383
+ * @MicroMilo).
384
+ *
385
+ * The per-package check above only ever looked at what an operation added,
386
+ * which misses the failure that was actually reported: pnpm re-extracts the
387
+ * WHOLE tree on any install, so updating one plugin can restore another
388
+ * plugin's pristine — and broken — bundle, or fail to re-apply a patch that
389
+ * was holding it together. The damage then surfaces at the next boot as
390
+ * "failed to load plugins", with nothing connecting it to the install that
391
+ * caused it.
392
+ *
393
+ * Cheap enough to run on every operation: 0.40ms per plugin including the
394
+ * read (measured on a 385KB bundle), so a 30-plugin profile costs ~12ms
395
+ * against an install that takes seconds.
396
+ *
397
+ * Silent on ESM bundles, like the per-package check it calls — see there for
398
+ * why. Widening the sweep is exactly what would have turned that one false
399
+ * "corrupt" into one per ESM plugin in the profile.
400
+ *
401
+ * Callers compare a before-list with an after-list rather than reporting
402
+ * this one directly — a profile can carry a broken bundle indefinitely, and
403
+ * re-reporting a problem the user already had would put them in front of
404
+ * something this operation did not cause and cannot undo.
405
+ */
406
+ export function brokenClientBundles(profile: string, explicitDir?: string): { name: string; reason: string }[] {
407
+ const broken: { name: string; reason: string }[] = []
408
+ for (const name of Object.keys(readInstalled(profile, explicitDir))) {
409
+ const check = checkClientBundle(profile, name, explicitDir)
410
+ if (!check.ok) broken.push({ name, reason: check.reason ?? 'parse failed' })
411
+ }
412
+ return broken
413
+ }
414
+
415
+ /** Bundles broken after an operation that were intact before it. */
416
+ export function newlyBrokenBundles(
417
+ before: readonly { name: string; reason: string }[],
418
+ after: readonly { name: string; reason: string }[],
419
+ ): { name: string; reason: string }[] {
420
+ const seen = new Set(before.map(entry => entry.name))
421
+ return after.filter(entry => !seen.has(entry.name))
422
+ }
423
+
370
424
  export function checkClientBundle(profile: string, name: string, explicitDir?: string): BundleCheck {
371
425
  const root = join(profileDir(profile, explicitDir), 'node_modules', name)
372
426
  let manifest: { dsh?: { client?: unknown }; exports?: unknown }
@@ -394,6 +448,17 @@ export function checkClientBundle(profile: string, name: string, explicitDir?: s
394
448
  new Script(source, { filename: relative })
395
449
  return { ok: true, reason: null }
396
450
  } catch (error) {
397
- return { ok: false, reason: error instanceof Error ? error.message : String(error) }
451
+ const message = error instanceof Error ? error.message : String(error)
452
+ // `new Script` compiles a CLASSIC script, so perfectly valid module
453
+ // syntax is a SyntaxError to it: `export`, `import`, and top-level
454
+ // `await` all throw. This package ships CJS, which is why nobody noticed
455
+ // — but a plugin whose client bundle is ESM was being told its file was
456
+ // corrupt, and offered a rollback for a file that is fine.
457
+ //
458
+ // That is the exact outcome this check's own contract forbids: a false
459
+ // "your plugin is corrupt" is worse than not checking. Module syntax
460
+ // means "cannot judge with this parser", not "broken".
461
+ if (MODULE_SYNTAX_ERROR.test(message)) return { ok: true, reason: null }
462
+ return { ok: false, reason: message }
398
463
  }
399
464
  }