This commit is contained in:
kumar vaibhav 2025-11-21 19:45:32 +05:30
parent 0cf22d6e5e
commit 980b1c01a8
2 changed files with 336 additions and 524 deletions

289
README.md
View file

@ -1,14 +1,15 @@
# fedora-home-manager # Forge - A Home-Manager Like Tool for Fedora
A home-manager like script for Fedora that provides dotfile management and package installation functionality. A lightweight configuration management tool for Fedora that provides dotfile management and package installation functionality through a centralized TOML configuration.
## Overview ## Overview
fedora-home-manager is a bash script that helps you manage your Fedora system configuration by: Forge is a bash script that helps you manage your Fedora system configuration by:
- Installing and managing system packages via dnf - Installing and managing system packages via dnf
- Deploying and managing dotfiles to your home directory - Managing dotfiles through symlinks to actual files
- Automatically cleaning up packages that are no longer needed - Centralized configuration via a single `forge.toml` file
- Tracking state of managed files and packages - User-level configuration similar to home-manager/nixos
- Automatic backup of existing files before replacement
## Installation ## Installation
@ -19,7 +20,7 @@ fedora-home-manager is a bash script that helps you manage your Fedora system co
``` ```
3. Optionally, move it to a directory in your PATH: 3. Optionally, move it to a directory in your PATH:
```bash ```bash
sudo mv forge /usr/local/bin/fedora-home-manager sudo mv forge /usr/local/bin/forge
``` ```
## Quick Start ## Quick Start
@ -28,88 +29,44 @@ fedora-home-manager is a bash script that helps you manage your Fedora system co
# Initialize the configuration structure # Initialize the configuration structure
./forge init ./forge init
# Add a dotfile to management # Edit the configuration file to add packages and dotfiles
./forge add ~/.bashrc nano ~/.config/forge/forge.toml
# Add packages to install (edit the packages file)
echo "git = \"latest\"" >> ~/.config/fedora-home-manager/packages.toml
echo "vim = \"latest\"" >> ~/.config/fedora-home-manager/packages.toml
# Apply configuration # Apply configuration
./forge switch ./forge switch
# Check status
./forge status
``` ```
## Commands ## Commands
### `init` ### `init`
Initialize the configuration structure and create example files. Initialize the configuration structure and create `forge.toml`.
```bash ```bash
./forge init ./forge init
``` ```
Creates: Creates:
- `~/.config/fedora-home-manager/` - Main configuration directory - `~/.config/forge/` - Main configuration directory
- `~/.config/fedora-home-manager/dotfiles/` - Directory for your dotfiles - `~/.config/forge/forge.toml` - Central configuration file
- `~/.config/fedora-home-manager/packages.toml` - TOML file with packages to install
- `~/.config/fedora-home-manager/state.json` - State tracking file
- `~/.config/fedora-home-manager/backup/` - Backup directory for existing files
### `switch` ### `switch`
Apply the current configuration (install packages, deploy dotfiles, cleanup old packages). Apply the current configuration (install packages, deploy dotfiles).
```bash ```bash
./forge switch ./forge switch
``` ```
This command: This command:
1. Installs all packages listed in `packages.toml` 1. Installs all packages listed in `forge.toml`
2. Uninstalls packages that are no longer in `packages.toml` 2. Creates symlinks for all configured dotfiles
3. Deploys all dotfiles from the dotfiles directory to your home 3. Creates backups of existing files before replacing them
4. Creates backups of existing files before overwriting 4. Updates the last switch timestamp
5. Updates the state tracking
### `build`
Alias for `switch` command.
```bash
./forge build
```
### `add <file>`
Add a file to dotfiles management.
```bash
./forge add ~/.bashrc
./forge add ~/.vimrc
./forge add ~/.config/alacritty/alacritty.yml
```
The file is copied to the dotfiles directory while maintaining its relative path structure.
### `remove <path>`
Remove a dotfile from management.
```bash
./forge remove .bashrc
./forge remove .config/alacritty/alacritty.yml
```
Optionally restores the original file from backup.
### `list`
List all managed files and packages.
```bash
./forge list
```
Shows:
- All dotfiles currently under management
- All packages that will be installed
### `status` ### `status`
Show current status and configuration information. Show current configuration status and information.
```bash ```bash
./forge status ./forge status
@ -118,7 +75,7 @@ Show current status and configuration information.
Displays: Displays:
- Configuration directory paths - Configuration directory paths
- Last switch timestamp - Last switch timestamp
- List of managed files and packages - List of configured packages and dotfiles
### `help` ### `help`
Show help message with all available commands. Show help message with all available commands.
@ -127,122 +84,168 @@ Show help message with all available commands.
./forge help ./forge help
``` ```
## Configuration Files ## Configuration
### forge.toml
The central configuration file located at `~/.config/forge/forge.toml`:
### packages.toml
List of Fedora packages to install in TOML format:
```toml ```toml
# Fedora packages configuration # Forge Configuration File
# TOML format for better package management # User-level configuration similar to home-manager/nixos
[forge]
version = "1.0"
last_switch = null
[packages] [packages]
# Core development tools # List of packages to install using dnf
git = "latest" # Set to true to install, false or comment out to skip
vim = "latest" git = true
curl = "latest" vim = true
wget = "latest" curl = true
wget = true
# Additional packages can be added with version specifications [dotfiles]
# Example: package_name = "version" or "latest" # Dotfiles to manage with symlinks
# Format: "target_path" = "source_path"
# target_path: where the symlink should be created (relative to home directory)
# source_path: where the actual file is stored (relative to forge directory)
".bashrc" = "dotfiles/.bashrc"
".config/vimrc" = "dotfiles/vimrc"
".config/alacritty/alacritty.yml" = "dotfiles/alacritty.yml"
[options]
# Additional options
backup = true
backup_dir = "backup"
``` ```
### dotfiles directory ### Configuration Sections
Place your dotfiles in `~/.config/fedora-home-manager/dotfiles/` maintaining the same directory structure as in your home:
#### `[forge]`
- `version`: Configuration file version
- `last_switch`: Timestamp of last switch operation (auto-updated)
#### `[packages]`
List of packages to install via dnf. Set to `true` to install, `false` or comment out to skip.
#### `[dotfiles]`
Dotfile mappings using symlinks:
- **Key**: Target path where symlink should be created (relative to home directory)
- **Value**: Source path where actual file is stored (relative to forge directory)
#### `[options]`
Additional configuration options:
- `backup`: Enable/disable backup of existing files (default: true)
- `backup_dir`: Directory name for backups (relative to forge directory)
## File Structure
``` ```
~/.config/fedora-home-manager/dotfiles/ ~/.config/forge/
├── .bashrc ├── forge.toml # Main configuration file
├── .vimrc ├── dotfiles/ # Your actual dotfiles (source files)
└── .config/ │ ├── .bashrc
└── alacritty/ │ ├── vimrc
└── alacritty.yml │ └── alacritty.yml
└── backup/ # Backups of replaced files
├── .bashrc.20231121_143022.bak
└── vimrc.20231121_143022.bak
``` ```
### state.json ## How It Works
Internal state tracking file (managed automatically):
- Tracks installed packages for cleanup
- Records last switch timestamp
- Maintains list of managed files
## Features ### Dotfile Management
Forge uses symlinks to manage dotfiles:
1. Your actual dotfiles are stored in `~/.config/forge/dotfiles/`
2. Symlinks are created from your home directory to these files
3. This allows you to version control your dotfiles in one place
4. Changes to the source files are immediately reflected in your home directory
### Automatic Package Cleanup ### Package Management
When you remove a package from `packages.toml` and run `switch`, the package will be automatically uninstalled from your system. Packages are managed through the `[packages]` section in `forge.toml`:
- Packages set to `true` are installed via `dnf`
### Package State Tracking - Packages set to `false` or commented out are ignored
Previously installed packages are tracked to enable cleanup operations. - No automatic uninstallation (unlike the previous version)
### TOML Format Support
Packages are managed in TOML format for better organization and version support.
### Backup System ### Backup System
Before overwriting existing files, the script creates timestamped backups in the backup directory. Before creating symlinks, Forge:
1. Checks if the target file exists and is not a symlink
### State Tracking 2. Creates a timestamped backup in the backup directory
Previously installed packages are tracked to enable intelligent cleanup operations. 3. Removes the original file
4. Creates the symlink to your managed dotfile
## Environment Variables
- `HOME_MANAGER_DIR`: Override the default configuration directory (default: `~/.config/fedora-home-manager`)
## Dependencies
- `dnf` - Fedora package manager
- `jq` - JSON processor (optional, for enhanced state tracking)
## Examples ## Examples
### Basic Setup ### Basic Setup
```bash ```bash
# Initialize # Initialize forge
./forge init ./forge init
# Add your shell configuration # Create your dotfiles directory and add files
./forge add ~/.bashrc mkdir -p ~/.config/forge/dotfiles
./forge add ~/.bash_profile echo "export EDITOR=vim" > ~/.config/forge/dotfiles/.bashrc
# Add development tools # Edit forge.toml to configure
cat >> ~/.config/fedora-home-manager/packages.toml << 'EOF' nano ~/.config/forge/forge.toml
git = "latest"
vim = "latest" # Add to [packages] section:
nodejs = "latest" git = true
npm = "latest" vim = true
EOF
# Add to [dotfiles] section:
".bashrc" = "dotfiles/.bashrc"
# Apply configuration # Apply configuration
./forge switch ./forge switch
``` ```
### Managing Configuration Files ### Managing Application Configurations
```bash ```bash
# Add application configurations # Add alacritty configuration
./forge add ~/.config/alacritty/alacritty.yml mkdir -p ~/.config/forge/dotfiles/.config/alacritty
./forge add ~/.config/git/config cp ~/.config/alacritty/alacritty.yml ~/.config/forge/dotfiles/.config/alacritty/
./forge add ~/.config/nvim/init.vim
# Check what's managed # Edit forge.toml
./forge list nano ~/.config/forge/forge.toml
# Add to [dotfiles] section:
".config/alacritty/alacritty.yml" = "dotfiles/.config/alacritty/alacritty.yml"
# Apply changes # Apply changes
./forge switch ./forge switch
``` ```
### Removing Packages ### Version Control Your Configuration
```bash ```bash
# Edit packages.toml and remove entries for packages you don't want # Initialize git repository in forge directory
# Then run switch to uninstall them cd ~/.config/forge
./forge switch git init
git add .
git commit -m "Initial configuration"
# Now you can version control your entire system configuration
git add forge.toml dotfiles/
git commit -m "Updated vim configuration"
``` ```
## File Structure ## Environment Variables
``` - `FORGE_DIR`: Override the default configuration directory (default: `~/.config/forge`)
~/.config/fedora-home-manager/
├── dotfiles/ # Your dotfiles to deploy ## Dependencies
├── packages.toml # TOML file with packages to install
├── state.json # State tracking - `dnf` - Fedora package manager
└── backup/ # Backups of replaced files - `sed` - For updating configuration file (usually pre-installed)
```
## Migration from Previous Version
If you were using the old version of forge:
1. Your existing configuration will not be automatically migrated
2. Run `./forge init` to create the new `forge.toml` structure
3. Manually migrate your packages and dotfile configurations to the new TOML format
4. Move your existing dotfiles from the old directory to `~/.config/forge/dotfiles/`
## License ## License

529
forge
View file

@ -7,10 +7,7 @@ set -euo pipefail
# Configuration # Configuration
FORGE_DIR="${FORGE_DIR:-$HOME/.config/forge}" FORGE_DIR="${FORGE_DIR:-$HOME/.config/forge}"
DOTFILES_DIR="$FORGE_DIR/dotfiles" FORGE_TOML="$FORGE_DIR/forge.toml"
PACKAGES_FILE="$FORGE_DIR/packages.toml"
STATE_FILE="$FORGE_DIR/state.json"
BACKUP_DIR="$FORGE_DIR/backup"
# Colors # Colors
RED='\033[0;31m' RED='\033[0;31m'
@ -36,114 +33,88 @@ log_error() {
echo -e "${RED}[ERROR]${NC} $1" echo -e "${RED}[ERROR]${NC} $1"
} }
# Initialize command - set up the configuration structure # Initialize command - creates forge.toml and .config directory
init() { init() {
log_info "Initializing forge..." log_info "Initializing forge..."
# Create directories # Create directory
mkdir -p "$FORGE_DIR" mkdir -p "$FORGE_DIR"
mkdir -p "$DOTFILES_DIR"
mkdir -p "$BACKUP_DIR"
# Create initial state file if it doesn't exist # Create initial forge.toml if it doesn't exist
if [[ ! -f "$STATE_FILE" ]]; then if [[ ! -f "$FORGE_TOML" ]]; then
echo '{"version": "1.0", "last_switch": null, "managed_files": []}' >"$STATE_FILE" cat >"$FORGE_TOML" <<'EOF'
log_success "Created state file: $STATE_FILE" # Forge Configuration File
fi # User-level configuration similar to home-manager/nixos
# Create initial packages file with simplified format if it doesn't exist [forge]
if [[ ! -f "$PACKAGES_FILE" ]]; then version = "1.0"
cat >"$PACKAGES_FILE" <<'EOF' last_switch = null
# Forge packages configuration
# Simple format: just list package names, one per line
# Core development tools [packages]
# git # List of packages to install using dnf
# vim # Examples:
# curl # git = true
# wget # vim = true
# curl = true
# Additional packages can be added as simple package names [dotfiles]
# Example: package_name # Dotfiles to manage with symlinks
# Format: "target_path" = "source_path"
# target_path: where the symlink should be created (relative to home directory)
# source_path: where the actual file is stored (relative to forge directory)
# Examples:
# ".bashrc" = "dotfiles/.bashrc"
# ".config/vimrc" = "dotfiles/vimrc"
[options]
# Additional options
backup = true
backup_dir = "backup"
EOF EOF
log_success "Created packages file: $PACKAGES_FILE" log_success "Created forge.toml: $FORGE_TOML"
fi
# Create example dotfiles structure
if [[ ! -f "$DOTFILES_DIR/.gitkeep" ]]; then
touch "$DOTFILES_DIR/.gitkeep"
log_info "Created dotfiles directory: $DOTFILES_DIR"
fi fi
log_success "Initialization complete!" log_success "Initialization complete!"
log_info "Add packages to $PACKAGES_FILE and dotfiles to $DOTFILES_DIR/" log_info "Edit $FORGE_TOML to configure packages and dotfiles"
log_info "Run 'forge switch' to apply your configuration" log_info "Run 'forge switch' to apply your configuration"
} }
# Initialize directories # Parse TOML file (basic implementation)
init_dirs() { parse_toml() {
mkdir -p "$FORGE_DIR" local section=""
mkdir -p "$DOTFILES_DIR"
mkdir -p "$BACKUP_DIR"
# Create initial state file if it doesn't exist
if [[ ! -f "$STATE_FILE" ]]; then
echo '{"version": "1.0", "last_switch": null, "managed_files": []}' >"$STATE_FILE"
fi
# Create initial packages file if it doesn't exist
if [[ ! -f "$PACKAGES_FILE" ]]; then
touch "$PACKAGES_FILE"
fi
}
# Backup existing files
backup_file() {
local file="$1"
local backup_path="$BACKUP_DIR/$(basename "$file").$(date +%Y%m%d_%H%M%S).bak"
if [[ -f "$file" ]]; then
cp "$file" "$backup_path"
log_info "Backed up $file to $backup_path"
fi
}
# Restore files from backup
restore_backup() {
local file="$1"
local latest_backup=$(ls -t "$BACKUP_DIR"/$(basename "$file")*.bak 2>/dev/null | head -1)
if [[ -n "$latest_backup" ]]; then
cp "$latest_backup" "$file"
log_success "Restored $file from backup"
return 0
else
log_warning "No backup found for $file"
return 1
fi
}
# Install packages using dnf
install_packages() {
if [[ ! -f "$PACKAGES_FILE" ]] || [[ ! -s "$PACKAGES_FILE" ]]; then
log_info "No packages to install"
return 0
fi
log_info "Installing packages..."
local current_packages=()
# Parse simple format and extract package names
while IFS= read -r line; do while IFS= read -r line; do
# Skip empty lines and comments # Skip comments and empty lines
[[ -z "$line" || "$line" == \#* ]] && continue [[ -z "$line" || "$line" == \#* ]] && continue
# Remove whitespace and use as package name # Check for section headers
local package="${line// /}" # Remove whitespace if [[ "$line" =~ ^\[(.+)\]$ ]]; then
if [[ -n "$package" ]]; then section="${BASH_REMATCH[1]}"
current_packages+=("$package") continue
fi
# Parse key-value pairs
if [[ "$line" =~ ^([^=]+)=(.+)$ ]]; then
local key="${BASH_REMATCH[1]// /}"
local value="${BASH_REMATCH[2]// /}"
echo "$section:$key=$value"
fi
done <"$FORGE_TOML"
}
# Install packages from forge.toml
install_packages() {
log_info "Installing packages..."
local packages_installed=false
while IFS='=' read -r entry; do
local section="${entry%%:*}"
local key_value="${entry#*:}"
local package="${key_value%%=*}"
local enabled="${key_value##*=}"
if [[ "$section" == "packages" && "$enabled" == "true" ]]; then
packages_installed=true
log_info "Installing package: $package" log_info "Installing package: $package"
if sudo dnf install -y "$package"; then if sudo dnf install -y "$package"; then
log_success "Installed $package" log_success "Installed $package"
@ -151,143 +122,74 @@ install_packages() {
log_error "Failed to install $package" log_error "Failed to install $package"
fi fi
fi fi
done <"$PACKAGES_FILE" done < <(parse_toml)
# Update state with current packages if [[ "$packages_installed" == false ]]; then
update_installed_packages "${current_packages[@]}" log_info "No packages to install"
}
# Uninstall packages that are no longer in packages.toml
cleanup_packages() {
log_info "Cleaning up packages that are no longer needed..."
# Get current packages from packages.toml
local current_packages=()
if [[ -f "$PACKAGES_FILE" ]]; then
while IFS= read -r line; do
# Skip empty lines and comments
[[ -z "$line" || "$line" == \#* ]] && continue
# Remove whitespace and use as package name
local package="${line// /}" # Remove whitespace
if [[ -n "$package" ]]; then
current_packages+=("$package")
fi
done <"$PACKAGES_FILE"
fi
# Get previously installed packages from state
local previous_packages=()
while IFS= read -r package; do
[[ -n "$package" ]] && previous_packages+=("$package")
done < <(get_installed_packages)
# Find packages to uninstall (in previous but not in current)
local packages_to_uninstall=()
for prev_pkg in "${previous_packages[@]}"; do
local found=false
for curr_pkg in "${current_packages[@]}"; do
if [[ "$prev_pkg" == "$curr_pkg" ]]; then
found=true
break
fi
done
if [[ "$found" == false ]]; then
packages_to_uninstall+=("$prev_pkg")
fi
done
# Uninstall packages that are no longer needed
if [[ ${#packages_to_uninstall[@]} -gt 0 ]]; then
log_info "Uninstalling ${#packages_to_uninstall[@]} packages that are no longer needed..."
for package in "${packages_to_uninstall[@]}"; do
log_info "Uninstalling package: $package"
if sudo dnf remove -y "$package"; then
log_success "Uninstalled $package"
else
log_error "Failed to uninstall $package"
fi
done
else
log_info "No packages to uninstall"
fi fi
} }
# Deploy dotfiles # Deploy dotfiles using symlinks
deploy_dotfiles() { deploy_dotfiles() {
if [[ ! -d "$DOTFILES_DIR" ]] || [[ -z "$(ls -A "$DOTFILES_DIR" 2>/dev/null)" ]]; then
log_info "No dotfiles to deploy"
return 0
fi
log_info "Deploying dotfiles..." log_info "Deploying dotfiles..."
# Find all dotfiles and deploy them local dotfiles_deployed=false
find "$DOTFILES_DIR" -type f | while read -r src_file; do
# Get relative path from DOTFILES_DIR
rel_path="${src_file#$DOTFILES_DIR/}"
dest_file="$HOME/$rel_path"
# Create destination directory if it doesn't exist while IFS='=' read -r entry; do
dest_dir=$(dirname "$dest_file") local section="${entry%%:*}"
mkdir -p "$dest_dir" local key_value="${entry#*:}"
local target="${key_value%%=*}"
local source="${key_value##*=}"
# Backup existing file if [[ "$section" == "dotfiles" ]]; then
backup_file "$dest_file" dotfiles_deployed=true
# Copy file to destination local target_path="$HOME/$target"
cp "$src_file" "$dest_file" local source_path="$FORGE_DIR/$source"
log_success "Deployed $rel_path to $dest_file"
# Add to managed files list # Create source directory if it doesn't exist
update_managed_files "$dest_file" local source_dir=$(dirname "$source_path")
done mkdir -p "$source_dir"
}
# Update managed files list in state # Create target directory if it doesn't exist
update_managed_files() { local target_dir=$(dirname "$target_path")
local file="$1" mkdir -p "$target_dir"
# Use jq to update the state file if available, otherwise use a simple approach # Backup existing file if it exists and is not a symlink
if command -v jq >/dev/null 2>&1; then if [[ -f "$target_path" && ! -L "$target_path" ]]; then
jq --arg file "$file" '.managed_files += [$file] | .managed_files = unique' "$STATE_FILE" >"$STATE_FILE.tmp" && local backup_dir="$FORGE_DIR/backup"
mv "$STATE_FILE.tmp" "$STATE_FILE" mkdir -p "$backup_dir"
local backup_path="$backup_dir/$(basename "$target").$(date +%Y%m%d_%H%M%S).bak"
cp "$target_path" "$backup_path"
log_info "Backed up $target_path to $backup_path"
fi
# Remove existing file/symlink
rm -f "$target_path"
# Create symlink
if ln -s "$source_path" "$target_path"; then
log_success "Created symlink: $target_path -> $source_path"
else else
# Simple approach without jq log_error "Failed to create symlink: $target_path -> $source_path"
log_warning "jq not found, state tracking limited" fi
fi
done < <(parse_toml)
if [[ "$dotfiles_deployed" == false ]]; then
log_info "No dotfiles to deploy"
fi fi
} }
# Update installed packages list in state # Switch command - apply configuration
update_installed_packages() {
local packages=("$@")
if command -v jq >/dev/null 2>&1; then
# Create JSON array of packages
local packages_json=$(printf '%s\n' "${packages[@]}" | jq -R . | jq -s .)
jq --argjson packages "$packages_json" '.installed_packages = $packages' "$STATE_FILE" >"$STATE_FILE.tmp" &&
mv "$STATE_FILE.tmp" "$STATE_FILE"
else
log_warning "jq not found, package tracking limited"
fi
}
# Get previously installed packages from state
get_installed_packages() {
if command -v jq >/dev/null 2>&1 && [[ -f "$STATE_FILE" ]]; then
jq -r '.installed_packages[]? // empty' "$STATE_FILE" 2>/dev/null
fi
}
# Switch command - main deployment function
switch() { switch() {
log_info "Starting forge switch..." log_info "Starting forge switch..."
# Initialize directories # Check if forge.toml exists
init_dirs if [[ ! -f "$FORGE_TOML" ]]; then
log_error "forge.toml not found. Run 'forge init' first."
# Clean up packages that are no longer needed exit 1
cleanup_packages fi
# Install packages # Install packages
install_packages install_packages
@ -295,142 +197,76 @@ switch() {
# Deploy dotfiles # Deploy dotfiles
deploy_dotfiles deploy_dotfiles
# Update state # Update last_switch timestamp
if command -v jq >/dev/null 2>&1; then if command -v sed >/dev/null 2>&1; then
jq --arg timestamp "$(date -Iseconds)" '.last_switch = $timestamp' "$STATE_FILE" >"$STATE_FILE.tmp" && sed -i "s/last_switch = .*/last_switch = \"$(date -Iseconds)\"/" "$FORGE_TOML"
mv "$STATE_FILE.tmp" "$STATE_FILE"
fi fi
log_success "Switch completed successfully!" log_success "Switch completed successfully!"
} }
# Build command (alias for switch) # Status command - show current configuration
build() { status() {
switch log_info "Forge Status"
}
# Add a dotfile to management
add_dotfile() {
local source_file="$1"
if [[ ! -f "$source_file" ]]; then
log_error "File $source_file does not exist"
return 1
fi
init_dirs
# Get relative path from home directory
if [[ "$source_file" == "$HOME"* ]]; then
rel_path="${source_file#$HOME/}"
else
rel_path="$(basename "$source_file")"
fi
dest_file="$DOTFILES_DIR/$rel_path"
dest_dir=$(dirname "$dest_file")
# Create destination directory
mkdir -p "$dest_dir"
# Copy file to dotfiles directory
cp "$source_file" "$dest_file"
log_success "Added $source_file to dotfiles management"
}
# Remove a dotfile from management
remove_dotfile() {
local rel_path="$1"
local dotfile="$DOTFILES_DIR/$rel_path"
local home_file="$HOME/$rel_path"
if [[ -f "$dotfile" ]]; then
rm "$dotfile"
log_success "Removed $rel_path from dotfiles management"
fi
# Ask if user wants to restore from backup
read -p "Do you want to restore $rel_path from backup? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
restore_backup "$home_file"
fi
}
# Generate random Linux-inspired name
generate_forge_name() {
local adjectives=("swift" "bold" "noble" "fierce" "wise" "ancient" "crystal" "iron" "steel" "shadow" "light" "dark" "golden" "silver" "bronze" "cosmic" "quantum" "neural" "atomic" "stellar")
local nouns=("eagle" "wolf" "lion" "tiger" "dragon" "phoenix" "hawk" "falcon" "bear" "panther" "lynx" "owl" "raven" "cobra" "viper" "python" "jaguar" "leopard" "cheetah" "cougar")
local suffix=("linux" "kernel" "system" "forge" "core" "engine" "matrix" "network" "protocol" "framework" "platform" "architecture" "interface" "module" "library" "runtime" "daemon" "service" "process" "thread")
local adj=${adjectives[$RANDOM % ${#adjectives[@]}]}
local noun=${nouns[$RANDOM % ${#nouns[@]}]}
local suff=${suffix[$RANDOM % ${#suffix[@]}]}
echo "${adj}-${noun}-${suff}"
}
# List managed files with status
list_managed() {
local forge_name=$(generate_forge_name)
log_info "Forge Status: $forge_name"
echo " Config directory: $FORGE_DIR" echo " Config directory: $FORGE_DIR"
echo " Dotfiles directory: $DOTFILES_DIR" echo " Config file: $FORGE_TOML"
echo " Packages file: $PACKAGES_FILE"
if command -v jq >/dev/null 2>&1 && [[ -f "$STATE_FILE" ]]; then if [[ -f "$FORGE_TOML" ]]; then
last_switch=$(jq -r '.last_switch' "$STATE_FILE") echo
if [[ "$last_switch" != "null" ]]; then log_info "Configuration:"
# Show last switch time
local last_switch=$(grep "last_switch" "$FORGE_TOML" | cut -d'=' -f2 | tr -d ' "')
if [[ -n "$last_switch" && "$last_switch" != "null" ]]; then
echo " Last switch: $last_switch" echo " Last switch: $last_switch"
else else
echo " Last switch: Never" echo " Last switch: Never"
fi fi
fi
echo echo
log_info "Managed dotfiles:" log_info "Packages:"
if [[ -d "$DOTFILES_DIR" ]]; then
local dotfile_count=$(find "$DOTFILES_DIR" -type f | wc -l)
if [[ $dotfile_count -gt 0 ]]; then
find "$DOTFILES_DIR" -type f | sed "s|$DOTFILES_DIR/| |" | sort
echo " Total: $dotfile_count dotfiles"
else
echo " No dotfiles"
fi
else
echo " No dotfiles directory"
fi
echo
log_info "Managed packages:"
if [[ -f "$PACKAGES_FILE" ]]; then
local package_count=0 local package_count=0
while IFS= read -r line; do while IFS='=' read -r entry; do
# Skip empty lines and comments local section="${entry%%:*}"
[[ -z "$line" || "$line" == \#* ]] && continue local key_value="${entry#*:}"
local package="${key_value%%=*}"
local enabled="${key_value##*=}"
# Remove whitespace and count as package if [[ "$section" == "packages" && "$enabled" == "true" ]]; then
local package="${line// /}" # Remove whitespace
if [[ -n "$package" ]]; then
echo " $package" echo " $package"
((package_count++)) ((package_count++))
fi fi
done <"$PACKAGES_FILE" done < <(parse_toml)
if [[ $package_count -eq 0 ]]; then if [[ $package_count -eq 0 ]]; then
echo " No packages" echo " No packages configured"
else else
echo " Total: $package_count packages" echo " Total: $package_count packages"
fi fi
else
echo " No packages file"
fi
}
# Show status echo
status() { log_info "Dotfiles:"
list_managed local dotfile_count=0
while IFS='=' read -r entry; do
local section="${entry%%:*}"
local key_value="${entry#*:}"
local target="${key_value%%=*}"
local source="${key_value##*=}"
if [[ "$section" == "dotfiles" ]]; then
echo " $target -> $source"
((dotfile_count++))
fi
done < <(parse_toml)
if [[ $dotfile_count -eq 0 ]]; then
echo " No dotfiles configured"
else
echo " Total: $dotfile_count dotfiles"
fi
else
echo " No configuration file found"
fi
} }
# Show help # Show help
@ -442,33 +278,27 @@ show_help() {
forge <COMMAND> forge <COMMAND>
COMMANDS: COMMANDS:
init Initialize configuration structure init Initialize configuration structure (creates forge.toml)
switch Apply configuration (install packages, deploy dotfiles, cleanup old packages) switch Apply configuration (install packages, deploy dotfiles)
build Alias for switch status Show current configuration status
add <file> Add a file to dotfiles management
remove <path> Remove a dotfile from management
list List all managed files and packages with status
status Show current status
help Show this help message help Show this help message
FEATURES: FEATURES:
- Automatic package cleanup: When you remove a package from packages.toml and run switch, - TOML-based configuration in forge.toml
the package will be automatically uninstalled from your system. - Package management via dnf
- Package state tracking: Previously installed packages are tracked to enable cleanup. - Dotfile management via symlinks
- Simple format: Packages are managed in simple format (no version tags required). - Automatic backup of existing files
- Random Linux-inspired names: Each list command generates a unique forge name. - User-level configuration like home-manager/nixos
FILES: FILES:
$FORGE_DIR/ Main configuration directory $FORGE_DIR/ Main configuration directory
$DOTFILES_DIR/ Dotfiles to deploy $FORGE_TOML Configuration file for packages and dotfiles
$PACKAGES_FILE Simple file with packages to install (one package per line)
$STATE_FILE State tracking file for managed packages and files
EXAMPLES: EXAMPLES:
forge init forge init
forge add ~/.bashrc # Edit $FORGE_TOML to add packages and dotfiles
forge switch forge switch
forge list forge status
EOF EOF
} }
@ -482,26 +312,6 @@ main() {
"switch") "switch")
switch switch
;; ;;
"build")
build
;;
"add")
if [[ -z "${2:-}" ]]; then
log_error "Please specify a file to add"
exit 1
fi
add_dotfile "$2"
;;
"remove")
if [[ -z "${2:-}" ]]; then
log_error "Please specify a path to remove"
exit 1
fi
remove_dotfile "$2"
;;
"list")
list_managed
;;
"status") "status")
status status
;; ;;
@ -522,4 +332,3 @@ main() {
# Run main function with all arguments # Run main function with all arguments
main "$@" main "$@"