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:
Padreug 2026-06-15 21:18:49 +02:00
commit e38d313db2
17 changed files with 2925 additions and 147 deletions

View file

@ -1,17 +1,26 @@
# lnbits-sensei dev-env — option schema.
#
# Skeleton-only. Declares the option surface so consumers can wire
# values today and the substantive implementation can land later
# without churning the public API.
# Declares the full option surface for the dev environment: projects
# (bare repos + worktrees), the regtest stack, declarative tmux
# sessions, deploy targets, and the shared git hooks. Modules in this
# directory consume these via `config.lnbits-sensei.devEnv.*`.
#
# Identity model: this scaffold is git-host-agnostic. A project's
# `origin` is the explicit `url` you give it (any forgejo / gitea /
# codeberg / github URL) — there is no hardcoded host. The only
# github-specific knob is `github.forkUser`, used to derive a personal
# fork remote for the upstream-PR workflow.
{ 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.
# One entry in devEnv.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 `${devEnv.root}/repos/<basename>.git` by default; that path is
# materialized by the bootstrap script, never by `nixos-rebuild`.
projectType = types.submodule (
{ name, ... }:
{
@ -20,14 +29,59 @@ let
type = types.nullOr types.str;
default = null;
description = ''
Origin URL for this project. May be an upstream URL, a
personal fork, or a private mirror the dev-env bootstrap
script reconciles the rest of the remote topology from
`lnbits-sensei.git.remotes`.
Origin remote URL (ssh or https). This is the repo the
bootstrap script clones/fetches as `origin`. May be null for
a pure-upstream tracking project in that case `origin`
falls back to `upstream`.
'';
example = "git@git.example.com:you/lnbits.git";
};
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`/`rebase` helpers know how to sync from it.
'';
example = "https://github.com/lnbits/lnbits";
};
fork = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Full SSH URL of the personal fork used for upstream PRs
(the `fork` remote). If null and the project has an
`upstream`, it is derived from `devEnv.github.forkUser` +
the project basename.
'';
example = "git@github.com:you/lnbits.git";
};
category = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Sub-directory under `devEnv.root` that groups related
projects. e.g. "extensions" could group several LNbits
extension repos. null puts the project at the top level.
'';
example = "extensions";
};
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 {
@ -36,14 +90,31 @@ let
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, but
could be "upstream" for a read-only tracking worktree.
'';
};
};
}
);
default = { };
description = ''
Worktrees to materialize for this project, keyed by
worktree name. Each becomes a directory under the project
root.
Worktrees to materialize for this project, keyed by worktree
name. Each becomes a directory at
`''${devEnv.root}/''${category}/''${worktreeRoot}/<key>` (or
under root if category is null).
'';
example = lib.literalExpression ''
{
@ -54,22 +125,36 @@ let
};
isClone = mkEnableOption ''
Treat as a plain clone rather than a bare-repo + worktrees
set. Use for projects you won't have multiple simultaneous
branches checked out for
Treat this project as a regular clone rather than a bare-repo
worktree set. Use for projects you won't have multiple
simultaneous branches checked out for
'';
deployFlakeInput = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Name of the flake input in your deploy flake that this
project corresponds to. Used by `dev-deploy --local` to know
which `--override-input` to pass so a deploy builds against
your in-progress worktree.
'';
example = "lnbits";
};
};
}
);
# One entry in dev-env.tmux.sessions. Mirrors the omnixy shape so
# the `dev-tm <name>` launcher can ship later with a familiar API.
# One entry in devEnv.tmux.sessions.
tmuxSessionType = types.submodule {
options = {
cwd = mkOption {
type = types.nullOr types.str;
default = null;
description = "Session-default cwd, relative to dev-env.root.";
description = ''
Session-default cwd, relative to `devEnv.root`. Windows can
override with their own cwd.
'';
};
windows = mkOption {
type = types.listOf (
@ -79,6 +164,7 @@ let
cwd = mkOption {
type = types.nullOr types.str;
default = null;
description = "Window cwd, relative to devEnv.root.";
};
cmd = mkOption {
type = types.nullOr types.str;
@ -101,9 +187,9 @@ in
type = types.str;
description = ''
Absolute path to your lnbits-sensei checkout on this machine.
Used to source the seedable CLAUDE.md files (and, later, the
dev-env scripts) via `mkOutOfStoreSymlink` so edits in your
checkout take effect without a rebuild.
Used to source the seedable CLAUDE.md files via
`mkOutOfStoreSymlink` so edits in your checkout take effect
without a rebuild.
Required when any `claude.*` integration is enabled. Type is
`str` (not `path`) intentionally `path` would copy the file
@ -117,24 +203,116 @@ in
default = "/home/${config.lnbits-sensei.user or "user"}/dev";
defaultText = "/home/\${config.lnbits-sensei.user}/dev";
description = ''
Root directory for the dev environment. Worktrees and project
clones live under this prefix; bare repos under
`''${root}/repos/`.
Root directory for the dev environment. Bare repos live under
`''${root}/repos/`, worktrees under
`''${root}/<category>/<project>/`.
'';
};
gitHooks = {
enable = mkEnableOption ''
Shared pre-commit hook via `core.hooksPath`. Installs a single
secret-scanner hook under
`~/.local/share/lnbits-sensei/git-hooks/pre-commit` and points
the consumer's git config there so every repo on the machine
picks it up without per-repo wiring. Refuses to commit obvious
secrets and unencrypted sops files; false positives are handled
via `# pragma: allowlist secret` markers
github = {
forkUser = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Personal github username used for upstream PR forks (the
`fork` remote). When set, projects that declare an `upstream`
but no explicit `fork` automatically get a fork remote derived
from `git@github.com:<forkUser>/<basename>.git`.
'';
example = "octocat";
};
};
projects = mkOption {
type = types.attrsOf projectType;
default = { };
description = ''
Hand-authored project list. Consumed by `dev-env-bootstrap` to
materialize bare repos + worktrees on disk. Use the `devEnv.lib.
mkProject` helper to cut boilerplate, or write raw submodule
attrs. See `presets/example.nix` for a worked example.
'';
};
deploy = {
flakeInput = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Sub-directory under `''${root}/deploy/` holding your unified
deploy flake working copy. `dev-deploy` uses it as the
`--flake` source. null means `''${root}/deploy` itself.
'';
example = "unified";
};
targets = mkOption {
type = types.attrsOf types.str;
default = { };
description = ''
Map from hostname to SSH target used by `dev-deploy`. Each
entry is rendered to a `DEPLOY_TARGET_<HOST>` env var in
/etc/dev-env/config.sh.
'';
example = lib.literalExpression ''
{
prod = "root@prod-host";
staging = "root@staging-host";
}
'';
};
};
regtest = {
enable = mkEnableOption ''
Bitcoin/Lightning regtest docker stack. Wraps an upstream fork
of `lnbits/legend-regtest-enviroment` (LND + CLN + Eclair +
bitcoind + electrs). Installs `regtest-*` helpers. Requires a
container engine (docker) install it separately; this flag
does not pull docker in for you
'';
repoUrl = mkOption {
type = types.str;
default = "https://github.com/lnbits/legend-regtest-enviroment";
description = ''
Git URL of the regtest docker-compose repo. Cloned to
`''${root}/local/docker/regtest` by the bootstrap script.
Point at your own fork if you've customised the stack.
'';
};
};
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>` launcher 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
secret-scanner hook under
`~/.local/share/lnbits-sensei/git-hooks/pre-commit` and points
the consumer's git config there so every repo on the machine
picks it up without per-repo wiring. Refuses to commit obvious
secrets and unencrypted sops files; false positives are
handled via `# pragma: allowlist secret` markers
''
// {
default = true;
};
};
claude = {
enable = mkEnableOption ''
Seed `~/dev/lnbits/CLAUDE.md` from
@ -152,46 +330,14 @@ in
'';
};
projects = mkOption {
type = types.attrsOf projectType;
default = { };
writeDirenvHints = mkOption {
type = types.bool;
default = true;
description = ''
Hand-authored project list. Consumed by the bootstrap script
(later pass) to materialize repos and worktrees on disk.
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.
'';
};
regtest = {
enable = mkEnableOption ''
Bitcoin/Lightning regtest docker stack. Wraps an upstream
fork of `lnbits/legend-regtest-enviroment` (LND + CLN +
Eclair + bitcoind + electrs). Brought up via
`dev up --regtest`. Implies a container engine the
substantive pass will gate this on a containers feature
'';
repoUrl = mkOption {
type = types.str;
default = "https://github.com/lnbits/legend-regtest-enviroment";
description = ''
Git URL of the regtest docker-compose repo. Cloned by the
dev-env bootstrap script. Point at your own fork if you've
customised the stack.
'';
};
};
tmux = {
enable = mkEnableOption "declarative tmux session launcher";
sessions = mkOption {
type = types.attrsOf tmuxSessionType;
default = { };
description = ''
Named tmux session layouts. The launcher script (later pass)
reads these at runtime and recreates the session.
'';
};
};
};
}