Skip to content

Superseded: standardize CLI command invocation scopes #440

Closed as not planned

Description

@justinmakaila

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:

  1. use CLI: move Data API docs retrieval behind typed client boundary #439 as the reference implementation;
  2. migrate the remaining CLI commands to the same explicit module/scope boundaries;
  3. ship and verify that behavior;
  4. immediately follow with a dedicated DI-framework implementation/evaluation (Awilix is a candidate) that can replace the manual scope.ts mechanics 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.ts is the only registration surface consumed by program.ts;
  • command.ts parses options, resolves one invocation scope, then directly fetches/writes documentation and records diagnostics;
  • scope.ts owns credential resolution, generated DocApi construction, 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.ts imports only commands/whoami/index.ts;
  • index.ts owns internal command/scope wiring and exposes the module dependency contract;
  • scope.ts resolves 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 / --credentials compatibility 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.ts proving composition;
  • command tests with an explicit fake createScope and narrow capabilities;
  • direct scope tests with explicit dependency overrides;
  • createProgram() integration tests where useful.

Avoid:

  • raw fetch as 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}.ts reference pattern.
  • Normal runtime consumers import each command through its index.ts module boundary only.
  • program.ts does not perform two-step command/scope registration or import internal scope.ts files.
  • Executable command handlers resolve one invocation scope and perform their narrow command behavior directly.
  • No unnecessary run*Command() indirection is introduced.
  • whoami is migrated after reconciling the delivered Generate OAuth API client and replace ad hoc CLI HTTP calls #385 OAuth-client shape.
  • auth login is migrated to the same module/scope pattern while preserving credential-lifecycle behavior.
  • Structural parent commands such as auth remain 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 fetch or 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

  1. complete the docs pilot in CLI: move Data API docs retrieval behind typed client boundary #439;
  2. reconcile/migrate whoami against Generate OAuth API client and replace ad hoc CLI HTTP calls #385;
  3. migrate auth login;
  4. verify the normalized command-module boundary across the CLI;
  5. 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

  1. justinmakaila commented on Oct 9, 2026

    @justinmakaila
    ContributorAuthor

    Implementation is in draft PR #442 from issue-440-cli-command-modules, branched from current main at 166425169f5ece35da2fd810d7fd4fb0cfe8e1ce.

    Pickup reconciliation changed two assumptions in the issue inventory:

    The open-PR scan found no other PRs touching apps/iracing-data-cli besides #439 and #385.

    auth login is also split into nested index.ts / command.ts / scope.ts; the auth parent remains structural only. No DI framework or generic scope abstraction was introduced.

    Verification is running on #442 before it is marked ready.

  2. changed the title [-]CLI: standardize command invocation scopes and keep implementations dependency-resolved[/-] [+]Superseded: standardize CLI command invocation scopes[/+] on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions