282 lines
10 KiB
Markdown
282 lines
10 KiB
Markdown
# Flow CLI Refactor Plan
|
|
|
|
> Based on code review (2026-03-22) and architecture discussion.
|
|
> Spec: `docs/superpowers/specs/2026-03-16-flow-architecture-redesign.md`
|
|
|
|
## Current State
|
|
|
|
The rewrite from the original "vibe-coded" codebase is **largely complete**. The four-layer
|
|
architecture (core -> domain -> services -> commands) is in place, 303 tests pass, and the major
|
|
structural problems from the old codebase (duplicated code, monkeypatching, dead modules, singleton
|
|
abuse) have been resolved.
|
|
|
|
What remains is a second pass: finishing incomplete features, unifying the repo abstraction, and
|
|
trimming redundant commands.
|
|
|
|
---
|
|
|
|
## Agreed Command Surface
|
|
|
|
From the architecture discussion. This is the target.
|
|
|
|
```
|
|
flow remote enter <target> # Host only. SSH+tmux into VM.
|
|
flow remote list # List configured targets.
|
|
|
|
flow dev create <name> -i <image> # VM only. Create+start container.
|
|
flow dev attach <name> # Attach to container tmux session.
|
|
flow dev exec <name> [cmd...] # Run command in container.
|
|
flow dev enter <name> # Interactive shell in container.
|
|
flow dev list # List dev containers.
|
|
flow dev stop <name> # Stop container.
|
|
flow dev rm <name> # Remove container.
|
|
flow dev respawn <name> # Respawn tmux panes.
|
|
|
|
flow dotfiles init --repo <url> # Clone dotfiles repo + all module repos.
|
|
flow dotfiles link [--profile p] # Reconcile symlinks (creates, fixes broken, removes stale).
|
|
flow dotfiles unlink [packages...] # Remove managed symlinks.
|
|
flow dotfiles status [packages...] # Show packages, link health, module info.
|
|
flow dotfiles edit <package> # Pull -> $EDITOR -> commit+push.
|
|
|
|
flow dotfiles repos list # List ALL managed repos (dotfiles + modules).
|
|
flow dotfiles repos status [--repo=x] # Git status for one or all repos.
|
|
flow dotfiles repos pull [--repo=x] # Pull one or all repos.
|
|
flow dotfiles repos push [--repo=x] # Push one or all repos.
|
|
|
|
flow setup run [--profile p] # Bootstrap a machine.
|
|
flow setup list # List profiles.
|
|
flow setup show <profile> # Show profile plan.
|
|
|
|
flow packages install <name...> # Install packages from manifest.
|
|
flow packages list [--all] # List packages.
|
|
flow packages remove <name...> # Remove packages.
|
|
|
|
flow projects check [--fetch] # VM only. Git health across ~/projects.
|
|
flow projects fetch # Fetch all project remotes.
|
|
flow projects summary # Quick status overview.
|
|
```
|
|
|
|
### Aliases
|
|
|
|
- `dotfiles` -> `dot`
|
|
- `packages` -> `package`, `pkg`
|
|
- `projects` -> `project`
|
|
- `setup` -> `bootstrap`
|
|
- `dev attach` -> `dev connect`
|
|
- `dev rm` -> `dev remove`
|
|
- `dotfiles repos` -> `dotfiles repo`
|
|
|
|
### Global flags
|
|
|
|
- `--version`
|
|
- `--quiet` / `-q`
|
|
- `--no-color`
|
|
|
|
### Commands removed (vs current implementation)
|
|
|
|
| Removed | Reason |
|
|
|---------|--------|
|
|
| `dotfiles sync` | Redundant: `repos pull` + `link` |
|
|
| `dotfiles relink` | Redundant: `link` is idempotent |
|
|
| `dotfiles undo` | Redundant: `unlink` is the inverse of `link` |
|
|
| `dotfiles clean` | Folded into `link` (reconciliation handles broken symlinks) |
|
|
| `dotfiles modules list` | Replaced by `dotfiles repos list` |
|
|
| `dotfiles modules sync` | Replaced by `dotfiles repos pull` |
|
|
|
|
### Key design decision: modules are repos
|
|
|
|
The `_module.yaml` files define external git repos that provide content for dotfiles packages.
|
|
These module repos are **not** git submodules -- they are regular git clones managed by flow.
|
|
|
|
The dotfiles repo itself is also a git clone managed by flow. So **all managed git repos** (the
|
|
dotfiles repo + every module repo) share the same abstraction and the same commands.
|
|
|
|
`dotfiles repos` is the single entry point for all repo operations. There is no separate
|
|
`dotfiles modules` subcommand group. The `--repo=<name>` flag filters to a specific repo when
|
|
needed (dotfiles repo is named `dotfiles`, module repos are named by their package, e.g. `nvim`).
|
|
|
|
---
|
|
|
|
## 1. Unified Repos Abstraction
|
|
|
|
This is the most impactful change. Currently:
|
|
- `repo_status/pull/push` operate only on the dotfiles repo
|
|
- `sync_modules` handles module repos separately
|
|
- `list_modules` is a standalone method
|
|
|
|
**Target**: a single `_discover_repos() -> list[RepoInfo]` that returns all managed repos, and
|
|
repo commands iterate over them.
|
|
|
|
### 1.1 Add `RepoInfo` model
|
|
|
|
```python
|
|
# domain/dotfiles/models.py
|
|
@dataclass(frozen=True)
|
|
class RepoInfo:
|
|
name: str # "dotfiles" or module package name (e.g. "nvim")
|
|
path: Path # Local clone path
|
|
remote: str # Remote URL
|
|
is_module: bool # False for dotfiles repo, True for module repos
|
|
```
|
|
|
|
### 1.2 Implement `_discover_repos`
|
|
|
|
In `DotfilesService`: walk packages to find `_module.yaml` files, build `RepoInfo` for each
|
|
module repo, plus one for the dotfiles repo itself.
|
|
|
|
### 1.3 Refactor repo commands
|
|
|
|
Replace `repo_status`, `repo_pull`, `repo_push` with methods that iterate `_discover_repos()`,
|
|
filtered by `--repo`. Add `repos_list`.
|
|
|
|
### 1.4 Remove `dotfiles modules` subcommand group
|
|
|
|
Delete `modules list` and `modules sync` subparsers. Remove `sync_modules`, `list_modules`
|
|
methods. Remove from completion candidates.
|
|
|
|
### 1.5 Update `dotfiles init`
|
|
|
|
`init` should clone the dotfiles repo, then discover `_module.yaml` files and clone all module
|
|
repos. Currently it calls `sync_modules()` -- this should call `repos_pull()` instead (which
|
|
pulls/clones all repos).
|
|
|
|
**Files**: `models.py`, `services/dotfiles.py`, `commands/dotfiles.py`, `commands/completion.py`.
|
|
|
|
---
|
|
|
|
## 2. Remove Redundant Commands
|
|
|
|
### 2.1 Remove `dotfiles sync`
|
|
|
|
Currently does `git pull --ff-only` + `sync_modules` + optional relink. After the repos
|
|
unification, this is just `repos pull` + `link`. No need for a dedicated command.
|
|
|
|
### 2.2 Remove `dotfiles relink`
|
|
|
|
Currently calls `link()`. `link` is already idempotent -- calling it again reconciles state.
|
|
|
|
### 2.3 Remove `dotfiles undo`
|
|
|
|
`unlink` is the inverse of `link`. The backup/undo transaction machinery (`_save_backup`,
|
|
`_load_backup`, `_backup_path`) can be deleted.
|
|
|
|
### 2.4 Fold `dotfiles clean` into `dotfiles link`
|
|
|
|
`link` should detect and remove broken symlinks as part of reconciliation, not require a separate
|
|
`clean` step. Modify `plan_link` in `domain/dotfiles/planning.py` to include broken link removal
|
|
in its plan.
|
|
|
|
**Files**: `services/dotfiles.py`, `commands/dotfiles.py`, `commands/completion.py`,
|
|
`domain/dotfiles/planning.py`.
|
|
|
|
---
|
|
|
|
## 3. Previously Incomplete Features -- DONE
|
|
|
|
### 3.1 `dotfiles edit` -- DONE
|
|
|
|
Implemented: pull -> `$EDITOR`/`$VISUAL` -> scoped `git add` -> commit+push.
|
|
Flag: `--no-commit` to skip auto-commit/push.
|
|
|
|
### 3.2 `dotfiles status` -- DONE
|
|
|
|
Enhanced: shows module info (`branch:main`), link health (`ok`/`broken`/`not linked`),
|
|
package name filtering via positional args.
|
|
|
|
### 3.3 `dotfiles repos list` -- DONE
|
|
|
|
Shows all managed repos (dotfiles + modules) with: name, type, local path, clone status.
|
|
|
|
---
|
|
|
|
## 4. Spec Deviations
|
|
|
|
### 4.1 `--no-color` global flag -- DONE
|
|
|
|
Added to `cli.py`.
|
|
|
|
### 4.2 `--dry-run` coverage -- DONE for dotfiles
|
|
|
|
`repos pull` and `repos push` now have `--dry-run`. Remaining:
|
|
|
|
| Command | Has it | Should have |
|
|
|---------|--------|-------------|
|
|
| `dev stop` | No | Consider |
|
|
| `dev rm` | No | Consider |
|
|
|
|
### 4.3 Improvements over spec
|
|
|
|
These are correct deviations -- the implementation improved on the spec:
|
|
- `core/containers.py` + `core/tmux.py` extracted as adapters (spec had them inline)
|
|
- `core/config_parse.py` + `core/yaml.py` extracted for config parsing
|
|
- `SystemRuntime` extended with `.containers` and `.tmux` fields
|
|
|
|
---
|
|
|
|
## 5. Code Quality (done)
|
|
|
|
These were fixed in this session:
|
|
|
|
- `FakeRunner` consolidated from 3 test files into `tests/fakes.py`
|
|
- `services/dotfiles.py` now uses `flow.core.yaml.load_yaml_file` instead of raw `yaml`
|
|
- `ContainerRuntime.binary` no longer eagerly validates PATH for explicit modes
|
|
|
|
---
|
|
|
|
## 6. Future (defer)
|
|
|
|
### 6.1 Bootstrap as orchestrator
|
|
|
|
The spec envisions bootstrap as a pure orchestrator over packages + dotfiles + setup modules.
|
|
Current implementation works but has its own package resolution logic. Defer until dotfiles and
|
|
packages domains are fully stable.
|
|
|
|
### 6.2 Global `--dry-run`
|
|
|
|
If per-command `--dry-run` becomes a maintenance burden, promote to a global flag in `cli.py`.
|
|
|
|
---
|
|
|
|
## 7. Execution Status
|
|
|
|
All phases complete. 315 tests pass, 0 failures.
|
|
|
|
### Phase A: Unify repos + trim commands -- DONE
|
|
|
|
1. `RepoInfo` model with `module_ref` field
|
|
2. `_discover_repos()` finds dotfiles + module repos
|
|
3. `repos_list/status/pull/push` iterate all repos with `--repo` filter
|
|
4. `repos list` subcommand added
|
|
5. `dotfiles modules` subcommand removed entirely
|
|
6. `dotfiles sync`, `relink`, `undo`, `clean` removed
|
|
7. Broken-symlink handling folded into `plan_link`
|
|
8. `dotfiles init` uses unified `repos_pull()`
|
|
|
|
### Phase B: Complete features -- DONE
|
|
|
|
9. `dotfiles edit` implemented (pull -> $EDITOR -> commit+push, scoped git add)
|
|
10. `dotfiles status` enhanced (module info, link health, package filtering)
|
|
|
|
### Phase C: CLI polish -- DONE
|
|
|
|
11. `--no-color` global flag added to `cli.py`
|
|
12. `--dry-run` added to `repos pull` and `repos push`
|
|
13. Zsh completion updated for new command surface
|
|
|
|
### Phase D: Code quality -- DONE
|
|
|
|
14. Dispatch patterns: completion `complete()`, dotfiles `_git_checkout_ref`, bootstrap phases
|
|
15. `FakeRunner` consolidated to single `tests/fakes.py` (all 4 test files)
|
|
16. Bootstrap `VALID_PHASES` as single source of truth in models
|
|
17. Bootstrap models: `Any` types replaced with `ProfilePackageEntry` and `PackageDef`
|
|
18. Canonical `ssh-keys` field (removed `ssh-keygen` alias)
|
|
19. `getattr` defensive patterns removed from command handlers
|
|
20. Test coverage added: `repos_status`, `repos_push`, `repos_pull --dry-run`, status filtering,
|
|
broken symlink repair
|
|
21. README updated to reflect new command surface
|
|
|
|
### Remaining (deferred)
|
|
|
|
- Bootstrap as orchestrator (section 6.1)
|
|
- Global `--dry-run` (section 6.2)
|