diff --git a/.claude/commands/nix-add.md b/.claude/commands/nix-add.md new file mode 100644 index 00000000..ac7ca525 --- /dev/null +++ b/.claude/commands/nix-add.md @@ -0,0 +1,22 @@ +--- +description: Add a package, app, alias or env var to the right place in nix-config +argument-hint: [machine] +allowed-tools: Bash(nix search:*), Bash(brew search:*), Bash(brew info:*) +--- + +Add `$ARGUMENTS` to this nix-darwin repo. + +1. Find it: `nix search nixpkgs ` for CLI tools; GUI apps go through Homebrew casks (`brew search --cask `). Prefer nixpkgs for CLI tools, casks for GUI apps. If several matches, ask which one. +2. Pick the file from README "Where to add things": + | What | Where | + |---|---| + | GUI app | `homebrew.casks` in `hosts/common/darwin/homebrew.nix` | + | CLI tool, system-wide | `environment.systemPackages` in `hosts/common/common-packages.nix` | + | CLI tool, personal | `home.packages` in `home-manager/profiles/base.nix` | + | Shell alias | `programs.zsh.shellAliases` in `home-manager/profiles/base.nix` | + | Env var | `home.sessionVariables` in `home-manager/profiles/base.nix` | + | One machine only | `hosts/darwin//default.nix` or `home-manager/profiles/.nix` | + If a `home-manager/programs/.nix` module exists or the tool has a `programs.` home-manager option with config worth declaring, use that instead. +3. Check it isn't already declared (grep the repo). Keep the list's existing order (alphabetical if it is). +4. Secrets (tokens, keys, passwords) never go in `.nix` files: tell the user to add them with `nix run .#edit-secrets -- ` and reference them through sops-nix. +5. Run `/nix-check`. diff --git a/.claude/commands/nix-check.md b/.claude/commands/nix-check.md new file mode 100644 index 00000000..d1d88eee --- /dev/null +++ b/.claude/commands/nix-check.md @@ -0,0 +1,16 @@ +--- +description: Validate nix-config changes locally (mirrors CI) without switching +allowed-tools: Bash(nix run .#scan-keys), Bash(nixfmt --check:*), Bash(nix flake check:*), Bash(nix build:*), Bash(git ls-files:*), Bash(git diff:*), Bash(scutil --get LocalHostName) +--- + +Validate the current working tree of this nix-darwin repo. Run each step, stop at the first failure, and report the real error output. + +0. Secrets: `nix run .#scan-keys`. This repo is public. On failure, list the file names only, never print or read the key/secret contents, and tell the user to remove the file and rotate the key if it was ever committed. Public keys (`*.pub`, age recipients in `.sops.yaml`) are fine. +1. Format: `nixfmt --check` on changed/new `.nix` files only (`git ls-files -m -o --exclude-standard '*.nix'`). Some untouched files are not formatted yet; don't flag those. +2. Eval: `nix flake check --no-build` +3. Build current host: `nix build ".#darwinConfigurations.$(scutil --get LocalHostName).system" --no-link` + - If the change touches `hosts/common/`, `lib/`, `flake.nix`, `flake.lock` or `home-manager/profiles/base.nix`, also build the other host (`mac-pro` / `flow48`) — CI builds both. + +Never run `darwin-rebuild switch`, `nix run .#rebuild` or anything with `sudo`. On success, tell the user to apply with `nix run .#rebuild`. + +$ARGUMENTS diff --git a/.claude/commands/nix-diff.md b/.claude/commands/nix-diff.md new file mode 100644 index 00000000..a59e3f8c --- /dev/null +++ b/.claude/commands/nix-diff.md @@ -0,0 +1,12 @@ +--- +description: Show what would change vs the running system before switching +allowed-tools: Bash(nix build:*), Bash(nix store diff-closures:*), Bash(scutil --get LocalHostName) +--- + +1. Build the current host: `nix build ".#darwinConfigurations.$(scutil --get LocalHostName).system" --no-link --print-out-paths` +2. Diff against the running system: `nix store diff-closures /run/current-system ` +3. Summarize: version bumps, added/removed packages, notable size changes. Flag major version jumps and anything security-relevant. Skip noise (`-man`, `-doc`, unchanged-version rebuilds). + +Never switch. Tell the user to apply with `nix run .#rebuild` if it looks good. + +$ARGUMENTS diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3779daee..b0047638 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -4,14 +4,20 @@ on: push: branches: [main] pull_request: + # update-flake.yml dispatches this: PRs opened with GITHUB_TOKEN don't trigger pull_request + workflow_dispatch: + +permissions: + contents: read jobs: flake-check: runs-on: macos-14 steps: - - uses: actions/checkout@v5 - - uses: DeterminateSystems/nix-installer-action@main + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23 - run: nix flake check --no-build + - run: nix run .#scan-keys build: runs-on: macos-14 @@ -19,6 +25,6 @@ jobs: matrix: host: [mac-pro, flow48] steps: - - uses: actions/checkout@v5 - - uses: DeterminateSystems/nix-installer-action@main + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23 - run: nix build ".#darwinConfigurations.${{ matrix.host }}.system" --no-link diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index b5077dcd..11a5eebc 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -6,19 +6,27 @@ on: workflow_dispatch: permissions: - contents: write - pull-requests: write + contents: read jobs: update: runs-on: macos-14 + permissions: + contents: write + pull-requests: write + actions: write # dispatch check.yml on the PR branch steps: - - uses: actions/checkout@v5 - - uses: DeterminateSystems/nix-installer-action@main + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23 - run: nix flake update - - uses: peter-evans/create-pull-request@v7 + - id: pr + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: title: "chore: update flake inputs" commit-message: "chore: update flake inputs" branch: update-flake-inputs delete-branch: true + - if: steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated' + run: gh workflow run check.yml --ref update-flake-inputs + env: + GH_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 2e97eece..215a2d98 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,16 @@ result .idea secrets/**/*.yaml !secrets/**/*.enc.yaml -.claude +.claude/settings.local.json .DS_Store + +# private keys — never commit (public keys like *.pub / .sops.yaml recipients are fine) +*.pem +*.key +id_rsa +id_ed25519 +id_ecdsa +keys.txt + +# backup_flashspace.sh local copies +home-manager/programs/flashspace/backup_*.yaml diff --git a/README.md b/README.md index da15a15f..11df16d8 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ nix-config/ ├── secrets/ │ ├── /secrets.enc.yaml # Encrypted machine secrets │ └── secrets_example.yaml # Template -└── templates/ # Flake templates (node, esp32) +└── templates/ # Flake templates (node, esp32, api-rust) ``` --- @@ -226,7 +226,9 @@ cp secrets/secrets_example.yaml /tmp/secrets.yaml Edit `/tmp/secrets.yaml` — paste the contents of `~/.ssh/id_ed25519` as the `ssh_key` value. ```sh -nix shell nixpkgs#sops --command sops --encrypt /tmp/secrets.yaml > secrets//secrets.enc.yaml +nix shell nixpkgs#sops --command sops --encrypt \ + --filename-override secrets//secrets.enc.yaml \ + --output secrets//secrets.enc.yaml /tmp/secrets.yaml rm /tmp/secrets.yaml # never commit the unencrypted file ``` @@ -366,6 +368,7 @@ The `,` command is especially useful — e.g. `, ffmpeg -i video.mp4 output.gif` mkdir -p ~/Developer/my-app && cd ~/Developer/my-app tpl-node # Node.js (pnpm, typescript) tpl-esp32 # ESP32-S3 Rust project +tpl-api-rust # Rust API (Diesel, PostgreSQL) direnv allow # load dev environment ``` @@ -494,7 +497,9 @@ ssh_key: | Encrypt it: ```sh -nix shell nixpkgs#sops --command sops --encrypt /tmp/secrets.yaml > secrets//secrets.enc.yaml +nix shell nixpkgs#sops --command sops --encrypt \ + --filename-override secrets//secrets.enc.yaml \ + --output secrets//secrets.enc.yaml /tmp/secrets.yaml rm /tmp/secrets.yaml ``` diff --git a/flake.lock b/flake.lock index 0d3214e1..237e6bb6 100644 --- a/flake.lock +++ b/flake.lock @@ -25,11 +25,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1791482525, - "narHash": "sha256-qaXMGWyEx1VqHiLPyCRHV4ss6WOvigq6YPf2AWGQ05I=", + "lastModified": 1791736119, + "narHash": "sha256-e6V9dNQEfoJu1MPHb6ALx3ZJIWUOp8dj2Sn/NYgMJ54=", "owner": "sadjow", "repo": "claude-code-nix", - "rev": "2c1b920992b63bc3ecda75dae2ce8148867f7eda", + "rev": "7a91b64f9378dcff2180dc35b9ba033754d93b0a", "type": "github" }, "original": { @@ -45,11 +45,11 @@ ] }, "locked": { - "lastModified": 1791484883, - "narHash": "sha256-iKxFHJrg7Sltwhq3ssCZYQ4CFEDVvReVVuVuO9dj3sE=", + "lastModified": 1791642191, + "narHash": "sha256-0bK0A59rVUn1SRZzo4cNi4VSqAe7vR1gL2/MZl53Frw=", "owner": "nix-community", "repo": "home-manager", - "rev": "dfadbe5162d5e86bc0808badec8f346f81b4b3f0", + "rev": "c58f54c733a7894d84fa2fdff26cc12088c4b78c", "type": "github" }, "original": { @@ -93,11 +93,11 @@ "homebrew-cask": { "flake": false, "locked": { - "lastModified": 1791488243, - "narHash": "sha256-UXOH6aAw4cM90aw6ohrXai3qm2I9lygh9iR/SJ9fRdo=", + "lastModified": 1791815938, + "narHash": "sha256-/XnZr+j+Rdv9Y/TL1baL9OV37cuSmAPDPg3MpeRBIjo=", "owner": "homebrew", "repo": "homebrew-cask", - "rev": "6f60c242710e4afa77986047c64acc98283dcd49", + "rev": "0dd592ec4537bfcf577776ccd517f915133f76c7", "type": "github" }, "original": { @@ -106,14 +106,30 @@ "type": "github" } }, + "homebrew-claude-usage": { + "flake": false, + "locked": { + "lastModified": 1788006789, + "narHash": "sha256-ZWU1WFGzc+NtNSmLA0mw6Nc1cWyR+B6XPbaEJzAYmls=", + "owner": "hamed-elfayome", + "repo": "homebrew-claude-usage", + "rev": "949e2a7938382b295bf5aef064da32423255a4b9", + "type": "github" + }, + "original": { + "owner": "hamed-elfayome", + "repo": "homebrew-claude-usage", + "type": "github" + } + }, "homebrew-core": { "flake": false, "locked": { - "lastModified": 1791488215, - "narHash": "sha256-ehLeDrh1c1ntrOK8TGh5IJQ2WuInhzAhHqIcsj90NvU=", + "lastModified": 1791816329, + "narHash": "sha256-vJ/bgQ9xfPcRwLHKGNsW8au+7eO8oc3+MwqEtluVRwI=", "owner": "homebrew", "repo": "homebrew-core", - "rev": "c3dae98b472086d8388597d9c391ad3cfb6314ba", + "rev": "6237ff7f7c694a39d6b07e0bb8ea9197a2a908fe", "type": "github" }, "original": { @@ -157,11 +173,11 @@ "homebrew-tinycast": { "flake": false, "locked": { - "lastModified": 1791321749, - "narHash": "sha256-uYp9H40UP3gQn31N8S0Vy+2lJmeLQkIOjTUf+lCSa2Q=", + "lastModified": 1791646289, + "narHash": "sha256-GSG018gaRrzd0iycO3cM2rNPpamTXwt1Z5AlQG/kcZo=", "owner": "abue-ammar", "repo": "homebrew-tinycast", - "rev": "7786e9ee7b37628bfbc51583eaaef9234f912a4d", + "rev": "3a9a0fa4de664a0aa2b34880e71626dd55309a38", "type": "github" }, "original": { @@ -170,6 +186,22 @@ "type": "github" } }, + "mattpocock-skills": { + "flake": false, + "locked": { + "lastModified": 1791543409, + "narHash": "sha256-NljCSZNI3sZ+vbCw7pRdrwR7try4BxOFp4gdjuWYtzs=", + "owner": "mattpocock", + "repo": "skills", + "rev": "49dd158d1076134a641b33efb035946536778336", + "type": "github" + }, + "original": { + "owner": "mattpocock", + "repo": "skills", + "type": "github" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -210,11 +242,11 @@ }, "nixpkgs-darwin": { "locked": { - "lastModified": 1791456040, - "narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=", + "lastModified": 1791594108, + "narHash": "sha256-XK4KceonkwD8LuMAt/vNrz982mXJM5jeHAxMwsUXXAQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8", + "rev": "6510408d147d8c5e6d76043e899e9251756f6a75", "type": "github" }, "original": { @@ -231,10 +263,12 @@ "homebrew-artginzburg-tap": "homebrew-artginzburg-tap", "homebrew-bundle": "homebrew-bundle", "homebrew-cask": "homebrew-cask", + "homebrew-claude-usage": "homebrew-claude-usage", "homebrew-core": "homebrew-core", "homebrew-macshot": "homebrew-macshot", "homebrew-switch": "homebrew-switch", "homebrew-tinycast": "homebrew-tinycast", + "mattpocock-skills": "mattpocock-skills", "nix-darwin": "nix-darwin", "nix-homebrew": "nix-homebrew", "nixpkgs": [ @@ -252,11 +286,11 @@ ] }, "locked": { - "lastModified": 1791448976, - "narHash": "sha256-HJ4TEF3j7HmLYeEP/PtqjeiBX/6TppXB2qY53HcrWbs=", + "lastModified": 1791795448, + "narHash": "sha256-12C5ef8a3endLXBvLOVR/jynM9EHGgP1t+y5uBorwx4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "f91010a5c1b6125b715eb78a84fb5dd8fb74a2b0", + "rev": "404b7a5537f0adc6e5c558a1b94612e683dec504", "type": "github" }, "original": { @@ -272,11 +306,11 @@ ] }, "locked": { - "lastModified": 1791103873, - "narHash": "sha256-nFxM+pKoZ8LJAEnUXARyCaOAloWgaW9kZQOSjWzKcTE=", + "lastModified": 1791710405, + "narHash": "sha256-mVD4XIc5JkqMW2K04IkFUzHgHmyJZlzw5ktfgMDCbKU=", "owner": "Mic92", "repo": "sops-nix", - "rev": "dcd241ba97088c22569d1573286e1b9daad340c0", + "rev": "ee48786c6f9160d2c1a084aac841f68195b8420c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 9bcec3a3..c1c48738 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,16 @@ flake = false; }; + mattpocock-skills = { + url = "github:mattpocock/skills"; + flake = false; + }; + + homebrew-claude-usage = { + url = "github:hamed-elfayome/homebrew-claude-usage"; + flake = false; + }; + # Secrets management sops-nix.url = "github:Mic92/sops-nix"; sops-nix.inputs.nixpkgs.follows = "nixpkgs"; @@ -116,7 +126,7 @@ MACHINE="''${1:-}" if [ -z "$MACHINE" ]; then echo "Usage: nix run .#edit-secrets -- " - echo "Available: $(ls secrets/ | grep -v secrets_example | tr '\n' ' ')" + echo "Available: $(cd secrets && printf '%s ' */ | tr -d /)" exit 1 fi @@ -128,17 +138,21 @@ exit 1 fi - TMP=$(mktemp /tmp/secrets-XXXXXX.yaml) - trap 'rm -f "$TMP"' EXIT - - ${pkgs.sops}/bin/sops --decrypt "$SECRETS_FILE" > "$TMP" - ''${EDITOR:-nano} "$TMP" - ${pkgs.sops}/bin/sops --encrypt "$TMP" > "$SECRETS_FILE" + # sops edits via a private temp file and only re-encrypts on a clean exit + rc=0 + EDITOR="''${EDITOR:-nano}" ${pkgs.sops}/bin/sops "$SECRETS_FILE" || rc=$? + # 200 = sops "file has not changed" + if [ "$rc" -eq 200 ]; then + echo "No changes for $MACHINE" + exit 0 + fi + [ "$rc" -eq 0 ] || exit "$rc" echo "Secrets updated for $MACHINE" ''; check-secrets = mkApp "check-secrets" '' MACHINE="''${1:-$(scutil --get LocalHostName)}" - SECRETS_FILE="secrets/$MACHINE/secrets.enc.yaml" + FLAKE=$(git rev-parse --show-toplevel 2>/dev/null || pwd) + SECRETS_FILE="$FLAKE/secrets/$MACHINE/secrets.enc.yaml" AGE_KEY="$HOME/.config/sops/age/keys.txt" if [ ! -f "$SECRETS_FILE" ]; then @@ -151,13 +165,39 @@ exit 1 fi - if ${pkgs.sops}/bin/sops --decrypt "$SECRETS_FILE" > /dev/null; then + if SOPS_AGE_KEY_FILE="$AGE_KEY" ${pkgs.sops}/bin/sops --decrypt "$SECRETS_FILE" > /dev/null; then echo "OK: $SECRETS_FILE decrypts cleanly with $AGE_KEY" else echo "Error: failed to decrypt $SECRETS_FILE" exit 1 fi ''; + # Fails if a private key or plaintext secret is in the tree (tracked, incl. gitignored-but-tracked, + untracked) + scan-keys = mkApp "scan-keys" '' + cd "$(git rev-parse --show-toplevel)" + fail=0 + + # -l: print filenames only, never the key itself + hits=$(git ls-files -z -co --exclude-standard -- ':!secrets/secrets_example.yaml' \ + | xargs -0 grep -lsIE \ + -e '-----BEGIN ([A-Z]+ )?PRIVATE KEY-----' \ + -e 'AGE-SECRET-KEY-1[0-9A-Z]{20,}' || true) + if [ -n "$hits" ]; then + echo "$hits" + echo "Error: private key material in the files above" + fail=1 + fi + + for f in $(git ls-files -co --exclude-standard 'secrets/*/*.yaml'); do + if ! grep -q '^sops:' "$f"; then + echo "Error: $f is not sops-encrypted" + fail=1 + fi + done + + [ "$fail" -eq 0 ] && echo "OK: no private keys or plaintext secrets" + exit "$fail" + ''; }; # Bootstrap env for fresh machines: nix develop diff --git a/home-manager/modules/iterm2/default.nix b/home-manager/modules/iterm2/default.nix index dc41aed0..12bc22bc 100644 --- a/home-manager/modules/iterm2/default.nix +++ b/home-manager/modules/iterm2/default.nix @@ -412,8 +412,7 @@ let "Columns" = profile.window.columns; "Rows" = profile.window.rows; - "Transparency" = - if profile.transparency.enable then profile.transparency.value else 0.0; + "Transparency" = if profile.transparency.enable then profile.transparency.value else 0.0; "Blur" = profile.blur.enable; "Blur Radius" = if profile.blur.enable then profile.blur.value else 0.0; @@ -525,24 +524,24 @@ in home.activation = mkMerge [ (mkIf cfg.copyApplications { copyITerm2ToApplications = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - if [ -d "$HOME/Applications/iTerm2.app" ] && cmp -s "${cfg.package}/Applications/iTerm2.app/Contents/Info.plist" "$HOME/Applications/iTerm2.app/Contents/Info.plist" 2>/dev/null; then - echo "❯❯❯❯ · iTerm2.app already up to date, skipping." - else - if [ -d "$HOME/Applications/iTerm2.app" ]; then - echo "❯❯❯❯ · Removing existing iTerm2.app..." - chmod -R +w "$HOME/Applications/iTerm2.app" || true - $DRY_RUN_CMD rm -rf "$HOME/Applications/iTerm2.app" || { - echo "❯❯❯❯ ✗ Failed to remove existing iTerm2.app, trying alternative method..." - $DRY_RUN_CMD find "$HOME/Applications/iTerm2.app" -type f -exec chmod 644 {} \; || true - $DRY_RUN_CMD find "$HOME/Applications/iTerm2.app" -type d -exec chmod 755 {} \; || true - $DRY_RUN_CMD rm -rf "$HOME/Applications/iTerm2.app" - } - fi - - echo "❯❯❯❯ · Copying iTerm2.app to $HOME/Applications..." - $DRY_RUN_CMD mkdir -p "$HOME/Applications" - $DRY_RUN_CMD cp -R "${cfg.package}/Applications/iTerm2.app" "$HOME/Applications/iTerm2.app" - fi + if [ -d "$HOME/Applications/iTerm2.app" ] && cmp -s "${cfg.package}/Applications/iTerm2.app/Contents/Info.plist" "$HOME/Applications/iTerm2.app/Contents/Info.plist" 2>/dev/null; then + echo "❯❯❯❯ · iTerm2.app already up to date, skipping." + else + if [ -d "$HOME/Applications/iTerm2.app" ]; then + echo "❯❯❯❯ · Removing existing iTerm2.app..." + chmod -R +w "$HOME/Applications/iTerm2.app" || true + $DRY_RUN_CMD rm -rf "$HOME/Applications/iTerm2.app" || { + echo "❯❯❯❯ ✗ Failed to remove existing iTerm2.app, trying alternative method..." + $DRY_RUN_CMD find "$HOME/Applications/iTerm2.app" -type f -exec chmod 644 {} \; || true + $DRY_RUN_CMD find "$HOME/Applications/iTerm2.app" -type d -exec chmod 755 {} \; || true + $DRY_RUN_CMD rm -rf "$HOME/Applications/iTerm2.app" + } + fi + + echo "❯❯❯❯ · Copying iTerm2.app to $HOME/Applications..." + $DRY_RUN_CMD mkdir -p "$HOME/Applications" + $DRY_RUN_CMD cp -R "${cfg.package}/Applications/iTerm2.app" "$HOME/Applications/iTerm2.app" + fi ''; }) @@ -595,6 +594,7 @@ in ]; xdg.configFile."iTerm2/com.googlecode.iterm2.plist".source = - (pkgs.formats.plist { }).generate "com.googlecode.iterm2.plist" iTerm2PlistContent; + (pkgs.formats.plist { }).generate "com.googlecode.iterm2.plist" + iTerm2PlistContent; }; } diff --git a/home-manager/profiles/base.nix b/home-manager/profiles/base.nix index 6eb41314..032bf75e 100644 --- a/home-manager/profiles/base.nix +++ b/home-manager/profiles/base.nix @@ -25,12 +25,11 @@ in ../programs/rust.nix ../programs/nix.nix ../programs/node.nix + ../programs/claude.nix ]; home.packages = with pkgs; [ neovim - claude-code - nightlight openjdk25 ]; @@ -59,7 +58,7 @@ in User = "${username}"; AddKeysToAgent = "yes"; UseKeychain = "yes"; - StrictHostKeyChecking = "no"; + StrictHostKeyChecking = "accept-new"; }; }; }; @@ -127,6 +126,7 @@ in tpl-node = "nix flake init -t 'github:utopiaeh/nix-config#node'"; tpl-esp32 = "nix flake init -t 'github:utopiaeh/nix-config#esp32-rust'"; + tpl-api-rust = "nix flake init -t 'github:utopiaeh/nix-config#api-rust'"; }; initContent = '' diff --git a/home-manager/profiles/flow48.nix b/home-manager/profiles/flow48.nix index 12aa43fd..28637724 100644 --- a/home-manager/profiles/flow48.nix +++ b/home-manager/profiles/flow48.nix @@ -10,9 +10,11 @@ # AWS Amplify CLI has no Nix package — installed via npm on first activation. # To remove: npm uninstall -g @aws-amplify/cli home.activation.installAmplifyCli = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - if ! command -v amplify >/dev/null 2>&1; then + # activation PATH lacks ~/.npm-global/bin, so check the binary directly + if [ ! -x "$HOME/.npm-global/bin/amplify" ]; then export NPM_CONFIG_PREFIX="$HOME/.npm-global" - ${pkgs.nodejs}/bin/npm install -g --fund=false @aws-amplify/cli + ${pkgs.nodejs}/bin/npm install -g --fund=false @aws-amplify/cli@14.5.1 \ + || echo "warning: amplify install failed (offline?), retrying next switch" fi ''; } diff --git a/home-manager/programs/claude.nix b/home-manager/programs/claude.nix new file mode 100644 index 00000000..4d277ce0 --- /dev/null +++ b/home-manager/programs/claude.nix @@ -0,0 +1,112 @@ +{ lib, inputs, ... }: +{ + programs.claude-code = { + enable = true; # package = pkgs.claude-code (claude-code overlay) + + # Claude and /plugin still write settings.json; declared keys are merged in on switch + mutableSettings = true; + + settings = { + # Marketplaces + enabled plugins: Claude installs/updates them itself on startup + extraKnownMarketplaces = { + claude-plugins-official.source = { + source = "github"; + repo = "anthropics/claude-plugins-official"; + }; + caveman.source = { + source = "github"; + repo = "JuliusBrussee/caveman"; + }; + ponytail.source = { + source = "github"; + repo = "DietrichGebert/ponytail"; + }; + }; + enabledPlugins = { + "caveman@caveman" = true; # SessionStart hook → caveman every session + "ponytail@ponytail" = true; # SessionStart hook → ponytail every session + "feature-dev@claude-plugins-official" = true; + "figma@claude-plugins-official" = true; + }; + + permissions = { + # Hard block, CLAUDE.md rules are only advisory + deny = [ + # no commits/pushes/PRs (also covers push --force) + "Bash(git commit:*)" + "Bash(git push:*)" + # "merge *" (word boundary) so `git merge-base` stays allowed + "Bash(git merge *)" + # global flags before the subcommand (git -C dir commit, git -c k=v push) + "Bash(git * commit *)" + "Bash(git * push *)" + "Bash(git * merge *)" + "Bash(gh pr create:*)" + "Bash(gh pr merge:*)" + # destructive + "Bash(git reset --hard:*)" + "Bash(git clean:*)" + # secrets (Read tool only; shell reads like `cat .env` are not covered) + "Read(**/.env*)" + "Read(**/*.pem)" + "Read(**/*.key)" + "Read(~/.ssh/**)" + "Read(~/.aws/**)" + "Read(~/.config/sops/**)" + "Read(~/.gnupg/**)" + "Read(**/secrets/**)" + ]; + + # Always prompt, even in auto/accept-edits mode + ask = [ + "Bash(rm -rf:*)" + "Bash(rm -fr:*)" + ]; + + # From /fewer-permission-prompts: frequent read-only / check commands + allow = [ + "Bash(yarn verify)" + "Bash(yarn lint)" + "Bash(yarn tsc)" + "Bash(yarn tsc --noEmit *)" + "Bash(yarn -s tsc --noEmit *)" + "Bash(yarn test:run)" + "Bash(yarn vitest run)" + "Bash(yarn vitest run *)" + "Bash(docker compose ps)" + "Bash(docker compose ps *)" + "Bash(brew list *)" + "mcp__claude_ai_Atlassian_MCP__getJiraIssue" + "mcp__claude_ai_Figma__get_design_context" + "mcp__claude_ai_Figma__get_metadata" + ]; + }; + + statusLine = { + type = "command"; + command = ''bash "$HOME/.claude/hooks/caveman-statusline.sh"''; + }; + effortLevel = "medium"; + theme = "dark"; + tui = "fullscreen"; + }; + + # Global rules → ~/.claude/CLAUDE.md + context = ./claude/CLAUDE.md; + + # mattpocock/skills pinned via flake input + skills = + lib.mapAttrs (_: path: "${inputs.mattpocock-skills}/skills/${path}") { + code-review = "engineering/code-review"; + diagnosing-bugs = "engineering/diagnosing-bugs"; + grill-me = "productivity/grill-me"; + grill-with-docs = "engineering/grill-with-docs"; + grilling = "productivity/grilling"; + prototype = "engineering/prototype"; + tdd = "engineering/tdd"; + } + // { + java-coach = ./claude/skills/java-coach; + }; + }; +} diff --git a/home-manager/programs/claude/CLAUDE.md b/home-manager/programs/claude/CLAUDE.md new file mode 100644 index 00000000..bc3bd4e0 --- /dev/null +++ b/home-manager/programs/claude/CLAUDE.md @@ -0,0 +1,27 @@ +# Global rules + + + +## Git + +- Never `git commit`, `git push`, merge, or open/merge PRs. Stage nothing unless asked. The user commits and pushes. +- Leave the working tree for the user to review: summarize what changed and suggest a commit message instead. + +## Comments + +- No comments in code that restate what the code does. Only keep a comment when the *why* is non-obvious (workaround, constraint, surprising behavior) or it is a `ponytail:` marker. +- Explanations, context, and reasoning belong in the PR description or PR review comments, not in the code. When useful, draft that text for the user. + +## When a task is done + +1. Run the project's own validation — only what the project actually has. Discover it from `package.json` scripts, `Makefile`, `justfile`, `flake.nix` checks, `pyproject.toml`, `Cargo.toml`, CI config, etc. Typical order: format check (prettier, nixfmt, ...), lint, typecheck, tests. + - Use the package manager matching the lockfile (pnpm / yarn / npm / bun). + - Prefer check-only modes (e.g. `prettier --check`) unless the project's formatter is expected to be applied. + - Report failures with the real output. Never claim green without running it. +2. Review the diff for bugs, security issues, and convention breaks (use `/code-review` or the `feature-dev:code-reviewer` agent for non-trivial changes). +3. If the review finds bugs: list them (location, problem, impact) and **ask before fixing** — do not fix silently. + +## Choosing an approach + +- For bug fixes, features, and refactors where more than one reasonable approach exists, present 2–4 options with trade-offs via `AskUserQuestion`, best-practice option first marked "(Recommended)", and let the user pick. +- Skip the question when there is one obvious correct fix or the change is trivial. diff --git a/home-manager/programs/claude/skills/java-coach/SKILL.md b/home-manager/programs/claude/skills/java-coach/SKILL.md new file mode 100644 index 00000000..dc380186 --- /dev/null +++ b/home-manager/programs/claude/skills/java-coach/SKILL.md @@ -0,0 +1,72 @@ +--- +name: java-coach +description: Coach the user through learning Java 25 by building their own project by hand. The user writes the code; Claude guides, reviews, and unblocks. The user is an experienced TypeScript developer. +disable-model-invocation: true +--- + +# Java coach + +The user is learning Java 25 by building a real project (a gym app). They are an experienced **TypeScript** developer: skip programming basics, teach the *Java way* and map it to what they already know. + +## Prime rule: the user types the code + +- **Never write or edit the user's source files** in this mode. No `Edit`/`Write` on their code unless they explicitly say "write it for me" / "show me the solution". +- Read their code freely (Read/Grep/Bash read-only) to understand where they are. +- You may run their build/tests (`./gradlew test`, `./mvnw test`, `java File.java`) to show them real output. +- Exception: on request, write a **failing test** as an exercise target. Never the implementation. + +## Overrides (this mode wins) + +While this skill is active it takes precedence over other session instructions that conflict with it: + +- **ponytail**: ignore its "code first, ≤3 lines" output rule and its push toward writing the shortest solution. Here the goal is the user's understanding, not the smallest diff. Its taste (simple, stdlib-first, no over-engineering) still applies to *review feedback*. +- **caveman**: don't compress teaching. Explanations, TS → Java comparisons, and reviews are written in clear, normal sentences. Keep them short, but no fragments or dropped words. Plain status lines (e.g. "tests pass") can stay terse. +- **CLAUDE.md "when a task is done"**: run checks and review as usual, but report findings for the *user* to fix. Never offer to fix them yourself. +- **CLAUDE.md "choosing an approach"**: still applies — offer options when picking the next step or a design. + +## Session loop + +1. **Orient.** Read `LEARNING.md` at the project root (create it on the first session — the only file you write). Read the relevant code. Say in 2–3 lines where they are. +2. **Pick the next step** with the user: one small, shippable piece of the gym app (e.g. "a `Member` record", "list workouts from a REST endpoint"). Each step teaches 1–2 Java concepts, no more. +3. **Brief.** Explain the concept(s) for this step with a TS → Java comparison, then state the task: what to build, which file, how they'll know it works (a test, a curl, console output). No solution code. +4. **They build.** Wait. Answer questions. +5. **Review** when they say done: run it, then give feedback — correctness first, then idiomatic Java (what a senior Java dev would change and *why*). Ask them to apply fixes themselves. +6. **Log** in `LEARNING.md`: step done, concepts learned, open questions, next step candidates. + +## When they're stuck: hint ladder + +Climb one rung at a time. Only go up when they ask again or are clearly still stuck. + +1. **Question** — a Socratic nudge toward the concept ("what type does `stream().map()` return?"). +2. **Pointer** — the exact API / Javadoc / JEP to look at, and the TS equivalent. +3. **Shape** — pseudocode or a signature with the body left out. +4. **Skeleton** — code with `// TODO` blanks for the key lines. +5. **Solution** — only when they explicitly ask. Then have them retype it and explain it back. + +Read the compiler/test error *with* them: teach how to read Java stack traces and javac messages, don't just fix. + +## TS → Java mapping to lean on + +| TypeScript | Java 25 | +|---|---| +| `type`/`interface` data shape | `record` | +| union type / discriminated union | `sealed interface` + `record` permits, exhaustive `switch` | +| `switch` on `kind` | pattern matching `switch` with record deconstruction | +| `T \| undefined` | `Optional` for return values; never `null` for collections | +| `Array.map/filter/reduce` | `Stream` API, `.toList()` | +| `Promise` / `async`-`await` | virtual threads (`Thread.ofVirtual()`, `Executors.newVirtualThreadPerTaskExecutor()`) — plain blocking code | +| `readonly` / `as const` | `final`, records, `List.of()` (immutable) | +| structural typing | nominal typing — implements must be explicit | +| `npm` + `package.json` | Gradle (`build.gradle.kts`) or Maven (`pom.xml`) | +| `vitest`/`jest` | JUnit 5 + AssertJ | +| `throw` anything | checked vs unchecked exceptions | +| `const _ = ...` | unnamed variables `_` | + +Java 25 features worth teaching when they fit naturally: compact source files & instance `main` (JEP 512), module import declarations (JEP 511), flexible constructor bodies (JEP 513), scoped values (JEP 506), records/sealed types/pattern matching (finalized earlier). Mark **preview** features as preview (`--enable-preview`) and don't make the project depend on them. + +## Style + +- Short explanations, one concept at a time, concrete to *their* code. +- Explain *why* Java does it this way when it differs from TS (e.g. nominal types, checked exceptions, JVM startup). +- Point out common TS-dev traps: `==` vs `.equals()`, mutable default collections, `null` everywhere, overusing `Optional` as a field type, forgetting `final`. +- Encourage using IntelliJ features (inspections, quick-fix preview, debugger) — but they should understand a quick-fix before accepting it. diff --git a/home-manager/programs/flashspace/backup_flashspace.sh b/home-manager/programs/flashspace/backup_flashspace.sh index 7614f9eb..d8aba3d8 100644 --- a/home-manager/programs/flashspace/backup_flashspace.sh +++ b/home-manager/programs/flashspace/backup_flashspace.sh @@ -4,7 +4,7 @@ set -euo pipefail # Exit on errors, undefined variables, or failed pipes # Define paths FLASHSAPCE_PATH="$HOME/.config/flashspace" -DEST_PATH="/Users/$USER/nix-config/home-manager/programs/flashspace" +DEST_PATH="${NIX_CONFIG_DIR:-$HOME/nix-config}/home-manager/programs/flashspace" # Define files to sync FILES=("profiles.yaml" "settings.yaml") @@ -51,9 +51,5 @@ for file in "${FILES[@]}"; do fi done -cd "$DEST_PATH" -if [ -n "$(git status --porcelain)" ]; then - git add . - git commit -m "Backup FlashSpace profiles: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" - git push origin main -fi +# Public repo: review and commit yourself +git -C "$DEST_PATH" status --short -- "${FILES[@]}" diff --git a/home-manager/programs/flashspace/default.nix b/home-manager/programs/flashspace/default.nix index cb779788..f4aad9da 100644 --- a/home-manager/programs/flashspace/default.nix +++ b/home-manager/programs/flashspace/default.nix @@ -16,7 +16,8 @@ in home.activation.installFlashSpaceConfig = config.lib.dag.entryAfter [ "writeBoundary" ] '' echo "❯❯❯❯ · Installing FlashSpace profile and settings..." mkdir -p "${targetPath}" - cmp -s ${profileSource} "${targetPath}/profiles.yaml" || cp ${profileSource} "${targetPath}/profiles.yaml" - cmp -s ${settingSource} "${targetPath}/settings.yaml" || cp ${settingSource} "${targetPath}/settings.yaml" + # Seed only: FlashSpace owns these after first run; save UI changes back with backup_flashspace.sh + [ -e "${targetPath}/profiles.yaml" ] || install -m 644 ${profileSource} "${targetPath}/profiles.yaml" + [ -e "${targetPath}/settings.yaml" ] || install -m 644 ${settingSource} "${targetPath}/settings.yaml" ''; } diff --git a/home-manager/programs/git.nix b/home-manager/programs/git.nix index f5f78d72..10fa2240 100644 --- a/home-manager/programs/git.nix +++ b/home-manager/programs/git.nix @@ -14,7 +14,6 @@ }; merge = { conflictStyle = "diff3"; - tool = "meld"; }; pull = { rebase = true; diff --git a/home-manager/programs/rust.nix b/home-manager/programs/rust.nix index 448956c4..209d83e9 100644 --- a/home-manager/programs/rust.nix +++ b/home-manager/programs/rust.nix @@ -6,7 +6,8 @@ extensions = [ "rust-src" "llvm-tools" - ]; }) + ]; + }) pkgs.rust-analyzer pkgs.pkg-config diff --git a/hosts/common/darwin/default.nix b/hosts/common/darwin/default.nix index d13e0fe9..56db0f25 100644 --- a/hosts/common/darwin/default.nix +++ b/hosts/common/darwin/default.nix @@ -45,9 +45,4 @@ # Add ability to used TouchID for sudo authentication security.pam.services.sudo_local.touchIdAuth = true; - system.activationScripts.postActivation.text = '' - echo "❯❯❯❯ · Removing quarantine attribute from /Applications..." - find /Applications -type d -name "*.app" -exec xattr -r -d com.apple.quarantine {} \; 2>/dev/null - ''; - } diff --git a/hosts/common/darwin/homebrew.nix b/hosts/common/darwin/homebrew.nix index 5ded1221..4c51991e 100644 --- a/hosts/common/darwin/homebrew.nix +++ b/hosts/common/darwin/homebrew.nix @@ -15,7 +15,6 @@ # taps derived from nix-homebrew.taps (see lib/default.nix) casks = [ - # "raycast" "telegram" "iterm2" @@ -27,18 +26,16 @@ "notion" # Utils - "logi-options+" + "openlogi" "artginzburg/tap/wheelclick" "transmission" "pearcleaner" "crisp" "discord" - "zed" - "sublime-text" - "visual-studio-code" - "postman" + "intellij-idea" + "bruno" "figma" @@ -50,6 +47,7 @@ "sanyam-g/homebrew-switch/switch" "abue-ammar/homebrew-tinycast/tinycast@beta" + "hamed-elfayome/claude-usage/claude-usage-tracker" ]; masApps = { diff --git a/hosts/common/darwin/settings/disableHotkeys.nix b/hosts/common/darwin/settings/disableHotkeys.nix index 557acb01..dd4f7d45 100644 --- a/hosts/common/darwin/settings/disableHotkeys.nix +++ b/hosts/common/darwin/settings/disableHotkeys.nix @@ -1,19 +1,47 @@ +let + # parameters = [ ascii keycode modifiers ]; space = 32/49; ⌃ 262144, ⌥ 524288, ⌘ 1048576 + on = modifiers: { + enabled = true; + value = { + parameters = [ + 32 + 49 + modifiers + ]; + type = "standard"; + }; + }; +in { - + # Replaces the whole AppleSymbolicHotKeys dict: IDs not listed fall back to macOS defaults system.defaults.CustomUserPreferences = { "com.apple.symbolichotkeys" = { AppleSymbolicHotKeys = { - "60" = { enabled = false; }; # ⌘ + Space - "61" = { enabled = true; }; # ⌃ + opt + Space -> Select previous input source - "64" = { enabled = false; }; # Spotlight - "65" = { enabled = true; }; # ⌃ + opt + Space -> Select next input source + "60" = { + enabled = false; + }; # ⌃ + Space -> Select previous input source + "61" = on 786432; # ⌃ + ⌥ + Space -> Select next input source + "64" = { + enabled = false; + }; # ⌘ + Space -> Spotlight + "65" = on 1572864; # ⌘ + ⌥ + Space -> Finder search window # Screenshots - "28" = { enabled = false; }; # Save picture of screen as a file - "29" = { enabled = false; }; # Copy picture of screen to the clipboard - "30" = { enabled = false; }; # Save picture of selected area as a file - "31" = { enabled = false; }; # Copy picture of selected area to the clipboard - "184" = { enabled = false; }; # Screenshot and recording options + "28" = { + enabled = false; + }; # Save picture of screen as a file + "29" = { + enabled = false; + }; # Copy picture of screen to the clipboard + "30" = { + enabled = false; + }; # Save picture of selected area as a file + "31" = { + enabled = false; + }; # Copy picture of selected area to the clipboard + "184" = { + enabled = false; + }; # Screenshot and recording options }; }; }; diff --git a/hosts/common/darwin/settings/system.nix b/hosts/common/darwin/settings/system.nix index 634ff57d..44f04195 100644 --- a/hosts/common/darwin/settings/system.nix +++ b/hosts/common/darwin/settings/system.nix @@ -22,7 +22,6 @@ NSGlobalDomain.NSWindowShouldDragOnGesture = true; NSGlobalDomain.NSAutomaticSpellingCorrectionEnabled = false; NSGlobalDomain.AppleICUForce24HourTime = false; # 12-hour clock - LaunchServices.LSQuarantine = false; # disables "Are you sure?" for new apps loginwindow.GuestEnabled = false; universalaccess = { diff --git a/hosts/common/darwin/settings/userPreferences.nix b/hosts/common/darwin/settings/userPreferences.nix index c39e9162..f8404e86 100644 --- a/hosts/common/darwin/settings/userPreferences.nix +++ b/hosts/common/darwin/settings/userPreferences.nix @@ -1,21 +1,11 @@ { system.defaults.CustomUserPreferences = { "com.apple.finder" = { - # No typed nix-darwin equivalent for these two + # No typed nix-darwin equivalent; the rest lives in system.defaults.finder (system.nix) DisableAllAnimations = true; WarnOnEmptyTrash = false; FXArrangeGroupViewBy = "Name"; FXPreferredGroupBy = "Name"; - ShowPathbar = true; - ShowStatusBar = true; - "_FXSortFoldersFirst" = true; - FXDefaultSearchScope = "SCcf"; # search the current folder by default - NewWindowTarget = "PfDe"; # new Finder windows open to... - NewWindowTargetPath = "file://\${HOME}/Desktop/"; # ...the Desktop - ShowHardDrivesOnDesktop = false; - ShowExternalHardDrivesOnDesktop = true; - ShowRemovableMediaOnDesktop = true; - ShowMountedServersOnDesktop = false; }; "com.apple.WindowManager" = { GloballyEnabled = false; # Stage Manager off diff --git a/lib/default.nix b/lib/default.nix index 3d223162..84af0bd8 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -69,6 +69,7 @@ in "sanyam-g/homebrew-switch" = homebrew-switch; "abue-ammar/homebrew-tinycast" = homebrew-tinycast; "artginzburg/homebrew-tap" = homebrew-artginzburg-tap; + "hamed-elfayome/homebrew-claude-usage" = homebrew-claude-usage; }; }; } diff --git a/secrets/flow48/secrets.enc.yaml b/secrets/flow48/secrets.enc.yaml index eb63887d..02209a3e 100644 --- a/secrets/flow48/secrets.enc.yaml +++ b/secrets/flow48/secrets.enc.yaml @@ -1,8 +1,7 @@ ssh_key: ENC[AES256_GCM,data:p4pp+G6HL/jSudgCGIz7o6D4ccZd7/cNjojRjMy5VQsaNVPmC9VlZTyM/RD0UZxpGq4F2fJTr7yJON0VCVJTna8VRVEnAflKs2PoFxOCOzby0JK9/+EuTO28tIasijaTGO+lsTt4+EmzrZ/jLoXu2uhY5GjLrDt2yvVlfGfyB5VQJ1tYX+e0WYpFYzgSh1t+fJ0AUBZmkMoo2Xua+OJzPHBJJ+JX0VHRQZz/AvmZNuwFs5wg52xdZwFH1K84Zt432cscY/bbsGWNeENtCDie0+VPJ6o00BrU1fj1NJT6ygYcIBm+a+rLyhflOgg4Hct7BWRRkvZTcCCOc9f7fh1opRnrovIAG5daa0o4+FweRhTu+amc42Cy2wJlyuM5QHc2ybAdcWS4OqxlRadYeyH+SnvKk2YrFdIqDjpWGTj8EORsYdBZY6xFwksjxTESmLpL/KGIZ2URD9R/ZEO/a1zBZQJYlRtes8u4B148RWk0vIwqVVOAqeuGpUgwVpS604N+WhIyHMV9a14vmDfxFNU52aJCsbHEN/+2bfhe,iv:irlYpvzXA1zEVBZQrM8jLb8mBXGPfO7v/gjif3saOMc=,tag:lNoeDUka9FfjET5C/cDy6Q==,type:str] sops: age: - - recipient: age16flp6pl33nnasnzvvn0fja28l4c4e8y0c3v4qjfsem5hanmjkaeqdhlrvw - enc: | + - enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOSnQwWSs4SFJCTnk1WkpY czV3cEs0YkZQK3pqNDNadHNVcjlvdzMxWEdjCnMxZzRaUm13UkRnM1UvNi81djBG @@ -10,7 +9,8 @@ sops: c1lEQVo2U2s0MHJPM1Y0QWNLd3lySjgKcfCrbN38fbG28K8sXyYtK0NYhbagSzDy gweidcEoxODHnmuhqt05ddwMZFLBqMXjDGn+BJw+in1m8kt1tOvbmg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-06T13:32:19Z" - mac: ENC[AES256_GCM,data:twkuGP5fjqna1cHQKG68H+DHlhWwbxDVd9W2p/D3ePd/QNz8PsDJ5ozD73s+f2Pne8PVrugASdALxPydkHXYNm2monbPOEtjZxiNgQR/xwzZmasKVffO9BpKr2owhekWgEWkzZvDy7+90GAxITNtdrtT+CFOEs2OHjIm6Cj+btY=,iv:FII7bBY+eRZ8mInCubXCYe64PXbzMdrJLoyex96RRHg=,tag:04TPC5dvsPcmo4Ysq9R+kQ==,type:str] + recipient: age16flp6pl33nnasnzvvn0fja28l4c4e8y0c3v4qjfsem5hanmjkaeqdhlrvw + lastmodified: "2026-10-13T19:09:28Z" + mac: ENC[AES256_GCM,data:Hlc3u/O4LSFxYAvtMPG1H+kdw04Qe4Kw2lkF2ns7DQ/rbnPdtZfyOLyiPXVVCxbjyHLET4zCpbAMyz5Jf96bw62AUYTWe62K0QEVL6GN8wgkwsP20dZmOOzUVFZFCVKQKsjutP8UgRjPllC0TZz5fXu3DFMKLaz7CS/KfGNZarI=,iv:LRS4xQFtxK5vipiJND6g0iqv/0vbqzInCwo6IOjGVNg=,tag:kPrHFvgw3SP7lgnyDF6X3g==,type:str] unencrypted_suffix: _unencrypted - version: 3.12.2 + version: 3.13.3 diff --git a/templates/api-rust/flake.nix b/templates/api-rust/flake.nix index 3e2382e8..ddd1ffd3 100644 --- a/templates/api-rust/flake.nix +++ b/templates/api-rust/flake.nix @@ -26,6 +26,8 @@ { devShells.default = pkgs.mkShell { packages = with pkgs; [ + cargo + rustc diesel-cli postgresql ];