diff --git a/apps/cli/README.md b/apps/cli/README.md index b8f45a0..5ebeaff 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -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 @@ -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. diff --git a/packages/clipc/README.md b/packages/clipc/README.md index a52f6ca..edb0586 100644 --- a/packages/clipc/README.md +++ b/packages/clipc/README.md @@ -3,10 +3,12 @@ [![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 @@ -14,7 +16,7 @@ extracted so other Deno CLIs can reuse it. deno add jsr:@dv-cli/clipc ``` -## Use +## Quick start ```typescript import { defineCli, forCtx, done } from "@dv-cli/clipc"; @@ -48,17 +50,106 @@ const cli = defineCli({ 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(); + +// 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({ + 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.