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