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,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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue