chore(dotfiles): streamline workstation configuration workflows
This commit is contained in:
@@ -252,14 +252,6 @@ mime ^ranger/x-terminal-emulator, has konsole = konsole -e "$@"
|
|||||||
mime ^ranger/x-terminal-emulator, has gnome-terminal = gnome-terminal -- "$@"
|
mime ^ranger/x-terminal-emulator, has gnome-terminal = gnome-terminal -- "$@"
|
||||||
mime ^ranger/x-terminal-emulator, has xterm = xterm -e "$@"
|
mime ^ranger/x-terminal-emulator, has xterm = xterm -e "$@"
|
||||||
|
|
||||||
#-------------------------------------------
|
|
||||||
# Misc
|
|
||||||
#-------------------------------------------
|
|
||||||
label wallpaper, number 11, mime ^image, has feh, X = feh --bg-scale "$1"
|
|
||||||
label wallpaper, number 12, mime ^image, has feh, X = feh --bg-tile "$1"
|
|
||||||
label wallpaper, number 13, mime ^image, has feh, X = feh --bg-center "$1"
|
|
||||||
label wallpaper, number 14, mime ^image, has feh, X = feh --bg-fill "$1"
|
|
||||||
|
|
||||||
#-------------------------------------------
|
#-------------------------------------------
|
||||||
# Generic file openers
|
# Generic file openers
|
||||||
#-------------------------------------------
|
#-------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"quietStartup": true,
|
"quietStartup": true,
|
||||||
"lastChangelogVersion": "0.80.10",
|
"lastChangelogVersion": "0.84.2",
|
||||||
"defaultProvider": "openai-codex",
|
"defaultProvider": "openai-codex",
|
||||||
"defaultModel": "gpt-5.6-terra",
|
"defaultModel": "gpt-5.6-terra",
|
||||||
"defaultThinkingLevel": "high",
|
"defaultThinkingLevel": "high",
|
||||||
@@ -30,5 +30,7 @@
|
|||||||
"prompts/english-default.md",
|
"prompts/english-default.md",
|
||||||
"prompts/todo-hygiene.md"
|
"prompts/todo-hygiene.md"
|
||||||
],
|
],
|
||||||
"theme": "dark"
|
"theme": "dark",
|
||||||
|
"transport": "auto",
|
||||||
|
"tuiMode": "fullscreen"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Agent Guide: Alex's Dotfiles
|
||||||
|
|
||||||
|
## Repository and safety model
|
||||||
|
|
||||||
|
This directory (`/home/alex`) is the **yadm worktree** for personal, Arch-oriented dotfiles. Use `yadm` (rather than assuming a conventional clone) for status, diff, and tracked-file operations.
|
||||||
|
|
||||||
|
- This is machine-specific configuration, **not** a bootstrap installer. There is no authoritative root install, link, overwrite, backup, or rollback command.
|
||||||
|
- Preserve `##class.*` filename variants; they are yadm alternatives for machine classes.
|
||||||
|
- Do not expose, copy, or add private hostnames, IP addresses, account names, tokens, or credentials. Keep machine-specific secrets in local/private configuration.
|
||||||
|
- Scope changes narrowly. Do not reformat unrelated dotfiles or overwrite existing user changes.
|
||||||
|
- Treat scripts that use `sudo`, modify `/etc`, install packages, rebuild boot artifacts, or alter services as potentially disruptive. Inspect them first; do not run them merely to validate an edit.
|
||||||
|
|
||||||
|
Start with the root `README.md` for the dotfile-specific constraints and manual-validation guidance.
|
||||||
|
|
||||||
|
## Ownership map
|
||||||
|
|
||||||
|
| Path | Owns |
|
||||||
|
| --- | --- |
|
||||||
|
| Root dotfiles (`.zshrc`, `.config/`, `.scripts/`, etc.) | Ordinary yadm-managed user configuration and helpers |
|
||||||
|
| `.setup/` | Legacy/manual setup assets and package lists; it is not a complete installer |
|
||||||
|
| `system-config/` | Declarative Decman configuration for this Arch laptop |
|
||||||
|
| `system-config/config/system/` | Files deployed into system locations; do **not** edit deployed `/etc` targets |
|
||||||
|
| `system-config/config/user/` | Selected user dotfiles deployed by Decman; do **not** edit deployed copies |
|
||||||
|
| `system-config/packages/` | Native and AUR package lists organized by concern |
|
||||||
|
| `system-config/manifests/` | Enabled system and user unit manifests |
|
||||||
|
| `system-config/modules/` | Thin Decman deployment/loading adapters only |
|
||||||
|
|
||||||
|
`system-config/` belongs to this yadm worktree; it is not a nested Git repository.
|
||||||
|
|
||||||
|
## Decman architecture
|
||||||
|
|
||||||
|
`system-config/source.py` is the Decman entrypoint. It currently selects the `laptop()` profile from `hosts.py` and applies modules in this order:
|
||||||
|
|
||||||
|
1. `files`
|
||||||
|
2. `pacman`
|
||||||
|
3. `aur`
|
||||||
|
4. `systemd`
|
||||||
|
|
||||||
|
`hosts.py` composes host profiles from package concerns:
|
||||||
|
|
||||||
|
- Shared concerns: `common/core`, `wayland`, `fonts`, `development`, `productivity`, `media`, `research`, `network`, and `services`.
|
||||||
|
- Laptop-specific concern: `laptop/hardware`.
|
||||||
|
- Desktop support is intentionally sparse and future-facing.
|
||||||
|
|
||||||
|
Keep policy in the declared data directories, not in Python glue:
|
||||||
|
|
||||||
|
- Add a package to the narrowest explanatory `packages/<scope>/<concern>-repo.txt` or `-aur.txt` file. A package must be owned exactly once.
|
||||||
|
- Put shared versus host-specific system policy in `config/system/common/` or the applicable host directory.
|
||||||
|
- Put selected user configuration in `config/user/{common,laptop,desktop}/`.
|
||||||
|
- Put service enablement in matching `manifests/{common,laptop,desktop}/` concern files.
|
||||||
|
- Keep `modules/` reusable and thin; it adapts package lists, file trees, and manifests to Decman.
|
||||||
|
|
||||||
|
The ignored dependency lists are adoption guards that prevent Decman orphan cleanup from removing dependencies. Do not delete or regenerate them casually.
|
||||||
|
|
||||||
|
## Change and validation workflow
|
||||||
|
|
||||||
|
For any change, first identify the owning layer above. For Decman changes:
|
||||||
|
|
||||||
|
1. Edit the source in `system-config/`, never the generated/deployed target.
|
||||||
|
2. Run the read-only consistency check from that directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/verify.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. After an intentional package-list change, regenerate and review the derived lists before applying:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/split_packages.py
|
||||||
|
./scripts/refresh_orphan_guards.py
|
||||||
|
./scripts/verify.py
|
||||||
|
yadm diff -- system-config
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Require explicit human approval before applying system state. The review-first commands are:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo decman --source source.py --dry-run --no-hooks
|
||||||
|
sudo decman --source source.py --no-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
The second command mutates system files, packages, and services; do not run it as routine validation.
|
||||||
|
|
||||||
|
For ordinary dotfiles, use the smallest relevant validation: shell syntax/source checks for shell files, application/session testing for desktop configuration, or an explicit manual review. Preserve a rollback path when changing active configuration.
|
||||||
|
|
||||||
|
## Before finishing
|
||||||
|
|
||||||
|
- Review with `yadm diff --check` and `yadm diff`.
|
||||||
|
- Confirm that unrelated working-tree changes were not modified.
|
||||||
|
- State which validation was run and which system-changing operations were intentionally not run.
|
||||||
@@ -22,7 +22,7 @@ Package lists in `.setup/packages/` are inputs for a user-managed installation;
|
|||||||
Start by reviewing the intended files and any machine-class variants. Typical areas are:
|
Start by reviewing the intended files and any machine-class variants. Typical areas are:
|
||||||
|
|
||||||
- `.zshrc`, `.bashrc`, `.terminal_aliases`, `.tmux.conf`, and `.p10k.zsh` — shell and terminal setup.
|
- `.zshrc`, `.bashrc`, `.terminal_aliases`, `.tmux.conf`, and `.p10k.zsh` — shell and terminal setup.
|
||||||
- `.xmonad/`, `.Xresources##class.*`, `.xprofile##class.*`, and `.config/` — desktop/window-manager and application configuration.
|
- `.config/` — desktop and application configuration.
|
||||||
- `.scripts/` — optional shell, network, virtual-environment, and update helpers.
|
- `.scripts/` — optional shell, network, virtual-environment, and update helpers.
|
||||||
- `.setup/packages/` — package lists grouped by purpose.
|
- `.setup/packages/` — package lists grouped by purpose.
|
||||||
|
|
||||||
@@ -45,7 +45,6 @@ update_pacman_packages
|
|||||||
|
|
||||||
The following scripts mutate files outside this repository and must be inspected before use:
|
The following scripts mutate files outside this repository and must be inspected before use:
|
||||||
|
|
||||||
- `.setup/config/setup_touchpad.sh` copies an X11 configuration into `/etc/X11/xorg.conf.d/` when absent.
|
|
||||||
- `.setup/config/exclude_from_update.sh` edits `/etc/pacman.conf` with `sudo` based on `~/.setup/packages/exclude_from_updating.list`.
|
- `.setup/config/exclude_from_update.sh` edits `/etc/pacman.conf` with `sudo` based on `~/.setup/packages/exclude_from_updating.list`.
|
||||||
- `.setup/config/dinit_add_logs_to_all_services.sh` edits files in `/etc/dinit.d` and writes log specifications under `/var/log/dinit`; its `-o` option removes existing logfile lines before adding them.
|
- `.setup/config/dinit_add_logs_to_all_services.sh` edits files in `/etc/dinit.d` and writes log specifications under `/var/log/dinit`; its `-o` option removes existing logfile lines before adding them.
|
||||||
|
|
||||||
|
|||||||
+26
-6
@@ -61,9 +61,27 @@ does not explain a package. System and user units use the same naming:
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
From the repository root, run common workflows through the
|
||||||
|
`./system-config/decman` wrapper, which uses fixed Decman arguments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./system-config/decman verify
|
||||||
|
./system-config/decman dry-run
|
||||||
|
./system-config/decman apply
|
||||||
|
```
|
||||||
|
|
||||||
|
`dry-run` and `apply` first run `verify`. They invoke Decman with `--no-hooks`,
|
||||||
|
matching the deliberately review-first direct commands below. `apply` changes
|
||||||
|
system state; use it only after reviewing the dry run.
|
||||||
|
|
||||||
|
The wrapper locates its configuration relative to itself, so its commands run
|
||||||
|
from the `system-config` directory. Command output and errors stream directly
|
||||||
|
to the terminal. If a child command fails, the wrapper prints the failed command
|
||||||
|
and returns its exit status. A missing executable returns `127`, another
|
||||||
|
execution error (including a permission error) returns `126`, and an
|
||||||
|
interrupted workflow returns `130`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/system-config
|
|
||||||
./scripts/verify.py
|
|
||||||
sudo decman --source source.py --dry-run --no-hooks
|
sudo decman --source source.py --dry-run --no-hooks
|
||||||
sudo decman --source source.py --no-hooks
|
sudo decman --source source.py --no-hooks
|
||||||
```
|
```
|
||||||
@@ -75,8 +93,10 @@ orphan cleanup from removing dependencies during adoption.
|
|||||||
After intentional package changes, regenerate and review concern lists:
|
After intentional package changes, regenerate and review concern lists:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/split_packages.py
|
./system-config/decman refresh-package-state
|
||||||
./scripts/refresh_orphan_guards.py
|
yadm diff -- system-config
|
||||||
./scripts/verify.py
|
|
||||||
git diff
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`refresh-package-state` runs `split-packages`, `refresh-orphan-guards`, and
|
||||||
|
`verify` in that order. Those individual file-changing commands are also
|
||||||
|
available through the dispatcher when only one is needed.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# Used by the Noctalia Pi Status Bridge adapter and the Hyprland launcher.
|
# Used by the Noctalia Pi Status Bridge adapter and the Hyprland launcher.
|
||||||
PI_STATUS_BRIDGE_CLIENT=/home/alex/.local/bin/pi-status-bridge-client
|
PI_STATUS_BRIDGE_CLIENT=/home/alex/.local/bin/pi-status-bridge-client
|
||||||
PI_STATUS_UI_BINARY=/home/alex/.local/bin/pi-status-ui
|
PI_STATUS_UI_BINARY=/home/alex/.local/bin/pi-status-ui
|
||||||
|
# Ctrl+Super+Space starts a fresh Pi session in this worktree. Change as needed.
|
||||||
|
PI_STATUS_DEFAULT_WORKTREE=/home/alex/projects/pi-status-bridge
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
-- stacking/tabbed, and resize-mode behavior is intentionally deferred.
|
-- stacking/tabbed, and resize-mode behavior is intentionally deferred.
|
||||||
local machine = dofile(os.getenv("HOME") .. "/.config/hypr/machine.lua")
|
local machine = dofile(os.getenv("HOME") .. "/.config/hypr/machine.lua")
|
||||||
|
|
||||||
|
-- Machine-specific session environment must be set before Hyprland initializes
|
||||||
|
-- its graphics backend. The laptop profile selects only the Intel iGPU here.
|
||||||
|
for name, value in pairs(machine.environment or {}) do
|
||||||
|
hl.env(name, value)
|
||||||
|
end
|
||||||
|
|
||||||
for _, monitor in ipairs(machine.monitors or {}) do
|
for _, monitor in ipairs(machine.monitors or {}) do
|
||||||
hl.monitor(monitor)
|
hl.monitor(monitor)
|
||||||
end
|
end
|
||||||
@@ -63,12 +69,14 @@ hl.workspace_rule({
|
|||||||
-- Application and session bindings.
|
-- Application and session bindings.
|
||||||
local noctalia_ipc = "noctalia msg "
|
local noctalia_ipc = "noctalia msg "
|
||||||
local pi_status_ui_toggle = 'ui="${PI_STATUS_UI_BINARY:-$HOME/.local/bin/pi-status-ui}"; if test -x "$ui"; then setsid -f env TMPDIR=/tmp "$ui" --toggle >/tmp/pi-status-ui.log 2>&1; fi'
|
local pi_status_ui_toggle = 'ui="${PI_STATUS_UI_BINARY:-$HOME/.local/bin/pi-status-ui}"; if test -x "$ui"; then setsid -f env TMPDIR=/tmp "$ui" --toggle >/tmp/pi-status-ui.log 2>&1; fi'
|
||||||
|
local pi_status_ui_new_session = 'ui="${PI_STATUS_UI_BINARY:-$HOME/.local/bin/pi-status-ui}"; if test -x "$ui"; then setsid -f env TMPDIR=/tmp "$ui" --new >/tmp/pi-status-ui.log 2>&1; fi'
|
||||||
-- Tap and release Super on its own to open Noctalia's control-center home.
|
-- Tap and release Super on its own to open Noctalia's control-center home.
|
||||||
hl.bind(main_mod .. " + SUPER_L", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"), { release = true })
|
hl.bind(main_mod .. " + SUPER_L", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"), { release = true })
|
||||||
hl.bind(main_mod .. " + RETURN", hl.dsp.exec_cmd(terminal))
|
hl.bind(main_mod .. " + RETURN", hl.dsp.exec_cmd(terminal))
|
||||||
hl.bind(main_mod .. " + CTRL + RETURN", hl.dsp.exec_cmd(terminal .. " -e zellij"))
|
hl.bind(main_mod .. " + CTRL + RETURN", hl.dsp.exec_cmd(terminal .. " -e zellij"))
|
||||||
hl.bind(main_mod .. " + SHIFT + RETURN", hl.dsp.exec_cmd("rofi -show combi"))
|
hl.bind(main_mod .. " + SHIFT + RETURN", hl.dsp.exec_cmd("rofi -show combi"))
|
||||||
hl.bind(main_mod .. " + SPACE", hl.dsp.exec_cmd(pi_status_ui_toggle))
|
hl.bind(main_mod .. " + SPACE", hl.dsp.exec_cmd(pi_status_ui_toggle))
|
||||||
|
hl.bind(main_mod .. " + CTRL + SPACE", hl.dsp.exec_cmd(pi_status_ui_new_session))
|
||||||
hl.bind(main_mod .. " + N", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle alex/knowledge-lookup:panel"))
|
hl.bind(main_mod .. " + N", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle alex/knowledge-lookup:panel"))
|
||||||
hl.bind(main_mod .. " + S", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"))
|
hl.bind(main_mod .. " + S", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"))
|
||||||
hl.bind(main_mod .. " + 0", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle session"))
|
hl.bind(main_mod .. " + 0", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle session"))
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
-- Noctalia replaces the i3-era Redshift, Polybar, Blueman Applet, and Picom stack.
|
-- Noctalia replaces the i3-era Redshift, Polybar, Blueman Applet, and Picom stack.
|
||||||
return {
|
return {
|
||||||
is_laptop = true,
|
is_laptop = true,
|
||||||
|
-- Use the colon-free user-config alias for the stable PCI path rather than
|
||||||
|
-- cardN, whose numbering can change between boots. AQ_DRM_DEVICES itself
|
||||||
|
-- uses colons as device separators. This keeps Hyprland off the NVIDIA dGPU
|
||||||
|
-- so it can enter Runtime D3 when no application is explicitly offloaded.
|
||||||
|
environment = {
|
||||||
|
AQ_DRM_DEVICES = os.getenv("HOME") .. "/.config/hypr/intel-drm",
|
||||||
|
},
|
||||||
input = {
|
input = {
|
||||||
touchpad = {
|
touchpad = {
|
||||||
-- Standard laptop behavior: one/two/three-finger taps map to
|
-- Standard laptop behavior: one/two/three-finger taps map to
|
||||||
|
|||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Repository-local Decman interface; avoids changing the system `decman` command.
|
||||||
|
exec "$(dirname "$0")/scripts/decman_cli.py" "$@"
|
||||||
@@ -30,11 +30,13 @@ class UserConfigConcern(decman.Module):
|
|||||||
name: str,
|
name: str,
|
||||||
files: dict[str, str | tuple[str, int]] | None = None,
|
files: dict[str, str | tuple[str, int]] | None = None,
|
||||||
directories: dict[str, str] | None = None,
|
directories: dict[str, str] | None = None,
|
||||||
|
symlinks: dict[str, str] | None = None,
|
||||||
user: str = "alex",
|
user: str = "alex",
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(f"user:{name}")
|
super().__init__(f"user:{name}")
|
||||||
self._files = files or {}
|
self._files = files or {}
|
||||||
self._directories = directories or {}
|
self._directories = directories or {}
|
||||||
|
self._symlinks = symlinks or {}
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
def files(self) -> dict[str, decman.File]:
|
def files(self) -> dict[str, decman.File]:
|
||||||
@@ -60,6 +62,12 @@ class UserConfigConcern(decman.Module):
|
|||||||
for target, source in self._directories.items()
|
for target, source in self._directories.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def symlinks(self) -> dict[str, str | decman.Symlink]:
|
||||||
|
return {
|
||||||
|
link_name: decman.Symlink(target=target, owner=self.user)
|
||||||
|
for link_name, target in self._symlinks.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SystemConcern(decman.Module):
|
class SystemConcern(decman.Module):
|
||||||
"""One host/system concern with explicit files and unit manifests."""
|
"""One host/system concern with explicit files and unit manifests."""
|
||||||
@@ -177,6 +185,11 @@ def laptop_user_config() -> list[UserConfigConcern]:
|
|||||||
"config/user/laptop/noctalia/99-extend.toml"
|
"config/user/laptop/noctalia/99-extend.toml"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
symlinks={
|
||||||
|
"/home/alex/.config/hypr/intel-drm": (
|
||||||
|
"/dev/dri/by-path/pci-0000:00:02.0-card"
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -242,6 +255,8 @@ def laptop_system() -> list[SystemConcern | UserConfigConcern]:
|
|||||||
"/etc/keyd/default.conf": "config/system/common/keyd.conf",
|
"/etc/keyd/default.conf": "config/system/common/keyd.conf",
|
||||||
"/etc/NetworkManager/conf.d/wifi-powersave.conf": "config/system/laptop/wifi-powersave.conf",
|
"/etc/NetworkManager/conf.d/wifi-powersave.conf": "config/system/laptop/wifi-powersave.conf",
|
||||||
"/etc/modprobe.d/iwlwifi.conf": "config/system/laptop/iwlwifi.conf",
|
"/etc/modprobe.d/iwlwifi.conf": "config/system/laptop/iwlwifi.conf",
|
||||||
|
"/etc/modprobe.d/nvidia-runtime-pm.conf": "config/system/laptop/modprobe.d/nvidia-runtime-pm.conf",
|
||||||
|
"/etc/udev/rules.d/80-nvidia-runtime-pm.rules": "config/system/laptop/udev/80-nvidia-runtime-pm.rules",
|
||||||
"/etc/X11/xorg.conf.d/30-touchpad.conf": "config/system/laptop/touchpad.conf",
|
"/etc/X11/xorg.conf.d/30-touchpad.conf": "config/system/laptop/touchpad.conf",
|
||||||
"/etc/tlp.conf": "config/system/laptop/power/tlp.conf",
|
"/etc/tlp.conf": "config/system/laptop/power/tlp.conf",
|
||||||
"/etc/systemd/sleep.conf.d/90-laptop.conf": "config/system/laptop/sleep.conf.d/90-laptop.conf",
|
"/etc/systemd/sleep.conf.d/90-laptop.conf": "config/system/laptop/sleep.conf.d/90-laptop.conf",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ go
|
|||||||
gperf
|
gperf
|
||||||
ibus
|
ibus
|
||||||
imake
|
imake
|
||||||
|
jsoncpp
|
||||||
karchive5
|
karchive5
|
||||||
kauth
|
kauth
|
||||||
kauth5
|
kauth5
|
||||||
|
|||||||
Executable
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the repository's common Decman workflows with safe, fixed commands."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPTS = ROOT / "scripts"
|
||||||
|
|
||||||
|
|
||||||
|
def format_command(command: Sequence[str] | str) -> str:
|
||||||
|
"""Render a shell-safe command for diagnostic output."""
|
||||||
|
return command if isinstance(command, str) else shlex.join(command)
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: Sequence[str]) -> None:
|
||||||
|
"""Print and run a command from the repository root."""
|
||||||
|
print(f"+ {format_command(command)}", file=sys.stderr)
|
||||||
|
subprocess.run(command, check=True, cwd=ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def run_script(name: str) -> None:
|
||||||
|
run([sys.executable, str(SCRIPTS / name)])
|
||||||
|
|
||||||
|
|
||||||
|
def verify() -> None:
|
||||||
|
"""Check the active profile's package and unit invariants."""
|
||||||
|
run_script("verify.py")
|
||||||
|
|
||||||
|
|
||||||
|
def dry_run() -> None:
|
||||||
|
"""Verify the configuration, then preview Decman's changes."""
|
||||||
|
verify()
|
||||||
|
run(["sudo", "decman", "--source", "source.py", "--dry-run", "--no-hooks"])
|
||||||
|
|
||||||
|
|
||||||
|
def apply() -> None:
|
||||||
|
"""Verify the configuration, then apply it without Decman hooks."""
|
||||||
|
verify()
|
||||||
|
run(["sudo", "decman", "--source", "source.py", "--no-hooks"])
|
||||||
|
|
||||||
|
|
||||||
|
def split_packages() -> None:
|
||||||
|
"""Regenerate concern package lists from explicit installed packages."""
|
||||||
|
run_script("split_packages.py")
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_orphan_guards() -> None:
|
||||||
|
"""Regenerate dependency-orphan guards from installed packages."""
|
||||||
|
run_script("refresh_orphan_guards.py")
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_package_state() -> None:
|
||||||
|
"""Regenerate package data and verify the resulting configuration."""
|
||||||
|
split_packages()
|
||||||
|
refresh_orphan_guards()
|
||||||
|
verify()
|
||||||
|
|
||||||
|
|
||||||
|
COMMANDS: dict[str, Callable[[], None]] = {
|
||||||
|
"verify": verify,
|
||||||
|
"dry-run": dry_run,
|
||||||
|
"apply": apply,
|
||||||
|
"split-packages": split_packages,
|
||||||
|
"refresh-orphan-guards": refresh_orphan_guards,
|
||||||
|
"refresh-package-state": refresh_package_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def failure_status(error: Exception) -> int | None:
|
||||||
|
"""Report a launch failure and return its shell-compatible status."""
|
||||||
|
if isinstance(error, subprocess.CalledProcessError):
|
||||||
|
status = error.returncode if error.returncode > 0 else 128 - error.returncode
|
||||||
|
print(
|
||||||
|
f"error: command exited with status {status}: {format_command(error.cmd)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return status
|
||||||
|
if isinstance(error, FileNotFoundError):
|
||||||
|
print(f"error: executable not found: {error.filename}", file=sys.stderr)
|
||||||
|
return 127
|
||||||
|
if isinstance(error, PermissionError):
|
||||||
|
print(f"error: executable is not permitted: {error.filename}", file=sys.stderr)
|
||||||
|
return 126
|
||||||
|
if isinstance(error, OSError):
|
||||||
|
print(
|
||||||
|
f"error: unable to execute {error.filename}: {error.strerror}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 126
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"command",
|
||||||
|
choices=COMMANDS,
|
||||||
|
help="workflow to run; apply and package-state commands modify system or files",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
COMMANDS[args.command]()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("error: interrupted", file=sys.stderr)
|
||||||
|
return 130
|
||||||
|
except Exception as error:
|
||||||
|
status = failure_status(error)
|
||||||
|
if status is None:
|
||||||
|
raise
|
||||||
|
return status
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user