gel is a command-line application built with Rust.
| Platform | Channel | Command / Link |
|---|---|---|
| All | GitHub Releases | Download from releases page |
| All | crates.io | cargo install gel |
| macOS / Linux | Homebrew | brew install omnidotdev/tap/gel |
| Arch Linux | AUR / AUR (bin) | paru -S omnidotdev-gel or paru -S omnidotdev-gel-bin |
git clone https://github.com/omnidotdev/gel
cd gel
cargo build --release
# Binary will be at target/release/gelgel configures an already-installed, booted Arch system. It is not an OS
installer: do the base install first (archinstall, or a manual pacstrap +
bootloader + network), boot into it, then bring the machine up with gel.
One command (installs gel, then applies a config crate you point it at):
# on the fresh box, as a normal user with sudo
curl -fsSL https://raw.githubusercontent.com/omnidotdev/gel/master/scripts/bootstrap.sh | bash -s -- \
--config ./my-config --applyOr the minimum manual steps:
# 1. install gel (build from source until the first release lands)
sudo pacman -S --needed base-devel git rust
git clone https://github.com/omnidotdev/gel && cd gel
cargo build --release --features arch
sudo install -Dm755 target/release/gel /usr/local/bin/gel
# 2. author your machine as config (copy the starter; it depends on gel-core
# via git, so it works from anywhere, not just inside this checkout)
cp -r examples/starter ~/my-config # edit ~/my-config/src/main.rs
# 3. evaluate -> preview -> converge
gel eval ~/my-config --out ~/desired.json
gel diff --artifact ~/desired.json # read-only plan
gel apply --artifact ~/desired.json # converge the machine
gel rollback # undo the last applyWhat apply brings up in one shot: your explicit packages (native + AUR), managed
config files, enabled/disabled services, and system settings (hostname, timezone,
locale). Two caveats for a truly bare box:
- AUR packages use paru, gel's
opinionated AUR helper.
bootstrap.shinstalls it by default (pass--no-paruto skip). Native packages, files, services, and settings work without it. - Snapshot rollback needs a btrfs root with snapper. On other filesystems gel falls back to its transaction journal (which still rolls back packages, files, services, and settings).
gel --helpgel manages the explicitly installed packages on an Arch machine declaratively. You describe the machine you want in a small Rust config crate, then converge the system toward it. The workflow is an eval/apply split: evaluating your config into an artifact is pure and safe, while touching the system is a separate step.
A gel config is an ordinary Rust program that builds a System and prints the
resulting desired state as JSON. See examples/host-config
for a complete crate:
use gel_core::config::System;
fn main() {
let system = System::new()
.native(["git", "ripgrep", "fd", "bat"]) // official-repo packages (pacman)
.foreign(["some-aur-pkg"]) // AUR packages (installed via paru)
// a managed file: gel writes this content verbatim on apply
.file("/tmp/gel-demo.conf", "greeting = hello\n")
.enable("systemd-timesyncd.service") // ensure a unit is enabled
.disable("bluetooth.service") // ensure a unit is disabled
.hostname("gelbox") // manage the hostname
.timezone("Etc/UTC") // manage the timezone
.locale("en_US.UTF-8"); // manage the locale
print!("{}", serde_json::to_string(&system.build()).expect("serialize"));
}System::build() sorts and deduplicates each package list, so authoring order
does not matter. Managed files are likewise sorted by path and deduplicated by
path; declaring the same path twice is last-wins (the final content is kept).
Service enable/disable lists are sorted and deduplicated too, with disable
winning any enable/disable conflict for the same unit.
System::file(path, content) declares a file whose full content gel owns. On
apply, gel writes each declared file whose content differs from what is on disk
(creating parent directories as needed) and records the prior content, so
rollback restores it: a file gel created is deleted, and a file gel overwrote
is returned to its exact prior bytes. Writes are atomic (written to a temp
sibling then renamed into place), so a crash mid-write cannot leave a partially
written config.
Not yet handled (planned for later phases): file pruning (removing a file just because it left the config), file permissions and ownership, content templating, and drift detection against package-provided default config files. gel manages only the files you declare, by full content.
System::enable(unit) and System::disable(unit) declare explicit intent over
systemd units. This is deliberately not full-set convergence: gel only ever
touches the units you name, so a unit absent from both lists is left exactly as
it is and gel never disables a unit it was not told about.
build() sorts and deduplicates each list. When the same unit is both enabled
and disabled, disable wins: the unit is dropped from the enable list, so the
built state never names a unit in both and an ambiguous declaration can never
leave a unit enabled. On apply, gel enables each declared-enable unit that is
currently disabled and disables each declared-disable unit that is currently
enabled (units already in the desired state are skipped). It records each touched
unit's prior enabled state, so rollback restores it: a unit gel enabled is
disabled again, and a unit gel disabled is re-enabled. is_enabled treats
systemd's enabled-runtime state as enabled.
Not yet handled (planned for later phases): unit masking, drop-in override files,
--user units, runtime start/stop state (gel manages enablement, not whether a
unit is currently running), and templated/instanced units. gel manages only the
enable/disable state of the units you declare.
System::hostname(name), System::timezone(tz), and System::locale(locale)
declare explicit intent over three global system settings, converged with
hostnamectl, timedatectl, and localectl respectively. Like services, this is
explicit intent, not full-set convergence: a setting you do not declare is left
None and gel never touches it. Each setter is last-call-wins per field, so the
final value for a given setting is the one lowered into the state.
On apply, gel reads each declared setting's current value and changes only those
that differ, recording each changed setting's prior value. On rollback, gel
restores that prior value; a setting that had no prior value (it was previously
unset) is left as-is rather than cleared, so rollback never invents an empty
hostname, timezone, or locale.
Not yet handled (planned for later phases): users and groups, sysctl kernel
parameters, kernel modules, the console keymap and font, and locale categories
beyond LANG (for example LC_TIME or LC_MESSAGES). gel manages only the
hostname, timezone, and single LANG locale you declare.
gel eval compiles and runs the config crate and writes the desired state to an
artifact. This runs cargo and writes a file; it never touches packages, so it
works in any build:
gel eval examples/host-config # writes the default artifact
gel eval examples/host-config --out /tmp/desired.jsongel diff # preview the plan: packages, files, service, and setting changes (read-only)
gel apply # additive converge: install what is missing, write files, enable/disable units, change settings
gel apply --prune # also remove explicit packages absent from the config
gel import # capture the current explicit packages as a desired state
gel rollback # invert the most recent apply (packages + files + services + settings)diff also lists the managed files whose content would change (~N files to write plus each target path), the service actions (+N to enable, -N to disable
plus each unit), and the setting changes (~N settings to change plus each
setting), and stays read-only. apply takes a filesystem snapshot first (via
snapper on btrfs; it degrades to a warning when snapshots are unavailable), prints
the plan, converges packages, writes managed files, applies the service
enable/disable actions, changes the settings, and records a transaction in the
journal so it can be rolled back. rollback reverses packages, restores managed
files to their prior content, restores each touched unit's prior enabled state,
and restores each changed setting's prior value (a setting that was previously
unset is left as-is); a full snapshot-based filesystem restore is planned for a
later phase.
The system-touching commands (import, diff, apply, rollback) drive real
pacman/paru/systemctl/snapper tooling and are only compiled with the arch feature:
cargo build --release --features arch # binary with the real Arch backendThe default build is pure: those subcommands fast-fail with a clear rebuild
message and nothing touches the host, while gel eval still works.
gel keeps its state under ${XDG_STATE_HOME:-~/.local/state}/gel:
| Path | Purpose |
|---|---|
<state-dir>/desired.json |
default desired-state artifact (override with --out/--artifact) |
<state-dir>/journal/ |
transaction journal used by rollback |
cargo build # Build
cargo run -- --help # Run
cargo test # Test
cargo clippy # LintThis project uses a dual-package setup (Rust crate + npm package) with automated version synchronization:
- Source of truth:
package.jsonholds the canonical version, and is used for Changesets - Sync script:
scripts/syncVersion.tspropagates the version toCargo.toml - Changesets: Manages version bumps and changelog generation
The sync script runs automatically during the release process via the version npm script:
bun run version # syncs `package.json` version β `Cargo.toml`| Workflow | Trigger | Purpose |
|---|---|---|
test.yml |
Push/PR to master |
Runs fmt, clippy, and tests |
sync.yml |
PR to master |
Validates version sync, fmt, clippy, test, build |
release.yml |
Push to master |
Creates releases via Changesets, builds multi-platform binaries |
- Create a changeset:
bun changeset - Push to
master - Changesets action creates a "Version Packages" PR
- Merge the PR to trigger a release with binaries for:
x86_64-unknown-linux-gnuaarch64-unknown-linux-gnux86_64-apple-darwinaarch64-apple-darwin
- Manually publish to crates.io:
cargo publish
- Omni CLI: Agentic CLI for the Omni ecosystem
- Omni Terminal: GPU-accelerated terminal emulator built to run everywhere
The code in this repository is licensed under Apache 2.0, Β© Omni LLC. See LICENSE.md for more information.
