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 skipped_throwaway = 0;
27    let mut newly_added_repos: Vec<PathBuf> = Vec::new();
28
29    for path_str in paths {
30        let path = Path::new(path_str)
31            .canonicalize()
32            .with_context(|| format!("Path not found: {path_str}"))?;
33
34        output::print_info(&format!(
35            "Scanning {} for Git repositories...",
36            output::clean_path(&path)
37        ));
38
39        let repos = scanner::scan_for_repos(&path)?;
40        total_found += repos.len();
41
42        for repo in repos {
43            // A plugin manager's throwaway clone is not a workspace. Skipped quietly and
44            // counted, rather than listed: on the scan that motivated this there were
45            // twenty-eight of them, and twenty-eight lines of explanation would have
46            // buried the repositories the user actually wanted registered.
47            if super::link::is_throwaway_checkout(&path, &repo) {
48                skipped_throwaway += 1;
49                continue;
50            }
51            if registry.add_repo(repo.clone()) {
52                newly_added_repos.push(repo.clone());
53                let verb = if dry_run {
54                    "Would register"
55                } else {
56                    "Registered"
57                };
58                output::print_success(&format!("{verb}: {}", output::clean_path(&repo)));
59                let adoption =
60                    registry.adopt_moved_entry(&repo, scanner::git::repo_identity(&repo));
61                super::link::report_adoption(&adoption);
62            } else {
63                output::print_info(&format!(
64                    "Already registered: {}",
65                    output::clean_path(&repo)
66                ));
67                // Backfill, so one `devp init ~/code` teaches the whole registry to
68                // recognise a move later. Only when it is missing: this scan visits
69                // every repository under the path, every time it is run.
70                if registry.needs_identity(&repo) {
71                    let adoption =
72                        registry.adopt_moved_entry(&repo, scanner::git::repo_identity(&repo));
73                    super::link::report_adoption(&adoption);
74                }
75            }
76        }
77    }
78
79    if !dry_run {
80        registry.last_added_repos = newly_added_repos.clone();
81        registry.save()?;
82    }
83
84    output::print_header("Summary");
85    // The registry was mutated in memory either way; only the save is skipped. Saying
86    // "added" after a `--dry-run` would describe a file that was never written.
87    output::print_info(&format!(
88        "Found {total_found} Git {}, {} {} new",
89        output::plural(total_found, "repo", "repos"),
90        if dry_run { "would add" } else { "added" },
91        newly_added_repos.len()
92    ));
93    if skipped_throwaway > 0 {
94        // Named, not silent. A scan that quietly drops repositories is one the user
95        // cannot debug when it drops one they wanted — and `devp link` is the way back.
96        output::print_info(&format!(
97            "Skipped {skipped_throwaway} disposable {} (plugin-manager clones, temp              directories). `devp link <path>` registers one anyway.",
98            output::plural(skipped_throwaway, "checkout", "checkouts"),
99        ));
100    }
101    // After a dry run the in-memory count includes repositories that were never
102    // written; "would be tracked" is the honest phrasing for it.
103    output::print_info(&format!(
104        "{}: {}",
105        if dry_run {
106            "Would be tracked"
107        } else {
108            "Total tracked"
109        },
110        registry.repo_count()
111    ));
112
113    if !dry_run {
114        // Install anything missing. Idempotent, and identical to what `devp setup` and
115        // the post-upgrade pass do, so there is one code path and one set of rules.
116        if let Some(report) = crate::setup::ensure_integrations_if_enabled(&registry)
117            && (report.changed_anything() || report.needs_attention())
118        {
119            output::print_header("Integrations");
120            report.print(false);
121        }
122        crate::setup::suppress_next_auto_setup();
123
124        if registry.settings.auto_config {
125            for repo in &newly_added_repos {
126                crate::commands::link::ensure_default_repo_config(repo);
127            }
128        }
129
130        // Validate (do NOT rewrite) existing per-repo configs.
131        //
132        // The previous behaviour re-serialised `unwrap_or_default()` into every repo,
133        // which silently replaced a malformed `.devprune.json` with defaults and created
134        // a config plus an exclude entry in repos that never had one. Report instead.
135        for repo in registry.repositories.keys() {
136            if let Err(e) = PerRepoConfig::load_with_diagnostics(repo) {
137                output::print_warning(&format!(
138                    "{}: `.devprune.json` could not be parsed and was left untouched — {e}",
139                    output::clean_path(repo)
140                ));
141            }
142        }
143    }
144
145    // Setting a machine up is exactly the moment to find out the binary is a version
146    // behind, so this asks now rather than waiting for the weekly interval that governs
147    // `devp run`. `--dry-run` included: the check writes nothing to disk of its own, and
148    // knowing before you commit to the real run is the point.
149    output::print_header("Version");
150    output::print_info(&format!("Installed: v{}", crate::constants::VERSION));
151    if registry.settings.update_check {
152        let mut checked = registry;
153        if crate::commands::update::check_now(&mut checked) && !dry_run {
154            let _ = checked.save();
155        }
156        registry = checked;
157    } else {
158        output::print_info(
159            "The release check is off (`devp config set update_check true` re-enables it).",
160        );
161    }
162
163    if dry_run {
164        output::print_header("Dry Run Complete");
165        output::print_info("Nothing was written. Re-run without `--dry-run` to register.");
166        return Ok(());
167    }
168
169    output::print_header("Initialization Complete");
170    output::print_success(&format!(
171        "dev-prune initialization complete! All {} tracked {} registered & verified.",
172        registry.repo_count(),
173        output::plural(registry.repo_count(), "repository", "repositories")
174    ));
175
176    output::print_info("Review or undo the integrations with `devp setup --status`.");
177
178    Ok(())
179}