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