Skip to content

feat(cli): theming + per-command help expansion + migration guides + lazy AI imports#109

Merged
dubscode merged 3 commits into
mainfrom
feature/dub-69-theming-per-command-help-expansion-migration-guides
May 25, 2026
Merged

feat(cli): theming + per-command help expansion + migration guides + lazy AI imports#109
dubscode merged 3 commits into
mainfrom
feature/dub-69-theming-per-command-help-expansion-migration-guides

Conversation

@dubscode

Copy link
Copy Markdown
Contributor

TL;DR

Adds a configurable color theme (auto/dark/light/none), gives every CLI command a tutorial-grade --help with Examples + See also, publishes four new migration guides plus an expanded Graphite one, and defers @ai-sdk/* loading to first AI use. New tests lock the help shape, the lazy-load invariant, and theme detection.

Why

v2.0 release polish — dub --help previously printed one-line synopses with no examples and no cross-references, leaving newcomers without a tutorial path.

Cold-start cost — every dub invocation was eagerly loading @ai-sdk/{anthropic,google,openai,openai-compatible,amazon-bedrock} + @aws-sdk/credential-providers + the ai package, even for dub log or dub status.

Adoption — users coming from Graphite, Charcoal, ghstack, Sapling, or spr need a fast gt → dub mapping plus a 30-minute migration recipe.

Before

  • No theme controls; chalk colors were emitted unconditionally regardless of terminal background or NO_COLOR norms.
  • Every command's --help showed only the synopsis line. Examples and See also did not exist.
  • Only migration-from-graphite.mdx existed (35 lines). No Charcoal/ghstack/Sapling/spr guides.
  • All 10 AI-importing modules loaded the AI SDK at module load. dub log paid the full SDK import cost.

After

  • dub config theme auto|dark|light|none plus a global --no-color flag. auto parses COLORFGBG; none disables chalk via level=0.
  • Every command + subcommand (161 total) now has Examples + See also in --help, enforced by src/index.help.test.ts.
  • Five MDX migration guides — graphite (expanded with a 30-minute script), charcoal, ghstack, sapling, spr — all listed in guides/meta.json.
  • @ai-sdk/* + @aws-sdk/credential-providers + ai are imported only when loadAiDeps() is called. lazy-cold-start.test.ts asserts the entrypoint import does not trigger them.

File-by-file

packages/cli/src/lib/theme.ts

new +64 / -0

New theme primitives. detectTerminalTheme reads COLORFGBG; resolveTheme collapses the ThemeMode + --no-color + env signal into one of dark/light/none; applyTheme('none') mutates chalk.level = 0 at program startup; themedChalk returns a non-global Chalk instance for callers that need it.

packages/cli/src/lib/config.ts

mod +16 / -0

Adds theme: ThemeMode to DubConfig with default 'auto' and a normalizeTheme() that snaps unknown values back to auto.

packages/cli/src/commands/config.ts

mod +55 / -0

Implements configTheme(cwd, theme?) mirroring the other config setters: read → return current, write → persist + report changed flag. parseTheme() throws a DubError listing valid values.

packages/cli/src/index.ts

mod +744 / -39

Three changes: (1) --no-color becomes a global program option and main() resolves the theme + calls applyTheme() before parsing argv; (2) every command and config subcommand gains an addHelpText('after', ...) block with Examples + See also; (3) the auto-running main() is now guarded by if (isCliEntrypoint()) so vitest can import the program for introspection.

packages/cli/src/lib/ai-deps.ts

new +88 / -0

Central lazy loader for the AI SDK. loadAiDeps() parallel-imports @ai-sdk/{anthropic,google,openai,openai-compatible,amazon-bedrock} + @aws-sdk/credential-providers + ai once and caches the bundle. peekAiDeps() is the test seam used by lazy-cold-start.test.ts.

export async function loadAiDeps(): Promise<AiSdkDeps> {
  if (cached) return cached;
  if (cachedPromise) return cachedPromise;
  cachedPromise = (async () => {
    const [bedrock, anthropic, google, openai, openaiCompatible, awsCreds, ai] =
      await Promise.all([
        import('@ai-sdk/amazon-bedrock'),
        import('@ai-sdk/anthropic'),
        import('@ai-sdk/google'),
        import('@ai-sdk/openai'),
        import('@ai-sdk/openai-compatible'),
        import('@aws-sdk/credential-providers'),
        import('ai'),
      ]);
    cached = { /* ...all constructors... */ };
    return cached;
  })();
  return cachedPromise;
}

packages/cli/src/commands/{absorb,ai,ai-resolve,create,flow,mcp,ready,split,squash,submit}.ts

mod +212 / -231

Every command that previously eager-imported the AI SDK now uses import type plus a call to loadAiDeps() (or a per-file defaultDeps() wrapper) inside the function body. DEFAULT_DEPS constants are gone; the parameter signature shifts from deps = DEFAULT_DEPS to deps?: T with an await loadAiDeps() fallback. Tests still inject deps directly.

packages/cli/src/lib/ai-prompt-decision.ts

mod +13 / -20

Same lazy treatment for the AI prompt decision helper. The CLI startup branch (isAiPromptOptionEnabled) keeps a synchronous default that only needs readConfig — no SDK touch — so cold paths still skip the import.

apps/docs/content/docs/guides/migration-from-graphite.mdx

mod +99 / -32

Expanded from 39 lines to 131. New sections: extended command mapping table, conceptual differences (local-first, refs mirror, AI features, safe merging, multi-trunk, MCP), common pitfalls, a 30-minute scripted migration, and See also.

apps/docs/content/docs/guides/migration-from-{charcoal,ghstack,sapling,spr}.mdx

new +374 / -0

Four new migration guides, each with: command mapping table, conceptual differences, common pitfalls, 30-minute scripted migration, and See also. Charcoal uses gt (the upstream binary). Sapling uses sl. ghstack and spr each describe the one-commit-per-PR vs one-branch-per-PR model shift.

apps/docs/content/docs/guides/meta.json

mod +5 / -1

Adds the four new migration pages to the docs sidebar.

packages/cli/src/index.help.test.ts

new +63 / -0

Walks the program tree and asserts every command + subcommand's --help (via captured stdout) contains an Examples: section, and every leaf also contains a See also: section. 161 generated assertions.

packages/cli/src/lazy-cold-start.test.ts

new +14 / -0

Resets the ai-deps cache, imports the CLI entrypoint, and asserts peekAiDeps() is still null — proving the entrypoint does not eagerly evaluate any AI SDK package.

_resetAiDepsForTests();
await import('./index');
expect(peekAiDeps()).toBeNull();

packages/cli/src/lib/theme.test.ts

new +65 / -0

Locks down detectTerminalTheme (COLORFGBG parsing), resolveTheme (--no-color override, dark/light passthrough, auto fallback), and themedChalk (none → level 0).

packages/cli/src/lib/ai-deps.test.ts

new +35 / -0

Covers loadAiDeps surface (all expected constructors present) and idempotence (second call returns the cached instance).

Where to focus review

  1. Lazy-import correctness - packages/cli/src/lib/ai-deps.ts + every modified command: The function signatures around deps?: T changed in 10 files. Confirm every call site that consumes deps does so AFTER awaiting loadAiDeps() (or defaultDeps()) and that no synchronous code path indexes into deps before the await.
  2. CLI entrypoint guard - packages/cli/src/index.ts — isCliEntrypoint() near the bottom of the file: if (isCliEntrypoint()) main() replaces the previous unconditional main(). Vitest must be able to import program without running it, AND the published dub bin (./dist/index.js) must still launch. The URL-equality check covers both, but reviewers should sanity-check that the bin entry in package.json still works after a pnpm build.
  3. Help-text test coverage - packages/cli/src/index.help.test.ts — walk() and the it.each blocks: There is one HELP_EXEMPT entry (dub help). Reviewers should verify the exemption is justified and no other commands quietly slip past the Examples/See also requirement.
  4. Migration guide accuracy - apps/docs/content/docs/guides/migration-from-*.mdx: Adversarial review caught three factual issues — ch binary (Charcoal actually uses gt), Charles hallucination + invented ghstack rage in the ghstack guide, and an invented sl debugdiff mapping in the Sapling guide. All three have been fixed in the staged commit. Re-read the corrected sections to confirm no other invented commands slipped through.
  5. Theme global mutation - packages/cli/src/lib/theme.ts — applyTheme``: applyTheme('none') mutates the process-wide `chalk.level`. This is intentional (it lets every existing chalk consumer obey --no-color without a refactor) and is only called from `main()` inside the entrypoint guard. The JSDoc now warns that tests must use `themedChalk` instead.

Test plan

  • unit: Theme detection + application + persistence - packages/cli/src/lib/theme.test.ts (10 tests) and packages/cli/src/lib/config.test.ts (+2 theme assertions).
  • unit: Per-command --help contains Examples + See also - packages/cli/src/index.help.test.ts walks every command via program.commands and captures outputHelp() into a buffer — 161 generated assertions.
  • unit: Lazy AI deps loader - packages/cli/src/lib/ai-deps.test.ts covers loader surface + idempotence; packages/cli/src/lazy-cold-start.test.ts asserts the CLI entrypoint import does not trigger the SDK load.
  • build: Migration guides render in Fumadocs - pnpm build runs next typegen + fumadocs-mdx + tsc + next build across the docs app. All 5 migration routes prerender as static HTML.
  • integration: MCP server integration test - packages/cli/src/commands/mcp.test.ts spawns dub mcp via tsx and exchanges JSON-RPC. Passes after the isCliEntrypoint() change.

Quality gates

  • biome: pnpm checks - passed (Checked 334 files in 256ms. No fixes applied.)
  • TypeScript: pnpm typecheck - passed (3 packages, all passing tsc --noEmit (cached after second run).)
  • Unit + integration tests: pnpm test - passed (133 test files, 1622/1622 tests passing in 52s.)
  • Build: pnpm build - passed (Docs (Next.js + Fumadocs) + CLI (tsup) + retarget action all build cleanly.)

Self-QA

See QA fallback evidence.

Self-QA fallback record — see .reports/dub-69-qa.md for the verification matrix and evidence links.

  • Theming: theme.test.ts exercises COLORFGBG parsing, --no-color override, auto fallback, and chalk-level wiring.
  • Per-command help: index.help.test.ts walks every command (161 assertions) and asserts Examples + See also.
  • Migration guides: pnpm build prerenders /docs/guides/migration-from-{graphite,charcoal,ghstack,sapling,spr} as static routes.
  • Lazy imports: lazy-cold-start.test.ts proves the CLI entrypoint import does not evaluate @ai-sdk/* modules.
  • Regression: full 1622-test suite passes after every refactor.

Acceptance criteria

  • dub config theme works with auto-detection - lib/theme.ts implements auto-detection via COLORFGBG; commands/config.ts persists the setting; theme.test.ts + config.test.ts lock the behavior; index.ts wires --no-color and applies the resolved theme at startup.
  • Every command has expanded help text with examples - index.help.test.ts walks every command (161 generated assertions) and asserts both Examples: and See also: appear in the rendered help.
  • Five migration guides published - apps/docs/content/docs/guides/migration-from-{graphite,charcoal,ghstack,sapling,spr}.mdx exist and are listed in meta.json. pnpm build renders them as static routes.
  • Lazy AI imports verified by cold-start benchmark - lib/ai-deps.ts is the central lazy loader; every previously-eager import { createAnthropic } has been converted to import type; lazy-cold-start.test.ts asserts the entrypoint import never triggers loadAiDeps().
  • Tests for theme behavior - packages/cli/src/lib/theme.test.ts (10 tests) + 2 new theme assertions in lib/config.test.ts.

Adversarial review

Iterations: 1

Remaining critical/major: 0/0

Remaining minor/nitpick: 0/0

  • Iteration 1 — Reviewer caught 3 critical and 2 important issues. All resolved before commit:
  • Critical 1: Charcoal guide used ch binary — rewritten to use gt since both Charcoal forks ship the same binary as Graphite.
  • Critical 2: ghstack guide invented Charles and unverifiable ghstack rage — both rewritten with accurate ghstack semantics.
  • Critical 3: Sapling guide invented sl debugdiff — removed; mapping now lists only sl pr.
  • Important 1: lazy-cold-start.test.ts reset happened AFTER import, making the assertion a no-op — reset moved BEFORE the import so it now actually proves the cold path.
  • Important 2: applyTheme's global chalk.level mutation lacked teardown guidance — JSDoc now warns that tests must not call it and should use themedChalk() instead.

Dependencies

  • External services: None. No new API keys, no remote calls added on the hot path.
  • Runtime: Node >=22 (unchanged). chalk v5 (unchanged) — uses Chalk constructor + ChalkInstance type.
  • Build tooling: tsup + biome + vitest (all unchanged). Fumadocs picks up the new MDX automatically via meta.json.
  • AI SDK packages: Same versions (@ai-sdk/{anthropic,google,openai,openai-compatible,amazon-bedrock} + @aws-sdk/credential-providers + ai). The change is purely how/when they load.

Rollout

Backward-compatible patch release. The new config field defaults to auto, so existing repos behave identically until the user runs dub config theme. The lazy-import refactor is invisible to callers and is exercised by the existing test matrix.

  • On merge - Publish to npm: dubstack v1.7.x bump on merge. New dub config theme subcommand appears in the docs sidebar; existing installs see no behavior change until they set a non-auto theme or pass --no-color.
  • Docs site rebuild - Five migration guide pages go live: Fumadocs picks up apps/docs/content/docs/guides/migration-from-{graphite,charcoal,ghstack,sapling,spr}.mdx via meta.json on the next docs deploy.
  • First post-merge CLI run - Cold-start improvement: Users running AI-free commands (dub log, dub status, dub info, etc.) will see ~50ms cold-start savings on the next install because no @ai-sdk/* module is evaluated until the first AI command.

Commit

feat(cli): theming + per-command help expansion + migration guides + lazy AI imports

Copilot AI review requested due to automatic review settings May 25, 2026 21:02
@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dubstack Ready Ready Preview, Comment May 25, 2026 9:25pm

@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown

DubStack AI evals

Cheap-model eval status: skipped: DUBSTACK_GEMINI_API_KEY is not configured

Report artifact: workflow run

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR polishes the DubStack CLI for a v2.0-style release by adding configurable color theming, greatly expanding per-command --help content (Examples + See also), publishing new migration guides, and introducing a centralized lazy loader for AI SDK dependencies to improve cold-start performance.

Changes:

  • Added theme detection/config (auto|dark|light|none) plus a global --no-color flag and wired startup theme resolution.
  • Expanded --help output for commands/subcommands and added a test to enforce the new help shape.
  • Introduced lib/ai-deps.ts as a centralized lazy-import layer and refactored AI-using commands to depend on it; added tests for the lazy-load invariant.
  • Added/expanded migration guide MDX pages and updated the guides sidebar metadata.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/cli/src/lib/theme.ts New theme detection/resolution utilities and chalk application helpers
packages/cli/src/lib/theme.test.ts Tests for COLORFGBG parsing, theme resolution, and themed chalk instances
packages/cli/src/lib/config.ts Adds theme to config schema + normalization
packages/cli/src/lib/config.test.ts Asserts theme default/persistence/normalization
packages/cli/src/lib/ai-prompt-decision.ts Refactors prompt decision deps toward lazy AI loading
packages/cli/src/lib/ai-prompt-decision.test.ts Updates config fixture to include new theme field
packages/cli/src/lib/ai-deps.ts New centralized lazy loader for AI SDK constructors
packages/cli/src/lib/ai-deps.test.ts Tests for AI deps loader surface and caching behavior
packages/cli/src/lazy-cold-start.test.ts New test intended to assert entrypoint import doesn’t trigger AI deps load
packages/cli/src/index.ts Wires global --no-color, theme application, entrypoint guard, and expanded help text
packages/cli/src/index.help.test.ts New test enforcing Examples/See also sections in help output
packages/cli/src/commands/submit.ts Refactors AI deps usage to rely on loadAiDeps()
packages/cli/src/commands/squash.ts Refactors AI deps usage to rely on loadAiDeps()
packages/cli/src/commands/split.ts Refactors AI deps usage to rely on loadAiDeps()
packages/cli/src/commands/ready.ts Lazily resolves AI deps only when AI readiness checks are exercised
packages/cli/src/commands/mcp.ts Switches AI deps wiring to a lazy default deps resolver
packages/cli/src/commands/flow.ts Builds default deps via loadAiDeps() and composes flow dependencies
packages/cli/src/commands/create.ts Removes eager AI imports and loads deps only when AI path is taken
packages/cli/src/commands/config.ts Adds dub config theme implementation and parsing/validation
packages/cli/src/commands/ai.ts Refactors to lazy-load AI + bash-tool deps via defaultDeps()
packages/cli/src/commands/ai-resolve.ts Refactors AI deps wiring through loadAiDeps()
packages/cli/src/commands/absorb.ts Refactors AI deps usage to rely on loadAiDeps()
apps/docs/content/docs/guides/migration-from-spr.mdx New migration guide for spr users
apps/docs/content/docs/guides/migration-from-sapling.mdx New migration guide for Sapling users
apps/docs/content/docs/guides/migration-from-graphite.mdx Expanded Graphite migration guide
apps/docs/content/docs/guides/migration-from-ghstack.mdx New migration guide for ghstack users
apps/docs/content/docs/guides/migration-from-charcoal.mdx New migration guide for Charcoal users
apps/docs/content/docs/guides/meta.json Adds new migration pages to the guides sidebar
Comments suppressed due to low confidence (1)

packages/cli/src/lib/ai-prompt-decision.ts:73

  • resolveAiPromptDecision always calls loadDefaultDeps() (and therefore loadAiDeps()) before checking whether AI prompt options are enabled in config. This defeats the lazy-import goal for commands like dub sync/dub post-merge when AI prompts are disabled. Consider reading config using the non-AI deps first, returning fallbackPrompt() early when disabled, and only then awaiting loadAiDeps() to build the full deps object for the AI path.
  const deps = input.deps ?? (await loadDefaultDeps());
  const config = await deps.readConfig(input.cwd);
  if (!aiPromptOptionsEnabled(config)) {
    return input.fallbackPrompt();
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/lazy-cold-start.test.ts
Comment thread packages/cli/src/commands/submit.ts Outdated
Comment thread packages/cli/src/commands/squash.ts Outdated
Comment thread packages/cli/src/commands/split.ts Outdated
Comment thread packages/cli/src/commands/absorb.ts
Comment thread packages/cli/src/lib/theme.ts
Comment thread packages/cli/src/index.help.test.ts
Comment thread apps/docs/content/docs/guides/migration-from-charcoal.mdx
Comment thread apps/docs/content/docs/guides/migration-from-ghstack.mdx Outdated
Comment thread apps/docs/content/docs/guides/migration-from-sapling.mdx
dubscode added 3 commits May 25, 2026 14:23
…lazy AI imports

* Add `dub config theme auto|dark|light|none` with COLORFGBG-based
  auto-detection and a global --no-color flag that overrides the
  configured theme. The resolved theme controls chalk's color level at
  program startup.
* Expand every command's --help with Examples + See also sections,
  enforced by src/index.help.test.ts which walks the program tree and
  asserts both sections via outputHelp(). Required gating the
  auto-running main() so vitest can import the program for introspection.
* Publish four new migration guides (charcoal, ghstack, sapling, spr)
  and expand the existing graphite guide with a 30-minute scripted
  migration.
* Defer @ai-sdk/* + @aws-sdk/credential-providers + ai package imports
  to a central lib/ai-deps.ts loader that dynamic-imports the SDKs only
  when an AI codepath actually runs. lazy-cold-start.test.ts asserts
  the entrypoint import never triggers SDK loading.

Completes DUB-69
- submit/squash/split/absorb: defer loadAiDeps() to the AI branch so non-AI
  runs keep the fast cold-start path. submit() now passes a `getDeps` thunk
  into the PR-body helpers.
- index.ts: use realpathSync when comparing import.meta.url against
  process.argv[1] so the published `dub` bin works through pnpm/npm's
  symlinked node_modules/.bin/dub shim.
- index.help.test.ts: replace the global process.stdout.write override with
  Commander's per-command configureOutput() to avoid cross-worker races.
- lazy-cold-start.test.ts: reset the AI-deps cache BEFORE importing the
  entrypoint so the assertion is meaningful.
- lib/theme.ts: document that dark/light/auto produce identical output today
  and only `none` is observably different; the API surface is reserved for a
  future palette swap.
- migration-from-charcoal.mdx: use the real `gt` binary (both Charcoal forks
  ship as `gt`) and the right cache file names (.graphite_cache_persist).
- migration-from-ghstack.mdx: drop the fabricated "Charles" sentence and the
  unverifiable `ghstack rage` mapping.
- migration-from-sapling.mdx: drop the invented `sl debugdiff` mapping.
- migration-from-spr.mdx: spr's reviewer key is `defaultReviewers` in
  .spr.yml, not `spr config.reviewers`; spr does not install a commit-msg
  hook so don't tell users to remove one.

Refs DUB-69
The DUB-69 help-test invariant walks every command and asserts an Examples
plus See also section. The new `dub completion` and `dub man` commands
from main only had Examples, so the test failed after rebase. Add the
missing See also footers.

Refs DUB-69
@dubscode dubscode force-pushed the feature/dub-69-theming-per-command-help-expansion-migration-guides branch from 148f187 to 3ff5a53 Compare May 25, 2026 21:24
@dubscode dubscode enabled auto-merge (squash) May 25, 2026 21:26
@dubscode dubscode merged commit d3bb370 into main May 25, 2026
14 checks passed
@dubscode dubscode deleted the feature/dub-69-theming-per-command-help-expansion-migration-guides branch May 25, 2026 21:27
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.10.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants