Skip to content

refactor(opencode): lazy-load top-level CLI commands for faster --help/--version/completion#27800

Open
danfry1 wants to merge 1 commit into
anomalyco:devfrom
danfry1:perf/cli-lazy-load
Open

refactor(opencode): lazy-load top-level CLI commands for faster --help/--version/completion#27800
danfry1 wants to merge 1 commit into
anomalyco:devfrom
danfry1:perf/cli-lazy-load

Conversation

@danfry1
Copy link
Copy Markdown

@danfry1 danfry1 commented May 15, 2026

Issue for this PR

Closes #27799

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Reduces CLI startup cost for parser-only hot paths — --help, --version, and shell tab completion — by deferring command-module imports until a builder or handler actually fires.

Compiled-binary medians (warm, n=10):

Command Before After Change
opencode --help 213ms 69ms −68%
opencode --version 193ms 42ms −78%
opencode --get-yargs-completions … 192ms 42ms −78%
opencode db --help 199ms 63ms −68%
opencode mcp --help 195ms 148ms −24%

The Tab completion path is the one users feel most — it fires on every Tab keystroke and previously paid the full app-bootstrap import cost each time.

How it works. A new lazy() helper (src/cli/lazy.ts) wraps a yargs CommandModule so its command / aliases / describe / deprecated register synchronously while the implementation module is dynamic-imported on first builder or handler call. yargs supports async builders natively, so help, completion, and argv matching work without ever touching the implementation. One shared cached import() per command. Loader rejections are rewrapped with the command name so a missing compiled-binary chunk surfaces a useful error instead of an anonymous dynamic-import stack.

The default $0 [project] command can't be fully lazy — yargs renders its option spec inline in the top-level --help, so the spec must be resolvable synchronously. The fix: extract a single TuiThreadSpec (src/cli/cmd/tui/thread-spec.ts) that both src/index.ts and src/cli/cmd/tui/thread.ts consume by reference. A drift-guard test asserts TuiThreadCommand.{command, describe, builder} === TuiThreadSpec.{...} so the two can't diverge.

Also defers a handful of entrypoint imports (Log, Installation, Heap, NamedError, FormatError, plus drizzle + Database + JsonMigration for the first-run migration) to their use sites. CancelledError moves out of src/cli/ui.ts into its own module so the eagerly loaded UI namespace no longer transitively imports effect/Schema.

This change only affects CLI startup. Full command execution after middleware boot — logging init, heap setup, migration probe, instance bootstrap — runs exactly as before once a real command dispatches.

How did you verify your code works?

  • bun run typecheck clean across the workspace
  • bun test test/cli/ — 307 / 307 pass (includes new test/cli/lazy.test.ts with 9 tests covering the helper's cache / error-wrap / builder-form / metadata contract, plus the TuiThreadSpec drift guard in test/cli/tui/thread-spec.test.ts)
  • Compiled binary built locally with bun run build --single; smoke test in script/build.ts passes
  • Verified byte-identical output versus baseline for top-level --help, opencode mcp --help, opencode run --help, opencode debug --help, and --get-yargs-completions …
  • Verified default TUI launch (opencode with no args) dispatches correctly — opentui escape codes confirm the lazy handler loaded TuiThreadCommand and the worker bootstrap ran

Screenshots / recordings

n/a — no UI changes; help / completion output is byte-identical to baseline.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Reduces CLI startup cost for parser-only hot paths (--help, --version,
shell tab completion) by deferring command-module imports until a
builder or handler actually fires. yargs supports async builders, so
the top-level metadata stays synchronous while implementations load
on demand.

The default ($0) command's option spec is extracted into a shared
module so src/index.ts and src/cli/cmd/tui/thread.ts can't drift.

Compiled-binary medians (warm, n=10):

  --help              213ms -> 69ms   (-68%)
  --version           193ms -> 42ms   (-78%)
  completion handler  192ms -> 42ms   (-78%)
  db --help           199ms -> 63ms   (-68%)
  mcp --help          195ms -> 148ms  (-24%)

Top-level --help output is byte-identical to baseline. Behaviour for
running commands is unchanged - middleware still initialises logging,
heap tracking, and migration as before once a real command dispatches.

Closes anomalyco#27799
@github-actions
Copy link
Copy Markdown
Contributor

Hey! Your PR title perf(cli): lazy-load top-level command registration for faster --help/--version/completion doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@danfry1 danfry1 changed the title perf(cli): lazy-load top-level command registration for faster --help/--version/completion refactor(opencode): lazy-load top-level CLI commands for faster --help/--version/completion May 15, 2026
@danfry1
Copy link
Copy Markdown
Author

danfry1 commented May 16, 2026

After posting this I came across #25693 (open since 2026-05-04), which tackles the same shell-completion latency with a different approach — detecting completion invocations in argv and short-circuiting middleware for them.

The two cover different surfaces:

Happy to defer, combine (the detection helper from #25693 is compatible with this approach), or proceed — whichever direction maintainers prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI parser-only paths (--help, --version, completion) pay full app-bootstrap import cost

1 participant