Skip to content

Commit ffb10a9

Browse files
committed
Fold per-command run-logic into commands/<cmd>/ packages
Each command that owns private run-logic becomes a package instead of a flat module plus a sibling *_exec.py at the package root: __init__.py holds the Typer app + SPEC, and its support modules sit beside it underscore- prefixed (_exec.py for the run_<cmd> body, plus _select/_data/_hf_api/_listen helpers). This colocates a command's exec with the command file and follows the Prefect/spaCy convention (flat file by default; promote to a folder only when a command has earned multiple modules; underscore-prefix private modules, which also dodges the webhooks `listen` command-vs-module name collision). 13 commands folded: stream, agent, speak, llm, clip, dictate, caption, dub, evaluate, deploy, dev, share, webhooks. transcribe and init stay at the root alongside doctor_checks/setup_exec because the onboarding wizard (onboard/sections.py) reuses transcribe_exec/render/batch and init_exec, so they are shared beyond a single command. command_registry already discovers packages (pkgutil.iter_modules), so registration, help ordering, and the snapshot partition are unchanged. import-linter contract 1 drops the folded execs (now covered by contract 2's independence rule) and contract 3 points at the new _select/_data paths; all three contracts stay green. AGENTS.md documents the module-or-package rule. https://claude.ai/code/session_01XfcMSCxZPLJgg2UQBy8a4v
1 parent 8e39d2a commit ffb10a9

63 files changed

Lines changed: 264 additions & 211 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.importlinter

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,27 @@ include_external_packages = True
55
[importlinter:contract:1]
66
name = Core modules do not import command modules
77
type = forbidden
8+
; A command's private run-logic now lives inside its own package
9+
; (aai_cli/commands/<cmd>/_exec.py, …) and is governed by contract 2's
10+
; independence rule, so those modules are intentionally absent here. The
11+
; *_exec modules that remain are the ones still at the package root because
12+
; they're shared beyond their command (transcribe_exec/render/batch and
13+
; init_exec are reused by the onboarding wizard; setup_exec/doctor_checks too).
814
source_modules =
915
aai_cli.agent
10-
aai_cli.agent_exec
1116
aai_cli.argscan
1217
aai_cli.auth
13-
aai_cli.caption_exec
1418
aai_cli.choices
1519
aai_cli.client
16-
aai_cli.clip_exec
17-
aai_cli.clip_select
1820
aai_cli.code_gen
1921
aai_cli.coding_agent
2022
aai_cli.config
2123
aai_cli.config_builder
2224
aai_cli.context
2325
aai_cli.debuglog
24-
aai_cli.deploy_exec
25-
aai_cli.dev_exec
26-
aai_cli.dictate_exec
2726
aai_cli.doctor_checks
28-
aai_cli.dub_exec
2927
aai_cli.environments
3028
aai_cli.errors
31-
aai_cli.eval_data
32-
aai_cli.eval_hf_api
33-
aai_cli.evaluate_exec
3429
aai_cli.follow
3530
aai_cli.help_panels
3631
aai_cli.help_text
@@ -39,7 +34,6 @@ source_modules =
3934
aai_cli.init_exec
4035
aai_cli.jsonshape
4136
aai_cli.llm
42-
aai_cli.llm_exec
4337
aai_cli.mediafile
4438
aai_cli.microphone
4539
aai_cli.onboard
@@ -49,11 +43,8 @@ source_modules =
4943
aai_cli.remotefs
5044
aai_cli.render
5145
aai_cli.setup_exec
52-
aai_cli.share_exec
53-
aai_cli.speak_exec
5446
aai_cli.stdio
5547
aai_cli.steps
56-
aai_cli.stream_exec
5748
aai_cli.streaming
5849
aai_cli.sync_stt
5950
aai_cli.telemetry
@@ -65,7 +56,6 @@ source_modules =
6556
aai_cli.tts
6657
aai_cli.typer_patches
6758
aai_cli.update_check
68-
aai_cli.webhook_listen
6959
aai_cli.wer
7060
aai_cli.ws
7161
aai_cli.youtube
@@ -87,13 +77,13 @@ type = forbidden
8777
source_modules =
8878
aai_cli.argscan
8979
aai_cli.client
90-
aai_cli.clip_select
80+
aai_cli.commands.clip._select
81+
aai_cli.commands.evaluate._data
9182
aai_cli.config
9283
aai_cli.config_builder
9384
aai_cli.debuglog
9485
aai_cli.environments
9586
aai_cli.errors
96-
aai_cli.eval_data
9787
aai_cli.hotkey
9888
aai_cli.llm
9989
aai_cli.remotefs

aai_cli/AGENTS.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,30 @@ pipe → exit 0).
1616

1717
### Command layer & the registration convention
1818

19-
Each file in `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`,
19+
Each entry under `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`,
2020
`dictate`, `agent`, `speak`, `llm`, `clip`, `dub`, `caption`, `eval`,
2121
`transcripts`, `login` (login/logout/whoami), `doctor`, `init`, `dev`, `share`,
2222
`deploy`, `setup`, `onboard`, `account` (balance/usage/limits), `keys`,
2323
`sessions`, `audit`, `telemetry` (status/enable/disable), `webhooks` (listen)).
2424

25+
**A command is either a single module *or* a package**`command_registry`
26+
discovers both (it iterates `pkgutil.iter_modules`, which enumerates packages
27+
too). A simple command stays a flat `commands/<cmd>.py`. A command with private
28+
run-logic becomes a package `commands/<cmd>/`: `__init__.py` holds the Typer
29+
`app` + `SPEC` (and is what gets imported as `aai_cli.commands.<cmd>`), and its
30+
support modules sit beside it **underscore-prefixed**`_exec.py` for the
31+
`run_<cmd>` body, plus any private helpers (`clip/_select.py`,
32+
`evaluate/_data.py`, `evaluate/_hf_api.py`). The underscore both marks them
33+
private and avoids colliding with the package's own command functions (the
34+
`webhooks` package binds a `listen` command, so its module is `_listen.py`, not
35+
`listen.py`). This is the Prefect/spaCy convention: flat file by default,
36+
promote to a folder only when the command has earned multiple modules. Run-logic
37+
that's **shared beyond one command stays at the package root**, not inside a
38+
command package — `transcribe_exec`/`transcribe_render`/`transcribe_batch` and
39+
`init_exec` are reused by the onboarding wizard (`onboard/sections.py`), so they
40+
live at the root alongside `doctor_checks`/`setup_exec` rather than under
41+
`commands/transcribe/` or `commands/init/`.
42+
2543
**Adding a command is purely additive — no shared file edits.** Every command
2644
module declares a module-level
2745
`SPEC = command_registry.CommandModuleSpec(panel=…, order=…, commands=…)`:
@@ -61,9 +79,10 @@ command module from another.
6179
function only parses argv into a frozen `<Cmd>Options` dataclass and hands it
6280
to a module-level `run_<cmd>(opts, state, *, json_mode)` through a thin lambda
6381
adapter in `run_command(ctx, ..., json=...)`. The run commands follow it —
64-
`aai_cli/stream_exec.py` (the reference implementation), `transcribe_exec.py`,
65-
`agent_exec.py`, `speak_exec.py`, `llm_exec.py`, `clip_exec.py`,
66-
`dictate_exec.py`. Because the run path is a plain function of data, tests
82+
`commands/stream/_exec.py` (the reference implementation), `transcribe_exec.py`
83+
(at the root — shared with onboarding), `commands/agent/_exec.py`,
84+
`commands/speak/_exec.py`, `commands/llm/_exec.py`, `commands/clip/_exec.py`,
85+
`commands/dictate/_exec.py`. Because the run path is a plain function of data, tests
6786
construct options directly (`dataclasses.replace` off a defaults instance, see
6887
`tests/test_stream_exec.py` and `tests/test_command_options_seam.py`) instead
6988
of round-tripping argv through `CliRunner` — which is also the cheap way to
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
import typer
66

7-
from aai_cli import agent_exec, choices, command_registry, help_panels, options, output
7+
from aai_cli import choices, command_registry, help_panels, options, output
88
from aai_cli.agent.session import DEFAULT_GREETING, DEFAULT_PROMPT
99
from aai_cli.agent.voices import (
1010
DEFAULT_VOICE,
1111
VOICES,
1212
complete_voice,
1313
format_voice_list,
1414
)
15+
from aai_cli.commands.agent import _exec as agent_exec
1516
from aai_cli.context import AppState, run_command
1617
from aai_cli.help_text import examples_epilog
1718

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Run logic for `assembly agent`: the options/run split (see AGENTS.md).
22
3-
The command module (aai_cli/commands/agent.py) only parses argv — it builds an
3+
The command module (aai_cli/commands/agent/__init__.py) only parses argv — it builds an
44
``AgentOptions`` and hands it to ``run_agent`` via ``context.run_command``, so tests
55
can drive validation, --show-code, and session wiring by constructing options
66
directly, with no CliRunner argv round-trip.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import typer
66

7-
from aai_cli import caption_exec, command_registry, help_panels, options
7+
from aai_cli import command_registry, help_panels, options
8+
from aai_cli.commands.caption import _exec as caption_exec
89
from aai_cli.context import run_command
910
from aai_cli.help_text import examples_epilog
1011

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Run logic for `assembly caption`: transcribe → SRT export → ffmpeg burn-in.
22
3-
The command module (aai_cli/commands/caption.py) only parses argv — it builds a
3+
The command module (aai_cli/commands/caption/__init__.py) only parses argv — it builds a
44
``CaptionOptions`` and hands it to ``run_caption`` via ``context.run_command``
55
(the options/run split, see AGENTS.md), so tests drive the whole pipeline by
66
constructing options directly.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import typer
66

7-
from aai_cli import clip_exec, command_registry, help_panels, llm, options
7+
from aai_cli import command_registry, help_panels, llm, options
8+
from aai_cli.commands.clip import _exec as clip_exec
89
from aai_cli.context import run_command
910
from aai_cli.help_text import examples_epilog
1011

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Run logic for `assembly clip`: cut a media file by transcript content.
22
3-
The command module (aai_cli/commands/clip.py) only parses argv — it builds a
3+
The command module (aai_cli/commands/clip/__init__.py) only parses argv — it builds a
44
``ClipOptions`` and hands it to ``run_clip`` via ``context.run_command`` (the
55
options/run split, see AGENTS.md), so tests drive transcript resolution and the
66
ffmpeg orchestration by constructing options directly. The pure selection logic
@@ -27,8 +27,9 @@
2727

2828
from rich.markup import escape
2929

30-
from aai_cli import clip_select, jsonshape, llm, mediafile, output, stdio, youtube
31-
from aai_cli.clip_select import Segment
30+
from aai_cli import jsonshape, llm, mediafile, output, stdio, youtube
31+
from aai_cli.commands.clip import _select as clip_select
32+
from aai_cli.commands.clip._select import Segment
3233
from aai_cli.context import AppState
3334
from aai_cli.errors import CLIError, UsageError
3435

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
# aai_cli/commands/deploy.py
1+
# aai_cli/commands/deploy/__init__.py
22
from __future__ import annotations
33

44
import typer
55

6-
from aai_cli import command_registry, deploy_exec, help_panels, options
6+
from aai_cli import command_registry, help_panels, options
7+
from aai_cli.commands.deploy import _exec as deploy_exec
78
from aai_cli.context import run_command
89
from aai_cli.help_text import examples_epilog
910

0 commit comments

Comments
 (0)