Files
suaveplan/openspec/archive/2026-05-25-enterprise-electrobun/design.md
2026-05-27 23:29:53 +01:00

6.2 KiB

Design — @suaveplan/electrobun (genesis port)

Architecture

              ┌───────────────────────────────────────────┐
              │           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

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.