feat(dev-env): aiolabs dev environment — options, lib, config, presets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
02504f9fd3
commit
7960f82494
6 changed files with 1689 additions and 0 deletions
143
modules/dev-env/README.md
Normal file
143
modules/dev-env/README.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# modules/dev-env
|
||||
|
||||
Declarative NixOS module for managing a multi-project dev environment.
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Nix owns the configuration**, bash owns the runtime. Nix renders
|
||||
`/etc/dev-env/config.sh` and `projects.json`; installed bash scripts
|
||||
source those at call time. Navigation helpers walk the filesystem at
|
||||
runtime — adding a new branch is `git worktree add`, never
|
||||
`nixos-rebuild`.
|
||||
|
||||
2. **The deploy flake is the single source of truth** for deployed refs.
|
||||
Set `dev-env.deploy.flakeInput = "deploy-flake"` and projects will be
|
||||
derived from `inputs.deploy-flake.inputs` whose URLs live under the
|
||||
configured forgejo host. Hand-authored extras go in
|
||||
`dev-env.projects`.
|
||||
|
||||
3. **Bootstrap is user-invoked, not an activation hook.**
|
||||
`dev-env-bootstrap` materializes bare repos + worktrees. It is never
|
||||
run during `nixos-rebuild` — rebuilds stay fast and offline.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `default.nix` | Imports options, lib, config. Module entry point. |
|
||||
| `options.nix` | `mkOption` declarations for every knob. |
|
||||
| `lib.nix` | `mkProject` constructor + `deriveFromFlakeInput` parser. |
|
||||
| `config.nix` | Renders config files, installs scripts, wires git hooks. |
|
||||
| `presets/aiolabs.nix` | Hand-authored project list for the aiolabs ecosystem. |
|
||||
| `scripts/*.sh` | Ported + new bash helpers loaded via `builtins.readFile`. |
|
||||
| `scripts/git-hooks/pre-commit` | Shared secret-scanner hook (via `core.hooksPath`). |
|
||||
| `docs/*.md` | Runbooks shipped to the user's `~/Documents/dev-env/`. |
|
||||
|
||||
## Using from omni
|
||||
|
||||
```nix
|
||||
# omni/flake.nix
|
||||
inputs.deploy-flake = {
|
||||
url = "git+ssh://forgejo@git.atitlan.io/padreug/deploy-unified";
|
||||
};
|
||||
|
||||
# omni/configuration.nix
|
||||
imports = [
|
||||
./modules/dev-env
|
||||
./modules/dev-env/presets/aiolabs.nix # opt-in to aiolabs defaults
|
||||
];
|
||||
|
||||
dev-env = {
|
||||
enable = true;
|
||||
forgejo.org = "aiolabs";
|
||||
github.forkUser = "your-github-username";
|
||||
deploy.flakeInput = "deploy-flake";
|
||||
deploy.targets = {
|
||||
host1 = "root@host1.example";
|
||||
host2 = "root@host2.example";
|
||||
host3 = "root@host3.example";
|
||||
host4 = "root@host4.example";
|
||||
host5 = "root@host5.example";
|
||||
host6 = "root@host6.example";
|
||||
host7 = "root@host7.example";
|
||||
host8 = "root@host8.example";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Bootstrap workflow
|
||||
|
||||
```bash
|
||||
# 1. Rebuild with dev-env enabled
|
||||
sudo nixos-rebuild switch --flake .#omni
|
||||
|
||||
# 2. Dry-run to see what will be created
|
||||
dev-env-bootstrap --dry-run
|
||||
|
||||
# 3. Materialize bare repos + worktrees
|
||||
dev-env-bootstrap
|
||||
|
||||
# 4. Navigate
|
||||
lb dev # → ~/dev/lnbits/dev
|
||||
bs # → ~/dev/bitspire/bitspire/dev
|
||||
lam lightning-pub # → ~/dev/lamassu-next/lightning-pub/dev
|
||||
prb lnbits fix-x # → ~/dev/upstream-prs/lnbits-fix-x on upstream/main
|
||||
|
||||
# 5. Deploy
|
||||
dev-deploy host5 # uses locked deploy-flake input
|
||||
dev-deploy --local host1 # overrides inputs with local worktrees
|
||||
```
|
||||
|
||||
## Extending
|
||||
|
||||
Add a new project in `presets/aiolabs.nix` (or a new preset):
|
||||
|
||||
```nix
|
||||
dev-env.projects.my-new-project = lib.mkProject {
|
||||
category = "shared";
|
||||
upstream = "https://github.com/example/my-project";
|
||||
worktrees.main.branch = "main";
|
||||
};
|
||||
```
|
||||
|
||||
Then rebuild and `dev-env-bootstrap`. Existing worktrees are never
|
||||
clobbered — the bootstrap script refuses to touch a worktree whose
|
||||
current branch differs from the declared one.
|
||||
|
||||
## What this module does NOT do
|
||||
|
||||
- Manage bitSpire's internal `flake.nix`/`devenv.nix` — that project
|
||||
has its own dev environment.
|
||||
- Run `git fetch`/`git pull` automatically — use `wts`/`wtu` helpers or
|
||||
a manual `git fetch --all`.
|
||||
- Replace `direnv` per-worktree `.envrc` files — it just makes sure
|
||||
`nix-direnv` is enabled (omni's home.nix already does this).
|
||||
|
||||
## Reference docs (`docs/`)
|
||||
|
||||
LNbits-domain guides that travel with this module — so the `lnbits` pack
|
||||
is a self-contained dev environment whether used on omni or imported
|
||||
standalone (`nixosModules.lnbits`). These were consolidated here from the
|
||||
former standalone `lnbits-sensei` scaffold, which this pack supersedes.
|
||||
|
||||
- [`lnbits-workspace-notes.md`](docs/lnbits-workspace-notes.md) — practical
|
||||
day-to-day gotchas: ports, `LNBITS_SRC` build-context traps,
|
||||
extension-folder-upgrade wiping forks, settings precedence (`.env` vs
|
||||
DB), Nostr keys, CLINK scope, fork versioning.
|
||||
- [`lnbits-extension-dev.md`](docs/lnbits-extension-dev.md) — building
|
||||
extensions: auth-decorator distinctions, FakeWallet vs regtest testing,
|
||||
the `migrations_fork.py` pattern.
|
||||
- [`lnbits-frontend-gotchas.md`](docs/lnbits-frontend-gotchas.md) —
|
||||
Vue/Quasar UMD traps (no self-closing tags, CSS specificity vs Quasar
|
||||
`!important`, cache busting, dark-mode discipline).
|
||||
- [`lnbits-upstream-flow.md`](docs/lnbits-upstream-flow.md) — how
|
||||
`lnbits/lnbits` itself moves (dev/main branch model, squash-merge,
|
||||
release tagging).
|
||||
- [`upstream-prs.md`](docs/upstream-prs.md) — sending a PR upstream via the
|
||||
`~/dev/upstream-prs/` worktree flow (with a fork/PR primer).
|
||||
- [`remotes.md`](docs/remotes.md) — the three remote-topology patterns
|
||||
(upstream-only / github-fork / multi-remote with a private host).
|
||||
- [`secrets-management.md`](docs/secrets-management.md) — getting secrets
|
||||
out of `.env` into sops-encrypted YAML.
|
||||
- [`stack-overview.md`](docs/stack-overview.md) — the aiolabs stack map
|
||||
(which repo is what, how the pieces fit).
|
||||
355
modules/dev-env/config.nix
Normal file
355
modules/dev-env/config.nix
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs ? { },
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
mkIf
|
||||
mkDefault
|
||||
mkMerge
|
||||
optional
|
||||
;
|
||||
cfg = config.dev-env;
|
||||
helpers = cfg.lib;
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Auto-derive projects from the deploy flake's inputs.
|
||||
#
|
||||
# Reads the deploy flake's flake.lock at eval time, walks every
|
||||
# direct input declared by the deploy flake, filters to URLs under
|
||||
# our configured forgejo host, and constructs project entries.
|
||||
# Multiple inputs pointing at the same repo with different refs are
|
||||
# collapsed into a single project with multiple worktrees via
|
||||
# groupDerivedProjects (e.g., webapp + webapp-dev → one `webapp`
|
||||
# project with two worktrees, main and dev).
|
||||
#
|
||||
# Returns {} when:
|
||||
# - deriveProjectsFromInputs = false
|
||||
# - flakeInput = null
|
||||
# - the input isn't in scope (e.g. smoke test running without
|
||||
# specialArgs.inputs)
|
||||
#
|
||||
# Reading the lock file directly is more robust than walking
|
||||
# inputs.<flake>.inputs because the lock file has a stable,
|
||||
# documented shape regardless of flake-compat quirks.
|
||||
# ---------------------------------------------------------------------
|
||||
derivedProjects =
|
||||
if
|
||||
cfg.deploy.deriveProjectsFromInputs
|
||||
&& cfg.deploy.flakeInput != null
|
||||
&& (inputs ? ${cfg.deploy.flakeInput})
|
||||
then
|
||||
let
|
||||
deployFlake = inputs.${cfg.deploy.flakeInput};
|
||||
lockData = builtins.fromJSON (builtins.readFile "${deployFlake}/flake.lock");
|
||||
|
||||
# Root node enumerates the deploy flake's direct inputs as
|
||||
# { inputName = nodeName; ... }. A few inputs reference shared
|
||||
# nodes via a list (inherited inputs); we drop those since
|
||||
# they aren't the direct declarations we care about.
|
||||
rootInputs = lib.filterAttrs (_: v: builtins.isString v) (lockData.nodes.root.inputs or { });
|
||||
|
||||
# Reconstruct a URL in the format parseForgejoUrl expects
|
||||
# (git+<scheme>://host/org/repo[.git][?ref=branch]) from a
|
||||
# flake.lock node. Lock entries store the URL without the
|
||||
# `git+` prefix and put the ref in a separate field, so we
|
||||
# normalize both.
|
||||
#
|
||||
# The `original` block records what the user declared. When
|
||||
# they declared `?ref=main` explicitly, original.ref is set.
|
||||
# When they declared no ref (implicit default branch), the
|
||||
# `locked` block still has the resolved ref — but in the
|
||||
# form `refs/heads/main`. We fall back to that and strip
|
||||
# the prefix so the output matches the format parseForgejoUrl
|
||||
# expects.
|
||||
normalizeLockEntry =
|
||||
nodeName:
|
||||
let
|
||||
node = lockData.nodes.${nodeName} or { };
|
||||
src = node.original or node.locked or { };
|
||||
url = src.url or "";
|
||||
rawRef =
|
||||
if src ? ref && src.ref != null then
|
||||
src.ref
|
||||
else if node ? locked && node.locked ? ref then
|
||||
node.locked.ref
|
||||
else
|
||||
null;
|
||||
ref = if rawRef == null then null else lib.removePrefix "refs/heads/" rawRef;
|
||||
refSuffix = if ref != null then "?ref=${ref}" else "";
|
||||
in
|
||||
if url == "" then null else "git+${url}${refSuffix}";
|
||||
|
||||
parsed = lib.mapAttrsToList (
|
||||
inputName: nodeName:
|
||||
let
|
||||
url = normalizeLockEntry nodeName;
|
||||
in
|
||||
if url == null then null else helpers.deriveFromFlakeInput { inherit inputName url; }
|
||||
) rootInputs;
|
||||
in
|
||||
helpers.groupDerivedProjects parsed
|
||||
else
|
||||
{ };
|
||||
|
||||
# Final project set — hand-authored entries from cfg.projects
|
||||
# completely override derived ones on key collision (shallow merge
|
||||
# via //). Keys only in derivedProjects get auto-populated; keys in
|
||||
# cfg.projects use the hand-authored version as-is.
|
||||
#
|
||||
# Rationale: deep-merge would give inconsistent behavior because
|
||||
# cfg.projects entries already have every submodule default applied
|
||||
# (e.g. `upstream = null`), and those defaults would silently
|
||||
# replace derived values. Shallow merge forces the user to write
|
||||
# complete entries when overriding — predictable and obvious.
|
||||
mergedProjects = derivedProjects // cfg.projects;
|
||||
|
||||
# Resolve a project's complete shape (paths + remotes) once, so the
|
||||
# JSON renderer and the bash scripts both see identical data.
|
||||
resolveProject =
|
||||
name: project:
|
||||
let
|
||||
bare = helpers.bareRepoPath name project;
|
||||
remotes = helpers.projectRemotes project;
|
||||
|
||||
# Where a non-worktree clone would land
|
||||
cloneCategoryDir = if project.category != null then "${cfg.root}/${project.category}" else cfg.root;
|
||||
clonePath = "${cloneCategoryDir}/${project.worktreeRoot}";
|
||||
|
||||
resolvedWorktrees = lib.mapAttrs (wtName: wt: {
|
||||
inherit (wt) branch remote;
|
||||
path = helpers.worktreePath name project wtName;
|
||||
}) project.worktrees;
|
||||
in
|
||||
{
|
||||
inherit (project)
|
||||
forgejoRepo
|
||||
upstream
|
||||
githubFork
|
||||
category
|
||||
worktreeRoot
|
||||
isClone
|
||||
deployFlakeInput
|
||||
;
|
||||
barePath = bare;
|
||||
clonePath = clonePath;
|
||||
remotes = remotes;
|
||||
worktrees = resolvedWorktrees;
|
||||
};
|
||||
|
||||
resolvedProjects = lib.mapAttrs resolveProject mergedProjects;
|
||||
|
||||
projectsJson = pkgs.writeText "dev-env-projects.json" (builtins.toJSON resolvedProjects);
|
||||
|
||||
tmuxSessionsJson = pkgs.writeText "dev-env-tmux-sessions.json" (builtins.toJSON cfg.tmux.sessions);
|
||||
|
||||
# Render /etc/dev-env/config.sh — the bash-readable runtime config.
|
||||
# Provides DEV_ROOT, REPOS_DIR, etc. and per-host DEPLOY_TARGET_<HOST>
|
||||
# env vars (the format dev-deploy looks for).
|
||||
renderConfigSh = pkgs.writeText "dev-env-config.sh" ''
|
||||
# Auto-generated by dev-env module — do not edit.
|
||||
# Source from /etc/dev-env/config.sh
|
||||
|
||||
export DEV_ROOT="${cfg.root}"
|
||||
export REPOS_DIR="${cfg.root}/repos"
|
||||
export LNBITS_DIR="${cfg.root}/lnbits"
|
||||
export WEBAPP_DIR="${cfg.root}/webapp"
|
||||
export DEPLOY_DIR="${cfg.root}/deploy"
|
||||
export SHARED_DIR="${cfg.root}/shared"
|
||||
export LOCAL_DIR="${cfg.root}/local"
|
||||
export DOCS_DIR="${cfg.root}/docs"
|
||||
export UPSTREAM_PRS_DIR="${cfg.root}/upstream-prs"
|
||||
export BITSPIRE_DIR="${cfg.root}/bitspire"
|
||||
export LAMASSU_NEXT_DIR="${cfg.root}/lamassu-next"
|
||||
|
||||
export FORGEJO_HOST="${cfg.forgejo.host}"
|
||||
export FORGEJO_SSH="${cfg.forgejo.sshUser}@${cfg.forgejo.host}"
|
||||
export FORGEJO_ORG="${cfg.forgejo.org}"
|
||||
export GITHUB_SSH="git@github.com"
|
||||
${lib.optionalString (cfg.github.forkUser != null) ''
|
||||
export GITHUB_FORK_USER="${cfg.github.forkUser}"
|
||||
''}
|
||||
|
||||
export DEVENV_PROJECTS_JSON="/etc/dev-env/projects.json"
|
||||
export DEVENV_WRITE_DIRENV_HINTS="${if cfg.writeDirenvHints then "1" else "0"}"
|
||||
${lib.optionalString (cfg.deploy.flakeInput != null) ''
|
||||
export DEVENV_DEPLOY_FLAKE_INPUT="${cfg.deploy.flakeInput}"
|
||||
''}
|
||||
|
||||
# Deploy targets — one env var per host
|
||||
${lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
host: target: ''export DEPLOY_TARGET_${lib.replaceStrings [ "-" ] [ "_" ] host}="${target}"''
|
||||
) cfg.deploy.targets
|
||||
)}
|
||||
|
||||
# List of all deploy hosts (bash array)
|
||||
export DEPLOY_TARGETS=(${
|
||||
lib.concatStringsSep " " (lib.mapAttrsToList (host: _: ''"${host}"'') cfg.deploy.targets)
|
||||
})
|
||||
'';
|
||||
|
||||
# Bash script wrappers — load source verbatim from ./scripts/*.sh.
|
||||
# Using readFile keeps editor tooling/shellcheck working on the .sh files.
|
||||
mkScriptBin = name: src: pkgs.writeShellScriptBin name (builtins.readFile src);
|
||||
|
||||
# Sourceable bash modules (functions only) that get loaded by
|
||||
# /etc/profile.d/dev-env-functions.sh into every interactive shell.
|
||||
shellFnSources = [
|
||||
./scripts/nav.sh
|
||||
./scripts/worktree.sh
|
||||
./scripts/pr-helpers.sh
|
||||
]
|
||||
++ optional cfg.regtest.enable ./scripts/regtest.sh;
|
||||
|
||||
shellFnLoader = pkgs.writeText "dev-env-functions.sh" ''
|
||||
# Auto-generated by dev-env module.
|
||||
# Sources every dev-env shell-function module into the current shell.
|
||||
${lib.concatMapStringsSep "\n" (src: ''
|
||||
if [[ -r ${src} ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source ${src}
|
||||
fi
|
||||
'') shellFnSources}
|
||||
'';
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
config = mkIf cfg.enable (mkMerge [
|
||||
{
|
||||
# 1) /etc/dev-env/* config files (machine-readable)
|
||||
environment.etc = {
|
||||
"dev-env/config.sh".source = renderConfigSh;
|
||||
"dev-env/projects.json".source = projectsJson;
|
||||
"dev-env/tmux-sessions.json".source = tmuxSessionsJson;
|
||||
# Shared bash libraries sourced by the scripts at runtime —
|
||||
# config loader (function modules) + colour palette (bins).
|
||||
"dev-env/lib.sh".source = ./scripts/lib.sh;
|
||||
"dev-env/lib-colors.sh".source = ./scripts/lib-colors.sh;
|
||||
};
|
||||
|
||||
# 2) Loader so interactive shells (login OR non-login) get the
|
||||
# functions. We can't rely on /etc/profile.d/*.sh alone
|
||||
# because NixOS only sources that from /etc/profile (login
|
||||
# shells). Hyprland-launched terminals (Alacritty, etc.)
|
||||
# are interactive non-login shells, so they would never see
|
||||
# these functions. `environment.interactiveShellInit` is
|
||||
# sourced by both /etc/bashrc and /etc/zshrc on every
|
||||
# interactive shell, which is what we want. We still install
|
||||
# the file under /etc/profile.d for ssh-without-tty cases
|
||||
# and for users who want to source it explicitly.
|
||||
environment.etc."profile.d/dev-env-functions.sh" = {
|
||||
source = shellFnLoader;
|
||||
};
|
||||
environment.interactiveShellInit = ''
|
||||
if [[ -r /etc/profile.d/dev-env-functions.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/profile.d/dev-env-functions.sh
|
||||
fi
|
||||
'';
|
||||
|
||||
# 3) System packages — every standalone helper.
|
||||
environment.systemPackages = [
|
||||
# core deps used by every script
|
||||
pkgs.git
|
||||
pkgs.jq
|
||||
|
||||
# standalone helpers
|
||||
(mkScriptBin "dev-env-bootstrap" ./scripts/bootstrap.sh)
|
||||
(mkScriptBin "dev-status" ./scripts/status.sh)
|
||||
(mkScriptBin "dev-tm" ./scripts/tmux-launch.sh)
|
||||
(mkScriptBin "dev-deploy" ./scripts/deploy.sh)
|
||||
(mkScriptBin "rebase" ./scripts/rebase.sh)
|
||||
]
|
||||
++ lib.optionals cfg.regtest.enable [
|
||||
(mkScriptBin "regtest-start" (
|
||||
pkgs.writeShellScript "rs" ''
|
||||
source ${./scripts/regtest.sh}
|
||||
regtest-start "$@"
|
||||
''
|
||||
))
|
||||
(mkScriptBin "regtest-stop" (
|
||||
pkgs.writeShellScript "rs2" ''
|
||||
source ${./scripts/regtest.sh}
|
||||
regtest-stop "$@"
|
||||
''
|
||||
))
|
||||
(mkScriptBin "regtest-status" (
|
||||
pkgs.writeShellScript "rs3" ''
|
||||
source ${./scripts/regtest.sh}
|
||||
regtest-status "$@"
|
||||
''
|
||||
))
|
||||
(mkScriptBin "regtest-lnbits-rebuild" (
|
||||
pkgs.writeShellScript "rs5" ''
|
||||
source ${./scripts/regtest.sh}
|
||||
regtest-lnbits-rebuild "$@"
|
||||
''
|
||||
))
|
||||
(mkScriptBin "regtest-lnbits-restart" (
|
||||
pkgs.writeShellScript "rs6" ''
|
||||
source ${./scripts/regtest.sh}
|
||||
regtest-lnbits-restart "$@"
|
||||
''
|
||||
))
|
||||
];
|
||||
|
||||
# 4) tmpfiles to ensure user dirs exist (only the leaf state dir;
|
||||
# everything else is created by dev-env-bootstrap on demand).
|
||||
systemd.tmpfiles.rules = lib.optional (config.dev-env.user or null != null) (
|
||||
let
|
||||
user = config.dev-env.user;
|
||||
in
|
||||
"d /home/${user}/.local/state/dev-env 0755 ${user} users -"
|
||||
);
|
||||
}
|
||||
|
||||
# 5) regtest implies docker. Set the standard option directly rather
|
||||
# than toggling omni.features.containers, so this module is
|
||||
# importable without omni. On omni the developer preset still
|
||||
# enables features.containers (→ the fuller docker block in
|
||||
# core.nix), and this mkDefault yields to it.
|
||||
(mkIf cfg.regtest.enable {
|
||||
virtualisation.docker.enable = mkDefault true;
|
||||
})
|
||||
|
||||
# 6) Shared git pre-commit via core.hooksPath, applied per-user via
|
||||
# home-manager so the user's git config picks it up.
|
||||
(mkIf (cfg.gitHooks.enable && (config.dev-env.user or null) != null) {
|
||||
home-manager.users.${config.dev-env.user} =
|
||||
{ ... }:
|
||||
{
|
||||
home.file.".local/share/dev-env/git-hooks/pre-commit" = {
|
||||
source = ./scripts/git-hooks/pre-commit;
|
||||
executable = true;
|
||||
};
|
||||
programs.git.settings.core.hooksPath = "/home/${config.dev-env.user}/.local/share/dev-env/git-hooks";
|
||||
};
|
||||
})
|
||||
|
||||
# 7) Optional legacy compat — also write the old .devenv.conf so any
|
||||
# loose bash scripts still reading it keep working during migration.
|
||||
(mkIf (cfg.legacyConfigFile != null && (config.dev-env.user or null) != null) {
|
||||
home-manager.users.${config.dev-env.user} =
|
||||
{ ... }:
|
||||
{
|
||||
home.file.${
|
||||
# home.file is keyed relative to $HOME, so strip the prefix
|
||||
lib.removePrefix "/home/${config.dev-env.user}/" cfg.legacyConfigFile
|
||||
} =
|
||||
{
|
||||
text = ''
|
||||
# Legacy compat shim — sourced by old .devenv.d/*.sh scripts.
|
||||
# Canonical config is /etc/dev-env/config.sh.
|
||||
source /etc/dev-env/config.sh
|
||||
'';
|
||||
};
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
17
modules/dev-env/default.nix
Normal file
17
modules/dev-env/default.nix
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# dev-env module entry point.
|
||||
#
|
||||
# This module declaratively manages a multi-project development environment
|
||||
# (bare repos, worktrees, navigation helpers, tmux sessions, regtest docker
|
||||
# env, upstream-PR workflow). It sits on top of omni and consumes the
|
||||
# unified deploy flake as a source of truth for deployed refs.
|
||||
#
|
||||
# The module only configures tools; it does not materialize repos on disk.
|
||||
# That is the job of the user-invoked `dev-env-bootstrap` script installed
|
||||
# by config.nix.
|
||||
{
|
||||
imports = [
|
||||
./options.nix
|
||||
./lib.nix
|
||||
./config.nix
|
||||
];
|
||||
}
|
||||
241
modules/dev-env/lib.nix
Normal file
241
modules/dev-env/lib.nix
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
cfg = config.dev-env;
|
||||
|
||||
# Parse a forgejo URL into { namespace, repo, ref }.
|
||||
#
|
||||
# Accepts:
|
||||
# git+ssh://forgejo@host/org/repo.git?ref=branch
|
||||
# git+ssh://forgejo@host/org/repo?ref=branch
|
||||
# git+https://host/org/repo.git?ref=branch
|
||||
# git+https://host/org/repo?ref=branch
|
||||
#
|
||||
# Returns null if the URL does not match a forgejo host.
|
||||
#
|
||||
# Implementation note: Nix's `builtins.match` uses POSIX ERE, which
|
||||
# does NOT support non-greedy quantifiers (`+?`/`*?`). A naive regex
|
||||
# like `([^?]+?)(\.git)?` lets the `+?` act greedy, producing a repo
|
||||
# name of `webapp.git`. Instead we match everything before `?ref=`
|
||||
# or end-of-string as the raw repo name, then strip `.git` after.
|
||||
parseForgejoUrl =
|
||||
url:
|
||||
let
|
||||
host = cfg.forgejo.host;
|
||||
# One regex covering both schemes. Groups:
|
||||
# 1: namespace
|
||||
# 2: repo (possibly with .git suffix)
|
||||
# 3: optional "?ref=..." (kept for completeness)
|
||||
# 4: optional ref value
|
||||
sshMatch = builtins.match "git\\+ssh://[^@]+@${host}/([^/]+)/([^?]+)(\\?ref=(.+))?" url;
|
||||
httpsMatch = builtins.match "git\\+https://${host}/([^/]+)/([^?]+)(\\?ref=(.+))?" url;
|
||||
m =
|
||||
if sshMatch != null then
|
||||
sshMatch
|
||||
else if httpsMatch != null then
|
||||
httpsMatch
|
||||
else
|
||||
null;
|
||||
in
|
||||
if m == null then
|
||||
null
|
||||
else
|
||||
{
|
||||
namespace = builtins.elemAt m 0;
|
||||
repo = lib.removeSuffix ".git" (builtins.elemAt m 1);
|
||||
ref = builtins.elemAt m 3;
|
||||
};
|
||||
|
||||
# mkProject: shorthand constructor for `dev-env.projects.<name>`.
|
||||
#
|
||||
# Fills in conventional defaults so users write less boilerplate.
|
||||
# The returned attrset matches the `projectType` submodule schema.
|
||||
mkProject =
|
||||
{
|
||||
name ? null,
|
||||
category ? null,
|
||||
upstream ? null,
|
||||
forgejoRepo ? null, # defaults to "${cfg.forgejo.org}/${name}"
|
||||
originUrl ? null, # override origin URL (e.g. GitHub-only repos)
|
||||
githubFork ? null, # defaults from github.forkUser
|
||||
worktrees ? { },
|
||||
isClone ? false,
|
||||
deployFlakeInput ? null,
|
||||
worktreeRoot ? null, # defaults to name
|
||||
}:
|
||||
{
|
||||
forgejoRepo =
|
||||
if forgejoRepo != null then
|
||||
forgejoRepo
|
||||
else if originUrl != null then
|
||||
null
|
||||
else if name != null then
|
||||
"${cfg.forgejo.org}/${name}"
|
||||
else
|
||||
throw "dev-env.lib.mkProject: set forgejoRepo, originUrl, or name";
|
||||
inherit
|
||||
originUrl
|
||||
upstream
|
||||
category
|
||||
isClone
|
||||
deployFlakeInput
|
||||
;
|
||||
githubFork =
|
||||
if githubFork != null then
|
||||
githubFork
|
||||
else if upstream != null && cfg.github.forkUser != null && name != null then
|
||||
"git@github.com:${cfg.github.forkUser}/${name}.git"
|
||||
else
|
||||
null;
|
||||
worktreeRoot =
|
||||
if worktreeRoot != null then
|
||||
worktreeRoot
|
||||
else if name != null then
|
||||
name
|
||||
else
|
||||
"";
|
||||
inherit worktrees;
|
||||
};
|
||||
|
||||
# deriveFromFlakeInput: parse one flake input url into a project entry
|
||||
# (or null if the URL doesn't belong to our forgejo).
|
||||
#
|
||||
# Input: { name = "webapp-host5"; url = "git+ssh://forgejo@git.atitlan.io/aiolabs/webapp.git?ref=host5"; }
|
||||
# Output: { namespace = "aiolabs"; repo = "webapp"; ref = "host5"; }
|
||||
deriveFromFlakeInput =
|
||||
{ inputName, url }:
|
||||
let
|
||||
parsed = parseForgejoUrl url;
|
||||
in
|
||||
if parsed == null then
|
||||
null
|
||||
else
|
||||
{
|
||||
inputName = inputName;
|
||||
repo = parsed.repo;
|
||||
namespace = parsed.namespace;
|
||||
ref = parsed.ref;
|
||||
};
|
||||
|
||||
# Collapse a list of `{ inputName, namespace, repo, ref }` into
|
||||
# `{ <repo> = { forgejoRepo, worktrees = { <ref> = { branch = ref; }; ... }; }; }`.
|
||||
#
|
||||
# Multiple inputs can share the same repo with different refs; this is
|
||||
# the common case for webapp + webapp-dev — both become worktrees of
|
||||
# `webapp` (main and dev respectively).
|
||||
groupDerivedProjects =
|
||||
parsedList:
|
||||
lib.foldl' (
|
||||
acc: entry:
|
||||
if entry == null || entry.ref == null then
|
||||
acc
|
||||
else
|
||||
let
|
||||
key = entry.repo;
|
||||
existing =
|
||||
acc.${key} or {
|
||||
forgejoRepo = "${entry.namespace}/${entry.repo}";
|
||||
originUrl = null;
|
||||
worktreeRoot = entry.repo;
|
||||
worktrees = { };
|
||||
upstream = null;
|
||||
githubFork = null;
|
||||
category = null;
|
||||
isClone = false;
|
||||
deployFlakeInput = null;
|
||||
};
|
||||
newWorktree = {
|
||||
branch = entry.ref;
|
||||
path = null;
|
||||
remote = "origin";
|
||||
};
|
||||
in
|
||||
acc
|
||||
// {
|
||||
${key} = existing // {
|
||||
worktrees = existing.worktrees // {
|
||||
${entry.ref} = newWorktree;
|
||||
};
|
||||
# Record the LAST input name that contributed — good enough
|
||||
# for --override-input wiring.
|
||||
deployFlakeInput = entry.inputName;
|
||||
};
|
||||
}
|
||||
) { } parsedList;
|
||||
|
||||
# Resolve worktree filesystem paths. Projects with category live under
|
||||
# ${root}/${category}/${worktreeRoot}/<worktree>; otherwise
|
||||
# ${root}/${worktreeRoot}/<worktree>.
|
||||
worktreePath =
|
||||
projectName: project: worktreeName:
|
||||
let
|
||||
root = cfg.root;
|
||||
sub =
|
||||
if project.category != null then
|
||||
"${project.category}/${project.worktreeRoot}"
|
||||
else
|
||||
project.worktreeRoot;
|
||||
leaf =
|
||||
let
|
||||
wt = project.worktrees.${worktreeName} or null;
|
||||
in
|
||||
if wt != null && wt.path != null then wt.path else worktreeName;
|
||||
in
|
||||
"${root}/${sub}/${leaf}";
|
||||
|
||||
# Bare repo path (always ${root}/repos/<basename>.git).
|
||||
# Falls back to projectName when forgejoRepo is null (GitHub-only projects).
|
||||
bareRepoPath =
|
||||
projectName: project:
|
||||
let
|
||||
basename =
|
||||
if project.forgejoRepo != null then
|
||||
lib.last (lib.splitString "/" project.forgejoRepo)
|
||||
else
|
||||
projectName;
|
||||
in
|
||||
"${cfg.root}/repos/${basename}.git";
|
||||
|
||||
# Construct the remotes for a project. `origin` is `originUrl` if set,
|
||||
# otherwise derived from the forgejo host + forgejoRepo. Projects with
|
||||
# neither end up with no origin (filtered out below).
|
||||
projectRemotes =
|
||||
project:
|
||||
lib.filterAttrs (_: v: v != null) {
|
||||
origin =
|
||||
if project.originUrl or null != null then
|
||||
project.originUrl
|
||||
else if project.forgejoRepo != null then
|
||||
"${cfg.forgejo.sshUser}@${cfg.forgejo.host}:${project.forgejoRepo}.git"
|
||||
else
|
||||
null;
|
||||
upstream = project.upstream;
|
||||
github-fork = project.githubFork;
|
||||
};
|
||||
|
||||
helpers = {
|
||||
inherit
|
||||
mkProject
|
||||
deriveFromFlakeInput
|
||||
parseForgejoUrl
|
||||
groupDerivedProjects
|
||||
worktreePath
|
||||
bareRepoPath
|
||||
projectRemotes
|
||||
;
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options.dev-env.lib = mkOption {
|
||||
type = types.attrs;
|
||||
internal = true;
|
||||
description = "dev-env module helper functions (internal).";
|
||||
};
|
||||
|
||||
config = {
|
||||
dev-env.lib = helpers;
|
||||
};
|
||||
}
|
||||
369
modules/dev-env/options.nix
Normal file
369
modules/dev-env/options.nix
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkEnableOption mkOption types;
|
||||
|
||||
# One entry in dev-env.projects.
|
||||
#
|
||||
# A project is a git repo with one or more worktrees (or a single clone
|
||||
# if `isClone = true`). Worktrees are derived from a bare repo at
|
||||
# `${dev-env.root}/repos/${name}.git` by default; that path is managed
|
||||
# by the bootstrap script.
|
||||
projectType = types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
forgejoRepo = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
<org>/<repo> on the configured forgejo host. Used to construct
|
||||
the `origin` remote. May be null for projects whose origin
|
||||
lives outside forgejo — in that case set `originUrl`.
|
||||
'';
|
||||
example = "aiolabs/lnbits";
|
||||
};
|
||||
|
||||
originUrl = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Override the origin remote URL. By default, origin is derived
|
||||
from the forgejo host + forgejoRepo. Set this to clone from
|
||||
an arbitrary URL (e.g. a GitHub repo that has no forgejo mirror).
|
||||
'';
|
||||
example = "https://github.com/nostr-protocol/nips";
|
||||
};
|
||||
|
||||
upstream = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
HTTPS URL of the upstream OSS repo. Set to null for projects
|
||||
with no upstream (divergent branches, original work).
|
||||
When set, the `upstream` remote is added to the bare repo and
|
||||
the `prb`/`prc`/rebase helpers know how to sync from it.
|
||||
'';
|
||||
example = "https://github.com/lnbits/lnbits";
|
||||
};
|
||||
|
||||
githubFork = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Full SSH URL of the personal github fork used for upstream PRs
|
||||
(the `github-fork` remote). If null, derived from the global
|
||||
`dev-env.github.forkUser` + the basename of `forgejoRepo`.
|
||||
'';
|
||||
example = "git@github.com:your-github-username/lnbits.git";
|
||||
};
|
||||
|
||||
category = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Sub-directory under `dev-env.root` that groups related
|
||||
projects. e.g. "bitspire" groups the bitSpire frontend +
|
||||
atm-tui. null puts the project at the top level.
|
||||
'';
|
||||
example = "bitspire";
|
||||
};
|
||||
|
||||
worktreeRoot = mkOption {
|
||||
type = types.str;
|
||||
default = if name == null then "" else name;
|
||||
defaultText = "project name";
|
||||
description = ''
|
||||
Directory name (relative to `category` or root) under which
|
||||
worktrees live. Defaults to the project name.
|
||||
'';
|
||||
};
|
||||
|
||||
worktrees = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
options = {
|
||||
branch = mkOption {
|
||||
type = types.str;
|
||||
description = "Branch to check out in this worktree.";
|
||||
};
|
||||
path = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Override the worktree directory name. Defaults to the
|
||||
attribute key.
|
||||
'';
|
||||
};
|
||||
remote = mkOption {
|
||||
type = types.str;
|
||||
default = "origin";
|
||||
description = ''
|
||||
Which remote the branch tracks. Usually origin (forgejo)
|
||||
but could be "upstream" for read-only tracking worktrees.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
description = ''
|
||||
Worktrees to materialize for this project, keyed by worktree
|
||||
name. Each worktree becomes a directory at
|
||||
`''${dev-env.root}/''${category}/''${worktreeRoot}/<key>` (or under
|
||||
root if category is null).
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
dev = { branch = "dev"; };
|
||||
main = { branch = "main"; };
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
isClone = mkEnableOption ''
|
||||
Treat this project as a regular clone rather than a bare-repo
|
||||
worktree set. Used for projects that won't have multiple
|
||||
simultaneous branches checked out (e.g. atm-tui)
|
||||
'';
|
||||
|
||||
deployFlakeInput = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Name of the flake input in the deploy flake that this project
|
||||
corresponds to. Used by `dev-deploy --local` to know which
|
||||
--override-input to pass.
|
||||
'';
|
||||
example = "lnbits";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
# One entry in dev-env.tmux.sessions.
|
||||
tmuxSessionType = types.submodule {
|
||||
options = {
|
||||
cwd = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Session-default cwd, relative to `dev-env.root`. Windows can
|
||||
override with their own cwd.
|
||||
'';
|
||||
};
|
||||
windows = mkOption {
|
||||
type = types.listOf (
|
||||
types.submodule {
|
||||
options = {
|
||||
name = mkOption { type = types.str; };
|
||||
cwd = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Window cwd, relative to dev-env.root.";
|
||||
};
|
||||
cmd = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Command to run in the window on creation.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = [ ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options.dev-env = {
|
||||
enable = mkEnableOption "dev-env multi-project development environment";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "user";
|
||||
description = ''
|
||||
Owner of the dev tree. Drives `root`'s default and the per-user
|
||||
git-hooks / state-dir paths. Set this to your login user when
|
||||
importing dev-env standalone; on omni the host wires it to
|
||||
`settings.user`. Kept self-contained (a plain default, no
|
||||
`config.omni.*` read) so this pack is importable without omni.
|
||||
'';
|
||||
};
|
||||
|
||||
root = mkOption {
|
||||
type = types.str;
|
||||
default = "/home/${config.dev-env.user}/dev";
|
||||
defaultText = "/home/\${config.dev-env.user}/dev";
|
||||
description = ''
|
||||
Root directory for the dev environment. Bare repos live under
|
||||
`''${root}/repos/`, worktrees under `''${root}/<category>/<project>/`.
|
||||
'';
|
||||
};
|
||||
|
||||
forgejo = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "git.atitlan.io";
|
||||
description = "Forgejo server hostname.";
|
||||
};
|
||||
sshUser = mkOption {
|
||||
type = types.str;
|
||||
default = "forgejo";
|
||||
description = ''
|
||||
SSH user for git+ssh access to forgejo. Some installations use
|
||||
"git", others use "forgejo". Defaults to "forgejo" because the
|
||||
reference deployment uses that.
|
||||
'';
|
||||
};
|
||||
org = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Default forgejo org/user under which your forks live. Used as
|
||||
the namespace when a project's `forgejoRepo` is a bare name.
|
||||
'';
|
||||
example = "aiolabs";
|
||||
};
|
||||
};
|
||||
|
||||
github = {
|
||||
forkUser = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Personal github username used for upstream PR forks (the
|
||||
`github-fork` remote). When set, projects that declare an
|
||||
upstream automatically get a github-fork remote derived from
|
||||
`git@github.com:<forkUser>/<basename>.git`.
|
||||
'';
|
||||
example = "your-github-username";
|
||||
};
|
||||
};
|
||||
|
||||
projects = mkOption {
|
||||
type = types.attrsOf projectType;
|
||||
default = { };
|
||||
description = ''
|
||||
Hand-authored project list. This is merged with projects derived
|
||||
from the deploy flake (when `deploy.deriveProjectsFromInputs` is
|
||||
true). Hand entries win on key collision.
|
||||
'';
|
||||
};
|
||||
|
||||
deploy = {
|
||||
flakeInput = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Name of the omni flake input that points at the unified
|
||||
deploy flake. When set, the bootstrap script clones it to
|
||||
`''${root}/deploy/<flakeInput>` as a working copy, and
|
||||
`dev-deploy` uses it as the --flake source by default.
|
||||
'';
|
||||
example = "deploy-flake";
|
||||
};
|
||||
|
||||
targets = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
description = ''
|
||||
Map from hostname to SSH target used by `dev-deploy`. Mirrors
|
||||
the TARGETS associative array in optimize-deploys/unified/
|
||||
deploy.sh.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
host1 = "root@host1.example";
|
||||
host5 = "root@host5.example";
|
||||
host7 = "root@host7.example";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
deriveProjectsFromInputs = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
When true and `flakeInput` is set, derive a default project
|
||||
entry for every flake input of the deploy flake whose URL is
|
||||
under the forgejo host. Hand-authored `projects` win on key
|
||||
collision.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
regtest = {
|
||||
enable = mkEnableOption ''
|
||||
Unified Bitcoin/Lightning regtest docker environment. Enables
|
||||
virtualisation.docker (via mkDefault)
|
||||
'';
|
||||
|
||||
repoUrl = mkOption {
|
||||
type = types.str;
|
||||
default = "forgejo@git.atitlan.io:aiolabs/lnbits-regtest.git";
|
||||
description = ''
|
||||
Git URL of the regtest docker-compose repo. Cloned to
|
||||
`''${root}/local/docker/regtest` by the bootstrap script.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
tmux = {
|
||||
enable = mkEnableOption "declarative tmux session launcher (dev-tm)";
|
||||
|
||||
sessions = mkOption {
|
||||
type = types.attrsOf tmuxSessionType;
|
||||
default = { };
|
||||
description = ''
|
||||
Named tmux session layouts. The `dev-tm <name>` script reads
|
||||
these from /etc/dev-env/tmux-sessions.json at runtime and
|
||||
recreates the session.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
gitHooks = {
|
||||
enable =
|
||||
mkEnableOption ''
|
||||
Shared pre-commit hook via core.hooksPath. Installs a single
|
||||
hook that scans for secrets (prv keys, hardcoded passwords,
|
||||
unencrypted sops files) in all repos under dev-env.root
|
||||
''
|
||||
// {
|
||||
default = true;
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "${config.dev-env.root or "/tmp"}/.git-hooks";
|
||||
defaultText = ''"''${root}/.git-hooks"'';
|
||||
description = "Directory used for git core.hooksPath.";
|
||||
};
|
||||
};
|
||||
|
||||
writeDirenvHints = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
When true, the bootstrap script writes a default `.envrc`
|
||||
containing `use flake` into each worktree that has a flake.nix
|
||||
but no existing .envrc. Never clobbers existing files.
|
||||
'';
|
||||
};
|
||||
|
||||
legacyConfigFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = "${config.dev-env.root or "/tmp"}/.devenv.conf";
|
||||
defaultText = ''"''${root}/.devenv.conf"'';
|
||||
description = ''
|
||||
Path at which to also write a legacy `.devenv.conf` for any
|
||||
loose bash scripts still reading it. The canonical config is at
|
||||
/etc/dev-env/config.sh; this is a transitional aid. Set to null
|
||||
to disable once migration is complete.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
564
modules/dev-env/presets/aiolabs.nix
Normal file
564
modules/dev-env/presets/aiolabs.nix
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
# aiolabs preset — opinionated project list for the aiolabs ecosystem
|
||||
#
|
||||
# Reproduces the current ~/dev tree declaratively. Importing this file
|
||||
# is opt-in; the dev-env module itself ships no projects by default.
|
||||
#
|
||||
# Includes the projects pinned by optimize-deploys/unified/flake.nix
|
||||
# (which dev-env.deploy.deriveProjectsFromInputs would derive
|
||||
# automatically when wired up) plus the dev-only worktrees that don't
|
||||
# have a deploy artifact.
|
||||
#
|
||||
# Tmux sessions mirror the layouts in ~/dev/.devenv.d/20-tmux-sessions.sh
|
||||
# but expressed as a Nix attrset rendered to JSON for dev-tm.
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
mk = config.dev-env.lib.mkProject;
|
||||
in
|
||||
|
||||
{
|
||||
# Gated: applies only when the aiolabs pack is enabled. Imported (inert)
|
||||
# via modules/packs/aiolabs.nix. Enabling aiolabs implies lnbits.
|
||||
config = lib.mkIf config.omni.packs.aiolabs.enable {
|
||||
omni.packs.lnbits.enable = true;
|
||||
|
||||
dev-env = {
|
||||
forgejo.org = lib.mkDefault "aiolabs";
|
||||
github.forkUser = lib.mkDefault "your-github-username";
|
||||
|
||||
deploy.targets = lib.mkDefault {
|
||||
host1 = "root@host1.example";
|
||||
host2 = "root@host2.example";
|
||||
host3 = "root@host3.example";
|
||||
host4 = "root@host4.example";
|
||||
host5 = "root@host5.example";
|
||||
host6 = "root@host6.example";
|
||||
host7 = "root@host7.example";
|
||||
host8 = "root@host8.example";
|
||||
};
|
||||
|
||||
projects = {
|
||||
|
||||
# ─── lnbits: shared codebase, dev/main branches ────────────────
|
||||
|
||||
lnbits = mk {
|
||||
name = "lnbits";
|
||||
category = null;
|
||||
upstream = "https://github.com/lnbits/lnbits";
|
||||
deployFlakeInput = "lnbits";
|
||||
worktrees = {
|
||||
dev = {
|
||||
branch = "dev";
|
||||
};
|
||||
main = {
|
||||
branch = "main";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# ─── nsecbunkerd: nostr remote signer daemon (NIP-46) ──────────
|
||||
|
||||
nsecbunkerd = mk {
|
||||
name = "nsecbunkerd";
|
||||
category = null;
|
||||
upstream = "https://github.com/kind-0/nsecbunkerd";
|
||||
worktrees = {
|
||||
master = {
|
||||
branch = "master";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# ─── webapp: divergent per-target branches ─────────────────────
|
||||
|
||||
webapp = mk {
|
||||
name = "webapp";
|
||||
category = null;
|
||||
upstream = null; # no upstream — original work
|
||||
deployFlakeInput = "webapp";
|
||||
isClone = true; # single branch — per-target config lives
|
||||
# in the deploy flake's services.webapp.*
|
||||
# options, not in separate branches
|
||||
};
|
||||
|
||||
# ─── boilerplate-website: vue+vite starter, spawn worktrees per site ─
|
||||
|
||||
boilerplate-website = mk {
|
||||
name = "boilerplate-website";
|
||||
category = null;
|
||||
upstream = null; # original work
|
||||
worktrees = {
|
||||
main = {
|
||||
branch = "main";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# ─── ATM stack ──────────────────────────────────────────────────
|
||||
#
|
||||
# bitspire (active) groups the bitSpire ATM frontend + atm-tui.
|
||||
# lamassu-next (legacy, retained until the last machines migrate)
|
||||
# groups the lamassu-next frontend + the lightning-pub fork it
|
||||
# used. nostr-lightning-wallet is a single clone at top level.
|
||||
|
||||
bitspire = mk {
|
||||
name = "bitspire";
|
||||
category = "bitspire";
|
||||
forgejoRepo = "aiolabs/bitspire";
|
||||
upstream = null; # original
|
||||
worktrees = {
|
||||
main = {
|
||||
branch = "main";
|
||||
};
|
||||
dev = {
|
||||
branch = "dev";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
lamassu-next = mk {
|
||||
name = "lamassu-next";
|
||||
category = "lamassu-next";
|
||||
forgejoRepo = "aiolabs/lamassu-next";
|
||||
upstream = null;
|
||||
deployFlakeInput = "lamassu-next";
|
||||
worktrees = {
|
||||
main = {
|
||||
branch = "main";
|
||||
};
|
||||
dev = {
|
||||
branch = "dev";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
lightning-pub = mk {
|
||||
name = "lightning-pub";
|
||||
category = "lamassu-next";
|
||||
forgejoRepo = "aiolabs/lightning-pub";
|
||||
upstream = "https://github.com/shocknet/Lightning.Pub";
|
||||
worktrees = {
|
||||
master = {
|
||||
branch = "master";
|
||||
};
|
||||
dev = {
|
||||
branch = "dev";
|
||||
};
|
||||
nip05 = {
|
||||
branch = "feature/nip05";
|
||||
};
|
||||
marketplace = {
|
||||
branch = "feature/marketplace";
|
||||
};
|
||||
withdraw = {
|
||||
branch = "feature/withdraw";
|
||||
};
|
||||
extension-loader = {
|
||||
branch = "feature/extension-loader";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nostr-lightning-wallet = mk {
|
||||
name = "nostr-lightning-wallet";
|
||||
category = null;
|
||||
forgejoRepo = "aiolabs/shockwallet-vue";
|
||||
upstream = null;
|
||||
isClone = true; # single clone, has its own flake
|
||||
deployFlakeInput = "shockwallet-vue";
|
||||
};
|
||||
|
||||
atm-tui = mk {
|
||||
name = "atm-tui";
|
||||
category = "bitspire";
|
||||
forgejoRepo = "aiolabs/atm-tui";
|
||||
upstream = null; # original
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# ─── shared OSS forks ────────────────────────────────────────────
|
||||
|
||||
lamassu-server = mk {
|
||||
name = "lamassu-server";
|
||||
category = "shared";
|
||||
forgejoRepo = "aiolabs/lamassu-server";
|
||||
upstream = "https://github.com/lamassu/lamassu-server";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
lamassu-machine = mk {
|
||||
name = "lamassu-machine";
|
||||
category = "shared";
|
||||
forgejoRepo = "aiolabs/lamassu-machine";
|
||||
upstream = "https://github.com/lamassu/lamassu-machine";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
nix-bitcoin = mk {
|
||||
name = "nix-bitcoin";
|
||||
category = "shared";
|
||||
forgejoRepo = "aiolabs/nix-bitcoin";
|
||||
upstream = "https://github.com/fort-nix/nix-bitcoin";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# ─── lnbits extensions (forked + original) ───────────────────────
|
||||
|
||||
ext-nostrrelay = mk {
|
||||
name = "nostrrelay";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/nostrrelay";
|
||||
upstream = "https://github.com/lnbits/nostrrelay";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-nostrclient = mk {
|
||||
name = "nostrclient";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/nostrclient";
|
||||
upstream = "https://github.com/lnbits/nostrclient";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-nostrmarket = mk {
|
||||
name = "nostrmarket";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/nostrmarket";
|
||||
upstream = "https://github.com/lnbits/nostrmarket";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-events = mk {
|
||||
name = "events";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/events";
|
||||
upstream = "https://github.com/lnbits/events";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-spirekeeper = mk {
|
||||
name = "spirekeeper";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/spirekeeper";
|
||||
upstream = null; # original — bitSpire operator dashboard
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-libra = mk {
|
||||
name = "libra";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/libra";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-restaurant = mk {
|
||||
name = "restaurant";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/restaurant";
|
||||
upstream = null; # original
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-withdraw = mk {
|
||||
name = "withdraw";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/withdraw";
|
||||
upstream = "https://github.com/lnbits/withdraw";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-lnurlp = mk {
|
||||
name = "lnurlp";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/lnurlp";
|
||||
upstream = "https://github.com/lnbits/lnurlp";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-splitpayments = mk {
|
||||
name = "splitpayments";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/splitpayments";
|
||||
upstream = "https://github.com/lnbits/splitpayments";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
ext-tasks = mk {
|
||||
name = "tasks";
|
||||
category = "shared/extensions";
|
||||
forgejoRepo = "aiolabs/tasks";
|
||||
upstream = null; # original
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# ─── community-organizer surfaces ───────────────────────────────
|
||||
#
|
||||
# Matrix-side bot plugins + the foyer e-ink renderer. Both consume
|
||||
# the NIP-52/NIP-72 community-organizer protocol defined in
|
||||
# ~/dev/maubot-plugins/docs/community-organizer-spec.md and feed
|
||||
# into the wider aiolabs Nostr-native stack (see /etc/nixos/docs/
|
||||
# system-map.md for the end-to-end picture).
|
||||
|
||||
maubot-plugins = mk {
|
||||
name = "maubot-plugins";
|
||||
category = null;
|
||||
forgejoRepo = "aiolabs/maubot-plugins";
|
||||
upstream = null; # original
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
inky-impression = mk {
|
||||
name = "inky-impression";
|
||||
category = null;
|
||||
forgejoRepo = "aiolabs/inky-impression";
|
||||
upstream = null; # original
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# ─── auxiliary ──────────────────────────────────────────────────
|
||||
#
|
||||
# Note: host5-home and atitlanio-pitch are both auto-derived from
|
||||
# the server-deploy flake's inputs, so they don't need preset
|
||||
# entries. See modules/dev-env/config.nix `derivedProjects`.
|
||||
|
||||
# The deploy flake itself (also a project so dev-env-bootstrap
|
||||
# clones it for in-place editing alongside everything else).
|
||||
# Not derived from the flake because a flake can't list itself
|
||||
# as an input — this must be hand-authored.
|
||||
server-deploy = mk {
|
||||
name = "server-deploy";
|
||||
category = "deploy";
|
||||
forgejoRepo = "aiolabs/server-deploy";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# Regtest environment (Bitcoin/Lightning local dev)
|
||||
regtest = mk {
|
||||
name = "regtest";
|
||||
category = "local/docker";
|
||||
forgejoRepo = "aiolabs/regtest";
|
||||
upstream = "https://github.com/lnbits/legend-regtest-enviroment";
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# Curated upstream-reference catalogue + weekly digests.
|
||||
# Bootstrap clones the repo to ~/dev/refs/; bin/refresh
|
||||
# then materialises repos/<group>/<owner>/<name> from refs.toml.
|
||||
# The home-manager refs-refresh systemd timer runs that weekly;
|
||||
# for first-bootstrap immediacy, run `~/dev/refs/bin/refresh`.
|
||||
refs = mk {
|
||||
name = "refs";
|
||||
category = null;
|
||||
forgejoRepo = "aiolabs/refs";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# Documentation (Quartz static site)
|
||||
castle-docs = mk {
|
||||
name = "castle-docs";
|
||||
category = "docs";
|
||||
forgejoRepo = "padreug/castle-docs";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# ─── reference material ─────────────────────────────────────────
|
||||
|
||||
nips = mk {
|
||||
name = "nips";
|
||||
category = "nostr-protocol";
|
||||
originUrl = "https://github.com/nostr-protocol/nips";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
quasar = mk {
|
||||
name = "quasar";
|
||||
category = "vue-framework";
|
||||
originUrl = "https://github.com/quasarframework/quasar";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
# Lightning.Pub + wallet2 upstreams now live as read-only mirrors
|
||||
# under ~/dev/refs/ (see refs.toml), not as project worktrees.
|
||||
|
||||
extension-builder-stub = mk {
|
||||
name = "extension_builder_stub";
|
||||
category = "lnbits-reference";
|
||||
originUrl = "https://github.com/lnbits/extension_builder_stub";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
|
||||
regtest-upstream = mk {
|
||||
name = "regtest-upstream";
|
||||
category = "lnbits-upstream";
|
||||
originUrl = "https://github.com/lnbits/legend-regtest-enviroment";
|
||||
upstream = null;
|
||||
isClone = true;
|
||||
};
|
||||
};
|
||||
|
||||
# ─── tmux sessions (mirror the existing layouts) ──────────────────
|
||||
|
||||
tmux.enable = lib.mkDefault true;
|
||||
|
||||
tmux.sessions = {
|
||||
lnbits = {
|
||||
cwd = "lnbits";
|
||||
windows = [
|
||||
{
|
||||
name = "dev";
|
||||
cwd = "lnbits/dev";
|
||||
cmd = "nvim .";
|
||||
}
|
||||
{
|
||||
name = "main";
|
||||
cwd = "lnbits/main";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "extensions";
|
||||
cwd = "shared/extensions";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "term";
|
||||
cwd = "lnbits/dev";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "git";
|
||||
cwd = "lnbits/dev";
|
||||
cmd = "lazygit";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
webapp = {
|
||||
cwd = "webapp";
|
||||
windows = [
|
||||
{
|
||||
name = "edit";
|
||||
cwd = "webapp";
|
||||
cmd = "nvim .";
|
||||
}
|
||||
{
|
||||
name = "term";
|
||||
cwd = "webapp";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "git";
|
||||
cwd = "webapp";
|
||||
cmd = "lazygit";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
shared = {
|
||||
cwd = "shared";
|
||||
windows = [
|
||||
{
|
||||
name = "extensions";
|
||||
cwd = "shared/extensions";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "lamassu";
|
||||
cwd = "shared/lamassu-server";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "nix-bitcoin";
|
||||
cwd = "shared/nix-bitcoin";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "git";
|
||||
cwd = "shared";
|
||||
cmd = "wts";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
local = {
|
||||
cwd = "local";
|
||||
windows = [
|
||||
{
|
||||
name = "docker";
|
||||
cwd = "local/docker";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "lazydocker";
|
||||
cwd = "local/docker";
|
||||
cmd = "lazydocker";
|
||||
}
|
||||
{
|
||||
name = "logs";
|
||||
cwd = "local";
|
||||
cmd = null;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
bitspire = {
|
||||
cwd = "bitspire/bitspire";
|
||||
windows = [
|
||||
{
|
||||
name = "dev";
|
||||
cwd = "bitspire/bitspire/dev";
|
||||
cmd = "nvim .";
|
||||
}
|
||||
{
|
||||
name = "main";
|
||||
cwd = "bitspire/bitspire/main";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "atm-tui";
|
||||
cwd = "bitspire/atm-tui";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "git";
|
||||
cwd = "bitspire/bitspire/dev";
|
||||
cmd = "lazygit";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
lightning-pub = {
|
||||
cwd = "lamassu-next/lightning-pub";
|
||||
windows = [
|
||||
{
|
||||
name = "dev";
|
||||
cwd = "lamassu-next/lightning-pub/dev";
|
||||
cmd = "nvim .";
|
||||
}
|
||||
{
|
||||
name = "master";
|
||||
cwd = "lamassu-next/lightning-pub/master";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "extensions";
|
||||
cwd = "lamassu-next/lightning-pub/extension-loader";
|
||||
cmd = null;
|
||||
}
|
||||
{
|
||||
name = "git";
|
||||
cwd = "lamassu-next/lightning-pub/dev";
|
||||
cmd = "lazygit";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue