add helpful cli hints for ability discovery#3
Conversation
- abilities list shows copy-pasteable run commands under each ability - command_not_found hook suggests `abilities run` for ability names - fuzzy "did you mean?" for typos in command names
Restructure so users hit a working command before reading concepts.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughAdds a custom oclif Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI
participant Hook as CommandNotFoundHook
participant Config as CommandRegistry
participant Output as ErrorOutput
User->>CLI: run unknown command "some-id"
CLI->>Hook: emit command_not_found(id)
Hook->>Hook: normalize id (strip "mainwp/" prefix)
alt id matches ability-like pattern (-v<digits>)
Hook->>Output: error "run: mainwpcontrol abilities run <name>"
else
Hook->>Config: fetch visible command IDs and aliases
Hook->>Hook: compute Levenshtein distances
alt best match within threshold
Hook->>Output: suggestion "Did you mean: <closest>?"
else
Hook->>Output: generic "command not found" + help hint
end
end
Hook->>CLI: exit with INPUT_ERROR (non-zero)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 21-37: Update the README sample output to match the current
renderer: replace the boxed table with the plain header/separator table style
produced by the formatter (e.g., formatter.renderTable or equivalent) and move
each "mainwpcontrol abilities run ..." hint so it appears directly under its
corresponding ability row (as printed by the abilities list command handler,
e.g., listAbilities or the function in commands/abilities/list.ts) rather than
appended all together at the end.
In `@src/commands/abilities/list.ts`:
- Around line 107-108: The hint currently injects raw ability.name into the
terminal output (shortName used in lines.push), allowing control characters/ANSI
escapes/newlines from the Dashboard to be printed; sanitize the value before
using it by deriving shortName from a cleaned version of ability.name (e.g., run
the name through the same sanitizer used by formatTable or strip control
sequences with a regex like removing ANSI CSI sequences and trimming
newlines/extra whitespace), and then use that sanitized shortName in the
lines.push call so no raw control characters are emitted.
In `@src/hooks/command-not-found.ts`:
- Around line 63-65: The hook currently returns non-conforming exit codes (127
and 2) when a command is not found; update the two this.error calls that build
the command-not-found messages (the one using id, config.bin and displayCmd and
the fallback at the other occurrence) to return a documented CLI exit code
instead of 127/2—use one of the allowed codes (e.g., exit: 1) consistently for
command-not-found so scripts can rely on the published contract.
- Around line 46-51: The current filter for commandIDs lets aliases of hidden
commands through because it only checks cmd.id === cid; change the predicate so
it finds any command where either cmd.id === cid or cmd.aliases includes cid and
excludes the cid when that matching command has hidden === true. Update the
filter used when building commandIDs (the array built from config.commandIDs and
config.commands.flatMap(...)) to use something like finding a command via
config.commands.find(cmd => cmd.id === cid || cmd.aliases?.includes(cid)) and
then filter out when that command?.hidden is true.
- Line 32: The hook handler hook: Hook<'command_not_found'> currently
destructures only { id, config } and incorrectly calls this.error(); update the
parameter destructuring to include context (e.g., { id, config, context }) and
replace any this.error() / this.exit() calls with context.error(...) or
context.exit(...); locate and update all occurrences in the command_not_found
hook implementation (including the blocks around the initial declaration and the
branches noted at the other occurrences) so the hook exits using the provided
options.context API.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7e222ed1-a95b-4c19-9c2e-95403053f914
📒 Files selected for processing (5)
README.mdpackage.jsonsrc/commands/abilities/list.tssrc/commands/login.tssrc/hooks/command-not-found.ts
| return dp[m]![n]!; | ||
| } | ||
|
|
||
| const hook: Hook<'command_not_found'> = async function ({ id, config }) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/hooks/command-not-found.tsRepository: mainwp/mainwp-control
Length of output: 3120
🏁 Script executed:
# Search for oclif Hook type definitions to understand the API
rg -t ts "Hook.*command_not_found" -A 5 -B 5Repository: mainwp/mainwp-control
Length of output: 703
🌐 Web query:
oclif command_not_found hook context API documentation
💡 Result:
The command_not_found hook in oclif is a lifecycle hook that runs when a command is not found before the default error message is displayed. It allows custom handling, such as suggesting similar commands (as done by the @oclif/plugin-not-found plugin) or other logic. To implement it, declare the hook in your CLI's package.json under oclif.hooks: "oclif": { "hooks": { "command_not_found": "./path/to/your/hook.js" } } The hook function receives an options parameter with a context property. Use options.context.error or options.context.exit to exit during the hook. All hooks share this standard structure, as shown in oclif docs examples for other hooks like init. Official documentation: https://oclif.github.io/docs/hooks/ Example from docs (adapted for command_not_found): import {Hook} from '@oclif/core' const hook: Hook.CommandNotFound = async function (options) { // Custom logic, e.g., log or suggest commands // Access options.argv for input args, options.config for CLI config console.log(Command not found: ${options.argv.join(' ')}) // Optionally exit: options.context.exit(1) } export default hook The exact TypeScript interface Hook.CommandNotFound is not explicitly detailed in public docs, but follows the pattern of other hooks with options: { argv?: string[], config: Config, context: HookContext }. The @oclif/plugin-not-found implements this hook to provide "did you mean" suggestions using Levenshtein distance. For full source, see @oclif/core types or plugin-not-found repo: https://github.com/oclif/plugin-not-found
Citations:
- 1: https://oclif.github.io/docs/hooks/
- 2: https://oclif.github.io/docs/plugins
- 3: https://github.com/oclif/plugin-not-found
- 4: https://registry.npmjs.org/%40oclif%2Fplugin-not-found
- 5: cryptic "command not found" returned on typercript transpile error oclif/oclif#1141
🏁 Script executed:
# Check README.md for CLI contract and exit codes
find . -name "README.md" -type f | head -5Repository: mainwp/mainwp-control
Length of output: 138
🏁 Script executed:
# Look for oclif Hook type definition
rg -t ts "type Hook" -A 3 --max-count 5Repository: mainwp/mainwp-control
Length of output: 47
🏁 Script executed:
# Check if there are other hooks in the codebase to see the pattern
find . -path "./src/hooks/*.ts" -type fRepository: mainwp/mainwp-control
Length of output: 97
🌐 Web query:
oclif Hook typescript type definition command_not_found signature
💡 Result:
The TypeScript type definition for the oclif command_not_found hook is Hook.CommandNotFound from @oclif/core. To implement it: typescript import { Hook } from '@oclif/core'; const hook: Hook.CommandNotFound = async function (options) { // Handle command not found, e.g., suggest alternatives // Use options.context.error or options.context.exit to exit // options likely includes: argv (command args), config, context }; export default hook; Register in package.json: json { "oclif": { "hooks": { "command_not_found": "./path/to/hook.js" } } } This hook runs when a command is not found before displaying the error. Official docs confirm command_not_found as a lifecycle event [9]. TypeDoc-like interface lists command_not_found: object [2]. Example usage seen in issues and plugin-not-found which implements it [14].
Citations:
- 1: https://oclif.io/config/interfaces/hooks.html
- 2: https://oclif.github.io/docs/hooks/
- 3: cryptic "command not found" returned on typercript transpile error oclif/oclif#1141
- 4: https://github.com/oclif/plugin-not-found
- 5: Hooks are not working properly oclif/core#515
🏁 Script executed:
cat README.md | head -100Repository: mainwp/mainwp-control
Length of output: 3035
🏁 Script executed:
# Check package.json for `@oclif/core` version and dependencies
cat package.json | grep -A 5 "@oclif"Repository: mainwp/mainwp-control
Length of output: 664
🏁 Script executed:
# Look for type definitions - check if node_modules has `@oclif/core` types
find node_modules/@oclif/core -name "*.d.ts" -path "*hook*" 2>/dev/null | head -10Repository: mainwp/mainwp-control
Length of output: 47
🏁 Script executed:
# Search for Hook interface in the project or dependencies
rg "interface.*Hook|type.*Hook" --max-count 10Repository: mainwp/mainwp-control
Length of output: 47
🏁 Script executed:
# Get the actual Hook type definition from `@oclif/core`
cat node_modules/@oclif/core/lib/types/hooks.d.ts 2>/dev/null || echo "File not found, searching for hook types..."Repository: mainwp/mainwp-control
Length of output: 108
🏁 Script executed:
# Search for Hook type exports in oclif/core
rg "command_not_found|Hook" node_modules/@oclif/core/lib --max-count 20 2>/dev/null | head -50Repository: mainwp/mainwp-control
Length of output: 47
🏁 Script executed:
# Check if there are any examples or actual hook implementations in oclif
find node_modules/@oclif -name "*not-found*" -o -name "*hook*" | grep -E "\.(ts|js)$" | head -10Repository: mainwp/mainwp-control
Length of output: 119
🏁 Script executed:
# Look for `@oclif/plugin-not-found` to see how they implement it
cat node_modules/@oclif/plugin-not-found/lib/hook.js 2>/dev/null || cat node_modules/@oclif/plugin-not-found/lib/hook.ts 2>/dev/null || echo "Checking directory structure..."Repository: mainwp/mainwp-control
Length of output: 96
🏁 Script executed:
# Check directory structure of `@oclif` packages
ls -la node_modules/@oclif/plugin-not-found/lib/ 2>/dev/null || ls -la node_modules/@oclif/plugin-not-found/ 2>/dev/null || echo "Plugin not found"Repository: mainwp/mainwp-control
Length of output: 81
🏁 Script executed:
# Check README.md for any mention of exit codes or error handling
grep -i "exit" README.mdRepository: mainwp/mainwp-control
Length of output: 859
🏁 Script executed:
# Look for any documentation about what `this` context would be in the hook
grep -rn "this\.error" src/ --include="*.ts" | head -20Repository: mainwp/mainwp-control
Length of output: 300
🏁 Script executed:
# Check if there are type stubs or d.ts files in src
find src -name "*.d.ts" -type fRepository: mainwp/mainwp-control
Length of output: 47
🏁 Script executed:
# Look at the actual Hook import and check the file
grep -A 20 "import.*Hook" src/hooks/command-not-found.tsRepository: mainwp/mainwp-control
Length of output: 702
🌐 Web query:
@oclif/core 4.0 hook context error method api reference
💡 Result:
In @oclif/core 4.0 (and later versions up to 4.9.0), hooks receive an options parameter that includes a context object with error and exit methods for proper CLI exit during hook execution. Throwing plain Errors is caught and does not exit the CLI, to prevent one hook from breaking the entire process (e.g., in multi-plugin scenarios). Use options.context.error(msg) or options.context.exit(code) instead. Example from official docs: typescript import {Hook} from '@oclif/core' const hook: Hook.Init = async function (options) { // ... if (needToExit) { options.context.error('Error message') // or options.context.exit(1) } } The Hook type is defined in @oclif/core's interfaces (src/interfaces/hooks.ts), exported publicly. Hook functions are typed as Hook.EventName = async (options: Hook.Options.EventName) => void | Promise, where options.context provides CLI context similar to Command methods like this.error and this.exit. No full API reference page for Hook.Context found in docs, but usage is documented in Hooks guide. Source code confirms availability in v4 via TypeScript definitions and exports.
Citations:
- 1: https://oclif.github.io/docs/hooks/
- 2: https://www.npmjs.com/package/@oclif/core?activeTab=readme
- 3: feat: add generic for extending
Hooksonconfig.runHookoclif/core#1359 - 4: feat: improved types and exports oclif/core#1076
🏁 Script executed:
# Check if there are tests for the hook
find . -name "*command-not-found*test*" -o -name "*hook*.test.ts" 2>/dev/null | head -10Repository: mainwp/mainwp-control
Length of output: 47
🏁 Script executed:
# Look at chat.ts to see how this.error is used in commands (should work there)
sed -n '180,210p' src/commands/chat.tsRepository: mainwp/mainwp-control
Length of output: 1099
🏁 Script executed:
# Check the base Command class to understand what `this` is in a hook vs command
grep -n "class.*Hook\|class.*Command" src/**/*.ts 2>/dev/null | head -10Repository: mainwp/mainwp-control
Length of output: 379
🏁 Script executed:
# Verify the exact Hook type signature from `@oclif/core` package
cat node_modules/@oclif/core/package.json 2>/dev/null | grep -A 2 '"main"'Repository: mainwp/mainwp-control
Length of output: 47
🌐 Web query:
oclif hook function this context available async function hook vs command
💡 Result:
In oclif, hooks and commands have different contexts available in their functions. Hooks are standalone async functions that receive an options parameter specific to the hook event. For example, the init hook receives options with an id property (e.g., options.id for the command ID). They do not have a this context like commands. To exit or error from a hook, use options.context.error or options.context.exit. Throwing an Error in a hook does not exit the CLI. Commands extend the Command class and have this bound in their async run method, providing access to this.config, this.log, this.error, this.exit, etc. Hooks: async function(options) { ... } — no this, use options.context for limited actions. Commands: class extends Command { async run { ... } } — full this Command instance available. Hooks run in parallel and are for lifecycle or custom events; commands are the main executable units with full CLI context.
Citations:
- 1: https://oclif.github.io/docs/hooks/
- 2: https://oclif.github.io/docs/commands/
- 3: feat: add generic for extending
Hooksonconfig.runHookoclif/core#1359 - 4: https://oclif.github.io/docs/hooks
🏁 Script executed:
# Look for actual implementation patterns in oclif examples or other hooks
rg "Hook.*command_not_found|options\.context" --max-count 10Repository: mainwp/mainwp-control
Length of output: 169
🏁 Script executed:
# Check if there are any TypeScript interfaces defined locally that extend Hook
rg "interface.*Hook|Hook\<" src/ -A 3 --max-count 10Repository: mainwp/mainwp-control
Length of output: 173
🌐 Web query:
oclif command_not_found hook options parameter destructuring context signature example
💡 Result:
The command_not_found hook in oclif is a lifecycle hook that runs when a command is not found before displaying the error. It receives an options parameter containing context and likely details about the attempted command (such as argv or id). Hook signature (TypeScript): import { Hook } from '@oclif/core' const hook: Hook.CommandNotFound = async (options) => { // options.context provides error and exit methods for handling // Other properties like attempted command ID or argv may be available } export default hook Example with parameter destructuring: const hook: Hook.CommandNotFound = async ({ context, id }) => { // 'id' is the attempted command ID (inferred from init hook pattern) this.log(Command ${id} not found) // Use context methods if needed if (context) { context.exit(1) // Exit during hook } } export default hook Register in package.json: "oclif": { "hooks": { "command_not_found": "./src/hooks/command_not_found.js" } } This hook is used by @oclif/plugin-not-found for "did you mean" suggestions. Official docs confirm options.context.error or options.context.exit for exiting. Exact options interface follows patterns like init hook's {id, argv, config, context}.
Citations:
- 1: https://oclif.github.io/docs/hooks/
- 2: https://oclif.github.io/docs/flexible_taxonomy
- 3: https://github.com/oclif/plugin-not-found
Use context.error() instead of this.error() in the hook handler.
Oclif's command_not_found hook receives an options parameter (not a Command instance). To exit the hook, use options.context.error() or options.context.exit() — not this.error(). The hook currently destructures only { id, config }, missing the context parameter needed to properly terminate execution.
🐛 Proposed fix
-const hook: Hook<'command_not_found'> = async function ({ id, config }) {
+const hook: Hook<'command_not_found'> = async function ({ id, config, context }) {
// Strip common prefix mistakes (e.g., "mainwp/list-updates-v1")
const name = id.replace(/^mainwp\//, '');
// Ability name pattern: suggest abilities run
if (/-v\d+$/.test(name)) {
const suggestion = `${config.bin} abilities run ${name}`;
- this.error(
+ context.error(
`"${id}" is not a command. It looks like an ability name.\n\nRun it with:\n ${suggestion}`,
{ exit: 1 },
);
}
// Find closest matching command using Levenshtein distance
...
- this.error(
+ context.error(
`"${id.replace(/:/g, ' ')}" is not a ${config.bin} command. Did you mean "${displayCmd}"?\n\nRun ${config.bin} help for a list of available commands.`,
{ exit: 127 },
);
}
}
- this.error(
+ context.error(
`command "${id.replace(/:/g, ' ')}" not found. Run ${config.bin} help for a list of available commands.`,
{ exit: 2 },
);Also applies to: 39-42, 63-65, 71-73
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/command-not-found.ts` at line 32, The hook handler hook:
Hook<'command_not_found'> currently destructures only { id, config } and
incorrectly calls this.error(); update the parameter destructuring to include
context (e.g., { id, config, context }) and replace any this.error() /
this.exit() calls with context.error(...) or context.exit(...); locate and
update all occurrences in the command_not_found hook implementation (including
the blocks around the initial declaration and the branches noted at the other
occurrences) so the hook exits using the provided options.context API.
There was a problem hiding this comment.
Not changing this. Oclif binds this: Hook.Context in hook callbacks, so this.error() is already calling the context API. You can verify in node_modules/@oclif/core/lib/interfaces/hooks.d.ts where the hook type is defined with this: Hook.Context. Switching to options.context.error() would be equivalent but less idiomatic for oclif hooks.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the Hook type definition in `@oclif/core`
find . -path "*/node_modules/@oclif/core/lib/interfaces/hooks.d.ts" -exec cat {} \; 2>/dev/null || \
find . -path "*/node_modules/@oclif/core*" -name "hooks.d.ts" -exec cat {} \; 2>/dev/null || \
echo "File not found in node_modules, searching elsewhere..."
# Also search for Hook type with `this` binding
rg "this.*Hook\.Context\|Hook\.Context.*this\|hook.*this" --type=ts --max-count=10 2>/dev/null | head -30
Length of output: 0
🌐 Web query:
oclif Hook type definition "this: Hook.Context" hooks.d.ts `@oclif/core`
💡 Result:
In @oclif/core, the hook function type is declared in lib/interfaces/hooks.d.ts as a function with an explicit this parameter:
export type Hook<T extends keyof P, P extends Hooks = Hooks> =
(this: Hook.Context,
options: P[T]['options'] & { config: Config; context: Context }
) => Promise<P[T]['return']>;…and Hook.Context is an interface that includes config plus logging/error helpers (e.g. this.config, this.log, this.error, etc.). (app.unpkg.com)
Practical implications:
this: Hook.Contextmeans oclif will bindthiswhen it invokes the hook, and TypeScript will typethisaccordingly. (app.unpkg.com)- To use
thisin your implementation, don’t use an arrow function (arrows don’t have their ownthis):
import type {Hook} from '@oclif/core'
const hook: Hook<'init'> = async function (options) {
this.log('hello') // typed as Hook.Context
options.config // Config
options.context.log('x') // Context (separate from Hook.Context)
}
export default hookThe oclif hooks docs show hooks are exported functions keyed by event name (e.g. init, prerun, postrun) and receive an options object from oclif. (oclif.io)
Citations:
- 1: https://app.unpkg.com/%40oclif/core%404.2.9/files/lib/interfaces/hooks.d.ts?utm_source=openai
- 2: https://app.unpkg.com/%40oclif/core%404.2.9/files/lib/interfaces/hooks.d.ts?utm_source=openai
- 3: https://oclif.io/docs/hooks/?utm_source=openai
@dennisdornon, you're absolutely right — I apologize for the incorrect review comment.
The Hook type in @oclif/core is defined as (this: Hook.Context, options: ...) => Promise<...>, meaning oclif explicitly binds this to a Hook.Context instance when invoking hook callbacks. Since the hook is implemented as a regular async function (not an arrow function), this is correctly bound and this.error() is both valid and idiomatic.
✏️ Learnings added
Learnt from: dennisdornon
Repo: mainwp/mainwp-control PR: 3
File: src/hooks/command-not-found.ts:33-33
Timestamp: 2026-03-24T19:03:59.327Z
Learning: In `oclif/core`, hook callbacks are typed with an explicit `this: Hook.Context` parameter (see `lib/interfaces/hooks.d.ts`). Oclif binds `this` to a `Hook.Context` instance at invocation, so `this.error()`, `this.log()`, etc. are the correct and idiomatic way to call context methods inside hooks declared as regular `async function` expressions. Arrow functions would not work for this pattern. Switching to `options.context.error()` would be functionally equivalent but is non-idiomatic. File: `src/hooks/command-not-found.ts` in mainwp/mainwp-control.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
- use ExitCode.INPUT_ERROR consistently in command-not-found hook - sanitize ability.name before building hint line - fix hidden command alias filter to check aliases not just id - update readme sample to match actual formatTable output
Guide new users from login through to running their first ability.
loginnow shows "Next: mainwpcontrol abilities list" after successabilities listprints a copy-pasteableabilities run <name>hint under each rowcommand_not_foundhook catches ability names typed as commands and suggests the right syntax, plus did-you-mean for typosTested locally — login hint, abilities list hints, and command-not-found suggestions all work.
Summary by CodeRabbit
Documentation
New Features
Tests