1 # Plan: decouple build/deploy from the CLI into `smbcloud-deploy`
2
3 Status: in progress. Stages 0 to 3 (vite_spa slice) shipped. Remaining work below
4 is the **next milestone**.
5
6 ## Goal
7
8 Move the deploy logic out of `crates/cli` into a reusable `crates/smbcloud-deploy`
9 engine so the CLI, the CI action, and (later) the server-side git receiver all
10 drive the same code. They differ only in how they report progress and how they
11 authenticate.
12
13 Two inversions make it reusable:
14 - `Reporter` replaces direct `spinners` / `dialoguer` / `println!`, so the engine
15 never owns the terminal. The CLI supplies `SpinnerReporter`.
16 - Auth is passed in (a token / credentials); the engine never reads `~/.smb` or
17 prompts for login.
18
19 Decisions already made:
20 - Networking crates (`smbcloud-networking*`) stay direct deps of the engine.
21 Only reporting and auth are inverted.
22 - `git.rs` (SSH-git remote setup) stays in the CLI for now. It is the SSH-git
23 path slated for replacement by git-smart-HTTP, so decoupling it is wasted work.
24 - Interactive setup (`setup*.rs`, `process_migrate.rs`) stays in the CLI. It is
25 UX, not engine.
26
27 ## Done
28
29 - **Stage 0** engine crate: `error::DeployError`, `report::{Reporter, NoopReporter}`,
30 `runner::detect_runner`.
31 - **Stage 1** CLI depends on the engine; `ui/reporter.rs::SpinnerReporter`;
32 `detect_runner` rewired; old `deploy/detect_runner.rs` removed.
33 - **Stage 2** transport: `transport::{Transport, RsyncTransport}` + moved
34 `known_hosts`. CLI helper `deploy::rsync_transport(config, runner, user_id)`
35 resolves the local `~/.ssh` identity and remote path.
36 - **Stage 3 (vite_spa slice)** build split: `build::{BuildStrategy, BuildArtifact,
37 ViteSpaBuild}`. `process_deploy_vite_spa.rs` now calls the engine for the build
38 and reuses one `SpinnerReporter` for build + transport.
39
40 ## Next milestone
41
42 ### 1. Shared remote-command step
43
44 Several strategies do the same "SSH in and restart the service" step, inlined
45 per file. Extract it into the engine first (e.g. `transport::RemoteCommand` or a
46 `remote` module) so the strategies can share it. Model it behind the same pinned
47 host-key SSH used by `RsyncTransport`.
48
49 ### 2. Remaining `BuildStrategy` extractions (one per pass, compile-green each)
50
51 Do smallest first. Each: lift build mechanics into a `BuildStrategy`, swap
52 `spinners` + `stop_and_persist` for `Reporter`, take auth as a param, route the
53 remote-restart step through step 1.
54
55 | File | Lines | Shape |
56 | --- | --- | --- |
57 | `process_deploy_rails.rs` | ~395 | rsync shared lib, SSH compile native gem, git force-push |
58 | `process_deploy_rust.rs` | ~641 | cross-build Linux binary, rsync, SSH restart |
59 | `process_deploy_nextjs_ssr.rs` | ~700 | install, build, upload `.next/standalone`, SSH restart |
60 | `process_deploy_swift.rs` | ~832 | Docker Linux build, rsync binary + Resources, SSH restart |
61
62 Follow `ViteSpaBuild` as the template. Keep the deployment-record API calls
63 (`create_deployment` / `update`) in the CLI router, not the engine.
64
65 ### 3. Stage 4: router + retire the CommandResult wart
66
67 - Flip `process_deploy.rs`'s router to call an `engine::deploy(...)` orchestrator.
68 - Retire `CommandResult`'s `spinner: Spinner` field (the "return a live spinner,
69 caller stops it" pattern) now that the engine reports the whole flow through
70 `Reporter`. `CommandResult` has ~50 construction sites, so this is its own pass:
71 either make `spinner` optional or drop it and have commands return plain data.
72
73 ## Reference
74
75 - Spec: `smbcloud` repo `.agents/specs/smbcloud-deploy.md` (product direction),
76 `smbcloud-actions.md` (CI wrapper). Direction is git-smart-HTTP, local-first
77 build, drop SSH transport later.
78 - Engine crate: `crates/smbcloud-deploy/`.