Files
flow/docs/refactor-plan.md
2026-05-13 23:02:47 +03:00

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)