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    if (frozen || offline)
49        && existing
50            .as_ref()
51            .is_some_and(LockFile::requires_git_hash_migration)
52    {
53        return Err(format!(
54            "{} contains pre-v5 Git content hashes; run `harn install` and commit the migrated lockfile before using --locked or --offline",
55            ctx.lock_path().display()
56        )
57        .into());
58    }
59
60    let desired = build_lockfile(
61        workspace,
62        &ctx,
63        existing.as_ref(),
64        None,
65        false,
66        !frozen && !offline,
67        offline,
68    )?;
69    if frozen || offline {
70        if !existing
71            .as_ref()
72            .is_some_and(|lock| lock.same_resolution(&desired))
73        {
74            return Err(format!("{} would need to change", ctx.lock_path().display()).into());
75        }
76    } else {
77        desired.save(&ctx.lock_path())?;
78    }
79    materialize_dependencies_from_lock(workspace, &ctx, &desired, refetch, offline)
80}
81
82pub fn install_packages(frozen: bool, refetch: Option<&str>, offline: bool, json: bool) {
83    match install_packages_impl(frozen, refetch, offline) {
84        Ok(installed) if json => {
85            print_install_summary_json("install", installed, frozen, offline);
86        }
87        Ok(0) => println!("No dependencies to install."),
88        Ok(installed) => {
89            println!("Installed {installed} package(s) in a new immutable generation.");
90        }
91        Err(error) if json => {
92            print_install_error_json("install", &error);
93            process::exit(1);
94        }
95        Err(error) => {
96            eprintln!("error: {error}");
97            process::exit(1);
98        }
99    }
100}
101
102fn print_install_summary_json(action: &str, installed: usize, frozen: bool, offline: bool) {
103    let body = serde_json::json!({
104        "action": action,
105        "ok": true,
106        "installed": installed,
107        "frozen": frozen,
108        "offline": offline,
109        "lock_file": LOCK_FILE,
110        "package_pointer": ".harn/package-current.toml",
111    });
112    println!(
113        "{}",
114        serde_json::to_string_pretty(&body).unwrap_or_default()
115    );
116}
117
118fn print_install_error_json(action: &str, error: &PackageError) {
119    let body = serde_json::json!({
120        "action": action,
121        "ok": false,
122        "error": error.to_string(),
123    });
124    println!(
125        "{}",
126        serde_json::to_string_pretty(&body).unwrap_or_default()
127    );
128}
129pub fn lock_packages() {
130    let result = (|| -> Result<usize, PackageError> {
131        let workspace = PackageWorkspace::from_current_dir()?;
132        let _mutation_lock = acquire_package_mutation_lock(&workspace)?;
133        let ctx = workspace.load_manifest_context()?;
134        let existing = LockFile::load(&ctx.lock_path())?;
135        let lock = build_lockfile(&workspace, &ctx, existing.as_ref(), None, true, true, false)?;
136        lock.save(&ctx.lock_path())?;
137        Ok(lock.packages.len())
138    })();
139
140    match result {
141        Ok(count) => println!("Wrote {LOCK_FILE} with {count} package(s)."),
142        Err(error) => {
143            eprintln!("error: {error}");
144            process::exit(1);
145        }
146    }
147}
148pub fn update_packages(alias: Option<&str>, all: bool, json: bool) {
149    let result = PackageWorkspace::from_current_dir()
150        .and_then(|workspace| update_packages_in(&workspace, alias, all));
151    print_update_packages_result(result, json);
152}
153
154pub(crate) fn update_packages_in(
155    workspace: &PackageWorkspace,
156    alias: Option<&str>,
157    all: bool,
158) -> Result<usize, PackageError> {
159    let _mutation_lock = acquire_package_mutation_lock(workspace)?;
160    if !all && alias.is_none() {
161        return Err("specify a dependency alias or pass --all"
162            .to_string()
163            .into());
164    }
165
166    let ctx = workspace.load_manifest_context()?;
167    if let Some(alias) = alias {
168        validate_package_alias(alias)?;
169        if !ctx.manifest.dependencies.contains_key(alias) {
170            return Err(format!("{alias} is not present in [dependencies]").into());
171        }
172    }
173    let existing = LockFile::load(&ctx.lock_path())?;
174    let lock = build_lockfile(workspace, &ctx, existing.as_ref(), alias, all, true, false)?;
175    lock.save(&ctx.lock_path())?;
176    materialize_dependencies_from_lock(workspace, &ctx, &lock, None, false)
177}
178
179fn print_update_packages_result(result: Result<usize, PackageError>, json: bool) {
180    match result {
181        Ok(installed) if json => print_install_summary_json("update", installed, false, false),
182        Ok(installed) => println!("Updated {installed} package(s)."),
183        Err(error) if json => {
184            print_install_error_json("update", &error);
185            process::exit(1);
186        }
187        Err(error) => {
188            eprintln!("error: {error}");
189            process::exit(1);
190        }
191    }
192}
193pub fn remove_package(alias: &str) {
194    let result = PackageWorkspace::from_current_dir()
195        .and_then(|workspace| remove_package_in(&workspace, alias));
196    print_remove_package_result(alias, result);
197}
198
199pub(crate) fn remove_package_in(
200    workspace: &PackageWorkspace,
201    alias: &str,
202) -> Result<bool, PackageError> {
203    let _mutation_lock = acquire_package_mutation_lock(workspace)?;
204    validate_package_alias(alias)?;
205    let ctx = workspace.load_manifest_context()?;
206    let removed = remove_dependency_from_manifest(&ctx.manifest_path(), alias)?;
207    if !removed {
208        return Ok(false);
209    }
210    let mut lock = LockFile::load(&ctx.lock_path())?.unwrap_or_default();
211    lock.remove(alias);
212    lock.save(&ctx.lock_path())?;
213    materialize_dependencies_from_lock(workspace, &ctx, &lock, None, false)?;
214    Ok(true)
215}
216
217fn print_remove_package_result(alias: &str, result: Result<bool, PackageError>) {
218    match result {
219        Ok(true) => println!("Removed {alias} from {MANIFEST} and {LOCK_FILE}."),
220        Ok(false) => {
221            eprintln!("error: {alias} is not present in [dependencies]");
222            process::exit(1);
223        }
224        Err(error) => {
225            eprintln!("error: {error}");
226            process::exit(1);
227        }
228    }
229}