@jaspermayone.com's dotfiles

V1

dots v1

authored by jaspermayone.com and committed by jaspermayone.com bfb18510 0446edf5

+4616 -44
+257
README.md
··· 1 + # Jasper's Dotfiles 2 + 3 + NixOS and nix-darwin configurations for the Hogwarts network. 4 + 5 + ## Status 6 + 7 + <img src="https://img.shields.io/website?label=alastor&up_color=green&up_message=online&down_message=offline&url=https%3A%2F%2Falastor.hogwarts.channel%2Fstatus%2Falastor"> 8 + 9 + *Status badges run through alastor — if all badges are red, alastor is probably down.* 10 + 11 + ## Hosts 12 + 13 + | Host | Domain | Type | Description | 14 + |------|--------|------|-------------| 15 + | **alastor** | `alastor.hogwarts.channel` | NixOS (x86_64) | VPS hub - tunnels, status, reverse proxy (Mad-Eye Moody) | 16 + | **remus** | `remus.hogwarts.channel` | Darwin (aarch64) | MacBook Pro M4 - proxied via alastor over Tailscale | 17 + 18 + ### Domain Structure 19 + 20 + - `tun.hogwarts.channel` — bore/frp tunnels only 21 + - `*.tun.hogwarts.channel` — dynamic tunnel subdomains 22 + - `alastor.hogwarts.channel` — alastor services (status API, etc.) 23 + - `remus.hogwarts.channel` — reverse proxy to remus via Tailscale 24 + - `knot.jaspermayone.com` — Tangled Knot git server 25 + 26 + ## Secrets Management (agenix) 27 + 28 + This repo uses [agenix](https://github.com/ryantm/agenix) for secrets. Secrets are encrypted with age using SSH keys and stored in git. 29 + 30 + ### Initial Setup 31 + 32 + 1. Get your SSH public key: 33 + ```bash 34 + cat ~/.ssh/id_ed25519.pub 35 + ``` 36 + 37 + 2. Edit `secrets/secrets.nix` and add your public key: 38 + ```nix 39 + let 40 + jsp = "ssh-ed25519 AAAA... jasper@remus"; 41 + # ... 42 + ``` 43 + 44 + 3. After provisioning alastor, get its host key: 45 + ```bash 46 + ssh-keyscan -t ed25519 tun.hogwarts.channel 47 + ``` 48 + 49 + 4. Add the host key to `secrets/secrets.nix` 50 + 51 + ### Creating Secrets 52 + 53 + ```bash 54 + # From the repo root 55 + cd secrets 56 + 57 + # Create/edit a secret (opens $EDITOR) 58 + agenix -e frps-token.age 59 + 60 + # For frps-token, just paste a random token: 61 + # openssl rand -hex 32 62 + 63 + # For cloudflare-credentials.age: 64 + # CF_DNS_API_TOKEN=your-token-here 65 + 66 + # For bore-token.age, use the same value as frps-token 67 + ``` 68 + 69 + ### Re-keying Secrets 70 + 71 + If you add new keys to `secrets.nix`: 72 + ```bash 73 + cd secrets 74 + agenix -r # Re-encrypt all secrets with new keys 75 + ``` 76 + 77 + ## Quick Start 78 + 79 + ### Setting up Remus (Mac) 80 + 81 + 1. Install Nix (using Determinate Systems installer): 82 + ```bash 83 + curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install 84 + ``` 85 + 86 + 2. Clone this repo: 87 + ```bash 88 + git clone https://github.com/jaspermayone/dots.git ~/dots 89 + cd ~/dots 90 + ``` 91 + 92 + 3. Create the secrets (see Secrets Management above): 93 + ```bash 94 + cd secrets 95 + agenix -e bore-token.age 96 + cd .. 97 + ``` 98 + 99 + 4. Build and switch: 100 + ```bash 101 + nix run nix-darwin -- switch --flake .#remus 102 + ``` 103 + 104 + After the first build, use: 105 + ```bash 106 + darwin-rebuild switch --flake ~/dots#remus 107 + ``` 108 + 109 + ### Setting up Alastor (Server) 110 + 111 + 1. Provision a VPS with NixOS (Hetzner has this in marketplace) 112 + 113 + 2. SSH in and clone: 114 + ```bash 115 + git clone https://github.com/jaspermayone/dots.git /etc/nixos 116 + cd /etc/nixos 117 + ``` 118 + 119 + 3. Generate hardware config: 120 + ```bash 121 + nixos-generate-config --show-hardware-config > hosts/alastor/hardware-configuration.nix 122 + ``` 123 + 124 + 4. Get the host's SSH public key and add to `secrets/secrets.nix`: 125 + ```bash 126 + cat /etc/ssh/ssh_host_ed25519_key.pub 127 + ``` 128 + 129 + 5. On your local machine, re-key secrets with the new host key: 130 + ```bash 131 + cd secrets && agenix -r && cd .. 132 + git add . && git commit -m "Add alastor host key" 133 + git push 134 + ``` 135 + 136 + 6. Back on the server, pull and build: 137 + ```bash 138 + git pull 139 + nixos-rebuild switch --flake .#alastor 140 + ``` 141 + 142 + ### Remote Deployment 143 + 144 + From your Mac: 145 + ```bash 146 + nixos-rebuild switch --flake .#alastor --target-host root@tun.hogwarts.channel 147 + ``` 148 + 149 + ## DNS Setup (Cloudflare) 150 + 151 + | Type | Name | Content | Proxy | 152 + |------|------|---------|-------| 153 + | A | tun | server-ip | Off (gray) | 154 + | A | *.tun | server-ip | Off (gray) | 155 + | A | alastor | server-ip | Off (gray) | 156 + | A | remus | server-ip | Off (gray) | 157 + 158 + **Create Cloudflare API Token:** 159 + 1. https://dash.cloudflare.com/profile/api-tokens 160 + 2. Create Token → Custom Token 161 + 3. Permissions: `Zone - DNS - Edit` 162 + 4. Zone Resources: `Include - Specific zone - hogwarts.channel` 163 + 164 + ## Usage 165 + 166 + ### Creating a tunnel 167 + 168 + ```bash 169 + # Interactive 170 + bore 171 + 172 + # Quick tunnel 173 + bore myapp 3000 174 + 175 + # With options 176 + bore api 8080 --protocol http --label dev --save 177 + ``` 178 + 179 + ### Listing tunnels 180 + 181 + ```bash 182 + bore --list # Active tunnels on server 183 + bore --saved # Saved tunnels in bore.toml 184 + ``` 185 + 186 + ## Structure 187 + 188 + ``` 189 + dots/ 190 + ├── flake.nix # Entry point 191 + ├── secrets/ 192 + │ ├── secrets.nix # Declares keys and secrets 193 + │ ├── frps-token.age # Encrypted frp auth token 194 + │ ├── cloudflare-credentials.age 195 + │ └── bore-token.age # Client token (same as frps-token) 196 + ├── common/ 197 + │ ├── bore.nix # Bore client config 198 + │ ├── git.nix # Git configuration 199 + │ └── shell.nix # Shell configuration 200 + ├── darwin/ 201 + │ └── default.nix # macOS-specific settings 202 + ├── home/ 203 + │ └── default.nix # Home Manager config 204 + ├── hosts/ 205 + │ ├── alastor/ # NixOS server (Mad-Eye Moody) 206 + │ │ ├── configuration.nix 207 + │ │ └── hardware-configuration.nix 208 + │ └── remus/ # Mac laptop 209 + │ └── default.nix 210 + └── modules/ 211 + ├── bore/ # Bore client module 212 + │ ├── default.nix 213 + │ ├── bore.1.md 214 + │ └── completions/ 215 + ├── frps/ # Frp server module 216 + │ └── default.nix 217 + └── status/ # Status monitoring module 218 + └── default.nix 219 + ``` 220 + 221 + ## Adding New Hosts 222 + 223 + ### NixOS 224 + 1. Create `hosts/hostname/configuration.nix` 225 + 2. Create `hosts/hostname/hardware-configuration.nix` 226 + 3. Add host key to `secrets/secrets.nix` and re-key 227 + 4. Add to `flake.nix`: 228 + ```nix 229 + nixosConfigurations.hostname = mkNixos "hostname" "x86_64-linux"; 230 + ``` 231 + 232 + ### Darwin (Mac) 233 + 1. Create `hosts/hostname/default.nix` 234 + 2. Add user key to `secrets/secrets.nix` and re-key 235 + 3. Add to `flake.nix`: 236 + ```nix 237 + darwinConfigurations.hostname = mkDarwin "hostname" "aarch64-darwin"; 238 + ``` 239 + 240 + ## Useful Commands 241 + 242 + ```bash 243 + # Edit a secret 244 + agenix -e secrets/frps-token.age 245 + 246 + # Re-key all secrets (after adding new keys) 247 + cd secrets && agenix -r 248 + 249 + # Check flake 250 + nix flake check 251 + 252 + # Update flake inputs 253 + nix flake update 254 + 255 + # Garbage collect old generations 256 + nix-collect-garbage -d 257 + ```
common/bore.nix

This is a binary file and will not be displayed.

common/git.nix

This is a binary file and will not be displayed.

common/shell.nix

This is a binary file and will not be displayed.

+381
darwin/default.nix
··· 1 + # Darwin (macOS) specific configuration 2 + { config, pkgs, lib, inputs, hostname, ... }: 3 + 4 + { 5 + # Nix configuration 6 + nix.settings.experimental-features = [ "nix-command" "flakes" ]; 7 + 8 + # Fix GID mismatch for nixbld group (new installs use 350, old used 30000) 9 + ids.gids.nixbld = 350; 10 + 11 + # Allow unfree packages 12 + nixpkgs.config.allowUnfree = true; 13 + 14 + # Primary user (required for homebrew and other user-specific options) 15 + system.primaryUser = "jsp"; 16 + 17 + # System-level packages available to all users 18 + environment.systemPackages = with pkgs; [ 19 + vim 20 + git 21 + inputs.agenix.packages.${pkgs.stdenv.hostPlatform.system}.default 22 + ]; 23 + 24 + # Homebrew integration 25 + homebrew = { 26 + enable = true; 27 + onActivation = { 28 + autoUpdate = true; 29 + # cleanup = "zap"; # Remove unlisted packages 30 + upgrade = true; 31 + }; 32 + 33 + taps = [ 34 + "bramstein/webfonttools" 35 + ]; 36 + 37 + # CLI tools 38 + brews = [ 39 + "mas" 40 + "coreutils" 41 + "moreutils" 42 + "findutils" 43 + "git" 44 + "git-lfs" 45 + "gnupg" 46 + "grep" 47 + "openssh" 48 + "screen" 49 + "ssh-copy-id" 50 + "zsh" 51 + "ffmpeg" 52 + { name = "imagemagick"; args = [ "with-webp" ]; } 53 + "wget" 54 + # Font tools 55 + "sfnt2woff" 56 + "sfnt2woff-zopfli" 57 + "woff2" 58 + ]; 59 + 60 + # GUI apps (casks) 61 + casks = [ 62 + "discord" 63 + "raycast" 64 + ]; 65 + 66 + # Mac App Store apps (requires `mas` CLI) 67 + # masApps = { 68 + # "Xcode" = 497799835; 69 + # }; 70 + }; 71 + 72 + # macOS system preferences 73 + system.defaults = { 74 + # Global preferences 75 + NSGlobalDomain = { 76 + # Mouse/scrolling 77 + AppleEnableMouseSwipeNavigateWithScrolls = false; 78 + AppleEnableSwipeNavigateWithScrolls = false; 79 + 80 + # Auto-corrections 81 + NSAutomaticCapitalizationEnabled = false; 82 + NSAutomaticDashSubstitutionEnabled = false; 83 + NSAutomaticInlinePredictionEnabled = false; 84 + NSAutomaticPeriodSubstitutionEnabled = false; 85 + NSAutomaticQuoteSubstitutionEnabled = false; 86 + NSAutomaticSpellingCorrectionEnabled = false; 87 + 88 + # Interface 89 + AppleInterfaceStyle = "Dark"; 90 + AppleICUForce24HourTime = false; 91 + _HIHideMenuBar = false; 92 + 93 + # Keyboard 94 + InitialKeyRepeat = 15; 95 + KeyRepeat = 2; 96 + ApplePressAndHoldEnabled = false; 97 + AppleKeyboardUIMode = 3; # Full keyboard access 98 + 99 + # Scrolling 100 + "com.apple.swipescrolldirection" = false; # (true = Natural scrolling 101 + 102 + # Appearance 103 + AppleShowAllExtensions = true; 104 + NSTableViewDefaultSizeMode = 1; 105 + 106 + # Units 107 + AppleMeasurementUnits = "Inches"; 108 + AppleMetricUnits = 0; 109 + AppleTemperatureUnit = "Fahrenheit"; 110 + 111 + # Window behavior 112 + NSWindowResizeTime = 0.001; # Faster window resize 113 + NSNavPanelExpandedStateForSaveMode = true; # Expand save panel 114 + NSNavPanelExpandedStateForSaveMode2 = true; 115 + PMPrintingExpandedStateForPrint = true; # Expand print panel 116 + PMPrintingExpandedStateForPrint2 = true; 117 + NSDocumentSaveNewDocumentsToCloud = false; # Save to disk, not iCloud 118 + NSDisableAutomaticTermination = true; # Prevent auto-termination of apps 119 + }; 120 + 121 + # Finder 122 + finder = { 123 + AppleShowAllExtensions = true; 124 + AppleShowAllFiles = false; 125 + CreateDesktop = true; 126 + FXEnableExtensionChangeWarning = false; 127 + FXPreferredViewStyle = "clmv"; # Column view 128 + QuitMenuItem = true; 129 + ShowPathbar = true; 130 + ShowStatusBar = true; 131 + _FXShowPosixPathInTitle = true; 132 + # Desktop icons 133 + ShowExternalHardDrivesOnDesktop = true; 134 + ShowHardDrivesOnDesktop = true; 135 + ShowMountedServersOnDesktop = true; 136 + ShowRemovableMediaOnDesktop = true; 137 + # Sorting and search 138 + _FXSortFoldersFirst = true; 139 + FXDefaultSearchScope = "SCcf"; # Search current folder 140 + }; 141 + 142 + # Screenshots 143 + screencapture = { 144 + disable-shadow = true; 145 + type = "png"; 146 + }; 147 + 148 + # Screen saver / lock 149 + screensaver = { 150 + askForPassword = true; 151 + askForPasswordDelay = 0; 152 + }; 153 + 154 + # Menu bar clock 155 + menuExtraClock = { 156 + Show24Hour = true; 157 + ShowDayOfMonth = true; 158 + ShowDayOfWeek = true; 159 + ShowSeconds = false; 160 + }; 161 + 162 + # Dock 163 + dock = { 164 + autohide = true; 165 + autohide-delay = 0.0; 166 + mineffect = "scale"; 167 + minimize-to-application = false; 168 + mru-spaces = false; 169 + orientation = "left"; 170 + show-recents = false; 171 + tilesize = 48; 172 + launchanim = false; 173 + expose-animation-duration = 0.1; # Faster Mission Control 174 + showhidden = true; # Translucent icons for hidden apps 175 + }; 176 + 177 + # Trackpad 178 + trackpad = { 179 + Clicking = true; # Tap to click 180 + TrackpadRightClick = true; 181 + TrackpadThreeFingerDrag = true; 182 + Dragging = true; 183 + }; 184 + 185 + # Spaces 186 + spaces = { 187 + spans-displays = false; 188 + }; 189 + 190 + # Window Manager (Stage Manager) 191 + WindowManager = { 192 + EnableStandardClickToShowDesktop = false; 193 + EnableTiledWindowMargins = false; 194 + GloballyEnabled = false; 195 + }; 196 + 197 + # Login window 198 + loginwindow = { 199 + GuestEnabled = false; 200 + }; 201 + 202 + # Custom settings 203 + CustomUserPreferences = { 204 + # System sound 205 + "com.apple.systemsound" = { 206 + "com.apple.sound.uiaudio.enabled" = 0; # Disable boot sound 207 + }; 208 + 209 + # Help Viewer non-floating 210 + "com.apple.helpviewer" = { 211 + DevMode = true; 212 + }; 213 + 214 + # Disable "Are you sure you want to open this application?" dialog 215 + "com.apple.LaunchServices" = { 216 + LSQuarantine = false; 217 + }; 218 + 219 + # Disable Resume system-wide 220 + "com.apple.systempreferences" = { 221 + NSQuitAlwaysKeepsWindows = false; 222 + }; 223 + 224 + # Printer: quit when finished 225 + "com.apple.print.PrintingPrefs" = { 226 + "Quit When Finished" = true; 227 + }; 228 + 229 + # Finder extras 230 + "com.apple.finder" = { 231 + ShowRecentTags = false; 232 + OpenWindowForNewRemovableDisk = true; # Auto-open for mounted volumes 233 + }; 234 + 235 + # Disk images: skip verification 236 + "com.apple.frameworks.diskimages" = { 237 + skip-verify = true; 238 + skip-verify-locked = true; 239 + skip-verify-remote = true; 240 + }; 241 + 242 + # Dock spring loading 243 + "com.apple.dock" = { 244 + "springboard-show-duration" = 0; 245 + }; 246 + 247 + # Safari 248 + "com.apple.Safari" = { 249 + UniversalSearchEnabled = false; # Don't send search queries to Apple 250 + SuppressSearchSuggestions = true; 251 + ShowFullURLInSmartSearchField = true; 252 + HomePage = "about:blank"; 253 + IncludeDevelopMenu = true; 254 + WebKitDeveloperExtrasEnabledPreferenceKey = true; 255 + "com.apple.Safari.ContentPageGroupIdentifier.WebKit2DeveloperExtrasEnabled" = true; 256 + WarnAboutFraudulentWebsites = true; 257 + SendDoNotTrackHTTPHeader = true; 258 + }; 259 + 260 + # Mail 261 + "com.apple.mail" = { 262 + AddressesIncludeNameOnPasteboard = false; # Copy addresses without name 263 + NSUserKeyEquivalents = { 264 + Send = "@\\U21a9"; # Cmd+Enter to send 265 + }; 266 + DisableInlineAttachmentViewing = true; 267 + }; 268 + 269 + # Terminal 270 + "com.apple.terminal" = { 271 + StringEncodings = [ 4 ]; # UTF-8 only 272 + }; 273 + 274 + # iTerm2 275 + "com.googlecode.iterm2" = { 276 + PromptOnQuit = false; 277 + }; 278 + 279 + # Time Machine 280 + "com.apple.TimeMachine" = { 281 + DoNotOfferNewDisksForBackup = true; 282 + }; 283 + 284 + # Activity Monitor 285 + "com.apple.ActivityMonitor" = { 286 + OpenMainWindow = true; 287 + ShowCategory = 0; # All processes 288 + SortColumn = "CPUUsage"; 289 + SortDirection = 0; 290 + }; 291 + 292 + # TextEdit 293 + "com.apple.TextEdit" = { 294 + RichText = 0; # Plain text mode 295 + PlainTextEncoding = 4; # UTF-8 296 + PlainTextEncodingForWrite = 4; 297 + }; 298 + 299 + # Disk Utility 300 + "com.apple.DiskUtility" = { 301 + DUDebugMenuEnabled = true; 302 + "advanced-image-options" = true; 303 + }; 304 + 305 + # Mac App Store 306 + "com.apple.appstore" = { 307 + ShowDebugMenu = true; 308 + WebKitDeveloperExtras = true; 309 + }; 310 + 311 + # Software Update 312 + "com.apple.SoftwareUpdate" = { 313 + AutomaticCheckEnabled = true; 314 + ScheduleFrequency = 1; # Daily 315 + AutomaticDownload = 1; 316 + CriticalUpdateInstall = 1; # Auto-install security updates 317 + AutoUpdate = true; 318 + }; 319 + 320 + # Don't write .DS_Store files on network or USB volumes 321 + "com.apple.desktopservices" = { 322 + DSDontWriteNetworkStores = true; 323 + DSDontWriteUSBStores = true; 324 + }; 325 + 326 + # Photos: don't open automatically 327 + "com.apple.ImageCapture" = { 328 + disableHotPlug = true; 329 + }; 330 + 331 + # Messages 332 + "com.apple.messageshelper.MessageController" = { 333 + SOInputLineSettings = { 334 + automaticQuoteSubstitutionEnabled = false; 335 + }; 336 + }; 337 + "com.apple.messages.text" = { 338 + NSAutomaticSpellingCorrectionEnabled = false; 339 + }; 340 + }; 341 + }; 342 + 343 + # Activation scripts for settings that can't be done declaratively 344 + # Note: these run as root during activation 345 + system.activationScripts.extraActivation.text = '' 346 + # Show ~/Library folder (for primary user) 347 + chflags nohidden /Users/jsp/Library 348 + 349 + # Show /Volumes folder 350 + chflags nohidden /Volumes 351 + 352 + # Power management settings 353 + # Wake on lid open 354 + pmset -a lidwake 1 355 + # Display sleep: 15 min on power, 5 min on battery 356 + pmset -c displaysleep 15 357 + pmset -b displaysleep 5 358 + # Disable sleep while charging (system sleep never on AC) 359 + pmset -c sleep 0 360 + # 24-hour delay before standby (in seconds: 86400) 361 + pmset -a standbydelay 86400 362 + 363 + # Enable HiDPI display modes 364 + defaults write /Library/Preferences/com.apple.windowserver DisplayResolutionEnabled -bool true 365 + ''; 366 + 367 + # Enable Touch ID for sudo 368 + security.pam.services.sudo_local.touchIdAuth = true; 369 + 370 + # Create /etc/zshrc that loads nix-darwin environment 371 + programs.zsh.enable = true; 372 + 373 + # Fonts 374 + fonts.packages = with pkgs; [ 375 + nerd-fonts.jetbrains-mono 376 + nerd-fonts.fira-code 377 + ]; 378 + 379 + # Used for backwards compatibility 380 + system.stateVersion = 4; 381 + }
+681
flake.lock
··· 1 + { 2 + "nodes": { 3 + "actor-typeahead-src": { 4 + "flake": false, 5 + "locked": { 6 + "lastModified": 1762835797, 7 + "narHash": "sha256-heizoWUKDdar6ymfZTnj3ytcEv/L4d4fzSmtr0HlXsQ=", 8 + "ref": "refs/heads/main", 9 + "rev": "677fe7f743050a4e7f09d4a6f87bbf1325a06f6b", 10 + "revCount": 6, 11 + "type": "git", 12 + "url": "https://tangled.org/@jakelazaroff.com/actor-typeahead" 13 + }, 14 + "original": { 15 + "type": "git", 16 + "url": "https://tangled.org/@jakelazaroff.com/actor-typeahead" 17 + } 18 + }, 19 + "agenix": { 20 + "inputs": { 21 + "darwin": "darwin", 22 + "home-manager": "home-manager", 23 + "nixpkgs": "nixpkgs", 24 + "systems": "systems" 25 + }, 26 + "locked": { 27 + "lastModified": 1762618334, 28 + "narHash": "sha256-wyT7Pl6tMFbFrs8Lk/TlEs81N6L+VSybPfiIgzU8lbQ=", 29 + "owner": "ryantm", 30 + "repo": "agenix", 31 + "rev": "fcdea223397448d35d9b31f798479227e80183f6", 32 + "type": "github" 33 + }, 34 + "original": { 35 + "owner": "ryantm", 36 + "repo": "agenix", 37 + "type": "github" 38 + } 39 + }, 40 + "claude-desktop": { 41 + "inputs": { 42 + "flake-utils": "flake-utils", 43 + "nixpkgs": [ 44 + "nixpkgs" 45 + ] 46 + }, 47 + "locked": { 48 + "lastModified": 1764098187, 49 + "narHash": "sha256-H6JjWXhKqxZ8QLMoqndZx9e5x0Sv5AiipSmqvIxIbgo=", 50 + "owner": "k3d3", 51 + "repo": "claude-desktop-linux-flake", 52 + "rev": "b2b040cb68231d2118906507d9cc8fd181ca6308", 53 + "type": "github" 54 + }, 55 + "original": { 56 + "owner": "k3d3", 57 + "repo": "claude-desktop-linux-flake", 58 + "type": "github" 59 + } 60 + }, 61 + "darwin": { 62 + "inputs": { 63 + "nixpkgs": [ 64 + "agenix", 65 + "nixpkgs" 66 + ] 67 + }, 68 + "locked": { 69 + "lastModified": 1744478979, 70 + "narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=", 71 + "owner": "lnl7", 72 + "repo": "nix-darwin", 73 + "rev": "43975d782b418ebf4969e9ccba82466728c2851b", 74 + "type": "github" 75 + }, 76 + "original": { 77 + "owner": "lnl7", 78 + "ref": "master", 79 + "repo": "nix-darwin", 80 + "type": "github" 81 + } 82 + }, 83 + "deploy-rs": { 84 + "inputs": { 85 + "flake-compat": "flake-compat", 86 + "nixpkgs": [ 87 + "nixpkgs" 88 + ], 89 + "utils": "utils" 90 + }, 91 + "locked": { 92 + "lastModified": 1766051518, 93 + "narHash": "sha256-znKOwPXQnt3o7lDb3hdf19oDo0BLP4MfBOYiWkEHoik=", 94 + "owner": "serokell", 95 + "repo": "deploy-rs", 96 + "rev": "d5eff7f948535b9c723d60cd8239f8f11ddc90fa", 97 + "type": "github" 98 + }, 99 + "original": { 100 + "owner": "serokell", 101 + "repo": "deploy-rs", 102 + "type": "github" 103 + } 104 + }, 105 + "flake-compat": { 106 + "flake": false, 107 + "locked": { 108 + "lastModified": 1733328505, 109 + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 110 + "owner": "edolstra", 111 + "repo": "flake-compat", 112 + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 113 + "type": "github" 114 + }, 115 + "original": { 116 + "owner": "edolstra", 117 + "repo": "flake-compat", 118 + "type": "github" 119 + } 120 + }, 121 + "flake-compat_2": { 122 + "flake": false, 123 + "locked": { 124 + "lastModified": 1751685974, 125 + "narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=", 126 + "rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1", 127 + "type": "tarball", 128 + "url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/549f2762aebeff29a2e5ece7a7dc0f955281a1d1.tar.gz?rev=549f2762aebeff29a2e5ece7a7dc0f955281a1d1" 129 + }, 130 + "original": { 131 + "type": "tarball", 132 + "url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz" 133 + } 134 + }, 135 + "flake-parts": { 136 + "inputs": { 137 + "nixpkgs-lib": [ 138 + "nur", 139 + "nixpkgs" 140 + ] 141 + }, 142 + "locked": { 143 + "lastModified": 1733312601, 144 + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", 145 + "owner": "hercules-ci", 146 + "repo": "flake-parts", 147 + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", 148 + "type": "github" 149 + }, 150 + "original": { 151 + "owner": "hercules-ci", 152 + "repo": "flake-parts", 153 + "type": "github" 154 + } 155 + }, 156 + "flake-utils": { 157 + "inputs": { 158 + "systems": "systems_2" 159 + }, 160 + "locked": { 161 + "lastModified": 1731533236, 162 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 163 + "owner": "numtide", 164 + "repo": "flake-utils", 165 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 166 + "type": "github" 167 + }, 168 + "original": { 169 + "owner": "numtide", 170 + "repo": "flake-utils", 171 + "type": "github" 172 + } 173 + }, 174 + "flake-utils_2": { 175 + "inputs": { 176 + "systems": "systems_4" 177 + }, 178 + "locked": { 179 + "lastModified": 1731533236, 180 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 181 + "owner": "numtide", 182 + "repo": "flake-utils", 183 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 184 + "type": "github" 185 + }, 186 + "original": { 187 + "owner": "numtide", 188 + "repo": "flake-utils", 189 + "type": "github" 190 + } 191 + }, 192 + "flake-utils_3": { 193 + "inputs": { 194 + "systems": "systems_5" 195 + }, 196 + "locked": { 197 + "lastModified": 1731533236, 198 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 199 + "owner": "numtide", 200 + "repo": "flake-utils", 201 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 202 + "type": "github" 203 + }, 204 + "original": { 205 + "owner": "numtide", 206 + "repo": "flake-utils", 207 + "type": "github" 208 + } 209 + }, 210 + "gomod2nix": { 211 + "inputs": { 212 + "flake-utils": "flake-utils_2", 213 + "nixpkgs": [ 214 + "tangled", 215 + "nixpkgs" 216 + ] 217 + }, 218 + "locked": { 219 + "lastModified": 1763982521, 220 + "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=", 221 + "owner": "nix-community", 222 + "repo": "gomod2nix", 223 + "rev": "02e63a239d6eabd595db56852535992c898eba72", 224 + "type": "github" 225 + }, 226 + "original": { 227 + "owner": "nix-community", 228 + "repo": "gomod2nix", 229 + "type": "github" 230 + } 231 + }, 232 + "hardware": { 233 + "locked": { 234 + "lastModified": 1766568855, 235 + "narHash": "sha256-UXVtN77D7pzKmzOotFTStgZBqpOcf8cO95FcupWp4Zo=", 236 + "owner": "NixOS", 237 + "repo": "nixos-hardware", 238 + "rev": "c5db9569ac9cc70929c268ac461f4003e3e5ca80", 239 + "type": "github" 240 + }, 241 + "original": { 242 + "owner": "NixOS", 243 + "ref": "master", 244 + "repo": "nixos-hardware", 245 + "type": "github" 246 + } 247 + }, 248 + "home-manager": { 249 + "inputs": { 250 + "nixpkgs": [ 251 + "agenix", 252 + "nixpkgs" 253 + ] 254 + }, 255 + "locked": { 256 + "lastModified": 1745494811, 257 + "narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=", 258 + "owner": "nix-community", 259 + "repo": "home-manager", 260 + "rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be", 261 + "type": "github" 262 + }, 263 + "original": { 264 + "owner": "nix-community", 265 + "repo": "home-manager", 266 + "type": "github" 267 + } 268 + }, 269 + "home-manager_2": { 270 + "inputs": { 271 + "nixpkgs": [ 272 + "nixpkgs" 273 + ] 274 + }, 275 + "locked": { 276 + "lastModified": 1766553861, 277 + "narHash": "sha256-ZbnG01yA3O8Yr1vUm3+NQ2qk9iRhS5bloAnuXHHy7+c=", 278 + "owner": "nix-community", 279 + "repo": "home-manager", 280 + "rev": "0999ed8f965bbbd991437ad9c5ed3434cecbc30e", 281 + "type": "github" 282 + }, 283 + "original": { 284 + "owner": "nix-community", 285 + "ref": "release-25.11", 286 + "repo": "home-manager", 287 + "type": "github" 288 + } 289 + }, 290 + "htmx-src": { 291 + "flake": false, 292 + "locked": { 293 + "narHash": "sha256-nm6avZuEBg67SSyyZUhjpXVNstHHgUxrtBHqJgowU08=", 294 + "type": "file", 295 + "url": "https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js" 296 + }, 297 + "original": { 298 + "type": "file", 299 + "url": "https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js" 300 + } 301 + }, 302 + "htmx-ws-src": { 303 + "flake": false, 304 + "locked": { 305 + "narHash": "sha256-2fg6KyEJoO24q0fQqbz9RMaYNPQrMwpZh29tkSqdqGY=", 306 + "type": "file", 307 + "url": "https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.2" 308 + }, 309 + "original": { 310 + "type": "file", 311 + "url": "https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.2" 312 + } 313 + }, 314 + "ibm-plex-mono-src": { 315 + "flake": false, 316 + "locked": { 317 + "lastModified": 1731402384, 318 + "narHash": "sha256-OwUmrPfEehLDz0fl2ChYLK8FQM2p0G1+EMrGsYEq+6g=", 319 + "type": "tarball", 320 + "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 321 + }, 322 + "original": { 323 + "type": "tarball", 324 + "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 325 + } 326 + }, 327 + "import-tree": { 328 + "locked": { 329 + "lastModified": 1763762820, 330 + "narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=", 331 + "owner": "vic", 332 + "repo": "import-tree", 333 + "rev": "3c23749d8013ec6daa1d7255057590e9ca726646", 334 + "type": "github" 335 + }, 336 + "original": { 337 + "owner": "vic", 338 + "repo": "import-tree", 339 + "type": "github" 340 + } 341 + }, 342 + "indigo": { 343 + "flake": false, 344 + "locked": { 345 + "lastModified": 1753693716, 346 + "narHash": "sha256-DMIKnCJRODQXEHUxA+7mLzRALmnZhkkbHlFT2rCQYrE=", 347 + "owner": "oppiliappan", 348 + "repo": "indigo", 349 + "rev": "5f170569da9360f57add450a278d73538092d8ca", 350 + "type": "github" 351 + }, 352 + "original": { 353 + "owner": "oppiliappan", 354 + "repo": "indigo", 355 + "type": "github" 356 + } 357 + }, 358 + "inter-fonts-src": { 359 + "flake": false, 360 + "locked": { 361 + "lastModified": 1731687360, 362 + "narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=", 363 + "type": "tarball", 364 + "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" 365 + }, 366 + "original": { 367 + "type": "tarball", 368 + "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" 369 + } 370 + }, 371 + "lucide-src": { 372 + "flake": false, 373 + "locked": { 374 + "lastModified": 1754044466, 375 + "narHash": "sha256-+exBR2OToB1iv7ZQI2S4B0lXA/QRvC9n6U99UxGpJGs=", 376 + "type": "tarball", 377 + "url": "https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip" 378 + }, 379 + "original": { 380 + "type": "tarball", 381 + "url": "https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip" 382 + } 383 + }, 384 + "nix-darwin": { 385 + "inputs": { 386 + "nixpkgs": [ 387 + "nixpkgs" 388 + ] 389 + }, 390 + "locked": { 391 + "lastModified": 1765066094, 392 + "narHash": "sha256-0YSU35gfRFJzx/lTGgOt6ubP8K6LeW0vaywzNNqxkl4=", 393 + "owner": "nix-darwin", 394 + "repo": "nix-darwin", 395 + "rev": "688427b1aab9afb478ca07989dc754fa543e03d5", 396 + "type": "github" 397 + }, 398 + "original": { 399 + "owner": "nix-darwin", 400 + "ref": "nix-darwin-25.11", 401 + "repo": "nix-darwin", 402 + "type": "github" 403 + } 404 + }, 405 + "nixpkgs": { 406 + "locked": { 407 + "lastModified": 1766399428, 408 + "narHash": "sha256-vS6LSOMDOB3s+L6tqw9IGujxnmUAZQnEG+Vi640LayI=", 409 + "owner": "NixOS", 410 + "repo": "nixpkgs", 411 + "rev": "a6c3a6141ec1b367c58ead3f7f846c772a25f4e5", 412 + "type": "github" 413 + }, 414 + "original": { 415 + "owner": "NixOS", 416 + "ref": "nixos-25.05", 417 + "repo": "nixpkgs", 418 + "type": "github" 419 + } 420 + }, 421 + "nixpkgs-unstable": { 422 + "locked": { 423 + "lastModified": 1766309749, 424 + "narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=", 425 + "owner": "nixos", 426 + "repo": "nixpkgs", 427 + "rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816", 428 + "type": "github" 429 + }, 430 + "original": { 431 + "owner": "nixos", 432 + "ref": "nixos-unstable", 433 + "repo": "nixpkgs", 434 + "type": "github" 435 + } 436 + }, 437 + "nixpkgs_2": { 438 + "locked": { 439 + "lastModified": 1766473571, 440 + "narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=", 441 + "owner": "nixos", 442 + "repo": "nixpkgs", 443 + "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", 444 + "type": "github" 445 + }, 446 + "original": { 447 + "owner": "nixos", 448 + "ref": "nixos-25.11", 449 + "repo": "nixpkgs", 450 + "type": "github" 451 + } 452 + }, 453 + "nixpkgs_3": { 454 + "locked": { 455 + "lastModified": 1764635402, 456 + "narHash": "sha256-6rYcajRLe2C5ZYnV1HYskJl+QAkhvseWTzbdQiTN9OI=", 457 + "owner": "nixos", 458 + "repo": "nixpkgs", 459 + "rev": "5f53b0d46d320352684242d000b36dcfbbf7b0bc", 460 + "type": "github" 461 + }, 462 + "original": { 463 + "owner": "nixos", 464 + "repo": "nixpkgs", 465 + "type": "github" 466 + } 467 + }, 468 + "nur": { 469 + "inputs": { 470 + "flake-parts": "flake-parts", 471 + "nixpkgs": [ 472 + "nixpkgs" 473 + ] 474 + }, 475 + "locked": { 476 + "lastModified": 1766599192, 477 + "narHash": "sha256-33vb0shMkgUgTEkh/jP/WXyoHz1nZ3dQtA/gw756+8c=", 478 + "owner": "nix-community", 479 + "repo": "NUR", 480 + "rev": "4988d64ccb03e95759deb75efd9c38876b0134b3", 481 + "type": "github" 482 + }, 483 + "original": { 484 + "owner": "nix-community", 485 + "repo": "NUR", 486 + "type": "github" 487 + } 488 + }, 489 + "root": { 490 + "inputs": { 491 + "agenix": "agenix", 492 + "claude-desktop": "claude-desktop", 493 + "deploy-rs": "deploy-rs", 494 + "hardware": "hardware", 495 + "home-manager": "home-manager_2", 496 + "import-tree": "import-tree", 497 + "nix-darwin": "nix-darwin", 498 + "nixpkgs": "nixpkgs_2", 499 + "nixpkgs-unstable": "nixpkgs-unstable", 500 + "nur": "nur", 501 + "tangled": "tangled", 502 + "zmx": "zmx" 503 + } 504 + }, 505 + "sqlite-lib-src": { 506 + "flake": false, 507 + "locked": { 508 + "lastModified": 1706631843, 509 + "narHash": "sha256-bJoMjirsBjm2Qk9KPiy3yV3+8b/POlYe76/FQbciHro=", 510 + "type": "tarball", 511 + "url": "https://sqlite.org/2024/sqlite-amalgamation-3450100.zip" 512 + }, 513 + "original": { 514 + "type": "tarball", 515 + "url": "https://sqlite.org/2024/sqlite-amalgamation-3450100.zip" 516 + } 517 + }, 518 + "systems": { 519 + "locked": { 520 + "lastModified": 1681028828, 521 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 522 + "owner": "nix-systems", 523 + "repo": "default", 524 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 525 + "type": "github" 526 + }, 527 + "original": { 528 + "owner": "nix-systems", 529 + "repo": "default", 530 + "type": "github" 531 + } 532 + }, 533 + "systems_2": { 534 + "locked": { 535 + "lastModified": 1681028828, 536 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 537 + "owner": "nix-systems", 538 + "repo": "default", 539 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 540 + "type": "github" 541 + }, 542 + "original": { 543 + "owner": "nix-systems", 544 + "repo": "default", 545 + "type": "github" 546 + } 547 + }, 548 + "systems_3": { 549 + "locked": { 550 + "lastModified": 1681028828, 551 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 552 + "owner": "nix-systems", 553 + "repo": "default", 554 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 555 + "type": "github" 556 + }, 557 + "original": { 558 + "owner": "nix-systems", 559 + "repo": "default", 560 + "type": "github" 561 + } 562 + }, 563 + "systems_4": { 564 + "locked": { 565 + "lastModified": 1681028828, 566 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 567 + "owner": "nix-systems", 568 + "repo": "default", 569 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 570 + "type": "github" 571 + }, 572 + "original": { 573 + "owner": "nix-systems", 574 + "repo": "default", 575 + "type": "github" 576 + } 577 + }, 578 + "systems_5": { 579 + "locked": { 580 + "lastModified": 1681028828, 581 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 582 + "owner": "nix-systems", 583 + "repo": "default", 584 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 585 + "type": "github" 586 + }, 587 + "original": { 588 + "owner": "nix-systems", 589 + "repo": "default", 590 + "type": "github" 591 + } 592 + }, 593 + "tangled": { 594 + "inputs": { 595 + "actor-typeahead-src": "actor-typeahead-src", 596 + "flake-compat": "flake-compat_2", 597 + "gomod2nix": "gomod2nix", 598 + "htmx-src": "htmx-src", 599 + "htmx-ws-src": "htmx-ws-src", 600 + "ibm-plex-mono-src": "ibm-plex-mono-src", 601 + "indigo": "indigo", 602 + "inter-fonts-src": "inter-fonts-src", 603 + "lucide-src": "lucide-src", 604 + "nixpkgs": [ 605 + "nixpkgs" 606 + ], 607 + "sqlite-lib-src": "sqlite-lib-src" 608 + }, 609 + "locked": { 610 + "lastModified": 1766504962, 611 + "narHash": "sha256-is2yCWdVQ0S5NY893TfTVFgBUpWkh8SFZd0yy+gsjAU=", 612 + "ref": "refs/heads/master", 613 + "rev": "3596ea2d0f5aba3f13ccf92ceb2560b7fa581364", 614 + "revCount": 1775, 615 + "type": "git", 616 + "url": "https://tangled.org/tangled.org/core" 617 + }, 618 + "original": { 619 + "type": "git", 620 + "url": "https://tangled.org/tangled.org/core" 621 + } 622 + }, 623 + "utils": { 624 + "inputs": { 625 + "systems": "systems_3" 626 + }, 627 + "locked": { 628 + "lastModified": 1731533236, 629 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 630 + "owner": "numtide", 631 + "repo": "flake-utils", 632 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 633 + "type": "github" 634 + }, 635 + "original": { 636 + "owner": "numtide", 637 + "repo": "flake-utils", 638 + "type": "github" 639 + } 640 + }, 641 + "zig2nix": { 642 + "inputs": { 643 + "flake-utils": "flake-utils_3", 644 + "nixpkgs": "nixpkgs_3" 645 + }, 646 + "locked": { 647 + "lastModified": 1764678235, 648 + "narHash": "sha256-NNQWR3DAufaH7fs6ZplfAv1xPHEc0Ne3Z0v4MNHCqSw=", 649 + "owner": "Cloudef", 650 + "repo": "zig2nix", 651 + "rev": "8b6ec85bccdf6b91ded19e9ef671205937e271e6", 652 + "type": "github" 653 + }, 654 + "original": { 655 + "owner": "Cloudef", 656 + "repo": "zig2nix", 657 + "type": "github" 658 + } 659 + }, 660 + "zmx": { 661 + "inputs": { 662 + "zig2nix": "zig2nix" 663 + }, 664 + "locked": { 665 + "lastModified": 1766453911, 666 + "narHash": "sha256-VXpzABEcRdErvL9gcqPUDAnq6Y6NNE0iyKgxLBs0NOk=", 667 + "owner": "neurosnap", 668 + "repo": "zmx", 669 + "rev": "9e8f01eaab92ad842fb691da5ca6816040cdf0ba", 670 + "type": "github" 671 + }, 672 + "original": { 673 + "owner": "neurosnap", 674 + "repo": "zmx", 675 + "type": "github" 676 + } 677 + } 678 + }, 679 + "root": "root", 680 + "version": 7 681 + }
+154
flake.nix
··· 1 + { 2 + description = "@jaspermayone's NixOS and nix-darwin config"; 3 + 4 + inputs = { 5 + # Nixpkgs 6 + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; 7 + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; 8 + 9 + # NixOS hardware configuration 10 + hardware.url = "github:NixOS/nixos-hardware/master"; 11 + 12 + # Home manager 13 + home-manager.url = "github:nix-community/home-manager/release-25.11"; 14 + home-manager.inputs.nixpkgs.follows = "nixpkgs"; 15 + 16 + # Nix-Darwin 17 + nix-darwin.url = "github:nix-darwin/nix-darwin/nix-darwin-25.11"; 18 + nix-darwin.inputs.nixpkgs.follows = "nixpkgs"; 19 + 20 + agenix.url = "github:ryantm/agenix"; 21 + 22 + claude-desktop = { 23 + url = "github:k3d3/claude-desktop-linux-flake"; 24 + inputs.nixpkgs.follows = "nixpkgs"; 25 + }; 26 + 27 + import-tree.url = "github:vic/import-tree"; 28 + 29 + nur = { 30 + url = "github:nix-community/NUR"; 31 + inputs.nixpkgs.follows = "nixpkgs"; 32 + }; 33 + 34 + deploy-rs = { 35 + url = "github:serokell/deploy-rs"; 36 + inputs.nixpkgs.follows = "nixpkgs"; 37 + }; 38 + 39 + tangled = { 40 + url = "git+https://tangled.org/tangled.org/core"; 41 + inputs.nixpkgs.follows = "nixpkgs"; 42 + }; 43 + 44 + zmx = { 45 + url = "github:neurosnap/zmx"; 46 + }; 47 + }; 48 + 49 + outputs = { 50 + self, 51 + nixpkgs, 52 + nixpkgs-unstable, 53 + agenix, 54 + home-manager, 55 + nur, 56 + nix-darwin, 57 + deploy-rs, 58 + tangled, 59 + ... 60 + }@inputs: 61 + let 62 + outputs = inputs.self.outputs; 63 + 64 + # Overlay to make unstable packages available as pkgs.unstable.* 65 + # Also includes custom packages 66 + unstable-overlays = { 67 + nixpkgs.overlays = [ 68 + (final: prev: { 69 + unstable = import nixpkgs-unstable { 70 + system = final.stdenv.hostPlatform.system; 71 + config.allowUnfree = true; 72 + }; 73 + 74 + # Custom packages 75 + zmx-binary = prev.callPackage ./packages/zmx.nix { }; 76 + }) 77 + ]; 78 + }; 79 + 80 + # Helper function to create NixOS configurations 81 + mkNixos = hostname: system: nixpkgs.lib.nixosSystem { 82 + inherit system; 83 + specialArgs = { inherit inputs outputs hostname; }; 84 + modules = [ 85 + ./hosts/${hostname}/configuration.nix 86 + agenix.nixosModules.default 87 + unstable-overlays 88 + nur.modules.nixos.default 89 + home-manager.nixosModules.home-manager 90 + { 91 + home-manager.useGlobalPkgs = true; 92 + home-manager.useUserPackages = true; 93 + home-manager.backupFileExtension = "backup"; 94 + home-manager.extraSpecialArgs = { inherit inputs outputs hostname; isDarwin = false; }; 95 + home-manager.users.jsp = import ./home; 96 + } 97 + ]; 98 + }; 99 + 100 + # Helper function to create Darwin configurations 101 + mkDarwin = hostname: system: nix-darwin.lib.darwinSystem { 102 + inherit system; 103 + specialArgs = { inherit inputs outputs hostname; }; 104 + modules = [ 105 + ./darwin 106 + ./hosts/${hostname} 107 + agenix.darwinModules.default 108 + unstable-overlays 109 + nur.modules.darwin.default 110 + home-manager.darwinModules.home-manager 111 + { 112 + home-manager.useGlobalPkgs = true; 113 + home-manager.useUserPackages = true; 114 + home-manager.backupFileExtension = "backup"; 115 + home-manager.extraSpecialArgs = { inherit inputs outputs hostname; isDarwin = true; }; 116 + home-manager.users.jsp = import ./home; 117 + } 118 + ]; 119 + }; 120 + in 121 + { 122 + # NixOS configurations 123 + # Available through 'nixos-rebuild --flake .#hostname' 124 + nixosConfigurations = { 125 + alastor = mkNixos "alastor" "x86_64-linux"; 126 + }; 127 + 128 + # Darwin configurations 129 + # Available through 'darwin-rebuild switch --flake .#hostname' 130 + darwinConfigurations = { 131 + remus = mkDarwin "remus" "aarch64-darwin"; 132 + }; 133 + 134 + # Formatters 135 + formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-tree; 136 + formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.nixfmt-tree; 137 + 138 + # Deploy-rs configurations 139 + # Available through 'deploy .#alastor' 140 + deploy.nodes = { 141 + alastor = { 142 + hostname = "alastor"; 143 + profiles.system = { 144 + sshUser = "jsp"; 145 + user = "root"; 146 + path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.alastor; 147 + }; 148 + }; 149 + }; 150 + 151 + # Validation checks for deploy-rs 152 + checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib; 153 + }; 154 + }
+123
home/default.nix
··· 1 + # Home Manager configuration 2 + # Shared between NixOS and Darwin 3 + { config, pkgs, lib, hostname, isDarwin, ... }: 4 + 5 + { 6 + imports = [ 7 + ../profiles/bore.nix 8 + ../modules/shell.nix 9 + ../modules/ssh.nix 10 + ../modules/git.nix 11 + ]; 12 + 13 + home.stateVersion = "24.05"; 14 + 15 + # Let Home Manager manage itself 16 + programs.home-manager.enable = true; 17 + 18 + home.username = "jsp"; 19 + home.homeDirectory = lib.mkForce (if isDarwin then "/Users/jsp" else "/home/jsp"); 20 + 21 + # Environment variables 22 + home.sessionVariables = { 23 + EDITOR = "vim"; 24 + VISUAL = "vim"; 25 + PAGER = "less"; 26 + }; 27 + 28 + # Vim configuration 29 + programs.vim = { 30 + enable = true; 31 + defaultEditor = true; 32 + settings = { 33 + number = true; 34 + relativenumber = true; 35 + tabstop = 2; 36 + shiftwidth = 2; 37 + expandtab = true; 38 + }; 39 + extraConfig = '' 40 + set nocompatible 41 + syntax on 42 + set encoding=utf-8 43 + set autoindent 44 + set smartindent 45 + set hlsearch 46 + set incsearch 47 + set ignorecase 48 + set smartcase 49 + set backspace=indent,eol,start 50 + 51 + " Strip trailing whitespace on save 52 + autocmd BufWritePre * :%s/\s\+$//e 53 + ''; 54 + }; 55 + 56 + # Direnv for per-project environments 57 + programs.direnv = { 58 + enable = true; 59 + enableZshIntegration = true; 60 + nix-direnv.enable = true; 61 + }; 62 + 63 + # RC files from ../rc/ directory (each file is linked as-is to ~/) 64 + home.file = builtins.listToAttrs ( 65 + map (name: { 66 + name = name; 67 + value = { source = ../rc/${name}; }; 68 + }) (builtins.attrNames (builtins.readDir ../rc)) 69 + ); 70 + 71 + home.packages = with pkgs; [ 72 + eza 73 + ]; 74 + 75 + # Git configuration 76 + jsp.git.enable = true; 77 + 78 + # SSH configuration 79 + jsp.ssh = { 80 + enable = true; 81 + zmx.enable = true; 82 + 83 + hosts = { 84 + # Default settings for all hosts 85 + "*" = { 86 + addKeysToAgent = "yes"; 87 + }; 88 + 89 + # Alastor (tunnel server, named after Mad-Eye Moody) 90 + alastor = { 91 + hostname = "tun.hogwarts.channel"; 92 + user = "jsp"; 93 + identityFile = "~/.ssh/id_ed25519"; 94 + zmx = true; # auto-attach zmx session 95 + }; 96 + 97 + # Proxmox and VMs 98 + pve = { 99 + hostname = "10.100.0.222"; 100 + user = "root"; 101 + zmx = true; 102 + }; 103 + 104 + proxy = { 105 + hostname = "10.100.0.229"; 106 + user = "jasper"; 107 + zmx = true; 108 + }; 109 + 110 + # GitHub 111 + "github.com" = { 112 + hostname = "github.com"; 113 + user = "git"; 114 + identityFile = "~/.ssh/id_ed25519"; 115 + identitiesOnly = true; 116 + extraOptions = { 117 + PubkeyAcceptedAlgorithms = "+ssh-ed25519"; 118 + HostkeyAlgorithms = "+ssh-ed25519"; 119 + }; 120 + }; 121 + }; 122 + }; 123 + }
+198
hosts/alastor/configuration.nix
··· 1 + # Alastor - NixOS server running frp tunnel service (named after Mad-Eye Moody) 2 + { config, pkgs, lib, inputs, hostname, ... }: 3 + 4 + { 5 + imports = [ 6 + ./hardware-configuration.nix 7 + ../../modules/frps 8 + ../../modules/status 9 + ../../modules/knot/sync.nix 10 + inputs.tangled.nixosModules.knot 11 + ]; 12 + 13 + # System version 14 + system.stateVersion = "24.05"; 15 + 16 + # Hostname 17 + networking.hostName = hostname; 18 + 19 + # Nix settings 20 + nix = { 21 + settings.experimental-features = [ "nix-command" "flakes" ]; 22 + optimise.automatic = true; 23 + }; 24 + 25 + # Allow unfree packages 26 + nixpkgs.config.allowUnfree = true; 27 + 28 + # Timezone 29 + time.timeZone = "America/New_York"; 30 + 31 + # Locale 32 + i18n.defaultLocale = "en_US.UTF-8"; 33 + 34 + # Basic packages 35 + environment.systemPackages = with pkgs; [ 36 + vim 37 + git 38 + htop 39 + curl 40 + jq 41 + tmux 42 + inputs.agenix.packages.${pkgs.system}.default # agenix CLI 43 + ]; 44 + 45 + # NH - NixOS helper 46 + programs.nh = { 47 + enable = true; 48 + clean.enable = true; 49 + clean.extraArgs = "--keep-since 4d --keep 3"; 50 + flake = "/home/jsp/dots"; 51 + }; 52 + 53 + # Enable SSH 54 + services.openssh = { 55 + enable = true; 56 + settings = { 57 + PasswordAuthentication = false; 58 + PermitRootLogin = "prohibit-password"; 59 + KbdInteractiveAuthentication = false; 60 + }; 61 + }; 62 + 63 + # Fail2ban for SSH protection 64 + services.fail2ban = { 65 + enable = true; 66 + maxretry = 5; 67 + }; 68 + 69 + # Tailscale VPN 70 + services.tailscale = { 71 + enable = true; 72 + useRoutingFeatures = "client"; 73 + }; 74 + 75 + # User account 76 + users.users.jsp = { 77 + isNormalUser = true; 78 + extraGroups = [ "wheel" ]; 79 + shell = pkgs.zsh; 80 + openssh.authorizedKeys.keys = [ 81 + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHm7lo7umraewipgQu1Pifmoo/V8jYGDHjBTmt+7SOCe jsp@remus" 82 + ]; 83 + }; 84 + 85 + # Enable zsh system-wide 86 + programs.zsh.enable = true; 87 + 88 + # Sudo without password for wheel group 89 + security.sudo.wheelNeedsPassword = false; 90 + 91 + # Agenix secrets 92 + age.secrets = { 93 + frps-token = { 94 + file = ../../secrets/frps-token.age; 95 + mode = "400"; 96 + }; 97 + cloudflare-credentials = { 98 + file = ../../secrets/cloudflare-credentials.age; 99 + mode = "400"; 100 + }; 101 + bore-token = { 102 + file = ../../secrets/bore-token.age; 103 + mode = "400"; 104 + owner = "jsp"; 105 + }; 106 + github-token = { 107 + file = ../../secrets/github-token.age; 108 + mode = "400"; 109 + owner = "git"; # tangled uses git user 110 + }; 111 + }; 112 + 113 + # FRP tunnel server 114 + atelier.services.frps = { 115 + enable = true; 116 + domain = "tun.hogwarts.channel"; 117 + bindPort = 7000; 118 + vhostHTTPPort = 7080; 119 + authTokenFile = config.age.secrets.frps-token.path; 120 + enableCaddy = true; 121 + }; 122 + 123 + # Status monitoring (served on alastor.hogwarts.channel) 124 + atelier.services.status = { 125 + enable = true; 126 + hostname = "alastor"; 127 + domain = "alastor.hogwarts.channel"; 128 + services = [ "frps" "caddy" "tailscaled" "tangled-knot" ]; 129 + cloudflareCredentialsFile = config.age.secrets.cloudflare-credentials.path; 130 + }; 131 + 132 + # Tangled Knot server (official module) 133 + services.tangled.knot = { 134 + enable = true; 135 + package = inputs.tangled.packages.x86_64-linux.knot; 136 + server = { 137 + owner = "did:plc:krxbvxvis5skq7jj6eot23ul"; 138 + hostname = "knot.jaspermayone.com"; 139 + listenAddr = "127.0.0.1:5555"; 140 + }; 141 + }; 142 + 143 + # Knot to GitHub sync service 144 + jsp.services.knot-sync = { 145 + enable = true; 146 + repoDir = "/var/lib/knot/repos/did:plc:krxbvxvis5skq7jj6eot23ul"; 147 + secretsFile = config.age.secrets.github-token.path; 148 + }; 149 + 150 + # Caddy reverse proxy 151 + services.caddy = { 152 + enable = true; 153 + virtualHosts."knot.jaspermayone.com" = { 154 + extraConfig = '' 155 + tls { 156 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 157 + } 158 + header { 159 + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 160 + } 161 + reverse_proxy localhost:5555 { 162 + header_up X-Forwarded-Proto {scheme} 163 + header_up X-Forwarded-For {remote} 164 + } 165 + ''; 166 + }; 167 + # Reverse proxy for remus via Tailscale 168 + virtualHosts."remus.hogwarts.channel" = { 169 + extraConfig = '' 170 + tls { 171 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 172 + } 173 + reverse_proxy remus:80 { 174 + header_up X-Forwarded-Proto {scheme} 175 + header_up X-Forwarded-For {remote} 176 + } 177 + ''; 178 + }; 179 + }; 180 + 181 + systemd.services.caddy.serviceConfig.EnvironmentFile = config.age.secrets.cloudflare-credentials.path; 182 + 183 + networking.firewall.allowedTCPPorts = [ 80 443 2222 ]; # 2222 for knot SSH 184 + 185 + # Automatic garbage collection 186 + nix.gc = { 187 + automatic = true; 188 + dates = "weekly"; 189 + options = "--delete-older-than 30d"; 190 + }; 191 + 192 + # Automatic updates (optional) 193 + # system.autoUpgrade = { 194 + # enable = true; 195 + # flake = "github:jaspermayone/dots#alastor"; 196 + # dates = "04:00"; 197 + # }; 198 + }
+16
hosts/alastor/hardware-configuration.nix
··· 1 + # Placeholder - generate on actual server with: 2 + # nixos-generate-config --show-hardware-config > hardware-configuration.nix 3 + { config, lib, pkgs, modulesPath, ... }: 4 + 5 + { 6 + imports = [ ]; 7 + 8 + # This will be overwritten when deployed to actual hardware 9 + boot.loader.grub.enable = true; 10 + boot.loader.grub.device = "/dev/sda"; 11 + 12 + fileSystems."/" = { 13 + device = "/dev/sda1"; 14 + fsType = "ext4"; 15 + }; 16 + }
hosts/connector/configuration.nix

This is a binary file and will not be displayed.

hosts/connector/hardware-configuration.nix

This is a binary file and will not be displayed.

+30
hosts/remus/default.nix
··· 1 + # Remus - MacBook Pro M4 2 + { config, pkgs, lib, inputs, hostname, ... }: 3 + 4 + { 5 + # Host-specific overrides go here 6 + # Most configuration is inherited from darwin/default.nix and home/default.nix 7 + 8 + # Agenix secrets for bore client 9 + age.secrets.bore-token = { 10 + file = ../../secrets/bore-token.age; 11 + path = "/Users/jsp/.config/bore/token"; 12 + owner = "jsp"; 13 + mode = "400"; 14 + }; 15 + 16 + # Remus-specific homebrew casks 17 + homebrew.casks = [ 18 + # Add Mac apps specific to your laptop 19 + # "raycast" 20 + # "arc" 21 + # "1password" 22 + ]; 23 + 24 + # Any remus-specific system defaults 25 + # system.defaults = { }; 26 + 27 + # Set the hostname 28 + networking.hostName = "remus"; 29 + networking.computerName = "Remus"; 30 + }
+2 -2
modules/bore/bore.1.md
··· 120 120 121 121 # SEE ALSO 122 122 123 - Dashboard: https://tun.hogwarts.dev 123 + Dashboard: https://bore.dunkirk.sh 124 124 125 125 # BUGS 126 126 127 - Report bugs at: https://github.com/jaspermayone/dots/issues 127 + Report bugs at: https://github.com/yourusername/dots/issues 128 128 129 129 # AUTHORS 130 130
+38
modules/bore/completions/bore.bash
··· 1 + # bash completion for bore 2 + 3 + _bore_completion() { 4 + local cur prev opts 5 + COMPREPLY=() 6 + cur="${COMP_WORDS[COMP_CWORD]}" 7 + prev="${COMP_WORDS[COMP_CWORD-1]}" 8 + opts="--list --saved --protocol --label --save -l -s -p" 9 + 10 + # Complete flags 11 + if [[ ${cur} == -* ]]; then 12 + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 13 + return 0 14 + fi 15 + 16 + # Complete protocol values after --protocol or -p 17 + if [[ ${prev} == "--protocol" ]] || [[ ${prev} == "-p" ]]; then 18 + COMPREPLY=( $(compgen -W "http tcp udp" -- ${cur}) ) 19 + return 0 20 + fi 21 + 22 + # Complete label value after --label or -l 23 + if [[ ${prev} == "--label" ]] || [[ ${prev} == "-l" ]]; then 24 + # Could potentially read from bore.toml for label suggestions 25 + return 0 26 + fi 27 + 28 + # Complete saved tunnel names as first argument 29 + if [[ ${COMP_CWORD} -eq 1 ]] && [[ -f "bore.toml" ]]; then 30 + local tunnels=$(grep '^\[' bore.toml | sed 's/^\[\(.*\)\]$/\1/') 31 + COMPREPLY=( $(compgen -W "${tunnels}" -- ${cur}) ) 32 + return 0 33 + fi 34 + 35 + return 0 36 + } 37 + 38 + complete -F _bore_completion bore
+21
modules/bore/completions/bore.fish
··· 1 + # fish completion for bore 2 + 3 + # Helper function to get saved tunnel names 4 + function __bore_saved_tunnels 5 + if test -f bore.toml 6 + grep '^\[' bore.toml | sed 's/^\[\(.*\)\]$/\1/' 7 + end 8 + end 9 + 10 + # Complete flags 11 + complete -c bore -s l -l list -d 'List active tunnels' 12 + complete -c bore -s s -l saved -d 'List saved tunnels from bore.toml' 13 + complete -c bore -s p -l protocol -d 'Specify protocol' -xa 'http tcp udp' 14 + complete -c bore -l label -d 'Assign a label to the tunnel' -r 15 + complete -c bore -l save -d 'Save tunnel configuration to bore.toml' 16 + 17 + # Complete subdomain from saved tunnels (first argument) 18 + complete -c bore -n '__fish_is_first_token' -a '(__bore_saved_tunnels)' -d 'Saved tunnel' 19 + 20 + # Port is always a number (second argument) 21 + complete -c bore -n 'test (count (commandline -opc)) -eq 2' -d 'Local port'
+30 -42
modules/bore/default.nix
··· 323 323 # Build proxy configuration based on protocol 324 324 if [ "$protocol" = "http" ]; then 325 325 ${pkgs.coreutils}/bin/cat > $config_file <<EOF 326 - serverAddr = "${cfg.serverAddr}" 327 - serverPort = ${toString cfg.serverPort} 326 + serverAddr = "${cfg.serverAddr}" 327 + serverPort = ${toString cfg.serverPort} 328 328 329 - auth.method = "token" 330 - auth.tokenSource.type = "file" 331 - auth.tokenSource.file.path = "${cfg.authTokenFile}" 329 + auth.method = "token" 330 + auth.tokenSource.type = "file" 331 + auth.tokenSource.file.path = "${cfg.authTokenFile}" 332 332 333 - [[proxies]] 334 - name = "$proxy_name" 335 - type = "http" 336 - localIP = "127.0.0.1" 337 - localPort = $port 338 - subdomain = "$tunnel_name" 339 - EOF 333 + [[proxies]] 334 + name = "$proxy_name" 335 + type = "http" 336 + localIP = "127.0.0.1" 337 + localPort = $port 338 + subdomain = "$tunnel_name" 339 + EOF 340 340 elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then 341 341 # For TCP/UDP, enable admin API to query allocated port 342 - # Use Python to find a free port (cross-platform and guaranteed to work) 343 342 admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()') 344 343 345 344 ${pkgs.coreutils}/bin/cat > $config_file <<EOF 346 - serverAddr = "${cfg.serverAddr}" 347 - serverPort = ${toString cfg.serverPort} 345 + serverAddr = "${cfg.serverAddr}" 346 + serverPort = ${toString cfg.serverPort} 348 347 349 - auth.method = "token" 350 - auth.tokenSource.type = "file" 351 - auth.tokenSource.file.path = "${cfg.authTokenFile}" 348 + auth.method = "token" 349 + auth.tokenSource.type = "file" 350 + auth.tokenSource.file.path = "${cfg.authTokenFile}" 352 351 353 - webServer.addr = "127.0.0.1" 354 - webServer.port = $admin_port 352 + webServer.addr = "127.0.0.1" 353 + webServer.port = $admin_port 355 354 356 - [[proxies]] 357 - name = "$proxy_name" 358 - type = "$protocol" 359 - localIP = "127.0.0.1" 360 - localPort = $port 361 - remotePort = 0 362 - EOF 355 + [[proxies]] 356 + name = "$proxy_name" 357 + type = "$protocol" 358 + localIP = "127.0.0.1" 359 + localPort = $port 360 + remotePort = 0 361 + EOF 363 362 else 364 363 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)" 365 364 exit 1 ··· 382 381 383 382 # For TCP/UDP, capture output to parse allocated port 384 383 if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then 385 - # Start frpc in background and capture its PID 386 384 ${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do 387 385 echo "$line" 388 386 389 387 # Look for successful proxy start 390 388 if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then 391 - # Wait a moment for the proxy to fully initialize 392 389 sleep 1 393 390 394 - # Query the frpc admin API for proxy status 395 391 proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}") 396 392 397 - # Try to extract remote port from JSON response 398 - # Format: "remote_addr":"tun.hogwarts.dev:20097" 399 393 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null) 400 394 if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then 401 395 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null) 402 396 fi 403 397 404 - # Extract just the port number 405 398 remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$') 406 399 407 400 if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then ··· 428 421 nativeBuildInputs = with pkgs; [ pandoc installShellFiles ]; 429 422 430 423 manPageSrc = ./bore.1.md; 431 - bashCompletionSrc = ./completions/bore.bash; 432 424 zshCompletionSrc = ./completions/bore.zsh; 433 - fishCompletionSrc = ./completions/bore.fish; 434 425 435 426 buildPhase = '' 436 - # Convert markdown man page to man format 437 427 ${pkgs.pandoc}/bin/pandoc -s -t man $manPageSrc -o bore.1 438 428 ''; 439 429 ··· 448 438 installManPage bore.1 449 439 450 440 # Install completions 451 - installShellCompletion --bash --name bore $bashCompletionSrc 452 441 installShellCompletion --zsh --name _bore $zshCompletionSrc 453 - installShellCompletion --fish --name bore.fish $fishCompletionSrc 454 442 ''; 455 443 456 444 meta = with lib; { 457 445 description = "Secure tunneling service CLI"; 458 - homepage = "https://tun.hogwarts.dev"; 446 + homepage = "https://tun.hogwarts.channel"; 459 447 license = licenses.mit; 460 448 maintainers = [ ]; 461 449 }; ··· 467 455 468 456 serverAddr = lib.mkOption { 469 457 type = lib.types.str; 470 - default = "tun.hogwarts.dev"; 458 + default = "tun.hogwarts.channel"; 471 459 description = "bore server address"; 472 460 }; 473 461 ··· 479 467 480 468 domain = lib.mkOption { 481 469 type = lib.types.str; 482 - default = "tun.hogwarts.dev"; 470 + default = "tun.hogwarts.channel"; 483 471 description = "Domain for public tunnel URLs"; 484 472 }; 485 473 ··· 496 484 bore 497 485 ]; 498 486 }; 499 - } 487 + }
+97
modules/frps/404.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>404 - bore</title> 8 + <link rel="icon" 9 + href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚇</text></svg>"> 10 + <style> 11 + * { 12 + margin: 0; 13 + padding: 0; 14 + box-sizing: border-box; 15 + } 16 + 17 + body { 18 + font-family: 'SF Mono', 'Monaco', monospace; 19 + background: #0d1117; 20 + color: #e6edf3; 21 + min-height: 100vh; 22 + display: flex; 23 + align-items: center; 24 + justify-content: center; 25 + padding: 2rem; 26 + } 27 + 28 + .container { 29 + text-align: center; 30 + max-width: 600px; 31 + } 32 + 33 + .error-code { 34 + font-size: 8rem; 35 + font-weight: 700; 36 + color: #8b949e; 37 + line-height: 1; 38 + margin-bottom: 1rem; 39 + } 40 + 41 + .emoji { 42 + font-size: 4rem; 43 + margin-bottom: 2rem; 44 + opacity: 0.5; 45 + } 46 + 47 + h1 { 48 + font-size: 2rem; 49 + margin-bottom: 1rem; 50 + color: #e6edf3; 51 + } 52 + 53 + p { 54 + color: #8b949e; 55 + font-size: 1rem; 56 + margin-bottom: 2rem; 57 + line-height: 1.6; 58 + } 59 + 60 + .countdown { 61 + color: #14b8a6; 62 + font-size: 1.2rem; 63 + font-weight: 600; 64 + font-variant-numeric: tabular-nums; 65 + } 66 + </style> 67 + </head> 68 + 69 + <body> 70 + <div class="container"> 71 + <div class="emoji">🚇</div> 72 + <div class="error-code">404</div> 73 + <h1>wrong stop!</h1> 74 + <p>this tunnel doesn't go anywhere. maybe it was never built, or perhaps it collapsed?</p> 75 + <p>redirecting in <span class="countdown" id="countdown">5.000</span>s</p> 76 + </div> 77 + 78 + <script> 79 + let timeLeft = 5000; // 5 seconds in milliseconds 80 + const countdownEl = document.getElementById('countdown'); 81 + const startTime = Date.now(); 82 + 83 + const interval = setInterval(() => { 84 + const elapsed = Date.now() - startTime; 85 + timeLeft = Math.max(0, 5000 - elapsed); 86 + 87 + countdownEl.textContent = (timeLeft / 1000).toFixed(3); 88 + 89 + if (timeLeft <= 0) { 90 + clearInterval(interval); 91 + window.location.href = 'https://tun.hogwarts.channel'; 92 + } 93 + }, 10); // Update every 10ms for smooth countdown 94 + </script> 95 + </body> 96 + 97 + </html>
+627
modules/frps/dashboard.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>bore</title> 8 + <meta name="description" content="bore - secure tunneling service for exposing local services to the internet"> 9 + <meta property="og:title" content="bore - tunnel dashboard"> 10 + <meta property="og:description" content="secure tunneling service powered by frp on tun.hogwarts.channel"> 11 + <meta property="og:type" content="website"> 12 + <meta property="og:url" content="https://tun.hogwarts.channel"> 13 + <meta name="twitter:card" content="summary"> 14 + <meta name="twitter:title" content="bore - tunnel dashboard"> 15 + <meta name="twitter:description" content="secure tunneling service powered by frp on tun.hogwarts.channel"> 16 + <link rel="icon" 17 + href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚇</text></svg>"> 18 + <style> 19 + * { 20 + margin: 0; 21 + padding: 0; 22 + box-sizing: border-box; 23 + } 24 + 25 + body { 26 + font-family: 'SF Mono', 'Monaco', monospace; 27 + background: #0d1117; 28 + color: #e6edf3; 29 + padding: 2rem; 30 + line-height: 1.5; 31 + min-height: 100vh; 32 + display: flex; 33 + flex-direction: column; 34 + } 35 + 36 + body.loading .container { 37 + opacity: 0; 38 + } 39 + 40 + .loading-bar { 41 + position: fixed; 42 + top: 0; 43 + left: 0; 44 + width: 100%; 45 + height: 3px; 46 + background: transparent; 47 + z-index: 9999; 48 + overflow: hidden; 49 + } 50 + 51 + .loading-bar::before { 52 + content: ''; 53 + position: absolute; 54 + top: 0; 55 + left: 0; 56 + width: 100%; 57 + height: 100%; 58 + background: linear-gradient(90deg, #14b8a6, #fb923c); 59 + animation: loading 1.5s ease-in-out infinite; 60 + } 61 + 62 + body:not(.loading) .loading-bar { 63 + display: none; 64 + } 65 + 66 + @keyframes loading { 67 + 0% { 68 + transform: translateX(-100%); 69 + } 70 + 50% { 71 + transform: translateX(0%); 72 + } 73 + 100% { 74 + transform: translateX(100%); 75 + } 76 + } 77 + 78 + .container { 79 + max-width: 1200px; 80 + margin: 0 auto; 81 + flex: 1; 82 + width: 100%; 83 + opacity: 1; 84 + transition: opacity 0.3s ease-in-out; 85 + } 86 + 87 + header { 88 + margin-bottom: 3rem; 89 + } 90 + 91 + h1 { 92 + font-size: 2.5rem; 93 + background: linear-gradient(135deg, #14b8a6, #fb923c); 94 + -webkit-background-clip: text; 95 + -webkit-text-fill-color: transparent; 96 + margin-bottom: 0.5rem; 97 + } 98 + 99 + .subtitle { 100 + color: #8b949e; 101 + font-size: 0.95rem; 102 + } 103 + 104 + .stats { 105 + display: grid; 106 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 107 + gap: 1rem; 108 + margin-bottom: 2rem; 109 + } 110 + 111 + .stat-card { 112 + background: #161b22; 113 + border: 1px solid #30363d; 114 + border-radius: 0; 115 + padding: 1.5rem; 116 + } 117 + 118 + .stat-label { 119 + color: #8b949e; 120 + font-size: 0.85rem; 121 + margin-bottom: 0.5rem; 122 + } 123 + 124 + .stat-value { 125 + font-size: 2rem; 126 + font-weight: 600; 127 + color: #14b8a6; 128 + } 129 + 130 + .stat-value.orange { 131 + color: #fb923c; 132 + } 133 + 134 + .section { 135 + background: #161b22; 136 + border: 1px solid #30363d; 137 + border-radius: 0; 138 + padding: 1.5rem; 139 + margin-bottom: 2rem; 140 + } 141 + 142 + h2 { 143 + color: #14b8a6; 144 + font-size: 1.2rem; 145 + margin-bottom: 1.5rem; 146 + display: flex; 147 + align-items: center; 148 + gap: 0.5rem; 149 + } 150 + 151 + .tunnel-list { 152 + display: flex; 153 + flex-direction: column; 154 + gap: 1rem; 155 + } 156 + 157 + .tunnel { 158 + background: #0d1117; 159 + border: 1px solid #30363d; 160 + border-radius: 0; 161 + padding: 1rem; 162 + display: grid; 163 + grid-template-columns: 1fr auto; 164 + gap: 1rem; 165 + align-items: center; 166 + } 167 + 168 + .tunnel-icon { 169 + display: none; 170 + } 171 + 172 + .tunnel-info { 173 + flex: 1; 174 + } 175 + 176 + .tunnel-name { 177 + color: #e6edf3; 178 + font-weight: 600; 179 + margin-bottom: 0.25rem; 180 + display: flex; 181 + align-items: center; 182 + gap: 0.5rem; 183 + } 184 + 185 + .tunnel-label { 186 + display: inline-block; 187 + padding: 0.125rem 0.5rem; 188 + font-size: 0.7rem; 189 + font-weight: 500; 190 + border-radius: 0; 191 + margin-left: 0.25rem; 192 + border: 1px solid; 193 + } 194 + 195 + .tunnel-url { 196 + color: #8b949e; 197 + font-size: 0.85rem; 198 + } 199 + 200 + .tunnel-url a { 201 + color: #14b8a6; 202 + text-decoration: none; 203 + } 204 + 205 + .tunnel-url a:hover { 206 + text-decoration: underline; 207 + } 208 + 209 + .tunnel-status { 210 + padding: 0.25rem 0.75rem; 211 + border-radius: 0; 212 + font-size: 0.8rem; 213 + font-weight: 500; 214 + } 215 + 216 + .status-online { 217 + background: rgba(20, 184, 166, 0.2); 218 + color: #14b8a6; 219 + border: 1px solid #14b8a6; 220 + } 221 + 222 + .empty-state { 223 + text-align: center; 224 + padding: 3rem 1rem; 225 + color: #8b949e; 226 + } 227 + 228 + .empty-icon { 229 + font-size: 3rem; 230 + margin-bottom: 1rem; 231 + opacity: 0.5; 232 + } 233 + 234 + code { 235 + background: #0d1117; 236 + padding: 0.2rem 0.5rem; 237 + border-radius: 0; 238 + color: #fb923c; 239 + font-size: 0.9rem; 240 + } 241 + 242 + .usage { 243 + background: #0d1117; 244 + padding: 1rem; 245 + border-radius: 0; 246 + margin-top: 1rem; 247 + } 248 + 249 + .usage pre { 250 + color: #8b949e; 251 + font-size: 0.9rem; 252 + overflow-x: auto; 253 + } 254 + 255 + .last-updated { 256 + text-align: center; 257 + color: #8b949e; 258 + font-size: 0.8rem; 259 + margin-top: 2rem; 260 + padding: 2rem 0; 261 + } 262 + 263 + .last-updated a { 264 + color: #14b8a6; 265 + text-decoration: none; 266 + } 267 + 268 + .last-updated a:hover { 269 + text-decoration: underline; 270 + } 271 + 272 + .offline-tunnels { 273 + margin-top: 1.5rem; 274 + padding-top: 1.5rem; 275 + border-top: 1px solid #30363d; 276 + } 277 + 278 + .offline-tunnel { 279 + padding: 0.5rem 0; 280 + color: #8b949e; 281 + font-size: 0.85rem; 282 + display: flex; 283 + justify-content: space-between; 284 + align-items: center; 285 + } 286 + 287 + .offline-tunnel-name { 288 + opacity: 0.6; 289 + } 290 + 291 + .offline-tunnel-stats { 292 + font-size: 0.75rem; 293 + opacity: 0.5; 294 + } 295 + </style> 296 + </head> 297 + 298 + <body class="loading"> 299 + <div class="loading-bar"></div> 300 + <main class="container"> 301 + <header> 302 + <h1>🚇 bore</h1> 303 + <p class="subtitle">fancy tunnels @ hogwarts</p> 304 + </header> 305 + 306 + <div class="stats"> 307 + <div class="stat-card"> 308 + <div class="stat-label">active tunnels</div> 309 + <div class="stat-value" id="activeTunnels">—</div> 310 + </div> 311 + <div class="stat-card"> 312 + <div class="stat-label">active connections</div> 313 + <div class="stat-value" id="totalConnections">—</div> 314 + </div> 315 + <div class="stat-card"> 316 + <div class="stat-label">server status</div> 317 + <div class="stat-value orange" id="serverStatus">—</div> 318 + </div> 319 + <div class="stat-card"> 320 + <div class="stat-label">total upload</div> 321 + <div class="stat-value" id="totalUpload">—</div> 322 + </div> 323 + <div class="stat-card"> 324 + <div class="stat-label">total download</div> 325 + <div class="stat-value" id="totalDownload">—</div> 326 + </div> 327 + </div> 328 + 329 + <section class="section"> 330 + <h2>~boreholes</h2> 331 + <div class="tunnel-list" id="tunnelList"> 332 + <div class="empty-state"> 333 + <div class="empty-icon">🚇</div> 334 + <p>no active tunnels</p> 335 + </div> 336 + </div> 337 + </section> 338 + </main> 339 + 340 + <footer class="last-updated"> 341 + last updated: <span id="lastUpdated">never</span><br> 342 + made with ♥︎ by <a href="https://jaspermayone.com" target="_blank">jasper mayone</a> 343 + (based on work by <a href="https://dunkirk.sh" target="_blank">kieran klukas</a>) 344 + </footer> 345 + 346 + <script> 347 + let fetchFailCount = 0; 348 + const MAX_FAIL_COUNT = 3; 349 + let lastProxiesState = null; 350 + 351 + // Predefined color palette for labels 352 + const labelColors = [ 353 + { color: '#a78bfa', bg: 'rgba(167, 139, 250, 0.2)' }, // purple 354 + { color: '#f472b6', bg: 'rgba(244, 114, 182, 0.2)' }, // pink 355 + { color: '#facc15', bg: 'rgba(250, 204, 21, 0.2)' }, // yellow 356 + { color: '#60a5fa', bg: 'rgba(96, 165, 250, 0.2)' }, // blue 357 + { color: '#f87171', bg: 'rgba(248, 113, 113, 0.2)' }, // red 358 + { color: '#38bdf8', bg: 'rgba(56, 189, 248, 0.2)' }, // sky 359 + { color: '#c084fc', bg: 'rgba(192, 132, 252, 0.2)' }, // violet 360 + { color: '#fb7185', bg: 'rgba(251, 113, 133, 0.2)' }, // rose 361 + ]; 362 + 363 + // Hash string to index 364 + function stringToColorIndex(str) { 365 + let hash = 0; 366 + for (let i = 0; i < str.length; i++) { 367 + hash = str.charCodeAt(i) + ((hash << 5) - hash); 368 + } 369 + return Math.abs(hash) % labelColors.length; 370 + } 371 + 372 + // Get label color and styles 373 + function getLabelStyle(label) { 374 + const trimmedLabel = label.trim(); 375 + if (trimmedLabel === 'prod') { 376 + return { 377 + color: '#22c55e', 378 + bgColor: 'rgba(34, 197, 94, 0.2)', 379 + borderColor: '#22c55e' 380 + }; 381 + } 382 + 383 + if (trimmedLabel === 'dev') { 384 + return { 385 + color: '#fb923c', 386 + bgColor: 'rgba(251, 146, 60, 0.2)', 387 + borderColor: '#fb923c' 388 + }; 389 + } 390 + 391 + const colorIndex = stringToColorIndex(trimmedLabel); 392 + const colorScheme = labelColors[colorIndex]; 393 + return { 394 + color: colorScheme.color, 395 + bgColor: colorScheme.bg, 396 + borderColor: colorScheme.color 397 + }; 398 + } 399 + 400 + async function fetchStats() { 401 + try { 402 + // Fetch server info 403 + const serverResponse = await fetch('/api/serverinfo'); 404 + if (!serverResponse.ok) throw new Error('API unavailable'); 405 + const serverData = await serverResponse.json(); 406 + 407 + // Fetch HTTP proxies (tunnels) 408 + const proxiesResponse = await fetch('/api/proxy/http'); 409 + const proxiesData = await proxiesResponse.json(); 410 + 411 + // Reset fail count on success 412 + fetchFailCount = 0; 413 + 414 + // Update stats 415 + document.getElementById('activeTunnels').textContent = serverData.clientCounts || 0; 416 + document.getElementById('serverStatus').textContent = 'online'; 417 + document.getElementById('totalConnections').textContent = serverData.curConns || 0; 418 + document.getElementById('totalUpload').textContent = formatBytes(serverData.totalTrafficOut || 0); 419 + document.getElementById('totalDownload').textContent = formatBytes(serverData.totalTrafficIn || 0); 420 + 421 + // Update page title 422 + const tunnelCount = serverData.clientCounts || 0; 423 + const totalTraffic = formatBytes((serverData.totalTrafficIn || 0) + (serverData.totalTrafficOut || 0)); 424 + document.title = tunnelCount > 0 425 + ? `bore - ${tunnelCount} active • ${totalTraffic}` 426 + : 'bore'; 427 + 428 + // Check if tunnel list structure changed 429 + const proxies = proxiesData.proxies || []; 430 + const currentState = JSON.stringify(proxies.map(p => ({ name: p.name, status: p.status }))); 431 + 432 + if (currentState !== lastProxiesState) { 433 + // Structure changed, rebuild DOM 434 + lastProxiesState = currentState; 435 + renderTunnelList(proxies); 436 + } else { 437 + // Structure unchanged, just update data 438 + updateTunnelData(proxies); 439 + } 440 + 441 + document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString(); 442 + 443 + // Remove loading class after first successful fetch 444 + document.body.classList.remove('loading'); 445 + } catch (error) { 446 + fetchFailCount++; 447 + document.getElementById('serverStatus').textContent = 'offline'; 448 + console.error('Failed to fetch stats:', error); 449 + 450 + // Reload page if failed multiple times (server might have updated) 451 + if (fetchFailCount >= MAX_FAIL_COUNT) { 452 + console.log('Multiple fetch failures detected, reloading page...'); 453 + window.location.reload(); 454 + } 455 + } 456 + } 457 + 458 + function renderTunnelList(proxies) { 459 + const tunnelList = document.getElementById('tunnelList'); 460 + const onlineTunnels = proxies.filter(p => p.status === 'online'); 461 + const offlineTunnels = proxies.filter(p => p.status !== 'online'); 462 + 463 + if (onlineTunnels.length === 0 && offlineTunnels.length === 0) { 464 + tunnelList.innerHTML = ` 465 + <div class="empty-state"> 466 + <div class="empty-icon">🚇</div> 467 + <p>no active tunnels</p> 468 + </div> 469 + `; 470 + } else { 471 + let html = ''; 472 + 473 + // Render online tunnels 474 + if (onlineTunnels.length > 0) { 475 + html += onlineTunnels.map(proxy => { 476 + const subdomain = proxy.conf?.subdomain || 'unknown'; 477 + const url = `https://${subdomain}.tun.hogwarts.channel`; 478 + 479 + // Parse labels from proxy name (format: subdomain[label1,label2]) 480 + const labelMatch = proxy.name.match(/\[([^\]]+)\]$/); 481 + const labels = labelMatch ? labelMatch[1].split(',') : []; 482 + const displayName = labels.length > 0 ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name; 483 + 484 + const labelHtml = labels.map(label => { 485 + const trimmedLabel = label.trim(); 486 + const style = getLabelStyle(trimmedLabel); 487 + return `<span class="tunnel-label" style="color: ${style.color}; background: ${style.bgColor}; border-color: ${style.borderColor};">${trimmedLabel}</span>`; 488 + }).join(''); 489 + 490 + return ` 491 + <div class="tunnel" data-tunnel="${proxy.name}"> 492 + <div class="tunnel-info"> 493 + <div class="tunnel-name"> 494 + ${displayName || 'unnamed'} 495 + ${labelHtml} 496 + </div> 497 + <div class="tunnel-url"> 498 + <a href="${url}" target="_blank">${url}</a> 499 + </div> 500 + <div style="color: #8b949e; font-size: 0.75rem; margin-top: 0.25rem;"> 501 + started: <span data-start-time="${proxy.lastStartTime || ''}"></span> • traffic in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span> 502 + </div> 503 + </div> 504 + <div class="tunnel-status status-online">online</div> 505 + </div> 506 + `; 507 + }).join(''); 508 + } 509 + 510 + // Render offline tunnels 511 + if (offlineTunnels.length > 0) { 512 + html += '<div class="offline-tunnels">'; 513 + html += '<div style="color: #8b949e; font-size: 0.85rem; margin-bottom: 0.75rem;">recently disconnected</div>'; 514 + html += offlineTunnels.map(proxy => { 515 + // Parse labels from proxy name (format: subdomain[label1,label2]) 516 + const labelMatch = proxy.name.match(/\[([^\]]+)\]$/); 517 + const labels = labelMatch ? labelMatch[1].split(',').map(l => l.trim()) : []; 518 + const displayName = labels.length > 0 ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name; 519 + const labelStr = labels.length > 0 ? ` [${labels.join(', ')}]` : ''; 520 + 521 + if (!proxy.conf) { 522 + return ` 523 + <div class="offline-tunnel" data-tunnel="${proxy.name}"> 524 + <span class="offline-tunnel-name">${displayName || 'unnamed'}${labelStr}</span> 525 + <span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span></span> 526 + </div> 527 + `; 528 + } 529 + 530 + const subdomain = proxy.conf.subdomain || 'unknown'; 531 + const url = `https://${subdomain}.tun.hogwarts.channel`; 532 + return ` 533 + <div class="offline-tunnel" data-tunnel="${proxy.name}"> 534 + <span class="offline-tunnel-name">${displayName || 'unnamed'}${labelStr} → ${url}</span> 535 + <span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span></span> 536 + </div> 537 + `; 538 + }).join(''); 539 + html += '</div>'; 540 + } 541 + 542 + tunnelList.innerHTML = html; 543 + 544 + // Update all relative times 545 + updateRelativeTimes(); 546 + 547 + 548 + } 549 + 550 + // Update data 551 + updateTunnelData(proxies); 552 + } 553 + 554 + function updateTunnelData(proxies) { 555 + proxies.forEach(proxy => { 556 + const trafficInEl = document.querySelector(`[data-traffic-in="${proxy.name}"]`); 557 + const trafficOutEl = document.querySelector(`[data-traffic-out="${proxy.name}"]`); 558 + 559 + if (trafficInEl) trafficInEl.textContent = formatBytes(proxy.todayTrafficIn || 0); 560 + if (trafficOutEl) trafficOutEl.textContent = formatBytes(proxy.todayTrafficOut || 0); 561 + 562 + 563 + }); 564 + 565 + // Update relative times 566 + updateRelativeTimes(); 567 + } 568 + 569 + function formatBytes(bytes) { 570 + if (bytes === 0) return '0 B'; 571 + const k = 1024; 572 + const sizes = ['B', 'KB', 'MB', 'GB']; 573 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 574 + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; 575 + } 576 + 577 + function formatTime(timeStr) { 578 + // Input format: "12-08 20:15:20" (MM-DD HH:MM:SS) 579 + const [datePart, timePart] = timeStr.split(' '); 580 + const [month, day] = datePart.split('-'); 581 + const [hour, minute, second] = timePart.split(':'); 582 + 583 + const now = new Date(); 584 + const inputDate = new Date(now.getFullYear(), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute), parseInt(second)); 585 + 586 + const diffInSeconds = (now.getTime() - inputDate.getTime()) / 1000; 587 + const diffInMinutes = Math.round(diffInSeconds / 60); 588 + const diffInHours = Math.round(diffInMinutes / 60); 589 + 590 + if (diffInSeconds < 60) { 591 + return 'just now'; 592 + } else if (diffInHours < 1) { 593 + return diffInMinutes === 1 ? '1 minute ago' : `${diffInMinutes} minutes ago`; 594 + } else if (now.toDateString() === inputDate.toDateString()) { 595 + return 'today'; 596 + } else if ( 597 + new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1).toDateString() === 598 + inputDate.toDateString() 599 + ) { 600 + return 'yesterday'; 601 + } else { 602 + return inputDate.toLocaleTimeString([], { 603 + month: 'numeric', 604 + day: 'numeric', 605 + hour: 'numeric', 606 + minute: 'numeric', 607 + }); 608 + } 609 + } 610 + 611 + function updateRelativeTimes() { 612 + document.querySelectorAll('[data-start-time]').forEach(element => { 613 + const timeStr = element.getAttribute('data-start-time'); 614 + element.textContent = formatTime(timeStr); 615 + }); 616 + } 617 + 618 + // Fetch immediately and then every 5 seconds 619 + fetchStats(); 620 + setInterval(fetchStats, 5000); 621 + 622 + // Update relative times every 10 seconds 623 + setInterval(updateRelativeTimes, 10000); 624 + </script> 625 + </body> 626 + 627 + </html>
+192
modules/frps/default.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + cfg = config.atelier.services.frps; 9 + in 10 + { 11 + options.atelier.services.frps = { 12 + enable = lib.mkEnableOption "frp server for tunneling services"; 13 + 14 + bindAddr = lib.mkOption { 15 + type = lib.types.str; 16 + default = "0.0.0.0"; 17 + description = "Address to bind frp server to"; 18 + }; 19 + 20 + bindPort = lib.mkOption { 21 + type = lib.types.port; 22 + default = 7000; 23 + description = "Port for frp control connection"; 24 + }; 25 + 26 + vhostHTTPPort = lib.mkOption { 27 + type = lib.types.port; 28 + default = 7080; 29 + description = "Port for HTTP virtual host traffic"; 30 + }; 31 + 32 + allowedTCPPorts = lib.mkOption { 33 + type = lib.types.listOf lib.types.port; 34 + default = lib.lists.range 20000 20099; 35 + example = [ 20000 20001 20002 20003 20004 ]; 36 + description = "TCP port range to allow for TCP tunnels (default: 20000-20099)"; 37 + }; 38 + 39 + allowedUDPPorts = lib.mkOption { 40 + type = lib.types.listOf lib.types.port; 41 + default = lib.lists.range 20000 20099; 42 + example = [ 20000 20001 20002 20003 20004 ]; 43 + description = "UDP port range to allow for UDP tunnels (default: 20000-20099)"; 44 + }; 45 + 46 + authToken = lib.mkOption { 47 + type = lib.types.nullOr lib.types.str; 48 + default = null; 49 + description = "Authentication token for clients (deprecated: use authTokenFile)"; 50 + }; 51 + 52 + authTokenFile = lib.mkOption { 53 + type = lib.types.nullOr lib.types.path; 54 + default = null; 55 + description = "Path to file containing authentication token"; 56 + }; 57 + 58 + domain = lib.mkOption { 59 + type = lib.types.str; 60 + example = "tun.hogwarts.channel"; 61 + description = "Base domain for subdomains (e.g., *.tun.hogwarts.channel)"; 62 + }; 63 + 64 + enableCaddy = lib.mkOption { 65 + type = lib.types.bool; 66 + default = true; 67 + description = "Automatically configure Caddy reverse proxy for wildcard domain"; 68 + }; 69 + }; 70 + 71 + config = lib.mkIf cfg.enable { 72 + assertions = [ 73 + { 74 + assertion = cfg.authToken != null || cfg.authTokenFile != null; 75 + message = "Either authToken or authTokenFile must be set for frps"; 76 + } 77 + ]; 78 + 79 + # Open firewall ports for frp control connection and TCP/UDP tunnels 80 + networking.firewall.allowedTCPPorts = [ cfg.bindPort ] ++ cfg.allowedTCPPorts; 81 + networking.firewall.allowedUDPPorts = cfg.allowedUDPPorts; 82 + 83 + # frp server service 84 + systemd.services.frps = 85 + let 86 + tokenConfig = 87 + if cfg.authTokenFile != null then 88 + '' 89 + auth.tokenSource.type = "file" 90 + auth.tokenSource.file.path = "${cfg.authTokenFile}" 91 + '' 92 + else 93 + ''auth.token = "${cfg.authToken}"''; 94 + 95 + configFile = pkgs.writeText "frps.toml" '' 96 + bindAddr = "${cfg.bindAddr}" 97 + bindPort = ${toString cfg.bindPort} 98 + vhostHTTPPort = ${toString cfg.vhostHTTPPort} 99 + 100 + # Dashboard and Prometheus metrics 101 + webServer.addr = "127.0.0.1" 102 + webServer.port = 7400 103 + enablePrometheus = true 104 + 105 + # Authentication token - clients need this to connect 106 + auth.method = "token" 107 + ${tokenConfig} 108 + 109 + # Subdomain support for *.${cfg.domain} 110 + subDomainHost = "${cfg.domain}" 111 + 112 + # Allow port ranges for TCP/UDP tunnels 113 + # Format: [[{"start": 20000, "end": 20099}]] 114 + allowPorts = [ 115 + { start = 20000, end = 20099 } 116 + ] 117 + 118 + # Custom 404 page 119 + custom404Page = "${./404.html}" 120 + 121 + # Logging 122 + log.to = "console" 123 + log.level = "info" 124 + ''; 125 + in 126 + { 127 + description = "frp server for ${cfg.domain} tunneling"; 128 + after = [ "network.target" ]; 129 + wantedBy = [ "multi-user.target" ]; 130 + serviceConfig = { 131 + Type = "simple"; 132 + Restart = "on-failure"; 133 + RestartSec = "5s"; 134 + ExecStart = "${pkgs.frp}/bin/frps -c ${configFile}"; 135 + }; 136 + }; 137 + 138 + # Automatically configure Caddy for wildcard domain 139 + services.caddy = lib.mkIf cfg.enableCaddy { 140 + enable = true; 141 + 142 + # Dashboard for base domain 143 + virtualHosts."${cfg.domain}" = { 144 + extraConfig = '' 145 + tls { 146 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 147 + } 148 + header { 149 + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 150 + } 151 + 152 + # Proxy /api/* to frps dashboard 153 + handle /api/* { 154 + reverse_proxy localhost:7400 155 + } 156 + 157 + # Serve dashboard HTML 158 + handle { 159 + root * ${./.} 160 + try_files dashboard.html 161 + file_server 162 + } 163 + ''; 164 + }; 165 + 166 + # Wildcard subdomain proxy to frps 167 + virtualHosts."*.${cfg.domain}" = { 168 + extraConfig = '' 169 + tls { 170 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 171 + } 172 + header { 173 + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 174 + } 175 + reverse_proxy localhost:${toString cfg.vhostHTTPPort} { 176 + header_up X-Forwarded-Proto {scheme} 177 + header_up X-Forwarded-For {remote} 178 + header_up Host {host} 179 + } 180 + handle_errors { 181 + @404 expression {http.error.status_code} == 404 182 + handle @404 { 183 + root * ${./.} 184 + rewrite * /404.html 185 + file_server 186 + } 187 + } 188 + ''; 189 + }; 190 + }; 191 + }; 192 + }
+424
modules/git.nix
··· 1 + { 2 + lib, 3 + config, 4 + pkgs, 5 + isDarwin, 6 + ... 7 + }: 8 + with lib; 9 + { 10 + options.jsp.git = { 11 + enable = mkEnableOption "Git configuration"; 12 + }; 13 + 14 + config = mkIf config.jsp.git.enable { 15 + programs.git = { 16 + enable = true; 17 + lfs.enable = true; 18 + 19 + # Conditional includes for different work contexts 20 + includes = [ 21 + { 22 + path = pkgs.writeText "gitconfig-phishdir" '' 23 + [user] 24 + email = jasper.mayone@phish.directory 25 + ''; 26 + condition = "gitdir/i:~/dev/projects/phishdirectory/**"; 27 + } 28 + { 29 + path = pkgs.writeText "gitconfig-singlefeather" '' 30 + [user] 31 + email = jasper.mayone@singlefeather.com 32 + ''; 33 + condition = "gitdir/i:~/dev/work/singlefeather/**"; 34 + } 35 + { 36 + path = pkgs.writeText "gitconfig-mlh" '' 37 + [user] 38 + email = jasper.mayone@majorleaguehacking.com 39 + ''; 40 + condition = "gitdir/i:~/dev/work/mlh/eng/**"; 41 + } 42 + { 43 + path = pkgs.writeText "gitconfig-patchwork" '' 44 + [user] 45 + email = jasper@patchworklabs.org 46 + ''; 47 + condition = "gitdir/i:~/dev/patchwork/**"; 48 + } 49 + { 50 + path = pkgs.writeText "gitconfig-school" '' 51 + [user] 52 + email = mayonej@wit.edu 53 + ''; 54 + condition = "gitdir/i:~/dev/school/**"; 55 + } 56 + { 57 + path = pkgs.writeText "gitconfig-personal" '' 58 + [user] 59 + email = me@jaspermayone.com 60 + ''; 61 + condition = "gitdir/i:~/dev/personal/**"; 62 + } 63 + ]; 64 + 65 + # Global git ignore 66 + ignores = [ 67 + # Compiled source 68 + "*.com" 69 + "*.class" 70 + "*.dll" 71 + "*.exe" 72 + "*.o" 73 + "*.so" 74 + 75 + # Packages 76 + "*.7z" 77 + "*.dmg" 78 + "*.gz" 79 + "*.iso" 80 + "*.jar" 81 + "*.rar" 82 + "*.tar" 83 + "*.zip" 84 + 85 + # Logs 86 + "*.log" 87 + 88 + # OS generated files 89 + ".DS_Store" 90 + ".DS_Store?" 91 + "*/.DS_Store" 92 + "**/.DS_Store" 93 + "._*" 94 + ".Spotlight-V100" 95 + ".Trashes" 96 + "ehthumbs.db" 97 + "Thumbs.db" 98 + 99 + # Claude Code 100 + ".llm-orc/*" 101 + "CLAUDE.local.md" 102 + ]; 103 + 104 + settings = { 105 + alias = { 106 + # Quick shortcuts 107 + co = "checkout"; 108 + br = "branch"; 109 + ci = "commit"; 110 + st = "status"; 111 + unstage = "reset HEAD --"; 112 + last = "log -1 HEAD"; 113 + pushfwl = "push --force-with-lease --force-if-includes"; 114 + 115 + # View abbreviated SHA, description, and history graph of the latest 20 commits 116 + l = "log --pretty=oneline -n 20 --graph --abbrev-commit"; 117 + lg = "log --oneline --graph --decorate"; 118 + 119 + # View the current working tree status using the short format 120 + s = "status -s"; 121 + 122 + # Show the diff between the latest commit and the current state 123 + d = "!git diff-index --quiet HEAD -- || clear; git --no-pager diff --patch-with-stat"; 124 + 125 + # `git di $number` shows the diff between the state `$number` revisions ago and the current state 126 + di = "!d() { git diff --patch-with-stat HEAD~$1; }; git diff-index --quiet HEAD -- || clear; d"; 127 + 128 + # Pull in remote changes for the current repository and all its submodules 129 + p = "pull --recurse-submodules"; 130 + 131 + # Clone a repository including all submodules 132 + c = "clone --recursive"; 133 + 134 + # Commit all changes 135 + ca = "!git add ':(exclude,attr:builtin_objectmode=160000)' && git commit -av"; 136 + 137 + # Switch to a branch, creating it if necessary 138 + go = "!f() { git checkout -b \"$1\" 2> /dev/null || git checkout \"$1\"; }; f"; 139 + 140 + # Show verbose output about tags, branches or remotes 141 + tags = "tag -l"; 142 + branches = "branch --all"; 143 + remotes = "remote --verbose"; 144 + 145 + # List aliases 146 + aliases = "config --get-regexp alias"; 147 + 148 + # Amend the currently staged files to the latest commit 149 + amend = "commit --amend --reuse-message=HEAD"; 150 + 151 + # Credit an author on the latest commit 152 + credit = "!f() { git commit --amend --author \"$1 <$2>\" -C HEAD; }; f"; 153 + 154 + # Interactive rebase with the given number of latest commits 155 + reb = "!r() { git rebase -i HEAD~$1; }; r"; 156 + 157 + # Remove the old tag with this name and tag the latest commit with it 158 + retag = "!r() { git tag -d $1 && git push origin :refs/tags/$1 && git tag $1; }; r"; 159 + 160 + # Find branches containing commit 161 + fb = "!f() { git branch -a --contains $1; }; f"; 162 + 163 + # Find tags containing commit 164 + ft = "!f() { git describe --always --contains $1; }; f"; 165 + 166 + # Find commits by source code 167 + fc = "!f() { git log --pretty=format:'%C(yellow)%h %Cblue%ad %Creset%s%Cgreen [%cn] %Cred%d' --decorate --date=short -S$1; }; f"; 168 + 169 + # Find commits by commit message 170 + fm = "!f() { git log --pretty=format:'%C(yellow)%h %Cblue%ad %Creset%s%Cgreen [%cn] %Cred%d' --decorate --date=short --grep=$1; }; f"; 171 + 172 + # Remove branches that have already been merged with main (a.k.a. 'delete merged') 173 + dm = "!git branch --merged | grep -v '\\\\*' | xargs -n 1 git branch -d"; 174 + 175 + # List contributors with number of commits 176 + contributors = "shortlog --summary --numbered"; 177 + 178 + # Show the user email for the current repository 179 + whoami = "config user.email"; 180 + }; 181 + 182 + user = { 183 + name = "Jasper Mayone"; 184 + email = "me@jaspermayone.com"; 185 + signingKey = "14D0D45A1DADAAFA"; 186 + }; 187 + 188 + init.defaultBranch = "main"; 189 + 190 + apply.whitespace = "fix"; 191 + 192 + core = { 193 + editor = "code --wait"; 194 + pager = "less"; 195 + # Treat spaces before tabs and trailing whitespace as errors 196 + whitespace = "space-before-tab,-indent-with-non-tab,trailing-space"; 197 + # Make `git rebase` safer on macOS 198 + trustctime = false; 199 + # Prevent showing files with non-ASCII names as unversioned 200 + precomposeunicode = false; 201 + # Speed up commands involving untracked files 202 + untrackedCache = true; 203 + }; 204 + 205 + color = { 206 + ui = "auto"; 207 + branch = { 208 + current = "yellow reverse"; 209 + local = "yellow"; 210 + remote = "green"; 211 + }; 212 + }; 213 + 214 + diff = { 215 + algorithm = "histogram"; 216 + tool = "windsurf"; 217 + renames = "copies"; # Detect copies as well as renames 218 + }; 219 + 220 + "difftool \"windsurf\"".cmd = "windsurf --diff $LOCAL $REMOTE"; 221 + 222 + # Binary file diff using hexdump 223 + "diff \"bin\"".textconv = "hexdump -v -C"; 224 + 225 + # Bun lockfile diff 226 + "diff \"lockb\"" = { 227 + textconv = "bun"; 228 + binary = true; 229 + }; 230 + 231 + # Include summaries of merged commits in merge commit messages 232 + merge.log = true; 233 + 234 + pull.rebase = true; 235 + 236 + push = { 237 + default = "simple"; 238 + followTags = true; 239 + autoSetupRemote = true; 240 + }; 241 + 242 + rebase.autoStash = true; 243 + 244 + status = { 245 + submoduleSummary = true; 246 + showUntrackedFiles = "all"; 247 + }; 248 + 249 + tag = { 250 + sort = "version:refname"; 251 + forceSignAnnotated = true; 252 + gpgsign = true; 253 + }; 254 + 255 + versionsort = { 256 + prereleaseSuffix = [ 257 + "-pre" 258 + ".pre" 259 + "-beta" 260 + ".beta" 261 + "-rc" 262 + ".rc" 263 + ]; 264 + }; 265 + 266 + commit.gpgSign = true; 267 + 268 + gpg = { 269 + program = if isDarwin then "/opt/homebrew/bin/gpg" else "gpg"; 270 + format = "openpgp"; 271 + }; 272 + 273 + help.autocorrect = 1; 274 + 275 + # URL shorthands 276 + "url \"git@github.com:\"" = { 277 + insteadOf = "gh:"; 278 + pushInsteadOf = "https://github.com/"; 279 + }; 280 + "url \"git://github.com/\"".insteadOf = "github:"; 281 + "url \"git@gist.github.com:\"".insteadOf = "gst:"; 282 + "url \"git://gist.github.com/\"".insteadOf = "gist:"; 283 + 284 + sequence.editor = "code --wait"; 285 + 286 + branch.sort = "-committerdate"; 287 + column.ui = "auto"; 288 + } // (if isDarwin then { 289 + # macOS specific 290 + credential = { 291 + helper = "osxkeychain"; 292 + }; 293 + "credential \"https://dev.azure.com\"".useHttpPath = true; 294 + } else { }); 295 + }; 296 + 297 + # Delta for better diffs 298 + programs.delta = { 299 + enable = true; 300 + options = { 301 + navigate = true; 302 + light = false; 303 + line-numbers = true; 304 + }; 305 + }; 306 + 307 + # GitHub CLI 308 + programs.gh = { 309 + enable = true; 310 + settings = { 311 + git_protocol = "ssh"; 312 + }; 313 + }; 314 + 315 + # Lazygit 316 + programs.lazygit = { 317 + enable = true; 318 + settings = { 319 + gui.theme = { 320 + lightTheme = false; 321 + activeBorderColor = [ 322 + "blue" 323 + "bold" 324 + ]; 325 + inactiveBorderColor = [ "black" ]; 326 + selectedLineBgColor = [ "default" ]; 327 + }; 328 + }; 329 + }; 330 + 331 + # GitHub Dashboard 332 + programs.gh-dash = { 333 + enable = true; 334 + settings = { 335 + prSections = [ 336 + { 337 + title = "Mine"; 338 + filters = "is:open author:@me updated:>={{ nowModify \"-3w\" }} sort:updated-desc archived:false"; 339 + layout.author.hidden = true; 340 + } 341 + { 342 + title = "Review"; 343 + filters = "sort:updated-desc is:pr is:open review-requested:jaspermayone archived:false"; 344 + } 345 + { 346 + title = "All"; 347 + filters = "sort:updated-desc is:pr is:open user:@me archived:false"; 348 + } 349 + ]; 350 + issuesSections = [ 351 + { 352 + title = "Assigned"; 353 + filters = "is:issue state:open archived:false assignee:@me sort:updated-desc"; 354 + } 355 + { 356 + title = "Created"; 357 + filters = "author:@me is:open archived:false"; 358 + } 359 + { 360 + title = "All"; 361 + filters = "is:issue involves:@me archived:false sort:updated-desc is:open"; 362 + } 363 + ]; 364 + defaults = { 365 + view = "prs"; 366 + refetchIntervalMinutes = 5; 367 + layout.prs = { 368 + repoName = { 369 + grow = true; 370 + width = 10; 371 + hidden = false; 372 + }; 373 + base.hidden = true; 374 + }; 375 + preview = { 376 + open = true; 377 + width = 84; 378 + }; 379 + prsLimit = 20; 380 + issuesLimit = 20; 381 + }; 382 + repoPaths = { 383 + "jaspermayone/*" = "~/dev/personal/*"; 384 + "phishdirectory/*" = "~/dev/projects/phishdirectory/*"; 385 + }; 386 + keybindings = { 387 + universal = [ 388 + { 389 + key = "g"; 390 + name = "lazygit"; 391 + command = "cd {{.RepoPath}} && lazygit"; 392 + } 393 + ]; 394 + prs = [ 395 + { 396 + key = "O"; 397 + builtin = "checkout"; 398 + } 399 + { 400 + key = "m"; 401 + command = "gh pr merge --admin --repo {{.RepoName}} {{.PrNumber}}"; 402 + } 403 + { 404 + key = "a"; 405 + name = "lazygit add"; 406 + command = "cd {{.RepoPath}} && git add -A && lazygit"; 407 + } 408 + { 409 + key = "v"; 410 + name = "approve"; 411 + command = "gh pr review --repo {{.RepoName}} --approve --body \"$(gum input --prompt='Approval Comment: ')\" {{.PrNumber}}"; 412 + } 413 + ]; 414 + }; 415 + theme = { 416 + ui = { 417 + sectionsShowCount = true; 418 + table.compact = false; 419 + }; 420 + }; 421 + }; 422 + }; 423 + }; 424 + }
+187
modules/knot/sync.nix
··· 1 + # Knot to GitHub sync service 2 + # Automatically mirrors repositories from your knot server to GitHub 3 + { 4 + config, 5 + lib, 6 + pkgs, 7 + ... 8 + }: 9 + 10 + let 11 + cfg = config.jsp.services.knot-sync; 12 + in 13 + { 14 + options.jsp.services.knot-sync = { 15 + enable = lib.mkEnableOption "Knot to GitHub sync service"; 16 + 17 + repoDir = lib.mkOption { 18 + type = lib.types.str; 19 + default = "/var/lib/knot/repos/did:plc:krxbvxvis5skq7jj6eot23ul"; 20 + description = "Directory containing git repositories"; 21 + }; 22 + 23 + githubUsername = lib.mkOption { 24 + type = lib.types.str; 25 + default = "jaspermayone"; 26 + description = "GitHub username"; 27 + }; 28 + 29 + secretsFile = lib.mkOption { 30 + type = lib.types.path; 31 + description = "Path to secrets file containing GITHUB_TOKEN"; 32 + }; 33 + 34 + logFile = lib.mkOption { 35 + type = lib.types.str; 36 + default = "/var/lib/knot/knot-sync.log"; 37 + description = "Log file location"; 38 + }; 39 + 40 + interval = lib.mkOption { 41 + type = lib.types.str; 42 + default = "*/5 * * * *"; 43 + description = "Cron schedule for sync (default: every 5 minutes)"; 44 + }; 45 + }; 46 + 47 + config = lib.mkIf cfg.enable { 48 + systemd.services.knot-sync = { 49 + description = "Sync Knot repositories to GitHub"; 50 + serviceConfig = { 51 + Type = "oneshot"; 52 + User = "git"; # official tangled module uses git user 53 + EnvironmentFile = cfg.secretsFile; 54 + ExecStart = pkgs.writeShellScript "knot-sync" '' 55 + set -euo pipefail 56 + 57 + # Variables 58 + REPO_DIR="${cfg.repoDir}" 59 + GITHUB_USERNAME="${cfg.githubUsername}" 60 + LOG_FILE="${cfg.logFile}" 61 + 62 + # Log function 63 + log() { echo "$(date +'%Y-%m-%d %H:%M:%S'): $1" >> "$LOG_FILE"; } 64 + 65 + # Create the post-receive hook template 66 + cat <<'EOF' > /tmp/post-receive.template 67 + #!${pkgs.bash}/bin/bash 68 + # post-receive hook to sync to GitHub - AUTOGENERATED 69 + 70 + # Load environment variables from secrets file 71 + if [ -f "${cfg.secretsFile}" ]; then 72 + source "${cfg.secretsFile}" 73 + fi 74 + 75 + # Variables 76 + GITHUB_USERNAME="${cfg.githubUsername}" 77 + LOG_FILE="${cfg.logFile}" 78 + REPO_NAME=$(basename $(pwd)) 79 + 80 + # Log function 81 + log() { echo "$(date +'%Y-%m-%d %H:%M:%S'): $1" >> "''${LOG_FILE}"; } 82 + 83 + # Check for nosync marker 84 + if [ -f "$(pwd)/.nosync" ]; then 85 + log "Skipping sync for $REPO_NAME (nosync marker present)" 86 + exit 0 87 + fi 88 + 89 + # Function to sync to GitHub 90 + sync_to_github() { 91 + log "Syncing $REPO_NAME to GitHub" 92 + expected_url="https://''${GITHUB_USERNAME}:''${GITHUB_TOKEN}@github.com/''${GITHUB_USERNAME}/''${REPO_NAME}.git" 93 + current_url=$(${pkgs.git}/bin/git remote get-url origin 2>/dev/null || echo "") 94 + 95 + if [ -z "$current_url" ]; then 96 + log "Adding origin remote" 97 + ${pkgs.git}/bin/git remote add origin "$expected_url" 98 + elif [ "$current_url" != "$expected_url" ]; then 99 + log "Updating origin remote URL" 100 + ${pkgs.git}/bin/git remote set-url origin "$expected_url" 101 + fi 102 + 103 + # Mirror push everything (refs, tags, branches) 104 + if ${pkgs.git}/bin/git push --mirror origin 2>&1 | tee -a "''${LOG_FILE}"; then 105 + log "Sync succeeded for $REPO_NAME" 106 + return 0 107 + else 108 + log "Sync failed for $REPO_NAME" 109 + return 1 110 + fi 111 + } 112 + 113 + # Main 114 + while read oldrev newrev refname; do 115 + log "Received push for ref '$refname' (old revision: $oldrev, new revision: $newrev)" 116 + sync_to_github 117 + done 118 + EOF 119 + 120 + HOOK_TEMPLATE="/tmp/post-receive.template" 121 + 122 + # Create the post-receive hook 123 + create_hook() { 124 + local new_repo_path="$1" 125 + local hook_path="$new_repo_path/hooks/post-receive.d/forward" 126 + local nosync_marker="$new_repo_path/.nosync" 127 + 128 + # Skip if .nosync marker exists 129 + if [ -f "$nosync_marker" ]; then 130 + log "Skipping $new_repo_path (nosync marker present)" 131 + return 0 132 + fi 133 + 134 + if [ -d "$new_repo_path" ] && [ ! -f "$hook_path" ]; then 135 + # Check that it's a git repository, specifically a bare repo 136 + if [ -f "$new_repo_path/config" ]; then 137 + # Create hooks directory if it doesn't exist 138 + mkdir -p "$(dirname "$hook_path")" 139 + # Create hook from the template file, substituting variables. 140 + if cat "$HOOK_TEMPLATE" > "$hook_path" && chmod +x "$hook_path"; then 141 + log "Created hook for $new_repo_path" 142 + # Check if repo has any commits before pushing 143 + if (cd "$new_repo_path" && ${pkgs.git}/bin/git rev-parse HEAD >/dev/null 2>&1); then 144 + # Auto push by simulating a post-receive hook trigger 145 + log "Triggering initial push for $new_repo_path" 146 + (cd "$new_repo_path" && \ 147 + echo "0000000000000000000000000000000000000000 $(${pkgs.git}/bin/git rev-parse HEAD) refs/heads/main" | \ 148 + "$hook_path") 149 + fi 150 + else 151 + log "Hook creation failed for $new_repo_path" 152 + fi 153 + fi 154 + fi 155 + } 156 + 157 + # Keep track of hooks created 158 + hooks_created=0 159 + 160 + # Find all directories that look like bare Git repos without a post-receive hook 161 + ${pkgs.findutils}/bin/find "$REPO_DIR" -mindepth 1 -maxdepth 1 -type d \! -name ".*" -print0 | 162 + while IFS= read -r -d $'\0' repo_path; do 163 + create_hook "$repo_path" 164 + if [ $? -eq 0 ]; then 165 + hooks_created=$((hooks_created + 1)) 166 + fi 167 + done 168 + 169 + # Only log completion if hooks were actually created 170 + if [ $hooks_created -gt 0 ]; then 171 + log "Sync job complete - Created $hooks_created hooks" 172 + fi 173 + ''; 174 + }; 175 + }; 176 + 177 + systemd.paths.knot-sync = { 178 + description = "Watch for new Knot repositories"; 179 + wantedBy = [ "multi-user.target" ]; 180 + pathConfig = { 181 + PathModified = cfg.repoDir; 182 + Unit = "knot-sync.service"; 183 + MakeDirectory = true; 184 + }; 185 + }; 186 + }; 187 + }
+396
modules/shell.nix
··· 1 + # Shell configuration 2 + { config, lib, pkgs, hostname, ... }: 3 + 4 + let 5 + # Tangled setup script for configuring git remotes 6 + tangled-setup = pkgs.writeShellScriptBin "tangled-setup" '' 7 + # Configuration 8 + default_plc_id="did:plc:krxbvxvis5skq7jj6eot23ul" 9 + default_github_username="jaspermayone" 10 + default_knot_host="knot.jaspermayone.com" 11 + 12 + # Verify git repository 13 + if ! ${pkgs.git}/bin/git rev-parse --is-inside-work-tree &>/dev/null; then 14 + ${pkgs.gum}/bin/gum style --foreground 196 "Not a git repository" 15 + exit 1 16 + fi 17 + 18 + repo_name=$(basename "$(${pkgs.git}/bin/git rev-parse --show-toplevel)") 19 + ${pkgs.gum}/bin/gum style --bold --foreground 212 "Configuring tangled remotes for: $repo_name" 20 + echo 21 + 22 + # Check current remotes 23 + origin_url=$(${pkgs.git}/bin/git remote get-url origin 2>/dev/null) 24 + github_url=$(${pkgs.git}/bin/git remote get-url github 2>/dev/null) 25 + origin_is_knot=false 26 + github_username="$default_github_username" 27 + 28 + # Extract GitHub username from existing origin if it's GitHub 29 + if [[ "$origin_url" == *"github.com"* ]]; then 30 + github_username=$(echo "$origin_url" | ${pkgs.gnused}/bin/sed -E 's/.*github\.com[:/]([^/]+)\/.*$/\1/') 31 + fi 32 + 33 + # Check if origin points to knot 34 + if [[ "$origin_url" == *"$default_knot_host"* ]] || [[ "$origin_url" == *"tangled"* ]]; then 35 + origin_is_knot=true 36 + ${pkgs.gum}/bin/gum style --foreground 35 "✓ Origin → knot ($origin_url)" 37 + elif [[ -n "$origin_url" ]]; then 38 + ${pkgs.gum}/bin/gum style --foreground 214 "! Origin → $origin_url (not knot)" 39 + else 40 + ${pkgs.gum}/bin/gum style --foreground 214 "! Origin not configured" 41 + fi 42 + 43 + # Check github remote 44 + if [[ -n "$github_url" ]]; then 45 + ${pkgs.gum}/bin/gum style --foreground 35 "✓ GitHub → $github_url" 46 + else 47 + ${pkgs.gum}/bin/gum style --foreground 214 "! GitHub remote not configured" 48 + fi 49 + 50 + echo 51 + 52 + # Configure origin remote if needed 53 + if [[ "$origin_is_knot" = false ]]; then 54 + should_migrate=true 55 + if [[ -n "$origin_url" ]]; then 56 + ${pkgs.gum}/bin/gum confirm "Migrate origin from $origin_url to knot?" || should_migrate=false 57 + fi 58 + 59 + if [[ "$should_migrate" = true ]]; then 60 + plc_id=$(${pkgs.gum}/bin/gum input --placeholder "$default_plc_id" --prompt "PLC ID: " --value "$default_plc_id") 61 + plc_id=''${plc_id:-$default_plc_id} 62 + 63 + if ${pkgs.git}/bin/git remote get-url origin &>/dev/null; then 64 + ${pkgs.git}/bin/git remote remove origin 65 + fi 66 + ${pkgs.git}/bin/git remote add origin "git@$default_knot_host:''${plc_id}/''${repo_name}" 67 + ${pkgs.gum}/bin/gum style --foreground 35 "✓ Configured origin → git@$default_knot_host:''${plc_id}/''${repo_name}" 68 + fi 69 + fi 70 + 71 + # Configure github remote if needed 72 + if [[ -z "$github_url" ]]; then 73 + username=$(${pkgs.gum}/bin/gum input --placeholder "$github_username" --prompt "GitHub username: " --value "$github_username") 74 + username=''${username:-$github_username} 75 + 76 + ${pkgs.git}/bin/git remote add github "git@github.com:''${username}/''${repo_name}.git" 77 + ${pkgs.gum}/bin/gum style --foreground 35 "✓ Configured github → git@github.com:''${username}/''${repo_name}.git" 78 + fi 79 + 80 + echo 81 + 82 + # Configure default push remote 83 + current_remote=$(${pkgs.git}/bin/git config --get branch.main.remote 2>/dev/null) 84 + if [[ -z "$current_remote" ]]; then 85 + if ${pkgs.gum}/bin/gum confirm "Set origin (knot) as default push remote?"; then 86 + ${pkgs.git}/bin/git config branch.main.remote origin 87 + ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote → origin" 88 + fi 89 + elif [[ "$current_remote" != "origin" ]]; then 90 + ${pkgs.gum}/bin/gum style --foreground 117 "Current default: $current_remote" 91 + if ${pkgs.gum}/bin/gum confirm "Change default push remote to origin (knot)?"; then 92 + ${pkgs.git}/bin/git config branch.main.remote origin 93 + ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote → origin" 94 + fi 95 + else 96 + ${pkgs.gum}/bin/gum style --foreground 35 "✓ Default push remote is origin" 97 + fi 98 + ''; 99 + 100 + in 101 + { 102 + programs.zsh = { 103 + enable = true; 104 + enableCompletion = true; 105 + autosuggestion.enable = true; 106 + syntaxHighlighting.enable = true; 107 + 108 + shellAliases = { 109 + # Navigation 110 + ll = "eza -l"; 111 + la = "eza -la"; 112 + l = "eza"; 113 + ls = "eza"; 114 + ".." = "cd .."; 115 + "..." = "cd ../.."; 116 + 117 + # Safety 118 + rm = "rm -i"; 119 + cp = "cp -i"; 120 + mv = "mv -i"; 121 + rr = "rm -Rf"; 122 + 123 + # Enhanced commands 124 + cd = "z"; 125 + cat = "bat --paging=never"; 126 + 127 + # Quick commands 128 + e = "exit"; 129 + c = "clear"; 130 + 131 + # Directory shortcuts 132 + dev = "z ~/dev"; 133 + proj = "z ~/dev/projects"; 134 + scripts = "z ~/dev/scripts"; 135 + 136 + # Git shortcuts 137 + g = "git"; 138 + gs = "git status"; 139 + gd = "git diff"; 140 + ga = "git add"; 141 + gc = "git commit"; 142 + gp = "git push"; 143 + gl = "git pull"; 144 + s = "git status"; 145 + push = "git push"; 146 + pull = "git pull"; 147 + goops = "git commit --amend --no-edit && git push --force-with-lease"; 148 + 149 + # Python 150 + pip = "pip3"; 151 + python = "python3"; 152 + 153 + # Project shortcuts 154 + dns = "z dev/dns && source .env && source .venv/bin/activate"; 155 + 156 + # Zsh config 157 + zshedit = "code ~/.zshrc"; 158 + zshreload = "source ~/.zshrc"; 159 + 160 + # GPG key sync 161 + keysync = "gpg --keyserver pgp.mit.edu --send-keys 00E643C21FAC965FFB28D3B714D0D45A1DADAAFA && gpg --keyserver keyserver.ubuntu.com --send-keys 00E643C21FAC965FFB28D3B714D0D45A1DADAAFA && gpg --keyserver keys.openpgp.org --send-keys 00E643C21FAC965FFB28D3B714D0D45A1DADAAFA && gpg --export me@jaspermayone.com | curl -T - https://keys.openpgp.org"; 162 + gpgend = "gpg --keyserver hkps://keys.openpgp.org --send-keys 14D0D45A1DADAAFA"; 163 + 164 + path="echo -e \${PATH//:/\\n}"; 165 + 166 + # Vim 167 + vi = "vim"; 168 + 169 + afk="/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend"; 170 + reload="exec \${SHELL} -l"; 171 + }; 172 + 173 + initContent = '' 174 + # ============================================================================ 175 + # POWERLEVEL10K INSTANT PROMPT 176 + # ============================================================================ 177 + if [[ -r "''${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-''${(%):-%n}.zsh" ]]; then 178 + source "''${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-''${(%):-%n}.zsh" 179 + fi 180 + 181 + # ============================================================================ 182 + # HOMEBREW 183 + # ============================================================================ 184 + if [[ -f /opt/homebrew/bin/brew ]]; then 185 + eval "$(/opt/homebrew/bin/brew shellenv)" 186 + fi 187 + 188 + # ============================================================================ 189 + # ZINIT PLUGIN MANAGER 190 + # ============================================================================ 191 + if [[ ! -f $HOME/.local/share/zinit/zinit.git/zinit.zsh ]]; then 192 + print -P "%F{33} %F{220}Installing %F{33}ZDHARMA-CONTINUUM%F{220} Initiative Plugin Manager (%F{33}zdharma-continuum/zinit%F{220})…%f" 193 + command mkdir -p "$HOME/.local/share/zinit" && command chmod g-rwX "$HOME/.local/share/zinit" 194 + command git clone https://github.com/zdharma-continuum/zinit "$HOME/.local/share/zinit/zinit.git" && \ 195 + print -P "%F{33} %F{34}Installation successful.%f%b" || \ 196 + print -P "%F{160} The clone has failed.%f%b" 197 + fi 198 + 199 + source "$HOME/.local/share/zinit/zinit.git/zinit.zsh" 200 + autoload -Uz _zinit 201 + (( ''${+_comps} )) && _comps[zinit]=_zinit 202 + 203 + # ============================================================================ 204 + # ZINIT THEME 205 + # ============================================================================ 206 + zinit ice depth"1"; zinit light romkatv/powerlevel10k 207 + 208 + # ============================================================================ 209 + # ZINIT PLUGINS 210 + # ============================================================================ 211 + zinit light zsh-users/zsh-completions 212 + zinit light zsh-users/zsh-autosuggestions 213 + 214 + # Defer fzf-tab 215 + zinit ice wait"0a" lucid 216 + zinit light Aloxaf/fzf-tab 217 + 218 + # Syntax highlighting loads in background 219 + zinit ice wait"1" lucid 220 + zinit light zsh-users/zsh-syntax-highlighting 221 + 222 + # ============================================================================ 223 + # OH-MY-ZSH SNIPPETS (deferred) 224 + # ============================================================================ 225 + zinit ice wait"0b" lucid 226 + zinit snippet OMZP::git-commit 227 + 228 + zinit ice wait"0b" lucid 229 + zinit snippet OMZP::sudo 230 + 231 + zinit ice wait"0b" lucid 232 + zinit snippet OMZP::command-not-found 233 + 234 + zinit ice wait"0b" lucid 235 + zinit snippet OMZP::iterm2 236 + 237 + # ============================================================================ 238 + # COMPLETIONS 239 + # ============================================================================ 240 + autoload -Uz compinit 241 + if [[ -n ''${HOME}/.zcompdump(#qN.mh+24) ]]; then 242 + compinit 243 + else 244 + compinit -C 245 + fi 246 + zinit cdreplay -q 247 + 248 + # ============================================================================ 249 + # KEY BINDINGS 250 + # ============================================================================ 251 + bindkey '^f' autosuggest-accept 252 + bindkey '^p' history-search-backward 253 + bindkey '^n' history-search-forward 254 + 255 + # ============================================================================ 256 + # HISTORY CONFIGURATION 257 + # ============================================================================ 258 + HISTFILE=~/.zsh_history 259 + HISTSIZE=999999999999 260 + SAVEHIST=999999999999 261 + HISTDUP=erase 262 + setopt appendhistory 263 + setopt extendedhistory 264 + setopt hist_ignore_space 265 + setopt hist_ignore_all_dups 266 + setopt hist_save_no_dups 267 + setopt hist_ignore_dups 268 + setopt hist_find_no_dups 269 + 270 + # ============================================================================ 271 + # COMPLETION STYLING 272 + # ============================================================================ 273 + zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' 274 + zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}" 275 + zstyle ':completion:*' menu no 276 + zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath' 277 + zstyle ':fzf-tab:complete:__zoxide_z:*' fzf-preview 'ls --color $realpath' 278 + 279 + # ============================================================================ 280 + # PATH EXPORTS 281 + # ============================================================================ 282 + # Note: flyctl, bun, pnpm are now managed by nix 283 + export PATH="$HOME/bin:$PATH" 284 + export PATH="$HOME/.local/bin:$PATH" 285 + export PATH="/Users/jsp/.lmstudio/bin:$PATH" 286 + export PATH="/Users/jsp/.codeium/windsurf/bin:$PATH" 287 + export PATH="/opt/homebrew/opt/mysql@8.0/bin:$PATH" 288 + export PATH="/Users/jsp/.antigravity/antigravity/bin:$PATH" 289 + export PATH="$HOME/go/bin:$PATH" 290 + 291 + # ============================================================================ 292 + # ENVIRONMENT VARIABLES 293 + # ============================================================================ 294 + export COMPOSE_BAKE=true 295 + export VISUAL="code --wait" 296 + export EDITOR="code --wait" 297 + export LDFLAGS="-L/opt/homebrew/opt/postgresql@17/lib" 298 + export CPPFLAGS="-I/opt/homebrew/opt/postgresql@17/include" 299 + export ENABLE_BACKGROUND_TASKS=1 300 + 301 + # Fix GPG issues with Homebrew install 302 + export GPG_TTY=$(tty) 303 + 304 + # ============================================================================ 305 + # SHELL INTEGRATIONS 306 + # ============================================================================ 307 + # Note: zoxide, fzf, atuin are initialized by home-manager programs.* 308 + 309 + # Mise activation 310 + eval "$(mise activate zsh)" 311 + 312 + # ============================================================================ 313 + # POWERLEVEL10K CONFIGURATION 314 + # ============================================================================ 315 + [[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh 316 + ''; 317 + }; 318 + 319 + # Common CLI tools 320 + home.packages = with pkgs; [ 321 + # Custom scripts 322 + tangled-setup 323 + 324 + # File management 325 + tree 326 + fd 327 + ripgrep 328 + bat 329 + eza 330 + unzip 331 + 332 + # System monitoring 333 + htop 334 + btop 335 + 336 + # Networking 337 + curl 338 + wget 339 + httpie 340 + 341 + # JSON/YAML 342 + jq 343 + yq 344 + 345 + # Misc 346 + fzf 347 + tmux 348 + watch 349 + gum # Required for tangled-setup script 350 + 351 + # Dev tools 352 + mise # Version manager (formerly rtx) 353 + flyctl # Fly.io CLI 354 + bun # JavaScript runtime 355 + nodePackages.pnpm # Package manager 356 + zmx-binary # Session persistence for terminal processes 357 + ]; 358 + 359 + # Fuzzy finder integration 360 + programs.fzf = { 361 + enable = true; 362 + enableZshIntegration = true; 363 + colors = { 364 + bg = lib.mkForce ""; 365 + }; 366 + }; 367 + 368 + # Better cat 369 + programs.bat = { 370 + enable = true; 371 + config = { 372 + theme = "TwoDark"; 373 + }; 374 + }; 375 + 376 + # Zoxide (better cd) 377 + programs.zoxide = { 378 + enable = true; 379 + enableZshIntegration = true; 380 + }; 381 + 382 + # Atuin (shell history) with sync 383 + programs.atuin = { 384 + enable = true; 385 + enableZshIntegration = true; 386 + settings = { 387 + auto_sync = true; 388 + sync_frequency = "5m"; 389 + sync_address = "https://api.atuin.sh"; 390 + search_mode = "fuzzy"; 391 + update_check = false; 392 + style = "auto"; 393 + }; 394 + }; 395 + 396 + }
+180
modules/ssh.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + with lib; 8 + let 9 + cfg = config.jsp.ssh; 10 + in 11 + { 12 + options.jsp.ssh = { 13 + enable = mkEnableOption "SSH configuration"; 14 + 15 + zmx = { 16 + enable = mkEnableOption "zmx integration for persistent sessions"; 17 + hosts = mkOption { 18 + type = types.listOf types.str; 19 + default = [ ]; 20 + description = "List of host patterns to enable zmx auto-attach (e.g., 'pve.*')"; 21 + }; 22 + }; 23 + 24 + extraConfig = mkOption { 25 + type = types.lines; 26 + default = ""; 27 + description = "Extra SSH configuration"; 28 + }; 29 + 30 + hosts = mkOption { 31 + type = types.attrsOf ( 32 + types.submodule { 33 + options = { 34 + hostname = mkOption { 35 + type = types.nullOr types.str; 36 + default = null; 37 + description = "Hostname or IP address"; 38 + }; 39 + 40 + port = mkOption { 41 + type = types.nullOr types.int; 42 + default = null; 43 + description = "SSH port"; 44 + }; 45 + 46 + user = mkOption { 47 + type = types.nullOr types.str; 48 + default = null; 49 + description = "Username for SSH connection"; 50 + }; 51 + 52 + identityFile = mkOption { 53 + type = types.nullOr types.str; 54 + default = null; 55 + description = "Path to SSH identity file"; 56 + }; 57 + 58 + identitiesOnly = mkOption { 59 + type = types.nullOr types.bool; 60 + default = null; 61 + description = "Only use specified identity files"; 62 + }; 63 + 64 + forwardAgent = mkOption { 65 + type = types.nullOr types.bool; 66 + default = null; 67 + description = "Enable SSH agent forwarding"; 68 + }; 69 + 70 + addKeysToAgent = mkOption { 71 + type = types.nullOr types.str; 72 + default = null; 73 + description = "Add keys to SSH agent (yes/no/confirm/ask)"; 74 + }; 75 + 76 + extraOptions = mkOption { 77 + type = types.attrsOf types.str; 78 + default = { }; 79 + description = "Additional SSH options for this host"; 80 + }; 81 + 82 + zmx = mkOption { 83 + type = types.bool; 84 + default = false; 85 + description = "Enable zmx persistent sessions for this host"; 86 + }; 87 + }; 88 + } 89 + ); 90 + default = { }; 91 + description = "SSH host configurations"; 92 + }; 93 + }; 94 + 95 + config = mkIf cfg.enable { 96 + # Install zmx and autossh when zmx is enabled 97 + home.packages = optionals cfg.zmx.enable [ 98 + pkgs.zmx-binary 99 + pkgs.autossh 100 + ]; 101 + 102 + programs.ssh = { 103 + enable = true; 104 + enableDefaultConfig = false; 105 + 106 + matchBlocks = 107 + let 108 + # Convert jsp.ssh.hosts to SSH matchBlocks 109 + hostConfigs = mapAttrs ( 110 + name: hostCfg: 111 + { 112 + hostname = mkIf (hostCfg.hostname != null) hostCfg.hostname; 113 + port = mkIf (hostCfg.port != null) hostCfg.port; 114 + user = mkIf (hostCfg.user != null) hostCfg.user; 115 + identityFile = mkIf (hostCfg.identityFile != null) hostCfg.identityFile; 116 + identitiesOnly = mkIf (hostCfg.identitiesOnly != null) hostCfg.identitiesOnly; 117 + forwardAgent = mkIf (hostCfg.forwardAgent != null) hostCfg.forwardAgent; 118 + addKeysToAgent = mkIf (hostCfg.addKeysToAgent != null) hostCfg.addKeysToAgent; 119 + extraOptions = hostCfg.extraOptions // ( 120 + if hostCfg.zmx then 121 + { 122 + RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %n"; 123 + RequestTTY = "yes"; 124 + ControlPath = "~/.ssh/cm-%r@%h:%p"; 125 + ControlMaster = "auto"; 126 + ControlPersist = "10m"; 127 + } 128 + else 129 + { } 130 + ); 131 + } 132 + ) cfg.hosts; 133 + 134 + # Create zmx pattern hosts if enabled 135 + zmxPatternHosts = if cfg.zmx.enable then 136 + listToAttrs ( 137 + map (pattern: 138 + let 139 + patternHost = cfg.hosts.${pattern} or {}; 140 + in { 141 + name = pattern; 142 + value = { 143 + hostname = mkIf (patternHost.hostname or null != null) patternHost.hostname; 144 + port = mkIf (patternHost.port or null != null) patternHost.port; 145 + user = mkIf (patternHost.user or null != null) patternHost.user; 146 + extraOptions = { 147 + RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %k"; 148 + RequestTTY = "yes"; 149 + ControlPath = "~/.ssh/cm-%r@%h:%p"; 150 + ControlMaster = "auto"; 151 + ControlPersist = "10m"; 152 + }; 153 + }; 154 + }) cfg.zmx.hosts 155 + ) 156 + else 157 + { }; 158 + 159 + # Default match block for extraConfig 160 + defaultBlock = if cfg.extraConfig != "" then 161 + { 162 + "*" = { }; 163 + } 164 + else 165 + { }; 166 + in 167 + defaultBlock // hostConfigs // zmxPatternHosts; 168 + 169 + extraConfig = cfg.extraConfig; 170 + }; 171 + 172 + # Add shell aliases for zmx usage 173 + programs.zsh.shellAliases = mkIf cfg.zmx.enable { 174 + zmls = "zmx list"; 175 + zmk = "zmx kill"; 176 + zma = "zmx attach"; 177 + ash = "autossh -M 0 -q"; 178 + }; 179 + }; 180 + }
+148
modules/status/default.nix
··· 1 + # Status monitoring module - serves /status endpoints for shields.io badges 2 + { config, lib, pkgs, ... }: 3 + 4 + with lib; 5 + 6 + let 7 + cfg = config.atelier.services.status; 8 + 9 + # Script to check services and write status JSON 10 + statusScript = pkgs.writeShellScript "status-check" '' 11 + set -euo pipefail 12 + STATUS_DIR="/var/lib/status" 13 + mkdir -p "$STATUS_DIR" 14 + 15 + # Check each configured service 16 + ${concatStringsSep "\n" (map (svc: '' 17 + if systemctl is-active --quiet ${escapeShellArg svc}; then 18 + echo "ok" > "$STATUS_DIR/${svc}" 19 + else 20 + rm -f "$STATUS_DIR/${svc}" 21 + fi 22 + '') cfg.services)} 23 + 24 + # Always write host status (if this runs, host is up) 25 + echo "ok" > "$STATUS_DIR/${cfg.hostname}" 26 + 27 + # Build services JSON 28 + SERVICES_JSON="{" 29 + ${concatStringsSep "\n" (imap0 (i: svc: '' 30 + if systemctl is-active --quiet ${escapeShellArg svc}; then 31 + SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":true" 32 + else 33 + SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":false" 34 + fi 35 + '') cfg.services)} 36 + SERVICES_JSON="$SERVICES_JSON}" 37 + 38 + # Write full status JSON 39 + cat > "$STATUS_DIR/status.json" << EOF 40 + { 41 + "hostname": "${cfg.hostname}", 42 + "timestamp": "$(date -Iseconds)", 43 + "services": $SERVICES_JSON 44 + } 45 + EOF 46 + ''; 47 + in 48 + { 49 + options.atelier.services.status = { 50 + enable = mkEnableOption "status monitoring endpoints"; 51 + 52 + hostname = mkOption { 53 + type = types.str; 54 + description = "Hostname for this machine's status endpoint"; 55 + }; 56 + 57 + domain = mkOption { 58 + type = types.str; 59 + description = "Domain to serve status on"; 60 + }; 61 + 62 + services = mkOption { 63 + type = types.listOf types.str; 64 + default = []; 65 + description = "List of systemd services to monitor"; 66 + }; 67 + 68 + cloudflareCredentialsFile = mkOption { 69 + type = types.nullOr types.path; 70 + default = null; 71 + description = "Path to Cloudflare credentials file for DNS challenge"; 72 + }; 73 + }; 74 + 75 + config = mkIf cfg.enable { 76 + # Timer to update status every minute 77 + systemd.services.status-check = { 78 + description = "Update status endpoints"; 79 + serviceConfig = { 80 + Type = "oneshot"; 81 + ExecStart = statusScript; 82 + }; 83 + }; 84 + 85 + systemd.timers.status-check = { 86 + description = "Run status check every minute"; 87 + wantedBy = [ "timers.target" ]; 88 + timerConfig = { 89 + OnBootSec = "30s"; 90 + OnUnitActiveSec = "1min"; 91 + }; 92 + }; 93 + 94 + # Ensure status directory exists 95 + systemd.tmpfiles.rules = [ 96 + "d /var/lib/status 0755 root root -" 97 + ]; 98 + 99 + # Caddy virtual host for status 100 + services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' 101 + ${optionalString (cfg.cloudflareCredentialsFile != null) '' 102 + tls { 103 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 104 + } 105 + ''} 106 + 107 + # Individual host status (returns 200 if file exists) 108 + @status_host path /status/${cfg.hostname} 109 + handle @status_host { 110 + @online file /var/lib/status/${cfg.hostname} 111 + handle @online { 112 + respond "ok" 200 113 + } 114 + handle { 115 + respond "offline" 503 116 + } 117 + } 118 + 119 + # Service status endpoints 120 + ${concatStringsSep "\n" (map (svc: '' 121 + @status_${svc} path /status/service/${svc} 122 + handle @status_${svc} { 123 + @online_${svc} file /var/lib/status/${svc} 124 + handle @online_${svc} { 125 + respond "ok" 200 126 + } 127 + handle { 128 + respond "offline" 503 129 + } 130 + } 131 + '') cfg.services)} 132 + 133 + # Full status JSON 134 + @status_json path /status 135 + handle @status_json { 136 + root * /var/lib/status 137 + rewrite * /status.json 138 + file_server 139 + header Content-Type application/json 140 + } 141 + 142 + # Root redirect to status 143 + handle { 144 + respond "alastor.hogwarts.channel - see /status" 200 145 + } 146 + ''; 147 + }; 148 + }
+188
modules/wifi.nix
··· 1 + # Simple NetworkManager WiFi module (NixOS only) 2 + # 3 + # Provides a simpler way to declare wifi profiles with NetworkManager. 4 + # - Pass PSK via environment variable, direct value, or file 5 + # - Supports eduroam networks with the `eduroam = true` flag 6 + # 7 + # Example usage: 8 + # jsp.network.wifi = { 9 + # enable = true; 10 + # profiles = { 11 + # "MySSID" = { psk = "supersecret"; }; 12 + # "eduroam" = { 13 + # eduroam = true; 14 + # identity = "user@university.edu"; 15 + # psk = "password"; 16 + # }; 17 + # }; 18 + # }; 19 + 20 + { 21 + lib, 22 + config, 23 + pkgs, 24 + ... 25 + }: 26 + let 27 + cfg = config.jsp.network.wifi; 28 + mkProfile = 29 + name: 30 + { 31 + pskVar ? null, 32 + psk ? null, 33 + pskFile ? null, 34 + eduroam ? false, 35 + identity ? null, 36 + }: 37 + let 38 + base = { 39 + connection = { 40 + id = name; 41 + type = "wifi"; 42 + }; 43 + ipv4.method = "auto"; 44 + ipv6 = { 45 + addr-gen-mode = "stable-privacy"; 46 + method = "auto"; 47 + }; 48 + wifi = { 49 + mode = "infrastructure"; 50 + ssid = name; 51 + }; 52 + }; 53 + sec = 54 + if eduroam then 55 + if pskVar != null then 56 + { 57 + wifi-security = { 58 + key-mgmt = "wpa-eap"; 59 + password = "$" + pskVar; 60 + identity = identity; 61 + phase2-auth = "mschapv2"; 62 + }; 63 + } 64 + else if psk != null then 65 + { 66 + wifi-security = { 67 + key-mgmt = "wpa-eap"; 68 + password = psk; 69 + identity = identity; 70 + phase2-auth = "mschapv2"; 71 + }; 72 + } 73 + else if pskFile != null then 74 + { 75 + wifi-security = { 76 + key-mgmt = "wpa-eap"; 77 + password = "$(" + pkgs.coreutils + "/bin/cat " + pskFile + ")"; 78 + identity = identity; 79 + phase2-auth = "mschapv2"; 80 + }; 81 + } 82 + else 83 + { } 84 + else if pskVar != null then 85 + { 86 + wifi-security = { 87 + key-mgmt = "wpa-psk"; 88 + psk = "$" + pskVar; 89 + }; 90 + } 91 + else if psk != null then 92 + { 93 + wifi-security = { 94 + key-mgmt = "wpa-psk"; 95 + psk = psk; 96 + }; 97 + } 98 + else if pskFile != null then 99 + { 100 + wifi-security = { 101 + key-mgmt = "wpa-psk"; 102 + psk = "$(" + pkgs.coreutils + "/bin/cat " + pskFile + ")"; 103 + }; 104 + } 105 + else 106 + { }; 107 + in 108 + base // sec; 109 + in 110 + { 111 + options.jsp.network.wifi = { 112 + enable = lib.mkEnableOption "NetworkManager with simplified Wi-Fi profiles"; 113 + 114 + hostName = lib.mkOption { 115 + type = lib.types.str; 116 + default = config.networking.hostName or "nixos"; 117 + description = "Hostname for the machine"; 118 + }; 119 + 120 + nameservers = lib.mkOption { 121 + type = lib.types.listOf lib.types.str; 122 + default = [ ]; 123 + description = "List of DNS nameservers"; 124 + }; 125 + 126 + envFile = lib.mkOption { 127 + type = lib.types.nullOr lib.types.path; 128 + default = null; 129 + description = "Environment file with PSK variables"; 130 + }; 131 + 132 + profiles = lib.mkOption { 133 + type = lib.types.attrsOf ( 134 + lib.types.submodule ( 135 + { name, ... }: 136 + { 137 + options = { 138 + pskVar = lib.mkOption { 139 + type = lib.types.nullOr lib.types.str; 140 + default = null; 141 + description = "Variable name in envFile providing PSK"; 142 + }; 143 + psk = lib.mkOption { 144 + type = lib.types.nullOr lib.types.str; 145 + default = null; 146 + description = "WiFi password (plaintext - prefer pskVar or pskFile)"; 147 + }; 148 + pskFile = lib.mkOption { 149 + type = lib.types.nullOr lib.types.path; 150 + default = null; 151 + description = "File containing the PSK"; 152 + }; 153 + eduroam = lib.mkOption { 154 + type = lib.types.bool; 155 + default = false; 156 + description = "Enable eduroam configuration"; 157 + }; 158 + identity = lib.mkOption { 159 + type = lib.types.nullOr lib.types.str; 160 + default = null; 161 + description = "Identity for eduroam authentication"; 162 + }; 163 + }; 164 + } 165 + ) 166 + ); 167 + default = { }; 168 + description = "Map of SSID -> WiFi configuration"; 169 + }; 170 + }; 171 + 172 + config = lib.mkIf cfg.enable { 173 + networking = { 174 + hostName = lib.mkIf (cfg.hostName != "") cfg.hostName; 175 + nameservers = lib.mkIf (cfg.nameservers != [ ]) cfg.nameservers; 176 + useDHCP = false; 177 + dhcpcd.enable = false; 178 + networkmanager = { 179 + enable = true; 180 + dns = "none"; 181 + ensureProfiles = { 182 + environmentFiles = lib.optional (cfg.envFile != null) cfg.envFile; 183 + profiles = lib.mapAttrs mkProfile cfg.profiles; 184 + }; 185 + }; 186 + }; 187 + }; 188 + }
+48
packages/zmx.nix
··· 1 + { pkgs, lib, stdenv, fetchurl, autoPatchelfHook }: 2 + 3 + stdenv.mkDerivation rec { 4 + pname = "zmx"; 5 + version = "0.1.0"; 6 + 7 + src = fetchurl { 8 + url = if stdenv.isLinux then 9 + (if stdenv.isAarch64 then 10 + "https://zmx.sh/a/zmx-${version}-linux-aarch64.tar.gz" 11 + else 12 + "https://zmx.sh/a/zmx-${version}-linux-x86_64.tar.gz") 13 + else if stdenv.isDarwin then 14 + (if stdenv.isAarch64 then 15 + "https://zmx.sh/a/zmx-${version}-macos-aarch64.tar.gz" 16 + else 17 + "https://zmx.sh/a/zmx-${version}-macos-x86_64.tar.gz") 18 + else throw "Unsupported platform"; 19 + 20 + hash = if stdenv.isLinux && stdenv.isAarch64 then 21 + "sha256-cMGo+Af0VRY3c2EoNzVZFU53Kz5wKL8zsSSXIOtZVU8=" 22 + else if stdenv.isLinux then 23 + "sha256-Zmqs/Y3be2z9KMuSwyTLZWKbIInzHgoC9Bm0S2jv3XI=" 24 + else if stdenv.isDarwin && stdenv.isAarch64 then 25 + "sha256-34k5Q1cIr3+foubtMJVoHVHZtCLoSjwJK00e1p0JdLg=" 26 + else 27 + "sha256-0epjoQhUSBYlE0L7Ubwn/sJF61+4BbxeaRx6EY/SklE="; 28 + }; 29 + 30 + nativeBuildInputs = lib.optionals stdenv.isLinux [ autoPatchelfHook ]; 31 + 32 + sourceRoot = "."; 33 + 34 + installPhase = '' 35 + runHook preInstall 36 + mkdir -p $out/bin 37 + cp zmx $out/bin/ 38 + chmod +x $out/bin/zmx 39 + runHook postInstall 40 + ''; 41 + 42 + meta = with lib; { 43 + description = "Session persistence for terminal processes"; 44 + homepage = "https://zmx.sh"; 45 + license = licenses.mit; 46 + platforms = platforms.unix; 47 + }; 48 + }
+17
profiles/bore.nix
··· 1 + # Bore tunnel client configuration 2 + { config, lib, pkgs, ... }: 3 + 4 + { 5 + imports = [ ../modules/bore ]; 6 + 7 + atelier.bore = { 8 + enable = true; 9 + serverAddr = "tun.hogwarts.channel"; 10 + serverPort = 7000; 11 + domain = "tun.hogwarts.channel"; 12 + authTokenFile = 13 + if pkgs.stdenv.isDarwin 14 + then "/Users/jsp/.config/bore/token" 15 + else "/home/jsp/.config/bore/token"; 16 + }; 17 + }
+8
rc/.curlrc
··· 1 + # Disguise as IE 9 on Windows 7. 2 + user-agent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)" 3 + 4 + # When following a redirect, automatically set the previous URL as referer. 5 + referer = ";auto" 6 + 7 + # Wait 60 seconds before timing out. 8 + connect-timeout = 60
+8
rc/.editorconfig
··· 1 + root = true 2 + 3 + [*] 4 + charset = utf-8 5 + indent_style = tab 6 + end_of_line = lf 7 + insert_final_newline = true 8 + trim_trailing_whitespace = true
+3
rc/.gitattributes
··· 1 + # Automatically normalize line endings for all text-based files 2 + #* text=auto 3 + # Disabled because of https://github.com/mathiasbynens/dotfiles/issues/149 :(
+49
rc/.gitignore
··· 1 + # Compiled source # 2 + ################### 3 + *.com 4 + *.class 5 + *.dll 6 + *.exe 7 + *.o 8 + *.so 9 + 10 + # Packages # 11 + ############ 12 + # it's better to unpack these files and commit the raw source 13 + # git has its own built in compression methods 14 + *.7z 15 + *.dmg 16 + *.gz 17 + *.iso 18 + *.jar 19 + *.rar 20 + *.tar 21 + *.zip 22 + 23 + # Logs and databases # 24 + ###################### 25 + *.log 26 + 27 + # OS generated files # 28 + ###################### 29 + .DS_Store 30 + .DS_Store? 31 + */.DS_Store 32 + ._* 33 + .Spotlight-V100 34 + .Trashes 35 + ehthumbs.db 36 + Thumbs.db% 37 + **/.DS_Store 38 + 39 + .llm-orc/* 40 + CLAUDE.local.md 41 + 42 + # Compiled Python files 43 + *.pyc 44 + 45 + Desktop.ini 46 + 47 + # Thumbnail cache files 48 + ._* 49 + Thumbs.db
+4
rc/.hushlogin
··· 1 + # The mere presence of this file in the home directory disables the system 2 + # copyright notice, the date and time of the last login, the message of the 3 + # day as well as other information that may otherwise appear on login. 4 + # See `man login`.
+8
rc/.screenrc
··· 1 + # Disable the startup message 2 + startup_message off 3 + 4 + # Set a large scrollback buffer 5 + defscrollback 32000 6 + 7 + # Always start `screen` with UTF-8 enabled (`screen -U`) 8 + defutf8 on
+32
rc/.wgetrc
··· 1 + # Use the server-provided last modification date, if available 2 + timestamping = on 3 + 4 + # Do not go up in the directory structure when downloading recursively 5 + no_parent = on 6 + 7 + # Wait 60 seconds before timing out. This applies to all timeouts: DNS, connect and read. (The default read timeout is 15 minutes!) 8 + timeout = 60 9 + 10 + # Retry a few times when a download fails, but don't overdo it. (The default is 20!) 11 + tries = 3 12 + 13 + # Retry even when the connection was refused 14 + retry_connrefused = on 15 + 16 + # Use the last component of a redirection URL for the local file name 17 + trust_server_names = on 18 + 19 + # Follow FTP links from HTML documents by default 20 + follow_ftp = on 21 + 22 + # Add a `.html` extension to `text/html` or `application/xhtml+xml` files that lack one, or a `.css` extension to `text/css` files that lack one 23 + adjust_extension = on 24 + 25 + # Ignore `robots.txt` and `<meta name=robots content=nofollow>` 26 + robots = off 27 + 28 + # Print the HTTP and FTP server responses 29 + server_response = on 30 + 31 + # Disguise as IE 9 on Windows 7 32 + user_agent = Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
+5
secrets/bore-token.age
··· 1 + age-encryption.org/v1 2 + -> ssh-ed25519 1uIO/w qOghAA1qK+aOlhwzWBbHioFW0Vk2LILv/h7lpgVUh1M 3 + gW0d+VYlib9bZjynm7hCFvvYNieixxE52l8zpZzbPPo 4 + --- v1GN0zn2mqOFLgTfPdcDOZcZMjuLaWYXwQGY5Gcgm5w 5 + �Rܑ��C1l�>&_V���p�T�baQ��n�� Z�'������Jcʖ�To֡�f�YV�F�}�ƒ�$�ˌ+�6=�E��� nC|������u���{�
+7
secrets/cloudflare-credentials.age
··· 1 + age-encryption.org/v1 2 + -> ssh-ed25519 1uIO/w wYO2WXQotsJxQoIU/m3ehHoPrzRlAjzgzFhJztxZkxY 3 + g4EXvMpf+ty3ZY3kY9ao4RJZ2AFdLClQah8dh45HevY 4 + --- cqsVo5ZEz6TRyA6bsCxg2DmxdT/9grdZA5OEiPV9KT4 5 + ���}��p��Y 6 + F 7 + �3[���S}�o�����qb����� ���^>UN ��s4(m�>A���Ld����б������c:G��d���_��]�
secrets/frps-token.age

This is a binary file and will not be displayed.

+6
secrets/github-token.age
··· 1 + age-encryption.org/v1 2 + -> ssh-ed25519 1uIO/w 6LYIliLMisjIdNQ8ERrgu+BSNrDC8jdiEhxB9j5BtQ4 3 + 8BI5lL2z/Z7+x2OnrzMSO1aK7V8SkRMrwGvA62MGChc 4 + --- l1QoB+v+xS3l8iVVnIvenj+VBQ644FIG4eKcaVQA83w 5 + ��EK�׵�� �<��I�:��� �u�?�?���U8�pI����h�� 6 + �Xz�8�[��&�+੻��6�"�1ZU���2.^��B�L�'�!�V��|��b��7;�{�BN����E����.�s�����3���A��nVÛ
secrets/knot-secret.age

This is a binary file and will not be displayed.

+46
secrets/secrets.nix
··· 1 + # Agenix secrets configuration 2 + # 3 + # This file declares which SSH keys can decrypt which secrets. 4 + # Run `agenix -e <secret>.age` to create/edit secrets. 5 + 6 + let 7 + # User SSH public keys (from ~/.ssh/id_ed25519.pub or similar) 8 + jsp = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHm7lo7umraewipgQu1Pifmoo/V8jYGDHjBTmt+7SOCe jsp@remus"; 9 + 10 + # Host SSH public keys (from /etc/ssh/ssh_host_ed25519_key.pub) 11 + # These are generated when you first boot NixOS 12 + # TODO: Replace with actual host keys after provisioning 13 + # alastor = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... root@alastor"; 14 + 15 + # Groups for convenience 16 + allUsers = [ jsp ]; 17 + allHosts = [ ]; 18 + # allHosts = [ alastor ]; 19 + all = allUsers ++ allHosts; 20 + in 21 + { 22 + # frp authentication token (used by both server and clients) 23 + # This is the shared secret between frps and bore clients 24 + "frps-token.age".publicKeys = all; 25 + 26 + # Cloudflare API credentials for ACME DNS challenge 27 + # Format: CF_DNS_API_TOKEN=xxxxx 28 + "cloudflare-credentials.age".publicKeys = [ jsp ]; 29 + # "cloudflare-credentials.age".publicKeys = [ jsp alastor ]; 30 + 31 + # Bore client token (same as frps-token, but separate file for clarity) 32 + # Used on client machines (remus, etc) 33 + "bore-token.age".publicKeys = all; 34 + 35 + # Tangled Knot server secret 36 + # Generate with: openssl rand -hex 32 37 + "knot-secret.age".publicKeys = all; 38 + 39 + # WiFi passwords for NixOS machines 40 + # Format: NETWORK_PSK=password 41 + "wifi-passwords.age".publicKeys = all; 42 + 43 + # GitHub token for knot-sync service 44 + # Format: GITHUB_TOKEN=ghp_xxxxx 45 + "github-token.age".publicKeys = all; 46 + }
+5
secrets/wifi-passwords.age
··· 1 + age-encryption.org/v1 2 + -> ssh-ed25519 1uIO/w iNRheD7WP6grtbcBW1/3/Eq/j75DgLllVNnEmDKkLBo 3 + l0A8n93RdcG5siaKY5pfihPV1/TGagPHESl1x1Huupg 4 + --- pJj9mo96JTyMOxBGwnafImnm8Ae082UUBVU1Pat042U 5 + �2��*u�p;�* ��k��-��������kr�t�< [q��-OB�Ӭ�#y��=�v`�����A�@a�I� Q����k;!c��