feat(dev-env): backport matured dev-env implementation from /etc/nixos
Replace the stub dev-env with the real, working implementation that grew
in the reference machine config — de-identified for the public scaffold.
Nix layer:
- options.nix: full project schema (url/upstream/fork/category/
worktreeRoot/worktrees{branch,path,remote}/isClone/deployFlakeInput),
deploy.targets, github.forkUser, writeDirenvHints. Drops the
forgejo-URL block + deploy-flake auto-derivation (incoherent in a
scaffold that uses explicit per-project urls).
- lib.nix: mkProject + worktreePath/bareRepoPath/projectRemotes,
generalized to the explicit-url model (origin falls back to upstream).
- config.nix: renders /etc/dev-env/{config.sh,projects.json,
tmux-sessions.json}, installs helpers via writeShellScriptBin, loads
shell functions into interactive shells, wires the git pre-commit hook.
Scripts (config-driven, read /etc/dev-env at runtime):
- bootstrap.sh, nav.sh, worktree.sh, pr-helpers.sh, rebase.sh,
status.sh, deploy.sh, regtest.sh, tmux-launch.sh.
- Stripped aiolabs/forgejo/bitspire/lamassu/webapp hardcoding; the
github-fork remote is renamed 'fork' to match git.remotes vocabulary.
- Removes the dev.sh stub (the matured impl uses discrete commands +
shell functions, not a unified 'dev' CLI).
presets/example.nix: a worked, generic project list replacing the
identity-specific aiolabs preset. tests/smoke.nix + flake checks
exercise the schema; 'nix flake check' is green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
773632562e
commit
e38d313db2
17 changed files with 2925 additions and 147 deletions
|
|
@ -1,14 +1,11 @@
|
|||
# lnbits-sensei dev-env — wire-up.
|
||||
#
|
||||
# Skeleton-only. The substantive pass will:
|
||||
# - install the regtest.sh / fakewallet.sh wrappers on PATH
|
||||
# - render a /etc/dev-env/config.sh consumed by the loose bash
|
||||
# helpers (worktree nav, upstream-PR helper)
|
||||
# - emit systemd.user units for any long-running pieces
|
||||
# - hook into config.lnbits-sensei.git.remotes to drive the
|
||||
# bootstrap script's remote reconciliation
|
||||
#
|
||||
# Empty body for now so the module composes cleanly.
|
||||
# Nix owns the configuration; bash owns the runtime. This file renders
|
||||
# the machine-readable config (/etc/dev-env/config.sh + projects.json +
|
||||
# tmux-sessions.json), installs the helper scripts on PATH, loads the
|
||||
# shell-function modules into interactive shells, and wires the shared
|
||||
# git pre-commit hook. It never materializes repos on disk — that is the
|
||||
# job of the user-invoked `dev-env-bootstrap` script.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
|
|
@ -20,17 +17,186 @@ let
|
|||
inherit (lib) mkIf mkMerge;
|
||||
cfg = config.lnbits-sensei.devEnv;
|
||||
user = config.lnbits-sensei.user;
|
||||
helpers = cfg.lib;
|
||||
|
||||
# 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;
|
||||
|
||||
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)
|
||||
upstream
|
||||
fork
|
||||
category
|
||||
worktreeRoot
|
||||
isClone
|
||||
deployFlakeInput
|
||||
;
|
||||
barePath = bare;
|
||||
clonePath = clonePath;
|
||||
remotes = remotes;
|
||||
worktrees = resolvedWorktrees;
|
||||
};
|
||||
|
||||
resolvedProjects = lib.mapAttrs resolveProject cfg.projects;
|
||||
|
||||
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 the lnbits-sensei 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 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 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
|
||||
)}
|
||||
'';
|
||||
|
||||
# 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) loaded by
|
||||
# /etc/profile.d/dev-env-functions.sh into every interactive shell.
|
||||
shellFnSources = [
|
||||
./scripts/nav.sh
|
||||
./scripts/worktree.sh
|
||||
./scripts/pr-helpers.sh
|
||||
]
|
||||
++ lib.optional cfg.regtest.enable ./scripts/regtest.sh;
|
||||
|
||||
shellFnLoader = pkgs.writeText "dev-env-functions.sh" ''
|
||||
# Auto-generated by the lnbits-sensei 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 [
|
||||
# TODO(skeleton): wire scripts, systemd units, and the
|
||||
# /etc/dev-env/config.sh render here. See omnixy
|
||||
# modules/dev-env/config.nix for the reference shape.
|
||||
{
|
||||
# 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 pre-commit hook via core.hooksPath. Installs the
|
||||
# secret-scanner under ~/.local/share/lnbits-sensei/git-hooks/
|
||||
# and points the consumer's git config at that directory, so
|
||||
# every repo on the machine picks it up automatically.
|
||||
# 2) Loader so interactive shells (login OR non-login) get the
|
||||
# functions. NixOS only sources /etc/profile.d/*.sh from
|
||||
# /etc/profile (login shells); GUI-launched terminals are
|
||||
# interactive non-login shells. `interactiveShellInit` is
|
||||
# sourced by both /etc/bashrc and /etc/zshrc on every
|
||||
# interactive shell, which is what we want.
|
||||
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 the rebase-log state dir exists. Everything
|
||||
# else is created by dev-env-bootstrap on demand.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /home/${user}/.local/state/dev-env 0755 ${user} ${user} -"
|
||||
];
|
||||
}
|
||||
|
||||
# 5) 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 {
|
||||
home-manager.users.${user} =
|
||||
{ ... }:
|
||||
|
|
|
|||
Reference in a new issue