This file provides guidance to LLM-based code agents (e.g., Claude Code, OpenCode) when working with code in this repository.
Optique is a type-safe combinatorial CLI parser for TypeScript, inspired by Haskell's optparse-applicative and TypeScript's Zod. It provides a functional approach to building command-line interfaces using composable parsers with full type safety.
This project is hosted on GitHub at dahlia/optique.
This is a polyglot monorepo supporting Deno, Node.js, and Bun. Use mise to manage runtime versions and run development tasks.
This project uses Deno as the primary development tool and pnpm for npm-related tasks (building for npm publishing).
Important
Do not use npm or Yarn as package managers in this project. Always use
mise tasks (mise run ... or mise <task>) for development workflows.
mise install # Install runtime tools (Deno, Node.js, Bun, pnpm)
mise deps # Install project dependenciesmise check # Type check, lint, format check, and dry-run publish
deno fmt # Format code
deno lint # Run lintermise test:deno # Run tests with Deno (primary test environment)
mise test:node # Run tests with Node.js
mise test:bun # Run tests with Bun
mise test # Run all checks and tests across all runtimesmise build # Build all packages with tsdownAll packages must share the same version. Use the check-versions task:
mise check-versions # Check for version mismatches
mise check-versions --fix # Auto-fix version mismatchesWhen adding new dependencies, always check for the latest version:
- npm packages: Use
npm view <package> versionto find the latest version - JSR packages: Use the JSR API to find the latest version
Always prefer the latest stable version unless there is a specific reason to use an older version.
Important
Because this project supports both Deno and Node.js/Bun, dependencies must be added to both configuration files:
- deno.json: Add to the
importsfield (for Deno) - package.json: Add to
dependenciesordevDependencies(for Node.js/Bun)
For workspace packages, use the pnpm catalog (pnpm-workspace.yaml) to manage
versions centrally. In package.json, reference catalog versions with
"catalog:" instead of hardcoding version numbers.
Forgetting to add a dependency to package.json will cause Node.js and Bun
tests to fail with ERR_MODULE_NOT_FOUND, even if Deno tests pass.
When creating temporary test scripts, save them in the tmp/ directory at the project root (not the system /tmp directory). This directory is already in .gitignore.
Using the project-local tmp/ directory allows you to import @optique/*
packages with relative imports, whereas using the system /tmp would require
absolute paths since it is outside the workspace.
- @optique/core (packages/core/): Core parsing library. Contains parser
combinators (
parser.ts), value parsers (valueparser.ts), help text generation (usage.ts), and error handling (message.ts). This package is pure TypeScript and works in any JavaScript environment. - @optique/run (packages/run/): CLI integration wrapper. Provides
process-integrated
run()function, argument reading fromprocess.argvorDeno.args, andprocess.exit()handling. - @optique/config (packages/config/): Configuration file integration.
Provides
createConfigContext()andbindConfig()for config fallbacks. - @optique/env (packages/env/): Environment variable integration.
Provides
createEnvContext(),bindEnv(), andbool(). - @optique/temporal (packages/temporal/): Temporal/Date parsers. Provides parsers for date and time values.
- @optique/git (packages/git/): Git reference parsers. Provides async value parsers for validating Git references (branches, tags, commits, remotes) using isomorphic-git.
- @optique/inquirer (packages/inquirer/): Interactive prompt integration.
Provides
prompt()for interactive fallback when CLI values are absent.
Each package is published to both JSR (Deno) and npm (Node.js/Bun):
- JSR uses deno.json with TypeScript source directly
- npm uses package.json with tsdown-built dist/ output (ESM + CJS + .d.ts)
When adding subpath exports to a package, update the following files:
- deno.json: Add the subpath to the
exportsfield - package.json: Add the subpath to the
exportsfield - tsdown.config.ts: Add the entry point to the build configuration
When adding a new package to the monorepo, update the following files:
- README.md (root): Add the package to the Packages table
- AGENTS.md: Add the package to the Package structure list (if applicable)
- docs/package.json: Add
"@optique/<name>": "workspace:"todevDependencies(required for Twoslash type checking in documentation)
This project follows test-driven development (TDD) practices:
- Write tests first: Before implementing new functionality, write tests that describe the expected behavior. Confirm that the tests fail before proceeding with the implementation.
- Regression tests for bugs: When fixing bugs, first write a regression test that reproduces the bug. Confirm that the test fails, then fix the bug and verify the test passes.
-
Do not use Conventional Commits (no
fix:,feat:, etc. prefixes). Keep the first line under 50 characters when possible. -
Focus on why the change was made, not just what changed.
-
When referencing issues or PRs, use permalink URLs instead of just numbers (e.g.,
#123). This preserves context if the repository is moved later. -
When listing items after a colon, add a blank line after the colon:
This commit includes the following changes: - Added foo - Fixed bar -
When using LLMs or coding agents, include credit via
Co-Authored-By:. Include a permalink to the agent session if available.
- Run all tests: Before committing any changes, run
mise testto ensure all tests pass across Deno, Node.js, and Bun runtimes.
- All code must be type-safe. Avoid using the
anytype. - Do not use unsafe type assertions like
as unknown as ...to bypass the type system. - Prefer immutable data structures unless there is a specific reason to
use mutable ones. Use
readonly T[]for array types and add thereadonlymodifier to all interface fields. - Use the nullish coalescing operator (
??) instead of the logical OR operator (||) for default values. - The library emphasizes compile-time type safety with automatic type inference for parser results. When working with parsers, the TypeScript compiler will infer complex union types and optional fields based on the combinator composition.
Optique supports both synchronous and asynchronous parsing through a generic
Mode type parameter ("sync" | "async"). TypeScript has a fundamental
limitation: it cannot narrow conditional types like ModeValue<M, T> based
on runtime checks of the mode value.
// This doesn't work - TypeScript can't narrow ModeValue<M, T>
if (mode === "async") {
return asyncResult; // Type error: ModeValue<M, T> expected
}To handle this limitation while maintaining type safety at API boundaries:
- All mode-based type assertions are isolated in mode-dispatch.ts.
- Use
dispatchByMode()for value returns anddispatchIterableByMode()for iterables instead of manualif (mode === "async")checks. - These helpers encapsulate the necessary
as ModeValue<M, T>assertions.
// Correct pattern:
return dispatchByMode(
parser.$mode,
() => parseSync(context),
() => parseAsync(context),
);When refactoring mode-related code, always use these dispatch helpers rather than adding new type assertions elsewhere. This keeps unsafe casts contained in a single, well-documented location.
-
All exported APIs must have JSDoc comments describing their purpose, parameters, and return values.
-
For APIs added in a specific version, include the
@sincetag with the version number:/** * Creates a string parser. * * @returns A parser that accepts any string value. * @since 1.2.3 */ export function string(): ValueParser<string> { // ... }
-
Use the
node:testandnode:assert/strictAPIs to ensure tests run across all runtimes (Node.js, Deno, and Bun). -
Test files are co-located with source files using
.test.tssuffix. -
Avoid the
assert.equal(..., true)orassert.equal(..., false)patterns. Useassert.ok(...)andassert.ok(!...)instead. -
To conditionally skip tests (e.g., platform-specific or tool-dependent tests), use both the
skipoption and an early return. Theskipoption ensures Deno and Node.js report the test as skipped, while the early return is needed because Bun ignores theskipoption and runs the test body regardless:it("should work on Windows only", { skip: process.platform !== "win32", }, () => { // Bun ignores the skip option, so we need an early return as well: if (process.platform !== "win32") return; // test body ... });
-
Prefer specific error types over generic
Error. Use built-in types likeTypeError,RangeError, orSyntaxErrorwhen appropriate. -
End error messages with a period:
throw new Error("Translation did not complete.");
-
When the message ends with a value after a colon, the period can be omitted:
throw new Error(`Failed to load file: ${filePath}`);
-
Functions or methods that throw exceptions must include the
@throwstag in their JSDoc comments.
- When writing code that runs across Deno, Node.js, and Bun, prefer using
Node.js built-in modules (e.g.,
node:fs,node:buffer,node:path,node:test). Deno provides compatibility layers for most Node.js built-in modules, making them a practical common ground for cross-runtime code. - Avoid using Deno-specific APIs (e.g.,
Deno.readTextFile()) in shared library code. Reserve Deno-specific APIs for Deno-only entry points or when wrapped with runtime detection.
When writing documentation in English:
- Use sentence case for titles and headings (capitalize only the first word and proper nouns), not Title Case.
- Use italics for emphasis rather than bold. Do not overuse emphasis.
- Avoid common LLM writing patterns: overusing em dashes, excessive emphasis, compulsive summarizing and categorizing, and rigid textbook-like structure at the expense of natural flow.
When creating or editing Markdown documentation files in this project, follow these style conventions to maintain consistency with existing documentation:
-
Setext-style headings: Use underline-style for the document title (with
=) and sections (with-):Document Title ============== Section Name ------------ -
ATX-style headings: Use only for subsections within a section:
### Subsection Name -
Heading case: Use sentence case (capitalize only the first word and proper nouns) rather than Title Case:
Development commands ← Correct Development Commands ← Incorrect
- Italics (
*text*): Use for package names (@optique/core, @optique/run), emphasis, and to distinguish concepts - Bold (
**text**): Use sparingly for strong emphasis - Inline code (
`code`): Use for code spans, function names, filenames, and command-line options
-
Use
-(space-hyphen-two spaces) for unordered list items -
Indent nested items with 4 spaces
-
Align continuation text with the item content:
- *First item*: Description text that continues on the next line with proper alignment - *Second item**: Another item
-
Use four tildes (
~~~~) for code fences instead of backticks -
Always specify the language identifier:
~~~~ typescript const example = "Hello, world!"; ~~~~ -
For shell commands, use
bash:~~~~ bash deno test ~~~~
-
Use reference-style links placed at the end of each section (not at document end)
-
Format reference links with consistent spacing:
See the [optparse-applicative] library for inspiration. [optparse-applicative]: https://github.com/pcapriotti/optparse-applicative
Use GitHub-style alert blocks for important information:
- Note:
> [!NOTE] - Tip:
> [!TIP] - Important:
> [!IMPORTANT] - Warning:
> [!WARNING] - Caution:
> [!CAUTION]
Continue alert content on subsequent lines with >:
> [!CAUTION]
> This feature is experimental and may change in future versions.
Use pipe tables with proper alignment markers:
| Feature | @optique/core | @optique/run |
|-----------------|---------------|--------------|
| Argument source | Manual | Automatic |
- Wrap lines at approximately 80 characters for readability
- Use one blank line between sections and major elements
- Use two blank lines before Setext-style section headings
- Place one blank line before and after code blocks
- End sections with reference links (if any) followed by a blank line
The docs/ directory contains VitePress documentation with additional features beyond standard Markdown.
Use the twoslash modifier to enable TypeScript type checking and hover
information in code blocks:
~~~~ typescript twoslash
import { run, command, option } from "@optique/run";
const cmd = command("hello", () => "Hello world");
~~~~
When code examples need variables that shouldn't be shown to readers,
declare them before the // ---cut-before--- directive. Content before
this directive is compiled but hidden from display:
~~~~ typescript twoslash
const input = ["--help"];
// ---cut-before---
import { run } from "@optique/run";
await run(cmd, input);
~~~~
The reader sees only the code after ---cut-before---, but TypeScript
checks the entire block including the hidden fixture.
For functions that need to exist but shouldn't be shown, use declare:
~~~~ typescript twoslash
declare function fetchConfig(): Promise<string>;
// ---cut-before---
import { parse } from "@optique/core/parser";
const config = await fetchConfig();
~~~~
VitePress supports definition lists for documenting terms, options, or properties:
`--help`
: Show help message
`--version`
: Show version information
This renders as a formatted definition list with the term on one line and the description indented below.
Use code groups to show the same content for different package managers or environments:
::: code-group
~~~~ bash [Deno]
deno add jsr:@optique/run
~~~~
~~~~ bash [npm]
npm add @optique/run
~~~~
~~~~ bash [pnpm]
pnpm add @optique/run
~~~~
:::
- Internal links: When linking to other VitePress documents within
the docs/ directory, use inline link syntax (e.g.,
[text](./path/to/file.md)) instead of reference-style links. - Relative paths: Always use relative paths for internal links.
- File extensions: Include the
.mdextension in internal link paths.
cd docs
pnpm build # Build for production (runs Twoslash type checking)
pnpm dev # Start development serverAlways run pnpm build before committing to catch Twoslash type errors.