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