1#![forbid(unsafe_code)]
6
7use std::cell::RefCell;
8use std::collections::{BTreeMap, HashSet};
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use vanta_core::{Area, ExitCode, Platform, Request, StoreKey, VersionReq, VtaError, VtaResult};
12use vanta_install::Engine;
13use vanta_registry::Registry;
14use vanta_resolve::{artifact_for, Resolver};
15use vanta_ui::Progress;
16
17pub const VERSION: &str = env!("CARGO_PKG_VERSION");
19
20struct InstallUi {
26 label: String,
28 bar: RefCell<Option<Progress>>,
29}
30
31impl InstallUi {
32 fn new(label: String) -> InstallUi {
33 InstallUi {
34 label,
35 bar: RefCell::new(None),
36 }
37 }
38
39 fn finish_ok(&self, msg: &str) {
41 match self.bar.borrow().as_ref() {
42 Some(bar) => bar.finish_ok(msg),
43 None => vanta_ui::step(msg),
44 }
45 }
46}
47
48impl vanta_install::Reporter for InstallUi {
49 fn fetch_start(&self, total: Option<u64>) {
50 let bar = Progress::new_bar(&format!("downloading {}", self.label), total);
51 *self.bar.borrow_mut() = Some(bar);
52 }
53
54 fn fetch_inc(&self, n: u64) {
55 if let Some(bar) = self.bar.borrow().as_ref() {
56 bar.inc(n);
57 }
58 }
59
60 fn phase(&self, name: &str) {
61 let mut slot = self.bar.borrow_mut();
64 if let Some(old) = slot.as_ref() {
65 old.clear();
66 }
67 *slot = Some(Progress::new_spinner(&format!("{name} {}", self.label)));
68 }
69}
70
71fn install_with_ui(
74 engine: &Engine,
75 tool: &str,
76 version: &str,
77 artifact: &vanta_core::Artifact,
78) -> VtaResult<StoreKey> {
79 let ui = InstallUi::new(format!("{tool} {version}"));
80 let key = engine.install_artifact_reported(tool, version, artifact, &ui)?;
81 ui.finish_ok(&format!("{tool} {version} → {key}"));
82 Ok(key)
83}
84
85pub fn run(args: &[String]) -> VtaResult<ExitCode> {
87 let cmd = args.first().map(String::as_str).unwrap_or("help");
88 let rest: &[String] = args.get(1..).unwrap_or(&[]);
89 if wants_banner(cmd) {
94 vanta_ui::banner(VERSION);
95 }
96 match cmd {
97 "--version" | "-V" | "version" => {
98 println!("vanta {VERSION}");
99 Ok(ExitCode::Ok)
100 }
101 "--help" | "-h" | "help" => {
102 print_help();
103 Ok(ExitCode::Ok)
104 }
105 "add" | "install" => cmd_add(rest),
106 "search" => cmd_search(rest),
107 "info" => cmd_info(rest),
108 "activate" => cmd_activate(rest),
109 "list" | "ls" => cmd_list(),
110 "which" => cmd_which(rest),
111 "doctor" => cmd_doctor(),
112 "sync" => cmd_sync(),
113 "generations" | "gen" => cmd_generations(),
114 "rollback" => cmd_rollback(rest),
115 "gc" => cmd_gc(),
116 "init" | "migrate" => cmd_import(has_flag(rest, "--force") || has_flag(rest, "-f")),
117 "exec" => cmd_exec(rest),
118 "x" => cmd_x(rest),
119 "remove" | "rm" => cmd_remove(rest),
120 "run" => cmd_run(rest),
121 "bundle" => cmd_bundle(rest),
122 "restore" => cmd_restore(rest),
123 "use" => cmd_add(rest),
124 "update" | "up" => cmd_sync(),
125 "outdated" => cmd_outdated(),
126 "cache" => cmd_cache(rest),
127 "config" => cmd_config(),
128 "completions" => cmd_completions(rest),
129 "trust" => cmd_trust(rest),
130 "registry" => cmd_registry(rest),
131 "shell" => cmd_shell(rest),
132 "self" => cmd_self(rest),
133 other => {
134 eprintln!("vanta: unknown command `{other}` (try `vanta help`)");
135 Ok(ExitCode::Usage)
136 }
137 }
138}
139
140fn cmd_add(rest: &[String]) -> VtaResult<ExitCode> {
142 let tools: Vec<&String> = rest.iter().filter(|a| !a.starts_with('-')).collect();
143 if tools.is_empty() {
144 eprintln!("usage: vanta add <tool>[@version] ...");
145 return Ok(ExitCode::Usage);
146 }
147
148 let registry = load_registry()?;
149 let resolver = Resolver::new(®istry);
150 let platform = Platform::current();
151
152 let mut resolutions = Vec::new();
154 for tool in &tools {
155 let request = Request::parse(tool)?;
156 resolutions.push(resolver.resolve(&request, &[platform])?);
157 }
158
159 let engine = open_engine()?;
161 for resolution in &resolutions {
162 let artifact = artifact_for(resolution, &platform).ok_or_else(|| {
163 VtaError::new(
164 Area::Res,
165 5,
166 format!(
167 "no artifact for `{}` on {}",
168 resolution.tool,
169 platform.token()
170 ),
171 )
172 })?;
173 install_with_ui(&engine, &resolution.tool, &resolution.version, artifact)?;
174 }
175 Ok(ExitCode::Ok)
176}
177
178fn cmd_search(rest: &[String]) -> VtaResult<ExitCode> {
180 let query = rest
181 .iter()
182 .find(|a| !a.starts_with('-'))
183 .cloned()
184 .unwrap_or_default();
185 let registry = load_registry()?;
186 for name in registry.search(&query) {
187 println!("{name}");
188 }
189 Ok(ExitCode::Ok)
190}
191
192fn cmd_info(rest: &[String]) -> VtaResult<ExitCode> {
194 let name = match rest.iter().find(|a| !a.starts_with('-')) {
195 Some(n) => n,
196 None => {
197 eprintln!("usage: vanta info <tool>");
198 return Ok(ExitCode::Usage);
199 }
200 };
201 let registry = load_registry()?;
202 let entry = registry
203 .tool(name)
204 .ok_or_else(|| VtaError::new(Area::Res, 3, format!("unknown tool `{name}`")))?;
205 println!("{name} (provider: {})", entry.provider.id);
206 if let Some(summary) = &entry.summary {
207 println!(" {summary}");
208 }
209 println!(" versions:");
210 for v in &entry.versions {
211 let chan = v.channel.as_deref().unwrap_or("");
212 println!(" {} {}", v.version, chan);
213 }
214 Ok(ExitCode::Ok)
215}
216
217fn cmd_activate(rest: &[String]) -> VtaResult<ExitCode> {
219 let shell = match rest.iter().find(|a| !a.starts_with('-')) {
220 Some(s) => s,
221 None => {
222 eprintln!("usage: vanta activate <bash|zsh|fish|pwsh>");
223 return Ok(ExitCode::Usage);
224 }
225 };
226 match vanta_env::activate_hook(shell) {
227 Some(hook) => {
228 print!("{hook}");
229 Ok(ExitCode::Ok)
230 }
231 None => {
232 eprintln!("vanta: unsupported shell `{shell}`");
233 Ok(ExitCode::Usage)
234 }
235 }
236}
237
238fn cmd_list() -> VtaResult<ExitCode> {
240 let engine = Engine::open(home()?)?;
241 match engine.state().current()? {
242 Some(id) => match engine.state().get_generation(id)? {
243 Some(gen) if !gen.tools.is_empty() => {
244 for (tool, key) in &gen.tools {
245 println!("{tool} ({key})");
246 }
247 }
248 _ => println!("(no tools installed)"),
249 },
250 None => println!("(no tools installed)"),
251 }
252 Ok(ExitCode::Ok)
253}
254
255fn cmd_which(rest: &[String]) -> VtaResult<ExitCode> {
257 let name = match rest.iter().find(|a| !a.starts_with('-')) {
258 Some(n) => n,
259 None => {
260 eprintln!("usage: vanta which <tool>");
261 return Ok(ExitCode::Usage);
262 }
263 };
264 let engine = Engine::open(home()?)?;
265 let id = engine
266 .state()
267 .current()?
268 .ok_or_else(|| VtaError::new(Area::Env, 2, format!("`{name}` is not active")))?;
269 let gen = engine
270 .state()
271 .get_generation(id)?
272 .ok_or_else(|| VtaError::new(Area::Env, 2, format!("`{name}` is not active")))?;
273 let (_, key) = gen
274 .tools
275 .iter()
276 .find(|(t, _)| t == name)
277 .ok_or_else(|| VtaError::new(Area::Env, 2, format!("`{name}` is not active")))?;
278 let store_key = StoreKey::new(key.clone())?;
279 println!("{}", engine.store().entry_path(&store_key).display());
280 Ok(ExitCode::Ok)
281}
282
283fn cmd_generations() -> VtaResult<ExitCode> {
285 let engine = Engine::open(home()?)?;
286 match engine.state().current()? {
287 None => println!("(no generations)"),
288 Some(current) => {
289 for id in 1..=current {
290 if let Some(gen) = engine.state().get_generation(id)? {
291 let mark = if id == current { "*" } else { " " };
292 println!("{mark} {id:04} {} [{}]", gen.command, gen.reason);
293 }
294 }
295 }
296 }
297 Ok(ExitCode::Ok)
298}
299
300fn cmd_rollback(rest: &[String]) -> VtaResult<ExitCode> {
302 let engine = Engine::open(home()?)?;
303 let current = engine
304 .state()
305 .current()?
306 .ok_or_else(|| VtaError::new(Area::Env, 2, "no generations to roll back".to_string()))?;
307 let target = match rest
308 .iter()
309 .find(|a| !a.starts_with('-'))
310 .and_then(|s| s.parse::<u64>().ok())
311 {
312 Some(n) => n,
313 None if current > 1 => current - 1,
314 None => {
315 return Err(VtaError::new(
316 Area::Env,
317 2,
318 "already at the earliest generation".to_string(),
319 ))
320 }
321 };
322 if engine.state().get_generation(target)?.is_none() {
323 return Err(VtaError::new(
324 Area::Env,
325 2,
326 format!("generation {target} not found"),
327 ));
328 }
329 engine.state().set_current(target)?;
330 println!("rolled back to generation {target:04}");
331 Ok(ExitCode::Ok)
332}
333
334fn cmd_gc() -> VtaResult<ExitCode> {
337 const RETAIN: u64 = 5;
338 let engine = Engine::open(home()?)?;
339 let mut roots: HashSet<StoreKey> = HashSet::new();
340 if let Some(current) = engine.state().current()? {
341 let start = current.saturating_sub(RETAIN - 1).max(1);
342 for id in start..=current {
343 if let Some(gen) = engine.state().get_generation(id)? {
344 for (_, key) in &gen.tools {
345 if let Ok(k) = StoreKey::new(key.clone()) {
346 roots.insert(k);
347 }
348 }
349 }
350 }
351 }
352 let removed = engine.store().gc(&roots)?;
353 println!(
354 "removed {removed} unreferenced store entr{}",
355 if removed == 1 { "y" } else { "ies" }
356 );
357 Ok(ExitCode::Ok)
358}
359
360fn cmd_doctor() -> VtaResult<ExitCode> {
362 let home = home()?;
363 let checks = vanta_diag::run(&home);
364 for c in &checks {
365 let mark = if c.ok { "✓" } else { "✗" };
366 println!("{mark} {} — {}", c.name, c.detail);
367 }
368 Ok(if vanta_diag::all_ok(&checks) {
369 ExitCode::Ok
370 } else {
371 ExitCode::Failure
372 })
373}
374
375fn cmd_sync() -> VtaResult<ExitCode> {
379 let manifest_path = find_manifest()?;
380 if !ensure_manifest_trusted(&manifest_path)? {
384 return Ok(ExitCode::Usage);
385 }
386 let manifest = vanta_config::load_file(&manifest_path)?;
387 if manifest.tools.is_empty() {
388 println!(
389 "nothing to sync ({} has no [tools])",
390 manifest_path.display()
391 );
392 return Ok(ExitCode::Ok);
393 }
394
395 let current = Platform::current();
398 let mut platforms: Vec<Platform> = manifest
399 .settings
400 .targets
401 .clone()
402 .unwrap_or_else(default_targets)
403 .iter()
404 .filter_map(|t| Platform::parse(t).ok())
405 .collect();
406 if !platforms.contains(¤t) {
407 platforms.push(current);
408 }
409
410 let registry = load_registry()?;
411 let resolver = Resolver::new(®istry);
412 let engine = open_engine()?;
413 let mut lock = vanta_lock::Lock::new(
414 format!("vanta {VERSION}"),
415 platforms.iter().map(|p| p.token()).collect(),
416 );
417
418 for (tool, spec) in &manifest.tools {
419 let request_str = spec.version().to_string();
420 let request = Request {
421 tool: tool.clone(),
422 version: VersionReq::parse(&request_str),
423 };
424 let resolution = resolver.resolve(&request, &platforms)?;
425
426 let current_artifact = artifact_for(&resolution, ¤t).ok_or_else(|| {
428 VtaError::new(
429 Area::Res,
430 5,
431 format!("no artifact for `{tool}` on {}", current.token()),
432 )
433 })?;
434 let key = install_with_ui(
435 &engine,
436 &resolution.tool,
437 &resolution.version,
438 current_artifact,
439 )?;
440
441 let mut platform_map = BTreeMap::new();
442 for (plat, art) in &resolution.per_platform {
443 let store_key = if *plat == current {
446 key.as_str().to_string()
447 } else {
448 String::new()
449 };
450 platform_map.insert(
451 plat.token(),
452 vanta_lock::PlatformPin {
453 store_key,
454 url: art.url.clone(),
455 size: art.size,
456 sha256: art.checksum.value.clone(),
457 blake3: None,
458 signature: art.signature.clone(),
459 bin: art.bin.clone(),
460 },
461 );
462 }
463 lock.tools.push(vanta_lock::LockedTool {
464 name: tool.clone(),
465 request: request_str,
466 version: resolution.version.clone(),
467 provider: resolution.provider.clone(),
468 platform: platform_map,
469 });
470 }
471
472 let lock_path = manifest_path
473 .parent()
474 .unwrap_or(Path::new("."))
475 .join("vanta.lock");
476 lock.write_file(&lock_path)?;
477 println!(
478 "✓ wrote {} ({} targets)",
479 lock_path.display(),
480 platforms.len()
481 );
482 Ok(ExitCode::Ok)
483}
484
485fn default_targets() -> Vec<String> {
487 [
488 "macos/aarch64",
489 "macos/x86_64",
490 "linux/x86_64/gnu",
491 "linux/aarch64/gnu",
492 "windows/x86_64",
493 ]
494 .iter()
495 .map(|s| s.to_string())
496 .collect()
497}
498
499fn find_manifest() -> VtaResult<PathBuf> {
501 let mut dir = std::env::current_dir()
502 .map_err(|e| VtaError::new(Area::Cfg, 1, format!("cannot read current directory: {e}")))?;
503 loop {
504 let candidate = dir.join("vanta.toml");
505 if candidate.is_file() {
506 return Ok(candidate);
507 }
508 if !dir.pop() {
509 return Err(VtaError::new(
510 Area::Cfg,
511 1,
512 "no vanta.toml found in this directory or any parent".to_string(),
513 ));
514 }
515 }
516}
517
518fn cmd_exec(rest: &[String]) -> VtaResult<ExitCode> {
520 let cmdv: &[String] = match rest.iter().position(|a| a == "--") {
521 Some(i) => &rest[i + 1..],
522 None => rest,
523 };
524 if cmdv.is_empty() {
525 eprintln!("usage: vanta exec -- <command> [args]");
526 return Ok(ExitCode::Usage);
527 }
528 run_header(&cmdv[0], &cmdv[1..]);
529 let status = Command::new(&cmdv[0])
530 .args(&cmdv[1..])
531 .env("PATH", env_path_with_bin()?)
532 .status()
533 .map_err(|e| VtaError::new(Area::Env, 1, format!("running {}: {e}", cmdv[0])))?;
534 Ok(status_exit(status))
535}
536
537fn cmd_x(rest: &[String]) -> VtaResult<ExitCode> {
539 let spec = match rest.iter().find(|a| !a.starts_with('-')) {
540 Some(s) => s.clone(),
541 None => {
542 eprintln!("usage: vanta x <tool>[@version] [args]");
543 return Ok(ExitCode::Usage);
544 }
545 };
546 let request = Request::parse(&spec)?;
547 let registry = load_registry()?;
548 let resolver = Resolver::new(®istry);
549 let platform = Platform::current();
550 let resolution = resolver.resolve(&request, &[platform])?;
551 let engine = open_engine()?;
552 let artifact = artifact_for(&resolution, &platform).ok_or_else(|| {
553 VtaError::new(
554 Area::Res,
555 5,
556 format!("no artifact for `{}`", resolution.tool),
557 )
558 })?;
559 install_with_ui(&engine, &resolution.tool, &resolution.version, artifact)?;
560
561 let idx = rest.iter().position(|a| a == &spec).unwrap_or(0);
562 let args: &[String] = rest.get(idx + 1..).unwrap_or(&[]);
563 let tool_bin = home()?.join("bin").join(&resolution.tool);
564 run_header(&resolution.tool, args);
565 let status = Command::new(&tool_bin)
566 .args(args)
567 .env("PATH", env_path_with_bin()?)
568 .status()
569 .map_err(|e| VtaError::new(Area::Env, 1, format!("running {}: {e}", resolution.tool)))?;
570 Ok(status_exit(status))
571}
572
573fn cmd_remove(rest: &[String]) -> VtaResult<ExitCode> {
575 let tool = match rest.iter().find(|a| !a.starts_with('-')) {
576 Some(t) => t,
577 None => {
578 eprintln!("usage: vanta remove <tool>");
579 return Ok(ExitCode::Usage);
580 }
581 };
582 let engine = Engine::open(home()?)?;
583 if engine.remove(tool)? {
584 println!("removed {tool}");
585 Ok(ExitCode::Ok)
586 } else {
587 Err(VtaError::new(
588 Area::Env,
589 2,
590 format!("`{tool}` is not installed"),
591 ))
592 }
593}
594
595fn cmd_run(rest: &[String]) -> VtaResult<ExitCode> {
597 let name = match rest.iter().find(|a| !a.starts_with('-')) {
598 Some(n) => n.clone(),
599 None => {
600 eprintln!("usage: vanta run <task|tool> [args]");
601 return Ok(ExitCode::Usage);
602 }
603 };
604 if let Ok(manifest_path) = find_manifest() {
606 if let Ok(manifest) = vanta_config::load_file(&manifest_path) {
607 if let Some(task) = manifest.tasks.get(&name) {
608 if !ensure_manifest_trusted(&manifest_path)? {
612 return Ok(ExitCode::Usage);
613 }
614 let cmd = match task {
615 vanta_config::model::Task::Command(s) => s.clone(),
616 vanta_config::model::Task::Detailed(d) => d.run.clone(),
617 };
618 vanta_ui::running(&cmd);
619 let status = shell_command(&cmd)
620 .env("PATH", env_path_with_bin()?)
621 .status()
622 .map_err(|e| {
623 VtaError::new(Area::Env, 1, format!("running task `{name}`: {e}"))
624 })?;
625 return Ok(status_exit(status));
626 }
627 }
628 }
629 let idx = rest.iter().position(|a| a == &name).unwrap_or(0);
630 let args: &[String] = rest.get(idx + 1..).unwrap_or(&[]);
631 let tool_bin = home()?.join("bin").join(&name);
632 run_header(&name, args);
633 let status = Command::new(&tool_bin)
634 .args(args)
635 .env("PATH", env_path_with_bin()?)
636 .status()
637 .map_err(|e| VtaError::new(Area::Env, 1, format!("running `{name}`: {e}")))?;
638 Ok(status_exit(status))
639}
640
641fn cmd_bundle(rest: &[String]) -> VtaResult<ExitCode> {
643 let out = rest
644 .iter()
645 .position(|a| a == "--out")
646 .and_then(|i| rest.get(i + 1))
647 .cloned()
648 .unwrap_or_else(|| "vanta-bundle.vbundle".to_string());
649 let engine = Engine::open(home()?)?;
650 let progress = Progress::new_spinner(&format!("bundling active generation → {out}"));
651 let n = match engine.bundle_current(Path::new(&out)) {
652 Ok(n) => n,
653 Err(e) => {
654 progress.finish_err("bundle failed");
655 return Err(e);
656 }
657 };
658 progress.finish_ok(&format!("bundled {n} store entries → {out}"));
659 Ok(ExitCode::Ok)
660}
661
662fn cmd_restore(rest: &[String]) -> VtaResult<ExitCode> {
664 let file = match rest.iter().find(|a| !a.starts_with('-')) {
665 Some(f) => f,
666 None => {
667 eprintln!("usage: vanta restore <file>");
668 return Ok(ExitCode::Usage);
669 }
670 };
671 let engine = Engine::open(home()?)?;
672 let progress = Progress::new_spinner(&format!("restoring bundle {file}"));
673 let n = match engine.restore(Path::new(file)) {
674 Ok(n) => n,
675 Err(e) => {
676 progress.finish_err("restore failed");
677 return Err(e);
678 }
679 };
680 progress.finish_ok(&format!("restored {n} store entries"));
681 Ok(ExitCode::Ok)
682}
683
684#[allow(clippy::print_literal)] fn cmd_outdated() -> VtaResult<ExitCode> {
687 let manifest_path = find_manifest()?;
688 let manifest = vanta_config::load_file(&manifest_path)?;
689 let registry = load_registry()?;
690 let resolver = Resolver::new(®istry);
691 let platform = Platform::current();
692
693 let lock_path = manifest_path
694 .parent()
695 .unwrap_or(Path::new("."))
696 .join("vanta.lock");
697 let locked: BTreeMap<String, String> = if lock_path.exists() {
698 vanta_lock::Lock::load_file(&lock_path)
699 .map(|l| l.tools.into_iter().map(|t| (t.name, t.version)).collect())
700 .unwrap_or_default()
701 } else {
702 BTreeMap::new()
703 };
704
705 println!(
706 "{:<16} {:<12} {:<12} {}",
707 "tool", "current", "allowed", "latest"
708 );
709 for (tool, spec) in &manifest.tools {
710 let allowed = resolver
711 .resolve(
712 &Request {
713 tool: tool.clone(),
714 version: VersionReq::parse(spec.version()),
715 },
716 &[platform],
717 )
718 .map(|r| r.version)
719 .unwrap_or_else(|_| "-".to_string());
720 let latest = resolver
721 .resolve(
722 &Request {
723 tool: tool.clone(),
724 version: VersionReq::Latest,
725 },
726 &[platform],
727 )
728 .map(|r| r.version)
729 .unwrap_or_else(|_| "-".to_string());
730 let current = locked.get(tool).cloned().unwrap_or_else(|| "-".to_string());
731 println!("{tool:<16} {current:<12} {allowed:<12} {latest}");
732 }
733 Ok(ExitCode::Ok)
734}
735
736fn cmd_cache(rest: &[String]) -> VtaResult<ExitCode> {
738 let sub = rest
739 .iter()
740 .find(|a| !a.starts_with('-'))
741 .map(|s| s.as_str())
742 .unwrap_or("stats");
743 let downloads = home()?.join("cache").join("downloads");
744 match sub {
745 "prune" => {
746 let mut n = 0;
747 if let Ok(rd) = std::fs::read_dir(&downloads) {
748 for e in rd.flatten() {
749 if std::fs::remove_file(e.path()).is_ok() {
750 n += 1;
751 }
752 }
753 }
754 println!("pruned {n} cached downloads");
755 }
756 _ => {
757 let (mut files, mut bytes) = (0u64, 0u64);
758 if let Ok(rd) = std::fs::read_dir(&downloads) {
759 for e in rd.flatten() {
760 if let Ok(m) = e.metadata() {
761 if m.is_file() {
762 files += 1;
763 bytes += m.len();
764 }
765 }
766 }
767 }
768 println!("download cache: {files} files, {} KB", bytes / 1024);
769 }
770 }
771 Ok(ExitCode::Ok)
772}
773
774fn cmd_config() -> VtaResult<ExitCode> {
776 let path = home()?.join("config.toml");
777 println!("config: {}", path.display());
778 match std::fs::read_to_string(&path) {
779 Ok(contents) => {
780 println!("---");
781 print!("{contents}");
782 }
783 Err(_) => println!("(no global config; create it to set [tools]/[settings])"),
784 }
785 Ok(ExitCode::Ok)
786}
787
788fn cmd_completions(rest: &[String]) -> VtaResult<ExitCode> {
790 let shell = rest
791 .iter()
792 .find(|a| !a.starts_with('-'))
793 .map(|s| s.as_str())
794 .unwrap_or("bash");
795 let cmds = "add remove update sync list which search info outdated init migrate doctor activate gc rollback generations run exec x bundle restore cache config completions use";
796 match shell {
797 "bash" => println!("complete -W \"{cmds}\" vanta vt"),
798 "zsh" => println!("#compdef vanta vt\n_values 'vanta command' {cmds}"),
799 "fish" => {
800 for c in cmds.split(' ') {
801 println!("complete -c vanta -a {c}");
802 }
803 }
804 other => {
805 eprintln!("vanta: no completions for `{other}`");
806 return Ok(ExitCode::Usage);
807 }
808 }
809 Ok(ExitCode::Ok)
810}
811
812fn cmd_trust(rest: &[String]) -> VtaResult<ExitCode> {
814 let trust_dir = home()?.join("trust");
815 if has_flag(rest, "--list") {
816 match std::fs::read_dir(&trust_dir) {
817 Ok(rd) => {
818 for e in rd.flatten() {
819 if let Ok(target) = std::fs::read_to_string(e.path()) {
820 println!("{} {}", e.file_name().to_string_lossy(), target);
821 }
822 }
823 }
824 Err(_) => println!("(nothing trusted yet)"),
825 }
826 return Ok(ExitCode::Ok);
827 }
828 let path = match rest.iter().find(|a| !a.starts_with('-')) {
829 Some(p) => PathBuf::from(p),
830 None => find_manifest()?,
831 };
832 let hash = vanta_security::sha256_file(&path)?;
833 std::fs::create_dir_all(&trust_dir)
834 .map_err(|e| VtaError::new(Area::Vrf, 3, format!("trust dir: {e}")))?;
835 std::fs::write(trust_dir.join(&hash), path.display().to_string())
836 .map_err(|e| VtaError::new(Area::Vrf, 3, format!("recording trust: {e}")))?;
837 println!("trusted {} ({hash})", path.display());
838 Ok(ExitCode::Ok)
839}
840
841fn manifest_is_trusted(trust_dir: &Path, hash: &str) -> bool {
843 trust_dir.join(hash).is_file()
844}
845
846fn ensure_manifest_trusted(manifest_path: &Path) -> VtaResult<bool> {
854 use std::io::IsTerminal;
855 let trust_dir = home()?.join("trust");
856 let hash = vanta_security::sha256_file(manifest_path)?;
857 if manifest_is_trusted(&trust_dir, &hash) {
858 return Ok(true);
859 }
860 let assume = matches!(
861 std::env::var("VANTA_ASSUME_TRUST").ok().as_deref(),
862 Some("1") | Some("true") | Some("yes")
863 );
864 let approved = if assume {
865 true
866 } else if std::io::stdin().is_terminal() {
867 eprint!(
868 "vanta: {} is not trusted. Trust it and continue? [y/N] ",
869 manifest_path.display()
870 );
871 use std::io::Write;
872 let _ = std::io::stderr().flush();
873 let mut line = String::new();
874 std::io::stdin().read_line(&mut line).ok();
875 matches!(line.trim().to_ascii_lowercase().as_str(), "y" | "yes")
876 } else {
877 false
878 };
879 if approved {
880 std::fs::create_dir_all(&trust_dir)
881 .map_err(|e| VtaError::new(Area::Vrf, 3, format!("trust dir: {e}")))?;
882 let _ = std::fs::write(trust_dir.join(&hash), manifest_path.display().to_string());
883 Ok(true)
884 } else {
885 eprintln!(
886 "vanta: refusing to use untrusted manifest {} \
887 (run `vanta trust` to approve it)",
888 manifest_path.display()
889 );
890 Ok(false)
891 }
892}
893
894fn cmd_registry(rest: &[String]) -> VtaResult<ExitCode> {
896 let nonflags: Vec<&String> = rest.iter().filter(|a| !a.starts_with('-')).collect();
897 let cfg = home()?.join("config.toml");
898 match nonflags.first().map(|s| s.as_str()) {
899 Some("add") => {
900 if nonflags.len() < 3 {
901 eprintln!("usage: vanta registry add <name> <url>");
902 return Ok(ExitCode::Usage);
903 }
904 let (name, url) = (nonflags[1], nonflags[2]);
905 let block = format!("\n[registries.{name}]\nurl = \"{url}\"\n");
906 if let Some(parent) = cfg.parent() {
907 let _ = std::fs::create_dir_all(parent);
908 }
909 let mut existing = std::fs::read_to_string(&cfg).unwrap_or_default();
910 existing.push_str(&block);
911 std::fs::write(&cfg, existing)
912 .map_err(|e| VtaError::new(Area::Cfg, 1, format!("writing config: {e}")))?;
913 println!("added registry {name} → {url}");
914 Ok(ExitCode::Ok)
915 }
916 _ => {
917 if cfg.exists() {
918 let manifest = vanta_config::load_file(&cfg)?;
919 if manifest.registries.is_empty() {
920 println!("(no registries configured; the official registry is used)");
921 } else {
922 for (name, reg) in &manifest.registries {
923 println!("{name} {}", reg.url);
924 }
925 }
926 } else {
927 println!("(no config; the official registry is used by default)");
928 }
929 Ok(ExitCode::Ok)
930 }
931 }
932}
933
934fn cmd_shell(rest: &[String]) -> VtaResult<ExitCode> {
937 let specs: Vec<&String> = rest.iter().filter(|a| !a.starts_with('-')).collect();
938 if specs.is_empty() {
939 eprintln!("usage: vanta shell <tool>[@version] ...");
940 return Ok(ExitCode::Usage);
941 }
942 let registry = load_registry()?;
943 let resolver = Resolver::new(®istry);
944 let platform = Platform::current();
945 let engine = open_engine()?;
946 for spec in &specs {
947 let request = Request::parse(spec)?;
948 let resolution = resolver.resolve(&request, &[platform])?;
949 let artifact = artifact_for(&resolution, &platform).ok_or_else(|| {
950 VtaError::new(
951 Area::Res,
952 5,
953 format!("no artifact for `{}`", resolution.tool),
954 )
955 })?;
956 install_with_ui(&engine, &resolution.tool, &resolution.version, artifact)?;
957 }
958 let shell = std::env::var("SHELL").unwrap_or_else(|_| {
959 if cfg!(windows) {
960 "cmd".to_string()
961 } else {
962 "/bin/sh".to_string()
963 }
964 });
965 vanta_ui::running(&format!(
966 "{shell} (vanta subshell with {} tool(s); type `exit` to leave)",
967 specs.len()
968 ));
969 let status = Command::new(shell)
970 .env("PATH", env_path_with_bin()?)
971 .status()
972 .map_err(|e| VtaError::new(Area::Env, 1, format!("starting subshell: {e}")))?;
973 Ok(status_exit(status))
974}
975
976fn cmd_self(rest: &[String]) -> VtaResult<ExitCode> {
978 match rest
979 .iter()
980 .find(|a| !a.starts_with('-'))
981 .map(|s| s.as_str())
982 {
983 Some("uninstall") => {
984 let h = home()?;
985 if !has_flag(rest, "--yes") {
986 eprintln!(
987 "this will permanently remove {} — re-run with --yes",
988 h.display()
989 );
990 return Ok(ExitCode::Usage);
991 }
992 std::fs::remove_dir_all(&h).map_err(|e| {
993 VtaError::new(Area::Sys, 2, format!("removing {}: {e}", h.display()))
994 })?;
995 println!("removed {}", h.display());
996 Ok(ExitCode::Ok)
997 }
998 Some("update") => {
999 println!(
1000 "self-update is handled by the channel you installed from; \
1001 see docs/32-release-engineering.md"
1002 );
1003 Ok(ExitCode::Ok)
1004 }
1005 _ => {
1006 eprintln!("usage: vanta self <uninstall|update>");
1007 Ok(ExitCode::Usage)
1008 }
1009 }
1010}
1011
1012fn env_path_with_bin() -> VtaResult<String> {
1013 let bin = home()?.join("bin");
1014 let sep = if cfg!(windows) { ';' } else { ':' };
1015 Ok(format!(
1016 "{}{}{}",
1017 bin.display(),
1018 sep,
1019 std::env::var("PATH").unwrap_or_default()
1020 ))
1021}
1022
1023fn run_header(program: &str, args: &[String]) {
1026 let mut line = program.to_string();
1027 if !args.is_empty() {
1028 line.push(' ');
1029 line.push_str(&args.join(" "));
1030 }
1031 vanta_ui::running(&line);
1032}
1033
1034fn shell_command(cmd: &str) -> Command {
1035 if cfg!(windows) {
1036 let mut c = Command::new("cmd");
1037 c.arg("/C").arg(cmd);
1038 c
1039 } else {
1040 let mut c = Command::new("sh");
1041 c.arg("-c").arg(cmd);
1042 c
1043 }
1044}
1045
1046fn status_exit(status: std::process::ExitStatus) -> ExitCode {
1047 if status.success() {
1048 ExitCode::Ok
1049 } else {
1050 ExitCode::Failure
1051 }
1052}
1053
1054fn cmd_import(force: bool) -> VtaResult<ExitCode> {
1057 let cwd = std::env::current_dir()
1058 .map_err(|e| VtaError::new(Area::Cfg, 1, format!("cannot read current directory: {e}")))?;
1059 let imported = vanta_migrate::import_dir(&cwd);
1060 if imported.is_empty() {
1061 println!("no version files detected in {}", cwd.display());
1062 return Ok(ExitCode::Ok);
1063 }
1064 let target = cwd.join("vanta.toml");
1065 if target.exists() && !force {
1066 eprintln!("vanta.toml already exists (use --force to overwrite)");
1067 return Ok(ExitCode::Usage);
1068 }
1069 println!("detected:");
1070 for i in &imported {
1071 println!(" {} = \"{}\" (from {})", i.tool, i.version, i.source);
1072 }
1073 let body = vanta_migrate::to_manifest_toml(&imported);
1074 std::fs::write(&target, body)
1075 .map_err(|e| VtaError::new(Area::Cfg, 1, format!("writing {}: {e}", target.display())))?;
1076 println!(
1077 "✓ wrote {} — run `vanta sync` to install + lock",
1078 target.display()
1079 );
1080 Ok(ExitCode::Ok)
1081}
1082
1083fn has_flag(rest: &[String], flag: &str) -> bool {
1084 rest.iter().any(|a| a == flag)
1085}
1086
1087fn wants_banner(cmd: &str) -> bool {
1093 !matches!(
1094 cmd,
1095 "--version"
1096 | "-V"
1097 | "version"
1098 | "--help"
1099 | "-h"
1100 | "help"
1101 | "completions"
1102 | "activate"
1103 | "which"
1104 | "exec"
1105 )
1106}
1107
1108fn home() -> VtaResult<PathBuf> {
1110 if let Ok(h) = std::env::var("VANTA_HOME") {
1111 return Ok(PathBuf::from(h));
1112 }
1113 let base = std::env::var("HOME")
1114 .or_else(|_| std::env::var("USERPROFILE"))
1115 .map_err(|_| {
1116 VtaError::new(
1117 Area::Sys,
1118 2,
1119 "cannot determine home directory; set VANTA_HOME".to_string(),
1120 )
1121 })?;
1122 Ok(PathBuf::from(base).join(".vanta"))
1123}
1124
1125const REGISTRY_MAX_BYTES: u64 = 64 * 1024 * 1024; const DEFAULT_REGISTRY_URL: &str =
1134 "https://raw.githubusercontent.com/squaretick/vanta/main/registry/registry.toml";
1135
1136fn registry_insecure_optin() -> bool {
1142 matches!(
1143 std::env::var("VANTA_INSECURE_REGISTRY").ok().as_deref(),
1144 Some("1") | Some("true") | Some("yes")
1145 )
1146}
1147
1148fn load_registry() -> VtaResult<Registry> {
1162 let roots = vanta_security::trust::load_root_keys(&home()?.join("trust"));
1163 match std::env::var("VANTA_REGISTRY") {
1164 Ok(loc) if loc.starts_with("http://") || loc.starts_with("https://") => {
1165 fetch_signed_index(&loc, roots)
1166 }
1167 Ok(path) => {
1168 let mut registry = Registry::load_file(Path::new(&path))?;
1170 registry.index_verified = true;
1171 registry.trusted_root_keys = roots;
1172 Ok(registry)
1173 }
1174 Err(_) => {
1175 match fetch_signed_index(DEFAULT_REGISTRY_URL, roots) {
1180 Ok(registry) => Ok(registry),
1181 Err(e) => {
1182 eprintln!(
1183 "vanta: WARNING — could not load the official registry \
1184 ({DEFAULT_REGISTRY_URL}): {e}. Falling back to the empty \
1185 built-in index. Set $VANTA_REGISTRY to a reachable signed \
1186 https URL or a local file path to override."
1187 );
1188 Ok(Registry::builtin())
1189 }
1190 }
1191 }
1192 }
1193}
1194
1195fn fetch_signed_index(loc: &str, roots: Vec<String>) -> VtaResult<Registry> {
1199 let insecure = registry_insecure_optin();
1200 if loc.starts_with("http://") && !insecure {
1201 return Err(VtaError::new(
1202 Area::Reg,
1203 5,
1204 format!(
1205 "refusing plaintext http registry {loc} (https required; \
1206 set VANTA_INSECURE_REGISTRY=1 to override — DANGEROUS)"
1207 ),
1208 ));
1209 }
1210 let downloader = if insecure {
1211 vanta_net::Downloader::insecure()?
1212 } else {
1213 vanta_net::Downloader::new()?
1214 };
1215 let pid = std::process::id();
1216 let tmp = std::env::temp_dir().join(format!("vanta-registry-{pid}.toml"));
1217 let progress = Progress::new_bar("fetching registry index", None);
1220 let dl = downloader.download_capped_with_progress(
1221 loc,
1222 &tmp,
1223 Some(REGISTRY_MAX_BYTES),
1224 Some(&|n| progress.inc(n)),
1225 );
1226 if let Err(e) = dl {
1227 progress.finish_err("registry index download failed");
1228 return Err(e);
1229 }
1230 progress.finish_ok("fetched registry index");
1231 let index_bytes = std::fs::read(&tmp)
1232 .map_err(|e| VtaError::new(Area::Reg, 1, format!("reading index: {e}")))?;
1233
1234 let sig_url = format!("{loc}.minisig");
1236 let sig_tmp = std::env::temp_dir().join(format!("vanta-registry-{pid}.minisig"));
1237 let signature = downloader
1238 .download_capped(&sig_url, &sig_tmp, Some(1024 * 1024))
1239 .ok()
1240 .and_then(|_| std::fs::read_to_string(&sig_tmp).ok());
1241 let _ = std::fs::remove_file(&sig_tmp);
1242 let index_verified = signature
1243 .as_deref()
1244 .map(|s| vanta_security::trust::index_signed_by_root(&index_bytes, s, &roots))
1245 .unwrap_or(false);
1246
1247 if !index_verified && !insecure {
1248 let _ = std::fs::remove_file(&tmp);
1249 return Err(VtaError::new(
1250 Area::Reg,
1251 6,
1252 format!(
1253 "registry index {loc} is not signed by a pinned trust root \
1254 (expected detached signature at {loc}.minisig). Refusing to trust it. \
1255 Add a root to ~/.vanta/trust/roots.toml, or set \
1256 VANTA_INSECURE_REGISTRY=1 to override — DANGEROUS."
1257 ),
1258 ));
1259 }
1260 if insecure && !index_verified {
1261 eprintln!(
1262 "vanta: WARNING — using unverified registry {loc} (VANTA_INSECURE_REGISTRY). \
1263 Per-artifact signing keys will be treated as untrusted."
1264 );
1265 }
1266
1267 let src = String::from_utf8(index_bytes)
1268 .map_err(|e| VtaError::new(Area::Reg, 2, format!("index is not UTF-8: {e}")))?;
1269 let _ = std::fs::remove_file(&tmp);
1270 let mut registry = Registry::from_toml(&src)?;
1271 registry.index_verified = index_verified;
1272 registry.trusted_root_keys = roots;
1273 Ok(registry)
1274}
1275
1276fn install_policy() -> vanta_security::Policy {
1282 let mut policy = vanta_security::Policy::default();
1283 let mut verify: Option<String> = None;
1284 if let Ok(h) = home() {
1285 if let Ok(m) = vanta_config::load_file(&h.join("config.toml")) {
1286 verify = m.settings.verify;
1287 }
1288 }
1289 if let Ok(path) = find_manifest() {
1290 if let Ok(m) = vanta_config::load_file(&path) {
1291 if m.settings.verify.is_some() {
1292 verify = m.settings.verify;
1293 }
1294 }
1295 }
1296 if let Some(v) = verify {
1297 if matches!(
1298 v.to_ascii_lowercase().as_str(),
1299 "require" | "required" | "signature" | "strict"
1300 ) {
1301 policy.require_signature = true;
1302 }
1303 }
1304 policy
1305}
1306
1307fn open_engine() -> VtaResult<Engine> {
1309 Engine::open_with_policy(home()?, install_policy())
1310}
1311
1312fn print_help() {
1313 println!(
1314 "vanta — every developer tool, one command\n\
1315 \n\
1316 USAGE:\n vanta <command> [args]\n\
1317 \n\
1318 COMMON COMMANDS:\n\
1319 \x20 add <tool>[@ver] resolve and install a tool (alias: install)\n\
1320 \x20 search <query> search the registry\n\
1321 \x20 info <tool> show a tool's versions\n\
1322 \x20 remove <tool> remove a tool\n\
1323 \x20 update [tool] update within constraints\n\
1324 \x20 sync reconcile to vanta.toml + vanta.lock\n\
1325 \x20 doctor diagnose the installation\n\
1326 \n\
1327 REGISTRY:\n\
1328 \x20 By default vanta uses the official, minisign-signed registry\n\
1329 \x20 (verified against a pinned root key). Override the source with\n\
1330 \x20 $VANTA_REGISTRY — an https:// URL (must carry a <url>.minisig\n\
1331 \x20 signed by a pinned root) or a local file path (trusted as-is).\n\
1332 \n\
1333 See docs/04-cli.md for the full reference."
1334 );
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use super::*;
1340
1341 #[test]
1342 fn version_ok() {
1343 assert_eq!(run(&["--version".into()]).unwrap(), ExitCode::Ok);
1344 }
1345
1346 #[test]
1347 fn unknown_is_usage() {
1348 assert_eq!(run(&["frobnicate".into()]).unwrap(), ExitCode::Usage);
1349 }
1350
1351 #[test]
1352 fn add_no_args_is_usage() {
1353 assert_eq!(run(&["add".into()]).unwrap(), ExitCode::Usage);
1354 }
1355
1356 fn use_empty_registry() {
1359 let p = std::env::temp_dir().join(format!("vanta-empty-reg-{}.toml", std::process::id()));
1360 std::fs::write(&p, "").unwrap();
1361 std::env::set_var("VANTA_REGISTRY", &p);
1362 }
1363
1364 #[test]
1365 fn add_unknown_tool_resolves_to_error() {
1366 use_empty_registry();
1368 let err = run(&["add".into(), "totally-unknown-tool".into()]).unwrap_err();
1369 assert_eq!(err.area, Area::Res);
1370 }
1371
1372 #[test]
1373 fn search_succeeds() {
1374 use_empty_registry();
1375 assert_eq!(
1376 run(&["search".into(), "node".into()]).unwrap(),
1377 ExitCode::Ok
1378 );
1379 }
1380
1381 #[test]
1384 fn trust_list_gates_untrusted_manifest() {
1385 let dir = std::env::temp_dir().join(format!("vanta-cli-trust-{}", std::process::id()));
1386 let _ = std::fs::remove_dir_all(&dir);
1387 std::fs::create_dir_all(&dir).unwrap();
1388 let hash = "a".repeat(64);
1389 assert!(!manifest_is_trusted(&dir, &hash));
1391 std::fs::write(dir.join(&hash), "manifest path").unwrap();
1393 assert!(manifest_is_trusted(&dir, &hash));
1394 assert!(!manifest_is_trusted(&dir, &"b".repeat(64)));
1396 let _ = std::fs::remove_dir_all(&dir);
1397 }
1398}