decopin-cli

↑ ↓ to navigate · Enter to open · Esc to close

ProjectDevelopment

Development

Working on decopin-cli itself.

bun install
bun run ci            # build, then typecheck / test / lint / format in parallel, then the Intent document
bun run build         # build demo/app with the checkout itself (no bunx)
bun run intent:doc    # rerun every test, then print which Behavior each Intent proves and where
bun run intent:list   # the index alone: one line per Intent, with its purpose
bun run dev           # watch demo/app and rebuild on every save
bun run gen --conv cmd --path demo/app/hello   # scaffold into demo/app
bun run site          # build the documentation site into site/dist
bun run site:dev      # build and preview at http://localhost:4173 (restart after edits)
bun run bench         # startup time
bun run format        # rewrite files (ci only checks)

bun run ci runs exactly what CI runs (.github/workflows/ci.yml).

Layout of the source

src/
├── cli/                  the decopin command: init, gen, build, dev
├── core/
│   ├── build/            scanner, evaluator, type emitter, codegen
│   ├── runtime/          what runs inside your built CLI
│   ├── jsx/ renderer/    the JSX runtime and the terminal renderer
│   ├── components/       Line, Text, Box, Table, ...
│   └── validation/       the Type.* components and their valibot mapping
└── features/
    ├── conventions/      cmd, argv, data, output, stdin, help, shell, complete
    ├── inherited/        layout, middleware, error, not-found
    └── root-only/        env, version, global-error, not-found

The three directories under features/ are the same three kinds of files this site is organised by. Each feature owns its file name, its build-time evaluation, its runtime, and its contribution to the generated types.

Tests

DirectoryWhat it pins
test/contract/promised behaviour, as tables
test/docs/the README and this site: tsx blocks type-check, shell examples run
test/renderer/the terminal renderer and display width
test/features/each file convention on its own
test/intent/the Intents, and the runtime that turns tests into their Evidence

Intents

Every subcommand states what it is for, as an Intent (ADR 48). A test that shows a user reaching one of those outcomes is that Intent's Evidence — it is not a separate kind of test, it is the same test, named after the outcome and registered against a Behavior.

Start from the index, not from the source:

bun run intent:list                          # 8 Intents, one purpose each
bun test/intent/doc.ts run-commands-as-declared   # one Intent in full

The purpose lines carry the words a user would use ("打ち間違えた", "型が 遅れない"), so grepping the index finds the Intent before you know which file implements it.

Adding one

A new subcommand under src/cli/<name>/ starts from its Intent, and test/docs/decisions.test.ts fails until it has one. Four files in test/intent/<name>/:

FileHolds
intent.tsone sentence: what someone can do that they could not before
behavior.tsthe outcomes, each one a place you would go to fix a failure
implementation.tsBehavior → the functions that carry it (implementation stays in src/)
<name>.test.tsthe Evidence, ending in report(IMPLEMENTATION)
// <name>.test.ts — the shape. proves() only counts a test that passed.
describeBehavior(IMPLEMENTATION, 'tells-the-next-step', () => {
  proves('書いたファイルと次に打つコマンドを出す', async () => {
    /* ... */
  });
});

report(IMPLEMENTATION); // last line, once. The runtime throws if anything follows.

An outcome you cannot prove is declared, not omitted: waived(id, description, why) keeps it in the document with its reason, and fails once it can be proved.

Proving one Intent from several files

A test that already lives next to its convention stays there. Wrap it and declare the split — no file moves:

// test/runtime/handle-error.test.tsx
describeBehavior(RUNTIME, 'handles-errors-where-declared', () => {
  /* proves(...) */
});
report(RUNTIME, { partial: true });

Every file proving that Intent passes partial, including the one in test/intent/; whether all Behaviors were reached is then decided after doc.ts merges the shards. Two rules the runtime enforces: describe does not nest inside describeBehavior (fold the heading into the proof name), and report() is the last line.

bun test writes the shards, bun run ci reads them. A filtered run leaves stale shards behind, so rebuild the document with bun run intent:doc, which clears them and runs everything.