-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.rugo
More file actions
298 lines (291 loc) · 12.2 KB
/
Copy pathcli.rugo
File metadata and controls
298 lines (291 loc) · 12.2 KB
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# cli.rugo — CLI argument parsing, help, and version for yolo.
#
# yolo's CLI parser is hand-rolled rather than using rugo's `cli` native
# module, because that module does not handle the bare `yolo -- cmd args`
# form (which is documented and well-used). See rugo-quirks.md for details.
#
# This module is otherwise a straight extraction of what used to live in
# yolo.rugo's top half. Functions take their few external dependencies
# (AI_AGENTS hash, VERSION string, DEFAULT_AI_AGENT name) as parameters
# rather than reaching back into yolo.rugo, because rugo modules can only
# see what they require — never the other direction.
use "os"
use "str"
use "color"
use "conv"
# ---------------- Logging ----------------
# Duplicated from yolo.rugo so this module can stand alone (same pattern
# the backend modules follow). Tag is kept identical to yolo.rugo's so
# error output looks uniform — the user shouldn't have to know whether a
# message came from the CLI layer or somewhere else.
def _log(msg)
prefix = color.dim("[yolo]")
puts "#{prefix} #{msg}"
end
def _warn(msg)
prefix = color.dim("[yolo]")
puts "#{prefix} #{color.yellow(msg)}"
end
def _err(msg)
prefix = color.dim("[yolo]")
puts "#{prefix} #{color.red(msg)}"
end
# ---------------- Disk size parsing ----------------
# Parse a human disk size like "32G", "32g", "32M", "32m" (with optional
# trailing "B"/"b" — so "32gb" / "32mb" also work), or a bare integer
# interpreted as MiB (e.g. "16384") for parity with $YOLO_DISK_MB. Returns
# the size as a MiB integer string, or "" if the input can't be parsed.
#
# Also used by yolofile.rugo for `memory:` / `disk-size:` front matter, where
# it is duplicated (small, pure, stable) to avoid a cross-module dependency.
def parse_disk_size(s)
if s == ""
return ""
end
lower = str.lower(s)
if str.ends_with(lower, "b")
lower = str.slice(lower, 0, len(lower) - 1)
end
multiplier = 1
unit_len = 0
if str.ends_with(lower, "g")
multiplier = 1024
unit_len = 1
elsif str.ends_with(lower, "m")
multiplier = 1
unit_len = 1
end
num_str = lower
if unit_len > 0
num_str = str.slice(lower, 0, len(lower) - unit_len)
end
if num_str == ""
return ""
end
n = try conv.to_i(num_str) or -1
if n <= 0
return ""
end
return conv.to_s(n * multiplier)
end
# ---------------- Arg parsing ----------------
# Parse a flat argv into: subcommand, -n name, --provisioner, --yolofile,
# --ephemeral, --ai-agent, --backend, --gui / --no-gui, rest, -- passthrough.
#
# `ai_agents` is the yolo-side AI_AGENTS hash, used only for the
# `--ai-agent` optional-value heuristic (if the next arg matches a known
# agent name we consume it; otherwise we leave it for the subcommand /
# positional parser).
#
# `default_ai_agent` is the fallback used when `--ai-agent` is passed with
# no value (or with a value that isn't a known agent name — that path keeps
# the default rather than failing, by historical accident; kept for
# backward compatibility).
#
# `version` is the yolo VERSION string, printed by -V / --version.
def parse_args(args, ai_agents, default_ai_agent, version)
sub = ""
name = ""
prov = ""
no_prov = false
agent = ""
agent_set = false
rest = []
passthrough = []
in_passthrough = false
out_path = ""
force = false
disk_mb_override = ""
backend = ""
gui = false
gui_set = false
audio = false
audio_set = false
yolofile_path = ""
ephemeral = false
i = 0
while i < len(args)
a = args[i]
if in_passthrough
passthrough = append(passthrough, a)
elsif a == "--"
in_passthrough = true
elsif a == "-n" || a == "--name"
i = i + 1
name = args[i]
elsif a == "--provisioner" || a == "-P"
i = i + 1
prov = args[i]
elsif a == "--yolofile"
i = i + 1
if i >= len(args)
_err("--yolofile requires a path or https:// URL")
os.exit(2)
end
yolofile_path = args[i]
elsif a == "--ephemeral"
ephemeral = true
elsif a == "--no-provision" || a == "--no-provisioner"
no_prov = true
elsif a == "-o" || a == "--output"
i = i + 1
out_path = args[i]
elsif a == "--force"
force = true
elsif a == "--backend" || a == "-b"
i = i + 1
if i >= len(args)
_err("--backend requires a value (matchlock or podman)")
os.exit(2)
end
backend = args[i]
elsif a == "--gui"
gui = true
gui_set = true
elsif a == "--no-gui"
gui = false
gui_set = true
elsif a == "--audio"
audio = true
audio_set = true
elsif a == "--no-audio"
audio = false
audio_set = true
elsif a == "--disk-size"
i = i + 1
if i >= len(args)
_err("--disk-size requires a value (e.g. 32G, 512M, or a bare MiB integer)")
os.exit(2)
end
parsed_sz = parse_disk_size(args[i])
if parsed_sz == ""
_err("invalid --disk-size value: '#{args[i]}' (expected e.g. 32G, 32g, 512M, 512m, or a bare MiB integer)")
os.exit(2)
end
disk_mb_override = parsed_sz
elsif a == "--ai-agent"
# Optional value: if the next arg exists and isn't another flag /
# subcommand, consume it as the agent name; otherwise default.
agent_set = true
nxt = ""
if i + 1 < len(args)
nxt = args[i + 1]
end
if nxt != "" && !str.starts_with(nxt, "-") && nxt != "--"
# Heuristic: treat as a value only if it matches a known agent name.
# Otherwise it's a subcommand or positional and we keep the default.
if ai_agents[nxt] != nil
agent = nxt
i = i + 1
else
agent = default_ai_agent
end
else
agent = default_ai_agent
end
elsif a == "--no-ai-agent"
agent = ""
agent_set = true
elsif a == "-h" || a == "--help"
print_help(default_ai_agent)
os.exit(0)
elsif a == "-V" || a == "--version"
print_version(version)
os.exit(0)
elsif sub == "" && (a == "ls" || a == "du" || a == "stop" || a == "rm" || a == "logs" || a == "id" || a == "status" || a == "prune" || a == "provision" || a == "provisioners" || a == "export" || a == "import" || a == "install-skills")
sub = a
else
rest = append(rest, a)
end
i = i + 1
end
return _result(sub, name, prov, no_prov, agent, agent_set, rest, passthrough, out_path, force, disk_mb_override, backend, gui, gui_set, yolofile_path, ephemeral, audio, audio_set)
end
# Build the parsed-args hash. Factored out only because parse_args has two
# return points (the -h sentinel path and the normal end-of-loop path) and
# duplicating the 15-field literal twice is asking for drift.
def _result(sub, name, prov, no_prov, agent, agent_set, rest, passthrough, out_path, force, disk_mb, backend, gui, gui_set, yolofile_path, ephemeral, audio, audio_set)
return {sub: sub, name: name, prov: prov, no_prov: no_prov, agent: agent, agent_set: agent_set, rest: rest, passthrough: passthrough, out_path: out_path, force: force, disk_mb: disk_mb, backend: backend, gui: gui, gui_set: gui_set, yolofile_path: yolofile_path, ephemeral: ephemeral, audio: audio, audio_set: audio_set}
end
# ---------------- Help / version ----------------
def print_version(version)
puts "yolo #{version}"
end
def print_help(default_ai_agent)
puts "yolo — fast persistent per-directory dev environments (matchlock VMs or podman containers)."
puts ""
puts "Usage:"
puts " yolo Ensure VM, auto-provision (once), shell in."
puts " yolo -- CMD ARGS... Run CMD inside the VM."
puts " yolo --provisioner NAME [...] Use a specific provisioner (overrides Yolofile)."
puts " yolo --yolofile PATH|URL [...] Use a Yolofile from a local path or https URL."
puts " yolo --ephemeral [...] Use a throwaway VM and empty temp workspace."
puts " yolo --no-provision [...] Skip auto-provisioning (also: --no-provisioner)."
puts " yolo --ai-agent [NAME] [...] Also install an AI agent (default: #{default_ai_agent})."
puts " Known agents: copilot, opencode."
puts " yolo --backend NAME [...] Pick a backend: matchlock (microVM, default)"
puts " or podman (container, supports GUI). The"
puts " binding is sticky: once a VM is created the"
puts " backend is recorded and re-used on attach."
puts " yolo --gui [...] Bind-mount the host Wayland socket into the"
puts " guest so graphical apps render on your"
puts " compositor. Requires --backend podman."
puts " yolo --audio [...] Bind-mount the host PipeWire/PulseAudio"
puts " socket into the guest so apps can play"
puts " sound. Requires --backend podman."
puts " yolo -n NAME [...] Use a named VM."
puts " yolo --disk-size SIZE [...] Override rootfs disk size for this run."
puts " Accepts 32G, 32g, 512M, 512m, or a bare"
puts " MiB integer. Takes effect when the VM is"
puts " first created."
puts " yolo -V, --version Print yolo version and exit."
puts " yolo -h, --help Print this help and exit."
puts " yolo ls List tracked VMs with live status."
puts " yolo du List tracked VMs with disk usage."
puts " yolo stop [-n NAME] Stop VM (podman: preserves state)."
puts " yolo rm [-n NAME] Stop + remove VM and binding."
puts " yolo logs [-n NAME] Show VM log."
puts " yolo id [-n NAME] Print vm-id."
puts " yolo status [-n NAME] Print state + vm-id + applied provisioners."
puts " yolo prune Drop dead name bindings."
puts " yolo provision [--provisioner NAME] [-n NAME]"
puts " Force re-apply a provisioner."
puts " yolo provisioners List provisioners (embedded + ./Yolofile)."
puts " yolo install-skills Install bundled agent skills into"
puts " ~/.agents/skills (overwrites existing)."
puts ""
puts " yolo export [-n NAME] [-o FILE] Export VM rootfs + state into a single"
puts " .tar.gz (matchlock backend only)."
puts " yolo import FILE [-n NAME] [--force]"
puts " Import an export bundle; pins a custom"
puts " matchlock image so the first `yolo -n NAME`"
puts " boots from the captured rootfs."
puts ""
puts "Provisioner resolution (in order):"
puts " 1. --provisioner NAME (explicit)"
puts " 2. ./Yolofile (if present)"
puts " 3. Auto-detected from $PWD:"
puts " go.mod / go.sum / *.go → fedora-go"
puts " Cargo.toml / rust-toolchain.toml → fedora-rust"
puts " Gemfile / *.gemspec / .ruby-version → fedora-ruby"
puts " build.gradle[.kts] / settings.gradle → fedora-android"
puts " 4. Otherwise: no provisioner runs."
puts ""
puts "Yolofile:"
puts " A plain bash script (run as root in the VM). Edit-and-rerun"
puts " automatically re-provisions (content-hashed for the marker)."
puts " May begin with a YAML-style '---' front matter block declaring"
puts " VM-creation overrides (image, cpus, memory, disk-size, backend,"
puts " gui, audio). See docs/05-yolofile.md for the full format."
puts ""
puts "Env vars (defaults):"
puts " YOLO_IMAGE=fedora:44 OCI image (default depends on backend)"
puts " YOLO_CPUS=2 vCPU count"
puts " YOLO_MEM_MB=2048 Memory (MiB)"
puts " YOLO_DISK_MB=32768 Rootfs disk (MiB) (matchlock only)"
puts " YOLO_WORKSPACE=/work Guest mount point for $PWD"
puts " YOLO_ALLOW= Comma list of allow-listed hosts (enables MITM)"
puts " YOLO_USER= Run as uid:gid inside guest"
puts " YOLO_NAME= Override the auto-derived per-CWD name"
puts " YOLO_BACKEND= Default backend (matchlock or podman)"
end