1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
use crate::core::orchestrate;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "stack", version, about = "Native, zero-container multi-project dev environment manager. No Docker, no VMs.")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Scaffold a new project interactively (`stack new <name>` or `stack new .`)
New { target: String },
/// Add a language/service/tool entry to stack.toml (format-preserving —
/// comments and existing entries are left untouched). `[[clone]]` isn't
/// supported yet; hand-edit it directly.
Add {
kind: String,
name: String,
/// Version (language/service) — omit for a tool, or for a language using
/// `--path` (a BYO binary needs no version-manager version at all).
version: Option<String>,
/// service: schema name (defaults to the project name if unset)
#[arg(long)]
schema: Option<String>,
/// service: port (defaults to the engine's conventional port if unset)
#[arg(long)]
port: Option<u16>,
/// service: start command override
#[arg(long)]
command: Option<String>,
/// service/tool/language: a BYO binary path, bypassing any manager
#[arg(long)]
path: Option<String>,
/// language: manager to use ("vfox" or "uv")
#[arg(long)]
manager: Option<String>,
/// language: vfox plugin name override (defaults to `name`)
#[arg(long)]
plugin: Option<String>,
/// language: binary filename override (defaults to `{name}{EXE_SUFFIX}`)
#[arg(long)]
binary: Option<String>,
},
/// Remove a language/service/tool entry from stack.toml
Remove { kind: String, name: String },
/// Register a BYO path (service/tool/language) for reuse across every project
/// referencing the same kind+name+version. For a service, `--external --port
/// <port>` registers an already-running instance instead of a path — `stack` only
/// ever connects to it, never starts it (not valid for tool/language, which have
/// no "already running" concept).
Register {
kind: String,
name: String,
version: String,
/// BYO binary path — omit when using --external instead
path: Option<String>,
/// Register as an already-running instance (service only) instead of a path
#[arg(long)]
external: bool,
/// Port the already-running instance listens on — required with --external
#[arg(long)]
port: Option<u16>,
},
/// Live-merged view of everything installed — vfox, uv, and the registry — no
/// cache, queried fresh every time
List,
/// Orphan detection: what's installed/registered but no longer referenced by any
/// known project. Dry-run by default.
Prune {
/// Actually uninstall orphaned language SDKs and drop orphaned registry
/// entries (never touches a service's data directory on its own)
#[arg(long)]
yes: bool,
/// Also delete orphaned services' data directories — requires --yes
#[arg(long)]
purge_data: bool,
},
/// Explicit, session-scoped `.env` loading (defaults to `.env` in cwd). Only
/// works through the shell hook — prints shell syntax for the `stack` wrapper
/// function to eval, since a child process can't mutate its parent's env
/// directly. Not meant to be run without the hook installed (`stack setup`).
LoadEnv { path: Option<String> },
/// Activate languages/services, spawn [run], and route the domain
Up {
#[arg(default_value = ".")]
dir: PathBuf,
/// Allow interactive prompting for unresolved `{VAR_NAME}` placeholders.
/// Without this, missing values fail fast instead of hanging — important for
/// non-interactive runs (CI, background scripts).
#[arg(long)]
prompt: bool,
},
/// Stop a project's dev-server process (leaves shared services running).
/// Name is optional if run from inside the project's own folder.
Down {
project: Option<String>,
#[arg(long)]
all: bool,
},
/// List everything currently running
#[command(alias = "ps")]
Status,
/// Show live CPU/memory usage for everything currently running (like `docker
/// stats`) — clears and redraws roughly once a second until interrupted (Ctrl+C)
Stats {
/// Print one snapshot and exit, instead of streaming (like `docker stats --no-stream`)
#[arg(long)]
no_stream: bool,
},
/// Print a project's or service's log (like `docker logs`) — stdout/stderr are
/// redirected here since spawned processes are detached and have no console.
/// Name is optional if run from inside the project's own folder.
Logs {
name: Option<String>,
/// Keep streaming new output as it's written, like `docker logs -f`
#[arg(short = 'f', long)]
follow: bool,
/// Only show the last N lines
#[arg(long)]
tail: Option<usize>,
},
/// Verify vfox/uv/caddy are on PATH and registered service/tool paths exist
Doctor {
/// Install any missing tool at its pinned version (never touches one
/// that's already present, even on a version mismatch)
#[arg(long)]
fix: bool,
},
/// Print the shell hook to add to your profile
Hook { shell: String },
/// Runs on every shell prompt via the hook `stack setup`/`Hook` installs — prints
/// PATH-prepend commands for the current directory's pinned language versions, if
/// any. Not meant to be run by hand.
Activate { shell: String },
/// One-time bootstrap: wires the shell hook into your profile, installs any
/// missing vfox/uv/caddy at their pinned versions, and prints an optional
/// DNS-fix pointer. Safe to run more than once.
Setup {
#[arg(long, default_value = "pwsh")]
shell: String,
},
}
pub fn run() {
let cli = Cli::parse();
let result: anyhow::Result<()> = match cli.command {
Command::New { target } => orchestrate::new_project(&target),
Command::Add { kind, name, version, schema, port, command, path, manager, plugin, binary } => {
orchestrate::add(&kind, &name, version, schema, port, command, path, manager, plugin, binary)
}
Command::Remove { kind, name } => orchestrate::remove(&kind, &name),
Command::Register { kind, name, version, path, external, port } => orchestrate::register(&kind, &name, &version, path.as_deref(), external, port),
Command::List => {
orchestrate::list();
Ok(())
}
Command::Prune { yes, purge_data } => orchestrate::prune(yes, purge_data),
Command::LoadEnv { path } => orchestrate::load_env(path),
Command::Up { dir, prompt } => orchestrate::up(&dir, prompt),
Command::Down { project, all } => orchestrate::down(project, all),
Command::Status => {
orchestrate::status();
Ok(())
}
Command::Stats { no_stream } => {
orchestrate::stats(no_stream);
Ok(())
}
Command::Logs { name, follow, tail } => orchestrate::logs(name, follow, tail),
Command::Doctor { fix } => orchestrate::doctor(fix),
Command::Hook { shell } => orchestrate::hook(&shell),
Command::Activate { shell } => {
orchestrate::activate(&shell);
Ok(())
}
Command::Setup { shell } => {
orchestrate::setup(&shell);
Ok(())
}
};
if let Err(e) = result {
eprintln!("stack: {e:#}");
std::process::exit(1);
}
}