Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches:
- "*"
pull_request:
branches: [master]
branches: [main]

jobs:
test:
Expand All @@ -23,7 +23,7 @@ jobs:
fetch-depth: 1

- name: Use Deno Version ${{ matrix.deno-version }}
uses: denolib/setup-deno@master
uses: denoland/setup-deno@v1
with:
deno-version: ${{ matrix.deno-version }}

Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/jsr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: JSR release

on:
release:
types: [created]
workflow_dispatch:

jobs:
test-and-publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- name: Checkout cobra.js
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: 2.x

- name: Run tests
run: deno test

- name: Check types
run: deno check mod.ts

- name: Publish to JSR
run: deno publish
160 changes: 125 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
# cobra.js

A small cli framework for Deno/Node. This is heavily inspired on the excellent
[Cobra](https://github.com/spf13/cobra). The framework relies on
[minimist](https://github.com/minimistjs/minimist) library for the parsing of
arguments.
A lightweight, type-safe CLI framework for Deno and Node.js. Heavily inspired by
the excellent [Cobra](https://github.com/spf13/cobra) library from Go. The
framework uses [minimist](https://github.com/minimistjs/minimist) for argument
parsing.

The foundation to a good cli is good help. Most of the complexity in Cobra.js is
to auto-generate usage information. Cobra.js self-generates help from a
command's configuration.
The foundation of a good CLI is good help. Most of the complexity in cobra.js is
dedicated to auto-generating usage information. Cobra.js automatically generates
help from your command configurations.

All clis in cobra.js will use commands and flags.
All CLIs in cobra.js use commands and flags with full TypeScript type safety.

## Installation

**Deno** (via JSR):

```typescript
import { cli } from "jsr:@aricart/cobra";
```

**Node.js** (via JSR):

```bash
npx jsr add @aricart/cobra
```

```typescript
import { cli } from "@aricart/cobra";
```

## Commands

The basic configuration for a command, specifies the following:
The basic configuration for a command specifies the following:

```typescript
export interface Cmd {
export type Cmd = {
use: string; // name and usage
short?: string; // short description shown in help
long?: string; // long help (cmd action --help)
run: Run;
}
long?: string; // long help (shown with --help)
run?: Run; // command handler
};

// Command handler signature
export type Run = (
cmd: Command,
args: string[],
flags: Flags,
) => Promise<number>;
```

```typescript
Expand All @@ -45,10 +70,10 @@ const hello = root.addCommand({
// the command being executed, any args following a `--`
// and an object to let you access relevant flags.
run: (cmd, args, flags): Promise<number> => {
const strong = (flags.value<boolean>("strong") ?? false) ? "!!!" : "";
const strong = flags.value<boolean>("strong") ? "!!!" : "";
let n = flags.value<string>("name");
n = n === "" ? "mystery person" : n;
cmd.stdout(`hello ${n}${strong}`);
cmd.stdout(`hello ${n}${strong}\n`);
return Promise.resolve(0);
},
});
Expand All @@ -65,23 +90,23 @@ to provide to the `Deno.exit(n)` function.

## Flags

The second component is flags. While parsing arguments can easily be one with
utils such as [`parse`](from https://deno.land/std/flags) when creating cli
tools, you'll also want to provide long/short flag names, the type of the data
that you expect, and usage:
The second component is flags. While parsing arguments can easily be done with
utils such as [`parse`](https://deno.land/std/flags), when creating CLI tools,
you'll also want to provide long/short flag names, the type of data you expect,
and usage information:

```typescript
export interface Flag {
export type FlagValue = string | boolean | number;

export type Flag = {
type: "string" | "boolean" | "number";
name: string;
usage: string;
short: string;
required: boolean;
persistent: boolean;
changed: boolean;
default: null | unknown | unknown[];
value: null | unknown | unknown[];
}
default: null | FlagValue | FlagValue[];
};
```

Adding flags to the command above:
Expand All @@ -99,37 +124,71 @@ hello.addFlag({
type: "boolean",
usage: "say hello strongly",
});

// add goodbye command
const goodbye = root.addCommand({
use: "goodbye --name string [--strong]",
short: "says goodbye",
run: (cmd, args, flags): Promise<number> => {
const strong = flags.value<boolean>("strong") ? "!!!" : "";
let n = flags.value<string>("name");
n = n === "" ? "mystery person" : n;
cmd.stdout(`goodbye ${n}${strong}\n`);
return Promise.resolve(0);
},
});
goodbye.addFlag({
short: "n",
name: "name",
type: "string",
usage: "name to say goodbye to",
});
```

Processing flags is similarly trivial:
Processing flags is straightforward with full type safety:

```typescript
const n = flags.value<string>("name");
const strong = flags.value<boolean>("strong");
const count = flags.value<number>("count");
```

The value returned will be the one entered by the user, or the specified default
in the configuration for the default value for the type - `""` or `false` or
`0`.
The `value<T>()` method is type-safe and constrained to `FlagValue` types
(string, boolean, or number). The value returned will be the one entered by the
user, or the specified default, or the type's default value (`""` for strings,
`false` for booleans, `0` for numbers).

Flags marked as `persistent` can be associated with a container command and are
available to all subcommands, reducing code duplication.

## Running your commands

Once you build your command tree and related, flags, you simply call the root
command's `execute()` with an optinoal list of arguments. If not provided it
will try to get them from `Deno.args` or `process.argv.slice(2)`
Once you build your command tree and flags, simply call the root command's
`execute()` with an optional list of arguments. If not provided, it will
automatically get them from `Deno.args` or `process.argv.slice(2)`:

```typescript
// Execute with auto-detected args
await root.execute();

// Or with explicit args
await root.execute(["hello", "--name", "world"]);

// Use with Deno or Node
Deno.exit(await root.execute());
// or
process.exit(await root.execute());
```

The `execute()` method returns a `Promise<number>` representing the exit code (0
for success, non-zero for errors).

## Help is built-in

Help is implemented as a persistent flag on the root command, simply passing
`-h` or `--help` to any command, will display help. The help you get will be
context-sensitive. Container commands will list possible subcommands, and their
persistent flags. A leaf command will show usage, flags and persistent flags.
Help is implemented as a persistent flag on the root command. Simply pass `-h`
or `--help` to any command to display context-sensitive help. Container commands
list their subcommands and persistent flags. Leaf commands show usage, flags,
and inherited persistent flags.

This results in help that looks like this:

Expand Down Expand Up @@ -162,3 +221,34 @@ Flags:
-n, --name name to say hello to
-s, --strong say hello strongly
```

## TypeScript Support

cobra.js is written in TypeScript and provides full type safety:

- **Type-safe flag values**: The `FlagValue` type constrains flag values to
`string | boolean | number`
- **Generic constraints**: Methods like `value<T>()` and `values<T>()` use
`T extends FlagValue` to ensure type safety
- **Proper null handling**: All nullable fields are explicitly typed (e.g.,
`Command | null`)
- **Strong interface contracts**: All public APIs have well-defined types

Example with full type safety:

```typescript
import { cli, type Command, type Flags } from "jsr:@aricart/cobra";

const root = cli({
use: "myapp",
run: async (cmd: Command, args: string[], flags: Flags): Promise<number> => {
// TypeScript knows these are the correct types
const name = flags.value<string>("name"); // string
const verbose = flags.value<boolean>("verbose"); // boolean
const port = flags.value<number>("port"); // number

// Return exit code
return 0;
},
});
```
7 changes: 4 additions & 3 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"strict": true
},
"name": "@aricart/cobra",
"version": "0.0.15",
"version": "0.1.0-1",
"exports": {
".": "./mod.ts"
},
Expand All @@ -16,8 +16,9 @@
]
},
"imports": {
"@aricart/runtime": "jsr:@aricart/runtime@^0.0.1",
"@std/fmt": "jsr:@std/fmt@^1.0.3",
"@aricart/runtime": "jsr:@aricart/runtime@^0.0.2",
"@std/fmt": "jsr:@std/fmt@^1.0.8",
"@std/assert": "jsr:@std/assert@^1.0.15",
"minimist": "npm:minimist@^1.2.8"
}
}
10 changes: 5 additions & 5 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ const hello = root.addCommand({
// the command being executed, any args following a `--`
// and an object to let you access relevant flags.
run: (cmd, _args, flags): Promise<number> => {
const strong = (flags.value<boolean>("strong") ?? false) ? "!!!" : "";
const strong = flags.value<boolean>("strong") ? "!!!" : "";
let n = flags.value<string>("name");
n = n === "" ? "mystery person" : n;
cmd.stdout(`hello ${n}${strong}`);
cmd.stdout(`hello ${n}${strong}\n`);
return Promise.resolve(0);
},
});
Expand All @@ -35,10 +35,10 @@ const goodbye = root.addCommand({
use: "goodbye --name string [--strong]",
short: "says goodbye",
run: (cmd, _args, flags): Promise<number> => {
const strong = (flags.value<boolean>("strong") ?? false) ? "!!!" : "";
const strong = flags.value<boolean>("strong") ? "!!!" : "";
let n = flags.value<string>("name");
n = n === "" ? "mystery person" : n;
cmd.stdout(`goodbye ${n}${strong}`);
cmd.stdout(`goodbye ${n}${strong}\n`);
return Promise.resolve(0);
},
});
Expand All @@ -49,4 +49,4 @@ goodbye.addFlag({
usage: "name to say goodbye to",
});

Deno.exit(await root.execute(Deno.args));
Deno.exit(await root.execute());
Loading