Compare commits

...

256 Commits

Author SHA1 Message Date
85eb20c4cb stuff 2026-04-10 15:41:24 +02:00
276378b57c flk 2026-04-09 08:33:04 +00:00
03b968513b flk 2026-04-08 08:07:51 +00:00
e545d38314 sm 2026-04-08 08:07:51 +00:00
564ccd2559 keys 2026-04-07 11:43:58 +00:00
f32f51970b up 2026-04-07 11:41:55 +00:00
0e50839ce0 up 2026-04-07 11:02:37 +00:00
ed995a1edd flk 2026-04-07 11:02:37 +00:00
2424b87b46 flk 2026-04-07 11:02:37 +00:00
37ef245374 flk 2026-04-02 14:46:44 +00:00
ac40abe696 flk 2026-04-02 14:45:05 +00:00
a611d7fb99 clean 2026-04-02 13:01:08 +00:00
89c430b940 review more 2026-04-02 12:57:50 +00:00
1dd7d8a2d8 fix review 2026-04-02 12:57:50 +00:00
69697c822c model 2026-04-02 11:12:13 +00:00
8def00f368 review 2026-04-02 10:58:32 +00:00
c907354a4f flk 2026-04-02 10:09:06 +00:00
80ff1f8b03 fuck 2026-04-01 16:11:02 +00:00
9b5069693a fix opensrc mcp command 2026-04-01 16:11:02 +00:00
2cad84ff26 rm direnv 2026-04-01 16:11:02 +00:00
bbfcc366c2 up 2026-04-01 11:40:32 +00:00
8b09aa9705 cleanup 2026-04-01 11:40:32 +00:00
0eaa830050 Add jj-review TUI plugin to global opencode config 2026-04-01 11:40:32 +00:00
8577807650 Add task classification taxonomy to moonshot agent 2026-04-01 11:40:32 +00:00
a186c136f0 Rename anvil agent to moonshot, bump reasoningEffort to xhigh 2026-04-01 11:40:32 +00:00
e6b5ff0fb8 Add anvil agent — autonomous GPT-5.4 deep worker 2026-04-01 11:40:32 +00:00
6a8bda9031 flk 2026-04-01 11:40:32 +00:00
073cc1aa38 fix TUI plugin: use default export with id (required by plugin loader) 2026-04-01 11:40:32 +00:00
cea666f3d8 rename review plugin slash command to /jj-review to avoid built-in conflict 2026-04-01 11:40:32 +00:00
813fd347d5 Remove pi agent infrastructure 2026-04-01 11:40:32 +00:00
66ff22f9e6 add opencode review plugin (port from pi-coding-agent extension) 2026-04-01 11:40:32 +00:00
86afae7d6c sneaky 2026-04-01 11:40:32 +00:00
5e46938488 bring oc back 2026-04-01 11:40:32 +00:00
8f3951522c chatgpt 2026-04-01 11:40:32 +00:00
6e5af04278 up 2026-04-01 11:40:32 +00:00
01cf320c2e jj 2026-03-30 14:40:06 +00:00
b69cc789b1 flk 2026-03-30 10:35:50 +00:00
642598bbab chore: update cog-cli to 0.24.1 2026-03-30 10:35:50 +00:00
31b87c7177 tuist-pr clone 2026-03-30 12:35:39 +02:00
aae96c7b5a fix breW 2026-03-29 21:09:43 +02:00
980bd7c993 rm wipr2 2026-03-29 21:04:50 +02:00
dd77ed07e5 fix: make pi-harness pnpm deps reproducible across platforms 2026-03-29 18:36:38 +00:00
724abba247 fix: regenerate flake inputs for deploy 2026-03-29 18:17:27 +00:00
9239e8dc6d flk 2026-03-29 18:17:27 +00:00
11c816b2c2 fix(hosts): restore user home-manager wiring and refresh pi-harness deps hash 2026-03-29 18:17:27 +00:00
901059d0cd fix: update qmd deps and deploy hosts 2026-03-29 18:17:27 +00:00
4c69dddcd3 flk 2026-03-29 18:17:27 +00:00
447d7f1dd7 refactor: dedupe theme values and app helpers 2026-03-29 18:17:27 +00:00
a8b07b0c30 up 2026-03-29 18:17:27 +00:00
94baea90d6 refactor(modules): reduce host repetition 2026-03-29 18:17:27 +00:00
1bb97448a4 refactor(hosts): rename jason to janet 2026-03-29 18:17:27 +00:00
3ede8cd2c2 flk 2026-03-29 18:17:27 +00:00
c9e986121b fix 2026-03-26 19:57:46 +01:00
067312dddf up 2026-03-26 18:52:13 +00:00
a5e387a81d up 2026-03-26 13:49:19 +00:00
1005313bd0 up 2026-03-26 12:02:43 +00:00
2d3e15231a flk 2026-03-26 07:56:26 +00:00
a6cc5dcc4a up 2026-03-26 07:56:26 +00:00
958c332bf1 up 2026-03-26 07:56:26 +00:00
49fa4d623e refactor notability ingest stack 2026-03-26 07:56:26 +00:00
4eefa6b337 feat(notes): add Notability WebDAV ingest pipeline 2026-03-26 07:56:26 +00:00
bef2afed66 flk 2026-03-26 07:56:26 +00:00
5ad97d97a7 up 2026-03-26 07:56:26 +00:00
51b0bd4b1d fix 2026-03-26 07:56:26 +00:00
19c770c163 fix 2026-03-26 07:56:26 +00:00
9bd22bc5de fix(zellij): inline rose-pine theme for 0.44 UI 2026-03-24 21:07:47 +00:00
85f2d5c19f fix(zellij): update rose-pine-dawn theme for 0.44 2026-03-24 09:04:09 +00:00
d3ceac88fc update hash 2026-03-24 09:04:09 +00:00
799207efaa flk 2026-03-24 07:51:45 +00:00
260da9cbfc fix 2026-03-24 08:49:54 +01:00
34ecdaf528 fix stuff 2026-03-23 21:32:37 +01:00
d1964e8212 -codex 2026-03-23 20:30:17 +00:00
03228cfdf1 flk 2026-03-23 20:30:17 +00:00
4defd577d3 refactor(darwin): install GUI apps via homebrew 2026-03-23 09:52:25 +00:00
4adc8329a1 feat(opencode): add workstation API key secret 2026-03-23 09:52:25 +00:00
c859f5e41e up 2026-03-23 07:39:17 +00:00
7d93a9e09e flk 2026-03-23 07:31:09 +00:00
0a79986914 refactor 2026-03-23 07:31:09 +00:00
9598d68a84 up 2026-03-23 07:31:09 +00:00
4f507d6bd1 up 2026-03-22 21:34:30 +00:00
04d7eda8c4 Add pi-harness breadcrumbs and vendored session naming 2026-03-22 21:34:30 +00:00
32fda4a7e9 up 2026-03-22 21:34:30 +00:00
6c816ae5ab review -> jj 2026-03-22 21:34:30 +00:00
7810be3cc1 deps 2026-03-22 16:50:56 +00:00
d2f1555309 file backend 2026-03-21 21:39:26 +00:00
80f24ce11f pi 2026-03-21 21:39:26 +00:00
3b345614f7 deps 2026-03-21 21:39:26 +00:00
4fa8026ce5 up 2026-03-21 21:39:26 +00:00
f989e96fb8 remove opencode.nix import from default.nix 2026-03-21 21:39:26 +00:00
0f39be78da update flake.lock after removing opencode-nvim 2026-03-21 21:39:26 +00:00
1edbafd18a remove opencode-nvim plugin 2026-03-20 14:18:12 +00:00
e28dbf2236 refactor 2026-03-19 21:31:04 +01:00
944ee0e6e7 refactor 2026-03-19 20:01:42 +01:00
ce490cacdc up 2026-03-19 17:36:07 +00:00
2452683a0c cog 2026-03-19 17:36:07 +00:00
7be22a5210 fix home 2026-03-19 17:36:07 +00:00
3e8f143752 fix 2026-03-19 17:36:07 +00:00
2b44191e73 flk 2026-03-19 17:36:07 +00:00
a25be94c48 flk 2026-03-17 07:48:05 +00:00
e829a9ff39 sentry 2026-03-17 07:48:05 +00:00
0c70cb0707 fuck 2026-03-17 07:48:05 +00:00
b4a1f09841 flk 2026-03-17 07:48:05 +00:00
227caee599 flk 2026-03-13 18:55:42 +00:00
7045dee36e opus is dumb 2026-03-13 18:54:03 +00:00
d6d5b33d4c fuck2 2026-03-13 18:54:03 +00:00
4b8e1215a5 fuck 2026-03-13 17:23:36 +00:00
b21a150452 paperless-gpt 2026-03-13 17:23:36 +00:00
b24065bf5c ld 2026-03-13 17:23:36 +00:00
5d9f25747d no 2026-03-13 16:37:14 +00:00
b882adc7ea tahani: add himayala alias to catch LLM typo of himalaya 2026-03-13 16:33:59 +00:00
e88b11d8bb tahani: allow opencode external_directory access for inbox-triage ingestion 2026-03-13 16:27:17 +00:00
09d9501427 fix tag 2026-03-13 16:27:17 +00:00
8f893a7216 tahani: auto-ingest email attachments into paperless via shared consumption directory 2026-03-13 16:07:18 +00:00
12a24b83f4 remove useless skill 2026-03-13 16:00:02 +00:00
0d9213e461 fix inbox-triage service: use home profile PATH so himalaya can find sh for password command 2026-03-13 15:58:56 +00:00
8c84c70152 fix oc 2026-03-13 15:58:27 +00:00
a92d0cfe8b fix oc 2026-03-13 15:58:27 +00:00
d19a0ffb81 nvim -> flake 2026-03-13 15:43:49 +00:00
e4a20ddeb9 consistency 2026-03-13 15:24:32 +00:00
e4d859a6ae small fix 2026-03-13 15:24:32 +00:00
627e5cc5cd nu vi mode 2026-03-13 15:24:32 +00:00
e5416214ad icons 2026-03-13 15:24:32 +00:00
ec0c33109e fix font 2026-03-13 11:24:25 +00:00
bf35e81d0b small changes 2026-03-13 11:21:44 +00:00
ecffadc9c4 back to iosevka 2026-03-13 11:08:52 +00:00
110f9fa8ee remove lazygit, git shell aliases, and nushell git functions 2026-03-13 10:39:35 +00:00
263fe067e5 oc tweaks 2026-03-13 10:39:35 +00:00
ec04ea123d broder 2026-03-13 10:39:35 +00:00
46cd921ef8 stuff 2026-03-13 10:39:35 +00:00
f40284b3be bg 2026-03-13 10:39:35 +00:00
b45d3c65ac border 2026-03-13 10:39:35 +00:00
10ebe9e1af lnx 2026-03-13 10:39:35 +00:00
6b93e33607 border 2026-03-13 10:39:35 +00:00
5839f63ad5 flk 2026-03-13 08:35:40 +00:00
dbfcacb6e7 colors 2026-03-13 08:35:40 +00:00
c620d32c51 fix 2026-03-13 08:35:40 +00:00
aa80055d58 lock 2026-03-13 08:35:40 +00:00
6e1547ae48 more 2026-03-13 08:35:40 +00:00
f2b2302f61 opencode glm-5 2026-03-13 08:35:40 +00:00
f357586217 opencode tailscale 2026-03-13 08:35:40 +00:00
4c29259470 rose pine 2026-03-12 19:01:53 +00:00
7b72236b4c yes 2026-03-12 19:01:53 +00:00
60120d46bf win 2026-03-12 19:01:53 +00:00
b64a7416c9 fix 2026-03-12 19:01:53 +00:00
3138f6ce11 the big restyling 2026-03-12 19:01:53 +00:00
6569d7d4d8 tighten service boundaries and clean up config structure 2026-03-12 19:01:53 +00:00
eae286c5ab update oc.nvim 2026-03-12 19:01:53 +00:00
e12d425df5 nvim 2026-03-11 13:34:39 +00:00
671036416b flk 2026-03-11 07:24:05 +00:00
a778a124bf use kimi for triage 2026-03-10 15:34:31 +00:00
f896135f36 use jj properly 2026-03-10 15:17:46 +00:00
0b882455f8 fix 2026-03-10 15:17:46 +00:00
31a6f7d5d4 flk 2026-03-10 15:17:46 +00:00
c63372082c stuff 2026-03-10 13:11:08 +00:00
82b7c96edf Add jj.nvim, diffview.nvim, and code-review.nvim with <leader>v (+VCS) and <leader>r (+Review) keybinding groups 2026-03-10 12:46:05 +00:00
c53d54865f direnv 2026-03-10 12:46:05 +00:00
c4ed4758fd fix? 2026-03-10 12:46:05 +00:00
e836072b51 fmt 2026-03-10 10:51:02 +00:00
5f26b3fac8 fix oc 2026-03-10 10:51:02 +00:00
a64db4cbe2 revert fff.nvim, restore mini.pick 2026-03-10 09:42:38 +00:00
5a5e5e9b67 up 2026-03-10 09:42:38 +00:00
0d23f2b0f4 fixes 2026-03-09 11:31:02 +00:00
f6ecc783ff noc
noc
2026-03-09 11:31:02 +00:00
564e6e37fd fix: disable ast-grep tests on darwin (sandbox locale issue) 2026-03-09 11:26:46 +01:00
393c38f82f flk 2026-03-09 10:00:31 +00:00
a4f17a97c7 stuff 2026-03-08 21:26:47 +00:00
bfc9af382f refactor: distribute packages from _lib/packages.nix into aspect modules 2026-03-08 21:08:52 +00:00
0aa8606153 up 2026-03-08 17:14:59 +00:00
0a955151d0 noc 2026-03-08 17:14:59 +00:00
c2a898888f flk 2026-03-08 17:14:59 +00:00
f101b5f2fc deploy: build locally instead of on target 2026-03-08 13:29:26 +00:00
1d961dbb8c flk 2026-03-08 13:29:26 +00:00
ff6b0aff22 fix zj 2026-03-08 13:29:26 +00:00
dd59c33256 flk 2026-03-07 11:27:50 +00:00
2e45bc69f2 Fix den user class default option path 2026-03-07 11:27:50 +00:00
37125d8d01 flk 2026-03-07 11:27:50 +00:00
e315aa6221 Use exec zellij attach in SSH Nushell startup 2026-03-07 11:27:50 +00:00
7d8df70308 Add nono AI agent sandbox CLI 2026-03-07 11:27:50 +00:00
efb313d16a flk 2026-03-06 16:06:32 +00:00
d5197777b4 Fix darwin apply to use darwin-rebuild directly instead of nix run 2026-03-06 16:05:27 +00:00
497b98cb70 Unify apply script across platforms and remove build-switch 2026-03-06 16:01:25 +00:00
b662770683 save tokens 2026-03-06 10:03:27 +00:00
c2edf7fe47 flk 2026-03-06 10:03:27 +00:00
c4314ab4d6 flk 2026-03-05 22:28:42 +00:00
86b0e5bd6c fix zjstatus fullscreen and sync tab indicator icons 2026-03-05 22:08:33 +00:00
01a7f77ab8 disable git-master skill 2026-03-05 16:49:37 +00:00
4c533fc570 remove migration scaffolding 2026-03-05 16:46:35 +00:00
849f65e455 use den.flakeModules.dendritic per template 2026-03-05 16:45:51 +00:00
6c9a0adf23 remove duplicate 2026-03-05 16:45:51 +00:00
eb145c067f flk 2026-03-05 16:09:44 +00:00
2213db9800 remote build 2026-03-05 16:09:44 +00:00
c3345aaebb flk 2026-03-05 16:05:51 +00:00
2a98e4f8db update script 2026-03-05 16:05:51 +00:00
c8316d578a up again 2026-03-05 16:02:28 +00:00
d65aac71ec up 2026-03-05 15:54:59 +00:00
f55137f7ca tabs workaround 2026-03-05 15:45:08 +00:00
e463c42740 dendritic migration
dendritic migration
2026-03-05 15:41:41 +00:00
05544d0597 go 2026-03-05 15:41:41 +00:00
ea3653c824 flk 2026-03-05 09:24:26 +00:00
9ad45386b1 up 2026-03-04 19:43:02 +00:00
3ec12e649d extract global AGENTS.md to file and add nushell scripting rule 2026-03-04 19:41:15 +00:00
4b26de32bb add opencode plugin to block scripting languages in favor of nu 2026-03-04 19:36:59 +00:00
ed157d8f67 ensure opencode updates plugins 2026-03-04 19:34:11 +00:00
18e289d8e1 global agents 2026-03-04 11:42:07 +00:00
4e0245848e block git 2026-03-04 11:37:38 +00:00
9cce8cf9af aerospace: swap main and secondary monitor assignments 2026-03-04 11:25:44 +00:00
c19b7686d7 fix 2026-03-04 10:52:18 +00:00
58cc4dd82a lock 2026-03-04 10:49:36 +00:00
90e3b752b0 add tuicr, replace buildRustPackage with naersk, remove overseer 2026-03-04 10:48:59 +00:00
98adb4061b opencode: disable playwright and dev-browser skills 2026-03-04 10:27:50 +00:00
4536fb90f4 opencode: add vcs-detect skill, enhance code-review with oracle 2026-03-04 10:27:50 +00:00
09d0050014 starship: strip prompt to essentials 2026-03-04 10:27:50 +00:00
fd24b0d613 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-04 10:11:24 +00:00
0985ef935f starship 2026-03-04 10:05:08 +00:00
a3c9102409 flk 2026-03-04 09:38:24 +00:00
95ae7ff959 inbox-triage: remove German text that triggers oh-my-opencode think mode 2026-03-04 09:38:24 +00:00
1e546585bc inbox-triage: prioritise actionable unread messages, add Correspondence folder 2026-03-03 09:19:14 +00:00
325976d70b up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-03 08:37:52 +00:00
b27fe54da4 flk
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-03 08:24:57 +00:00
35eda93886 himalaya: add bash to wrapper runtimeInputs for passwordCommand shell execution 2026-03-02 15:00:56 +00:00
8044ade9c6 himalaya: add coreutils to wrapper for passwordCommand 2026-03-02 14:57:49 +00:00
91b0e90a19 tahani: add coreutils to inbox-triage PATH for password command 2026-03-02 14:51:27 +00:00
68db6f15b8 tahani: rename let bindings to himalaya/opencode 2026-03-02 14:44:25 +00:00
e67b84aa99 tahani: add himalaya to opencode-inbox-triage service PATH 2026-03-02 14:42:39 +00:00
05b44342c4 tahani: add systemd timer for opencode inbox-triage every 10 minutes 2026-03-02 14:30:55 +00:00
bfc10a890d fix himalaya move argument order in inbox-triage command 2026-03-02 14:22:22 +00:00
90029b9a27 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-02 14:17:45 +00:00
cdfd96bf94 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-02 10:24:16 +00:00
be289bdc9d flk
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-02 08:52:55 +00:00
d69f435d63 opencode stuff
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-02 08:52:53 +00:00
8651e1f505 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 21:42:23 +00:00
af9e0a5a92 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 21:37:04 +00:00
a25b1a4f20 inbox-triage: harden prompt with pagination, explicit commands, and tighter classification 2026-03-01 21:11:10 +00:00
e8f1d597e3 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 21:01:22 +00:00
2b3db24465 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 20:18:17 +00:00
c03981ba38 inbox-triage: rename Einlieferungen to Outgoing Shipments 2026-03-01 20:11:25 +00:00
d1cc2e2a6a up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 20:05:23 +00:00
dc234bbfd0 harden inbox triage run semantics 2026-03-01 20:04:45 +00:00
cfe0cc2fb7 enforce paged inbox triage sequencing 2026-03-01 19:47:42 +00:00
149df11987 cleanup
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 19:43:10 +00:00
31b7a25e6e neverest: add clients-pool-size, remove systemd sync service 2026-03-01 19:33:43 +00:00
b396a8e575 add inbox triage opencode command 2026-03-01 19:22:34 +00:00
2e4591f9b8 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 18:48:32 +00:00
b0d086a0e7 set up neverest imap-to-maildir sync, switch himalaya to maildir backend 2026-03-01 18:29:41 +00:00
4f1d2fead7 add himalaya and neverest flake inputs 2026-03-01 18:29:36 +00:00
a95188d218 up
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 17:54:12 +00:00
8ed58c39d3 use square separators for yazi theme 2026-03-01 15:39:01 +00:00
fe39468ba1 fix jj-diffconflicts hash 2026-03-01 15:00:51 +00:00
ef3e5c836c rm lumen 2026-03-01 14:57:48 +00:00
f090642bf8 restore jj/jjui alongside git 2026-03-01 14:57:44 +00:00
31a5057517 add yazi, dust, ouch, serie; drop zip/unzip 2026-03-01 14:33:16 +00:00
5a0e6a1e94 flk
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 13:24:40 +00:00
3aa24701ef autostart zj
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-03-01 13:24:14 +00:00
5ca98860b1 haiku
Signed-off-by: Christoph Schmatzler <christoph@schmatzler.com>
2026-02-27 12:41:26 +00:00
5e09c7613a fix albanian lesson prompt 2026-02-26 15:32:53 +00:00
757793ea97 add albanian lesson command 2026-02-26 15:24:00 +00:00
269 changed files with 6664 additions and 16656 deletions

View File

@@ -1,13 +1,15 @@
keys:
- &user_cschmatzler age1xate984yhl9qk9d4q99pyxmzz48sq56nfhu8weyzkgum4ed5tc5shjmrs7
- &host_tahani age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm
- &host_michael age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j
- &host_jason age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2
- &host_janet age1f9h725ewwwwwkelnrvdvrurg6fcsn3zxrxdt0v6v8ys0nzngcsvqu77nc8
- &host_chidi age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3
creation_rules:
- path_regex: secrets/[^/]+$
key_groups:
- age:
- *user_cschmatzler
- *host_tahani
- *host_michael
- *host_jason
- *host_janet
- *host_chidi

102
AGENTS.md
View File

@@ -5,18 +5,20 @@
### Local Development
```bash
nix run .#build # Build current host config
nix run .#build -- <hostname> # Build specific host (chidi, jason, michael, tahani)
nix run .#build -- <hostname> # Build specific host (chidi, janet, michael, tahani)
nix run .#apply # Build and apply locally (darwin-rebuild/nixos-rebuild switch)
nix flake check # Validate flake
```
### Remote Deployment (NixOS only)
```bash
colmena build # Build all NixOS hosts
colmena apply --on <host> # Deploy to specific NixOS host (michael, tahani)
colmena apply # Deploy to all NixOS hosts
nix run .#deploy # Deploy to all NixOS hosts
nix run .#deploy -- .#michael # Deploy to specific NixOS host
nix run .#deploy -- .#tahani # Deploy to specific NixOS host
```
When you're on tahani and asked to apply, that means running `nix run .#deploy`.
### Formatting
```bash
alejandra . # Format all Nix files
@@ -30,36 +32,51 @@ alejandra . # Format all Nix files
- **Command**: Run `alejandra .` before committing
### File Structure
- **Hosts**: `hosts/<hostname>/` - Per-machine configurations
- Darwin: `chidi`, `jason`
- NixOS: `michael`, `tahani`
- **Profiles**: `profiles/` - Reusable program/service configurations (imported by hosts)
- **Modules**: `modules/` - Custom NixOS/darwin modules
- **Lib**: `lib/` - Shared constants and utilities
- **Modules**: `modules/` - All configuration (flake-parts modules, auto-imported by import-tree)
- `hosts/` - Per-host composition modules
- `profiles/` - Shared host and user profile bundles
- `_lib/` - Utility functions (underscore = ignored by import-tree)
- `_darwin/` - Darwin-specific sub-modules
- `_neovim/` - Neovim plugin configs
- `hosts/_parts/` - Host-specific leaf files (disk-config, hardware, service fragments, etc.)
- **Apps**: `apps/` - Per-system app scripts (Nushell)
- **Secrets**: `secrets/` - SOPS-encrypted secrets (`.sops.yaml` for config)
### Architecture
**Framework**: den (vic/den) — every .nix file in `modules/` is a flake-parts module
**Pattern**: Feature/aspect-centric, not host-centric
**Aspects**: `den.aspects.<name>.<class>` where class is:
- `nixos` - NixOS-only configuration
- `darwin` - macOS-only configuration
- `homeManager` - Home Manager configuration
- `os` - Applies to both NixOS and darwin
**Hosts**: `den.hosts.<system>.<name>` declared in `modules/inventory.nix`
**Profiles**: shared bundles live under `modules/profiles/{host,user}` and are exposed as `den.aspects.host-*` and `den.aspects.user-*`
**Defaults**: `den.default.*` defined in `modules/defaults.nix`
**Imports**: Auto-imported by import-tree; underscore-prefixed dirs (`_lib/`, `_darwin/`, etc.) are excluded from auto-import
**Deployment**: deploy-rs for NixOS hosts (michael, tahani); darwin hosts (chidi, janet) are local-only
### Nix Language Conventions
**Function Arguments**:
```nix
{inputs, pkgs, lib, ...}:
```
Destructure arguments on separate lines. Use `...` to capture remaining args.
**Imports**:
```nix
../../profiles/foo.nix
```
Use relative paths from file location, not absolute paths.
Use `...` to capture remaining args. Let Alejandra control the exact layout.
**Attribute Sets**:
```nix
options.my.gitea = {
enable = lib.mkEnableOption "Gitea git hosting service";
bucket = lib.mkOption {
type = lib.types.str;
description = "S3 bucket name";
};
den.aspects.myfeature.os = {
enable = true;
config = "value";
};
```
One attribute per line with trailing semicolons.
@@ -75,7 +92,7 @@ with pkgs;
```
Use `with pkgs;` for package lists, one item per line.
**Modules**:
**Aspect Definition**:
```nix
{
config,
@@ -84,9 +101,9 @@ Use `with pkgs;` for package lists, one item per line.
...
}:
with lib; let
cfg = config.my.feature;
cfg = config.den.aspects.myfeature;
in {
options.my.feature = {
options.den.aspects.myfeature = {
enable = mkEnableOption "Feature description";
};
config = mkIf cfg.enable {
@@ -94,7 +111,6 @@ in {
};
}
```
- Destructure args on separate lines
- Use `with lib;` for brevity with NixOS lib functions
- Define `cfg` for config options
- Use `mkIf`, `mkForce`, `mkDefault` appropriately
@@ -111,22 +127,28 @@ in {
```
### Naming Conventions
- **Option names**: `my.<feature>.<option>` for custom modules
- **Hostnames**: Lowercase, descriptive (e.g., `michael`, `tahani`)
- **Profile files**: Descriptive, lowercase with hyphens (e.g., `homebrew.nix`)
- **Aspect names**: `den.aspects.<name>.<class>` for feature configuration
- **Hostnames**: Lowercase, descriptive (e.g., `michael`, `tahani`, `chidi`, `janet`)
- **Module files**: Descriptive, lowercase with hyphens (e.g., `neovim-config.nix`)
### Secrets Management
- Use SOPS for secrets (see `.sops.yaml`)
- Never commit unencrypted secrets
- Secrets files in `hosts/<host>/secrets.nix` import SOPS-generated files
- Secret definitions live in per-host modules (`modules/hosts/michael.nix`, `modules/hosts/tahani.nix`, etc.)
- Shared SOPS defaults (module imports, key paths) in `modules/secrets.nix`
### Imports Pattern
Host configs import:
1. System modules (`modulesPath + "/..."`)
2. Host-specific files (`./disk-config.nix`, `./hardware-configuration.nix`)
3. SOPS secrets (`./secrets.nix`)
4. Custom modules (`../../modules/*.nix`)
5. Base profiles (`../../profiles/*.nix`)
6. Input modules (`inputs.<module>.xxxModules.module`)
### Aspect Composition
Use `den.aspects.<name>.includes` to compose aspects:
```nix
den.aspects.myfeature.includes = [
"other-aspect"
"another-aspect"
];
```
Home-manager users import profiles in a similar manner.
### Key Conventions
- No `specialArgs` — den batteries handle input passing
- No hostname string comparisons in shared aspects
- Host-specific config goes in `den.aspects.<hostname>.*`
- Shared config uses `os` class (applies to both NixOS and darwin)
- Non-module files go in `_`-prefixed subdirs

64
README.md Normal file
View File

@@ -0,0 +1,64 @@
# NixOS Config
Personal Nix flake for four machines:
- `michael` - x86_64 Linux server
- `tahani` - x86_64 Linux home server / workstation
- `chidi` - aarch64 Darwin work laptop
- `janet` - aarch64 Darwin personal laptop
## Repository Map
- `modules/` - flake-parts modules, auto-imported via `import-tree`
- `modules/hosts/` - per-host composition modules
- `modules/hosts/_parts/` - host-private leaf modules like hardware, disks, and literal networking
- `modules/profiles/` - shared host and user profile bundles
- `modules/_lib/` - local helper functions
- `modules/_notability/`, `modules/_paperless/` - feature-owned scripts and templates
- `apps/` - Nushell apps exposed through the flake
- `secrets/` - SOPS-encrypted secrets
- `flake.nix` - generated flake entrypoint
- `modules/dendritic.nix` - source of truth for flake inputs and `flake.nix` generation
## How It Is Structured
This repo uses `den` and organizes configuration around aspects instead of putting everything directly in host files.
- shared behavior lives in `den.aspects.<name>.<class>` modules under `modules/*.nix`
- the machine inventory lives in `modules/inventory.nix`
- shared bundles live in `modules/profiles/{host,user}/`
- host composition happens in `modules/hosts/<host>.nix`
- host-private imports live in `modules/hosts/_parts/<host>/` and stay limited to true machine leaf files
- feature-owned services live in top-level modules like `modules/gitea.nix`, `modules/notability.nix`, and `modules/paperless.nix`
- user-level config mostly lives in Home Manager aspects
Common examples:
- `modules/core.nix` - shared Nix and shell foundation
- `modules/dev-tools.nix` - VCS, language, and developer tooling
- `modules/network.nix` - SSH, fail2ban, and tailscale aspects
- `modules/gitea.nix` - Gitea, Litestream, and backup stack for `michael`
- `modules/notability.nix` - Notability ingest services and user tooling for `tahani`
- `modules/profiles/user/workstation.nix` - shared developer workstation user bundle
- `modules/hosts/michael.nix` - server composition for `michael`
- `modules/hosts/tahani.nix` - server/workstation composition for `tahani`
## Common Commands
```bash
nix run .#build
nix run .#build -- michael
nix run .#apply
nix run .#deploy -- .#tahani
nix flake check
alejandra .
```
## Updating The Flake
`flake.nix` is generated. Update inputs in `modules/dendritic.nix`, then regenerate:
```bash
nix run .#write-flake
alejandra .
```

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/../common.sh"
HOSTNAME="${1:-$(scutil --get LocalHostName 2>/dev/null || hostname -s)}"
print_info "Applying configuration for $HOSTNAME"
nix run nix-darwin -- switch --flake ".#$HOSTNAME" "${@:2}"
print_success "Configuration applied successfully"

View File

@@ -1,16 +1,7 @@
#!/usr/bin/env bash
#!/usr/bin/env nu
set -euo pipefail
source "$(dirname "$0")/../common.sh"
use ../common.nu *
HOSTNAME="${1:-$(scutil --get LocalHostName 2>/dev/null || hostname -s)}"
print_info "Building configuration for $HOSTNAME"
nix build ".#darwinConfigurations.$HOSTNAME.system" --show-trace "${@:2}"
if [[ -L ./result ]]; then
unlink ./result
fi
print_success "Build completed successfully"
def main [hostname?: string, ...rest: string] {
build-config "darwin" $hostname ...$rest
}

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/../common.sh"
HOSTNAME="${1:-$(scutil --get LocalHostName 2>/dev/null || hostname -s)}"
print_info "Building and switching configuration for $HOSTNAME"
# Build
print_info "Building configuration..."
if ! nix build ".#darwinConfigurations.$HOSTNAME.system" --show-trace "${@:2}"; then
print_error "Build failed"
exit 1
fi
print_success "Build completed"
# Switch
print_info "Switching to new configuration..."
sudo ./result/sw/bin/darwin-rebuild switch --flake ".#$HOSTNAME" "${@:2}"
if [[ -L ./result ]]; then
unlink ./result
fi
print_success "Build and switch completed successfully"

View File

@@ -1,20 +1,20 @@
#!/usr/bin/env bash
#!/usr/bin/env nu
set -euo pipefail
source "$(dirname "$0")/../common.sh"
use ../common.nu *
print_info "Available generations:"
darwin-rebuild --list-generations
def main [] {
print_info "Available generations:"
darwin-rebuild --list-generations
echo -n "Enter generation number to rollback to: "
read -r GEN_NUM
let gen_num = input "Enter generation number to rollback to: "
if [[ -z "$GEN_NUM" ]]; then
if ($gen_num | is-empty) {
print_error "No generation number provided"
exit 1
fi
}
print_warning "Rolling back to generation $GEN_NUM..."
darwin-rebuild switch --switch-generation "$GEN_NUM"
print_warning $"Rolling back to generation ($gen_num)..."
darwin-rebuild switch --switch-generation $gen_num
print_success "Rollback to generation $GEN_NUM complete"
print_success $"Rollback to generation ($gen_num) complete"
}

22
apps/apply Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env nu
use ./common.nu *
def main [hostname?: string, ...rest: string] {
let host = resolve-host $hostname
print_info $"Applying configuration for ($host)"
if $nu.os-info.name == "macos" {
sudo darwin-rebuild switch --flake $".#($host)" ...$rest
} else {
let euid = (id -u | str trim | into int)
if $euid != 0 {
sudo nixos-rebuild switch --flake $".#($host)" ...$rest
} else {
nixos-rebuild switch --flake $".#($host)" ...$rest
}
}
print_success "Configuration applied successfully"
}

72
apps/common.nu Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env nu
export def print_info [msg: string] {
print $"(ansi blue)[INFO](ansi reset) ($msg)"
}
export def print_success [msg: string] {
print $"(ansi green)[OK](ansi reset) ($msg)"
}
export def print_error [msg: string] {
print $"(ansi red)[ERROR](ansi reset) ($msg)"
}
export def print_warning [msg: string] {
print $"(ansi yellow)[WARN](ansi reset) ($msg)"
}
export def get-hostname [] {
if $nu.os-info.name == "macos" {
try { ^scutil --get LocalHostName | str trim } catch { ^hostname -s | str trim }
} else {
^hostname | str trim
}
}
export def resolve-host [hostname?: string] {
if ($hostname | is-empty) {
get-hostname
} else {
$hostname
}
}
export def cleanup-result-link [] {
if ("./result" | path exists) {
rm ./result
}
}
export def build-config [kind: string, hostname?: string, ...rest: string] {
let host = resolve-host $hostname
print_info $"Building configuration for ($host)"
if $kind == "darwin" {
nix build $".#darwinConfigurations.($host).system" --show-trace ...$rest
} else {
nix build $".#nixosConfigurations.($host).config.system.build.toplevel" --show-trace ...$rest
}
cleanup-result-link
print_success "Build completed successfully"
}
export def update-flake [inputs: list<string>] {
if ($inputs | is-empty) {
print_info "Updating all flake inputs"
nix flake update
} else {
print_info $"Updating flake inputs: ($inputs | str join ', ')"
nix flake update ...$inputs
}
print_info "Regenerating flake.nix"
nix run .#write-flake
print_info "Formatting"
alejandra .
print_success "Flake updated"
}

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}

7
apps/update Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env nu
use ./common.nu *
def main [...inputs: string] {
update-flake $inputs
}

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/../common.sh"
HOSTNAME="${1:-$(hostname)}"
print_info "Applying configuration for $HOSTNAME"
if [[ "$EUID" -ne 0 ]]; then
sudo nixos-rebuild switch --flake ".#$HOSTNAME" "${@:2}"
else
nixos-rebuild switch --flake ".#$HOSTNAME" "${@:2}"
fi
print_success "Configuration applied successfully"

View File

@@ -1,16 +1,7 @@
#!/usr/bin/env bash
#!/usr/bin/env nu
set -euo pipefail
source "$(dirname "$0")/../common.sh"
use ../common.nu *
HOSTNAME="${1:-$(hostname)}"
print_info "Building configuration for $HOSTNAME"
nix build ".#nixosConfigurations.$HOSTNAME.config.system.build.toplevel" --show-trace "${@:2}"
if [[ -L ./result ]]; then
unlink ./result
fi
print_success "Build completed successfully"
def main [hostname?: string, ...rest: string] {
build-config "nixos" $hostname ...$rest
}

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/../common.sh"
HOSTNAME="${1:-$(hostname)}"
print_info "Building and switching configuration for $HOSTNAME"
# Build
print_info "Building configuration..."
if ! nix build ".#nixosConfigurations.$HOSTNAME.config.system.build.toplevel" --no-link "${@:2}"; then
print_error "Build failed"
exit 1
fi
print_success "Build completed"
print_info "Switching to new configuration..."
if [[ "$EUID" -ne 0 ]]; then
sudo nixos-rebuild switch --flake ".#$HOSTNAME" "${@:2}"
else
nixos-rebuild switch --flake ".#$HOSTNAME" "${@:2}"
fi
print_success "Build and switch completed successfully"

40
apps/x86_64-linux/rollback Normal file → Executable file
View File

@@ -1,30 +1,34 @@
#!/usr/bin/env bash
#!/usr/bin/env nu
set -euo pipefail
source "$(dirname "$0")/../common.sh"
use ../common.nu *
print_info "Available generations:"
if [[ "$EUID" -ne 0 ]]; then
def main [] {
print_info "Available generations:"
let euid = (id -u | str trim | into int)
if $euid != 0 {
sudo nix-env --profile /nix/var/nix/profiles/system --list-generations
else
} else {
nix-env --profile /nix/var/nix/profiles/system --list-generations
fi
}
echo -n "Enter generation number to rollback to: "
read -r GEN_NUM
let gen_num = input "Enter generation number to rollback to: "
if [[ -z "$GEN_NUM" ]]; then
if ($gen_num | is-empty) {
print_error "No generation number provided"
exit 1
fi
}
print_warning "Rolling back to generation $GEN_NUM..."
if [[ "$EUID" -ne 0 ]]; then
sudo nix-env --profile /nix/var/nix/profiles/system --switch-generation "$GEN_NUM" && \
print_warning $"Rolling back to generation ($gen_num)..."
if $euid != 0 {
sudo nix-env --profile /nix/var/nix/profiles/system --switch-generation $gen_num
sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch
else
nix-env --profile /nix/var/nix/profiles/system --switch-generation "$GEN_NUM" && \
} else {
nix-env --profile /nix/var/nix/profiles/system --switch-generation $gen_num
/nix/var/nix/profiles/system/bin/switch-to-configuration switch
fi
}
print_success "Rollback to generation $GEN_NUM complete"
print_success $"Rollback to generation ($gen_num) complete"
}

700
flake.lock generated

File diff suppressed because it is too large Load Diff

196
flake.nix
View File

@@ -1,165 +1,77 @@
# DO-NOT-EDIT. This file was auto-generated using github:vic/flake-file.
# Use `nix run .#write-flake` to regenerate it.
{
description = "Configuration for my macOS laptops and NixOS server";
outputs = inputs: inputs.flake-parts.lib.mkFlake {inherit inputs;} (inputs.import-tree ./modules);
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/master";
flake-parts.url = "github:hercules-ci/flake-parts";
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
code-review-nvim = {
url = "github:choplin/code-review.nvim";
flake = false;
};
darwin = {
url = "github:LnL7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-homebrew.url = "github:zhaofengli-wip/nix-homebrew";
homebrew-core = {
url = "github:homebrew/homebrew-core";
flake = false;
den.url = "github:vic/den";
deploy-rs.url = "github:serokell/deploy-rs";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-aspects.url = "github:vic/flake-aspects";
flake-file.url = "github:vic/flake-file";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
himalaya.url = "github:pimalaya/himalaya";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
homebrew-cask = {
url = "github:homebrew/homebrew-cask";
flake = false;
};
nixvim.url = "github:nix-community/nixvim";
zjstatus.url = "github:dj95/zjstatus";
llm-agents.url = "github:numtide/llm-agents.nix";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
colmena = {
url = "github:zhaofengli/colmena";
inputs.nixpkgs.follows = "nixpkgs";
};
lumen = {
url = "github:jnsahaj/lumen";
inputs.nixpkgs.follows = "nixpkgs";
};
overseer = {
url = "github:dmmulroy/overseer";
homebrew-core = {
url = "github:homebrew/homebrew-core";
flake = false;
};
import-tree.url = "github:vic/import-tree";
jj-diffconflicts = {
url = "github:rafikdraoui/jj-diffconflicts";
flake = false;
};
outputs = inputs @ {flake-parts, ...}:
flake-parts.lib.mkFlake {inherit inputs;} (
let
inherit (inputs.nixpkgs) lib;
constants = import ./lib/constants.nix;
inherit (constants) user;
darwinHosts = ["chidi" "jason"];
nixosHosts = ["michael" "tahani"];
overlays = import ./overlays {inherit inputs;};
nixpkgsConfig = hostPlatform: {
nixpkgs = {inherit hostPlatform overlays;};
jj-nvim = {
url = "github:NicolasGB/jj.nvim";
flake = false;
};
in {
systems = [
"x86_64-linux"
"aarch64-darwin"
];
flake.darwinConfigurations =
lib.genAttrs darwinHosts (
hostname:
inputs.darwin.lib.darwinSystem {
specialArgs = {inherit inputs user hostname constants;};
modules = [
inputs.home-manager.darwinModules.home-manager
inputs.nix-homebrew.darwinModules.nix-homebrew
(nixpkgsConfig "aarch64-darwin")
{
nix-homebrew = {
inherit user;
enable = true;
mutableTaps = true;
taps = {
"homebrew/homebrew-core" = inputs.homebrew-core;
"homebrew/homebrew-cask" = inputs.homebrew-cask;
jj-ryu = {
url = "github:dmmulroy/jj-ryu";
flake = false;
};
jj-starship.url = "github:dmmulroy/jj-starship";
llm-agents.url = "github:numtide/llm-agents.nix";
naersk = {
url = "github:nix-community/naersk/master";
inputs.nixpkgs.follows = "nixpkgs";
};
}
./hosts/${hostname}
];
}
);
flake.nixosConfigurations =
lib.genAttrs nixosHosts (
hostname:
lib.nixosSystem {
specialArgs = {inherit inputs user hostname constants;};
modules = [
inputs.home-manager.nixosModules.home-manager
(nixpkgsConfig "x86_64-linux")
./hosts/${hostname}
];
}
);
flake.colmena =
{
meta = {
nixpkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;
specialArgs = {inherit inputs user constants;};
neovim-nightly-overlay = {
url = "github:nix-community/neovim-nightly-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
}
// lib.genAttrs nixosHosts (
hostname: {
deployment = {
targetHost = hostname;
targetUser = user;
nix-homebrew.url = "github:zhaofengli-wip/nix-homebrew";
nixpkgs.url = "github:nixos/nixpkgs/master";
nixpkgs-lib.follows = "nixpkgs";
nixvim.url = "github:nix-community/nixvim";
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
imports = [
inputs.home-manager.nixosModules.home-manager
(nixpkgsConfig "x86_64-linux")
{_module.args.hostname = hostname;}
./hosts/${hostname}
];
}
);
flake.nixosModules = {
pgbackrest = ./modules/pgbackrest.nix;
zjstatus.url = "github:dj95/zjstatus";
};
flake.overlays = {
default = lib.composeManyExtensions overlays;
list = overlays;
};
flake.lib = {inherit constants;};
perSystem = {
pkgs,
system,
...
}: let
mkApp = name: {
type = "app";
program = "${(pkgs.writeShellScriptBin name ''
PATH=${pkgs.git}/bin:$PATH
echo "Running ${name} for ${system}"
exec ${inputs.self}/apps/${system}/${name} "$@"
'')}/bin/${name}";
};
appNames = [
"apply"
"build"
"build-switch"
"rollback"
];
in {
apps = pkgs.lib.genAttrs appNames mkApp;
};
}
);
}

View File

@@ -1,55 +0,0 @@
{
pkgs,
inputs,
user,
hostname,
...
}: {
imports = [
./secrets.nix
../../profiles/core.nix
../../profiles/darwin.nix
../../profiles/dock.nix
../../profiles/homebrew.nix
../../profiles/tailscale.nix
inputs.sops-nix.darwinModules.sops
];
networking.hostName = hostname;
networking.computerName = hostname;
home-manager.users.${user} = {
imports = [
../../profiles/atuin.nix
../../profiles/aerospace.nix
../../profiles/bash.nix
../../profiles/bat.nix
../../profiles/direnv.nix
../../profiles/nushell.nix
../../profiles/fzf.nix
../../profiles/ghostty.nix
../../profiles/git.nix
../../profiles/home.nix
../../profiles/lazygit.nix
../../profiles/lumen.nix
../../profiles/mise.nix
../../profiles/neovim
../../profiles/opencode.nix
../../profiles/claude-code.nix
../../profiles/ripgrep.nix
../../profiles/ssh.nix
../../profiles/starship.nix
../../profiles/zellij.nix
../../profiles/zk.nix
../../profiles/zoxide.nix
../../profiles/zsh.nix
inputs.nixvim.homeModules.nixvim
];
fonts.fontconfig.enable = true;
programs.git.settings.user.email = "christoph@tuist.dev";
};
environment.systemPackages = with pkgs; [
slack
];
}

View File

@@ -1,5 +0,0 @@
{user, ...}: {
sops.age.keyFile = "/Users/${user}/.config/sops/age/keys.txt";
sops.age.sshKeyPaths = [];
sops.gnupg.sshKeyPaths = [];
}

View File

@@ -1,50 +0,0 @@
{
inputs,
user,
hostname,
...
}: {
imports = [
./secrets.nix
../../profiles/core.nix
../../profiles/darwin.nix
../../profiles/dock.nix
../../profiles/homebrew.nix
../../profiles/tailscale.nix
inputs.sops-nix.darwinModules.sops
];
networking.hostName = hostname;
networking.computerName = hostname;
home-manager.users.${user} = {
imports = [
../../profiles/atuin.nix
../../profiles/aerospace.nix
../../profiles/bash.nix
../../profiles/bat.nix
../../profiles/direnv.nix
../../profiles/nushell.nix
../../profiles/fzf.nix
../../profiles/ghostty.nix
../../profiles/git.nix
../../profiles/home.nix
../../profiles/lazygit.nix
../../profiles/lumen.nix
../../profiles/mise.nix
../../profiles/neovim
../../profiles/opencode.nix
../../profiles/claude-code.nix
../../profiles/ripgrep.nix
../../profiles/ssh.nix
../../profiles/starship.nix
../../profiles/zellij.nix
../../profiles/zk.nix
../../profiles/zoxide.nix
../../profiles/zsh.nix
inputs.nixvim.homeModules.nixvim
];
fonts.fontconfig.enable = true;
programs.git.settings.user.email = "christoph@schmatzler.com";
};
}

View File

@@ -1,5 +0,0 @@
{user, ...}: {
sops.age.keyFile = "/Users/${user}/.config/sops/age/keys.txt";
sops.age.sshKeyPaths = [];
sops.gnupg.sshKeyPaths = [];
}

View File

@@ -1,48 +0,0 @@
{
config,
inputs,
user,
hostname,
modulesPath,
...
}: {
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
(modulesPath + "/profiles/qemu-guest.nix")
./disk-config.nix
./hardware-configuration.nix
./secrets.nix
../../modules/gitea.nix
../../profiles/core.nix
../../profiles/fail2ban.nix
../../profiles/nixos.nix
../../profiles/openssh.nix
../../profiles/tailscale.nix
inputs.disko.nixosModules.disko
inputs.sops-nix.nixosModules.sops
];
my.gitea = {
enable = true;
litestream = {
bucket = "michael-gitea-litestream";
secretFile = config.sops.secrets.michael-gitea-litestream.path;
};
restic = {
bucket = "michael-gitea-repositories";
passwordFile = config.sops.secrets.michael-gitea-restic-password.path;
environmentFile = config.sops.secrets.michael-gitea-restic-env.path;
};
};
networking.hostName = hostname;
home-manager.users.${user} = {
imports = [
../../profiles/nushell.nix
../../profiles/home.nix
../../profiles/ssh.nix
inputs.nixvim.homeModules.nixvim
];
};
}

View File

@@ -1,22 +0,0 @@
{...}: {
sops.secrets = {
michael-gitea-litestream = {
sopsFile = ../../secrets/michael-gitea-litestream;
format = "binary";
owner = "gitea";
group = "gitea";
};
michael-gitea-restic-password = {
sopsFile = ../../secrets/michael-gitea-restic-password;
format = "binary";
owner = "gitea";
group = "gitea";
};
michael-gitea-restic-env = {
sopsFile = ../../secrets/michael-gitea-restic-env;
format = "binary";
owner = "gitea";
group = "gitea";
};
};
}

View File

@@ -1,60 +0,0 @@
{
services.adguardhome = {
enable = true;
host = "0.0.0.0";
port = 10000;
settings = {
dhcp = {
enabled = false;
};
dns = {
upstream_dns = [
"1.1.1.1"
"1.0.0.1"
];
};
filtering = {
protection_enabled = true;
filtering_enabled = true;
safe_search = {
enabled = false;
};
safebrowsing_enabled = true;
blocked_response_ttl = 10;
filters_update_interval = 24;
blocked_services = {
ids = [
"reddit"
"twitter"
];
};
};
filters = [
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/pro.txt";
name = "HaGeZi Multi PRO";
id = 1;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/tif.txt";
name = "HaGeZi Threat Intelligence Feeds";
id = 2;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/gambling.txt";
name = "HaGeZi Gambling";
id = 3;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/nsfw.txt";
name = "HaGeZi NSFW";
id = 4;
}
];
};
};
}

View File

@@ -1,10 +0,0 @@
{...}: {
services.caddy.virtualHosts."cache.manticore-hippocampus.ts.net" = {
extraConfig = ''
tls {
get_certificate tailscale
}
reverse_proxy localhost:32843
'';
};
}

View File

@@ -1,63 +0,0 @@
{
pkgs,
inputs,
user,
hostname,
...
}: {
imports = [
./adguardhome.nix
./cache.nix
./networking.nix
./paperless.nix
./secrets.nix
../../profiles/core.nix
../../profiles/nixos.nix
../../profiles/openssh.nix
../../profiles/tailscale.nix
inputs.sops-nix.nixosModules.sops
];
networking.hostName = hostname;
home-manager.users.${user} = {
imports = [
../../profiles/atuin.nix
../../profiles/bash.nix
../../profiles/bat.nix
../../profiles/direnv.nix
../../profiles/nushell.nix
../../profiles/fzf.nix
../../profiles/git.nix
../../profiles/home.nix
../../profiles/lazygit.nix
../../profiles/lumen.nix
../../profiles/mise.nix
../../profiles/neovim
../../profiles/opencode.nix
../../profiles/overseer.nix
../../profiles/claude-code.nix
../../profiles/ripgrep.nix
../../profiles/ssh.nix
../../profiles/starship.nix
../../profiles/zellij.nix
../../profiles/zk.nix
../../profiles/zoxide.nix
../../profiles/zsh.nix
inputs.nixvim.homeModules.nixvim
];
programs.git.settings.user.email = "christoph@schmatzler.com";
};
virtualisation.docker.enable = true;
users.users.${user}.extraGroups = ["docker"];
swapDevices = [
{
device = "/swapfile";
size = 16 * 1024;
}
];
}

View File

@@ -1,73 +0,0 @@
{config, ...}: {
services.caddy = {
enable = true;
globalConfig = ''
admin off
'';
virtualHosts."docs.manticore-hippocampus.ts.net" = {
extraConfig = ''
tls {
get_certificate tailscale
}
reverse_proxy localhost:${toString config.services.paperless.port}
'';
};
virtualHosts."docs-ai.manticore-hippocampus.ts.net" = {
extraConfig = ''
tls {
get_certificate tailscale
}
reverse_proxy localhost:3000
'';
};
};
virtualisation.oci-containers = {
backend = "docker";
containers.paperless-ai = {
image = "clusterzx/paperless-ai:latest";
autoStart = true;
volumes = [
"paperless-ai-data:/app/data"
];
environment = {
PUID = "1000";
PGID = "1000";
PAPERLESS_AI_PORT = "3000";
# Initial setup wizard will configure the rest
PAPERLESS_AI_INITIAL_SETUP = "yes";
# Paperless-ngx API URL accessible from container (using host network)
PAPERLESS_API_URL = "http://127.0.0.1:${toString config.services.paperless.port}/api";
};
extraOptions = [
"--network=host"
];
};
};
services.redis.servers.paperless = {
enable = true;
port = 6379;
bind = "127.0.0.1";
settings = {
maxmemory = "256mb";
maxmemory-policy = "allkeys-lru";
};
};
services.paperless = {
enable = true;
address = "0.0.0.0";
passwordFile = config.sops.secrets.tahani-paperless-password.path;
settings = {
PAPERLESS_DBENGINE = "sqlite";
PAPERLESS_REDIS = "redis://127.0.0.1:6379";
PAPERLESS_CONSUMER_IGNORE_PATTERN = [
".DS_STORE/*"
"desktop.ini"
];
PAPERLESS_OCR_LANGUAGE = "deu+eng";
PAPERLESS_CSRF_TRUSTED_ORIGINS = "https://docs.manticore-hippocampus.ts.net";
};
};
}

View File

@@ -1,8 +0,0 @@
{...}: {
sops.secrets = {
tahani-paperless-password = {
sopsFile = ../../secrets/tahani-paperless-password;
format = "binary";
};
};
}

View File

@@ -1,20 +0,0 @@
{
input,
prev,
}: let
manifest = (prev.lib.importTOML "${input}/Cargo.toml").package;
in
prev.rustPlatform.buildRustPackage {
pname = manifest.name;
version = manifest.version;
cargoLock.lockFile = "${input}/Cargo.lock";
src = input;
nativeBuildInputs = [prev.pkg-config];
buildInputs = [prev.openssl];
OPENSSL_NO_VENDOR = 1;
doCheck = false;
}

View File

@@ -1,14 +0,0 @@
{
user = "cschmatzler";
sshKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/"
];
stateVersions = {
darwin = 6;
nixos = "25.11";
homeManager = "25.11";
};
}

View File

@@ -2,12 +2,13 @@
config,
lib,
pkgs,
user,
...
}:
with lib; let
cfg = config.local.dock;
inherit (pkgs) stdenv dockutil;
inherit (pkgs) dockutil stdenv;
local = import ../_lib/local.nix;
userHome = "/Users/${local.user.name}";
in {
options = {
local.dock = {
@@ -38,7 +39,7 @@ in {
};
});
default = [
{path = "/Applications/Helium.app/";}
{path = "/Applications/Safari.app/";}
{path = "/Applications/Ghostty.app/";}
{path = "/System/Applications/Calendar.app/";}
{path = "/System/Applications/Mail.app/";}
@@ -46,7 +47,7 @@ in {
{path = "/System/Applications/Music.app/";}
{path = "/System/Applications/System Settings.app/";}
{
path = "${config.users.users.${user}.home}/Downloads";
path = "${userHome}/Downloads";
section = "others";
options = "--sort name --view grid --display stack";
}
@@ -57,7 +58,7 @@ in {
mkOption {
description = "Username to apply the dock settings to";
type = types.str;
default = user;
default = local.user.name;
};
};
};

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env nu
const repo = "tuist/tuist"
def fail [msg: string] {
error make {msg: $msg}
}
def clean [s: string] {
$s | str replace --all "\t" " " | str replace --all "\n" " "
}
def pick-pr [] {
let prs = (
gh pr list --repo $repo --state open --limit 200 --json number,title,headRefName,author
| from json
)
if ($prs | is-empty) {
fail $"No open PRs found for ($repo)"
}
let choice = (
$prs
| each {|pr|
let title = (clean $pr.title)
let branch = (clean $pr.headRefName)
let author = $pr.author.login
$"($pr.number)\t($title)\t($author)\t($branch)"
}
| str join (char newline)
| fzf --prompt "tuist pr > " --delimiter "\t" --with-nth "1,2,3,4" --preview "gh pr view --repo tuist/tuist {1}" --preview-window "right:70%"
)
if ($choice | str trim | is-empty) {
exit 130
}
$choice | split row "\t" | first | into int
}
def main [pr_number?: int] {
let number = if ($pr_number | is-empty) { pick-pr } else { $pr_number }
let pr = (
gh pr view --repo $repo $number --json number,title,url
| from json
)
let base = ([$env.HOME "Projects" "Work"] | path join)
let dest = ([$base $"tuist-pr-($pr.number)"] | path join)
if ($dest | path exists) {
fail $"Destination already exists: ($dest)"
}
^mkdir -p $base
print $"Cloning ($repo) PR #($pr.number): ($pr.title)"
jj git clone $"https://github.com/($repo).git" $dest
do {
cd $dest
gh pr checkout $pr.number
}
print $"Ready: ($dest)"
}

19
modules/_lib/caddy.nix Normal file
View File

@@ -0,0 +1,19 @@
let
local = import ./local.nix;
in {
inherit (local) tailscaleHost;
mkTailscaleVHost = {
name,
configText,
}: {
"${local.tailscaleHost name}" = {
extraConfig = ''
tls {
get_certificate tailscale
}
${configText}
'';
};
};
}

37
modules/_lib/hosts.nix Normal file
View File

@@ -0,0 +1,37 @@
{
den,
lib,
}: let
merge = lib.recursiveUpdate;
in {
mkUserHost = {
system,
host,
user,
userAspect ? "${host}-${user}",
includes ? [],
homeManager ? null,
}:
merge
(lib.setAttrByPath ["den" "hosts" system host "users" user "aspect"] userAspect)
(lib.setAttrByPath ["den" "aspects" userAspect] ({inherit includes;}
// lib.optionalAttrs (homeManager != null) {
inherit homeManager;
}));
mkPerHostAspect = {
host,
includes ? [],
darwin ? null,
nixos ? null,
}:
lib.setAttrByPath ["den" "aspects" host "includes"] [
(den.lib.perHost ({inherit includes;}
// lib.optionalAttrs (darwin != null) {
inherit darwin;
}
// lib.optionalAttrs (nixos != null) {
inherit nixos;
}))
];
}

33
modules/_lib/local.nix Normal file
View File

@@ -0,0 +1,33 @@
rec {
user = {
name = "cschmatzler";
fullName = "Christoph Schmatzler";
emails = {
personal = "christoph@schmatzler.com";
work = "christoph@tuist.dev";
icloud = "christoph.schmatzler@icloud.com";
};
};
secretPath = name: "/run/secrets/${name}";
mkHome = system:
if builtins.match ".*-darwin" system != null
then "/Users/${user.name}"
else "/home/${user.name}";
mkHost = system: {
inherit system;
home = mkHome system;
};
hosts = {
chidi = mkHost "aarch64-darwin";
janet = mkHost "aarch64-darwin";
michael = mkHost "x86_64-linux";
tahani = mkHost "x86_64-linux";
};
tailscaleDomain = "manticore-hippocampus.ts.net";
tailscaleHost = name: "${name}.${tailscaleDomain}";
}

44
modules/_lib/secrets.nix Normal file
View File

@@ -0,0 +1,44 @@
{lib}: let
local = import ./local.nix;
in rec {
mkBinarySecret = {
name,
sopsFile,
owner ? null,
group ? null,
path ? local.secretPath name,
}:
{
inherit path sopsFile;
format = "binary";
}
// lib.optionalAttrs (owner != null) {
inherit owner;
}
// lib.optionalAttrs (group != null) {
inherit group;
};
mkUserBinarySecret = {
name,
sopsFile,
owner ? local.user.name,
path ? local.secretPath name,
}:
mkBinarySecret {
inherit name owner path sopsFile;
};
mkServiceBinarySecret = {
name,
sopsFile,
serviceUser,
serviceGroup ? serviceUser,
path ? local.secretPath name,
}:
mkBinarySecret {
inherit name path sopsFile;
group = serviceGroup;
owner = serviceUser;
};
}

46
modules/_lib/theme.nix Normal file
View File

@@ -0,0 +1,46 @@
{
rosePineDawn = {
slug = "rose-pine-dawn";
displayName = "Rosé Pine Dawn";
ghosttyName = "Rose Pine Dawn";
hex = {
love = "#b4637a";
gold = "#ea9d34";
rose = "#d7827e";
pine = "#286983";
foam = "#56949f";
iris = "#907aa9";
leaf = "#6d8f89";
text = "#575279";
subtle = "#797593";
muted = "#9893a5";
highlightHigh = "#cecacd";
highlightMed = "#dfdad9";
highlightLow = "#f4ede8";
overlay = "#f2e9e1";
surface = "#fffaf3";
base = "#faf4ed";
};
rgb = {
love = "180 99 122";
gold = "234 157 52";
rose = "215 130 126";
pine = "40 105 131";
foam = "86 148 159";
iris = "144 122 169";
leaf = "109 143 137";
text = "87 82 121";
subtle = "121 117 147";
muted = "152 147 165";
highlightHigh = "206 202 205";
highlightMed = "223 218 217";
highlightLow = "244 237 232";
overlay = "242 233 225";
surface = "255 250 243";
base = "250 244 237";
black = "0 0 0";
};
};
}

View File

@@ -0,0 +1,52 @@
{
programs.nixvim = {
autoGroups = {
Christoph = {};
};
autoCmd = [
{
event = ["VimEnter" "ColorScheme"];
group = "Christoph";
pattern = "*";
callback.__raw = ''
function()
local p = require("rose-pine.palette")
vim.api.nvim_set_hl(0, "NormalFloat", { bg = p.base })
vim.api.nvim_set_hl(0, "FloatTitle", { fg = p.foam, bg = p.base, bold = true })
vim.api.nvim_set_hl(0, "Pmenu", { fg = p.subtle, bg = p.base })
vim.api.nvim_set_hl(0, "PmenuExtra", { fg = p.muted, bg = p.base })
vim.api.nvim_set_hl(0, "PmenuKind", { fg = p.foam, bg = p.base })
vim.api.nvim_set_hl(0, "PmenuSbar", { bg = p.base })
vim.api.nvim_set_hl(0, "MiniPickPrompt", { bg = p.base, bold = true })
vim.api.nvim_set_hl(0, "MiniPickBorderText", { bg = p.base })
end
'';
}
{
event = "BufWritePre";
group = "Christoph";
pattern = "*";
command = "%s/\\s\\+$//e";
}
{
event = "BufReadPost";
group = "Christoph";
pattern = "*";
command = "normal zR";
}
{
event = "FileReadPost";
group = "Christoph";
pattern = "*";
command = "normal zR";
}
{
event = "FileType";
group = "Christoph";
pattern = "elixir,eelixir,heex";
command = "setlocal expandtab tabstop=2 shiftwidth=2 softtabstop=2";
}
];
};
}

View File

@@ -1,16 +1,22 @@
{
{inputs', ...}: {
imports = [
./autocmd.nix
./mappings.nix
./options.nix
./plugins/blink-cmp.nix
./plugins/code-review.nix
./plugins/conform.nix
./plugins/diffview.nix
./plugins/grug-far.nix
./plugins/hardtime.nix
./plugins/harpoon.nix
./plugins/hunk.nix
./plugins/jj-diffconflicts.nix
./plugins/jj-nvim.nix
./plugins/lsp.nix
./plugins/mini.nix
./plugins/oil.nix
./plugins/render-markdown.nix
./plugins/toggleterm.nix
./plugins/treesitter.nix
./plugins/zk.nix
@@ -19,16 +25,18 @@
programs.nixvim = {
enable = true;
defaultEditor = true;
package = inputs'.neovim-nightly-overlay.packages.default;
luaLoader.enable = true;
colorschemes.catppuccin = {
colorschemes.rose-pine = {
enable = true;
settings = {
flavour = "latte";
variant = "dawn";
extend_background_behind_borders = false;
styles = {
italic = false;
};
};
};
extraConfigLua = ''
vim.ui.select = MiniPick.ui_select
'';
};
home.shellAliases = {

View File

@@ -117,16 +117,181 @@
action = ":Pick visit_paths<CR>";
options.desc = "Visit paths (cwd)";
}
# g - git
# v - vcs
{
mode = "n";
key = "<leader>gg";
key = "<leader>va";
action = ":J annotate<CR>";
options.desc = "Annotate (blame)";
}
{
mode = "n";
key = "<leader>vc";
action = ":JJDiffConflicts<CR>";
options.desc = "Resolve conflicts";
}
{
mode = "n";
key = "<leader>vd";
action.__raw = ''
function()
require('toggleterm.terminal').Terminal:new({ cmd = 'lazygit', direction = 'float' }):toggle()
require('jj.cmd').diff()
end
'';
options.desc = "lazygit";
options.desc = "Diff (current file)";
}
{
mode = "n";
key = "<leader>vD";
action.__raw = ''
function()
require('jj.diff').show_revision({})
end
'';
options.desc = "Diff (all changes)";
}
{
mode = "n";
key = "<leader>ve";
action.__raw = ''
function()
require('jj.cmd').describe()
end
'';
options.desc = "Describe (edit message)";
}
{
mode = "n";
key = "<leader>vf";
action = ":J fetch<CR>";
options.desc = "Fetch";
}
{
mode = "n";
key = "<leader>vv";
action.__raw = ''
function()
require('toggleterm.terminal').Terminal:new({ cmd = 'jjui', direction = 'float' }):toggle()
end
'';
options.desc = "jjui";
}
{
mode = "n";
key = "<leader>vh";
action.__raw = ''
function()
require('jj.diff').show_revision({ rev = '@-' })
end
'';
options.desc = "Diff parent revision";
}
{
mode = "n";
key = "<leader>vl";
action.__raw = ''
function()
require('jj.cmd').log()
end
'';
options.desc = "Log";
}
{
mode = "n";
key = "<leader>vn";
action.__raw = ''
function()
require('jj.cmd').new()
end
'';
options.desc = "New change";
}
{
mode = "n";
key = "<leader>vp";
action = ":J git push<CR>";
options.desc = "Push";
}
{
mode = "n";
key = "<leader>vq";
action = ":DiffviewClose<CR>";
options.desc = "Close diffview";
}
{
mode = "n";
key = "<leader>vR";
action.__raw = ''
function()
require('jj.diff').diff_revisions({ left = 'trunk()', right = '@' })
end
'';
options.desc = "Review bookmark (trunk..@)";
}
{
mode = "n";
key = "<leader>vs";
action.__raw = ''
function()
require('jj.cmd').status()
end
'';
options.desc = "Status";
}
# r - review
{
mode = ["n" "v"];
key = "<leader>rc";
action = ":CodeReviewComment<CR>";
options.desc = "Add comment";
}
{
mode = "n";
key = "<leader>rd";
action = ":CodeReviewDeleteComment<CR>";
options.desc = "Delete comment";
}
{
mode = "n";
key = "<leader>rl";
action = ":CodeReviewList<CR>";
options.desc = "List comments";
}
{
mode = "n";
key = "<leader>ro";
action = ":CodeReviewResolve<CR>";
options.desc = "Resolve thread";
}
{
mode = "n";
key = "<leader>rp";
action = ":CodeReviewPreview<CR>";
options.desc = "Preview review";
}
{
mode = "n";
key = "<leader>rr";
action = ":CodeReviewReply<CR>";
options.desc = "Reply to comment";
}
{
mode = "n";
key = "<leader>rs";
action = ":CodeReviewShowComment<CR>";
options.desc = "Show comment";
}
{
mode = "n";
key = "<leader>rx";
action = ":CodeReviewClear<CR>";
options.desc = "Clear all comments";
}
{
mode = "n";
key = "<leader>ry";
action = ":CodeReviewCopy<CR>";
options.desc = "Copy review to clipboard";
}
# l - lsp/formatter
{
@@ -156,13 +321,13 @@
{
mode = "n";
key = "<leader>lj";
action = ":lua vim.diagnostic.goto_next()<CR>";
action = ":lua vim.diagnostic.jump({ count = 1 })<CR>";
options.desc = "Next diagnostic";
}
{
mode = "n";
key = "<leader>lk";
action = ":lua vim.diagnostic.goto_prev()<CR>";
action = ":lua vim.diagnostic.jump({ count = -1 })<CR>";
options.desc = "Prev diagnostic";
}
{
@@ -183,6 +348,111 @@
action = ":lua vim.lsp.buf.definition()<CR>";
options.desc = "Source definition";
}
# t - tab
{
mode = "n";
key = "<leader>tc";
action = ":tabclose<CR>";
options.desc = "Close tab";
}
{
mode = "n";
key = "<leader>tn";
action = ":tabnew<CR>";
options.desc = "New tab";
}
{
mode = "n";
key = "<leader>to";
action = ":tabonly<CR>";
options.desc = "Close other tabs";
}
{
mode = "n";
key = "<leader>th";
action = ":tabprevious<CR>";
options.desc = "Previous tab";
}
{
mode = "n";
key = "<leader>tl";
action = ":tabnext<CR>";
options.desc = "Next tab";
}
# w - window
{
mode = "n";
key = "<leader>wh";
action = "<C-w>h";
options.desc = "Go left";
}
{
mode = "n";
key = "<leader>wj";
action = "<C-w>j";
options.desc = "Go down";
}
{
mode = "n";
key = "<leader>wk";
action = "<C-w>k";
options.desc = "Go up";
}
{
mode = "n";
key = "<leader>wl";
action = "<C-w>l";
options.desc = "Go right";
}
{
mode = "n";
key = "<leader>ws";
action = ":split<CR>";
options.desc = "Split horizontal";
}
{
mode = "n";
key = "<leader>wv";
action = ":vsplit<CR>";
options.desc = "Split vertical";
}
{
mode = "n";
key = "<leader>wc";
action = ":close<CR>";
options.desc = "Close window";
}
{
mode = "n";
key = "<leader>wq";
action = ":q<CR>";
options.desc = "Quit window";
}
{
mode = "n";
key = "<leader>wo";
action = ":only<CR>";
options.desc = "Close other windows";
}
{
mode = "n";
key = "<leader>w=";
action = "<C-w>=";
options.desc = "Equalize windows";
}
# scrolling
{
mode = "n";
key = "<C-d>";
action = "<C-d>zz";
options.desc = "Scroll down and center";
}
{
mode = "n";
key = "<C-u>";
action = "<C-u>zz";
options.desc = "Scroll up and center";
}
# other
{
mode = "n";

View File

@@ -4,14 +4,17 @@
clipboard = "osc52";
};
opts = {
winborder = "single";
expandtab = false;
tabstop = 2;
ignorecase = true;
list = false;
mouse = "";
relativenumber = true;
scrolloff = 8;
shiftwidth = 2;
smartcase = true;
undofile = true;
};
};
}

View File

@@ -0,0 +1,39 @@
{
pkgs,
nvim-plugin-sources,
...
}: let
code-review-nvim =
pkgs.vimUtils.buildVimPlugin {
pname = "code-review-nvim";
version = "unstable";
src = nvim-plugin-sources.code-review-nvim;
doCheck = false;
};
in {
programs.nixvim = {
extraPlugins = [
code-review-nvim
];
extraConfigLua = ''
require('code-review').setup({
keymaps = false,
comment = {
storage = {
backend = "file",
},
},
ui = {
input_window = {
border = "single",
},
preview = {
float = {
border = "single",
},
},
},
})
'';
};
}

View File

@@ -5,9 +5,8 @@
format_on_save = {};
formatters_by_ft = {
nix = ["alejandra"];
javascript = ["prettier"];
typescript = ["prettier"];
vue = ["prettier"];
javascript = ["oxfmt"];
typescript = ["oxfmt"];
};
};
};

View File

@@ -0,0 +1,26 @@
{pkgs, ...}: {
programs.nixvim = {
extraPlugins = with pkgs.vimPlugins; [
diffview-nvim
];
extraConfigLua = ''
require('diffview').setup({
enhanced_diff_hl = true,
view = {
default = { layout = "diff2_horizontal" },
merge_tool = { layout = "diff3_mixed", disable_diagnostics = true },
file_history = { layout = "diff2_horizontal" },
},
default_args = {
DiffviewOpen = { "--imply-local" },
},
hooks = {
diff_buf_read = function(bufnr)
vim.opt_local.wrap = false
vim.opt_local.list = false
end,
},
})
'';
};
}

View File

@@ -0,0 +1,7 @@
{
programs.nixvim.plugins = {
hardtime = {
enable = true;
};
};
}

View File

@@ -0,0 +1,12 @@
{
pkgs,
nvim-plugin-sources,
...
}: {
programs.nixvim.extraPlugins = [
(pkgs.vimUtils.buildVimPlugin {
name = "jj-diffconflicts";
src = nvim-plugin-sources.jj-diffconflicts;
})
];
}

View File

@@ -0,0 +1,39 @@
{
pkgs,
nvim-plugin-sources,
...
}: let
jj-nvim =
pkgs.vimUtils.buildVimPlugin {
pname = "jj-nvim";
version = "unstable";
src = nvim-plugin-sources.jj-nvim;
doCheck = false;
};
in {
programs.nixvim = {
extraPlugins = [
jj-nvim
];
extraConfigLua = ''
require('jj').setup({
diff = {
backend = "diffview",
},
cmd = {
describe = {
editor = { type = "buffer" },
},
log = {
close_on_edit = false,
},
},
ui = {
log = {
keymaps = true,
},
},
})
'';
};
}

View File

@@ -0,0 +1,175 @@
{
programs.nixvim = {
plugins.mini = {
enable = true;
modules = {
ai = {
custom_textobjects = {
B.__raw = "require('mini.extra').gen_ai_spec.buffer()";
F.__raw = "require('mini.ai').gen_spec.treesitter({ a = '@function.outer', i = '@function.inner' })";
};
};
align = {};
basics = {
options = {
basic = true;
extra_ui = true;
};
mappings = {
basic = false;
};
autocommands = {
basic = true;
};
};
bracketed = {};
clue = {
clues.__raw = ''
{
{ mode = 'n', keys = '<Leader>e', desc = '+Explore/+Edit' },
{ mode = 'n', keys = '<Leader>f', desc = '+Find' },
{ mode = 'n', keys = '<Leader>v', desc = '+VCS' },
{ mode = 'n', keys = '<Leader>l', desc = '+LSP' },
{ mode = 'x', keys = '<Leader>l', desc = '+LSP' },
{ mode = 'n', keys = '<Leader>r', desc = '+Review' },
{ mode = 'v', keys = '<Leader>r', desc = '+Review' },
{ mode = 'n', keys = '<Leader>t', desc = '+Tab' },
{ mode = 'n', keys = '<Leader>w', desc = '+Window' },
require("mini.clue").gen_clues.builtin_completion(),
require("mini.clue").gen_clues.g(),
require("mini.clue").gen_clues.marks(),
require("mini.clue").gen_clues.registers(),
require("mini.clue").gen_clues.windows({ submode_resize = true }),
require("mini.clue").gen_clues.z(),
}
'';
triggers = [
{
mode = "n";
keys = "<Leader>";
}
{
mode = "x";
keys = "<Leader>";
}
{
mode = "n";
keys = "[";
}
{
mode = "n";
keys = "]";
}
{
mode = "x";
keys = "[";
}
{
mode = "x";
keys = "]";
}
{
mode = "i";
keys = "<C-x>";
}
{
mode = "n";
keys = "g";
}
{
mode = "x";
keys = "g";
}
{
mode = "n";
keys = "\"";
}
{
mode = "x";
keys = "\"";
}
{
mode = "i";
keys = "<C-r>";
}
{
mode = "c";
keys = "<C-r>";
}
{
mode = "n";
keys = "<C-w>";
}
{
mode = "n";
keys = "z";
}
{
mode = "x";
keys = "z";
}
{
mode = "n";
keys = "'";
}
{
mode = "n";
keys = "`";
}
{
mode = "x";
keys = "'";
}
{
mode = "x";
keys = "`";
}
];
};
cmdline = {};
comment = {};
diff = {};
extra = {};
git = {};
hipatterns = {
highlighters = {
fixme.__raw = "{ pattern = '%f[%w]()FIXME()%f[%W]', group = 'MiniHipatternsFixme' }";
hack.__raw = "{ pattern = '%f[%w]()HACK()%f[%W]', group = 'MiniHipatternsHack' }";
todo.__raw = "{ pattern = '%f[%w]()TODO()%f[%W]', group = 'MiniHipatternsTodo' }";
note.__raw = "{ pattern = '%f[%w]()NOTE()%f[%W]', group = 'MiniHipatternsNote' }";
hex_color.__raw = "require('mini.hipatterns').gen_highlighter.hex_color()";
};
};
icons = {};
indentscope = {
settings = {
symbol = "|";
};
};
jump = {};
jump2d = {
settings = {
spotter.__raw = "require('mini.jump2d').gen_spotter.pattern('[^%s%p]+')";
labels = "asdfghjkl";
view = {
dim = true;
n_steps_ahead = 2;
};
};
};
move = {};
notify = {};
pairs = {};
pick = {};
splitjoin = {};
starter = {};
statusline = {};
surround = {};
trailspace = {};
visits = {};
};
mockDevIcons = true;
};
};
}

View File

@@ -0,0 +1,9 @@
{
programs.nixvim.plugins.render-markdown = {
enable = true;
settings = {
anti_conceal = {enabled = false;};
file_types = ["markdown"];
};
};
}

View File

@@ -0,0 +1,22 @@
{pkgs, ...}: {
programs.nixvim = {
plugins.treesitter = {
enable = true;
nixGrammars = true;
grammarPackages = with pkgs.vimPlugins.nvim-treesitter-parsers; [
css
elixir
javascript
lua
markdown
markdown_inline
nix
typescript
];
settings = {
highlight.enable = true;
indent.enable = true;
};
};
};
}

View File

@@ -0,0 +1,28 @@
# AGENTS.md
## Version Control
- Use `jj` for version control, not `git`.
- `jj tug` is an alias for `jj bookmark move --from closest_bookmark(@-) --to @-`.
- Never attempt historically destructive Git commands.
- Make small, frequent commits.
- "Commit" means `jj commit`, not `jj desc`; `desc` stays on the same working copy.
## Scripting
- Use Nushell (`nu`) for scripting.
- Do not use Python, Perl, Lua, awk, or any other scripting language. You are programatically blocked from doing so.
## Workflow
- Always complete the requested work.
- If there is any ambiguity about what to do next, do NOT make a decision yourself. Stop your work and ask.
- Do not end with “If you want me to…” or “I can…”; take the next necessary step and finish the job without waiting for additional confirmation.
- Do not future-proof things. Stick to the original plan.
- Do not add fallbacks or backward compatibility unless explicitly required by the user. By default, replace the previous implementation with the new one entirely.
## Validation
- Do not ignore failing tests or checks, even if they appear unrelated to your changes.
- After completing and validating your work, the final step is to run the project's full validation and test commands and ensure they all pass.

View File

@@ -0,0 +1,49 @@
---
description: Turn pasted Albanian lesson into translated notes and solved exercises in zk
---
Process the pasted Albanian lesson content and create two `zk` notes: one for lesson material and one for exercises.
<lesson-material>
$ARGUMENTS
</lesson-material>
Requirements:
1. Parse the lesson content and produce two markdown outputs:
- `material` output: lesson material only.
- `exercises` output: exercises and solutions.
2. Use today's date in both notes (date in title and inside content).
3. In the `material` output:
- Keep clean markdown structure with headings and bullet points.
- Do not add a top-level title heading (no `# ...`) because `zk new --title` already sets the note title.
- Translate examples, dialogues, and all lesson texts into English when not already translated.
- For bigger reading passages, include a word-by-word breakdown.
- For declension/conjugation/grammar tables, provide a complete table of possibilities relevant to the topic.
- Spell out numbers only when the source token is Albanian; do not spell out English numbers.
4. In the `exercises` output:
- Include every exercise in markdown.
- Do not add a top-level title heading (no `# ...`) because `zk new --title` already sets the note title.
- Translate each exercise to English.
- Solve all non-free-writing tasks (multiple choice, fill in the blanks, etc.) and include example solutions.
- For free-writing tasks, provide expanded examples using basic vocabulary from the lesson (if prompted for 3, provide 10).
- Translate free-writing example answers into English.
- Spell out numbers only when the source token is Albanian; do not spell out English numbers.
Execution steps:
1. Generate two markdown contents in memory (do not create temporary files):
- `MATERIAL_CONTENT`
- `EXERCISES_CONTENT`
2. Set `TODAY="$(date +%F)"` once and reuse it for both notes.
3. Create note 1 with `zk` by piping markdown directly to stdin:
- Title format: `Albanian Lesson Material - YYYY-MM-DD`
- Command pattern:
- `printf "%s\n" "$MATERIAL_CONTENT" | zk new --interactive --title "Albanian Lesson Material - $TODAY" --date "$TODAY" --print-path`
4. Create note 2 with `zk` by piping markdown directly to stdin:
- Title format: `Albanian Lesson Exercises - YYYY-MM-DD`
- Command pattern:
- `printf "%s\n" "$EXERCISES_CONTENT" | zk new --interactive --title "Albanian Lesson Exercises - $TODAY" --date "$TODAY" --print-path`
5. Print both created note paths and a short checklist of what was included.
If no lesson material was provided in `$ARGUMENTS`, stop and ask the user to paste it.

View File

@@ -0,0 +1,108 @@
---
description: Triage inbox one message at a time with himalaya only
---
Process email with strict manual triage using Himalaya only.
Hard requirements:
- Use `himalaya` for every mailbox interaction (folders, listing, reading, moving, deleting, attachments).
- Process exactly one message ID at a time. Never run bulk actions on multiple IDs.
- Do not use pattern-matching commands or searches (`grep`, `rg`, `awk`, `sed`, `himalaya envelope list` query filters, etc.).
- Always inspect current folders first, then triage.
- Treat this as a single deterministic run over a snapshot of message IDs discovered during this run.
- Ingest valuable document attachments into Paperless (see Document Ingestion section below).
Workflow:
1. Run `himalaya folder list` first and use those folders as the primary taxonomy.
2. Use this existing folder set as defaults when it fits:
- `INBOX`
- `Correspondence`
- `Orders and Invoices`
- `Payments`
- `Outgoing Shipments`
- `Newsletters and Marketing`
- `Junk`
- `Deleted Messages`
3. Determine source folder:
- If `$ARGUMENTS` is a single known folder name (matches a folder from step 1), use that as source.
- Otherwise use `INBOX`.
4. Build a run scope safely:
- List with fixed page size `20` and JSON output: `himalaya envelope list -f "<source>" -p 1 -s 20 --output json`.
- Start at page `1`. Enumerate IDs in returned order.
- Process each ID fully before touching the next ID.
- Keep an in-memory reviewed set for this run to avoid reprocessing IDs already handled or intentionally left untouched.
- When all IDs on the current page are in the reviewed set, advance to the next page.
- Stop when a page returns fewer results than the page size (end of folder) and all its IDs are in the reviewed set.
5. For each single envelope ID, do all checks before any move/delete:
- Check envelope flags from the JSON listing (seen/answered/flagged) before reading.
- Read the message: `himalaya message read -f "<source>" <id>`.
- If needed for classification or ingestion, download attachments: `himalaya attachment download -f "<source>" <id> --dir /tmp/himalaya-triage`.
- If the message qualifies for document ingestion (see Document Ingestion below), copy eligible attachments to the Paperless consume directory before cleanup.
- Always `rm` downloaded files from `/tmp/himalaya-triage` after processing (whether ingested or not).
- Move: `himalaya message move -f "<source>" "<destination>" <id>`.
- Delete: `himalaya message delete -f "<source>" <id>`.
6. Classification precedence (higher rule wins on conflict):
- **Actionable and unhandled** — if the message needs a reply, requires manual payment, needs a confirmation, or demands any human action, AND has NOT been replied to (no `answered` flag), leave it in the source folder untouched. This is the highest-priority rule: anything that still needs attention stays in `INBOX`.
- Human correspondence already handled — freeform natural-language messages written by a human that have been replied to (`answered` flag set): move to `Correspondence`.
- Human communication not yet replied to but not clearly actionable — when in doubt whether a human message requires action, leave it untouched.
- Clearly ephemeral automated/system message (alerts, bot/status updates, OTP/2FA, password reset codes, login codes) with no archival value: move to `Deleted Messages`.
- Automatic payment transaction notifications (charge/payment confirmations, receipts, failed-payment notices, provider payment events such as Klarna/PayPal/Stripe) that are purely informational and require no action: move to `Payments`.
- Subscription renewal notifications (auto-renew reminders, "will renew soon", price-change notices without a concrete transaction) are operational alerts, not payment records: move to `Deleted Messages`.
- Installment plan activation notifications (e.g. Barclays installment purchase confirmations) are operational confirmations, not payment records: move to `Deleted Messages`.
- "Kontoauszug verfügbar/ist online" notifications are availability alerts, not payment records: move to `Deleted Messages`.
- Orders/invoices/business records: move to `Orders and Invoices`.
- Shipping/tracking notifications (dispatch confirmations, carrier updates, delivery ETAs) without invoice or order-document value: move to `Deleted Messages`.
- Marketing/newsletters: move to `Newsletters and Marketing`.
- Delivery/submission confirmations for items you shipped outbound: move to `Outgoing Shipments`.
- Long-term but uncategorized messages: create a concise new folder and move there.
7. Folder creation rule:
- Create a new folder only if no existing folder fits and the message should be kept.
- Naming constraints: concise topic name, avoid duplicates, and avoid broad catch-all names.
- Command: `himalaya folder add "<new-folder>"`.
Document Ingestion (Paperless):
- **Purpose**: Automatically archive valuable document attachments into Paperless via its consumption directory.
- **Ingestion path**: `/var/lib/paperless/consume/inbox-triage/`
- **When to ingest**: Only for messages whose attachments have long-term archival value. Eligible categories:
- Invoices, receipts, and billing statements (messages going to `Orders and Invoices` or `Payments`)
- Contracts, agreements, and legal documents
- Tax documents, account statements, and financial summaries
- Insurance documents and policy papers
- Official correspondence with document attachments (government, institutions)
- **When NOT to ingest**:
- Marketing emails, newsletters, promotional material
- Shipping/tracking notifications without invoice attachments
- OTP codes, login alerts, password resets, ephemeral notifications
- Subscription renewal reminders without actual invoices
- Duplicate documents already seen in this run
- Inline images, email signatures, logos, and non-document attachments
- **Eligible file types**: PDF, PNG, JPG/JPEG, TIFF, WEBP (documents and scans only). Skip archive files (ZIP, etc.), calendar invites (ICS), and other non-document formats.
- **Procedure**:
1. After downloading attachments to `/tmp/himalaya-triage`, check if any are eligible documents.
2. Copy eligible files: `cp /tmp/himalaya-triage/<filename> /var/lib/paperless/consume/inbox-triage/`
3. If multiple messages could produce filename collisions, prefix the filename with the message ID: `<id>-<filename>`.
4. Log each ingested file in the action log at the end of the run.
- **Conservative rule**: When in doubt whether an attachment is worth archiving, skip it. Paperless storage is cheap, but noise degrades searchability. Prefer false negatives over false positives for marketing material, but prefer false positives over false negatives for anything that looks like a financial or legal document.
Execution rules:
- Never perform bulk operations. One message ID per `read`, `move`, `delete`, and attachment command.
- Always use page size 20 for envelope listing (`-s 20`).
- If any single-ID command fails, log the error and continue with the next unreviewed ID.
- Never skip reading message content before deciding.
- Keep decisions conservative: when in doubt about whether something needs action, leave it in `INBOX`.
- Never move or delete unhandled actionable messages.
- Never move human communications that haven't been replied to, unless clearly non-actionable.
- Define "processed" as "reviewed once in this run" (including intentionally untouched human messages).
- Include only messages observed during this run's listings; if new mail arrives mid-run, leave it for the next run.
- Report a compact action log at the end with:
- source folder,
- total reviewed IDs,
- counts by action (untouched/moved-to-folder/deleted),
- per-destination-folder counts,
- created folders,
- documents ingested to Paperless (count and filenames),
- short rationale for non-obvious classifications.
<user-request>
$ARGUMENTS
</user-request>

View File

@@ -0,0 +1,150 @@
---
description: Initialize Supermemory with comprehensive codebase knowledge
---
# Initializing Supermemory
You are initializing persistent memory for this codebase. This is not just data collection - you're building context that will make you significantly more effective across all future sessions.
## Understanding Context
You are a **stateful** coding agent. Users expect to work with you over extended periods - potentially the entire lifecycle of a project. Your memory is how you get better over time and maintain continuity.
## What to Remember
### 1. Procedures (Rules & Workflows)
Explicit rules that should always be followed:
- "Never commit directly to main - always use feature branches"
- "Always run lint before tests"
- "Use conventional commits format"
### 2. Preferences (Style & Conventions)
Project and user coding style:
- "Prefer functional components over class components"
- "Use early returns instead of nested conditionals"
- "Always add JSDoc to exported functions"
### 3. Architecture & Context
How the codebase works and why:
- "Auth system was refactored in v2.0 - old patterns deprecated"
- "The monorepo used to have 3 modules before consolidation"
- "This pagination bug was fixed before - similar to PR #234"
## Memory Scopes
**Project-scoped** (\`scope: "project"\`):
- Build/test/lint commands
- Architecture and key directories
- Team conventions specific to this codebase
- Technology stack and framework choices
- Known issues and their solutions
**User-scoped** (\`scope: "user"\`):
- Personal coding preferences across all projects
- Communication style preferences
- General workflow habits
## Research Approach
This is a **deep research** initialization. Take your time and be thorough (~50+ tool calls). The goal is to genuinely understand the project, not just collect surface-level facts.
**What to uncover:**
- Tech stack and dependencies (explicit and implicit)
- Project structure and architecture
- Build/test/deploy commands and workflows
- Contributors & team dynamics (who works on what?)
- Commit conventions and branching strategy
- Code evolution (major refactors, architecture changes)
- Pain points (areas with lots of bug fixes)
- Implicit conventions not documented anywhere
## Research Techniques
### File-based
- README.md, CONTRIBUTING.md, AGENTS.md, CLAUDE.md
- Package manifests (package.json, Cargo.toml, pyproject.toml, go.mod)
- Config files (.eslintrc, tsconfig.json, .prettierrc)
- CI/CD configs (.github/workflows/)
### Git-based
- \`git log --oneline -20\` - Recent history
- \`git branch -a\` - Branching strategy
- \`git log --format="%s" -50\` - Commit conventions
- \`git shortlog -sn --all | head -10\` - Main contributors
### Explore Agent
Fire parallel explore queries for broad understanding:
\`\`\`
Task(explore, "What is the tech stack and key dependencies?")
Task(explore, "What is the project structure? Key directories?")
Task(explore, "How do you build, test, and run this project?")
Task(explore, "What are the main architectural patterns?")
Task(explore, "What conventions or patterns are used?")
\`\`\`
## How to Do Thorough Research
**Don't just collect data - analyze and cross-reference.**
Bad (shallow):
- Run commands, copy output
- List facts without understanding
Good (thorough):
- Cross-reference findings (if inconsistent, dig deeper)
- Resolve ambiguities (don't leave questions unanswered)
- Read actual file content, not just names
- Look for patterns (what do commits tell you about workflow?)
- Think like a new team member - what would you want to know?
## Saving Memories
Use the \`supermemory\` tool for each distinct insight:
\`\`\`
supermemory(mode: "add", content: "...", type: "...", scope: "project")
\`\`\`
**Types:**
- \`project-config\` - tech stack, commands, tooling
- \`architecture\` - codebase structure, key components, data flow
- \`learned-pattern\` - conventions specific to this codebase
- \`error-solution\` - known issues and their fixes
- \`preference\` - coding style preferences (use with user scope)
**Guidelines:**
- Save each distinct insight as a separate memory
- Be concise but include enough context to be useful
- Include the "why" not just the "what" when relevant
- Update memories incrementally as you research (don't wait until the end)
**Good memories:**
- "Uses Bun runtime and package manager. Commands: bun install, bun run dev, bun test"
- "API routes in src/routes/, handlers in src/handlers/. Hono framework."
- "Auth uses Redis sessions, not JWT. Implementation in src/lib/auth.ts"
- "Never use \`any\` type - strict TypeScript. Use \`unknown\` and narrow."
- "Database migrations must be backward compatible - we do rolling deploys"
## Upfront Questions
Before diving in, ask:
1. "Any specific rules I should always follow?"
2. "Preferences for how I communicate? (terse/detailed)"
## Reflection Phase
Before finishing, reflect:
1. **Completeness**: Did you cover commands, architecture, conventions, gotchas?
2. **Quality**: Are memories concise and searchable?
3. **Scope**: Did you correctly separate project vs user knowledge?
Then ask: "I've initialized memory with X insights. Want me to continue refining, or is this good?"
## Your Task
1. Ask upfront questions (research depth, rules, preferences)
2. Check existing memories: \`supermemory(mode: "list", scope: "project")\`
3. Research based on chosen depth
4. Save memories incrementally as you discover insights
5. Reflect and verify completeness
6. Summarize what was learned and ask if user wants refinement

View File

@@ -0,0 +1,49 @@
import type { Plugin } from "@opencode-ai/plugin";
const COMMAND_PREFIXES = new Set([
"env",
"command",
"builtin",
"time",
"sudo",
"nohup",
"nice",
]);
function findCommandWord(words: string[]): string | undefined {
for (const word of words) {
if (COMMAND_PREFIXES.has(word)) continue;
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) continue;
return word;
}
return undefined;
}
function segmentHasGit(words: string[]): boolean {
const cmd = findCommandWord(words);
return cmd === "git";
}
function containsBlockedGit(command: string): boolean {
const segments = command.split(/\s*(?:&&|\|\||[;&|]|\$\(|`)\s*/);
for (const segment of segments) {
const words = segment.trim().split(/\s+/).filter(Boolean);
if (segmentHasGit(words)) return true;
}
return false;
}
export const BlockGitPlugin: Plugin = async () => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
const command = output.args.command as string;
if (containsBlockedGit(command)) {
throw new Error(
"This project uses jj, only use `jj` commands, not `git`.",
);
}
}
},
};
};

View File

@@ -0,0 +1,19 @@
import type { Plugin } from "@opencode-ai/plugin";
const SCRIPTING_PATTERN =
/(?:^|[;&|]\s*|&&\s*|\|\|\s*|\$\(\s*|`\s*)(?:python[23]?|perl|ruby|php|lua|node\s+-e|bash\s+-c|sh\s+-c)\s/;
export const BlockScriptingPlugin: Plugin = async () => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
const command = output.args.command as string;
if (SCRIPTING_PATTERN.test(command)) {
throw new Error(
"Do not use python, perl, ruby, php, lua, or inline bash/sh for scripting. Use `nu -c` instead.",
);
}
}
},
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
{inputs, ...}: final: prev: {
ast-grep =
prev.ast-grep.overrideAttrs (old: {
doCheck = false;
});
}

View File

@@ -0,0 +1,44 @@
{inputs, ...}: final: prev: let
version = "0.24.1";
srcs = {
x86_64-linux =
prev.fetchurl {
url = "https://github.com/trycog/cog-cli/releases/download/v${version}/cog-linux-x86_64.tar.gz";
hash = "sha256-/ioEuM58F3ppO0wlc5nw7ZNHunoweOXL/Gda65r0Ig4=";
};
aarch64-darwin =
prev.fetchurl {
url = "https://github.com/trycog/cog-cli/releases/download/v${version}/cog-darwin-arm64.tar.gz";
hash = "sha256-o/A2hVU3Jzmlzx5RbGLFCpfGAghcLGTD8Bm+bVR5OkQ=";
};
};
in {
cog-cli =
prev.stdenvNoCC.mkDerivation {
pname = "cog-cli";
inherit version;
src =
srcs.${prev.stdenv.hostPlatform.system}
or (throw "Unsupported system for cog-cli: ${prev.stdenv.hostPlatform.system}");
dontUnpack = true;
dontConfigure = true;
dontBuild = true;
installPhase = ''
runHook preInstall
tar -xzf "$src"
install -Dm755 cog "$out/bin/cog"
runHook postInstall
'';
meta = with prev.lib; {
description = "Memory, code intelligence, and debugging for AI agents";
homepage = "https://github.com/trycog/cog-cli";
license = licenses.mit;
mainProgram = "cog";
platforms = builtins.attrNames srcs;
sourceProvenance = [sourceTypes.binaryNativeCode];
};
};
}

View File

@@ -0,0 +1,3 @@
{inputs, ...}: final: prev: {
himalaya = inputs.himalaya.packages.${prev.stdenv.hostPlatform.system}.default;
}

View File

@@ -0,0 +1,15 @@
{inputs, ...}: final: prev: let
naersk-lib = prev.callPackage inputs.naersk {};
manifest = (prev.lib.importTOML "${inputs.jj-ryu}/Cargo.toml").package;
in {
jj-ryu =
naersk-lib.buildPackage {
pname = manifest.name;
version = manifest.version;
src = inputs.jj-ryu;
nativeBuildInputs = [prev.pkg-config];
buildInputs = [prev.openssl];
OPENSSL_NO_VENDOR = 1;
doCheck = false;
};
}

View File

@@ -0,0 +1 @@
{inputs, ...}: inputs.jj-starship.overlays.default

View File

@@ -1,3 +1,3 @@
{inputs}: final: prev: {
{inputs, ...}: final: prev: {
zjstatus = inputs.zjstatus.packages.${prev.stdenv.hostPlatform.system}.default;
}

View File

@@ -0,0 +1,26 @@
I will provide you with the content and title of a document. Your task is to select appropriate tags for the document from the available list.
Only select tags from the provided list.
Rules:
1. Focus on WHAT the document IS (document type) and what TOPIC it relates to — not on incidental details mentioned in the content.
- GOOD tags for a server hosting invoice: "Invoice", "Hosting"
- BAD tags for a server hosting invoice: "IBAN", "VAT", "Bank account" — these are just details that appear on any invoice.
2. Pick 1-4 tags maximum. Fewer is better. Every tag must add distinct, meaningful categorisation value.
3. All tags must be in English.
4. Never tag based on formatting details, payment methods, reference numbers, or boilerplate text.
The content is likely in {{.Language}}, but tags must always be in English.
<available_tags>
{{.AvailableTags | join ", "}}
</available_tags>
<title>
{{.Title}}
</title>
<content>
{{.Content}}
</content>
Respond only with the selected tags as a comma-separated list, without any additional information.

View File

@@ -0,0 +1,26 @@
I will provide you with the content of a document that has been partially read by OCR (so it may contain errors).
Your task is to generate a clear, consistent document title for use in paperless-ngx.
Title format: "YYYY-MM-DD - Sender - Description"
- YYYY-MM-DD: The document date (issue date, statement date, etc.). Use the most specific date available. If no date is found, omit the date prefix.
- Sender: The company, organisation, or person who sent/issued the document. Use their common short name (e.g. "Hetzner" not "Hetzner Online GmbH").
- Description: A brief description of what the document is (e.g. "Server hosting invoice", "Payslip January", "Employment contract", "Tax assessment 2024"). Keep it concise but specific enough to distinguish from similar documents.
Examples:
- "2025-03-01 - Hetzner - Server hosting invoice"
- "2024-12-15 - Techniker Krankenkasse - Health insurance statement"
- "2024-06-30 - Acme Corp - Payslip June"
- "2024-01-10 - Finanzamt Berlin - Tax assessment 2023"
Rules:
1. Always write the title in English, regardless of the document language.
2. Keep the description part under 6 words.
3. If the original title contains useful information, use it to inform your suggestion.
4. Respond only with the title, without any additional information.
The content is likely in {{.Language}}.
<original_title>{{.Title}}</original_title>
<content>
{{.Content}}
</content>

View File

@@ -0,0 +1,181 @@
{
"document": {
"block_prefix": "\n",
"block_suffix": "\n",
"color": "#575279",
"margin": 2
},
"block_quote": {
"color": "#797593",
"italic": true,
"indent": 1,
"indent_token": "│ "
},
"list": {
"color": "#575279",
"level_indent": 2
},
"heading": {
"block_suffix": "\n",
"color": "#907aa9",
"bold": true
},
"h1": {
"prefix": "# ",
"bold": true
},
"h2": {
"prefix": "## "
},
"h3": {
"prefix": "### "
},
"h4": {
"prefix": "#### "
},
"h5": {
"prefix": "##### "
},
"h6": {
"prefix": "###### "
},
"strikethrough": {
"crossed_out": true
},
"emph": {
"italic": true,
"color": "#d7827e"
},
"strong": {
"bold": true,
"color": "#286983"
},
"hr": {
"color": "#dfdad9",
"format": "\n--------\n"
},
"item": {
"block_prefix": "• "
},
"enumeration": {
"block_prefix": ". ",
"color": "#286983"
},
"task": {
"ticked": "[✓] ",
"unticked": "[ ] "
},
"link": {
"color": "#286983",
"underline": true
},
"link_text": {
"color": "#56949f"
},
"image": {
"color": "#286983",
"underline": true
},
"image_text": {
"color": "#56949f",
"format": "Image: {{.text}} →"
},
"code": {
"color": "#ea9d34",
"background_color": "#f2e9e1",
"prefix": " ",
"suffix": " "
},
"code_block": {
"color": "#ea9d34",
"margin": 2,
"chroma": {
"text": {
"color": "#575279"
},
"error": {
"color": "#faf4ed",
"background_color": "#b4637a"
},
"comment": {
"color": "#9893a5"
},
"comment_preproc": {
"color": "#56949f"
},
"keyword": {
"color": "#b4637a"
},
"keyword_reserved": {
"color": "#b4637a"
},
"keyword_namespace": {
"color": "#b4637a"
},
"keyword_type": {
"color": "#907aa9"
},
"operator": {
"color": "#56949f"
},
"punctuation": {
"color": "#797593"
},
"name": {
"color": "#286983"
},
"name_constant": {
"color": "#907aa9"
},
"name_builtin": {
"color": "#d7827e"
},
"name_tag": {
"color": "#b4637a"
},
"name_attribute": {
"color": "#d7827e"
},
"name_class": {
"color": "#907aa9"
},
"name_decorator": {
"color": "#56949f"
},
"name_function": {
"color": "#286983"
},
"literal_number": {
"color": "#ea9d34"
},
"literal_string": {
"color": "#ea9d34"
},
"literal_string_escape": {
"color": "#d7827e"
},
"generic_deleted": {
"color": "#b4637a"
},
"generic_emph": {
"italic": true
},
"generic_inserted": {
"color": "#286983"
},
"generic_strong": {
"bold": true
},
"generic_subheading": {
"color": "#907aa9"
},
"background": {
"background_color": "#f2e9e1"
}
}
},
"table": {},
"definition_description": {
"block_prefix": "\n🠶 "
}
}

62
modules/adguardhome.nix Normal file
View File

@@ -0,0 +1,62 @@
{...}: let
caddyLib = import ./_lib/caddy.nix;
in {
den.aspects.adguardhome.nixos = {config, ...}: {
services.adguardhome = {
enable = true;
host = "127.0.0.1";
port = 10000;
settings = {
dhcp.enabled = false;
dns.upstream_dns = [
"1.1.1.1"
"1.0.0.1"
];
filtering = {
protection_enabled = true;
filtering_enabled = true;
safe_search.enabled = false;
safebrowsing_enabled = true;
blocked_response_ttl = 10;
filters_update_interval = 24;
blocked_services.ids = [
"reddit"
"twitter"
];
};
filters = [
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/pro.txt";
name = "HaGeZi Multi PRO";
id = 1;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/tif.txt";
name = "HaGeZi Threat Intelligence Feeds";
id = 2;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/gambling.txt";
name = "HaGeZi Gambling";
id = 3;
}
{
enabled = true;
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/adblock/nsfw.txt";
name = "HaGeZi NSFW";
id = 4;
}
];
};
};
services.caddy.virtualHosts =
caddyLib.mkTailscaleVHost {
name = "adguard";
configText = "reverse_proxy localhost:${toString config.services.adguardhome.port}";
};
};
}

180
modules/ai-tools.nix Normal file
View File

@@ -0,0 +1,180 @@
{inputs, ...}: let
local = import ./_lib/local.nix;
inherit (local) secretPath;
opencodeSecretPath = secretPath "opencode-api-key";
in {
den.aspects.ai-tools.homeManager = {
lib,
pkgs,
inputs',
...
}: {
home.packages = [
inputs'.llm-agents.packages.claude-code
pkgs.cog-cli
];
programs.nushell.extraEnv =
lib.mkAfter ''
if ("${opencodeSecretPath}" | path exists) {
$env.OPENCODE_API_KEY = (open --raw "${opencodeSecretPath}" | str trim)
}
'';
programs.opencode = {
enable = true;
package = inputs'.llm-agents.packages.opencode;
tui = {
theme = "rosepine";
plugin = ["./plugin/review.ts"];
};
settings = {
model = "openai/gpt-5.4";
small_model = "openai/gpt-5.1-codex-mini";
plugin = [
"opencode-claude-auth"
"opencode-supermemory"
];
permission = {
external_directory = {
"*" = "allow";
"**/.gnupg/**" = "deny";
"**/.ssh/**" = "deny";
"~/.config/gh/hosts.yml" = "deny";
"~/.config/sops/age/keys.txt" = "deny";
"~/.local/share/opencode/mcp-auth.json" = "deny";
"/etc/ssh/ssh_host_*" = "deny";
"/run/secrets/*" = "deny";
};
bash = {
"*" = "allow";
env = "deny";
"env *" = "deny";
printenv = "deny";
"printenv *" = "deny";
"export *" = "deny";
"gh auth *" = "deny";
ssh = "ask";
"ssh *" = "ask";
mosh = "ask";
"mosh *" = "ask";
"cat *.env" = "deny";
"cat *.env.*" = "deny";
"cat **/.env" = "deny";
"cat **/.env.*" = "deny";
"cat *.envrc" = "deny";
"cat **/.envrc" = "deny";
"cat .dev.vars" = "deny";
"cat **/.dev.vars" = "deny";
"cat *.pem" = "deny";
"cat *.key" = "deny";
"cat **/.gnupg/**" = "deny";
"cat **/.ssh/**" = "deny";
"cat ~/.config/gh/hosts.yml" = "deny";
"cat ~/.config/sops/age/keys.txt" = "deny";
"cat ~/.local/share/opencode/mcp-auth.json" = "deny";
"cat /etc/ssh/ssh_host_*" = "deny";
"cat /run/secrets/*" = "deny";
};
edit = {
"*" = "allow";
"**/.gnupg/**" = "deny";
"**/.ssh/**" = "deny";
"**/secrets/**" = "deny";
"secrets/*" = "deny";
"~/.config/gh/hosts.yml" = "deny";
"~/.config/sops/age/keys.txt" = "deny";
"~/.local/share/opencode/mcp-auth.json" = "deny";
"/etc/ssh/ssh_host_*" = "deny";
"/run/secrets/*" = "deny";
};
glob = "allow";
grep = "allow";
list = "allow";
lsp = "allow";
question = "allow";
read = {
"*" = "allow";
"*.env" = "deny";
"*.env.*" = "deny";
"*.envrc" = "deny";
"**/.env" = "deny";
"**/.env.*" = "deny";
"**/.envrc" = "deny";
".dev.vars" = "deny";
"**/.dev.vars" = "deny";
"**/.gnupg/**" = "deny";
"**/.ssh/**" = "deny";
"*.key" = "deny";
"*.pem" = "deny";
"**/secrets/**" = "deny";
"secrets/*" = "deny";
"~/.config/gh/hosts.yml" = "deny";
"~/.config/sops/age/keys.txt" = "deny";
"~/.local/share/opencode/mcp-auth.json" = "deny";
"/etc/ssh/ssh_host_*" = "deny";
"/run/secrets/*" = "deny";
};
skill = "allow";
task = "allow";
webfetch = "allow";
websearch = "allow";
codesearch = "allow";
};
agent = {
explore = {
model = "openai/gpt-5.1-codex-mini";
};
};
instructions = [
"CLAUDE.md"
"AGENT.md"
# "AGENTS.md"
"AGENTS.local.md"
];
formatter = {
mix = {
disabled = true;
};
};
mcp = {
opensrc = {
enabled = true;
type = "local";
command = ["node" "/home/cschmatzler/.bun/bin/opensrc-mcp"];
};
context7 = {
enabled = true;
type = "remote";
url = "https://mcp.context7.com/mcp";
};
grep_app = {
enabled = true;
type = "remote";
url = "https://mcp.grep.app";
};
};
};
};
xdg.configFile = {
# "opencode/agent" = {
# source = ./_opencode/agent;
# recursive = true;
# };
"opencode/command" = {
source = ./_opencode/command;
recursive = true;
};
"opencode/skill" = {
source = ./_opencode/skill;
recursive = true;
};
"opencode/plugin" = {
source = ./_opencode/plugin;
recursive = true;
};
"opencode/AGENTS.md".source = ./_opencode/AGENTS.md;
};
};
}

43
modules/apps.nix Normal file
View File

@@ -0,0 +1,43 @@
{inputs, ...}: {
perSystem = {
pkgs,
system,
...
}: let
descriptions = {
apply = "Build and apply configuration";
build = "Build configuration";
rollback = "Rollback to previous generation";
update = "Update flake inputs and regenerate flake.nix";
};
mkPlatformApp = name: {
type = "app";
program = "${(pkgs.writeShellScriptBin name ''
PATH=${pkgs.git}/bin:$PATH
exec ${inputs.self}/apps/${system}/${name} "$@"
'')}/bin/${name}";
meta.description = descriptions.${name};
};
mkSharedApp = name: {
type = "app";
program = "${(pkgs.writeShellScriptBin name ''
PATH=${pkgs.git}/bin:$PATH
exec ${inputs.self}/apps/${name} "$@"
'')}/bin/${name}";
meta.description = descriptions.${name};
};
platformAppNames = ["build" "rollback"];
sharedAppNames = ["apply" "update"];
in {
apps =
pkgs.lib.genAttrs platformAppNames mkPlatformApp
// pkgs.lib.genAttrs sharedAppNames mkSharedApp
// {
deploy = {
type = "app";
program = "${inputs.deploy-rs.packages.${system}.deploy-rs}/bin/deploy";
meta.description = "Deploy to NixOS hosts via deploy-rs";
};
};
};
}

17
modules/atuin.nix Normal file
View File

@@ -0,0 +1,17 @@
{...}: {
den.aspects.atuin.homeManager = {...}: {
programs.atuin = {
enable = true;
enableNushellIntegration = true;
flags = [
"--disable-up-arrow"
];
settings = {
style = "compact";
inline_height = 0;
show_help = false;
show_tabs = false;
};
};
};
}

11
modules/cache.nix Normal file
View File

@@ -0,0 +1,11 @@
{...}: let
caddyLib = import ./_lib/caddy.nix;
in {
den.aspects.cache.nixos = {
services.caddy.virtualHosts =
caddyLib.mkTailscaleVHost {
name = "cache";
configText = "reverse_proxy localhost:32843";
};
};
}

44
modules/core.nix Normal file
View File

@@ -0,0 +1,44 @@
{...}: {
den.aspects.core.os = {
pkgs,
lib,
...
}: {
# System utilities
environment.systemPackages =
lib.optionals pkgs.stdenv.isLinux [
pkgs.lm_sensors
];
programs.fish.enable = true;
environment.shells = [pkgs.nushell];
nixpkgs = {
config = {
allowUnfree = true;
};
};
nix = {
package = pkgs.nix;
settings = {
cores = 4;
substituters = [
"https://nix-community.cachix.org"
"https://cache.nixos.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
};
gc = {
automatic = true;
options = "--delete-older-than 30d";
};
extraOptions = ''
experimental-features = nix-command flakes
'';
};
};
}

View File

@@ -1,17 +1,23 @@
{
pkgs,
inputs,
user,
constants,
...
}: {
home-manager.extraSpecialArgs = {inherit user constants inputs;};
{inputs, ...}: let
local = import ./_lib/local.nix;
userHome = "/Users/${local.user.name}";
in {
den.aspects.darwin-system.darwin = {pkgs, ...}: {
imports = [
inputs.nix-homebrew.darwinModules.nix-homebrew
inputs.home-manager.darwinModules.home-manager
./_darwin/dock.nix
];
system = {
primaryUser = user;
stateVersion = constants.stateVersions.darwin;
system.primaryUser = local.user.name;
defaults = {
# Darwin system utilities
environment.systemPackages = with pkgs; [
dockutil
mas
];
system.defaults = {
NSGlobalDomain = {
AppleInterfaceStyle = null;
AppleShowAllExtensions = true;
@@ -38,7 +44,7 @@
autohide = true;
show-recents = false;
launchanim = true;
orientation = "bottom";
orientation = "left";
tilesize = 60;
minimize-to-application = true;
mru-spaces = false;
@@ -99,14 +105,16 @@
allowApplePersonalizedAdvertising = false;
};
"com.apple.Spotlight" = {
MenuItemHidden = true;
"NSStatusItem Visible Item-0" = false;
};
"com.apple.TextInputMenu" = {
visible = false;
};
};
};
nix = {
settings.trusted-users = ["@admin" "${user}"];
settings.trusted-users = [local.user.name];
gc.interval = {
Weekday = 0;
Hour = 2;
@@ -114,12 +122,44 @@
};
};
users.users.${user} = {
name = user;
home = "/Users/${user}";
users.users.${local.user.name} = {
name = local.user.name;
home = userHome;
isHidden = false;
shell = pkgs.nushell;
};
home-manager.useGlobalPkgs = true;
nix-homebrew = {
enable = true;
user = local.user.name;
mutableTaps = true;
taps = {
"homebrew/homebrew-core" = inputs.homebrew-core;
"homebrew/homebrew-cask" = inputs.homebrew-cask;
};
};
homebrew = {
enable = true;
onActivation = {
autoUpdate = true;
cleanup = "uninstall";
upgrade = true;
};
taps = [
"homebrew/cask"
];
casks = [
"1password"
"alcove"
"aqua-voice"
"chatgpt"
"ghostty@tip"
"raycast"
"spotify"
"tailscale"
"whatsapp"
];
};
};
}

70
modules/defaults.nix Normal file
View File

@@ -0,0 +1,70 @@
{
den,
lib,
...
}: {
options.flake = {
darwinConfigurations =
lib.mkOption {
type = lib.types.lazyAttrsOf lib.types.raw;
default = {};
};
deploy =
lib.mkOption {
type = lib.types.lazyAttrsOf lib.types.raw;
default = {};
};
flakeModules =
lib.mkOption {
type = lib.types.lazyAttrsOf lib.types.raw;
default = {};
};
};
config = {
flake.flakeModules = {
# Shared system foundations
core = ./core.nix;
darwin = ./darwin.nix;
network = ./network.nix;
nixos-system = ./nixos-system.nix;
overlays = ./overlays.nix;
secrets = ./secrets.nix;
# Shared host features
adguardhome = ./adguardhome.nix;
cache = ./cache.nix;
gitea = ./gitea.nix;
opencode = ./opencode.nix;
paperless = ./paperless.nix;
# User environment
ai-tools = ./ai-tools.nix;
atuin = ./atuin.nix;
desktop = ./desktop.nix;
dev-tools = ./dev-tools.nix;
email = ./email.nix;
neovim = ./neovim.nix;
shell = ./shell.nix;
ssh-client = ./ssh-client.nix;
terminal = ./terminal.nix;
zellij = ./zellij.nix;
zk = ./zk.nix;
};
den.default.nixos.system.stateVersion = "25.11";
den.default.darwin.system.stateVersion = 6;
den.default.homeManager = {
home.stateVersion = "25.11";
programs.home-manager.enable = true;
};
den.default.nixos.home-manager.useGlobalPkgs = true;
den.default.darwin.home-manager.useGlobalPkgs = true;
den.default.includes = [
den.provides.define-user
den.provides.inputs'
];
den.schema.user.classes = lib.mkDefault ["homeManager"];
};
}

92
modules/dendritic.nix Normal file
View File

@@ -0,0 +1,92 @@
{inputs, ...}: {
imports = [
(inputs.den.flakeModules.dendritic or {})
(inputs.flake-file.flakeModules.dendritic or {})
];
# Use alejandra with tabs for flake.nix formatting (matches alejandra.toml)
flake-file.formatter = pkgs:
pkgs.writeShellApplication {
name = "alejandra-tabs";
runtimeInputs = [pkgs.alejandra];
text = ''
echo 'indentation = "Tabs"' > alejandra.toml
alejandra "$@"
'';
};
# Declare all framework and module inputs via flake-file
flake-file.inputs = {
den.url = "github:vic/den";
flake-file.url = "github:vic/flake-file";
import-tree.url = "github:vic/import-tree";
flake-aspects.url = "github:vic/flake-aspects";
nixpkgs.url = "github:nixos/nixpkgs/master";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
darwin = {
url = "github:LnL7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
deploy-rs.url = "github:serokell/deploy-rs";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-homebrew.url = "github:zhaofengli-wip/nix-homebrew";
homebrew-core = {
url = "github:homebrew/homebrew-core";
flake = false;
};
homebrew-cask = {
url = "github:homebrew/homebrew-cask";
flake = false;
};
nixvim.url = "github:nix-community/nixvim";
neovim-nightly-overlay = {
url = "github:nix-community/neovim-nightly-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
llm-agents.url = "github:numtide/llm-agents.nix";
# Overlay inputs
himalaya.url = "github:pimalaya/himalaya";
jj-ryu = {
url = "github:dmmulroy/jj-ryu";
flake = false;
};
jj-starship.url = "github:dmmulroy/jj-starship";
zjstatus.url = "github:dj95/zjstatus";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk = {
url = "github:nix-community/naersk/master";
inputs.nixpkgs.follows = "nixpkgs";
};
# Neovim plugin inputs
code-review-nvim = {
url = "github:choplin/code-review.nvim";
flake = false;
};
jj-nvim = {
url = "github:NicolasGB/jj.nvim";
flake = false;
};
jj-diffconflicts = {
url = "github:rafikdraoui/jj-diffconflicts";
flake = false;
};
# Secrets inputs
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}

38
modules/deploy.nix Normal file
View File

@@ -0,0 +1,38 @@
{
inputs,
config,
...
}: let
local = import ./_lib/local.nix;
acceptNewHostKeys = [
"-o"
"StrictHostKeyChecking=accept-new"
];
mkSystemNode = {
hostname,
host,
}: {
inherit hostname;
sshUser = local.user.name;
sshOpts = acceptNewHostKeys;
profiles.system = {
user = "root";
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos config.flake.nixosConfigurations.${host};
};
};
in {
flake.deploy.nodes = {
michael =
mkSystemNode {
hostname = "git.schmatzler.com";
host = "michael";
};
tahani =
mkSystemNode {
hostname = "127.0.0.1";
host = "tahani";
};
};
flake.checks.x86_64-linux = inputs.deploy-rs.lib.x86_64-linux.deployChecks config.flake.deploy;
}

148
modules/desktop.nix Normal file
View File

@@ -0,0 +1,148 @@
{...}: {
den.aspects.desktop.homeManager = {
lib,
pkgs,
...
}: {
programs.aerospace = {
enable = true;
launchd.enable = true;
settings = {
start-at-login = true;
accordion-padding = 30;
default-root-container-layout = "tiles";
default-root-container-orientation = "auto";
on-focused-monitor-changed = [
"move-mouse monitor-lazy-center"
];
workspace-to-monitor-force-assignment = {
"1" = "main";
"2" = "main";
"3" = "main";
"4" = "main";
"5" = "main";
"6" = "main";
"7" = "main";
"8" = "main";
"9" = "secondary";
};
gaps = {
inner = {
horizontal = 8;
vertical = 8;
};
outer = {
left = 8;
right = 8;
top = 8;
bottom = 8;
};
};
on-window-detected = [
{
"if" = {
"app-id" = "com.apple.systempreferences";
};
run = "layout floating";
}
{
"if" = {
"app-id" = "com.mitchellh.ghostty";
};
run = ["layout tiling" "move-node-to-workspace 3"];
}
{
"if" = {
"app-id" = "com.apple.Safari";
};
run = "move-node-to-workspace 2";
}
{
"if" = {
"app-id" = "com.tinyspeck.slackmacgap";
};
run = "move-node-to-workspace 5";
}
{
"if" = {
"app-id" = "net.whatsapp.WhatsApp";
};
run = "move-node-to-workspace 5";
}
{
"if" = {
"app-id" = "com.tidal.desktop";
};
run = "move-node-to-workspace 6";
}
];
mode = {
main.binding = {
"alt-enter" = "exec-and-forget open -a Ghostty";
"alt-h" = "focus left";
"alt-j" = "focus down";
"alt-k" = "focus up";
"alt-l" = "focus right";
"alt-shift-h" = "move left";
"alt-shift-j" = "move down";
"alt-shift-k" = "move up";
"alt-shift-l" = "move right";
"alt-ctrl-h" = "focus-monitor --wrap-around left";
"alt-ctrl-j" = "focus-monitor --wrap-around down";
"alt-ctrl-k" = "focus-monitor --wrap-around up";
"alt-ctrl-l" = "focus-monitor --wrap-around right";
"alt-ctrl-shift-h" = "move-node-to-monitor --focus-follows-window --wrap-around left";
"alt-ctrl-shift-j" = "move-node-to-monitor --focus-follows-window --wrap-around down";
"alt-ctrl-shift-k" = "move-node-to-monitor --focus-follows-window --wrap-around up";
"alt-ctrl-shift-l" = "move-node-to-monitor --focus-follows-window --wrap-around right";
"alt-space" = "layout tiles accordion";
"alt-shift-space" = "layout floating tiling";
"alt-slash" = "layout horizontal vertical";
"alt-f" = "fullscreen";
"alt-tab" = "workspace-back-and-forth";
"alt-shift-tab" = "move-workspace-to-monitor --wrap-around next";
"alt-r" = "mode resize";
"alt-shift-semicolon" = "mode service";
"alt-1" = "workspace 1";
"alt-2" = "workspace 2";
"alt-3" = "workspace 3";
"alt-4" = "workspace 4";
"alt-5" = "workspace 5";
"alt-6" = "workspace 6";
"alt-7" = "workspace 7";
"alt-8" = "workspace 8";
"alt-9" = "workspace 9";
"alt-shift-1" = "move-node-to-workspace --focus-follows-window 1";
"alt-shift-2" = "move-node-to-workspace --focus-follows-window 2";
"alt-shift-3" = "move-node-to-workspace --focus-follows-window 3";
"alt-shift-4" = "move-node-to-workspace --focus-follows-window 4";
"alt-shift-5" = "move-node-to-workspace --focus-follows-window 5";
"alt-shift-6" = "move-node-to-workspace --focus-follows-window 6";
"alt-shift-7" = "move-node-to-workspace --focus-follows-window 7";
"alt-shift-8" = "move-node-to-workspace --focus-follows-window 8";
"alt-shift-9" = "move-node-to-workspace --focus-follows-window 9";
};
resize.binding = {
"h" = "resize width -50";
"j" = "resize height +50";
"k" = "resize height -50";
"l" = "resize width +50";
"enter" = "mode main";
"esc" = "mode main";
};
service.binding = {
"esc" = "mode main";
"r" = ["reload-config" "mode main"];
"b" = ["balance-sizes" "mode main"];
"f" = ["layout floating tiling" "mode main"];
"backspace" = ["close-all-windows-but-current" "mode main"];
};
};
};
};
};
}

246
modules/dev-tools.nix Normal file
View File

@@ -0,0 +1,246 @@
{...}: let
local = import ./_lib/local.nix;
palette = (import ./_lib/theme.nix).rosePineDawn.hex;
in {
den.aspects.dev-tools.homeManager = {
pkgs,
lib,
...
}: let
name = local.user.fullName;
in {
home.packages = with pkgs;
[
alejandra
ast-grep
bun
delta
deadnix
devenv
docker
docker-compose
lazydocker
gh
gnumake
hyperfine
jj-ryu
jj-starship
nil
nodejs_24
nurl
pnpm
postgresql_17
serie
sqlite
statix
tea
tokei
tree-sitter
(pkgs.writeShellApplication {
name = "tuist-pr";
runtimeInputs = with pkgs; [coreutils fzf gh git nushell];
text = ''
exec nu ${./_dev-tools/tuist-pr.nu} "$@"
'';
})
]
++ lib.optionals stdenv.isDarwin [
xcodes
]
++ lib.optionals stdenv.isLinux [
gcc15
];
# Git configuration
programs.git = {
enable = true;
ignores = ["*.swp"];
settings = {
user.name = name;
init.defaultBranch = "main";
core = {
editor = "vim";
autocrlf = "input";
pager = "delta";
};
credential = {
helper = "!gh auth git-credential";
"https://github.com".useHttpPath = true;
"https://gist.github.com".useHttpPath = true;
};
pull.rebase = true;
rebase.autoStash = true;
interactive.diffFilter = "delta --color-only";
delta = {
navigate = true;
line-numbers = true;
syntax-theme = "GitHub";
side-by-side = true;
pager = "less -FRX";
};
pager = {
diff = "delta";
log = "delta";
show = "delta";
};
};
lfs = {
enable = true;
};
};
# Jujutsu configuration
programs.jujutsu = {
enable = true;
settings = {
user = {
name = name;
email = local.user.emails.personal;
};
git = {
sign-on-push = true;
subprocess = true;
write-change-id-header = true;
private-commits = "description(glob:'wip:*') | description(glob:'WIP:*') | description(exact:'')";
};
fsmonitor = {
backend = "watchman";
};
ui = {
default-command = "status";
diff-formatter = ":git";
pager = ["delta" "--pager" "less -FRX"];
diff-editor = ["nvim" "-c" "DiffEditor $left $right $output"];
movement = {
edit = true;
};
};
aliases = {
n = ["new"];
tug = ["bookmark" "move" "--from" "closest_bookmark(@-)" "--to" "@-"];
stack = ["log" "-r" "stack()"];
retrunk = ["rebase" "-d" "trunk()"];
bm = ["bookmark"];
gf = ["git" "fetch"];
gp = ["git" "push"];
};
revset-aliases = {
"closest_bookmark(to)" = "heads(::to & bookmarks())";
"closest_pushable(to)" = "heads(::to & mutable() & ~description(exact:\"\") & (~empty() | merges()))";
"mine()" = "author(\"${local.user.emails.personal}\")";
"wip()" = "mine() ~ immutable()";
"open()" = "mine() ~ ::trunk()";
"current()" = "@:: & mutable()";
"stack()" = "reachable(@, mutable())";
};
templates = {
draft_commit_description = ''
concat(
coalesce(description, default_commit_description, "\n"),
surround(
"\nJJ: This commit contains the following changes:\n", "",
indent("JJ: ", diff.stat(72)),
),
"\nJJ: ignore-rest\n",
diff.git(),
)
'';
};
};
};
# JJUI configuration
programs.jjui = {
enable = true;
settings.ui.colors = {
text = {fg = palette.text;};
dimmed = {fg = palette.muted;};
selected = {
bg = palette.overlay;
fg = palette.text;
bold = true;
};
border = {fg = palette.muted;};
title = {
fg = palette.iris;
bold = true;
};
shortcut = {
fg = palette.pine;
bold = true;
};
matched = {
fg = palette.gold;
bold = true;
};
"revisions selected" = {
bg = palette.overlay;
fg = palette.text;
bold = true;
};
"status" = {bg = palette.overlay;};
"status title" = {
bg = palette.iris;
fg = palette.base;
bold = true;
};
"status shortcut" = {fg = palette.pine;};
"status dimmed" = {fg = palette.muted;};
"menu" = {bg = palette.base;};
"menu selected" = {
bg = palette.overlay;
fg = palette.text;
bold = true;
};
"menu border" = {fg = palette.muted;};
"menu title" = {
fg = palette.iris;
bold = true;
};
"menu shortcut" = {fg = palette.pine;};
"menu matched" = {
fg = palette.gold;
bold = true;
};
"preview border" = {fg = palette.muted;};
"help" = {bg = palette.base;};
"help border" = {fg = palette.muted;};
"help title" = {
fg = palette.iris;
bold = true;
};
"confirmation" = {bg = palette.base;};
"confirmation border" = {fg = palette.muted;};
"confirmation selected" = {
bg = palette.overlay;
fg = palette.text;
bold = true;
};
"confirmation dimmed" = {fg = palette.muted;};
source_marker = {
fg = palette.foam;
bold = true;
};
target_marker = {
fg = palette.rose;
bold = true;
};
};
};
# Direnv configuration
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
# Mise configuration
programs.mise = {
enable = true;
enableNushellIntegration = true;
globalConfig.settings = {
auto_install = false;
};
};
};
}

62
modules/email.nix Normal file
View File

@@ -0,0 +1,62 @@
{...}: let
local = import ./_lib/local.nix;
in {
den.aspects.email.homeManager = {pkgs, ...}: {
programs.aerc = {
enable = true;
extraConfig.general.unsafe-accounts-conf = true;
};
programs.himalaya = {
enable = true;
package =
pkgs.writeShellApplication {
name = "himalaya";
runtimeInputs = [pkgs.bash pkgs.coreutils pkgs.himalaya];
text = ''
exec env RUST_LOG="warn,imap_codec::response=error" ${pkgs.himalaya}/bin/himalaya "$@"
'';
};
};
programs.mbsync.enable = true;
services.mbsync = {
enable = true;
frequency = "*:0/5";
};
accounts.email = {
accounts.${local.user.emails.personal} = {
primary = true;
maildir.path = local.user.emails.personal;
address = local.user.emails.personal;
userName = local.user.emails.icloud;
realName = local.user.fullName;
passwordCommand = ["${pkgs.coreutils}/bin/cat" (local.secretPath "tahani-email-password")];
folders = {
inbox = "INBOX";
drafts = "Drafts";
sent = "Sent Messages";
trash = "Deleted Messages";
};
smtp = {
host = "smtp.mail.me.com";
port = 587;
tls.useStartTls = true;
};
himalaya.enable = true;
mbsync = {
enable = true;
create = "both";
expunge = "both";
};
imap = {
host = "imap.mail.me.com";
port = 993;
tls.enable = true;
};
aerc.enable = true;
};
};
};
}

View File

@@ -1,61 +1,33 @@
{
{lib, ...}: let
secretLib = import ./_lib/secrets.nix {inherit lib;};
in {
den.aspects.gitea.nixos = {
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.my.gitea;
in {
options.my.gitea = {
enable = mkEnableOption "Gitea git hosting service";
litestream = {
bucket =
mkOption {
type = types.str;
description = "S3 bucket name for Litestream database replication";
}: {
sops.secrets = {
michael-gitea-litestream =
secretLib.mkServiceBinarySecret {
name = "michael-gitea-litestream";
serviceUser = "gitea";
sopsFile = ../secrets/michael-gitea-litestream;
};
secretFile =
mkOption {
type = types.path;
description = "Path to the environment file containing S3 credentials for Litestream";
michael-gitea-restic-password =
secretLib.mkServiceBinarySecret {
name = "michael-gitea-restic-password";
serviceUser = "gitea";
sopsFile = ../secrets/michael-gitea-restic-password;
};
michael-gitea-restic-env =
secretLib.mkServiceBinarySecret {
name = "michael-gitea-restic-env";
serviceUser = "gitea";
sopsFile = ../secrets/michael-gitea-restic-env;
};
};
restic = {
bucket =
mkOption {
type = types.str;
description = "S3 bucket name for Restic repository backups";
};
passwordFile =
mkOption {
type = types.path;
description = "Path to the file containing the Restic repository password";
};
environmentFile =
mkOption {
type = types.path;
description = "Path to the environment file containing S3 credentials for Restic";
};
};
s3 = {
endpoint =
mkOption {
type = types.str;
default = "s3.eu-central-003.backblazeb2.com";
description = "S3 endpoint URL";
};
};
};
config =
mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [80 443];
services.redis.servers.gitea = {
@@ -105,29 +77,25 @@ in {
services.litestream = {
enable = true;
environmentFile = cfg.litestream.secretFile;
settings = {
dbs = [
environmentFile = config.sops.secrets.michael-gitea-litestream.path;
settings.dbs = [
{
path = "/var/lib/gitea/data/gitea.db";
replicas = [
{
type = "s3";
bucket = cfg.litestream.bucket;
bucket = "michael-gitea-litestream";
path = "gitea";
endpoint = cfg.s3.endpoint;
endpoint = "s3.eu-central-003.backblazeb2.com";
}
];
}
];
};
};
systemd.services.litestream = {
serviceConfig = {
User = mkForce "gitea";
Group = mkForce "gitea";
};
systemd.services.litestream.serviceConfig = {
User = lib.mkForce "gitea";
Group = lib.mkForce "gitea";
};
services.caddy = {
@@ -144,7 +112,7 @@ in {
};
services.restic.backups.gitea = {
repository = "s3:${cfg.s3.endpoint}/${cfg.restic.bucket}";
repository = "s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories";
paths = ["/var/lib/gitea"];
exclude = [
"/var/lib/gitea/log"
@@ -152,8 +120,8 @@ in {
"/var/lib/gitea/data/gitea.db-shm"
"/var/lib/gitea/data/gitea.db-wal"
];
passwordFile = cfg.restic.passwordFile;
environmentFile = cfg.restic.environmentFile;
passwordFile = config.sops.secrets.michael-gitea-restic-password.path;
environmentFile = config.sops.secrets.michael-gitea-restic-env.path;
pruneOpts = [
"--keep-daily 7"
"--keep-weekly 4"
@@ -170,8 +138,8 @@ in {
wants = ["restic-init-gitea.service"];
after = ["restic-init-gitea.service"];
serviceConfig = {
User = mkForce "gitea";
Group = mkForce "gitea";
User = lib.mkForce "gitea";
Group = lib.mkForce "gitea";
};
};
@@ -186,12 +154,12 @@ in {
User = "gitea";
Group = "gitea";
RemainAfterExit = true;
EnvironmentFile = cfg.restic.environmentFile;
EnvironmentFile = config.sops.secrets.michael-gitea-restic-env.path;
};
script = ''
export RESTIC_PASSWORD=$(cat ${cfg.restic.passwordFile})
restic -r s3:${cfg.s3.endpoint}/${cfg.restic.bucket} snapshots &>/dev/null || \
restic -r s3:${cfg.s3.endpoint}/${cfg.restic.bucket} init
export RESTIC_PASSWORD=$(cat ${config.sops.secrets.michael-gitea-restic-password.path})
restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories snapshots &>/dev/null || \
restic -r s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories init
'';
};
};

View File

@@ -13,7 +13,7 @@
nameservers = ["1.1.1.1"];
firewall = {
enable = true;
trustedInterfaces = ["eno1" "tailscale0"];
trustedInterfaces = ["eno1" "tailscale0" "docker0"];
allowedUDPPorts = [
53
config.services.tailscale.port

35
modules/hosts/chidi.nix Normal file
View File

@@ -0,0 +1,35 @@
{
den,
lib,
...
}: let
hostLib = import ../_lib/hosts.nix {inherit den lib;};
local = import ../_lib/local.nix;
host = "chidi";
hostMeta = local.hosts.chidi;
in
lib.recursiveUpdate
(hostLib.mkUserHost {
system = hostMeta.system;
inherit host;
user = local.user.name;
includes = [den.aspects.user-darwin-laptop];
homeManager = {...}: {
programs.git.settings.user.email = local.user.emails.work;
};
})
(hostLib.mkPerHostAspect {
inherit host;
includes = [
den.aspects.host-darwin-base
den.aspects.opencode-api-key
];
darwin = {...}: {
networking.hostName = host;
networking.computerName = host;
homebrew.casks = [
"slack"
];
};
})

31
modules/hosts/janet.nix Normal file
View File

@@ -0,0 +1,31 @@
{
den,
lib,
...
}: let
hostLib = import ../_lib/hosts.nix {inherit den lib;};
local = import ../_lib/local.nix;
host = "janet";
hostMeta = local.hosts.janet;
in
lib.recursiveUpdate
(hostLib.mkUserHost {
system = hostMeta.system;
inherit host;
user = local.user.name;
includes = [
den.aspects.user-darwin-laptop
den.aspects.user-personal
];
})
(hostLib.mkPerHostAspect {
inherit host;
includes = [
den.aspects.host-darwin-base
den.aspects.opencode-api-key
];
darwin = {...}: {
networking.hostName = host;
networking.computerName = host;
};
})

35
modules/hosts/michael.nix Normal file
View File

@@ -0,0 +1,35 @@
{
den,
inputs,
lib,
...
}: let
hostLib = import ../_lib/hosts.nix {inherit den lib;};
local = import ../_lib/local.nix;
host = "michael";
hostMeta = local.hosts.michael;
in
lib.recursiveUpdate
(hostLib.mkUserHost {
system = hostMeta.system;
inherit host;
user = local.user.name;
includes = [den.aspects.user-minimal];
})
(hostLib.mkPerHostAspect {
inherit host;
includes = [
den.aspects.host-public-server
den.aspects.gitea
];
nixos = {modulesPath, ...}: {
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
./_parts/michael/disk-config.nix
./_parts/michael/hardware-configuration.nix
inputs.disko.nixosModules.default
];
networking.hostName = host;
};
})

109
modules/hosts/tahani.nix Normal file
View File

@@ -0,0 +1,109 @@
{
den,
lib,
...
}: let
hostLib = import ../_lib/hosts.nix {inherit den lib;};
local = import ../_lib/local.nix;
secretLib = import ../_lib/secrets.nix {inherit lib;};
host = "tahani";
hostMeta = local.hosts.tahani;
in
lib.recursiveUpdate
(hostLib.mkUserHost {
system = hostMeta.system;
inherit host;
user = local.user.name;
includes = [
den.aspects.user-workstation
den.aspects.user-personal
den.aspects.email
];
homeManager = {
config,
inputs',
...
}: let
opencode = inputs'.llm-agents.packages.opencode;
in {
programs.opencode.settings.permission.external_directory = {
"/tmp/himalaya-triage/*" = "allow";
"/var/lib/paperless/consume/inbox-triage/*" = "allow";
};
programs.nushell.extraConfig = ''
if $nu.is-interactive and ('SSH_CONNECTION' in ($env | columns)) and ('ZELLIJ' not-in ($env | columns)) {
try {
zellij attach -c main
exit
} catch {
print "zellij auto-start failed; staying in shell"
}
}
'';
systemd.user.services.opencode-inbox-triage = {
Unit = {
Description = "OpenCode inbox triage";
};
Service = {
Type = "oneshot";
ExecStart = "${opencode}/bin/opencode run --command inbox-triage --model opencode-go/glm-5";
Environment = "PATH=${config.home.profileDirectory}/bin:/run/current-system/sw/bin";
};
};
systemd.user.timers.opencode-inbox-triage = {
Unit = {
Description = "Run OpenCode inbox triage every 12 hours";
};
Timer = {
OnCalendar = "*-*-* 0/12:00:00";
Persistent = true;
};
Install = {
WantedBy = ["timers.target"];
};
};
};
})
(hostLib.mkPerHostAspect {
inherit host;
includes = [
den.aspects.host-nixos-base
den.aspects.opencode-api-key
den.aspects.adguardhome
den.aspects.cache
den.aspects.paperless
];
nixos = {...}: {
imports = [
./_parts/tahani/networking.nix
];
networking.hostName = host;
sops.secrets.tahani-email-password =
secretLib.mkUserBinarySecret {
name = "tahani-email-password";
sopsFile = ../../secrets/tahani-email-password;
};
virtualisation.docker.enable = true;
users.users.${local.user.name}.extraGroups = [
"docker"
"paperless"
];
systemd.tmpfiles.rules = [
"d /var/lib/paperless/consume 2775 paperless paperless -"
"d /var/lib/paperless/consume/inbox-triage 2775 paperless paperless -"
];
swapDevices = [
{
device = "/swapfile";
size = 16 * 1024;
}
];
};
})

10
modules/inventory.nix Normal file
View File

@@ -0,0 +1,10 @@
{lib, ...}: let
local = import ./_lib/local.nix;
in
lib.foldl' lib.recursiveUpdate {} (
lib.mapAttrsToList (
host: hostMeta:
lib.setAttrByPath ["den" "hosts" hostMeta.system host "users" local.user.name] {}
)
local.hosts
)

14
modules/neovim.nix Normal file
View File

@@ -0,0 +1,14 @@
{inputs, ...}: {
den.aspects.neovim.homeManager = {pkgs, ...}: {
imports = [
inputs.nixvim.homeModules.nixvim
./_neovim/default.nix
];
_module.args.nvim-plugin-sources = {
code-review-nvim = inputs.code-review-nvim;
jj-nvim = inputs.jj-nvim;
jj-diffconflicts = inputs.jj-diffconflicts;
};
};
}

76
modules/network.nix Normal file
View File

@@ -0,0 +1,76 @@
{...}: {
den.aspects.openssh.nixos = {
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
};
};
};
den.aspects.fail2ban.nixos = {
services.fail2ban = {
enable = true;
maxretry = 5;
bantime = "10m";
bantime-increment = {
enable = true;
multipliers = "1 2 4 8 16 32 64";
maxtime = "168h";
overalljails = true;
};
jails = {
sshd.settings = {
enabled = true;
port = "ssh";
filter = "sshd";
maxretry = 3;
};
gitea.settings = {
enabled = true;
filter = "gitea";
logpath = "/var/lib/gitea/log/gitea.log";
maxretry = 10;
findtime = 3600;
bantime = 900;
action = "iptables-allports";
};
};
};
environment.etc."fail2ban/filter.d/gitea.local".text = ''
[Definition]
failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
ignoreregex =
'';
};
den.aspects.tailscale.nixos = {
services.tailscale = {
enable = true;
extraSetFlags = ["--ssh"];
openFirewall = true;
permitCertUid = "caddy";
useRoutingFeatures = "server";
};
};
den.aspects.mosh.nixos = {
programs.mosh = {
enable = true;
openFirewall = false;
};
networking.firewall.interfaces.tailscale0.allowedUDPPortRanges = [
{
from = 60000;
to = 61000;
}
];
};
den.aspects.tailscale.darwin = {
services.tailscale.enable = true;
};
}

Some files were not shown because too many files have changed in this diff Show More