Add a new command to the kidd CLI end-to-end: handler, registration, and verification.
- Familiarity with the CLI concepts and architecture
- The project builds successfully (
pnpm typecheck)
Create a new file in the commands directory. The filename becomes the command name (e.g., check.ts registers as the check command).
With Zod args:
import { command } from '@kidd-cli/core'
import { z } from 'zod'
export default command({
description: 'Validate all scripts can be imported',
args: z.object({
fix: z.boolean().optional(),
}),
handler: async (ctx) => {
ctx.spinner.start('Validating scripts')
if (ctx.args.fix) {
ctx.log.raw('Running with auto-fix enabled')
}
ctx.spinner.stop('Validation complete')
},
})Without args:
import { command } from '@kidd-cli/core'
export default command({
description: 'List available scripts',
handler: async (ctx) => {
process.stdout.write(ctx.format.table(scripts))
},
})Hidden or deprecated:
Commands can be hidden from --help output or marked as deprecated. Both hidden and deprecated accept a static value or a function (Resolvable<T>), resolved at registration time.
import { command } from '@kidd-cli/core'
// Hidden from help, still executable via `mycli debug`
export default command({
description: 'Internal debugging tools',
hidden: true,
handler: async (ctx) => {
/* ... */
},
})
// Deprecated with message
export default command({
description: 'Deploy (legacy)',
deprecated: 'Use "deploy-v2" instead',
handler: async (ctx) => {
/* ... */
},
})Individual flags also support hidden, deprecated, and group:
export default command({
description: 'Build the project',
options: {
trace: { type: 'boolean', description: 'Enable tracing', hidden: true },
format: { type: 'string', description: 'Output format', group: 'Output Options:' },
legacy: { type: 'boolean', description: 'Legacy mode', deprecated: 'Use --modern' },
},
handler: async (ctx) => {
/* ... */
},
})With subcommands:
Create a directory with an index.ts for the parent command and individual files for each subcommand:
commands/
└── auth/
├── index.ts # Parent command (optional handler)
├── login.ts # "auth login" subcommand
└── logout.ts # "auth logout" subcommand
import { command, autoload } from '@kidd-cli/core'
export default command({
description: 'Auth commands',
commands: autoload({ dir: './auth' }),
})With render mode (.tsx):
Commands that need React/Ink UI use a render function instead of handler. The file must use the .tsx extension.
import { render } from 'ink'
import { command } from '@kidd-cli/core'
import { StatusView } from './_components/StatusView.js'
export default command({
description: 'Show live status dashboard',
render(props) {
const { waitUntilExit } = render(<StatusView {...props} />)
return waitUntilExit()
},
})The render function receives RenderProps (with args, config, meta, store, colors) and owns the full Ink lifecycle. Place command-private components in a _components/ directory next to the command file. See the Components standard for full conventions.
Commands are auto-registered via the autoloader when placed in the commands directory. The autoloader discovers files that:
- Have a
.ts,.tsx, or.jsextension (not.d.ts) - Do not start with
_or. - Export a default
Commandobject (created by thecommand()factory)
No manual registration is needed.
If the command needs new shared logic, add it to packages/core/src/lib/. Follow existing patterns:
- Return
Resulttuples for operations that can fail - Use Zod for runtime validation at boundaries
- Keep functions pure where possible
Create *.test.ts files in the test/ directory following the existing structure. Test the handler directly by constructing a mock context:
- Test the success path with valid args
- Test each failure path with expected errors
- Test Zod validation rejects invalid inputs
Run the full CI check suite:
pnpm lint && pnpm format && pnpm typecheckAfter completing all steps:
- Run
pnpm typecheckand confirm no errors - Run
pnpm testand confirm all tests pass - Run
pnpm kidd <name> --helpand confirm the command appears - Run the command and verify the expected behavior
Issue: The new command does not show up in kidd --help.
Fix: Ensure the file is in the commands directory, has a .ts or .js extension, does not start with _ or ., and exports a default Command created by the command() factory.
Issue: The handler receives a validation error for a valid-looking input.
Fix: Verify the Zod schema matches the expected args shape. Args are validated against the schema before the handler runs. Check that optional fields use .optional() and defaults use .default().
Issue: Properties on ctx are missing or mistyped.
Fix: Verify the command uses command() from @kidd-cli/core (not a custom wrapper). Check that module augmentation interfaces (KiddArgs, CliConfig, KiddStore) are correctly declared if using typed store keys or global args.