High-level overview of how kidd is structured, its design philosophy, and how data flows through the system.
kidd is a CLI framework for building composable, type-safe command-line tools. It provides a modular architecture with commands, middleware, context, and config layers that combine to build CLIs with full type inference and a rich terminal UI.
The codebase follows a functional, immutable, composition-first design. There are no classes, no let, no throw statements, and no loops. Errors are returned as Result tuples. Side effects (process exit, terminal output) are pushed to the outermost edges.
packages/
├── core/ # Core CLI framework (commands, middleware, context, config)
├── cli/ # CLI entrypoint and DX tooling (init, dev, build, compile)
├── config/ # Configuration loading, validation, and schema (internal)
├── utils/ # Shared functional utilities (internal)
└── bundler/ # tsdown bundling and binary compilation (internal)
| Package | Purpose |
|---|---|
@kidd-cli/core |
Core framework: cli(), command(), middleware(), context |
@kidd-cli/cli |
DX companion CLI: scaffolding, dev mode, build, compile |
@kidd-cli/config |
Configuration loading, validation, and schema (internal) |
@kidd-cli/utils |
Shared functional utilities (internal) |
@kidd-cli/bundler |
tsdown bundling and binary compilation (internal) |
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#313244',
'primaryTextColor': '#cdd6f4',
'primaryBorderColor': '#6c7086',
'lineColor': '#89b4fa',
'secondaryColor': '#45475a',
'tertiaryColor': '#1e1e2e',
'background': '#1e1e2e',
'mainBkg': '#313244',
'clusterBkg': '#1e1e2e',
'clusterBorder': '#45475a'
},
'flowchart': { 'curve': 'basis', 'padding': 15 }
}}%%
flowchart TB
subgraph entry ["Entry Layer"]
INDEX(["cli/src/index.ts"])
end
subgraph core ["Core Layer"]
CLICORE(["cli"])
CMD(["command"])
MW(["middleware"])
AUTO(["autoload"])
CTX(["context"])
end
subgraph lib ["Lib Layer"]
CONFIG(["config"])
STORE(["store"])
LOGGER(["logger"])
FORMAT(["format"])
PROMPTS(["prompts"])
ERRORS(["errors"])
RESULT(["result"])
PROJECT(["project"])
end
INDEX --> CLICORE
CLICORE --> CMD & MW & AUTO & CTX
CTX --> LOGGER & STORE & FORMAT & PROMPTS & ERRORS
CMD & MW --> CTX
classDef core fill:#313244,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
classDef gateway fill:#313244,stroke:#fab387,stroke-width:2px,color:#cdd6f4
classDef agent fill:#313244,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
classDef external fill:#313244,stroke:#f5c2e7,stroke-width:2px,color:#cdd6f4
class INDEX external
class CLICORE gateway
class CMD,MW,AUTO,CTX core
class CONFIG,STORE,LOGGER,FORMAT,PROMPTS,ERRORS,RESULT,PROJECT agent
style entry fill:#181825,stroke:#f5c2e7,stroke-width:2px
style core fill:#181825,stroke:#fab387,stroke-width:2px
style lib fill:#181825,stroke:#89b4fa,stroke-width:2px
Package: packages/cli
The CLI binary entrypoint. Calls cli() from @kidd-cli/core with the CLI name, version, commands, and middleware. This is the only layer that reads package.json for version and calls process.exit.
Package: packages/core/src/
The framework primitives:
| Module | Purpose |
|---|---|
cli.ts |
Entry function that wires yargs, config, and context |
command.ts |
Factory for creating typed commands |
middleware.ts |
Factory and pipeline runner for middleware composition |
autoloader.ts |
Auto-discovery and dynamic import of command files |
context/ |
Context creation, types, and error handling |
Package: packages/core/src/lib/
Shared utilities consumed by the core and extension layers:
| Module | Purpose |
|---|---|
config.ts |
Config file discovery, parsing, Zod validation |
store.ts |
File-backed JSON store (local and global) |
logger.ts |
Structured logging with @clack/prompts |
format/ |
Pure format functions (check, finding, code-frame, tally, duration) |
prompts.ts |
Interactive prompts and spinner via @clack/prompts |
errors.ts |
Sensitive data redaction and sanitization |
result.ts |
Result<T, E> type constructors (ok, err) |
validate.ts |
Zod schema validation returning Result tuples |
project.ts |
Git project root detection and submodule handling |
The Context is the central object threaded through every middleware and command handler. It carries all request-scoped data and utilities for a single CLI invocation.
| Property | Type | Mutable | Description |
|---|---|---|---|
args |
DeepReadonly<TArgs> |
No | Parsed and validated command arguments |
config |
DeepReadonly<TConfig> |
No | Loaded and validated config file contents |
log |
Log |
No | Logging methods (info, success, error, warn, etc.) |
prompts |
Prompts |
No | Interactive prompts (confirm, text, select, etc.) |
spinner |
Spinner |
No | Spinner for long-running operations (start, stop, message) |
colors |
Colors |
No | Color formatting utilities (picocolors) |
format |
Format |
No | Pure string formatters (json, table) |
store |
Store |
Yes | In-memory key-value store for middleware data |
fail |
(message, options?) => never |
No | Throw a user-facing error with clean exit |
meta |
DeepReadonly<Meta> |
No | CLI name, version, resolved command path |
All data properties (args, config, meta) are deeply readonly at the type level. The store is the only mutable property -- it exists for middleware-to-handler data flow.
Consumers extend the context type system via declaration merging without threading generics:
declare module '@kidd-cli/core' {
interface KiddArgs {
verbose: boolean
}
interface CliConfig {
apiUrl: string
}
interface KiddStore {
auth: AuthState
}
}After augmentation, ctx.args.verbose, ctx.config.apiUrl, and ctx.store.get('auth') are fully typed across all commands and middleware.
A CLI invocation flows through the system in this order:
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#313244',
'primaryTextColor': '#cdd6f4',
'primaryBorderColor': '#6c7086',
'lineColor': '#89b4fa',
'secondaryColor': '#45475a',
'tertiaryColor': '#1e1e2e',
'actorBkg': '#313244',
'actorBorder': '#89b4fa',
'actorTextColor': '#cdd6f4',
'signalColor': '#cdd6f4',
'signalTextColor': '#cdd6f4'
}
}}%%
sequenceDiagram
participant CLI as CLI Entry
participant Y as Yargs
participant CFG as Config
participant CTX as Context
participant MW as Middleware
participant CMD as Command
rect rgb(49, 50, 68)
Note over CLI,Y: Parse & Resolve
CLI->>Y: Parse argv
Y->>Y: Match command from registry
Y->>Y: Clean and validate args (Zod)
end
rect rgb(49, 50, 68)
Note over CFG,CTX: Bootstrap
Y->>CFG: Load and validate config
CFG-->>CTX: Create context (args, config, meta)
end
rect rgb(49, 50, 68)
Note over MW,CMD: Execute
CTX->>MW: Run middleware chain in order
MW->>MW: Each calls next() to continue
MW->>CMD: Final handler receives context
CMD-->>CLI: Exit with code 0 or ContextError
end
- Parse argv -- Yargs parses
process.argvand matches a registered command - Clean args -- Internal yargs keys (
_,$0, dashed duplicates) are stripped - Validate args -- If the command defines a Zod schema, args are validated against it
- Load config -- Config file (
.{name}.jsonc,.json, or.yaml) is discovered, parsed, and validated - Create context --
createContext()assembles store, format, colors, log, prompts, spinner, errors, and meta - Run middleware -- Root middleware wraps command middleware in an onion model; each calls
next()to continue - Execute handler -- The matched command's handler runs with the fully constructed context
- Exit --
ContextErrorcaught at the CLI boundary produces a clean exit with code; success exits 0
Commands are created with the command() factory:
export default command({
description: 'Deploy the application',
args: z.object({
environment: z.enum(['staging', 'production']),
force: z.boolean().optional(),
}),
handler: async (ctx) => {
process.stdout.write(ctx.format.json({ environment: ctx.args.environment }))
},
})Commands support nested subcommands via the commands property, which accepts either a static CommandMap or a Promise<CommandMap> from autoload() for lazy loading.
Middleware wraps command execution with pre/post logic. Created with the middleware() factory:
middleware(async (ctx, next) => {
ctx.spinner.start('Loading')
ctx.store.set('startTime', Date.now())
await next()
ctx.spinner.stop('Done')
})Middleware follows an onion model: root middleware (from cli()) wraps command middleware (from command()), which wraps the handler. Each middleware calls next() to pass control inward. Data flows between middleware and handlers via ctx.store. See Lifecycle for the full execution model.
The autoload() function discovers command files from a directory:
commands/
├── deploy.ts -> { deploy: Command }
├── status.ts -> { status: Command }
└── auth/
├── index.ts -> parent handler for "auth"
├── login.ts -> { auth.commands.login: Command }
└── logout.ts -> { auth.commands.logout: Command }
Discovery rules:
- Files must export a default
Command(created viacommand()) - Extensions:
.tsor.js(not.d.ts) - Ignored: files starting with
_or., files namedindex - Subdirectories become parent commands;
index.tsin a subdirectory becomes the parent handler
The config client discovers, parses, and validates config files:
Search order:
- Custom
searchPaths(if provided) - Current working directory
- Project root (detected via
.git)
File formats: .{name}.jsonc, .{name}.json, .{name}.yaml
Config is validated against a Zod schema and errors are returned as discriminated ConfigError unions (ConfigParseError or ConfigValidationError).
kidd uses two error strategies depending on the layer:
| Layer | Strategy | Type |
|---|---|---|
| Lib/internal | Result<T, E> tuples |
[error, null] or [null, value] |
| Context/CLI | ContextError (thrown at boundary) |
{ code, exitCode } |
Result tuples are used for expected failures (config parsing, validation, file I/O). Chain with early returns:
const [error, config] = loadConfig(workspace)
if (error) return [error, null]ContextError is used for user-facing errors via ctx.fail(). It is the only thrown type, and is caught at the CLI boundary for clean exit handling.
- Immutable by default -- All context properties are deeply readonly; only
ctx.storeis mutable (for middleware data flow) - Factories over classes -- All components are factory functions returning plain objects
- Result tuples over throw -- Expected failures use
Result<T, E>;ContextErroris the only thrown type at the CLI boundary - Module augmentation --
KiddArgs,CliConfig,KiddStoreinterfaces allow typed extensions without generics threading - Discriminated unions -- Domain types use
typefields or symbol-based tags for exhaustive pattern matching viats-pattern - Lazy subcommand loading -- Commands accept
Promise<CommandMap>fromautoload()for deferred imports - Zod at boundaries -- Runtime config, args, and external data validated with Zod schemas
- Sensitive data redaction -- Deep object redaction and regex pattern sanitization built into the context
All packages in this monorepo follow strict conventions to ensure consistency, type safety, and modern JavaScript practices.
ESM Only:
- All packages use
"type": "module"inpackage.json - No CommonJS (
require,module.exports) - All imports use ESM syntax (
import/export)
tsdown:
- All packages built with tsdown
- Configuration:
dts: true,format: 'esm',clean: true,outDir: 'dist' - Generates
.jsfiles and.d.tsdeclaration files - Tree-shakeable by default
Strict Mode:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"isolatedDeclarations": true
}
}Key Settings:
target: ES2022— Modern JavaScript features (top-level await, class fields, etc.)module: ESNext— Latest module syntaxmoduleResolution: bundler— Optimized for bundlers (tsdown, vite, etc.)strict: true— All strict checks enabledisolatedDeclarations: true— Critical: Forces explicit return types on all exported functions
Vitest Workspace:
- Root
vitest.config.tsdefines workspace - Unit tests: Colocated in
src/**/*.test.tsalongside source files - Integration tests:
test/integration/*.test.tsat package root - Coverage thresholds defined per package
Example Structure:
packages/core/
├── src/
│ ├── cli.ts
│ ├── cli.test.ts # Unit test (colocated)
│ ├── command.ts
│ └── command.test.ts # Unit test (colocated)
└── test/
└── integration/
└── cli.test.ts # Integration test
All Public Properties readonly:
- All exported interfaces/types must have
readonlymodifiers - Deep immutability enforced with
DeepReadonly<T>from type-fest - Prevents accidental mutation of shared objects
Example:
interface Config {
readonly apiUrl: string
readonly timeout: number
readonly headers: readonly string[]
}Zod at Boundaries:
- All config files validated with Zod schemas
- All CLI arguments validated with Zod
- Runtime validation at system boundaries (file I/O, user input)
Required by isolatedDeclarations:
- All exported functions must have explicit return types
- TypeScript compiler will error without them
- Ensures declaration files can be generated without full type inference
Example:
// ✅ Correct
export const loadConfig = (path: string): Result<Config, ConfigError> => {
// ...
}
// ❌ Incorrect (compiler error with isolatedDeclarations)
export const loadConfig = (path: string) => {
// ...
}Convention:
- Scope:
@kidd-cli/ - Name: Lowercase, single word or hyphenated (e.g.,
@kidd-cli/core,@kidd-cli/cli)
Standard Layout:
packages/{name}/
├── src/ # Source files (.ts)
├── dist/ # Build output (.js, .d.ts) [gitignored]
├── test/ # Integration tests
├── package.json # Package manifest
├── tsconfig.json # TypeScript config (extends root)
├── tsdown.config.ts # Build config (optional)
└── README.md # Package docs