Split ncli across multiple files in its own directory

This commit is contained in:
2026-05-27 14:34:25 +02:00
parent 6b60113552
commit d94e534ee4
10 changed files with 892 additions and 762 deletions

View File

@ -14,7 +14,7 @@
home.username = username;
home.homeDirectory = "/home/${username}";
home.packages = [
(import ./ncli.nix {
(import ./ncli/default.nix {
inherit pkgs host project;
backupFiles = [
".gtkrc-2.0.backup"

View File

@ -1,761 +0,0 @@
{
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 <tag>'"
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
''

133
modules/ncli/builder.nix Normal file
View File

@ -0,0 +1,133 @@
# ncli/builder.nix — Assembles all modules into the final script
{
pkgs,
host,
backupFiles ? [".config/mimeapps.list.backup"],
project,
...
}: let
# --- Shared library ---
lib = import ./lib.nix {inherit pkgs host project backupFiles;};
# --- Command modules ---
rebuild = import ./commands/rebuild.nix lib;
maintenance = import ./commands/maintenance.nix lib;
git = import ./commands/git.nix lib;
switch = import ./commands/switch.nix lib;
dev = import ./commands/dev.nix lib;
# --- Help text ---
print_help = ''
echo "NixOS CLI Utility -- version 2.1.3"
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 ""
'';
in
pkgs.writeShellScriptBin "ncli" ''
#!${pkgs.bash}/bin/bash
set -euo pipefail
# --- Configuration ---
PROJECT="${project}"
HOST="${host}"
BACKUP_FILES_STR="${lib.backupFilesString}"
VERSION="2.2.0"
FLAKE_NIX_PATH="$HOME/$PROJECT/flake.nix"
read -r -a BACKUP_FILES <<< "$BACKUP_FILES_STR"
# --- Read Colors file ---
source ${lib.colorsSource}
# --- Helper Functions ---
${lib.handle_backups}
${lib.handle_build_error}
${lib.print_header}
${lib.print_success}
${lib.print_error}
${lib.print_info}
# --- Main Logic ---
if [ "$#" -eq 0 ]; then
echo "Error: No command provided." >&2
print_help
exit 1
fi
case "$1" in
rebuild)
${rebuild.rebuild_case}
;;
update)
${rebuild.update_case}
;;
cleanup)
${maintenance.cleanup_case}
;;
diag)
${maintenance.diag_case}
;;
list-gens)
${maintenance.list_gens_case}
;;
trim)
${maintenance.trim_case}
;;
home-backups)
${maintenance.home_backups_case}
;;
commit)
${git.commit_case}
;;
push)
${git.push_case}
;;
pull)
${git.pull_case}
;;
status)
${git.status_case}
;;
format)
${git.format_case}
;;
switch)
${switch.switch_case}
;;
dev)
${dev.dev_case}
;;
help)
${print_help}
;;
*)
echo "Error: Invalid command '$1'" >&2
print_help
exit 1
;;
esac
''

View File

@ -0,0 +1,367 @@
# ncli/commands/dev.nix — dev init / track / untrack
lib:
{
dev_case = ''
# 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_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|*)
SOURCE_FLAKE="$HOME/${lib.project}/other/dev-template.nix"
if [[ ! -f "$SOURCE_FLAKE" ]]; then
print_error "Source flake template not found: $SOURCE_FLAKE"
echo "Please ensure dev-template.nix exists in $HOME/${lib.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="
if [ ! -d .venv ]; then
echo 'Creating Python virtual environment...'
python3 -m venv .venv
fi
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 ""
_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" << 'FLAKE_LOCK_EOF'
{
"nodes": {
"root": {}
},
"root": "root",
"version": 7
}
FLAKE_LOCK_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
;;
'';
}

View File

@ -0,0 +1,48 @@
# ncli/commands/git.nix — commit, push, pull, status, format
lib:
let
inherit (lib) project;
in
{
commit_case = ''
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"
;;
'';
push_case = ''
cd "$HOME/${project}" || { echo "Error: Could not change to $HOME/${project}"; exit 1; }
git push origin $(git branch --show-current)
;;
'';
pull_case = ''
cd "$HOME/${project}" || { echo "Error: Could not change to $HOME/${project}"; exit 1; }
git pull origin $(git branch --show-current)
;;
'';
status_case = ''
cd "$HOME/${project}" || { echo "Error: Could not change to $HOME/${project}"; exit 1; }
git status
;;
'';
format_case = ''
nix fmt .
;;
'';
}

View File

@ -0,0 +1,83 @@
# ncli/commands/maintenance.nix — cleanup, diag, list-gens, trim, home-backups
lib:
let
inherit (lib) project;
in
{
cleanup_case = ''
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_case = ''
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"
;;
'';
list_gens_case = ''
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."
;;
'';
trim_case = ''
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
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Running fstrim..."
sudo fstrim -v /
echo "fstrim complete."
else
echo "Trim operation cancelled."
fi
;;
'';
home_backups_case = ''
ls -a ~ | grep backup
;;
'';
}

View File

@ -0,0 +1,132 @@
# ncli/commands/rebuild.nix — Rebuild + Update logic
lib:
let
inherit (lib) host project handle_backups handle_build_error;
in
{
# Shared rebuild snippet used by both `rebuild` and `update`
rebuild_logic = ''
handle_backups
geno=$(sudo nix-env --list-generations --profile /nix/var/nix/profiles/system | grep current | awk '{print $1}')
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
'';
rebuild_case = ''
${rebuild_logic}
echo -e "Starting NixOS rebuild for current host: ${host} on generation: $YELLOW$geno$NOCOLOR"
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_case = ''
${rebuild_logic}
echo -e "Updating flake and rebuilding system for current host: ${host} on generation: $YELLOW$geno$NOCOLOR"
# --- 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 ---
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
;;
'';
}

View File

@ -0,0 +1,40 @@
# ncli/commands/switch.nix — Specialization switching
lib:
{}:
{
switch_case = ''
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 <tag>'"
fi
fi
;;
'';
}

12
modules/ncli/default.nix Normal file
View File

@ -0,0 +1,12 @@
# ncli/default.nix — Entry point
# Usage in your flake:
# ncli = import ./ncli { inherit pkgs host project; };
# environment.systemPackages = [ ncli ];
{
pkgs,
host,
backupFiles ? [".config/mimeapps.list.backup"],
project,
...
}:
import ./builder.nix {inherit pkgs host backupFiles project;}

76
modules/ncli/lib.nix Normal file
View File

@ -0,0 +1,76 @@
# ncli/lib.nix — Shared helpers and utilities
{ pkgs, host, project, backupFiles }:
let
backupFilesString = pkgs.lib.strings.concatStringsSep " " backupFiles;
in
{
# --- Injected config ---
inherit pkgs host project backupFiles backupFilesString;
# --- Color / banner source ---
colorsSource = "$HOME/${project}/other/colors.sh";
# --- Backup helper ---
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
'';
# --- OOM / build error handler ---
handle_build_error = ''
local exit_code=$?
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
'';
# --- Print helpers (used by dev init) ---
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"
'';
}