Skip to content

Commit a51e2a2

Browse files
committed
first pass
1 parent 0cca6a1 commit a51e2a2

File tree

11 files changed

+458
-0
lines changed

11 files changed

+458
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules/
2+
dist
3+
.DS_Store
4+
*.tgz
5+
.env
6+
.env.development.local
7+
.env.test.local
8+
.env.production.local
9+
.env.local
10+
.turbo

.vscode/settings.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"editor.defaultFormatter": "biomejs.biome",
3+
"editor.formatOnSave": true,
4+
"editor.formatOnType": true,
5+
"[typescript]": {
6+
"editor.defaultFormatter": "biomejs.biome"
7+
},
8+
"[typescriptreact]": {
9+
"editor.defaultFormatter": "biomejs.biome"
10+
},
11+
"[json]": {
12+
"editor.defaultFormatter": "biomejs.biome"
13+
},
14+
"editor.codeActionsOnSave": {
15+
"quickfix.biome": "explicit",
16+
"source.organizeImports.biome": "explicit"
17+
},
18+
"[prisma]": {
19+
"editor.defaultFormatter": "Prisma.prisma"
20+
}
21+
}

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- [2025-04-07] [first pass](https://github.com/RubricLab/cli/commit/392698be5e5b6fd3706876e8d91340d7e1d2951f)
2+
# Changelog
3+

README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# @rubriclab/cli
2+
3+
A lightweight, type-safe CLI framework built with Zod for creating command-line applications with minimal boilerplate.
4+
5+
## Features
6+
7+
- Type-safe command and argument definitions with Zod schemas
8+
- Automatic help text generation
9+
- Colored terminal output formatting
10+
- Simple API for defining commands and handlers
11+
12+
## Installation
13+
14+
```bash
15+
npm install @rubriclab/cli
16+
```
17+
18+
## Usage
19+
20+
### Basic Example
21+
22+
```typescript
23+
import { createCLI, z } from '@rubriclab/cli';
24+
25+
const cli = createCLI({
26+
name: 'mycli',
27+
version: '1.0.0',
28+
description: 'My CLI tool',
29+
commands: [
30+
{
31+
name: 'greet',
32+
description: 'Greet someone',
33+
args: z.object({
34+
name: z.string().describe('Name to greet'),
35+
uppercase: z.boolean().default(false).describe('Uppercase the greeting')
36+
}),
37+
handler: ({ name, uppercase }) => {
38+
const greeting = `Hello, ${name}!`;
39+
console.log(uppercase ? greeting.toUpperCase() : greeting);
40+
}
41+
}
42+
]
43+
});
44+
45+
cli.parse();
46+
```
47+
48+
### Running the CLI
49+
50+
```bash
51+
# Show help
52+
mycli --help
53+
54+
# Run a command
55+
mycli greet --name "World"
56+
57+
# With a boolean flag
58+
mycli greet --name "World" --uppercase
59+
```
60+
61+
## API Reference
62+
63+
### `createCLI(config)`
64+
65+
Creates a new CLI instance with the specified configuration.
66+
67+
```typescript
68+
type CLIConfig = {
69+
name: string; // Name of your CLI tool
70+
version: string; // Version number
71+
description: string; // Brief description
72+
commands: Command[]; // Array of command definitions
73+
};
74+
```
75+
76+
### `Command` Interface
77+
78+
```typescript
79+
type Command<TArgs extends z.ZodType = z.ZodObject<any>> = {
80+
name: string; // Command name used in CLI
81+
description: string; // Command description shown in help
82+
args: TArgs; // Zod schema for command arguments
83+
handler: (args: z.infer<TArgs>) => void | Promise<void>; // Command implementation
84+
};
85+
```
86+
87+
### Formatting Output
88+
89+
The library provides utilities for formatting terminal output:
90+
91+
```typescript
92+
import { format } from '@rubriclab/cli';
93+
94+
console.log(format.success('Operation completed!'));
95+
console.log(format.error('Something went wrong'));
96+
console.log(format.warning('Proceed with caution'));
97+
console.log(format.info('Did you know?'));
98+
console.log(format.command('command-name'));
99+
console.log(format.parameter('--flag'));
100+
console.log(format.title('SECTION TITLE'));
101+
```
102+
103+
## Argument Parsing
104+
105+
Arguments are automatically parsed from command line flags:
106+
107+
- `--flag value` for string/number values
108+
- `--flag` for boolean flags (true if present)
109+
- Validation and default values from Zod schemas
110+
111+
## License
112+
113+
MIT

biome.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["@rubriclab/config/biome"]
3+
}

package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"scripts": {
3+
"prepare": "bun x simple-git-hooks",
4+
"bleed": "bun x npm-check-updates -u && bun i",
5+
"clean": "rm -rf .next && rm -rf node_modules",
6+
"format": "bun x biome format --write .",
7+
"lint": "bun x biome check . && bun x biome lint .",
8+
"lint:fix": "bun x biome check --fix --unsafe . && bun x biome lint --write --unsafe ."
9+
},
10+
"name": "@rubriclab/cli",
11+
"version": "0.0.1",
12+
"main": "index.ts",
13+
"dependencies": {
14+
"@rubriclab/package": "*",
15+
"@rubriclab/config": "*"
16+
},
17+
"simple-git-hooks": {
18+
"post-commit": "bun run rubriclab-postcommit"
19+
},
20+
"publishConfig": {
21+
"access": "public"
22+
}
23+
}

src/colors.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const colors = {
2+
reset: '\x1b[0m',
3+
bold: '\x1b[1m',
4+
red: '\x1b[31m',
5+
green: '\x1b[32m',
6+
yellow: '\x1b[33m',
7+
blue: '\x1b[34m',
8+
magenta: '\x1b[35m',
9+
cyan: '\x1b[36m'
10+
}
11+
12+
export const format = {
13+
error: (text: string) => `${colors.red}${colors.bold}${text}${colors.reset}`,
14+
success: (text: string) => `${colors.green}${colors.bold}${text}${colors.reset}`,
15+
warning: (text: string) => `${colors.yellow}${colors.bold}${text}${colors.reset}`,
16+
info: (text: string) => `${colors.blue}${colors.bold}${text}${colors.reset}`,
17+
command: (text: string) => `${colors.cyan}${text}${colors.reset}`,
18+
parameter: (text: string) => `${colors.magenta}${text}${colors.reset}`,
19+
title: (text: string) => `${colors.bold}${text}${colors.reset}`
20+
}

src/help.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { z } from 'zod'
2+
import { format } from './colors'
3+
import type { Command } from './types'
4+
5+
export function showHelp({
6+
commands,
7+
cliName,
8+
command
9+
}: {
10+
commands: Command[]
11+
cliName: string
12+
command?: string
13+
}): void {
14+
if (command) {
15+
const cmd = commands.find(c => c.name === command)
16+
if (!cmd) {
17+
console.error(format.error(`Unknown command: ${command}`))
18+
showHelp({ commands, cliName })
19+
return
20+
}
21+
showCommandHelp({ command: cmd, cliName })
22+
return
23+
}
24+
25+
console.log(`\n${format.title('USAGE')}`)
26+
console.log(` ${cliName} [command] [options]`)
27+
28+
console.log(`\n${format.title('COMMANDS')}`)
29+
for (const cmd of commands) {
30+
console.log(` ${format.command(cmd.name)}`)
31+
console.log(` ${cmd.description}`)
32+
}
33+
34+
console.log(`\n${format.title('OPTIONS')}`)
35+
console.log(` ${format.parameter('--help, -h')}`)
36+
console.log(' Show help information')
37+
console.log(` ${format.parameter('--version, -v')}`)
38+
console.log(' Show version information')
39+
}
40+
41+
function showCommandHelp({
42+
command,
43+
cliName
44+
}: {
45+
command: Command
46+
cliName: string
47+
}): void {
48+
console.log(`\n${format.title('USAGE')}`)
49+
console.log(` ${cliName} ${command.name} [options]`)
50+
51+
console.log(`\n${format.title('DESCRIPTION')}`)
52+
console.log(` ${command.description}`)
53+
54+
if (command.args instanceof z.ZodObject) {
55+
const shape = command.args.shape as z.AnyZodObject
56+
const entries = Object.entries(shape)
57+
58+
if (entries.length > 0) {
59+
console.log(`\n${format.title('OPTIONS')}`)
60+
61+
for (const [key, schema] of entries) {
62+
const type = getSchemaType(schema)
63+
const isRequired = schema.isOptional()
64+
const description = getSchemaDescription(schema)
65+
const defaultValue = getSchemaDefaultValue(schema)
66+
67+
const flagStr = type === 'boolean' ? `--${key}` : `--${key} <${type}>`
68+
69+
const requiredStr = isRequired ? ' (required)' : ''
70+
const defaultStr = defaultValue !== undefined ? ` (default: ${defaultValue})` : ''
71+
72+
console.log(` ${format.parameter(flagStr)}${requiredStr}${defaultStr}`)
73+
if (description) {
74+
console.log(` ${description}`)
75+
}
76+
console.log('')
77+
}
78+
}
79+
}
80+
}
81+
82+
function getSchemaType(schema: z.ZodTypeAny): string {
83+
if (schema instanceof z.ZodBoolean) return 'boolean'
84+
if (schema instanceof z.ZodString) return 'string'
85+
if (schema instanceof z.ZodNumber) return 'number'
86+
if (schema instanceof z.ZodArray) return 'array'
87+
if (schema instanceof z.ZodOptional) return getSchemaType(schema.unwrap())
88+
if (schema instanceof z.ZodDefault) return getSchemaType(schema.removeDefault())
89+
return 'value'
90+
}
91+
92+
function getSchemaDescription(schema: z.ZodTypeAny): string | undefined {
93+
if (schema instanceof z.ZodOptional) return getSchemaDescription(schema.unwrap())
94+
if (schema instanceof z.ZodDefault) return getSchemaDescription(schema.removeDefault())
95+
return schema.description
96+
}
97+
98+
function getSchemaDefaultValue(schema: z.ZodTypeAny): unknown {
99+
if (schema instanceof z.ZodDefault) {
100+
const defaultValue = schema._def.defaultValue()
101+
return defaultValue
102+
}
103+
if (schema instanceof z.ZodOptional) return getSchemaDefaultValue(schema.unwrap())
104+
return undefined
105+
}

src/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { parseCommand } from './parse'
2+
import type { CLI, Command } from './types'
3+
export { format } from './colors'
4+
export type { Command, CLI }
5+
6+
export function createCLI(config: {
7+
name: string
8+
version: string
9+
description: string
10+
commands: Command[]
11+
}): CLI {
12+
return {
13+
commands: config.commands,
14+
parse: async (argv = process.argv.slice(2)) => {
15+
await parseCommand({
16+
commands: config.commands,
17+
argv,
18+
cliName: config.name,
19+
version: config.version
20+
})
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)