-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJustfile
More file actions
1101 lines (1001 loc) · 52.1 KB
/
Copy pathJustfile
File metadata and controls
1101 lines (1001 loc) · 52.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Dotfiles task runner. Use `just` to list tasks.
# https://just.systems
set shell := ["bash", "-cu"]
# Run native Windows recipes directly in PowerShell.
set windows-shell := ["powershell.exe", "-NoProfile", "-Command"]
# Treat `#` lines in recipes as comments so they are not echoed during runs
# (otherwise every comment inside e.g. `_rebuild-macos` prints on `just rebuild`).
set ignore-comments := true
flake := justfile_directory() + "/nix"
default:
@just --list --unsorted
# ─────────────────────────────────────────────
# Build
# ─────────────────────────────────────────────
[group('Build')]
[doc('Rebuild the system and user configuration')]
rebuild:
@just _rebuild-{{os()}}
[group('Build')]
[doc('Build every flake check available on this architecture')]
check-all *args:
@cd {{flake}} && nix run .#check-all -- {{args}}
[group('Build')]
[doc('Build every flake package available on this architecture')]
build-all *args:
@cd {{flake}} && nix run .#build-all -- {{args}}
[group('Build')]
[doc('Build the non-destructive NixOS recovery ISO (Linux builder required)')]
recovery-iso:
@nix build {{flake}}#recovery-iso --out-link result-recovery
@echo "ISO: {{justfile_directory()}}/result-recovery/iso/gapul-nixos-recovery.iso"
# Satisfy Homebrew's tap-trust requirement. Formulae/casks from unofficial taps
# managed by nix-darwin are refused on load by brew upgrade / install unless trusted.
# Shared because both the rebuild and upgrade(maintain) paths need it.
[private]
_brew-trust-taps:
@-brew tap 2>/dev/null | grep -v '^homebrew/' | xargs -I% env -u XDG_CONFIG_HOME brew trust % >/dev/null
@-env -u XDG_CONFIG_HOME brew trust --cask gerlero/openfoam/openfoam@2606 >/dev/null
@-brew list --cask --full-name 2>/dev/null | grep '/' | xargs -I% env -u XDG_CONFIG_HOME brew trust --cask % >/dev/null
[private]
_rebuild-macos:
#!/usr/bin/env bash
set -euo pipefail
just _brew-trust-taps
# Tee the whole run to a fixed log so a failure can be inspected after the fact
# without re-running — crucially the Homebrew bundle step, which runs during
# nix-darwin activation and so is absent from `nix log <drv>`. When stdout is not
# a TTY (git hooks, CI, an agent shell) drop nix-output-monitor's live TUI so the
# log stays greppable plain text instead of cursor-control escape soup.
mkdir -p "$HOME/tmp"
log="$HOME/tmp/nix-rebuild.log"
nom_flag=""; [ -t 1 ] || nom_flag="--no-nom"
: > "$log"
# taskpolicy -b drops the build to background QoS. If the build pins the CPU,
# Ghostty's event handling misses its deadline and the global keybind's CGEventTap
# gets disabled with kCGEventTapDisabledByTimeout, after which cmd+space stops
# working while unfocused (Ghostty does not re-enable the tap itself: ghostty#11883).
# Lowering the priority keeps Ghostty from starving, preventing the tap disable.
echo "━━━ nix-darwin" | tee -a "$log"
taskpolicy -b nh darwin switch -q -Q --diff never $nom_flag 2>&1 | tee -a "$log"
echo "✓ nix-darwin" | tee -a "$log"
echo "━━━ home-manager" | tee -a "$log"
taskpolicy -b nh home switch -q -Q --diff never $nom_flag 2>&1 | tee -a "$log"
echo "✓ home-manager" | tee -a "$log"
open -a Ghostty >/dev/null 2>&1 || true
[private]
_rebuild-linux:
#!/usr/bin/env bash
set -euo pipefail
echo "━━━ home-manager"
nh home switch -q -Q --diff never
echo "✓ home-manager"
[private]
_rebuild-windows:
@just win-bootstrap
# List or compare system generations.
[group('Build')]
gen action="" a="" b="":
#!/usr/bin/env bash
set -euo pipefail
p=/nix/var/nix/profiles
cur=$(readlink $p/system | sed -E 's/system-([0-9]+)-link/\1/')
case "{{action}}" in
"")
for l in $p/system-*-link; do
n=$(echo "$l" | sed -E 's#.*/system-([0-9]+)-link#\1#')
d=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$l")
mark=""; [ "$n" = "$cur" ] && mark=" <- current"
printf '%4s %s%s\n' "$n" "$d" "$mark"
done | sort -n
;;
diff)
a="{{a}}"; b="{{b}}"; a="${a:-$((cur-1))}"; b="${b:-$cur}"
echo "Package diff: generation $a -> $b"
nix store diff-closures "$p/system-$a-link" "$p/system-$b-link"
;;
*) echo "usage: just gen [diff [a] [b]]" >&2; exit 2 ;;
esac
# Roll back to the previous or selected system generation.
[group('Build')]
rollback gen="":
#!/usr/bin/env bash
set -euo pipefail
if [ -n "{{gen}}" ]; then
echo "-> Switching to generation {{gen}}"
sudo darwin-rebuild --switch-generation {{gen}}
else
echo "-> Rolling back to previous generation"
sudo darwin-rebuild --rollback
fi
# Update flake inputs, then rebuild.
[group('Build')]
update *inputs:
#!/usr/bin/env bash
set -euo pipefail
echo "━━━ flake inputs"
lock="{{flake}}/flake.lock"
old_lock=$(mktemp)
cp "$lock" "$old_lock"
trap 'rc=$?; if [ $rc -ne 0 ]; then cp "$old_lock" "$lock"; echo "Restored flake.lock after failed update" >&2; fi; rm -f "$old_lock"; exit $rc' EXIT
just _update-lock {{inputs}}
if cmp -s "$old_lock" "$lock"; then
echo "✓ already up to date"
else
echo "✓ flake.lock updated"
fi
just rebuild
trap - EXIT
rm -f "$old_lock"
[private]
_update-lock *inputs:
@nix flake update --quiet {{inputs}} --flake {{flake}}
# Upgrade all package layers.
[group('Build')]
upgrade:
@just _upgrade-{{os()}}
[private]
_upgrade-macos:
@just _upgrade-nix-runtime-macos
@just _upgrade-packages-macos
@just update
[private]
_upgrade-nix-runtime-macos:
@echo "━━━ Nix runtime"
@if command -v determinate-nixd >/dev/null; then sudo determinate-nixd upgrade; else echo "– Determinate Nixd not installed"; fi
[private]
_upgrade-packages-macos:
# Order matters: `brew update` resets the trust of unofficial taps, so run update
# first, then trust, and run the subsequent upgrade with HOMEBREW_NO_AUTO_UPDATE=1
# so the trust is not reset again. Skip this and update ->
# (trust lost) -> upgrade makes qmk/qmk (formula) and y3owk1n/tap/neru (cask)
# get refused as untrusted; the cask is swallowed by `|| true` and the final check fails.
@brew update --quiet || true
@just _brew-trust-taps
@just _brew-formula-upgrade
@echo "━━━ Homebrew casks"
@HOMEBREW_NO_AUTO_UPDATE=1 brew upgrade --quiet --cask --greedy || true
@just _xcode-upgrade
@just sketchybar-font
# The --greedy upgrade above also targets auto_updates/`version :latest` casks,
# but those stay "outdated" forever and can never "complete" (e.g. figma-agent,
# pear-desktop; for the latter the upstream cask errors on upgrade itself due to a
# missing URL, swallowed by `|| true`). So judge completion with outdated without --greedy.
# Non-greedy does not list auto_updates/latest, so it detects only missed real
# version updates and does not break as auto_updates casks grow (previously
# figma-agent was grep-excluded, but enumerating them breaks, so that was removed).
@remaining=$(HOMEBREW_NO_AUTO_UPDATE=1 brew outdated --cask 2>/dev/null || true); if [ -n "$remaining" ]; then echo "ERROR: cask upgrade incomplete:" >&2; echo "$remaining" >&2; exit 1; fi
# Upgrade Homebrew formulae, resilient to a single broken formula (mirrors the cask handling).
# `brew upgrade --formula` aborts the whole run if ANY formula fails — e.g. concord 2.5.0, whose
# tap formula ungated `depends_on "alsa-lib"`/`"pipewire"` (Linux-only, no macOS bottle) so it
# can't install on macOS. Previously that one failure aborted `just maintain` and rolled back the
# flake update. Now individual failures are tolerated (|| true) and we fail only if a NON-pinned
# formula is still outdated (a genuine unhandled failure). Pinned-but-outdated (intentionally held,
# e.g. `brew pin concord` at 2.4.8) is fine. Names are normalized to basename because
# `brew outdated` prints tap-qualified names (chojs23/tap/concord) while `brew list --pinned`
# prints short names (concord).
[private]
_brew-formula-upgrade:
#!/usr/bin/env bash
set -u
echo "━━━ Homebrew formulae"
HOMEBREW_NO_AUTO_UPDATE=1 brew upgrade --quiet --formula || true
pinned=$(brew list --pinned 2>/dev/null | sed 's#.*/##')
bad=""
for f in $(HOMEBREW_NO_AUTO_UPDATE=1 brew outdated --formula --quiet 2>/dev/null | sed 's#.*/##'); do
printf '%s\n' "$pinned" | grep -qxF "$f" || bad="$bad $f"
done
if [ -n "$bad" ]; then
echo "ERROR: formula upgrade incomplete (non-pinned):$bad" >&2
exit 1
fi
# Keep Xcode current via xcodes (replaces mas for Xcode). Reads Apple ID from the
# sops-decrypted files so username/password auth is non-interactive; 2FA is prompted
# only on first login / expired session. Never aborts the outer upgrade (|| true / echo)
# so a skipped 2FA prompt doesn't fail `just upgrade`.
[private]
_xcode-upgrade:
#!/usr/bin/env bash
set -u
command -v xcodes >/dev/null || { echo "– xcodes not installed, skip"; exit 0; }
echo "━━━ Xcode (xcodes)"
aid="$HOME/.config/xcodes/apple_id"; pw="$HOME/.config/xcodes/password"
if [ -r "$aid" ] && [ -r "$pw" ]; then
export XCODES_USERNAME XCODES_PASSWORD
XCODES_USERNAME=$(cat "$aid"); XCODES_PASSWORD=$(cat "$pw")
else
echo "– Apple ID not set in sops (xcodes/apple_id, xcodes/password); will prompt if needed"
fi
# --latest installs the newest release (no-op if already current) and selects it. aria2 is
# auto-used for the parallel .xip download. On failure (e.g. skipped 2FA) don't break upgrade.
if xcodes install --latest; then
sudo xcodebuild -license accept 2>/dev/null || true
sudo xcodebuild -runFirstLaunch 2>/dev/null || true
else
echo "– xcodes install skipped/failed (2FA or auth?); re-run: xcodes install --latest"
fi
[private]
_upgrade-linux:
@just update
[private]
_upgrade-windows:
@just win-upgrade
[group('Build')]
[doc('Update, upgrade, rebuild, and garbage-collect')]
maintain:
@just _maintain-{{os()}}
[private]
_maintain-macos:
#!/usr/bin/env bash
set -euo pipefail
maintenance_lock="$HOME/.local/state/dotfiles-maintenance.lock"
mkdir -p "$HOME/.local/state"
if ! mkdir "$maintenance_lock" 2>/dev/null; then
# If the holder PID is alive it is genuinely running. If dead it is a leftover from an interrupt, so reclaim it
# (a mkdir-only lock always goes stale on SIGKILL / an interrupted build).
holder=$(cat "$maintenance_lock/pid" 2>/dev/null || true)
if [ -n "$holder" ] && kill -0 "$holder" 2>/dev/null; then
echo "another dotfiles maintenance task is running (pid $holder)" >&2; exit 1
fi
echo "reclaiming stale maintenance lock (pid ${holder:-?} not running)" >&2
rm -rf "$maintenance_lock"
mkdir "$maintenance_lock" 2>/dev/null || { echo "failed to acquire maintenance lock" >&2; exit 1; }
fi
echo $$ > "$maintenance_lock/pid"
export DOTFILES_MAINTAIN=1
# To avoid accidentally maintaining with stale config, first bring our own dotfiles up to date.
# Only on the main branch (do not pull on a feature branch or when detached). Git operations
# always target the main tree. The pull fires the post-merge/post-rewrite hook, but the
# DOTFILES_MAINTAIN=1 exported above makes the hook-side rebuild skip
# (we run just rebuild ourselves afterward, avoiding a double rebuild).
if [ "$(git -C "$HOME/.dotfiles" branch --show-current 2>/dev/null)" = "main" ]; then
echo "━━━ git pull (--rebase --autostash)"
git -C "$HOME/.dotfiles" pull --rebase --autostash
fi
just outdated
lock="{{flake}}/flake.lock"
old_lock=$(mktemp)
cp "$lock" "$old_lock"
trap 'rc=$?; if [ $rc -ne 0 ]; then cp "$old_lock" "$lock"; echo "Restored flake.lock after failed maintain" >&2; fi; rm -f "$old_lock"; rm -rf "$maintenance_lock"; exit $rc' EXIT
just _upgrade-nix-runtime-macos
just _update-lock
just _upgrade-packages-macos
just rebuild
just gc
brew services cleanup || true
just _maintain-user-tools
just doctor || true
trap - EXIT
rm -f "$old_lock"
rm -rf "$maintenance_lock"
[private]
_maintain-linux:
#!/usr/bin/env bash
set -euo pipefail
maintenance_lock="$HOME/.local/state/dotfiles-maintenance.lock"
mkdir -p "$HOME/.local/state"
if ! mkdir "$maintenance_lock" 2>/dev/null; then
# If the holder PID is alive it is genuinely running. If dead it is a leftover from an interrupt, so reclaim it
# (a mkdir-only lock always goes stale on SIGKILL / an interrupted build).
holder=$(cat "$maintenance_lock/pid" 2>/dev/null || true)
if [ -n "$holder" ] && kill -0 "$holder" 2>/dev/null; then
echo "another dotfiles maintenance task is running (pid $holder)" >&2; exit 1
fi
echo "reclaiming stale maintenance lock (pid ${holder:-?} not running)" >&2
rm -rf "$maintenance_lock"
mkdir "$maintenance_lock" 2>/dev/null || { echo "failed to acquire maintenance lock" >&2; exit 1; }
fi
echo $$ > "$maintenance_lock/pid"
export DOTFILES_MAINTAIN=1
# To avoid accidentally maintaining with stale config, first bring our own dotfiles up to date.
# Only on the main branch (do not pull on a feature branch or when detached). Git operations
# always target the main tree. The pull fires the post-merge/post-rewrite hook, but the
# DOTFILES_MAINTAIN=1 exported above makes the hook-side rebuild skip
# (we run just rebuild ourselves afterward, avoiding a double rebuild).
if [ "$(git -C "$HOME/.dotfiles" branch --show-current 2>/dev/null)" = "main" ]; then
echo "━━━ git pull (--rebase --autostash)"
git -C "$HOME/.dotfiles" pull --rebase --autostash
fi
just outdated
lock="{{flake}}/flake.lock"
old_lock=$(mktemp)
cp "$lock" "$old_lock"
trap 'rc=$?; if [ $rc -ne 0 ]; then cp "$old_lock" "$lock"; echo "Restored flake.lock after failed maintain" >&2; fi; rm -f "$old_lock"; rm -rf "$maintenance_lock"; exit $rc' EXIT
just _update-lock
just rebuild
just gc
just _maintain-user-tools
just doctor || true
trap - EXIT
rm -f "$old_lock"
rm -rf "$maintenance_lock"
[private]
_maintain-user-tools:
@if command -v tldr >/dev/null; then echo "━━━ tldr cache ━━━"; tldr --update || true; fi
@if command -v gh >/dev/null && gh extension list 2>/dev/null | grep -q .; then echo "━━━ GitHub CLI extensions ━━━"; gh extension upgrade --all || true; fi
[private]
_maintain-windows:
just win-bootstrap
just win-upgrade
@echo "Windows: gc recipe is not defined; package update completed."
# Update sketchybar-app-font assets from the same release.
[private]
sketchybar-font:
#!/usr/bin/env bash
set -euo pipefail
repo="kvndrsslr/sketchybar-app-font"
dir="{{justfile_directory()}}"
ttf="$dir/configs/fonts/sketchybar-app-font.ttf"
map="$dir/configs/wm/sketchybar/plugins/icon_map.sh"
# A transient network failure here used to abort `just maintain` entirely and roll back
# flake.lock. The font is a nice-to-have, so treat an unreachable API as "skip", like xcodes.
tag=$(gh release view --repo "$repo" --json tagName -q .tagName) || {
echo "– sketchybar-app-font: release lookup failed (offline?); skipped" >&2
exit 0
}
cur=$(awk '/pname = "sketchybar-app-font"/{getline; if (match($0,/[0-9][0-9.]*/)) print substr($0,RSTART,RLENGTH); exit}' "$dir/nix/hosts/darwin.nix")
if [ "$tag" = "v$cur" ]; then
exit 0
fi
echo "sketchybar-app-font: updating $cur -> $tag"
gh release download "$tag" --repo "$repo" --pattern sketchybar-app-font.ttf --output "$ttf" --clobber
gh release download "$tag" --repo "$repo" --pattern icon_map.sh --output "$map" --clobber
awk '/^### END-OF-ICON-MAP/{print; print "__icon_map \"$1\""; print "[ -r \"${BASH_SOURCE%/*}/icon_map_local.sh\" ] && source \"${BASH_SOURCE%/*}/icon_map_local.sh\""; print "echo \"$icon_result\""; exit} {print}' "$map" > "$map.tmp" && mv "$map.tmp" "$map"
# gh release assets are non-executable, so restore +x; plugins run icon_map.sh
# directly ($(...)), and without +x it fails with "permission denied" -> the
# workspace app icons silently vanish. `chmod` before `git add` so the staged
# mode is recorded as 100755 too.
chmod +x "$map"
sed -i "" -E '/pname = "sketchybar-app-font"/{n;s/version = "[0-9.]+"/version = "'"${tag#v}"'"/;}' "$dir/nix/hosts/darwin.nix"
git -C "$dir" add "$ttf" "$map" "$dir/nix/hosts/darwin.nix"
echo "Updated ($tag). Apply with: just rebuild (automatic when run via upgrade)"
# ─────────────────────────────────────────────
# Inspect (diff / type-check / diagnostics)
# ─────────────────────────────────────────────
# Type-check / show diff (`just check` = syntax/type-check, `just check diff` = diff build)
[group('Inspect')]
check what="":
#!/usr/bin/env bash
set -euo pipefail
case "{{what}}" in
"") nix flake check --no-build {{flake}} ;; # syntax/type-check (no build)
diff) nh darwin build ;; # current vs. flake diff
*) echo "usage: just check [diff]" >&2; exit 2 ;;
esac
# Package search (`just search <q>` = brew+nixpkgs, `just search <q> all` = + cargo)
# all is for discovering ecosystem-only tools not in nix/brew. The default is less noisy.
[group('Inspect')]
[doc('Package search (`just search <q>` = brew+nixpkgs, `just search <q> all` = + cargo)')]
search query scope="":
#!/usr/bin/env bash
set -u
if [ -z "{{query}}" ]; then echo "usage: just search <name> [all]" >&2; exit 2; fi
case "{{scope}}" in ""|all) ;; *) echo "usage: just search <name> [all]" >&2; exit 2 ;; esac
echo "━━━ Homebrew (formula + cask) ━━━"
brew search {{query}} 2>&1 || true
echo ""
echo "━━━ nixpkgs (local eval) ━━━"
# nh search depends on the search.nixos.org API and is flaky, so use nix search
# (the eval cache kicks in, so subsequent runs take a few seconds; warnings suppressed)
nix search nixpkgs {{query}} 2>/dev/null || echo " (none)"
# Only for all, also sweep the ecosystem-only source (cargo)
if [ "{{scope}}" = "all" ]; then
echo ""
echo "━━━ crates.io (cargo) ━━━"
cargo search {{query}} 2>&1 | head -10 || echo " (none)"
fi
# List what can be updated (preview before upgrade; brew + mas + flake inputs; non-destructive)
[group('Inspect')]
outdated:
#!/usr/bin/env bash
set -u
echo "━━━ Homebrew (formula + cask, --greedy) ━━━"
o=$(brew outdated --greedy 2>/dev/null); [ -n "$o" ] && echo "$o" || echo " (up to date)"
echo ""
echo "━━━ Xcode (xcodes) ━━━"
if command -v xcodes >/dev/null; then
# auth-free preview: show what's installed. `xcodes update` (Apple login + maybe 2FA)
# is intentionally left to `just upgrade` so this stays non-interactive.
inst=$(xcodes installed 2>/dev/null | tr -s ' ' | paste -sd', ' - || true)
echo " installed: ${inst:-none} (just upgrade -> xcodes install --latest)"
else
echo " (xcodes not installed)"
fi
echo ""
echo "━━━ flake inputs (lock last-modified) ━━━"
if command -v jq >/dev/null; then
nix flake metadata {{flake}} --json 2>/dev/null \
| jq -r '.locks.nodes | to_entries[] | select(.value.locked.lastModified) | "\(.key)\t\(.value.locked.lastModified)"' \
| while IFS=$'\t' read -r name ts; do printf ' %-22s %s\n' "$name" "$(date -r "$ts" '+%Y-%m-%d')"; done \
| sort -k2
else
echo " (jq not installed, skip)"
fi
echo ""
echo "-> Update: just upgrade (all) / just update <input> (individual)"
# Environment health check (run after e.g. a Determinate upgrade)
[group('Inspect')]
doctor format="":
#!/usr/bin/env bash
set -u
if [[ "{{format}}" == "json" || "{{format}}" == "--json" ]]; then
brew_json=$({ "{{justfile_directory()}}/scripts/check-brew-drift.sh" --json; } 2>/dev/null || true)
[[ -n $brew_json ]] || brew_json='{"ok":false,"error":"brew drift check failed"}'
result=$(jq -n \
--argjson nixMounted "$([[ $(mount | grep -c ' on /nix ') -gt 0 ]] && echo true || echo false)" \
--argjson fstabOk "$(! grep '/nix' /etc/fstab 2>/dev/null | grep -q noauto && echo true || echo false)" \
--argjson nixDecrypted "$(! diskutil apfs list 2>/dev/null | grep -A 6 'Nix Store' | grep -q 'FileVault: *Yes' && echo true || echo false)" \
--argjson sketchybar "$(pgrep -fq '/opt/homebrew/opt/sketchybar/bin/sketchybar' && echo true || echo false)" \
--argjson omniwm "$(pgrep -xq OmniWM && echo true || echo false)" \
--argjson karabiner "$(pgrep -fq Karabiner-Core-Service && echo true || echo false)" \
--argjson ageKey "$([[ -f $HOME/.config/sops/age/keys.txt ]] && echo true || echo false)" \
--argjson clean "$([[ -z $(git -C {{justfile_directory()}} status --short) ]] && echo true || echo false)" \
--argjson brew "$brew_json" \
'{ok: ($nixMounted and $fstabOk and $nixDecrypted and $sketchybar and $omniwm and $karabiner and $ageKey and $brew.ok), checks: {nixMounted: $nixMounted, fstab: $fstabOk, nixDecrypted: $nixDecrypted, sketchybar: $sketchybar, omniwm: $omniwm, karabiner: $karabiner, ageKey: $ageKey, workingTreeClean: $clean}, brew: $brew}')
printf '%s\n' "$result"
jq -e '.ok' <<<"$result" >/dev/null
exit $?
fi
if [[ -n "{{format}}" ]]; then echo "usage: just doctor [--json|json]" >&2; exit 2; fi
pass=0; fail=0
check() { if eval "$2"; then echo " [ok] $1"; pass=$((pass+1)); else echo " [FAIL] $1"; fail=$((fail+1)); fi; }
# warn: informational check that does not fail even if unmet ($3 = message when unmet)
warn() { if eval "$2"; then echo " [ok] $1"; else echo " [warn] $3"; fi; }
echo "== /nix mount =="
check "/nix is mounted" 'mount | grep -q " on /nix "'
check "/etc/fstab has no noauto (Login Items fix)" '! grep "/nix" /etc/fstab | grep -q noauto'
check "/nix decrypted (FileVault: No)" '! diskutil apfs list 2>/dev/null | grep -A 6 "Nix Store" | grep -q "FileVault: *Yes"'
echo "== Login Items =="
check "OmniWM registered" 'osascript -e "tell application \"System Events\" to get name of login items" | grep -q OmniWM'
check "Ghostty registered" 'osascript -e "tell application \"System Events\" to get name of login items" | grep -q Ghostty'
echo "== Key apps running =="
check "sketchybar" 'pgrep -fq "/opt/homebrew/opt/sketchybar/bin/sketchybar"'
check "OmniWM" 'pgrep -xq OmniWM'
check "Karabiner Core-Service" 'pgrep -fq Karabiner-Core-Service'
echo "== dotfiles =="
warn "Working tree clean (no uncommitted)" '[[ -z "$(git -C {{justfile_directory()}} status --short)" ]]' "Uncommitted changes -> commit/push recommended"
check "age private key present" '[[ -f ~/.config/sops/age/keys.txt ]]'
echo "== Homebrew declaration drift =="
if "{{justfile_directory()}}/scripts/check-brew-drift.sh"; then
echo " [ok] Homebrew matches declaration"; pass=$((pass+1))
else
echo " [FAIL] Homebrew drift detected -> apply: just rebuild"; fail=$((fail+1))
fi
echo
# If the bar/WM stack is down, surface a recovery path (to the restart recipe)
down=()
pgrep -fq "/opt/homebrew/opt/sketchybar/bin/sketchybar" || down+=(sketchybar)
pgrep -xq OmniWM || down+=(omniwm)
pgrep -xq borders || down+=(borders)
if [ ${#down[@]} -gt 0 ]; then
echo "[warn] Down: ${down[*]} -> recover: just restart (individual: just restart <name>)"
echo
fi
echo "Result: $pass passed, $fail failed"
exit $fail
# NOTE: nix fmt (treefmt in bulk) is unusable because the flake lives in nix/ and detection of the
# tree-root flake.nix fails (see the treefmt comment in flake.nix). Run it via pre-commit, which bundles per-file hooks.
# Format code + lint across all tracked files (OS auto-detected: Mac/Linux=pre-commit, Win=PSScriptAnalyzer)
[group('Inspect')]
fmt:
@just _fmt-{{os()}}
[private]
_fmt-macos: _fmt-unix
[private]
_fmt-linux: _fmt-unix
[private]
_fmt-unix:
nix develop {{flake}} --command pre-commit run --all-files
# The actual work is win-fmt (single source of the command)
[private]
_fmt-windows:
@just win-fmt
# ─────────────────────────────────────────────
# Clean (clean / GC and junk files)
# ─────────────────────────────────────────────
# GC all layers at once (only regenerable caches; Trash and whole-home deletion are in gc-deep)
[group('Clean')]
gc:
#!/usr/bin/env bash
set -u
echo "━━━ Nix store (remove old generations) ━━━"
nh clean all --keep 5 --keep-since 7d || true
echo ""
echo "━━━ Homebrew (downloads + old versions) ━━━"
brew autoremove 2>&1 | tail -3 || true
brew cleanup --prune=all 2>&1 | tail -3 || true
echo ""
echo "━━━ pnpm store ━━━"
command -v pnpm >/dev/null && pnpm store prune 2>&1 | tail -2 || true
echo ""
echo "━━━ npm cache ━━━"
command -v npm >/dev/null && npm cache clean --force 2>/dev/null && echo " cleaned" || true
echo ""
echo "━━━ uv cache ━━━"
command -v uv >/dev/null && uv cache prune 2>&1 | tail -2 || true
echo ""
echo "━━━ cargo (large build artifacts only, keep registry) ━━━"
command -v cargo-cache >/dev/null && cargo cache --autoclean 2>&1 | tail -2 || echo " (cargo-cache not installed, skip)"
echo ""
echo "━━━ iOS Simulator (unavailable devices) ━━━"
command -v xcrun >/dev/null && xcrun simctl delete unavailable 2>&1 | tail -1 || true
du -sh ~/Library/Developer/CoreSimulator 2>/dev/null | sed 's|^| |' || true
echo ""
echo "━━━ podman (stopped containers + dangling images) ━━━"
# A stopped machine causes connection errors, so skip silently
if command -v podman >/dev/null && podman info >/dev/null 2>&1; then
podman system prune -f 2>&1 | tail -2 || true
else
echo " (podman not available / machine stopped, skip)"
fi
echo ""
echo "━━━ Electron updater leftovers (~/Library/Caches/*.ShipIt) ━━━"
# Leftover downloaded updates from Squirrel.framework. Re-fetched on the next update
sz=$(du -shc ~/Library/Caches/*.ShipIt 2>/dev/null | tail -1 | cut -f1)
echo " total: ${sz:-0}"
rm -rf ~/Library/Caches/*.ShipIt/* 2>/dev/null || true
echo ""
echo "━━━ Repo junk files (.DS_Store / AppleDouble / vim swap etc.) ━━━"
dir="{{justfile_directory()}}"
# Name patterns of junk scattered by macOS / editors / the OS (.git excluded)
names=(
".DS_Store" ".AppleDouble" ".LSOverride" "._*"
".Spotlight-V100" ".Trashes" ".fseventsd" ".DocumentRevisions-V100"
".TemporaryItems" ".apdisk" ".localized"
"Thumbs.db" "Thumbs.db:encryptable" "ehthumbs.db" "ehthumbs_vista.db" "desktop.ini"
"*.swp" "*.swo" "*~" "*.bak" "*.orig"
)
fexpr=()
for n in "${names[@]}"; do fexpr+=( -name "$n" -o ); done
unset 'fexpr[${#fexpr[@]}-1]' # remove the trailing -o
n=$(find "$dir" -path "$dir/.git" -prune -o -type f \( "${fexpr[@]}" \) -print -delete | wc -l | tr -d ' ')
echo " $n removed"
echo ""
echo "━━━ Auto-backups in ~/.config (zellij *.bak etc.) ━━━"
m=$(find ~/.config -maxdepth 3 \( -name '*.bak' -o -name '*.bak.[0-9]*' \) -type f -print -delete 2>/dev/null | wc -l | tr -d ' ')
echo " $m removed"
echo ""
echo "━━━ Dev caches (__pycache__/*.pyc/.pytest_cache etc., excl Library, regenerated) ━━━"
tmp=$(mktemp)
find "$HOME" \( -path "$HOME/Library" -o -name .Trash \) -prune -o -type d \( -name '__pycache__' -o -name '.pytest_cache' -o -name '.mypy_cache' -o -name '.ruff_cache' -o -name '.ipynb_checkpoints' \) -prune -print 2>/dev/null > "$tmp"
pc=$(wc -l < "$tmp" | tr -d ' ')
xargs -I{} rm -rf "{}" < "$tmp" 2>/dev/null; rm -f "$tmp"
py=$(find "$HOME" \( -path "$HOME/Library" -o -name .Trash \) -prune -o -type f -name '*.pyc' -print -delete 2>/dev/null | wc -l | tr -d ' ')
echo " cache dirs: $pc, *.pyc: $py removed"
echo ""
echo "━━━ ~/.cache (uv done, status of other large items) ━━━"
du -sh ~/.cache/*/ 2>/dev/null | sort -hr | head -5
echo ""
echo "━━━ Done ━━━"
df -h / 2>&1 | head -2 | tail -1
# Interactively delete heavy regenerable data (zap of retired casks / CoreSimulator cache / podman / old build artifacts)
[group('Clean')]
gc-deep:
#!/usr/bin/env bash
set -u
echo "━━━ macOS Trash (with confirmation; unrecoverable) ━━━"
sz=$(du -sh "$HOME/.Trash" 2>/dev/null | cut -f1); echo " size: ${sz:-0}"
read -rp " Empty Trash? [y/N] " ans
if [[ "$ans" == [yY] ]]; then
find "$HOME/.Trash" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true
echo " emptied"
else
echo " skipped"
fi
echo ""
echo "━━━ OS/editor junk across HOME (with confirmation) ━━━"
read -rp " Delete .DS_Store/AppleDouble/swap/orig/rej/backup files? [y/N] " ans
if [[ "$ans" == [yY] ]]; then
d=$(find "$HOME" \( -path "$HOME/.Trash" -o -path "$HOME/Library" \) -prune -o -type f \( -name '.DS_Store' -o -name '._*' -o -name '*.swp' -o -name '*.swo' -o -name '*.orig' -o -name '*.rej' -o -name '*~' \) -print -delete 2>/dev/null | wc -l | tr -d ' ')
echo " $d removed"
else
echo " skipped"
fi
echo ""
echo "━━━ Retired GUI cask data (Homebrew zap) ━━━"
# Only pin casks moved to CLI/TUI and dropped from the declaration. --force lets the zap stanza
# (settings, caches, etc.) run even after a normal uninstall. ollama-app is excluded because it deletes
# the models in ~/.ollama, and unity-hub because it may share settings with the Unity CLI.
zap_candidates=(cyberduck wireshark-app syncthing-app picview iina vlc)
printf ' %s\n' "${zap_candidates[@]}"
echo " Removes app settings/caches; Cyberduck bookmarks and Wireshark profiles are included."
echo " Preserved: ~/.ollama models, Syncthing core config, Unity Hub/CLI metadata."
read -rp " Zap all listed cask data? [y/N] " ans
if [[ "$ans" == [yY] ]]; then
brew uninstall --cask --zap --force "${zap_candidates[@]}" || true
else
echo " skipped"
fi
echo ""
echo "━━━ CoreSimulator dyld cache (/Library/Developer/CoreSimulator/Caches) ━━━"
# Cache regenerated when the iOS simulator runs. Left alone it grows past 10GB
sim_cache="/Library/Developer/CoreSimulator/Caches"
if [ -d "$sim_cache" ]; then
sz=$(du -sh "$sim_cache" 2>/dev/null | cut -f1)
echo " size: ${sz:-?} (regenerated on next use)"
read -rp " Delete? (sudo required) [y/N] " ans
if [[ "$ans" == [yY] ]]; then
sudo rm -rf "$sim_cache" && echo " removed ($sz)"
else
echo " skipped"
fi
else
echo " (not found)"
fi
echo ""
echo "━━━ podman full prune (removes all unused images; re-pull needed on next use) ━━━"
if command -v podman >/dev/null && podman info >/dev/null 2>&1; then
podman system df 2>/dev/null | sed 's|^| |'
read -rp " Prune all unused images? [y/N] " ans
if [[ "$ans" == [yY] ]]; then
podman system prune -af 2>&1 | tail -3
else
echo " skipped"
fi
else
echo " (podman not available / machine stopped, skip)"
fi
echo ""
echo "━━━ Scanning node_modules / rust target untouched >30 days (in ~/Developer) ━━━"
tmp=$(mktemp)
# Limit the scan to the dev-repo location (~/Developer = ghq root).
# Targeting all of $HOME would sweep up everyday tools that build in-place under ~/.config etc.
# (e.g. the ghostty launcher's launcher-search/target) as cleanup candidates,
# and an incident occurred where the binary was deleted and the tool could no longer start (2026-07).
scan_root="$HOME/Developer"
if [ ! -d "$scan_root" ]; then echo " ($scan_root not found, skip)"; rm -f "$tmp"; exit 0; fi
# node_modules: prune keeps only one level of nesting; limited to those untouched for 30 days
find "$scan_root" -type d -name node_modules -prune -mtime +30 -print 2>/dev/null >> "$tmp"
# rust target: avoid false hits on generic same-named dirs; only when a Cargo.toml sits alongside
find "$scan_root" -type d -name target -prune -mtime +30 -print 2>/dev/null | \
while read -r d; do [ -f "$(dirname "$d")/Cargo.toml" ] && echo "$d"; done >> "$tmp"
cnt=$(wc -l < "$tmp" | tr -d ' ')
if [ "$cnt" -eq 0 ]; then echo " None (all updated within 30 days)"; rm -f "$tmp"; exit 0; fi
total=$(xargs -I{} du -sk "{}" < "$tmp" 2>/dev/null | awk '{s+=$1}END{printf "%.1fG", s/1024/1024}')
sed "s|$HOME|~|" "$tmp" | head -40
[ "$cnt" -gt 40 ] && echo " … $((cnt-40)) more"
echo " Total: $total / $cnt items (each project needs reinstall after deletion)"
read -rp "Delete? [y/N] " ans
if [[ "$ans" == [yY] ]]; then
xargs -I{} rm -rf "{}" < "$tmp" 2>/dev/null
echo " $cnt removed (reclaimed: $total)"
else
echo " Aborted"
fi
rm -f "$tmp"
# ─────────────────────────────────────────────
# Service (restart / restarting the menu-bar and WM stack)
# ─────────────────────────────────────────────
# NOTE: omniwm full restart = the workspace layout is reset, so only on explicit request (omniwm/all).
# borders config is consolidated in ~/.config/borders/bordersrc; the daemon itself is the
# nix-declared launchd agent (org.nix-community.home.borders), so restart = kickstart.
# Restart the menu-bar/WM stack (`just restart`=bar-related / individual: sketchybar|borders|omniwm / all=everything)
[group('Service')]
restart what="bar":
#!/usr/bin/env bash
set -u
uid=$(id -u)
sb() { echo "-> sketchybar"; launchctl kickstart -k "gui/$uid/homebrew.mxcl.sketchybar"; }
bd() { echo "-> borders"; launchctl kickstart -k "gui/$uid/org.nix-community.home.borders"; }
om() {
echo "-> OmniWM (full restart)"
osascript -e 'quit app "OmniWM"' 2>/dev/null
# Poll up to 4s for quit to finish (a fixed sleep would let open miss when quit is slow)
for _ in $(seq 1 20); do pgrep -xq OmniWM || break; sleep 0.2; done
open -a OmniWM
}
case "{{what}}" in
bar) sb; bd ;;
sketchybar|sb) sb ;;
borders|bd) bd ;;
omniwm|om|wm) om ;;
all) sb; bd; om ;;
*) echo "usage: just restart [bar|sketchybar|borders|omniwm|all]" >&2; exit 2 ;;
esac
echo "Done"
# ─────────────────────────────────────────────
# secrets (sops encryption)
# ─────────────────────────────────────────────
# sops-encrypted secrets (`just secrets` = edit, `just secrets rekey` = re-encrypt for all recipients)
[group('secrets')]
secrets cmd="edit":
#!/usr/bin/env bash
set -euo pipefail
f="{{justfile_directory()}}/secrets/secrets.yaml"
case "{{cmd}}" in
edit) sops "$f" ;; # edit (default)
rekey) sops updatekeys "$f" ;; # run after changing .sops.yaml
*) echo "usage: just secrets [edit|rekey]" >&2; exit 2 ;;
esac
# ─────────────────────────────────────────────
# Setup / misc
# ─────────────────────────────────────────────
# The entry shellHook installs pre-commit into .git/hooks (.pre-commit-config.yaml generation + install).
# install only needs to run once on a new Mac's first time (merged the old pre-commit-install).
# devShell (`just dev`=enter [shellcheck/statix available] / `just dev install`=install hooks only [non-interactive])
[group('Setup')]
dev what="":
#!/usr/bin/env bash
set -eu
case "{{what}}" in
"") exec nix develop {{flake}} ;; # enter the interactive shell
install) nix develop {{flake}} --command true ;; # enter = install hooks only, then exit immediately
*) echo "usage: just dev [install]" >&2; exit 2 ;;
esac
# Use remote-env on another host
[group('Setup')]
ssh host:
nssh {{host}}
# A hand-written command list in the README drifts from the Justfile, so treat `just --list` as the single source and
# regenerate the <!-- BEGIN/END <name> --> blocks in README / CHEATSHEET from config (SSOT).
# Targets: the just recipe list, the pre-commit hook table, the shell alias table (see scripts/gen-docs.py).
# Run this after changing a recipe/hook/alias. CI drift detection is handled by check-generated.sh.
[group('Setup')]
docs:
#!/usr/bin/env bash
set -euo pipefail
cd "{{justfile_directory()}}"
python3 scripts/gen-docs.py
# One-way snapshot of Obsidian config into public dotfiles (tracking-only, vault->dotfiles)
# - Copy only whitelisted safe json. The vault side is authoritative (this is a read-only mirror).
# - plugins/*/data.json (LiveSync's CouchDB creds, various API keys, etc.), workspace, and caches are
# never included by design. If any non-empty secret value is detected, abort and do not push to public.
# - Always run `gitleaks` before commit. For config you want to keep with its secrets, use sops encryption (`just secrets`).
[group('Setup')]
[doc('One-way snapshot of Obsidian config into public dotfiles (tracking-only, vault->dotfiles)')]
obsidian-snapshot:
#!/usr/bin/env bash
set -euo pipefail
src="$HOME/Documents/notes/.obsidian"
dst="{{justfile_directory()}}/configs/apps/obsidian"
[ -d "$src" ] || { echo "vault not found: $src" >&2; exit 1; }
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
# ★Whitelist: only settings confirmed safe (include only safe items rather than excluding dangerous ones)
for f in app.json appearance.json hotkeys.json \
community-plugins.json core-plugins.json \
graph.json daily-notes.json types.json canvas.json; do
[ -f "$src/$f" ] && cp -f "$src/$f" "$tmp/$f"
done
# ★Secret guard: if a non-empty secret value ("key": "value") is detected, abort without pushing to the public repo
if grep -rEil '"(api[_-]?key|secret|token|password|passphrase|access[_-]?key|couchdb_[a-z]+)"[[:space:]]*:[[:space:]]*"[^"]+"' "$tmp"; then
echo "🛑 Secret-like value detected. Aborting snapshot (protecting public)" >&2; exit 1
fi
mkdir -p "$dst"
rsync -a --delete --exclude='README.md' "$tmp/" "$dst/"
echo "✅ snapshot -> $dst"
echo " After git add, run gitleaks before committing."
# Pull GUI-side preference changes back into the repo (live -> dotfiles).
# darwin-apps.nix imports these plists wholesale on activation, so anything changed in an app's
# own UI is silently reverted by the next rebuild unless it is captured here.
# Only keys the repo file already tracks are updated: these apps also store volatile state
# (window frames, status item positions, color panel geometry) that would add churn on every run.
# To start tracking a new key, add it to the repo plist once (`plutil -insert`), then this picks it up.
[group('Setup')]
[doc('Sync GUI app preference changes back into dotfiles (live -> repo)')]
plist-sync:
#!/usr/bin/env bash
set -euo pipefail
dir="{{justfile_directory()}}"
# Same set darwin-apps.nix imports. Add a line here when an import is added there.
pairs=(
"org.p0deje.Maccy configs/clipboard/maccy/Maccy.plist"
"net.mtgto.inputmethod.macSKK configs/ime/skk/macSKK.plist"
"io.github.gitusp.azoo-key-skkserv configs/ime/skk/azoo-key-skkserv.plist"
)
changed=0
for pair in "${pairs[@]}"; do
set -- $pair; domain="$1"; rel="$2"; repo="$dir/$rel"
[ -f "$repo" ] || { echo "– $rel: not in repo, skipped"; continue; }
live=$(defaults export "$domain" - 2>/dev/null | plutil -convert json -o - - 2>/dev/null) || live=""
# An app that was never launched (or a sandbox not yet created) exports an empty dict.
# Overwriting from that would quietly wipe the tracked settings, so skip instead.
if [ -z "$live" ] || [ "$(jq 'keys|length' <<<"$live")" -eq 0 ]; then
echo "– $domain: no live prefs, skipped"; continue
fi
old=$(plutil -convert json -o - "$repo")
# read-loop, not `for`: some keys contain spaces ("NSStatusItem VisibleCC Item-0")
while IFS= read -r key; do
lv=$(jq -c --arg k "$key" '.[$k]' <<<"$live")
rv=$(jq -c --arg k "$key" '.[$k]' <<<"$old")
[ "$lv" = "null" ] && continue # dropped on the live side: keep the tracked value
[ "$lv" = "$rv" ] && continue
plutil -replace "$key" -json "$lv" "$repo"
echo " $rel $key: $rv -> $lv"
changed=1
done < <(jq -r 'keys[]' <<<"$old")
done
if [ "$changed" -eq 0 ]; then
echo "✓ tracked plist keys already match the live prefs"
else
git -C "$dir" --no-pager diff --stat -- configs
echo "Review the diff, then commit on a branch as usual."
fi
# ─────────────────────────────────────────────
# Windows (native pwsh)
# ─────────────────────────────────────────────
# Run the native Windows bootstrap (`just win-bootstrap` / `just win-bootstrap -DryRun`)
[group('Windows')]
win-bootstrap *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/bootstrap.ps1 {{flags}}
# Verify every PackageIdentifier in winget/apps.json exists (`just win-verify` / `just win-verify -Strict`)
[group('Windows')]
win-verify *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/winget/verify.ps1 {{flags}}
# Diff between apps.json (declaration) and winget list (actual install). exit 1 if any MISSING
[group('Windows')]
win-status *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/winget/status.ps1 {{flags}}
# Upgrade every app installed via winget (--silent --accept-*)
[group('Windows')]
win-upgrade:
pwsh.exe -NoProfile -Command "winget upgrade --all --silent --accept-package-agreements --accept-source-agreements"
# Declaratively apply telemetry/built-in feature settings (Win11Debloat + WinUtil)
# Pass `-DryRun` `-SkipWinUtil` `-SkipWin11Debloat` via `*flags`
[group('Windows')]
win-privacy *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/privacy/apply.ps1 {{flags}}
# Declaratively apply Scoop buckets + apps (for sideloading MS Store-only apps)
# Pass `-DryRun` `-SkipBuckets` `-SkipApps` via `*flags`
[group('Windows')]
win-scoop *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/scoop/apply.ps1 {{flags}}
# Declaratively apply locale / language (en-US UI / UTF-8 / SKK only / US Region)
# Pass `-DryRun` `-SkipLanguageList` `-SkipSystemLocale` `-SkipHomeLocation` via `*flags`
[group('Windows')]
win-locale *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/locale/apply.ps1 {{flags}}
# User-scope install of .ttf / .otf from configs/fonts/ (HKCU registration, no admin required)
# Pass `-DryRun` `-Force` (overwrite existing too) via `*flags`
[group('Windows')]
win-fonts *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/fonts/apply.ps1 {{flags}}
# Render the theme (palette) from palettes.json into each config
# Pass `-DryRun` `-ActivePalette rose-pine-dawn` etc. via `*flags`
[group('Windows')]
win-theme *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/theme/apply.ps1 {{flags}}
# ─────────────────────────────────────────────
# Theme (cross-OS, unified switching with palettes.json as SSO)
# ─────────────────────────────────────────────
# Render all environments with the current active in palettes.json (can also switch active via arg)
# `just theme` = re-apply all environments with the current active
# `just theme rose-pine-dawn` = switch to light and render + rebuild all environments
# NOTE: shebang recipes do not work because Windows' just requires cygpath, so
# this cross-OS recipe dispatches immediately and puts the bash logic in _theme-unix.
[group('Theme')]
[doc('Render all environments with the current active in palettes.json (`just theme rose-pine-dawn` also switches active)')]
theme name="":
@just _theme-{{os()}} "{{name}}"
[private]
_theme-macos name="": (_theme-unix name)
[private]
_theme-linux name="": (_theme-unix name)
# On both Mac/WSL, nh home switch makes nvim/zellij/sketchybar/bat/atuin etc. all follow
[private]
_theme-unix name="":
#!/usr/bin/env bash
set -euo pipefail
if [ -n "{{name}}" ]; then
# Rewrite the active field in palettes.json
sed -i.bak 's/"active": *"[^"]*"/"active": "{{name}}"/' configs/theme/palettes.json
rm -f configs/theme/palettes.json.bak
echo "→ palettes.json active = {{name}}"
fi
nh home switch
# Write the active back here rather than in apply.ps1 (paired with the unix-side sed; UTF-8 without BOM).
# The actual render is win-theme (renders zebar / glazewm / WT / wezterm in one go)
[private]
_theme-windows name="":
@if ('{{name}}' -ne '') { $p = Resolve-Path 'configs/theme/palettes.json'; $c = (Get-Content $p -Raw -Encoding UTF8) -replace '"active":\s*"[^"]*"', '"active": "{{name}}"'; [System.IO.File]::WriteAllText($p, $c, [System.Text.UTF8Encoding]::new($false)); Write-Host ('-> palettes.json active = {{name}}') }
@just win-theme
# Apply keymap (SharpKeys = write Scancode Map directly + reload AHK script)
# Pass `-DryRun` `-Clear` (delete Scancode Map and return to standard) via `*flags`
[group('Windows')]
win-keymap *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/sharpkeys/apply.ps1 {{flags}}
pwsh.exe -NoProfile -Command "Get-Process AutoHotkey* -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Process 'windows/autohotkey/keymap.ahk' -ErrorAction SilentlyContinue"
# Register GlazeWM login auto-start in Task Scheduler (password entered once)
# Pass `-Unregister` (delete the task) via `*flags`
[group('Windows')]
win-autostart-glazewm *flags:
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File windows/tasks/setup-glazewm-autostart.ps1 {{flags}}
# Lint Windows-related .ps1 with PSScriptAnalyzer (exit 1 on Warning or above)
[group('Windows')]
win-fmt:
pwsh.exe -NoProfile -Command "if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) { Install-Module PSScriptAnalyzer -Force -Scope CurrentUser }; Invoke-ScriptAnalyzer -Path windows -Recurse -Severity Warning -EnableExit -Settings windows/PSScriptAnalyzerSettings.psd1"
# ─────────────────────────────────────────────
# Backup / archive (a single restic repository: google-drive:restic-backup, encrypted)
# warm = untagged, daily automatic (restic-backup.nix) / cold = --tag archive, manual, kept forever.
# Dedup, integrity verification, and key are shared. The shared ~/Cloud/GoogleDrive mount ops are consolidated here too.
# ─────────────────────────────────────────────
# restic environment (shared by warm/cold). The values' only source is ~/.config/restic/env generated by nix
# (nix/lib/restic-common.nix). RESTIC_ARCHIVE_TAG also comes from here.
restic_env := 'source "$HOME/.config/restic/env"'