From 92b742ce70ae4bdbda0f47dc6c3ed804e4f60544 Mon Sep 17 00:00:00 2001 From: forrestlinfeng Date: Fri, 23 Oct 2026 21:59:32 +0800 Subject: [PATCH] Drop the empty input hint that stopped the whole plugin loading (0.3.0) dsh-commands rejects a command whose `input` is present with a blank hint, and `evolver-status` declared `input: { hint: '' }`. The TypeError escaped the `ctx.inject(['commands'])` callback inside `apply`, so every host with a commands service failed to load Evolver at all: no recall, no reports, no skill, no commands. A command that takes no argument omits `input`. The fake host in the tests accepted what the real one rejects, which is how this shipped; it now mirrors dsh-commands' own validation, so the eight definitions are checked at registration the way the host checks them. Co-Authored-By: Claude Opus 5 --- src/commands.js | 1 - test/support/host.js | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/commands.js b/src/commands.js index 104a10e..6e2ab3b 100644 --- a/src/commands.js +++ b/src/commands.js @@ -141,7 +141,6 @@ export function evolverCommands({ status, search, distillNow, runCli = runEvolve { name: `${NAME_PREFIX}status`, description: 'Show Evolver health: the Proxy, Hub auth, recent recalls and reuse reports.', - input: { hint: '' }, handler: async ({ signal }) => success(shown(await status(signal))), }, { diff --git a/test/support/host.js b/test/support/host.js index 201f7f4..be94ea4 100644 --- a/test/support/host.js +++ b/test/support/host.js @@ -43,13 +43,28 @@ after(() => { process.env.PATH = originalPath; }); +// Mirrors the host's own command validation: a fake that accepts more than dsh-commands does lets a +// definition the host rejects at registration — an empty input hint, say — ship green and kill the plugin. +const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u; + +function rejectInvalidCommand(definition) { + const { name, description, input, handler } = definition; + if (!COMMAND_NAME.test(name)) throw new TypeError(`command name "${name}" must match ${String(COMMAND_NAME)}`); + if (typeof description !== 'string' || description.trim().length === 0) throw new TypeError(`command "${name}" description must not be empty`); + if (typeof handler !== 'function') throw new TypeError(`command "${name}" handler must be a function`); + if (input !== undefined && (typeof input?.hint !== 'string' || input.hint.trim().length === 0)) { + throw new TypeError(`command "${name}" input hint must not be empty`); + } + return definition; +} + export function fakeContext() { const registered = { tools: [], skills: [], commands: [] }; const listeners = new Map(); const ctx = { tools: { register: (definition) => registered.tools.push(definition) }, skills: { registerProvider: (create) => registered.skills.push(create({})) }, - commands: { register: (definition) => registered.commands.push(definition) }, + commands: { register: (definition) => registered.commands.push(rejectInvalidCommand(definition)) }, on: (event, listener) => { listeners.set(event, listener); return () => listeners.delete(event);