Skip to main content

dev_prune/commands/
init.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Handler for the `dev-prune init` command.
5//
6// Scans provided paths for Git repositories, registers them in the registry,
7// auto-configures background daemon & hooks, and self-heals project configuration schemas.
8
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12
13use crate::config::{PerRepoConfig, Registry};
14use crate::output;
15use crate::scanner;
16
17/// Run the `init` command.
18///
19/// Scans each provided path for Git repositories and adds them to the registry.
20pub fn run(paths: &[String], dry_run: bool) -> Result<()> {
21    output::print_banner();
22    output::print_header("dev-prune init");
23
24    let mut registry = Registry::load()?;
25    let mut total_found = 0;
26    let mut newly_added_repos: Vec<PathBuf> = Vec::new();
27
28    for path_str in paths {
29        let path = Path::new(path_str)
30            .canonicalize()
31            .with_context(|| format!("Path not found: {path_str}"))?;
32
33        output::print_info(&format!(
34            "Scanning {} for Git repositories...",
35            output::clean_path(&path)
36        ));
37
38        let repos = scanner::scan_for_repos(&path)?;
39        total_found += repos.len();
40
41        for repo in repos {
42            if registry.add_repo(repo.clone()) {
43                newly_added_repos.push(repo.clone());
44                let verb = if dry_run {
45                    "Would register"
46                } else {
47                    "Registered"
48                };
49                output::print_success(&format!("{verb}: {}", output::clean_path(&repo)));
50            } else {
51                output::print_info(&format!(
52                    "Already registered: {}",
53                    output::clean_path(&repo)
54                ));
55            }
56        }
57    }
58
59    if !dry_run {
60        registry.last_added_repos = newly_added_repos.clone();
61        registry.save()?;
62    }
63
64    output::print_header("Summary");
65    // The registry was mutated in memory either way; only the save is skipped. Saying
66    // "added" after a `--dry-run` would describe a file that was never written.
67    output::print_info(&format!(
68        "Found {total_found} Git {}, {} {} new",
69        output::plural(total_found, "repo", "repos"),
70        if dry_run { "would add" } else { "added" },
71        newly_added_repos.len()
72    ));
73    output::print_info(&format!("Total tracked: {}", registry.repo_count()));
74
75    if !dry_run {
76        // Install anything missing. Idempotent, and identical to what `devp setup` and
77        // the post-upgrade pass do, so there is one code path and one set of rules.
78        if let Some(report) = crate::setup::ensure_integrations_if_enabled(&registry) {
79            if report.changed_anything() || report.needs_attention() {
80                output::print_header("Integrations");
81                report.print(false);
82            }
83        }
84        crate::setup::suppress_next_auto_setup();
85
86        // Validate (do NOT rewrite) existing per-repo configs.
87        //
88        // The previous behaviour re-serialised `unwrap_or_default()` into every repo,
89        // which silently replaced a malformed `.devprune.json` with defaults and created
90        // a config plus a `.gitignore` entry in repos that never had one. Report instead.
91        for repo in registry.repositories.keys() {
92            if let Err(e) = PerRepoConfig::load_with_diagnostics(repo) {
93                output::print_warning(&format!(
94                    "{}: `.devprune.json` could not be parsed and was left untouched — {e}",
95                    output::clean_path(repo)
96                ));
97            }
98        }
99    }
100
101    // Setting a machine up is exactly the moment to find out the binary is a version
102    // behind, so this asks now rather than waiting for the weekly interval that governs
103    // `devp run`. `--dry-run` included: the check writes nothing to disk of its own, and
104    // knowing before you commit to the real run is the point.
105    output::print_header("Version");
106    output::print_info(&format!("Installed: v{}", crate::constants::VERSION));
107    if registry.settings.update_check {
108        let mut checked = registry;
109        if crate::commands::update::check_now(&mut checked) && !dry_run {
110            let _ = checked.save();
111        }
112        registry = checked;
113    } else {
114        output::print_info(
115            "The release check is off (`devp config set update_check true` re-enables it).",
116        );
117    }
118
119    if dry_run {
120        output::print_header("Dry Run Complete");
121        output::print_info("Nothing was written. Re-run without `--dry-run` to register.");
122        return Ok(());
123    }
124
125    output::print_header("Initialization Complete");
126    output::print_success(&format!(
127        "dev-prune initialization complete! All {} tracked {} registered & verified.",
128        registry.repo_count(),
129        output::plural(registry.repo_count(), "repository", "repositories")
130    ));
131
132    // Icons stay opt-in. Unlike the other integrations they write into the desktop's
133    // shared MIME and icon directories rather than into dev-prune's own, and nothing
134    // about pruning depends on them.
135    output::print_info(
136        "Optional extra: `devp icon` gives .devprune.json its own icon in your file manager.",
137    );
138    output::print_info("Review or undo the integrations with `devp setup --status`.");
139
140    Ok(())
141}