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    // After a dry run the in-memory count includes repositories that were never
74    // written; "would be tracked" is the honest phrasing for it.
75    output::print_info(&format!(
76        "{}: {}",
77        if dry_run {
78            "Would be tracked"
79        } else {
80            "Total tracked"
81        },
82        registry.repo_count()
83    ));
84
85    if !dry_run {
86        // Install anything missing. Idempotent, and identical to what `devp setup` and
87        // the post-upgrade pass do, so there is one code path and one set of rules.
88        if let Some(report) = crate::setup::ensure_integrations_if_enabled(&registry)
89            && (report.changed_anything() || report.needs_attention())
90        {
91            output::print_header("Integrations");
92            report.print(false);
93        }
94        crate::setup::suppress_next_auto_setup();
95
96        if registry.settings.auto_config {
97            for repo in &newly_added_repos {
98                crate::commands::link::ensure_default_repo_config(repo);
99            }
100        }
101
102        // Validate (do NOT rewrite) existing per-repo configs.
103        //
104        // The previous behaviour re-serialised `unwrap_or_default()` into every repo,
105        // which silently replaced a malformed `.devprune.json` with defaults and created
106        // a config plus an exclude entry in repos that never had one. Report instead.
107        for repo in registry.repositories.keys() {
108            if let Err(e) = PerRepoConfig::load_with_diagnostics(repo) {
109                output::print_warning(&format!(
110                    "{}: `.devprune.json` could not be parsed and was left untouched — {e}",
111                    output::clean_path(repo)
112                ));
113            }
114        }
115    }
116
117    // Setting a machine up is exactly the moment to find out the binary is a version
118    // behind, so this asks now rather than waiting for the weekly interval that governs
119    // `devp run`. `--dry-run` included: the check writes nothing to disk of its own, and
120    // knowing before you commit to the real run is the point.
121    output::print_header("Version");
122    output::print_info(&format!("Installed: v{}", crate::constants::VERSION));
123    if registry.settings.update_check {
124        let mut checked = registry;
125        if crate::commands::update::check_now(&mut checked) && !dry_run {
126            let _ = checked.save();
127        }
128        registry = checked;
129    } else {
130        output::print_info(
131            "The release check is off (`devp config set update_check true` re-enables it).",
132        );
133    }
134
135    if dry_run {
136        output::print_header("Dry Run Complete");
137        output::print_info("Nothing was written. Re-run without `--dry-run` to register.");
138        return Ok(());
139    }
140
141    output::print_header("Initialization Complete");
142    output::print_success(&format!(
143        "dev-prune initialization complete! All {} tracked {} registered & verified.",
144        registry.repo_count(),
145        output::plural(registry.repo_count(), "repository", "repositories")
146    ));
147
148    output::print_info("Review or undo the integrations with `devp setup --status`.");
149
150    Ok(())
151}