diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml
index 4e43839..220b0f9 100644
--- a/.github/workflows/verify.yaml
+++ b/.github/workflows/verify.yaml
@@ -28,5 +28,8 @@ jobs:
- name: lint
run: deno lint
+ - name: check
+ run: deno check mod.ts
+
- name: test
- run: deno test
+ run: deno task test
diff --git a/AGENTS.md b/AGENTS.md
index d204ffb..b5dd1bd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,19 +1,13 @@
# Configliere
-Functional configuration parser that validates all program inputs (config files,
-environment variables, CLI options) ahead of time using a single schema based on
-the [Standard Schema](https://standardschema.dev) spec.
+Configuration for programs.
-## Architecture
+## Current state
-Configliere is a parser-combinator library. Small parsers compose into larger
-ones. Every parser implements the `Parser` interface from `lib/types.ts` and
-returns an `Increment` — either `Done|Fail` for single-step parsers or
-`Next|Fail` for multi-step (phased) parsers.
-
-### Key design concepts
-
-- **Source tracking**: Every parsed value carries metadata about where it came
+Configliere is undergoing a ground-up rebuild. No implementation architecture is
+established yet. Design from the bottom up with small API sketches, exact type
+expectations, and tests before implementation. Do not carry architectural
+assumptions forward from pre-rebuild history.
## Conventions
@@ -30,20 +24,14 @@ Strongly prefer one-word variable and function names (e.g. `dir` not `tmpDir`,
### Testing
- Framework: `@std/testing/bdd` (`describe`/`it`) with `@std/expect`
-- Schema library in tests: `arktype` (via `type()`)
-- Test helpers in `test/test-helpers.ts`: `parseOk()` and `parseNotOk()` wrap
- `parseSync` with assertions
-- Tests live in `test/` and mirror `lib/` module names (e.g.
- `test/field.test.ts`)
+- Tests live in `test/` and mirror the source layout.
### Runtime and tooling
- **Runtime**: Deno
- **Registry**: JSR (`@frontside/configliere`)
-- **Linting**: `deno lint` (excludes `prefer-const` and `require-yield`)
-- **Testing**: `deno test`
-- **Examples**: Files in `examples/` must compile (`deno check`) and run
- (`deno run`) without errors
+- **Linting**: `deno lint` (excludes `prefer-const`)
+- **Testing**: `deno task test`
- **Build**: `deno task build:npm` (dnt) and `deno task build:jsr`
### Dependencies
diff --git a/README.md b/README.md
index ba3c62c..d906d30 100644
--- a/README.md
+++ b/README.md
@@ -1,327 +1,212 @@
-#
Configliere
-
-Smart, FP configuration parser that validates all program inputs ahead of time,
-including config files, environment variables, and command line options using a
-single schema.
-
-## Introduction
-
-Conceptually, "config" is a palette of settings and switches that is accessed
-from various portions of our program to alter its behavior without altering its
-code. However the story around config is often made very messy by the fact that
-it is read at _different_ times during program execution and from many
-_different_ sources such as configuration files and environment variables.
-
-
-
-As a result, configuration is often fragile and difficult to understand. Some of
-the symptoms of a fragmented config are:
-
-- **The deferred crash** Happens when a program begins running, perhaps even for
- awhile, but then it reaches for a configuration value via an interface like
- `app.getConfig("field")` only to find that the value is invalid, or missing
- altogether. Happens a lot with config files and enviroment variables.
-- **conflicting configs** When a program has more than one way of specifying the
- same parameter, and they clobber each other.
- - **Unclear priority** Which parameter wins? The one coming from a CLI option
- or an environment variable?
- - **Mysterious provenance** You can see a value for a github token, but where
- does it come from? It could be from `app-config.yaml`, or
- `app-config.production.yaml`. Then again, it could be specified in the
- `GITHUB_TOKEN` environment variable. But don't forget there is also a
- `--github-token` command line option.
- - **magical behavior** A furiously frustrating deployment-specific failure is
- tracked down to some random environment variable overriding a parameter that
- isn't configured like that in any other enviroment.
-- **Ad-hoc validation** - There are different ways to verify the runtime type of
- a configuration value depending on whether it comes from the CLI, environment,
- or a configuration file.
-
-Configliere solves all of these problems by re-imagining "config" not as a
-constellation of globally floating objects from which we can read in values at
-any point, but instead as a _single_, _pre-validated_, type-safe data structure
-that is passed as the input of our program's entry point. It tracks the _source_
-of each value that ends up in the final config, so where a parameter is set is
-never a mystery.
-
-
-
-This has a profound impact on our program as a whole because it lets us treat
-the entire process as one function call that takes a single value as its input.
-
-## Quick Start
-
-Configliere uses [Standard Schema][standard-schema] to define the static type of
-each configuration parameter as well as to validate that type at runtime. In
-these examples, we'll use [Arktype][arktype], but you can use
-[any library that implements the Standard Schema spec][schema-libs].
-
-### Simple program
-
-A basic program with options, flags, and positional arguments:
+# Configliere
-```ts
-import { type } from "arktype";
-import { cli, field, object, program } from "configliere";
-
-let serve = program({
- name: "serve",
- version: "1.0.0",
- config: object({
- host: {
- description: "hostname to bind",
- aliases: ["-H"],
- ...field(type("string"), field.default("localhost")),
- },
- port: {
- description: "port to listen on",
- aliases: ["-p"],
- ...field(type("number"), field.default(3000)),
- },
- debug: {
- description: "enable debug logging",
- aliases: ["-d"],
- ...field(type("boolean"), field.default(false)),
- },
- entry: {
- description: "entrypoint file",
- ...field(type("string"), cli.argument()),
- },
- }),
-});
-
-let parser = serve.createParser({
- args: process.argv.slice(2),
- envs: [{ name: "env", value: process.env }],
-});
-
-switch (parser.type) {
- case "help":
- console.log(parser.print());
- break;
- case "version":
- console.log(parser.print());
- break;
- case "main": {
- let parsed = parser.parse();
- if (!parsed.ok) {
- console.error(parsed.error.message);
- process.exit(1);
- }
- let { host, port, debug, entry } = parsed.value;
- // start your server...
- }
-}
-```
-
-Running `serve --help` prints:
+> [!NOTE]
+> Configliere is undergoing a ground-up rebuild. The ideas below are working,
+> but the API is not yet stable.
-```
-Usage: serve [OPTIONS]
+Configliere models a command-line program as a tree of typed routes. Parsing
+does not merely produce a bag of options: it resolves an intent such as
+`HELP /`, `VERSION /`, or `EXECUTE /serve` and, for execution, binds a model for
+every route that led there.
-Arguments:
- entrypoint file
+The architecture has four small pieces:
-Options:
- -H, --host hostname to bind [default: localhost]
- -p, --port port to listen on [default: 3000]
- -d, --debug enable debug logging [default: false]
- -h, --help show help
- -v, --version show version
-```
+- A **route** is an application-relative address such as `/` or `/serve`.
+- A **method** says what to do with that route. Every route supports help;
+ `version()` adds version and `command()` creates an executable route.
+- Each route owns its own **model**. An execute intent contains the current
+ `model` and a path-addressed `models` map for the entire matched lineage.
+- Definitions are immutable composition pipelines. Elements such as
+ `description()`, `option()`, `toggle()`, `version()`, and `routes()` enrich
+ both the runtime definition and its TypeScript type.
-All of these are equivalent:
+Parameters are validated with [Standard Schema](https://standardschema.dev/), so
+Configliere works with ArkType, Zod, Valibot, and other conforming schema
+libraries. The examples below use ArkType.
-```sh
-serve app.ts -p 8080 --host localhost
-HOST=localhost serve app.ts -p 8080
-serve app.ts --port=8080 -H localhost
-```
+## One command, one model
-### Commands
-
-For programs with subcommands, use `commands()`:
+`command()` is an executable route. Here it creates the root route `/` with
+help, version, and execute methods:
```ts
+import process from "node:process";
+import {
+ command,
+ description,
+ name,
+ option,
+ parse,
+ printHelp,
+ printVersion,
+ schema,
+ toggle,
+ version,
+} from "@frontside/configliere";
import { type } from "arktype";
-import { cli, commands, field, help, object, program } from "configliere";
-
-let app = program({
- name: "myapp",
- version: "3.2.0",
- config: commands({
- help,
- dev: {
- description: "start dev server",
- ...object({
- port: {
- description: "port to listen on",
- aliases: ["-p"],
- ...field(type("number"), field.default(3000)),
- },
- open: {
- description: "open browser on start",
- ...field(type("boolean"), field.default(false)),
- },
- }),
- },
- build: {
- description: "build for production",
- ...object({
- outdir: {
- description: "output directory",
- aliases: ["-o"],
- ...field(type("string"), field.default("dist")),
- },
- }),
- },
- }),
-});
-```
-
-Running `myapp -h` prints:
-```
-Usage: myapp [OPTIONS]
+const app = command(
+ name("server"),
+ description("Run the HTTP server."),
+ version("1.0.0"),
+ option(
+ name("port"),
+ description("Port to listen on."),
+ schema(type("number")),
+ ),
+ toggle(
+ name("verbose"),
+ description("Print request diagnostics."),
+ ),
+);
-Commands:
- help show help for a command
- dev start dev server
- build build for production
+const result = parse(app, { argv: process.argv.slice(2) });
-Options:
- -h, --help show help
- -v, --version show version
-```
+if (!result.ok) {
+ console.error(result.code, result);
+ process.exit(1);
+}
-The built-in `help` command provides per-command help. Running `myapp help dev`
-prints:
+switch (result.method) {
+ case "help":
+ console.log(printHelp(result));
+ break;
+ case "version":
+ console.log(printVersion(result));
+ break;
+ case "execute":
+ // Exactly { port: number; verbose: boolean }, not Record.
+ serve(result.model);
+ break;
+}
+function serve(model: { port: number; verbose: boolean }): void {
+ console.log(`listening on ${model.port}; verbose=${model.verbose}`);
+}
```
-Usage: dev [OPTIONS]
-Options:
- -p, --port port to listen on [default: 3000]
- --open open browser on start [default: false]
-```
+`parse()` receives the arguments _after_ the executable name, which is why the
+example passes `process.argv.slice(2)`. The full command lines above resolve as
+follows:
-Parse results are discriminated unions on `name`:
+| Invocation | Result |
+| ------------------------------ | ---------------------------------------------------------------------------- |
+| `server --help` | `HELP /`; `printHelp()` renders the route's usage, options, and descriptions |
+| `server --version` | `VERSION /`; `printVersion()` returns `server 1.0.0` |
+| `server --port 4000` | `EXECUTE /`; `model` is `{ port: 4000, verbose: false }` |
+| `server --port=4000 --verbose` | `EXECUTE /`; `model` is `{ port: 4000, verbose: true }` |
+| `server --port nope` | `unprocessable-content`; execution never receives an invalid model |
-```ts
-let parsed = result.parse();
-if (parsed.ok) {
- switch (parsed.value.name) {
- case "dev":
- // parsed.value.config is { port: number; open: boolean }
- break;
- case "build":
- // parsed.value.config is { outdir: string }
- break;
- case "help":
- // parsed.value.config is { command: string; text: string }
- console.log(parsed.value.config.text);
- break;
- }
-}
-```
+The method is a discriminant. Once the switch reaches `"execute"`, TypeScript
+knows that `model` exists and has exactly the shape assembled by the command's
+parameters. Help and version are resolved before execution validation, so
+`server --help` does not require a port.
-### Loading config files
+## Add commands, keep dispatch flat
-Use `sequence()` to split parsing into phases. The first phase reads the config
-file path, then you load the file and feed its contents into the second phase:
+Routes form a tree, but callers do not have to mirror that tree with nested
+conditionals. `IntentsOf` is a flat union of every method and route
+the definition can reach, so `result.route` is another useful discriminant.
```ts
-import { readFileSync } from "node:fs";
+import process from "node:process";
+import {
+ command,
+ name,
+ option,
+ parse,
+ printHelp,
+ routes,
+ schema,
+ toggle,
+} from "@frontside/configliere";
import { type } from "arktype";
-import { cli, field, object, program, sequence } from "configliere";
-
-let app = program({
- name: "myctl",
- version: "2.0.0",
- config: sequence(
- object({
- config: {
- description: "config file",
- aliases: ["-c"],
- ...field(type("string")),
- },
- }),
- object({
- port: {
- description: "port to listen on",
- aliases: ["-p"],
- ...field(type("number"), field.default(3000)),
- },
- host: {
- description: "hostname to bind",
- aliases: ["-H"],
- ...field(type("string"), field.default("localhost")),
- },
- }),
+
+const app = command(
+ name("simulacrum"),
+ toggle(name("verbose")),
+ routes(
+ command(
+ name("serve"),
+ option(name("port"), schema(type("number"))),
+ ),
+ command(
+ name("inspect"),
+ toggle(name("json")),
+ ),
),
-});
-
-let result = app.createParser({
- args: process.argv.slice(2),
- envs: [{ name: "env", value: process.env }],
-});
-
-if (result.type === "main") {
- // phase 1: extract the config file path
- let step1 = result.parse();
- if (!step1.ok) {
- console.error(step1.error.message);
- process.exit(1);
- }
+);
- // load the config file
- let config = JSON.parse(readFileSync(step1.value.config, "utf-8"));
-
- // phase 2: parse remaining args merged with config file values
- let step2 = step1.parse({
- args: step1.remainder.args,
- values: [{ name: step1.value.config, value: config }],
- });
- if (!step2.ok) {
- console.error(step2.error.message);
- process.exit(1);
- }
+const result = parse(app, { argv: process.argv.slice(2) });
- let { port, host } = step2.value;
- // start your server...
+if (!result.ok) {
+ console.error(result.code, result);
+ process.exit(1);
}
-```
-
-Running `myctl -c app.json -p 8080` first extracts `config: "app.json"`, then
-loads `app.json` and merges its values with the remaining CLI args. Values from
-the CLI take priority over the config file, and all values go through the same
-schema validation.
-## Configuration Sources
+if (result.method === "help") {
+ console.log(printHelp(result));
+} else {
+ switch (result.route) {
+ case "/":
+ // model: { verbose: boolean }
+ // models: { "/": { verbose: boolean } }
+ start(result.model);
+ break;
-Configliere unifies three input sources. Every value, regardless of source,
-passes through the same Standard Schema validation.
+ case "/serve":
+ // model is only the selected route's model.
+ result.model.port; // number
-### CLI arguments
+ // models contains every model on the matched route lineage.
+ result.models["/"].verbose; // boolean
+ result.models["/serve"].port; // number
-Options are specified with `--name value` or `--name=value`. Boolean fields are
-switches: `--debug` enables, `--no-debug` disables. Positional arguments are
-declared with `cli.argument()`. Array fields accept repeated values:
-`--user alice --user bob`.
+ // @ts-expect-error: /inspect was not part of this match.
+ result.models["/inspect"];
+ serve(result.model, result.models["/"]);
+ break;
-### Environment variables
+ case "/inspect":
+ // model: { json: boolean }
+ // models: { "/": { verbose: boolean }, "/inspect": { json: boolean } }
+ inspect(result.model, result.models["/"]);
+ break;
+ }
+}
-Field names are automatically mapped to `UPPER_SNAKE_CASE` environment
-variables. Nested scopes (e.g. command names) are joined with `_`. For example,
-a `port` field inside a `serve` command reads from `SERVE_PORT`.
+function start(model: { verbose: boolean }): void {
+ console.log(`simulacrum; verbose=${model.verbose}`);
+}
-### Object values
+function serve(
+ model: { port: number },
+ root: { verbose: boolean },
+): void {
+ console.log(`serving on ${model.port}; verbose=${root.verbose}`);
+}
-Plain objects can be passed directly (e.g. from a parsed JSON/YAML config file).
-Each object value is tagged with a source name for provenance tracking. When
-multiple sources provide the same field, CLI args take priority over env vars,
-which take priority over object values.
+function inspect(
+ model: { json: boolean },
+ root: { verbose: boolean },
+): void {
+ console.log(`inspecting; json=${model.json}; verbose=${root.verbose}`);
+}
+```
-[standard-schema]: https://standardschema.dev
-[arktype]: https://arktype.io
-[schema-libs]: https://standardschema.dev/#what-schema-libraries-implement-the-spec
+| Invocation | Result |
+| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| `simulacrum --verbose` | `EXECUTE /`; `model` and `models["/"]` are `{ verbose: true }` |
+| `simulacrum --verbose serve --port 4000` | `EXECUTE /serve`; `model` is `{ port: 4000 }`, while `models` also contains `"/": { verbose: true }` |
+| `simulacrum inspect --json` | `EXECUTE /inspect`; `model` is `{ json: true }` and the root model has `{ verbose: false }` |
+| `simulacrum serve --help` | `HELP /serve`; no root or command model is validated |
+
+Route tokens select the deepest matching route; they are not positional
+arguments. Options on each segment bind only to the model owned by that route.
+The root definition's name identifies the executable but is not repeated in
+route IDs: the root is `/`, not `/simulacrum`.
+
+For a deeper route such as `/database/clean`, the same rule applies:
+`result.model` is the clean command's model, while `result.models` has exact
+entries for `/`, `/database`, and `/database/clean`. Sibling models are absent
+at runtime and from the TypeScript type.
diff --git a/deno.json b/deno.json
index fc99879..bea876c 100644
--- a/deno.json
+++ b/deno.json
@@ -2,18 +2,19 @@
"name": "@frontside/configliere",
"exports": "./mod.ts",
"license": "MIT",
- "publish": { "include": ["vendor", "lib", "mod.ts", "README.md"] },
+ "publish": { "include": ["lib", "mod.ts", "README.md", "LICENSE.md"] },
"imports": {
- "@standard-schema/spec": "npm:@standard-schema/spec@^1.0.0",
- "@std/expect": "jsr:@std/expect@^1.0.16",
- "@std/testing": "jsr:@std/testing@^1.0.15",
- "arktype": "npm:arktype@^2.1.20",
- "dnt": "jsr:@deno/dnt@0.42.3"
+ "@std/expect": "jsr:@std/expect@^1.0.19",
+ "@std/testing/bdd": "jsr:@std/testing@^1.0.19/bdd",
+ "arktype": "npm:arktype@^2.2.3",
+ "dnt": "jsr:@deno/dnt@0.42.3",
+ "@standard-schema/spec": "npm:@standard-schema/spec@^1.1.0"
},
"lint": {
- "rules": { "exclude": ["prefer-const", "require-yield", "ban-types"] }
+ "rules": { "exclude": ["prefer-const"] }
},
"tasks": {
+ "test": "deno test",
"build:npm": "deno run -A tasks/build-npm.ts",
"build:jsr": "deno run -A tasks/build-jsr.ts"
}
diff --git a/deno.lock b/deno.lock
index e5bb209..bedac31 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1,121 +1,92 @@
{
"version": "5",
"specifiers": {
- "jsr:@david/code-block-writer@^13.0.3": "13.0.3",
- "jsr:@deno/dnt@0.42.3": "0.42.3",
- "jsr:@std/assert@^1.0.14": "1.0.18",
- "jsr:@std/assert@^1.0.17": "1.0.18",
- "jsr:@std/expect@^1.0.16": "1.0.17",
- "jsr:@std/fmt@1": "1.0.8",
- "jsr:@std/fs@1": "1.0.22",
- "jsr:@std/internal@^1.0.10": "1.0.12",
- "jsr:@std/internal@^1.0.12": "1.0.12",
- "jsr:@std/path@1": "1.1.4",
- "jsr:@std/path@^1.1.4": "1.1.4",
- "jsr:@std/testing@^1.0.15": "1.0.17",
- "jsr:@ts-morph/bootstrap@0.27": "0.27.0",
- "jsr:@ts-morph/common@0.27": "0.27.0",
- "npm:@standard-schema/spec@1": "1.1.0",
- "npm:arktype@^2.1.20": "2.1.20"
+ "jsr:@std/assert@^1.0.19": "1.0.19",
+ "jsr:@std/expect@^1.0.19": "1.0.19",
+ "jsr:@std/internal@^1.0.12": "1.0.14",
+ "jsr:@std/internal@^1.0.13": "1.0.14",
+ "jsr:@std/internal@^1.0.14": "1.0.14",
+ "jsr:@std/path@^1.1.4": "1.1.6",
+ "jsr:@std/testing@^1.0.19": "1.0.19",
+ "npm:@standard-schema/spec@*": "1.1.0",
+ "npm:@standard-schema/spec@1.1.0": "1.1.0",
+ "npm:@standard-schema/spec@^1.1.0": "1.1.0",
+ "npm:arktype@^2.2.3": "2.2.3",
+ "npm:zod@*": "4.4.3"
},
"jsr": {
- "@david/code-block-writer@13.0.3": {
- "integrity": "f98c77d320f5957899a61bfb7a9bead7c6d83ad1515daee92dbacc861e13bb7f"
- },
- "@deno/dnt@0.42.3": {
- "integrity": "62a917a0492f3c8af002dce90605bb0d41f7d29debc06aca40dba72ab65d8ae3",
- "dependencies": [
- "jsr:@david/code-block-writer",
- "jsr:@std/fmt",
- "jsr:@std/fs",
- "jsr:@std/path@1",
- "jsr:@ts-morph/bootstrap"
- ]
- },
- "@std/assert@1.0.18": {
- "integrity": "270245e9c2c13b446286de475131dc688ca9abcd94fc5db41d43a219b34d1c78",
+ "@std/assert@1.0.19": {
+ "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
"dependencies": [
"jsr:@std/internal@^1.0.12"
]
},
- "@std/expect@1.0.17": {
- "integrity": "316b47dd65c33e3151344eb3267bf42efba17d1415425f07ed96185d67fc04d9",
+ "@std/expect@1.0.19": {
+ "integrity": "25271785688c7ff902fa54875d6aa4001f552c5578f7f0fefb5b5aac1fc2ed8a",
"dependencies": [
- "jsr:@std/assert@^1.0.14",
- "jsr:@std/internal@^1.0.10"
- ]
- },
- "@std/fmt@1.0.8": {
- "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7"
- },
- "@std/fs@1.0.22": {
- "integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308",
- "dependencies": [
- "jsr:@std/internal@^1.0.12",
- "jsr:@std/path@^1.1.4"
+ "jsr:@std/assert",
+ "jsr:@std/internal@^1.0.13",
+ "jsr:@std/path"
]
},
"@std/internal@1.0.8": {
"integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5"
},
- "@std/internal@1.0.12": {
- "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
- },
- "@std/path@1.1.4": {
- "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
- "dependencies": [
- "jsr:@std/internal@^1.0.12"
- ]
- },
- "@std/testing@1.0.17": {
- "integrity": "87bdc2700fa98249d48a17cd72413352d3d3680dcfbdb64947fd0982d6bbf681",
- "dependencies": [
- "jsr:@std/assert@^1.0.17",
- "jsr:@std/internal@^1.0.12"
- ]
+ "@std/internal@1.0.14": {
+ "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"
},
- "@ts-morph/bootstrap@0.27.0": {
- "integrity": "b8d7bc8f7942ce853dde4161b28f9aa96769cef3d8eebafb379a81800b9e2448",
+ "@std/path@1.1.6": {
+ "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe",
"dependencies": [
- "jsr:@ts-morph/common"
+ "jsr:@std/internal@^1.0.14"
]
},
- "@ts-morph/common@0.27.0": {
- "integrity": "c7b73592d78ce8479b356fd4f3d6ec3c460d77753a8680ff196effea7a939052",
+ "@std/testing@1.0.19": {
+ "integrity": "f4236172365b216728dc3cc8b5e80a9f4c33083d1e4ede7613d5b25b4014898e",
"dependencies": [
- "jsr:@std/fs",
- "jsr:@std/path@1"
+ "jsr:@std/assert",
+ "jsr:@std/internal@^1.0.14"
]
}
},
"npm": {
- "@ark/schema@0.46.0": {
- "integrity": "sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ==",
+ "@ark/schema@0.56.2": {
+ "integrity": "sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==",
"dependencies": [
"@ark/util"
]
},
- "@ark/util@0.46.0": {
- "integrity": "sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg=="
+ "@ark/util@0.56.2": {
+ "integrity": "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ=="
},
"@standard-schema/spec@1.1.0": {
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
},
- "arktype@2.1.20": {
- "integrity": "sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q==",
+ "arkregex@0.0.8": {
+ "integrity": "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==",
"dependencies": [
- "@ark/schema",
"@ark/util"
]
+ },
+ "arktype@2.2.3": {
+ "integrity": "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==",
+ "dependencies": [
+ "@ark/schema",
+ "@ark/util",
+ "arkregex"
+ ]
+ },
+ "zod@4.4.3": {
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="
}
},
"workspace": {
"dependencies": [
"jsr:@deno/dnt@0.42.3",
- "jsr:@std/expect@^1.0.16",
- "jsr:@std/testing@^1.0.15",
- "npm:@standard-schema/spec@1",
- "npm:arktype@^2.1.20"
+ "jsr:@std/expect@^1.0.19",
+ "jsr:@std/testing@^1.0.19",
+ "npm:@standard-schema/spec@^1.1.0",
+ "npm:arktype@^2.2.3"
]
}
}
diff --git a/examples/commands.ts b/examples/commands.ts
deleted file mode 100644
index 48b26f7..0000000
--- a/examples/commands.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import assert from "node:assert";
-import { type } from "arktype";
-import { cli, field } from "../lib/field.ts";
-import { object } from "../lib/object.ts";
-import { commands } from "../lib/commands.ts";
-import { program } from "../lib/program.ts";
-
-let app = program({
- name: "myapp",
- version: "3.2.0",
- config: commands({
- dev: {
- description: "start dev server",
- ...object({
- port: {
- description: "port to listen on",
- aliases: ["-p"],
- ...field(type("number"), field.default(3000)),
- },
- open: {
- description: "open browser on start",
- ...field(type("boolean"), field.default(false)),
- },
- }),
- },
- build: {
- description: "build for production",
- ...object({
- outdir: {
- description: "output directory",
- aliases: ["-o"],
- ...field(type("string"), field.default("dist")),
- },
- minify: {
- description: "minify output",
- ...field(type("boolean"), field.default(true)),
- },
- }),
- },
- deploy: {
- description: "deploy to target",
- ...object({
- target: {
- description: "deployment target",
- ...field(type("string"), cli.argument()),
- },
- dry: {
- description: "dry run without deploying",
- ...field(type("boolean"), field.default(false)),
- },
- }),
- },
- }),
-});
-
-console.log("=== -h ===\n");
-let r1 = app.parse({ args: ["-h"] });
-assert(r1.ok);
-assert(r1.value.help);
-console.log(app.help());
-
-console.log("\n=== -v ===\n");
-let r2 = app.parse({ args: ["-v"] });
-assert(r2.ok);
-assert(r2.value.version);
-console.log(r2.value.version);
-
-console.log("\n=== dev --help ===\n");
-let r3 = app.parse({ args: ["dev", "--help"] });
-assert(r3.ok);
-let cmd3 = r3.value.config;
-assert(cmd3.name === "dev");
-assert(cmd3.help);
-console.log(cmd3.text);
-
-console.log("\n=== dev --open -p 4000 ===\n");
-let r4 = app.parse({ args: ["dev", "--open", "-p", "4000"] });
-assert(r4.ok);
-let cmd4 = r4.value.config;
-assert(cmd4.name === "dev" && !cmd4.help);
-console.log(cmd4.config);
-
-console.log("\n=== help() ===\n");
-console.log(app.help());
diff --git a/examples/phased-commands.ts b/examples/phased-commands.ts
deleted file mode 100644
index dd779b7..0000000
--- a/examples/phased-commands.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import assert from "node:assert";
-import { type } from "arktype";
-import { cli, field } from "../lib/field.ts";
-import { object } from "../lib/object.ts";
-import { commands } from "../lib/commands.ts";
-import { inject } from "../lib/inject.ts";
-import { program } from "../lib/program.ts";
-import type { Parser } from "../lib/types.ts";
-
-let app = program({
- name: "myctl",
- version: "2.0.0",
- config: object({
- config: {
- description: "config file",
- aliases: ["-c"],
- ...field(type("string")),
- },
- next: inject((_config: { serve?: { host?: string } }) =>
- commands({
- init: {
- description: "initialize a new project",
- ...object({
- template: {
- description: "project template",
- aliases: ["-t"],
- ...field(type("string"), field.default("default")),
- },
- }),
- },
- serve: {
- description: "start the server",
- ...object({
- port: {
- description: "port to listen on",
- aliases: ["-p"],
- ...field(type("number"), field.default(3000)),
- },
- host: {
- description: "hostname to bind",
- aliases: ["-H"],
- ...field(type("string"), field.default("localhost")),
- },
- }),
- },
- migrate: {
- description: "run database migrations",
- ...object({
- target: {
- description: "migration target version",
- ...field(type("string"), cli.argument()),
- },
- dry: {
- description: "dry run without applying",
- ...field(type("boolean"), field.default(false)),
- },
- }),
- },
- })
- ),
- }),
-});
-
-console.log("=== --help ===\n");
-let rh = app.parse({ args: ["--help"] });
-assert(rh.ok);
-assert(rh.value.help);
-console.log(app.help());
-
-console.log("\n=== --version ===\n");
-let rv = app.parse({ args: ["--version"] });
-assert(rv.ok);
-assert(rv.value.version);
-console.log(rv.value.version);
-
-console.log("\n=== -c app.json serve -p 8080 ===\n");
-let r = app.parse({ args: ["-c", "app.json", "serve", "-p", "8080"] });
-assert(r.ok);
-
-let phase1 = r.value.config;
-console.log("phase 1 config:", phase1.config);
-
-// simulate loading config, resume into phase 2
-let resume = phase1.next as unknown as (
- deps: { serve?: { host?: string } },
-) => Parser;
-let parser2 = resume({ serve: { host: "0.0.0.0" } });
-let phase2 = parser2.parse({
- values: [{ name: "app.json", value: { serve: { host: "0.0.0.0" } } }],
-});
-assert(phase2.ok);
-console.log("phase 2:", phase2.value);
-
-console.log("\n=== help() ===\n");
-console.log(app.help());
diff --git a/examples/simple.ts b/examples/simple.ts
deleted file mode 100644
index 24a7c8e..0000000
--- a/examples/simple.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import assert from "node:assert";
-import { type } from "arktype";
-import { cli, field } from "../lib/field.ts";
-import { object } from "../lib/object.ts";
-import { program } from "../lib/program.ts";
-import { createContext } from "../lib/context.ts";
-
-let serve = program({
- name: "serve",
- version: "1.0.0",
- config: object({
- host: {
- description: "hostname to bind",
- aliases: ["-H"],
- ...field(type("string"), field.default("localhost")),
- },
- port: {
- description: "port to listen on",
- aliases: ["-p"],
- ...field(type("number"), field.default(3000)),
- },
- debug: {
- description: "enable debug logging",
- aliases: ["-d"],
- ...field(type("boolean"), field.default(false)),
- },
- entry: {
- description: "entrypoint file",
- ...field(type("string"), cli.argument()),
- },
- }),
-});
-
-console.log("=== --help ===\n");
-let r1 = serve.parse({ args: ["--help"] });
-assert(r1.ok);
-assert(r1.value.help);
-console.log(serve.help());
-
-console.log("\n=== --version ===\n");
-let r2 = serve.parse({ args: ["--version"] });
-assert(r2.ok);
-assert(r2.value.version);
-console.log(r2.value.version);
-
-console.log("\n=== app.ts -p 8080 --debug ===\n");
-let r3 = serve.parse({ args: ["app.ts", "-p", "8080", "--debug"] });
-assert(r3.ok);
-console.log(r3.value.config);
-
-console.log("\n=== inspect() ===\n");
-console.log(serve.inspect(createContext()));
-
-console.log("\n=== help() ===\n");
-console.log(serve.help());
-
-console.log("\n=== inspect() with env source ===\n");
-console.log(serve.inspect(createContext({
- envs: [{ name: "env", value: { PORT: "9090" } }],
-})));
diff --git a/images/configliere-after.svg b/images/configliere-after.svg
deleted file mode 100644
index 657d558..0000000
--- a/images/configliere-after.svg
+++ /dev/null
@@ -1,870 +0,0 @@
-
-
-
diff --git a/images/configliere-before.svg b/images/configliere-before.svg
deleted file mode 100644
index 48878e2..0000000
--- a/images/configliere-before.svg
+++ /dev/null
@@ -1,602 +0,0 @@
-
-
-
diff --git a/lib/bind.ts b/lib/bind.ts
new file mode 100644
index 0000000..b49ebdf
--- /dev/null
+++ b/lib/bind.ts
@@ -0,0 +1,99 @@
+import type { Param } from "./param.ts";
+import type { Symbol } from "./read.ts";
+import type { Result } from "./result.ts";
+import type { Flag, Setter, Word } from "./tokenize.ts";
+import type { Tokenizer } from "./tokenizer.ts";
+import type { Issue } from "./types.ts";
+
+export interface Binding {
+ rest: Tokenizer;
+ result: Result;
+}
+
+export function bind>(options: {
+ param: P;
+ tokens: Tokenizer;
+}): Binding {
+ let { param, tokens } = options;
+ let path = [param.name];
+ let cli = param.cli(tokens);
+
+ if (cli.result.ok) {
+ let read = cli.result.value;
+ let value: unknown = read.exists ? read.value : undefined;
+
+ let candidates = typeof value === "string" ? param.decode(value) : [value];
+
+ if (candidates.length === 0) {
+ return {
+ rest: cli.claim.rest,
+ result: {
+ ok: false,
+ issues: [{
+ message: `unable to decode ${JSON.stringify(value)}`,
+ path,
+ }],
+ },
+ };
+ }
+
+ let issues: readonly Issue[] | undefined = undefined;
+ for (let candidate of candidates) {
+ let result = validate(param, candidate, path);
+ if (result.ok) {
+ return {
+ rest: cli.claim.rest,
+ result,
+ };
+ } else {
+ issues = issues ?? result.issues;
+ }
+ }
+
+ return {
+ rest: cli.claim.rest,
+ result: {
+ ok: false,
+ issues: issues ?? [],
+ },
+ };
+ }
+
+ return {
+ rest: cli.claim.rest,
+ result: {
+ ok: false,
+ issues: cli.result.issues,
+ },
+ };
+}
+
+function validate>(
+ param: P,
+ value: unknown,
+ path: string[],
+): Result {
+ let validated = param.schema["~standard"].validate(value);
+ if (validated instanceof Promise) {
+ return {
+ ok: false,
+ issues: [{
+ message: `async schemas are not allowed`,
+ path,
+ }],
+ };
+ }
+
+ if (validated.issues) {
+ return {
+ ok: false,
+ issues: validated.issues.map((i) => ({ ...i, path })),
+ };
+ } else {
+ return {
+ ok: true,
+ issues: [],
+ value: validated.value,
+ };
+ }
+}
diff --git a/lib/case.ts b/lib/case.ts
deleted file mode 100644
index a45c479..0000000
--- a/lib/case.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-type Upper =
- | "A"
- | "B"
- | "C"
- | "D"
- | "E"
- | "F"
- | "G"
- | "H"
- | "I"
- | "J"
- | "K"
- | "L"
- | "M"
- | "N"
- | "O"
- | "P"
- | "Q"
- | "R"
- | "S"
- | "T"
- | "U"
- | "V"
- | "W"
- | "X"
- | "Y"
- | "Z";
-
-type Lower = Lowercase;
-
-type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
-
-type Separator = "_" | "-" | "." | " ";
-
-type Kind = C extends Separator ? "sep"
- : C extends Upper ? "upper"
- : C extends Lower ? "lower"
- : C extends Digit ? "digit"
- : "other";
-
-type Push = S extends "" ? T : [...T, S];
-
-type HasMany = S extends `${infer _}${infer Rest}`
- ? Rest extends "" ? false : true
- : false;
-
-type NextKind = S extends `${infer Head}${string}`
- ? Kind
- : "end";
-
-type ShouldBreak<
- Current extends string,
- Prev extends "start" | "sep" | "upper" | "lower" | "digit" | "other",
- CurrentKind extends "upper" | "lower" | "digit" | "other",
- Next extends "upper" | "lower" | "digit" | "sep" | "other" | "end",
-> = Current extends "" ? false
- : CurrentKind extends "upper" ? Prev extends "lower" | "digit" ? true
- : Prev extends "upper" ? Next extends "lower" ? HasMany
- : false
- : false
- : CurrentKind extends "lower" ? Prev extends "digit" ? true
- : Prev extends "upper" ? HasMany
- : false
- : CurrentKind extends "digit" ? Prev extends "lower" ? true : false
- : false;
-
-type SnakeWords<
- S extends string,
- Prev extends "start" | "sep" | "upper" | "lower" | "digit" | "other" =
- "start",
- Current extends string = "",
- Words extends string[] = [],
-> = S extends `${infer Head}${infer Tail}`
- ? Kind extends "sep" ? SnakeWords>
- : Kind extends infer CurrentKind
- ? CurrentKind extends "upper" | "lower" | "digit" | "other"
- ? ShouldBreak> extends true
- ? SnakeWords<
- Tail,
- CurrentKind,
- `${Lowercase}`,
- Push
- >
- : SnakeWords<
- Tail,
- CurrentKind,
- `${Current}${Lowercase}`,
- Words
- >
- : never
- : never
- : Push;
-
-type JoinWithUnderscores = T extends
- [infer Head, ...infer Tail]
- ? Head extends string
- ? Tail extends string[]
- ? Tail["length"] extends 0 ? Head : `${Head}_${JoinWithUnderscores}`
- : never
- : never
- : "";
-
-type SnakeCase = JoinWithUnderscores>;
-
-type ReplaceUnderscores = S extends
- `${infer Head}_${infer Tail}` ? `${Head}-${ReplaceUnderscores}`
- : S;
-
-export type EnvCase = Uppercase>;
-
-export type KebabCase = ReplaceUnderscores>;
-
-export function toKebabCase(str: S): KebabCase {
- return normalize(str).replaceAll("_", "-") as KebabCase;
-}
-
-export function toEnvCase(str: S): EnvCase {
- return normalize(str).toUpperCase() as EnvCase;
-}
-
-export function toOptionPath(path: string[]): string {
- return path.map((part) => toKebabCase(part)).join(".").toLowerCase();
-}
-
-export function toEnvKey(path: string[]): string {
- return path.map((part) => toEnvCase(part)).join("_");
-}
-
-function normalize(str: string): string {
- let words: string[] = [];
- let current = "";
- let prev: "start" | "sep" | "upper" | "lower" | "digit" | "other" = "start";
-
- for (let index = 0; index < str.length; index += 1) {
- let char = str[index];
- let kind = classify(char);
-
- if (kind === "sep") {
- if (current !== "") {
- words.push(current);
- current = "";
- }
- prev = "sep";
- continue;
- }
-
- if (shouldBreak(current, prev, kind, classify(str[index + 1] ?? ""))) {
- words.push(current);
- current = char.toLowerCase();
- } else {
- current += char.toLowerCase();
- }
-
- prev = kind;
- }
-
- if (current !== "") {
- words.push(current);
- }
-
- return words.join("_");
-}
-
-function classify(char: string): "sep" | "upper" | "lower" | "digit" | "other" {
- if (char === "_" || char === "-" || char === "." || char === " ") {
- return "sep";
- }
- if (char >= "A" && char <= "Z") {
- return "upper";
- }
- if (char >= "a" && char <= "z") {
- return "lower";
- }
- if (char >= "0" && char <= "9") {
- return "digit";
- }
- return "other";
-}
-
-function shouldBreak(
- current: string,
- prev: "start" | "sep" | "upper" | "lower" | "digit" | "other",
- kind: "upper" | "lower" | "digit" | "other",
- next: "sep" | "upper" | "lower" | "digit" | "other",
-): boolean {
- if (current === "") {
- return false;
- }
-
- switch (kind) {
- case "upper":
- if (prev === "lower" || prev === "digit") {
- return true;
- }
- return prev === "upper" && next === "lower" && current.length > 1;
- case "lower":
- if (prev === "digit") {
- return true;
- }
- return prev === "upper" && current.length > 1;
- case "digit":
- return prev === "lower";
- default:
- return false;
- }
-}
diff --git a/lib/command.ts b/lib/command.ts
new file mode 100644
index 0000000..cdcf915
--- /dev/null
+++ b/lib/command.ts
@@ -0,0 +1,1101 @@
+// deno-lint-ignore-file ban-types
+import { executable, route } from "./route.ts";
+import type { Definition, Route } from "./types.ts";
+
+export type CommandZero = Route<
+ N,
+ "help" | "execute",
+ {},
+ []
+>;
+
+export function command(
+ start: Definition,
+): CommandZero;
+
+export function command(
+ start: Definition,
+ na: (value: CommandZero) => A,
+): A;
+
+export function command(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+): B;
+
+export function command(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+): C;
+
+export function command(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+): D;
+
+export function command(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+): E;
+
+export function command(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+): F;
+
+export function command<
+ const N extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+): G;
+
+export function command<
+ const N extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+): H;
+
+export function command<
+ const N extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+): I;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+): J;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+): K;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+): L;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+): M;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+): N;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+): O;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+): P;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+): Q;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+): R;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+): S;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+): T;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+): U;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+): V;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+): W;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+ wx: (value: W) => X,
+): X;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+ wx: (value: W) => X,
+ xy: (value: X) => Y,
+): Y;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+ wx: (value: W) => X,
+ xy: (value: X) => Y,
+ yz: (value: Y) => Z,
+): Z;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+ AA,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+ wx: (value: W) => X,
+ xy: (value: X) => Y,
+ yz: (value: Y) => Z,
+ zaa: (value: Z) => AA,
+): AA;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+ AA,
+ AB,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+ wx: (value: W) => X,
+ xy: (value: X) => Y,
+ yz: (value: Y) => Z,
+ zaa: (value: Z) => AA,
+ aaab: (value: AA) => AB,
+): AB;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+ AA,
+ AB,
+ AC,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+ wx: (value: W) => X,
+ xy: (value: X) => Y,
+ yz: (value: Y) => Z,
+ zaa: (value: Z) => AA,
+ aaab: (value: AA) => AB,
+ abac: (value: AB) => AC,
+): AC;
+
+export function command<
+ const Name extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+ AA,
+ AB,
+ AC,
+ AD,
+>(
+ start: Definition,
+ na: (value: CommandZero) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gh: (value: G) => H,
+ hi: (value: H) => I,
+ ij: (value: I) => J,
+ jk: (value: J) => K,
+ kl: (value: K) => L,
+ lm: (value: L) => M,
+ mn: (value: M) => N,
+ no: (value: N) => O,
+ op: (value: O) => P,
+ pq: (value: P) => Q,
+ qr: (value: Q) => R,
+ rs: (value: R) => S,
+ st: (value: S) => T,
+ tu: (value: T) => U,
+ uv: (value: U) => V,
+ vw: (value: V) => W,
+ wx: (value: W) => X,
+ xy: (value: X) => Y,
+ yz: (value: Y) => Z,
+ zaa: (value: Z) => AA,
+ aaab: (value: AA) => AB,
+ abac: (value: AB) => AC,
+ acad: (value: AC) => AD,
+): AD;
+
+export function command(
+ start: Definition,
+ ...elements: readonly ((value: never) => unknown)[]
+): unknown {
+ return elements.reduce(
+ (value, element) => element(value as never),
+ route(start, executable()),
+ );
+}
diff --git a/lib/commands.ts b/lib/commands.ts
deleted file mode 100644
index 1880d95..0000000
--- a/lib/commands.ts
+++ /dev/null
@@ -1,248 +0,0 @@
-import type {
- Command,
- CommandInfo,
- CommandsInfo,
- HelpInfo,
- Input,
- ParseContext,
- Parser,
-} from "./types.ts";
-import { constant } from "./constant.ts";
-import { toEnvCase } from "./case.ts";
-import { format } from "./help.ts";
-import { helpOpt } from "./program.ts";
-import { createContext } from "./context.ts";
-
-export type CommandEntry = Partial>;
-
-export type CommandEntries> = {
- [K in keyof T & string]: Partial>;
-};
-
-export interface CommandParser>
- extends Parser> {
-}
-
-export interface CommandsParser>
- extends Parser> {
- default?: string;
-}
-
-export function commands>(
- map: CommandEntries,
- opts?: { default?: string },
-): CommandsParser<
- { [K in keyof T & string]: Command }[keyof T & string]
-> {
- let cmds: Record>> = {};
-
- let parser = {
- default: opts?.default,
- parse(input: Input, ctx?: ParseContext) {
- return parser.inspect(ctx ?? createContext(input)).result;
- },
- inspect(ctx: ParseContext): CommandsInfo> {
- let withCmds = { ...ctx, commands: cmds };
- let args = ctx.args;
- let matched = match(args, cmds, opts);
- let infos: Record>> = {};
- for (let [name, cmd] of Object.entries(cmds)) {
- let info = cmd.inspect(scope(name, withCmds));
- infos[name] = info;
- }
-
- let help = {
- progname: ctx.progname,
- args: [] as HelpInfo["args"],
- opts: [] as HelpInfo["opts"],
- commands: Object.values(infos).map((info) => ({
- name: info.name,
- description: info.description,
- aliases: info.aliases,
- })) as HelpInfo["commands"],
- };
-
- let remainder = { args: ctx.args, values: ctx.values, envs: ctx.envs };
-
- if (!matched) {
- return {
- type: "commands",
- parser,
- result: {
- ok: false,
- error: new NoCommandMatchError(Object.keys(cmds)),
- remainder,
- },
- remainder,
- commands: infos,
- help,
- } as unknown as CommandsInfo>;
- }
-
- let [name, cmd, rest] = matched;
- let inner = cmd.inspect(scope(name, { ...withCmds, args: rest }));
-
- return {
- type: "commands",
- parser,
- result: inner.result,
- remainder,
- commands: infos,
- help,
- } as unknown as CommandsInfo>;
- },
- help(input: Input = {}, ctx?: ParseContext): string {
- let c = ctx ?? createContext(input);
- let matched = match(c.args, cmds);
- if (matched) {
- let [name, cmd, rest] = matched;
- return cmd.help(
- { ...input, args: rest },
- scope(name, { ...c, commands: cmds, args: rest }),
- );
- }
- return format(parser.inspect(c));
- },
- } as CommandsParser>;
-
- for (
- let [name, entry] of Object.entries(map) as [string, CommandEntry][]
- ) {
- let p = typeof entry.parse === "function"
- ? entry as Parser
- : Object.assign(constant(true), entry);
- cmds[name] = command(name, p) as CommandParser<
- Command
- >;
- }
-
- return parser as unknown as CommandsParser<
- { [K in keyof T & string]: Command }[keyof T & string]
- >;
-}
-
-export class NoCommandMatchError extends Error {
- constructor(public available: string[]) {
- super(`No command matched. Available: ${available.join(", ")}`);
- this.name = "NoCommandMatchError";
- }
-}
-
-// --- internal ---
-
-function match(
- args: string[],
- cmds: Record>>,
- opts?: { default?: string },
-): [string, CommandParser>, string[]] | undefined {
- if (args.length > 0) {
- if (args[0] in cmds) return [args[0], cmds[args[0]], args.slice(1)];
- for (let [name, cmd] of Object.entries(cmds)) {
- if (cmd.aliases?.includes(args[0])) return [name, cmd, args.slice(1)];
- }
- }
- if (opts?.default && cmds[opts.default]) {
- return [opts.default, cmds[opts.default], args];
- }
- return undefined;
-}
-
-function command(
- name: Name,
- inner: Parser,
-): CommandParser> {
- let parser: CommandParser> = {
- description: inner.description,
- aliases: inner.aliases,
- parse(input: Input, ctx?: ParseContext) {
- return parser.inspect(ctx ?? createContext(input)).result;
- },
- inspect(ctx: ParseContext): CommandInfo> {
- let cmdCtx = {
- ...ctx,
- progname: [...ctx.progname, name],
- commands: ctx.commands,
- };
- let remainder = { args: ctx.args, values: ctx.values, envs: ctx.envs };
-
- if (ctx.args[0] === "--help" || ctx.args[0] === "-h") {
- let config = inner.inspect({ ...cmdCtx, args: [] });
- let help = {
- ...config.help,
- progname: [...ctx.progname, name],
- opts: [...config.help.opts, helpOpt],
- };
- let text = format({ ...config, help });
- return {
- type: "command",
- parser,
- result: {
- ok: true as const,
- value: { name, help: true as const, text } as Command,
- remainder,
- },
- name,
- description: inner.description,
- aliases: inner.aliases,
- config,
- commands: {},
- help,
- remainder,
- };
- }
-
- let config = inner.inspect(cmdCtx);
- let result = config.result.ok
- ? {
- ok: true as const,
- value: { name, config: config.result.value } as Command,
- remainder: { ...remainder, args: config.result.remainder.args },
- }
- : config.result;
-
- return {
- type: "command",
- parser,
- result,
- name,
- description: inner.description,
- aliases: inner.aliases,
- config,
- commands: {},
- help: {
- progname: [...ctx.progname, name],
- args: config.help.args,
- opts: [...config.help.opts, helpOpt],
- commands: config.help.commands,
- },
- remainder,
- };
- },
- help(input: Input = {}, ctx?: ParseContext): string {
- return format(parser.inspect(ctx ?? createContext(input)));
- },
- };
- return parser;
-}
-
-function scope(name: string, ctx: ParseContext): ParseContext {
- let prefix = toEnvCase(name) + "_";
- let values = ctx.values.flatMap((v) => {
- if (v.value == null) return [];
- let obj = v.value as Record;
- if (!(name in obj)) return [];
- return [{ name: v.name, value: obj[name] }];
- });
- let envs = ctx.envs.map((env) => {
- let scoped: Record = {};
- for (let [key, val] of Object.entries(env.value)) {
- if (key.startsWith(prefix)) {
- scoped[key.slice(prefix.length)] = val;
- } else {
- scoped[key] = val;
- }
- }
- return { name: env.name, value: scoped };
- });
- return { ...ctx, values, envs };
-}
diff --git a/lib/constant.ts b/lib/constant.ts
deleted file mode 100644
index 910cb65..0000000
--- a/lib/constant.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { ConstantInfo, ParseContext, Parser } from "./types.ts";
-import { createContext } from "./context.ts";
-
-export function constant(value: T): Parser> {
- let parser: Parser> = {
- parse(input, ctx) {
- return parser.inspect(ctx ?? createContext(input)).result;
- },
- inspect(ctx: ParseContext): ConstantInfo {
- let remainder = { args: ctx.args, values: ctx.values, envs: ctx.envs };
- return {
- type: "constant",
- parser,
- value,
- result: { ok: true, value, remainder },
- remainder,
- help: { progname: [], args: [], opts: [], commands: [] },
- };
- },
- help() {
- return "";
- },
- };
- return parser;
-}
diff --git a/lib/context.ts b/lib/context.ts
deleted file mode 100644
index e98f9fa..0000000
--- a/lib/context.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { Input, ParseContext } from "./types.ts";
-
-export function createContext(input: Input = {}): ParseContext {
- return {
- progname: [],
- path: [],
- commands: {},
- args: input.args ?? [],
- values: input.values ?? [],
- envs: input.envs ?? [],
- };
-}
diff --git a/lib/dasherize.ts b/lib/dasherize.ts
new file mode 100644
index 0000000..a35f0c5
--- /dev/null
+++ b/lib/dasherize.ts
@@ -0,0 +1,7 @@
+export function dasherize(name: string): string {
+ return name
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
+ .replace(/([a-z\d])([A-Z])/g, "$1-$2")
+ .replace(/[\s_]+/g, "-")
+ .toLowerCase();
+}
diff --git a/lib/decode.ts b/lib/decode.ts
new file mode 100644
index 0000000..c88f36b
--- /dev/null
+++ b/lib/decode.ts
@@ -0,0 +1,16 @@
+export type Decoder = (value: string) => unknown[];
+
+export const number: Decoder = (value) => {
+ if (!numeric.test(value)) {
+ return [];
+ }
+
+ let decoded = Number(value);
+ return Number.isFinite(decoded) ? [decoded] : [];
+};
+
+export const scalar: Decoder = (value) => {
+ return [...number(value), value];
+};
+
+const numeric = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
diff --git a/lib/definition.ts b/lib/definition.ts
new file mode 100644
index 0000000..b781db3
--- /dev/null
+++ b/lib/definition.ts
@@ -0,0 +1,14 @@
+import type { Definition } from "./types.ts";
+
+export function name(name: N): Definition {
+ return { name };
+}
+
+export function description(
+ description: string,
+): >(definition: D) => D {
+ return (definition) => ({
+ ...definition,
+ description,
+ });
+}
diff --git a/lib/field.ts b/lib/field.ts
deleted file mode 100644
index e5ef8ff..0000000
--- a/lib/field.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import type { StandardSchemaV1 } from "@standard-schema/spec";
-
-import { validate, ValidationError } from "./validate.ts";
-import type { Field, FieldInfo, ParseContext, Source } from "./types.ts";
-import { toEnvKey } from "./case.ts";
-import { isBoolean } from "./parse-args.ts";
-import { format } from "./help.ts";
-import { createContext } from "./context.ts";
-
-export function field(
- schema: StandardSchemaV1,
- ...mod: Mod[]
-): Field {
- let mods = mod.reduce((all, current) => {
- return current(all);
- }, {
- argument: false,
- array: false,
- } as Mods);
-
- let f: Field = {
- required: !!validate(schema, undefined).issues,
- schema,
- argument: mods.argument,
- array: mods.array,
- default: mods.default,
- boolean: isBoolean(schema),
- parse(input, ctx) {
- return f.inspect(ctx ?? createContext(input)).result;
- },
- inspect(ctx: ParseContext): FieldInfo {
- let sources: Source[] = [];
-
- // none is always a source (lowest priority)
- let none = validate(schema, undefined);
- sources.push({
- sourceName: "none",
- sourceType: "none",
- value: (none.issues ? undefined : none.value) as T,
- issues: none.issues,
- });
-
- // default comes after none but before actual input sources
- if (f.default !== undefined) {
- let { issues } = validate(schema, f.default);
- sources.push({
- sourceName: "default",
- sourceType: "default",
- value: f.default as T,
- issues,
- });
- }
-
- // collect value sources
- for (let value of ctx.values) {
- let result = validate(schema, value.value);
- sources.push({
- sourceName: value.name,
- sourceType: "value",
- value: (result.issues ? value.value : result.value) as T,
- issues: result.issues,
- });
- }
-
- // collect env sources
- let envKey = toEnvKey(ctx.path);
- for (let env of ctx.envs) {
- let strval = env.value[envKey];
- if (strval === undefined) continue;
- let value = parseEnvValue(f, strval);
- let result = validate(schema, value);
- sources.push({
- sourceName: env.name,
- sourceType: "env",
- value: (result.issues ? value : result.value) as T,
- issues: result.issues,
- });
- }
-
- // pick the best valid source (last one wins), fall back to last source
- let winner = sources.findLast((s) => !s.issues) ??
- sources[sources.length - 1];
-
- let remainder = { args: ctx.args, values: ctx.values, envs: ctx.envs };
-
- let result = winner.issues
- ? {
- ok: false as const,
- error: new ValidationError(sources),
- remainder,
- }
- : { ok: true as const, value: winner.value as T, remainder };
-
- // read description/aliases from the actual object (may be a spread copy)
- let desc = (this as unknown as Partial>)?.description ??
- f.description;
- let ali = (this as unknown as Partial>)?.aliases ?? f.aliases;
-
- let info: FieldInfo = {
- type: "field",
- parser: f,
- result,
- remainder,
- path: ctx.path,
- required: f.required,
- argument: f.argument,
- array: f.array,
- aliases: ali,
- description: desc,
- default: f.default,
- boolean: f.boolean,
- source: winner,
- sources,
- help: { progname: [], args: [], opts: [], commands: [] },
- };
- if (f.argument) {
- info.help.args.push(info);
- } else {
- info.help.opts.push(info);
- }
- return info;
- },
- help(input, ctx) {
- let info = f.inspect(ctx ?? createContext(input));
- return format(info, info.path.join("."));
- },
- };
- return f;
-}
-
-// --- internal ---
-
-function parseEnvValue(field: Field, value: string): unknown {
- if (isBoolean(field.schema)) {
- return parseEnvBoolean(value);
- } else if (field.array) {
- return parseEnvArray(field, value);
- } else if (!isNaN(Number(value))) {
- return Number(value);
- } else {
- return value;
- }
-}
-
-function parseEnvBoolean(value: string) {
- switch (value.toLowerCase().trim()) {
- case "true":
- case "yes":
- case "1":
- return true;
- case "false":
- case "no":
- case "0":
- return false;
- default:
- return value;
- }
-}
-
-function parseEnvArray(field: Field, value: string) {
- return value.split(",").map((s) => s.trim()).map((s) =>
- parseEnvValue(field, s)
- );
-}
-
-field.array = (): Mod => {
- return (field) => ({
- ...field,
- array: true,
- });
-};
-
-field.default = (value: T): Mod => {
- return (mods) => ({
- ...mods,
- default: value,
- });
-};
-
-export const cli: CLIMods = {
- argument(): Mod {
- return (mods) => ({
- ...mods,
- argument: true,
- });
- },
-};
-
-export interface CLIMods {
- argument: () => Mod;
-}
-
-export interface Mod {
- (mods: Mods): Mods;
-}
-
-interface Mods {
- default?: unknown;
- argument: boolean;
- array: boolean;
-}
diff --git a/lib/help.ts b/lib/help.ts
deleted file mode 100644
index e5f792d..0000000
--- a/lib/help.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import type { FieldInfo, ParserInfo } from "./types.ts";
-import { optionKey } from "./parse-args.ts";
-import { toEnvKey } from "./case.ts";
-
-export function format(info: ParserInfo, name?: string): string {
- let { progname, args, opts, commands } = info.help;
- let sections: string[] = [];
-
- // usage line
- let label = progname.length ? progname.join(" ") : name ?? "";
- let usage = [`Usage: ${label}`];
- if (commands.length > 0) usage.push("");
- if (opts.length > 0) usage.push("[OPTIONS]");
- for (let arg of args) {
- usage.push(printArg(arg));
- }
- sections.push(usage.join(" "));
-
- // commands section
- if (commands.length > 0) {
- let lines = commands.map((cmd) => {
- let label = cmd.name;
- if (cmd.aliases && cmd.aliases.length > 0) {
- label += ` (${cmd.aliases.join(", ")})`;
- }
- let desc = cmd.description ?? "";
- return ` ${label.padEnd(25)} ${desc}`;
- });
- sections.push(["Commands:", ...lines].join("\n"));
- }
-
- // arguments section
- if (args.length > 0) {
- let lines = args.map((arg) => {
- let name = printArg(arg);
- let desc = arg.description ?? "";
- let source = formatSource(arg);
- let right = [desc, source].filter(Boolean).join(" ");
- return ` ${name.padEnd(25)} ${right}`;
- });
- sections.push(["Arguments:", ...lines].join("\n"));
- }
-
- // options section
- if (opts.length > 0) {
- let lines = opts.map((opt) => {
- let aliases = opt.aliases?.length ? opt.aliases.join(", ") + ", " : "";
- let key = optionKey(opt.path);
- let value = opt.boolean ? "" : " " + printArg(opt).toUpperCase();
- let optStr = `${aliases}${key}${value}`;
- let desc = opt.description ?? "";
- let source = formatSource(opt);
- let right = [desc, source].filter(Boolean).join(" ");
- return ` ${optStr.padEnd(25)} ${right}`;
- });
- sections.push(["Options:", ...lines].join("\n"));
- }
-
- return sections.join("\n\n");
-}
-
-// --- internal ---
-
-function printArg(info: FieldInfo): string {
- let key = info.path.join(".");
- if (info.array) {
- return `<${key}>...`;
- }
- return info.required ? `<${key}>` : `[${key}]`;
-}
-
-function formatSource(info: FieldInfo): string {
- if (!info.source || info.source.issues) return "";
- let { sourceType, sourceName, value } = info.source;
- switch (sourceType) {
- case "env": {
- let key = toEnvKey(info.path);
- return `[env: ${key}=${value}]`;
- }
- case "value":
- return `[${sourceName}: ${info.path.join(".")}=${value}]`;
- case "default":
- return `[default: ${info.default}]`;
- case "none":
- if (info.default !== undefined) {
- return `[default: ${info.default}]`;
- }
- return "";
- default:
- return `[${sourceType}: ${value}]`;
- }
-}
diff --git a/lib/inject.ts b/lib/inject.ts
deleted file mode 100644
index 2184016..0000000
--- a/lib/inject.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { ParseContext, Parser, ParserInfo } from "./types.ts";
-import { createContext } from "./context.ts";
-import { format } from "./help.ts";
-
-export function inject(
- fn: (dep: D) => Parser,
-): Parser<(dep: D) => Parser> {
- let parser: Parser<(dep: D) => Parser> = {
- parse(input, ctx) {
- return parser.inspect(ctx ?? createContext(input)).result;
- },
- inspect(ctx: ParseContext): ParserInfo<(dep: D) => Parser> {
- let remainder = { args: ctx.args, values: ctx.values, envs: ctx.envs };
- let resolve = (dep: D): Parser => {
- let inner = fn(dep);
- return {
- ...inner,
- parse(input, override) {
- return inner.inspect(
- override ?? {
- ...ctx,
- args: input?.args ?? ctx.args,
- values: input?.values ?? ctx.values,
- envs: input?.envs ?? ctx.envs,
- },
- ).result;
- },
- inspect(override) {
- return inner.inspect(override);
- },
- help(input, override) {
- return inner.help({
- args: input?.args ?? ctx.args,
- values: input?.values ?? ctx.values,
- envs: input?.envs ?? ctx.envs,
- }, override ?? ctx);
- },
- };
- };
- return {
- type: "inject",
- parser,
- result: { ok: true, value: resolve, remainder },
- remainder,
- help: { progname: ctx.progname, args: [], opts: [], commands: [] },
- };
- },
- help(input, ctx) {
- return format(parser.inspect(ctx ?? createContext(input)));
- },
- };
- return parser;
-}
diff --git a/lib/lens.ts b/lib/lens.ts
deleted file mode 100644
index 192b735..0000000
--- a/lib/lens.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export const lens = {
- set(path: string[], value: unknown, target: T): T {
- if (path.length === 0) {
- return value as T;
- }
- let [key, ...rest] = path;
-
- let current = (target ?? {} as T)[key as keyof T] ?? {};
-
- return {
- ...target,
- [key]: lens.set(rest, value, current),
- };
- },
-};
diff --git a/lib/maybe.ts b/lib/maybe.ts
new file mode 100644
index 0000000..f384e57
--- /dev/null
+++ b/lib/maybe.ts
@@ -0,0 +1,22 @@
+export type Maybe = {
+ readonly exists: false;
+} | {
+ readonly exists: true;
+ readonly value: T;
+};
+
+export function Just(): Maybe;
+export function Just(value: T): Maybe;
+export function Just(value?: T): Maybe {
+ if (typeof value === "undefined") {
+ return { exists: true } as Maybe;
+ } else {
+ return { exists: true, value };
+ }
+}
+
+export function Nothing(): Maybe {
+ return nothing as Maybe;
+}
+
+const nothing = { exists: false };
diff --git a/lib/mod.ts b/lib/mod.ts
deleted file mode 100644
index 2120e44..0000000
--- a/lib/mod.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export * from "./types.ts";
-export * from "./constant.ts";
-export * from "./field.ts";
-export * from "./object.ts";
-export * from "./commands.ts";
-export * from "./help.ts";
-export * from "./program.ts";
-export * from "./inject.ts";
-export * from "./context.ts";
diff --git a/lib/object.ts b/lib/object.ts
deleted file mode 100644
index 21bba78..0000000
--- a/lib/object.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-import type {
- Field,
- ObjectInfo,
- ParseContext,
- Parser,
- ParserInfo,
-} from "./types.ts";
-import {
- matchAll,
- parseArgument,
- parseLongOption,
- parseLongOptionEql,
- parseNegativeSwitch,
- parseSwitch,
- primitive,
-} from "./parse-args.ts";
-import { field } from "./field.ts";
-import { format } from "./help.ts";
-import { optionalBoolean } from "./schema.ts";
-import { createContext } from "./context.ts";
-
-export type Attrs = {
- [K in keyof T]: Partial>;
-};
-
-export function object(
- attrs: Attrs,
-): Parser> {
- let entries = Object.entries(attrs).map(([key, value]) => {
- let entry = value as Partial>;
- let parser = typeof entry.parse === "function"
- ? entry as Parser
- : Object.assign(field(optionalBoolean), entry);
- return [key as keyof T, parser] as [keyof T, Parser];
- });
-
- let parser: Parser> = {
- parse(input, ctx) {
- return parser.inspect(ctx ?? createContext(input)).result;
- },
- inspect(ctx: ParseContext): ObjectInfo {
- let { scoped, args } = scopeInput(entries, ctx);
- let attrs: Record> = {};
- let value = {} as Record;
- let errors: { path: string[]; error: Error }[] = [];
-
- for (let [key, child] of entries) {
- let path = [...ctx.path, String(key)];
- let childCtx = { ...ctx, args, ...(scoped.get(key) ?? {}), path };
- let info = child.inspect(childCtx);
- attrs[String(key)] = info;
- if (info.result.ok) {
- value[key] = info.result.value;
- } else {
- errors.push({ path, error: info.result.error });
- }
- }
-
- let remainder = { args, values: ctx.values, envs: ctx.envs };
- let result = errors.length > 0
- ? {
- ok: false as const,
- error: new ObjectValidationError(errors),
- remainder,
- }
- : { ok: true as const, value: value as T, remainder };
-
- let help = {
- progname: [] as string[],
- args: [],
- opts: [],
- commands: [],
- } as ObjectInfo["help"];
- for (let child of Object.values(attrs) as ParserInfo[]) {
- help.args.push(...child.help.args);
- help.opts.push(...child.help.opts);
- help.commands.push(...child.help.commands);
- }
-
- return {
- type: "object",
- parser,
- result,
- remainder,
- attrs,
- help,
- } as ObjectInfo;
- },
- help(input, ctx) {
- return format(parser.inspect(ctx ?? createContext(input)));
- },
- };
-
- return parser;
-}
-
-// --- internal ---
-
-function asField(parser: Parser): Field | undefined {
- return "schema" in parser ? parser as Field : undefined;
-}
-
-function scopeInput(
- entries: [keyof V, Parser][],
- ctx: ParseContext,
-): {
- scoped: Map<
- keyof V,
- {
- values: { name: string; value: unknown }[];
- envs: { name: string; value: Record }[];
- }
- >;
- args: string[];
-} {
- let cliValues = new Map();
- let args = ctx.args;
-
- if (args.length > 0) {
- let matched = new Set();
- let prev: string[] | undefined;
- while (
- args.length > 0 && (prev === undefined || args.length < prev.length)
- ) {
- prev = args;
- for (let [key, parser] of entries) {
- let f = asField(parser);
- if (!f) continue;
- if (matched.has(key) && !f.array) continue;
- let path = [...ctx.path, String(key)];
- let matcher = matchAll(
- parseSwitch(f.schema, f.aliases ?? []),
- parseNegativeSwitch(f.schema),
- parseLongOption(f.aliases ?? []),
- parseLongOptionEql(),
- parseArgument(f.argument),
- );
- let match = matcher(args, path);
- if (match.matched) {
- let val = primitive(match.value);
- if (f.array) {
- let existing = cliValues.get(key);
- if (existing && existing.length > 0) {
- let last = existing[existing.length - 1];
- (last.value as unknown[]).push(val);
- } else {
- cliValues.set(key, [{ name: "cli", value: [val] }]);
- }
- } else {
- let existing = cliValues.get(key) ?? [];
- existing.push({ name: "cli", value: val });
- cliValues.set(key, existing);
- }
- matched.add(key);
- args = match.remainder;
- break;
- }
- }
- }
- }
-
- let scoped = new Map<
- keyof V,
- {
- values: { name: string; value: unknown }[];
- envs: { name: string; value: Record }[];
- }
- >();
- for (let [key] of entries) {
- scoped.set(key, {
- values: [
- ...ctx.values.flatMap((v) => {
- if (v.value == null) return [];
- let obj = v.value as Record;
- if (!(String(key) in obj)) return [];
- return [{ name: v.name, value: obj[String(key)] }];
- }),
- ...(cliValues.get(key) ?? []),
- ],
- envs: ctx.envs,
- });
- }
-
- return { scoped, args };
-}
-
-export class ObjectValidationError extends Error {
- constructor(public fields: { path: string[]; error: Error }[]) {
- let message = fields.map(({ path, error }) => {
- return `${path.join(".")}: ${error.message}`;
- }).join("\n");
- super(message);
- this.name = "ObjectValidationError";
- }
-}
diff --git a/lib/option.ts b/lib/option.ts
new file mode 100644
index 0000000..50e15b8
--- /dev/null
+++ b/lib/option.ts
@@ -0,0 +1,146 @@
+import { type Param, param } from "./param.ts";
+import { dasherize } from "./dasherize.ts";
+import { cli } from "./read.ts";
+import type { AnyRoute, Definition, Method, Route } from "./types.ts";
+
+export function option(
+ named: Definition,
+): Option;
+
+export function option(
+ named: Definition,
+ nt: (value: Param) => Param,
+): Option;
+
+export function option(
+ named: Definition,
+ na: (value: Param) => A,
+ at: (value: A) => Param,
+): Option;
+
+export function option(
+ named: Definition,
+ na: (value: Param) => A,
+ ab: (value: A) => B,
+ bt: (value: B) => Param,
+): Option;
+
+export function option(
+ named: Definition,
+ na: (value: Param) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ ct: (value: C) => Param,
+): Option;
+
+export function option(
+ named: Definition,
+ na: (value: Param) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ dt: (value: D) => Param,
+): Option;
+
+export function option(
+ named: Definition,
+ na: (value: Param) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ et: (value: E) => Param,
+): Option;
+
+export function option(
+ named: Definition,
+ na: (value: Param) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ ft: (value: F) => Param,
+): Option;
+
+export function option<
+ const N extends string,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ T,
+>(
+ named: Definition,
+ na: (value: Param) => A,
+ ab: (value: A) => B,
+ bc: (value: B) => C,
+ cd: (value: C) => D,
+ de: (value: D) => E,
+ ef: (value: E) => F,
+ fg: (value: F) => G,
+ gt: (value: G) => Param,
+): Option