Skip to main content

harn_cli/package/lockfile/
install.rs

1//! The `harn install` / `lock` / `update` / `remove` CLI entry points —
2//! argument handling, human and JSON rendering, and process exit codes.
3
4use crate::package::*;
5
6pub(crate) fn install_packages_impl(
7    frozen: bool,
8    refetch: Option<&str>,
9    offline: bool,
10) -> Result<usize, PackageError> {
11    install_packages_in(
12        &PackageWorkspace::from_current_dir()?,
13        frozen,
14        refetch,
15        offline,
16    )
17}
18
19pub(crate) fn install_packages_in_locked(
20    workspace: &PackageWorkspace,
21    frozen: bool,
22    refetch: Option<&str>,
23    offline: bool,
24) -> Result<usize, PackageError> {
25    let ctx = workspace.load_manifest_context()?;
26    let existing = LockFile::load(&ctx.lock_path())?;
27    if ctx.manifest.dependencies.is_empty() {
28        let empty = LockFile::default();
29        if frozen || offline {
30            // A lock that still pins packages the manifest no longer
31            // declares is a substantive change; surface it instead of
32            // silently succeeding against a stale lock.
33            if existing
34                .as_ref()
35                .is_some_and(|lock| !lock.packages.is_empty())
36            {
37                return Err(format!("{} would need to change", ctx.lock_path().display()).into());
38            }
39        } else {
40            empty.save(&ctx.lock_path())?;
41        }
42        return materialize_dependencies_from_lock(workspace, &ctx, &empty, refetch, offline);
43    }
44
45    if (frozen || offline) && existing.is_none() {
46        return Err(format!("{} is missing", ctx.lock_path().display()).into());
47    }
48
49    let desired = build_lockfile(
50        workspace,
51        &ctx,
52        existing.as_ref(),
53        None,
54        false,
55        !frozen && !offline,
56        offline,
57    )?;
58    if frozen || offline {
59        if !existing
60            .as_ref()
61            .is_some_and(|lock| lock.same_resolution(&desired))
62        {
63            return Err(format!("{} would need to change", ctx.lock_path().display()).into());
64        }
65    } else {
66        desired.save(&ctx.lock_path())?;
67    }
68    materialize_dependencies_from_lock(workspace, &ctx, &desired, refetch, offline)
69}
70
71pub fn install_packages(frozen: bool, refetch: Option<&str>, offline: bool, json: bool) {
72    match install_packages_impl(frozen, refetch, offline) {
73        Ok(installed) if json => {
74            print_install_summary_json("install", installed, frozen, offline);
75        }
76        Ok(0) => println!("No dependencies to install."),
77        Ok(installed) => {
78            println!("Installed {installed} package(s) in a new immutable generation.");
79        }
80        Err(error) if json => {
81            print_install_error_json("install", &error);
82            process::exit(1);
83        }
84        Err(error) => {
85            eprintln!("error: {error}");
86            process::exit(1);
87        }
88    }
89}
90
91fn print_install_summary_json(action: &str, installed: usize, frozen: bool, offline: bool) {
92    let body = serde_json::json!({
93        "action": action,
94        "ok": true,
95        "installed": installed,
96        "frozen": frozen,
97        "offline": offline,
98        "lock_file": LOCK_FILE,
99        "package_pointer": ".harn/package-current.toml",
100    });
101    println!(
102        "{}",
103        serde_json::to_string_pretty(&body).unwrap_or_default()
104    );
105}
106
107fn print_install_error_json(action: &str, error: &PackageError) {
108    let body = serde_json::json!({
109        "action": action,
110        "ok": false,
111        "error": error.to_string(),
112    });
113    println!(
114        "{}",
115        serde_json::to_string_pretty(&body).unwrap_or_default()
116    );
117}
118pub fn lock_packages() {
119    let result = (|| -> Result<usize, PackageError> {
120        let workspace = PackageWorkspace::from_current_dir()?;
121        let _mutation_lock = acquire_package_mutation_lock(&workspace)?;
122        let ctx = workspace.load_manifest_context()?;
123        let existing = LockFile::load(&ctx.lock_path())?;
124        let lock = build_lockfile(&workspace, &ctx, existing.as_ref(), None, true, true, false)?;
125        lock.save(&ctx.lock_path())?;
126        Ok(lock.packages.len())
127    })();
128
129    match result {
130        Ok(count) => println!("Wrote {LOCK_FILE} with {count} package(s)."),
131        Err(error) => {
132            eprintln!("error: {error}");
133            process::exit(1);
134        }
135    }
136}
137pub fn update_packages(alias: Option<&str>, all: bool, json: bool) {
138    let result = PackageWorkspace::from_current_dir()
139        .and_then(|workspace| update_packages_in(&workspace, alias, all));
140    print_update_packages_result(result, json);
141}
142
143pub(crate) fn update_packages_in(
144    workspace: &PackageWorkspace,
145    alias: Option<&str>,
146    all: bool,
147) -> Result<usize, PackageError> {
148    let _mutation_lock = acquire_package_mutation_lock(workspace)?;
149    if !all && alias.is_none() {
150        return Err("specify a dependency alias or pass --all"
151            .to_string()
152            .into());
153    }
154
155    let ctx = workspace.load_manifest_context()?;
156    if let Some(alias) = alias {
157        validate_package_alias(alias)?;
158        if !ctx.manifest.dependencies.contains_key(alias) {
159            return Err(format!("{alias} is not present in [dependencies]").into());
160        }
161    }
162    let existing = LockFile::load(&ctx.lock_path())?;
163    let lock = build_lockfile(workspace, &ctx, existing.as_ref(), alias, all, true, false)?;
164    lock.save(&ctx.lock_path())?;
165    materialize_dependencies_from_lock(workspace, &ctx, &lock, None, false)
166}
167
168fn print_update_packages_result(result: Result<usize, PackageError>, json: bool) {
169    match result {
170        Ok(installed) if json => print_install_summary_json("update", installed, false, false),
171        Ok(installed) => println!("Updated {installed} package(s)."),
172        Err(error) if json => {
173            print_install_error_json("update", &error);
174            process::exit(1);
175        }
176        Err(error) => {
177            eprintln!("error: {error}");
178            process::exit(1);
179        }
180    }
181}
182pub fn remove_package(alias: &str) {
183    let result = PackageWorkspace::from_current_dir()
184        .and_then(|workspace| remove_package_in(&workspace, alias));
185    print_remove_package_result(alias, result);
186}
187
188pub(crate) fn remove_package_in(
189    workspace: &PackageWorkspace,
190    alias: &str,
191) -> Result<bool, PackageError> {
192    let _mutation_lock = acquire_package_mutation_lock(workspace)?;
193    validate_package_alias(alias)?;
194    let ctx = workspace.load_manifest_context()?;
195    let removed = remove_dependency_from_manifest(&ctx.manifest_path(), alias)?;
196    if !removed {
197        return Ok(false);
198    }
199    let mut lock = LockFile::load(&ctx.lock_path())?.unwrap_or_default();
200    lock.remove(alias);
201    lock.save(&ctx.lock_path())?;
202    materialize_dependencies_from_lock(workspace, &ctx, &lock, None, false)?;
203    Ok(true)
204}
205
206fn print_remove_package_result(alias: &str, result: Result<bool, PackageError>) {
207    match result {
208        Ok(true) => println!("Removed {alias} from {MANIFEST} and {LOCK_FILE}."),
209        Ok(false) => {
210            eprintln!("error: {alias} is not present in [dependencies]");
211            process::exit(1);
212        }
213        Err(error) => {
214            eprintln!("error: {error}");
215            process::exit(1);
216        }
217    }
218}