Compare commits
No commits in common. "fd7ff14ac633b8c37933ffff997cd4cc82b71877" and "75e8f2db85050c211c896cad8f03ff40d76a6e71" have entirely different histories.
fd7ff14ac6
...
75e8f2db85
15 changed files with 638 additions and 1957 deletions
75
.gitignore
vendored
75
.gitignore
vendored
|
|
@ -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
357
README.md
|
|
@ -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
|
## 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)
|
- Managing COPR repositories (enable/disable automatically)
|
||||||
- Installing and managing system packages via `dnf` or `rpm-ostree`
|
- Installing and managing system packages via dnf
|
||||||
- Installing and managing userspace packages via `brew`, `flatpak`, or `nix`
|
|
||||||
- Managing dotfiles through symlinks to actual files
|
- Managing dotfiles through symlinks to actual files
|
||||||
- Centralized configuration via a single `inventory.toml` file
|
- Centralized configuration via a single `forge.toml` file
|
||||||
- Declarative user-level configuration similar to `home-manager`/`nixos`
|
- User-level configuration similar to home-manager/nixos
|
||||||
- Automatic backup of existing files before replacement
|
- Automatic backup of existing files before replacement
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install the CLI directly from GitHub with [uv](https://github.com/astral-sh/uv):
|
1. Clone or download this repository
|
||||||
|
2. Make the script executable:
|
||||||
```bash
|
```bash
|
||||||
uv tool install --from git+https://codeberg.org/randogoth/curator/ curator
|
chmod +x forge
|
||||||
```
|
```
|
||||||
|
3. Optionally, move it to a directory in your PATH:
|
||||||
|
```bash
|
||||||
|
sudo mv forge /usr/local/bin/forge
|
||||||
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Initialize the configuration structure
|
# Initialize the configuration structure
|
||||||
curator init
|
./forge init
|
||||||
|
|
||||||
# Edit the configuration file to add packages and dotfiles
|
# Edit the configuration file to add packages and dotfiles
|
||||||
nano ~/.config/curator/inventory.toml
|
nano ~/.config/forge/forge.toml
|
||||||
|
|
||||||
# Apply configuration
|
# Apply configuration
|
||||||
curator switch
|
./forge switch
|
||||||
|
|
||||||
# Check status
|
# Check status
|
||||||
curator status
|
./forge status
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### `init`
|
### `init`
|
||||||
Initialize the configuration structure and create `inventory.toml`.
|
Initialize the configuration structure and create `forge.toml`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curator init
|
./forge init
|
||||||
# or ./curator init
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates:
|
Creates:
|
||||||
- `~/.config/curator/` - Main configuration directory
|
- `~/.config/forge/` - Main configuration directory
|
||||||
- `~/.config/curator/inventory.toml` - Central configuration file
|
- `~/.config/forge/forge.toml` - Central configuration file
|
||||||
|
|
||||||
### `switch`
|
### `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
|
```bash
|
||||||
curator switch
|
./forge switch
|
||||||
# or ./curator switch
|
|
||||||
# rollback to the previous inventory.toml and apply it
|
|
||||||
curator switch --rollback
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This command:
|
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
|
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
|
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
|
5. Creates backups of existing files before replacing them
|
||||||
6. Creates backups of existing files before replacing them
|
6. Updates the last switch timestamp
|
||||||
7. Updates the last switch timestamp
|
|
||||||
|
|
||||||
### `status`
|
### `status`
|
||||||
Show current configuration status and information.
|
Show current configuration status and information.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curator status
|
./forge status
|
||||||
# or ./curator status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Displays:
|
Displays:
|
||||||
|
|
@ -87,99 +84,44 @@ Displays:
|
||||||
Show help message with all available commands.
|
Show help message with all available commands.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curator help
|
./forge help
|
||||||
# or ./curator 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
|
## Configuration
|
||||||
|
|
||||||
### inventory.toml
|
### forge.toml
|
||||||
The central configuration file located at `~/.config/curator/inventory.toml`:
|
The central configuration file located at `~/.config/forge/forge.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# curator Configuration File
|
# Forge Configuration File
|
||||||
# User-level configuration similar to home-manager/nixos
|
# User-level configuration similar to home-manager/nixos
|
||||||
|
|
||||||
[curator]
|
[forge]
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
last_switch = ""
|
last_switch = null
|
||||||
|
|
||||||
[copr]
|
[copr]
|
||||||
# copr.fedorainfracloud.org/username/repository
|
# COPR repositories to enable
|
||||||
# copr.fedorainfracloud.org/anotheruser/anotherrepo
|
# Just list COPR repository names - presence means enable, absence means don't enable
|
||||||
|
copr.fedorainfracloud.org/username/repository
|
||||||
|
copr.fedorainfracloud.org/anotheruser/anotherrepo
|
||||||
|
|
||||||
[dnf]
|
[packages]
|
||||||
# git
|
# List of packages to install using dnf
|
||||||
# vim
|
# Just list package names - presence means install, absence means don't install
|
||||||
# curl
|
git
|
||||||
# wget
|
vim
|
||||||
|
curl
|
||||||
[brew]
|
wget
|
||||||
# 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"
|
|
||||||
|
|
||||||
[dotfiles]
|
[dotfiles]
|
||||||
# Dotfiles to manage with symlinks
|
# Dotfiles to manage with symlinks
|
||||||
# Format: "target_path" = "source_path"
|
# Format: "target_path" = "source_path"
|
||||||
# target_path: where the symlink should be created (relative to home directory)
|
# target_path: where the symlink should be created (relative to home directory)
|
||||||
# source_path: where the actual file is stored (relative to dotfiles directory)
|
# source_path: where the actual file is stored (relative to forge directory)
|
||||||
".bashrc" = ".bashrc"
|
".bashrc" = "dotfiles/.bashrc"
|
||||||
".config/vimrc" = "vimrc"
|
".config/vimrc" = "dotfiles/vimrc"
|
||||||
".config/alacritty/alacritty.yml" = "alacritty.yml"
|
".config/alacritty/alacritty.yml" = "dotfiles/alacritty.yml"
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
# Additional options
|
# Additional options
|
||||||
|
|
@ -189,12 +131,12 @@ backup_dir = "backup"
|
||||||
|
|
||||||
### Configuration Sections
|
### Configuration Sections
|
||||||
|
|
||||||
#### `[curator]`
|
#### `[forge]`
|
||||||
- `version`: Configuration file version
|
- `version`: Configuration file version
|
||||||
- `last_switch`: Timestamp of last switch operation (auto-updated)
|
- `last_switch`: Timestamp of last switch operation (auto-updated)
|
||||||
|
|
||||||
#### `[copr]`
|
#### `[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:**
|
**Examples:**
|
||||||
```toml
|
```toml
|
||||||
|
|
@ -203,14 +145,14 @@ copr.fedorainfracloud.org/username/repository
|
||||||
copr.fedorainfracloud.org/anotheruser/anotherrepo
|
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]`
|
#### `[packages]`
|
||||||
List of packages to install via dnf. **One package per line**—presence means install, absence means don't install.
|
List of packages to install via dnf. **Just list package names** - presence means install, absence means don't install.
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
```toml
|
```toml
|
||||||
[dnf]
|
[packages]
|
||||||
git
|
git
|
||||||
vim
|
vim
|
||||||
curl
|
curl
|
||||||
|
|
@ -221,49 +163,21 @@ npm
|
||||||
|
|
||||||
To remove a package, simply delete the line containing the package name.
|
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]`
|
#### `[dotfiles]`
|
||||||
Dotfile mappings using symlinks:
|
Dotfile mappings using symlinks:
|
||||||
- **Key**: Target path where symlink should be created (relative to home directory)
|
- **Key**: Target path where symlink should be created (relative to home directory)
|
||||||
- **Value**: Source path where actual file is stored (relative to dotfiles directory)
|
- **Value**: Source path where actual file is stored (relative to forge directory)
|
||||||
- Note: Source path automatically prefixed with "dotfiles/" if not present
|
|
||||||
|
|
||||||
#### `[options]`
|
#### `[options]`
|
||||||
Additional configuration options:
|
Additional configuration options:
|
||||||
- `backup`: Enable/disable backup of existing files (default: true)
|
- `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
|
## File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.config/curator/
|
~/.config/forge/
|
||||||
├── inventory.toml # Main configuration file
|
├── forge.toml # Main configuration file
|
||||||
├── dotfiles/ # Your actual dotfiles (source files)
|
├── dotfiles/ # Your actual dotfiles (source files)
|
||||||
│ ├── .bashrc
|
│ ├── .bashrc
|
||||||
│ ├── vimrc
|
│ ├── vimrc
|
||||||
|
|
@ -273,6 +187,153 @@ Additional configuration options:
|
||||||
└── vimrc.20231121_143022.bak
|
└── 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
|
## 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
24
curator
|
|
@ -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
429
forge
Executable 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 "$@"
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# curator package
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1334
src/curator/cli.py
1334
src/curator/cli.py
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -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"]
|
|
||||||
14
tmpcfg.toml
14
tmpcfg.toml
|
|
@ -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
79
uv.lock
generated
|
|
@ -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" },
|
|
||||||
]
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue