Description
Superseded October 18, 2026 by #515 and its in-progress PR #516. The explicit scope.ts/invocation-scope-factory standardization and proposed follow-up DI-framework migration below are no longer the target architecture. The adopted direction is a cohesive Commander adapter, invocation-time token resolution, a small typed Fetch-client facade, and shared output/error utilities without per-command scope factories. Do not implement the historical requirements below; follow #515. Closing as not planned to prevent duplicate/contradictory CLI tracks.
Historical proposal (superseded)
Context
Follow-up from #435 / PR #439.
The docs command in #439 establishes the current CLI composition pattern. The code in #439 is authoritative for this issue: command modules have an explicit registration boundary and per-invocation scope, but the executable command body remains flattened into the Commander async handler rather than being split into a separate run*Command() helper.
The goal here is to normalize the remaining CLI commands to that same shape without introducing a DI framework or a second command abstraction.
Canonical command-module pattern
Executable commands should be self-contained modules under apps/iracing-data-cli/src/commands/<command>/.
Reference layout from #439:
commands/
docs/
index.ts # public registration/module boundary
command.ts # Commander adapter + command body
scope.ts # per-invocation manual composition
index.ts — public module contract
This is the only file normal application registration should import.
It owns the internal wiring between the command and its invocation scope and exposes the dependencies the module needs as one coherent factory contract. Production callers may use defaults; tests or future application composition may provide overrides through this entry point.
Expected registration shape:
import { createDocsCommand } from "./commands/docs/index.js";
program.addCommand(createDocsCommand({ diagnostics }));program.ts must not import command.ts and scope.ts separately or perform a two-step createScope + createCommand registration.
Tests may import internal files directly when specifically testing those internal boundaries.
command.ts — CLI adapter + flattened command body
Owns:
- Commander metadata/options;
- command-specific option validation and semantic interpretation;
- invocation orchestration;
- resolving exactly one invocation scope;
- the narrow command behavior itself after scope resolution.
The intended flow is:
parsed CLI options
-> create invocation scope
-> destructure narrow resolved capabilities
-> execute command behavior directly in the async handler
Do not introduce a separate run*Command() layer merely to move a few lines out of .action(). #439 intentionally flattened that indirection.
The command body must not:
- resolve credentials;
- read process environment for infrastructure configuration;
- instantiate generated/authored API clients;
- inspect generated transport exceptions;
- map raw HTTP errors;
- construct persistence/network implementations;
- receive a DI container, cradle, or broad service locator.
scope.ts — transitional manual invocation composition
Owns invocation-specific composition such as:
- credential/config resolution;
- generated/authored client construction;
- transport/generated-client error adaptation;
- output/persistence capability binding;
- command-lifetime resources and cleanup where applicable.
The command handler receives only narrow capabilities from the resolved scope and never a service locator/container.
Important: this is transitional manual DI
The scope.ts pattern is deliberately a stepping stone, not the intended permanent DI framework.
The sequencing is:
- use CLI: move Data API docs retrieval behind typed client boundary #439 as the reference implementation;
- migrate the remaining CLI commands to the same explicit module/scope boundaries;
- ship and verify that behavior;
- immediately follow with a dedicated DI-framework implementation/evaluation (Awilix is a candidate) that can replace the manual
scope.tsmechanics without rewriting command behavior or module registration.
Do not allow the manual scope pattern to grow into a long-lived bespoke DI framework.
Specifically, do not introduce:
- generic command runners;
- a generic
CommandScopeFactory<T>; - a shared service registry;
- a global container;
- broad service bags;
- generic abstractions merely because two scopes contain similar code.
A future DI implementation should preserve this external shape:
program.ts
-> register command module
command module index
-> wire command + invocation scope
command handler
-> resolve one invocation scope
-> consume narrow capabilities only
The container/cradle must terminate at the command-module orchestration boundary.
Inventory
docs — pilot / handled by #439
Target/current structure:
commands/docs/
index.ts
command.ts
scope.ts
index.tsis the only registration surface consumed byprogram.ts;command.tsparses options, resolves one invocation scope, then directly fetches/writes documentation and records diagnostics;scope.tsowns credential resolution, generatedDocApiconstruction, generated-client error normalization, and output-policy binding;- no separate
runDocsCommand()helper is required; - no Awilix dependency is introduced.
This live implementation is the reference pattern for the remaining migrations.
whoami — reconcile with #383 / PR #385
Target structure:
commands/whoami/
index.ts
command.ts
scope.ts
Requirements:
program.tsimports onlycommands/whoami/index.ts;index.tsowns internal command/scope wiring and exposes the module dependency contract;scope.tsresolves credentials and the profile capability;- OAuth/generated-client error adaptation is outside the command body;
- the Commander handler receives only the resolved identity/output/diagnostics capabilities and performs the narrow profile/output behavior directly;
- command code does not construct
OAuthApiClient, inspect transport errors, or resolve access tokens; - do not add a
runWhoamiCommand()indirection solely for structural symmetry.
auth login — nested executable module
Target structure:
commands/auth/
index.ts
login/
index.ts
command.ts
scope.ts
Command-specific behavior may remain in command.ts, including:
--output/--credentialscompatibility validation;- OAuth scope parsing/validation;
- interpreting command-specific flags.
Infrastructure/composition belongs in scope.ts, including:
- OAuth client ID/secret/redirect URI lookup;
- browser authentication implementation binding;
- credential persistence/output implementation binding;
- concrete network/filesystem dependencies.
After scope resolution, the Commander handler may directly authenticate, persist credentials, and emit success diagnostics. Do not add a separate runLoginCommand() layer merely to wrap those calls.
auth parent command — structural parent only
commands/auth/index.ts is a structural Commander group that creates auth and registers login. It has no independent runtime operation.
Keep it thin. Do not invent an invocation scope for it.
program.ts — registration root
The top-level program should remain intentionally boring:
program.addCommand(createAuthCommand({ diagnostics }));
program.addCommand(createDocsCommand({ diagnostics }));
program.addCommand(createWhoamiCommand({ diagnostics }));It must not accumulate command-internal knowledge such as scope factories, credential factories, generated-client construction, or transport/error adapters.
Testing
Preserve behavior while moving ownership boundaries.
Prefer:
- module-level tests through
index.tsproving composition; - command tests with an explicit fake
createScopeand narrow capabilities; - direct scope tests with explicit dependency overrides;
createProgram()integration tests where useful.
Avoid:
- raw
fetchas the principal command seam; - module mocking where explicit dependency substitution exists;
- broad fake service bags;
- tests for artificial
run*Command()helpers that do not exist in the production architecture.
Keep tests deterministic and offline.
Error ownership
Generated-client and transport errors should not be interpreted repeatedly by command bodies.
Place safe CLI/application adaptation at the scope/application boundary and preserve security requirements:
- do not expose bearer/access/refresh tokens;
- do not expose upstream response bodies;
- do not log arbitrary upstream exception text;
- preserve actionable fixed/status-based recovery messages.
Acceptance criteria
- Reconcile this inventory against current
main, CLI: move Data API docs retrieval behind typed client boundary #439, Generate OAuth API client and replace ad hoc CLI HTTP calls #385, and other open CLI work before implementation. - CLI: move Data API docs retrieval behind typed client boundary #439 establishes the authoritative
commands/<name>/{index,command,scope}.tsreference pattern. - Normal runtime consumers import each command through its
index.tsmodule boundary only. -
program.tsdoes not perform two-step command/scope registration or import internalscope.tsfiles. - Executable command handlers resolve one invocation scope and perform their narrow command behavior directly.
- No unnecessary
run*Command()indirection is introduced. -
whoamiis migrated after reconciling the delivered Generate OAuth API client and replace ad hoc CLI HTTP calls #385 OAuth-client shape. -
auth loginis migrated to the same module/scope pattern while preserving credential-lifecycle behavior. - Structural parent commands such as
authremain thin and do not receive meaningless scopes. - Generated-client/transport error normalization is owned outside command bodies.
- Credential/environment/config resolution is owned outside command bodies unless a value is genuinely command-domain behavior.
- Tests substitute module dependencies/scopes/capabilities directly rather than raw
fetchor broad service bags. - No command receives a DI container/cradle/service locator.
- No DI framework or generic scope framework is introduced by this issue.
- Relevant CLI tests and repository verification pass.
- After these migrations ship, create/pick up a dedicated DI-framework follow-up that replaces manual scope mechanics while preserving module registration and flattened command behavior.
Suggested delivery
- complete the
docspilot in CLI: move Data API docs retrieval behind typed client boundary #439; - reconcile/migrate
whoamiagainst Generate OAuth API client and replace ad hoc CLI HTTP calls #385; - migrate
auth login; - verify the normalized command-module boundary across the CLI;
- immediately follow with the DI-framework implementation/evaluation rather than evolving the manual scopes into a bespoke container.
Pickup status
In progress. #442 is stacked on #439 and should preserve #439's live flattened command-body architecture while normalizing the remaining command modules.
Activity
justinmakaila commentedon Oct 9, 2026
Implementation is in draft PR #442 from
issue-440-cli-command-modules, branched from currentmainat166425169f5ece35da2fd810d7fd4fb0cfe8e1ce.Pickup reconciliation changed two assumptions in the issue inventory:
docs/{index,command,scope}.tspattern forward directly and keepsprogram.tsat the module boundary.whoamicommand/module/scope contract against currentmainand isolates the current profile transport behind the scope's narrow identity capability; Generate OAuth API client and replace ad hoc CLI HTTP calls #385's generated/authored OAuth client can later replace that concrete scope adapter without changingprogram.tsor the command implementation.The open-PR scan found no other PRs touching
apps/iracing-data-clibesides #439 and #385.auth loginis also split into nestedindex.ts/command.ts/scope.ts; theauthparent remains structural only. No DI framework or generic scope abstraction was introduced.Verification is running on #442 before it is marked ready.
refactor(cli): normalize command module composition
refactor(cli): normalize remaining command modules
refactor(cli): normalize remaining command modules
refactor(cli): normalize remaining command modules
[-]CLI: standardize command invocation scopes and keep implementations dependency-resolved[/-][+]Superseded: standardize CLI command invocation scopes[/+]