Skip to content

Commit 4eb88c1

Browse files
committed
initial commit
0 parents  commit 4eb88c1

26 files changed

Lines changed: 3999 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
ci:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v6
14+
- uses: oven-sh/setup-bun@v2
15+
- run: bun install
16+
- run: bun run lint
17+
- run: bun run typecheck
18+
- run: bun test

.github/workflows/release.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
release:
8+
name: Release
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: write
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v5
15+
16+
- run: |
17+
git config user.name "${GITHUB_ACTOR}"
18+
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
19+
20+
- uses: oven-sh/setup-bun@v2
21+
with:
22+
bun-version: latest
23+
24+
- run: bun install --frozen-lockfile
25+
- run: bun run build
26+
27+
- uses: actions/setup-node@v6
28+
with:
29+
node-version: "lts/*"
30+
registry-url: "https://registry.npmjs.org"
31+
32+
- run: npm install -g npm@latest
33+
- run: npx release-it --ci
34+
env:
35+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# dependencies (bun install)
2+
node_modules
3+
4+
# output
5+
out
6+
dist
7+
*.tgz
8+
9+
# code coverage
10+
coverage
11+
*.lcov
12+
13+
# logs
14+
logs
15+
_.log
16+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17+
18+
# dotenv environment variable files
19+
.env
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
.env.local
24+
25+
# caches
26+
.eslintcache
27+
.cache
28+
*.tsbuildinfo
29+
30+
# IntelliJ based IDEs
31+
.idea
32+
33+
# Finder (MacOS) folder config
34+
.DS_Store

.husky/pre-commit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bunx lint-staged
2+
bun test

.prototools

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node = "~24"

.release-it.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"git": {
3+
"commitMessage": "chore: release v${version}"
4+
},
5+
"github": {
6+
"release": true
7+
},
8+
"npm": {
9+
"skipChecks": true
10+
}
11+
}

CLAUDE.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
@truyman/cli is a TypeScript CLI framework library for building command-line applications. The project is in early development.
8+
9+
## Commands
10+
11+
- `bun run lint` - Check formatting and linting (oxfmt + oxlint)
12+
- `bun run format` - Auto-fix formatting and lint issues
13+
- `bun run typecheck` - Run TypeScript type checking
14+
- `bun test` - Run all tests
15+
- `bun test path/to/file` - Run a single test file
16+
17+
## Tech Stack
18+
19+
- **Runtime**: Bun
20+
- **Language**: TypeScript (strict mode, ESNext target)
21+
- **Formatter/Linter**: oxfmt + oxlint
22+
- **Git Hooks**: husky with lint-staged (runs oxfmt + oxlint on all files pre-commit)
23+
24+
## Architecture
25+
26+
- `src/index.ts` - Public API exports (`command`, `run`) and top-level error handling
27+
- `src/command.ts` - `Command` class with argv parsing (via mri) and handler execution
28+
- `src/types.ts` - Type definitions with generics for type-safe args/options inference
29+
- `src/help.ts` - Help text formatting
30+
- `src/errors.ts` - Custom error classes (`MissingArgumentError`, `InvalidOptionError`)
31+
32+
Key pattern: `Command.run()` throws errors, `run()` (top-level) catches and pretty-prints them.

README.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# > cli
2+
3+
Build CLIs. TypeScript does the rest.
4+
5+
## Install
6+
7+
```bash
8+
npm i @truyman/cli
9+
```
10+
11+
## Quick Start
12+
13+
```typescript
14+
import { command, run } from "@truyman/cli";
15+
16+
const greet = command({
17+
name: "greet",
18+
args: [{ name: "name", type: "string" }],
19+
handler: ([name]) => console.log(`Hello, ${name}!`),
20+
});
21+
22+
run(greet, Bun.argv.slice(2));
23+
```
24+
25+
```bash
26+
$ bun greet.ts World
27+
Hello, World!
28+
```
29+
30+
That's it. Your args are typed. Your handler knows what it's getting.
31+
32+
## Features
33+
34+
- **Type-safe everything** - Args and options flow into your handler with full type inference
35+
- **Subcommands** - Nest commands infinitely: `cli remote add origin`
36+
- **Built-in help** - `-h` and `--help` just work
37+
- **Graceful errors** - `run()` catches known errors and prints them pretty
38+
- **Short & long flags** - `-v` and `--verbose`, the way nature intended
39+
40+
## Full Example
41+
42+
```typescript
43+
import { command, run } from "@truyman/cli";
44+
45+
const greet = command({
46+
name: "greet",
47+
description: "A friendly greeting CLI",
48+
version: "1.0.0",
49+
args: [
50+
{ name: "name", type: "string", description: "Who to greet" },
51+
],
52+
options: {
53+
shout: {
54+
type: "boolean",
55+
long: "shout",
56+
short: "s",
57+
description: "LOUD MODE",
58+
},
59+
times: {
60+
type: "number",
61+
long: "times",
62+
short: "n",
63+
description: "Repeat N times",
64+
},
65+
},
66+
handler: ([name], { shout, times }) => {
67+
let msg = `Hello, ${name}!`;
68+
if (shout) msg = msg.toUpperCase();
69+
for (let i = 0; i < (times || 1); i++) {
70+
console.log(msg);
71+
}
72+
},
73+
});
74+
75+
run(greet, Bun.argv.slice(2));
76+
```
77+
78+
```bash
79+
$ bun greet.ts Ada --shout -n 3
80+
HELLO, ADA!
81+
HELLO, ADA!
82+
HELLO, ADA!
83+
```
84+
85+
## API
86+
87+
### `command(options)`
88+
89+
| Property | Type | Required | Description |
90+
| ------------- | ------------------------- | -------- | ------------------------------ |
91+
| `name` | `string` | Yes | Command name |
92+
| `description` | `string` | No | Shown in help |
93+
| `version` | `string` | No | Version string |
94+
| `args` | `PositionalArg[]` | No | Positional arguments |
95+
| `options` | `Options` | No | Flag options |
96+
| `inherits` | `Options` | No | Options inherited from parents |
97+
| `handler` | `(args, options) => void` | \* | Your code goes here |
98+
| `subcommands` | `Command[]` | \* | Nested commands |
99+
100+
\* A command has either `handler` OR `subcommands`, never both.
101+
102+
### Subcommands
103+
104+
```typescript
105+
import { command, run, type Options } from "@truyman/cli";
106+
107+
// Shared options for all subcommands
108+
const GlobalOptions = {
109+
verbose: { type: "boolean", long: "verbose", short: "v" },
110+
} as const satisfies Options;
111+
112+
// Leaf command inherits parent options
113+
const add = command({
114+
name: "add",
115+
inherits: GlobalOptions,
116+
args: [{ name: "url", type: "string" }] as const,
117+
handler: ([url], { verbose }) => {
118+
if (verbose) console.log("[verbose] Adding remote...");
119+
console.log(`Added ${url}`);
120+
},
121+
});
122+
123+
// Parent command defines options, has subcommands
124+
const remote = command({
125+
name: "remote",
126+
options: GlobalOptions,
127+
subcommands: [add],
128+
});
129+
130+
const git = command({
131+
name: "git",
132+
subcommands: [remote],
133+
});
134+
135+
run(git, Bun.argv.slice(2));
136+
// $ bun index.ts remote add https://github.com/... --verbose
137+
```
138+
139+
The `inherits` property tells the leaf command which parent options it should parse and receive in its handler. This enables full type inference for inherited options.
140+
141+
### Positional Args
142+
143+
```typescript
144+
{ name: "file", type: "string", optional: true, description: "Input file" }
145+
```
146+
147+
Types: `"string"` | `"number"` | `"boolean"`
148+
149+
### Options
150+
151+
```typescript
152+
{
153+
verbose: {
154+
type: "boolean",
155+
long: "verbose",
156+
short: "v",
157+
description: "Extra output"
158+
}
159+
}
160+
```
161+
162+
### `run(command, argv)`
163+
164+
Runs the command. Handles `-h`/`--help` automatically. Missing args? Shows help. Bad option? Red error + usage.
165+
166+
### `command.help()`
167+
168+
Returns the auto-generated help string. For when you need it manually.
169+
170+
## License
171+
172+
MIT

0 commit comments

Comments
 (0)