Skip to content
Merged
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
16 changes: 12 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
## Adding or Updating CLI Commands

Before adding, updating, 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.
[Shared Command Application Architecture](design/shared.md) and
[Specify CLI Command Architecture](design/cli.md). They define the shared
operation boundary, command-module naming, private phases, nested command
groups, registration ownership, and mirrored tests.

## Adding or Updating MCP Commands

Before adding or changing MCP tools for Specify commands, read
[Shared Command Application Architecture](design/shared.md) and
[Specify MCP Command Architecture](design/mcp.md). They define the shared
operation boundary, typed contracts, explicit inventory, side-effect metadata,
the local stdio protocol boundary, and mirrored tests.

## Adding or Updating Agent Integrations

Expand Down
140 changes: 101 additions & 39 deletions design/cli.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
# 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.
This document defines the CLI adapter structure for Specify commands. It
explains where Typer handlers, CLI infrastructure, CLI-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.
Every CLI leaf that has another delivery adapter invokes the shared application
operation defined by
[Shared Command Application Architecture](shared.md). This document owns the
CLI surface; it does not redefine semantic validation, orchestration, results,
warnings, errors, or side effects.
Comment thread
Copilot marked this conversation as resolved.

The extension hierarchy supplies the reference command-group shape used in
this document. Do not create extra modules merely to make a small command
conform visually.

## Design goals

Expand All @@ -22,6 +28,8 @@ The design optimizes for:
the same file.
- **Explicit ownership:** shared infrastructure and command-private behavior
should not be mixed.
- **Adapter discipline:** CLI modules invoke shared operations rather than
becoming the application implementation.
- **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
Expand Down Expand Up @@ -50,17 +58,24 @@ Only modules representing actual CLI commands use the non-underscored

- The Typer-decorated handler.
- User-facing arguments and options.
- Command-specific orchestration.
- Mapping parsed values into the shared operation request.
- CLI prompting, progress, rendering, JSON streams, and exit-code mapping.
- Small helpers used only by that command.

Semantic validation, application orchestration, typed outcomes, warnings,
expected errors, and side effects belong below the adapter as defined in
[the shared design](shared.md). A CLI-only command may keep small behavior in
its command module, but behavior needed by another adapter must first move to a
shared operation or domain module.

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:
When a CLI adapter has cohesive *CLI-specific* phases that are independently
understandable or testable, use:

```text
_command_<name>_<phase>.py
Expand All @@ -69,26 +84,30 @@ _command_<name>_<phase>.py
For example:

```text
command_update.py
_command_update_discovery.py
_command_update_artifacts.py
_command_update_transaction.py
command_init.py
_command_init_prompting.py
_command_init_rendering.py
Comment thread
Copilot marked this conversation as resolved.
```

The leading underscore marks the module as private implementation. The
`command_update` portion groups it with the registered handler in searches and
`command_init` 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:

- Is specific to CLI invocation or presentation.
- 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.

If the phase performs semantic validation, planning, mutation, rollback, or
other behavior another adapter needs, use `_operation_<name>_<phase>.py`
instead, following [the shared design](shared.md#naming-and-layout).

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.
Expand All @@ -99,7 +118,8 @@ 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.
- CLI infrastructure genuinely shared by multiple commands or external CLI
flows.
- Thin compatibility forwarders needed to preserve established import or
monkeypatch paths.

Expand Down Expand Up @@ -178,8 +198,9 @@ re-exports the command symbols for compatibility.

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.
`extension update ...` subcommand group. Use `_command_update_<phase>.py` for
a CLI-private phase and `_operation_update_<phase>.py` for semantic validation,
planning, mutation, rollback, or other behavior shared with another adapter.

## Registration

Expand All @@ -200,6 +221,27 @@ 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.

## Shared operation invocation

The CLI adapter maps parsed arguments and options into the shared typed request,
invokes the operation, and renders its outcome.

For project-scoped commands, the adapter supplies the explicit project or
target directory. When the user omits it, the CLI uses the cwd captured when
the invocation begins. The shared operation validates and resolves that path;
neither layer changes process-wide cwd.

The CLI runs with the invoking user's ordinary operating-system permissions.
It does not construct a shared policy or filesystem runtime. Prompting remains
CLI-specific:

- `--json`, `--non-interactive`, and local invocation do not imply `force`.
- External-source trust and overwrite consent remain explicit request values.
- A prompt may collect consent before constructing or retrying the request, but
the adapter does not add consent silently.
- Network and self-modifying behavior remain those defined by the operation and
CLI contract.

## Test structure

Command-focused tests mirror the source command surface under
Expand All @@ -215,23 +257,25 @@ src/specify_cli/extensions/catalog/command_add.py
tests/specify_cli/extensions/catalog/test_command_add.py
```

Private phases use:
CLI-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
src/specify_cli/_command_init_prompting.py
tests/specify_cli/test_command_init_prompting.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.

CLI adapter tests verify explicit targets outside the launch directory and the
rule that machine modes do not add force, trust, or destructive consent.

Shared operation and phase tests use `test_operation_<name>.py` and
`test_operation_<name>_<phase>.py` as defined in
[the shared testing structure](shared.md#testing-structure). They do not move
under `test_command_*.py` merely because the CLI is one caller.

Domain source remains in the parent package's `__init__.py` or a focused
domain module without the `command_` prefix. Its mirrored tests use the domain
subject name, for example:
Expand Down Expand Up @@ -290,7 +334,8 @@ must remain represented. A matching total alone does not prove preservation.

## Reference layout

The extension command group currently demonstrates the complete pattern:
An extension command group using the shared application boundary has this
shape:

```text
src/specify_cli/extensions/
Expand All @@ -305,9 +350,10 @@ src/specify_cli/extensions/
├── command_search.py
├── command_set_priority.py
├── command_update.py
├── _command_update_discovery.py
├── _command_update_artifacts.py
├── _command_update_transaction.py
├── _operation_update.py
├── _operation_update_discovery.py
├── _operation_update_artifacts.py
├── _operation_update_transaction.py
Comment thread
Copilot marked this conversation as resolved.
└── catalog/
├── __init__.py
├── _helpers.py
Expand All @@ -319,10 +365,11 @@ src/specify_cli/extensions/
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.
- `_operation_update.py` is the shared application entry point.
- `_operation_update_discovery.py` determines available updates.
- `_operation_update_artifacts.py` prepares and validates update archives.
- `_operation_update_transaction.py` owns backup, installation, rollback, and
cleanup behavior for every adapter.

## Decision guide

Expand All @@ -331,9 +378,11 @@ 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 |
| Is it a small CLI-only mapping or rendering helper? | That command module |
| Is it a cohesive CLI-only phase? | `_command_<name>_<phase>.py` |
| Does it define semantic validation or orchestration for an operation? | Existing domain module or `_operation_<name>.py` |
| Is it a cohesive shared operation phase? | `_operation_<name>_<phase>.py` |
| Is it CLI infrastructure shared by multiple command adapters? | `_commands.py` or a focused CLI helper |
| 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 |
Expand All @@ -346,6 +395,10 @@ Avoid:
- 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.
- Keeping semantic validation, orchestration, or side effects in a CLI adapter
when another adapter exposes the same logical operation.
- Calling or parsing another delivery adapter instead of invoking the shared
operation.
- 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
Expand All @@ -359,9 +412,18 @@ 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`.
- [ ] The owning hierarchy records exactly one MCP inventory disposition for
the CLI leaf, including when it is unavailable or excluded.
- [ ] If another adapter exposes the operation, the CLI adapter maps into the
shared operation defined by `design/shared.md`.
- [ ] For a multi-adapter operation, semantic validation, orchestration, and
side effects are below the CLI adapter.
- [ ] When a shared request exists, project and target directories map into it
explicitly.
- [ ] CLI-private phase modules use `_command_<name>_<phase>.py`; shared phases
use `_operation_<name>_<phase>.py`.
- [ ] `_commands.py` contains only group infrastructure and genuinely shared
behavior.
CLI behavior.
- [ ] Nested directories correspond to real CLI namespaces.
- [ ] Command tests mirror the source structure.
- [ ] Domain and cross-domain tests remain in their appropriate suites.
Expand Down
Loading
Loading