Kieran's opinionated (and probably slightly dumb) nix config

Compare changes

Choose any two refs to compare.

+8
.github/workflows/deploy.yaml
···
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@main
- name: Setup Tailscale
uses: tailscale/github-action@v3
···
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@main
+
with:
+
extra-conf: |
+
extra-platforms = aarch64-linux
+
+
- name: Set up QEMU
+
uses: docker/setup-qemu-action@v3
+
with:
+
platforms: arm64
- name: Setup Tailscale
uses: tailscale/github-action@v3
-1
README.md
···
โ”œโ”€โ”€ machines
โ”‚ โ”œโ”€โ”€ atalanta # my macOS M4 machine
โ”‚ โ”œโ”€โ”€ ember # my dell r210 server (in my basement)
-
โ”‚ โ”œโ”€โ”€ john # shared server for cedarville
โ”‚ โ”œโ”€โ”€ moonlark # my framework 13 <dead>
โ”‚ โ”œโ”€โ”€ nest # shared tilde server through hc
โ”‚ โ”œโ”€โ”€ prattle # oracle cloud x86_64 server
···
โ”œโ”€โ”€ machines
โ”‚ โ”œโ”€โ”€ atalanta # my macOS M4 machine
โ”‚ โ”œโ”€โ”€ ember # my dell r210 server (in my basement)
โ”‚ โ”œโ”€โ”€ moonlark # my framework 13 <dead>
โ”‚ โ”œโ”€โ”€ nest # shared tilde server through hc
โ”‚ โ”œโ”€โ”€ prattle # oracle cloud x86_64 server
+101 -49
flake.lock
···
"utils": "utils"
},
"locked": {
-
"lastModified": 1762286984,
-
"narHash": "sha256-9I2H9x5We6Pl+DBYHjR1s3UT8wgwcpAH03kn9CqtdQc=",
"owner": "serokell",
"repo": "deploy-rs",
-
"rev": "9c870f63e28ec1e83305f7f6cb73c941e699f74f",
"type": "github"
},
"original": {
···
]
},
"locked": {
-
"lastModified": 1764627417,
-
"narHash": "sha256-D6xc3Rl8Ab6wucJWdvjNsGYGSxNjQHzRc2EZ6eeQ6l4=",
"owner": "nix-community",
"repo": "disko",
-
"rev": "5a88a6eceb8fd732b983e72b732f6f4b8269bef3",
"type": "github"
},
"original": {
···
},
"flake-utils_5": {
"inputs": {
-
"systems": "systems_7"
},
"locked": {
-
"lastModified": 1731533236,
-
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
-
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
···
"systems": "systems_9"
},
"locked": {
-
"lastModified": 1694529238,
-
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
-
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
···
},
"frc-nix": {
"inputs": {
-
"flake-utils": "flake-utils_4",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
-
"lastModified": 1764827700,
-
"narHash": "sha256-9xpbmlCXEYU3rWv23OT4qcW0lyeelud0ZrNRicqoTkQ=",
"owner": "frc4451",
"repo": "frc-nix",
-
"rev": "2a52c5c29093d1cd9494ac0e5b7c70ef970ef7ff",
"type": "github"
},
"original": {
···
},
"gomod2nix": {
"inputs": {
-
"flake-utils": "flake-utils_6",
"nixpkgs": [
"tangled",
"nixpkgs"
···
]
},
"locked": {
-
"lastModified": 1765170903,
-
"narHash": "sha256-O8VTGey1xxiRW+Fpb+Ps9zU7ShmxUA1a7cMTcENCVNg=",
"owner": "nix-community",
"repo": "home-manager",
-
"rev": "20561be440a11ec57a89715480717baf19fe6343",
"type": "github"
},
"original": {
···
]
},
"locked": {
-
"lastModified": 1765113580,
-
"narHash": "sha256-b8YOwGDFprkQJjXsKGuSNS1pWe8w4cUW36YxlUelNpU=",
"owner": "hyprwm",
"repo": "contrib",
-
"rev": "db18f83bebbc2cf43a21dbb26cd99aabe672d923",
"type": "github"
},
"original": {
···
"nixpkgs": "nixpkgs_3"
},
"locked": {
-
"lastModified": 1765159287,
-
"narHash": "sha256-C+dVEekU31QPaPShMaUbs3LqOVVqzq0b4gKC1jX8Mlk=",
"owner": "nix-community",
"repo": "nix-vscode-extensions",
-
"rev": "dccd0cc3693bff67e4856b5a22445223aabc4d4b",
"type": "github"
},
"original": {
···
},
"nixos-facter-modules": {
"locked": {
-
"lastModified": 1764252389,
-
"narHash": "sha256-3bbuneTKZBkYXlm0bE36kUjiDsasoIC1GWBw/UEJ9T4=",
"owner": "numtide",
"repo": "nixos-facter-modules",
-
"rev": "5ea68886d95218646d11d3551a476d458df00778",
"type": "github"
},
"original": {
···
},
"nixpkgs-unstable": {
"locked": {
-
"lastModified": 1764950072,
-
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "f61125a668a320878494449750330ca58b78c557",
"type": "github"
},
"original": {
···
},
"nixpkgs_4": {
"locked": {
-
"lastModified": 1764983851,
-
"narHash": "sha256-y7RPKl/jJ/KAP/VKLMghMgXTlvNIJMHKskl8/Uuar7o=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "d9bc5c7dceb30d8d6fafa10aeb6aa8a48c218454",
"type": "github"
},
"original": {
···
"type": "github"
}
},
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
···
]
},
"locked": {
-
"lastModified": 1765213466,
-
"narHash": "sha256-JdQa7m3a/oWun8TGJ+jamAdxn820RFjqDLNnl4d8a+0=",
"owner": "nix-community",
"repo": "NUR",
-
"rev": "0c5cabc4f46e5ce7e45827c22b21173a887acff2",
"type": "github"
},
"original": {
···
},
"nuschtosSearch": {
"inputs": {
-
"flake-utils": "flake-utils_5",
"ixx": "ixx",
"nixpkgs": [
"nixvim",
···
"spicetify-nix": "spicetify-nix",
"tangled": "tangled",
"terminal-wakatime": "terminal-wakatime",
-
"wakatime-ls": "wakatime-ls"
}
},
"rust-overlay": {
···
"nixpkgs": [
"nixpkgs"
],
-
"systems": "systems_8"
},
"locked": {
-
"lastModified": 1765082296,
-
"narHash": "sha256-EcefoixU9ht+P6QB/TfjLY9E3MdJVfeSec6G8Ges0pA=",
"owner": "Gerg-L",
"repo": "spicetify-nix",
-
"rev": "ac4927ea1ec7e7ea3635a1d8b933106a596c4356",
"type": "github"
},
"original": {
···
"sqlite-lib-src": "sqlite-lib-src"
},
"locked": {
-
"lastModified": 1765171220,
-
"narHash": "sha256-K+Cs6k0nQYRwW+RwlKCZabLBOVel84C2wPEZjYOH6JA=",
"ref": "refs/heads/master",
-
"rev": "ca8217e99806280fa77316b46b0b243647ed491c",
-
"revCount": 1722,
"type": "git",
"url": "https://tangled.org/tangled.org/core"
},
···
"original": {
"owner": "mrnossiom",
"repo": "wakatime-ls",
"type": "github"
}
}
···
"utils": "utils"
},
"locked": {
+
"lastModified": 1766051518,
+
"narHash": "sha256-znKOwPXQnt3o7lDb3hdf19oDo0BLP4MfBOYiWkEHoik=",
"owner": "serokell",
"repo": "deploy-rs",
+
"rev": "d5eff7f948535b9c723d60cd8239f8f11ddc90fa",
"type": "github"
},
"original": {
···
]
},
"locked": {
+
"lastModified": 1765794845,
+
"narHash": "sha256-YD5QWlGnusNbZCqR3pxG8tRxx9yUXayLZfAJRWspq2s=",
"owner": "nix-community",
"repo": "disko",
+
"rev": "7194cfe5b7a3660726b0fe7296070eaef601cae9",
"type": "github"
},
"original": {
···
},
"flake-utils_5": {
"inputs": {
+
"systems": "systems_8"
},
"locked": {
+
"lastModified": 1694529238,
+
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
+
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
···
"systems": "systems_9"
},
"locked": {
+
"lastModified": 1731533236,
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
···
},
"frc-nix": {
"inputs": {
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
+
"lastModified": 1765909964,
+
"narHash": "sha256-KQkzB9ZH8zBbby6ac+5IOiJ6ZlEumaMxyrVkQZHAnM0=",
"owner": "frc4451",
"repo": "frc-nix",
+
"rev": "62539cce28a168018ab3e8cee933d9e18631c810",
"type": "github"
},
"original": {
···
},
"gomod2nix": {
"inputs": {
+
"flake-utils": "flake-utils_5",
"nixpkgs": [
"tangled",
"nixpkgs"
···
]
},
"locked": {
+
"lastModified": 1765979862,
+
"narHash": "sha256-/r9/1KamvbHJx6I40H4HsSXnEcBAkj46ZwibhBx9kg0=",
"owner": "nix-community",
"repo": "home-manager",
+
"rev": "d3135ab747fd9dac250ffb90b4a7e80634eacbe9",
"type": "github"
},
"original": {
···
]
},
"locked": {
+
"lastModified": 1766066098,
+
"narHash": "sha256-d3HmUbmfTDIt9mXEHszqyo2byqQMoyJtUJCZ9U1IqHQ=",
"owner": "hyprwm",
"repo": "contrib",
+
"rev": "41dbcac8183bb1b3a4ade0d8276b2f2df6ae4690",
"type": "github"
},
"original": {
···
"nixpkgs": "nixpkgs_3"
},
"locked": {
+
"lastModified": 1766057406,
+
"narHash": "sha256-0OaDiJCjxTDevUcz8rd+Ka9guj5YG4QgR5FtPYWg5jo=",
"owner": "nix-community",
"repo": "nix-vscode-extensions",
+
"rev": "f42f162adc29cb55d1651a3df81cfcde62308716",
"type": "github"
},
"original": {
···
},
"nixos-facter-modules": {
"locked": {
+
"lastModified": 1765442039,
+
"narHash": "sha256-k3lYQ+A1F7aTz8HnlU++bd9t/x/NP2A4v9+x6opcVg0=",
"owner": "numtide",
"repo": "nixos-facter-modules",
+
"rev": "9dd775ee92de63f14edd021d59416e18ac2c00f1",
"type": "github"
},
"original": {
···
},
"nixpkgs-unstable": {
"locked": {
+
"lastModified": 1765779637,
+
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "nixos",
"repo": "nixpkgs",
+
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
"type": "github"
},
"original": {
···
},
"nixpkgs_4": {
"locked": {
+
"lastModified": 1765838191,
+
"narHash": "sha256-m5KWt1nOm76ILk/JSCxBM4MfK3rYY7Wq9/TZIIeGnT8=",
"owner": "nixos",
"repo": "nixpkgs",
+
"rev": "c6f52ebd45e5925c188d1a20119978aa4ffd5ef6",
"type": "github"
},
"original": {
···
"type": "github"
}
},
+
"nixpkgs_8": {
+
"locked": {
+
"lastModified": 1764635402,
+
"narHash": "sha256-6rYcajRLe2C5ZYnV1HYskJl+QAkhvseWTzbdQiTN9OI=",
+
"owner": "nixos",
+
"repo": "nixpkgs",
+
"rev": "5f53b0d46d320352684242d000b36dcfbbf7b0bc",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nixos",
+
"repo": "nixpkgs",
+
"type": "github"
+
}
+
},
"nixvim": {
"inputs": {
"flake-parts": "flake-parts",
···
]
},
"locked": {
+
"lastModified": 1766071242,
+
"narHash": "sha256-hqD/pvTosQZLCcwHTxf2x20H/rHbmFlMovdjbtG887E=",
"owner": "nix-community",
"repo": "NUR",
+
"rev": "a8ec94a89518167e9c61f3c57aee6578246da35c",
"type": "github"
},
"original": {
···
},
"nuschtosSearch": {
"inputs": {
+
"flake-utils": "flake-utils_4",
"ixx": "ixx",
"nixpkgs": [
"nixvim",
···
"spicetify-nix": "spicetify-nix",
"tangled": "tangled",
"terminal-wakatime": "terminal-wakatime",
+
"wakatime-ls": "wakatime-ls",
+
"zmx": "zmx"
}
},
"rust-overlay": {
···
"nixpkgs": [
"nixpkgs"
],
+
"systems": "systems_7"
},
"locked": {
+
"lastModified": 1765687197,
+
"narHash": "sha256-5aJgT+lEC7ypuAGE3DQLj3LzYDQ+kRG6MnkVr3ZF9RU=",
"owner": "Gerg-L",
"repo": "spicetify-nix",
+
"rev": "fa6a5dde9d95bf7b8f075ff5aceeb1d97fa9043a",
"type": "github"
},
"original": {
···
"sqlite-lib-src": "sqlite-lib-src"
},
"locked": {
+
"lastModified": 1766055661,
+
"narHash": "sha256-QCFHtHZahijBUtqPvno7EUnMlsIHbtxieYIq2wY4NZw=",
"ref": "refs/heads/master",
+
"rev": "28bb7a936df5ed3065e42c8522984441c9dfb8f7",
+
"revCount": 1754,
"type": "git",
"url": "https://tangled.org/tangled.org/core"
},
···
"original": {
"owner": "mrnossiom",
"repo": "wakatime-ls",
+
"type": "github"
+
}
+
},
+
"zig2nix": {
+
"inputs": {
+
"flake-utils": "flake-utils_6",
+
"nixpkgs": "nixpkgs_8"
+
},
+
"locked": {
+
"lastModified": 1764678235,
+
"narHash": "sha256-NNQWR3DAufaH7fs6ZplfAv1xPHEc0Ne3Z0v4MNHCqSw=",
+
"owner": "Cloudef",
+
"repo": "zig2nix",
+
"rev": "8b6ec85bccdf6b91ded19e9ef671205937e271e6",
+
"type": "github"
+
},
+
"original": {
+
"owner": "Cloudef",
+
"repo": "zig2nix",
+
"type": "github"
+
}
+
},
+
"zmx": {
+
"inputs": {
+
"zig2nix": "zig2nix"
+
},
+
"locked": {
+
"lastModified": 1765941419,
+
"narHash": "sha256-rkkTsi2P5BTZldQQvVwMUmyq3gqn5EK9NP7dvI3P6Hw=",
+
"owner": "neurosnap",
+
"repo": "zmx",
+
"rev": "d34f8d6f0a3b8ac83de35d354ea3ac1ddfd95b87",
+
"type": "github"
+
},
+
"original": {
+
"owner": "neurosnap",
+
"repo": "zmx",
"type": "github"
}
}
+7 -1
flake.nix
···
url = "github:taciturnaxolotl/battleship-arena";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
···
hash = "sha256-7mkrPl2CQSfc1lRjl1ilwxdYcK5iRU//QGKmdCicK30=";
};
});
})
];
};
···
modules = [
home-manager.darwinModules.home-manager
agenix.darwinModules.default
-
unstable-overlays
./machines/atalanta
];
};
···
url = "github:taciturnaxolotl/battleship-arena";
inputs.nixpkgs.follows = "nixpkgs";
};
+
+
zmx = {
+
url = "github:neurosnap/zmx";
+
};
};
outputs =
···
hash = "sha256-7mkrPl2CQSfc1lRjl1ilwxdYcK5iRU//QGKmdCicK30=";
};
});
+
+
zmx-binary = prev.callPackage ./packages/zmx.nix { };
})
];
};
···
modules = [
home-manager.darwinModules.home-manager
agenix.darwinModules.default
+
nur.modules.darwin.default
./machines/atalanta
];
};
+10
machines/atalanta/default.nix
···
config = {
allowUnfree = true;
};
};
# Enable nix-darwin
···
config = {
allowUnfree = true;
};
+
overlays = [
+
(final: prev: {
+
unstable = import inputs.nixpkgs-unstable {
+
system = final.stdenv.hostPlatform.system;
+
config.allowUnfree = true;
+
};
+
+
zmx-binary = prev.callPackage ../../packages/zmx.nix { };
+
})
+
];
};
# Enable nix-darwin
+68 -2
machines/atalanta/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/Users/kierank";
···
bore = {
enable = true;
authTokenFile = osConfig.age.secrets.frp-auth-token.path;
};
};
···
(inputs.import-tree ../../../modules/home)
];
home = {
username = "kierank";
homeDirectory = "/Users/kierank";
···
bore = {
enable = true;
authTokenFile = osConfig.age.secrets.frp-auth-token.path;
+
};
+
ssh = {
+
enable = true;
+
+
zmx = {
+
enable = true;
+
hosts = [ "t.*" "p.*" "e.*" "j.*" ];
+
};
+
+
hosts = {
+
# Dynamic zmx sessions per server
+
"t.*" = {
+
hostname = "150.136.15.177"; # terebithia
+
};
+
+
"p.*" = {
+
hostname = "150.136.63.103"; # prattle
+
};
+
+
"e.*" = {
+
hostname = "192.168.0.94"; # ember
+
};
+
+
"j.*" = {
+
hostname = "john.cedarville.edu";
+
user = "klukas";
+
};
+
+
# Regular hosts
+
john = {
+
hostname = "john.cedarville.edu";
+
user = "klukas";
+
zmx = true;
+
};
+
+
bandit = {
+
hostname = "bandit.labs.overthewire.org";
+
port = 2220;
+
};
+
+
kali = {
+
user = "kali";
+
};
+
+
terebithia = {
+
hostname = "150.136.15.177";
+
zmx = true;
+
};
+
+
prattle = {
+
hostname = "150.136.63.103";
+
zmx = true;
+
};
+
+
ember = {
+
hostname = "192.168.0.94";
+
zmx = true;
+
};
+
+
remarkable = {
+
hostname = "10.11.99.01";
+
user = "root";
+
};
+
};
+
+
extraConfig = ''
+
IdentityFile ~/.ssh/id_rsa
+
'';
};
};
+1
machines/atalanta/home-manager.nix
···
];
home-manager = {
extraSpecialArgs = {
inherit inputs outputs;
};
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+5
machines/ember/default.nix
···
{
imports = [
(inputs.import-tree ../../modules/home)
];
nixpkgs.enable = true;
···
shell.enable = true;
apps = {
helix.enable = true;
};
};
···
{
imports = [
(inputs.import-tree ../../modules/home)
+
../../modules/home/system/nixpkgs.nix.disabled
];
nixpkgs.enable = true;
···
shell.enable = true;
apps = {
helix.enable = true;
+
};
+
ssh = {
+
enable = true;
+
zmx.enable = true;
};
};
-35
machines/john/default.nix
···
-
{
-
inputs,
-
pkgs,
-
...
-
}:
-
{
-
imports = [
-
(inputs.import-tree ../../modules/home)
-
];
-
-
nixpkgs.enable = true;
-
-
home = {
-
username = "klukas";
-
homeDirectory = "/home/students/2029/klukas";
-
-
packages = with pkgs; [ ];
-
};
-
-
atelier = {
-
shell.enable = true;
-
};
-
-
# Enable home-manager
-
programs.home-manager.enable = true;
-
-
# keep hm in .local/state since we are using nix-portable
-
xdg.enable = true;
-
-
# Nicely reload system units when changing configs
-
systemd.user.startServices = "sd-switch";
-
-
# https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
-
home.stateVersion = "23.05";
-
}
···
-2
machines/moonlark/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/home/kierank";
···
(inputs.import-tree ../../../modules/home)
];
home = {
username = "kierank";
homeDirectory = "/home/kierank";
+1
machines/moonlark/home-manager.nix
···
];
home-manager = {
extraSpecialArgs = {
inherit inputs outputs;
};
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+5
machines/nest/default.nix
···
{
imports = [
(inputs.import-tree ../../modules/home)
];
nixpkgs.enable = true;
···
shell.enable = true;
apps = {
helix.enable = true;
};
};
···
{
imports = [
(inputs.import-tree ../../modules/home)
+
../../modules/home/system/nixpkgs.nix.disabled
];
nixpkgs.enable = true;
···
shell.enable = true;
apps = {
helix.enable = true;
+
};
+
ssh = {
+
enable = true;
+
zmx.enable = true;
};
};
+1 -1
machines/prattle/default.nix
···
enable = true;
package = pkgs.caddy.withPlugins {
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ];
-
hash = "sha256-ea8PC/+SlPRdEVVF/I3c1CBprlVp1nrumKM5cMwJJ3U=";
};
email = "me@dunkirk.sh";
globalConfig = ''
···
enable = true;
package = pkgs.caddy.withPlugins {
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ];
+
hash = "sha256-dnhEjopeA0UiI+XVYHYpsjcEI6Y1Hacbi28hVKYQURg=";
};
email = "me@dunkirk.sh";
globalConfig = ''
+7 -2
machines/prattle/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/home/kierank";
};
programs.home-manager.enable = true;
···
(inputs.import-tree ../../../modules/home)
];
home = {
username = "kierank";
homeDirectory = "/home/kierank";
+
};
+
+
atelier = {
+
ssh = {
+
enable = true;
+
zmx.enable = true;
+
};
};
programs.home-manager.enable = true;
+1
machines/prattle/home-manager.nix
···
];
home-manager = {
extraSpecialArgs = {
inherit inputs outputs;
};
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+1
machines/tacyon/default.nix
···
{
imports = [
(inputs.import-tree ../../modules/home)
];
nixpkgs.enable = true;
···
{
imports = [
(inputs.import-tree ../../modules/home)
+
../../modules/home/system/nixpkgs.nix.disabled
];
nixpkgs.enable = true;
+1 -1
machines/terebithia/default.nix
···
enable = true;
package = pkgs.caddy.withPlugins {
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ];
-
hash = "sha256-ea8PC/+SlPRdEVVF/I3c1CBprlVp1nrumKM5cMwJJ3U=";
};
email = "me@dunkirk.sh";
globalConfig = ''
···
enable = true;
package = pkgs.caddy.withPlugins {
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ];
+
hash = "sha256-dnhEjopeA0UiI+XVYHYpsjcEI6Y1Hacbi28hVKYQURg=";
};
email = "me@dunkirk.sh";
globalConfig = ''
+4 -2
machines/terebithia/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/home/kierank";
···
apps = {
helix.enable = true;
irssi.enable = true;
};
};
···
(inputs.import-tree ../../../modules/home)
];
home = {
username = "kierank";
homeDirectory = "/home/kierank";
···
apps = {
helix.enable = true;
irssi.enable = true;
+
};
+
ssh = {
+
enable = true;
+
zmx.enable = true;
};
};
+1
machines/terebithia/home-manager.nix
···
];
home-manager = {
extraSpecialArgs = {
inherit inputs outputs;
};
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+158
modules/home/apps/anthropic-manager/anthropic-manager.1.md
···
···
+
% ANTHROPIC-MANAGER(1) | Anthropic OAuth Profile Manager
+
% Kieran Klukas
+
% December 2024
+
+
# NAME
+
+
anthropic-manager - Manage Anthropic OAuth credential profiles
+
+
# SYNOPSIS
+
+
**anthropic-manager** [*OPTIONS*]
+
+
**anthropic-manager** **--init** [*PROFILE*]
+
+
**anthropic-manager** **--swap** [*PROFILE*]
+
+
**anthropic-manager** **--token**
+
+
**anthropic-manager** **--list**
+
+
**anthropic-manager** **--current**
+
+
# DESCRIPTION
+
+
**anthropic-manager** is a tool for managing multiple Anthropic OAuth credential profiles. It implements PKCE-based OAuth authentication with automatic token refresh, allowing you to switch between different Anthropic accounts easily.
+
+
Profile credentials are stored in **~/.config/crush/anthropic.\***profile\* directories with individual bearer tokens, refresh tokens, and expiration timestamps.
+
+
# OPTIONS
+
+
**--init**, **-i** [*PROFILE*]
+
: Initialize a new OAuth profile. Opens browser for authentication and stores credentials.
+
+
**--swap**, **-s** [*PROFILE*]
+
: Switch to a different profile. If no profile specified, shows interactive selection.
+
+
**--token**, **-t**
+
: Print the current bearer token to stdout. Automatically refreshes if expired. Designed for non-interactive use.
+
+
**--list**, **-l**
+
: List all available profiles with their status (valid/expired/invalid).
+
+
**--current**, **-c**
+
: Show the currently active profile name.
+
+
**--help**, **-h**
+
: Display help information.
+
+
# INTERACTIVE MENU
+
+
When run without arguments in an interactive terminal, **anthropic-manager** displays a menu with the following options:
+
+
- Switch profile
+
- Create new profile
+
- List all profiles
+
- Get current token
+
+
# PROFILE STORAGE
+
+
Profiles are stored in **~/.config/crush/** with the following structure:
+
+
```
+
~/.config/crush/
+
โ”œโ”€โ”€ anthropic -> anthropic.work (symlink to active profile)
+
โ”œโ”€โ”€ anthropic.work/
+
โ”‚ โ”œโ”€โ”€ bearer_token (OAuth access token, mode 600)
+
โ”‚ โ”œโ”€โ”€ bearer_token.expires (Unix timestamp)
+
โ”‚ โ””โ”€โ”€ refresh_token (OAuth refresh token, mode 600)
+
โ””โ”€โ”€ anthropic.personal/
+
โ””โ”€โ”€ ...
+
```
+
+
The active profile is determined by the **anthropic** symlink.
+
+
# ENVIRONMENT
+
+
**ANTHROPIC_CONFIG_DIR**
+
: Override the default configuration directory (~/.config/crush).
+
+
# EXIT STATUS
+
+
**0**
+
: Success
+
+
**1**
+
: Error (no active profile, authentication failed, invalid token, etc.)
+
+
# EXAMPLES
+
+
Initialize a new work profile:
+
+
```
+
$ anthropic-manager --init work
+
```
+
+
Switch to the work profile:
+
+
```
+
$ anthropic-manager --swap work
+
```
+
+
Get the current bearer token (for scripts):
+
+
```
+
$ TOKEN=$(anthropic-manager --token)
+
```
+
+
List all profiles:
+
+
```
+
$ anthropic-manager --list
+
```
+
+
Open interactive menu:
+
+
```
+
$ anthropic-manager
+
```
+
+
# INTEGRATION
+
+
**anthropic-manager** is designed to replace **bunx anthropic-api-key** in crush configurations:
+
+
```nix
+
api_key = "Bearer $(anthropic-manager --token)";
+
```
+
+
The **--token** flag automatically handles:
+
- Loading cached tokens
+
- Checking expiration (refreshes if <60s remaining)
+
- Refreshing using refresh token
+
- Non-interactive operation (errors to stderr, token to stdout)
+
+
# FILES
+
+
**~/.config/crush/anthropic**
+
: Symlink to active profile directory
+
+
**~/.config/crush/anthropic.*/bearer_token**
+
: OAuth access token for each profile
+
+
**~/.config/crush/anthropic.*/refresh_token**
+
: OAuth refresh token for each profile
+
+
**~/.config/crush/anthropic.*/bearer_token.expires**
+
: Token expiration timestamp (Unix epoch)
+
+
# SEE ALSO
+
+
**crush**(1)
+
+
# BUGS
+
+
Report bugs to: <https://github.com/taciturnaxolotl/dots>
+
+
# COPYRIGHT
+
+
Copyright ยฉ 2024 Kieran Klukas. Licensed under MIT License.
+25
modules/home/apps/anthropic-manager/completions/anthropic-manager.bash
···
···
+
# Bash completion for anthropic-manager
+
+
_anthropic_manager() {
+
local cur prev opts
+
COMPREPLY=()
+
cur="${COMP_WORDS[COMP_CWORD]}"
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
+
+
# Main options
+
opts="--init -i --swap -s --token -t --list -l --current -c --help -h"
+
+
# If previous word was --init or --swap, complete with profile names
+
if [[ "$prev" == "--init" ]] || [[ "$prev" == "-i" ]] || [[ "$prev" == "--swap" ]] || [[ "$prev" == "-s" ]]; then
+
local config_dir="${ANTHROPIC_CONFIG_DIR:-$HOME/.config/crush}"
+
local profiles=$(find "$config_dir" -maxdepth 1 -type d -name "anthropic.*" 2>/dev/null | sed 's/.*anthropic\.//' | sort)
+
COMPREPLY=( $(compgen -W "${profiles}" -- ${cur}) )
+
return 0
+
fi
+
+
# Complete with options
+
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+
return 0
+
}
+
+
complete -F _anthropic_manager anthropic-manager
+17
modules/home/apps/anthropic-manager/completions/anthropic-manager.fish
···
···
+
# Fish completion for anthropic-manager
+
+
# Helper function to get profile list
+
function __anthropic_manager_profiles
+
set -l config_dir (test -n "$ANTHROPIC_CONFIG_DIR"; and echo $ANTHROPIC_CONFIG_DIR; or echo "$HOME/.config/crush")
+
if test -d "$config_dir"
+
find "$config_dir" -maxdepth 1 -type d -name "anthropic.*" 2>/dev/null | sed 's/.*anthropic\.//' | sort
+
end
+
end
+
+
# Main options
+
complete -c anthropic-manager -s h -l help -d "Show help information"
+
complete -c anthropic-manager -s i -l init -d "Initialize a new profile" -xa "(__anthropic_manager_profiles)"
+
complete -c anthropic-manager -s s -l swap -d "Switch to a profile" -xa "(__anthropic_manager_profiles)"
+
complete -c anthropic-manager -s t -l token -d "Print current bearer token"
+
complete -c anthropic-manager -s l -l list -d "List all profiles"
+
complete -c anthropic-manager -s c -l current -d "Show current profile"
+21
modules/home/apps/anthropic-manager/completions/anthropic-manager.zsh
···
···
+
#compdef anthropic-manager
+
+
_anthropic_manager() {
+
local config_dir="${ANTHROPIC_CONFIG_DIR:-$HOME/.config/crush}"
+
local -a profiles
+
+
# Get list of profiles
+
if [[ -d "$config_dir" ]]; then
+
profiles=(${(f)"$(find "$config_dir" -maxdepth 1 -type d -name "anthropic.*" 2>/dev/null | sed 's/.*anthropic\.//' | sort)"})
+
fi
+
+
_arguments -C \
+
'(- *)'{-h,--help}'[Show help information]' \
+
'(-i --init)'{-i,--init}'[Initialize a new profile]:profile name:' \
+
'(-s --swap)'{-s,--swap}'[Switch to a profile]:profile:($profiles)' \
+
'(-t --token)'{-t,--token}'[Print current bearer token]' \
+
'(-l --list)'{-l,--list}'[List all profiles]' \
+
'(-c --current)'{-c,--current}'[Show current profile]'
+
}
+
+
_anthropic_manager "$@"
+505
modules/home/apps/anthropic-manager/default.nix
···
···
+
{
+
lib,
+
pkgs,
+
config,
+
...
+
}:
+
let
+
cfg = config.atelier.apps.anthropic-manager;
+
+
anthropicManagerScript = pkgs.writeShellScript "anthropic-manager" ''
+
# Manage Anthropic OAuth credential profiles
+
# Implements the same functionality as anthropic-api-key but with profile management
+
+
set -uo pipefail
+
+
CONFIG_DIR="''${ANTHROPIC_CONFIG_DIR:-$HOME/.config/crush}"
+
CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e"
+
+
# Utilities
+
base64url() {
+
${pkgs.coreutils}/bin/base64 -w0 | ${pkgs.gnused}/bin/sed 's/=//g; s/+/-/g; s/\//_/g'
+
}
+
+
sha256() {
+
echo -n "$1" | ${pkgs.openssl}/bin/openssl dgst -binary -sha256
+
}
+
+
pkce_pair() {
+
verifier=$(${pkgs.openssl}/bin/openssl rand -base64 32 | base64url)
+
challenge=$(sha256 "$verifier" | base64url)
+
echo "$verifier"
+
echo "$challenge"
+
}
+
+
authorize_url() {
+
local verifier="$1"
+
local challenge="$2"
+
echo "https://claude.ai/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=https://console.anthropic.com/oauth/code/callback&scope=org:create_api_key+user:profile+user:inference&code_challenge=$challenge&code_challenge_method=S256&state=$verifier"
+
}
+
+
clean_pasted_code() {
+
local input="$1"
+
input="''${input#code:}"
+
input="''${input#code=}"
+
input="''${input#\"}"
+
input="''${input%\"}"
+
input="''${input#\'}"
+
input="''${input%\'}"
+
input="''${input#\`}"
+
input="''${input%\`}"
+
echo "$input" | ${pkgs.gnused}/bin/sed -E 's/[^A-Za-z0-9._~#-]//g'
+
}
+
+
exchange_code() {
+
local code="$1"
+
local verifier="$2"
+
local cleaned
+
cleaned=$(clean_pasted_code "$code")
+
local pure="''${cleaned%%#*}"
+
local state="''${cleaned#*#}"
+
[[ "$state" == "$pure" ]] && state=""
+
+
${pkgs.curl}/bin/curl -s -X POST \
+
-H "Content-Type: application/json" \
+
-H "User-Agent: anthropic-manager/1.0" \
+
-d "$(${pkgs.jq}/bin/jq -n \
+
--arg code "$pure" \
+
--arg state "$state" \
+
--arg verifier "$verifier" \
+
'{
+
code: $code,
+
state: $state,
+
grant_type: "authorization_code",
+
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
+
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
+
code_verifier: $verifier
+
}')" \
+
"https://console.anthropic.com/v1/oauth/token"
+
}
+
+
exchange_refresh() {
+
local refresh_token="$1"
+
${pkgs.curl}/bin/curl -s -X POST \
+
-H "Content-Type: application/json" \
+
-H "User-Agent: anthropic-manager/1.0" \
+
-d "$(${pkgs.jq}/bin/jq -n \
+
--arg refresh "$refresh_token" \
+
'{
+
grant_type: "refresh_token",
+
refresh_token: $refresh,
+
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
+
}')" \
+
"https://console.anthropic.com/v1/oauth/token"
+
}
+
+
save_tokens() {
+
local profile_dir="$1"
+
local access_token="$2"
+
local refresh_token="$3"
+
local expires_at="$4"
+
+
mkdir -p "$profile_dir"
+
echo -n "$access_token" > "$profile_dir/bearer_token"
+
echo -n "$refresh_token" > "$profile_dir/refresh_token"
+
echo -n "$expires_at" > "$profile_dir/bearer_token.expires"
+
chmod 600 "$profile_dir/bearer_token" "$profile_dir/refresh_token" "$profile_dir/bearer_token.expires"
+
}
+
+
load_tokens() {
+
local profile_dir="$1"
+
[[ -f "$profile_dir/bearer_token" ]] || return 1
+
[[ -f "$profile_dir/refresh_token" ]] || return 1
+
[[ -f "$profile_dir/bearer_token.expires" ]] || return 1
+
+
cat "$profile_dir/bearer_token"
+
cat "$profile_dir/refresh_token"
+
cat "$profile_dir/bearer_token.expires"
+
return 0
+
}
+
+
get_token() {
+
local profile_dir="$1"
+
local print_token="''${2:-true}"
+
+
if ! load_tokens "$profile_dir" >/dev/null 2>&1; then
+
return 1
+
fi
+
+
local bearer refresh expires
+
read -r bearer < "$profile_dir/bearer_token"
+
read -r refresh < "$profile_dir/refresh_token"
+
read -r expires < "$profile_dir/bearer_token.expires"
+
+
local now
+
now=$(date +%s)
+
+
# If token valid for more than 60s, return it
+
if [[ $now -lt $((expires - 60)) ]]; then
+
[[ "$print_token" == "true" ]] && echo "$bearer"
+
return 0
+
fi
+
+
# Try to refresh
+
local response
+
response=$(exchange_refresh "$refresh")
+
+
if ! echo "$response" | ${pkgs.jq}/bin/jq -e '.access_token' >/dev/null 2>&1; then
+
return 1
+
fi
+
+
local new_access new_refresh new_expires_in
+
new_access=$(echo "$response" | ${pkgs.jq}/bin/jq -r '.access_token')
+
new_refresh=$(echo "$response" | ${pkgs.jq}/bin/jq -r '.refresh_token // empty')
+
new_expires_in=$(echo "$response" | ${pkgs.jq}/bin/jq -r '.expires_in')
+
+
[[ -z "$new_refresh" ]] && new_refresh="$refresh"
+
local new_expires=$((now + new_expires_in))
+
+
save_tokens "$profile_dir" "$new_access" "$new_refresh" "$new_expires"
+
[[ "$print_token" == "true" ]] && echo "$new_access"
+
return 0
+
}
+
+
oauth_flow() {
+
local profile_dir="$1"
+
+
${pkgs.gum}/bin/gum style --foreground 212 "Starting OAuth flow..."
+
echo
+
+
read -r verifier challenge < <(pkce_pair)
+
local auth_url
+
auth_url=$(authorize_url "$verifier" "$challenge")
+
+
${pkgs.gum}/bin/gum style --foreground 35 "Opening browser for authorization..."
+
${pkgs.gum}/bin/gum style --foreground 117 "$auth_url"
+
echo
+
+
if command -v ${pkgs.xdg-utils}/bin/xdg-open &>/dev/null; then
+
${pkgs.xdg-utils}/bin/xdg-open "$auth_url" 2>/dev/null &
+
elif command -v open &>/dev/null; then
+
open "$auth_url" 2>/dev/null &
+
fi
+
+
local code
+
code=$(${pkgs.gum}/bin/gum input --placeholder "Paste the authorization code from Anthropic" --prompt "Code: ")
+
+
if [[ -z "$code" ]]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No code provided"
+
return 1
+
fi
+
+
${pkgs.gum}/bin/gum style --foreground 212 "Exchanging code for tokens..."
+
+
local response
+
response=$(exchange_code "$code" "$verifier")
+
+
if ! echo "$response" | ${pkgs.jq}/bin/jq -e '.access_token' >/dev/null 2>&1; then
+
${pkgs.gum}/bin/gum style --foreground 196 "Failed to exchange code"
+
echo "$response" | ${pkgs.jq}/bin/jq '.' 2>&1 || echo "$response"
+
return 1
+
fi
+
+
local access_token refresh_token expires_in
+
access_token=$(echo "$response" | ${pkgs.jq}/bin/jq -r '.access_token')
+
refresh_token=$(echo "$response" | ${pkgs.jq}/bin/jq -r '.refresh_token')
+
expires_in=$(echo "$response" | ${pkgs.jq}/bin/jq -r '.expires_in')
+
+
local expires_at
+
expires_at=$(($(date +%s) + expires_in))
+
+
save_tokens "$profile_dir" "$access_token" "$refresh_token" "$expires_at"
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Authenticated successfully"
+
return 0
+
}
+
+
list_profiles() {
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Available Anthropic profiles:"
+
echo
+
+
local current_profile=""
+
if [[ -L "$CONFIG_DIR/anthropic" ]]; then
+
current_profile=$(basename "$(readlink "$CONFIG_DIR/anthropic")" | ${pkgs.gnused}/bin/sed 's/^anthropic\.//')
+
fi
+
+
local found_any=false
+
for profile_dir in "$CONFIG_DIR"/anthropic.*; do
+
if [[ -d "$profile_dir" ]]; then
+
found_any=true
+
local profile_name
+
profile_name=$(basename "$profile_dir" | ${pkgs.gnused}/bin/sed 's/^anthropic\.//')
+
+
local status=""
+
if get_token "$profile_dir" false 2>/dev/null; then
+
local expires
+
read -r expires < "$profile_dir/bearer_token.expires"
+
local now
+
now=$(date +%s)
+
if [[ $now -lt $expires ]]; then
+
status=" (valid)"
+
else
+
status=" (expired)"
+
fi
+
else
+
status=" (invalid)"
+
fi
+
+
if [[ "$profile_name" == "$current_profile" ]]; then
+
${pkgs.gum}/bin/gum style --foreground 35 " โœ“ $profile_name$status (active)"
+
else
+
echo " $profile_name$status"
+
fi
+
fi
+
done
+
+
if [[ "$found_any" == "false" ]]; then
+
${pkgs.gum}/bin/gum style --foreground 214 "No profiles found. Use 'anthropic-manager --init <name>' to create one."
+
fi
+
}
+
+
show_current() {
+
if [[ -L "$CONFIG_DIR/anthropic" ]]; then
+
local current
+
current=$(basename "$(readlink "$CONFIG_DIR/anthropic")" | ${pkgs.gnused}/bin/sed 's/^anthropic\.//')
+
${pkgs.gum}/bin/gum style --foreground 35 "Current profile: $current"
+
else
+
${pkgs.gum}/bin/gum style --foreground 214 "No active profile"
+
fi
+
}
+
+
init_profile() {
+
local profile="$1"
+
+
if [[ -z "$profile" ]]; then
+
profile=$(${pkgs.gum}/bin/gum input --placeholder "Profile name (e.g., work, personal)" --prompt "Profile name: ")
+
if [[ -z "$profile" ]]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No profile name provided"
+
exit 1
+
fi
+
fi
+
+
local profile_dir="$CONFIG_DIR/anthropic.$profile"
+
+
if [[ -d "$profile_dir" ]]; then
+
${pkgs.gum}/bin/gum style --foreground 214 "Profile '$profile' already exists"
+
if ${pkgs.gum}/bin/gum confirm "Re-authenticate?"; then
+
rm -rf "$profile_dir"
+
else
+
exit 1
+
fi
+
fi
+
+
if ! oauth_flow "$profile_dir"; then
+
rm -rf "$profile_dir"
+
exit 1
+
fi
+
+
# Ask to set as active
+
if [[ ! -L "$CONFIG_DIR/anthropic" ]] || ${pkgs.gum}/bin/gum confirm "Set '$profile' as active profile?"; then
+
[[ -L "$CONFIG_DIR/anthropic" ]] && rm "$CONFIG_DIR/anthropic"
+
ln -sf "anthropic.$profile" "$CONFIG_DIR/anthropic"
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Set as active profile"
+
fi
+
}
+
+
swap_profile() {
+
local target="$1"
+
+
if [[ -n "$target" ]]; then
+
local target_dir="$CONFIG_DIR/anthropic.$target"
+
if [[ ! -d "$target_dir" ]]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "Profile '$target' does not exist"
+
echo
+
list_profiles
+
exit 1
+
fi
+
+
[[ -L "$CONFIG_DIR/anthropic" ]] && rm "$CONFIG_DIR/anthropic"
+
ln -sf "anthropic.$target" "$CONFIG_DIR/anthropic"
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Switched to profile '$target'"
+
exit 0
+
fi
+
+
# Interactive selection
+
local profiles=()
+
for profile_dir in "$CONFIG_DIR"/anthropic.*; do
+
if [[ -d "$profile_dir" ]]; then
+
profiles+=("$(basename "$profile_dir" | ${pkgs.gnused}/bin/sed 's/^anthropic\.//')")
+
fi
+
done
+
+
if [[ ''${#profiles[@]} -eq 0 ]]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No profiles found"
+
${pkgs.gum}/bin/gum style --foreground 214 "Use 'anthropic-manager --init <name>' to create one"
+
exit 1
+
fi
+
+
local selected
+
selected=$(printf '%s\n' "''${profiles[@]}" | ${pkgs.gum}/bin/gum choose --header "Select profile:")
+
+
if [[ -n "$selected" ]]; then
+
[[ -L "$CONFIG_DIR/anthropic" ]] && rm "$CONFIG_DIR/anthropic"
+
ln -sf "anthropic.$selected" "$CONFIG_DIR/anthropic"
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Switched to profile '$selected'"
+
fi
+
}
+
+
print_token() {
+
if [[ ! -L "$CONFIG_DIR/anthropic" ]]; then
+
echo "Error: No active profile" >&2
+
exit 1
+
fi
+
+
local profile_dir
+
profile_dir=$(readlink -f "$CONFIG_DIR/anthropic")
+
+
if ! get_token "$profile_dir" true 2>/dev/null; then
+
echo "Error: Token invalid or expired" >&2
+
exit 1
+
fi
+
}
+
+
interactive_menu() {
+
echo
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Anthropic Profile Manager"
+
echo
+
+
local current_profile=""
+
if [[ -L "$CONFIG_DIR/anthropic" ]]; then
+
current_profile=$(basename "$(readlink "$CONFIG_DIR/anthropic")" | ${pkgs.gnused}/bin/sed 's/^anthropic\.//')
+
${pkgs.gum}/bin/gum style --foreground 117 "Active: $current_profile"
+
else
+
${pkgs.gum}/bin/gum style --foreground 214 "No active profile"
+
fi
+
+
echo
+
+
local choice
+
choice=$(${pkgs.gum}/bin/gum choose \
+
"Switch profile" \
+
"Create new profile" \
+
"List all profiles" \
+
"Get current token")
+
+
case "$choice" in
+
"Switch profile")
+
swap_profile ""
+
;;
+
"Create new profile")
+
init_profile ""
+
;;
+
"List all profiles")
+
echo
+
list_profiles
+
;;
+
"Get current token")
+
echo
+
print_token
+
;;
+
esac
+
}
+
+
# Main
+
mkdir -p "$CONFIG_DIR"
+
+
case "''${1:-}" in
+
--init|-i)
+
init_profile "''${2:-}"
+
;;
+
--list|-l)
+
list_profiles
+
;;
+
--current|-c)
+
show_current
+
;;
+
--token|-t|token)
+
print_token
+
;;
+
--swap|-s|swap)
+
swap_profile "''${2:-}"
+
;;
+
--help|-h|help)
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "anthropic-manager - Manage Anthropic OAuth profiles"
+
echo
+
echo "Usage:"
+
echo " anthropic-manager Interactive menu"
+
echo " anthropic-manager --init [profile] Initialize/create a new profile"
+
echo " anthropic-manager --swap [profile] Switch to a profile (interactive if no profile given)"
+
echo " anthropic-manager --token Print current bearer token (refresh if needed)"
+
echo " anthropic-manager --list List all profiles with status"
+
echo " anthropic-manager --current Show current active profile"
+
echo " anthropic-manager --help Show this help"
+
echo
+
echo "Examples:"
+
echo " anthropic-manager Open interactive menu"
+
echo " anthropic-manager --init work Create 'work' profile"
+
echo " anthropic-manager --swap work Switch to 'work' profile"
+
echo " anthropic-manager --token Get current bearer token"
+
;;
+
"")
+
# No args - check if interactive
+
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
+
echo "Error: anthropic-manager requires an interactive terminal when called without arguments" >&2
+
exit 1
+
fi
+
interactive_menu
+
;;
+
*)
+
${pkgs.gum}/bin/gum style --foreground 196 "Unknown option: $1"
+
echo "Use --help for usage information"
+
exit 1
+
;;
+
esac
+
'';
+
+
anthropicManager = pkgs.stdenv.mkDerivation {
+
pname = "anthropic-manager";
+
version = "1.0";
+
+
dontUnpack = true;
+
+
nativeBuildInputs = with pkgs; [ pandoc installShellFiles ];
+
+
manPageSrc = ./anthropic-manager.1.md;
+
bashCompletionSrc = ./completions/anthropic-manager.bash;
+
zshCompletionSrc = ./completions/anthropic-manager.zsh;
+
fishCompletionSrc = ./completions/anthropic-manager.fish;
+
+
buildPhase = ''
+
# Convert markdown man page to man format
+
${pkgs.pandoc}/bin/pandoc -s -t man $manPageSrc -o anthropic-manager.1
+
'';
+
+
installPhase = ''
+
mkdir -p $out/bin
+
+
# Install binary
+
cp ${anthropicManagerScript} $out/bin/anthropic-manager
+
chmod +x $out/bin/anthropic-manager
+
+
# Install man page
+
installManPage anthropic-manager.1
+
+
# Install completions
+
installShellCompletion --bash --name anthropic-manager $bashCompletionSrc
+
installShellCompletion --zsh --name _anthropic-manager $zshCompletionSrc
+
installShellCompletion --fish --name anthropic-manager.fish $fishCompletionSrc
+
'';
+
+
meta = with lib; {
+
description = "Anthropic OAuth profile manager";
+
homepage = "https://github.com/taciturnaxolotl/dots";
+
license = licenses.mit;
+
maintainers = [ ];
+
};
+
};
+
in
+
{
+
options.atelier.apps.anthropic-manager.enable = lib.mkEnableOption "Enable anthropic-manager";
+
+
config = lib.mkIf cfg.enable {
+
home.packages = [
+
anthropicManager
+
];
+
};
+
}
+131
modules/home/apps/bore/bore.1.md
···
···
+
% BORE(1) bore 1.0
+
% Kieran Klukas
+
% December 2024
+
+
# NAME
+
+
bore - secure tunneling service for exposing local services to the internet
+
+
# SYNOPSIS
+
+
**bore** [*SUBDOMAIN*] [*PORT*] [**--protocol** *PROTOCOL*] [**--label** *LABEL*] [**--save**]
+
+
**bore** **--list** | **-l**
+
+
**bore** **--saved** | **-s**
+
+
# DESCRIPTION
+
+
**bore** is a tunneling service that uses frp (fast reverse proxy) to expose local services to the internet via bore.dunkirk.sh. It provides a simple CLI for creating and managing HTTP, TCP, and UDP tunnels with optional labels and persistent configuration.
+
+
# OPTIONS
+
+
**-l**, **--list**
+
: List all active tunnels on the bore server.
+
+
**-s**, **--saved**
+
: List all saved tunnel configurations from bore.toml in the current directory.
+
+
**-p**, **--protocol** *PROTOCOL*
+
: Specify the protocol to use for the tunnel: **http** (default), **tcp**, or **udp**.
+
+
**--label** *LABEL*
+
: Assign a label/tag to the tunnel for organization and identification.
+
+
**--save**
+
: Save the tunnel configuration to bore.toml in the current directory for future use.
+
+
# ARGUMENTS
+
+
*SUBDOMAIN*
+
: The subdomain to use for the tunnel (e.g., "myapp" creates myapp.bore.dunkirk.sh). Must contain only lowercase letters, numbers, and hyphens.
+
+
*PORT*
+
: The local port to expose (e.g., 8000 for localhost:8000).
+
+
# CONFIGURATION
+
+
Tunnel configurations can be saved to a **bore.toml** file in the current directory. This file uses TOML format and can be committed to repositories.
+
+
## bore.toml Format
+
+
```toml
+
[myapp]
+
port = 8000
+
+
[api]
+
port = 3000
+
protocol = "http"
+
label = "dev"
+
+
[database]
+
port = 5432
+
protocol = "tcp"
+
label = "postgres"
+
+
[game-server]
+
port = 27015
+
protocol = "udp"
+
label = "game"
+
```
+
+
When running **bore** without arguments in a directory with bore.toml, you'll be prompted to choose between creating a new tunnel or using a saved configuration.
+
+
# EXAMPLES
+
+
Create a simple HTTP tunnel:
+
```
+
$ bore myapp 8000
+
```
+
+
Create an HTTP tunnel with a label:
+
```
+
$ bore api 3000 --label dev
+
```
+
+
Create a TCP tunnel for a database:
+
```
+
$ bore database 5432 --protocol tcp --label postgres
+
```
+
+
Create a UDP tunnel for a game server:
+
```
+
$ bore game-server 27015 --protocol udp --label game
+
```
+
+
Save a tunnel configuration:
+
```
+
$ bore frontend 5173 --label local --save
+
```
+
+
List active tunnels:
+
```
+
$ bore --list
+
```
+
+
List saved configurations:
+
```
+
$ bore --saved
+
```
+
+
Interactive mode (choose saved or new):
+
```
+
$ bore
+
```
+
+
# FILES
+
+
**bore.toml**
+
: Local tunnel configuration file (current directory)
+
+
# SEE ALSO
+
+
Dashboard: https://bore.dunkirk.sh
+
+
# BUGS
+
+
Report bugs at: https://github.com/yourusername/dots/issues
+
+
# AUTHORS
+
+
Kieran Klukas <crush@charm.land>
+38
modules/home/apps/bore/completions/bore.bash
···
···
+
# bash completion for bore
+
+
_bore_completion() {
+
local cur prev opts
+
COMPREPLY=()
+
cur="${COMP_WORDS[COMP_CWORD]}"
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
+
opts="--list --saved --protocol --label --save -l -s -p"
+
+
# Complete flags
+
if [[ ${cur} == -* ]]; then
+
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+
return 0
+
fi
+
+
# Complete protocol values after --protocol or -p
+
if [[ ${prev} == "--protocol" ]] || [[ ${prev} == "-p" ]]; then
+
COMPREPLY=( $(compgen -W "http tcp udp" -- ${cur}) )
+
return 0
+
fi
+
+
# Complete label value after --label or -l
+
if [[ ${prev} == "--label" ]] || [[ ${prev} == "-l" ]]; then
+
# Could potentially read from bore.toml for label suggestions
+
return 0
+
fi
+
+
# Complete saved tunnel names as first argument
+
if [[ ${COMP_CWORD} -eq 1 ]] && [[ -f "bore.toml" ]]; then
+
local tunnels=$(grep '^\[' bore.toml | sed 's/^\[\(.*\)\]$/\1/')
+
COMPREPLY=( $(compgen -W "${tunnels}" -- ${cur}) )
+
return 0
+
fi
+
+
return 0
+
}
+
+
complete -F _bore_completion bore
+21
modules/home/apps/bore/completions/bore.fish
···
···
+
# fish completion for bore
+
+
# Helper function to get saved tunnel names
+
function __bore_saved_tunnels
+
if test -f bore.toml
+
grep '^\[' bore.toml | sed 's/^\[\(.*\)\]$/\1/'
+
end
+
end
+
+
# Complete flags
+
complete -c bore -s l -l list -d 'List active tunnels'
+
complete -c bore -s s -l saved -d 'List saved tunnels from bore.toml'
+
complete -c bore -s p -l protocol -d 'Specify protocol' -xa 'http tcp udp'
+
complete -c bore -l label -d 'Assign a label to the tunnel' -r
+
complete -c bore -l save -d 'Save tunnel configuration to bore.toml'
+
+
# Complete subdomain from saved tunnels (first argument)
+
complete -c bore -n '__fish_is_first_token' -a '(__bore_saved_tunnels)' -d 'Saved tunnel'
+
+
# Port is always a number (second argument)
+
complete -c bore -n 'test (count (commandline -opc)) -eq 2' -d 'Local port'
+42
modules/home/apps/bore/completions/bore.zsh
···
···
+
#compdef bore
+
+
_bore() {
+
local -a tunnels
+
local curcontext="$curcontext" state line
+
typeset -A opt_args
+
+
# Read saved tunnels from bore.toml if it exists
+
if [[ -f "bore.toml" ]]; then
+
tunnels=(${(f)"$(grep '^\[' bore.toml | sed 's/^\[\(.*\)\]$/\1/')"})
+
fi
+
+
_arguments -C \
+
'1: :->subdomain' \
+
'2: :->port' \
+
'--list[List active tunnels]' \
+
'-l[List active tunnels]' \
+
'--saved[List saved tunnels from bore.toml]' \
+
'-s[List saved tunnels from bore.toml]' \
+
'--protocol[Specify protocol]:protocol:(http tcp udp)' \
+
'-p[Specify protocol]:protocol:(http tcp udp)' \
+
'--label[Assign a label to the tunnel]:label:' \
+
'--save[Save tunnel configuration to bore.toml]' \
+
&& return 0
+
+
case $state in
+
subdomain)
+
if [[ ${#tunnels[@]} -gt 0 ]]; then
+
_describe 'saved tunnels' tunnels
+
else
+
_message 'subdomain (e.g., myapp)'
+
fi
+
;;
+
port)
+
_message 'local port (e.g., 8000)'
+
;;
+
esac
+
+
return 0
+
}
+
+
_bore "$@"
+499
modules/home/apps/bore/default.nix
···
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
let
+
cfg = config.atelier.bore;
+
+
boreScript = pkgs.writeShellScript "bore" ''
+
CONFIG_FILE="bore.toml"
+
+
# Trap exit signals to ensure cleanup and exit immediately
+
trap 'exit 130' INT
+
trap 'exit 143' TERM
+
trap 'exit 129' HUP
+
+
# Enable immediate exit on error or pipe failure
+
set -e
+
set -o pipefail
+
+
# Check for flags
+
if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Active tunnels"
+
echo
+
+
tunnels=$(${pkgs.curl}/bin/curl -s https://${cfg.domain}/api/proxy/http)
+
+
if ! echo "$tunnels" | ${pkgs.jq}/bin/jq -e '.proxies | length > 0' >/dev/null 2>&1; then
+
${pkgs.gum}/bin/gum style --foreground 117 "No active tunnels"
+
exit 0
+
fi
+
+
# Filter only online tunnels with valid conf
+
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | if .type == "http" then "\(.name) โ†’ https://\(.conf.subdomain).${cfg.domain} [http]" elif .type == "tcp" then "\(.name) โ†’ tcp://\(.conf.remotePort) โ†’ localhost:\(.conf.localPort) [tcp]" elif .type == "udp" then "\(.name) โ†’ udp://\(.conf.remotePort) โ†’ localhost:\(.conf.localPort) [udp]" else "\(.name) [\(.type)]" end' | while read -r line; do
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ $line"
+
done
+
exit 0
+
fi
+
+
if [ "$1" = "--saved" ] || [ "$1" = "-s" ]; then
+
if [ ! -f "$CONFIG_FILE" ]; then
+
${pkgs.gum}/bin/gum style --foreground 117 "No bore.toml found in current directory"
+
exit 0
+
fi
+
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Saved tunnels in bore.toml"
+
echo
+
+
# Parse TOML and show tunnels
+
while IFS= read -r line; do
+
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
+
current_tunnel="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
+
port="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
protocol="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
label="''${BASH_REMATCH[1]}"
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ $current_tunnel โ†’ localhost:$port [$proto_display] [$label]"
+
label=""
+
protocol=""
+
elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ $current_tunnel โ†’ localhost:$port [$proto_display]"
+
current_tunnel=""
+
port=""
+
protocol=""
+
fi
+
done < "$CONFIG_FILE"
+
+
# Handle last entry if file doesn't end with blank line
+
if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
+
proto_display="''${protocol:-http}"
+
if [[ -n "$label" ]]; then
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ $current_tunnel โ†’ localhost:$port [$proto_display] [$label]"
+
else
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ $current_tunnel โ†’ localhost:$port [$proto_display]"
+
fi
+
fi
+
exit 0
+
fi
+
+
# Get tunnel name/subdomain
+
if [ -n "$1" ]; then
+
tunnel_name="$1"
+
else
+
# Check if we have a bore.toml in current directory
+
if [ -f "$CONFIG_FILE" ]; then
+
# Count tunnels in TOML
+
tunnel_count=$(${pkgs.gnugrep}/bin/grep -c '^\[' "$CONFIG_FILE" 2>/dev/null || echo "0")
+
+
if [ "$tunnel_count" -gt 0 ]; then
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
+
echo
+
+
# Show choice between new or saved
+
choice=$(${pkgs.gum}/bin/gum choose "New tunnel" "Use saved tunnel")
+
+
if [ "$choice" = "Use saved tunnel" ]; then
+
# Extract tunnel names from TOML
+
saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/')
+
tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected"
+
exit 1
+
fi
+
+
# Parse TOML for this tunnel's config
+
in_section=false
+
while IFS= read -r line; do
+
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
+
if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then
+
in_section=true
+
else
+
in_section=false
+
fi
+
elif [[ "$in_section" = true ]]; then
+
if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
+
port="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
protocol="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
label="''${BASH_REMATCH[1]}"
+
fi
+
fi
+
done < "$CONFIG_FILE"
+
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Loaded from bore.toml: $tunnel_name โ†’ localhost:$port [$proto_display]''${label:+ [$label]}"
+
else
+
# New tunnel - prompt for protocol first to determine what to ask for
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
+
exit 1
+
fi
+
fi
+
else
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
+
echo
+
# Prompt for protocol first
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
+
exit 1
+
fi
+
fi
+
else
+
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
+
echo
+
# Prompt for protocol first
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
+
exit 1
+
fi
+
fi
+
fi
+
+
# Validate tunnel name (only for http subdomains)
+
if [ "$protocol" = "http" ]; then
+
if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
+
exit 1
+
fi
+
fi
+
+
# Get port (skip if loaded from saved config)
+
if [ -z "$port" ]; then
+
if [ -n "$2" ]; then
+
port="$2"
+
else
+
port=$(${pkgs.gum}/bin/gum input --placeholder "8000" --prompt "Local port: ")
+
if [ -z "$port" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No port provided"
+
exit 1
+
fi
+
fi
+
fi
+
+
# Validate port
+
if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid port (must be a number)"
+
exit 1
+
fi
+
+
# Get optional protocol, label and save flag (skip if loaded from saved config)
+
save_config=false
+
if [ -z "$label" ]; then
+
shift 2 2>/dev/null || true
+
while [[ $# -gt 0 ]]; do
+
case "$1" in
+
--protocol|-p)
+
protocol="$2"
+
shift 2
+
;;
+
--label|-l)
+
label="$2"
+
shift 2
+
;;
+
--save)
+
save_config=true
+
shift
+
;;
+
*)
+
shift
+
;;
+
esac
+
done
+
+
# Prompt for protocol if not provided via flag and not loaded from saved config and not already set
+
if [ -z "$protocol" ]; then
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
fi
+
+
# Prompt for label if not provided via flag and not loaded from saved config
+
if [ -z "$label" ]; then
+
# Allow multiple labels selection
+
labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom")
+
+
if [ -n "$labels" ]; then
+
# Check if custom was selected
+
if echo "$labels" | ${pkgs.gnugrep}/bin/grep -q "custom"; then
+
custom_label=$(${pkgs.gum}/bin/gum input --placeholder "my-label" --prompt "Custom label: ")
+
if [ -z "$custom_label" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No custom label provided"
+
exit 1
+
fi
+
# Replace 'custom' with the actual custom label
+
labels=$(echo "$labels" | ${pkgs.gnused}/bin/sed "s/custom/$custom_label/")
+
fi
+
# Join labels with comma
+
label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//')
+
fi
+
fi
+
fi
+
+
# Default protocol to http if still not set
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
# Check if local port is accessible
+
if ! ${pkgs.netcat}/bin/nc -z 127.0.0.1 "$port" 2>/dev/null; then
+
${pkgs.gum}/bin/gum style --foreground 214 "! Warning: Nothing listening on localhost:$port"
+
fi
+
+
# Save configuration if requested
+
if [ "$save_config" = true ]; then
+
# Check if tunnel already exists in TOML
+
if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then
+
# Update existing entry
+
${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{
+
s/^port[[:space:]]*=.*/port = $port/
+
s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/
+
''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/}
+
}" "$CONFIG_FILE"
+
else
+
# Append new entry
+
{
+
echo ""
+
echo "[$tunnel_name]"
+
echo "port = $port"
+
if [ "$protocol" != "http" ]; then
+
echo "protocol = \"$protocol\""
+
fi
+
if [ -n "$label" ]; then
+
echo "label = \"$label\""
+
fi
+
} >> "$CONFIG_FILE"
+
fi
+
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Configuration saved to bore.toml"
+
echo
+
fi
+
+
# Create config file
+
config_file=$(${pkgs.coreutils}/bin/mktemp)
+
trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
+
+
# Encode label into proxy name if provided (format: tunnel_name[label1,label2])
+
proxy_name="$tunnel_name"
+
if [ -n "$label" ]; then
+
proxy_name="''${tunnel_name}[''${label}]"
+
fi
+
+
# Build proxy configuration based on protocol
+
if [ "$protocol" = "http" ]; then
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
+
serverAddr = "${cfg.serverAddr}"
+
serverPort = ${toString cfg.serverPort}
+
+
auth.method = "token"
+
auth.tokenSource.type = "file"
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
+
+
[[proxies]]
+
name = "$proxy_name"
+
type = "http"
+
localIP = "127.0.0.1"
+
localPort = $port
+
subdomain = "$tunnel_name"
+
EOF
+
elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
+
# For TCP/UDP, enable admin API to query allocated port
+
# Use Python to find a free port (cross-platform and guaranteed to work)
+
admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')
+
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
+
serverAddr = "${cfg.serverAddr}"
+
serverPort = ${toString cfg.serverPort}
+
+
auth.method = "token"
+
auth.tokenSource.type = "file"
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
+
+
webServer.addr = "127.0.0.1"
+
webServer.port = $admin_port
+
+
[[proxies]]
+
name = "$proxy_name"
+
type = "$protocol"
+
localIP = "127.0.0.1"
+
localPort = $port
+
remotePort = 0
+
EOF
+
else
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)"
+
exit 1
+
fi
+
+
# Start tunnel
+
echo
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Tunnel configured"
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
+
if [ "$protocol" = "http" ]; then
+
public_url="https://$tunnel_name.${cfg.domain}"
+
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
+
else
+
${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol"
+
${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..."
+
fi
+
echo
+
${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..."
+
echo
+
+
# For TCP/UDP, capture output to parse allocated port
+
if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
+
# Start frpc in background and capture its PID
+
${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do
+
echo "$line"
+
+
# Look for successful proxy start
+
if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then
+
# Wait a moment for the proxy to fully initialize
+
sleep 1
+
+
# Query the frpc admin API for proxy status
+
proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}")
+
+
# Try to extract remote port from JSON response
+
# Format: "remote_addr":"bore.dunkirk.sh:20097"
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
+
if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
+
fi
+
+
# Extract just the port number
+
remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$')
+
+
if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then
+
echo
+
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Tunnel established"
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
+
${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port"
+
${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol"
+
echo
+
fi
+
fi
+
done
+
else
+
exec ${pkgs.frp}/bin/frpc -c $config_file
+
fi
+
'';
+
+
bore = pkgs.stdenv.mkDerivation {
+
pname = "bore";
+
version = "1.0";
+
+
dontUnpack = true;
+
+
nativeBuildInputs = with pkgs; [ pandoc installShellFiles ];
+
+
manPageSrc = ./bore.1.md;
+
bashCompletionSrc = ./completions/bore.bash;
+
zshCompletionSrc = ./completions/bore.zsh;
+
fishCompletionSrc = ./completions/bore.fish;
+
+
buildPhase = ''
+
# Convert markdown man page to man format
+
${pkgs.pandoc}/bin/pandoc -s -t man $manPageSrc -o bore.1
+
'';
+
+
installPhase = ''
+
mkdir -p $out/bin
+
+
# Install binary
+
cp ${boreScript} $out/bin/bore
+
chmod +x $out/bin/bore
+
+
# Install man page
+
installManPage bore.1
+
+
# Install completions
+
installShellCompletion --bash --name bore $bashCompletionSrc
+
installShellCompletion --zsh --name _bore $zshCompletionSrc
+
installShellCompletion --fish --name bore.fish $fishCompletionSrc
+
'';
+
+
meta = with lib; {
+
description = "Secure tunneling service CLI";
+
homepage = "https://bore.dunkirk.sh";
+
license = licenses.mit;
+
maintainers = [ ];
+
};
+
};
+
in
+
{
+
options.atelier.bore = {
+
enable = lib.mkEnableOption "bore tunneling service";
+
+
serverAddr = lib.mkOption {
+
type = lib.types.str;
+
default = "bore.dunkirk.sh";
+
description = "bore server address";
+
};
+
+
serverPort = lib.mkOption {
+
type = lib.types.port;
+
default = 7000;
+
description = "bore server port";
+
};
+
+
domain = lib.mkOption {
+
type = lib.types.str;
+
default = "bore.dunkirk.sh";
+
description = "Domain for public tunnel URLs";
+
};
+
+
authTokenFile = lib.mkOption {
+
type = lib.types.nullOr lib.types.path;
+
default = null;
+
description = "Path to file containing authentication token";
+
};
+
};
+
+
config = lib.mkIf cfg.enable {
+
home.packages = [
+
pkgs.frp
+
bore
+
];
+
};
+
}
+3 -1
modules/home/apps/crush.nix
···
{
options.atelier.apps.crush.enable = lib.mkEnableOption "Enable Crush config";
config = lib.mkIf config.atelier.apps.crush.enable {
programs.crush = {
enable = true;
settings = {
···
name = "Claude Pro";
type = "anthropic";
base_url = "https://api.anthropic.com";
-
api_key = "Bearer $(bunx anthropic-api-key)";
system_prompt_prefix = "You are Claude Code, Anthropic's official CLI for Claude.";
extra_headers = {
"anthropic-version" = "2023-06-01";
···
{
options.atelier.apps.crush.enable = lib.mkEnableOption "Enable Crush config";
config = lib.mkIf config.atelier.apps.crush.enable {
+
atelier.apps.anthropic-manager.enable = lib.mkDefault true;
+
programs.crush = {
enable = true;
settings = {
···
name = "Claude Pro";
type = "anthropic";
base_url = "https://api.anthropic.com";
+
api_key = "Bearer $(anthropic-manager --token)";
system_prompt_prefix = "You are Claude Code, Anthropic's official CLI for Claude.";
extra_headers = {
"anthropic-version" = "2023-06-01";
-138
modules/home/apps/frpc.nix
···
-
{
-
config,
-
lib,
-
pkgs,
-
...
-
}:
-
let
-
cfg = config.atelier.bore;
-
-
bore = pkgs.writeShellScriptBin "bore" ''
-
# Check for flags
-
if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
-
${pkgs.gum}/bin/gum style --bold --foreground 212 "Active tunnels"
-
echo
-
-
tunnels=$(${pkgs.curl}/bin/curl -s https://${cfg.domain}/api/proxy/http)
-
-
if ! echo "$tunnels" | ${pkgs.jq}/bin/jq -e '.proxies | length > 0' >/dev/null 2>&1; then
-
${pkgs.gum}/bin/gum style --foreground 117 "No active tunnels"
-
exit 0
-
fi
-
-
# Filter only online tunnels with valid conf
-
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | "\(.name) โ†’ https://\(.conf.subdomain).${cfg.domain}"' | while read -r line; do
-
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ $line"
-
done
-
exit 0
-
fi
-
-
# Get subdomain
-
if [ -n "$1" ]; then
-
subdomain="$1"
-
else
-
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
-
echo
-
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
-
if [ -z "$subdomain" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No subdomain provided"
-
exit 1
-
fi
-
fi
-
-
# Validate subdomain
-
if ! echo "$subdomain" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
-
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
-
exit 1
-
fi
-
-
# Get port
-
if [ -n "$2" ]; then
-
port="$2"
-
else
-
port=$(${pkgs.gum}/bin/gum input --placeholder "8000" --prompt "Local port: ")
-
if [ -z "$port" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No port provided"
-
exit 1
-
fi
-
fi
-
-
# Validate port
-
if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then
-
${pkgs.gum}/bin/gum style --foreground 196 "Invalid port (must be a number)"
-
exit 1
-
fi
-
-
# Check if local port is accessible
-
if ! ${pkgs.netcat}/bin/nc -z 127.0.0.1 "$port" 2>/dev/null; then
-
${pkgs.gum}/bin/gum style --foreground 214 "! Warning: Nothing listening on localhost:$port"
-
fi
-
-
# Create config file
-
config_file=$(${pkgs.coreutils}/bin/mktemp)
-
trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
-
-
${pkgs.coreutils}/bin/cat > $config_file <<EOF
-
serverAddr = "${cfg.serverAddr}"
-
serverPort = ${toString cfg.serverPort}
-
-
auth.method = "token"
-
auth.tokenSource.type = "file"
-
auth.tokenSource.file.path = "${cfg.authTokenFile}"
-
-
[[proxies]]
-
name = "$subdomain"
-
type = "http"
-
localIP = "127.0.0.1"
-
localPort = $port
-
subdomain = "$subdomain"
-
EOF
-
-
# Start tunnel
-
public_url="https://$subdomain.${cfg.domain}"
-
echo
-
${pkgs.gum}/bin/gum style --foreground 35 "โœ“ Tunnel configured"
-
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
-
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
-
echo
-
${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..."
-
-
exec ${pkgs.frp}/bin/frpc -c $config_file
-
'';
-
in
-
{
-
options.atelier.bore = {
-
enable = lib.mkEnableOption "bore tunneling service";
-
-
serverAddr = lib.mkOption {
-
type = lib.types.str;
-
default = "bore.dunkirk.sh";
-
description = "bore server address";
-
};
-
-
serverPort = lib.mkOption {
-
type = lib.types.port;
-
default = 7000;
-
description = "bore server port";
-
};
-
-
domain = lib.mkOption {
-
type = lib.types.str;
-
default = "bore.dunkirk.sh";
-
description = "Domain for public tunnel URLs";
-
};
-
-
authTokenFile = lib.mkOption {
-
type = lib.types.nullOr lib.types.path;
-
default = null;
-
description = "Path to file containing authentication token";
-
};
-
};
-
-
config = lib.mkIf cfg.enable {
-
home.packages = [
-
pkgs.frp
-
bore
-
];
-
};
-
}
···
+183
modules/home/apps/ssh.nix
···
···
+
{
+
config,
+
lib,
+
pkgs,
+
inputs,
+
...
+
}:
+
with lib;
+
let
+
cfg = config.atelier.ssh;
+
in
+
{
+
options.atelier.ssh = {
+
enable = mkEnableOption "SSH configuration";
+
+
zmx = {
+
enable = mkEnableOption "zmx integration for persistent sessions";
+
hosts = mkOption {
+
type = types.listOf types.str;
+
default = [ ];
+
description = "List of host patterns to enable zmx auto-attach (e.g., 'd.*')";
+
};
+
};
+
+
extraConfig = mkOption {
+
type = types.lines;
+
default = "";
+
description = "Extra SSH configuration";
+
};
+
+
hosts = mkOption {
+
type = types.attrsOf (
+
types.submodule {
+
options = {
+
hostname = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = "Hostname or IP address";
+
};
+
+
port = mkOption {
+
type = types.nullOr types.int;
+
default = null;
+
description = "SSH port";
+
};
+
+
user = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = "Username for SSH connection";
+
};
+
+
identityFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
description = "Path to SSH identity file";
+
};
+
+
forwardAgent = mkOption {
+
type = types.nullOr types.bool;
+
default = null;
+
description = "Enable SSH agent forwarding";
+
};
+
+
extraOptions = mkOption {
+
type = types.attrsOf types.str;
+
default = { };
+
description = "Additional SSH options for this host";
+
};
+
+
zmx = mkOption {
+
type = types.bool;
+
default = false;
+
description = "Enable zmx persistent sessions for this host";
+
};
+
};
+
}
+
);
+
default = { };
+
description = "SSH host configurations";
+
};
+
};
+
+
config = mkIf cfg.enable {
+
# zmx provides pre-built binaries that we download instead of building from source
+
# This avoids the zig2nix dependency which causes issues in CI
+
home.packages =
+
(optionals cfg.zmx.enable [
+
pkgs.zmx-binary
+
pkgs.autossh
+
]);
+
+
programs.ssh = {
+
enable = true;
+
enableDefaultConfig = false;
+
+
matchBlocks =
+
let
+
# Convert atelier.ssh.hosts to SSH matchBlocks
+
hostConfigs = mapAttrs (
+
name: hostCfg:
+
{
+
hostname = mkIf (hostCfg.hostname != null) hostCfg.hostname;
+
port = mkIf (hostCfg.port != null) hostCfg.port;
+
user = mkIf (hostCfg.user != null) hostCfg.user;
+
identityFile = mkIf (hostCfg.identityFile != null) hostCfg.identityFile;
+
forwardAgent = mkIf (hostCfg.forwardAgent != null) hostCfg.forwardAgent;
+
extraOptions = hostCfg.extraOptions // (
+
if hostCfg.zmx then
+
{
+
RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %n";
+
RequestTTY = "yes";
+
ControlPath = "~/.ssh/cm-%r@%h:%p";
+
ControlMaster = "auto";
+
ControlPersist = "10m";
+
}
+
else
+
{ }
+
);
+
}
+
) cfg.hosts;
+
+
# Create zmx pattern hosts if enabled
+
zmxPatternHosts = if cfg.zmx.enable then
+
listToAttrs (
+
map (pattern:
+
let
+
patternHost = cfg.hosts.${pattern} or {};
+
in {
+
name = pattern;
+
value = {
+
hostname = mkIf (patternHost.hostname or null != null) patternHost.hostname;
+
port = mkIf (patternHost.port or null != null) patternHost.port;
+
user = mkIf (patternHost.user or null != null) patternHost.user;
+
extraOptions = {
+
RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %k";
+
RequestTTY = "yes";
+
ControlPath = "~/.ssh/cm-%r@%h:%p";
+
ControlMaster = "auto";
+
ControlPersist = "10m";
+
};
+
};
+
}) cfg.zmx.hosts
+
)
+
else
+
{ };
+
+
# Default match block for extraConfig
+
defaultBlock = if cfg.extraConfig != "" then
+
{
+
"*" = { };
+
}
+
else
+
{ };
+
in
+
defaultBlock // hostConfigs // zmxPatternHosts;
+
+
extraConfig = cfg.extraConfig;
+
};
+
+
# Add shell aliases for easier zmx usage
+
programs.zsh.shellAliases = mkIf cfg.zmx.enable {
+
zmls = "zmx list";
+
zmk = "zmx kill";
+
zma = "zmx attach";
+
ash = "autossh -M 0 -q";
+
};
+
+
programs.bash.shellAliases = mkIf cfg.zmx.enable {
+
zmls = "zmx list";
+
zmk = "zmx kill";
+
zma = "zmx attach";
+
ash = "autossh -M 0 -q";
+
};
+
+
programs.fish.shellAliases = mkIf cfg.zmx.enable {
+
zmls = "zmx list";
+
zmk = "zmx kill";
+
zma = "zmx attach";
+
ash = "autossh -M 0 -q";
+
};
+
};
+
}
-85
modules/home/apps/vscode.nix
···
-
{
-
lib,
-
pkgs,
-
config,
-
inputs,
-
...
-
}:
-
{
-
options.atelier.apps.vscode.enable = lib.mkEnableOption "Enable VSCode config";
-
config = lib.mkIf config.atelier.apps.vscode.enable {
-
nixpkgs.overlays = [
-
inputs.nix-vscode-extensions.overlays.default
-
inputs.catppuccin-vsc.overlays.default
-
];
-
programs.vscode = {
-
enable = true;
-
package = pkgs.unstable.vscode;
-
profiles.default = {
-
extensions = with pkgs.vscode-marketplace; [
-
ms-vscode.live-server
-
formulahendry.auto-rename-tag
-
edwinkofler.vscode-assorted-languages
-
golang.go
-
eamodio.gitlens
-
yzhang.markdown-all-in-one
-
github.vscode-github-actions
-
yoavbls.pretty-ts-errors
-
esbenp.prettier-vscode
-
ms-vscode.vscode-serial-monitor
-
prisma.prisma
-
ms-azuretools.vscode-docker
-
astro-build.astro-vscode
-
github.copilot
-
github.copilot-chat
-
dotjoshjohnson.xml
-
mikestead.dotenv
-
bradlc.vscode-tailwindcss
-
mechatroner.rainbow-csv
-
wakatime.vscode-wakatime
-
paulober.pico-w-go
-
ms-python.python
-
karunamurti.tera
-
biomejs.biome
-
bschulte.love
-
yinfei.luahelper
-
tamasfe.even-better-toml
-
fill-labs.dependi
-
rust-lang.rust-analyzer
-
dustypomerleau.rust-syntax
-
catppuccin.catppuccin-vsc
-
inputs.frc-nix.packages.${pkgs.stdenv.hostPlatform.system}.vscode-wpilib
-
];
-
userSettings = {
-
"editor.semanticHighlighting.enabled" = true;
-
"terminal.integrated.minimumContrastRatio" = 1;
-
"window.titleBarStyle" = "custom";
-
"gopls" = {
-
"ui.semanticTokens" = true;
-
};
-
"workbench.colorTheme" = "Catppuccin Macchiato";
-
"workbench.iconTheme" = "catppuccin-macchiato";
-
"catppuccin.accentColor" = lib.mkForce "blue";
-
"editor.fontFamily" = "'FiraCode Nerd Font', 'monospace', monospace";
-
"git.autofetch" = true;
-
"git.confirmSync" = false;
-
"github.copilot.editor.enableAutoCompletions" = false;
-
"editor.formatOnSave" = true;
-
"editor.defaultFormatter" = "biomejs.biome";
-
"[go]" = {
-
"editor.defaultFormatter" = "golang.go";
-
};
-
"[yaml]" = {
-
"editor.defaultFormatter" = "esbenp.prettier-vscode";
-
};
-
"[lua]" = {
-
"editor.defaultFormatter" = "yinfei.luahelper";
-
};
-
"[html]" = {
-
"editor.defaultFormatter" = "esbenp.prettier-vscode";
-
};
-
};
-
};
-
};
-
};
-
}
···
-26
modules/home/system/nixpkgs.nix
···
-
{
-
lib,
-
pkgs,
-
config,
-
inputs,
-
...
-
}:
-
{
-
options.nixpkgs.enable = lib.mkEnableOption "Enable custom nixpkgs overlays/config";
-
config = lib.mkIf config.nixpkgs.enable {
-
nixpkgs = {
-
overlays = [
-
(final: prev: {
-
unstable = import inputs.nixpkgs-unstable {
-
inherit (pkgs.stdenv.hostPlatform) system;
-
config.allowUnfree = true;
-
};
-
})
-
];
-
config = {
-
allowUnfree = true;
-
allowUnfreePredicate = _: true;
-
};
-
};
-
};
-
}
···
+26
modules/home/system/nixpkgs.nix.disabled
···
···
+
{
+
lib,
+
pkgs,
+
config,
+
inputs,
+
...
+
}:
+
{
+
options.nixpkgs.enable = lib.mkEnableOption "Enable custom nixpkgs overlays/config";
+
config = lib.mkIf config.nixpkgs.enable {
+
nixpkgs = {
+
overlays = [
+
(final: prev: {
+
unstable = import inputs.nixpkgs-unstable {
+
inherit (pkgs.stdenv.hostPlatform) system;
+
config.allowUnfree = true;
+
};
+
})
+
];
+
config = {
+
allowUnfree = true;
+
allowUnfreePredicate = _: true;
+
};
+
};
+
};
+
}
+11 -3
modules/home/system/shell.nix
···
echo
${pkgs.gum}/bin/gum style --foreground 35 --bold "Done! Ghostty is ready on $target"
'';
in
{
options.atelier.shell.enable = lib.mkEnableOption "Custom shell config";
···
template = "{{ if .SSHSession }}{{.HostName}} {{ end }}";
}
{
type = "path";
style = "plain";
background = "transparent";
···
style = "plain";
foreground = "p:grey";
background = "transparent";
-
template = "{{if not .Detached}}{{ .HEAD }}{{else}}@{{ printf \"%.7s\" .Commit.Sha }}{{end}}{{ if .Staging.Changed }} ({{ .Staging.String }}){{ end }}{{ if .Working.Changed }}*{{ end }} <cyan>{{ if .BranchStatus }}{{ .BranchStatus }}{{ end }}</>";
properties = {
branch_icon = "";
branch_identical_icon = "";
···
style = "plain";
foreground_templates = [
"{{if gt .Code 0}}red{{end}}"
-
"{{if eq .Code 0}}magenta{{end}}"
];
background = "transparent";
template = "โฏ";
···
transient_prompt = {
foreground_templates = [
"{{if gt .Code 0}}red{{end}}"
-
"{{if eq .Code 0}}magenta{{end}}"
];
background = "transparent";
template = "โฏ ";
···
echo
${pkgs.gum}/bin/gum style --foreground 35 --bold "Done! Ghostty is ready on $target"
'';
+
in
{
options.atelier.shell.enable = lib.mkEnableOption "Custom shell config";
···
template = "{{ if .SSHSession }}{{.HostName}} {{ end }}";
}
{
+
type = "text";
+
style = "plain";
+
background = "transparent";
+
foreground = "green";
+
template = "{{ if .Env.ZMX_SESSION }}[{{ .Env.ZMX_SESSION }}] {{ end }}";
+
}
+
{
type = "path";
style = "plain";
background = "transparent";
···
style = "plain";
foreground = "p:grey";
background = "transparent";
+
template = "{{if not .Detached}}{{ .HEAD }}{{else}}@{{ printf \"%.7s\" .Commit.Sha }}{{end}}{{ if .Staging.Changed }} ({{ .Staging.String }}){{ end }}{{ if .Working.Changed }}*{{ end }}{{ if .BranchStatus }}<cyan> {{ .BranchStatus }}</>{{ end }}";
properties = {
branch_icon = "";
branch_identical_icon = "";
···
style = "plain";
foreground_templates = [
"{{if gt .Code 0}}red{{end}}"
+
"{{if eq .Code 0}}{{if .Env.SSH_CONNECTION}}cyan{{else}}magenta{{end}}{{end}}"
];
background = "transparent";
template = "โฏ";
···
transient_prompt = {
foreground_templates = [
"{{if gt .Code 0}}red{{end}}"
+
"{{if eq .Code 0}}{{if .Env.SSH_CONNECTION}}cyan{{else}}magenta{{end}}{{end}}"
];
background = "transparent";
template = "โฏ ";
+48
modules/nixos/services/bore/README.md
···
···
+
# Bore
+
+
![screenshot](https://hc-cdn.hel1.your-objectstorage.com/s/v3/7652f29dacb8f76d_screenshot_2025-12-09_at_16.57.47.png)
+
+
Bore is a lightweight wrapper around `frp` which provides a dashboard and a nice `gum` based cli. It supports HTTP, TCP, and UDP tunneling. If you would like to run this in your own nix flake then simplify vendor this folder and `./modules/home/bore` and import the folders into the appropriate home manager and nixos configurations.
+
+
## Client Configuration
+
+
```nix
+
atelier = {
+
bore = {
+
enable = true;
+
authTokenFile = osConfig.age.secrets.bore.path
+
};
+
}
+
```
+
+
and be sure to have a definition for your agenix secret in the osConfig as well:
+
+
```nix
+
age = {
+
identityPaths = [
+
"path/to/ssh/key"
+
];
+
secrets = {
+
bore = {
+
file = ./path/to/bore.age;
+
owner = "username";
+
};
+
};
+
}
+
```
+
+
## Server Configuration
+
+
For TCP and UDP tunneling support, configure the server with allowed port ranges:
+
+
```nix
+
atelier.services.frps = {
+
enable = true;
+
domain = "bore.dunkirk.sh";
+
authTokenFile = config.age.secrets.bore.path;
+
allowedTCPPorts = [ 20000 20001 20002 20003 20004 ];
+
allowedUDPPorts = [ 20000 20001 20002 20003 20004 ];
+
};
+
```
+
+
The secret file is just a oneline file with the key in it. If you do end up deploying this feel free to email me and let me know! I would love to hear about your setup!
+190
modules/nixos/services/bore/bore.nix
···
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
let
+
cfg = config.atelier.services.frps;
+
in
+
{
+
options.atelier.services.frps = {
+
enable = lib.mkEnableOption "frp server for tunneling services";
+
+
bindAddr = lib.mkOption {
+
type = lib.types.str;
+
default = "0.0.0.0";
+
description = "Address to bind frp server to";
+
};
+
+
bindPort = lib.mkOption {
+
type = lib.types.port;
+
default = 7000;
+
description = "Port for frp control connection";
+
};
+
+
vhostHTTPPort = lib.mkOption {
+
type = lib.types.port;
+
default = 7080;
+
description = "Port for HTTP virtual host traffic";
+
};
+
+
allowedTCPPorts = lib.mkOption {
+
type = lib.types.listOf lib.types.port;
+
default = lib.lists.range 20000 20099;
+
example = [ 20000 20001 20002 20003 20004 ];
+
description = "TCP port range to allow for TCP tunnels (default: 20000-20099)";
+
};
+
+
allowedUDPPorts = lib.mkOption {
+
type = lib.types.listOf lib.types.port;
+
default = lib.lists.range 20000 20099;
+
example = [ 20000 20001 20002 20003 20004 ];
+
description = "UDP port range to allow for UDP tunnels (default: 20000-20099)";
+
};
+
+
authToken = lib.mkOption {
+
type = lib.types.nullOr lib.types.str;
+
default = null;
+
description = "Authentication token for clients (deprecated: use authTokenFile)";
+
};
+
+
authTokenFile = lib.mkOption {
+
type = lib.types.nullOr lib.types.path;
+
default = null;
+
description = "Path to file containing authentication token";
+
};
+
+
domain = lib.mkOption {
+
type = lib.types.str;
+
example = "bore.dunkirk.sh";
+
description = "Base domain for subdomains (e.g., *.bore.dunkirk.sh)";
+
};
+
+
enableCaddy = lib.mkOption {
+
type = lib.types.bool;
+
default = true;
+
description = "Automatically configure Caddy reverse proxy for wildcard domain";
+
};
+
};
+
+
config = lib.mkIf cfg.enable {
+
assertions = [
+
{
+
assertion = cfg.authToken != null || cfg.authTokenFile != null;
+
message = "Either authToken or authTokenFile must be set for frps";
+
}
+
];
+
+
# Open firewall ports for frp control connection and TCP/UDP tunnels
+
networking.firewall.allowedTCPPorts = [ cfg.bindPort ] ++ cfg.allowedTCPPorts;
+
networking.firewall.allowedUDPPorts = cfg.allowedUDPPorts;
+
+
# frp server service
+
systemd.services.frps =
+
let
+
tokenConfig =
+
if cfg.authTokenFile != null then
+
''
+
auth.tokenSource.type = "file"
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
+
''
+
else
+
''auth.token = "${cfg.authToken}"'';
+
+
configFile = pkgs.writeText "frps.toml" ''
+
bindAddr = "${cfg.bindAddr}"
+
bindPort = ${toString cfg.bindPort}
+
vhostHTTPPort = ${toString cfg.vhostHTTPPort}
+
+
# Dashboard and Prometheus metrics
+
webServer.addr = "127.0.0.1"
+
webServer.port = 7400
+
enablePrometheus = true
+
+
# Authentication token - clients need this to connect
+
auth.method = "token"
+
${tokenConfig}
+
+
# Subdomain support for *.${cfg.domain}
+
subDomainHost = "${cfg.domain}"
+
+
# Allow port ranges for TCP/UDP tunnels
+
# Format: [[{"start": 20000, "end": 20099}]]
+
allowPorts = [
+
{ start = 20000, end = 20099 }
+
]
+
+
# Custom 404 page
+
custom404Page = "${./404.html}"
+
+
# Logging
+
log.to = "console"
+
log.level = "info"
+
'';
+
in
+
{
+
description = "frp server for ${cfg.domain} tunneling";
+
after = [ "network.target" ];
+
wantedBy = [ "multi-user.target" ];
+
serviceConfig = {
+
Type = "simple";
+
Restart = "on-failure";
+
RestartSec = "5s";
+
ExecStart = "${pkgs.frp}/bin/frps -c ${configFile}";
+
};
+
};
+
+
# Automatically configure Caddy for wildcard domain
+
services.caddy = lib.mkIf cfg.enableCaddy {
+
# Dashboard for base domain
+
virtualHosts."${cfg.domain}" = {
+
extraConfig = ''
+
tls {
+
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
+
}
+
header {
+
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+
}
+
+
# Proxy /api/* to frps dashboard
+
handle /api/* {
+
reverse_proxy localhost:7400
+
}
+
+
# Serve dashboard HTML
+
handle {
+
root * ${./.}
+
try_files dashboard.html
+
file_server
+
}
+
'';
+
};
+
+
# Wildcard subdomain proxy to frps
+
virtualHosts."*.${cfg.domain}" = {
+
extraConfig = ''
+
tls {
+
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
+
}
+
header {
+
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+
}
+
reverse_proxy localhost:${toString cfg.vhostHTTPPort} {
+
header_up X-Forwarded-Proto {scheme}
+
header_up X-Forwarded-For {remote}
+
header_up Host {host}
+
}
+
handle_errors {
+
@404 expression {http.error.status_code} == 404
+
handle @404 {
+
root * ${./.}
+
rewrite * /404.html
+
file_server
+
}
+
}
+
'';
+
};
+
};
+
};
+
}
+24
modules/nixos/services/bore/bore.toml.example
···
···
+
# bore tunnel configuration
+
# Save this file as "bore.toml" in your project directory
+
+
[myapp]
+
port = 8000
+
+
[api]
+
port = 3000
+
protocol = "http"
+
label = "dev"
+
+
[frontend]
+
port = 5173
+
label = "local"
+
+
[database]
+
port = 5432
+
protocol = "tcp"
+
label = "postgres"
+
+
[game-server]
+
port = 27015
+
protocol = "udp"
+
label = "game"
+147 -8
modules/nixos/services/bore/dashboard.html
···
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bore</title>
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>๐Ÿš‡</text></svg>">
<style>
···
opacity: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
···
color: #e6edf3;
font-weight: 600;
margin-bottom: 0.25rem;
}
.tunnel-url {
···
</head>
<body class="loading">
<main class="container">
<header>
<h1>๐Ÿš‡ bore</h1>
···
<div class="stat-card">
<div class="stat-label">active tunnels</div>
<div class="stat-value" id="activeTunnels">โ€”</div>
</div>
<div class="stat-card">
<div class="stat-label">server status</div>
<div class="stat-value orange" id="serverStatus">โ€”</div>
</div>
<div class="stat-card">
-
<div class="stat-label">active connections</div>
-
<div class="stat-value" id="totalConnections">โ€”</div>
</div>
</div>
···
const MAX_FAIL_COUNT = 3;
let lastProxiesState = null;
async function fetchStats() {
try {
// Fetch server info
···
document.getElementById('activeTunnels').textContent = serverData.clientCounts || 0;
document.getElementById('serverStatus').textContent = 'online';
document.getElementById('totalConnections').textContent = serverData.curConns || 0;
// Update page title
const tunnelCount = serverData.clientCounts || 0;
···
}
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
} catch (error) {
fetchFailCount++;
document.getElementById('serverStatus').textContent = 'offline';
···
html += onlineTunnels.map(proxy => {
const subdomain = proxy.conf?.subdomain || 'unknown';
const url = `https://${subdomain}.bore.dunkirk.sh`;
return `
<div class="tunnel" data-tunnel="${proxy.name}">
<div class="tunnel-info">
-
<div class="tunnel-name">${proxy.name || 'unnamed'}</div>
<div class="tunnel-url">
<a href="${url}" target="_blank">${url}</a>
</div>
···
html += '<div class="offline-tunnels">';
html += '<div style="color: #8b949e; font-size: 0.85rem; margin-bottom: 0.75rem;">recently disconnected</div>';
html += offlineTunnels.map(proxy => {
if (!proxy.conf) {
return `
<div class="offline-tunnel" data-tunnel="${proxy.name}">
-
<span class="offline-tunnel-name">${proxy.name || 'unnamed'}</span>
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> โ€ข out: <span data-traffic-out="${proxy.name}">0 B</span></span>
</div>
`;
···
const url = `https://${subdomain}.bore.dunkirk.sh`;
return `
<div class="offline-tunnel" data-tunnel="${proxy.name}">
-
<span class="offline-tunnel-name">${proxy.name || 'unnamed'} โ†’ ${url}</span>
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> โ€ข out: <span data-traffic-out="${proxy.name}">0 B</span></span>
</div>
`;
···
// Update relative times every 10 seconds
setInterval(updateRelativeTimes, 10000);
-
-
// Remove loading class after initial fetch
-
document.body.classList.remove('loading');
</script>
</body>
···
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bore</title>
+
<meta name="description" content="bore - secure tunneling service for exposing local services to the internet">
+
<meta property="og:title" content="bore - tunnel dashboard">
+
<meta property="og:description" content="secure tunneling service powered by frp on bore.dunkirk.sh">
+
<meta property="og:type" content="website">
+
<meta property="og:url" content="https://bore.dunkirk.sh">
+
<meta name="twitter:card" content="summary">
+
<meta name="twitter:title" content="bore - tunnel dashboard">
+
<meta name="twitter:description" content="secure tunneling service powered by frp on bore.dunkirk.sh">
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>๐Ÿš‡</text></svg>">
<style>
···
opacity: 0;
}
+
.loading-bar {
+
position: fixed;
+
top: 0;
+
left: 0;
+
width: 100%;
+
height: 3px;
+
background: transparent;
+
z-index: 9999;
+
overflow: hidden;
+
}
+
+
.loading-bar::before {
+
content: '';
+
position: absolute;
+
top: 0;
+
left: 0;
+
width: 100%;
+
height: 100%;
+
background: linear-gradient(90deg, #14b8a6, #fb923c);
+
animation: loading 1.5s ease-in-out infinite;
+
}
+
+
body:not(.loading) .loading-bar {
+
display: none;
+
}
+
+
@keyframes loading {
+
0% {
+
transform: translateX(-100%);
+
}
+
50% {
+
transform: translateX(0%);
+
}
+
100% {
+
transform: translateX(100%);
+
}
+
}
+
.container {
max-width: 1200px;
margin: 0 auto;
···
color: #e6edf3;
font-weight: 600;
margin-bottom: 0.25rem;
+
display: flex;
+
align-items: center;
+
gap: 0.5rem;
+
}
+
+
.tunnel-label {
+
display: inline-block;
+
padding: 0.125rem 0.5rem;
+
font-size: 0.7rem;
+
font-weight: 500;
+
border-radius: 0;
+
margin-left: 0.25rem;
+
border: 1px solid;
}
.tunnel-url {
···
</head>
<body class="loading">
+
<div class="loading-bar"></div>
<main class="container">
<header>
<h1>๐Ÿš‡ bore</h1>
···
<div class="stat-card">
<div class="stat-label">active tunnels</div>
<div class="stat-value" id="activeTunnels">โ€”</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-label">active connections</div>
+
<div class="stat-value" id="totalConnections">โ€”</div>
</div>
<div class="stat-card">
<div class="stat-label">server status</div>
<div class="stat-value orange" id="serverStatus">โ€”</div>
</div>
<div class="stat-card">
+
<div class="stat-label">total upload</div>
+
<div class="stat-value" id="totalUpload">โ€”</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-label">total download</div>
+
<div class="stat-value" id="totalDownload">โ€”</div>
</div>
</div>
···
const MAX_FAIL_COUNT = 3;
let lastProxiesState = null;
+
// Predefined color palette for labels
+
const labelColors = [
+
{ color: '#a78bfa', bg: 'rgba(167, 139, 250, 0.2)' }, // purple
+
{ color: '#f472b6', bg: 'rgba(244, 114, 182, 0.2)' }, // pink
+
{ color: '#facc15', bg: 'rgba(250, 204, 21, 0.2)' }, // yellow
+
{ color: '#60a5fa', bg: 'rgba(96, 165, 250, 0.2)' }, // blue
+
{ color: '#f87171', bg: 'rgba(248, 113, 113, 0.2)' }, // red
+
{ color: '#38bdf8', bg: 'rgba(56, 189, 248, 0.2)' }, // sky
+
{ color: '#c084fc', bg: 'rgba(192, 132, 252, 0.2)' }, // violet
+
{ color: '#fb7185', bg: 'rgba(251, 113, 133, 0.2)' }, // rose
+
];
+
+
// Hash string to index
+
function stringToColorIndex(str) {
+
let hash = 0;
+
for (let i = 0; i < str.length; i++) {
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
+
}
+
return Math.abs(hash) % labelColors.length;
+
}
+
+
// Get label color and styles
+
function getLabelStyle(label) {
+
const trimmedLabel = label.trim();
+
if (trimmedLabel === 'prod') {
+
return {
+
color: '#22c55e',
+
bgColor: 'rgba(34, 197, 94, 0.2)',
+
borderColor: '#22c55e'
+
};
+
}
+
+
if (trimmedLabel === 'dev') {
+
return {
+
color: '#fb923c',
+
bgColor: 'rgba(251, 146, 60, 0.2)',
+
borderColor: '#fb923c'
+
};
+
}
+
+
const colorIndex = stringToColorIndex(trimmedLabel);
+
const colorScheme = labelColors[colorIndex];
+
return {
+
color: colorScheme.color,
+
bgColor: colorScheme.bg,
+
borderColor: colorScheme.color
+
};
+
}
+
async function fetchStats() {
try {
// Fetch server info
···
document.getElementById('activeTunnels').textContent = serverData.clientCounts || 0;
document.getElementById('serverStatus').textContent = 'online';
document.getElementById('totalConnections').textContent = serverData.curConns || 0;
+
document.getElementById('totalUpload').textContent = formatBytes(serverData.totalTrafficOut || 0);
+
document.getElementById('totalDownload').textContent = formatBytes(serverData.totalTrafficIn || 0);
// Update page title
const tunnelCount = serverData.clientCounts || 0;
···
}
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
+
+
// Remove loading class after first successful fetch
+
document.body.classList.remove('loading');
} catch (error) {
fetchFailCount++;
document.getElementById('serverStatus').textContent = 'offline';
···
html += onlineTunnels.map(proxy => {
const subdomain = proxy.conf?.subdomain || 'unknown';
const url = `https://${subdomain}.bore.dunkirk.sh`;
+
+
// Parse labels from proxy name (format: subdomain[label1,label2])
+
const labelMatch = proxy.name.match(/\[([^\]]+)\]$/);
+
const labels = labelMatch ? labelMatch[1].split(',') : [];
+
const displayName = labels.length > 0 ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name;
+
+
const labelHtml = labels.map(label => {
+
const trimmedLabel = label.trim();
+
const style = getLabelStyle(trimmedLabel);
+
return `<span class="tunnel-label" style="color: ${style.color}; background: ${style.bgColor}; border-color: ${style.borderColor};">${trimmedLabel}</span>`;
+
}).join('');
return `
<div class="tunnel" data-tunnel="${proxy.name}">
<div class="tunnel-info">
+
<div class="tunnel-name">
+
${displayName || 'unnamed'}
+
${labelHtml}
+
</div>
<div class="tunnel-url">
<a href="${url}" target="_blank">${url}</a>
</div>
···
html += '<div class="offline-tunnels">';
html += '<div style="color: #8b949e; font-size: 0.85rem; margin-bottom: 0.75rem;">recently disconnected</div>';
html += offlineTunnels.map(proxy => {
+
// Parse labels from proxy name (format: subdomain[label1,label2])
+
const labelMatch = proxy.name.match(/\[([^\]]+)\]$/);
+
const labels = labelMatch ? labelMatch[1].split(',').map(l => l.trim()) : [];
+
const displayName = labels.length > 0 ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name;
+
const labelStr = labels.length > 0 ? ` [${labels.join(', ')}]` : '';
+
if (!proxy.conf) {
return `
<div class="offline-tunnel" data-tunnel="${proxy.name}">
+
<span class="offline-tunnel-name">${displayName || 'unnamed'}${labelStr}</span>
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> โ€ข out: <span data-traffic-out="${proxy.name}">0 B</span></span>
</div>
`;
···
const url = `https://${subdomain}.bore.dunkirk.sh`;
return `
<div class="offline-tunnel" data-tunnel="${proxy.name}">
+
<span class="offline-tunnel-name">${displayName || 'unnamed'}${labelStr} โ†’ ${url}</span>
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> โ€ข out: <span data-traffic-out="${proxy.name}">0 B</span></span>
</div>
`;
···
// Update relative times every 10 seconds
setInterval(updateRelativeTimes, 10000);
</script>
</body>
-174
modules/nixos/services/bore/frps.nix
···
-
{
-
config,
-
lib,
-
pkgs,
-
...
-
}:
-
let
-
cfg = config.atelier.services.frps;
-
in
-
{
-
options.atelier.services.frps = {
-
enable = lib.mkEnableOption "frp server for tunneling services";
-
-
bindAddr = lib.mkOption {
-
type = lib.types.str;
-
default = "0.0.0.0";
-
description = "Address to bind frp server to";
-
};
-
-
bindPort = lib.mkOption {
-
type = lib.types.port;
-
default = 7000;
-
description = "Port for frp control connection";
-
};
-
-
vhostHTTPPort = lib.mkOption {
-
type = lib.types.port;
-
default = 7080;
-
description = "Port for HTTP virtual host traffic";
-
};
-
-
authToken = lib.mkOption {
-
type = lib.types.nullOr lib.types.str;
-
default = null;
-
description = "Authentication token for clients (deprecated: use authTokenFile)";
-
};
-
-
authTokenFile = lib.mkOption {
-
type = lib.types.nullOr lib.types.path;
-
default = null;
-
description = "Path to file containing authentication token";
-
};
-
-
domain = lib.mkOption {
-
type = lib.types.str;
-
example = "bore.dunkirk.sh";
-
description = "Base domain for subdomains (e.g., *.bore.dunkirk.sh)";
-
};
-
-
enableCaddy = lib.mkOption {
-
type = lib.types.bool;
-
default = true;
-
description = "Automatically configure Caddy reverse proxy for wildcard domain";
-
};
-
};
-
-
config = lib.mkIf cfg.enable {
-
assertions = [
-
{
-
assertion = cfg.authToken != null || cfg.authTokenFile != null;
-
message = "Either authToken or authTokenFile must be set for frps";
-
}
-
];
-
-
# Open firewall port for frp control connection
-
networking.firewall.allowedTCPPorts = [ cfg.bindPort ];
-
-
# frp server service
-
systemd.services.frps =
-
let
-
tokenConfig =
-
if cfg.authTokenFile != null then
-
''
-
auth.tokenSource.type = "file"
-
auth.tokenSource.file.path = "${cfg.authTokenFile}"
-
''
-
else
-
''auth.token = "${cfg.authToken}"'';
-
-
configFile = pkgs.writeText "frps.toml" ''
-
bindAddr = "${cfg.bindAddr}"
-
bindPort = ${toString cfg.bindPort}
-
vhostHTTPPort = ${toString cfg.vhostHTTPPort}
-
-
# Dashboard and Prometheus metrics
-
webServer.addr = "127.0.0.1"
-
webServer.port = 7400
-
enablePrometheus = true
-
-
# Authentication token - clients need this to connect
-
auth.method = "token"
-
${tokenConfig}
-
-
# Subdomain support for *.${cfg.domain}
-
subDomainHost = "${cfg.domain}"
-
-
# Custom 404 page
-
custom404Page = "${./404.html}"
-
-
# Logging
-
log.to = "console"
-
log.level = "info"
-
'';
-
in
-
{
-
description = "frp server for ${cfg.domain} tunneling";
-
after = [ "network.target" ];
-
wantedBy = [ "multi-user.target" ];
-
serviceConfig = {
-
Type = "simple";
-
Restart = "on-failure";
-
RestartSec = "5s";
-
ExecStart = "${pkgs.frp}/bin/frps -c ${configFile}";
-
};
-
};
-
-
# Automatically configure Caddy for wildcard domain
-
services.caddy = lib.mkIf cfg.enableCaddy {
-
# Dashboard for base domain
-
virtualHosts."${cfg.domain}" = {
-
extraConfig = ''
-
tls {
-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
-
}
-
header {
-
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-
}
-
-
# Proxy /metrics to frps dashboard
-
handle /metrics {
-
reverse_proxy localhost:7400
-
}
-
-
# Proxy /api/* to frps dashboard
-
handle /api/* {
-
reverse_proxy localhost:7400
-
}
-
-
# Serve dashboard HTML
-
handle {
-
root * ${./.}
-
try_files dashboard.html
-
file_server
-
}
-
'';
-
};
-
-
# Wildcard subdomain proxy to frps
-
virtualHosts."*.${cfg.domain}" = {
-
extraConfig = ''
-
tls {
-
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
-
}
-
header {
-
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-
}
-
reverse_proxy localhost:${toString cfg.vhostHTTPPort} {
-
header_up X-Forwarded-Proto {scheme}
-
header_up X-Forwarded-For {remote}
-
header_up Host {host}
-
}
-
handle_errors {
-
@404 expression {http.error.status_code} == 404
-
handle @404 {
-
root * ${./.}
-
rewrite * /404.html
-
file_server
-
}
-
}
-
'';
-
};
-
};
-
};
-
}
···
+48
packages/zmx.nix
···
···
+
{ pkgs, lib, stdenv, fetchurl, autoPatchelfHook }:
+
+
stdenv.mkDerivation rec {
+
pname = "zmx";
+
version = "0.1.0";
+
+
src = fetchurl {
+
url = if stdenv.isLinux then
+
(if stdenv.isAarch64 then
+
"https://zmx.sh/a/zmx-${version}-linux-aarch64.tar.gz"
+
else
+
"https://zmx.sh/a/zmx-${version}-linux-x86_64.tar.gz")
+
else if stdenv.isDarwin then
+
(if stdenv.isAarch64 then
+
"https://zmx.sh/a/zmx-${version}-macos-aarch64.tar.gz"
+
else
+
"https://zmx.sh/a/zmx-${version}-macos-x86_64.tar.gz")
+
else throw "Unsupported platform";
+
+
hash = if stdenv.isLinux && stdenv.isAarch64 then
+
"sha256-cMGo+Af0VRY3c2EoNzVZFU53Kz5wKL8zsSSXIOtZVU8="
+
else if stdenv.isLinux then
+
"sha256-Zmqs/Y3be2z9KMuSwyTLZWKbIInzHgoC9Bm0S2jv3XI="
+
else if stdenv.isDarwin && stdenv.isAarch64 then
+
"sha256-34k5Q1cIr3+foubtMJVoHVHZtCLoSjwJK00e1p0JdLg="
+
else
+
"sha256-0epjoQhUSBYlE0L7Ubwn/sJF61+4BbxeaRx6EY/SklE=";
+
};
+
+
nativeBuildInputs = lib.optionals stdenv.isLinux [ autoPatchelfHook ];
+
+
sourceRoot = ".";
+
+
installPhase = ''
+
runHook preInstall
+
mkdir -p $out/bin
+
cp zmx $out/bin/
+
chmod +x $out/bin/zmx
+
runHook postInstall
+
'';
+
+
meta = with lib; {
+
description = "Session persistence for terminal processes";
+
homepage = "https://zmx.sh";
+
license = licenses.mit;
+
platforms = platforms.unix;
+
};
+
}