init
This commit is contained in:
parent
359c250101
commit
da6d24c74f
13 changed files with 973 additions and 555 deletions
206
README.md
206
README.md
|
|
@ -1,10 +1,10 @@
|
||||||
# Forge - A Home-Manager Like Tool for Fedora
|
# Forge - A Home-Manager Like Tool for Fedora
|
||||||
|
|
||||||
A lightweight configuration management tool 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 bash script that helps you manage your Fedora system configuration by:
|
Forge 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
|
||||||
|
|
@ -14,39 +14,50 @@ Forge is a bash script that helps you manage your Fedora system configuration by
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Clone or download this repository
|
1. Clone or download this repository.
|
||||||
2. Make the script executable:
|
2. Install dependencies with `uv` (none beyond the standard library, but this sets up the venv):
|
||||||
```bash
|
```bash
|
||||||
chmod +x forge
|
uv sync
|
||||||
```
|
```
|
||||||
3. Optionally, move it to a directory in your PATH:
|
3. Run with `uv`:
|
||||||
```bash
|
```bash
|
||||||
sudo mv forge /usr/local/bin/forge
|
uv run forge --help
|
||||||
|
```
|
||||||
|
4. Or use the local shim directly:
|
||||||
|
```bash
|
||||||
|
./forge --help
|
||||||
|
```
|
||||||
|
5. Optionally install globally via `uv`:
|
||||||
|
```bash
|
||||||
|
uv tool install .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Initialize the configuration structure
|
# Initialize the configuration structure
|
||||||
./forge init
|
uv run forge 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/forge/forge.toml
|
||||||
|
|
||||||
# Apply configuration
|
# Apply configuration
|
||||||
./forge switch
|
uv run forge switch
|
||||||
|
|
||||||
# Check status
|
# Check status
|
||||||
./forge status
|
uv run forge status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can swap `uv run forge ...` for `./forge ...` if you prefer the local shim.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### `init`
|
### `init`
|
||||||
Initialize the configuration structure and create `forge.toml`.
|
Initialize the configuration structure and create `forge.toml`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./forge init
|
uv run forge init
|
||||||
|
# or ./forge init
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates:
|
Creates:
|
||||||
|
|
@ -57,7 +68,8 @@ Creates:
|
||||||
Apply the current configuration (enable COPR, install packages, deploy dotfiles).
|
Apply the current configuration (enable COPR, install packages, deploy dotfiles).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./forge switch
|
uv run forge switch
|
||||||
|
# or ./forge switch
|
||||||
```
|
```
|
||||||
|
|
||||||
This command:
|
This command:
|
||||||
|
|
@ -72,7 +84,8 @@ This command:
|
||||||
Show current configuration status and information.
|
Show current configuration status and information.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./forge status
|
uv run forge status
|
||||||
|
# or ./forge status
|
||||||
```
|
```
|
||||||
|
|
||||||
Displays:
|
Displays:
|
||||||
|
|
@ -84,7 +97,8 @@ Displays:
|
||||||
Show help message with all available commands.
|
Show help message with all available commands.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./forge help
|
uv run forge help
|
||||||
|
# or ./forge help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
@ -96,23 +110,21 @@ The central configuration file located at `~/.config/forge/forge.toml`:
|
||||||
# Forge Configuration File
|
# Forge Configuration File
|
||||||
# User-level configuration similar to home-manager/nixos
|
# User-level configuration similar to home-manager/nixos
|
||||||
|
|
||||||
|
copr = [
|
||||||
|
# "copr.fedorainfracloud.org/username/repository",
|
||||||
|
# "copr.fedorainfracloud.org/anotheruser/anotherrepo",
|
||||||
|
]
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
# "git",
|
||||||
|
# "vim",
|
||||||
|
# "curl",
|
||||||
|
# "wget",
|
||||||
|
]
|
||||||
|
|
||||||
[forge]
|
[forge]
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
last_switch = null
|
last_switch = ""
|
||||||
|
|
||||||
[copr]
|
|
||||||
# COPR repositories to enable
|
|
||||||
# Just list COPR repository names - presence means enable, absence means don't enable
|
|
||||||
copr.fedorainfracloud.org/username/repository
|
|
||||||
copr.fedorainfracloud.org/anotheruser/anotherrepo
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
# List of packages to install using dnf
|
|
||||||
# Just list package names - presence means install, absence means don't install
|
|
||||||
git
|
|
||||||
vim
|
|
||||||
curl
|
|
||||||
wget
|
|
||||||
|
|
||||||
[dotfiles]
|
[dotfiles]
|
||||||
# Dotfiles to manage with symlinks
|
# Dotfiles to manage with symlinks
|
||||||
|
|
@ -135,30 +147,32 @@ backup_dir = "backup"
|
||||||
- `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`. **Just list COPR repository names** - presence means enable, absence means don't enable.
|
Array 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
|
||||||
[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. Forge will automatically disable it during the next switch.
|
||||||
|
|
||||||
#### `[packages]`
|
#### `packages`
|
||||||
List of packages to install via dnf. **Just list package names** - presence means install, absence means don't install.
|
Array of packages to install via dnf. **Just list package names** - presence means install, absence means don't install.
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
```toml
|
```toml
|
||||||
[packages]
|
packages = [
|
||||||
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.
|
||||||
|
|
@ -191,17 +205,19 @@ Additional configuration options:
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
### COPR Repository Management
|
### COPR Repository Management
|
||||||
COPR repositories are managed through the `[copr]` section in `forge.toml`:
|
COPR repositories are managed through the `copr` array in `forge.toml`:
|
||||||
- **Add COPR repo**: Simply add the repository name on a new line
|
- **Add COPR repo**: Add the repository name to the array
|
||||||
- **Remove COPR repo**: Delete the line containing the repository name
|
- **Remove COPR repo**: Remove the entry from the array
|
||||||
- **Automatic cleanup**: Forge automatically disables COPR repos that are removed from configuration
|
- **Automatic cleanup**: Forge 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.
|
||||||
|
|
||||||
### Package Management
|
### Package Management
|
||||||
Packages are managed through the `[packages]` section in `forge.toml`:
|
Packages are managed through the `packages` array in `forge.toml`:
|
||||||
- **Add package**: Simply add the package name on a new line
|
- **Add package**: Add the package name to the array
|
||||||
- **Remove package**: Delete the line containing the package name
|
- **Remove package**: Remove the entry from the array
|
||||||
- **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.
|
||||||
|
|
||||||
### Dotfile Management
|
### Dotfile Management
|
||||||
Forge uses symlinks to manage dotfiles:
|
Forge uses symlinks to manage dotfiles:
|
||||||
|
|
@ -218,23 +234,29 @@ Before creating symlinks, Forge:
|
||||||
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.
|
||||||
|
|
||||||
|
The previously applied configuration is stored separately as `~/.config/forge/forge.toml.prev` to compute diffs for COPR and package changes.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Basic Setup
|
### Basic Setup
|
||||||
```bash
|
```bash
|
||||||
# Initialize forge
|
# Initialize forge
|
||||||
./forge init
|
uv run forge init
|
||||||
|
|
||||||
# Edit forge.toml to add COPR repos and packages
|
# Edit forge.toml to add COPR repos and packages
|
||||||
nano ~/.config/forge/forge.toml
|
nano ~/.config/forge/forge.toml
|
||||||
|
|
||||||
# Add to [copr] section:
|
# Add to the arrays:
|
||||||
copr.fedorainfracloud.org/username/cool-repo
|
# copr = [
|
||||||
|
# "copr.fedorainfracloud.org/username/cool-repo",
|
||||||
# Add to [packages] section:
|
# ]
|
||||||
git
|
# packages = [
|
||||||
vim
|
# "git",
|
||||||
curl
|
# "vim",
|
||||||
|
# "curl",
|
||||||
|
# ]
|
||||||
|
|
||||||
# Create your dotfiles directory and add files
|
# Create your dotfiles directory and add files
|
||||||
mkdir -p ~/.config/forge/dotfiles
|
mkdir -p ~/.config/forge/dotfiles
|
||||||
|
|
@ -244,7 +266,7 @@ echo "export EDITOR=vim" > ~/.config/forge/dotfiles/.bashrc
|
||||||
".bashrc" = ".bashrc"
|
".bashrc" = ".bashrc"
|
||||||
|
|
||||||
# Apply configuration
|
# Apply configuration
|
||||||
./forge switch
|
uv run forge switch
|
||||||
```
|
```
|
||||||
|
|
||||||
### Managing Application Configurations
|
### Managing Application Configurations
|
||||||
|
|
@ -260,37 +282,36 @@ nano ~/.config/forge/forge.toml
|
||||||
".config/alacritty/alacritty.yml" = "alacritty.yml"
|
".config/alacritty/alacritty.yml" = "alacritty.yml"
|
||||||
|
|
||||||
# Apply changes
|
# Apply changes
|
||||||
./forge switch
|
uv run forge 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 line
|
# To remove a COPR repo, just delete the entry
|
||||||
# Forge will automatically disable it during the next switch
|
# Forge will automatically disable it during the next switch
|
||||||
```
|
```
|
||||||
|
|
||||||
### Package Management Examples
|
### Package Management Examples
|
||||||
```toml
|
```toml
|
||||||
[packages]
|
packages = [
|
||||||
# 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 line
|
# To remove a package, just delete the entry
|
||||||
# For example, to remove htop, delete the "htop" line
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Version Control Your Configuration
|
### Version Control Your Configuration
|
||||||
|
|
@ -312,30 +333,35 @@ git commit -m "Updated vim configuration"
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
- Python 3.11+
|
||||||
|
- `tomlkit` (installed automatically via `uv sync`)
|
||||||
|
- `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
|
||||||
- `sed` - For updating configuration file (usually pre-installed)
|
|
||||||
|
|
||||||
## Migration from Previous Version
|
## Migration from Previous Version
|
||||||
|
|
||||||
If you were using the old version of forge with `git = true` format:
|
If you were using the old version of forge with `git = true` or bare keys under `[packages]`/`[copr]`:
|
||||||
|
|
||||||
1. Your existing configuration will not be automatically migrated
|
1. Your existing configuration will not be automatically migrated, but the CLI will still read the legacy format.
|
||||||
2. Run `./forge init` to create the new `forge.toml` structure
|
2. Run `uv run forge init` (or `./forge init`) to create the new `forge.toml` structure.
|
||||||
3. Convert your package configuration from:
|
3. Convert to arrays:
|
||||||
```toml
|
```toml
|
||||||
# Old format
|
# Old formats
|
||||||
|
[packages]
|
||||||
git = true
|
git = true
|
||||||
vim = true
|
vim = true
|
||||||
```
|
# or
|
||||||
to:
|
[packages]
|
||||||
```toml
|
|
||||||
# New format
|
|
||||||
git
|
git
|
||||||
vim
|
vim
|
||||||
|
|
||||||
|
# New format
|
||||||
|
packages = ["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/forge/dotfiles/`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is open source. Feel free to contribute or report issues.
|
This project is open source. Feel free to contribute or report issues.
|
||||||
|
|
|
||||||
482
forge
482
forge
|
|
@ -1,472 +1,24 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
# forge - A home-manager like script for Fedora
|
import sys
|
||||||
# Provides dotfile management and package installation functionality
|
from pathlib import Path
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Configuration
|
def main() -> None:
|
||||||
FORGE_DIR="${FORGE_DIR:-$HOME/.config/forge}"
|
here = Path(__file__).resolve().parent
|
||||||
FORGE_TOML="$FORGE_DIR/forge.toml"
|
src_dir = here / "src"
|
||||||
|
if src_dir.exists():
|
||||||
|
sys.path.insert(0, str(src_dir))
|
||||||
|
|
||||||
# Colors
|
try:
|
||||||
RED='\033[0;31m'
|
from forge.cli import main as forge_main
|
||||||
GREEN='\033[0;32m'
|
except ImportError as exc: # pragma: no cover - fallback error path
|
||||||
YELLOW='\033[1;33m'
|
sys.stderr.write(f"Failed to import forge CLI: {exc}\n")
|
||||||
BLUE='\033[0;34m'
|
sys.exit(1)
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Logging functions
|
forge_main()
|
||||||
log_info() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warning() {
|
if __name__ == "__main__":
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
main()
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
# Note: source_path automatically prefixed with "dotfiles/" if not present
|
|
||||||
# Examples:
|
|
||||||
# ".bashrc" = ".bashrc"
|
|
||||||
# ".config/vimrc" = "vimrc"
|
|
||||||
# ".config/alacritty/alacritty.yml" = "alacritty.yml"
|
|
||||||
|
|
||||||
[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]}"
|
|
||||||
|
|
||||||
# Trim leading/trailing whitespace from key and value
|
|
||||||
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
||||||
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
||||||
|
|
||||||
# Remove quotes from quoted strings
|
|
||||||
if [[ "$key" =~ ^\"(.*)\"$ ]]; then
|
|
||||||
key="${BASH_REMATCH[1]}"
|
|
||||||
fi
|
|
||||||
if [[ "$value" =~ ^\"(.*)\"$ ]]; then
|
|
||||||
value="${BASH_REMATCH[1]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
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
|
|
||||||
local dotfiles_dir="$FORGE_DIR/dotfiles"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Normalize source path - if it doesn't start with dotfiles/, add it
|
|
||||||
if [[ ! "$source" =~ ^dotfiles/ ]]; then
|
|
||||||
source="dotfiles/$source"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local target_path="$HOME/$target"
|
|
||||||
local source_path="$FORGE_DIR/$source"
|
|
||||||
|
|
||||||
# Verify source file exists
|
|
||||||
if [[ ! -f "$source_path" && ! -d "$source_path" ]]; then
|
|
||||||
log_error "Source file not found: $source_path"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create target directory if it doesn't exist
|
|
||||||
local target_dir=$(dirname "$target_path")
|
|
||||||
mkdir -p "$target_dir"
|
|
||||||
|
|
||||||
# Handle existing file/symlink
|
|
||||||
if [[ -e "$target_path" || -L "$target_path" ]]; then
|
|
||||||
# Check if it's already a correct symlink
|
|
||||||
if [[ -L "$target_path" ]]; then
|
|
||||||
local current_target=$(readlink "$target_path")
|
|
||||||
if [[ "$current_target" == "$source_path" ]]; then
|
|
||||||
log_info "Symlink already correct: $target_path"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Backup existing file if it exists and is not a symlink
|
|
||||||
if [[ -e "$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 -r "$target_path" "$backup_path"
|
|
||||||
log_info "Backed up $target_path to $backup_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove existing file/symlink
|
|
||||||
rm -rf "$target_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create relative symlink for better portability
|
|
||||||
local relative_source
|
|
||||||
if [[ -d "$source_path" ]]; then
|
|
||||||
relative_source=$(realpath --relative-to="$target_dir" "$source_path")
|
|
||||||
else
|
|
||||||
relative_source=$(realpath --relative-to="$target_dir" "$source_path")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create symlink using relative path
|
|
||||||
if ln -s "$relative_source" "$target_path"; then
|
|
||||||
log_success "Created symlink: $target_path -> $relative_source"
|
|
||||||
else
|
|
||||||
log_error "Failed to create symlink: $target_path -> $relative_source"
|
|
||||||
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 "$@"
|
|
||||||
|
|
|
||||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[project]
|
||||||
|
name = "forge"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A home-manager style Fedora configuration helper."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = ["tomlkit>=0.12"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
forge = "forge.cli:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
package = true
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pytest>=7.4"]
|
||||||
1
src/forge/__init__.py
Normal file
1
src/forge/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Forge package
|
||||||
BIN
src/forge/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/forge/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/forge/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
src/forge/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
src/forge/__pycache__/cli.cpython-313.pyc
Normal file
BIN
src/forge/__pycache__/cli.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/forge/__pycache__/cli.cpython-314.pyc
Normal file
BIN
src/forge/__pycache__/cli.cpython-314.pyc
Normal file
Binary file not shown.
625
src/forge/cli.py
Normal file
625
src/forge/cli.py
Normal file
|
|
@ -0,0 +1,625 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import tomlkit
|
||||||
|
from tomlkit.exceptions import TOMLKitError
|
||||||
|
from tomlkit.items import Array, Table
|
||||||
|
|
||||||
|
BLUE = "\033[0;34m"
|
||||||
|
GREEN = "\033[0;32m"
|
||||||
|
YELLOW = "\033[1;33m"
|
||||||
|
RED = "\033[0;31m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = """# Forge Configuration File
|
||||||
|
# User-level configuration similar to home-manager/nixos
|
||||||
|
|
||||||
|
copr = [
|
||||||
|
# "copr.fedorainfracloud.org/username/repository",
|
||||||
|
# "copr.fedorainfracloud.org/anotheruser/anotherrepo",
|
||||||
|
]
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
# "git",
|
||||||
|
# "vim",
|
||||||
|
# "curl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[forge]
|
||||||
|
version = "1.0"
|
||||||
|
last_switch = ""
|
||||||
|
|
||||||
|
[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)
|
||||||
|
# Note: source_path automatically prefixed with "dotfiles/" if not present
|
||||||
|
# Examples:
|
||||||
|
# ".bashrc" = ".bashrc"
|
||||||
|
# ".config/vimrc" = "vimrc"
|
||||||
|
# ".config/alacritty/alacritty.yml" = "alacritty.yml"
|
||||||
|
|
||||||
|
[options]
|
||||||
|
# Additional options
|
||||||
|
backup = true
|
||||||
|
backup_dir = "backup"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ForgeConfig:
|
||||||
|
copr: list[str]
|
||||||
|
packages: list[str]
|
||||||
|
dotfiles: dict[str, str]
|
||||||
|
options: dict[str, Any]
|
||||||
|
forge_fields: dict[str, Any]
|
||||||
|
document: Optional[tomlkit.TOMLDocument]
|
||||||
|
legacy: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def log(prefix: str, color: str, message: str) -> None:
|
||||||
|
print(f"{color}{prefix}{RESET} {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def log_info(message: str) -> None:
|
||||||
|
log("[INFO]", BLUE, message)
|
||||||
|
|
||||||
|
|
||||||
|
def log_success(message: str) -> None:
|
||||||
|
log("[SUCCESS]", GREEN, message)
|
||||||
|
|
||||||
|
|
||||||
|
def log_warning(message: str) -> None:
|
||||||
|
log("[WARNING]", YELLOW, message)
|
||||||
|
|
||||||
|
|
||||||
|
def log_error(message: str) -> None:
|
||||||
|
log("[ERROR]", RED, message)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_quotes(value: str) -> str:
|
||||||
|
if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
|
||||||
|
return value[1:-1]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(value: str) -> Any:
|
||||||
|
lowered = value.lower()
|
||||||
|
if lowered in {"true", "false"}:
|
||||||
|
return lowered == "true"
|
||||||
|
if lowered in {"null", "none"}:
|
||||||
|
return None
|
||||||
|
return strip_quotes(value)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_legacy_config(config_path: Path) -> ForgeConfig:
|
||||||
|
copr: list[str] = []
|
||||||
|
packages: list[str] = []
|
||||||
|
dotfiles: dict[str, str] = {}
|
||||||
|
options: dict[str, Any] = {}
|
||||||
|
forge_fields: dict[str, Any] = {}
|
||||||
|
section = None
|
||||||
|
|
||||||
|
with config_path.open() as handle:
|
||||||
|
for raw_line in handle:
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
section_match = re.match(r"\[(.+)]$", line)
|
||||||
|
if section_match:
|
||||||
|
section = section_match.group(1).strip()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if section is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if section in {"packages", "copr"}:
|
||||||
|
entry = line.split("=", 1)[0].strip()
|
||||||
|
if entry:
|
||||||
|
target = packages if section == "packages" else copr
|
||||||
|
target.append(strip_quotes(entry))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if section == "dotfiles":
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
target_raw, source_raw = line.split("=", 1)
|
||||||
|
target = strip_quotes(target_raw.strip())
|
||||||
|
source = strip_quotes(source_raw.strip())
|
||||||
|
if target and source:
|
||||||
|
dotfiles[target] = source
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key_raw, value_raw = line.split("=", 1)
|
||||||
|
key = strip_quotes(key_raw.strip())
|
||||||
|
value = parse_value(value_raw.strip())
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if section == "options":
|
||||||
|
options[key] = value
|
||||||
|
elif section == "forge":
|
||||||
|
forge_fields[key] = value
|
||||||
|
|
||||||
|
merged_options: dict[str, Any] = {"backup": True, "backup_dir": "backup"}
|
||||||
|
merged_options.update(options)
|
||||||
|
|
||||||
|
if not merged_options.get("backup_dir"):
|
||||||
|
merged_options["backup_dir"] = "backup"
|
||||||
|
|
||||||
|
return ForgeConfig(
|
||||||
|
copr=copr,
|
||||||
|
packages=packages,
|
||||||
|
dotfiles=dotfiles,
|
||||||
|
options=merged_options,
|
||||||
|
forge_fields=forge_fields,
|
||||||
|
document=None,
|
||||||
|
legacy=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def empty_config() -> ForgeConfig:
|
||||||
|
return ForgeConfig(
|
||||||
|
copr=[],
|
||||||
|
packages=[],
|
||||||
|
dotfiles={},
|
||||||
|
options={"backup": True, "backup_dir": "backup"},
|
||||||
|
forge_fields={},
|
||||||
|
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 _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():
|
||||||
|
return empty_config()
|
||||||
|
try:
|
||||||
|
return parse_config(config_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return empty_config()
|
||||||
|
|
||||||
|
|
||||||
|
def diff_items(current: list[str], previous: list[str]) -> tuple[list[str], list[str]]:
|
||||||
|
current_set = {item for item in current if item}
|
||||||
|
previous_set = {item for item in previous if item}
|
||||||
|
added = sorted(current_set - previous_set)
|
||||||
|
removed = sorted(previous_set - current_set)
|
||||||
|
return added, removed
|
||||||
|
|
||||||
|
|
||||||
|
def get_paths() -> tuple[Path, Path, Path]:
|
||||||
|
forge_dir = Path(os.environ.get("FORGE_DIR", Path.home() / ".config" / "forge")).expanduser()
|
||||||
|
forge_toml = forge_dir / "forge.toml"
|
||||||
|
previous_toml = forge_dir / "forge.toml.prev"
|
||||||
|
return forge_dir, forge_toml, previous_toml
|
||||||
|
|
||||||
|
|
||||||
|
def init_command() -> None:
|
||||||
|
forge_dir, forge_toml, _ = get_paths()
|
||||||
|
log_info("Initializing forge...")
|
||||||
|
forge_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if forge_toml.exists():
|
||||||
|
log_warning(f"Configuration already exists at {forge_toml}")
|
||||||
|
else:
|
||||||
|
forge_toml.write_text(DEFAULT_CONFIG)
|
||||||
|
log_success(f"Created forge.toml: {forge_toml}")
|
||||||
|
|
||||||
|
log_success("Initialization complete!")
|
||||||
|
log_info(f"Edit {forge_toml} to configure packages and dotfiles")
|
||||||
|
log_info("Run 'forge switch' to apply your configuration")
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(command: list[str]) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, check=False)
|
||||||
|
except FileNotFoundError:
|
||||||
|
log_error(f"Command not found: {' '.join(command)}")
|
||||||
|
return False
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def enable_copr_repos(repos: list[str]) -> None:
|
||||||
|
log_info("Enabling COPR repositories...")
|
||||||
|
if not repos:
|
||||||
|
log_info("No COPR repositories to enable")
|
||||||
|
return
|
||||||
|
|
||||||
|
for copr_repo in repos:
|
||||||
|
log_info(f"Enabling COPR repository: {copr_repo}")
|
||||||
|
if run_command(["sudo", "dnf", "copr", "enable", "-y", copr_repo]):
|
||||||
|
log_success(f"Enabled COPR: {copr_repo}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to enable COPR: {copr_repo}")
|
||||||
|
|
||||||
|
|
||||||
|
def list_enabled_copr() -> set[str]:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["sudo", "dnf", "copr", "list", "--enabled"],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
log_error("dnf not found while listing enabled COPR repositories")
|
||||||
|
return set()
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
log_warning("Unable to list enabled COPR repositories")
|
||||||
|
return set()
|
||||||
|
|
||||||
|
enabled: set[str] = set()
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if "copr.fedorainfracloud.org" in line:
|
||||||
|
repo = line.split()[0]
|
||||||
|
enabled.add(repo)
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
|
def disable_copr_repos(configured: set[str], explicit_removed: set[str] | None = None) -> None:
|
||||||
|
log_info("Checking for COPR repositories to disable...")
|
||||||
|
enabled = list_enabled_copr()
|
||||||
|
to_disable: set[str] = {repo for repo in enabled if repo not in configured}
|
||||||
|
if explicit_removed:
|
||||||
|
to_disable.update(explicit_removed)
|
||||||
|
|
||||||
|
if not to_disable:
|
||||||
|
log_info("No COPR repositories to disable")
|
||||||
|
return
|
||||||
|
|
||||||
|
for repo in sorted(to_disable):
|
||||||
|
log_info(f"Disabling COPR repository: {repo}")
|
||||||
|
if run_command(["sudo", "dnf", "copr", "disable", "-y", repo]):
|
||||||
|
log_success(f"Disabled COPR: {repo}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to disable COPR: {repo}")
|
||||||
|
|
||||||
|
|
||||||
|
def install_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Installing packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No packages to install")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
log_info(f"Installing package: {package}")
|
||||||
|
if run_command(["sudo", "dnf", "install", "-y", package]):
|
||||||
|
log_success(f"Installed {package}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to install {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_packages(packages: list[str]) -> None:
|
||||||
|
log_info("Removing packages...")
|
||||||
|
if not packages:
|
||||||
|
log_info("No packages to remove")
|
||||||
|
return
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
log_info(f"Removing package: {package}")
|
||||||
|
if run_command(["sudo", "dnf", "remove", "-y", package]):
|
||||||
|
log_success(f"Removed {package}")
|
||||||
|
else:
|
||||||
|
log_error(f"Failed to remove {package}")
|
||||||
|
|
||||||
|
|
||||||
|
def backup_target(target_path: Path, backup_dir: Path) -> None:
|
||||||
|
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
backup_path = backup_dir / f"{target_path.name}.{timestamp}.bak"
|
||||||
|
|
||||||
|
if target_path.is_dir():
|
||||||
|
shutil.copytree(target_path, backup_path)
|
||||||
|
else:
|
||||||
|
shutil.copy2(target_path, backup_path)
|
||||||
|
|
||||||
|
log_info(f"Backed up {target_path} to {backup_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_existing(target_path: Path) -> None:
|
||||||
|
if target_path.is_symlink() or target_path.is_file():
|
||||||
|
target_path.unlink()
|
||||||
|
elif target_path.is_dir():
|
||||||
|
shutil.rmtree(target_path)
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_dotfiles(config: ForgeConfig, forge_dir: Path) -> None:
|
||||||
|
log_info("Deploying dotfiles...")
|
||||||
|
if not config.dotfiles:
|
||||||
|
log_info("No dotfiles to deploy")
|
||||||
|
return
|
||||||
|
|
||||||
|
backup_enabled = bool(config.options.get("backup", True))
|
||||||
|
backup_dir_name = str(config.options.get("backup_dir", "backup")) or "backup"
|
||||||
|
backup_dir = forge_dir / backup_dir_name
|
||||||
|
|
||||||
|
for target, source in config.dotfiles.items():
|
||||||
|
source_path = forge_dir / (source if source.startswith("dotfiles/") else f"dotfiles/{source}")
|
||||||
|
target_path = Path.home() / target
|
||||||
|
|
||||||
|
if not source_path.exists():
|
||||||
|
log_error(f"Source file not found: {source_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if target_path.exists() or target_path.is_symlink():
|
||||||
|
if target_path.is_symlink():
|
||||||
|
try:
|
||||||
|
if target_path.resolve() == source_path.resolve():
|
||||||
|
log_info(f"Symlink already correct: {target_path}")
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if backup_enabled and target_path.exists() and not target_path.is_symlink():
|
||||||
|
backup_target(target_path, backup_dir)
|
||||||
|
|
||||||
|
remove_existing(target_path)
|
||||||
|
|
||||||
|
relative_source = os.path.relpath(source_path, target_path.parent)
|
||||||
|
try:
|
||||||
|
target_path.symlink_to(relative_source)
|
||||||
|
log_success(f"Created symlink: {target_path} -> {relative_source}")
|
||||||
|
except OSError as exc:
|
||||||
|
log_error(f"Failed to create symlink: {target_path} -> {relative_source} ({exc})")
|
||||||
|
|
||||||
|
|
||||||
|
def update_last_switch(config_path: Path, options: dict[str, Any]) -> None:
|
||||||
|
backup_enabled = bool(options.get("backup", True))
|
||||||
|
backup_dir_name = str(options.get("backup_dir", "backup") or "backup")
|
||||||
|
backup_dir = config_path.parent / backup_dir_name
|
||||||
|
|
||||||
|
if backup_enabled and config_path.exists():
|
||||||
|
try:
|
||||||
|
backup_target(config_path, backup_dir)
|
||||||
|
except OSError as exc:
|
||||||
|
log_warning(f"Failed to back up forge.toml: {exc}")
|
||||||
|
|
||||||
|
timestamp = dt.datetime.now().astimezone()
|
||||||
|
raw_text = config_path.read_text()
|
||||||
|
document = parse_document(raw_text)
|
||||||
|
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")
|
||||||
|
if not isinstance(forge_table, Table):
|
||||||
|
forge_table = tomlkit.table()
|
||||||
|
document["forge"] = forge_table
|
||||||
|
|
||||||
|
forge_table["last_switch"] = timestamp
|
||||||
|
config_path.write_text(tomlkit.dumps(document))
|
||||||
|
|
||||||
|
|
||||||
|
def save_previous_config(current_path: Path, previous_path: Path) -> None:
|
||||||
|
try:
|
||||||
|
shutil.copy2(current_path, previous_path)
|
||||||
|
except OSError as exc:
|
||||||
|
log_warning(f"Failed to write previous configuration snapshot: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def switch_command() -> None:
|
||||||
|
forge_dir, forge_toml, previous_toml = get_paths()
|
||||||
|
if not forge_toml.exists():
|
||||||
|
log_error("forge.toml not found. Run 'forge init' first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
current_config = parse_config(forge_toml)
|
||||||
|
previous_config = load_config(previous_toml)
|
||||||
|
|
||||||
|
copr_added, copr_removed = diff_items(current_config.copr, previous_config.copr)
|
||||||
|
packages_added, packages_removed = diff_items(current_config.packages, previous_config.packages)
|
||||||
|
|
||||||
|
enable_copr_repos(copr_added if copr_added else current_config.copr)
|
||||||
|
disable_copr_repos(set(current_config.copr), set(copr_removed))
|
||||||
|
install_packages(packages_added if packages_added else current_config.packages)
|
||||||
|
remove_packages(packages_removed)
|
||||||
|
deploy_dotfiles(current_config, forge_dir)
|
||||||
|
update_last_switch(forge_toml, current_config.options)
|
||||||
|
save_previous_config(forge_toml, previous_toml)
|
||||||
|
log_success("Switch completed successfully!")
|
||||||
|
|
||||||
|
|
||||||
|
def status_command() -> None:
|
||||||
|
forge_dir, forge_toml, _ = get_paths()
|
||||||
|
log_info("Forge Status")
|
||||||
|
print(f" Config directory: {forge_dir}")
|
||||||
|
print(f" Config file: {forge_toml}")
|
||||||
|
|
||||||
|
if not forge_toml.exists():
|
||||||
|
print(" No configuration file found")
|
||||||
|
return
|
||||||
|
|
||||||
|
config = parse_config(forge_toml)
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("Configuration:")
|
||||||
|
last_switch = config.forge_fields.get("last_switch")
|
||||||
|
if last_switch and str(last_switch).lower() != "null":
|
||||||
|
print(f" Last switch: {last_switch}")
|
||||||
|
else:
|
||||||
|
print(" Last switch: Never")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("COPR Repositories:")
|
||||||
|
if config.copr:
|
||||||
|
for repo in config.copr:
|
||||||
|
print(f" {repo}")
|
||||||
|
print(f" Total: {len(config.copr)} COPR repositories")
|
||||||
|
else:
|
||||||
|
print(" No COPR repositories configured")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("Packages:")
|
||||||
|
if config.packages:
|
||||||
|
for package in config.packages:
|
||||||
|
print(f" {package}")
|
||||||
|
print(f" Total: {len(config.packages)} packages")
|
||||||
|
else:
|
||||||
|
print(" No packages configured")
|
||||||
|
|
||||||
|
print()
|
||||||
|
log_info("Dotfiles:")
|
||||||
|
if config.dotfiles:
|
||||||
|
for target, source in config.dotfiles.items():
|
||||||
|
print(f" {target} -> {source}")
|
||||||
|
print(f" Total: {len(config.dotfiles)} dotfiles")
|
||||||
|
else:
|
||||||
|
print(" No dotfiles configured")
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="forge",
|
||||||
|
description="Forge - A home-manager like script for Fedora",
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
subparsers.add_parser("init", help="Initialize configuration structure")
|
||||||
|
subparsers.add_parser("switch", help="Apply configuration")
|
||||||
|
subparsers.add_parser("status", help="Show current configuration status")
|
||||||
|
subparsers.add_parser("help", help="Show help message")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> None:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.command == "init":
|
||||||
|
init_command()
|
||||||
|
elif args.command == "switch":
|
||||||
|
switch_command()
|
||||||
|
elif args.command == "status":
|
||||||
|
status_command()
|
||||||
|
elif args.command == "help":
|
||||||
|
parser.print_help()
|
||||||
|
else:
|
||||||
|
log_error(f"Unknown command: {args.command}")
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
tests/__pycache__/test_config.cpython-313-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_config.cpython-313-pytest-9.0.2.pyc
Normal file
Binary file not shown.
88
tests/test_config.py
Normal file
88
tests/test_config.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import datetime as dt
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import tomlkit
|
||||||
|
|
||||||
|
from forge import cli
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_config_arrays(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "forge.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
copr = ["copr.fedorainfracloud.org/user/repo"]
|
||||||
|
packages = ["git", "vim"]
|
||||||
|
|
||||||
|
[forge]
|
||||||
|
version = "1.0"
|
||||||
|
last_switch = ""
|
||||||
|
|
||||||
|
[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.dotfiles == {".bashrc": ".bashrc"}
|
||||||
|
assert config.options["backup"] is True
|
||||||
|
assert config.legacy is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_config_legacy_format(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "forge.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[packages]
|
||||||
|
git
|
||||||
|
vim
|
||||||
|
|
||||||
|
[copr]
|
||||||
|
copr.fedorainfracloud.org/user/repo
|
||||||
|
|
||||||
|
[dotfiles]
|
||||||
|
".bashrc" = ".bashrc"
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
config = cli.parse_config(config_path)
|
||||||
|
|
||||||
|
assert config.packages == ["git", "vim"]
|
||||||
|
assert config.copr == ["copr.fedorainfracloud.org/user/repo"]
|
||||||
|
assert config.dotfiles == {".bashrc": ".bashrc"}
|
||||||
|
assert config.legacy is True
|
||||||
|
|
||||||
|
|
||||||
|
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 / "forge.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[forge]
|
||||||
|
version = "1.0"
|
||||||
|
last_switch = ""
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
options = {"backup": True, "backup_dir": "backup"}
|
||||||
|
cli.update_last_switch(config_path, options)
|
||||||
|
|
||||||
|
doc = tomlkit.parse(config_path.read_text())
|
||||||
|
assert isinstance(doc["forge"]["last_switch"], dt.datetime)
|
||||||
|
|
||||||
|
backup_dir = config_dir / "backup"
|
||||||
|
backups = list(backup_dir.iterdir())
|
||||||
|
assert backups, "expected a backup of forge.toml to be created"
|
||||||
|
assert any(path.name.startswith("forge.toml.") for path in backups)
|
||||||
14
tmpcfg.toml
Normal file
14
tmpcfg.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
[forge]
|
||||||
|
version = "1.0"
|
||||||
|
last_switch = ""
|
||||||
|
|
||||||
|
copr = ["copr.fedorainfracloud.org/user/repo"]
|
||||||
|
packages = ["git", "vim"]
|
||||||
|
|
||||||
|
[dotfiles]
|
||||||
|
".bashrc" = ".bashrc"
|
||||||
|
|
||||||
|
[options]
|
||||||
|
backup = true
|
||||||
|
backup_dir = "backup"
|
||||||
92
uv.lock
generated
Normal file
92
uv.lock
generated
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
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 = "forge"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tomlkit" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "tomlkit", specifier = ">=0.12" }]
|
||||||
|
|
||||||
|
[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" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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