Compare commits

..

No commits in common. "fd7ff14ac633b8c37933ffff997cd4cc82b71877" and "75e8f2db85050c211c896cad8f03ff40d76a6e71" have entirely different histories.

15 changed files with 638 additions and 1957 deletions

75
.gitignore vendored
View file

@ -1,75 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Jupyter
.ipynb_checkpoints
# pyenv
.python-version
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre
.pyre/
# UV
uv.lock
# Backup/config artifacts
*.bak
inventory.toml.last
inventory.toml.rollback

357
README.md
View file

@ -1,81 +1,78 @@
# curator - A Home-Manager Like Tool for Universal Blue builds
# Forge - A Home-Manager Like Tool for Fedora
A lightweight CLI that provides repository and dotfile management and package installation functionality through a centralized TOML configuration. Ported and expanded from [forge](https://github.com/ijadux2/forge) and inspired by [home-manager](https://github.com/nix-community/home-manager/).
A lightweight configuration management tool for Fedora that provides COPR repository management, dotfile management and package installation functionality through a centralized TOML configuration.
## Overview
curator is a Python CLI that helps you manage your Fedora system configuration by:
Forge is a bash script that helps you manage your Fedora system configuration by:
- Managing COPR repositories (enable/disable automatically)
- Installing and managing system packages via `dnf` or `rpm-ostree`
- Installing and managing userspace packages via `brew`, `flatpak`, or `nix`
- Installing and managing system packages via dnf
- Managing dotfiles through symlinks to actual files
- Centralized configuration via a single `inventory.toml` file
- Declarative user-level configuration similar to `home-manager`/`nixos`
- Centralized configuration via a single `forge.toml` file
- User-level configuration similar to home-manager/nixos
- Automatic backup of existing files before replacement
## Installation
Install the CLI directly from GitHub with [uv](https://github.com/astral-sh/uv):
```bash
uv tool install --from git+https://codeberg.org/randogoth/curator/ curator
```
1. Clone or download this repository
2. Make the script executable:
```bash
chmod +x forge
```
3. Optionally, move it to a directory in your PATH:
```bash
sudo mv forge /usr/local/bin/forge
```
## Quick Start
```bash
# Initialize the configuration structure
curator init
./forge init
# Edit the configuration file to add packages and dotfiles
nano ~/.config/curator/inventory.toml
nano ~/.config/forge/forge.toml
# Apply configuration
curator switch
./forge switch
# Check status
curator status
./forge status
```
## Commands
### `init`
Initialize the configuration structure and create `inventory.toml`.
Initialize the configuration structure and create `forge.toml`.
```bash
curator init
# or ./curator init
./forge init
```
Creates:
- `~/.config/curator/` - Main configuration directory
- `~/.config/curator/inventory.toml` - Central configuration file
- `~/.config/forge/` - Main configuration directory
- `~/.config/forge/forge.toml` - Central configuration file
### `switch`
Apply the current configuration (enable COPR, install packages, deploy dotfiles). Use `--rollback` to restore the previous `inventory.toml` snapshot before applying.
Apply the current configuration (enable COPR, install packages, deploy dotfiles).
```bash
curator switch
# or ./curator switch
# rollback to the previous inventory.toml and apply it
curator switch --rollback
./forge switch
```
This command:
1. Enables all COPR repositories listed in `inventory.toml`
1. Enables all COPR repositories listed in `forge.toml`
2. Disables COPR repositories that are no longer configured
3. Installs all packages listed in `inventory.toml`
3. Installs all packages listed in `forge.toml`
4. Creates symlinks for all configured dotfiles
5. Applies environment variables to `~/.config/environment.d/20-curator.conf` and imports them into the user session
6. Creates backups of existing files before replacing them
7. Updates the last switch timestamp
5. Creates backups of existing files before replacing them
6. Updates the last switch timestamp
### `status`
Show current configuration status and information.
```bash
curator status
# or ./curator status
./forge status
```
Displays:
@ -87,99 +84,44 @@ Displays:
Show help message with all available commands.
```bash
curator help
# or ./curator help
./forge help
```
### `from`
Import currently installed packages/repos for a manager into `inventory.toml` so curator can manage them.
```bash
curator from dnf
curator from brew
curator from flatpak
curator from rpm-ostree
curator from nix
curator from copr
```
### `add` / `remove`
Add or remove entries directly in `inventory.toml` using `section:value` pairs. Supports `dnf`, `brew`, `flatpak`, `rpm-ostree`, `nix`, and `copr`.
```bash
curator add dnf:uv nix:micro
curator remove dnf:curl flatpak:org.mozilla.firefox
```
### `reset`
Remove the rollback snapshot (`inventory.toml.rollback`).
```bash
curator reset
```
### `env`
Apply environment variables defined in `[variables]` and propagate them to systemd/DBus. For your current shell, run:
```bash
curator env
# then, to update this shell:
eval "$(curator env --eval-only)"
```
Use `curator env --eval-only` in shell init files if you want each shell to export the current `[variables]` values without touching `environment.d` or systemd/DBus.
## Configuration
### inventory.toml
The central configuration file located at `~/.config/curator/inventory.toml`:
### forge.toml
The central configuration file located at `~/.config/forge/forge.toml`:
```toml
# curator Configuration File
# Forge Configuration File
# User-level configuration similar to home-manager/nixos
[curator]
[forge]
version = "1.0"
last_switch = ""
last_switch = null
[copr]
# copr.fedorainfracloud.org/username/repository
# copr.fedorainfracloud.org/anotheruser/anotherrepo
# COPR repositories to enable
# Just list COPR repository names - presence means enable, absence means don't enable
copr.fedorainfracloud.org/username/repository
copr.fedorainfracloud.org/anotheruser/anotherrepo
[dnf]
# git
# vim
# curl
# wget
[brew]
# wget
# coreutils
[flatpak]
# org.mozilla.firefox
# com.spotify.Client
[rpm-ostree]
# podman
# htop
[nix]
# nixpkgs#git # or just "git" (curator will prefix nixpkgs#)
# nixpkgs#htop # or just "htop"
[variables]
# ENV_VAR = "value"
# ANOTHER = "another value"
[packages]
# List of packages to install using dnf
# Just list package names - presence means install, absence means don't install
git
vim
curl
wget
[dotfiles]
# 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 dotfiles directory)
".bashrc" = ".bashrc"
".config/vimrc" = "vimrc"
".config/alacritty/alacritty.yml" = "alacritty.yml"
# 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
@ -189,12 +131,12 @@ backup_dir = "backup"
### Configuration Sections
#### `[curator]`
#### `[forge]`
- `version`: Configuration file version
- `last_switch`: Timestamp of last switch operation (auto-updated)
#### `[copr]`
List of COPR repositories to enable via `dnf copr enable`. **One repository per line**—presence means enable, absence means don't enable.
List of COPR repositories to enable via `dnf copr enable`. **Just list COPR repository names** - presence means enable, absence means don't enable.
**Examples:**
```toml
@ -203,14 +145,14 @@ copr.fedorainfracloud.org/username/repository
copr.fedorainfracloud.org/anotheruser/anotherrepo
```
To remove a COPR repository, simply delete the line containing the repository name. curator will automatically disable it during the next switch.
To remove a COPR repository, simply delete the line containing the repository name. Forge will automatically disable it during the next switch.
#### `[dnf]`
List of packages to install via dnf. **One package per line**—presence means install, absence means don't install.
#### `[packages]`
List of packages to install via dnf. **Just list package names** - presence means install, absence means don't install.
**Examples:**
```toml
[dnf]
[packages]
git
vim
curl
@ -221,49 +163,21 @@ npm
To remove a package, simply delete the line containing the package name.
#### `[brew]`
List of packages to install via Homebrew. **One package per line**—presence means install.
#### `[flatpak]`
List of Flatpak refs to install. **One ref per line**—presence means install.
#### `[rpm-ostree]`
List of rpm-ostree layered packages. **One package per line**—presence means install.
#### `[nix]`
List of nix packages. **One package per line**—presence means install. You can specify plain names (e.g., `neovim`) or `nixpkgs#name`; curator will prefix `nixpkgs#` for installs and use the base name for removals.
#### `[variables]`
User environment variables to set via systemd `environment.d`. curator writes them to `~/.config/environment.d/20-curator.conf` during `curator switch`; new sessions will load them automatically.
**Examples:**
```toml
[variables]
EDITOR = "nvim"
PAGER = "less -R"
```
Notes:
- Values are imported into the user systemd and DBus environments during `curator switch` or `curator env`.
- To update a currently running shell, run `eval "$(curator env --eval-only)"` after applying.
- Later shell init files (e.g., `.bashrc`, `.zshrc`) can still override these values.
#### `[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 dotfiles directory)
- Note: Source path automatically prefixed with "dotfiles/" if not present
- **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 curator directory)
- `backup_dir`: Directory name for backups (relative to forge directory)
## File Structure
```
~/.config/curator/
├── inventory.toml # Main configuration file
~/.config/forge/
├── forge.toml # Main configuration file
├── dotfiles/ # Your actual dotfiles (source files)
│ ├── .bashrc
│ ├── vimrc
@ -273,6 +187,153 @@ Additional configuration options:
└── vimrc.20231121_143022.bak
```
## How It Works
### COPR Repository Management
COPR repositories are managed through the `[copr]` section in `forge.toml`:
- **Add COPR repo**: Simply add the repository name on a new line
- **Remove COPR repo**: Delete the line containing the repository name
- **Automatic cleanup**: Forge automatically disables COPR repos that are removed from configuration
- **No flags needed**: Just presence/absence of the repository name matters
### Package Management
Packages are managed through the `[packages]` section in `forge.toml`:
- **Add package**: Simply add the package name on a new line
- **Remove package**: Delete the line containing the package name
- **No flags needed**: Just presence/absence of the package name matters
### 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
### Backup System
Before creating symlinks, Forge:
1. Checks if the target file exists and is not a symlink
2. Creates a timestamped backup in the backup directory
3. Removes the original file
4. Creates the symlink to your managed dotfile
## Examples
### Basic Setup
```bash
# Initialize forge
./forge init
# Edit forge.toml to add COPR repos and packages
nano ~/.config/forge/forge.toml
# Add to [copr] section:
copr.fedorainfracloud.org/username/cool-repo
# Add to [packages] section:
git
vim
curl
# Create your dotfiles directory and add files
mkdir -p ~/.config/forge/dotfiles
echo "export EDITOR=vim" > ~/.config/forge/dotfiles/.bashrc
# Add to [dotfiles] section:
".bashrc" = "dotfiles/.bashrc"
# Apply configuration
./forge switch
```
### Managing Application Configurations
```bash
# Add alacritty configuration
mkdir -p ~/.config/forge/dotfiles/.config/alacritty
cp ~/.config/alacritty/alacritty.yml ~/.config/forge/dotfiles/.config/alacritty/
# Edit forge.toml
nano ~/.config/forge/forge.toml
# Add to [dotfiles] section:
".config/alacritty/alacritty.yml" = "dotfiles/.config/alacritty/alacritty.yml"
# Apply changes
./forge switch
```
### COPR Repository Management Examples
```toml
[copr]
# Development tools COPR
copr.fedorainfracloud.org/development/tools
copr.fedorainfracloud.org/user/neovim-nightly
# To remove a COPR repo, just delete the line
# Forge will automatically disable it during the next switch
```
### Package Management Examples
```toml
[packages]
# Development tools
git
vim
nodejs
npm
# System utilities
curl
wget
tree
htop
# To remove a package, just delete the line
# For example, to remove htop, delete the "htop" line
```
### Version Control Your Configuration
```bash
# Initialize git repository in forge directory
cd ~/.config/forge
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"
```
## Environment Variables
- `CURATOR_DIR`: Override the default configuration directory (default: `~/.config/curator`)
- `FORGE_DIR`: Override the default configuration directory (default: `~/.config/forge`)
## Dependencies
- `dnf` - Fedora package manager
- `dnf-plugins-core` - For COPR repository management
- `sed` - For updating configuration file (usually pre-installed)
## Migration from Previous Version
If you were using the old version of forge with `git = true` format:
1. Your existing configuration will not be automatically migrated
2. Run `./forge init` to create the new `forge.toml` structure
3. Convert your package configuration from:
```toml
# Old format
git = true
vim = true
```
to:
```toml
# New format
git
vim
```
4. Move your existing dotfiles from the old directory to `~/.config/forge/dotfiles/`
## License
This project is open source. Feel free to contribute or report issues.

24
curator
View file

@ -1,24 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from pathlib import Path
def main() -> None:
here = Path(__file__).resolve().parent
src_dir = here / "src"
if src_dir.exists():
sys.path.insert(0, str(src_dir))
try:
from curator.cli import main as curator_main
except ImportError as exc: # pragma: no cover - fallback error path
sys.stderr.write(f"Failed to import curator CLI: {exc}\n")
sys.exit(1)
curator_main()
if __name__ == "__main__":
main()

429
forge Executable file
View file

@ -0,0 +1,429 @@
#!/bin/bash
# forge - A home-manager like script for Fedora
# Provides dotfile management and package installation functionality
set -euo pipefail
# Configuration
FORGE_DIR="${FORGE_DIR:-$HOME/.config/forge}"
FORGE_TOML="$FORGE_DIR/forge.toml"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Initialize command - creates forge.toml and .config directory
init() {
log_info "Initializing forge..."
# Create directory
mkdir -p "$FORGE_DIR"
# Create initial forge.toml if it doesn't exist
if [[ ! -f "$FORGE_TOML" ]]; then
cat >"$FORGE_TOML" <<'EOF'
# Forge Configuration File
# User-level configuration similar to home-manager/nixos
[forge]
version = "1.0"
last_switch = null
[copr]
# COPR repositories to enable
# Just list COPR repository names - presence means enable, absence means don't enable
# Examples:
# copr.fedorainfracloud.org/username/repository
# copr.fedorainfracloud.org/anotheruser/anotherrepo
[packages]
# List of packages to install using dnf
# Just list package names - presence means install, absence means don't install
# Examples:
# git
# vim
# curl
[dotfiles]
# 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
log_success "Created forge.toml: $FORGE_TOML"
fi
log_success "Initialization complete!"
log_info "Edit $FORGE_TOML to configure packages and dotfiles"
log_info "Run 'forge switch' to apply your configuration"
}
# Parse TOML file (basic implementation)
parse_toml() {
local section=""
while IFS= read -r line; do
# Skip comments and empty lines
[[ -z "$line" || "$line" == \#* ]] && continue
# Check for section headers
if [[ "$line" =~ ^\[(.+)\]$ ]]; then
section="${BASH_REMATCH[1]}"
continue
fi
# Parse key-value pairs for non-packages sections
if [[ "$section" != "packages" && "$line" =~ ^([^=]+)=(.+)$ ]]; then
local key="${BASH_REMATCH[1]// /}"
local value="${BASH_REMATCH[2]// /}"
echo "$section:$key=$value"
# Parse package names (just keys without values in packages section)
elif [[ "$section" == "packages" && "$line" =~ ^([^=]+)$ ]]; then
local package="${BASH_REMATCH[1]// /}"
echo "$section:$package"
# Parse COPR repository names (just keys without values in copr section)
elif [[ "$section" == "copr" && "$line" =~ ^([^=]+)$ ]]; then
local copr_repo="${BASH_REMATCH[1]// /}"
echo "$section:$copr_repo"
fi
done <"$FORGE_TOML"
}
# Enable COPR repositories from forge.toml
enable_copr_repos() {
log_info "Enabling COPR repositories..."
local copr_enabled=false
while IFS='=' read -r entry; do
local section="${entry%%:*}"
local copr_repo="${entry#*:}"
if [[ "$section" == "copr" && -n "$copr_repo" ]]; then
copr_enabled=true
log_info "Enabling COPR repository: $copr_repo"
if sudo dnf copr enable -y "$copr_repo"; then
log_success "Enabled COPR: $copr_repo"
else
log_error "Failed to enable COPR: $copr_repo"
fi
fi
done < <(parse_toml)
if [[ "$copr_enabled" == false ]]; then
log_info "No COPR repositories to enable"
fi
}
# Disable COPR repositories that are no longer in forge.toml
disable_copr_repos() {
log_info "Checking for COPR repositories to disable..."
# Get currently enabled COPR repos
local enabled_repos=$(sudo dnf copr list --enabled 2>/dev/null | grep -E "copr\.fedorainfracloud\.org" | awk '{print $1}' || true)
# Get configured COPR repos from forge.toml
local configured_repos=""
while IFS='=' read -r entry; do
local section="${entry%%:*}"
local copr_repo="${entry#*:}"
if [[ "$section" == "copr" && -n "$copr_repo" ]]; then
configured_repos="$configured_repos $copr_repo"
fi
done < <(parse_toml)
# Disable repos that are enabled but not configured
for repo in $enabled_repos; do
if [[ ! " $configured_repos " =~ " $repo " ]]; then
log_info "Disabling COPR repository: $repo"
if sudo dnf copr disable -y "$repo"; then
log_success "Disabled COPR: $repo"
else
log_error "Failed to disable COPR: $repo"
fi
fi
done
}
# 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 package="${entry#*:}"
if [[ "$section" == "packages" && -n "$package" ]]; then
packages_installed=true
log_info "Installing package: $package"
if sudo dnf install -y "$package"; then
log_success "Installed $package"
else
log_error "Failed to install $package"
fi
fi
done < <(parse_toml)
if [[ "$packages_installed" == false ]]; then
log_info "No packages to install"
fi
}
# Deploy dotfiles using symlinks
deploy_dotfiles() {
log_info "Deploying dotfiles..."
local dotfiles_deployed=false
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
dotfiles_deployed=true
local target_path="$HOME/$target"
local source_path="$FORGE_DIR/$source"
# Create source directory if it doesn't exist
local source_dir=$(dirname "$source_path")
mkdir -p "$source_dir"
# Create target directory if it doesn't exist
local target_dir=$(dirname "$target_path")
mkdir -p "$target_dir"
# Backup existing file if it exists and is not a symlink
if [[ -f "$target_path" && ! -L "$target_path" ]]; then
local backup_dir="$FORGE_DIR/backup"
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
log_error "Failed to create symlink: $target_path -> $source_path"
fi
fi
done < <(parse_toml)
if [[ "$dotfiles_deployed" == false ]]; then
log_info "No dotfiles to deploy"
fi
}
# Switch command - apply configuration
switch() {
log_info "Starting forge switch..."
# Check if forge.toml exists
if [[ ! -f "$FORGE_TOML" ]]; then
log_error "forge.toml not found. Run 'forge init' first."
exit 1
fi
# Enable COPR repositories
enable_copr_repos
# Disable COPR repositories that are no longer configured
disable_copr_repos
# Install packages
install_packages
# Deploy dotfiles
deploy_dotfiles
# Update last_switch timestamp
if command -v sed >/dev/null 2>&1; then
sed -i "s/last_switch = .*/last_switch = \"$(date -Iseconds)\"/" "$FORGE_TOML"
fi
log_success "Switch completed successfully!"
}
# Status command - show current configuration
status() {
log_info "Forge Status"
echo " Config directory: $FORGE_DIR"
echo " Config file: $FORGE_TOML"
if [[ -f "$FORGE_TOML" ]]; then
echo
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"
else
echo " Last switch: Never"
fi
echo
log_info "COPR Repositories:"
local copr_count=0
while IFS='=' read -r entry; do
local section="${entry%%:*}"
local copr_repo="${entry#*:}"
if [[ "$section" == "copr" && -n "$copr_repo" ]]; then
echo " $copr_repo"
((copr_count++))
fi
done < <(parse_toml)
if [[ $copr_count -eq 0 ]]; then
echo " No COPR repositories configured"
else
echo " Total: $copr_count COPR repositories"
fi
echo
log_info "Packages:"
local package_count=0
while IFS='=' read -r entry; do
local section="${entry%%:*}"
local package="${entry#*:}"
if [[ "$section" == "packages" && -n "$package" ]]; then
echo " $package"
((package_count++))
fi
done < <(parse_toml)
if [[ $package_count -eq 0 ]]; then
echo " No packages configured"
else
echo " Total: $package_count packages"
fi
echo
log_info "Dotfiles:"
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() {
cat <<EOF
forge - A home-manager like script for Fedora
USAGE:
forge <COMMAND>
COMMANDS:
init Initialize configuration structure (creates forge.toml)
switch Apply configuration (enable COPR, install packages, deploy dotfiles)
status Show current configuration status
help Show this help message
FEATURES:
- TOML-based configuration in forge.toml
- COPR repository management (enable/disable automatically)
- Package management via dnf
- Dotfile management via symlinks
- Automatic backup of existing files
- User-level configuration like home-manager/nixos
FILES:
$FORGE_DIR/ Main configuration directory
$FORGE_TOML Configuration file for COPR, packages and dotfiles
EXAMPLES:
forge init
# Edit $FORGE_TOML to add COPR repos, packages and dotfiles
forge switch
forge status
EOF
}
# Main script logic
main() {
case "${1:-}" in
"init")
init
;;
"switch")
switch
;;
"status")
status
;;
"help" | "--help" | "-h")
show_help
;;
"")
log_error "No command specified. Use 'help' for usage information."
exit 1
;;
*)
log_error "Unknown command: $1"
show_help
exit 1
;;
esac
}
# Run main function with all arguments
main "$@"

View file

@ -1,20 +0,0 @@
[project]
name = "curator"
version = "0.1.1"
description = "A home-manager style Fedora configuration helper."
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
[project.scripts]
curator = "curator.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
package = true
[dependency-groups]
dev = ["pytest>=7.4"]

View file

@ -1 +0,0 @@
# curator package

File diff suppressed because it is too large Load diff

View file

@ -1,262 +0,0 @@
from pathlib import Path
import datetime as dt
from curator import cli
def test_parse_config_line_format(tmp_path: Path) -> None:
config_path = tmp_path / "inventory.toml"
config_path.write_text(
"""
[curator]
version = "1.0"
last_switch = ""
[copr]
copr.fedorainfracloud.org/user/repo
[dnf]
git
vim
[brew]
wget
[flatpak]
org.mozilla.firefox
[rpm-ostree]
podman
[nix]
nixpkgs#git
[dotfiles]
".bashrc" = ".bashrc"
[options]
backup = true
backup_dir = "backup"
""".strip()
)
config = cli.parse_config(config_path)
assert config.copr == ["copr.fedorainfracloud.org/user/repo"]
assert config.packages == ["git", "vim"]
assert config.brew_packages == ["wget"]
assert config.flatpak_refs == ["org.mozilla.firefox"]
assert config.rpm_ostree_packages == ["podman"]
assert config.nix_packages == ["nixpkgs#git"]
assert config.dotfiles == {".bashrc": ".bashrc"}
assert config.options["backup"] is True
def test_parse_config_legacy_format(tmp_path: Path) -> None:
config_path = tmp_path / "inventory.toml"
config_path.write_text(
"""
[dnf]
git
vim
[brew]
wget
[flatpak]
org.mozilla.firefox
[rpm-ostree]
podman
[nix]
nixpkgs#git
[variables]
EDITOR = "nvim"
[copr]
copr.fedorainfracloud.org/user/repo
[dotfiles]
".bashrc" = ".bashrc"
""".strip()
)
config = cli.parse_config(config_path)
assert config.packages == ["git", "vim"]
assert config.brew_packages == ["wget"]
assert config.flatpak_refs == ["org.mozilla.firefox"]
assert config.rpm_ostree_packages == ["podman"]
assert config.nix_packages == ["nixpkgs#git"]
assert config.copr == ["copr.fedorainfracloud.org/user/repo"]
assert config.dotfiles == {".bashrc": ".bashrc"}
assert config.variables == {"EDITOR": "nvim"}
def test_diff_items() -> None:
added, removed = cli.diff_items(["a", "b"], ["b", "c"])
assert added == ["a"]
assert removed == ["c"]
def test_update_last_switch_creates_backup(tmp_path: Path) -> None:
config_dir = tmp_path
config_path = config_dir / "inventory.toml"
config_path.write_text(
"""
[curator]
version = "1.0"
last_switch = ""
""".strip()
)
options = {"backup": True, "backup_dir": "backup"}
cli.update_last_switch(config_path, options)
lines = [line.strip() for line in config_path.read_text().splitlines() if line.strip()]
last_switch_line = [line for line in lines if line.startswith("last_switch")]
assert last_switch_line, "last_switch should be written"
ts_str = last_switch_line[0].split("=", 1)[1].strip().strip('"')
dt.datetime.fromisoformat(ts_str)
backup_dir = config_dir / "backup"
backups = list(backup_dir.iterdir())
assert backups, "expected a backup of inventory.toml to be created"
assert any(path.name.startswith("inventory.toml.") for path in backups)
def test_reset_previous_config(tmp_path: Path, monkeypatch) -> None:
rollback = tmp_path / "inventory.toml.rollback"
rollback.write_text("prev")
def fake_get_paths():
return tmp_path, tmp_path / "inventory.toml", tmp_path / "inventory.toml.last", rollback
monkeypatch.setattr(cli, "get_paths", fake_get_paths)
cli.reset_previous_config()
assert not rollback.exists()
def test_update_section_entries_rewrites_section(tmp_path: Path) -> None:
config_path = tmp_path / "inventory.toml"
config_path.write_text(
"""
[dnf]
git
[copr]
copr.fedorainfracloud.org/user/repo
[brew]
wget
""".strip()
)
cli.update_section_entries(config_path, "dnf", ["vim", "curl", "vim"])
cli.update_section_entries(config_path, "copr", ["copr.fedorainfracloud.org/user/repo", "copr.fedorainfracloud.org/another/repo"])
content = config_path.read_text().strip().splitlines()
dnf_index = content.index("[dnf]")
assert content[dnf_index + 1 : dnf_index + 3] == ["curl", "vim"]
assert "[copr]" in content
copr_index = content.index("[copr]")
assert content[copr_index + 1 : copr_index + 3] == [
"copr.fedorainfracloud.org/another/repo",
"copr.fedorainfracloud.org/user/repo",
]
def test_merge_section_entries_preserves_spacing(tmp_path: Path) -> None:
config_path = tmp_path / "inventory.toml"
config_path.write_text(
"""
[dnf]
git
[brew]
wget
""".strip()
)
cli.merge_section_entries(config_path, "dnf", {"vim"}, set())
lines = config_path.read_text().splitlines()
assert "" in lines, "expected to keep a blank line between sections"
dnf_index = lines.index("[dnf]")
brew_index = lines.index("[brew]")
assert lines[dnf_index + 1 : dnf_index + 3] == ["git", "vim"]
assert lines[brew_index - 1] == ""
def test_apply_environment_variables(tmp_path: Path, monkeypatch) -> None:
curator_dir = tmp_path / "curator"
curator_dir.mkdir()
env_home = tmp_path / "xdg"
monkeypatch.setenv("XDG_CONFIG_HOME", str(env_home))
config = cli.empty_config()
config.variables = {"EDITOR": "nvim", "WITH_SPACE": "some value"}
config.options["backup"] = False
written, removed = cli.apply_environment_variables(config, curator_dir)
env_file = env_home / "environment.d" / "20-curator.conf"
assert env_file.exists()
lines = env_file.read_text().splitlines()
assert "EDITOR=nvim" in lines
assert 'WITH_SPACE="some value"' in lines
assert written == {"EDITOR": "nvim", "WITH_SPACE": "some value"}
assert removed == set()
def test_apply_environment_variables_removes_file_when_empty(tmp_path: Path, monkeypatch) -> None:
curator_dir = tmp_path / "curator"
curator_dir.mkdir()
env_home = tmp_path / "xdg"
env_dir = env_home / "environment.d"
env_dir.mkdir(parents=True, exist_ok=True)
env_file = env_dir / "20-curator.conf"
env_file.write_text("OLD=1\n")
monkeypatch.setenv("XDG_CONFIG_HOME", str(env_home))
config = cli.empty_config()
config.options["backup"] = False
variables, removed = cli.apply_environment_variables(config, curator_dir)
assert not env_file.exists()
assert variables == {}
assert removed == {"OLD"}
def test_env_command_eval_outputs_exports(tmp_path: Path, monkeypatch, capsys) -> None:
curator_dir = tmp_path / "curator"
curator_dir.mkdir()
env_file = curator_dir / "inventory.toml"
env_file.write_text(
"""
[variables]
EDITOR = "nvim"
""".strip()
)
env_home = tmp_path / "xdg"
env_dir = env_home / "environment.d"
env_dir.mkdir(parents=True, exist_ok=True)
env_conf = env_dir / "20-curator.conf"
env_conf.write_text("OLD=1\n")
monkeypatch.setenv("XDG_CONFIG_HOME", str(env_home))
def fake_get_paths():
return curator_dir, env_file, curator_dir / "last", curator_dir / "rollback"
def fail_apply_environment_variables(*_args, **_kwargs):
raise AssertionError("apply_environment_variables should not be called in eval-only mode")
monkeypatch.setattr(cli, "get_paths", fake_get_paths)
monkeypatch.setattr(cli, "apply_environment_variables", fail_apply_environment_variables)
cli.env_command(eval_only=True)
out = capsys.readouterr().out.strip().splitlines()
assert out == ["export EDITOR=nvim", "unset OLD"]

View file

@ -1,14 +0,0 @@
[curator]
version = "1.0"
last_switch = ""
copr = ["copr.fedorainfracloud.org/user/repo"]
packages = ["git", "vim"]
[dotfiles]
".bashrc" = ".bashrc"
[options]
backup = true
backup_dir = "backup"

79
uv.lock generated
View file

@ -1,79 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "curator"
version = "0.1.0"
source = { editable = "." }
[package.dev-dependencies]
dev = [
{ name = "pytest" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=7.4" }]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]