use anyhow::{Context, Result};
use console::style;
use crate::config::Config;
use crate::output;
pub fn run(config: &Config) -> Result<()> {
println!();
println!(" {} — checking configuration", style("nex doctor").bold());
println!();
let mut fixed = 0;
if check_mac_app_util(config, &mut fixed)? {
}
check_allow_unfree(config, &mut fixed)?;
check_session_path(config, &mut fixed)?;
if fixed > 0 {
let repo = &config.repo;
let _ = std::process::Command::new("git")
.args(["add", "-A"])
.current_dir(repo)
.output();
let _ = std::process::Command::new("git")
.args(["commit", "-m", "nex doctor: apply fixes"])
.current_dir(repo)
.output();
println!();
println!(
" {} {fixed} issue(s) fixed. Run {} to activate.",
style("✓").green().bold(),
style("nex switch").bold()
);
} else {
println!(" {} no issues found", style("✓").green().bold());
}
println!();
Ok(())
}
fn check_mac_app_util(config: &Config, fixed: &mut usize) -> Result<bool> {
let flake_path = config.repo.join("flake.nix");
let mkhost_path = config.repo.join("nix/lib/mkHost.nix");
let flake = std::fs::read_to_string(&flake_path)
.with_context(|| format!("reading {}", flake_path.display()))?;
if flake.contains("mac-app-util") {
ok("mac-app-util", "Spotlight app aliases enabled");
return Ok(false);
}
warn(
"mac-app-util",
"not configured — nix apps won't appear in Spotlight",
);
let patched_flake = flake
.replace(
"home-manager = {\n url = \"github:nix-community/home-manager\";\n inputs.nixpkgs.follows = \"nixpkgs\";\n };\n };",
"home-manager = {\n url = \"github:nix-community/home-manager\";\n inputs.nixpkgs.follows = \"nixpkgs\";\n };\n\n mac-app-util.url = \"github:hraban/mac-app-util\";\n };"
)
.replace(
"home-manager = {\n url = \"github:nix-community/home-manager\";\n inputs.nixpkgs.follows = \"nixpkgs\";\n };\n };\n\n outputs = { self, nixpkgs, nix-darwin, home-manager }:",
"home-manager = {\n url = \"github:nix-community/home-manager\";\n inputs.nixpkgs.follows = \"nixpkgs\";\n };\n\n mac-app-util.url = \"github:hraban/mac-app-util\";\n };\n\n outputs = { self, nixpkgs, nix-darwin, home-manager, mac-app-util }:"
);
let patched_flake = if patched_flake.contains("mac-app-util")
&& !patched_flake
.contains("outputs = { self, nixpkgs, nix-darwin, home-manager, mac-app-util }")
{
patched_flake.replace(
"outputs = { self, nixpkgs, nix-darwin, home-manager }:",
"outputs = { self, nixpkgs, nix-darwin, home-manager, mac-app-util }:",
)
} else {
patched_flake
};
let patched_flake = patched_flake.replace(
"inherit nixpkgs nix-darwin home-manager;",
"inherit nixpkgs nix-darwin home-manager mac-app-util;",
);
if !patched_flake.contains("mac-app-util") {
output::warn("could not auto-patch flake.nix — manual edit required");
return Ok(false);
}
let has_input = patched_flake.contains("mac-app-util.url");
let has_output = patched_flake.contains("mac-app-util }");
let has_inherit = patched_flake.contains("mac-app-util;");
if !has_input || !has_output || !has_inherit {
output::warn(
"could not fully patch flake.nix — partial changes would break the flake.\n\
Add mac-app-util manually: https://github.com/hraban/mac-app-util",
);
return Ok(false);
}
std::fs::write(&flake_path, &patched_flake)
.with_context(|| format!("writing {}", flake_path.display()))?;
info("patched", &flake_path.display().to_string());
if mkhost_path.exists() {
let mkhost = std::fs::read_to_string(&mkhost_path)
.with_context(|| format!("reading {}", mkhost_path.display()))?;
if !mkhost.contains("mac-app-util") {
let patched = mkhost
.replace(
"{ nixpkgs, nix-darwin, home-manager }:",
"{ nixpkgs, nix-darwin, home-manager, mac-app-util }:",
)
.replace(
" hostModule\n home-manager.darwinModules.home-manager",
" hostModule\n mac-app-util.darwinModules.default\n home-manager.darwinModules.home-manager",
)
.replace(
" extraSpecialArgs = { inherit hostname username; };\n };",
" extraSpecialArgs = { inherit hostname username; };\n sharedModules = [\n mac-app-util.homeManagerModules.default\n ];\n };",
);
std::fs::write(&mkhost_path, patched)
.with_context(|| format!("writing {}", mkhost_path.display()))?;
info("patched", &mkhost_path.display().to_string());
}
}
*fixed += 1;
Ok(true)
}
fn check_allow_unfree(config: &Config, fixed: &mut usize) -> Result<bool> {
let base_path = config.repo.join("nix/modules/darwin/base.nix");
if !base_path.exists() {
return Ok(false);
}
let content = std::fs::read_to_string(&base_path)
.with_context(|| format!("reading {}", base_path.display()))?;
if content.contains("allowUnfree") {
ok("unfree packages", "nixpkgs.config.allowUnfree is set");
return Ok(false);
}
warn(
"unfree packages",
"not allowed — vscode, slack, spotify, etc. will fail to install",
);
let patched = if content.contains("nix.enable = false;") {
content.replace(
"nix.enable = false;",
"nix.enable = false;\n\n nixpkgs.config.allowUnfree = true;",
)
} else if content.contains("nix.settings.experimental-features") {
content.replace(
"nix.settings.experimental-features",
"nixpkgs.config.allowUnfree = true;\n\n nix.settings.experimental-features",
)
} else {
output::warn(
"could not auto-patch base.nix — add `nixpkgs.config.allowUnfree = true;` manually",
);
return Ok(false);
};
std::fs::write(&base_path, patched)
.with_context(|| format!("writing {}", base_path.display()))?;
info("patched", &base_path.display().to_string());
*fixed += 1;
Ok(true)
}
fn check_session_path(config: &Config, fixed: &mut usize) -> Result<bool> {
let base_path = &config.nix_packages_file;
if !base_path.exists() {
return Ok(false);
}
let content = std::fs::read_to_string(base_path)
.with_context(|| format!("reading {}", base_path.display()))?;
let in_nix_config = content.contains("sessionPath");
let on_path = is_local_bin_on_path();
if in_nix_config && on_path {
ok("sessionPath", "~/.local/bin is on PATH");
return Ok(false);
}
if in_nix_config && !on_path {
warn(
"sessionPath",
"configured in nix but ~/.local/bin is not on PATH — run `nex switch` then open a new shell",
);
return Ok(false);
}
warn(
"sessionPath",
"~/.local/bin not in PATH — nex may not be found after install",
);
let patched = if content.contains("stateVersion =") {
content.replace(
"stateVersion =",
"sessionPath = [ \"$HOME/.local/bin\" ];\n stateVersion =",
)
} else {
output::warn(
"could not auto-patch — add `home.sessionPath = [ \"$HOME/.local/bin\" ];` manually",
);
return Ok(false);
};
std::fs::write(base_path, patched)
.with_context(|| format!("writing {}", base_path.display()))?;
info("patched", &base_path.display().to_string());
*fixed += 1;
Ok(true)
}
fn is_local_bin_on_path() -> bool {
let path_var = std::env::var("PATH").unwrap_or_default();
let home = std::env::var("HOME").unwrap_or_default();
let expanded = format!("{home}/.local/bin");
path_var
.split(':')
.any(|entry| entry == "~/.local/bin" || entry == "$HOME/.local/bin" || entry == expanded)
}
fn ok(label: &str, detail: &str) {
eprintln!(
" {} {}: {}",
style("✓").green().bold(),
label,
style(detail).dim()
);
}
fn warn(label: &str, detail: &str) {
eprintln!(
" {} {}: {}",
style("!").yellow().bold(),
label,
style(detail).dim()
);
}
fn info(label: &str, detail: &str) {
eprintln!(" {} {}: {}", style("→").cyan(), label, style(detail).dim());
}