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

10 KiB

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

# 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.

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.

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

  1. dotfiles edit implemented (pull -> $EDITOR -> commit+push, scoped git add)
  2. dotfiles status enhanced (module info, link health, package filtering)

Phase C: CLI polish -- DONE

  1. --no-color global flag added to cli.py
  2. --dry-run added to repos pull and repos push
  3. Zsh completion updated for new command surface

Phase D: Code quality -- DONE

  1. Dispatch patterns: completion complete(), dotfiles _git_checkout_ref, bootstrap phases
  2. FakeRunner consolidated to single tests/fakes.py (all 4 test files)
  3. Bootstrap VALID_PHASES as single source of truth in models
  4. Bootstrap models: Any types replaced with ProfilePackageEntry and PackageDef
  5. Canonical ssh-keys field (removed ssh-keygen alias)
  6. getattr defensive patterns removed from command handlers
  7. Test coverage added: repos_status, repos_push, repos_pull --dry-run, status filtering, broken symlink repair
  8. README updated to reflect new command surface

Remaining (deferred)

  • Bootstrap as orchestrator (section 6.1)
  • Global --dry-run (section 6.2)