Cursor rule
.cursor/rules/cli.mdcrelated to doughnut CLI
Cursor rules
Quality
96/100
Scores the file, not the repository.Length
1,812 words
12 headings · 1 code blocksRepository
49
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.123456# doughnut-cli78TypeScript CLI for Doughnut. Lives in `cli/`.910## Structure1112```13cli/14 src/ # Production code: `main`/`run`, `nonInteractiveCli`, `interactiveInkSession` (TTY → Ink edge), Ink app, `commands/`15 src/commonUIComponents/ # Context-neutral Ink UI reused across stages and the main prompt (borders, guidance lists, y/n, past user block, stage key context)16 src/sessionScrollback/ # One Ink `<Static>` session history above the live column; transcript factories + recall answered rows; append context for stages17 src/commands/ # Slash-command and subcommand implementations + aggregated help18 src/shims/ # Modules referenced only from the esbuild `bundle` script (aliases)19 tests/ # Vitest unit tests (*.test.ts)20 vitest.config.ts21 tsconfig.json22 package.json23```2425## TypeScript module exports2627Keep each module’s **public surface small**: `export` only what other modules actually use. Prefer leaving helpers, constants, and types **unexported** when they are implementation details. Do not add exports “for tests” or “maybe later” — if only tests need a symbol, test through a higher-level entry point when possible (see **Vitest: observable behavior** below). Avoid widening the export list when a single import site could instead live next to the code.2829## Commands3031| Task | Command |32|------|---------|33| Build bundle | `pnpm cli:bundle` |34| Run tests | `pnpm cli:test` |35| Mutation test (Stryker) | `CURSOR_DEV=true nix develop -c bash -c 'cd cli && pnpm test:mutation'` — see the `mutation-testing` skill |36| Format | `pnpm cli:format` |37| Lint | `pnpm cli:lint` |3839## Architecture Roadmap4041The architecture roadmap is at `ongoing/cli-architecture-roadmap.md` (legacy location; leave in place). New planning artifacts go under `.planning/phases/` or `.planning/quick/` — see `gsd-coexistence.mdc`.4243It is a guideline for growing the architecture, not a direct implementation checklist. Apply it when a feature needs it; challenge the fit first. Update it as decisions land and refine the forward-looking parts as understanding changes.4445**Session scrollback (interactive)** — Past session lines use **one** Ink `<Static>` (append-only) above the live column (stage + `MainInteractivePrompt`). Generic `SessionScrollback` stays domain-agnostic; shell transcript shapes live in `interactiveCliTranscript.tsx`; recall “answered” outcomes use `recallAnsweredScrollback.tsx`; stages append via `sessionScrollbackAppendContext`.4647**Interactive TTY boundary** — `interactiveInkSession.ts` only checks TTY, prints the welcome banner, and calls Ink `render` with injectable `stdin`/`stdout`. Domain behavior stays in `InteractiveCliApp` and `commands/`.4849**User-visible slash-command errors** — Map failures to assistant text with `userVisibleSlashCommandError` (see `cli/tests/userVisibleSlashCommandError.test.ts`). Red past-assistant blocks use `pastAssistantErrorBlock.tsx` for committed transcript lines.5051**Stage keyboard routing** — `SetStageKeyHandlerContext` (`stageKeyForwardContext.tsx`): the shell registers one handler so Esc and other stage keys are handled without competing Ink `useInput` instances; stages that need it install via context (e.g. `AsyncAssistantFetchStage`).5253## Terminal column width (TTY layout)5455**Do not use** JavaScript string `.length` or UTF-16 code units to measure how wide text is on screen. Terminals use **column count**: CJK and many emoji render as **2 columns**; grapheme clusters (flags, ZWJ families, text + VS16) must be measured as units.5657## Vitest: observable behavior5859For **interactive** behavior, prefer **`runInteractive`** (from `interactive.js`, implemented in `interactiveInkSession.ts`) with a **mock TTY** stdin and assert **stdout** / visible output — the test may **not import** the module you changed; coverage through the CLI surface is enough. For **argv routing** (`version`, `help`, interactive fallback), use **`run`** from `run.js` (see `cli/tests/index.test.ts`). For cross-cutting test strategy, see **Observable behavior first** in the `phased-planning` skill.6061**Mocking Doughnut HTTP from unit tests** — Use **`vi.spyOn`** on **`doughnut-api`** controller static methods (e.g. `RecallsController.recalling`, `MemoryTrackerController.showMemoryTracker`) and **`mockResolvedValue`** with the SDK success shape (`{ data: … }`, cast as **`Awaited<ReturnType<typeof Controller.method>>`** when needed). Build **`data`** values that match backend / SDK types with **`makeMe`** from **`doughnut-test-fixtures/makeMe`** (e.g. `makeMe.aMemoryTracker`, `makeMe.aNoteRealm`, `makeMe.aDueMemoryTrackersList`) instead of ad hoc object literals. Do **not** use **`http.createServer`** to fake `/api/…` for ordinary command behavior. Reserve a real local HTTP server for tests whose subject is transport or error classification (e.g. status codes), not for happy-path recall or token flows.6263**No fixed-time waits in unit tests** — Do not use `sleep`, `setTimeout(…, N)` with a duration, or similar wall-clock delays to “let Ink/React catch up.” Prefer driving the real async surface: `setImmediate` / microtask turns in a loop until an **observable** condition holds (e.g. `frames` or stdout contains the expected text), with a **turn-count** cap and a clear failure message if the condition never becomes true. E2E may still use bounded retries where appropriate; Vitest unit tests under `cli/tests/` should stay deterministic without arbitrary milliseconds.6465## Ink + React + Node (avoid flaky interactive tests)6667- **Defer `useApp().exit()` / unmount after UI updates:** Do not call **`exit()`** from the same synchronous turn as a slash command that still has to append transcript state (e.g. “Bye.”). **`/exit`** is special-cased in **`InteractiveCliApp`**: after the assistant line is committed, a **`useEffect`** runs **`exit()`**. In Node, **`setTimeout(…, 0)`** can still run **before** **`setImmediate`** work used by React/Ink — avoid **`setTimeout(…, 0)`** for this ordering.68- **Stable `useInput` handler:** Pass **`useCallback`** (with correct deps) to **`useInput`**, not a new inline function every render. Ink’s `useInput` effect depends on the handler reference; a new function each render tears down and re-attaches the internal listener and can drop keystrokes under load.69- **`ink-testing-library` + stdin:** **`useInput` registers via `useEffect`** — **`stdin.write` immediately after `render()` can race** empty listeners. Before real input, **wait on an observable** (e.g. write a harmless probe key, **`waitForFrames` until `lastFrame()` shows it**, then undo if needed), or use **`renderInkWhenCommandLineReady`** from `cli/tests/inkTestHelpers.ts` (probe key + wait; see `InteractiveCliApp` ink tests).70- **Ink test async helpers:** Import **`waitForFrames`**, **`waitForLastFrame`**, and **`stripAnsi`** from **`cli/tests/inkTestHelpers.ts`** instead of duplicating the `setImmediate` poll loop (or ANSI stripping) in each test file. After **`renderInkWhenCommandLineReady`**, prefer **`lastStrippedFrame()`** (current frame, ANSI-stripped), **`waitForLastFrameToInclude(pattern)`**, and **`waitForFramesToInclude(pattern)`** where **`pattern`** is a substring or **`RegExp`** (combined scrollback is ANSI-stripped for matching). Use **`waitForFrames`** / raw **`frames.join('\n')`** when the assertion must see **SGR sequences** (e.g. `\x1b[100m`) that stripping would remove.71- **`<Static>` scrollback:** Session history is rendered inside Ink `<Static>`; captured **`frames`** can **repeat** the same scrollback text every frame. For “appears once on screen” or row budgets, prefer **`lastFrame()`** / **`lastStrippedFrame()`**, not counting substrings across **`frames.join('\n')`**.72- **Typing simulations:** Do not use **`setImmediate` per character** as “Ink is ready.” **Wait until the frame shows the expected buffer** (or combined `frames` text) before the next `stdin.write`.7374## Domain terminology7576Vocabulary for Cucumber steps and page objects (`e2e_test/start/pageObjects/cli/`). **Exact TTY behavior** (past messages, user input history, cursor, rendering) lives in code + Vitest; **scenario-shaped coverage** in `e2e_test/features/cli/*.feature`.7778| Term | Definition |79|------|-------------|80| **Non-interactive output** | Full stdout for E2E subcommand spawns (e.g. installed `version` / `update`) and similar one-shot runs; no PTY interactive input-ready control sequence. |81| **Past CLI assistant messages** | Interactive: past CLI output blocks in the session scrollback (shell assistant lines, errors, session summaries, and recall **answered** lines such as `Correct!` / `Reviewed:` — the latter as `RecallAnsweredItem`, not duplicate `onSettled` assistant text). Gherkin: `in past CLI assistant messages`. |82| **Past user messages** | Interactive: past user lines as gray-background blocks (`\x1b[100m`…), one blank padded row above the text (E2E checks this); one padded row below before the command line (see Vitest `pastUserMessageBlock` / `InteractiveCliApp.test`). Gherkin: `in past user messages`. Recall **y/n** confirmations (stop recall, load more, just-review; prompt footer may show `(y/n)` or `(Y/n)` / `(y/N)` when Enter commits a default) do **not** add a separate past user message row — only the outcome lines appear. On **Load more from next 3 days?**, **Esc** declines load more (same outcome as **n**, session summary), not the card-level leave-recall confirm. |83| **User input history** | TTY: committed lines for ↑↓ recall + persistence (`mainInteractivePrompt/history.ts`, shared store `inputHistory/`) **while the command-line Ink region has focus**. The live command buffer is **single-line** (newlines from paste become spaces; no Shift+Enter newline). Masked before storage/display. Recall **y/n** answers are not appended (same rule as past user messages). |84| **Current Stage** | Conceptual state during a multi-step or long-running command (e.g. recall session, slow interactive network call). |85| **Current Stage Indicator** | On the TTY, when a stage is surfaced: the first line of the **Current prompt** block — full terminal width on the **Current stage band** (e.g. “Recalling” while in recall). Not part of **Current guidance**. |86| **Current stage band** | Shared background for the Current Stage Indicator line and, when the indicator is shown, the **Current prompt** separator under it, so the top of the block reads as one strip. Implemented as `CURRENT_STAGE_BAND_BACKGROUND_SGR` in `cli/src/renderer.ts`. |87| **Current prompt** | Block above the **command line** (live typing strip): optional **Current Stage Indicator** + separator (banded when the indicator is shown), then wrapped lines (MCQ stem and notebook line, fetch-wait prompt, y/n text, token-list copy, etc.). Recall **MCQ** (TTY): **numbered choices** live in **Current guidance**, not here. |88| **Current guidance** | Below the command line: `/` hints, token lists, **MCQ numbered choices** (wrapped to terminal width; ↑↓ selects by choice index). |8990## CLI E2E9192Features: **`e2e_test/features/cli/`**. Steps: **`e2e_test/step_definitions/cli.ts`** (thin glue only). Page objects and terminal assertions: **`e2e_test/start/pageObjects/cli/`**, especially **`outputAssertions.ts`** — put new “what appears in the terminal?” checks there (retries, ANSI-stripped snapshot text on failure, screenshot on the final throw path).9394- **Run:** Cypress Node tasks spawn `cli/dist/doughnut-cli.bundle.mjs` via `node` (same locally and in GitHub Actions). Before spawn, `ensureCliBundleFresh` rebuilds the bundle when `cli/src`, `cli/package.json`, `cli/tsconfig.json`, or `packages/doughnut-api/src` are newer than the bundle. Set **`DOUGHNUT_CLI_E2E_USE_TSX=1`** to force `pnpm -C cli exec tsx src/index.ts` for debugging. Installation scenarios use the E2E install bundle path (see `@bundleCliE2eInstall`).95- **`@bundleCliE2eInstall`:** Builds `cli/dist/e2e-install-doughnut-cli.bundle.mjs` before each scenario and removes it after; the local LB (`scripts/local-lb.mjs`) serves `/doughnut-cli-latest/doughnut` from that file when present so install tests do not overwrite `cli/dist/doughnut-cli.bundle.mjs`.96- **Active CLI E2E (CI):** `e2e_test/features/cli/cli_install_and_run.feature` non-ignored scenarios only. **`installCli`** runs the install script. Non-interactive steps use **`runInstalledCli`** (`node <installed binary> …` in a **managed PTY**, same geometry and env merge as interactive; waits for exit code 0) and **`cli.nonInteractiveOutput().expectContains`** → **`cliAssert`** with **`strippedTranscript`** (`nonInteractiveCliOutputAssertRequest` in `outputAssertions.ts`). The **Install and run the CLI in interactive mode** scenario uses **`runInstalledCliInteractive`**, **`cliInteractiveWriteLine`** for slash input, and transcript assertions **`interactiveCli().pastCliAssistantMessages().expectContains`** / **`pastUserMessages().expectDisplayed`** (two **`cliAssert`** requests: full-buffer gray-block rules, then stripped-transcript blank-line-above). Assertions run in the plugin via **`cliAssert`** → `tty-assert` managed session, not browser-side buffer polling. On assert failure, the plugin saves a **viewport PNG** and, when **`tty-assert`** has recorded at least two distinct viewport frames, an animated **GIF** (`buildViewportAnimationGif`), under the current spec folder via **`saveBufferToCurrentSpecFolder`** (`e2e_test/config/cliE2ePluginTasks.ts`, `cypressSpecScreenshotSink.ts`).9798## Build output99100- Bundle: `cli/dist/doughnut-cli.bundle.mjs` (shebang)101- Release: `gs://dough-frontend-01/doughnut-cli-latest/doughnut` (`cli-release.yml`)102- Local install URL: local LB serves `/doughnut-cli-latest/doughnut` (see `scripts/local-lb.mjs`, `docs/gcp/prod_env.md`)103104**Ink + esbuild (`react-devtools-core`):** Ink may load `devtools.js`, which imports `react-devtools-core`. That package is optional in Ink (used when `DEV=true`; see Ink’s README). Esbuild still resolves the import when producing the single-file bundle, so `cli/package.json` `bundle` aliases `react-devtools-core` to `cli/src/shims/react-devtools-core-stub.ts`. The shipped bundle therefore does not depend on installing `react-devtools-core`. For React DevTools against an **unbundled** run (e.g. `pnpm -C cli exec tsx src/index.ts`), install `react-devtools-core` and use `DEV=true` as Ink documents.105106## Install scripts107108- Bash: `backend/src/main/resources/install.sh`109- PowerShell: `backend/src/main/resources/install.ps1`110- Served at `/install` (`InstallController`; `?win32=true` for PowerShell)111
Also in nerds-odd-e/doughnut
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| nerds-odd-e/doughnut.clinerules/daisyui.md · 49 | Cline rules | setuplint-formatstyleui+1 | 57/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/architecture-decisions.mdc · 49 | Cursor rules | no sections | 16/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/backend-code.mdc · 49 | Cursor rules | styletypesdatabasedo-not | 61/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/backend-testing.mdc · 49 | Cursor rules | buildteststyletesting-strategy+2 | 73/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/db-migration.mdc · 49 | Cursor rules | stylearchdatabasedeployment | 64/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/e2e-authoring.mdc · 49 | Cursor rules | setupteststylearch+3 | 80/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/e2e-ocr.mdc · 49 | Cursor rules | setuptesting-strategydo-not | 46/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/frontend-api.mdc · 49 | Cursor rules | styletesting-strategyapido-not | 57/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/frontend-component.mdc · 49 | Cursor rules | testlint-formatstylearch+2 | 76/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/frontend-storybook.mdc · 49 | Cursor rules | buildteststyletesting-strategy+1 | 69/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/frontend-testing.mdc · 49 | Cursor rules | buildteststyletesting-strategy+2 | 89/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/general.mdc · 49 | Cursor rules | styledo-not | 49/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/gsd-coexistence.mdc · 49 | Cursor rules | style | 60/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/linting_formating.mdc · 49 | Cursor rules | testlint-formatstylearch+6 | 88/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/mcp-server.mdc · 49 | Cursor rules | buildtestlint-formatarch+2 | 85/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/planning.mdc · 49 | Cursor rules | teststylearchdo-not+1 | 75/100 | 3 days ago | |
| nerds-odd-e/doughnut.cursor/rules/script.mdc · 49 | Cursor rules | testarch | 58/100 | 3 days ago | |
| nerds-odd-e/doughnutAGENTS.md · 49 | AGENTS.md | no sections | 47/100 | 3 days ago | |
| nerds-odd-e/doughnutCLAUDE.md · 49 | CLAUDE.md | agent-behaviour | 47/100 | 3 days ago |
Diff against .clinerules/daisyui.md Diff against .cursor/rules/architecture-decisions.mdc Diff against .cursor/rules/backend-code.mdc Diff against .cursor/rules/backend-testing.mdc Diff against .cursor/rules/db-migration.mdc Diff against .cursor/rules/e2e-authoring.mdc Diff against .cursor/rules/e2e-ocr.mdc Diff against .cursor/rules/frontend-api.mdc Diff against .cursor/rules/frontend-component.mdc Diff against .cursor/rules/frontend-storybook.mdc Diff against .cursor/rules/frontend-testing.mdc Diff against .cursor/rules/general.mdc Diff against .cursor/rules/gsd-coexistence.mdc Diff against .cursor/rules/linting_formating.mdc Diff against .cursor/rules/mcp-server.mdc Diff against .cursor/rules/planning.mdc Diff against .cursor/rules/script.mdc Diff against AGENTS.md Diff against CLAUDE.md
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| hiromaily/go-crypto-wallet.cursor/rules/typescript.mdc · 126 | Cursor rules | setupbuildtestlint-format+6 | 100/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/10-hermes-api.mdc · 45 | Cursor rules | testlint-formatstylearch+5 | 100/100 | 3 days ago | |
| dodgecfr/combatfilms-webapp.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| deifos/clipmira-subtitles.cursor/rules/frontend.mdc · 1 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| markstev/mark-starter.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+6 | 99/100 | 3 days ago | |
| Allymahmoud/case-intake-platform.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| langflow-ai/langflow.cursor/rules/docs_development.mdc · 153k | Cursor rules | setupbuildtestlint-format+7 | 97/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/20-hermes-api-tests.mdc · 45 | Cursor rules | teststyletesting-strategysecurity+3 | 97/100 | 3 days ago |
