diff --git a/flake.lock b/flake.lock index 67bbfb2..702b008 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,26 @@ { "nodes": { + "agent-of-empires": { + "inputs": { + "crane": "crane", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1786554876, + "narHash": "sha256-akMknOIdX0QsBsM4lgreXSNNym7TNu9iOrDBqrQuTuQ=", + "owner": "agent-of-empires", + "repo": "agent-of-empires", + "rev": "9b0d691a6cdaa6d2451f6eda05e83628c54eb95a", + "type": "github" + }, + "original": { + "owner": "agent-of-empires", + "ref": "v1.14.1", + "repo": "agent-of-empires", + "type": "github" + } + }, "aquamarine": { "inputs": { "hyprutils": [ @@ -36,7 +57,7 @@ "claude-code": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1785285785, @@ -82,11 +103,11 @@ ] }, "locked": { - "lastModified": 1785336468, - "narHash": "sha256-zVeyr06Ra4+NUhMzkaxLmYGdgKhw9DuMR89jpzws82k=", + "lastModified": 1786822283, + "narHash": "sha256-RdoFKwNQ2m2zbHnq/QbgLf2lUI9cohsnlMwLfJuaTh0=", "owner": "ilysenko", "repo": "codex-desktop-linux", - "rev": "dc479a4caca73b2a29fe2f2945e72bdedde5a12c", + "rev": "e6b51d96ac2b9b5d9adafa852241bd0982972557", "type": "github" }, "original": { @@ -95,9 +116,24 @@ "type": "github" } }, + "crane": { + "locked": { + "lastModified": 1771796463, + "narHash": "sha256-9bCDuUzpwJXcHMQYMS1yNuzYMmKO/CCwCexpjWOl62I=", + "owner": "ipetkov", + "repo": "crane", + "rev": "3d3de3313e263e04894f284ac18177bd26169bad", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "ethui": { "inputs": { - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs_3", "rust": "rust", "utils": "utils" }, @@ -132,6 +168,42 @@ "type": "github" } }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -201,6 +273,24 @@ "type": "github" } }, + "flake-utils_5": { + "inputs": { + "systems": "systems_8" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "foundry": { "inputs": { "flake-utils": "flake-utils_4", @@ -353,7 +443,7 @@ "hyprutils": "hyprutils", "hyprwayland-scanner": "hyprwayland-scanner", "hyprwire": "hyprwire", - "nixpkgs": "nixpkgs_4", + "nixpkgs": "nixpkgs_5", "pre-commit-hooks": "pre-commit-hooks", "systems": "systems_5", "xdph": "xdph" @@ -648,6 +738,52 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1781074563, + "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1769909678, + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "72716169fe93074c333e8d0173151350670b824c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-lib_2": { + "locked": { + "lastModified": 1774748309, + "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "333c4e0545a6da976206c74db8773a1645b5870a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1784783405, "narHash": "sha256-4IHyyLgLBdKefkljdKod4IMn023pQiDXAWJA187cmdY=", @@ -663,7 +799,7 @@ "type": "github" } }, - "nixpkgs_2": { + "nixpkgs_3": { "locked": { "lastModified": 1764517877, "narHash": "sha256-pp3uT4hHijIC8JUK5MEqeAWmParJrgBVzHLNfJDZxg4=", @@ -679,7 +815,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { "lastModified": 1744536153, "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", @@ -695,7 +831,7 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { "lastModified": 1784356753, "narHash": "sha256-12KrbMiWLcf8m7pCvAtZh1ZrgF85ZXDXvfR/fWTKy84=", @@ -711,7 +847,7 @@ "type": "github" } }, - "nixpkgs_5": { + "nixpkgs_6": { "locked": { "lastModified": 1785090369, "narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=", @@ -727,18 +863,34 @@ "type": "github" } }, - "nixpkgs_6": { + "nixpkgs_7": { "locked": { - "lastModified": 1751274312, - "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "lastModified": 1775036866, + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.11", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_8": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -826,6 +978,7 @@ }, "root": { "inputs": { + "agent-of-empires": "agent-of-empires", "claude-code": "claude-code", "codex-cli": "codex-cli", "codex-desktop-linux": "codex-desktop-linux", @@ -835,14 +988,14 @@ "home-manager": "home-manager", "hyprland": "hyprland", "nix-index-database": "nix-index-database", - "nixpkgs": "nixpkgs_5", + "nixpkgs": "nixpkgs_6", "noctalia": "noctalia", "nvchad-starter": "nvchad-starter", "nvchad4nix": "nvchad4nix", "rose-pine-hyprcursor": "rose-pine-hyprcursor", + "sem": "sem", "spicetify-nix": "spicetify-nix", - "systems": "systems_9", - "yogurt": "yogurt", + "systems": "systems_10", "zen-browser": "zen-browser" } }, @@ -870,7 +1023,7 @@ }, "rust": { "inputs": { - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_4" }, "locked": { "lastModified": 1764729618, @@ -886,12 +1039,51 @@ "type": "github" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_8" + }, + "locked": { + "lastModified": 1775445266, + "narHash": "sha256-3fgIj85WHQbOamrpIw9WY3ZL1PoEvjPOjmzMYNsEQJo=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "61747bc3cf2da179b9af356ae9e70f3b895b0c24", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "sem": { + "inputs": { + "flake-parts": "flake-parts_2", + "flake-utils": "flake-utils_5", + "nixpkgs": "nixpkgs_7", + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1786903203, + "narHash": "sha256-MaISe6TG4I5zb6hSRsBj5ENe5JSh7MjQ+QIDWDhxTo0=", + "owner": "Ataraxy-Labs", + "repo": "sem", + "rev": "ba2dfbbe75836cfe6f69dc036c9060b7ce5cc158", + "type": "github" + }, + "original": { + "owner": "Ataraxy-Labs", + "repo": "sem", + "type": "github" + } + }, "spicetify-nix": { "inputs": { "nixpkgs": [ "nixpkgs" ], - "systems": "systems_8" + "systems": "systems_9" }, "locked": { "lastModified": 1785048222, @@ -922,6 +1114,21 @@ "type": "github" } }, + "systems_10": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + }, "systems_2": { "locked": { "lastModified": 1681028828, @@ -1029,16 +1236,16 @@ }, "systems_9": { "locked": { - "lastModified": 1689347949, - "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", - "repo": "default-linux", - "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", - "repo": "default-linux", + "repo": "default", "type": "github" } }, @@ -1119,24 +1326,6 @@ "type": "github" } }, - "yogurt": { - "inputs": { - "nixpkgs": "nixpkgs_6" - }, - "locked": { - "lastModified": 1783264766, - "narHash": "sha256-1iD8gdvxBYBfffHhNsnZPnPGDDttYVRUhghLbAa1Mho=", - "ref": "refs/heads/main", - "rev": "515ab16c76fc0779a1c6be93cfa1424a901373cb", - "revCount": 67, - "type": "git", - "url": "https://github.com/ZePedroResende/yogurt" - }, - "original": { - "type": "git", - "url": "https://github.com/ZePedroResende/yogurt" - } - }, "zen-browser": { "inputs": { "home-manager": "home-manager_2", diff --git a/flake.nix b/flake.nix index 75e22de..429d51e 100644 --- a/flake.nix +++ b/flake.nix @@ -66,7 +66,12 @@ url = "github:ilysenko/codex-desktop-linux"; inputs.nixpkgs.follows = "nixpkgs"; }; - yogurt.url = "git+https://github.com/ZePedroResende/yogurt"; + # Pinned to the tag, and pins its own nixpkgs for the same reason ethui does + # — it is a verified Rust build. Bump deliberately, not via `flake update`. + agent-of-empires.url = "github:agent-of-empires/agent-of-empires/v1.14.1"; + # Semantic-diff tool rev calls via REV_SEM_BIN. NOT nixpkgs' `sem`, which is + # the unrelated Semaphore CI cli. + sem.url = "github:Ataraxy-Labs/sem"; nix-index-database = { url = "github:nix-community/nix-index-database"; inputs.nixpkgs.follows = "nixpkgs"; @@ -124,6 +129,7 @@ nixosConfigurations = { arrakis = mkNixOS "arrakis"; konishi = mkNixOS "konishi"; + yolo = mkNixOS "yolo"; }; # Named "@" so `nh home switch` auto-detects them @@ -131,6 +137,7 @@ homeConfigurations = { "naps62@arrakis" = mkHome "arrakis" x86; "naps62@konishi" = mkHome "konishi" x86; + "naps62@yolo" = mkHome "yolo" x86; }; devShells = diff --git a/home/common/programs/default.nix b/home/common/programs/default.nix index cc9cbfa..5bc95df 100644 --- a/home/common/programs/default.nix +++ b/home/common/programs/default.nix @@ -2,7 +2,6 @@ lib, pkgs, config, - inputs, ... }: { @@ -34,7 +33,6 @@ packages = with pkgs; [ impala - inputs.yogurt.packages.${pkgs.system}.default ]; username = lib.mkDefault "naps62"; diff --git a/home/common/programs/hyprland/default.nix b/home/common/programs/hyprland/default.nix index 475f33c..e04ea6f 100644 --- a/home/common/programs/hyprland/default.nix +++ b/home/common/programs/hyprland/default.nix @@ -154,7 +154,6 @@ in hl.exec_cmd("hyprctl setcursor Nordzy-cursors ${toString cfg.cursorSize}") hl.exec_cmd("hyprsunset") hl.exec_cmd("noctalia") - hl.exec_cmd("yogurt --tray") end) -- layer rules diff --git a/home/yolo/bin/claude-rc b/home/yolo/bin/claude-rc new file mode 100755 index 0000000..2b8bba6 --- /dev/null +++ b/home/yolo/bin/claude-rc @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# Manage the live list of Claude Code Remote Control projects. +# The projects file is the source of truth; sync reconciles systemd to match. +set -euo pipefail + +LIST="${CLAUDE_RC_LIST:-$HOME/.config/claude-rc/projects}" +UNIT=claude-rc + +die() { echo "claude-rc: $*" >&2; exit 1; } + +# configured paths: strip comments, an optional "| Display Name" suffix, +# surrounding space, and any trailing slash. +wanted_paths() { + [ -f "$LIST" ] || return 0 + sed -e 's/#.*//' -e 's/|.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' "$LIST" \ + | grep -v '^$' \ + | sed -e 's#/\+$##' \ + | sort -u +} + +# display name for a path, if the list defines one +name_for() { + [ -f "$LIST" ] || return 0 + awk -F'|' -v t="$1" ' + { line=$0; sub(/#.*/,"",line) + p=line; if (index(line,"|")) { p=substr(line,1,index(line,"|")-1) } + gsub(/^[[:space:]]+|[[:space:]]+$/,"",p); sub(/\/+$/,"",p) + if (p == t && index(line,"|")) { + n=substr(line,index(line,"|")+1) + gsub(/^[[:space:]]+|[[:space:]]+$/,"",n) + if (n != "") { print n; exit } + } + }' "$LIST" 2>/dev/null +} + +# instance names systemd should have enabled +wanted_units() { + wanted_paths | while read -r p; do + printf '%s@%s.service\n' "$UNIT" "$(systemd-escape -p "$p")" + done | sort -u +} + +# instance names systemd currently has +current_units() { + systemctl --user list-units --all --no-legend --plain "${UNIT}@*.service" 2>/dev/null \ + | awk '{print $1}' | sort -u +} + +cmd_sync() { + local want have add rm + want=$(wanted_units); have=$(current_units) + add=$(comm -23 <(printf '%s\n' "$want") <(printf '%s\n' "$have") | grep -v '^$' || true) + rm=$(comm -13 <(printf '%s\n' "$want") <(printf '%s\n' "$have") | grep -v '^$' || true) + + if [ -n "$rm" ]; then + while read -r u; do + echo "- stop $u" + systemctl --user disable --now "$u" >/dev/null 2>&1 || true + done <<<"$rm" + fi + if [ -n "$add" ]; then + while read -r u; do + echo "+ start $u" + systemctl --user enable --now "$u" >/dev/null 2>&1 \ + || echo " FAILED: $u (see: claude-rc log $u)" >&2 + done <<<"$add" + fi + [ -z "$add$rm" ] && echo "in sync ($(wanted_paths | grep -c . || true) projects)" + return 0 +} + +cmd_list() { + local n=0 + printf '%-46s %-9s %s\n' PROJECT STATE GIT + while read -r p; do + n=$((n+1)) + local u state git + u="${UNIT}@$(systemd-escape -p "$p").service" + state=$(systemctl --user is-active "$u" 2>/dev/null || true) + if [ ! -d "$p" ]; then git="MISSING DIR" + elif git -C "$p" rev-parse --git-dir >/dev/null 2>&1; then git="worktree" + else git="same-dir (not git)"; fi + printf '%-46s %-9s %s\n' "${p/#$HOME/\~}" "$state" "$git" + done < <(wanted_paths) + [ "$n" = 0 ] && echo "(no projects configured; claude-rc add )" + return 0 +} + +cmd_add() { + local p="${1:-$PWD}" + p=$(cd "$p" 2>/dev/null && pwd) || die "no such directory: ${1:-$PWD}" + if wanted_paths | grep -qxF "$p"; then echo "already listed: $p"; return 0; fi + # revive a commented-out entry if present, else append; keeps the file tidy + # across add/rm cycles instead of accumulating dead duplicates. + if awk -v t="$p" ' + { l=$0; sub(/^[[:space:]]*#[[:space:]]*/,"",l); sub(/\/+$/,"",l) + gsub(/^[[:space:]]+|[[:space:]]+$/,"",l) + if ($0 ~ /^[[:space:]]*#/ && l == t) found=1 } + END { exit !found }' "$LIST"; then + awk -v t="$p" ' + { l=$0; sub(/^[[:space:]]*#[[:space:]]*/,"",l); sub(/\/+$/,"",l) + gsub(/^[[:space:]]+|[[:space:]]+$/,"",l) + if (!done && $0 ~ /^[[:space:]]*#/ && l == t) { print t; done=1 } + else print $0 } + ' "$LIST" > "$LIST.tmp" && mv "$LIST.tmp" "$LIST" + else + printf '%s\n' "$p" >> "$LIST" + fi + echo "added: $p" + cmd_sync +} + +cmd_rm() { + local p="${1:?usage: claude-rc rm }" + p=$(cd "$p" 2>/dev/null && pwd) || p="${p%/}" + wanted_paths | grep -qxF "$p" || die "not listed: $p" + # comment out rather than delete, so the entry is easy to restore. + # awk, not sed: the path is data, so no regex metachar/delimiter escaping. + awk -v target="$p" ' + { + line = $0 + sub(/#.*/, "", line) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) + sub(/\/+$/, "", line) + if (line != "" && line == target) print "# " $0 + else print $0 + } + ' "$LIST" > "$LIST.tmp" && mv "$LIST.tmp" "$LIST" + echo "removed: $p" + cmd_sync +} + +cmd_edit() { "${EDITOR:-vi}" "$LIST"; cmd_sync; } +cmd_log() { + local p="${1:?usage: claude-rc log }" + p=$(cd "$p" 2>/dev/null && pwd) || die "no such directory: $p" + journalctl --user -u "${UNIT}@$(systemd-escape -p "$p").service" -n 40 --no-pager +} + +case "${1:-list}" in + sync) cmd_sync ;; + list|ls|status) cmd_list ;; + add) shift; cmd_add "${1:-}" ;; + rm|remove|del) shift; cmd_rm "${1:-}" ;; + edit) cmd_edit ;; + log|logs) shift; cmd_log "${1:-}" ;; + *) cat >&2 < + list projects + running state (default) + add [path] add project (default: cwd) + rm remove project (comments it out) + edit \$EDITOR the list, then sync + sync force reconcile + log journal for one project +list file: $LIST +EOF + exit 1 ;; +esac diff --git a/home/yolo/bin/claude-rc-run b/home/yolo/bin/claude-rc-run new file mode 100755 index 0000000..505516d --- /dev/null +++ b/home/yolo/bin/claude-rc-run @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Launch a Claude Code Remote Control server for one project dir. +# Invoked by claude-rc@.service; arg is the project path. +set -euo pipefail + +dir="${1:?usage: claude-rc-run }" + +if [ ! -d "$dir" ]; then + echo "claude-rc-run: no such directory: $dir" >&2 + exit 78 # EX_CONFIG - systemd won't spin on Restart=on-failure +fi + +# --spawn worktree needs a git repo; fall back to same-dir if not one +spawn=worktree +git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || { + echo "claude-rc-run: $dir is not a git repo, using --spawn same-dir" >&2 + spawn=same-dir +} + +# Optional display name: a projects-file line may read +# /path/to/repo | My Display Name +# Falls back to the bare directory name. +LIST="${CLAUDE_RC_LIST:-$HOME/.config/claude-rc/projects}" +name=$(awk -F'|' -v t="$dir" ' + { line=$0; sub(/#.*/,"",line) + p=line; if (index(line,"|")) { p=substr(line,1,index(line,"|")-1) } + gsub(/^[[:space:]]+|[[:space:]]+$/,"",p); sub(/\/+$/,"",p) + if (p == t && index(line,"|")) { + n=substr(line,index(line,"|")+1) + gsub(/^[[:space:]]+|[[:space:]]+$/,"",n) + if (n != "") { print n; exit } + } + }' "$LIST" 2>/dev/null || true) +[ -n "$name" ] || name="$(basename "$dir")" + +cd "$dir" +exec claude remote-control \ + --spawn "$spawn" \ + --permission-mode bypassPermissions \ + --name "$name" diff --git a/home/yolo/default.nix b/home/yolo/default.nix new file mode 100644 index 0000000..34ea86d --- /dev/null +++ b/home/yolo/default.nix @@ -0,0 +1,36 @@ +{ + pkgs, + inputs, + ... +}: +{ + imports = [ + ../common/programs/default.nix + ../common/programs/desktop + ../common/programs/zen-browser.nix + ../common/programs/hyprland + ../common/programs/kitty + ../common/programs/gpg.nix + ../common/features/xdg.nix + ../common/features/downloads-cleanup.nix + ./monitors.nix + ./services.nix + ]; + + custom.hyprland.cursorSize = 32; + + # The agent session manager the hourlog/week-review timers drive. Was a + # hand-installed binary in ~/.local/bin on the Ubuntu box. + home.packages = [ inputs.agent-of-empires.packages.${pkgs.system}.default ]; + + # Blur and shadow cost a fullscreen pass per frame, and every frame here is + # also x264-encoded for the stream — on a virtio-gpu with no VirGL, in software. + wayland.windowManager.hyprland.extraConfig = '' + hl.config({ + decoration = { + blur = { enabled = false }, + shadow = { enabled = false }, + }, + }) + ''; +} diff --git a/home/yolo/monitors.nix b/home/yolo/monitors.nix new file mode 100644 index 0000000..95a3e33 --- /dev/null +++ b/home/yolo/monitors.nix @@ -0,0 +1,11 @@ +_: +{ + # Hyprland 0.55+ is Lua-only (see home/common/programs/hyprland). + # + # Matches every output rather than naming one: the virtio-gpu connector name + # varies by qemu display backend (Virtual-1 vs Virtual-0). Explicit mode, not + # `preferred` — this is the resolution Sunshine streams. + wayland.windowManager.hyprland.extraConfig = '' + hl.monitor({ output = "", mode = "2560x1440@60", position = "0x0", scale = 1 }) + ''; +} diff --git a/home/yolo/services.nix b/home/yolo/services.nix new file mode 100644 index 0000000..e7f8339 --- /dev/null +++ b/home/yolo/services.nix @@ -0,0 +1,193 @@ +{ + pkgs, + inputs, + ... +}: +# The user services this box exists to run, ported from hand-written units in +# ~/.config/systemd/user on the Ubuntu machine. +# +# NOT self-contained: every ExecStart under ~/.bun or ~/.local/bin is an +# imperatively-installed binary, and the WorkingDirectories are clones of +# separate repos. Nix owns the unit definitions here, nothing more. +let + # A user unit gets almost no PATH by default; these are the profile dirs the + # original units got for free from the system PATH on Ubuntu. + toolPath = "%h/.local/bin:%h/.nix-profile/bin:/etc/profiles/per-user/naps62/bin:/run/current-system/sw/bin"; + + sem = inputs.sem.packages.${pkgs.system}.default; + + # runtimeInputs is prepended to PATH, not a replacement, so `claude` still + # resolves from the unit's own PATH. + claude-rc-run = pkgs.writeShellApplication { + name = "claude-rc-run"; + runtimeInputs = with pkgs; [ + git + gawk + coreutils + ]; + text = builtins.readFile ./bin/claude-rc-run; + }; + + claude-rc = pkgs.writeShellApplication { + name = "claude-rc"; + runtimeInputs = with pkgs; [ + git + gawk + gnused + gnugrep + coreutils + systemd + ]; + text = builtins.readFile ./bin/claude-rc; + }; +in +{ + home.packages = [ + claude-rc + sem + pkgs.bun + ]; + + systemd.user.services = { + rev = { + Unit = { + Description = "rev — always-on local code review server"; + After = [ "network.target" ]; + }; + Service = { + Type = "simple"; + WorkingDirectory = "%h/tea/yolo/rev"; + # nodejs_26, not pkgs.nodejs: rev's package.json sets engines >=26 and + # the nixpkgs default is 24. + ExecStart = "${pkgs.nodejs_26}/bin/node server/index.ts"; + Environment = [ + "NODE_ENV=production" + "REV_ROOTS=%h" + "REV_DEPTH=3" + "REV_SEM_BIN=${sem}/bin/sem" + "PATH=${toolPath}" + ]; + Restart = "always"; + RestartSec = 2; + }; + Install.WantedBy = [ "default.target" ]; + }; + + rev-deploy = { + Unit = { + Description = "rev-deploy — Gitea webhook listener that deploys rev on push to main"; + After = [ "network.target" ]; + }; + Service = { + Type = "simple"; + WorkingDirectory = "%h/tea/yolo/rev"; + ExecStart = "${pkgs.bun}/bin/bun scripts/deploy-webhook.ts"; + EnvironmentFile = "%h/.config/rev/deploy.env"; + Environment = [ "PATH=${toolPath}" ]; + Restart = "always"; + RestartSec = 2; + }; + Install.WantedBy = [ "default.target" ]; + }; + + "claude-rc@" = { + Unit = { + Description = "Claude Code Remote Control (/%I)"; + Documentation = [ "https://code.claude.com/docs/en/remote-control" ]; + After = [ "network-online.target" ]; + Wants = [ "network-online.target" ]; + StopWhenUnneeded = false; + }; + Service = { + Type = "simple"; + # Leading "-": a missing dir must not be fatal, or systemd fails with + # 200/CHDIR before claude-rc-run can report the friendlier exit 78. + WorkingDirectory = "-/%I"; + ExecStart = "${claude-rc-run}/bin/claude-rc-run /%I"; + Environment = [ "PATH=${toolPath}" ]; + # `always`, not `on-failure`: a >10min outage times the session out and + # the process exits 0, which on-failure would not restart. + Restart = "always"; + RestartSec = 15; + # 78 = dir gone; 200 = systemd CHDIR failure. Without these, a deleted + # project dir restart-loops every 15s forever. + RestartPreventExitStatus = "78 200"; + StandardOutput = "append:%h/.local/state/claude-rc/%i.log"; + StandardError = "inherit"; + }; + Install.WantedBy = [ "default.target" ]; + }; + + claude-rc-sync = { + Unit.Description = "Reconcile Claude Remote Control servers with the project list"; + Service = { + Type = "oneshot"; + ExecStart = "${claude-rc}/bin/claude-rc sync"; + Environment = [ "PATH=${toolPath}" ]; + }; + }; + + hourlog = { + Unit = { + Description = "Start the Friday hour log in a tmux session"; + Documentation = [ "https://git.naps.pt/yolo/agent-skills" ]; + ConditionPathIsDirectory = "%h/tea/yolo/agent-skills"; + }; + Service = { + Type = "oneshot"; + ExecStart = "%h/tea/yolo/agent-skills/bin/hourlog-session.sh"; + Environment = [ "PATH=${toolPath}" ]; + # This unit may be what starts the tmux server; the default cgroup kill + # would take it back down as soon as ExecStart returns. + KillMode = "process"; + }; + }; + + week-review = { + Unit = { + Description = "Start the weekly agent-skills review in a tmux session"; + Documentation = [ "https://git.naps.pt/yolo/agent-skills" ]; + ConditionPathIsDirectory = "%h/tea/yolo/agent-skills"; + }; + Service = { + Type = "oneshot"; + ExecStart = "%h/tea/yolo/agent-skills/bin/week-review-session.sh"; + Environment = [ "PATH=${toolPath}" ]; + KillMode = "process"; + }; + }; + }; + + systemd.user.timers = { + hourlog = { + Unit.Description = "Friday hour log, 18:00 Europe/Lisbon"; + Timer = { + # Zone suffix pinned because the machine clock is UTC; keeps it at 18:00 + # wall time across DST. + OnCalendar = "Fri 18:00 Europe/Lisbon"; + Persistent = true; + AccuracySec = "1min"; + }; + Install.WantedBy = [ "timers.target" ]; + }; + + week-review = { + Unit.Description = "Weekly agent-skills review, Fridays 17:00 Europe/Lisbon"; + Timer = { + OnCalendar = "Fri 17:00 Europe/Lisbon"; + Persistent = true; + AccuracySec = "1min"; + }; + Install.WantedBy = [ "timers.target" ]; + }; + }; + + systemd.user.paths.claude-rc = { + Unit.Description = "Watch the Claude Remote Control project list for edits"; + Path = { + PathChanged = "%h/.config/claude-rc/projects"; + Unit = "claude-rc-sync.service"; + }; + Install.WantedBy = [ "default.target" ]; + }; +} diff --git a/hosts/common/features/remote-desktop.nix b/hosts/common/features/remote-desktop.nix new file mode 100644 index 0000000..b3ae751 --- /dev/null +++ b/hosts/common/features/remote-desktop.nix @@ -0,0 +1,31 @@ +_: +# Sunshine as a remote desktop. Same wlr-screencopy capture as +# features/gaming/sunshine.nix, minus its NVIDIA workarounds: no cudaSupport +# (virtio-gpu has no NVENC, so this software-encodes) and no uhid rule. +# +# First run: https://:47990 to set web-UI credentials, then pair Moonlight. +{ + services.sunshine = { + enable = true; + autoStart = true; + openFirewall = true; + capSysAdmin = false; # wlr-screencopy needs no CAP_SYS_ADMIN + + settings = { + # MUST be set. Auto-probe tries portalgrab first, and + # xdg-desktop-portal-hyprland implements no RemoteDesktop interface — the + # probe then hangs forever instead of falling back, so sunshine never + # binds its ports and the unit sits "active" doing nothing. + capture = "wlgrab"; + + # The web UI is only reachable over the network here — there is no local + # browser — and sunshine CSRF-rejects any origin but localhost unless it + # is listed. Both addresses: .250.1 now, .10.2 after the IP handover. + # + # Bare comma-separated, NOT a JSON array: sunshine splits this on commas + # and never strips brackets or quotes, so "[...]" makes every entry fail + # its starts_with("https://") check. + csrf_allowed_origins = "https://10.7.250.1:47990,https://10.7.10.2:47990"; + }; + }; +} diff --git a/hosts/yolo/default.nix b/hosts/yolo/default.nix new file mode 100644 index 0000000..9d7e297 --- /dev/null +++ b/hosts/yolo/default.nix @@ -0,0 +1,77 @@ +{ + lib, + pkgs, + ... +}: +{ + imports = [ + ./hardware-configuration.nix + ../common/global + ../common/features/user.nix + ../common/features/networking.nix + ../common/features/display + ../common/features/pipewire.nix + ../common/features/remote-desktop.nix + ../common/features/docker.nix + ../common/features/fonts.nix + ../common/features/nix-ld.nix + ../common/features/appimage.nix + ../common/features/home + ]; + + networking.hostName = "yolo"; + + # Lets the Proxmox host drive clean shutdowns/reboots and report the guest's + # IP. The qemu-guest profile in hardware-configuration.nix only sets up the + # virtio drivers; the agent itself is a separate service. + services.qemuGuest.enable = true; + + # Not a security downgrade: naps62 is in `docker`, which is already + # root-equivalent, and sshd is key-only — so the prompt guards nothing while + # blocking unattended rebuilds (nh shells out to sudo and needs a TTY). + security.sudo.wheelNeedsPassword = false; + + # Nix shells out to git for the `type = "git"` flake inputs (hyprland). + # Without it at system level, a fresh install cannot build the home config + # that would have provided git — so neither rebuild works. + environment.systemPackages = [ pkgs.git ]; + + # This clone lives under ~/tea, not the ~/projects path global/nix.nix assumes. + programs.nh.flake = lib.mkForce "/home/naps62/tea/naps62/nixos-config"; + + # No physical seat: Sunshine is a user service and cannot capture until a + # graphical session exists, so a cold boot must reach one unattended. No + # hyprlock on start (unlike konishi) — nobody could type the password in. + services.displayManager = { + autoLogin = { + enable = true; + user = "naps62"; + }; + defaultSession = "hyprland"; + }; + + # A Proxmox guest has no emulated sound card, so PipeWire starts with no sink + # and Sunshine has nothing to capture — the stream is silent. This sink is the + # only one, so it wins the default and Sunshine records its monitor. + services.pipewire.extraConfig.pipewire."10-null-sink" = { + "context.objects" = [ + { + factory = "adapter"; + args = { + "factory.name" = "support.null-audio-sink"; + "node.name" = "sunshine-sink"; + "node.description" = "Sunshine"; + "media.class" = "Audio/Sink"; + "audio.position" = "FL,FR"; + }; + } + ]; + }; + + # Pinned to 16 to match the cluster being migrated off Ubuntu; restoring that + # dump into a newer default would need a pg_upgrade first. + services.postgresql = { + enable = true; + package = pkgs.postgresql_16; + }; +} diff --git a/hosts/yolo/hardware-configuration.nix b/hosts/yolo/hardware-configuration.nix new file mode 100644 index 0000000..499e59d --- /dev/null +++ b/hosts/yolo/hardware-configuration.nix @@ -0,0 +1,52 @@ +{ + config, + lib, + modulesPath, + ... +}: + +# Proxmox guest (VM 132), q35 + OVMF. Hand-written rather than generated: +# filesystems are matched by label, set when the disk is partitioned, so this +# stays valid if the VM is ever rebuilt from scratch. +{ + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot = { + initrd.availableKernelModules = [ + "ahci" + "xhci_pci" + "virtio_pci" + "virtio_scsi" + "sd_mod" + "sr_mod" + ]; + # In the initrd, not kernelModules: otherwise there is no DRM device until + # late boot and SDDM races it. + initrd.kernelModules = [ "virtio_gpu" ]; + kernelModules = [ "kvm-intel" ]; + extraModulePackages = [ ]; + }; + + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0077" + "dmask=0077" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/swap"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +}