Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@

The toolkit supports multiple AI coding assistants, allowing teams to use their preferred tools while maintaining consistent project structure and development practices.

## Repository Design References

Before adding or reorganizing Specify CLI commands, read
[Specify CLI Command Architecture](design/cli.md). It defines command-module
naming, private command phases, nested command groups, registration ownership,
mirrored tests, and the rationale for making the CLI structure predictable for
both humans and coding agents.

---

## Quickstart — Add a New Integration in 5 Steps
Expand Down
303 changes: 303 additions & 0 deletions design/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
# Specify CLI Command Architecture

This document defines the target structure for multi-command groups in the
Specify Python CLI. It explains where command handlers, shared infrastructure,
command-private phases, nested command groups, and their tests belong.

`src/specify_cli/extensions/` is the reference implementation. Apply this
design incrementally when adding or refactoring other command groups; do not
create extra modules merely to make a small command conform visually.

## Design goals

The CLI structure should make the answer to "where does this command live?"
predictable from the command line itself.

The design optimizes for:

- **Direct navigation:** a command maps to an obvious source file and test.
- **Small working context:** changing one command should not require loading an
entire command group into memory.
- **Parallel development:** unrelated commands should rarely require edits to
the same file.
- **Explicit ownership:** shared infrastructure and command-private behavior
should not be mixed.
- **Stable behavior:** structural refactoring must preserve registration,
output, error handling, compatibility paths, and tests.
- **Agentic development:** coding agents should be able to infer the relevant
files from the CLI surface without broad repository searches.

## Naming and ownership

### Registered command modules

Each real CLI command uses:

```text
command_<name>.py
```

For example:

```text
specify extension add -> extensions/command_add.py
specify extension set-priority -> extensions/command_set_priority.py
specify extension update -> extensions/command_update.py
```

Only modules representing actual CLI commands use the non-underscored
`command_*.py` prefix. A command module owns:

- The Typer-decorated handler.
- User-facing arguments and options.
- Command-specific orchestration.
- Small helpers used only by that command.

The command function's docstring is user-facing because Typer may display it
as help text. A module docstring is internal and should identify the command,
registration path, and any adjacent private implementation modules.

### Command-private implementation modules

When a command has cohesive phases that are independently understandable or
testable, use:

```text
_command_<name>_<phase>.py
```

For example:

```text
command_update.py
_command_update_discovery.py
_command_update_artifacts.py
_command_update_transaction.py
```

The leading underscore marks the module as private implementation. The
`command_update` portion groups it with the registered handler in searches and
file listings. The phase suffix communicates its ownership.

Private phase modules must not register additional CLI commands. The public
`command_<name>.py` module remains the sole CLI adapter.

Split a command when a phase:

- Has distinct invariants or failure behavior.
- Can be tested as a meaningful boundary.
- Has enough implementation detail to distract from the CLI handler.
- Is likely to change independently from other phases.

Do not split a command solely because it crossed an arbitrary line count.
Excessive fragmentation makes control flow harder to follow and increases the
number of files an agent must inspect.

### Command-group infrastructure

For a multi-command group, `_commands.py` owns:

- The command group's Typer application.
- Registration of the group's command modules.
- Infrastructure genuinely shared by multiple commands or external CLI flows.
- Thin compatibility forwarders needed to preserve established import or
monkeypatch paths.

`_commands.py` must not contain decorated command handlers. A helper used by
only one command belongs in that command's module or one of its private phase
modules.

Compatibility forwarders do not transfer ownership back to `_commands.py`.
They should remain thin and delegate to the module that owns the behavior.
Avoid turning `_commands.py` into a service locator for new code.

### Package `__init__.py`

The package `__init__.py` owns the package's domain API and package-level
behavior. It should provide a brief map to the CLI modules, but it is not the
home for command handlers.

Moving command handlers out of `__init__.py` keeps importing the domain package
separate from understanding or modifying its CLI surface.

## Nested command groups

Nested CLI groups use directories matching the command surface:

```text
specify extension catalog add
list
remove
```

maps to:

```text
extensions/
├── catalog/
│ ├── __init__.py
│ ├── _helpers.py
│ ├── command_add.py
│ ├── command_list.py
│ └── command_remove.py
├── command_add.py
├── command_list.py
└── ...
```

The nested package's `__init__.py` owns its Typer application and registration.
Shared helpers for that nested surface can live in `_helpers.py`.

Do not add a nested `_commands.py` merely for symmetry. Create one only when
the nested group develops substantial shared command infrastructure that no
longer fits cleanly in `__init__.py` and `_helpers.py`.

Do not create a nested directory for an implementation phase that is not a CLI
subcommand. For example, an `update/` directory would incorrectly suggest an
`extension update ...` subcommand group. Use `_command_update_<phase>.py`
instead.

## Registration

Command registration remains centralized at the command-group boundary.

For the extension group:

1. `src/specify_cli/extensions/_commands.py` owns `extension_app`.
2. `_commands.register()` registers the nested catalog group.
3. It imports each `command_*.py` module so its decorator registers the
handler.
4. It attaches `extension_app` to the root application.

The nested catalog group follows the same pattern through
`catalog.register()`.

Registration imports should be explicit and ordered consistently. Do not rely
on filesystem discovery to import arbitrary modules, because command exposure
should remain reviewable in one place.

## Test structure

Command-focused tests mirror the source command surface under
`tests/specify_cli/`.

For example:

```text
src/specify_cli/extensions/command_add.py
tests/specify_cli/extensions/test_command_add.py

src/specify_cli/extensions/catalog/command_add.py
tests/specify_cli/extensions/catalog/test_command_add.py
```

Private phases use:

```text
src/specify_cli/extensions/_command_update_discovery.py
tests/specify_cli/extensions/test_command_update_discovery.py

src/specify_cli/extensions/_command_update_artifacts.py
tests/specify_cli/extensions/test_command_update_artifacts.py

src/specify_cli/extensions/_command_update_transaction.py
tests/specify_cli/extensions/test_command_update_transaction.py
```

The primary `test_command_<name>.py` suite verifies the public command surface.
Phase-specific suites verify detailed invariants without obscuring the primary
command behavior.

Not every test belongs in the mirrored command tree:

- Domain model, registry, manager, and catalog behavior remains in domain test
suites such as `tests/test_extensions.py`.
- Cross-domain CLI contracts remain with the broader integration tests.
- Shared fixtures belong in the narrowest `conftest.py` that serves all of
their consumers.
- Test helpers should be shared rather than copied when both command and domain
tests depend on the same behavior.

Moving tests must preserve coverage rather than duplicating it. Run both the
new command-focused suites and the legacy suites from which tests were moved.

## Reference layout

The extension command group currently demonstrates the complete pattern:

```text
src/specify_cli/extensions/
├── __init__.py
├── _commands.py
├── command_add.py
├── command_disable.py
├── command_enable.py
├── command_info.py
├── command_list.py
├── command_remove.py
├── command_search.py
├── command_set_priority.py
├── command_update.py
├── _command_update_discovery.py
├── _command_update_artifacts.py
├── _command_update_transaction.py
└── catalog/
├── __init__.py
├── _helpers.py
├── command_add.py
├── command_list.py
└── command_remove.py
```

The update command illustrates the distinction:

- `command_update.py` is the registered CLI adapter.
- `_command_update_discovery.py` determines available updates.
- `_command_update_artifacts.py` prepares and validates update archives.
- `_command_update_transaction.py` owns backup, installation, rollback, and
cleanup behavior.

## Decision guide

When deciding where code belongs:

| Question | Location |
|---|---|
| Does it define a real CLI command? | `command_<name>.py` |
| Is it used only by one small command? | That command module |
| Is it a cohesive private phase of one complex command? | `_command_<name>_<phase>.py` |
| Is it shared by multiple commands or an external CLI flow? | `_commands.py` or a focused shared module |
| Does it define a nested CLI namespace? | A directory matching that namespace |
| Is it shared only by commands in a nested namespace? | The nested package's `_helpers.py` |
| Is it domain behavior independent of the CLI? | The package domain modules, not command modules |

## Anti-patterns

Avoid:

- Adding decorated handlers back to `_commands.py` or package `__init__.py`.
- Naming a private implementation module `command_*.py`.
- Creating nested directories that do not correspond to CLI namespaces.
- Creating `_commands.py` files only for visual symmetry.
- Moving command-private helpers into shared infrastructure preemptively.
- Duplicating fixtures or helpers to make tests appear more mirrored.
- Splitting a linear function into many files without cohesive phase
boundaries.
- Changing established monkeypatch or import paths without either migrating
their consumers or preserving a thin compatibility forwarder.

## Review checklist

For a new or refactored command:

- [ ] The CLI path maps predictably to a `command_<name>.py` module.
- [ ] Only the real command module registers a handler.
- [ ] Private phase modules use `_command_<name>_<phase>.py`.
- [ ] `_commands.py` contains only group infrastructure and genuinely shared
behavior.
- [ ] Nested directories correspond to real CLI namespaces.
- [ ] Command tests mirror the source structure.
- [ ] Domain and cross-domain tests remain in their appropriate suites.
- [ ] Compatibility paths and user-visible help remain unchanged unless the
change explicitly requires otherwise.
- [ ] Focused tests, relevant legacy suites, lint, and the full test suite pass.
4 changes: 4 additions & 0 deletions src/specify_cli/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
Handles installation, removal, and management of Spec Kit extensions.
Extensions are modular packages that add commands and functionality to spec-kit
without bloating the core framework.

CLI handlers live in ``command_*.py`` modules, registered through
``_commands.py``. Command-private phases use ``_command_<name>_*.py``;
nested catalog handlers live under ``catalog/``.
"""

from __future__ import annotations
Expand Down
Loading
Loading