added package removal, brew, flatpak, ostree, nix xupport
This commit is contained in:
parent
da6d24c74f
commit
df1f387795
16 changed files with 596 additions and 362 deletions
317
README.md
317
README.md
|
|
@ -1,14 +1,14 @@
|
||||||
# Forge - A Home-Manager Like Tool for Fedora
|
# curator - A Home-Manager Like Tool for Fedora
|
||||||
|
|
||||||
A lightweight Python CLI (managed with `uv`) for Fedora that provides COPR repository management, dotfile management and package installation functionality through a centralized TOML configuration.
|
A lightweight Python CLI (managed with `uv`) for Fedora that provides COPR repository management, dotfile management and package installation functionality through a centralized TOML configuration.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Forge is a Python CLI that helps you manage your Fedora system configuration by:
|
curator is a Python CLI 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
|
- Installing and managing system packages via dnf
|
||||||
- Managing dotfiles through symlinks to actual files
|
- Managing dotfiles through symlinks to actual files
|
||||||
- Centralized configuration via a single `forge.toml` file
|
- Centralized configuration via a single `inventory.toml` file
|
||||||
- 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
|
||||||
|
|
||||||
|
|
@ -21,11 +21,11 @@ Forge is a Python CLI that helps you manage your Fedora system configuration by:
|
||||||
```
|
```
|
||||||
3. Run with `uv`:
|
3. Run with `uv`:
|
||||||
```bash
|
```bash
|
||||||
uv run forge --help
|
uv run curator --help
|
||||||
```
|
```
|
||||||
4. Or use the local shim directly:
|
4. Or use the local shim directly:
|
||||||
```bash
|
```bash
|
||||||
./forge --help
|
./curator --help
|
||||||
```
|
```
|
||||||
5. Optionally install globally via `uv`:
|
5. Optionally install globally via `uv`:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -36,46 +36,48 @@ Forge is a Python CLI that helps you manage your Fedora system configuration by:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Initialize the configuration structure
|
# Initialize the configuration structure
|
||||||
uv run forge init
|
uv run curator init
|
||||||
|
|
||||||
# Edit the configuration file to add packages and dotfiles
|
# Edit the configuration file to add packages and dotfiles
|
||||||
nano ~/.config/forge/forge.toml
|
nano ~/.config/curator/inventory.toml
|
||||||
|
|
||||||
# Apply configuration
|
# Apply configuration
|
||||||
uv run forge switch
|
uv run curator switch
|
||||||
|
|
||||||
# Check status
|
# Check status
|
||||||
uv run forge status
|
uv run curator status
|
||||||
```
|
```
|
||||||
|
|
||||||
You can swap `uv run forge ...` for `./forge ...` if you prefer the local shim.
|
You can swap `uv run curator ...` for `./curator ...` if you prefer the local shim.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### `init`
|
### `init`
|
||||||
Initialize the configuration structure and create `forge.toml`.
|
Initialize the configuration structure and create `inventory.toml`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run forge init
|
uv run curator init
|
||||||
# or ./forge init
|
# or ./curator init
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates:
|
Creates:
|
||||||
- `~/.config/forge/` - Main configuration directory
|
- `~/.config/curator/` - Main configuration directory
|
||||||
- `~/.config/forge/forge.toml` - Central configuration file
|
- `~/.config/curator/inventory.toml` - Central configuration file
|
||||||
|
|
||||||
### `switch`
|
### `switch`
|
||||||
Apply the current configuration (enable COPR, install packages, deploy dotfiles).
|
Apply the current configuration (enable COPR, install packages, deploy dotfiles). Use `--rollback` to restore the previous `inventory.toml` snapshot before applying.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run forge switch
|
uv run curator switch
|
||||||
# or ./forge switch
|
# or ./curator switch
|
||||||
|
# rollback to the previous inventory.toml and apply it
|
||||||
|
uv run curator switch --rollback
|
||||||
```
|
```
|
||||||
|
|
||||||
This command:
|
This command:
|
||||||
1. Enables all COPR repositories listed in `forge.toml`
|
1. Enables all COPR repositories listed in `inventory.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 `forge.toml`
|
3. Installs all packages listed in `inventory.toml`
|
||||||
4. Creates symlinks for all configured dotfiles
|
4. Creates symlinks for all configured dotfiles
|
||||||
5. Creates backups of existing files before replacing them
|
5. Creates backups of existing files before replacing them
|
||||||
6. Updates the last switch timestamp
|
6. Updates the last switch timestamp
|
||||||
|
|
@ -84,8 +86,8 @@ This command:
|
||||||
Show current configuration status and information.
|
Show current configuration status and information.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run forge status
|
uv run curator status
|
||||||
# or ./forge status
|
# or ./curator status
|
||||||
```
|
```
|
||||||
|
|
||||||
Displays:
|
Displays:
|
||||||
|
|
@ -97,35 +99,49 @@ Displays:
|
||||||
Show help message with all available commands.
|
Show help message with all available commands.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run forge help
|
uv run curator help
|
||||||
# or ./forge help
|
# or ./curator help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### forge.toml
|
### inventory.toml
|
||||||
The central configuration file located at `~/.config/forge/forge.toml`:
|
The central configuration file located at `~/.config/curator/inventory.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Forge Configuration File
|
# curator Configuration File
|
||||||
# User-level configuration similar to home-manager/nixos
|
# User-level configuration similar to home-manager/nixos
|
||||||
|
|
||||||
copr = [
|
[curator]
|
||||||
# "copr.fedorainfracloud.org/username/repository",
|
|
||||||
# "copr.fedorainfracloud.org/anotheruser/anotherrepo",
|
|
||||||
]
|
|
||||||
|
|
||||||
packages = [
|
|
||||||
# "git",
|
|
||||||
# "vim",
|
|
||||||
# "curl",
|
|
||||||
# "wget",
|
|
||||||
]
|
|
||||||
|
|
||||||
[forge]
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
last_switch = ""
|
last_switch = ""
|
||||||
|
|
||||||
|
[copr]
|
||||||
|
# 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"
|
||||||
|
|
||||||
[dotfiles]
|
[dotfiles]
|
||||||
# Dotfiles to manage with symlinks
|
# Dotfiles to manage with symlinks
|
||||||
# Format: "target_path" = "source_path"
|
# Format: "target_path" = "source_path"
|
||||||
|
|
@ -143,40 +159,44 @@ backup_dir = "backup"
|
||||||
|
|
||||||
### Configuration Sections
|
### Configuration Sections
|
||||||
|
|
||||||
#### `[forge]`
|
#### `[curator]`
|
||||||
- `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]`
|
||||||
Array of COPR repositories to enable via `dnf copr enable`. **Just list COPR repository names** - presence means enable, absence means don't enable.
|
List of COPR repositories to enable via `dnf copr enable`. **One repository per line**—presence means enable, absence means don't enable.
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
```toml
|
```toml
|
||||||
copr = [
|
[copr]
|
||||||
"copr.fedorainfracloud.org/username/repository",
|
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. Forge will automatically disable it during the next switch.
|
To remove a COPR repository, simply delete the line containing the repository name. curator will automatically disable it during the next switch.
|
||||||
|
|
||||||
#### `packages`
|
#### `[dnf]`
|
||||||
Array of packages to install via dnf. **Just list package names** - presence means install, absence means don't install.
|
List of packages to install via dnf. **One package per line**—presence means install, absence means don't install.
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
```toml
|
```toml
|
||||||
packages = [
|
[dnf]
|
||||||
"git",
|
git
|
||||||
"vim",
|
vim
|
||||||
"curl",
|
curl
|
||||||
"wget",
|
wget
|
||||||
"nodejs",
|
nodejs
|
||||||
"npm",
|
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.
|
||||||
|
|
||||||
#### `[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)
|
||||||
|
|
@ -186,13 +206,13 @@ Dotfile mappings using symlinks:
|
||||||
#### `[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 forge directory)
|
- `backup_dir`: Directory name for backups (relative to curator directory)
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.config/forge/
|
~/.config/curator/
|
||||||
├── forge.toml # Main configuration file
|
├── inventory.toml # Main configuration file
|
||||||
├── dotfiles/ # Your actual dotfiles (source files)
|
├── dotfiles/ # Your actual dotfiles (source files)
|
||||||
│ ├── .bashrc
|
│ ├── .bashrc
|
||||||
│ ├── vimrc
|
│ ├── vimrc
|
||||||
|
|
@ -205,162 +225,205 @@ Additional configuration options:
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
### COPR Repository Management
|
### COPR Repository Management
|
||||||
COPR repositories are managed through the `copr` array in `forge.toml`:
|
COPR repositories are managed through the `[copr]` section in `inventory.toml`:
|
||||||
- **Add COPR repo**: Add the repository name to the array
|
- **Add COPR repo**: Add the repository name on its own line
|
||||||
- **Remove COPR repo**: Remove the entry from the array
|
- **Remove COPR repo**: Remove the entry
|
||||||
- **Automatic cleanup**: Forge automatically disables COPR repos that are removed from configuration
|
- **Automatic cleanup**: curator automatically disables COPR repos that are removed from configuration
|
||||||
- **No flags needed**: Just presence/absence of the repository name matters
|
- **No flags needed**: Just presence/absence of the repository name matters
|
||||||
- Forge stores the previous configuration at `~/.config/forge/forge.toml.prev` and compares it to the current file during `switch`; newly added repos are enabled, removed repos are disabled.
|
- curator stores the previous configuration at `~/.config/curator/inventory.toml.prev` and compares it to the current file during `switch`; newly added repos are enabled, removed repos are disabled.
|
||||||
|
|
||||||
### Package Management
|
### Package Management
|
||||||
Packages are managed through the `packages` array in `forge.toml`:
|
Packages are managed through the `[dnf]` section in `inventory.toml`:
|
||||||
- **Add package**: Add the package name to the array
|
- **Add package**: Add the package name on its own line
|
||||||
- **Remove package**: Remove the entry from the array
|
- **Remove package**: Remove the entry
|
||||||
- **No flags needed**: Just presence/absence of the package name matters
|
- **No flags needed**: Just presence/absence of the package name matters
|
||||||
- The previous configuration snapshot (`forge.toml.prev`) is used to detect additions/removals on each `switch`; added packages are installed and removed packages are uninstalled.
|
- The previous configuration snapshot (`inventory.toml.prev`) is used to detect additions/removals on each `switch`; added packages are installed and removed packages are uninstalled.
|
||||||
|
|
||||||
|
### Brew Management
|
||||||
|
Homebrew packages are managed through the `[brew]` section:
|
||||||
|
- **Add package**: Add the package name on its own line
|
||||||
|
- **Remove package**: Remove the entry
|
||||||
|
- Additions/removals are detected against `inventory.toml.prev` on `switch`; removed packages are uninstalled.
|
||||||
|
|
||||||
|
### Flatpak Management
|
||||||
|
Flatpaks are managed through the `[flatpak]` section:
|
||||||
|
- **Add ref**: Add the ref on its own line
|
||||||
|
- **Remove ref**: Remove the entry
|
||||||
|
- Additions/removals are detected against `inventory.toml.prev` on `switch`; removed refs are uninstalled.
|
||||||
|
|
||||||
|
### rpm-ostree Management
|
||||||
|
rpm-ostree packages are managed through the `[rpm-ostree]` section:
|
||||||
|
- **Add package**: Add the package name on its own line
|
||||||
|
- **Remove package**: Remove the entry
|
||||||
|
- Additions/removals are detected against `inventory.toml.prev` on `switch`; removed packages are uninstalled.
|
||||||
|
|
||||||
|
### Nix Management
|
||||||
|
Nix packages are managed through the `[nix]` section (if `nix` is available on the system):
|
||||||
|
- **Add package**: Add the package name (e.g., `nixpkgs#git` or just `git`) on its own line
|
||||||
|
- **Remove package**: Remove the entry
|
||||||
|
- Additions/removals are detected against `inventory.toml.prev` on `switch`; installs/removals use `nix profile` and will prefix `nixpkgs#` if missing.
|
||||||
|
|
||||||
### Dotfile Management
|
### Dotfile Management
|
||||||
Forge uses symlinks to manage dotfiles:
|
curator uses symlinks to manage dotfiles:
|
||||||
1. Your actual dotfiles are stored in `~/.config/forge/dotfiles/`
|
1. Your actual dotfiles are stored in `~/.config/curator/dotfiles/`
|
||||||
2. Symlinks are created from your home directory to these files using relative paths
|
2. Symlinks are created from your home directory to these files using relative paths
|
||||||
3. This allows you to version control your dotfiles in one place
|
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
|
4. Changes to the source files are immediately reflected in your home directory
|
||||||
5. Source paths are automatically prefixed with "dotfiles/" for convenience
|
5. Source paths are automatically prefixed with "dotfiles/" for convenience
|
||||||
|
|
||||||
### Backup System
|
### Backup System
|
||||||
Before creating symlinks, Forge:
|
Before creating symlinks, curator:
|
||||||
1. Checks if the target file exists and is not a symlink
|
1. Checks if the target file exists and is not a symlink
|
||||||
2. Creates a timestamped backup in the backup directory
|
2. Creates a timestamped backup in the backup directory
|
||||||
3. Removes the original file
|
3. Removes the original file
|
||||||
4. Creates the symlink to your managed dotfile
|
4. Creates the symlink to your managed dotfile
|
||||||
|
|
||||||
`forge.toml` is also backed up before the last switch timestamp is updated.
|
`inventory.toml` is also backed up before the last switch timestamp is updated.
|
||||||
|
|
||||||
The previously applied configuration is stored separately as `~/.config/forge/forge.toml.prev` to compute diffs for COPR and package changes.
|
The previously applied configuration is stored separately as `~/.config/curator/inventory.toml.prev` to compute diffs for COPR and package changes.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Basic Setup
|
### Basic Setup
|
||||||
```bash
|
```bash
|
||||||
# Initialize forge
|
# Initialize curator
|
||||||
uv run forge init
|
uv run curator init
|
||||||
|
|
||||||
# Edit forge.toml to add COPR repos and packages
|
# Edit inventory.toml to add COPR repos and packages
|
||||||
nano ~/.config/forge/forge.toml
|
nano ~/.config/curator/inventory.toml
|
||||||
|
|
||||||
# Add to the arrays:
|
# Add to the sections:
|
||||||
# copr = [
|
# [copr]
|
||||||
# "copr.fedorainfracloud.org/username/cool-repo",
|
# copr.fedorainfracloud.org/username/cool-repo
|
||||||
# ]
|
# [dnf]
|
||||||
# packages = [
|
# git
|
||||||
# "git",
|
# vim
|
||||||
# "vim",
|
# curl
|
||||||
# "curl",
|
# [brew]
|
||||||
# ]
|
# wget
|
||||||
|
# [flatpak]
|
||||||
|
# org.mozilla.firefox
|
||||||
|
# [rpm-ostree]
|
||||||
|
# podman
|
||||||
|
# [nix]
|
||||||
|
# nixpkgs#git (or just "git")
|
||||||
|
|
||||||
# Create your dotfiles directory and add files
|
# Create your dotfiles directory and add files
|
||||||
mkdir -p ~/.config/forge/dotfiles
|
mkdir -p ~/.config/curator/dotfiles
|
||||||
echo "export EDITOR=vim" > ~/.config/forge/dotfiles/.bashrc
|
echo "export EDITOR=vim" > ~/.config/curator/dotfiles/.bashrc
|
||||||
|
|
||||||
# Add to [dotfiles] section:
|
# Add to [dotfiles] section:
|
||||||
".bashrc" = ".bashrc"
|
".bashrc" = ".bashrc"
|
||||||
|
|
||||||
# Apply configuration
|
# Apply configuration
|
||||||
uv run forge switch
|
uv run curator switch
|
||||||
```
|
```
|
||||||
|
|
||||||
### Managing Application Configurations
|
### Managing Application Configurations
|
||||||
```bash
|
```bash
|
||||||
# Add alacritty configuration
|
# Add alacritty configuration
|
||||||
mkdir -p ~/.config/forge/dotfiles
|
mkdir -p ~/.config/curator/dotfiles
|
||||||
cp ~/.config/alacritty/alacritty.yml ~/.config/forge/dotfiles/
|
cp ~/.config/alacritty/alacritty.yml ~/.config/curator/dotfiles/
|
||||||
|
|
||||||
# Edit forge.toml
|
# Edit inventory.toml
|
||||||
nano ~/.config/forge/forge.toml
|
nano ~/.config/curator/inventory.toml
|
||||||
|
|
||||||
# Add to [dotfiles] section:
|
# Add to [dotfiles] section:
|
||||||
".config/alacritty/alacritty.yml" = "alacritty.yml"
|
".config/alacritty/alacritty.yml" = "alacritty.yml"
|
||||||
|
|
||||||
# Apply changes
|
# Apply changes
|
||||||
uv run forge switch
|
uv run curator switch
|
||||||
```
|
```
|
||||||
|
|
||||||
### COPR Repository Management Examples
|
### COPR Repository Management Examples
|
||||||
```toml
|
```toml
|
||||||
copr = [
|
[copr]
|
||||||
# Development tools COPR
|
# Development tools COPR
|
||||||
"copr.fedorainfracloud.org/development/tools",
|
copr.fedorainfracloud.org/development/tools
|
||||||
"copr.fedorainfracloud.org/user/neovim-nightly",
|
copr.fedorainfracloud.org/user/neovim-nightly
|
||||||
]
|
|
||||||
# To remove a COPR repo, just delete the entry
|
# To remove a COPR repo, just delete the entry
|
||||||
# Forge will automatically disable it during the next switch
|
# curator will automatically disable it during the next switch
|
||||||
```
|
```
|
||||||
|
|
||||||
### Package Management Examples
|
### Package Management Examples
|
||||||
```toml
|
```toml
|
||||||
packages = [
|
[dnf]
|
||||||
# Development tools
|
# Development tools
|
||||||
"git",
|
git
|
||||||
"vim",
|
vim
|
||||||
"nodejs",
|
nodejs
|
||||||
"npm",
|
npm
|
||||||
|
|
||||||
# System utilities
|
# System utilities
|
||||||
"curl",
|
curl
|
||||||
"wget",
|
wget
|
||||||
"tree",
|
tree
|
||||||
"htop",
|
htop
|
||||||
]
|
|
||||||
# To remove a package, just delete the entry
|
# To remove a package, just delete the entry
|
||||||
|
|
||||||
|
[brew]
|
||||||
|
# Utilities and tools
|
||||||
|
wget
|
||||||
|
coreutils
|
||||||
|
|
||||||
|
[flatpak]
|
||||||
|
org.mozilla.firefox
|
||||||
|
com.spotify.Client
|
||||||
|
|
||||||
|
[rpm-ostree]
|
||||||
|
podman
|
||||||
|
htop
|
||||||
```
|
```
|
||||||
|
|
||||||
### Version Control Your Configuration
|
### Version Control Your Configuration
|
||||||
```bash
|
```bash
|
||||||
# Initialize git repository in forge directory
|
# Initialize git repository in curator directory
|
||||||
cd ~/.config/forge
|
cd ~/.config/curator
|
||||||
git init
|
git init
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Initial configuration"
|
git commit -m "Initial configuration"
|
||||||
|
|
||||||
# Now you can version control your entire system configuration
|
# Now you can version control your entire system configuration
|
||||||
git add forge.toml dotfiles/
|
git add inventory.toml dotfiles/
|
||||||
git commit -m "Updated vim configuration"
|
git commit -m "Updated vim configuration"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
- `FORGE_DIR`: Override the default configuration directory (default: `~/.config/forge`)
|
- `CURATOR_DIR`: Override the default configuration directory (default: `~/.config/curator`)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- Python 3.11+
|
- Python 3.11+
|
||||||
- `tomlkit` (installed automatically via `uv sync`)
|
|
||||||
- `uv` for environment and script management
|
- `uv` for environment and script management
|
||||||
- `dnf` - Fedora package manager
|
- `dnf` - Fedora package manager
|
||||||
- `dnf-plugins-core` - For COPR repository management
|
- `dnf-plugins-core` - For COPR repository management
|
||||||
|
|
||||||
## Migration from Previous Version
|
## Migration from Previous Version
|
||||||
|
|
||||||
If you were using the old version of forge with `git = true` or bare keys under `[packages]`/`[copr]`:
|
If you were using the old version of curator with `git = true` or bare keys under `[packages]`/`[copr]`:
|
||||||
|
|
||||||
1. Your existing configuration will not be automatically migrated, but the CLI will still read the legacy format.
|
1. Your existing configuration will not be automatically migrated, but the CLI will still read the legacy format.
|
||||||
2. Run `uv run forge init` (or `./forge init`) to create the new `forge.toml` structure.
|
2. Run `uv run curator init` (or `./curator init`) to create the new `inventory.toml` structure.
|
||||||
3. Convert to arrays:
|
3. Convert to section-per-line format:
|
||||||
```toml
|
```toml
|
||||||
# Old formats
|
# Old formats
|
||||||
[packages]
|
[dnf]
|
||||||
git = true
|
git = true
|
||||||
vim = true
|
vim = true
|
||||||
# or
|
# or
|
||||||
[packages]
|
[dnf]
|
||||||
git
|
git
|
||||||
vim
|
vim
|
||||||
|
|
||||||
# New format
|
# New format
|
||||||
packages = ["git", "vim"]
|
[dnf]
|
||||||
copr = ["copr.fedorainfracloud.org/username/repository"]
|
git
|
||||||
|
vim
|
||||||
|
[copr]
|
||||||
|
copr.fedorainfracloud.org/username/repository
|
||||||
```
|
```
|
||||||
4. Move your existing dotfiles from the old directory to `~/.config/forge/dotfiles/`
|
4. Move your existing dotfiles from the old directory to `~/.config/curator/dotfiles/`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ def main() -> None:
|
||||||
sys.path.insert(0, str(src_dir))
|
sys.path.insert(0, str(src_dir))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from forge.cli import main as forge_main
|
from curator.cli import main as curator_main
|
||||||
except ImportError as exc: # pragma: no cover - fallback error path
|
except ImportError as exc: # pragma: no cover - fallback error path
|
||||||
sys.stderr.write(f"Failed to import forge CLI: {exc}\n")
|
sys.stderr.write(f"Failed to import curator CLI: {exc}\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
forge_main()
|
curator_main()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
[project]
|
[project]
|
||||||
name = "forge"
|
name = "curator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A home-manager style Fedora configuration helper."
|
description = "A home-manager style Fedora configuration helper."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["tomlkit>=0.12"]
|
dependencies = []
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
forge = "forge.cli:main"
|
curator = "curator.cli:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
|
|
|
||||||
1
src/curator/__init__.py
Normal file
1
src/curator/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# curator package
|
||||||
BIN
src/curator/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/curator/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/curator/__pycache__/cli.cpython-313.pyc
Normal file
BIN
src/curator/__pycache__/cli.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -12,39 +12,49 @@ from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import tomlkit
|
|
||||||
from tomlkit.exceptions import TOMLKitError
|
|
||||||
from tomlkit.items import Array, Table
|
|
||||||
|
|
||||||
BLUE = "\033[0;34m"
|
BLUE = "\033[0;34m"
|
||||||
GREEN = "\033[0;32m"
|
GREEN = "\033[0;32m"
|
||||||
YELLOW = "\033[1;33m"
|
YELLOW = "\033[1;33m"
|
||||||
RED = "\033[0;31m"
|
RED = "\033[0;31m"
|
||||||
RESET = "\033[0m"
|
RESET = "\033[0m"
|
||||||
|
|
||||||
DEFAULT_CONFIG = """# Forge Configuration File
|
DEFAULT_CONFIG = """# curator Configuration File
|
||||||
# User-level configuration similar to home-manager/nixos
|
# User-level configuration similar to home-manager/nixos
|
||||||
|
|
||||||
copr = [
|
[curator]
|
||||||
# "copr.fedorainfracloud.org/username/repository",
|
|
||||||
# "copr.fedorainfracloud.org/anotheruser/anotherrepo",
|
|
||||||
]
|
|
||||||
|
|
||||||
packages = [
|
|
||||||
# "git",
|
|
||||||
# "vim",
|
|
||||||
# "curl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[forge]
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
last_switch = ""
|
last_switch = ""
|
||||||
|
|
||||||
|
[copr]
|
||||||
|
# copr.fedorainfracloud.org/username/repository
|
||||||
|
# copr.fedorainfracloud.org/anotheruser/anotherrepo
|
||||||
|
|
||||||
|
[dnf]
|
||||||
|
# git
|
||||||
|
# vim
|
||||||
|
# curl
|
||||||
|
|
||||||
|
[brew]
|
||||||
|
# wget
|
||||||
|
# coreutils
|
||||||
|
|
||||||
|
[flatpak]
|
||||||
|
# org.mozilla.firefox
|
||||||
|
# com.spotify.Client
|
||||||
|
|
||||||
|
[rpm-ostree]
|
||||||
|
# podman
|
||||||
|
# htop
|
||||||
|
|
||||||
|
[nix]
|
||||||
|
# nixpkgs#git
|
||||||
|
# nixpkgs#htop
|
||||||
|
|
||||||
[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 forge directory)
|
# source_path: where the actual file is stored (relative to curator directory)
|
||||||
# Note: source_path automatically prefixed with "dotfiles/" if not present
|
# Note: source_path automatically prefixed with "dotfiles/" if not present
|
||||||
# Examples:
|
# Examples:
|
||||||
# ".bashrc" = ".bashrc"
|
# ".bashrc" = ".bashrc"
|
||||||
|
|
@ -59,14 +69,17 @@ backup_dir = "backup"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ForgeConfig:
|
class CuratorConfig:
|
||||||
copr: list[str]
|
copr: list[str]
|
||||||
packages: list[str]
|
packages: list[str]
|
||||||
|
brew_packages: list[str]
|
||||||
|
flatpak_refs: list[str]
|
||||||
|
rpm_ostree_packages: list[str]
|
||||||
|
nix_packages: list[str]
|
||||||
dotfiles: dict[str, str]
|
dotfiles: dict[str, str]
|
||||||
options: dict[str, Any]
|
options: dict[str, Any]
|
||||||
forge_fields: dict[str, Any]
|
curator_fields: dict[str, Any]
|
||||||
document: Optional[tomlkit.TOMLDocument]
|
document: Optional[Any] = None
|
||||||
legacy: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def log(prefix: str, color: str, message: str) -> None:
|
def log(prefix: str, color: str, message: str) -> None:
|
||||||
|
|
@ -104,12 +117,16 @@ def parse_value(value: str) -> Any:
|
||||||
return strip_quotes(value)
|
return strip_quotes(value)
|
||||||
|
|
||||||
|
|
||||||
def parse_legacy_config(config_path: Path) -> ForgeConfig:
|
def parse_config(config_path: Path) -> CuratorConfig:
|
||||||
copr: list[str] = []
|
copr: list[str] = []
|
||||||
packages: list[str] = []
|
dnf_packages: list[str] = []
|
||||||
|
brew_packages: list[str] = []
|
||||||
|
flatpak_refs: list[str] = []
|
||||||
|
rpm_ostree_packages: list[str] = []
|
||||||
|
nix_packages: list[str] = []
|
||||||
dotfiles: dict[str, str] = {}
|
dotfiles: dict[str, str] = {}
|
||||||
options: dict[str, Any] = {}
|
options: dict[str, Any] = {}
|
||||||
forge_fields: dict[str, Any] = {}
|
curator_fields: dict[str, Any] = {}
|
||||||
section = None
|
section = None
|
||||||
|
|
||||||
with config_path.open() as handle:
|
with config_path.open() as handle:
|
||||||
|
|
@ -126,11 +143,21 @@ def parse_legacy_config(config_path: Path) -> ForgeConfig:
|
||||||
if section is None:
|
if section is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if section in {"packages", "copr"}:
|
if section in {"dnf", "copr", "brew", "flatpak", "rpm-ostree", "nix"}:
|
||||||
entry = line.split("=", 1)[0].strip()
|
entry = line.split("=", 1)[0].strip()
|
||||||
if entry:
|
if entry:
|
||||||
target = packages if section == "packages" else copr
|
if section == "copr":
|
||||||
target.append(strip_quotes(entry))
|
copr.append(strip_quotes(entry))
|
||||||
|
elif section == "brew":
|
||||||
|
brew_packages.append(strip_quotes(entry))
|
||||||
|
elif section == "flatpak":
|
||||||
|
flatpak_refs.append(strip_quotes(entry))
|
||||||
|
elif section == "rpm-ostree":
|
||||||
|
rpm_ostree_packages.append(strip_quotes(entry))
|
||||||
|
elif section == "nix":
|
||||||
|
nix_packages.append(strip_quotes(entry))
|
||||||
|
else:
|
||||||
|
dnf_packages.append(strip_quotes(entry))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if section == "dotfiles":
|
if section == "dotfiles":
|
||||||
|
|
@ -154,8 +181,8 @@ def parse_legacy_config(config_path: Path) -> ForgeConfig:
|
||||||
|
|
||||||
if section == "options":
|
if section == "options":
|
||||||
options[key] = value
|
options[key] = value
|
||||||
elif section == "forge":
|
elif section == "curator":
|
||||||
forge_fields[key] = value
|
curator_fields[key] = value
|
||||||
|
|
||||||
merged_options: dict[str, Any] = {"backup": True, "backup_dir": "backup"}
|
merged_options: dict[str, Any] = {"backup": True, "backup_dir": "backup"}
|
||||||
merged_options.update(options)
|
merged_options.update(options)
|
||||||
|
|
@ -163,123 +190,37 @@ def parse_legacy_config(config_path: Path) -> ForgeConfig:
|
||||||
if not merged_options.get("backup_dir"):
|
if not merged_options.get("backup_dir"):
|
||||||
merged_options["backup_dir"] = "backup"
|
merged_options["backup_dir"] = "backup"
|
||||||
|
|
||||||
return ForgeConfig(
|
return CuratorConfig(
|
||||||
copr=copr,
|
copr=copr,
|
||||||
packages=packages,
|
packages=dnf_packages,
|
||||||
|
brew_packages=brew_packages,
|
||||||
|
flatpak_refs=flatpak_refs,
|
||||||
|
rpm_ostree_packages=rpm_ostree_packages,
|
||||||
|
nix_packages=nix_packages,
|
||||||
dotfiles=dotfiles,
|
dotfiles=dotfiles,
|
||||||
options=merged_options,
|
options=merged_options,
|
||||||
forge_fields=forge_fields,
|
curator_fields=curator_fields,
|
||||||
document=None,
|
document=None,
|
||||||
legacy=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def empty_config() -> ForgeConfig:
|
def empty_config() -> CuratorConfig:
|
||||||
return ForgeConfig(
|
return CuratorConfig(
|
||||||
copr=[],
|
copr=[],
|
||||||
packages=[],
|
packages=[],
|
||||||
|
brew_packages=[],
|
||||||
|
flatpak_refs=[],
|
||||||
|
rpm_ostree_packages=[],
|
||||||
|
nix_packages=[],
|
||||||
dotfiles={},
|
dotfiles={},
|
||||||
options={"backup": True, "backup_dir": "backup"},
|
options={"backup": True, "backup_dir": "backup"},
|
||||||
forge_fields={},
|
curator_fields={},
|
||||||
document=None,
|
document=None,
|
||||||
legacy=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_document(text: str) -> tomlkit.TOMLDocument | None:
|
|
||||||
try:
|
|
||||||
return tomlkit.parse(text)
|
|
||||||
except TOMLKitError:
|
|
||||||
sanitized = re.sub(r"=\s*null", '= ""', text)
|
|
||||||
if sanitized != text:
|
|
||||||
try:
|
|
||||||
return tomlkit.parse(sanitized)
|
|
||||||
except TOMLKitError:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
def load_config(config_path: Path) -> CuratorConfig:
|
||||||
def _coerce_list(raw: Any) -> list[str]:
|
|
||||||
if raw is None:
|
|
||||||
return []
|
|
||||||
if isinstance(raw, Array):
|
|
||||||
raw = list(raw)
|
|
||||||
if isinstance(raw, list):
|
|
||||||
return [str(item) for item in raw if str(item).strip()]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_table_list(raw: Any, list_key: str) -> list[str]:
|
|
||||||
if isinstance(raw, list):
|
|
||||||
return _coerce_list(raw)
|
|
||||||
if isinstance(raw, (dict, Table)):
|
|
||||||
items: list[str] = []
|
|
||||||
if list_key in raw:
|
|
||||||
items.extend(_coerce_list(raw[list_key]))
|
|
||||||
for key, value in raw.items():
|
|
||||||
if key == list_key:
|
|
||||||
continue
|
|
||||||
if isinstance(value, bool) and value:
|
|
||||||
items.append(str(key))
|
|
||||||
return items
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def parse_config(config_path: Path) -> ForgeConfig:
|
|
||||||
try:
|
|
||||||
raw_text = config_path.read_text()
|
|
||||||
document = parse_document(raw_text)
|
|
||||||
except FileNotFoundError:
|
|
||||||
document = None
|
|
||||||
|
|
||||||
if document is None:
|
|
||||||
log_warning("Configuration is not valid TOML; using legacy parser.")
|
|
||||||
return parse_legacy_config(config_path)
|
|
||||||
|
|
||||||
copr_raw = document.get("copr", [])
|
|
||||||
packages_raw = document.get("packages", [])
|
|
||||||
dotfiles_raw = document.get("dotfiles", {})
|
|
||||||
options_raw = document.get("options", {})
|
|
||||||
forge_raw = document.get("forge", {})
|
|
||||||
|
|
||||||
if not copr_raw and isinstance(forge_raw, (dict, Table)) and "copr" in forge_raw:
|
|
||||||
copr_raw = forge_raw.get("copr", copr_raw)
|
|
||||||
if not packages_raw and isinstance(forge_raw, (dict, Table)) and "packages" in forge_raw:
|
|
||||||
packages_raw = forge_raw.get("packages", packages_raw)
|
|
||||||
|
|
||||||
copr = _parse_table_list(copr_raw, "repos")
|
|
||||||
packages = _parse_table_list(packages_raw, "names")
|
|
||||||
|
|
||||||
dotfiles: dict[str, str] = {}
|
|
||||||
if isinstance(dotfiles_raw, (dict, Table)):
|
|
||||||
for target, source in dotfiles_raw.items():
|
|
||||||
if source is None:
|
|
||||||
continue
|
|
||||||
dotfiles[str(target)] = str(source)
|
|
||||||
|
|
||||||
options: dict[str, Any] = {}
|
|
||||||
if isinstance(options_raw, (dict, Table)):
|
|
||||||
options.update({str(key): value for key, value in options_raw.items()})
|
|
||||||
|
|
||||||
merged_options: dict[str, Any] = {"backup": True, "backup_dir": "backup"}
|
|
||||||
merged_options.update(options)
|
|
||||||
|
|
||||||
forge_fields: dict[str, Any] = {}
|
|
||||||
if isinstance(forge_raw, (dict, Table)):
|
|
||||||
forge_fields.update({str(key): value for key, value in forge_raw.items()})
|
|
||||||
|
|
||||||
return ForgeConfig(
|
|
||||||
copr=copr,
|
|
||||||
packages=packages,
|
|
||||||
dotfiles=dotfiles,
|
|
||||||
options=merged_options,
|
|
||||||
forge_fields=forge_fields,
|
|
||||||
document=document,
|
|
||||||
legacy=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path: Path) -> ForgeConfig:
|
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
return empty_config()
|
return empty_config()
|
||||||
try:
|
try:
|
||||||
|
|
@ -297,26 +238,26 @@ def diff_items(current: list[str], previous: list[str]) -> tuple[list[str], list
|
||||||
|
|
||||||
|
|
||||||
def get_paths() -> tuple[Path, Path, Path]:
|
def get_paths() -> tuple[Path, Path, Path]:
|
||||||
forge_dir = Path(os.environ.get("FORGE_DIR", Path.home() / ".config" / "forge")).expanduser()
|
curator_dir = Path(os.environ.get("CURATOR_DIR", Path.home() / ".config" / "curator")).expanduser()
|
||||||
forge_toml = forge_dir / "forge.toml"
|
curator_toml = curator_dir / "inventory.toml"
|
||||||
previous_toml = forge_dir / "forge.toml.prev"
|
previous_toml = curator_dir / "inventory.toml.prev"
|
||||||
return forge_dir, forge_toml, previous_toml
|
return curator_dir, curator_toml, previous_toml
|
||||||
|
|
||||||
|
|
||||||
def init_command() -> None:
|
def init_command() -> None:
|
||||||
forge_dir, forge_toml, _ = get_paths()
|
curator_dir, curator_toml, _ = get_paths()
|
||||||
log_info("Initializing forge...")
|
log_info("Initializing curator...")
|
||||||
forge_dir.mkdir(parents=True, exist_ok=True)
|
curator_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if forge_toml.exists():
|
if curator_toml.exists():
|
||||||
log_warning(f"Configuration already exists at {forge_toml}")
|
log_warning(f"Configuration already exists at {curator_toml}")
|
||||||
else:
|
else:
|
||||||
forge_toml.write_text(DEFAULT_CONFIG)
|
curator_toml.write_text(DEFAULT_CONFIG)
|
||||||
log_success(f"Created forge.toml: {forge_toml}")
|
log_success(f"Created inventory.toml: {curator_toml}")
|
||||||
|
|
||||||
log_success("Initialization complete!")
|
log_success("Initialization complete!")
|
||||||
log_info(f"Edit {forge_toml} to configure packages and dotfiles")
|
log_info(f"Edit {curator_toml} to configure packages and dotfiles")
|
||||||
log_info("Run 'forge switch' to apply your configuration")
|
log_info("Run 'curator switch' to apply your configuration")
|
||||||
|
|
||||||
|
|
||||||
def run_command(command: list[str]) -> bool:
|
def run_command(command: list[str]) -> bool:
|
||||||
|
|
@ -399,6 +340,74 @@ def install_packages(packages: list[str]) -> None:
|
||||||
log_error(f"Failed to install {package}")
|
log_error(f"Failed to install {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_brew_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Installing brew packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No brew packages to install")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
log_info(f"Installing brew package: {package}")
|
||||||
|
if run_command(["brew", "install", package]):
|
||||||
|
log_success(f"Installed {package}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to install {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_flatpaks(refs: list[str]) -> None:
|
||||||
|
log_info("Installing flatpak refs...")
|
||||||
|
if not refs:
|
||||||
|
log_info("No flatpaks to install")
|
||||||
|
return
|
||||||
|
|
||||||
|
for ref in refs:
|
||||||
|
log_info(f"Installing flatpak: {ref}")
|
||||||
|
if run_command(["flatpak", "install", "-y", ref]):
|
||||||
|
log_success(f"Installed {ref}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to install {ref}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_rpm_ostree_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Installing rpm-ostree packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No rpm-ostree packages to install")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
log_info(f"Installing rpm-ostree package: {package}")
|
||||||
|
if run_command(["rpm-ostree", "install", "-y", package]):
|
||||||
|
log_success(f"Installed {package}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to install {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def nix_available() -> bool:
|
||||||
|
return shutil.which("nix") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_nix_ref(ref: str) -> str:
|
||||||
|
return ref if "#" in ref else f"nixpkgs#{ref}"
|
||||||
|
|
||||||
|
|
||||||
|
def install_nix_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Installing nix packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No nix packages to install")
|
||||||
|
return
|
||||||
|
if not nix_available():
|
||||||
|
log_warning("nix not found on PATH; skipping nix package installs")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
ref = normalize_nix_ref(package)
|
||||||
|
log_info(f"Installing nix package: {ref}")
|
||||||
|
if run_command(["nix", "profile", "install", ref]):
|
||||||
|
log_success(f"Installed {ref}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to install {ref}")
|
||||||
|
|
||||||
|
|
||||||
def remove_packages(packages: list[str]) -> None:
|
def remove_packages(packages: list[str]) -> None:
|
||||||
log_info("Removing packages...")
|
log_info("Removing packages...")
|
||||||
if not packages:
|
if not packages:
|
||||||
|
|
@ -413,6 +422,66 @@ def remove_packages(packages: list[str]) -> None:
|
||||||
log_error(f"Failed to remove {package}")
|
log_error(f"Failed to remove {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_brew_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Removing brew packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No brew packages to remove")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
log_info(f"Removing brew package: {package}")
|
||||||
|
if run_command(["brew", "uninstall", package]):
|
||||||
|
log_success(f"Removed {package}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to remove {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_flatpaks(refs: list[str]) -> None:
|
||||||
|
log_info("Removing flatpaks...")
|
||||||
|
if not refs:
|
||||||
|
log_info("No flatpaks to remove")
|
||||||
|
return
|
||||||
|
|
||||||
|
for ref in refs:
|
||||||
|
log_info(f"Removing flatpak: {ref}")
|
||||||
|
if run_command(["flatpak", "uninstall", "-y", ref]):
|
||||||
|
log_success(f"Removed {ref}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to remove {ref}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_rpm_ostree_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Removing rpm-ostree packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No rpm-ostree packages to remove")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
log_info(f"Removing rpm-ostree package: {package}")
|
||||||
|
if run_command(["rpm-ostree", "uninstall", "-y", package]):
|
||||||
|
log_success(f"Removed {package}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to remove {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_nix_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Removing nix packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No nix packages to remove")
|
||||||
|
return
|
||||||
|
if not nix_available():
|
||||||
|
log_warning("nix not found on PATH; skipping nix package removals")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
ref = normalize_nix_ref(package)
|
||||||
|
log_info(f"Removing nix package: {ref}")
|
||||||
|
if run_command(["nix", "profile", "remove", ref]):
|
||||||
|
log_success(f"Removed {ref}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to remove {ref}")
|
||||||
|
|
||||||
|
|
||||||
def backup_target(target_path: Path, backup_dir: Path) -> None:
|
def backup_target(target_path: Path, backup_dir: Path) -> None:
|
||||||
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
@ -433,7 +502,7 @@ def remove_existing(target_path: Path) -> None:
|
||||||
shutil.rmtree(target_path)
|
shutil.rmtree(target_path)
|
||||||
|
|
||||||
|
|
||||||
def deploy_dotfiles(config: ForgeConfig, forge_dir: Path) -> None:
|
def deploy_dotfiles(config: CuratorConfig, curator_dir: Path) -> None:
|
||||||
log_info("Deploying dotfiles...")
|
log_info("Deploying dotfiles...")
|
||||||
if not config.dotfiles:
|
if not config.dotfiles:
|
||||||
log_info("No dotfiles to deploy")
|
log_info("No dotfiles to deploy")
|
||||||
|
|
@ -441,10 +510,10 @@ def deploy_dotfiles(config: ForgeConfig, forge_dir: Path) -> None:
|
||||||
|
|
||||||
backup_enabled = bool(config.options.get("backup", True))
|
backup_enabled = bool(config.options.get("backup", True))
|
||||||
backup_dir_name = str(config.options.get("backup_dir", "backup")) or "backup"
|
backup_dir_name = str(config.options.get("backup_dir", "backup")) or "backup"
|
||||||
backup_dir = forge_dir / backup_dir_name
|
backup_dir = curator_dir / backup_dir_name
|
||||||
|
|
||||||
for target, source in config.dotfiles.items():
|
for target, source in config.dotfiles.items():
|
||||||
source_path = forge_dir / (source if source.startswith("dotfiles/") else f"dotfiles/{source}")
|
source_path = curator_dir / (source if source.startswith("dotfiles/") else f"dotfiles/{source}")
|
||||||
target_path = Path.home() / target
|
target_path = Path.home() / target
|
||||||
|
|
||||||
if not source_path.exists():
|
if not source_path.exists():
|
||||||
|
|
@ -484,33 +553,29 @@ def update_last_switch(config_path: Path, options: dict[str, Any]) -> None:
|
||||||
try:
|
try:
|
||||||
backup_target(config_path, backup_dir)
|
backup_target(config_path, backup_dir)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
log_warning(f"Failed to back up forge.toml: {exc}")
|
log_warning(f"Failed to back up inventory.toml: {exc}")
|
||||||
|
|
||||||
timestamp = dt.datetime.now().astimezone()
|
timestamp = dt.datetime.now().astimezone().isoformat(timespec="seconds")
|
||||||
raw_text = config_path.read_text()
|
raw_text = config_path.read_text()
|
||||||
document = parse_document(raw_text)
|
lines = raw_text.splitlines()
|
||||||
if document is None:
|
|
||||||
# Fall back to legacy in-place update without rewriting structure.
|
|
||||||
ts_str = timestamp.isoformat(timespec="seconds")
|
|
||||||
lines = raw_text.splitlines()
|
|
||||||
updated = False
|
|
||||||
for idx, line in enumerate(lines):
|
|
||||||
if re.match(r"\s*last_switch\s*=", line):
|
|
||||||
lines[idx] = f'last_switch = "{ts_str}"'
|
|
||||||
updated = True
|
|
||||||
break
|
|
||||||
if not updated:
|
|
||||||
lines.append(f'last_switch = "{ts_str}"')
|
|
||||||
config_path.write_text("\n".join(lines) + "\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
forge_table = document.get("forge")
|
updated = False
|
||||||
if not isinstance(forge_table, Table):
|
insert_idx: Optional[int] = None
|
||||||
forge_table = tomlkit.table()
|
for idx, line in enumerate(lines):
|
||||||
document["forge"] = forge_table
|
if re.match(r"\s*\[curator\]\s*$", line):
|
||||||
|
insert_idx = idx + 1
|
||||||
|
if re.match(r"\s*last_switch\s*=", line):
|
||||||
|
lines[idx] = f'last_switch = "{timestamp}"'
|
||||||
|
updated = True
|
||||||
|
break
|
||||||
|
|
||||||
forge_table["last_switch"] = timestamp
|
if not updated:
|
||||||
config_path.write_text(tomlkit.dumps(document))
|
if insert_idx is None:
|
||||||
|
lines.append("[curator]")
|
||||||
|
insert_idx = len(lines)
|
||||||
|
lines.insert(insert_idx, f'last_switch = "{timestamp}"')
|
||||||
|
|
||||||
|
config_path.write_text("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|
||||||
def save_previous_config(current_path: Path, previous_path: Path) -> None:
|
def save_previous_config(current_path: Path, previous_path: Path) -> None:
|
||||||
|
|
@ -521,42 +586,84 @@ def save_previous_config(current_path: Path, previous_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def switch_command() -> None:
|
def switch_command() -> None:
|
||||||
forge_dir, forge_toml, previous_toml = get_paths()
|
_switch(rollback=False)
|
||||||
if not forge_toml.exists():
|
|
||||||
log_error("forge.toml not found. Run 'forge init' first.")
|
|
||||||
|
def switch_command_with_args(rollback: bool = False) -> None:
|
||||||
|
_switch(rollback=rollback)
|
||||||
|
|
||||||
|
|
||||||
|
def _switch(rollback: bool) -> None:
|
||||||
|
curator_dir, curator_toml, previous_toml = get_paths()
|
||||||
|
if not curator_toml.exists() and not rollback:
|
||||||
|
log_error("inventory.toml not found. Run 'curator init' first.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
current_config = parse_config(forge_toml)
|
if rollback:
|
||||||
previous_config = load_config(previous_toml)
|
if not previous_toml.exists():
|
||||||
|
log_error("No previous inventory.toml to roll back to.")
|
||||||
|
sys.exit(1)
|
||||||
|
# Backup current config if present
|
||||||
|
current_before = load_config(curator_toml) if curator_toml.exists() else empty_config()
|
||||||
|
current_options = current_before.options or {"backup": True, "backup_dir": "backup"}
|
||||||
|
backup_dir_name = str(current_options.get("backup_dir", "backup") or "backup")
|
||||||
|
backup_dir = curator_toml.parent / backup_dir_name
|
||||||
|
if curator_toml.exists():
|
||||||
|
try:
|
||||||
|
backup_target(curator_toml, backup_dir)
|
||||||
|
except OSError as exc:
|
||||||
|
log_warning(f"Failed to back up current inventory.toml before rollback: {exc}")
|
||||||
|
|
||||||
|
shutil.copy2(previous_toml, curator_toml)
|
||||||
|
log_info(f"Restored inventory.toml from {previous_toml}")
|
||||||
|
current_config = parse_config(curator_toml)
|
||||||
|
previous_config = current_before
|
||||||
|
else:
|
||||||
|
current_config = parse_config(curator_toml)
|
||||||
|
previous_config = load_config(previous_toml)
|
||||||
|
|
||||||
copr_added, copr_removed = diff_items(current_config.copr, previous_config.copr)
|
copr_added, copr_removed = diff_items(current_config.copr, previous_config.copr)
|
||||||
packages_added, packages_removed = diff_items(current_config.packages, previous_config.packages)
|
packages_added, packages_removed = diff_items(current_config.packages, previous_config.packages)
|
||||||
|
brew_added, brew_removed = diff_items(current_config.brew_packages, previous_config.brew_packages)
|
||||||
|
flatpak_added, flatpak_removed = diff_items(current_config.flatpak_refs, previous_config.flatpak_refs)
|
||||||
|
rpm_ostree_added, rpm_ostree_removed = diff_items(
|
||||||
|
current_config.rpm_ostree_packages, previous_config.rpm_ostree_packages
|
||||||
|
)
|
||||||
|
nix_added, nix_removed = diff_items(current_config.nix_packages, previous_config.nix_packages)
|
||||||
|
|
||||||
enable_copr_repos(copr_added if copr_added else current_config.copr)
|
enable_copr_repos(copr_added if copr_added else current_config.copr)
|
||||||
disable_copr_repos(set(current_config.copr), set(copr_removed))
|
disable_copr_repos(set(current_config.copr), set(copr_removed))
|
||||||
install_packages(packages_added if packages_added else current_config.packages)
|
install_packages(packages_added if packages_added else current_config.packages)
|
||||||
|
install_brew_packages(brew_added if brew_added else current_config.brew_packages)
|
||||||
|
install_flatpaks(flatpak_added if flatpak_added else current_config.flatpak_refs)
|
||||||
|
install_rpm_ostree_packages(rpm_ostree_added if rpm_ostree_added else current_config.rpm_ostree_packages)
|
||||||
|
install_nix_packages(nix_added if nix_added else current_config.nix_packages)
|
||||||
remove_packages(packages_removed)
|
remove_packages(packages_removed)
|
||||||
deploy_dotfiles(current_config, forge_dir)
|
remove_brew_packages(brew_removed)
|
||||||
update_last_switch(forge_toml, current_config.options)
|
remove_flatpaks(flatpak_removed)
|
||||||
save_previous_config(forge_toml, previous_toml)
|
remove_rpm_ostree_packages(rpm_ostree_removed)
|
||||||
|
remove_nix_packages(nix_removed)
|
||||||
|
deploy_dotfiles(current_config, curator_dir)
|
||||||
|
update_last_switch(curator_toml, current_config.options)
|
||||||
|
save_previous_config(curator_toml, previous_toml)
|
||||||
log_success("Switch completed successfully!")
|
log_success("Switch completed successfully!")
|
||||||
|
|
||||||
|
|
||||||
def status_command() -> None:
|
def status_command() -> None:
|
||||||
forge_dir, forge_toml, _ = get_paths()
|
curator_dir, curator_toml, _ = get_paths()
|
||||||
log_info("Forge Status")
|
log_info("curator Status")
|
||||||
print(f" Config directory: {forge_dir}")
|
print(f" Config directory: {curator_dir}")
|
||||||
print(f" Config file: {forge_toml}")
|
print(f" Config file: {curator_toml}")
|
||||||
|
|
||||||
if not forge_toml.exists():
|
if not curator_toml.exists():
|
||||||
print(" No configuration file found")
|
print(" No configuration file found")
|
||||||
return
|
return
|
||||||
|
|
||||||
config = parse_config(forge_toml)
|
config = parse_config(curator_toml)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
log_info("Configuration:")
|
log_info("Configuration:")
|
||||||
last_switch = config.forge_fields.get("last_switch")
|
last_switch = config.curator_fields.get("last_switch")
|
||||||
if last_switch and str(last_switch).lower() != "null":
|
if last_switch and str(last_switch).lower() != "null":
|
||||||
print(f" Last switch: {last_switch}")
|
print(f" Last switch: {last_switch}")
|
||||||
else:
|
else:
|
||||||
|
|
@ -572,13 +679,49 @@ def status_command() -> None:
|
||||||
print(" No COPR repositories configured")
|
print(" No COPR repositories configured")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
log_info("Packages:")
|
log_info("DNF Packages:")
|
||||||
if config.packages:
|
if config.packages:
|
||||||
for package in config.packages:
|
for package in config.packages:
|
||||||
print(f" {package}")
|
print(f" {package}")
|
||||||
print(f" Total: {len(config.packages)} packages")
|
print(f" Total: {len(config.packages)} dnf packages")
|
||||||
else:
|
else:
|
||||||
print(" No packages configured")
|
print(" No dnf packages configured")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("Brew Packages:")
|
||||||
|
if config.brew_packages:
|
||||||
|
for package in config.brew_packages:
|
||||||
|
print(f" {package}")
|
||||||
|
print(f" Total: {len(config.brew_packages)} brew packages")
|
||||||
|
else:
|
||||||
|
print(" No brew packages configured")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("Flatpaks:")
|
||||||
|
if config.flatpak_refs:
|
||||||
|
for ref in config.flatpak_refs:
|
||||||
|
print(f" {ref}")
|
||||||
|
print(f" Total: {len(config.flatpak_refs)} flatpaks")
|
||||||
|
else:
|
||||||
|
print(" No flatpaks configured")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("rpm-ostree Packages:")
|
||||||
|
if config.rpm_ostree_packages:
|
||||||
|
for package in config.rpm_ostree_packages:
|
||||||
|
print(f" {package}")
|
||||||
|
print(f" Total: {len(config.rpm_ostree_packages)} rpm-ostree packages")
|
||||||
|
else:
|
||||||
|
print(" No rpm-ostree packages configured")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("Nix Packages:")
|
||||||
|
if config.nix_packages:
|
||||||
|
for package in config.nix_packages:
|
||||||
|
print(f" {package}")
|
||||||
|
print(f" Total: {len(config.nix_packages)} nix packages")
|
||||||
|
else:
|
||||||
|
print(" No nix packages configured")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
log_info("Dotfiles:")
|
log_info("Dotfiles:")
|
||||||
|
|
@ -592,12 +735,17 @@ def status_command() -> None:
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="forge",
|
prog="curator",
|
||||||
description="Forge - A home-manager like script for Fedora",
|
description="curator - A home-manager like script for Fedora",
|
||||||
)
|
)
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
subparsers.add_parser("init", help="Initialize configuration structure")
|
subparsers.add_parser("init", help="Initialize configuration structure")
|
||||||
subparsers.add_parser("switch", help="Apply configuration")
|
switch_parser = subparsers.add_parser("switch", help="Apply configuration")
|
||||||
|
switch_parser.add_argument(
|
||||||
|
"--rollback",
|
||||||
|
action="store_true",
|
||||||
|
help="Restore the previous inventory.toml snapshot and apply it",
|
||||||
|
)
|
||||||
subparsers.add_parser("status", help="Show current configuration status")
|
subparsers.add_parser("status", help="Show current configuration status")
|
||||||
subparsers.add_parser("help", help="Show help message")
|
subparsers.add_parser("help", help="Show help message")
|
||||||
return parser
|
return parser
|
||||||
|
|
@ -610,7 +758,7 @@ def main(argv: list[str] | None = None) -> None:
|
||||||
if args.command == "init":
|
if args.command == "init":
|
||||||
init_command()
|
init_command()
|
||||||
elif args.command == "switch":
|
elif args.command == "switch":
|
||||||
switch_command()
|
switch_command_with_args(getattr(args, "rollback", False))
|
||||||
elif args.command == "status":
|
elif args.command == "status":
|
||||||
status_command()
|
status_command()
|
||||||
elif args.command == "help":
|
elif args.command == "help":
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# Forge package
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,22 +1,37 @@
|
||||||
import datetime as dt
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import tomlkit
|
import datetime as dt
|
||||||
|
|
||||||
from forge import cli
|
from curator import cli
|
||||||
|
|
||||||
|
|
||||||
def test_parse_config_arrays(tmp_path: Path) -> None:
|
def test_parse_config_line_format(tmp_path: Path) -> None:
|
||||||
config_path = tmp_path / "forge.toml"
|
config_path = tmp_path / "inventory.toml"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
"""
|
"""
|
||||||
copr = ["copr.fedorainfracloud.org/user/repo"]
|
[curator]
|
||||||
packages = ["git", "vim"]
|
|
||||||
|
|
||||||
[forge]
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
last_switch = ""
|
last_switch = ""
|
||||||
|
|
||||||
|
[copr]
|
||||||
|
copr.fedorainfracloud.org/user/repo
|
||||||
|
|
||||||
|
[dnf]
|
||||||
|
git
|
||||||
|
vim
|
||||||
|
|
||||||
|
[brew]
|
||||||
|
wget
|
||||||
|
|
||||||
|
[flatpak]
|
||||||
|
org.mozilla.firefox
|
||||||
|
|
||||||
|
[rpm-ostree]
|
||||||
|
podman
|
||||||
|
|
||||||
|
[nix]
|
||||||
|
nixpkgs#git
|
||||||
|
|
||||||
[dotfiles]
|
[dotfiles]
|
||||||
".bashrc" = ".bashrc"
|
".bashrc" = ".bashrc"
|
||||||
|
|
||||||
|
|
@ -30,18 +45,33 @@ backup_dir = "backup"
|
||||||
|
|
||||||
assert config.copr == ["copr.fedorainfracloud.org/user/repo"]
|
assert config.copr == ["copr.fedorainfracloud.org/user/repo"]
|
||||||
assert config.packages == ["git", "vim"]
|
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.dotfiles == {".bashrc": ".bashrc"}
|
||||||
assert config.options["backup"] is True
|
assert config.options["backup"] is True
|
||||||
assert config.legacy is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_config_legacy_format(tmp_path: Path) -> None:
|
def test_parse_config_legacy_format(tmp_path: Path) -> None:
|
||||||
config_path = tmp_path / "forge.toml"
|
config_path = tmp_path / "inventory.toml"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
"""
|
"""
|
||||||
[packages]
|
[dnf]
|
||||||
git
|
git
|
||||||
vim
|
vim
|
||||||
|
|
||||||
|
[brew]
|
||||||
|
wget
|
||||||
|
|
||||||
|
[flatpak]
|
||||||
|
org.mozilla.firefox
|
||||||
|
|
||||||
|
[rpm-ostree]
|
||||||
|
podman
|
||||||
|
|
||||||
|
[nix]
|
||||||
|
nixpkgs#git
|
||||||
|
|
||||||
[copr]
|
[copr]
|
||||||
copr.fedorainfracloud.org/user/repo
|
copr.fedorainfracloud.org/user/repo
|
||||||
|
|
@ -54,9 +84,12 @@ copr.fedorainfracloud.org/user/repo
|
||||||
config = cli.parse_config(config_path)
|
config = cli.parse_config(config_path)
|
||||||
|
|
||||||
assert config.packages == ["git", "vim"]
|
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.copr == ["copr.fedorainfracloud.org/user/repo"]
|
||||||
assert config.dotfiles == {".bashrc": ".bashrc"}
|
assert config.dotfiles == {".bashrc": ".bashrc"}
|
||||||
assert config.legacy is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_diff_items() -> None:
|
def test_diff_items() -> None:
|
||||||
|
|
@ -67,10 +100,10 @@ def test_diff_items() -> None:
|
||||||
|
|
||||||
def test_update_last_switch_creates_backup(tmp_path: Path) -> None:
|
def test_update_last_switch_creates_backup(tmp_path: Path) -> None:
|
||||||
config_dir = tmp_path
|
config_dir = tmp_path
|
||||||
config_path = config_dir / "forge.toml"
|
config_path = config_dir / "inventory.toml"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
"""
|
"""
|
||||||
[forge]
|
[curator]
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
last_switch = ""
|
last_switch = ""
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
@ -79,10 +112,13 @@ last_switch = ""
|
||||||
options = {"backup": True, "backup_dir": "backup"}
|
options = {"backup": True, "backup_dir": "backup"}
|
||||||
cli.update_last_switch(config_path, options)
|
cli.update_last_switch(config_path, options)
|
||||||
|
|
||||||
doc = tomlkit.parse(config_path.read_text())
|
lines = [line.strip() for line in config_path.read_text().splitlines() if line.strip()]
|
||||||
assert isinstance(doc["forge"]["last_switch"], dt.datetime)
|
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"
|
backup_dir = config_dir / "backup"
|
||||||
backups = list(backup_dir.iterdir())
|
backups = list(backup_dir.iterdir())
|
||||||
assert backups, "expected a backup of forge.toml to be created"
|
assert backups, "expected a backup of inventory.toml to be created"
|
||||||
assert any(path.name.startswith("forge.toml.") for path in backups)
|
assert any(path.name.startswith("inventory.toml.") for path in backups)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
[forge]
|
[curator]
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
last_switch = ""
|
last_switch = ""
|
||||||
|
|
||||||
|
|
|
||||||
15
uv.lock
generated
15
uv.lock
generated
|
|
@ -12,12 +12,9 @@ wheels = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "forge"
|
name = "curator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
|
||||||
{ name = "tomlkit" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
|
@ -25,7 +22,6 @@ dev = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "tomlkit", specifier = ">=0.12" }]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "pytest", specifier = ">=7.4" }]
|
dev = [{ name = "pytest", specifier = ">=7.4" }]
|
||||||
|
|
@ -81,12 +77,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049dd
|
||||||
wheels = [
|
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" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tomlkit"
|
|
||||||
version = "0.13.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
|
|
||||||
]
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue