# Design — @suaveplan/electrobun (genesis port) ## Architecture ```text ┌───────────────────────────────────────────┐ │ src/bin/suaveplan-electrobun │ │ (process.argv → exit code) │ └───────────────────┬───────────────────────┘ │ ▼ ┌───────────────────────────────────────────┐ │ src/cli/cli.ts │ │ runCli(argv) → { code } │ │ createCommand / devCommand / buildCommand│ │ packageCommand / signCommand │ └───┬──────┬──────┬──────┬──────┬───────────┘ │ │ │ │ │ ┌─────────┘ │ │ │ └──────────┐ ▼ ▼ ▼ ▼ ▼ generator/ runner/ runner/ runner/ runner/ ├─ collectFiles electrobun electrobun electrobun electrobun ├─ generate dev … build … package … sign … │ ├─ templates/ │ ├─ icons/ │ ├─ fs/ (FsAdapter) │ └─ config/ ▼ templates/ (project + feature renderers) ``` Every IO-touching primitive accepts an injectable adapter: | Adapter | Default | Used by | |---|---|---| | `FsAdapter` | `nodeFsAdapter` (real `node:fs`) | `generator`, `config`, `prompts`, `icons` | | `CommandRunner` | `nodeCommandRunner` (real `child_process.spawn`) | `runner`, `dev/build/package/sign` | | `IconRendererAdapter` | `iconzRendererAdapter` (real `iconz`) | `icons` | | `Prompter` | `@suaveplan/cli` prompt façade | `prompts` | This is the same seam pattern `@suaveplan/linter` uses for biome / fs / glob; the fakes used in tests are real-behaviour fakes (temp dirs, deterministic command output), not `vi.mock` stubs. ## File layout ```text packages/devtools/tooling/electrobun/ package.json tsconfig.lib.json vite.config.ts vitest.config.ts README.md tests/ setup/polyfills.ts package-shape.test.ts src/ index.ts bin/ suaveplan-electrobun.ts suaveplan-electrobun.md error-codes/ error-codes.ts error-codes.test.ts error-codes.md index.ts telemetry/ telemetry.ts telemetry.test.ts telemetry.md index.ts types/ electrobun.types.ts electrobun.types.md index.ts schemas/ schemas.ts schemas.test.ts schemas.md index.ts fs/ fs.ts fs.test.ts fs.md index.ts runner/ runner.ts runner.test.ts runner.md index.ts config/ config.ts config.test.ts config.md index.ts templates/ templates.ts templates.test.ts templates.md features.ts features.test.ts features.md index.ts icons/ icons.ts icons.test.ts icons.md index.ts generator/ generator.ts generator.test.ts generator.md index.ts prompts/ prompts.ts prompts.test.ts prompts.md index.ts cli/ cli.ts cli.test.ts cli.md index.ts ``` ## Error codes `defineCodes("ELECTROBUN", […])` with exactly: - `INVALID_OPTIONS` — Zod boundary failure. - `OUTPUT_NOT_EMPTY` — refusing to overwrite a non-empty target directory. - `CONFIG_PARSE_FAILED` — `.suaveplan-electrobun.json` exists but is malformed. - `ICON_SOURCE_MISSING` — `iconSource` path does not exist. - `ICON_RENDER_FAILED` — `iconz` raised. - `TEMPLATE_RENDER_FAILED` — a renderer threw. - `FS_WRITE_FAILED` — `writeFiles` could not commit. - `BINARY_UNAVAILABLE` — `bunx electrobun --version` returns non-zero. - `RUNNER_FAILED` — orchestrated command (dev/build/package/sign) returned non-zero. - `PROMPT_ABORTED` — user cancelled the interactive prompt. - `UNKNOWN_FLAG` — `runCli` saw an argv it does not recognise. ## OTel surface - `electrobun_projects_generated_total` (counter; tagged `features_count`). - `electrobun_icons_generated_total` (counter; tagged `outcome=generated|skipped|failed`). - `electrobun_runner_invocations_total` (counter; tagged `command=dev|build|package|sign|probe`). - `electrobun_runner_failures_total` (counter; tagged `command`, `exit_code`). - `electrobun_prompts_total` (counter; tagged `outcome=completed|aborted`). - `electrobun_generate_duration_ms` (histogram; tagged `features_count`). - `electrobun_runner_duration_ms` (histogram; tagged `command`). ## Test strategy Tests run under Vitest via `@suaveplan/testing/runner`. Everything IO-touching uses a real `mkdtemp` temp directory and the real `nodeFsAdapter` (no `vi.mock`). The runner surface is exercised with a real-behaviour fake `CommandRunner` that records the spawn args and returns a canned `CommandResult`; one integration scenario exercises the real `nodeCommandRunner` against `node --version` to prove the seam default works end-to-end. The icon renderer is exercised with a real-behaviour fake `IconRendererAdapter` that asserts the option shape passed to `iconz`; the real `iconz` path is a one-line dynamic-import call covered by the default adapter's smoke test. ## Why `runner` is the right level of abstraction Electrobun's dev/build/package/sign pipeline is implemented inside the `electrobun` binary that ships with the user's project. Re-implementing it here would duplicate the upstream and silently drift. Instead the wrapper shapes the argv, runs the binary through the `CommandRunner` seam, parses stdout/exit code, and produces a structured `*Report` that callers (CI, IDE extensions) can act on. This keeps the package thin enough to test exhaustively while still adding real value over directly calling `bunx electrobun build`.