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
//! # Module: install
//!
//! ## Spec
//! - `check_prereqs()`: checks for `tmux` and `claude` in `PATH` via `which`, printing `ok` or
//! `MISSING` with an install hint for each. Never fails — warnings only.
//! - `run(editor, skip_prereqs, skip_plugins)`: orchestrates the full install workflow.
//! - Runs `check_prereqs()` unless `skip_prereqs` is set.
//! - Skips plugin installation when `skip_plugins` is set.
//! - If `editor` is given, installs only for that editor; otherwise auto-detects installed editors
//! via `detect_editors()`.
//! - `detect_editors()` (private): detects JetBrains by checking
//! `~/.local/share/JetBrains/` (Linux) or `/Applications/IntelliJ*` (macOS); detects VS Code
//! family by probing `cursor`, `codium`, or `code` in PATH.
//! - For each detected editor, calls `crate::plugin::install(editor)` and collects
//! installed/failed lists.
//! - Prints a summary of installed and failed plugins to stderr.
//!
//! ## Agentic Contracts
//! - `run` always returns `Ok(())`; individual plugin failures are logged but do not propagate.
//! - `check_prereqs` is side-effect-free beyond stderr output.
//! - When no editors are detected and none is specified, installation is skipped with a hint to use
//! `--editor`.
//!
//! ## Evals
//! - check_prereqs_runs_without_panic: calling `check_prereqs()` on any system completes without panic
//! - run_skip_plugins: `skip_plugins=true` → no `plugin::install` called, returns Ok
//! - run_explicit_editor: `editor=Some("jetbrains")` → only JetBrains plugin install attempted
//! - run_no_editors_detected: empty PATH + no JetBrains dirs → skips plugin install, returns Ok
use anyhow::Result;
use std::path::PathBuf;
/// Check if a binary exists in PATH using `which`.
fn which(bin: &str) -> bool {
std::process::Command::new("which")
.arg(bin)
.output()
.is_ok_and(|o| o.status.success())
}
/// Check prerequisites and print status. Does not fail — only warns.
pub fn check_prereqs() {
let prereqs = [
("tmux", "Install tmux: https://github.com/tmux/tmux/wiki/Installing"),
("claude", "Install Claude Code CLI: https://docs.anthropic.com/en/docs/claude-code"),
];
for (bin, install_hint) in &prereqs {
if which(bin) {
eprintln!("[install] {} ... ok", bin);
} else {
eprintln!("[install] {} ... MISSING", bin);
eprintln!("[install] hint: {}", install_hint);
}
}
}
/// Detect which editors are installed and return their names.
fn detect_editors() -> Vec<&'static str> {
let mut editors = Vec::new();
// JetBrains: check for ~/.local/share/JetBrains/ (Linux) or /Applications/IntelliJ* (macOS)
let jetbrains_found = {
let home = std::env::var("HOME").unwrap_or_default();
let linux_path = PathBuf::from(&home).join(".local/share/JetBrains");
let macos_path = PathBuf::from("/Applications");
let linux_ok = linux_path.is_dir()
&& std::fs::read_dir(&linux_path)
.map(|mut d| d.next().is_some())
.unwrap_or(false);
let macos_ok = macos_path.is_dir()
&& std::fs::read_dir(&macos_path)
.map(|d| {
d.flatten()
.any(|e| e.file_name().to_string_lossy().starts_with("IntelliJ"))
})
.unwrap_or(false);
linux_ok || macos_ok
};
if jetbrains_found {
editors.push("jetbrains");
}
// VS Code / Cursor / Codium
if which("cursor") || which("codium") || which("code") {
editors.push("vscode");
}
editors
}
/// Main entry point for `agent-doc install`.
pub fn run(editor: Option<&str>, skip_prereqs: bool, skip_plugins: bool) -> Result<()> {
if !skip_prereqs {
check_prereqs();
}
if skip_plugins {
eprintln!("[install] Skipping plugin installation (--skip-plugins).");
return Ok(());
}
let editors_to_install: Vec<&str> = if let Some(e) = editor {
vec![e]
} else {
detect_editors()
};
if editors_to_install.is_empty() {
eprintln!("[install] No supported editors detected. Skipping plugin installation.");
eprintln!("[install] To install manually: agent-doc install --editor jetbrains|vscode");
return Ok(());
}
let mut installed = Vec::new();
let mut failed = Vec::new();
for ed in &editors_to_install {
eprintln!("[install] Installing plugin for {} ...", ed);
match crate::plugin::install(ed) {
Ok(()) => installed.push(*ed),
Err(e) => {
eprintln!("[install] Plugin install failed for {}: {:#}", ed, e);
failed.push(*ed);
}
}
}
// Summary
eprintln!("[install] ---");
if !installed.is_empty() {
eprintln!("[install] Installed plugins: {}", installed.join(", "));
}
if !failed.is_empty() {
eprintln!("[install] Failed plugins: {}", failed.join(", "));
}
if installed.is_empty() && failed.is_empty() {
eprintln!("[install] Nothing to install.");
}
Ok(())
}