dsh-plugin-toolkit 0.9.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## 0.9.2
2
+
3
+ - `doctor` reports a shipped client bundle that no `dsh.client` declares. The host
4
+ serves `client.js` only for packages declaring `dsh.client`, so a plugin could
5
+ ship the bundle, mount it in `verify`, and still have no panel in the app — the
6
+ same silent failure as a tarball without it.
7
+ - The toolkit's own manifest now declares `dsh.client` (`platform: web`,
8
+ `inject: ['@deepseek-ai/dsh-client-ui-sidebar-right']`), which is what makes the
9
+ Plugin doctor tab reachable at all.
10
+ - The `not-shipped` fixture declares its client entry, so it keeps demonstrating
11
+ the `files` gap instead of tripping the new declaration check.
12
+
13
+ ## 0.9.1
14
+
15
+ - `audit` separates problems from advisories. Running 0.9.0 on a real profile
16
+ said "17 of 19 installed plugin(s) have problems" — 13 of those load fine and
17
+ only lack `engines.dsh` or a README Requirements section, which the market index
18
+ needs and the loader does not. Those are now counted separately, so the plugins
19
+ that are actually broken stay visible.
20
+ - `client entry` and `dev` resolve a `./client` export written as a conditions
21
+ object (`{ default: './lib/client.cjs' }`), not only as a string.
22
+
1
23
  ## 0.9.0
2
24
 
3
25
  - `audit`: every plugin installed in a profile, in one pass — version, whether the
package/README.md CHANGED
@@ -212,6 +212,10 @@ demo-panel 0.3.0 not loaded 1 problem: root export
212
212
  1 installed plugin(s) are not in the running app; restart the app once after installing.
213
213
  ```
214
214
 
215
+ Advisories (`engines.dsh`, README Requirements) are counted apart from problems:
216
+ the market index needs them, the loader does not, so they should not bury a plugin
217
+ that is actually broken.
218
+
215
219
  Without `--host` the loaded column is `?`, because guessing would be worse than
216
220
  saying nothing. Only packages that declare a `dsh` field are audited — the rest of
217
221
  `node_modules` is ordinary dependencies, and auditing those is noise.
@@ -309,6 +313,12 @@ dsh-plugin verify: stopped at pack
309
313
 
310
314
  ## When the panel does not appear
311
315
 
316
+ The first question is whether the host ever served your bundle. A package that
317
+ ships `client.js` but declares no `dsh.client` loads its host half and shows no
318
+ panel — `doctor` now fails that, because the host serves client bundles only for
319
+ packages that declare one:
320
+
321
+
312
322
  The install reports success and nothing shows up. In this order:
313
323
 
314
324
  1. **Did the host restart?** Client bundles are read at startup. `dsh plugin add`
package/lib/audit.mjs CHANGED
@@ -48,9 +48,26 @@ export function listInstalled({ home = path.join(os.homedir(), '.dsh') } = {}) {
48
48
  return found;
49
49
  }
50
50
 
51
- function describe({ failures }) {
52
- if (failures.length === 0) return 'ok';
53
- return `${failures.length} problem${failures.length === 1 ? '' : 's'}: ${failures[0].check}`;
51
+ /**
52
+ * `engines.dsh` and the README Requirements section are what the market index
53
+ * needs. The loader does not read either, so a plugin without them still loads —
54
+ * calling that "a problem" would bury the plugins that are actually broken.
55
+ */
56
+ const ADVISORY = new Set(['engines.dsh', 'readme']);
57
+
58
+ function split(failures) {
59
+ return {
60
+ hard: failures.filter((failure) => ADVISORY.has(failure.check) === false),
61
+ advisory: failures.filter((failure) => ADVISORY.has(failure.check) === true),
62
+ };
63
+ }
64
+
65
+ function describe({ hard, advisory }) {
66
+ if (hard.length === 0 && advisory.length === 0) return 'ok';
67
+ const parts = [];
68
+ if (hard.length > 0) parts.push(`${hard.length} problem${hard.length === 1 ? '' : 's'}: ${hard[0].check}`);
69
+ if (advisory.length > 0) parts.push(`${advisory.length} advisor${advisory.length === 1 ? 'y' : 'ies'}`);
70
+ return parts.join(', ');
54
71
  }
55
72
 
56
73
  export async function audit({ home, host, io = console, fetchImpl, timeoutMs = 3000 } = {}) {
@@ -74,7 +91,7 @@ export async function audit({ home, host, io = console, fetchImpl, timeoutMs = 3
74
91
  failures = [{ check: 'manifest', message: error.message }];
75
92
  }
76
93
  const loaded = graph?.ok === true ? graph.ids.has(manifest.name) : null;
77
- return { name: manifest.name, version: manifest.version ?? '?', profile, dir, failures, loaded };
94
+ return { name: manifest.name, version: manifest.version ?? '?', profile, dir, failures, ...split(failures), loaded };
78
95
  });
79
96
 
80
97
  const width = Math.max(...rows.map((row) => row.name.length), 6);
@@ -84,12 +101,17 @@ export async function audit({ home, host, io = console, fetchImpl, timeoutMs = 3
84
101
  for (const failure of row.failures) io.log(`${' '.repeat(width + 22)}${failure.check}: ${failure.message}`);
85
102
  }
86
103
 
87
- const failed = rows.filter((row) => row.failures.length > 0);
104
+ const failed = rows.filter((row) => row.hard.length > 0);
105
+ const advised = rows.filter((row) => row.hard.length === 0 && row.advisory.length > 0);
88
106
  const notLoaded = rows.filter((row) => row.loaded === false);
89
107
  if (notLoaded.length > 0) {
90
108
  io.log('');
91
109
  io.log(`${notLoaded.length} installed plugin(s) are not in the running app; restart the app once after installing.`);
92
110
  }
111
+ if (advised.length > 0) {
112
+ io.log('');
113
+ io.log(`${advised.length} plugin(s) load but are not ready for the market index (engines.dsh, README Requirements).`);
114
+ }
93
115
  if (failed.length > 0) {
94
116
  io.log('');
95
117
  io.log(`${failed.length} of ${rows.length} installed plugin(s) have problems.`);
package/lib/dev.mjs CHANGED
@@ -12,12 +12,14 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { spawnSync } from 'node:child_process';
14
14
 
15
+ import { exportTarget } from './doctor.mjs';
16
+
15
17
  const IGNORED = new Set(['node_modules', '.git', 'docs', 'test', 'tests', 'coverage', 'fixtures']);
16
18
 
17
19
  export function resolveEntry(dir, manifest) {
18
20
  const declared = manifest?.dsh?.client?.entry;
19
21
  const exported = manifest?.exports;
20
- const subpath = typeof exported === 'string' ? undefined : exported?.['./client'];
22
+ const subpath = typeof exported === 'string' ? undefined : exportTarget(exported?.['./client']);
21
23
  const candidates = [declared, subpath, 'client.js', 'lib/client.js', 'dist/client.js'].filter((value) => typeof value === 'string');
22
24
  for (const candidate of candidates) {
23
25
  const absolute = path.resolve(dir, candidate);
package/lib/doctor.mjs CHANGED
@@ -84,6 +84,17 @@ export function checkPatchFile(dir, manifest) {
84
84
  return `dsh.bundle.patch points at ${bundle.patch}, which does not exist`;
85
85
  }
86
86
 
87
+ /** `exports['./client']` is a string in some packages and a conditions object in others. */
88
+ export function exportTarget(value) {
89
+ if (typeof value === 'string') return value;
90
+ if (value !== null && typeof value === 'object') {
91
+ for (const key of ['default', 'import', 'require']) {
92
+ if (typeof value[key] === 'string') return value[key];
93
+ }
94
+ }
95
+ return undefined;
96
+ }
97
+
87
98
  export function checkClientEntry(dir, manifest) {
88
99
  const client = manifest.dsh?.client;
89
100
  if (client === undefined || client === null) return null;
@@ -91,7 +102,7 @@ export function checkClientEntry(dir, manifest) {
91
102
  // with a `dsh/` folder names its bundle), then the plain file names. A plugin
92
103
  // that ships `dsh/client.js` is not wrong for failing to also ship `client.js`.
93
104
  const exported = manifest.exports;
94
- const subpath = typeof exported === 'string' ? undefined : exported?.['./client'];
105
+ const subpath = typeof exported === 'string' ? undefined : exportTarget(exported?.['./client']);
95
106
  const entries = [client.entry, subpath, 'client.js', 'lib/client.js', 'dist/client.js']
96
107
  .filter((entry) => typeof entry === 'string' && entry.length > 0)
97
108
  .map((entry) => entry.replace(/^\.\//u, ''));
@@ -99,6 +110,30 @@ export function checkClientEntry(dir, manifest) {
99
110
  return found ? null : `dsh.client is declared but none of ${entries.join(', ')} exists`;
100
111
  }
101
112
 
113
+ /**
114
+ * A client bundle the harness will never serve is the same failure as a tarball
115
+ * without one: the plugin loads, the row mounts, and no panel appears. The host
116
+ * serves `client.js` only for packages declaring `dsh.client`, so shipping the
117
+ * file by convention is not enough.
118
+ */
119
+ const CLIENT_FILES = ['client.js', 'lib/client.js', 'dist/client.js'];
120
+
121
+ export function checkClientDeclaration(dir, manifest) {
122
+ const client = manifest.dsh?.client;
123
+ if (client !== undefined && client !== null) {
124
+ const platform = client.platform;
125
+ if (platform === undefined || platform === 'web') return null;
126
+ return `dsh.client.platform is ${JSON.stringify(platform)}, but the harness serves client bundles on web`;
127
+ }
128
+ const exported = manifest.exports;
129
+ const subpath = typeof exported === 'string' ? undefined : exportTarget(exported?.['./client']);
130
+ const shipped = [subpath, ...CLIENT_FILES].find(
131
+ (file) => typeof file === 'string' && fs.existsSync(path.join(dir, file.replace(/^\.\//u, ''))),
132
+ );
133
+ if (shipped === undefined) return null;
134
+ return `${shipped.replace(/^\.\//u, '')} ships but dsh.client is not declared, so the harness never serves the bundle and the panel cannot appear`;
135
+ }
136
+
102
137
  /** Every path the manifest points at: exports, the bundle patch, the client entry. */
103
138
  export function declaredPaths(manifest) {
104
139
  const paths = new Set();
@@ -189,6 +224,7 @@ export function doctor(dir, { installed = false } = {}) {
189
224
  ['dsh field', checkDshField(manifest)],
190
225
  ['patch file', checkPatchFile(dir, manifest)],
191
226
  ['client entry', checkClientEntry(dir, manifest)],
227
+ ['client declaration', checkClientDeclaration(dir, manifest)],
192
228
  // After install the tarball is already unpacked, so `files` cannot be used to
193
229
  // decide what shipped: npm always ships package.json and the README.
194
230
  ['files list', installed === true ? null : checkFilesList(manifest)],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-toolkit",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Preflight, release and test DeepSeek Harness plugins: checks what the host needs, waits for npm to actually serve the version, and keeps the market index honest.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,6 +74,13 @@
74
74
  "dsh": {
75
75
  "bundle": {
76
76
  "patch": "./cordis.patch.yml"
77
+ },
78
+ "client": {
79
+ "platform": "web",
80
+ "immediately": true,
81
+ "inject": [
82
+ "@deepseek-ai/dsh-client-ui-sidebar-right"
83
+ ]
77
84
  }
78
85
  },
79
86
  "main": "index.js"