# cli-forge
A framework for building modern command-line interfaces
## motivation
Most CLI frameworks force you to choose between simple argument parsing and complex plugin architectures. You either get a lightweight parser that becomes unwieldy as your app grows, or a heavyweight framework that adds ceremony to every command.
cli-forge takes a different approach. It provides composable building blocks that let you start simple and add complexity only where needed. Commands are just functions. Middleware is opt-in. The type system guides you without getting in the way.
## architecture
```mermaid
graph TD
A[CLI Entry Point] --> B[Parser]
B --> C[Router]
C --> D[Middleware Chain]
D --> E[Command Handler]
E --> F[Output Formatter]
G[Plugin System] -.-> C
G -.-> D
H[Context] --> D
H --> E
I[Config Loader] --> A
style E fill:#e1f5ff
style H fill:#fff4e1npm install cli-forge
import { createCLI, command } from 'cli-forge';
const greet = command({
name: 'greet',
description: 'Greet a user',
args: {
name: { type: 'string', required: true },
enthusiastic: { type: 'boolean', flag: '-e' }
},
handler: async ({ args, output }) => {
const greeting = `Hello, ${args.name}`;
output.write(args.enthusiastic ? greeting + '!' : greeting);
}
});
const cli = createCLI({
name: 'mycli',
version: '1.0.0',
commands: [greet]
});
cli.run(process.argv.slice(2));cli-forge parses arguments into a normalized structure, then routes to the appropriate command handler. Each command receives a context object containing parsed arguments, output helpers, and any data injected by middleware.
The middleware system works like Express but for CLI apps. Each middleware can transform the context, validate inputs, or short-circuit execution. This keeps command handlers focused on business logic while common concerns like authentication, logging, and error handling live in reusable middleware.
Output formatting is abstracted so commands don't need to know whether they're writing plain text, JSON, or formatted tables. The output interface adapts based on flags like --json or --quiet.
const cli = createCLI({
name: 'mycli',
version: '1.0.0',
description: 'My awesome CLI tool',
commands: [/* ... */],
middleware: [/* ... */],
globalFlags: {
verbose: { type: 'boolean', flag: '-v', description: 'Verbose output' },
json: { type: 'boolean', description: 'Output as JSON' }
}
});import { middleware } from 'cli-forge';
const logger = middleware(async (ctx, next) => {
console.error(`Running: ${ctx.command}`);
const start = Date.now();
await next();
console.error(`Completed in ${Date.now() - start}ms`);
});
const cli = createCLI({
// ...
middleware: [logger]
});const db = command({
name: 'db',
description: 'Database operations',
subcommands: [
command({
name: 'migrate',
handler: async ({ output }) => {
output.write('Running migrations...');
}
}),
command({
name: 'seed',
handler: async ({ output }) => {
output.write('Seeding database...');
}
})
]
});Why another CLI framework?
Existing frameworks either lack type safety or require too much boilerplate. We wanted something that feels like writing regular TypeScript functions.
Does it work with CommonJS?
Yes. Both ESM and CommonJS are supported.
How do I handle interactive prompts?
cli-forge focuses on argument parsing and command routing. For prompts, use it with libraries like inquirer or prompts in your command handlers.
Can I use it for large CLIs with plugins?
Yes. The plugin system lets you register commands dynamically, and middleware can modify behavior globally or per-command.
What about testing?
Commands are just async functions, so they're easy to test. Pass in a mock context object and assert on the outputs.
import { createContext } from 'cli-forge/testing';
test('greet command', async () => {
const ctx = createContext({ args: { name: 'Alice' } });
await greet.handler(ctx);
expect(ctx.output.toString()).toBe('Hello, Alice');
});MIT