{"record":{"id":"b8a86cf05a712eda","repo":"agalwood/Motrix","slug":"plugin-command-not-declared-in-manifest","errorCode":"plugin.command.not_declared_in_manifest","errorMessage":"command \"${commandId}\" is not declared in plugin \"${callerId}\" manifest.contributes.commands[]","messagePattern":"command \"(.+?)\" is not declared in plugin \"(.+?)\" manifest\\.contributes\\.commands\\[\\]","errorType":"exception","errorClass":"CommandsError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/commands.ts","lineNumber":123,"sourceCode":"   * @throws {CommandsError} plugin.commands.id_out_of_namespace — wrong owner\n   * @throws {CommandsError} plugin.command.not_declared_in_manifest — missing from manifest\n   */\n  register(\n    callerId: string,\n    commandId: string,\n    handler: CommandHandler\n  ): CommandsRegistration {\n    if (!commandId.startsWith(`${callerId}.`)) {\n      throw new CommandsError(\n        'plugin.commands.id_out_of_namespace',\n        `command \"${commandId}\" is outside namespace \"${callerId}.\" — commands must start with \"${callerId}.\"`\n      )\n    }\n\n    if (this.resolveDeclared) {\n      const declared = this.resolveDeclared(callerId)\n      if (!declared?.has(commandId)) {\n        throw new CommandsError(\n          'plugin.command.not_declared_in_manifest',\n          `command \"${commandId}\" is not declared in plugin \"${callerId}\" manifest.contributes.commands[]`\n        )\n      }\n    }\n\n    if (this.handlers.has(commandId)) {\n      console.warn(\n        `[plugin:commands] handler for \"${commandId}\" registered more than once; previous handler replaced`\n      )\n    }\n\n    this.handlers.set(commandId, handler)\n\n    return {\n      dispose: () => {\n        this.handlers.delete(commandId)\n      },","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/commands.ts#L105-L141","documentation":"CommandsError with code 'plugin.command.not_declared_in_manifest', thrown by CommandsCapabilityHost.register when commandId is in the caller's namespace but is not present in the set returned by the manifestCommandIds resolver for that caller. The capability requires that every registered command be statically declared in the plugin's manifest.contributes.commands[] — a manifest-driven contract enforced only when a manifestCommandIds resolver is wired (production); in tests without a resolver this check is skipped for back-compat.","triggerScenarios":"Calling host.register('myPlugin', 'myPlugin.doThing', handler) where 'myPlugin.doThing' is not listed in the plugin's manifest.contributes.commands[] AND the host was constructed with a manifestCommandIds resolver (as capability-host.ts does in production). Distinguishable from error 78 because the namespace check passed.","commonSituations":"A new command was added in code but the manifest was not updated; the manifest entry has a typo or different casing; the plugin id in the manifest differs from the callerId passed to register; manifest reload did not pick up a freshly added command; resolver returns a stale snapshot.","solutions":["Add the command ID to manifest.contributes.commands[] for the plugin.","Confirm exact string match (case, dot, no trailing whitespace) between the manifest entry and the registered commandId.","Reload/rebuild the manifest resolver so it serves the updated set.","In tests where you intentionally skip declaration, construct CommandsCapabilityHost without manifestCommandIds."],"exampleFix":"// before — manifest.contributes.commands = [{ id: 'myPlugin.other' }]\nhost.register('myPlugin', 'myPlugin.doThing', handler)\n// after — declare it in the manifest\n// manifest.contributes.commands = [\n//   { id: 'myPlugin.other' },\n//   { id: 'myPlugin.doThing' },\n// ]\nhost.register('myPlugin', 'myPlugin.doThing', handler)","handlingStrategy":"validation","validationCode":"function isDeclaredInManifest(\n  declared: ReadonlySet<string> | undefined,\n  commandId: string\n): boolean {\n  return !declared || declared.has(commandId)\n}\nconst declared = manifestCommandIds?.(callerId)\nif (!isDeclaredInManifest(declared, commandId)) {\n  throw new Error(`${commandId} missing from manifest.contributes.commands[]`)\n}\nhost.register(callerId, commandId, handler)","typeGuard":"function isDeclaredInManifest(declared: ReadonlySet<string> | undefined, commandId: string): boolean {\n  return !declared || declared.has(commandId)\n}","tryCatchPattern":"try {\n  host.register(callerId, commandId, handler)\n} catch (e) {\n  if (e instanceof CommandsError && e.code === 'plugin.command.not_declared_in_manifest') {\n    // add commandId to manifest.contributes.commands[] and reload\n  } else throw e\n}","preventionTips":["Keep a single source of truth for command IDs and generate both the manifest entry and the register() call from it.","Add a CI step that diffs registered command IDs against manifest.contributes.commands[].","In unit tests, construct CommandsCapabilityHost without manifestCommandIds to skip the declaration check deliberately."],"tags":["plugin","commands","manifest","api-contract","configuration"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}