Skip to content

Latest commit

 

History

History
401 lines (313 loc) · 17.4 KB

File metadata and controls

401 lines (313 loc) · 17.4 KB

cli-core-yo — Implementation Plan

Derived from SPEC.md (normative). This plan covers phases, module order, dependencies, testing, risk, and validation checkpoints.


1. Implementation Phases

Phase 0 — Project Scaffolding

Item Detail
Goal Buildable, installable Python package skeleton
Outputs pyproject.toml, cli_core_yo/__init__.py, .gitignore, empty test scaffold
Risk Low

Tasks:

  • pyproject.toml with setuptools_scm (per global rule 06_github.md), pinned deps:
    • typer>=0.21.0,<0.22.0
    • rich>=14.0.0,<15.0.0
    • click>=8.3.0,<9.0.0
  • cli_core_yo/__init__.py with version import (from _version.py generated by setuptools_scm)
  • .gitignore (egg-info, _version.py, __pycache__, dist/, build/, .eggs/)
  • tests/ directory with conftest.py
  • Validate: pip install -e . succeeds, python -c "import cli_core_yo" succeeds

Checkpoint: Package installs in editable mode. import cli_core_yo works.


Phase 1 — Foundation Modules (No Cross-Dependencies)

Item Detail
Goal Pure dataclass and exception definitions
Modules errors.py, spec.py
Risk Low — pure data, no I/O

1a. cli_core_yo/errors.py

  • Define base CliCoreYoError(Exception)
  • ContextNotInitializedError — raised when get_context() called before init
  • RegistryFrozenError — raised on post-freeze registration
  • RegistryConflictError — raised on name collision
  • PluginLoadError — raised on import/registration failure
  • Map each to exit codes per §2.6

1b. cli_core_yo/spec.py

Immutable dataclasses (all frozen=True):

  • XdgSpec: app_dir_name: str, legacy_macos_config_dir: str | None, legacy_copy_files: list[str]
  • ConfigSpec: primary_filename: str, template_bytes: bytes | None, template_resource: tuple[str, str] | None, validator: Callable | None
  • EnvSpec: active_env_var: str, project_root_env_var: str, activate_script_name: str, deactivate_script_name: str
  • PluginSpec: explicit: list[str], entry_points: list[str]
  • CliSpec: prog_name: str, app_display_name: str, dist_name: str, root_help: str, xdg: XdgSpec, config: ConfigSpec | None, env: EnvSpec | None, plugins: PluginSpec, info_hooks: list[Callable]

Checkpoint: from cli_core_yo.spec import CliSpec works. Dataclasses freeze correctly.

Tests:

  • Frozen dataclass immutability
  • Required field enforcement
  • Default values

Phase 2 — Core Infrastructure

Item Detail
Goal XDG resolution, runtime context, output primitives
Modules xdg.py, runtime.py, output.py
Risk Medium — platform-specific XDG logic, Rich rendering

2a. cli_core_yo/xdg.py

  • resolve_paths(xdg_spec: XdgSpec) -> XdgPaths returning config/data/state/cache Path objects
  • Platform detection: macOS vs Linux for data/state/cache defaults (§6.4)
  • mkdir(parents=True, exist_ok=True) on resolution
  • Legacy macOS migration: copy listed files if legacy exists and XDG target does not
  • Preserve file metadata on copy (shutil.copy2)

Tests:

  • Env var overrides (XDG_CONFIG_HOME, etc.)
  • macOS defaults vs Linux defaults (mock sys.platform)
  • Legacy migration: copy occurs, idempotent, metadata preserved
  • Directory creation

2b. cli_core_yo/runtime.py

  • RuntimeContext dataclass: spec: CliSpec, xdg_paths: XdgPaths, json_mode: bool, debug: bool
  • Module-level _context: RuntimeContext | None = None
  • initialize(spec, json_mode, debug) -> RuntimeContext — sets module-level, raises if already set
  • get_context() -> RuntimeContext — raises ContextNotInitializedError if not set
  • _reset() — test-only reset

Tests:

  • Init once, get succeeds
  • Get before init raises
  • Double init raises
  • _reset() allows re-init

2c. cli_core_yo/output.py

  • Console wrapper: uses Rich.Console with stderr=False, respects NO_COLOR
  • Primitives (§6.2): heading(title), success(msg), warning(msg), error(msg), action(msg), detail(msg), bullet(msg)
  • emit_json(data: Any)json.dumps(data, indent=2, sort_keys=True, ensure_ascii=False) + \n, bypasses Rich
  • All primitives check get_context().json_mode and suppress if True
  • Color mapping per §6.1

Tests:

  • Each primitive produces correct prefix/markup
  • JSON mode suppresses human output
  • emit_json produces valid, deterministic JSON
  • NO_COLOR suppresses ANSI codes

Checkpoint: XDG paths resolve. Context initializes. Output primitives render correctly.


Phase 3 — Command Registry

Item Detail
Goal Registration, validation, conflict detection, freeze semantics
Module registry.py
Risk Medium — ordering and conflict logic
  • CommandRegistry class
    • Internal storage: ordered dict of registered nodes
    • add_group(name, help, order) — validates name regex ^[a-z][a-z0-9-]*$
    • add_command(group_path, name, callable, help, order) — validates name, checks conflicts (§4.3)
    • add_typer_app(group_path, typer_app, name, help, order) — registers a full Typer sub-app
    • freeze() — prevents further mutations
    • apply(app: typer.Typer) — materializes registrations onto the Typer app in deterministic order
  • Conflict rules per §4.3:
    • Group+Group: help must match or new help empty

Phase 4 — Plugin Loading

Item Detail
Goal Deterministic plugin discovery, import, invocation
Module plugins.py
Risk Medium — import machinery, entry points API
  • load_plugins(spec: CliSpec, registry: CommandRegistry) -> None
  • Step 1: Load explicit plugins (spec.plugins.explicit) — importlib.import_module + getattr to resolve dotted path to callable
  • Step 2: Load entry-point plugins (spec.plugins.entry_points) — importlib.metadata.entry_points(group="cli_core_yo.plugins"), filter to names in spec list, load in spec list order
  • Each callable invoked as callable(registry, spec)
  • On import failure or exception during registration: raise PluginLoadError with plugin name and original exception

Tests:

  • Explicit plugin loads and registers commands
  • Entry-point plugin loads (mock importlib.metadata)
  • Explicit before entry-point ordering
  • Import failure → PluginLoadError
  • Exception during registration → PluginLoadError
  • Each callable invoked exactly once

Checkpoint: Plugins load deterministically. Failures surface with clear messages.


Phase 5 — App Factory and Built-In Commands

Item Detail
Goal Complete create_app / run API, built-in commands, optional groups
Module app.py (plus command implementations)
Risk High — integrates everything; Typer API surface

5a. create_app(spec: CliSpec) -> typer.Typer

Startup sequence (§3.5):

  1. Validate CliSpec (non-empty required fields, valid regex for names)
  2. Construct root Typer app: name=spec.prog_name, help=spec.root_help, add_completion=True, no_args_is_help=True, no -h alias
  3. Initialize RuntimeContext
  4. Register built-in commands: version, info
  5. If spec.config not None → register config group (§4.6)
  6. If spec.env not None → register env group (§4.7)
  7. Load plugins via plugins.load_plugins()
  8. Freeze registry
  9. Apply registry to Typer app

5b. run(spec: CliSpec, argv: list[str] | None) -> int

  • Calls create_app(spec), then invokes Typer
  • Catches exceptions → maps to exit codes (§2.6)
  • MUST NOT call sys.exit()
  • Debug mode (§6.6): CLI_CORE_YO_DEBUG=1 → traceback to STDERR
  • SIGINT/KeyboardInterrupt → exit 130

5c. Built-in version command

  • <prog> version prints <app_display_name> <app_version> (§2.5)
  • Version resolved from importlib.metadata.version(spec.dist_name)
  • Cyan styling in human mode
  • Exit 0

5d. Built-in info command

  • Prints two-column table (§6.3): Version, Python, Config Dir, Data Dir, State Dir, Cache Dir, CLI Core
  • CLI Core version from importlib.metadata.version("cli-core-yo")
  • Invokes spec.info_hooks in order, appending rows
  • Exit 0

5e. Built-in config group (optional, §4.6)

Subcommands: path, init, show, validate, edit, reset

Command Key behavior
config path Print resolved config file path, exit 0
config init Create file from template; --force to overwrite; exit 1 if exists without --force
config show Print file contents; exit 1 if missing
config validate Run ConfigSpec.validator; exit 0 pass / exit 1 fail
config edit Open in editor (§6.5: VISUALEDITORvi); exit 1 if not interactive
config reset Backup (timestamped), replace with template; --yes to skip confirmation

5f. Built-in env group (optional, §4.7)

Subcommands: status, activate, deactivate, reset

Command Key behavior
env status Check EnvSpec.active_env_var; print active state + key paths; exit 0
env activate Print shell source command for activation script; exit 0
env deactivate Print shell source command for deactivation script; exit 0
env reset Print deactivate then activate sequence; exit 0

Project root detection: env var → upward pyproject.toml search from CWD.

Tests (Phase 5):

  • create_app returns a typer.Typer with correct settings
  • run returns exit codes without calling sys.exit()
  • version output format and exit code
  • info output contains all required rows
  • info hooks append extra rows
  • config path prints path
  • config init creates file, respects --force
  • config show prints contents, errors on missing
  • config validate runs validator
  • config edit launches editor
  • config reset creates backup, replaces
  • env status reports active/inactive
  • env activate/deactivate/reset print correct shell commands
  • No-args → help → exit 0
  • Unknown command → exit 2
  • --help at every level → exit 0
  • SIGINT → exit 130
  • Debug mode → traceback on STDERR

Checkpoint: Full CLI functional. All §2 contract behaviors pass. All §4.6/4.7 commands work.


Phase 6 — Integration Tests and Behavioral Equivalence

Item Detail
Goal End-to-end CLI tests via CliRunner, migration validation
Risk Low — test-only phase
  • All tests use typer.testing.CliRunner
  • Construct a test CliSpec with known values
  • Test matrix:
    • Root help output (§2.4)
    • Global flags: --help, --install-completion, --show-completion
    • No -h at root level
    • Command ordering in help matches registration order (§4.5)
    • JSON output: valid, sorted keys, indent 2, UTF-8, trailing newline (§2.8)
    • Exit codes: 0 (success), 1 (failure), 2 (usage error), 130 (interrupt)
    • NO_COLOR suppresses ANSI
    • CLI_CORE_YO_DEBUG=1 → traceback on STDERR
  • Plugin integration test: register a mock plugin, verify commands appear

Checkpoint: All SPEC §2, §4, §6 behaviors verified by automated tests.


2. Module Dependency Graph

errors.py  ←── (no deps)
spec.py    ←── (no deps)
xdg.py     ←── spec.py
runtime.py ←── spec.py, xdg.py
output.py  ←── runtime.py
registry.py ←── errors.py, spec.py
plugins.py  ←── registry.py, spec.py, errors.py
app.py      ←── spec.py, runtime.py, xdg.py, output.py, registry.py, plugins.py, errors.py

Implementation order follows a topological sort of this graph.


3. Risk Assessment

Module Complexity Risk Mitigation
spec.py Low Low Pure dataclasses, no logic
errors.py Low Low Simple exception hierarchy
xdg.py Medium Medium Platform-specific defaults; test with mocked sys.platform
runtime.py Low Low Module-level singleton; straightforward
output.py Medium Medium Rich Console integration; NO_COLOR and JSON mode interactions
registry.py Medium Medium Conflict rules require careful testing of all §4.3 branches
plugins.py Medium Medium Entry-point API version differences (importlib.metadata); mock in tests
app.py High High Integrates all modules; Typer internals for -h suppression, completion, help rendering
config group Medium Medium File I/O, editor launching, backup logic
env group Low-Medium Low Mostly printing shell commands; project root detection needs upward search

4. Testing Strategy Summary

Phase Test Type Tool Coverage Target
0 Smoke pip install -e . + import Package builds
1 Unit pytest All dataclass fields, exception types
2 Unit pytest XDG resolution, context lifecycle, output primitives
3 Unit pytest All conflict branches, freeze, ordering
4 Unit pytest (mocked imports) Plugin load/fail paths
5 Unit + Integration pytest + CliRunner Every CLI command and subcommand, exit codes
6 Integration pytest + CliRunner Full behavioral equivalence matrix

All tests run with pytest -q and pytest --cov=cli_core_yo.


5. Migration Validation Checkpoints (§7)

These are verified in Phase 6 but documented here for traceability:

  1. create_app returns correct Typer appadd_completion=True, no_args_is_help=True, rich help
  2. version output<app_display_name> <version> format, cyan in human mode
  3. info output — all §6.3 base rows present
  4. Command ordering — built-in first, then downstream in registration order
  5. JSON outputjson.loads() succeeds, sorted keys, indent 2
  6. Exit codes — 0/1/2/130 match §2.6
  7. -h not reserved globally — downstream can use -h for own options
  8. Plugin loading — explicit before entry-point, deterministic order
  9. Registry freeze — post-freeze registration raises error
  10. Conflict detection — all §4.3 branches tested

6. Clarifying Questions / Ambiguities

After thorough review of SPEC.md §1–§9, the following items are noted:

# Item Assessment
1 CliSpec.info_hooks signature — §6.3 says "each callable returns a list of (key, value) rows" but does not specify the callable's input signature. Assumption: Callable[[], list[tuple[str, str]]] (no arguments). This is the simplest contract and the spec doesn't mention any arguments being passed. Will implement this way unless directed otherwise.
2 ConfigSpec.template_resource — §4.6 says "template_bytes OR template_resource (exactly one is non-null)" but does not define template_resource format. Assumption: tuple[str, str] representing (package_name, resource_name) for importlib.resources. Will validate exactly one is non-null at CliSpec validation time.
3 config validate with null validator — §4.6 says "Runs validation using the downstream-provided validator hook." If ConfigSpec.validator is None, behavior is unspecified. Assumption: Print a success message ("No validator configured") and exit 0.
4 env activate / env deactivate shell command format — §4.7 says "Prints a shell command instructing the user to source the activation script" but does not specify the exact format. Assumption: Print source <path> where path is constructed from project root + script name. Compatible with bash and zsh per §2.9.
5 env status "key paths" content — §4.7 says "Prints active state and key paths (project root, python path, config dir)." "python path" is ambiguous (sys.executable? sys.path? PYTHONPATH?). Assumption: sys.executable (the Python interpreter path). This is the most useful single value and matches common CLI info patterns.
6 Help ordering enforcement via Typer — §4.5 requires registration-order listing. Typer sorts commands alphabetically by default. Mitigation: Use rich_help_panel or explicit ordering via Typer's internal _commands dict manipulation, or set rich_markup_mode="rich" and manage panel order. Will investigate Typer internals during Phase 5 to find the cleanest approach.
7 run() return type — §3.3 says "Returns the integer exit code" and "MUST NOT call sys.exit() internally." Typer's app() calls sys.exit() by default. Mitigation: Use standalone_mode=False when invoking Typer, catch Click's SystemExit, and return the exit code.

None of these are blocking. All have reasonable default interpretations consistent with the spec.


7. File Inventory (Post-Implementation)

cli-core-yo/
├── pyproject.toml
├── .gitignore
├── LICENSE
├── README.md
├── SPEC.md
├── SPEC_IMPLEMENTATION_PLAN.md
├── cli_core_yo/
│   ├── __init__.py
│   ├── spec.py
│   ├── errors.py
│   ├── xdg.py
│   ├── runtime.py
│   ├── output.py
│   ├── registry.py
│   ├── plugins.py
│   └── app.py
└── tests/
    ├── conftest.py
    ├── test_spec.py
    ├── test_errors.py
    ├── test_xdg.py
    ├── test_runtime.py
    ├── test_output.py
    ├── test_registry.py
    ├── test_plugins.py
    ├── test_app.py
    ├── test_config_group.py
    ├── test_env_group.py
    └── test_integration.py