Skip to main content

dev_prune/commands/
setup.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Handler for `dev-prune setup`.
5//
6// The same pass dev-prune runs by itself after an install or an upgrade, available to
7// run by hand — which is what the installer scripts do, and what the error messages
8// point people at when an integration was skipped.
9
10use anyhow::Result;
11
12use crate::commands::hook::{self, HookState};
13use crate::config::Registry;
14use crate::daemon;
15use crate::output;
16use crate::setup;
17
18pub fn run(status_only: bool) -> Result<()> {
19    if status_only {
20        return run_status();
21    }
22
23    output::print_header("dev-prune setup");
24    let registry = Registry::load()?;
25    let report = setup::ensure_integrations(&registry, setup::Consent::Explicit);
26    report.print(true);
27    setup::suppress_next_auto_setup();
28    // Asking for the pass by name settles the first-run question too — without this, a
29    // machine that started with `devp setup` would still be asked "may dev-prune set
30    // itself up?" on the next upgrade, about integrations it already has.
31    setup::record_consent_granted();
32
33    println!();
34    if report.needs_attention() {
35        output::print_info("Anything skipped above is optional — dev-prune works without it.");
36    } else {
37        output::print_success("Everything dev-prune needs is in place.");
38    }
39    output::print_info("Remove all of it again with `devp uninstall`.");
40
41    setup::offer_vscode_extension();
42
43    // Installing dev-prune tracks nothing on its own, and a first-time user who stops here
44    // gets an empty `devp status` with no hint about why. The installer scripts say this
45    // too, but `cargo install`, `npm i -g` and `pipx install` never run one — `devp setup`
46    // is the only step every channel has in common.
47    if registry.repositories.is_empty() {
48        println!();
49        output::print_info("No repositories are tracked yet. Register them either way:");
50        println!(
51            "    devp init {}  # crawl one folder for every Git repo inside it",
52            example_projects_dir()
53        );
54        // Both example directories are six characters wide, so one padding works for both.
55        println!("    devp link .       # or, from inside one project, register just that one");
56    }
57
58    Ok(())
59}
60
61/// A plausible "where your projects live" path to show in the onboarding hint.
62///
63/// Purely cosmetic, but a Windows user reading `~/code` and a Linux user reading `~\Code`
64/// both have to translate before they can paste, and the first command a new user runs is
65/// the worst place to make them do that.
66fn example_projects_dir() -> &'static str {
67    if cfg!(windows) { "~\\Code" } else { "~/code" }
68}
69
70/// Report each integration without touching anything.
71fn run_status() -> Result<()> {
72    output::print_header("dev-prune setup status");
73    let registry = Registry::load()?;
74
75    let alias = std::env::current_exe()
76        .ok()
77        .and_then(|exe| exe.parent().map(|d| d.to_path_buf()))
78        .map(|dir| dir.join(if cfg!(windows) { "devp.exe" } else { "devp" }))
79        .filter(|p| p.exists());
80    println!(
81        "  devp alias:            {}",
82        alias
83            .as_ref()
84            .map(output::clean_path)
85            .unwrap_or_else(|| "not installed".to_string())
86    );
87
88    print!("  Command on PATH:       ");
89    match setup::managed_bin_dir() {
90        Ok(dir) if crate::pathenv::is_reachable(&dir) => {
91            println!("{} is on your PATH", output::clean_path(&dir))
92        }
93        Ok(dir) => println!("{} is not on your PATH", output::clean_path(&dir)),
94        Err(_) => println!("unknown (no config directory)"),
95    }
96
97    let skill = setup::skill_path().ok().filter(|p| p.exists());
98    println!(
99        "  SKILL.md:              {}",
100        skill
101            .as_ref()
102            .map(output::clean_path)
103            .unwrap_or_else(|| "not exported".to_string())
104    );
105
106    let agent_roots = setup::agent_skill_roots();
107    if agent_roots.is_empty() {
108        println!("  AI agent skills:       no AI agent detected");
109    } else {
110        for root in &agent_roots {
111            println!(
112                "  AI agent skills:       {} ({})",
113                output::clean_path(root),
114                if root.join("SKILL.md").is_file() {
115                    "installed"
116                } else {
117                    "not installed"
118                }
119            );
120        }
121    }
122
123    println!(
124        "  File icons:            {}",
125        if crate::commands::icon::is_registered() {
126            "registered"
127        } else {
128            "not registered"
129        }
130    );
131
132    print!("  Git hooks:             ");
133    if !hook::git_available() {
134        println!("git is not on PATH");
135    } else {
136        match hook::state() {
137            Ok(HookState::Active) => println!("active"),
138            Ok(HookState::Absent) => println!("not installed"),
139            Ok(HookState::Chained { previous, drifted }) if drifted.is_empty() => {
140                println!("active, chained to `{previous}`")
141            }
142            Ok(HookState::Chained { previous, drifted }) => println!(
143                "active, chained to `{previous}` — {} not forwarded ({}); run `devp hook install --chain`",
144                drifted.len(),
145                drifted.join(", ")
146            ),
147            Ok(HookState::Foreign(p)) => println!(
148                "core.hooksPath belongs to `{p}` (install in front of it with `devp hook install --chain`)"
149            ),
150            Err(e) => println!("unknown ({e})"),
151        }
152    }
153
154    println!(
155        "  Background scheduler:  {}",
156        daemon::daemon_status()
157            .map(|s| s.to_string())
158            .unwrap_or_else(|e| format!("unknown ({e})"))
159    );
160
161    println!();
162    println!("  auto_setup  = {}", registry.settings.auto_setup);
163    println!("  auto_hooks  = {}", registry.settings.auto_hooks);
164    println!("  auto_daemon = {}", registry.settings.auto_daemon);
165
166    // Said out loud, because otherwise "auto_setup = true" and nothing ever installing
167    // is a contradiction the user has no way to explain.
168    if let Some(why) = setup::unattended_environment() {
169        println!();
170        output::print_info(&format!(
171            "Unattended installation is off because {why}. `devp setup` still works when asked."
172        ));
173    }
174
175    println!();
176    if setup::setup_is_due() {
177        output::print_info("A setup pass is due. Run `devp setup`.");
178    } else {
179        output::print_info("Install anything missing with `devp setup`.");
180    }
181
182    Ok(())
183}