dendritic migration
dendritic migration
This commit is contained in:
124
modules/_darwin/dock.nix
Normal file
124
modules/_darwin/dock.nix
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.local.dock;
|
||||
inherit (pkgs) stdenv dockutil;
|
||||
in {
|
||||
options = {
|
||||
local.dock = {
|
||||
enable =
|
||||
mkOption {
|
||||
description = "Enable dock";
|
||||
default = stdenv.isDarwin;
|
||||
example = false;
|
||||
};
|
||||
|
||||
entries =
|
||||
mkOption {
|
||||
description = "Entries on the Dock";
|
||||
type = with types;
|
||||
listOf (submodule {
|
||||
options = {
|
||||
path = lib.mkOption {type = str;};
|
||||
section =
|
||||
lib.mkOption {
|
||||
type = str;
|
||||
default = "apps";
|
||||
};
|
||||
options =
|
||||
lib.mkOption {
|
||||
type = str;
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [
|
||||
{path = "/Applications/Helium.app/";}
|
||||
{path = "/Applications/Ghostty.app/";}
|
||||
{path = "/System/Applications/Calendar.app/";}
|
||||
{path = "/System/Applications/Mail.app/";}
|
||||
{path = "/System/Applications/Notes.app/";}
|
||||
{path = "/System/Applications/Music.app/";}
|
||||
{path = "/System/Applications/System Settings.app/";}
|
||||
{
|
||||
path = "${config.users.users.cschmatzler.home}/Downloads";
|
||||
section = "others";
|
||||
options = "--sort name --view grid --display stack";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
username =
|
||||
mkOption {
|
||||
description = "Username to apply the dock settings to";
|
||||
type = types.str;
|
||||
default = "cschmatzler";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
mkIf cfg.enable (
|
||||
let
|
||||
normalize = path:
|
||||
if hasSuffix ".app" path
|
||||
then path + "/"
|
||||
else path;
|
||||
entryURI = path:
|
||||
"file://"
|
||||
+ (
|
||||
builtins.replaceStrings
|
||||
[
|
||||
" "
|
||||
"!"
|
||||
"\""
|
||||
"#"
|
||||
"$"
|
||||
"%"
|
||||
"&"
|
||||
"'"
|
||||
"("
|
||||
")"
|
||||
]
|
||||
[
|
||||
"%20"
|
||||
"%21"
|
||||
"%22"
|
||||
"%23"
|
||||
"%24"
|
||||
"%25"
|
||||
"%26"
|
||||
"%27"
|
||||
"%28"
|
||||
"%29"
|
||||
]
|
||||
(normalize path)
|
||||
);
|
||||
wantURIs = concatMapStrings (entry: "${entryURI entry.path}\n") cfg.entries;
|
||||
createEntries =
|
||||
concatMapStrings (
|
||||
entry: "${dockutil}/bin/dockutil --no-restart --add '${entry.path}' --section ${entry.section} ${entry.options}\n"
|
||||
)
|
||||
cfg.entries;
|
||||
in {
|
||||
system.activationScripts.postActivation.text = ''
|
||||
echo >&2 "Setting up the Dock for ${cfg.username}..."
|
||||
su ${cfg.username} -s /bin/sh <<-'USERBLOCK'
|
||||
haveURIs="$(${dockutil}/bin/dockutil --list | ${pkgs.coreutils}/bin/cut -f2)"
|
||||
if ! diff -wu <(echo -n "$haveURIs") <(echo -n '${wantURIs}') >&2 ; then
|
||||
echo >&2 "Resetting Dock."
|
||||
${dockutil}/bin/dockutil --no-restart --remove all
|
||||
${createEntries}
|
||||
killall Dock
|
||||
else
|
||||
echo >&2 "Dock setup complete."
|
||||
fi
|
||||
USERBLOCK
|
||||
'';
|
||||
}
|
||||
);
|
||||
}
|
||||
37
modules/_hosts/michael/disk-config.nix
Normal file
37
modules/_hosts/michael/disk-config.nix
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
disko.devices = {
|
||||
disk = {
|
||||
main = {
|
||||
type = "disk";
|
||||
device = "/dev/sda";
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
boot = {
|
||||
size = "1M";
|
||||
type = "EF02";
|
||||
};
|
||||
ESP = {
|
||||
size = "512M";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = ["umask=0077"];
|
||||
};
|
||||
};
|
||||
root = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
18
modules/_hosts/michael/hardware-configuration.nix
Normal file
18
modules/_hosts/michael/hardware-configuration.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
lib,
|
||||
modulesPath,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
||||
boot.initrd.availableKernelModules = ["ahci" "xhci_pci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod"];
|
||||
boot.initrd.kernelModules = [];
|
||||
boot.kernelModules = [];
|
||||
boot.extraModulePackages = [];
|
||||
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
|
||||
networking.useDHCP = lib.mkDefault true;
|
||||
}
|
||||
60
modules/_hosts/tahani/adguardhome.nix
Normal file
60
modules/_hosts/tahani/adguardhome.nix
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
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;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
10
modules/_hosts/tahani/cache.nix
Normal file
10
modules/_hosts/tahani/cache.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
{...}: {
|
||||
services.caddy.virtualHosts."cache.manticore-hippocampus.ts.net" = {
|
||||
extraConfig = ''
|
||||
tls {
|
||||
get_certificate tailscale
|
||||
}
|
||||
reverse_proxy localhost:32843
|
||||
'';
|
||||
};
|
||||
}
|
||||
38
modules/_hosts/tahani/networking.nix
Normal file
38
modules/_hosts/tahani/networking.nix
Normal file
@@ -0,0 +1,38 @@
|
||||
{config, ...}: {
|
||||
services.tailscale.extraSetFlags = ["--accept-routes=false"];
|
||||
|
||||
networking = {
|
||||
useDHCP = false;
|
||||
interfaces.eno1.ipv4.addresses = [
|
||||
{
|
||||
address = "192.168.1.10";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
defaultGateway = "192.168.1.1";
|
||||
nameservers = ["1.1.1.1"];
|
||||
firewall = {
|
||||
enable = true;
|
||||
trustedInterfaces = ["eno1" "tailscale0"];
|
||||
allowedUDPPorts = [
|
||||
53
|
||||
config.services.tailscale.port
|
||||
];
|
||||
allowedTCPPorts = [
|
||||
22
|
||||
53
|
||||
];
|
||||
checkReversePath = "loose";
|
||||
};
|
||||
};
|
||||
|
||||
fileSystems."/" = {
|
||||
device = "/dev/disk/by-label/NIXROOT";
|
||||
fsType = "ext4";
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-label/NIXBOOT";
|
||||
fsType = "vfat";
|
||||
};
|
||||
}
|
||||
73
modules/_hosts/tahani/paperless.nix
Normal file
73
modules/_hosts/tahani/paperless.nix
Normal file
@@ -0,0 +1,73 @@
|
||||
{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";
|
||||
};
|
||||
};
|
||||
}
|
||||
20
modules/_lib/build-rust-package.nix
Normal file
20
modules/_lib/build-rust-package.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
inputs,
|
||||
input,
|
||||
prev,
|
||||
}: let
|
||||
naersk-lib = prev.callPackage inputs.naersk {};
|
||||
manifest = (prev.lib.importTOML "${input}/Cargo.toml").package;
|
||||
in
|
||||
naersk-lib.buildPackage {
|
||||
pname = manifest.name;
|
||||
version = manifest.version;
|
||||
|
||||
src = input;
|
||||
|
||||
nativeBuildInputs = [prev.pkg-config];
|
||||
buildInputs = [prev.openssl];
|
||||
OPENSSL_NO_VENDOR = 1;
|
||||
|
||||
doCheck = false;
|
||||
}
|
||||
14
modules/_lib/constants.nix
Normal file
14
modules/_lib/constants.nix
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
user = "cschmatzler";
|
||||
|
||||
sshKeys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/"
|
||||
];
|
||||
|
||||
stateVersions = {
|
||||
darwin = 6;
|
||||
nixos = "25.11";
|
||||
homeManager = "25.11";
|
||||
};
|
||||
}
|
||||
10
modules/_lib/open-project.nix
Normal file
10
modules/_lib/open-project.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
{pkgs}:
|
||||
pkgs.writeShellScriptBin "open-project" ''
|
||||
TARGET=$(fd -t d --exact-depth 1 . $HOME/Projects |
|
||||
sed "s~$HOME/Projects/~~" |
|
||||
fzf --prompt "project > ")
|
||||
|
||||
if [ -n "$TARGET" ]; then
|
||||
echo "$HOME/Projects/$TARGET"
|
||||
fi
|
||||
''
|
||||
66
modules/_lib/packages.nix
Normal file
66
modules/_lib/packages.nix
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with pkgs;
|
||||
[
|
||||
(callPackage ./open-project.nix {})
|
||||
age
|
||||
alejandra
|
||||
ast-grep
|
||||
bun
|
||||
delta
|
||||
devenv
|
||||
dig
|
||||
docker
|
||||
docker-compose
|
||||
dust
|
||||
fastfetch
|
||||
fd
|
||||
gh
|
||||
git
|
||||
glow
|
||||
gnumake
|
||||
gnupg
|
||||
hledger
|
||||
htop
|
||||
hyperfine
|
||||
jj-ryu
|
||||
jj-starship
|
||||
jq
|
||||
killall
|
||||
lsof
|
||||
nodejs_24
|
||||
nurl
|
||||
openssh
|
||||
ouch
|
||||
ov
|
||||
pnpm
|
||||
postgresql_17
|
||||
sd
|
||||
serie
|
||||
sops
|
||||
sqlite
|
||||
tea
|
||||
tokei
|
||||
tree
|
||||
tree-sitter
|
||||
tuicr
|
||||
vivid
|
||||
]
|
||||
++ lib.optionals stdenv.isDarwin [
|
||||
_1password-gui
|
||||
alcove
|
||||
dockutil
|
||||
mas
|
||||
raycast
|
||||
tailscale
|
||||
xcodes
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux [
|
||||
gcc15
|
||||
ghostty.terminfo
|
||||
lm_sensors
|
||||
]
|
||||
11
modules/_lib/wallpaper.nix
Normal file
11
modules/_lib/wallpaper.nix
Normal file
@@ -0,0 +1,11 @@
|
||||
{pkgs}: let
|
||||
wallpaper =
|
||||
pkgs.fetchurl {
|
||||
url = "https://misc-assets.raycast.com/wallpapers/bright-rain.png";
|
||||
sha256 = "sha256-wQT4I2X3gS6QFsEb7MdRsn4oX7FNkflukXPGMFbJZ10=";
|
||||
};
|
||||
in
|
||||
pkgs.writeShellScriptBin "set-wallpaper-script" ''
|
||||
set -e
|
||||
/usr/bin/osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"${wallpaper}\""
|
||||
''
|
||||
34
modules/_neovim/autocmd.nix
Normal file
34
modules/_neovim/autocmd.nix
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
programs.nixvim = {
|
||||
autoGroups = {
|
||||
Christoph = {};
|
||||
};
|
||||
|
||||
autoCmd = [
|
||||
{
|
||||
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 = "*.ex,*.exs,*.heex";
|
||||
command = "setlocal expandtab tabstop=2 shiftwidth=2 softtabstop=2";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
38
modules/_neovim/default.nix
Normal file
38
modules/_neovim/default.nix
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
imports = [
|
||||
./autocmd.nix
|
||||
./mappings.nix
|
||||
./options.nix
|
||||
./plugins/blink-cmp.nix
|
||||
./plugins/conform.nix
|
||||
./plugins/grug-far.nix
|
||||
./plugins/harpoon.nix
|
||||
./plugins/hunk.nix
|
||||
./plugins/jj-diffconflicts.nix
|
||||
./plugins/lsp.nix
|
||||
./plugins/mini.nix
|
||||
./plugins/oil.nix
|
||||
./plugins/toggleterm.nix
|
||||
./plugins/treesitter.nix
|
||||
./plugins/zk.nix
|
||||
];
|
||||
|
||||
programs.nixvim = {
|
||||
enable = true;
|
||||
defaultEditor = true;
|
||||
luaLoader.enable = true;
|
||||
colorschemes.catppuccin = {
|
||||
enable = true;
|
||||
settings = {
|
||||
flavour = "latte";
|
||||
};
|
||||
};
|
||||
extraConfigLua = ''
|
||||
vim.ui.select = MiniPick.ui_select
|
||||
'';
|
||||
};
|
||||
|
||||
home.shellAliases = {
|
||||
v = "nvim";
|
||||
};
|
||||
}
|
||||
297
modules/_neovim/mappings.nix
Normal file
297
modules/_neovim/mappings.nix
Normal file
@@ -0,0 +1,297 @@
|
||||
{
|
||||
programs.nixvim.keymaps = [
|
||||
# clipboard - OSC52 yank and paste
|
||||
{
|
||||
mode = ["n" "v"];
|
||||
key = "<leader>y";
|
||||
action = ''"+y'';
|
||||
options.desc = "Yank to system clipboard (OSC52)";
|
||||
}
|
||||
# e - explore/edit
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>ef";
|
||||
action = ":lua require('oil').open()<CR>";
|
||||
options.desc = "File directory";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>er";
|
||||
action = ":lua require('grug-far').open()<CR>";
|
||||
options.desc = "Search and replace";
|
||||
}
|
||||
# f - find
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>f/";
|
||||
action = ":Pick history scope='/'<CR>";
|
||||
options.desc = "'/' history";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>f:";
|
||||
action = ":Pick history scope=':'<CR>";
|
||||
options.desc = "':' history";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fa";
|
||||
action = ":Pick git_hunks scope='staged'<CR>";
|
||||
options.desc = "Added hunks (all)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fA";
|
||||
action = ":Pick git_hunks path='%' scope='staged'<CR>";
|
||||
options.desc = "Added hunks (buffer)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fb";
|
||||
action = ":Pick buffers<CR>";
|
||||
options.desc = "Buffers";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fd";
|
||||
action = ":Pick diagnostic scope='all'<CR>";
|
||||
options.desc = "Diagnostic (workspace)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fD";
|
||||
action = ":Pick diagnostic scope='current'<CR>";
|
||||
options.desc = "Diagnostic (buffer)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>ff";
|
||||
action = ":Pick files<CR>";
|
||||
options.desc = "Search files";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fg";
|
||||
action = ":Pick grep_live<CR>";
|
||||
options.desc = "Grep";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fm";
|
||||
action = ":Pick git_hunks<CR>";
|
||||
options.desc = "Modified hunks (all)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fM";
|
||||
action = ":Pick git_hunks path='%'<CR>";
|
||||
options.desc = "Modified hunks (buffer)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fr";
|
||||
action = ":Pick lsp scope='references'<CR>";
|
||||
options.desc = "References (LSP)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fs";
|
||||
action = ":Pick lsp scope='workspace_symbol'<CR>";
|
||||
options.desc = "Symbols (LSP, workspace)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fS";
|
||||
action = ":Pick lsp scope='document_symbol'<CR>";
|
||||
options.desc = "Symbols (LSP, buffer)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fv";
|
||||
action = ":Pick visit_paths cwd=\"\"<CR>";
|
||||
options.desc = "Visit paths (all)";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>fV";
|
||||
action = ":Pick visit_paths<CR>";
|
||||
options.desc = "Visit paths (cwd)";
|
||||
}
|
||||
# g - git
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>gc";
|
||||
action = ":JJDiffConflicts<CR>";
|
||||
options.desc = "Resolve conflicts";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>gg";
|
||||
action.__raw = ''
|
||||
function()
|
||||
require('toggleterm.terminal').Terminal:new({ cmd = 'jjui', direction = 'float' }):toggle()
|
||||
end
|
||||
'';
|
||||
options.desc = "jjui";
|
||||
}
|
||||
# l - lsp/formatter
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>la";
|
||||
action = ":lua vim.lsp.buf.code_action()<CR>";
|
||||
options.desc = "Actions";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>ld";
|
||||
action = ":lua vim.diagnostic.open_float({ severity = { min = vim.diagnostic.severity.HINT } })<CR>";
|
||||
options.desc = "Diagnostics popup";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>lf";
|
||||
action = ":lua require('conform').format({ lsp_fallback = true })<CR>";
|
||||
options.desc = "Format";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>li";
|
||||
action = ":lua vim.lsp.buf.hover()<CR>";
|
||||
options.desc = "Information";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>lj";
|
||||
action = ":lua vim.diagnostic.goto_next()<CR>";
|
||||
options.desc = "Next diagnostic";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>lk";
|
||||
action = ":lua vim.diagnostic.goto_prev()<CR>";
|
||||
options.desc = "Prev diagnostic";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>lr";
|
||||
action = ":lua vim.lsp.buf.rename()<CR>";
|
||||
options.desc = "Rename";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>lR";
|
||||
action = ":lua vim.lsp.buf.references()<CR>";
|
||||
options.desc = "References";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>ls";
|
||||
action = ":lua vim.lsp.buf.definition()<CR>";
|
||||
options.desc = "Source definition";
|
||||
}
|
||||
# other
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>j";
|
||||
action = ":lua require('mini.jump2d').start(require('mini.jump2d').builtin_opts.query)<CR>";
|
||||
options.desc = "Jump to character";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>a";
|
||||
action = ":lua require('harpoon'):list():add()<CR>";
|
||||
options.desc = "Add harpoon";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<C-e>";
|
||||
action = ":lua require('harpoon').ui:toggle_quick_menu(require('harpoon'):list())<CR>";
|
||||
options.desc = "Toggle harpoon quick menu";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>1";
|
||||
action = ":lua require('harpoon'):list():select(1)<CR>";
|
||||
options.desc = "Go to harpoon 1";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>2";
|
||||
action = ":lua require('harpoon'):list():select(2)<CR>";
|
||||
options.desc = "Go to harpoon 2";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>3";
|
||||
action = ":lua require('harpoon'):list():select(3)<CR>";
|
||||
options.desc = "Go to harpoon 3";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>4";
|
||||
action = ":lua require('harpoon'):list():select(4)<CR>";
|
||||
options.desc = "Go to harpoon 4";
|
||||
}
|
||||
# z - zk (notes)
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>zn";
|
||||
action = ":ZkNew { title = vim.fn.input('Title: ') }<CR>";
|
||||
options.desc = "New note";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>zo";
|
||||
action = ":ZkNotes { sort = { 'modified' } }<CR>";
|
||||
options.desc = "Open notes";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>zt";
|
||||
action = ":ZkTags<CR>";
|
||||
options.desc = "Browse tags";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>zf";
|
||||
action = ":ZkNotes { sort = { 'modified' }, match = { vim.fn.input('Search: ') } }<CR>";
|
||||
options.desc = "Find notes";
|
||||
}
|
||||
{
|
||||
mode = "v";
|
||||
key = "<leader>zf";
|
||||
action = ":'<,'>ZkMatch<CR>";
|
||||
options.desc = "Find notes matching selection";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>zb";
|
||||
action = ":ZkBacklinks<CR>";
|
||||
options.desc = "Backlinks";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>zl";
|
||||
action = ":ZkLinks<CR>";
|
||||
options.desc = "Outbound links";
|
||||
}
|
||||
{
|
||||
mode = "n";
|
||||
key = "<leader>zi";
|
||||
action = ":ZkInsertLink<CR>";
|
||||
options.desc = "Insert link";
|
||||
}
|
||||
{
|
||||
mode = "v";
|
||||
key = "<leader>zi";
|
||||
action = ":'<,'>ZkInsertLinkAtSelection<CR>";
|
||||
options.desc = "Insert link at selection";
|
||||
}
|
||||
{
|
||||
mode = "v";
|
||||
key = "<leader>zc";
|
||||
action = ":'<,'>ZkNewFromTitleSelection<CR>";
|
||||
options.desc = "Create note from selection";
|
||||
}
|
||||
];
|
||||
}
|
||||
17
modules/_neovim/options.nix
Normal file
17
modules/_neovim/options.nix
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
programs.nixvim = {
|
||||
globals = {
|
||||
clipboard = "osc52";
|
||||
};
|
||||
opts = {
|
||||
expandtab = false;
|
||||
tabstop = 2;
|
||||
ignorecase = true;
|
||||
list = false;
|
||||
mouse = "";
|
||||
relativenumber = true;
|
||||
shiftwidth = 2;
|
||||
smartcase = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
17
modules/_neovim/plugins/blink-cmp.nix
Normal file
17
modules/_neovim/plugins/blink-cmp.nix
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
programs.nixvim.plugins.blink-cmp = {
|
||||
enable = true;
|
||||
settings = {
|
||||
signature.enabled = true;
|
||||
completion = {
|
||||
accept = {
|
||||
auto_brackets = {
|
||||
enabled = true;
|
||||
semantic_token_resolution.enabled = false;
|
||||
};
|
||||
};
|
||||
documentation.auto_show = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
14
modules/_neovim/plugins/conform.nix
Normal file
14
modules/_neovim/plugins/conform.nix
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
programs.nixvim.plugins.conform-nvim = {
|
||||
enable = true;
|
||||
settings = {
|
||||
format_on_save = {};
|
||||
formatters_by_ft = {
|
||||
nix = ["alejandra"];
|
||||
javascript = ["prettier"];
|
||||
typescript = ["prettier"];
|
||||
vue = ["prettier"];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
7
modules/_neovim/plugins/grug-far.nix
Normal file
7
modules/_neovim/plugins/grug-far.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
programs.nixvim.plugins = {
|
||||
grug-far = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
7
modules/_neovim/plugins/harpoon.nix
Normal file
7
modules/_neovim/plugins/harpoon.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
programs.nixvim.plugins = {
|
||||
harpoon = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
7
modules/_neovim/plugins/hunk.nix
Normal file
7
modules/_neovim/plugins/hunk.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
programs.nixvim.plugins = {
|
||||
hunk = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
14
modules/_neovim/plugins/jj-diffconflicts.nix
Normal file
14
modules/_neovim/plugins/jj-diffconflicts.nix
Normal file
@@ -0,0 +1,14 @@
|
||||
{pkgs, ...}: {
|
||||
programs.nixvim.extraPlugins = [
|
||||
(pkgs.vimUtils.buildVimPlugin {
|
||||
name = "jj-diffconflicts";
|
||||
src =
|
||||
pkgs.fetchFromGitHub {
|
||||
owner = "rafikdraoui";
|
||||
repo = "jj-diffconflicts";
|
||||
rev = "main";
|
||||
hash = "sha256-nzjRWHrE2jIcaDoPbixzpvflrtLhPZrihOEQWwqqU0s=";
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
17
modules/_neovim/plugins/lsp.nix
Normal file
17
modules/_neovim/plugins/lsp.nix
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
programs.nixvim.plugins = {
|
||||
lsp = {
|
||||
enable = true;
|
||||
inlayHints = true;
|
||||
servers = {
|
||||
cssls.enable = true;
|
||||
dockerls.enable = true;
|
||||
jsonls.enable = true;
|
||||
nil_ls.enable = true;
|
||||
vtsls.enable = true;
|
||||
yamlls.enable = true;
|
||||
zk.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
158
modules/_neovim/plugins/mini.nix
Normal file
158
modules/_neovim/plugins/mini.nix
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
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>g', desc = '+Git' },
|
||||
{ mode = 'n', keys = '<Leader>l', desc = '+LSP' },
|
||||
{ mode = 'x', keys = '<Leader>l', desc = '+LSP' },
|
||||
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 = {};
|
||||
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 = {};
|
||||
pairs = {};
|
||||
pick = {};
|
||||
starter = {};
|
||||
statusline = {};
|
||||
surround = {};
|
||||
trailspace = {};
|
||||
visits = {};
|
||||
};
|
||||
mockDevIcons = true;
|
||||
};
|
||||
}
|
||||
27
modules/_neovim/plugins/oil.nix
Normal file
27
modules/_neovim/plugins/oil.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
programs.nixvim.plugins.oil = {
|
||||
enable = true;
|
||||
settings = {
|
||||
keymaps = {
|
||||
"<C-r>" = "actions.refresh";
|
||||
"<leader>qq" = "actions.close";
|
||||
};
|
||||
skip_confirm_for_simple_edits = true;
|
||||
constrain_cursor = "editable";
|
||||
default_file_explorer = true;
|
||||
view_options = {
|
||||
show_hidden = true;
|
||||
};
|
||||
win_options = {
|
||||
concealcursor = "ncv";
|
||||
conceallevel = 3;
|
||||
cursorcolumn = false;
|
||||
foldcolumn = "0";
|
||||
list = false;
|
||||
signcolumn = "no";
|
||||
spell = false;
|
||||
wrap = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
20
modules/_neovim/plugins/toggleterm.nix
Normal file
20
modules/_neovim/plugins/toggleterm.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
programs.nixvim.plugins.toggleterm = {
|
||||
enable = true;
|
||||
settings = {
|
||||
open_mapping = null;
|
||||
direction = "float";
|
||||
float_opts = {
|
||||
border = "curved";
|
||||
winblend = 3;
|
||||
};
|
||||
size = 20;
|
||||
hide_numbers = true;
|
||||
shade_terminals = true;
|
||||
shading_factor = 2;
|
||||
start_in_insert = true;
|
||||
close_on_exit = true;
|
||||
shell = "fish";
|
||||
};
|
||||
};
|
||||
}
|
||||
40
modules/_neovim/plugins/treesitter.nix
Normal file
40
modules/_neovim/plugins/treesitter.nix
Normal file
@@ -0,0 +1,40 @@
|
||||
{pkgs, ...}: {
|
||||
programs.nixvim = {
|
||||
plugins.treesitter = {
|
||||
enable = true;
|
||||
nixGrammars = true;
|
||||
grammarPackages = pkgs.vimPlugins.nvim-treesitter.allGrammars;
|
||||
settings = {
|
||||
highlight.enable = true;
|
||||
indent.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Register missing treesitter predicates for compatibility with newer grammars
|
||||
extraConfigLuaPre = ''
|
||||
do
|
||||
local query = require("vim.treesitter.query")
|
||||
local predicates = query.list_predicates()
|
||||
if not vim.tbl_contains(predicates, "is-not?") then
|
||||
query.add_predicate("is-not?", function(match, pattern, source, predicate)
|
||||
local dominated_by = predicate[2]
|
||||
local dominated = false
|
||||
for _, node in pairs(match) do
|
||||
if type(node) == "userdata" then
|
||||
local current = node:parent()
|
||||
while current do
|
||||
if current:type() == dominated_by then
|
||||
dominated = true
|
||||
break
|
||||
end
|
||||
current = current:parent()
|
||||
end
|
||||
end
|
||||
end
|
||||
return not dominated
|
||||
end, { force = true, all = true })
|
||||
end
|
||||
end
|
||||
'';
|
||||
};
|
||||
}
|
||||
6
modules/_neovim/plugins/zk.nix
Normal file
6
modules/_neovim/plugins/zk.nix
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
programs.nixvim.plugins.zk = {
|
||||
enable = true;
|
||||
settings = {};
|
||||
};
|
||||
}
|
||||
11
modules/_opencode/AGENTS.md
Normal file
11
modules/_opencode/AGENTS.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Global AGENTS.md
|
||||
|
||||
## Version Control
|
||||
|
||||
- Use `jj` for VCS, not `git`
|
||||
- `jj tug` is an alias for `jj bookmark move --from closest_bookmark(@-) --to @-`
|
||||
|
||||
## Scripting
|
||||
|
||||
- Always use Nushell (`nu`) for scripting
|
||||
- Never use Python, Perl, Lua, awk, or any other scripting language
|
||||
45
modules/_opencode/agent/review.md
Normal file
45
modules/_opencode/agent/review.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
description: Reviews code for quality, bugs, security, and best practices
|
||||
mode: subagent
|
||||
temperature: 0.1
|
||||
tools:
|
||||
write: false
|
||||
edit: false
|
||||
permission:
|
||||
edit: deny
|
||||
webfetch: allow
|
||||
---
|
||||
You are a code reviewer. Provide actionable feedback on code changes.
|
||||
|
||||
**Diffs alone are not enough.** Read the full file(s) being modified to understand context. Code that looks wrong in isolation may be correct given surrounding logic.
|
||||
|
||||
## What to Look For
|
||||
|
||||
**Bugs** — Primary focus.
|
||||
- Logic errors, off-by-one mistakes, incorrect conditionals
|
||||
- Missing guards, unreachable code paths, broken error handling
|
||||
- Edge cases: null/empty inputs, race conditions
|
||||
- Security: injection, auth bypass, data exposure
|
||||
|
||||
**Structure** — Does the code fit the codebase?
|
||||
- Follows existing patterns and conventions?
|
||||
- Uses established abstractions?
|
||||
- Excessive nesting that could be flattened?
|
||||
|
||||
**Performance** — Only flag if obviously problematic.
|
||||
- O(n²) on unbounded data, N+1 queries, blocking I/O on hot paths
|
||||
|
||||
## Before You Flag Something
|
||||
|
||||
- **Be certain.** Don't flag something as a bug if you're unsure — investigate first.
|
||||
- **Don't invent hypothetical problems.** If an edge case matters, explain the realistic scenario.
|
||||
- **Don't be a zealot about style.** Some "violations" are acceptable when they're the simplest option.
|
||||
- Only review the changes — not pre-existing code that wasn't modified.
|
||||
|
||||
## Output
|
||||
|
||||
- Be direct about bugs and why they're bugs
|
||||
- Communicate severity honestly — don't overstate
|
||||
- Include file paths and line numbers
|
||||
- Suggest fixes when appropriate
|
||||
- Matter-of-fact tone, no flattery
|
||||
49
modules/_opencode/command/albanian-lesson.md
Normal file
49
modules/_opencode/command/albanian-lesson.md
Normal 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.
|
||||
10
modules/_opencode/command/code-review.md
Normal file
10
modules/_opencode/command/code-review.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: Review changes with parallel @code-review subagents
|
||||
---
|
||||
Review the code changes using THREE (3) @code-review subagents and correlate results into a summary ranked by severity. Use the provided user guidance to steer the review and focus on specific code paths, changes, and/or areas of concern. Once all three @code-review subagents return their findings and you have correlated and summarized the results, consult the @oracle subagent to perform a deep review on the findings focusing on accuracy and correctness by evaluating the surrounding code, system, subsystems, abstractions, and overall architecture of each item. Apply any recommendations from the oracle. NEVER SKIP ORACLE REVIEW.
|
||||
|
||||
Guidance: $ARGUMENTS
|
||||
|
||||
First, call `skill({ name: 'vcs-detect' })` to determine whether the repo uses git or jj, then use the appropriate VCS commands throughout.
|
||||
|
||||
Review uncommitted changes by default. If no uncommitted changes, review the last commit. If the user provides a pull request/merge request number or link, use CLI tools (gh/glab) to fetch it and then perform your review.
|
||||
81
modules/_opencode/command/inbox-triage.md
Normal file
81
modules/_opencode/command/inbox-triage.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
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.
|
||||
|
||||
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, inspect attachments: `himalaya attachment download -f "<source>" <id> --dir /tmp/himalaya-triage`.
|
||||
- If attachments are downloaded, inspect them and `rm` the downloaded files from `/tmp/himalaya-triage` after use.
|
||||
- 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>"`.
|
||||
|
||||
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,
|
||||
- short rationale for non-obvious classifications.
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
17
modules/_opencode/command/plan-spec.md
Normal file
17
modules/_opencode/command/plan-spec.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: Dialogue-driven spec development through skeptical questioning
|
||||
---
|
||||
|
||||
Develop implementation-ready specs through iterative dialogue and skeptical questioning.
|
||||
|
||||
First, invoke the skill tool to load the spec-planner skill:
|
||||
|
||||
```
|
||||
skill({ name: 'spec-planner' })
|
||||
```
|
||||
|
||||
Then follow the skill instructions to develop the spec.
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
17
modules/_opencode/command/session-export.md
Normal file
17
modules/_opencode/command/session-export.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: Add AI session summary to GitHub PR or GitLab MR description
|
||||
---
|
||||
|
||||
Update the PR/MR description with an AI session export summary.
|
||||
|
||||
First, invoke the skill tool to load the session-export skill:
|
||||
|
||||
```
|
||||
skill({ name: 'session-export' })
|
||||
```
|
||||
|
||||
Then follow the skill instructions to export the session summary.
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
18
modules/_opencode/plugin/block-git.ts
Normal file
18
modules/_opencode/plugin/block-git.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Plugin } from "@opencode-ai/plugin";
|
||||
|
||||
const GIT_PATTERN = /(?:^|[;&|]\s*|&&\s*|\|\|\s*|\$\(\s*|`\s*)git\s/;
|
||||
|
||||
export const BlockGitPlugin: Plugin = async () => {
|
||||
return {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
if (input.tool === "bash") {
|
||||
const command = output.args.command as string;
|
||||
if (GIT_PATTERN.test(command)) {
|
||||
throw new Error(
|
||||
"This project uses jj, only use `jj` commands, not `git`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
19
modules/_opencode/plugin/block-scripting.ts
Normal file
19
modules/_opencode/plugin/block-scripting.ts
Normal 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|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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
41
modules/_opencode/skill/frontend-design/SKILL.md
Normal file
41
modules/_opencode/skill/frontend-design/SKILL.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
123
modules/_opencode/skill/librarian/SKILL.md
Normal file
123
modules/_opencode/skill/librarian/SKILL.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: librarian
|
||||
description: Multi-repository codebase exploration. Research library internals, find code patterns, understand architecture, compare implementations across GitHub/npm/PyPI/crates. Use when needing deep understanding of how libraries work, finding implementations across open source, or exploring remote repository structure.
|
||||
references:
|
||||
- references/tool-routing.md
|
||||
- references/opensrc-api.md
|
||||
- references/opensrc-examples.md
|
||||
- references/linking.md
|
||||
- references/diagrams.md
|
||||
---
|
||||
|
||||
# Librarian Skill
|
||||
|
||||
Deep codebase exploration across remote repositories.
|
||||
|
||||
## How to Use This Skill
|
||||
|
||||
### Reference Structure
|
||||
|
||||
| File | Purpose | When to Read |
|
||||
|------|---------|--------------|
|
||||
| `tool-routing.md` | Tool selection decision trees | **Always read first** |
|
||||
| `opensrc-api.md` | API reference, types | Writing opensrc code |
|
||||
| `opensrc-examples.md` | JavaScript patterns, workflows | Implementation examples |
|
||||
| `linking.md` | GitHub URL patterns | Formatting responses |
|
||||
| `diagrams.md` | Mermaid patterns | Visualizing architecture |
|
||||
|
||||
### Reading Order
|
||||
|
||||
1. **Start** with `tool-routing.md` → choose tool strategy
|
||||
2. **If using opensrc:**
|
||||
- Read `opensrc-api.md` for API details
|
||||
- Read `opensrc-examples.md` for patterns
|
||||
3. **Before responding:** `linking.md` + `diagrams.md` for output formatting
|
||||
|
||||
## Tool Arsenal
|
||||
|
||||
| Tool | Best For | Limitations |
|
||||
|------|----------|-------------|
|
||||
| **grep_app** | Find patterns across ALL public GitHub | Literal search only |
|
||||
| **context7** | Library docs, API examples, usage | Known libraries only |
|
||||
| **opensrc** | Fetch full source for deep exploration | Must fetch before read |
|
||||
|
||||
## Quick Decision Trees
|
||||
|
||||
### "How does X work?"
|
||||
|
||||
```
|
||||
Known library?
|
||||
├─ Yes → context7.resolve-library-id → context7.query-docs
|
||||
│ └─ Need internals? → opensrc.fetch → read source
|
||||
└─ No → grep_app search → opensrc.fetch top result
|
||||
```
|
||||
|
||||
### "Find pattern X"
|
||||
|
||||
```
|
||||
Specific repo?
|
||||
├─ Yes → opensrc.fetch → opensrc.grep → read matches
|
||||
└─ No → grep_app (broad) → opensrc.fetch interesting repos
|
||||
```
|
||||
|
||||
### "Explore repo structure"
|
||||
|
||||
```
|
||||
1. opensrc.fetch(target)
|
||||
2. opensrc.tree(source.name) → quick overview
|
||||
3. opensrc.files(source.name, "**/*.ts") → detailed listing
|
||||
4. Read: README, package.json, src/index.*
|
||||
5. Create architecture diagram (see diagrams.md)
|
||||
```
|
||||
|
||||
### "Compare X vs Y"
|
||||
|
||||
```
|
||||
1. opensrc.fetch(["X", "Y"])
|
||||
2. Use source.name from results for subsequent calls
|
||||
3. opensrc.grep(pattern, { sources: [nameX, nameY] })
|
||||
4. Read comparable files, synthesize differences
|
||||
```
|
||||
|
||||
## Critical: Source Naming Convention
|
||||
|
||||
**After fetching, always use `source.name` for subsequent calls:**
|
||||
|
||||
```javascript
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
const files = await opensrc.files(source.name, "**/*.ts");
|
||||
```
|
||||
|
||||
| Type | Fetch Spec | Source Name |
|
||||
|------|------------|-------------|
|
||||
| npm | `"zod"` | `"zod"` |
|
||||
| npm scoped | `"@tanstack/react-query"` | `"@tanstack/react-query"` |
|
||||
| pypi | `"pypi:requests"` | `"requests"` |
|
||||
| crates | `"crates:serde"` | `"serde"` |
|
||||
| GitHub | `"vercel/ai"` | `"github.com/vercel/ai"` |
|
||||
| GitLab | `"gitlab:org/repo"` | `"gitlab.com/org/repo"` |
|
||||
|
||||
## When NOT to Use opensrc
|
||||
|
||||
| Scenario | Use Instead |
|
||||
|----------|-------------|
|
||||
| Simple library API questions | context7 |
|
||||
| Finding examples across many repos | grep_app |
|
||||
| Very large monorepos (>10GB) | Clone locally |
|
||||
| Private repositories | Direct access |
|
||||
|
||||
## Output Guidelines
|
||||
|
||||
1. **Comprehensive final message** - only last message returns to main agent
|
||||
2. **Parallel tool calls** - maximize efficiency
|
||||
3. **Link every file reference** - see `linking.md`
|
||||
4. **Diagram complex relationships** - see `diagrams.md`
|
||||
5. **Never mention tool names** - say "I'll search" not "I'll use opensrc"
|
||||
|
||||
## References
|
||||
|
||||
- [Tool Routing Decision Trees](references/tool-routing.md)
|
||||
- [opensrc API Reference](references/opensrc-api.md)
|
||||
- [opensrc Code Examples](references/opensrc-examples.md)
|
||||
- [GitHub Linking Patterns](references/linking.md)
|
||||
- [Mermaid Diagram Patterns](references/diagrams.md)
|
||||
51
modules/_opencode/skill/librarian/references/diagrams.md
Normal file
51
modules/_opencode/skill/librarian/references/diagrams.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Mermaid Diagram Patterns
|
||||
|
||||
Create diagrams for:
|
||||
- Architecture (component relationships)
|
||||
- Data flow (request → response)
|
||||
- Dependencies (import graph)
|
||||
- Sequences (step-by-step processes)
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Client] --> B[API Gateway]
|
||||
B --> C[Auth Service]
|
||||
B --> D[Data Service]
|
||||
D --> E[(Database)]
|
||||
```
|
||||
|
||||
## Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input --> Parse --> Validate --> Transform --> Output
|
||||
```
|
||||
|
||||
## Sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Client->>+Server: Request
|
||||
Server->>+DB: Query
|
||||
DB-->>-Server: Result
|
||||
Server-->>-Client: Response
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
| Type | Use For |
|
||||
|------|---------|
|
||||
| `graph TD` | Component hierarchy, dependencies |
|
||||
| `flowchart LR` | Data transformation, pipelines |
|
||||
| `sequenceDiagram` | Request/response, multi-party interaction |
|
||||
| `classDiagram` | Type relationships, inheritance |
|
||||
| `stateDiagram` | State machines, lifecycle |
|
||||
|
||||
## Tips
|
||||
|
||||
- Keep nodes short (3-4 words max)
|
||||
- Use subgraphs for grouping related components
|
||||
- Arrow labels for relationship types
|
||||
- Prefer LR (left-right) for flows, TD (top-down) for hierarchies
|
||||
61
modules/_opencode/skill/librarian/references/linking.md
Normal file
61
modules/_opencode/skill/librarian/references/linking.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# GitHub Linking Patterns
|
||||
|
||||
All file/dir/code refs → fluent markdown links. Never raw URLs.
|
||||
|
||||
## URL Formats
|
||||
|
||||
### File
|
||||
```
|
||||
https://github.com/{owner}/{repo}/blob/{ref}/{path}
|
||||
```
|
||||
|
||||
### File + Lines
|
||||
```
|
||||
https://github.com/{owner}/{repo}/blob/{ref}/{path}#L{start}-L{end}
|
||||
```
|
||||
|
||||
### Directory
|
||||
```
|
||||
https://github.com/{owner}/{repo}/tree/{ref}/{path}
|
||||
```
|
||||
|
||||
### GitLab (note `/-/blob/`)
|
||||
```
|
||||
https://gitlab.com/{owner}/{repo}/-/blob/{ref}/{path}
|
||||
```
|
||||
|
||||
## Ref Resolution
|
||||
|
||||
| Source | Use as ref |
|
||||
|--------|------------|
|
||||
| Known version | `v{version}` |
|
||||
| Default branch | `main` or `master` |
|
||||
| opensrc fetch | ref from result |
|
||||
| Specific commit | full SHA |
|
||||
|
||||
## Examples
|
||||
|
||||
### Correct
|
||||
```markdown
|
||||
The [`parseAsync`](https://github.com/colinhacks/zod/blob/main/src/types.ts#L450-L480) method handles...
|
||||
```
|
||||
|
||||
### Wrong
|
||||
```markdown
|
||||
See https://github.com/colinhacks/zod/blob/main/src/types.ts#L100
|
||||
The parseAsync method in src/types.ts handles...
|
||||
```
|
||||
|
||||
## Line Numbers
|
||||
|
||||
- Single: `#L42`
|
||||
- Range: `#L42-L50`
|
||||
- Prefer ranges for context (2-5 lines around key code)
|
||||
|
||||
## Registry → GitHub
|
||||
|
||||
| Registry | Find repo in |
|
||||
|----------|--------------|
|
||||
| npm | `package.json` → `repository` |
|
||||
| PyPI | `pyproject.toml` or setup.py |
|
||||
| crates | `Cargo.toml` |
|
||||
235
modules/_opencode/skill/librarian/references/opensrc-api.md
Normal file
235
modules/_opencode/skill/librarian/references/opensrc-api.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# opensrc API Reference
|
||||
|
||||
## Tool
|
||||
|
||||
Use the **opensrc MCP server** via single tool:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `opensrc_execute` | All operations (fetch, read, grep, files, remove, etc.) |
|
||||
|
||||
Takes a `code` parameter: JavaScript async arrow function executed server-side. Source trees stay on server, only results return.
|
||||
|
||||
## API Surface
|
||||
|
||||
### Read Operations
|
||||
|
||||
```typescript
|
||||
// List all fetched sources
|
||||
opensrc.list(): Source[]
|
||||
|
||||
// Check if source exists
|
||||
opensrc.has(name: string, version?: string): boolean
|
||||
|
||||
// Get source metadata
|
||||
opensrc.get(name: string): Source | undefined
|
||||
|
||||
// List files with optional glob
|
||||
opensrc.files(sourceName: string, glob?: string): Promise<FileEntry[]>
|
||||
|
||||
// Get directory tree structure (default depth: 3)
|
||||
opensrc.tree(sourceName: string, options?: { depth?: number }): Promise<TreeNode>
|
||||
|
||||
// Regex search file contents
|
||||
opensrc.grep(pattern: string, options?: GrepOptions): Promise<GrepResult[]>
|
||||
|
||||
// AST-based semantic code search
|
||||
opensrc.astGrep(sourceName: string, pattern: string, options?: AstGrepOptions): Promise<AstGrepMatch[]>
|
||||
|
||||
// Read single file
|
||||
opensrc.read(sourceName: string, filePath: string): Promise<string>
|
||||
|
||||
// Batch read multiple files (supports globs!)
|
||||
opensrc.readMany(sourceName: string, paths: string[]): Promise<Record<string, string>>
|
||||
|
||||
// Parse fetch spec
|
||||
opensrc.resolve(spec: string): Promise<ParsedSpec>
|
||||
```
|
||||
|
||||
### Mutation Operations
|
||||
|
||||
```typescript
|
||||
// Fetch packages/repos
|
||||
opensrc.fetch(specs: string | string[], options?: { modify?: boolean }): Promise<FetchedSource[]>
|
||||
|
||||
// Remove sources
|
||||
opensrc.remove(names: string[]): Promise<RemoveResult>
|
||||
|
||||
// Clean by type
|
||||
opensrc.clean(options?: CleanOptions): Promise<RemoveResult>
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### Source
|
||||
|
||||
```typescript
|
||||
interface Source {
|
||||
type: "npm" | "pypi" | "crates" | "repo";
|
||||
name: string; // Use this for all subsequent calls
|
||||
version?: string;
|
||||
ref?: string;
|
||||
path: string;
|
||||
fetchedAt: string;
|
||||
repository: string;
|
||||
}
|
||||
```
|
||||
|
||||
### FetchedSource
|
||||
|
||||
```typescript
|
||||
interface FetchedSource {
|
||||
source: Source; // IMPORTANT: use source.name for subsequent calls
|
||||
alreadyExists: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### GrepOptions
|
||||
|
||||
```typescript
|
||||
interface GrepOptions {
|
||||
sources?: string[]; // Filter to specific sources
|
||||
include?: string; // File glob pattern (e.g., "*.ts")
|
||||
maxResults?: number; // Limit results (default: 100)
|
||||
}
|
||||
```
|
||||
|
||||
### GrepResult
|
||||
|
||||
```typescript
|
||||
interface GrepResult {
|
||||
source: string;
|
||||
file: string;
|
||||
line: number;
|
||||
content: string;
|
||||
}
|
||||
```
|
||||
|
||||
### AstGrepOptions
|
||||
|
||||
```typescript
|
||||
interface AstGrepOptions {
|
||||
glob?: string; // File glob pattern (e.g., "**/*.ts")
|
||||
lang?: string | string[]; // Language(s): "js", "ts", "tsx", "html", "css"
|
||||
limit?: number; // Max results (default: 1000)
|
||||
}
|
||||
```
|
||||
|
||||
### AstGrepMatch
|
||||
|
||||
```typescript
|
||||
interface AstGrepMatch {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
text: string; // Matched code text
|
||||
metavars: Record<string, string>; // Captured $VAR → text
|
||||
}
|
||||
```
|
||||
|
||||
#### AST Pattern Syntax
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `$NAME` | Single node, captures to metavars |
|
||||
| `$$$ARGS` | Zero or more nodes (variadic), captures |
|
||||
| `$_` | Single node, no capture |
|
||||
| `$$$` | Zero or more nodes, no capture |
|
||||
|
||||
### FileEntry
|
||||
|
||||
```typescript
|
||||
interface FileEntry {
|
||||
path: string;
|
||||
size: number;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### TreeNode
|
||||
|
||||
```typescript
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
type: "file" | "dir";
|
||||
children?: TreeNode[]; // only for dirs
|
||||
}
|
||||
```
|
||||
|
||||
### CleanOptions
|
||||
|
||||
```typescript
|
||||
interface CleanOptions {
|
||||
packages?: boolean;
|
||||
repos?: boolean;
|
||||
npm?: boolean;
|
||||
pypi?: boolean;
|
||||
crates?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### RemoveResult
|
||||
|
||||
```typescript
|
||||
interface RemoveResult {
|
||||
success: boolean;
|
||||
removed: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Operations throw on errors. Wrap in try/catch if needed:
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
try {
|
||||
const content = await opensrc.read("zod", "missing.ts");
|
||||
return content;
|
||||
} catch (e) {
|
||||
return { error: e.message };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`readMany` returns errors as string values prefixed with `[Error:`:
|
||||
|
||||
```javascript
|
||||
const files = await opensrc.readMany("zod", ["exists.ts", "missing.ts"]);
|
||||
// { "exists.ts": "content...", "missing.ts": "[Error: ENOENT...]" }
|
||||
|
||||
// Filter successful reads
|
||||
const successful = Object.entries(files)
|
||||
.filter(([_, content]) => !content.startsWith("[Error:"));
|
||||
```
|
||||
|
||||
## Package Spec Formats
|
||||
|
||||
| Format | Example | Source Name After Fetch |
|
||||
|--------|---------|------------------------|
|
||||
| `<name>` | `"zod"` | `"zod"` |
|
||||
| `<name>@<version>` | `"zod@3.22.0"` | `"zod"` |
|
||||
| `pypi:<name>` | `"pypi:requests"` | `"requests"` |
|
||||
| `crates:<name>` | `"crates:serde"` | `"serde"` |
|
||||
| `owner/repo` | `"vercel/ai"` | `"github.com/vercel/ai"` |
|
||||
| `owner/repo@ref` | `"vercel/ai@v1.0.0"` | `"github.com/vercel/ai"` |
|
||||
| `gitlab:owner/repo` | `"gitlab:org/repo"` | `"gitlab.com/org/repo"` |
|
||||
|
||||
## Critical Pattern
|
||||
|
||||
**Always capture `source.name` from fetch results:**
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
|
||||
// GitHub repos: "vercel/ai" → "github.com/vercel/ai"
|
||||
const sourceName = source.name;
|
||||
|
||||
// Use sourceName for ALL subsequent calls
|
||||
const files = await opensrc.files(sourceName, "src/**/*.ts");
|
||||
return files;
|
||||
}
|
||||
```
|
||||
336
modules/_opencode/skill/librarian/references/opensrc-examples.md
Normal file
336
modules/_opencode/skill/librarian/references/opensrc-examples.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# opensrc Code Examples
|
||||
|
||||
## Workflow: Fetch → Explore
|
||||
|
||||
### Basic Fetch and Explore with tree()
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
// Get directory structure first
|
||||
const tree = await opensrc.tree(source.name, { depth: 2 });
|
||||
return tree;
|
||||
}
|
||||
```
|
||||
|
||||
### Fetch and Read Key Files
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
const sourceName = source.name; // "github.com/vercel/ai"
|
||||
|
||||
const files = await opensrc.readMany(sourceName, [
|
||||
"package.json",
|
||||
"README.md",
|
||||
"src/index.ts"
|
||||
]);
|
||||
|
||||
return { sourceName, files };
|
||||
}
|
||||
```
|
||||
|
||||
### readMany with Globs
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("zod");
|
||||
// Read all package.json files in monorepo
|
||||
const files = await opensrc.readMany(source.name, [
|
||||
"packages/*/package.json" // globs supported!
|
||||
]);
|
||||
return Object.keys(files);
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Fetch Multiple Packages
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const results = await opensrc.fetch(["zod", "valibot", "yup"]);
|
||||
const names = results.map(r => r.source.name);
|
||||
|
||||
// Compare how each handles string validation
|
||||
const comparisons = {};
|
||||
for (const name of names) {
|
||||
const matches = await opensrc.grep("string.*validate|validateString", {
|
||||
sources: [name],
|
||||
include: "*.ts",
|
||||
maxResults: 10
|
||||
});
|
||||
comparisons[name] = matches.map(m => `${m.file}:${m.line}`);
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
```
|
||||
|
||||
## Search Patterns
|
||||
|
||||
### Grep → Read Context
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const matches = await opensrc.grep("export function parse\\(", {
|
||||
sources: ["zod"],
|
||||
include: "*.ts"
|
||||
});
|
||||
|
||||
if (matches.length === 0) return "No matches";
|
||||
|
||||
const match = matches[0];
|
||||
const content = await opensrc.read(match.source, match.file);
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Return 40 lines starting from match
|
||||
return {
|
||||
file: match.file,
|
||||
code: lines.slice(match.line - 1, match.line + 39).join("\n")
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Search Across All Fetched Sources
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const sources = opensrc.list();
|
||||
const results = {};
|
||||
|
||||
for (const source of sources) {
|
||||
const errorHandling = await opensrc.grep("throw new|catch \\(|\\.catch\\(", {
|
||||
sources: [source.name],
|
||||
include: "*.ts",
|
||||
maxResults: 20
|
||||
});
|
||||
results[source.name] = {
|
||||
type: source.type,
|
||||
errorPatterns: errorHandling.length
|
||||
};
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## AST-Based Search
|
||||
|
||||
Use `astGrep` for semantic code search with pattern matching.
|
||||
|
||||
### Find Function Declarations
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("lodash");
|
||||
|
||||
const fns = await opensrc.astGrep(source.name, "function $NAME($$$ARGS) { $$$BODY }", {
|
||||
lang: "js",
|
||||
limit: 20
|
||||
});
|
||||
|
||||
return fns.map(m => ({
|
||||
file: m.file,
|
||||
line: m.line,
|
||||
name: m.metavars.NAME
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Find React Hooks Usage
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("vercel/ai");
|
||||
|
||||
const stateHooks = await opensrc.astGrep(
|
||||
source.name,
|
||||
"const [$STATE, $SETTER] = useState($$$INIT)",
|
||||
{ lang: ["ts", "tsx"], limit: 50 }
|
||||
);
|
||||
|
||||
return stateHooks.map(m => ({
|
||||
file: m.file,
|
||||
state: m.metavars.STATE,
|
||||
setter: m.metavars.SETTER
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Find Class Definitions with Context
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const [{ source }] = await opensrc.fetch("zod");
|
||||
|
||||
const classes = await opensrc.astGrep(source.name, "class $NAME", {
|
||||
glob: "**/*.ts"
|
||||
});
|
||||
|
||||
const details = [];
|
||||
for (const cls of classes.slice(0, 5)) {
|
||||
const content = await opensrc.read(source.name, cls.file);
|
||||
const lines = content.split("\n");
|
||||
details.push({
|
||||
name: cls.metavars.NAME,
|
||||
file: cls.file,
|
||||
preview: lines.slice(cls.line - 1, cls.line + 9).join("\n")
|
||||
});
|
||||
}
|
||||
return details;
|
||||
}
|
||||
```
|
||||
|
||||
### Compare Export Patterns Across Libraries
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const results = await opensrc.fetch(["zod", "valibot"]);
|
||||
const names = results.map(r => r.source.name);
|
||||
|
||||
const exports = {};
|
||||
for (const name of names) {
|
||||
const matches = await opensrc.astGrep(name, "export const $NAME = $_", {
|
||||
lang: "ts",
|
||||
limit: 30
|
||||
});
|
||||
exports[name] = matches.map(m => m.metavars.NAME);
|
||||
}
|
||||
return exports;
|
||||
}
|
||||
```
|
||||
|
||||
### grep vs astGrep
|
||||
|
||||
| Use Case | Tool |
|
||||
|----------|------|
|
||||
| Text/regex pattern | `grep` |
|
||||
| Function declarations | `astGrep`: `function $NAME($$$) { $$$ }` |
|
||||
| Arrow functions | `astGrep`: `const $N = ($$$) => $_` |
|
||||
| Class definitions | `astGrep`: `class $NAME extends $PARENT` |
|
||||
| Import statements | `astGrep`: `import { $$$IMPORTS } from "$MOD"` |
|
||||
| JSX components | `astGrep`: `<$COMP $$$PROPS />` |
|
||||
|
||||
## Repository Exploration
|
||||
|
||||
### Find Entry Points
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const name = "github.com/vercel/ai";
|
||||
|
||||
const allFiles = await opensrc.files(name, "**/*.{ts,js}");
|
||||
const entryPoints = allFiles.filter(f =>
|
||||
f.path.match(/^(src\/)?(index|main|mod)\.(ts|js)$/) ||
|
||||
f.path.includes("/index.ts")
|
||||
);
|
||||
|
||||
// Read all entry points
|
||||
const contents = {};
|
||||
for (const ep of entryPoints.slice(0, 5)) {
|
||||
contents[ep.path] = await opensrc.read(name, ep.path);
|
||||
}
|
||||
|
||||
return {
|
||||
totalFiles: allFiles.length,
|
||||
entryPoints: entryPoints.map(f => f.path),
|
||||
contents
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Explore Package Structure
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const name = "zod";
|
||||
|
||||
// Get all TypeScript files
|
||||
const tsFiles = await opensrc.files(name, "**/*.ts");
|
||||
|
||||
// Group by directory
|
||||
const byDir = {};
|
||||
for (const f of tsFiles) {
|
||||
const dir = f.path.split("/").slice(0, -1).join("/") || ".";
|
||||
byDir[dir] = (byDir[dir] || 0) + 1;
|
||||
}
|
||||
|
||||
// Read key files
|
||||
const pkg = await opensrc.read(name, "package.json");
|
||||
const readme = await opensrc.read(name, "README.md");
|
||||
|
||||
return {
|
||||
structure: byDir,
|
||||
package: JSON.parse(pkg),
|
||||
readmePreview: readme.slice(0, 500)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Batch Operations
|
||||
|
||||
### Read Many with Error Handling
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const files = await opensrc.readMany("zod", [
|
||||
"src/index.ts",
|
||||
"src/types.ts",
|
||||
"src/ZodError.ts",
|
||||
"src/helpers/parseUtil.ts"
|
||||
]);
|
||||
|
||||
// files is Record<string, string> - errors start with "[Error:"
|
||||
const successful = Object.entries(files)
|
||||
.filter(([_, content]) => !content.startsWith("[Error:"))
|
||||
.map(([path, content]) => ({ path, lines: content.split("\n").length }));
|
||||
|
||||
return successful;
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Grep Across Multiple Sources
|
||||
|
||||
```javascript
|
||||
async () => {
|
||||
const targets = ["zod", "valibot"];
|
||||
const pattern = "export (type|interface)";
|
||||
|
||||
const results = await Promise.all(
|
||||
targets.map(async (name) => {
|
||||
const matches = await opensrc.grep(pattern, {
|
||||
sources: [name],
|
||||
include: "*.ts",
|
||||
maxResults: 50
|
||||
});
|
||||
return { name, count: matches.length, matches };
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow Checklist
|
||||
|
||||
### Comprehensive Repository Analysis
|
||||
|
||||
```
|
||||
Repository Analysis Progress:
|
||||
- [ ] 1. Fetch repository
|
||||
- [ ] 2. Read package.json + README
|
||||
- [ ] 3. Identify entry points (src/index.*)
|
||||
- [ ] 4. Read main entry file
|
||||
- [ ] 5. Map exports and public API
|
||||
- [ ] 6. Trace key functionality
|
||||
- [ ] 7. Create architecture diagram
|
||||
```
|
||||
|
||||
### Library Comparison
|
||||
|
||||
```
|
||||
Comparison Progress:
|
||||
- [ ] 1. Fetch all libraries
|
||||
- [ ] 2. Grep for target pattern in each
|
||||
- [ ] 3. Read matching implementations
|
||||
- [ ] 4. Create comparison table
|
||||
- [ ] 5. Synthesize findings
|
||||
```
|
||||
109
modules/_opencode/skill/librarian/references/tool-routing.md
Normal file
109
modules/_opencode/skill/librarian/references/tool-routing.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Tool Routing
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Q[User Query] --> T{Query Type?}
|
||||
T -->|Understand/Explain| U[UNDERSTAND]
|
||||
T -->|Find/Search| F[FIND]
|
||||
T -->|Explore/Architecture| E[EXPLORE]
|
||||
T -->|Compare| C[COMPARE]
|
||||
|
||||
U --> U1{Known library?}
|
||||
U1 -->|Yes| U2[context7.resolve-library-id]
|
||||
U2 --> U3[context7.query-docs]
|
||||
U3 --> U4{Need source?}
|
||||
U4 -->|Yes| U5[opensrc.fetch → read]
|
||||
U1 -->|No| U6[grep_app → opensrc.fetch]
|
||||
|
||||
F --> F1{Specific repo?}
|
||||
F1 -->|Yes| F2[opensrc.fetch → grep → read]
|
||||
F1 -->|No| F3[grep_app broad search]
|
||||
F3 --> F4[opensrc.fetch interesting repos]
|
||||
|
||||
E --> E1[opensrc.fetch]
|
||||
E1 --> E2[opensrc.files]
|
||||
E2 --> E3[Read entry points]
|
||||
E3 --> E4[Create diagram]
|
||||
|
||||
C --> C1["opensrc.fetch([X, Y])"]
|
||||
C1 --> C2[grep same pattern]
|
||||
C2 --> C3[Read comparable files]
|
||||
C3 --> C4[Synthesize comparison]
|
||||
```
|
||||
|
||||
## Query Type Detection
|
||||
|
||||
| Keywords | Query Type | Start With |
|
||||
|----------|------------|------------|
|
||||
| "how does", "why does", "explain", "purpose of" | UNDERSTAND | context7 |
|
||||
| "find", "where is", "implementations of", "examples of" | FIND | grep_app |
|
||||
| "explore", "walk through", "architecture", "structure" | EXPLORE | opensrc |
|
||||
| "compare", "vs", "difference between" | COMPARE | opensrc |
|
||||
|
||||
## UNDERSTAND Queries
|
||||
|
||||
```
|
||||
Known library? → context7.resolve-library-id → context7.query-docs
|
||||
└─ Need source? → opensrc.fetch → read
|
||||
|
||||
Unknown? → grep_app search → opensrc.fetch top result → read
|
||||
```
|
||||
|
||||
**When to transition context7 → opensrc:**
|
||||
- Need implementation details (not just API docs)
|
||||
- Question about internals/private methods
|
||||
- Tracing code flow through library
|
||||
|
||||
## FIND Queries
|
||||
|
||||
```
|
||||
Specific repo? → opensrc.fetch → opensrc.grep → read matches
|
||||
|
||||
Broad search? → grep_app → analyze → opensrc.fetch interesting repos
|
||||
```
|
||||
|
||||
**grep_app query tips:**
|
||||
- Use literal code patterns: `useState(` not "react hooks"
|
||||
- Filter by language: `language: ["TypeScript"]`
|
||||
- Narrow by repo: `repo: "vercel/"` for org
|
||||
|
||||
## EXPLORE Queries
|
||||
|
||||
```
|
||||
1. opensrc.fetch(target)
|
||||
2. opensrc.files → understand structure
|
||||
3. Identify entry points: README, package.json, src/index.*
|
||||
4. Read entry → internals
|
||||
5. Create architecture diagram
|
||||
```
|
||||
|
||||
## COMPARE Queries
|
||||
|
||||
```
|
||||
1. opensrc.fetch([X, Y])
|
||||
2. Extract source.name from each result
|
||||
3. opensrc.grep same pattern in both
|
||||
4. Read comparable files
|
||||
5. Synthesize → comparison table
|
||||
```
|
||||
|
||||
## Tool Capabilities
|
||||
|
||||
| Tool | Best For | Not For |
|
||||
|------|----------|---------|
|
||||
| **grep_app** | Broad search, unknown scope, finding repos | Semantic queries |
|
||||
| **context7** | Library APIs, best practices, common patterns | Library internals |
|
||||
| **opensrc** | Deep exploration, reading internals, tracing flow | Initial discovery |
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| Don't | Do |
|
||||
|-------|-----|
|
||||
| grep_app for known library docs | context7 first |
|
||||
| opensrc.fetch before knowing target | grep_app to discover |
|
||||
| Multiple small reads | opensrc.readMany batch |
|
||||
| Describe without linking | Link every file ref |
|
||||
| Text for complex relationships | Mermaid diagram |
|
||||
| Use tool names in responses | "I'll search..." not "I'll use opensrc" |
|
||||
122
modules/_opencode/skill/session-export/SKILL.md
Normal file
122
modules/_opencode/skill/session-export/SKILL.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: session-export
|
||||
description: Update GitHub PR descriptions with AI session export summaries. Use when user asks to add session summary to PR/MR, document AI assistance in PR/MR, or export conversation summary to PR/MR description.
|
||||
---
|
||||
|
||||
# Session Export
|
||||
|
||||
Update PR/MR descriptions with a structured summary of the AI-assisted conversation.
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
> [!NOTE]
|
||||
> This PR was written with AI assistance.
|
||||
|
||||
<details><summary>AI Session Export</summary>
|
||||
<p>
|
||||
|
||||
```json
|
||||
{
|
||||
"info": {
|
||||
"title": "<brief task description>",
|
||||
"agent": "opencode",
|
||||
"models": ["<model(s) used>"]
|
||||
},
|
||||
"summary": [
|
||||
"<action 1>",
|
||||
"<action 2>",
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Export Session Data
|
||||
|
||||
Get session data using OpenCode CLI:
|
||||
|
||||
```bash
|
||||
opencode export [sessionID]
|
||||
```
|
||||
|
||||
Returns JSON with session info including models used. Use current session if no sessionID provided.
|
||||
|
||||
### 2. Generate Summary JSON
|
||||
|
||||
From exported data and conversation context, create summary:
|
||||
|
||||
- **title**: 2-5 word task description (lowercase)
|
||||
- **agent**: always "opencode"
|
||||
- **models**: array from export data
|
||||
- **summary**: array of terse action statements
|
||||
- Use past tense ("added", "fixed", "created")
|
||||
- Start with "user requested..." or "user asked..."
|
||||
- Chronological order
|
||||
- Attempt to keep the summary to a max of 25 turns ("user requested", "agent did")
|
||||
- **NEVER include sensitive data**: API keys, credentials, secrets, tokens, passwords, env vars
|
||||
|
||||
### 3. Update PR/MR Description
|
||||
|
||||
**GitHub:**
|
||||
```bash
|
||||
gh pr edit <PR_NUMBER> --body "$(cat <<'EOF'
|
||||
<existing description>
|
||||
|
||||
> [!NOTE]
|
||||
> This PR was written with AI assistance.
|
||||
|
||||
<details><summary>AI Session Export</summary>
|
||||
...
|
||||
</details>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### 4. Preserve Existing Content
|
||||
|
||||
Always fetch and preserve existing PR/MR description:
|
||||
|
||||
```bash
|
||||
# GitHub
|
||||
gh pr view <PR_NUMBER> --json body -q '.body'
|
||||
|
||||
Append session export after existing content with blank line separator.
|
||||
|
||||
## Example Summary
|
||||
|
||||
For a session where user asked to add dark mode:
|
||||
|
||||
```json
|
||||
{
|
||||
"info": {
|
||||
"title": "dark mode implementation",
|
||||
"agent": "opencode",
|
||||
"models": ["claude sonnet 4"]
|
||||
},
|
||||
"summary": [
|
||||
"user requested dark mode toggle in settings",
|
||||
"agent explored existing theme system",
|
||||
"agent created ThemeContext for state management",
|
||||
"agent added DarkModeToggle component",
|
||||
"agent updated CSS variables for dark theme",
|
||||
"agent ran tests and fixed 2 failures",
|
||||
"agent committed changes"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
**NEVER include in summary:**
|
||||
- API keys, tokens, secrets
|
||||
- Passwords, credentials
|
||||
- Environment variable values
|
||||
- Private URLs with auth tokens
|
||||
- Personal identifiable information
|
||||
- Internal hostnames/IPs
|
||||
70
modules/_opencode/skill/vcs-detect/SKILL.md
Normal file
70
modules/_opencode/skill/vcs-detect/SKILL.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: vcs-detect
|
||||
description: Detect whether the current project uses jj (Jujutsu) or git for version control. Run this BEFORE any VCS command to use the correct tool.
|
||||
---
|
||||
|
||||
# VCS Detection Skill
|
||||
|
||||
Detect the version control system in use before running any VCS commands.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
- jj (Jujutsu) and git have different CLIs and workflows
|
||||
- Running `git` commands in a jj repo (or vice versa) causes errors
|
||||
- Some repos use jj with git colocated (both `.jj/` and `.git/` exist)
|
||||
|
||||
## Detection Logic
|
||||
|
||||
Both `jj root` and `git rev-parse --show-toplevel` walk up the filesystem to find repo root.
|
||||
|
||||
**Priority order:**
|
||||
|
||||
1. `jj root` succeeds → jj (handles colocated too)
|
||||
2. `git rev-parse` succeeds → git
|
||||
3. Both fail → no VCS
|
||||
|
||||
## Detection Command
|
||||
|
||||
```bash
|
||||
if jj root &>/dev/null; then echo "jj"
|
||||
elif git rev-parse --show-toplevel &>/dev/null; then echo "git"
|
||||
else echo "none"
|
||||
fi
|
||||
```
|
||||
|
||||
## Command Mappings
|
||||
|
||||
| Operation | git | jj |
|
||||
|-----------|-----|-----|
|
||||
| Status | `git status` | `jj status` |
|
||||
| Log | `git log` | `jj log` |
|
||||
| Diff | `git diff` | `jj diff` |
|
||||
| Commit | `git commit` | `jj commit` / `jj describe` |
|
||||
| Branch list | `git branch` | `jj branch list` |
|
||||
| New branch | `git checkout -b <name>` | `jj branch create <name>` |
|
||||
| Push | `git push` | `jj git push` |
|
||||
| Pull/Fetch | `git pull` / `git fetch` | `jj git fetch` |
|
||||
| Rebase | `git rebase` | `jj rebase` |
|
||||
|
||||
## Usage
|
||||
|
||||
Before any VCS operation:
|
||||
|
||||
1. Run detection command
|
||||
2. Use appropriate CLI based on result
|
||||
3. If `none`, warn user directory is not version controlled
|
||||
|
||||
## Example Integration
|
||||
|
||||
```
|
||||
User: Show me the git log
|
||||
Agent: [Runs detection] -> Result: jj
|
||||
Agent: [Runs `jj log` instead of `git log`]
|
||||
```
|
||||
|
||||
## Colocated Repos
|
||||
|
||||
When both `.jj/` and `.git/` exist, the repo is "colocated":
|
||||
- jj manages the working copy
|
||||
- git is available for compatibility (GitHub, etc.)
|
||||
- **Always prefer jj commands** in colocated repos
|
||||
0
modules/_opencode/tool/.gitkeep
Normal file
0
modules/_opencode/tool/.gitkeep
Normal file
130
modules/ai-tools.nix
Normal file
130
modules/ai-tools.nix
Normal file
@@ -0,0 +1,130 @@
|
||||
{inputs, ...}: {
|
||||
den.aspects.ai-tools.homeManager = {
|
||||
pkgs,
|
||||
inputs',
|
||||
...
|
||||
}: {
|
||||
programs.opencode = {
|
||||
enable = true;
|
||||
package = inputs'.llm-agents.packages.opencode;
|
||||
settings = {
|
||||
model = "anthropic/claude-opus-4-6";
|
||||
small_model = "anthropic/claude-haiku-4-5";
|
||||
theme = "catppuccin";
|
||||
plugin = ["oh-my-opencode@latest" "opencode-anthropic-auth@latest"];
|
||||
permission = {
|
||||
read = {
|
||||
"*" = "allow";
|
||||
"*.env" = "deny";
|
||||
"*.env.*" = "deny";
|
||||
"*.envrc" = "deny";
|
||||
"secrets/*" = "deny";
|
||||
};
|
||||
};
|
||||
agent = {
|
||||
plan = {
|
||||
model = "anthropic/claude-opus-4-6";
|
||||
};
|
||||
explore = {
|
||||
model = "anthropic/claude-haiku-4-5";
|
||||
};
|
||||
};
|
||||
instructions = [
|
||||
"CLAUDE.md"
|
||||
"AGENT.md"
|
||||
# "AGENTS.md"
|
||||
"AGENTS.local.md"
|
||||
];
|
||||
formatter = {
|
||||
mix = {
|
||||
disabled = true;
|
||||
};
|
||||
};
|
||||
mcp = {
|
||||
opensrc = {
|
||||
enabled = true;
|
||||
type = "local";
|
||||
command = ["bunx" "opensrc-mcp"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
home.packages = [
|
||||
inputs'.llm-agents.packages.claude-code
|
||||
];
|
||||
|
||||
xdg.configFile = {
|
||||
"opencode/agent" = {
|
||||
source = ./_opencode/agent;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/command" = {
|
||||
source = ./_opencode/command;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/skill" = {
|
||||
source = ./_opencode/skill;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/tool" = {
|
||||
source = ./_opencode/tool;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/plugin" = {
|
||||
source = ./_opencode/plugin;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/AGENTS.md".source = ./_opencode/AGENTS.md;
|
||||
"opencode/oh-my-opencode.json".text =
|
||||
builtins.toJSON {
|
||||
"$schema" = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json";
|
||||
disabled_skills = ["playwright" "dev-browser"];
|
||||
git_master = {
|
||||
commit_footer = false;
|
||||
include_co_authored_by = false;
|
||||
};
|
||||
runtime_fallback = true;
|
||||
agents = {
|
||||
explore = {
|
||||
model = "opencode-go/minimax-m2.5";
|
||||
fallback_models = ["anthropic/claude-haiku-4-5"];
|
||||
};
|
||||
librarian = {
|
||||
model = "opencode-go/minimax-m2.5";
|
||||
fallback_models = ["opencode-go/glm-5"];
|
||||
};
|
||||
sisyphus = {
|
||||
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
|
||||
};
|
||||
};
|
||||
categories = {
|
||||
"visual-engineering" = {
|
||||
fallback_models = ["opencode-go/glm-5" "opencode-go/kimi-k2.5"];
|
||||
};
|
||||
ultrabrain = {
|
||||
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
|
||||
};
|
||||
deep = {
|
||||
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
|
||||
};
|
||||
artistry = {
|
||||
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
|
||||
};
|
||||
quick = {
|
||||
fallback_models = ["opencode-go/minimax-m2.5"];
|
||||
};
|
||||
"unspecified-low" = {
|
||||
fallback_models = ["opencode-go/minimax-m2.5" "opencode-go/kimi-k2.5"];
|
||||
};
|
||||
"unspecified-high" = {
|
||||
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/glm-5"];
|
||||
};
|
||||
writing = {
|
||||
fallback_models = ["opencode-go/kimi-k2.5" "opencode-go/minimax-m2.5"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
26
modules/apps.nix
Normal file
26
modules/apps.nix
Normal file
@@ -0,0 +1,26 @@
|
||||
{inputs, ...}: {
|
||||
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
|
||||
// {
|
||||
deploy = {
|
||||
type = "app";
|
||||
program = "${inputs.deploy-rs.packages.${system}.deploy-rs}/bin/deploy";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
17
modules/atuin.nix
Normal file
17
modules/atuin.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
29
modules/chidi.nix
Normal file
29
modules/chidi.nix
Normal file
@@ -0,0 +1,29 @@
|
||||
{den, ...}: {
|
||||
den.aspects.chidi.includes = [
|
||||
den.aspects.darwin-system
|
||||
den.aspects.core
|
||||
den.aspects.tailscale
|
||||
den.aspects.desktop
|
||||
den.aspects.terminal
|
||||
den.aspects.atuin
|
||||
den.aspects.dev-tools
|
||||
den.aspects.neovim
|
||||
den.aspects.ai-tools
|
||||
den.aspects.zellij
|
||||
den.aspects.zk
|
||||
];
|
||||
|
||||
den.aspects.chidi.darwin = {pkgs, ...}: {
|
||||
networking.hostName = "chidi";
|
||||
networking.computerName = "chidi";
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
slack
|
||||
];
|
||||
};
|
||||
|
||||
den.aspects.chidi.homeManager = {...}: {
|
||||
fonts.fontconfig.enable = true;
|
||||
programs.git.settings.user.email = "christoph@tuist.dev";
|
||||
};
|
||||
}
|
||||
33
modules/core.nix
Normal file
33
modules/core.nix
Normal file
@@ -0,0 +1,33 @@
|
||||
{...}: {
|
||||
den.aspects.core.os = {pkgs, ...}: {
|
||||
programs.fish.enable = true;
|
||||
environment.shells = [pkgs.nushell];
|
||||
|
||||
nixpkgs = {
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
};
|
||||
};
|
||||
|
||||
nix = {
|
||||
package = pkgs.nix;
|
||||
settings = {
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
141
modules/darwin.nix
Normal file
141
modules/darwin.nix
Normal file
@@ -0,0 +1,141 @@
|
||||
{inputs, ...}: {
|
||||
den.aspects.darwin-system.darwin = {pkgs, ...}: {
|
||||
imports = [
|
||||
inputs.nix-homebrew.darwinModules.nix-homebrew
|
||||
inputs.home-manager.darwinModules.home-manager
|
||||
./_darwin/dock.nix
|
||||
];
|
||||
|
||||
system.primaryUser = "cschmatzler";
|
||||
|
||||
system.defaults = {
|
||||
NSGlobalDomain = {
|
||||
AppleInterfaceStyle = null;
|
||||
AppleShowAllExtensions = true;
|
||||
ApplePressAndHoldEnabled = false;
|
||||
KeyRepeat = 2;
|
||||
InitialKeyRepeat = 15;
|
||||
"com.apple.mouse.tapBehavior" = 1;
|
||||
"com.apple.sound.beep.volume" = 0.0;
|
||||
"com.apple.sound.beep.feedback" = 0;
|
||||
AppleShowScrollBars = "WhenScrolling";
|
||||
NSAutomaticCapitalizationEnabled = false;
|
||||
NSAutomaticDashSubstitutionEnabled = false;
|
||||
NSAutomaticPeriodSubstitutionEnabled = false;
|
||||
NSAutomaticQuoteSubstitutionEnabled = false;
|
||||
NSAutomaticSpellingCorrectionEnabled = false;
|
||||
NSDocumentSaveNewDocumentsToCloud = false;
|
||||
NSNavPanelExpandedStateForSaveMode = true;
|
||||
NSNavPanelExpandedStateForSaveMode2 = true;
|
||||
PMPrintingExpandedStateForPrint = true;
|
||||
PMPrintingExpandedStateForPrint2 = true;
|
||||
};
|
||||
|
||||
dock = {
|
||||
autohide = true;
|
||||
show-recents = false;
|
||||
launchanim = true;
|
||||
orientation = "bottom";
|
||||
tilesize = 60;
|
||||
minimize-to-application = true;
|
||||
mru-spaces = false;
|
||||
expose-group-apps = true;
|
||||
wvous-bl-corner = 1;
|
||||
wvous-br-corner = 1;
|
||||
wvous-tl-corner = 1;
|
||||
wvous-tr-corner = 1;
|
||||
};
|
||||
|
||||
finder = {
|
||||
_FXShowPosixPathInTitle = false;
|
||||
AppleShowAllFiles = true;
|
||||
FXEnableExtensionChangeWarning = false;
|
||||
FXPreferredViewStyle = "clmv";
|
||||
ShowPathbar = true;
|
||||
ShowStatusBar = true;
|
||||
};
|
||||
|
||||
trackpad = {
|
||||
Clicking = true;
|
||||
TrackpadThreeFingerDrag = true;
|
||||
};
|
||||
|
||||
screencapture = {
|
||||
location = "~/Screenshots";
|
||||
type = "png";
|
||||
disable-shadow = true;
|
||||
};
|
||||
|
||||
screensaver = {
|
||||
askForPassword = true;
|
||||
askForPasswordDelay = 5;
|
||||
};
|
||||
|
||||
loginwindow = {
|
||||
GuestEnabled = false;
|
||||
DisableConsoleAccess = true;
|
||||
};
|
||||
|
||||
spaces.spans-displays = false;
|
||||
|
||||
WindowManager.StandardHideWidgets = true;
|
||||
|
||||
menuExtraClock = {
|
||||
Show24Hour = true;
|
||||
ShowDate = 1;
|
||||
ShowDayOfWeek = true;
|
||||
ShowSeconds = false;
|
||||
};
|
||||
|
||||
CustomUserPreferences = {
|
||||
"com.apple.desktopservices" = {
|
||||
DSDontWriteNetworkStores = true;
|
||||
DSDontWriteUSBStores = true;
|
||||
};
|
||||
"com.apple.AdLib" = {
|
||||
allowApplePersonalizedAdvertising = false;
|
||||
};
|
||||
"com.apple.Spotlight" = {
|
||||
MenuItemHidden = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nix = {
|
||||
settings.trusted-users = ["@admin" "cschmatzler"];
|
||||
gc.interval = {
|
||||
Weekday = 0;
|
||||
Hour = 2;
|
||||
Minute = 0;
|
||||
};
|
||||
};
|
||||
|
||||
users.users.cschmatzler = {
|
||||
name = "cschmatzler";
|
||||
home = "/Users/cschmatzler";
|
||||
isHidden = false;
|
||||
shell = pkgs.nushell;
|
||||
};
|
||||
|
||||
home-manager.useGlobalPkgs = true;
|
||||
|
||||
nix-homebrew = {
|
||||
enable = true;
|
||||
user = "cschmatzler";
|
||||
mutableTaps = true;
|
||||
taps = {
|
||||
"homebrew/homebrew-core" = inputs.homebrew-core;
|
||||
"homebrew/homebrew-cask" = inputs.homebrew-cask;
|
||||
};
|
||||
};
|
||||
|
||||
homebrew = {
|
||||
enable = true;
|
||||
casks = [
|
||||
"ghostty@tip"
|
||||
"helium-browser"
|
||||
"tidal"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
31
modules/defaults.nix
Normal file
31
modules/defaults.nix
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
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 = {};
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
den.default.nixos.system.stateVersion = "25.11";
|
||||
den.default.darwin.system.stateVersion = 6;
|
||||
den.default.homeManager.home.stateVersion = "25.11";
|
||||
|
||||
den.default.includes = [
|
||||
den.provides.define-user
|
||||
den.provides.inputs'
|
||||
];
|
||||
|
||||
den.base.user.classes = lib.mkDefault ["homeManager"];
|
||||
};
|
||||
}
|
||||
61
modules/dendritic.nix
Normal file
61
modules/dendritic.nix
Normal file
@@ -0,0 +1,61 @@
|
||||
{inputs, ...}: {
|
||||
imports = [
|
||||
inputs.den.flakeModule
|
||||
(inputs.flake-file.flakeModules.dendritic or {})
|
||||
];
|
||||
|
||||
# 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";
|
||||
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";
|
||||
tuicr.url = "github:agavra/tuicr";
|
||||
naersk = {
|
||||
url = "github:nix-community/naersk/master";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
# Secrets inputs
|
||||
sops-nix = {
|
||||
url = "github:Mic92/sops-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
}
|
||||
26
modules/deploy.nix
Normal file
26
modules/deploy.nix
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
inputs,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
flake.deploy.nodes = {
|
||||
michael = {
|
||||
hostname = "michael";
|
||||
sshUser = "cschmatzler";
|
||||
profiles.system = {
|
||||
user = "root";
|
||||
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos config.flake.nixosConfigurations.michael;
|
||||
};
|
||||
};
|
||||
tahani = {
|
||||
hostname = "tahani";
|
||||
sshUser = "cschmatzler";
|
||||
profiles.system = {
|
||||
user = "root";
|
||||
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos config.flake.nixosConfigurations.tahani;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
flake.checks.x86_64-linux = inputs.deploy-rs.lib.x86_64-linux.deployChecks config.flake.deploy;
|
||||
}
|
||||
144
modules/desktop.nix
Normal file
144
modules/desktop.nix
Normal file
@@ -0,0 +1,144 @@
|
||||
{...}: {
|
||||
den.aspects.desktop.homeManager = {...}: {
|
||||
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" = "net.imput.helium";
|
||||
};
|
||||
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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
371
modules/dev-tools.nix
Normal file
371
modules/dev-tools.nix
Normal file
@@ -0,0 +1,371 @@
|
||||
{...}: {
|
||||
den.aspects.dev-tools.homeManager = {...}: let
|
||||
name = "Christoph Schmatzler";
|
||||
in {
|
||||
# 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;
|
||||
};
|
||||
};
|
||||
|
||||
# Git shell aliases
|
||||
home.shellAliases = {
|
||||
g = "git";
|
||||
ga = "git add";
|
||||
gaa = "git add --all";
|
||||
gapa = "git add --patch";
|
||||
gau = "git add --update";
|
||||
gav = "git add --verbose";
|
||||
gap = "git apply";
|
||||
gapt = "git apply --3way";
|
||||
gb = "git branch";
|
||||
gba = "git branch --all";
|
||||
gbd = "git branch --delete";
|
||||
gbD = "git branch --delete --force";
|
||||
gbl = "git blame -w";
|
||||
gbnm = "git branch --no-merged";
|
||||
gbr = "git branch --remote";
|
||||
gbs = "git bisect";
|
||||
gbsb = "git bisect bad";
|
||||
gbsg = "git bisect good";
|
||||
gbsn = "git bisect new";
|
||||
gbso = "git bisect old";
|
||||
gbsr = "git bisect reset";
|
||||
gbss = "git bisect start";
|
||||
gc = "git commit --verbose";
|
||||
gca = "git commit --verbose --all";
|
||||
gcam = "git commit --all --message";
|
||||
gcas = "git commit --all --signoff";
|
||||
gcasm = "git commit --all --signoff --message";
|
||||
gcb = "git checkout -b";
|
||||
gcB = "git checkout -B";
|
||||
gcf = "git config --list";
|
||||
gclean = "git clean --interactive -d";
|
||||
gcl = "git clone --recurse-submodules";
|
||||
gclf = "git clone --recursive --shallow-submodules --filter=blob:none --also-filter-submodules";
|
||||
gcm = "git checkout main";
|
||||
gcmsg = "git commit --message";
|
||||
gcn = "git commit --verbose --no-edit";
|
||||
gco = "git checkout";
|
||||
gcor = "git checkout --recurse-submodules";
|
||||
gcount = "git shortlog --summary --numbered";
|
||||
gcp = "git cherry-pick";
|
||||
gcpa = "git cherry-pick --abort";
|
||||
gcpc = "git cherry-pick --continue";
|
||||
gcs = "git commit --gpg-sign";
|
||||
gcss = "git commit --gpg-sign --signoff";
|
||||
gcssm = "git commit --gpg-sign --signoff --message";
|
||||
gcsm = "git commit --signoff --message";
|
||||
gd = "git diff";
|
||||
gdca = "git diff --cached";
|
||||
gdcw = "git diff --cached --word-diff";
|
||||
gds = "git diff --staged";
|
||||
gdw = "git diff --word-diff";
|
||||
gdt = "git diff-tree --no-commit-id --name-only -r";
|
||||
gdup = "git diff @{upstream}";
|
||||
gf = "git fetch";
|
||||
gfa = "git fetch --all --tags --prune";
|
||||
gfo = "git fetch origin";
|
||||
gg = "git gui citool";
|
||||
gga = "git gui citool --amend";
|
||||
ghh = "git help";
|
||||
gignore = "git update-index --assume-unchanged";
|
||||
gl = "git pull";
|
||||
glg = "git log --stat";
|
||||
glgp = "git log --stat --patch";
|
||||
glgg = "git log --graph";
|
||||
glgga = "git log --graph --decorate --all";
|
||||
glgm = "git log --graph --max-count=10";
|
||||
glo = "git log --oneline --decorate";
|
||||
glog = "git log --oneline --decorate --graph";
|
||||
gloga = "git log --oneline --decorate --graph --all";
|
||||
glol = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset\"";
|
||||
glola = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset\" --all";
|
||||
glols = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset\" --stat";
|
||||
glod = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ad) %C(bold blue)<%an>%Creset\"";
|
||||
glods = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ad) %C(bold blue)<%an>%Creset\" --date=short";
|
||||
glum = "git pull upstream main";
|
||||
gm = "git merge";
|
||||
gma = "git merge --abort";
|
||||
gmc = "git merge --continue";
|
||||
gms = "git merge --squash";
|
||||
gmff = "git merge --ff-only";
|
||||
gmtl = "git mergetool --no-prompt";
|
||||
gmtlvim = "git mergetool --no-prompt --tool=vimdiff";
|
||||
gmum = "git merge upstream/main";
|
||||
gmom = "git merge origin/main";
|
||||
gp = "git push";
|
||||
gpd = "git push --dry-run";
|
||||
gpf = "git push --force-with-lease";
|
||||
gpod = "git push origin --delete";
|
||||
gpr = "git pull --rebase";
|
||||
gpra = "git pull --rebase --autostash";
|
||||
gprav = "git pull --rebase --autostash -v";
|
||||
gprom = "git pull --rebase origin main";
|
||||
gpromi = "git pull --rebase=interactive origin main";
|
||||
gprv = "git pull --rebase -v";
|
||||
gprum = "git pull --rebase upstream main";
|
||||
gprumi = "git pull --rebase=interactive upstream main";
|
||||
gpv = "git push --verbose";
|
||||
gpu = "git push upstream";
|
||||
gr = "git remote";
|
||||
gra = "git remote add";
|
||||
grb = "git rebase";
|
||||
grba = "git rebase --abort";
|
||||
grbc = "git rebase --continue";
|
||||
grbd = "git rebase develop";
|
||||
grbi = "git rebase --interactive";
|
||||
grbm = "git rebase main";
|
||||
grbo = "git rebase --onto";
|
||||
grbom = "git rebase origin/main";
|
||||
grbs = "git rebase --skip";
|
||||
grbum = "git rebase upstream/main";
|
||||
grev = "git revert";
|
||||
greva = "git revert --abort";
|
||||
grevc = "git revert --continue";
|
||||
grf = "git reflog";
|
||||
grh = "git reset";
|
||||
grhh = "git reset --hard";
|
||||
grhk = "git reset --keep";
|
||||
grhs = "git reset --soft";
|
||||
grm = "git rm";
|
||||
grmc = "git rm --cached";
|
||||
grmv = "git remote rename";
|
||||
grrm = "git remote remove";
|
||||
grs = "git restore";
|
||||
grset = "git remote set-url";
|
||||
grss = "git restore --source";
|
||||
grst = "git restore --staged";
|
||||
gru = "git reset --";
|
||||
grup = "git remote update";
|
||||
grv = "git remote --verbose";
|
||||
gsb = "git status --short --branch";
|
||||
gsh = "git show";
|
||||
gsi = "git submodule init";
|
||||
gsps = "git show --pretty=short --show-signature";
|
||||
gss = "git status --short";
|
||||
gst = "git status";
|
||||
gsta = "git stash push";
|
||||
gstaa = "git stash apply";
|
||||
gstall = "git stash --all";
|
||||
gstc = "git stash clear";
|
||||
gstd = "git stash drop";
|
||||
gstl = "git stash list";
|
||||
gstp = "git stash pop";
|
||||
gsts = "git stash show --patch";
|
||||
gstu = "git stash push --include-untracked";
|
||||
gsu = "git submodule update";
|
||||
gsw = "git switch";
|
||||
gswc = "git switch --create";
|
||||
gswd = "git switch develop";
|
||||
gswm = "git switch main";
|
||||
gta = "git tag --annotate";
|
||||
gts = "git tag --sign";
|
||||
gunignore = "git update-index --no-assume-unchanged";
|
||||
gwch = "git whatchanged -p --abbrev-commit --pretty=medium";
|
||||
gwt = "git worktree";
|
||||
gwta = "git worktree add";
|
||||
gwtls = "git worktree list";
|
||||
gwtmv = "git worktree move";
|
||||
gwtrm = "git worktree remove";
|
||||
lg = "lazygit";
|
||||
};
|
||||
|
||||
# Complex git aliases that require pipes/subshells — nushell `alias` can't
|
||||
# handle these, so they're defined as custom commands instead.
|
||||
programs.nushell.extraConfig = ''
|
||||
def ggpull [] { git pull origin (git branch --show-current | str trim) }
|
||||
def ggpush [] { git push origin (git branch --show-current | str trim) }
|
||||
def ggsup [] { git branch $"--set-upstream-to=origin/(git branch --show-current | str trim)" }
|
||||
def gluc [] { git pull upstream (git branch --show-current | str trim) }
|
||||
def gpsup [] { git push --set-upstream origin (git branch --show-current | str trim) }
|
||||
def gpsupf [] { git push --set-upstream origin (git branch --show-current | str trim) --force-with-lease }
|
||||
def groh [] { git reset $"origin/(git branch --show-current | str trim)" --hard }
|
||||
def --env grt [] {
|
||||
let toplevel = (do { git rev-parse --show-toplevel } | complete | get stdout | str trim)
|
||||
if ($toplevel | is-not-empty) { cd $toplevel } else { cd . }
|
||||
}
|
||||
def gfg [...pattern: string] { git ls-files | lines | where {|f| $f =~ ($pattern | str join ".*") } }
|
||||
def gignored [] { git ls-files -v | lines | where {|l| ($l | str substring 0..1) =~ "[a-z]" } }
|
||||
def gpoat [] { git push origin --all; git push origin --tags }
|
||||
def gtv [] { git tag | lines | sort }
|
||||
def gwipe [] { git reset --hard; git clean --force -df }
|
||||
def gunwip [] {
|
||||
let msg = (git rev-list --max-count=1 --format="%s" HEAD | lines | get 1)
|
||||
if ($msg | str contains "--wip--") { git reset HEAD~1 }
|
||||
}
|
||||
def gwip [] {
|
||||
git add -A
|
||||
let deleted = (git ls-files --deleted | lines)
|
||||
if ($deleted | is-not-empty) { git rm ...$deleted }
|
||||
git commit --no-verify --no-gpg-sign --message "--wip-- [skip ci]"
|
||||
}
|
||||
'';
|
||||
|
||||
# Jujutsu configuration
|
||||
programs.jujutsu = {
|
||||
enable = true;
|
||||
settings = {
|
||||
user = {
|
||||
name = name;
|
||||
email = "christoph@schmatzler.com";
|
||||
};
|
||||
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(\"christoph@schmatzler.com\")";
|
||||
"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(),
|
||||
)
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Lazygit configuration
|
||||
programs.lazygit = {
|
||||
enable = true;
|
||||
settings = {
|
||||
git = {
|
||||
commit.signOff = true;
|
||||
pagers = [
|
||||
{
|
||||
delta = {
|
||||
colorArg = "always";
|
||||
pager = "DELTA_FEATURES=decorations delta --light --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format=\"lazygit-edit://{path}:{line}\"";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
gui = {
|
||||
authorColors = {
|
||||
"*" = "#7287fd";
|
||||
};
|
||||
theme = {
|
||||
activeBorderColor = [
|
||||
"#8839ef"
|
||||
"bold"
|
||||
];
|
||||
inactiveBorderColor = [
|
||||
"#6c6f85"
|
||||
];
|
||||
optionsTextColor = [
|
||||
"#1e66f5"
|
||||
];
|
||||
selectedLineBgColor = [
|
||||
"#ccd0da"
|
||||
];
|
||||
cherryPickedCommitBgColor = [
|
||||
"#bcc0cc"
|
||||
];
|
||||
cherryPickedCommitFgColor = [
|
||||
"#8839ef"
|
||||
];
|
||||
defaultFgColor = [
|
||||
"#4c4f69"
|
||||
];
|
||||
searchingActiveBorderColor = [
|
||||
"#df8e1d"
|
||||
];
|
||||
unstagedChangesColor = [
|
||||
"#d20f39"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# JJUI configuration
|
||||
programs.jjui = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
# Direnv configuration
|
||||
programs.direnv = {
|
||||
enable = true;
|
||||
nix-direnv.enable = true;
|
||||
};
|
||||
|
||||
# Mise configuration
|
||||
programs.mise = {
|
||||
enable = true;
|
||||
enableNushellIntegration = true;
|
||||
globalConfig.settings = {
|
||||
auto_install = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
54
modules/email.nix
Normal file
54
modules/email.nix
Normal file
@@ -0,0 +1,54 @@
|
||||
{...}: {
|
||||
den.aspects.email.homeManager = {pkgs, ...}: {
|
||||
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."christoph@schmatzler.com" = {
|
||||
primary = true;
|
||||
maildir.path = "christoph@schmatzler.com";
|
||||
address = "christoph@schmatzler.com";
|
||||
userName = "christoph.schmatzler@icloud.com";
|
||||
realName = "Christoph Schmatzler";
|
||||
passwordCommand = ["cat" "/run/secrets/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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
{
|
||||
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";
|
||||
};
|
||||
|
||||
secretFile =
|
||||
mkOption {
|
||||
type = types.path;
|
||||
description = "Path to the environment file containing S3 credentials for Litestream";
|
||||
};
|
||||
};
|
||||
|
||||
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 = {
|
||||
enable = true;
|
||||
port = 6380;
|
||||
bind = "127.0.0.1";
|
||||
settings = {
|
||||
maxmemory = "64mb";
|
||||
maxmemory-policy = "allkeys-lru";
|
||||
};
|
||||
};
|
||||
|
||||
services.gitea = {
|
||||
enable = true;
|
||||
database = {
|
||||
type = "sqlite3";
|
||||
path = "/var/lib/gitea/data/gitea.db";
|
||||
};
|
||||
settings = {
|
||||
server = {
|
||||
ROOT_URL = "https://git.schmatzler.com/";
|
||||
DOMAIN = "git.schmatzler.com";
|
||||
HTTP_ADDR = "127.0.0.1";
|
||||
HTTP_PORT = 3000;
|
||||
LANDING_PAGE = "explore";
|
||||
};
|
||||
service.DISABLE_REGISTRATION = true;
|
||||
security.INSTALL_LOCK = true;
|
||||
cache = {
|
||||
ADAPTER = "redis";
|
||||
HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s";
|
||||
ITEM_TTL = "16h";
|
||||
};
|
||||
"cache.last_commit" = {
|
||||
ITEM_TTL = "8760h";
|
||||
COMMITS_COUNT = 100;
|
||||
};
|
||||
session = {
|
||||
PROVIDER = "redis";
|
||||
PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s";
|
||||
COOKIE_SECURE = true;
|
||||
SAME_SITE = "strict";
|
||||
};
|
||||
api.ENABLE_SWAGGER = false;
|
||||
};
|
||||
};
|
||||
|
||||
services.litestream = {
|
||||
enable = true;
|
||||
environmentFile = cfg.litestream.secretFile;
|
||||
settings = {
|
||||
dbs = [
|
||||
{
|
||||
path = "/var/lib/gitea/data/gitea.db";
|
||||
replicas = [
|
||||
{
|
||||
type = "s3";
|
||||
bucket = cfg.litestream.bucket;
|
||||
path = "gitea";
|
||||
endpoint = cfg.s3.endpoint;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.litestream = {
|
||||
serviceConfig = {
|
||||
User = mkForce "gitea";
|
||||
Group = mkForce "gitea";
|
||||
};
|
||||
};
|
||||
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
virtualHosts."git.schmatzler.com".extraConfig = ''
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
reverse_proxy localhost:3000
|
||||
'';
|
||||
};
|
||||
|
||||
services.restic.backups.gitea = {
|
||||
repository = "s3:${cfg.s3.endpoint}/${cfg.restic.bucket}";
|
||||
paths = ["/var/lib/gitea"];
|
||||
exclude = [
|
||||
"/var/lib/gitea/log"
|
||||
"/var/lib/gitea/data/gitea.db"
|
||||
"/var/lib/gitea/data/gitea.db-shm"
|
||||
"/var/lib/gitea/data/gitea.db-wal"
|
||||
];
|
||||
passwordFile = cfg.restic.passwordFile;
|
||||
environmentFile = cfg.restic.environmentFile;
|
||||
pruneOpts = [
|
||||
"--keep-daily 7"
|
||||
"--keep-weekly 4"
|
||||
"--keep-monthly 6"
|
||||
];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = "1h";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.restic-backups-gitea = {
|
||||
wants = ["restic-init-gitea.service"];
|
||||
after = ["restic-init-gitea.service"];
|
||||
serviceConfig = {
|
||||
User = mkForce "gitea";
|
||||
Group = mkForce "gitea";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.restic-init-gitea = {
|
||||
description = "Initialize Restic repository for Gitea backups";
|
||||
wantedBy = ["multi-user.target"];
|
||||
after = ["network-online.target"];
|
||||
wants = ["network-online.target"];
|
||||
path = [pkgs.restic];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "gitea";
|
||||
Group = "gitea";
|
||||
RemainAfterExit = true;
|
||||
EnvironmentFile = cfg.restic.environmentFile;
|
||||
};
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
6
modules/hosts.nix
Normal file
6
modules/hosts.nix
Normal file
@@ -0,0 +1,6 @@
|
||||
{...}: {
|
||||
den.hosts.aarch64-darwin.chidi.users.cschmatzler = {};
|
||||
den.hosts.aarch64-darwin.jason.users.cschmatzler = {};
|
||||
den.hosts.x86_64-linux.michael.users.cschmatzler = {};
|
||||
den.hosts.x86_64-linux.tahani.users.cschmatzler = {};
|
||||
}
|
||||
25
modules/jason.nix
Normal file
25
modules/jason.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
{den, ...}: {
|
||||
den.aspects.jason.includes = [
|
||||
den.aspects.darwin-system
|
||||
den.aspects.core
|
||||
den.aspects.tailscale
|
||||
den.aspects.desktop
|
||||
den.aspects.terminal
|
||||
den.aspects.atuin
|
||||
den.aspects.dev-tools
|
||||
den.aspects.neovim
|
||||
den.aspects.ai-tools
|
||||
den.aspects.zellij
|
||||
den.aspects.zk
|
||||
];
|
||||
|
||||
den.aspects.jason.darwin = {...}: {
|
||||
networking.hostName = "jason";
|
||||
networking.computerName = "jason";
|
||||
};
|
||||
|
||||
den.aspects.jason.homeManager = {...}: {
|
||||
fonts.fontconfig.enable = true;
|
||||
programs.git.settings.user.email = "christoph@schmatzler.com";
|
||||
};
|
||||
}
|
||||
191
modules/michael.nix
Normal file
191
modules/michael.nix
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
inputs,
|
||||
den,
|
||||
...
|
||||
}: {
|
||||
den.aspects.michael.includes = [
|
||||
den.aspects.nixos-system
|
||||
den.aspects.core
|
||||
den.aspects.openssh
|
||||
den.aspects.fail2ban
|
||||
den.aspects.tailscale
|
||||
];
|
||||
|
||||
den.aspects.michael.nixos = {
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
modulesPath,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
(modulesPath + "/installer/scan/not-detected.nix")
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
./_hosts/michael/disk-config.nix
|
||||
./_hosts/michael/hardware-configuration.nix
|
||||
inputs.disko.nixosModules.default
|
||||
];
|
||||
|
||||
networking.hostName = "michael";
|
||||
|
||||
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";
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [80 443];
|
||||
|
||||
services.redis.servers.gitea = {
|
||||
enable = true;
|
||||
port = 6380;
|
||||
bind = "127.0.0.1";
|
||||
settings = {
|
||||
maxmemory = "64mb";
|
||||
maxmemory-policy = "allkeys-lru";
|
||||
};
|
||||
};
|
||||
|
||||
services.gitea = {
|
||||
enable = true;
|
||||
database = {
|
||||
type = "sqlite3";
|
||||
path = "/var/lib/gitea/data/gitea.db";
|
||||
};
|
||||
settings = {
|
||||
server = {
|
||||
ROOT_URL = "https://git.schmatzler.com/";
|
||||
DOMAIN = "git.schmatzler.com";
|
||||
HTTP_ADDR = "127.0.0.1";
|
||||
HTTP_PORT = 3000;
|
||||
LANDING_PAGE = "explore";
|
||||
};
|
||||
service.DISABLE_REGISTRATION = true;
|
||||
security.INSTALL_LOCK = true;
|
||||
cache = {
|
||||
ADAPTER = "redis";
|
||||
HOST = "redis://127.0.0.1:6380/0?pool_size=100&idle_timeout=180s";
|
||||
ITEM_TTL = "16h";
|
||||
};
|
||||
"cache.last_commit" = {
|
||||
ITEM_TTL = "8760h";
|
||||
COMMITS_COUNT = 100;
|
||||
};
|
||||
session = {
|
||||
PROVIDER = "redis";
|
||||
PROVIDER_CONFIG = "redis://127.0.0.1:6380/1?pool_size=100&idle_timeout=180s";
|
||||
COOKIE_SECURE = true;
|
||||
SAME_SITE = "strict";
|
||||
};
|
||||
api.ENABLE_SWAGGER = false;
|
||||
};
|
||||
};
|
||||
|
||||
services.litestream = {
|
||||
enable = true;
|
||||
environmentFile = config.sops.secrets.michael-gitea-litestream.path;
|
||||
settings = {
|
||||
dbs = [
|
||||
{
|
||||
path = "/var/lib/gitea/data/gitea.db";
|
||||
replicas = [
|
||||
{
|
||||
type = "s3";
|
||||
bucket = "michael-gitea-litestream";
|
||||
path = "gitea";
|
||||
endpoint = "s3.eu-central-003.backblazeb2.com";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.litestream = {
|
||||
serviceConfig = {
|
||||
User = lib.mkForce "gitea";
|
||||
Group = lib.mkForce "gitea";
|
||||
};
|
||||
};
|
||||
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
virtualHosts."git.schmatzler.com".extraConfig = ''
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
reverse_proxy localhost:3000
|
||||
'';
|
||||
};
|
||||
|
||||
services.restic.backups.gitea = {
|
||||
repository = "s3:s3.eu-central-003.backblazeb2.com/michael-gitea-repositories";
|
||||
paths = ["/var/lib/gitea"];
|
||||
exclude = [
|
||||
"/var/lib/gitea/log"
|
||||
"/var/lib/gitea/data/gitea.db"
|
||||
"/var/lib/gitea/data/gitea.db-shm"
|
||||
"/var/lib/gitea/data/gitea.db-wal"
|
||||
];
|
||||
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"
|
||||
"--keep-monthly 6"
|
||||
];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = "1h";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.restic-backups-gitea = {
|
||||
wants = ["restic-init-gitea.service"];
|
||||
after = ["restic-init-gitea.service"];
|
||||
serviceConfig = {
|
||||
User = lib.mkForce "gitea";
|
||||
Group = lib.mkForce "gitea";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.restic-init-gitea = {
|
||||
description = "Initialize Restic repository for Gitea backups";
|
||||
wantedBy = ["multi-user.target"];
|
||||
after = ["network-online.target"];
|
||||
wants = ["network-online.target"];
|
||||
path = [pkgs.restic];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "gitea";
|
||||
Group = "gitea";
|
||||
RemainAfterExit = true;
|
||||
EnvironmentFile = config.sops.secrets.michael-gitea-restic-env.path;
|
||||
};
|
||||
script = ''
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
8
modules/neovim.nix
Normal file
8
modules/neovim.nix
Normal file
@@ -0,0 +1,8 @@
|
||||
{inputs, ...}: {
|
||||
den.aspects.neovim.homeManager = {pkgs, ...}: {
|
||||
imports = [
|
||||
inputs.nixvim.homeModules.nixvim
|
||||
./_neovim/default.nix
|
||||
];
|
||||
};
|
||||
}
|
||||
67
modules/network.nix
Normal file
67
modules/network.nix
Normal file
@@ -0,0 +1,67 @@
|
||||
{...}: {
|
||||
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;
|
||||
openFirewall = true;
|
||||
permitCertUid = "caddy";
|
||||
useRoutingFeatures = "server";
|
||||
};
|
||||
};
|
||||
|
||||
den.aspects.tailscale.darwin = {
|
||||
services.tailscale = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
87
modules/nixos-system.nix
Normal file
87
modules/nixos-system.nix
Normal file
@@ -0,0 +1,87 @@
|
||||
{inputs, ...}: {
|
||||
den.aspects.nixos-system.nixos = {pkgs, ...}: {
|
||||
imports = [inputs.home-manager.nixosModules.home-manager];
|
||||
|
||||
security.sudo.enable = true;
|
||||
security.sudo.extraRules = [
|
||||
{
|
||||
users = ["cschmatzler"];
|
||||
commands = [
|
||||
{
|
||||
command = "/run/current-system/sw/bin/nix-env";
|
||||
options = ["NOPASSWD"];
|
||||
}
|
||||
{
|
||||
command = "/nix/store/*/bin/switch-to-configuration";
|
||||
options = ["NOPASSWD"];
|
||||
}
|
||||
{
|
||||
command = "/nix/store/*/bin/activate";
|
||||
options = ["NOPASSWD"];
|
||||
}
|
||||
{
|
||||
command = "/nix/store/*/bin/activate-rs";
|
||||
options = ["NOPASSWD"];
|
||||
}
|
||||
{
|
||||
command = "/nix/store/*/bin/wait-activate";
|
||||
options = ["NOPASSWD"];
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
time.timeZone = "UTC";
|
||||
|
||||
nix = {
|
||||
settings.trusted-users = ["cschmatzler"];
|
||||
gc.dates = "weekly";
|
||||
nixPath = ["nixos-config=/home/cschmatzler/.local/share/src/nixos-config:/etc/nixos"];
|
||||
};
|
||||
|
||||
boot = {
|
||||
loader = {
|
||||
systemd-boot = {
|
||||
enable = true;
|
||||
configurationLimit = 42;
|
||||
};
|
||||
efi.canTouchEfiVariables = true;
|
||||
};
|
||||
initrd.availableKernelModules = [
|
||||
"xhci_pci"
|
||||
"ahci"
|
||||
"nvme"
|
||||
"usbhid"
|
||||
"usb_storage"
|
||||
"sd_mod"
|
||||
];
|
||||
kernelPackages = pkgs.linuxPackages_latest;
|
||||
};
|
||||
|
||||
users.users = {
|
||||
cschmatzler = {
|
||||
isNormalUser = true;
|
||||
home = "/home/cschmatzler";
|
||||
extraGroups = [
|
||||
"wheel"
|
||||
"sudo"
|
||||
"network"
|
||||
"systemd-journal"
|
||||
];
|
||||
shell = pkgs.nushell;
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/"
|
||||
];
|
||||
};
|
||||
root = {
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHfRZQ+7ejD3YHbyMTrV0gN1Gc0DxtGgl5CVZSupo5ws"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/I+/2QT47raegzMIyhwMEPKarJP/+Ox9ewA4ZFJwk/"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.useGlobalPkgs = true;
|
||||
};
|
||||
}
|
||||
29
modules/overlays.nix
Normal file
29
modules/overlays.nix
Normal file
@@ -0,0 +1,29 @@
|
||||
{inputs, ...}: let
|
||||
overlays = [
|
||||
# himalaya
|
||||
(final: prev: {
|
||||
himalaya = inputs.himalaya.packages.${prev.stdenv.hostPlatform.system}.default;
|
||||
})
|
||||
# jj-ryu (uses build-rust-package helper)
|
||||
(final: prev: {
|
||||
jj-ryu =
|
||||
import ./_lib/build-rust-package.nix {
|
||||
inherit inputs prev;
|
||||
input = inputs.jj-ryu;
|
||||
};
|
||||
})
|
||||
# jj-starship (passes through upstream overlay)
|
||||
inputs.jj-starship.overlays.default
|
||||
# zjstatus
|
||||
(final: prev: {
|
||||
zjstatus = inputs.zjstatus.packages.${prev.stdenv.hostPlatform.system}.default;
|
||||
})
|
||||
# tuicr
|
||||
(final: prev: {
|
||||
tuicr = inputs.tuicr.defaultPackage.${prev.stdenv.hostPlatform.system};
|
||||
})
|
||||
];
|
||||
in {
|
||||
den.default.nixos.nixpkgs.overlays = overlays;
|
||||
den.default.darwin.nixpkgs.overlays = overlays;
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.my.pgbackrest;
|
||||
in {
|
||||
options.my.pgbackrest = {
|
||||
enable = mkEnableOption "pgBackRest PostgreSQL backup";
|
||||
|
||||
stanza =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "main";
|
||||
description = "Name of the pgBackRest stanza";
|
||||
};
|
||||
|
||||
secretFile =
|
||||
mkOption {
|
||||
type = types.path;
|
||||
description = "Path to the environment file containing S3 credentials and cipher passphrase";
|
||||
};
|
||||
|
||||
s3 =
|
||||
mkOption {
|
||||
type =
|
||||
types.submodule {
|
||||
options = {
|
||||
endpoint =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "s3.eu-central-003.backblazeb2.com";
|
||||
description = "S3 endpoint URL";
|
||||
};
|
||||
bucket =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
description = "S3 bucket name";
|
||||
};
|
||||
region =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "eu-central-003";
|
||||
description = "S3 region";
|
||||
};
|
||||
path =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "/backups";
|
||||
description = "Path within the S3 bucket";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
description = "S3 storage configuration";
|
||||
};
|
||||
|
||||
retention =
|
||||
mkOption {
|
||||
type =
|
||||
types.submodule {
|
||||
options = {
|
||||
full =
|
||||
mkOption {
|
||||
type = types.int;
|
||||
default = 7;
|
||||
description = "Number of full backups to retain";
|
||||
};
|
||||
diff =
|
||||
mkOption {
|
||||
type = types.int;
|
||||
default = 7;
|
||||
description = "Number of differential backups to retain";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
description = "Backup retention configuration";
|
||||
};
|
||||
|
||||
compression =
|
||||
mkOption {
|
||||
type =
|
||||
types.submodule {
|
||||
options = {
|
||||
type =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "zst";
|
||||
description = "Compression algorithm (none, gz, lz4, zst)";
|
||||
};
|
||||
level =
|
||||
mkOption {
|
||||
type = types.int;
|
||||
default = 3;
|
||||
description = "Compression level";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
description = "Compression configuration";
|
||||
};
|
||||
|
||||
processMax =
|
||||
mkOption {
|
||||
type = types.int;
|
||||
default = 2;
|
||||
description = "Maximum number of processes for parallel operations";
|
||||
};
|
||||
|
||||
schedule =
|
||||
mkOption {
|
||||
type =
|
||||
types.submodule {
|
||||
options = {
|
||||
full =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "daily";
|
||||
description = "OnCalendar expression for full backups";
|
||||
};
|
||||
diff =
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "hourly";
|
||||
description = "OnCalendar expression for differential backups";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
description = "Backup schedule configuration";
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
mkIf cfg.enable (let
|
||||
archivePushScript =
|
||||
pkgs.writeShellScript "pgbackrest-archive-push" ''
|
||||
set -a
|
||||
source ${cfg.secretFile}
|
||||
set +a
|
||||
exec ${pkgs.pgbackrest}/bin/pgbackrest --stanza=${cfg.stanza} archive-push "$1"
|
||||
'';
|
||||
in {
|
||||
environment.systemPackages = [
|
||||
pkgs.pgbackrest
|
||||
(pkgs.writeShellScriptBin "pgbackrest-wrapper" ''
|
||||
set -a
|
||||
source ${cfg.secretFile}
|
||||
set +a
|
||||
exec ${pkgs.pgbackrest}/bin/pgbackrest "$@"
|
||||
'')
|
||||
];
|
||||
|
||||
services.postgresql.settings = {
|
||||
archive_mode = "on";
|
||||
archive_command = "${archivePushScript} %p";
|
||||
};
|
||||
|
||||
environment.etc."pgbackrest/pgbackrest.conf".text = ''
|
||||
[global]
|
||||
repo1-type=s3
|
||||
repo1-s3-endpoint=${cfg.s3.endpoint}
|
||||
repo1-s3-bucket=${cfg.s3.bucket}
|
||||
repo1-s3-region=${cfg.s3.region}
|
||||
repo1-path=${cfg.s3.path}
|
||||
repo1-retention-full=${toString cfg.retention.full}
|
||||
repo1-retention-diff=${toString cfg.retention.diff}
|
||||
repo1-cipher-type=aes-256-cbc
|
||||
compress-type=${cfg.compression.type}
|
||||
compress-level=${toString cfg.compression.level}
|
||||
process-max=${toString cfg.processMax}
|
||||
log-level-console=info
|
||||
log-level-file=detail
|
||||
log-path=/var/log/pgbackrest
|
||||
spool-path=/var/spool/pgbackrest
|
||||
|
||||
[${cfg.stanza}]
|
||||
pg1-path=/var/lib/postgresql/${config.services.postgresql.package.psqlSchema}
|
||||
pg1-user=postgres
|
||||
'';
|
||||
|
||||
systemd.services.pgbackrest-stanza-create = {
|
||||
description = "pgBackRest Stanza Create";
|
||||
after = ["postgresql.service"];
|
||||
requires = ["postgresql.service"];
|
||||
path = [pkgs.pgbackrest];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "postgres";
|
||||
EnvironmentFile = cfg.secretFile;
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
pgbackrest --stanza=${cfg.stanza} stanza-create || true
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.pgbackrest-backup = {
|
||||
description = "pgBackRest Full Backup";
|
||||
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
|
||||
requires = ["postgresql.service"];
|
||||
wants = ["pgbackrest-stanza-create.service"];
|
||||
path = [pkgs.pgbackrest];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "postgres";
|
||||
EnvironmentFile = cfg.secretFile;
|
||||
};
|
||||
script = ''
|
||||
pgbackrest --stanza=${cfg.stanza} backup --type=full
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.pgbackrest-backup = {
|
||||
wantedBy = ["timers.target"];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.schedule.full;
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = "1h";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.pgbackrest-backup-diff = {
|
||||
description = "pgBackRest Differential Backup";
|
||||
after = ["postgresql.service" "pgbackrest-stanza-create.service"];
|
||||
requires = ["postgresql.service"];
|
||||
wants = ["pgbackrest-stanza-create.service"];
|
||||
path = [pkgs.pgbackrest];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "postgres";
|
||||
EnvironmentFile = cfg.secretFile;
|
||||
};
|
||||
script = ''
|
||||
pgbackrest --stanza=${cfg.stanza} backup --type=diff
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.pgbackrest-backup-diff = {
|
||||
wantedBy = ["timers.target"];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.schedule.diff;
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = "5m";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/pgbackrest 0750 postgres postgres -"
|
||||
"d /var/log/pgbackrest 0750 postgres postgres -"
|
||||
"d /var/spool/pgbackrest 0750 postgres postgres -"
|
||||
];
|
||||
});
|
||||
}
|
||||
15
modules/secrets.nix
Normal file
15
modules/secrets.nix
Normal file
@@ -0,0 +1,15 @@
|
||||
{inputs, ...}: {
|
||||
# Import sops-nix modules into den.default per-class
|
||||
den.default.nixos.imports = [inputs.sops-nix.nixosModules.sops];
|
||||
den.default.darwin.imports = [inputs.sops-nix.darwinModules.sops];
|
||||
|
||||
# Configure NixOS SOPS defaults
|
||||
den.default.nixos.sops.age.sshKeyPaths = ["/etc/ssh/ssh_host_ed25519_key"];
|
||||
|
||||
# Configure Darwin SOPS defaults
|
||||
den.default.darwin = {
|
||||
sops.age.keyFile = "/Users/cschmatzler/.config/sops/age/keys.txt";
|
||||
sops.age.sshKeyPaths = [];
|
||||
sops.gnupg.sshKeyPaths = [];
|
||||
};
|
||||
}
|
||||
285
modules/shell.nix
Normal file
285
modules/shell.nix
Normal file
@@ -0,0 +1,285 @@
|
||||
{...}: {
|
||||
den.aspects.shell.homeManager = {
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
programs.nushell = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
show_banner = false;
|
||||
completions = {
|
||||
algorithm = "fuzzy";
|
||||
case_sensitive = false;
|
||||
};
|
||||
history = {
|
||||
file_format = "sqlite";
|
||||
};
|
||||
};
|
||||
|
||||
environmentVariables = {
|
||||
COLORTERM = "truecolor";
|
||||
COLORFGBG = "15;0";
|
||||
TERM_BACKGROUND = "light";
|
||||
EDITOR = "nvim";
|
||||
};
|
||||
|
||||
extraEnv =
|
||||
''
|
||||
$env.LS_COLORS = (${pkgs.vivid}/bin/vivid generate catppuccin-latte)
|
||||
''
|
||||
+ lib.optionalString pkgs.stdenv.isDarwin ''
|
||||
# Nushell on Darwin doesn't source /etc/zprofile or path_helper,
|
||||
# so nix-managed paths must be added explicitly.
|
||||
$env.PATH = ($env.PATH | split row (char esep) | prepend "/run/current-system/sw/bin" | prepend $"($env.HOME)/.nix-profile/bin")
|
||||
'';
|
||||
|
||||
extraConfig = ''
|
||||
# --- Catppuccin Latte Theme ---
|
||||
let theme = {
|
||||
rosewater: "#dc8a78"
|
||||
flamingo: "#dd7878"
|
||||
pink: "#ea76cb"
|
||||
mauve: "#8839ef"
|
||||
red: "#d20f39"
|
||||
maroon: "#e64553"
|
||||
peach: "#fe640b"
|
||||
yellow: "#df8e1d"
|
||||
green: "#40a02b"
|
||||
teal: "#179299"
|
||||
sky: "#04a5e5"
|
||||
sapphire: "#209fb5"
|
||||
blue: "#1e66f5"
|
||||
lavender: "#7287fd"
|
||||
text: "#4c4f69"
|
||||
subtext1: "#5c5f77"
|
||||
subtext0: "#6c6f85"
|
||||
overlay2: "#7c7f93"
|
||||
overlay1: "#8c8fa1"
|
||||
overlay0: "#9ca0b0"
|
||||
surface2: "#acb0be"
|
||||
surface1: "#bcc0cc"
|
||||
surface0: "#ccd0da"
|
||||
base: "#eff1f5"
|
||||
mantle: "#e6e9ef"
|
||||
crust: "#dce0e8"
|
||||
}
|
||||
|
||||
let scheme = {
|
||||
recognized_command: $theme.blue
|
||||
unrecognized_command: $theme.text
|
||||
constant: $theme.peach
|
||||
punctuation: $theme.overlay2
|
||||
operator: $theme.sky
|
||||
string: $theme.green
|
||||
virtual_text: $theme.surface2
|
||||
variable: { fg: $theme.flamingo attr: i }
|
||||
filepath: $theme.yellow
|
||||
}
|
||||
|
||||
$env.config.color_config = {
|
||||
separator: { fg: $theme.surface2 attr: b }
|
||||
leading_trailing_space_bg: { fg: $theme.lavender attr: u }
|
||||
header: { fg: $theme.text attr: b }
|
||||
row_index: $scheme.virtual_text
|
||||
record: $theme.text
|
||||
list: $theme.text
|
||||
hints: $scheme.virtual_text
|
||||
search_result: { fg: $theme.base bg: $theme.yellow }
|
||||
shape_closure: $theme.teal
|
||||
closure: $theme.teal
|
||||
shape_flag: { fg: $theme.maroon attr: i }
|
||||
shape_matching_brackets: { attr: u }
|
||||
shape_garbage: $theme.red
|
||||
shape_keyword: $theme.mauve
|
||||
shape_match_pattern: $theme.green
|
||||
shape_signature: $theme.teal
|
||||
shape_table: $scheme.punctuation
|
||||
cell-path: $scheme.punctuation
|
||||
shape_list: $scheme.punctuation
|
||||
shape_record: $scheme.punctuation
|
||||
shape_vardecl: $scheme.variable
|
||||
shape_variable: $scheme.variable
|
||||
empty: { attr: n }
|
||||
filesize: {||
|
||||
if $in < 1kb {
|
||||
$theme.teal
|
||||
} else if $in < 10kb {
|
||||
$theme.green
|
||||
} else if $in < 100kb {
|
||||
$theme.yellow
|
||||
} else if $in < 10mb {
|
||||
$theme.peach
|
||||
} else if $in < 100mb {
|
||||
$theme.maroon
|
||||
} else if $in < 1gb {
|
||||
$theme.red
|
||||
} else {
|
||||
$theme.mauve
|
||||
}
|
||||
}
|
||||
duration: {||
|
||||
if $in < 1day {
|
||||
$theme.teal
|
||||
} else if $in < 1wk {
|
||||
$theme.green
|
||||
} else if $in < 4wk {
|
||||
$theme.yellow
|
||||
} else if $in < 12wk {
|
||||
$theme.peach
|
||||
} else if $in < 24wk {
|
||||
$theme.maroon
|
||||
} else if $in < 52wk {
|
||||
$theme.red
|
||||
} else {
|
||||
$theme.mauve
|
||||
}
|
||||
}
|
||||
datetime: {|| (date now) - $in |
|
||||
if $in < 1day {
|
||||
$theme.teal
|
||||
} else if $in < 1wk {
|
||||
$theme.green
|
||||
} else if $in < 4wk {
|
||||
$theme.yellow
|
||||
} else if $in < 12wk {
|
||||
$theme.peach
|
||||
} else if $in < 24wk {
|
||||
$theme.maroon
|
||||
} else if $in < 52wk {
|
||||
$theme.red
|
||||
} else {
|
||||
$theme.mauve
|
||||
}
|
||||
}
|
||||
shape_external: $scheme.unrecognized_command
|
||||
shape_internalcall: $scheme.recognized_command
|
||||
shape_external_resolved: $scheme.recognized_command
|
||||
shape_block: $scheme.recognized_command
|
||||
block: $scheme.recognized_command
|
||||
shape_custom: $theme.pink
|
||||
custom: $theme.pink
|
||||
background: $theme.base
|
||||
foreground: $theme.text
|
||||
cursor: { bg: $theme.rosewater fg: $theme.base }
|
||||
shape_range: $scheme.operator
|
||||
range: $scheme.operator
|
||||
shape_pipe: $scheme.operator
|
||||
shape_operator: $scheme.operator
|
||||
shape_redirection: $scheme.operator
|
||||
glob: $scheme.filepath
|
||||
shape_directory: $scheme.filepath
|
||||
shape_filepath: $scheme.filepath
|
||||
shape_glob_interpolation: $scheme.filepath
|
||||
shape_globpattern: $scheme.filepath
|
||||
shape_int: $scheme.constant
|
||||
int: $scheme.constant
|
||||
bool: $scheme.constant
|
||||
float: $scheme.constant
|
||||
nothing: $scheme.constant
|
||||
binary: $scheme.constant
|
||||
shape_nothing: $scheme.constant
|
||||
shape_bool: $scheme.constant
|
||||
shape_float: $scheme.constant
|
||||
shape_binary: $scheme.constant
|
||||
shape_datetime: $scheme.constant
|
||||
shape_literal: $scheme.constant
|
||||
string: $scheme.string
|
||||
shape_string: $scheme.string
|
||||
shape_string_interpolation: $theme.flamingo
|
||||
shape_raw_string: $scheme.string
|
||||
shape_externalarg: $scheme.string
|
||||
}
|
||||
$env.config.highlight_resolved_externals = true
|
||||
$env.config.explore = {
|
||||
status_bar_background: { fg: $theme.text, bg: $theme.mantle },
|
||||
command_bar_text: { fg: $theme.text },
|
||||
highlight: { fg: $theme.base, bg: $theme.yellow },
|
||||
status: {
|
||||
error: $theme.red,
|
||||
warn: $theme.yellow,
|
||||
info: $theme.blue,
|
||||
},
|
||||
selected_cell: { bg: $theme.blue fg: $theme.base },
|
||||
}
|
||||
|
||||
# --- Custom Commands ---
|
||||
def --env open_project [] {
|
||||
let base = ($env.HOME | path join "Projects")
|
||||
let choice = (
|
||||
${pkgs.fd}/bin/fd -t d -d 1 -a . ($base | path join "Personal") ($base | path join "Work")
|
||||
| lines
|
||||
| each {|p| $p | str replace $"($base)/" "" }
|
||||
| str join "\n"
|
||||
| ${pkgs.fzf}/bin/fzf --prompt "project > "
|
||||
)
|
||||
if ($choice | str trim | is-not-empty) {
|
||||
cd ($base | path join ($choice | str trim))
|
||||
}
|
||||
}
|
||||
|
||||
# --- Keybinding: Ctrl+O for open_project ---
|
||||
$env.config.keybindings = ($env.config.keybindings | append [
|
||||
{
|
||||
name: open_project
|
||||
modifier: control
|
||||
keycode: char_o
|
||||
mode: [emacs vi_insert vi_normal]
|
||||
event: {
|
||||
send: executehostcommand
|
||||
cmd: "open_project"
|
||||
}
|
||||
}
|
||||
])
|
||||
'';
|
||||
};
|
||||
|
||||
programs.zsh = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
programs.starship = {
|
||||
enable = true;
|
||||
enableNushellIntegration = true;
|
||||
settings = {
|
||||
format = "$directory\${custom.scm}$hostname$line_break$character";
|
||||
buf = {
|
||||
disabled = true;
|
||||
};
|
||||
character = {
|
||||
error_symbol = "[](bold red)";
|
||||
success_symbol = "[](bold green)";
|
||||
};
|
||||
directory = {
|
||||
truncate_to_repo = false;
|
||||
};
|
||||
git_branch = {
|
||||
disabled = true;
|
||||
symbol = " ";
|
||||
truncation_length = 18;
|
||||
};
|
||||
git_status = {
|
||||
disabled = true;
|
||||
};
|
||||
git_commit = {
|
||||
disabled = true;
|
||||
};
|
||||
git_state = {
|
||||
disabled = true;
|
||||
};
|
||||
custom.scm = {
|
||||
when = "jj-starship detect";
|
||||
shell = ["jj-starship" "--strip-bookmark-prefix" "cschmatzler/" "--truncate-name" "20" "--bookmarks-display-limit" "1"];
|
||||
format = "$output ";
|
||||
};
|
||||
lua = {
|
||||
symbol = " ";
|
||||
};
|
||||
package = {
|
||||
disabled = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
27
modules/ssh-client.nix
Normal file
27
modules/ssh-client.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{...}: {
|
||||
den.aspects.ssh-client.homeManager = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
includes = [
|
||||
(lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/config_external")
|
||||
(lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/config_external")
|
||||
];
|
||||
matchBlocks = {
|
||||
"*" = {};
|
||||
"github.com" = {
|
||||
identitiesOnly = true;
|
||||
identityFile = [
|
||||
(lib.mkIf pkgs.stdenv.hostPlatform.isLinux "/home/${config.home.username}/.ssh/id_ed25519")
|
||||
(lib.mkIf pkgs.stdenv.hostPlatform.isDarwin "/Users/${config.home.username}/.ssh/id_ed25519")
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
89
modules/tahani.nix
Normal file
89
modules/tahani.nix
Normal file
@@ -0,0 +1,89 @@
|
||||
{den, ...}: {
|
||||
den.aspects.tahani.includes = [
|
||||
den.aspects.nixos-system
|
||||
den.aspects.core
|
||||
den.aspects.openssh
|
||||
den.aspects.tailscale
|
||||
den.aspects.terminal
|
||||
den.aspects.email
|
||||
den.aspects.atuin
|
||||
den.aspects.dev-tools
|
||||
den.aspects.neovim
|
||||
den.aspects.ai-tools
|
||||
den.aspects.zellij
|
||||
den.aspects.zk
|
||||
];
|
||||
|
||||
den.aspects.tahani.nixos = {...}: {
|
||||
imports = [
|
||||
./_hosts/tahani/adguardhome.nix
|
||||
./_hosts/tahani/cache.nix
|
||||
./_hosts/tahani/networking.nix
|
||||
./_hosts/tahani/paperless.nix
|
||||
];
|
||||
|
||||
networking.hostName = "tahani";
|
||||
|
||||
sops.secrets = {
|
||||
tahani-paperless-password = {
|
||||
sopsFile = ../secrets/tahani-paperless-password;
|
||||
format = "binary";
|
||||
};
|
||||
tahani-email-password = {
|
||||
sopsFile = ../secrets/tahani-email-password;
|
||||
format = "binary";
|
||||
owner = "cschmatzler";
|
||||
};
|
||||
};
|
||||
virtualisation.docker.enable = true;
|
||||
users.users.cschmatzler.extraGroups = ["docker"];
|
||||
swapDevices = [
|
||||
{
|
||||
device = "/swapfile";
|
||||
size = 16 * 1024;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
den.aspects.tahani.homeManager = {
|
||||
pkgs,
|
||||
inputs',
|
||||
...
|
||||
}: let
|
||||
opencode = inputs'.llm-agents.packages.opencode;
|
||||
in {
|
||||
programs.git.settings.user.email = "christoph@schmatzler.com";
|
||||
|
||||
# Auto-start zellij in nushell on tahani (headless server)
|
||||
programs.nushell.extraConfig = ''
|
||||
if 'ZELLIJ' not-in ($env | columns) {
|
||||
zellij
|
||||
}
|
||||
'';
|
||||
|
||||
# Inbox-triage systemd service
|
||||
systemd.user.services.opencode-inbox-triage = {
|
||||
Unit = {
|
||||
Description = "OpenCode inbox triage";
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${opencode}/bin/opencode run --command inbox-triage";
|
||||
Environment = "PATH=${pkgs.himalaya}/bin:${opencode}/bin:${pkgs.coreutils}/bin";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.timers.opencode-inbox-triage = {
|
||||
Unit = {
|
||||
Description = "Run OpenCode inbox triage every 10 minutes";
|
||||
};
|
||||
Timer = {
|
||||
OnCalendar = "*:0/10";
|
||||
Persistent = true;
|
||||
};
|
||||
Install = {
|
||||
WantedBy = ["timers.target"];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
129
modules/terminal.nix
Normal file
129
modules/terminal.nix
Normal file
@@ -0,0 +1,129 @@
|
||||
{...}: {
|
||||
den.aspects.terminal.homeManager = {pkgs, ...}: {
|
||||
xdg.configFile."ghostty/config".text = ''
|
||||
command = ${pkgs.nushell}/bin/nu
|
||||
theme = Catppuccin Latte
|
||||
window-padding-x = 12
|
||||
window-padding-y = 3
|
||||
window-padding-balance = true
|
||||
font-family = TX-02
|
||||
font-size = 16.5
|
||||
cursor-style = block
|
||||
mouse-hide-while-typing = true
|
||||
mouse-scroll-multiplier = 1.25
|
||||
shell-integration = none
|
||||
shell-integration-features = no-cursor
|
||||
clipboard-read = allow
|
||||
clipboard-write = allow
|
||||
'';
|
||||
|
||||
programs.bat = {
|
||||
enable = true;
|
||||
config = {
|
||||
theme = "Catppuccin Latte";
|
||||
pager = "ov";
|
||||
};
|
||||
themes = {
|
||||
"Catppuccin Latte" = {
|
||||
src =
|
||||
pkgs.fetchFromGitHub {
|
||||
owner = "catppuccin";
|
||||
repo = "bat";
|
||||
rev = "6810349b28055dce54076712fc05fc68da4b8ec0";
|
||||
sha256 = "lJapSgRVENTrbmpVyn+UQabC9fpV1G1e+CdlJ090uvg=";
|
||||
};
|
||||
file = "themes/Catppuccin Latte.tmTheme";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
programs.fzf = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
home.sessionVariables = {
|
||||
FZF_DEFAULT_OPTS = ''
|
||||
--bind=alt-k:up,alt-j:down
|
||||
--expect=tab,enter
|
||||
--layout=reverse
|
||||
--delimiter='\t'
|
||||
--with-nth=1
|
||||
--preview-window='border-rounded' --prompt=' ' --marker=' ' --pointer=' '
|
||||
--separator='─' --scrollbar='┃' --layout='reverse'
|
||||
|
||||
--color=bg+:#CCD0DA,bg:#EFF1F5,spinner:#DC8A78,hl:#D20F39
|
||||
--color=fg:#4C4F69,header:#D20F39,info:#8839EF,pointer:#DC8A78
|
||||
--color=marker:#7287FD,fg+:#4C4F69,prompt:#8839EF,hl+:#D20F39
|
||||
--color=selected-bg:#BCC0CC
|
||||
--color=border:#9CA0B0,label:#4C4F69
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ripgrep = {
|
||||
enable = true;
|
||||
arguments = [
|
||||
"--max-columns=150"
|
||||
"--max-columns-preview"
|
||||
"--hidden"
|
||||
"--smart-case"
|
||||
"--colors=column:none"
|
||||
"--colors=column:fg:4"
|
||||
"--colors=column:style:underline"
|
||||
"--colors=line:none"
|
||||
"--colors=line:fg:4"
|
||||
"--colors=match:none"
|
||||
"--colors=match:bg:0"
|
||||
"--colors=match:fg:6"
|
||||
"--colors=path:none"
|
||||
"--colors=path:fg:14"
|
||||
"--colors=path:style:bold"
|
||||
];
|
||||
};
|
||||
|
||||
programs.zoxide = {
|
||||
enable = true;
|
||||
enableNushellIntegration = true;
|
||||
};
|
||||
|
||||
programs.yazi = {
|
||||
enable = true;
|
||||
enableNushellIntegration = true;
|
||||
shellWrapperName = "y";
|
||||
settings = {
|
||||
manager = {
|
||||
show_hidden = true;
|
||||
sort_by = "natural";
|
||||
sort_dir_first = true;
|
||||
};
|
||||
};
|
||||
theme = {
|
||||
tabs = {
|
||||
sep_inner = {
|
||||
open = "";
|
||||
close = "";
|
||||
};
|
||||
sep_outer = {
|
||||
open = "";
|
||||
close = "";
|
||||
};
|
||||
};
|
||||
indicator = {
|
||||
padding = {
|
||||
open = "";
|
||||
close = "";
|
||||
};
|
||||
};
|
||||
status = {
|
||||
sep_left = {
|
||||
open = "";
|
||||
close = "";
|
||||
};
|
||||
sep_right = {
|
||||
open = "";
|
||||
close = "";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
27
modules/user.nix
Normal file
27
modules/user.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{den, ...}: {
|
||||
den.aspects.cschmatzler.includes = [
|
||||
den.provides.primary-user
|
||||
den.aspects.shell
|
||||
den.aspects.ssh-client
|
||||
];
|
||||
|
||||
den.aspects.cschmatzler.homeManager = {
|
||||
lib,
|
||||
pkgs,
|
||||
inputs',
|
||||
...
|
||||
}: {
|
||||
programs.home-manager.enable = true;
|
||||
|
||||
home.packages = pkgs.callPackage ./_lib/packages.nix {inputs = inputs';};
|
||||
|
||||
home.activation =
|
||||
lib.mkIf pkgs.stdenv.isDarwin {
|
||||
"setWallpaper" =
|
||||
lib.hm.dag.entryAfter ["revealHomeLibraryDirectory"] ''
|
||||
echo "[+] Setting wallpaper"
|
||||
${import ./_lib/wallpaper.nix {inherit pkgs;}}/bin/set-wallpaper-script
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
54
modules/zellij.nix
Normal file
54
modules/zellij.nix
Normal file
@@ -0,0 +1,54 @@
|
||||
{...}: {
|
||||
den.aspects.zellij.homeManager = {pkgs, ...}: {
|
||||
programs.zellij = {
|
||||
enable = true;
|
||||
settings = {
|
||||
theme = "catppuccin-latte";
|
||||
default_layout = "default";
|
||||
default_shell = "${pkgs.nushell}/bin/nu";
|
||||
pane_frames = false;
|
||||
show_startup_tips = false;
|
||||
show_release_notes = false;
|
||||
};
|
||||
};
|
||||
|
||||
xdg.configFile."zellij/layouts/default.kdl".text = ''
|
||||
layout {
|
||||
default_tab_template {
|
||||
pane split_direction="vertical" {
|
||||
pane
|
||||
}
|
||||
|
||||
pane size=1 borderless=true {
|
||||
plugin location="file:${pkgs.zjstatus}/bin/zjstatus.wasm" {
|
||||
hide_frame_for_single_pane "true"
|
||||
|
||||
format_left "{mode}#[fg=#1e66f5,bg=#eff1f5,bold] {session}#[bg=#eff1f5] {tabs}"
|
||||
format_right "{datetime}"
|
||||
format_space "#[bg=#eff1f5]"
|
||||
|
||||
mode_normal "#[fg=#eff1f5,bg=#1e66f5] "
|
||||
mode_locked "#[fg=#eff1f5,bg=#fe640b] L "
|
||||
mode_tab "#[fg=#eff1f5,bg=#40a02b] T "
|
||||
mode_pane "#[fg=#eff1f5,bg=#8839ef] P "
|
||||
mode_session "#[fg=#eff1f5,bg=#04a5e5] S "
|
||||
mode_resize "#[fg=#eff1f5,bg=#df8e1d] R "
|
||||
mode_move "#[fg=#eff1f5,bg=#ea76cb] M "
|
||||
mode_search "#[fg=#eff1f5,bg=#d20f39] S "
|
||||
|
||||
tab_normal "#[fg=#acb0be,bg=#eff1f5] {index} {name} {fullscreen_indicator}{sync_indicator}{floating_indicator}"
|
||||
tab_active "#[fg=#eff1f5,bg=#1e66f5,bold,underline] {index} {name} {fullscreen_indicator}{sync_indicator}{floating_indicator}"
|
||||
tab_fullscreen_indicator "□ "
|
||||
tab_sync_indicator " "
|
||||
tab_floating_indicator " "
|
||||
|
||||
datetime "#[fg=#4c4f69,bg=#eff1f5] {format} "
|
||||
datetime_format "%A, %d %b %Y %H:%M"
|
||||
datetime_timezone "Europe/Berlin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'';
|
||||
};
|
||||
}
|
||||
11
modules/zk.nix
Normal file
11
modules/zk.nix
Normal file
@@ -0,0 +1,11 @@
|
||||
{...}: {
|
||||
den.aspects.zk.homeManager = {...}: {
|
||||
programs.zk = {
|
||||
enable = true;
|
||||
settings = {};
|
||||
};
|
||||
home.sessionVariables = {
|
||||
ZK_NOTEBOOK_DIR = "$HOME/Projects/Personal/Zettelkasten";
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user