dsh-plugin-toolkit 0.9.5 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 0.9.6
2
+
3
+ - **The host half now survives a context it does not own.** `apply()` ended with
4
+ an unguarded `installRoutes(ctx)`, and cordis does not return `undefined` for
5
+ an undeclared service — it throws `cannot get property "webServer" without
6
+ inject`. So the whole `apply` failed, no route was registered, and the plugin
7
+ manager showed one red dot with no text: the exact symptom the 0.9.5 report
8
+ channel was built to explain, caused by the code next to it.
9
+ - `verify` now calls the host face's `apply` with a context that throws on every
10
+ undeclared property, the way cordis does. A plain `{}` hid this: reading
11
+ `ctx.webServer` off it is just `undefined`. Message:
12
+ `apply() throws when a service it never declared is read, which fails the row: …`
13
+ - The scaffold was never affected — its host half registers nothing.
14
+ - Regression tests: `test/host.test.mjs` (activation, deferred web server,
15
+ method guard) and a `verify` case that builds a plugin whose `apply` reads
16
+ `ctx.webServer` unguarded.
17
+
1
18
  ## 0.9.5
2
19
 
3
20
  - The panel reports its own failures. When a row fails to activate the app shows
package/index.js CHANGED
@@ -143,11 +143,19 @@ export function apply(ctx) {
143
143
  let installed = false;
144
144
  const installRoutes = (serviceCtx) => {
145
145
  if (installed) return;
146
- const server = serviceCtx !== null && serviceCtx !== undefined ? serviceCtx.webServer : undefined;
147
- if (server === null || server === undefined || typeof server.register !== 'function') return;
148
- server.register({ kind: 'exact', path: '/toolkit/check', handler: checkHandler });
149
- server.register({ kind: 'exact', path: '/toolkit/report', handler: reportHandler });
150
- installed = true;
146
+ // Reading `ctx.webServer` on a context that does not declare `inject` does
147
+ // not yield undefined: cordis throws "cannot get property ... without
148
+ // inject". An unguarded read here fails the whole apply, which is exactly
149
+ // the red dot with no message this panel exists to explain.
150
+ try {
151
+ const server = serviceCtx !== null && serviceCtx !== undefined ? serviceCtx.webServer : undefined;
152
+ if (server === null || server === undefined || typeof server.register !== 'function') return;
153
+ server.register({ kind: 'exact', path: '/toolkit/check', handler: checkHandler });
154
+ server.register({ kind: 'exact', path: '/toolkit/report', handler: reportHandler });
155
+ installed = true;
156
+ } catch {
157
+ // No web server on this context: the guard still runs, the panel does not.
158
+ }
151
159
  };
152
160
 
153
161
  const defer = (deps, callback) => {
package/lib/verify.mjs CHANGED
@@ -35,6 +35,21 @@ function packTarball(dir, destination) {
35
35
  return path.join(destination, document.filename);
36
36
  }
37
37
 
38
+ /**
39
+ * What cordis hands a plugin that declares no `inject`: every undeclared
40
+ * property read throws. A plain `{}` hides the bug this is looking for.
41
+ */
42
+ function undeclaredContext() {
43
+ const base = { inject: undefined };
44
+ return new Proxy(base, {
45
+ get(target, key) {
46
+ if (key in target) return target[key];
47
+ if (typeof key === 'symbol') return undefined;
48
+ throw new Error(`cannot get property "${String(key)}" without inject`);
49
+ },
50
+ });
51
+ }
52
+
38
53
  export async function verify({ dir, mount = true }) {
39
54
  const failures = [];
40
55
  const report = doctor(dir);
@@ -113,6 +128,7 @@ export async function verify({ dir, mount = true }) {
113
128
  // 0.8.0 shipped a package whose root export was the CLI, installed cleanly,
114
129
  // and mounted nothing.
115
130
  const rootFile = rootEntry(report.manifest);
131
+ let hostFace = null;
116
132
  if (rootFile !== null) {
117
133
  const absolute = path.join(installed, rootFile);
118
134
  if (fs.existsSync(absolute) === false) {
@@ -126,6 +142,7 @@ export async function verify({ dir, mount = true }) {
126
142
  try {
127
143
  const { pathToFileURL } = await import('node:url');
128
144
  const loaded = await import(pathToFileURL(absolute).href);
145
+ hostFace = loaded;
129
146
  if (typeof loaded.apply !== 'function') {
130
147
  return {
131
148
  ok: false,
@@ -144,6 +161,26 @@ export async function verify({ dir, mount = true }) {
144
161
  }
145
162
  }
146
163
 
164
+ // A host face that reads a service it never declared does not get undefined
165
+ // back: cordis throws "cannot get property ... without inject", the row fails,
166
+ // and the plugin manager shows one red dot. This cost a real user several
167
+ // restarts, so the activation itself is checked, not just the import.
168
+ if (hostFace !== null) {
169
+ try {
170
+ hostFace.apply(undeclaredContext());
171
+ } catch (error) {
172
+ return {
173
+ ok: false,
174
+ stage: 'install',
175
+ failures: [{
176
+ check: 'host apply',
177
+ message: `apply() throws when a service it never declared is read, which fails the row: ${error.message}`,
178
+ }],
179
+ files: packed.files,
180
+ };
181
+ }
182
+ }
183
+
147
184
  // `--no-mount` skips the DOM, not the install: the host half is the half
148
185
  // that silently fails, and importing it needs nothing but Node.
149
186
  if (mount === false) return { ok: true, stage: 'pack', failures: [], files: packed.files, size: packed.size };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-toolkit",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
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": {