refactor(opencode): lazy-load top-level CLI commands for faster --help/--version/completion#27800
Open
danfry1 wants to merge 1 commit into
Open
refactor(opencode): lazy-load top-level CLI commands for faster --help/--version/completion#27800danfry1 wants to merge 1 commit into
danfry1 wants to merge 1 commit into
Conversation
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
Contributor
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
Author
|
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #27799
Type of change
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):
opencode --helpopencode --versionopencode --get-yargs-completions …opencode db --helpopencode mcp --helpThe 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 yargsCommandModuleso itscommand/aliases/describe/deprecatedregister synchronously while the implementation module is dynamic-imported on firstbuilderorhandlercall. yargs supports async builders natively, so help, completion, and argv matching work without ever touching the implementation. One shared cachedimport()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 singleTuiThreadSpec(src/cli/cmd/tui/thread-spec.ts) that bothsrc/index.tsandsrc/cli/cmd/tui/thread.tsconsume by reference. A drift-guard test assertsTuiThreadCommand.{command, describe, builder} === TuiThreadSpec.{...}so the two can't diverge.Also defers a handful of entrypoint imports (
Log,Installation,Heap,NamedError,FormatError, plusdrizzle+Database+JsonMigrationfor the first-run migration) to their use sites.CancelledErrormoves out ofsrc/cli/ui.tsinto its own module so the eagerly loadedUInamespace no longer transitively importseffect/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 typecheckclean across the workspacebun test test/cli/— 307 / 307 pass (includes newtest/cli/lazy.test.tswith 9 tests covering the helper's cache / error-wrap / builder-form / metadata contract, plus theTuiThreadSpecdrift guard intest/cli/tui/thread-spec.test.ts)bun run build --single; smoke test inscript/build.tspasses--help,opencode mcp --help,opencode run --help,opencode debug --help, and--get-yargs-completions …opencodewith no args) dispatches correctly — opentui escape codes confirm the lazy handler loadedTuiThreadCommandand the worker bootstrap ranScreenshots / recordings
n/a — no UI changes; help / completion output is byte-identical to baseline.
Checklist