tangled
alpha
login
or
join now
jaspermayone.com
/
dots
0
fork
atom
@jaspermayone.com's dotfiles
0
fork
atom
overview
issues
pulls
pipelines
nix fmt
jaspermayone.com
2 months ago
d5ff32b0
98f4d5eb
+1002
-758
24 changed files
expand all
collapse all
unified
split
darwin
default.nix
flake.nix
home
default.nix
hosts
alastor
configuration.nix
hardware-configuration.nix
dippet
default.nix
horace
configuration.nix
hardware-configuration.nix
remus
default.nix
modules
atuin-server
default.nix
bluesky-pds
default.nix
bore
default.nix
configs.nix
frps
default.nix
git.nix
knot
sync.nix
restic
cli.nix
default.nix
shell.nix
ssh.nix
status
default.nix
packages
zmx.nix
profiles
bore.nix
secrets
secrets.nix
+50
-40
darwin/default.nix
···
1
1
# Darwin (macOS) specific configuration
2
2
-
{ config, pkgs, lib, inputs, hostname, ... }:
2
2
+
{
3
3
+
config,
4
4
+
pkgs,
5
5
+
lib,
6
6
+
inputs,
7
7
+
hostname,
8
8
+
...
9
9
+
}:
3
10
4
11
{
5
12
# Nix configuration
6
6
-
nix.settings.experimental-features = [ "nix-command" "flakes" ];
13
13
+
nix.settings.experimental-features = [
14
14
+
"nix-command"
15
15
+
"flakes"
16
16
+
];
7
17
8
18
# Fix GID mismatch for nixbld group (new installs use 350, old used 30000)
9
19
ids.gids.nixbld = 350;
···
39
49
lazygit
40
50
redis
41
51
mkcert
42
42
-
inetutils # telnet, ftp, etc.
52
52
+
inetutils # telnet, ftp, etc.
43
53
watchman
44
54
pipx
45
55
pwgen
···
58
68
enable = true;
59
69
onActivation = {
60
70
autoUpdate = true;
61
61
-
cleanup = "zap"; # Remove unlisted packages
71
71
+
cleanup = "zap"; # Remove unlisted packages
62
72
upgrade = true;
63
73
};
64
74
···
78
88
# CLI tools (only macOS-specific, special taps, or unavailable in nixpkgs)
79
89
brews = [
80
90
# macOS specific
81
81
-
"mas" # Mac App Store CLI
82
82
-
"libyaml" # Required for mise-installed Ruby (psych gem)
91
91
+
"mas" # Mac App Store CLI
92
92
+
"libyaml" # Required for mise-installed Ruby (psych gem)
83
93
84
94
# Font tools (bramstein tap)
85
95
"sfnt2woff"
···
109
119
"composer"
110
120
"openjdk"
111
121
"openjdk@21"
112
112
-
"rust" # Keep for toolchain management
122
122
+
"rust" # Keep for toolchain management
113
123
114
124
# Databases (specific versions)
115
125
"mysql@8.0"
···
140
150
141
151
# Mac App Store apps (requires `mas` CLI)
142
152
# masApps = {
143
143
-
# "Xcode" = 497799835;
153
153
+
# "Xcode" = 497799835;
144
154
# };
145
155
};
146
156
···
169
179
InitialKeyRepeat = 15;
170
180
KeyRepeat = 2;
171
181
ApplePressAndHoldEnabled = false;
172
172
-
AppleKeyboardUIMode = 3; # Full keyboard access
182
182
+
AppleKeyboardUIMode = 3; # Full keyboard access
173
183
174
184
# Scrolling
175
175
-
"com.apple.swipescrolldirection" = false; # (true = Natural scrolling
185
185
+
"com.apple.swipescrolldirection" = false; # (true = Natural scrolling
176
186
177
187
# Appearance
178
188
AppleShowAllExtensions = true;
···
184
194
AppleTemperatureUnit = "Fahrenheit";
185
195
186
196
# Window behavior
187
187
-
NSWindowResizeTime = 0.001; # Faster window resize
188
188
-
NSNavPanelExpandedStateForSaveMode = true; # Expand save panel
197
197
+
NSWindowResizeTime = 0.001; # Faster window resize
198
198
+
NSNavPanelExpandedStateForSaveMode = true; # Expand save panel
189
199
NSNavPanelExpandedStateForSaveMode2 = true;
190
190
-
PMPrintingExpandedStateForPrint = true; # Expand print panel
200
200
+
PMPrintingExpandedStateForPrint = true; # Expand print panel
191
201
PMPrintingExpandedStateForPrint2 = true;
192
192
-
NSDocumentSaveNewDocumentsToCloud = false; # Save to disk, not iCloud
193
193
-
NSDisableAutomaticTermination = true; # Prevent auto-termination of apps
202
202
+
NSDocumentSaveNewDocumentsToCloud = false; # Save to disk, not iCloud
203
203
+
NSDisableAutomaticTermination = true; # Prevent auto-termination of apps
194
204
};
195
205
196
206
# Finder
···
199
209
AppleShowAllFiles = false;
200
210
CreateDesktop = true;
201
211
FXEnableExtensionChangeWarning = false;
202
202
-
FXPreferredViewStyle = "clmv"; # Column view
212
212
+
FXPreferredViewStyle = "clmv"; # Column view
203
213
QuitMenuItem = true;
204
214
ShowPathbar = true;
205
215
ShowStatusBar = true;
···
211
221
ShowRemovableMediaOnDesktop = false;
212
222
# Sorting and search
213
223
_FXSortFoldersFirst = true;
214
214
-
FXDefaultSearchScope = "SCcf"; # Search current folder
224
224
+
FXDefaultSearchScope = "SCcf"; # Search current folder
215
225
};
216
226
217
227
# Screenshots
···
236
246
237
247
# Dock
238
248
dock = {
239
239
-
autohide = true; # Auto-hide the dock when not in use
240
240
-
autohide-delay = 0.0; # Delay before dock appears on hover (0 = instant)
241
241
-
mineffect = "scale"; # Minimize animation: "scale" or "genie"
242
242
-
minimize-to-application = false; # Minimize windows into app icon vs separate dock item
243
243
-
mru-spaces = false; # Rearrange spaces based on most recent use
244
244
-
orientation = "left"; # Dock position: "left", "bottom", or "right"
245
245
-
show-recents = false; # Show recently used apps in separate dock section
246
246
-
tilesize = 48; # Icon size in pixels
247
247
-
launchanim = false; # Animate app launch (bouncing icon)
248
248
-
expose-animation-duration = 0.1; # Mission Control animation speed (lower = faster)
249
249
-
showhidden = false; # Dim hidden app icons (Cmd+H) to show they're hidden
249
249
+
autohide = true; # Auto-hide the dock when not in use
250
250
+
autohide-delay = 0.0; # Delay before dock appears on hover (0 = instant)
251
251
+
mineffect = "scale"; # Minimize animation: "scale" or "genie"
252
252
+
minimize-to-application = false; # Minimize windows into app icon vs separate dock item
253
253
+
mru-spaces = false; # Rearrange spaces based on most recent use
254
254
+
orientation = "left"; # Dock position: "left", "bottom", or "right"
255
255
+
show-recents = false; # Show recently used apps in separate dock section
256
256
+
tilesize = 48; # Icon size in pixels
257
257
+
launchanim = false; # Animate app launch (bouncing icon)
258
258
+
expose-animation-duration = 0.1; # Mission Control animation speed (lower = faster)
259
259
+
showhidden = false; # Dim hidden app icons (Cmd+H) to show they're hidden
250
260
};
251
261
252
262
# Trackpad
253
263
trackpad = {
254
254
-
Clicking = true; # Tap to click
264
264
+
Clicking = true; # Tap to click
255
265
TrackpadRightClick = true;
256
266
TrackpadThreeFingerDrag = true;
257
267
Dragging = true;
···
278
288
CustomUserPreferences = {
279
289
# System sound
280
290
"com.apple.systemsound" = {
281
281
-
"com.apple.sound.uiaudio.enabled" = 0; # Disable boot sound
291
291
+
"com.apple.sound.uiaudio.enabled" = 0; # Disable boot sound
282
292
};
283
293
284
294
# Help Viewer non-floating
···
304
314
# Finder extras
305
315
"com.apple.finder" = {
306
316
ShowRecentTags = false;
307
307
-
OpenWindowForNewRemovableDisk = true; # Auto-open for mounted volumes
317
317
+
OpenWindowForNewRemovableDisk = true; # Auto-open for mounted volumes
308
318
};
309
319
310
320
# Disk images: skip verification
···
321
331
322
332
# Safari
323
333
"com.apple.Safari" = {
324
324
-
UniversalSearchEnabled = false; # Don't send search queries to Apple
334
334
+
UniversalSearchEnabled = false; # Don't send search queries to Apple
325
335
SuppressSearchSuggestions = true;
326
336
ShowFullURLInSmartSearchField = true;
327
337
HomePage = "about:blank";
···
334
344
335
345
# Mail
336
346
"com.apple.mail" = {
337
337
-
AddressesIncludeNameOnPasteboard = false; # Copy addresses without name
347
347
+
AddressesIncludeNameOnPasteboard = false; # Copy addresses without name
338
348
NSUserKeyEquivalents = {
339
339
-
Send = "@\\U21a9"; # Cmd+Enter to send
349
349
+
Send = "@\\U21a9"; # Cmd+Enter to send
340
350
};
341
351
DisableInlineAttachmentViewing = true;
342
352
};
343
353
344
354
# Terminal
345
355
"com.apple.terminal" = {
346
346
-
StringEncodings = [ 4 ]; # UTF-8 only
356
356
+
StringEncodings = [ 4 ]; # UTF-8 only
347
357
};
348
358
349
359
# iTerm2
···
359
369
# Activity Monitor
360
370
"com.apple.ActivityMonitor" = {
361
371
OpenMainWindow = true;
362
362
-
ShowCategory = 0; # All processes
372
372
+
ShowCategory = 0; # All processes
363
373
SortColumn = "CPUUsage";
364
374
SortDirection = 0;
365
375
};
366
376
367
377
# TextEdit
368
378
"com.apple.TextEdit" = {
369
369
-
RichText = 0; # Plain text mode
370
370
-
PlainTextEncoding = 4; # UTF-8
379
379
+
RichText = 0; # Plain text mode
380
380
+
PlainTextEncoding = 4; # UTF-8
371
381
PlainTextEncodingForWrite = 4;
372
382
};
373
383
···
386
396
# Software Update
387
397
"com.apple.SoftwareUpdate" = {
388
398
AutomaticCheckEnabled = true;
389
389
-
ScheduleFrequency = 1; # Daily
399
399
+
ScheduleFrequency = 1; # Daily
390
400
AutomaticDownload = 1;
391
391
-
CriticalUpdateInstall = 1; # Auto-install security updates
401
401
+
CriticalUpdateInstall = 1; # Auto-install security updates
392
402
AutoUpdate = true;
393
403
};
394
404
+123
-112
flake.nix
···
55
55
};
56
56
};
57
57
58
58
-
outputs = {
59
59
-
self,
60
60
-
nixpkgs,
61
61
-
nixpkgs-unstable,
62
62
-
agenix,
63
63
-
home-manager,
64
64
-
nur,
65
65
-
nix-darwin,
66
66
-
deploy-rs,
67
67
-
tangled,
68
68
-
tgirlpkgs,
69
69
-
rust-fp,
70
70
-
...
71
71
-
}@inputs:
72
72
-
let
73
73
-
outputs = inputs.self.outputs;
58
58
+
outputs =
59
59
+
{
60
60
+
self,
61
61
+
nixpkgs,
62
62
+
nixpkgs-unstable,
63
63
+
agenix,
64
64
+
home-manager,
65
65
+
nur,
66
66
+
nix-darwin,
67
67
+
deploy-rs,
68
68
+
tangled,
69
69
+
tgirlpkgs,
70
70
+
rust-fp,
71
71
+
...
72
72
+
}@inputs:
73
73
+
let
74
74
+
outputs = inputs.self.outputs;
74
75
75
75
-
# Overlay to make unstable packages available as pkgs.unstable.*
76
76
-
# Also includes custom packages
77
77
-
unstable-overlays = {
78
78
-
nixpkgs.overlays = [
79
79
-
(final: prev: {
80
80
-
unstable = import nixpkgs-unstable {
81
81
-
system = final.stdenv.hostPlatform.system;
82
82
-
config.allowUnfree = true;
83
83
-
};
76
76
+
# Overlay to make unstable packages available as pkgs.unstable.*
77
77
+
# Also includes custom packages
78
78
+
unstable-overlays = {
79
79
+
nixpkgs.overlays = [
80
80
+
(final: prev: {
81
81
+
unstable = import nixpkgs-unstable {
82
82
+
system = final.stdenv.hostPlatform.system;
83
83
+
config.allowUnfree = true;
84
84
+
};
84
85
85
85
-
# Custom packages
86
86
-
zmx-binary = prev.callPackage ./packages/zmx.nix { };
86
86
+
# Custom packages
87
87
+
zmx-binary = prev.callPackage ./packages/zmx.nix { };
87
88
88
88
-
# Caddy with Cloudflare DNS plugin for ACME DNS challenges
89
89
-
caddy-cloudflare = prev.caddy.withPlugins {
90
90
-
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ];
91
91
-
hash = "sha256-dnhEjopeA0UiI+XVYHYpsjcEI6Y1Hacbi28hVKYQURg=";
92
92
-
};
93
93
-
})
94
94
-
];
95
95
-
};
89
89
+
# Caddy with Cloudflare DNS plugin for ACME DNS challenges
90
90
+
caddy-cloudflare = prev.caddy.withPlugins {
91
91
+
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ];
92
92
+
hash = "sha256-dnhEjopeA0UiI+XVYHYpsjcEI6Y1Hacbi28hVKYQURg=";
93
93
+
};
94
94
+
})
95
95
+
];
96
96
+
};
96
97
97
97
-
# Helper function to create NixOS configurations
98
98
-
mkNixos = hostname: system: nixpkgs.lib.nixosSystem {
99
99
-
inherit system;
100
100
-
specialArgs = { inherit inputs outputs hostname; };
101
101
-
modules = [
102
102
-
./hosts/${hostname}/configuration.nix
103
103
-
agenix.nixosModules.default
104
104
-
tgirlpkgs.nixosModules.default
105
105
-
unstable-overlays
106
106
-
nur.modules.nixos.default
107
107
-
home-manager.nixosModules.home-manager
108
108
-
{
109
109
-
home-manager.useGlobalPkgs = true;
110
110
-
home-manager.useUserPackages = true;
111
111
-
home-manager.backupFileExtension = "backup";
112
112
-
home-manager.extraSpecialArgs = { inherit inputs outputs hostname; isDarwin = false; };
113
113
-
home-manager.users.jsp = import ./home;
114
114
-
}
115
115
-
];
116
116
-
};
98
98
+
# Helper function to create NixOS configurations
99
99
+
mkNixos =
100
100
+
hostname: system:
101
101
+
nixpkgs.lib.nixosSystem {
102
102
+
inherit system;
103
103
+
specialArgs = { inherit inputs outputs hostname; };
104
104
+
modules = [
105
105
+
./hosts/${hostname}/configuration.nix
106
106
+
agenix.nixosModules.default
107
107
+
tgirlpkgs.nixosModules.default
108
108
+
unstable-overlays
109
109
+
nur.modules.nixos.default
110
110
+
home-manager.nixosModules.home-manager
111
111
+
{
112
112
+
home-manager.useGlobalPkgs = true;
113
113
+
home-manager.useUserPackages = true;
114
114
+
home-manager.backupFileExtension = "backup";
115
115
+
home-manager.extraSpecialArgs = {
116
116
+
inherit inputs outputs hostname;
117
117
+
isDarwin = false;
118
118
+
};
119
119
+
home-manager.users.jsp = import ./home;
120
120
+
}
121
121
+
];
122
122
+
};
117
123
118
118
-
# Helper function to create Darwin configurations
119
119
-
mkDarwin = hostname: system: nix-darwin.lib.darwinSystem {
120
120
-
inherit system;
121
121
-
specialArgs = { inherit inputs outputs hostname; };
122
122
-
modules = [
123
123
-
./darwin
124
124
-
./hosts/${hostname}
125
125
-
agenix.darwinModules.default
126
126
-
unstable-overlays
127
127
-
nur.modules.darwin.default
128
128
-
home-manager.darwinModules.home-manager
129
129
-
{
130
130
-
home-manager.useGlobalPkgs = true;
131
131
-
home-manager.useUserPackages = true;
132
132
-
home-manager.backupFileExtension = "backup";
133
133
-
home-manager.extraSpecialArgs = { inherit inputs outputs hostname; isDarwin = true; };
134
134
-
home-manager.users.jsp = import ./home;
135
135
-
}
136
136
-
];
137
137
-
};
138
138
-
in
139
139
-
{
140
140
-
# NixOS configurations
141
141
-
# Available through 'nixos-rebuild --flake .#hostname'
142
142
-
nixosConfigurations = {
143
143
-
alastor = mkNixos "alastor" "aarch64-linux";
144
144
-
horace = mkNixos "horace" "x86_64-linux";
145
145
-
};
124
124
+
# Helper function to create Darwin configurations
125
125
+
mkDarwin =
126
126
+
hostname: system:
127
127
+
nix-darwin.lib.darwinSystem {
128
128
+
inherit system;
129
129
+
specialArgs = { inherit inputs outputs hostname; };
130
130
+
modules = [
131
131
+
./darwin
132
132
+
./hosts/${hostname}
133
133
+
agenix.darwinModules.default
134
134
+
unstable-overlays
135
135
+
nur.modules.darwin.default
136
136
+
home-manager.darwinModules.home-manager
137
137
+
{
138
138
+
home-manager.useGlobalPkgs = true;
139
139
+
home-manager.useUserPackages = true;
140
140
+
home-manager.backupFileExtension = "backup";
141
141
+
home-manager.extraSpecialArgs = {
142
142
+
inherit inputs outputs hostname;
143
143
+
isDarwin = true;
144
144
+
};
145
145
+
home-manager.users.jsp = import ./home;
146
146
+
}
147
147
+
];
148
148
+
};
149
149
+
in
150
150
+
{
151
151
+
# NixOS configurations
152
152
+
# Available through 'nixos-rebuild --flake .#hostname'
153
153
+
nixosConfigurations = {
154
154
+
alastor = mkNixos "alastor" "aarch64-linux";
155
155
+
horace = mkNixos "horace" "x86_64-linux";
156
156
+
};
146
157
147
147
-
# Darwin configurations
148
148
-
# Available through 'darwin-rebuild switch --flake .#hostname'
149
149
-
darwinConfigurations = {
150
150
-
remus = mkDarwin "remus" "aarch64-darwin";
151
151
-
dippet = mkDarwin "dippet" "aarch64-darwin";
152
152
-
};
158
158
+
# Darwin configurations
159
159
+
# Available through 'darwin-rebuild switch --flake .#hostname'
160
160
+
darwinConfigurations = {
161
161
+
remus = mkDarwin "remus" "aarch64-darwin";
162
162
+
dippet = mkDarwin "dippet" "aarch64-darwin";
163
163
+
};
153
164
154
154
-
# Formatters
155
155
-
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-tree;
156
156
-
formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.nixfmt-tree;
165
165
+
# Formatters
166
166
+
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-tree;
167
167
+
formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.nixfmt-tree;
157
168
158
158
-
# Deploy-rs configurations
159
159
-
# Available through 'deploy .#alastor'
160
160
-
deploy.nodes = {
161
161
-
alastor = {
162
162
-
hostname = "alastor";
163
163
-
profiles.system = {
164
164
-
sshUser = "jsp";
165
165
-
user = "root";
166
166
-
path = deploy-rs.lib.aarch64-linux.activate.nixos self.nixosConfigurations.alastor;
169
169
+
# Deploy-rs configurations
170
170
+
# Available through 'deploy .#alastor'
171
171
+
deploy.nodes = {
172
172
+
alastor = {
173
173
+
hostname = "alastor";
174
174
+
profiles.system = {
175
175
+
sshUser = "jsp";
176
176
+
user = "root";
177
177
+
path = deploy-rs.lib.aarch64-linux.activate.nixos self.nixosConfigurations.alastor;
178
178
+
};
167
179
};
168
168
-
};
169
169
-
horace = {
170
170
-
hostname = "horace";
171
171
-
profiles.system = {
172
172
-
sshUser = "jsp";
173
173
-
user = "root";
174
174
-
path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.horace;
180
180
+
horace = {
181
181
+
hostname = "horace";
182
182
+
profiles.system = {
183
183
+
sshUser = "jsp";
184
184
+
user = "root";
185
185
+
path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.horace;
186
186
+
};
175
187
};
176
188
};
177
177
-
};
178
189
179
179
-
# Validation checks for deploy-rs
180
180
-
checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
181
181
-
};
190
190
+
# Validation checks for deploy-rs
191
191
+
checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
192
192
+
};
182
193
}
+12
-3
home/default.nix
···
1
1
# Home Manager configuration
2
2
# Shared between NixOS and Darwin
3
3
-
{ config, pkgs, lib, hostname, isDarwin, ... }:
3
3
+
{
4
4
+
config,
5
5
+
pkgs,
6
6
+
lib,
7
7
+
hostname,
8
8
+
isDarwin,
9
9
+
...
10
10
+
}:
4
11
5
12
{
6
13
imports = [
···
65
72
home.file = builtins.listToAttrs (
66
73
map (name: {
67
74
name = name;
68
68
-
value = { source = ../rc/${name}; };
75
75
+
value = {
76
76
+
source = ../rc/${name};
77
77
+
};
69
78
}) (builtins.attrNames (builtins.readDir ../rc))
70
79
);
71
80
···
92
101
hostname = "tun.hogwarts.channel";
93
102
user = "jsp";
94
103
identityFile = "~/.ssh/id_ed25519";
95
95
-
zmx = true; # auto-attach zmx session
104
104
+
zmx = true; # auto-attach zmx session
96
105
};
97
106
98
107
# Horace (named after Horace Slughorn)
+35
-12
hosts/alastor/configuration.nix
···
1
1
# Alastor - NixOS server running frp tunnel service (named after Mad-Eye Moody)
2
2
-
{ config, pkgs, lib, inputs, hostname, ... }:
2
2
+
{
3
3
+
config,
4
4
+
pkgs,
5
5
+
lib,
6
6
+
inputs,
7
7
+
hostname,
8
8
+
...
9
9
+
}:
3
10
4
11
{
5
12
imports = [
···
21
28
22
29
# Nix settings
23
30
nix = {
24
24
-
settings.experimental-features = [ "nix-command" "flakes" ];
31
31
+
settings.experimental-features = [
32
32
+
"nix-command"
33
33
+
"flakes"
34
34
+
];
25
35
optimise.automatic = true;
26
36
};
27
37
···
43
53
jq
44
54
tmux
45
55
bluesky-pds
46
46
-
inputs.agenix.packages.${pkgs.stdenv.hostPlatform.system}.default # agenix CLI
56
56
+
inputs.agenix.packages.${pkgs.stdenv.hostPlatform.system}.default # agenix CLI
47
57
];
48
58
49
59
# NH - NixOS helper
···
82
92
extraGroups = [ "wheel" ];
83
93
shell = pkgs.zsh;
84
94
openssh.authorizedKeys.keys = [
85
85
-
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHm7lo7umraewipgQu1Pifmoo/V8jYGDHjBTmt+7SOCe jsp@remus"
95
95
+
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHm7lo7umraewipgQu1Pifmoo/V8jYGDHjBTmt+7SOCe jsp@remus"
86
96
];
87
97
};
88
98
···
111
121
github-token = {
112
122
file = ../../secrets/github-token.age;
113
123
mode = "400";
114
114
-
owner = "git"; # tangled uses git user
124
124
+
owner = "git"; # tangled uses git user
115
125
};
116
126
pds = {
117
127
file = ../../secrets/pds.age;
···
161
171
enable = true;
162
172
hostname = "alastor";
163
173
domain = "alastor.hogwarts.channel";
164
164
-
services = [ "frps" "caddy" "tailscaled" "tangled-knot" "atuin-server" ];
165
165
-
remoteHosts = [ "remus" "dippet" ];
174
174
+
services = [
175
175
+
"frps"
176
176
+
"caddy"
177
177
+
"tailscaled"
178
178
+
"tangled-knot"
179
179
+
"atuin-server"
180
180
+
];
181
181
+
remoteHosts = [
182
182
+
"remus"
183
183
+
"dippet"
184
184
+
];
166
185
cloudflareCredentialsFile = config.age.secrets.cloudflare-credentials.path;
167
186
};
168
187
···
184
203
adminEmail = "pds-admin@hogwarts.dev";
185
204
environmentFile = config.age.secrets.pds.path;
186
205
mailerEnvironmentFile = config.age.secrets.pds-mailer.path;
187
187
-
enableGatekeeper = false; # Disabled for now - was causing pdsadmin issues
206
206
+
enableGatekeeper = false; # Disabled for now - was causing pdsadmin issues
188
207
enableAgeAssurance = true;
189
208
};
190
209
···
194
213
hostname = "atuin.hogwarts.dev";
195
214
cloudflareCredentialsFile = config.age.secrets.cloudflare-credentials.path;
196
215
};
197
197
-
198
216
199
217
# Knot to GitHub sync service
200
218
jsp.services.knot-sync = {
···
223
241
};
224
242
};
225
243
226
226
-
systemd.services.caddy.serviceConfig.EnvironmentFile = config.age.secrets.cloudflare-credentials.path;
244
244
+
systemd.services.caddy.serviceConfig.EnvironmentFile =
245
245
+
config.age.secrets.cloudflare-credentials.path;
227
246
228
228
-
networking.firewall.allowedTCPPorts = [ 80 443 2222 ]; # 2222 for knot SSH
247
247
+
networking.firewall.allowedTCPPorts = [
248
248
+
80
249
249
+
443
250
250
+
2222
251
251
+
]; # 2222 for knot SSH
229
252
230
253
# Castle backup system (disabled for now - enable when secrets are ready)
231
254
# To enable:
···
280
303
enable = true;
281
304
flake = "github:jaspermayone/dots#alastor";
282
305
dates = "04:00";
283
283
-
allowReboot = false; # Set to true if you want automatic reboots when needed
306
306
+
allowReboot = false; # Set to true if you want automatic reboots when needed
284
307
};
285
308
}
+20
-4
hosts/alastor/hardware-configuration.nix
···
1
1
# Hardware configuration for alastor VPS
2
2
# Generated by nix-infect on OCI ARM
3
3
-
{ config, lib, pkgs, modulesPath, ... }:
3
3
+
{
4
4
+
config,
5
5
+
lib,
6
6
+
pkgs,
7
7
+
modulesPath,
8
8
+
...
9
9
+
}:
4
10
5
11
{
6
12
imports = [
···
15
21
};
16
22
17
23
# Filesystems
18
18
-
fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; };
19
19
-
fileSystems."/boot" = { device = "/dev/disk/by-uuid/3807-C85F"; fsType = "vfat"; };
24
24
+
fileSystems."/" = {
25
25
+
device = "/dev/sda1";
26
26
+
fsType = "ext4";
27
27
+
};
28
28
+
fileSystems."/boot" = {
29
29
+
device = "/dev/disk/by-uuid/3807-C85F";
30
30
+
fsType = "vfat";
31
31
+
};
20
32
21
33
# OCI/Xen kernel modules
22
22
-
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" ];
34
34
+
boot.initrd.availableKernelModules = [
35
35
+
"ata_piix"
36
36
+
"uhci_hcd"
37
37
+
"xen_blkfront"
38
38
+
];
23
39
boot.initrd.kernelModules = [ "nvme" ];
24
40
25
41
# Enable QEMU guest agent for better VM integration
+14
-2
hosts/dippet/default.nix
···
1
1
# Dippet - Mac Mini (server + desktop)
2
2
-
{ config, pkgs, lib, inputs, hostname, ... }:
2
2
+
{
3
3
+
config,
4
4
+
pkgs,
5
5
+
lib,
6
6
+
inputs,
7
7
+
hostname,
8
8
+
...
9
9
+
}:
3
10
4
11
{
5
12
# Disable nix-darwin's Nix management (using Determinate Nix installer)
···
11
18
/run/current-system/sw/bin/darwin-rebuild switch --flake github:jaspermayone/dots#dippet
12
19
'';
13
20
serviceConfig = {
14
14
-
StartCalendarInterval = [{ Hour = 4; Minute = 0; }];
21
21
+
StartCalendarInterval = [
22
22
+
{
23
23
+
Hour = 4;
24
24
+
Minute = 0;
25
25
+
}
26
26
+
];
15
27
StandardOutPath = "/var/log/nix-darwin-upgrade.log";
16
28
StandardErrorPath = "/var/log/nix-darwin-upgrade.log";
17
29
};
+17
-4
hosts/horace/configuration.nix
···
1
1
# Horace - NixOS desktop (named after Horace Slughorn)
2
2
-
{ config, pkgs, lib, inputs, hostname, ... }:
2
2
+
{
3
3
+
config,
4
4
+
pkgs,
5
5
+
lib,
6
6
+
inputs,
7
7
+
hostname,
8
8
+
...
9
9
+
}:
3
10
4
11
{
5
12
imports = [
···
22
29
23
30
# Nix settings
24
31
nix = {
25
25
-
settings.experimental-features = [ "nix-command" "flakes" ];
32
32
+
settings.experimental-features = [
33
33
+
"nix-command"
34
34
+
"flakes"
35
35
+
];
26
36
optimise.automatic = true;
27
37
};
28
38
···
123
133
users.users.jsp = {
124
134
isNormalUser = true;
125
135
description = "Jasper";
126
126
-
extraGroups = [ "networkmanager" "wheel" ];
136
136
+
extraGroups = [
137
137
+
"networkmanager"
138
138
+
"wheel"
139
139
+
];
127
140
shell = pkgs.zsh;
128
141
openssh.authorizedKeys.keys = [
129
129
-
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHm7lo7umraewipgQu1Pifmoo/V8jYGDHjBTmt+7SOCe jsp@remus"
142
142
+
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHm7lo7umraewipgQu1Pifmoo/V8jYGDHjBTmt+7SOCe jsp@remus"
130
143
];
131
144
};
132
145
+30
-17
hosts/horace/hardware-configuration.nix
···
1
1
# Hardware configuration for horace
2
2
# Generated by 'nixos-generate-config'
3
3
-
{ config, lib, pkgs, modulesPath, ... }:
3
3
+
{
4
4
+
config,
5
5
+
lib,
6
6
+
pkgs,
7
7
+
modulesPath,
8
8
+
...
9
9
+
}:
4
10
5
11
{
6
6
-
imports =
7
7
-
[ (modulesPath + "/installer/scan/not-detected.nix")
8
8
-
];
12
12
+
imports = [
13
13
+
(modulesPath + "/installer/scan/not-detected.nix")
14
14
+
];
9
15
10
10
-
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "sdhci_pci" ];
16
16
+
boot.initrd.availableKernelModules = [
17
17
+
"xhci_pci"
18
18
+
"ahci"
19
19
+
"sdhci_pci"
20
20
+
];
11
21
boot.initrd.kernelModules = [ ];
12
22
boot.kernelModules = [ "kvm-intel" ];
13
23
boot.extraModulePackages = [ ];
14
24
15
15
-
fileSystems."/" =
16
16
-
{ device = "/dev/disk/by-uuid/4a20f65f-aba4-4436-979e-3fb6150f9189";
17
17
-
fsType = "ext4";
18
18
-
};
25
25
+
fileSystems."/" = {
26
26
+
device = "/dev/disk/by-uuid/4a20f65f-aba4-4436-979e-3fb6150f9189";
27
27
+
fsType = "ext4";
28
28
+
};
19
29
20
20
-
fileSystems."/boot" =
21
21
-
{ device = "/dev/disk/by-uuid/A280-6001";
22
22
-
fsType = "vfat";
23
23
-
options = [ "fmask=0077" "dmask=0077" ];
24
24
-
};
30
30
+
fileSystems."/boot" = {
31
31
+
device = "/dev/disk/by-uuid/A280-6001";
32
32
+
fsType = "vfat";
33
33
+
options = [
34
34
+
"fmask=0077"
35
35
+
"dmask=0077"
36
36
+
];
37
37
+
};
25
38
26
26
-
swapDevices =
27
27
-
[ { device = "/dev/disk/by-uuid/8516975c-9b9c-4cb9-9f11-67d3527b832d"; }
28
28
-
];
39
39
+
swapDevices = [
40
40
+
{ device = "/dev/disk/by-uuid/8516975c-9b9c-4cb9-9f11-67d3527b832d"; }
41
41
+
];
29
42
30
43
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
31
44
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+8
-1
hosts/remus/default.nix
···
1
1
# Remus - MacBook Pro M4 (dev laptop)
2
2
-
{ config, pkgs, lib, inputs, hostname, ... }:
2
2
+
{
3
3
+
config,
4
4
+
pkgs,
5
5
+
lib,
6
6
+
inputs,
7
7
+
hostname,
8
8
+
...
9
9
+
}:
3
10
4
11
{
5
12
# Host-specific overrides go here
+6
-1
modules/atuin-server/default.nix
···
1
1
# modules/atuin-server/default.nix
2
2
# NixOS module for self-hosted Atuin sync server
3
3
-
{ lib, config, pkgs, ... }:
3
3
+
{
4
4
+
lib,
5
5
+
config,
6
6
+
pkgs,
7
7
+
...
8
8
+
}:
4
9
let
5
10
cfg = config.atelier.services.atuin-server;
6
11
in
+33
-25
modules/bluesky-pds/default.nix
···
1
1
# modules/bluesky-pds/default.nix
2
2
# NixOS module enabling Bluesky PDS with Caddy reverse proxy and optional gatekeeper
3
3
-
{ lib, config, pkgs, ... }:
3
3
+
{
4
4
+
lib,
5
5
+
config,
6
6
+
pkgs,
7
7
+
...
8
8
+
}:
4
9
let
5
10
cfg = config.services.bluesky-pds-hosting;
6
11
pdsSettings = config.services.bluesky-pds.settings;
7
12
gatekeeperPort = 3001;
8
13
# When gatekeeper is enabled, Caddy proxies to gatekeeper; otherwise directly to PDS
9
9
-
proxyTarget = if cfg.enableGatekeeper then "localhost:${toString gatekeeperPort}" else "localhost:${toString cfg.port}";
14
14
+
proxyTarget =
15
15
+
if cfg.enableGatekeeper then
16
16
+
"localhost:${toString gatekeeperPort}"
17
17
+
else
18
18
+
"localhost:${toString cfg.port}";
10
19
in
11
20
{
12
21
options.services.bluesky-pds-hosting = {
···
49
58
config = lib.mkIf cfg.enable {
50
59
services.bluesky-pds = {
51
60
enable = true;
52
52
-
environmentFiles =
53
53
-
lib.lists.flatten [
54
54
-
[ cfg.environmentFile ]
55
55
-
(lib.optional (cfg.mailerEnvironmentFile != null) cfg.mailerEnvironmentFile)
56
56
-
];
61
61
+
environmentFiles = lib.lists.flatten [
62
62
+
[ cfg.environmentFile ]
63
63
+
(lib.optional (cfg.mailerEnvironmentFile != null) cfg.mailerEnvironmentFile)
64
64
+
];
57
65
settings = {
58
66
PDS_PORT = cfg.port;
59
67
PDS_HOSTNAME = cfg.hostname;
···
95
103
}
96
104
97
105
${lib.optionalString cfg.enableAgeAssurance ''
98
98
-
handle /xrpc/app.bsky.unspecced.getAgeAssuranceState {
99
99
-
header content-type "application/json"
100
100
-
header access-control-allow-headers "authorization,dpop,atproto-accept-labelers,atproto-proxy"
101
101
-
header access-control-allow-origin "*"
102
102
-
respond `{"lastInitiatedAt":"2025-07-14T14:22:43.912Z","status":"assured"}` 200
103
103
-
}
106
106
+
handle /xrpc/app.bsky.unspecced.getAgeAssuranceState {
107
107
+
header content-type "application/json"
108
108
+
header access-control-allow-headers "authorization,dpop,atproto-accept-labelers,atproto-proxy"
109
109
+
header access-control-allow-origin "*"
110
110
+
respond `{"lastInitiatedAt":"2025-07-14T14:22:43.912Z","status":"assured"}` 200
111
111
+
}
104
112
105
105
-
handle /xrpc/app.bsky.ageassurance.getConfig {
106
106
-
header content-type "application/json"
107
107
-
header access-control-allow-headers "authorization,dpop,atproto-accept-labelers,atproto-proxy"
108
108
-
header access-control-allow-origin "*"
109
109
-
respond `{"regions":[]}` 200
110
110
-
}
113
113
+
handle /xrpc/app.bsky.ageassurance.getConfig {
114
114
+
header content-type "application/json"
115
115
+
header access-control-allow-headers "authorization,dpop,atproto-accept-labelers,atproto-proxy"
116
116
+
header access-control-allow-origin "*"
117
117
+
respond `{"regions":[]}` 200
118
118
+
}
111
119
112
112
-
handle /xrpc/app.bsky.ageassurance.getState {
113
113
-
header content-type "application/json"
114
114
-
header access-control-allow-headers "authorization,dpop,atproto-accept-labelers,atproto-proxy"
115
115
-
header access-control-allow-origin "*"
116
116
-
respond `{"state":{"lastInitiatedAt":"2025-07-14T14:22:43.912Z","status":"assured","access":"full"},"metadata":{"accountCreatedAt":"2022-11-17T00:35:16.391Z"}}` 200
117
117
-
}
120
120
+
handle /xrpc/app.bsky.ageassurance.getState {
121
121
+
header content-type "application/json"
122
122
+
header access-control-allow-headers "authorization,dpop,atproto-accept-labelers,atproto-proxy"
123
123
+
header access-control-allow-origin "*"
124
124
+
respond `{"state":{"lastInitiatedAt":"2025-07-14T14:22:43.912Z","status":"assured","access":"full"},"metadata":{"accountCreatedAt":"2022-11-17T00:35:16.391Z"}}` 200
125
125
+
}
118
126
''}
119
127
120
128
reverse_proxy ${proxyTarget}
+327
-324
modules/bore/default.nix
···
8
8
cfg = config.atelier.bore;
9
9
10
10
boreScript = pkgs.writeShellScript "bore" ''
11
11
-
CONFIG_FILE="bore.toml"
11
11
+
CONFIG_FILE="bore.toml"
12
12
13
13
-
# Trap exit signals to ensure cleanup and exit immediately
14
14
-
trap 'exit 130' INT
15
15
-
trap 'exit 143' TERM
16
16
-
trap 'exit 129' HUP
13
13
+
# Trap exit signals to ensure cleanup and exit immediately
14
14
+
trap 'exit 130' INT
15
15
+
trap 'exit 143' TERM
16
16
+
trap 'exit 129' HUP
17
17
18
18
-
# Enable immediate exit on error or pipe failure
19
19
-
set -e
20
20
-
set -o pipefail
18
18
+
# Enable immediate exit on error or pipe failure
19
19
+
set -e
20
20
+
set -o pipefail
21
21
22
22
-
# Check for flags
23
23
-
if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
24
24
-
${pkgs.gum}/bin/gum style --bold --foreground 212 "Active tunnels"
25
25
-
echo
22
22
+
# Check for flags
23
23
+
if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
24
24
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Active tunnels"
25
25
+
echo
26
26
27
27
-
tunnels=$(${pkgs.curl}/bin/curl -s https://${cfg.domain}/api/proxy/http)
27
27
+
tunnels=$(${pkgs.curl}/bin/curl -s https://${cfg.domain}/api/proxy/http)
28
28
29
29
-
if ! echo "$tunnels" | ${pkgs.jq}/bin/jq -e '.proxies | length > 0' >/dev/null 2>&1; then
30
30
-
${pkgs.gum}/bin/gum style --foreground 117 "No active tunnels"
31
31
-
exit 0
32
32
-
fi
29
29
+
if ! echo "$tunnels" | ${pkgs.jq}/bin/jq -e '.proxies | length > 0' >/dev/null 2>&1; then
30
30
+
${pkgs.gum}/bin/gum style --foreground 117 "No active tunnels"
31
31
+
exit 0
32
32
+
fi
33
33
34
34
-
# Filter only online tunnels with valid conf
35
35
-
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | if .type == "http" then "\(.name) → https://\(.conf.subdomain).${cfg.domain} [http]" elif .type == "tcp" then "\(.name) → tcp://\(.conf.remotePort) → localhost:\(.conf.localPort) [tcp]" elif .type == "udp" then "\(.name) → udp://\(.conf.remotePort) → localhost:\(.conf.localPort) [udp]" else "\(.name) [\(.type)]" end' | while read -r line; do
36
36
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $line"
37
37
-
done
38
38
-
exit 0
39
39
-
fi
34
34
+
# Filter only online tunnels with valid conf
35
35
+
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | if .type == "http" then "\(.name) → https://\(.conf.subdomain).${cfg.domain} [http]" elif .type == "tcp" then "\(.name) → tcp://\(.conf.remotePort) → localhost:\(.conf.localPort) [tcp]" elif .type == "udp" then "\(.name) → udp://\(.conf.remotePort) → localhost:\(.conf.localPort) [udp]" else "\(.name) [\(.type)]" end' | while read -r line; do
36
36
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $line"
37
37
+
done
38
38
+
exit 0
39
39
+
fi
40
40
+
41
41
+
if [ "$1" = "--saved" ] || [ "$1" = "-s" ]; then
42
42
+
if [ ! -f "$CONFIG_FILE" ]; then
43
43
+
${pkgs.gum}/bin/gum style --foreground 117 "No bore.toml found in current directory"
44
44
+
exit 0
45
45
+
fi
40
46
41
41
-
if [ "$1" = "--saved" ] || [ "$1" = "-s" ]; then
42
42
-
if [ ! -f "$CONFIG_FILE" ]; then
43
43
-
${pkgs.gum}/bin/gum style --foreground 117 "No bore.toml found in current directory"
44
44
-
exit 0
45
45
-
fi
47
47
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Saved tunnels in bore.toml"
48
48
+
echo
46
49
47
47
-
${pkgs.gum}/bin/gum style --bold --foreground 212 "Saved tunnels in bore.toml"
48
48
-
echo
50
50
+
# Parse TOML and show tunnels
51
51
+
while IFS= read -r line; do
52
52
+
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
53
53
+
current_tunnel="''${BASH_REMATCH[1]}"
54
54
+
elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
55
55
+
port="''${BASH_REMATCH[1]}"
56
56
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
57
57
+
protocol="''${BASH_REMATCH[1]}"
58
58
+
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
59
59
+
label="''${BASH_REMATCH[1]}"
60
60
+
proto_display="''${protocol:-http}"
61
61
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
62
62
+
label=""
63
63
+
protocol=""
64
64
+
elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
65
65
+
proto_display="''${protocol:-http}"
66
66
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
67
67
+
current_tunnel=""
68
68
+
port=""
69
69
+
protocol=""
70
70
+
fi
71
71
+
done < "$CONFIG_FILE"
49
72
50
50
-
# Parse TOML and show tunnels
51
51
-
while IFS= read -r line; do
52
52
-
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
53
53
-
current_tunnel="''${BASH_REMATCH[1]}"
54
54
-
elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
55
55
-
port="''${BASH_REMATCH[1]}"
56
56
-
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
57
57
-
protocol="''${BASH_REMATCH[1]}"
58
58
-
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
59
59
-
label="''${BASH_REMATCH[1]}"
60
60
-
proto_display="''${protocol:-http}"
61
61
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
62
62
-
label=""
63
63
-
protocol=""
64
64
-
elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
65
65
-
proto_display="''${protocol:-http}"
66
66
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
67
67
-
current_tunnel=""
68
68
-
port=""
69
69
-
protocol=""
73
73
+
# Handle last entry if file doesn't end with blank line
74
74
+
if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
75
75
+
proto_display="''${protocol:-http}"
76
76
+
if [[ -n "$label" ]]; then
77
77
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
78
78
+
else
79
79
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
80
80
+
fi
81
81
+
fi
82
82
+
exit 0
70
83
fi
71
71
-
done < "$CONFIG_FILE"
72
84
73
73
-
# Handle last entry if file doesn't end with blank line
74
74
-
if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
75
75
-
proto_display="''${protocol:-http}"
76
76
-
if [[ -n "$label" ]]; then
77
77
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
85
85
+
# Get tunnel name/subdomain
86
86
+
if [ -n "$1" ]; then
87
87
+
tunnel_name="$1"
78
88
else
79
79
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
80
80
-
fi
81
81
-
fi
82
82
-
exit 0
83
83
-
fi
89
89
+
# Check if we have a bore.toml in current directory
90
90
+
if [ -f "$CONFIG_FILE" ]; then
91
91
+
# Count tunnels in TOML
92
92
+
tunnel_count=$(${pkgs.gnugrep}/bin/grep -c '^\[' "$CONFIG_FILE" 2>/dev/null || echo "0")
84
93
85
85
-
# Get tunnel name/subdomain
86
86
-
if [ -n "$1" ]; then
87
87
-
tunnel_name="$1"
88
88
-
else
89
89
-
# Check if we have a bore.toml in current directory
90
90
-
if [ -f "$CONFIG_FILE" ]; then
91
91
-
# Count tunnels in TOML
92
92
-
tunnel_count=$(${pkgs.gnugrep}/bin/grep -c '^\[' "$CONFIG_FILE" 2>/dev/null || echo "0")
94
94
+
if [ "$tunnel_count" -gt 0 ]; then
95
95
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
96
96
+
echo
93
97
94
94
-
if [ "$tunnel_count" -gt 0 ]; then
95
95
-
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
96
96
-
echo
98
98
+
# Show choice between new or saved
99
99
+
choice=$(${pkgs.gum}/bin/gum choose "New tunnel" "Use saved tunnel")
97
100
98
98
-
# Show choice between new or saved
99
99
-
choice=$(${pkgs.gum}/bin/gum choose "New tunnel" "Use saved tunnel")
101
101
+
if [ "$choice" = "Use saved tunnel" ]; then
102
102
+
# Extract tunnel names from TOML
103
103
+
saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/')
104
104
+
tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
100
105
101
101
-
if [ "$choice" = "Use saved tunnel" ]; then
102
102
-
# Extract tunnel names from TOML
103
103
-
saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/')
104
104
-
tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
106
106
+
if [ -z "$tunnel_name" ]; then
107
107
+
${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected"
108
108
+
exit 1
109
109
+
fi
105
110
106
106
-
if [ -z "$tunnel_name" ]; then
107
107
-
${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected"
108
108
-
exit 1
109
109
-
fi
111
111
+
# Parse TOML for this tunnel's config
112
112
+
in_section=false
113
113
+
while IFS= read -r line; do
114
114
+
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
115
115
+
if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then
116
116
+
in_section=true
117
117
+
else
118
118
+
in_section=false
119
119
+
fi
120
120
+
elif [[ "$in_section" = true ]]; then
121
121
+
if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
122
122
+
port="''${BASH_REMATCH[1]}"
123
123
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
124
124
+
protocol="''${BASH_REMATCH[1]}"
125
125
+
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
126
126
+
label="''${BASH_REMATCH[1]}"
127
127
+
fi
128
128
+
fi
129
129
+
done < "$CONFIG_FILE"
130
130
+
131
131
+
proto_display="''${protocol:-http}"
132
132
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Loaded from bore.toml: $tunnel_name → localhost:$port [$proto_display]''${label:+ [$label]}"
133
133
+
else
134
134
+
# New tunnel - prompt for protocol first to determine what to ask for
135
135
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
136
136
+
if [ -z "$protocol" ]; then
137
137
+
protocol="http"
138
138
+
fi
110
139
111
111
-
# Parse TOML for this tunnel's config
112
112
-
in_section=false
113
113
-
while IFS= read -r line; do
114
114
-
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
115
115
-
if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then
116
116
-
in_section=true
140
140
+
if [ "$protocol" = "http" ]; then
141
141
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
117
142
else
118
118
-
in_section=false
143
143
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
119
144
fi
120
120
-
elif [[ "$in_section" = true ]]; then
121
121
-
if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
122
122
-
port="''${BASH_REMATCH[1]}"
123
123
-
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
124
124
-
protocol="''${BASH_REMATCH[1]}"
125
125
-
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
126
126
-
label="''${BASH_REMATCH[1]}"
145
145
+
146
146
+
if [ -z "$tunnel_name" ]; then
147
147
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
148
148
+
exit 1
127
149
fi
128
150
fi
129
129
-
done < "$CONFIG_FILE"
151
151
+
else
152
152
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
153
153
+
echo
154
154
+
# Prompt for protocol first
155
155
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
156
156
+
if [ -z "$protocol" ]; then
157
157
+
protocol="http"
158
158
+
fi
159
159
+
160
160
+
if [ "$protocol" = "http" ]; then
161
161
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
162
162
+
else
163
163
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
164
164
+
fi
130
165
131
131
-
proto_display="''${protocol:-http}"
132
132
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ Loaded from bore.toml: $tunnel_name → localhost:$port [$proto_display]''${label:+ [$label]}"
166
166
+
if [ -z "$tunnel_name" ]; then
167
167
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
168
168
+
exit 1
169
169
+
fi
170
170
+
fi
133
171
else
134
134
-
# New tunnel - prompt for protocol first to determine what to ask for
172
172
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
173
173
+
echo
174
174
+
# Prompt for protocol first
135
175
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
136
176
if [ -z "$protocol" ]; then
137
177
protocol="http"
···
148
188
exit 1
149
189
fi
150
190
fi
151
151
-
else
152
152
-
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
153
153
-
echo
154
154
-
# Prompt for protocol first
155
155
-
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
156
156
-
if [ -z "$protocol" ]; then
157
157
-
protocol="http"
158
158
-
fi
191
191
+
fi
159
192
160
160
-
if [ "$protocol" = "http" ]; then
161
161
-
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
162
162
-
else
163
163
-
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
164
164
-
fi
165
165
-
166
166
-
if [ -z "$tunnel_name" ]; then
167
167
-
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
193
193
+
# Validate tunnel name (only for http subdomains)
194
194
+
if [ "$protocol" = "http" ]; then
195
195
+
if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
196
196
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
168
197
exit 1
169
198
fi
170
199
fi
171
171
-
else
172
172
-
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
173
173
-
echo
174
174
-
# Prompt for protocol first
175
175
-
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
176
176
-
if [ -z "$protocol" ]; then
177
177
-
protocol="http"
178
178
-
fi
179
200
180
180
-
if [ "$protocol" = "http" ]; then
181
181
-
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
182
182
-
else
183
183
-
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
201
201
+
# Get port (skip if loaded from saved config)
202
202
+
if [ -z "$port" ]; then
203
203
+
if [ -n "$2" ]; then
204
204
+
port="$2"
205
205
+
else
206
206
+
port=$(${pkgs.gum}/bin/gum input --placeholder "8000" --prompt "Local port: ")
207
207
+
if [ -z "$port" ]; then
208
208
+
${pkgs.gum}/bin/gum style --foreground 196 "No port provided"
209
209
+
exit 1
210
210
+
fi
211
211
+
fi
184
212
fi
185
213
186
186
-
if [ -z "$tunnel_name" ]; then
187
187
-
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
214
214
+
# Validate port
215
215
+
if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then
216
216
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid port (must be a number)"
188
217
exit 1
189
218
fi
190
190
-
fi
191
191
-
fi
192
219
193
193
-
# Validate tunnel name (only for http subdomains)
194
194
-
if [ "$protocol" = "http" ]; then
195
195
-
if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
196
196
-
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
197
197
-
exit 1
198
198
-
fi
199
199
-
fi
220
220
+
# Get optional protocol, label and save flag (skip if loaded from saved config)
221
221
+
save_config=false
222
222
+
if [ -z "$label" ]; then
223
223
+
shift 2 2>/dev/null || true
224
224
+
while [[ $# -gt 0 ]]; do
225
225
+
case "$1" in
226
226
+
--protocol|-p)
227
227
+
protocol="$2"
228
228
+
shift 2
229
229
+
;;
230
230
+
--label|-l)
231
231
+
label="$2"
232
232
+
shift 2
233
233
+
;;
234
234
+
--save)
235
235
+
save_config=true
236
236
+
shift
237
237
+
;;
238
238
+
*)
239
239
+
shift
240
240
+
;;
241
241
+
esac
242
242
+
done
200
243
201
201
-
# Get port (skip if loaded from saved config)
202
202
-
if [ -z "$port" ]; then
203
203
-
if [ -n "$2" ]; then
204
204
-
port="$2"
205
205
-
else
206
206
-
port=$(${pkgs.gum}/bin/gum input --placeholder "8000" --prompt "Local port: ")
207
207
-
if [ -z "$port" ]; then
208
208
-
${pkgs.gum}/bin/gum style --foreground 196 "No port provided"
209
209
-
exit 1
210
210
-
fi
211
211
-
fi
212
212
-
fi
244
244
+
# Prompt for protocol if not provided via flag and not loaded from saved config and not already set
245
245
+
if [ -z "$protocol" ]; then
246
246
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
247
247
+
if [ -z "$protocol" ]; then
248
248
+
protocol="http"
249
249
+
fi
250
250
+
fi
213
251
214
214
-
# Validate port
215
215
-
if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then
216
216
-
${pkgs.gum}/bin/gum style --foreground 196 "Invalid port (must be a number)"
217
217
-
exit 1
218
218
-
fi
252
252
+
# Prompt for label if not provided via flag and not loaded from saved config
253
253
+
if [ -z "$label" ]; then
254
254
+
# Allow multiple labels selection
255
255
+
labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom")
219
256
220
220
-
# Get optional protocol, label and save flag (skip if loaded from saved config)
221
221
-
save_config=false
222
222
-
if [ -z "$label" ]; then
223
223
-
shift 2 2>/dev/null || true
224
224
-
while [[ $# -gt 0 ]]; do
225
225
-
case "$1" in
226
226
-
--protocol|-p)
227
227
-
protocol="$2"
228
228
-
shift 2
229
229
-
;;
230
230
-
--label|-l)
231
231
-
label="$2"
232
232
-
shift 2
233
233
-
;;
234
234
-
--save)
235
235
-
save_config=true
236
236
-
shift
237
237
-
;;
238
238
-
*)
239
239
-
shift
240
240
-
;;
241
241
-
esac
242
242
-
done
257
257
+
if [ -n "$labels" ]; then
258
258
+
# Check if custom was selected
259
259
+
if echo "$labels" | ${pkgs.gnugrep}/bin/grep -q "custom"; then
260
260
+
custom_label=$(${pkgs.gum}/bin/gum input --placeholder "my-label" --prompt "Custom label: ")
261
261
+
if [ -z "$custom_label" ]; then
262
262
+
${pkgs.gum}/bin/gum style --foreground 196 "No custom label provided"
263
263
+
exit 1
264
264
+
fi
265
265
+
# Replace 'custom' with the actual custom label
266
266
+
labels=$(echo "$labels" | ${pkgs.gnused}/bin/sed "s/custom/$custom_label/")
267
267
+
fi
268
268
+
# Join labels with comma
269
269
+
label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//')
270
270
+
fi
271
271
+
fi
272
272
+
fi
243
273
244
244
-
# Prompt for protocol if not provided via flag and not loaded from saved config and not already set
245
245
-
if [ -z "$protocol" ]; then
246
246
-
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
274
274
+
# Default protocol to http if still not set
247
275
if [ -z "$protocol" ]; then
248
276
protocol="http"
249
277
fi
250
250
-
fi
251
251
-
252
252
-
# Prompt for label if not provided via flag and not loaded from saved config
253
253
-
if [ -z "$label" ]; then
254
254
-
# Allow multiple labels selection
255
255
-
labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom")
256
278
257
257
-
if [ -n "$labels" ]; then
258
258
-
# Check if custom was selected
259
259
-
if echo "$labels" | ${pkgs.gnugrep}/bin/grep -q "custom"; then
260
260
-
custom_label=$(${pkgs.gum}/bin/gum input --placeholder "my-label" --prompt "Custom label: ")
261
261
-
if [ -z "$custom_label" ]; then
262
262
-
${pkgs.gum}/bin/gum style --foreground 196 "No custom label provided"
263
263
-
exit 1
264
264
-
fi
265
265
-
# Replace 'custom' with the actual custom label
266
266
-
labels=$(echo "$labels" | ${pkgs.gnused}/bin/sed "s/custom/$custom_label/")
267
267
-
fi
268
268
-
# Join labels with comma
269
269
-
label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//')
279
279
+
# Check if local port is accessible
280
280
+
if ! ${pkgs.netcat}/bin/nc -z 127.0.0.1 "$port" 2>/dev/null; then
281
281
+
${pkgs.gum}/bin/gum style --foreground 214 "! Warning: Nothing listening on localhost:$port"
270
282
fi
271
271
-
fi
272
272
-
fi
273
283
274
274
-
# Default protocol to http if still not set
275
275
-
if [ -z "$protocol" ]; then
276
276
-
protocol="http"
277
277
-
fi
278
278
-
279
279
-
# Check if local port is accessible
280
280
-
if ! ${pkgs.netcat}/bin/nc -z 127.0.0.1 "$port" 2>/dev/null; then
281
281
-
${pkgs.gum}/bin/gum style --foreground 214 "! Warning: Nothing listening on localhost:$port"
282
282
-
fi
283
283
-
284
284
-
# Save configuration if requested
285
285
-
if [ "$save_config" = true ]; then
286
286
-
# Check if tunnel already exists in TOML
287
287
-
if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then
288
288
-
# Update existing entry
289
289
-
${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{
290
290
-
s/^port[[:space:]]*=.*/port = $port/
291
291
-
s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/
292
292
-
''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/}
293
293
-
}" "$CONFIG_FILE"
294
294
-
else
295
295
-
# Append new entry
296
296
-
{
297
297
-
echo ""
298
298
-
echo "[$tunnel_name]"
299
299
-
echo "port = $port"
300
300
-
if [ "$protocol" != "http" ]; then
301
301
-
echo "protocol = \"$protocol\""
284
284
+
# Save configuration if requested
285
285
+
if [ "$save_config" = true ]; then
286
286
+
# Check if tunnel already exists in TOML
287
287
+
if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then
288
288
+
# Update existing entry
289
289
+
${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{
290
290
+
s/^port[[:space:]]*=.*/port = $port/
291
291
+
s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/
292
292
+
''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/}
293
293
+
}" "$CONFIG_FILE"
294
294
+
else
295
295
+
# Append new entry
296
296
+
{
297
297
+
echo ""
298
298
+
echo "[$tunnel_name]"
299
299
+
echo "port = $port"
300
300
+
if [ "$protocol" != "http" ]; then
301
301
+
echo "protocol = \"$protocol\""
302
302
+
fi
303
303
+
if [ -n "$label" ]; then
304
304
+
echo "label = \"$label\""
305
305
+
fi
306
306
+
} >> "$CONFIG_FILE"
302
307
fi
303
303
-
if [ -n "$label" ]; then
304
304
-
echo "label = \"$label\""
305
305
-
fi
306
306
-
} >> "$CONFIG_FILE"
307
307
-
fi
308
308
309
309
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ Configuration saved to bore.toml"
310
310
-
echo
311
311
-
fi
309
309
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Configuration saved to bore.toml"
310
310
+
echo
311
311
+
fi
312
312
313
313
-
# Create config file
314
314
-
config_file=$(${pkgs.coreutils}/bin/mktemp)
315
315
-
trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
313
313
+
# Create config file
314
314
+
config_file=$(${pkgs.coreutils}/bin/mktemp)
315
315
+
trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
316
316
317
317
-
# Encode label into proxy name if provided (format: tunnel_name[label1,label2])
318
318
-
proxy_name="$tunnel_name"
319
319
-
if [ -n "$label" ]; then
320
320
-
proxy_name="''${tunnel_name}[''${label}]"
321
321
-
fi
317
317
+
# Encode label into proxy name if provided (format: tunnel_name[label1,label2])
318
318
+
proxy_name="$tunnel_name"
319
319
+
if [ -n "$label" ]; then
320
320
+
proxy_name="''${tunnel_name}[''${label}]"
321
321
+
fi
322
322
323
323
-
# Build proxy configuration based on protocol
324
324
-
if [ "$protocol" = "http" ]; then
325
325
-
${pkgs.coreutils}/bin/cat > $config_file <<EOF
326
326
-
serverAddr = "${cfg.serverAddr}"
327
327
-
serverPort = ${toString cfg.serverPort}
323
323
+
# Build proxy configuration based on protocol
324
324
+
if [ "$protocol" = "http" ]; then
325
325
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
326
326
+
serverAddr = "${cfg.serverAddr}"
327
327
+
serverPort = ${toString cfg.serverPort}
328
328
329
329
-
auth.method = "token"
330
330
-
auth.tokenSource.type = "file"
331
331
-
auth.tokenSource.file.path = "${cfg.authTokenFile}"
329
329
+
auth.method = "token"
330
330
+
auth.tokenSource.type = "file"
331
331
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
332
332
333
333
-
[[proxies]]
334
334
-
name = "$proxy_name"
335
335
-
type = "http"
336
336
-
localIP = "127.0.0.1"
337
337
-
localPort = $port
338
338
-
subdomain = "$tunnel_name"
339
339
-
EOF
340
340
-
elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
341
341
-
# For TCP/UDP, enable admin API to query allocated port
342
342
-
admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')
333
333
+
[[proxies]]
334
334
+
name = "$proxy_name"
335
335
+
type = "http"
336
336
+
localIP = "127.0.0.1"
337
337
+
localPort = $port
338
338
+
subdomain = "$tunnel_name"
339
339
+
EOF
340
340
+
elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
341
341
+
# For TCP/UDP, enable admin API to query allocated port
342
342
+
admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')
343
343
344
344
-
${pkgs.coreutils}/bin/cat > $config_file <<EOF
345
345
-
serverAddr = "${cfg.serverAddr}"
346
346
-
serverPort = ${toString cfg.serverPort}
344
344
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
345
345
+
serverAddr = "${cfg.serverAddr}"
346
346
+
serverPort = ${toString cfg.serverPort}
347
347
348
348
-
auth.method = "token"
349
349
-
auth.tokenSource.type = "file"
350
350
-
auth.tokenSource.file.path = "${cfg.authTokenFile}"
348
348
+
auth.method = "token"
349
349
+
auth.tokenSource.type = "file"
350
350
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
351
351
352
352
-
webServer.addr = "127.0.0.1"
353
353
-
webServer.port = $admin_port
352
352
+
webServer.addr = "127.0.0.1"
353
353
+
webServer.port = $admin_port
354
354
355
355
-
[[proxies]]
356
356
-
name = "$proxy_name"
357
357
-
type = "$protocol"
358
358
-
localIP = "127.0.0.1"
359
359
-
localPort = $port
360
360
-
remotePort = 0
361
361
-
EOF
362
362
-
else
363
363
-
${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)"
364
364
-
exit 1
365
365
-
fi
355
355
+
[[proxies]]
356
356
+
name = "$proxy_name"
357
357
+
type = "$protocol"
358
358
+
localIP = "127.0.0.1"
359
359
+
localPort = $port
360
360
+
remotePort = 0
361
361
+
EOF
362
362
+
else
363
363
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)"
364
364
+
exit 1
365
365
+
fi
366
366
367
367
-
# Start tunnel
368
368
-
echo
369
369
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel configured"
370
370
-
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
371
371
-
if [ "$protocol" = "http" ]; then
372
372
-
public_url="https://$tunnel_name.${cfg.domain}"
373
373
-
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
374
374
-
else
375
375
-
${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol"
376
376
-
${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..."
377
377
-
fi
378
378
-
echo
379
379
-
${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..."
380
380
-
echo
367
367
+
# Start tunnel
368
368
+
echo
369
369
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel configured"
370
370
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
371
371
+
if [ "$protocol" = "http" ]; then
372
372
+
public_url="https://$tunnel_name.${cfg.domain}"
373
373
+
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
374
374
+
else
375
375
+
${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol"
376
376
+
${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..."
377
377
+
fi
378
378
+
echo
379
379
+
${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..."
380
380
+
echo
381
381
382
382
-
# For TCP/UDP, capture output to parse allocated port
383
383
-
if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
384
384
-
${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do
385
385
-
echo "$line"
382
382
+
# For TCP/UDP, capture output to parse allocated port
383
383
+
if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
384
384
+
${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do
385
385
+
echo "$line"
386
386
387
387
-
# Look for successful proxy start
388
388
-
if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then
389
389
-
sleep 1
387
387
+
# Look for successful proxy start
388
388
+
if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then
389
389
+
sleep 1
390
390
391
391
-
proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}")
391
391
+
proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}")
392
392
393
393
-
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
394
394
-
if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then
395
395
-
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
396
396
-
fi
393
393
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
394
394
+
if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then
395
395
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
396
396
+
fi
397
397
398
398
-
remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$')
398
398
+
remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$')
399
399
400
400
-
if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then
401
401
-
echo
402
402
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel established"
403
403
-
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
404
404
-
${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port"
405
405
-
${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol"
406
406
-
echo
407
407
-
fi
400
400
+
if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then
401
401
+
echo
402
402
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel established"
403
403
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
404
404
+
${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port"
405
405
+
${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol"
406
406
+
echo
407
407
+
fi
408
408
+
fi
409
409
+
done
410
410
+
else
411
411
+
exec ${pkgs.frp}/bin/frpc -c $config_file
408
412
fi
409
409
-
done
410
410
-
else
411
411
-
exec ${pkgs.frp}/bin/frpc -c $config_file
412
412
-
fi
413
413
'';
414
414
415
415
bore = pkgs.stdenv.mkDerivation {
···
418
418
419
419
dontUnpack = true;
420
420
421
421
-
nativeBuildInputs = with pkgs; [ pandoc installShellFiles ];
421
421
+
nativeBuildInputs = with pkgs; [
422
422
+
pandoc
423
423
+
installShellFiles
424
424
+
];
422
425
423
426
manPageSrc = ./bore.1.md;
424
427
zshCompletionSrc = ./completions/bore.zsh;
+54
-41
modules/configs.nix
···
1
1
# Centralized application configs
2
2
# Manages configs for espanso, btop, gh, wakatime, etc.
3
3
-
{ config, lib, pkgs, isDarwin, ... }:
3
3
+
{
4
4
+
config,
5
5
+
lib,
6
6
+
pkgs,
7
7
+
isDarwin,
8
8
+
...
9
9
+
}:
4
10
5
11
let
6
12
# Paths for secrets - platform-specific
···
24
30
home.file = lib.mkMerge [
25
31
# macOS espanso paths
26
32
(lib.mkIf isDarwin {
27
27
-
"Library/Application Support/espanso/config/default.yml".source = ../configs/espanso/config/default.yml;
33
33
+
"Library/Application Support/espanso/config/default.yml".source =
34
34
+
../configs/espanso/config/default.yml;
28
35
"Library/Application Support/espanso/match/base.yml".source = ../configs/espanso/match/base.yml;
29
36
})
30
37
···
37
44
# VS Code settings (macOS)
38
45
(lib.mkIf isDarwin {
39
46
"Library/Application Support/Code/User/settings.json".source = ../configs/vscode/settings.json;
40
40
-
"Library/Application Support/Code/User/keybindings.json".source = ../configs/vscode/keybindings.json;
47
47
+
"Library/Application Support/Code/User/keybindings.json".source =
48
48
+
../configs/vscode/keybindings.json;
41
49
})
42
50
43
51
# VS Code settings (Linux)
···
49
57
50
58
# Activation script to decrypt secrets for user configs
51
59
# This runs on every home-manager activation
52
52
-
home.activation.decryptUserSecrets = lib.hm.dag.entryAfter ["writeBoundary"] ''
53
53
-
SECRETS_DIR="${dotsDir}/secrets"
54
54
-
AGE="${pkgs.age}/bin/age"
55
55
-
SSH_KEY="$HOME/.ssh/id_ed25519"
56
56
-
${if isDarwin then ''
57
57
-
ESPANSO_DIR="$HOME/Library/Application Support/espanso/match"
58
58
-
'' else ''
59
59
-
ESPANSO_DIR="$HOME/.config/espanso/match"
60
60
-
''}
60
60
+
home.activation.decryptUserSecrets = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
61
61
+
SECRETS_DIR="${dotsDir}/secrets"
62
62
+
AGE="${pkgs.age}/bin/age"
63
63
+
SSH_KEY="$HOME/.ssh/id_ed25519"
64
64
+
${
65
65
+
if isDarwin then
66
66
+
''
67
67
+
ESPANSO_DIR="$HOME/Library/Application Support/espanso/match"
68
68
+
''
69
69
+
else
70
70
+
''
71
71
+
ESPANSO_DIR="$HOME/.config/espanso/match"
72
72
+
''
73
73
+
}
61
74
62
62
-
# Only proceed if we have the SSH key for decryption
63
63
-
if [ -f "$SSH_KEY" ]; then
64
64
-
# Decrypt espanso secrets
65
65
-
ESPANSO_SECRETS="$SECRETS_DIR/espanso-secrets.age"
66
66
-
if [ -f "$ESPANSO_SECRETS" ]; then
67
67
-
$DRY_RUN_CMD mkdir -p "$ESPANSO_DIR"
68
68
-
$DRY_RUN_CMD $AGE -d -i "$SSH_KEY" "$ESPANSO_SECRETS" > "$ESPANSO_DIR/secrets.yml" 2>/dev/null || echo "Warning: Failed to decrypt espanso secrets"
69
69
-
fi
75
75
+
# Only proceed if we have the SSH key for decryption
76
76
+
if [ -f "$SSH_KEY" ]; then
77
77
+
# Decrypt espanso secrets
78
78
+
ESPANSO_SECRETS="$SECRETS_DIR/espanso-secrets.age"
79
79
+
if [ -f "$ESPANSO_SECRETS" ]; then
80
80
+
$DRY_RUN_CMD mkdir -p "$ESPANSO_DIR"
81
81
+
$DRY_RUN_CMD $AGE -d -i "$SSH_KEY" "$ESPANSO_SECRETS" > "$ESPANSO_DIR/secrets.yml" 2>/dev/null || echo "Warning: Failed to decrypt espanso secrets"
82
82
+
fi
83
83
+
84
84
+
# Decrypt wakatime API key and merge with config
85
85
+
WAKATIME_SECRET="$SECRETS_DIR/wakatime-api-key.age"
86
86
+
if [ -f "$WAKATIME_SECRET" ]; then
87
87
+
API_KEY=$($AGE -d -i "$SSH_KEY" "$WAKATIME_SECRET" 2>/dev/null || echo "")
88
88
+
if [ -n "$API_KEY" ]; then
89
89
+
$DRY_RUN_CMD cat > "$HOME/.wakatime.cfg" << EOF
90
90
+
[settings]
91
91
+
api_url = https://waka.hogwarts.dev/api
92
92
+
api_key = $API_KEY
93
93
+
debug = false
94
94
+
status_bar_coding_activity = true
95
95
+
status_bar_enabled = false
96
96
+
EOF
97
97
+
fi
98
98
+
fi
70
99
71
71
-
# Decrypt wakatime API key and merge with config
72
72
-
WAKATIME_SECRET="$SECRETS_DIR/wakatime-api-key.age"
73
73
-
if [ -f "$WAKATIME_SECRET" ]; then
74
74
-
API_KEY=$($AGE -d -i "$SSH_KEY" "$WAKATIME_SECRET" 2>/dev/null || echo "")
75
75
-
if [ -n "$API_KEY" ]; then
76
76
-
$DRY_RUN_CMD cat > "$HOME/.wakatime.cfg" << EOF
77
77
-
[settings]
78
78
-
api_url = https://waka.hogwarts.dev/api
79
79
-
api_key = $API_KEY
80
80
-
debug = false
81
81
-
status_bar_coding_activity = true
82
82
-
status_bar_enabled = false
83
83
-
EOF
100
100
+
# Decrypt npmrc (contains registry auth tokens)
101
101
+
NPMRC_SECRET="$SECRETS_DIR/npmrc.age"
102
102
+
if [ -f "$NPMRC_SECRET" ]; then
103
103
+
$DRY_RUN_CMD $AGE -d -i "$SSH_KEY" "$NPMRC_SECRET" > "$HOME/.npmrc" 2>/dev/null || echo "Warning: Failed to decrypt npmrc"
104
104
+
fi
84
105
fi
85
85
-
fi
86
86
-
87
87
-
# Decrypt npmrc (contains registry auth tokens)
88
88
-
NPMRC_SECRET="$SECRETS_DIR/npmrc.age"
89
89
-
if [ -f "$NPMRC_SECRET" ]; then
90
90
-
$DRY_RUN_CMD $AGE -d -i "$SSH_KEY" "$NPMRC_SECRET" > "$HOME/.npmrc" 2>/dev/null || echo "Warning: Failed to decrypt npmrc"
91
91
-
fi
92
92
-
fi
93
106
'';
94
107
}
+19
-7
modules/frps/default.nix
···
32
32
allowedTCPPorts = lib.mkOption {
33
33
type = lib.types.listOf lib.types.port;
34
34
default = lib.lists.range 20000 20099;
35
35
-
example = [ 20000 20001 20002 20003 20004 ];
35
35
+
example = [
36
36
+
20000
37
37
+
20001
38
38
+
20002
39
39
+
20003
40
40
+
20004
41
41
+
];
36
42
description = "TCP port range to allow for TCP tunnels (default: 20000-20099)";
37
43
};
38
44
39
45
allowedUDPPorts = lib.mkOption {
40
46
type = lib.types.listOf lib.types.port;
41
47
default = lib.lists.range 20000 20099;
42
42
-
example = [ 20000 20001 20002 20003 20004 ];
48
48
+
example = [
49
49
+
20000
50
50
+
20001
51
51
+
20002
52
52
+
20003
53
53
+
20004
54
54
+
];
43
55
description = "UDP port range to allow for UDP tunnels (default: 20000-20099)";
44
56
};
45
57
···
91
103
''
92
104
else
93
105
''auth.token = "${cfg.authToken}"'';
94
94
-
106
106
+
95
107
configFile = pkgs.writeText "frps.toml" ''
96
108
bindAddr = "${cfg.bindAddr}"
97
109
bindPort = ${toString cfg.bindPort}
···
108
120
109
121
# Subdomain support for *.${cfg.domain}
110
122
subDomainHost = "${cfg.domain}"
111
111
-
123
123
+
112
124
# Allow port ranges for TCP/UDP tunnels
113
125
# Format: [[{"start": 20000, "end": 20099}]]
114
126
allowPorts = [
···
138
150
# Automatically configure Caddy for wildcard domain
139
151
services.caddy = lib.mkIf cfg.enableCaddy {
140
152
enable = true;
141
141
-
153
153
+
142
154
# Dashboard for base domain
143
155
virtualHosts."${cfg.domain}" = {
144
156
extraConfig = ''
···
148
160
header {
149
161
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
150
162
}
151
151
-
163
163
+
152
164
# Proxy /api/* to frps dashboard
153
165
handle /api/* {
154
166
reverse_proxy localhost:7400
155
167
}
156
156
-
168
168
+
157
169
# Serve dashboard HTML
158
170
handle {
159
171
root * ${./.}
+14
-8
modules/git.nix
···
214
214
diff = {
215
215
algorithm = "histogram";
216
216
tool = "windsurf";
217
217
-
renames = "copies"; # Detect copies as well as renames
217
217
+
renames = "copies"; # Detect copies as well as renames
218
218
};
219
219
220
220
"difftool \"windsurf\"".cmd = "windsurf --diff $LOCAL $REMOTE";
···
285
285
286
286
branch.sort = "-committerdate";
287
287
column.ui = "auto";
288
288
-
} // (if isDarwin then {
289
289
-
# macOS specific
290
290
-
credential = {
291
291
-
helper = "osxkeychain";
292
292
-
};
293
293
-
"credential \"https://dev.azure.com\"".useHttpPath = true;
294
294
-
} else { });
288
288
+
}
289
289
+
// (
290
290
+
if isDarwin then
291
291
+
{
292
292
+
# macOS specific
293
293
+
credential = {
294
294
+
helper = "osxkeychain";
295
295
+
};
296
296
+
"credential \"https://dev.azure.com\"".useHttpPath = true;
297
297
+
}
298
298
+
else
299
299
+
{ }
300
300
+
);
295
301
};
296
302
297
303
# Delta for better diffs
+1
-1
modules/knot/sync.nix
···
49
49
description = "Sync Knot repositories to GitHub";
50
50
serviceConfig = {
51
51
Type = "oneshot";
52
52
-
User = "git"; # official tangled module uses git user
52
52
+
User = "git"; # official tangled module uses git user
53
53
EnvironmentFile = cfg.secretsFile;
54
54
ExecStart = pkgs.writeShellScript "knot-sync" ''
55
55
set -euo pipefail
+24
-12
modules/restic/cli.nix
···
16
16
# castle deploy - Remote deployment tools
17
17
# castle logs - Service log viewer
18
18
19
19
-
{ config, lib, pkgs, ... }:
19
19
+
{
20
20
+
config,
21
21
+
lib,
22
22
+
pkgs,
23
23
+
...
24
24
+
}:
20
25
21
26
let
22
27
cfg = config.castle.backup;
···
25
30
allBackupServices = lib.attrNames cfg.services;
26
31
27
32
# Generate manifest for disaster recovery
28
28
-
backupManifest = pkgs.writeText "backup-manifest.json" (builtins.toJSON {
29
29
-
version = 1;
30
30
-
generated = "nixos-rebuild";
31
31
-
services = lib.mapAttrs (name: backupCfg: {
32
32
-
paths = backupCfg.paths;
33
33
-
exclude = backupCfg.exclude or [];
34
34
-
tags = backupCfg.tags or [];
35
35
-
}) cfg.services;
36
36
-
});
33
33
+
backupManifest = pkgs.writeText "backup-manifest.json" (
34
34
+
builtins.toJSON {
35
35
+
version = 1;
36
36
+
generated = "nixos-rebuild";
37
37
+
services = lib.mapAttrs (name: backupCfg: {
38
38
+
paths = backupCfg.paths;
39
39
+
exclude = backupCfg.exclude or [ ];
40
40
+
tags = backupCfg.tags or [ ];
41
41
+
}) cfg.services;
42
42
+
}
43
43
+
);
37
44
38
45
castleCliScript = pkgs.writeShellScript "castle" ''
39
46
set -e
···
351
358
};
352
359
};
353
360
354
354
-
in {
361
361
+
in
362
362
+
{
355
363
config = lib.mkIf cfg.enable {
356
356
-
environment.systemPackages = [ castleCli pkgs.gum pkgs.jq ];
364
364
+
environment.systemPackages = [
365
365
+
castleCli
366
366
+
pkgs.gum
367
367
+
pkgs.jq
368
368
+
];
357
369
358
370
# Store manifest for reference
359
371
environment.etc."castle/backup-manifest.json".source = backupManifest;
+19
-10
modules/restic/default.nix
···
25
25
passwordFile = config.age.secrets."restic/password".path;
26
26
27
27
# Tags for easier filtering during restore
28
28
-
extraBackupArgs =
29
29
-
(map (t: "--tag ${t}") (serviceCfg.tags or [ "service:${name}" ]))
30
30
-
++ [ "--verbose" ];
28
28
+
extraBackupArgs = (map (t: "--tag ${t}") (serviceCfg.tags or [ "service:${name}" ])) ++ [
29
29
+
"--verbose"
30
30
+
];
31
31
32
32
# Retention policy
33
33
pruneOpts = [
···
46
46
};
47
47
48
48
# Pre/post backup hooks for database consistency
49
49
-
backupPrepareCommand = lib.optionalString (serviceCfg.preBackup or null != null) serviceCfg.preBackup;
50
50
-
backupCleanupCommand = lib.optionalString (serviceCfg.postBackup or null != null) serviceCfg.postBackup;
49
49
+
backupPrepareCommand = lib.optionalString (
50
50
+
serviceCfg.preBackup or null != null
51
51
+
) serviceCfg.preBackup;
52
52
+
backupCleanupCommand = lib.optionalString (
53
53
+
serviceCfg.postBackup or null != null
54
54
+
) serviceCfg.postBackup;
51
55
};
52
56
53
57
in
···
74
78
75
79
exclude = lib.mkOption {
76
80
type = lib.types.listOf lib.types.str;
77
77
-
default = [ "*.log" "node_modules" ".git" ];
81
81
+
default = [
82
82
+
"*.log"
83
83
+
"node_modules"
84
84
+
".git"
85
85
+
];
78
86
description = "Glob patterns to exclude from backup";
79
87
};
80
88
···
121
129
];
122
130
123
131
# Create restic backup jobs for each enabled service
124
124
-
services.restic.backups = lib.mapAttrs mkBackupJob (
125
125
-
lib.filterAttrs (n: v: v.enable) cfg.services
126
126
-
);
132
132
+
services.restic.backups = lib.mapAttrs mkBackupJob (lib.filterAttrs (n: v: v.enable) cfg.services);
127
133
128
134
# Add restic and sqlite to system packages for manual operations
129
129
-
environment.systemPackages = [ pkgs.restic pkgs.sqlite ];
135
135
+
environment.systemPackages = [
136
136
+
pkgs.restic
137
137
+
pkgs.sqlite
138
138
+
];
130
139
};
131
140
}
+17
-11
modules/shell.nix
···
1
1
# Shell configuration
2
2
-
{ config, lib, pkgs, hostname, ... }:
2
2
+
{
3
3
+
config,
4
4
+
lib,
5
5
+
pkgs,
6
6
+
hostname,
7
7
+
...
8
8
+
}:
3
9
4
10
let
5
11
# Tangled setup script for configuring git remotes
···
286
292
zsh_indicator = "";
287
293
bash_indicator = "bsh";
288
294
fish_indicator = "fish";
289
289
-
disabled = true; # enable if you switch shells often
295
295
+
disabled = true; # enable if you switch shells often
290
296
};
291
297
};
292
298
};
···
353
359
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";
354
360
gpgend = "gpg --keyserver hkps://keys.openpgp.org --send-keys 14D0D45A1DADAAFA";
355
361
356
356
-
path="echo -e \${PATH//:/\\n}";
362
362
+
path = "echo -e \${PATH//:/\\n}";
357
363
358
364
# Vim
359
365
vi = "vim";
360
366
361
361
-
afk="/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend";
362
362
-
reload="exec \${SHELL} -l";
367
367
+
afk = "/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend";
368
368
+
reload = "exec \${SHELL} -l";
363
369
};
364
370
365
371
initContent = ''
···
521
527
fzf
522
528
tmux
523
529
watch
524
524
-
gum # Required for tangled-setup script
530
530
+
gum # Required for tangled-setup script
525
531
526
532
# Dev tools
527
527
-
mise # Version manager (formerly rtx)
528
528
-
flyctl # Fly.io CLI
529
529
-
bun # JavaScript runtime
530
530
-
nodePackages.pnpm # Package manager
531
531
-
zmx-binary # Session persistence for terminal processes
533
533
+
mise # Version manager (formerly rtx)
534
534
+
flyctl # Fly.io CLI
535
535
+
bun # JavaScript runtime
536
536
+
nodePackages.pnpm # Package manager
537
537
+
zmx-binary # Session persistence for terminal processes
532
538
];
533
539
534
540
# Fuzzy finder integration
+45
-41
modules/ssh.nix
···
106
106
matchBlocks =
107
107
let
108
108
# Convert jsp.ssh.hosts to SSH matchBlocks
109
109
-
hostConfigs = mapAttrs (
110
110
-
name: hostCfg:
111
111
-
{
112
112
-
hostname = mkIf (hostCfg.hostname != null) hostCfg.hostname;
113
113
-
port = mkIf (hostCfg.port != null) hostCfg.port;
114
114
-
user = mkIf (hostCfg.user != null) hostCfg.user;
115
115
-
identityFile = mkIf (hostCfg.identityFile != null) hostCfg.identityFile;
116
116
-
identitiesOnly = mkIf (hostCfg.identitiesOnly != null) hostCfg.identitiesOnly;
117
117
-
forwardAgent = mkIf (hostCfg.forwardAgent != null) hostCfg.forwardAgent;
118
118
-
addKeysToAgent = mkIf (hostCfg.addKeysToAgent != null) hostCfg.addKeysToAgent;
119
119
-
extraOptions = hostCfg.extraOptions // (
109
109
+
hostConfigs = mapAttrs (name: hostCfg: {
110
110
+
hostname = mkIf (hostCfg.hostname != null) hostCfg.hostname;
111
111
+
port = mkIf (hostCfg.port != null) hostCfg.port;
112
112
+
user = mkIf (hostCfg.user != null) hostCfg.user;
113
113
+
identityFile = mkIf (hostCfg.identityFile != null) hostCfg.identityFile;
114
114
+
identitiesOnly = mkIf (hostCfg.identitiesOnly != null) hostCfg.identitiesOnly;
115
115
+
forwardAgent = mkIf (hostCfg.forwardAgent != null) hostCfg.forwardAgent;
116
116
+
addKeysToAgent = mkIf (hostCfg.addKeysToAgent != null) hostCfg.addKeysToAgent;
117
117
+
extraOptions =
118
118
+
hostCfg.extraOptions
119
119
+
// (
120
120
if hostCfg.zmx then
121
121
{
122
122
RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %n";
···
128
128
else
129
129
{ }
130
130
);
131
131
-
}
132
132
-
) cfg.hosts;
131
131
+
}) cfg.hosts;
133
132
134
133
# Create zmx pattern hosts if enabled
135
135
-
zmxPatternHosts = if cfg.zmx.enable then
136
136
-
listToAttrs (
137
137
-
map (pattern:
138
138
-
let
139
139
-
patternHost = cfg.hosts.${pattern} or {};
140
140
-
in {
141
141
-
name = pattern;
142
142
-
value = {
143
143
-
hostname = mkIf (patternHost.hostname or null != null) patternHost.hostname;
144
144
-
port = mkIf (patternHost.port or null != null) patternHost.port;
145
145
-
user = mkIf (patternHost.user or null != null) patternHost.user;
146
146
-
extraOptions = {
147
147
-
RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %k";
148
148
-
RequestTTY = "yes";
149
149
-
ControlPath = "~/.ssh/cm-%r@%h:%p";
150
150
-
ControlMaster = "auto";
151
151
-
ControlPersist = "10m";
134
134
+
zmxPatternHosts =
135
135
+
if cfg.zmx.enable then
136
136
+
listToAttrs (
137
137
+
map (
138
138
+
pattern:
139
139
+
let
140
140
+
patternHost = cfg.hosts.${pattern} or { };
141
141
+
in
142
142
+
{
143
143
+
name = pattern;
144
144
+
value = {
145
145
+
hostname = mkIf (patternHost.hostname or null != null) patternHost.hostname;
146
146
+
port = mkIf (patternHost.port or null != null) patternHost.port;
147
147
+
user = mkIf (patternHost.user or null != null) patternHost.user;
148
148
+
extraOptions = {
149
149
+
RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %k";
150
150
+
RequestTTY = "yes";
151
151
+
ControlPath = "~/.ssh/cm-%r@%h:%p";
152
152
+
ControlMaster = "auto";
153
153
+
ControlPersist = "10m";
154
154
+
};
152
155
};
153
153
-
};
154
154
-
}) cfg.zmx.hosts
155
155
-
)
156
156
-
else
157
157
-
{ };
156
156
+
}
157
157
+
) cfg.zmx.hosts
158
158
+
)
159
159
+
else
160
160
+
{ };
158
161
159
162
# Default match block for extraConfig
160
160
-
defaultBlock = if cfg.extraConfig != "" then
161
161
-
{
162
162
-
"*" = { };
163
163
-
}
164
164
-
else
165
165
-
{ };
163
163
+
defaultBlock =
164
164
+
if cfg.extraConfig != "" then
165
165
+
{
166
166
+
"*" = { };
167
167
+
}
168
168
+
else
169
169
+
{ };
166
170
in
167
171
defaultBlock // hostConfigs // zmxPatternHosts;
168
172
+66
-51
modules/status/default.nix
···
1
1
# Status monitoring module - serves /status endpoints for shields.io badges
2
2
-
{ config, lib, pkgs, ... }:
2
2
+
{
3
3
+
config,
4
4
+
lib,
5
5
+
pkgs,
6
6
+
...
7
7
+
}:
3
8
4
9
with lib;
5
10
···
13
18
mkdir -p "$STATUS_DIR"
14
19
15
20
# Check each configured service
16
16
-
${concatStringsSep "\n" (map (svc: ''
17
17
-
if systemctl is-active --quiet ${escapeShellArg svc}; then
18
18
-
echo "ok" > "$STATUS_DIR/${svc}"
19
19
-
else
20
20
-
rm -f "$STATUS_DIR/${svc}"
21
21
-
fi
22
22
-
'') cfg.services)}
21
21
+
${concatStringsSep "\n" (
22
22
+
map (svc: ''
23
23
+
if systemctl is-active --quiet ${escapeShellArg svc}; then
24
24
+
echo "ok" > "$STATUS_DIR/${svc}"
25
25
+
else
26
26
+
rm -f "$STATUS_DIR/${svc}"
27
27
+
fi
28
28
+
'') cfg.services
29
29
+
)}
23
30
24
31
# Always write host status (if this runs, host is up)
25
32
echo "ok" > "$STATUS_DIR/${cfg.hostname}"
26
33
27
34
# Check remote hosts via ping (Tailscale)
28
28
-
${concatStringsSep "\n" (map (host: ''
29
29
-
if ${pkgs.iputils}/bin/ping -c 1 -W 2 ${escapeShellArg host} >/dev/null 2>&1; then
30
30
-
echo "ok" > "$STATUS_DIR/${host}"
31
31
-
else
32
32
-
rm -f "$STATUS_DIR/${host}"
33
33
-
fi
34
34
-
'') cfg.remoteHosts)}
35
35
+
${concatStringsSep "\n" (
36
36
+
map (host: ''
37
37
+
if ${pkgs.iputils}/bin/ping -c 1 -W 2 ${escapeShellArg host} >/dev/null 2>&1; then
38
38
+
echo "ok" > "$STATUS_DIR/${host}"
39
39
+
else
40
40
+
rm -f "$STATUS_DIR/${host}"
41
41
+
fi
42
42
+
'') cfg.remoteHosts
43
43
+
)}
35
44
36
45
# Build services JSON
37
46
SERVICES_JSON="{"
38
38
-
${concatStringsSep "\n" (imap0 (i: svc: ''
39
39
-
if systemctl is-active --quiet ${escapeShellArg svc}; then
40
40
-
SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":true"
41
41
-
else
42
42
-
SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":false"
43
43
-
fi
44
44
-
'') cfg.services)}
47
47
+
${concatStringsSep "\n" (
48
48
+
imap0 (i: svc: ''
49
49
+
if systemctl is-active --quiet ${escapeShellArg svc}; then
50
50
+
SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":true"
51
51
+
else
52
52
+
SERVICES_JSON="$SERVICES_JSON${if i > 0 then "," else ""}\"${svc}\":false"
53
53
+
fi
54
54
+
'') cfg.services
55
55
+
)}
45
56
SERVICES_JSON="$SERVICES_JSON}"
46
57
47
58
# Write full status JSON
···
70
81
71
82
services = mkOption {
72
83
type = types.listOf types.str;
73
73
-
default = [];
84
84
+
default = [ ];
74
85
description = "List of systemd services to monitor";
75
86
};
76
87
77
88
remoteHosts = mkOption {
78
89
type = types.listOf types.str;
79
79
-
default = [];
90
90
+
default = [ ];
80
91
description = "List of remote hosts to check via ping (e.g. Tailscale hosts)";
81
92
};
82
93
···
114
125
# Caddy virtual host for status
115
126
services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
116
127
${optionalString (cfg.cloudflareCredentialsFile != null) ''
117
117
-
tls {
118
118
-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
119
119
-
}
128
128
+
tls {
129
129
+
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
130
130
+
}
120
131
''}
121
132
122
133
# Individual host status (returns 200 if file exists)
···
132
143
}
133
144
134
145
# Service status endpoints
135
135
-
${concatStringsSep "\n" (map (svc: ''
136
136
-
@status_${svc} path /status/service/${svc}
137
137
-
handle @status_${svc} {
138
138
-
@online_${svc} file /var/lib/status/${svc}
139
139
-
handle @online_${svc} {
140
140
-
respond "ok" 200
141
141
-
}
142
142
-
handle {
143
143
-
respond "offline" 503
144
144
-
}
145
145
-
}
146
146
-
'') cfg.services)}
146
146
+
${concatStringsSep "\n" (
147
147
+
map (svc: ''
148
148
+
@status_${svc} path /status/service/${svc}
149
149
+
handle @status_${svc} {
150
150
+
@online_${svc} file /var/lib/status/${svc}
151
151
+
handle @online_${svc} {
152
152
+
respond "ok" 200
153
153
+
}
154
154
+
handle {
155
155
+
respond "offline" 503
156
156
+
}
157
157
+
}
158
158
+
'') cfg.services
159
159
+
)}
147
160
148
161
# Remote host status endpoints (Tailscale)
149
149
-
${concatStringsSep "\n" (map (host: ''
150
150
-
@status_${host} path /status/${host}
151
151
-
handle @status_${host} {
152
152
-
@online_${host} file /var/lib/status/${host}
153
153
-
handle @online_${host} {
154
154
-
respond "ok" 200
155
155
-
}
156
156
-
handle {
157
157
-
respond "offline" 503
158
158
-
}
159
159
-
}
160
160
-
'') cfg.remoteHosts)}
162
162
+
${concatStringsSep "\n" (
163
163
+
map (host: ''
164
164
+
@status_${host} path /status/${host}
165
165
+
handle @status_${host} {
166
166
+
@online_${host} file /var/lib/status/${host}
167
167
+
handle @online_${host} {
168
168
+
respond "ok" 200
169
169
+
}
170
170
+
handle {
171
171
+
respond "offline" 503
172
172
+
}
173
173
+
}
174
174
+
'') cfg.remoteHosts
175
175
+
)}
161
176
162
177
# Full status JSON
163
178
@status_json path /status
+32
-19
packages/zmx.nix
···
1
1
-
{ pkgs, lib, stdenv, fetchurl, autoPatchelfHook }:
1
1
+
{
2
2
+
pkgs,
3
3
+
lib,
4
4
+
stdenv,
5
5
+
fetchurl,
6
6
+
autoPatchelfHook,
7
7
+
}:
2
8
3
9
stdenv.mkDerivation rec {
4
10
pname = "zmx";
5
11
version = "0.1.0";
6
12
7
13
src = fetchurl {
8
8
-
url = if stdenv.isLinux then
9
9
-
(if stdenv.isAarch64 then
10
10
-
"https://zmx.sh/a/zmx-${version}-linux-aarch64.tar.gz"
14
14
+
url =
15
15
+
if stdenv.isLinux then
16
16
+
(
17
17
+
if stdenv.isAarch64 then
18
18
+
"https://zmx.sh/a/zmx-${version}-linux-aarch64.tar.gz"
19
19
+
else
20
20
+
"https://zmx.sh/a/zmx-${version}-linux-x86_64.tar.gz"
21
21
+
)
22
22
+
else if stdenv.isDarwin then
23
23
+
(
24
24
+
if stdenv.isAarch64 then
25
25
+
"https://zmx.sh/a/zmx-${version}-macos-aarch64.tar.gz"
26
26
+
else
27
27
+
"https://zmx.sh/a/zmx-${version}-macos-x86_64.tar.gz"
28
28
+
)
11
29
else
12
12
-
"https://zmx.sh/a/zmx-${version}-linux-x86_64.tar.gz")
13
13
-
else if stdenv.isDarwin then
14
14
-
(if stdenv.isAarch64 then
15
15
-
"https://zmx.sh/a/zmx-${version}-macos-aarch64.tar.gz"
30
30
+
throw "Unsupported platform";
31
31
+
32
32
+
hash =
33
33
+
if stdenv.isLinux && stdenv.isAarch64 then
34
34
+
"sha256-cMGo+Af0VRY3c2EoNzVZFU53Kz5wKL8zsSSXIOtZVU8="
35
35
+
else if stdenv.isLinux then
36
36
+
"sha256-Zmqs/Y3be2z9KMuSwyTLZWKbIInzHgoC9Bm0S2jv3XI="
37
37
+
else if stdenv.isDarwin && stdenv.isAarch64 then
38
38
+
"sha256-34k5Q1cIr3+foubtMJVoHVHZtCLoSjwJK00e1p0JdLg="
16
39
else
17
17
-
"https://zmx.sh/a/zmx-${version}-macos-x86_64.tar.gz")
18
18
-
else throw "Unsupported platform";
19
19
-
20
20
-
hash = if stdenv.isLinux && stdenv.isAarch64 then
21
21
-
"sha256-cMGo+Af0VRY3c2EoNzVZFU53Kz5wKL8zsSSXIOtZVU8="
22
22
-
else if stdenv.isLinux then
23
23
-
"sha256-Zmqs/Y3be2z9KMuSwyTLZWKbIInzHgoC9Bm0S2jv3XI="
24
24
-
else if stdenv.isDarwin && stdenv.isAarch64 then
25
25
-
"sha256-34k5Q1cIr3+foubtMJVoHVHZtCLoSjwJK00e1p0JdLg="
26
26
-
else
27
27
-
"sha256-0epjoQhUSBYlE0L7Ubwn/sJF61+4BbxeaRx6EY/SklE=";
40
40
+
"sha256-0epjoQhUSBYlE0L7Ubwn/sJF61+4BbxeaRx6EY/SklE=";
28
41
};
29
42
30
43
nativeBuildInputs = lib.optionals stdenv.isLinux [ autoPatchelfHook ];
+7
-4
profiles/bore.nix
···
1
1
# Bore tunnel client configuration
2
2
-
{ config, lib, pkgs, ... }:
2
2
+
{
3
3
+
config,
4
4
+
lib,
5
5
+
pkgs,
6
6
+
...
7
7
+
}:
3
8
4
9
{
5
10
imports = [ ../modules/bore ];
···
10
15
serverPort = 7000;
11
16
domain = "tun.hogwarts.channel";
12
17
authTokenFile =
13
13
-
if pkgs.stdenv.isDarwin
14
14
-
then "/Users/jsp/.config/bore/token"
15
15
-
else "/home/jsp/.config/bore/token";
18
18
+
if pkgs.stdenv.isDarwin then "/Users/jsp/.config/bore/token" else "/home/jsp/.config/bore/token";
16
19
};
17
20
}
+29
-8
secrets/secrets.nix
···
14
14
15
15
# Groups for convenience
16
16
allUsers = [ jsp ];
17
17
-
allHosts = [ alastor dippet horace ];
17
17
+
allHosts = [
18
18
+
alastor
19
19
+
dippet
20
20
+
horace
21
21
+
];
18
22
all = allUsers ++ allHosts;
19
23
in
20
24
{
···
24
28
25
29
# Cloudflare API credentials for ACME DNS challenge
26
30
# Format: CF_DNS_API_TOKEN=xxxxx
27
27
-
"cloudflare-credentials.age".publicKeys = [ jsp alastor ];
31
31
+
"cloudflare-credentials.age".publicKeys = [
32
32
+
jsp
33
33
+
alastor
34
34
+
];
28
35
29
36
# Bore client token (same as frps-token, but separate file for clarity)
30
37
# Used on client machines (remus, etc)
···
34
41
# Generate with: openssl rand -hex 32
35
42
"knot-secret.age".publicKeys = all;
36
43
37
37
-
"pds.age".publicKeys = [ jsp alastor ];
44
44
+
"pds.age".publicKeys = [
45
45
+
jsp
46
46
+
alastor
47
47
+
];
38
48
39
49
# If using Resend SMTP, include API key here too
40
40
-
"pds-mailer.age".publicKeys = [ jsp alastor ];
41
41
-
50
50
+
"pds-mailer.age".publicKeys = [
51
51
+
jsp
52
52
+
alastor
53
53
+
];
42
54
43
55
# WiFi passwords for NixOS machines
44
56
# Format: NETWORK_PSK=password
···
68
80
# restic/env.age: B2_ACCOUNT_ID and B2_ACCOUNT_KEY (or AWS_ACCESS_KEY_ID, etc.)
69
81
# restic/repo.age: Repository URL (e.g., b2:bucket-name:/path)
70
82
# restic/password.age: Repository encryption password
71
71
-
"restic/env.age".publicKeys = [ jsp alastor ];
72
72
-
"restic/repo.age".publicKeys = [ jsp alastor ];
73
73
-
"restic/password.age".publicKeys = [ jsp alastor ];
83
83
+
"restic/env.age".publicKeys = [
84
84
+
jsp
85
85
+
alastor
86
86
+
];
87
87
+
"restic/repo.age".publicKeys = [
88
88
+
jsp
89
89
+
alastor
90
90
+
];
91
91
+
"restic/password.age".publicKeys = [
92
92
+
jsp
93
93
+
alastor
94
94
+
];
74
95
}