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
57 changes: 50 additions & 7 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
[![JSR](https://jsr.io/badges/@dv-cli/dv)](https://jsr.io/@dv-cli/dv)
[![JSR Score](https://jsr.io/badges/@dv-cli/dv/score)](https://jsr.io/@dv-cli/dv)

A language-agnostic, git-native changelog CLI for monorepos. Records,
not commit messages.
A language-agnostic, git-native changelog and release CLI for monorepos.
Versioning is driven by **Records** — small files declaring intent — not by
parsing commit messages. Strict SemVer, per-package CHANGELOGs and git tags,
and plugins that teach `dv` about any ecosystem.

## Install

Expand All @@ -21,17 +23,58 @@ dv version # bump versions + write CHANGELOGs
dv release # tag + publish
```

## How it works

- **Records, not commit messages.** A Record is a markdown file in
`.dv/records/` declaring a single user-facing change: its `type` (`feat`,
`fix`, `feat!`, `fix!`), the packages it touches, and a note. `dv` never
parses git history, so contributors write commits however they like; the
Record is the authoritative declaration of intent. Teams already on
Conventional Commits get bonus affordances, but CC is an accelerator, never a
gate.
- **Plugins are executables.** `dv` itself is ecosystem-agnostic. A plugin —
any program speaking JSON over stdio — teaches it how to discover packages,
read and write versions, and rewrite dependency constraints for a given
ecosystem (Cargo, npm, Deno, …). No host-language lock-in; copyable example
plugins ship as references to adapt.
- **Two-phase release.** `dv version` computes the bumps from pending Records,
writes per-package CHANGELOGs, and stages one reviewable commit. `dv release`
then mints per-package git tags and runs publish hooks. Release state lives in
git tags alone — a package needs releasing iff its current version has no
matching tag — so there is no state file to drift.
- **Dry-run is first-class.** `dv version --dry-run` and `dv release --dry-run`
produce a complete preview with zero side effects, including no write-side
plugin calls. The same plan-building code runs in dry-run and real paths.

## Library API

`dv` ships its command runners as an importable library so other Deno programs
can drive it in-process instead of shelling out:

```typescript
import { runStatus, runVersion } from "@dv-cli/dv";
```

The surface mirrors the CLI — `runStatus`, `runVersion`, `runRelease`,
`runValidate`, `runV1`, `runInit`, `runAdd`, `runRename`, the `runPlugin*`
family, and `runMigrateConfig` — each returning a typed result envelope (the
same shape the CLI emits under `--json`). Shell scripts and agent fleets get the
identical machine-readable contract: non-interactive flags, versioned `--json`
output, and stable exit codes, with no privileged tier.

## Docs

Full documentation — tutorials, concepts, guides, reference — lives
at [the dv docs site](https://github.com/ben-laird/dv/tree/main/apps/docs/content).
The five-minute tutorial is [getting-started](https://github.com/ben-laird/dv/blob/main/apps/docs/content/getting-started.md).
Full documentation — tutorials, concepts, guides, and per-command reference —
lives at
[the dv docs site](https://github.com/ben-laird/dv/tree/main/apps/docs/content).
The five-minute tutorial is
[getting-started](https://github.com/ben-laird/dv/blob/main/apps/docs/content/getting-started.md).

## Repository

This package is one of two published from
[ben-laird/dv](https://github.com/ben-laird/dv). The other,
[`@dv-cli/clipc`](https://jsr.io/@dv-cli/clipc), is the typed CLI
framework dv is built on.
[`@dv-cli/clipc`](https://jsr.io/@dv-cli/clipc), is the typed CLI framework dv
is built on.

MIT licensed.
119 changes: 105 additions & 14 deletions packages/clipc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
[![JSR](https://jsr.io/badges/@dv-cli/clipc)](https://jsr.io/@dv-cli/clipc)
[![JSR Score](https://jsr.io/badges/@dv-cli/clipc/score)](https://jsr.io/@dv-cli/clipc)

**CLIPC** — Command Line Interface Procedure Call. A typed CLI
framework: routers, leaves, flag specs, and structured error
responses. The substrate [dv](https://jsr.io/@dv-cli/dv) is built on,
extracted so other Deno CLIs can reuse it.
**CLIPC** — Command Line Interface Procedure Call. A typed CLI framework for
Deno: a tree of routers and leaves, declarative flag specs, and structured
error responses. The framework owns argv parsing, dispatch, help generation,
and error rendering; you write the leaves. It is the substrate
[dv](https://jsr.io/@dv-cli/dv) is built on, extracted so other Deno CLIs can
reuse it.

## Install

```sh
deno add jsr:@dv-cli/clipc
```

## Use
## Quick start

```typescript
import { defineCli, forCtx, done } from "@dv-cli/clipc";
Expand Down Expand Up @@ -48,17 +50,106 @@ const cli = defineCli<MyCtx>({
if (import.meta.main) Deno.exit(await cli.run(Deno.args));
```

## What you get
## Core concepts

- **Typed routers + leaves** with full TS narrowing on flags + ctx
- **Structured error responses** (the `CliError` discriminated union)
with a versioned JSON envelope for machine consumers
- **Errors-as-values** — leaves return `{kind: "ok" | "error" | "help"}`
rather than throwing; the framework renders them
- **Built-in help** — `--help` at every level of the router tree
clipc is five ideas. Once you have them, the whole surface follows.

- **Typed routers** (`router`) — interior nodes of the command tree. A router
maps subcommand names to children (leaves or nested routers) and contributes
`--help` at its level. Nesting routers gives you `mycli plugin verify`-style
command paths with no manual argv slicing.
- **Typed leaves** (`command`) — the executable tips of the tree. A leaf
declares its flags and a `run` handler. Flag types flow through to `run`'s
`flags` argument, so `flags.name` is a `string | undefined`, not `any` — the
compiler enforces the contract you declared.
- **Flag specs** (`FlagSpec`, `lowerFlagSpec`) — flags are data, not parsing
code. Each flag declares its `kind` (`string`, `boolean`, …) and description;
the framework derives parsing, `--help` text, and the static `FlagsOf<…>` type
from one declaration. `inheritedFlags` is a typed-identity capture: declare a
shared flag map once, then spread it into each leaf's `flags` so cross-cutting
flags keep one source of truth without drifting per-flag kinds.
- **Errors-as-values** (`done`, `next`, `Step`) — handlers return a result
(`{ kind: "ok" | "error" | "help" }`) rather than throwing. `done` ends
dispatch; `next` hands control to a child. The framework renders the outcome
and picks the exit code, so control flow stays explicit and testable.
- **Structured errors** (`CliError`, `renderCliError`, `parseCliErrorEnvelope`)
— a discriminated-union error type with a **versioned JSON envelope**. Humans
get a formatted message; `--json` consumers get a stable shape they can parse
with `parseCliErrorEnvelope` (Zod-free results). Extend `CliError` with your
own `CliErrorShape` for typed narrowing at catch sites.

**Built-in help** falls out of the above: `--help` works at every level of the
tree, formatted from the same flag specs and descriptions you already wrote
(`formatRouterHelp`, `formatCommandHelp`).

## A larger example

Nested routers, a shared flag spread into a leaf, and a structured error:

```typescript
import { defineCli, forCtx, done, inheritedFlags, CliError } from "@dv-cli/clipc";

interface Ctx { cwd: string }
const { command, router } = forCtx<Ctx>();

// Declare cross-cutting flags once; spread them into any leaf that opts in.
const sharedFlags = inheritedFlags({
verbose: { kind: "boolean", description: "Chatty output" },
});

const build = command({
description: "Build the project",
flags: {
...sharedFlags,
release: { kind: "boolean", description: "Optimized build" },
},
run: async ({ flags }) => {
// `flags.verbose` and `flags.release` are both typed booleans here.
if (flags.release && !flags.verbose) {
return done({
kind: "error",
error: new CliError({
code: "needs-verbose",
message: "use --verbose for release builds",
}),
});
}
console.log(flags.release ? "release build" : "dev build");
return done({ kind: "ok" });
},
});

const project = router({ description: "Project commands", commands: { build } });
const root = router({ description: "My tool", commands: { project } });

const cli = defineCli<Ctx>({
name: "mytool",
version: "1.0.0",
rootRouter: root,
makeContext: () => ({ cwd: Deno.cwd() }),
});

// mytool project build --release --verbose
if (import.meta.main) Deno.exit(await cli.run(Deno.args));
```

## When to use clipc

Reach for clipc when you are building a Deno CLI that wants **typed routing,
declarative flags, and a machine-readable error contract** without hand-rolling
argv parsing and help text. It is deliberately small and unopinionated about
everything else: no config loading, no logging, no plugin system — just the
command tree and its dispatch.

If you only need to parse a flat set of flags, `@std/cli`'s `parseArgs` is
lighter. clipc earns its keep once you have nested subcommands, want flag types
to flow into handlers, or need a stable `--json` error envelope for other tools
(or agents) to consume.

## Repository

Source lives in
[ben-laird/dv](https://github.com/ben-laird/dv/tree/main/packages/clipc).
MIT licensed.
[ben-laird/dv](https://github.com/ben-laird/dv/tree/main/packages/clipc). For a
real-world consumer, read how
[dv](https://github.com/ben-laird/dv/tree/main/apps/cli/src/cli) wires its
command tree on top of clipc. MIT licensed.