{ pkgs, host, backupFiles ? [".config/mimeapps.list.backup"], project, ... }: let backupFilesString = pkgs.lib.strings.concatStringsSep " " backupFiles; in pkgs.writeShellScriptBin "ncli" '' #!${pkgs.bash}/bin/bash set -euo pipefail # --- Configuration --- PROJECT="${project}" HOST="${host}" BACKUP_FILES_STR="${backupFilesString}" VERSION="2.1.3" FLAKE_NIX_PATH="$HOME/$PROJECT/flake.nix" read -r -a BACKUP_FILES <<< "$BACKUP_FILES_STR" # --- Read Colors file --- source /$HOME/$PROJECT/other/colors.sh # --- Helper Functions --- print_help() { echo "NixOS CLI Utility -- version $VERSION" echo "" echo "Usage: ncli [command]" echo "" echo "System Commands:" echo " rebuild - Rebuild the NixOS system configuration." echo " update - Update the flake and rebuild the system." echo "" echo "Maintenance Commands:" echo " cleanup - Clean up old system generations. Can specify a number to keep." echo " diag - Create a system diagnostic report (saves to ~/diag.txt)." echo " list-gens - List user and system generations." echo " trim - Trim filesystems to improve SSD performance." echo "" echo "Git Commands:" echo " commit [msg] - Add all changes and commit with message." echo " push - Push changes to origin." echo " pull - Pull latest changes from origin." echo " status - Show git status." echo "" echo "Development Commands:" echo " dev - Initialize a Nix development environment (flake.nix + direnv)." echo " dev track - Remove assume-unchanged flag from flake files." echo " dev untrack - Mark flake files as assume-unchanged in a directory." echo "" echo " help - Show this help message." echo "" } handle_backups() { if [ ''${#BACKUP_FILES[@]} -eq 0 ]; then echo "No backup files configured to check." return fi echo "Checking for backup files to remove..." for file_path in "''${BACKUP_FILES[@]}"; do full_path="$HOME/$file_path" if [ -f "$full_path" ]; then echo "Removing stale backup file: $full_path" rm "$full_path" fi done } # --- Dev Init Helper Functions --- print_header() { echo "" echo -e "''${BLUE}==============================================''${NOCOLOR}" echo -e "''${BLUE} Nix Flake Development Environment Initializer''${NOCOLOR}" echo -e "''${BLUE}==============================================''${NOCOLOR}" echo "" } print_success() { echo -e "''${GREEN}[OK]''${NOCOLOR} $1" } print_error() { echo -e "''${RED}[ERR]''${NOCOLOR} $1" } print_info() { echo -e "''${YELLOW}[->]''${NOCOLOR} $1" } handle_build_error() { local exit_code=$? # Exit code 137 = 128+9 = SIGKILL, almost always OOM killer if [ "$exit_code" -eq 137 ]; then echo "" echo -e "''${RED}╔══════════════════════════════════════════════════════════╗''${NOCOLOR}" echo -e "''${RED}║ BUILD KILLED — Signal 9 (SIGKILL) detected ║''${NOCOLOR}" echo -e "''${RED}╚══════════════════════════════════════════════════════════╝''${NOCOLOR}" echo "" echo -e "''${YELLOW}What happened:''${NOCOLOR}" echo " The build process was forcefully terminated by the OS." echo " This is almost always the Linux kernel OOM killer running out" echo " of RAM + swap during Nix evaluation or compilation." echo "" echo -e "''${YELLOW}Suggested fixes (try in order):''${NOCOLOR}" echo "" echo -e " ''${GREEN}1. Reduce parallel jobs''${NOCOLOR} (lowest RAM usage):" echo " sudo nixos-rebuild switch --flake . --max-jobs 1 --cores 1" echo "" echo -e " ''${GREEN}2. Confirm OOM killer fired:''${NOCOLOR}" echo " journalctl -k --since '5 minutes ago' | grep -i oom" echo "" fi } # --- Main Logic --- if [ "$#" -eq 0 ]; then echo "Error: No command provided." >&2 print_help exit 1 fi case "$1" in cleanup) echo "Warning! This will remove old generations of your system." read -p "How many generations to keep (default: all)? " keep_count if [ -z "$keep_count" ]; then read -p "This will remove all but the current generation. Continue (y/N)? " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then nh clean all -v else echo "Cleanup cancelled." fi else read -p "This will keep the last $keep_count generations. Continue (y/N)? " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then nh clean all -k "$keep_count" -v else echo "Cleanup cancelled." fi fi LOG_DIR="$HOME/ncli-cleanup-logs" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/ncli-cleanup-$(date +%Y-%m-%d_%H-%M-%S).log" echo "Cleaning up old log files..." >> "$LOG_FILE" find "$LOG_DIR" -type f -mtime +3 -name "*.log" -delete >> "$LOG_FILE" 2>&1 echo "Cleanup process logged to $LOG_FILE" ;; diag) echo "Generating system diagnostic report..." { echo "=== NixOS System Diagnostic Report ===" echo "Generated: $(date)" echo "" echo "=== System Information ===" inxi --full 2>/dev/null || echo "inxi not available" echo "" echo "=== Git Status ===" cd "$HOME/$PROJECT" 2>/dev/null && git status 2>/dev/null || echo "Git status not available" echo "" } > "$HOME/diag.txt" echo "Diagnostic report saved to $HOME/diag.txt" ;; help) print_help ;; list-gens) echo "--- User Generations ---" nix-env --list-generations | cat || echo "Could not list user generations." echo "" echo "--- System Generations ---" nix profile history --profile /nix/var/nix/profiles/system | cat || echo "Could not list system generations." ;; rebuild) handle_backups geno=$(sudo nix-env --list-generations --profile /nix/var/nix/profiles/system | grep current | awk '{print $1}') echo -e "Starting NixOS rebuild for current host: $HOST on generation: $YELLOW$geno$NOCOLOR" cd "$HOME/$PROJECT" || { echo "Error: Could not change to $HOME/$PROJECT"; exit 1; } current="" if [ -f /etc/nixos-tags ]; then current=$(cat /etc/nixos-tags) fi sudo nixos-rebuild switch --flake . ; _rebuild_exit=$? if [ "$_rebuild_exit" -eq 0 ]; then echo "✓ Rebuild finished successfully for $HOST" if [ -n "$current" ]; then sudo /run/current-system/specialisation/$current/bin/switch-to-configuration test else echo "No specialization tag found, staying on default system." fi genn=$(sudo nix-env --list-generations --profile /nix/var/nix/profiles/system | grep current | awk '{print $1}') echo -e "Running on new generation: $YELLOW $geno $NOCOLOR-> $GREEN$genn$NOCOLOR" else ( exit "$_rebuild_exit" ); handle_build_error echo "✗ Rebuild failed for $HOST" >&2 exit 1 fi ;; update) handle_backups geno=$(sudo nix-env --list-generations --profile /nix/var/nix/profiles/system | grep current | awk '{print $1}') echo -e "Updating flake and rebuilding system for current host: $HOST on generation: $YELLOW$geno$NOCOLOR" cd "$HOME/$PROJECT" || { echo "Error: Could not change to $HOME/$PROJECT"; exit 1; } # --- Selective flake update --- read -rp "Update [a]ll inputs or [s]elect manually? (a/s): " choice case "$choice" in a|A) echo "Updating all inputs..." if nix flake update --flake .; then echo "✓ Flake updated successfully" else echo "✗ Flake update failed" >&2 exit 1 fi ;; s|S) echo "Fetching available updates (this may take a moment)..." TEMP_LOCK=$(mktemp) trap 'rm -f "$TEMP_LOCK"' EXIT nix flake update --output-lock-file "$TEMP_LOCK" --flake . 2>/dev/null outdated=$(jq -r --slurpfile new "$TEMP_LOCK" ' .nodes as $old | $new[0].nodes as $newn | ($old | keys[]) | select(. != "root") | select( ($old[.].locked.lastModified // 0) != ($newn[.].locked.lastModified // 0) ) ' flake.lock) if [[ -z "$outdated" ]]; then echo "✓ All inputs are already up to date, skipping flake update." else echo echo "Updates available for:" printf '%s\n' "$outdated" echo echo "Tab to select, Enter to update, Esc to cancel." selected=$(printf '%s\n' "$outdated" | fzf --multi) || { echo "No inputs selected, skipping flake update." selected="" } if [[ -n "$selected" ]]; then if nix flake update --flake . $selected; then echo "✓ Flake updated successfully" else echo "✗ Flake update failed" >&2 exit 1 fi fi fi ;; *) echo "Invalid choice, skipping flake update." ;; esac # --- End selective flake update --- current="" if [ -f /etc/nixos-tags ]; then current=$(cat /etc/nixos-tags) fi if [ -n "$current" ]; then echo "Rebuilding system... Current specialization: $current" else echo "Rebuilding system... Staying on current specialization" fi sudo nixos-rebuild switch --flake . ; _rebuild_exit=$? if [ "$_rebuild_exit" -eq 0 ]; then echo "✓ Update and rebuild finished successfully for $HOST" if [ -n "$current" ]; then sudo /run/current-system/specialisation/$current/bin/switch-to-configuration test else echo "No specialization tag found, staying on default system." fi genn=$(sudo nix-env --list-generations --profile /nix/var/nix/profiles/system | grep current | awk '{print $1}') echo -e "Running on new generation: $YELLOW $geno $NOCOLOR-> $GREEN$genn$NOCOLOR" else ( exit "$_rebuild_exit" ); handle_build_error echo "✗ Update and rebuild failed for $HOST" >&2 exit 1 fi ;; commit) cd "$HOME/$PROJECT" || { echo "Error: Could not change to $HOME/$PROJECT"; exit 1; } if [ "$#" -lt 2 ]; then read -p "Enter commit message: " commit_msg else shift commit_msg="$*" fi if [ -z "$commit_msg" ]; then echo "Error: Commit message cannot be empty" >&2 exit 1 fi git add -A && git commit -m "$commit_msg" ;; home-backups) ls -a ~ | grep backup ;; switch) current="" if [ -f /etc/nixos-tags ]; then current=$(cat /etc/nixos-tags) fi if [ "$#" -ge 2 ]; then spec_name="$2" if [ -n "$current" ]; then echo "Already on specialization: $current. Cannot switch directly to '$spec_name'. Please reboot or return to default first." else if [ -d "/run/current-system/specialisation/$spec_name" ]; then echo "Switching to specialization: $spec_name" sudo /run/current-system/specialisation/$spec_name/bin/switch-to-configuration test else echo "Error: Specialization '$spec_name' not found." echo "Available specializations:" ls /run/current-system/specialisation/ fi fi else if [ -n "$current" ]; then echo "Already on a specialization: $current. To switch, please reboot, or use 'sudo nixos-rebuild switch --flake .' to get back to default, and then switch after." else specs=$(ls /run/current-system/specialisation/) echo "Specializations available:" echo "$specs" echo "" echo "To switch to a specialization, run: 'ncli switch '" fi fi ;; push) cd "$HOME/$PROJECT" || { echo "Error: Could not change to $HOME/$PROJECT"; exit 1; } git push origin $(git branch --show-current) ;; pull) cd "$HOME/$PROJECT" || { echo "Error: Could not change to $HOME/$PROJECT"; exit 1; } git pull origin $(git branch --show-current) ;; status) cd "$HOME/$PROJECT" || { echo "Error: Could not change to $HOME/$PROJECT"; exit 1; } git status ;; format) nix fmt . ;; trim) echo "Running 'sudo fstrim -v /' may take a few minutes and impact system performance." read -p "Enter to run now or enter to exit (y/N): " -n 1 -r echo # move to a new line if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Running fstrim..." sudo fstrim -v / echo "fstrim complete." else echo "Trim operation cancelled." fi ;; dev) # Only flake files need assume-unchanged (Nix requires them in the index). # .envrc and .direnv are handled via .git/info/exclude instead. DEV_FILES=(flake.nix flake.lock) DIRENV_LOCAL_FILES=(.envrc .direnv) _dev_resolve_dir() { read -rp " Enter target directory [./]: " TARGET_DIR TARGET_DIR="''${TARGET_DIR:-./}" if [[ ! -d "$TARGET_DIR" ]]; then print_error "Directory '$TARGET_DIR' does not exist." exit 1 fi TARGET_ABS="$(cd "$TARGET_DIR" && pwd)" if ! git -C "$TARGET_ABS" rev-parse --git-dir > /dev/null 2>&1; then print_error "No Git repository found at '$TARGET_ABS'." exit 1 fi } # Add .envrc and .direnv to .git/info/exclude (local-only, never committed). _add_local_excludes() { local repo_abs="$1" local git_dir exclude_file git_dir="$(git -C "$repo_abs" rev-parse --git-dir)" exclude_file="$git_dir/info/exclude" mkdir -p "$(dirname "$exclude_file")" touch "$exclude_file" for entry in "''${DIRENV_LOCAL_FILES[@]}"; do if ! grep -qxF "$entry" "$exclude_file" 2>/dev/null; then echo "$entry" >> "$exclude_file" print_success "$entry → ignored via .git/info/exclude (local only)" else print_info "$entry already in .git/info/exclude, skipping." fi done } _remove_local_excludes() { local repo_abs="$1" local git_dir exclude_file tmp_file git_dir="$(git -C "$repo_abs" rev-parse --git-dir)" exclude_file="$git_dir/info/exclude" [[ -f "$exclude_file" ]] || return 0 tmp_file="$(mktemp)" cp "$exclude_file" "$tmp_file" for entry in "''${DIRENV_LOCAL_FILES[@]}"; do if grep -qxF "$entry" "$tmp_file" 2>/dev/null; then grep -vxF "$entry" "$tmp_file" > "''${tmp_file}.new" || true mv "''${tmp_file}.new" "$tmp_file" print_success "$entry → removed from .git/info/exclude" else print_info "$entry not present in .git/info/exclude, skipping." fi done mv "$tmp_file" "$exclude_file" } case "''${2:-init}" in untrack) echo "" echo -e "''${BLUE}--- Dev: Track Files (assume-unchanged) ---''${NOCOLOR}" echo "" _dev_resolve_dir acted=false for f in "''${DEV_FILES[@]}"; do full="$TARGET_ABS/$f" if [[ ! -f "$full" ]]; then print_info "$f not found in $TARGET_ABS — skipping." continue fi if ! git -C "$TARGET_ABS" ls-files --error-unmatch "$f" > /dev/null 2>&1; then git -C "$TARGET_ABS" add --intent-to-add "$f" fi git -C "$TARGET_ABS" update-index --assume-unchanged "$f" print_success "$f → untracked (assume-unchanged)" acted=true done _add_local_excludes "$TARGET_ABS" if ! $acted; then print_info "No dev files found in '$TARGET_ABS' to track." fi ;; track) echo "" echo -e "''${BLUE}--- Dev: Untrack Files (remove assume-unchanged) ---''${NOCOLOR}" echo "" _dev_resolve_dir acted=false for f in "''${DEV_FILES[@]}"; do if git -C "$TARGET_ABS" ls-files -v "$f" 2>/dev/null | grep -q "^h "; then git -C "$TARGET_ABS" update-index --no-assume-unchanged "$f" print_success "$f → tracked (assume-unchanged removed)" acted=true else print_info "$f is not marked assume-unchanged — skipping." fi done _remove_local_excludes "$TARGET_ABS" if ! $acted; then print_info "No files in '$TARGET_ABS' had the assume-unchanged bit set." fi ;; init|*) # ---- Defaults ----- SOURCE_FLAKE="$HOME/$PROJECT/other/dev-template.nix" # ---- Check source flake exists ------------------------------ if [[ ! -f "$SOURCE_FLAKE" ]]; then print_error "Source flake template not found: $SOURCE_FLAKE" echo "Please ensure dev-template.nix exists in $HOME/$PROJECT/other/" exit 1 fi print_header # ---- Step 1: Target Directory ----------------------------------- echo -e "''${YELLOW}Step 1/5: Target Directory''${NOCOLOR}" echo "" read -p " Enter target directory [./]: " TARGET_DIR if [[ -z "$TARGET_DIR" ]]; then TARGET_DIR="./" fi if [[ ! -d "$TARGET_DIR" ]]; then print_error "Directory '$TARGET_DIR' does not exist. Exiting." exit 1 fi print_success "Target directory: $TARGET_DIR" echo "" # ---- Check for existing flake ---------------------------------- if [[ -f "$TARGET_DIR/flake.nix" ]]; then echo "" print_error "A flake.nix already exists in '$TARGET_DIR'!" echo "" read -p " Would you like to just place the .envrc instead? [y/N]: " ENVRC_ONLY ENVRC_ONLY="''${ENVRC_ONLY:-N}" if [[ "$ENVRC_ONLY" =~ ^[Yy]$ ]]; then TARGET_ABS="$(cd "$TARGET_DIR" && pwd)" echo "" print_info "Placing .envrc only..." if ! grep -qxF "use flake" "$TARGET_ABS/.envrc" 2>/dev/null; then echo "use flake" >> "$TARGET_ABS/.envrc" print_success ".envrc created at $TARGET_ABS/" else print_info ".envrc already contains 'use flake', nothing to do." fi echo "" print_info "Next steps:" echo " cd $TARGET_DIR" echo " direnv allow" echo "" exit 0 else print_info "Aborting. No files were changed." exit 1 fi fi # ---- Step 2: Project Name --------------------------------------- echo -e "''${YELLOW}Step 2/5: Project Name''${NOCOLOR}" echo "" read -p " Enter development environment name [devShell]: " PROJECT_NAME if [[ -z "$PROJECT_NAME" ]]; then PROJECT_NAME="devShell" fi print_success "Project name: $PROJECT_NAME" echo "" # ---- Step 3: Select template ---------------------------------------- echo -e "''${YELLOW}Step 3/5: Select Template''${NOCOLOR}" echo "" echo " 1) Empty - Basic shell, add packages yourself" echo " 2) Python - Python 3, pip, venv setup" echo " 3) Node.js - Node.js, npm" echo " 4) Rust - Rustc, cargo, rust-analyzer" echo " 5) Go - Go, gopls, golangci-lint" echo " 6) C/C++ - GCC, CMake, GDB, pkg-config" echo " 7) Java - JDK 21, Maven" echo "" read -p " Select template [1-7] (default: 1): " TEMPLATE_CHOICE case "$TEMPLATE_CHOICE" in 2) TEMPLATE="python3" PACKAGES="python3 python3Packages.pip python3Packages.virtualenv" BUILD_INPUTS="" SHELL_HOOK_EXTRA=" # Create venv if it doesn't exist if [ ! -d .venv ]; then echo 'Creating Python virtual environment...' python3 -m venv .venv fi # Activate venv source .venv/bin/activate echo 'Python venv activated.' " ;; 3) TEMPLATE="node" PACKAGES="nodejs_22" BUILD_INPUTS="" SHELL_HOOK_EXTRA='echo "Node.js $(node --version) ready."' ;; 4) TEMPLATE="rust" PACKAGES="rustc cargo rust-analyzer" BUILD_INPUTS="openssl pkg-config" SHELL_HOOK_EXTRA='echo "Rust $(rustc --version) ready."' ;; 5) TEMPLATE="go" PACKAGES="go gopls golangci-lint" BUILD_INPUTS="" SHELL_HOOK_EXTRA=' export GOPATH="$PWD/.gopath" export PATH="$GOPATH/bin:$PATH" mkdir -p "$GOPATH" echo "Go $(go version) ready. GOPATH: $GOPATH" ' ;; 6) TEMPLATE="cpp" PACKAGES="gcc gdb cmake gnumake" BUILD_INPUTS="pkg-config" SHELL_HOOK_EXTRA='echo "GCC $(gcc --version | head -1) ready."' ;; 7) TEMPLATE="java" PACKAGES="jdk21 maven" BUILD_INPUTS="" SHELL_HOOK_EXTRA='echo "Java $(java --version | head -1) ready."' ;; *) TEMPLATE="empty" PACKAGES="" BUILD_INPUTS="" SHELL_HOOK_EXTRA="" ;; esac print_success "Selected template: $TEMPLATE" echo "" # ---- Step 4: Copy and modify flake ---------------------------------- echo -e "''${YELLOW}Step 4/5: Generating flake.nix''${NOCOLOR}" echo "" cp "$SOURCE_FLAKE" "$TARGET_DIR/flake.nix" print_success "Copied flake.nix to $TARGET_DIR/" sed -i "s|name = \"replaceNameHere\";|name = \"''${PROJECT_NAME}\";|" "$TARGET_DIR/flake.nix" print_success "Set project name: $PROJECT_NAME" if [[ -n "$PACKAGES" ]]; then sed -i "s|# replacePackagesHere|$PACKAGES|" "$TARGET_DIR/flake.nix" print_success "Added packages: $PACKAGES" fi if [[ -n "$BUILD_INPUTS" ]]; then BUILD_INPUTS_LINE="buildInputs = with pkgs; [ $BUILD_INPUTS ];" sed -i "s|# replaceBuildInputsHere|$BUILD_INPUTS_LINE|" "$TARGET_DIR/flake.nix" print_success "Added build inputs: $BUILD_INPUTS" fi if [[ -n "$SHELL_HOOK_EXTRA" ]]; then awk -v hook="$SHELL_HOOK_EXTRA" '/# replaceShellHookHere/ { print hook; next } { print }' \ "$TARGET_DIR/flake.nix" > "$TARGET_DIR/flake.nix.tmp" \ && mv "$TARGET_DIR/flake.nix.tmp" "$TARGET_DIR/flake.nix" print_success "Added template-specific shell hook" fi # ---- Step 5: Git Integration & .envrc -------------------------------- echo -e "''${YELLOW}Step 5/5: Git Integration''${NOCOLOR}" echo "" TARGET_ABS="$(cd "$TARGET_DIR" && pwd)" echo "Creating .envrc for direnv to automatically work!" if ! grep -qxF "use flake" "$TARGET_ABS/.envrc" 2>/dev/null; then echo "use flake" >> "$TARGET_ABS/.envrc" print_success ".envrc created at $TARGET_ABS/" else print_info ".envrc already contains 'use flake', skipping." fi if git -C "$TARGET_ABS" rev-parse --git-dir > /dev/null 2>&1; then echo " A Git repository was detected at:" echo " $(git -C "$TARGET_ABS" rev-parse --show-toplevel)" echo "" # Always add .envrc and .direnv to local exclude — no prompt needed, # these should never be committed regardless. _add_local_excludes "$TARGET_ABS" echo "" echo " Nix flakes require flake.nix (and flake.lock) to be Git-tracked." echo " This script can stage them as 'assume-unchanged' so Nix sees them," echo " but they will NEVER be committed or show up in git status." echo "" read -p " Set up flake files as tracked-but-invisible to Git? [y/N]: " GIT_CHOICE GIT_CHOICE="''${GIT_CHOICE:-N}" if [[ "$GIT_CHOICE" =~ ^[Yy]$ ]]; then cat > "$TARGET_ABS/flake.lock" << 'EOF' { "nodes": { "root": {} }, "root": "root", "version": 7 } EOF print_success "flake.lock ready (Nix will populate it on first run if empty)" git -C "$TARGET_ABS" add --intent-to-add flake.nix flake.lock git -C "$TARGET_ABS" update-index --assume-unchanged flake.nix flake.lock print_success "flake.nix + flake.lock → tracked (assume-unchanged)" else print_info "Skipped. You can do this manually later:" echo "" echo " git -C \"$TARGET_ABS\" add --intent-to-add flake.nix flake.lock" echo " git -C \"$TARGET_ABS\" update-index --assume-unchanged flake.nix flake.lock" echo "" fi else print_info "No Git repository detected — skipping Git integration." echo "" fi # ---- Done --------------------------------------------------- echo -e "''${GREEN}==============================================''${NOCOLOR}" echo -e "''${GREEN} Done! Your flake is ready at:''${NOCOLOR}" echo -e "''${GREEN} $TARGET_DIR/flake.nix''${NOCOLOR}" echo -e "''${GREEN}==============================================''${NOCOLOR}" echo "" print_info "Next steps:" echo " cd $TARGET_DIR" echo " direnv allow # Trust the .envrc so direnv auto-activates" echo " nix develop # Or just: cd out and back in" echo "" ;; esac ;; *) echo "Error: Invalid command '$1'" >&2 print_help exit 1 ;; esac ''