diff --git a/README.md b/README.md index b1eaed9..76cc987 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,29 @@ # lnbits-sensei -> **Status:** working dev-env. Module schema, flake wiring, and the -> dev-env tooling (bootstrap, worktree/nav helpers, regtest wrappers, -> upstream-PR flow, deploy wrapper, declarative tmux) are all real and -> `nix flake check`-green. Fill in `settings.nix` + a project list and -> it's usable end-to-end. +> **Status:** eval-green skeleton. Module schema + flake wiring are real; +> the `dev` CLI and dev-env bootstrap are stubs. Not yet usable end-to-end. Opinionated, bare-skeleton scaffold for running an LNbits stack — inspired by the [omnixy](https://github.com/TheArctesian/omnixy) NixOS module pattern but stripped of any personal identity so the template is consumable as-is. -The goal: clone, fill in your identity + remote topology + a project -list, rebuild, and get a working multi-project LNbits dev environment — -bare repos + worktrees materialized in one command, with navigation, -sync, regtest, and upstream-PR helpers on PATH: +The goal: clone, fill in your identity + remote topology, and get a +working LNbits dev environment driven by one command: ``` -dev-env-bootstrap # materialize bare repos + worktrees on disk -lb dev # cd to the lnbits dev worktree -dev-status # dirty + ahead/behind across every worktree -regtest-start dev # build lnbits from a worktree, bring up regtest -prb lnbits fix-x # spin a throwaway worktree for an upstream PR +dev up # default: FakeWallet — no docker, instant +dev up --regtest # full multi-node regtest stack (LND + CLN + Eclair + # + bitcoind + electrs); for channel/payment + # integration testing +dev down # tear down whatever's running +dev logs # follow lnbits (+ docker, if regtest) +dev shell # drop into the lnbits venv (or a regtest container) ``` -See [`modules/dev-env/README.md`](modules/dev-env/README.md) for the -full command set and bootstrap walkthrough. - -FakeWallet (`LNBITS_BACKEND_WALLET_CLASS=FakeWallet`) is the default -iteration path for extension/frontend work — no docker needed. The -`regtest-*` helpers (gated on `devEnv.regtest.enable`) exist for the -moments you need real channels/payments. +`--fakewallet` is the default mode because the iteration loop for +extension and frontend work doesn't need real chains. `--regtest` +exists for the moments when it does. ## Remote topology @@ -61,36 +54,27 @@ hardware-configuration.nix # placeholder; overwrite with nixos-generate-con modules/ ├── core.nix # lnbits-sensei.* option schema -├── lib.nix # shared helpers — config.lnbits-sensei.lib -├── secrets.nix # sops-nix wiring (inert until you add a secret) +├── lib.nix # shared helpers — config.lnbits-sensei.lib (stub) ├── git/ │ └── remotes.nix # remote topology (upstream / fork / extras) └── dev-env/ - ├── README.md # command set + bootstrap walkthrough - ├── default.nix # module entry; imports options + lib + config - ├── options.nix # devEnv schema (projects, deploy, regtest, tmux) - ├── lib.nix # mkProject + path/remote helpers - ├── config.nix # renders /etc/dev-env/*, installs scripts - ├── presets/example.nix # worked, generic project list — copy and edit - ├── tests/smoke.nix # nix flake check schema test + ├── default.nix # module entry; imports options + config + lib + ├── options.nix # devEnv schema (projects, regtest, tmux) + ├── config.nix # wires the schema into NixOS config (stub) + ├── lib.nix # dev-env-internal helpers (stub) └── scripts/ - ├── bootstrap.sh # materialize bare repos + worktrees - ├── nav.sh # lb / g / ext / dep / prs / shared / repos - ├── worktree.sh # wt / wts / wtu / wtn + lnbits sync helpers - ├── pr-helpers.sh # prb / prc / prl — upstream-PR worktrees - ├── rebase.sh # safe fork-onto-upstream rebase - ├── status.sh # dev-status — divergence across worktrees - ├── deploy.sh # dev-deploy — nixos-rebuild wrapper - ├── regtest.sh # regtest-start/-stop/-status (gated) - ├── tmux-launch.sh # dev-tm — declarative tmux sessions - └── git-hooks/pre-commit # shared secret-scanner hook + └── dev.sh # `dev` CLI dispatcher (stub) -docs/ # human-facing references (also seeded to ~/dev/) +docs/ └── remotes.md # the three remote-topology patterns LICENSE # MIT (placeholders — fill in year + holder) ``` +`(stub)` files contain option schemas and TODO bodies; they evaluate +cleanly but don't do real work yet. Substantive implementations land +in follow-up passes. + ## Development flow ### First-time bootstrap (per machine) @@ -125,29 +109,12 @@ LICENSE # MIT (placeholders — fill in year + holder) sudo nixos-rebuild switch --flake .# ``` -### Day-to-day +### Day-to-day (planned — `dev.sh` is a stub today) -After a rebuild, the dev-env helpers are on PATH (standalone commands) -or sourced into interactive shells (navigation/worktree functions). -Run `dev-env-bootstrap` once to materialize the repos + worktrees you -declared, then: - -``` -dev-env-bootstrap # bring the ~/dev/ tree up to spec (idempotent) -lb dev / g # navigate worktrees -dev-status # divergence + dirty state across everything -wts / wtu # sync worktrees / fetch upstream + show divergence -rebase status # which forks need rebasing onto upstream -regtest-start dev # build lnbits from a worktree, bring up regtest -prb lnbits fix-x # throwaway worktree for an upstream PR -dev-tm # launch a declared tmux session -dev-deploy # nixos-rebuild against your deploy flake -``` - -The full command table + bootstrap walkthrough lives in -[`modules/dev-env/README.md`](modules/dev-env/README.md). See -**[Claude orientation](#claude-orientation)** below if you want -AI-assisted work to pick up the project's gotchas for free. +The `dev` CLI is the single entry point. See the top of this README +for the verb set. See **[Claude orientation](#claude-orientation)** +below if you want AI-assisted work to pick up the project's gotchas +for free. ### Workspace layout (the `~/dev/` tree) @@ -194,14 +161,9 @@ devEnv.projects = { }; ``` -`dev-env-bootstrap` materializes the bare repo at -`~/dev/repos/lnbits.git`, wires its remotes, and checks out one -worktree per declared entry. It's idempotent — re-running only fills in -what's missing, and never touches a worktree whose branch has drifted -from the spec. See [`docs/remotes.md`](docs/remotes.md) and -[`modules/dev-env/presets/example.nix`](modules/dev-env/presets/example.nix) -for richer project declarations (explicit `url`, `upstream`, `fork`, -`category`, single-clone projects). +The forthcoming bootstrap (`dev bootstrap`, name TBD) materializes the +bare repo at `~/dev/repos/lnbits.git` and checks out one worktree per +declared entry. **Why bare repos + worktrees** (rather than fresh clones per branch): @@ -217,23 +179,19 @@ PRs against upstream repos (e.g. `lnbits/lnbits`) want to branch from the per-project tree pollutes the branch list and tempts "I'll just commit this here" accidents. The `upstream-prs/` tree makes the contract explicit: this worktree is on `upstream/main`, branch off, -push to your fork, open the PR. The `prb` helper does this: +push to your fork, open the PR. Helper (planned): ``` prb lnbits fix-payment-race # create ~/dev/upstream-prs/lnbits-fix-payment-race # branched from upstream/main, fork as push target -prc lnbits fix-payment-race # remove the worktree after the PR merges -prl # list active PR worktrees ``` -**Navigation helpers** — shell functions sourced into interactive -shells, driven by what's on disk under `~/dev/`: +**Navigation helpers** (planned) — short aliases generated from +`devEnv.projects`: ``` lb dev # cd ~/dev/lnbits/dev lb fix-issue-123 # cd ~/dev/lnbits/fix-issue-123 -g extensions myext # cd ~/dev/extensions/myext -ext # cd ~/dev/shared/extensions/ ``` ## Claude orientation diff --git a/flake.nix b/flake.nix index e08296e..5d3144f 100644 --- a/flake.nix +++ b/flake.nix @@ -49,23 +49,8 @@ # `settings` (single source of truth — user, host, identity, # remote topology) is threaded into every NixOS module and into # the home-manager module set, so neither has to re-import it. - - # dev-env schema smoke test — a minimal nixosSystem that imports - # only core.nix + the dev-env module and exercises the option - # surface. `nix flake check` builds the rendered config artifacts, - # which forces full module evaluation (catching schema breakage) - # without building a whole system. - smoke = import ./modules/dev-env/tests/smoke.nix { - inherit nixpkgs home-manager; - }; in { - checks.${system} = { - dev-env-projects-json = smoke.config.environment.etc."dev-env/projects.json".source; - dev-env-config-sh = smoke.config.environment.etc."dev-env/config.sh".source; - dev-env-tmux-sessions = smoke.config.environment.etc."dev-env/tmux-sessions.json".source; - }; - nixosConfigurations.${settings.hostName} = nixpkgs.lib.nixosSystem { inherit system; diff --git a/modules/dev-env/README.md b/modules/dev-env/README.md deleted file mode 100644 index 8ac0d09..0000000 --- a/modules/dev-env/README.md +++ /dev/null @@ -1,112 +0,0 @@ -# modules/dev-env - -Declarative NixOS module for managing a multi-project dev environment -around an LNbits stack — bare repos, worktrees, navigation helpers, tmux -sessions, the regtest docker env, and the upstream-PR workflow. - -## Design principles - -1. **Nix owns the configuration, bash owns the runtime.** Nix renders - `/etc/dev-env/config.sh` + `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. **Projects are explicit, git-host-agnostic.** Each project declares - its `origin` `url` directly (any forgejo / gitea / codeberg / github - URL). The only github-specific knob is `github.forkUser`, used to - derive a personal `fork` remote for upstream PRs. - -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 + path/remote helpers. | -| `config.nix` | Renders config files, installs scripts, wires git hooks. | -| `presets/example.nix` | A worked, generic project list — copy and edit. | -| `scripts/*.sh` | Bash helpers loaded via `builtins.readFile`. | -| `scripts/git-hooks/pre-commit` | Shared secret-scanner hook (via `core.hooksPath`). | -| `tests/smoke.nix` | `nix flake check` evaluation test for the schema. | - -## Using it - -```nix -# configuration.nix -imports = [ - ./modules/dev-env - ./modules/dev-env/presets/example.nix # opt-in; copy and edit -]; - -lnbits-sensei.devEnv = { - enable = true; - scaffoldPath = "/home/you/dev/lnbits-sensei"; - github.forkUser = "octocat"; - deploy.targets = { - prod = "root@prod-host"; - staging = "root@staging-host"; - }; -}; -``` - -## Bootstrap workflow - -```bash -# 1. Rebuild with dev-env enabled -sudo nixos-rebuild switch --flake .# - -# 2. Dry-run to see what will be created -dev-env-bootstrap --dry-run - -# 3. Materialize bare repos + worktrees -dev-env-bootstrap - -# 4. Navigate (shell functions, sourced into interactive shells) -lb dev # → ~/dev/lnbits/dev -g extensions myext # → ~/dev/extensions/myext -ext # → ~/dev/shared/extensions/ -prb lnbits fix-x # → ~/dev/upstream-prs/lnbits-fix-x on upstream/main - -# 5. Inspect / sync -dev-status # dirty + ahead/behind for every worktree -wts # fetch all bare repos, summarize worktree status -rebase status # which forks need rebasing onto upstream -lnbits-status # lnbits dev/main divergence vs upstream - -# 6. Regtest (when devEnv.regtest.enable = true; needs docker) -regtest-start dev # build lnbits from ~/dev/lnbits/dev, bring stack up -regtest-stop - -# 7. Deploy -dev-deploy prod # uses the locked deploy-flake input -dev-deploy --local staging # overrides inputs with local worktrees -``` - -## Command summary - -| Command | What | -|---|---| -| `dev-env-bootstrap` | Materialize bare repos + worktrees from `projects.json`. | -| `dev-status` | Dirty/ahead/behind report across all worktrees. | -| `dev-tm ` | Launch a declarative tmux session. | -| `dev-deploy ` | `nixos-rebuild` against your deploy flake. | -| `rebase [status\|all\|]` | Safe fork-onto-upstream rebase with backups. | -| `regtest-start` / `-stop` / `-status` | Bitcoin/Lightning regtest stack. | -| `lb` / `g` / `ext` / `dep` / `prs` / `shared` / `repos` | Navigation (shell functions). | -| `wt` / `wts` / `wtu` / `wtn` | Worktree list / sync / upstream-fetch / spawn. | -| `prb` / `prc` / `prl` | Upstream-PR worktree branch / cleanup / list. | -| `lnbits-status` / `lnbits-sync-dev` / `lnbits-sync-main` | lnbits fork workflow. | - -## What this module does NOT do - -- Run `git fetch`/`git pull` automatically — use `wts`/`wtu` or a manual - `git fetch --all`. -- Manage per-worktree `.envrc` beyond writing a default `use flake` hint - on bootstrap (never clobbers an existing file). -- Install docker for you — `devEnv.regtest.enable` installs the - `regtest-*` helpers, but you must provide the container engine. diff --git a/modules/dev-env/config.nix b/modules/dev-env/config.nix index 8a0e21c..50b05b8 100644 --- a/modules/dev-env/config.nix +++ b/modules/dev-env/config.nix @@ -1,11 +1,14 @@ # lnbits-sensei dev-env — wire-up. # -# 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. +# 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. { config, lib, @@ -17,186 +20,17 @@ 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_ - # 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 [ - { - # 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; - }; + # 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. - # 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. + # 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. (mkIf cfg.gitHooks.enable { home-manager.users.${user} = { ... }: diff --git a/modules/dev-env/lib.nix b/modules/dev-env/lib.nix index 1693dfc..565278d 100644 --- a/modules/dev-env/lib.nix +++ b/modules/dev-env/lib.nix @@ -1,111 +1,22 @@ # lnbits-sensei dev-env — helpers. # -# dev-env-internal helpers scoped to this module (project path -# resolution, worktree-path expansion, remote construction) rather than -# the global `lnbits-sensei.lib` namespace. Exposed as -# `config.lnbits-sensei.devEnv.lib` so config.nix and the example preset -# can build project entries with less boilerplate. +# Skeleton-only. Place dev-env-internal helpers here (project path +# resolution, worktree-path expansion, remote-URL canonicalisation) +# rather than in the global `lnbits-sensei.lib` so they're scoped to +# the dev-env module and don't pollute the public helper namespace. { config, lib, ... }: let inherit (lib) mkOption types; - cfg = config.lnbits-sensei.devEnv; - - # mkProject: shorthand constructor for `devEnv.projects.`. - # - # Fills in conventional defaults so consumers write less boilerplate. - # The returned attrset matches the `projectType` submodule schema. - # - # mkProject { - # name = "lnbits"; - # url = "git@git.example.com:you/lnbits.git"; - # upstream = "https://github.com/lnbits/lnbits"; - # worktrees = { main.branch = "main"; dev.branch = "dev"; }; - # } - mkProject = - { - name ? null, - category ? null, - url ? null, # origin remote URL - upstream ? null, - fork ? null, # defaults from github.forkUser when upstream is set - worktrees ? { }, - isClone ? false, - deployFlakeInput ? null, - worktreeRoot ? null, # defaults to name - }: - { - inherit - url - upstream - category - isClone - deployFlakeInput - ; - fork = - if fork != null then - fork - 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; - }; - - # Resolve worktree filesystem paths. Projects with a category live - # under ${root}/${category}/${worktreeRoot}/; otherwise - # ${root}/${worktreeRoot}/. - 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/.git). The basename - # is derived from the project name; the bare object DB is shared by - # every worktree of the project. - bareRepoPath = projectName: _project: "${cfg.root}/repos/${projectName}.git"; - - # Construct the remotes for a project. `origin` is `url` if set, - # otherwise falls back to `upstream` (pure-upstream tracking repo). - # Projects with neither end up with no origin (filtered out). - projectRemotes = - project: - lib.filterAttrs (_: v: v != null) { - origin = - if project.url != null then - project.url - else - project.upstream; - upstream = project.upstream; - fork = project.fork; - }; helpers = { - inherit - mkProject - worktreePath - bareRepoPath - projectRemotes - ; + # Resolve a project's on-disk root given a project name. + # projectRoot = name: "${config.lnbits-sensei.devEnv.root}/${name}"; + projectRoot = _name: throw "dev-env.lib.projectRoot: not yet implemented"; + + # Resolve a worktree path: //. + worktreePath = + _project: _worktree: throw "dev-env.lib.worktreePath: not yet implemented"; }; in { diff --git a/modules/dev-env/options.nix b/modules/dev-env/options.nix index 7aecbf9..e8aa95b 100644 --- a/modules/dev-env/options.nix +++ b/modules/dev-env/options.nix @@ -1,26 +1,17 @@ # lnbits-sensei dev-env — option schema. # -# 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. +# Skeleton-only. Declares the option surface so consumers can wire +# values today and the substantive implementation can land later +# without churning the public API. { config, lib, ... }: let inherit (lib) mkEnableOption mkOption types; - # 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/.git` by default; that path is - # materialized by the bootstrap script, never by `nixos-rebuild`. + # 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/.git` + # by default. projectType = types.submodule ( { name, ... }: { @@ -29,59 +20,14 @@ let type = types.nullOr types.str; default = null; description = '' - 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. + 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`. ''; 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 { @@ -90,31 +36,14 @@ 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 at - `''${devEnv.root}/''${category}/''${worktreeRoot}/` (or - under root if category is null). + Worktrees to materialize for this project, keyed by + worktree name. Each becomes a directory under the project + root. ''; example = lib.literalExpression '' { @@ -125,36 +54,22 @@ let }; isClone = mkEnableOption '' - 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 + 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 ''; - - 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 devEnv.tmux.sessions. + # One entry in dev-env.tmux.sessions. Mirrors the omnixy shape so + # the `dev-tm ` launcher can ship later with a familiar API. tmuxSessionType = types.submodule { options = { cwd = mkOption { type = types.nullOr types.str; default = null; - description = '' - Session-default cwd, relative to `devEnv.root`. Windows can - override with their own cwd. - ''; + description = "Session-default cwd, relative to dev-env.root."; }; windows = mkOption { type = types.listOf ( @@ -164,7 +79,6 @@ let cwd = mkOption { type = types.nullOr types.str; default = null; - description = "Window cwd, relative to devEnv.root."; }; cmd = mkOption { type = types.nullOr types.str; @@ -187,9 +101,9 @@ in type = types.str; description = '' Absolute path to your lnbits-sensei checkout on this machine. - Used to source the seedable CLAUDE.md files via - `mkOutOfStoreSymlink` so edits in your checkout take effect - without a rebuild. + 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. Required when any `claude.*` integration is enabled. Type is `str` (not `path`) intentionally — `path` would copy the file @@ -203,114 +117,22 @@ in default = "/home/${config.lnbits-sensei.user or "user"}/dev"; defaultText = "/home/\${config.lnbits-sensei.user}/dev"; description = '' - Root directory for the dev environment. Bare repos live under - `''${root}/repos/`, worktrees under - `''${root}///`. + Root directory for the dev environment. Worktrees and project + clones live under this prefix; bare repos under + `''${root}/repos/`. ''; }; - 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:/.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_` 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 ` 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; - }; + 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 + ''; }; claude = { @@ -330,14 +152,46 @@ in ''; }; - writeDirenvHints = mkOption { - type = types.bool; - default = true; + projects = mkOption { + type = types.attrsOf projectType; + default = { }; 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. + Hand-authored project list. Consumed by the bootstrap script + (later pass) to materialize repos and worktrees on disk. ''; }; + + 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. + ''; + }; + }; }; } diff --git a/modules/dev-env/presets/example.nix b/modules/dev-env/presets/example.nix deleted file mode 100644 index 7c519f5..0000000 --- a/modules/dev-env/presets/example.nix +++ /dev/null @@ -1,104 +0,0 @@ -# Example dev-env preset — a worked, generic project list. -# -# Importing this file is opt-in and illustrative. Copy it, rename it, -# and edit the URLs / worktrees / targets for your own stack. The -# dev-env module ships **no** projects by default — you declare them -# either here or inline in your host config. -# -# Wire it up in configuration.nix: -# -# imports = [ -# ./modules/dev-env -# ./modules/dev-env/presets/example.nix -# ]; -# -# Then `nixos-rebuild switch` and `dev-env-bootstrap` to materialize the -# bare repos + worktrees on disk. -{ config, lib, ... }: - -let - # `mk` cuts boilerplate: derives the `fork` remote from - # github.forkUser, defaults worktreeRoot to the project name, etc. - mk = config.lnbits-sensei.devEnv.lib.mkProject; -in -{ - lnbits-sensei.devEnv = { - # Personal github username — used to derive `fork` remotes for any - # project that declares an upstream. Leave null if you never send - # upstream PRs. - github.forkUser = lib.mkDefault "octocat"; - - # Hostname → SSH target map for `dev-deploy`. Empty by default. - deploy.targets = lib.mkDefault { - # prod = "root@prod-host"; - # staging = "root@staging-host"; - }; - - projects = { - - # ─── lnbits: your fork, dev + main worktrees tracking upstream ── - # - # `url` is your origin (your fork on whatever git host you use). - # `upstream` adds the lnbits/lnbits remote so the rebase + sync - # helpers (lnbits-status, lnbits-sync-dev, rebase) work. - lnbits = mk { - name = "lnbits"; - url = "git@github.com:octocat/lnbits.git"; - upstream = "https://github.com/lnbits/lnbits"; - deployFlakeInput = "lnbits"; - worktrees = { - dev.branch = "dev"; - main.branch = "main"; - }; - }; - - # ─── an LNbits extension, grouped under an `extensions` category ─ - # - # Single clone (one branch checked out at a time). Lands at - # ${root}/extensions/myext. - myext = mk { - name = "myext"; - category = "extensions"; - url = "git@github.com:octocat/myext.git"; - upstream = "https://github.com/lnbits/myext"; - isClone = true; - }; - - # ─── a pure-upstream reference checkout (no fork, read-only) ───── - # - # No `url` → origin falls back to `upstream`. Useful for grepping - # a reference codebase alongside your own work. - nostr-tools = mk { - name = "nostr-tools"; - category = "refs"; - upstream = "https://github.com/nbd-wtf/nostr-tools"; - worktrees.master.branch = "master"; - }; - }; - - # ─── a declarative tmux session for the lnbits dev loop ─────────── - tmux = { - enable = lib.mkDefault true; - sessions.lnbits = { - cwd = "lnbits/dev"; - windows = [ - { - name = "edit"; - cwd = "lnbits/dev"; - cmd = "nvim ."; - } - { - name = "run"; - cwd = "lnbits/dev"; - cmd = null; - } - { - name = "git"; - cwd = "lnbits/dev"; - cmd = "lazygit"; - } - ]; - }; - }; - }; -} diff --git a/modules/dev-env/scripts/bootstrap.sh b/modules/dev-env/scripts/bootstrap.sh deleted file mode 100644 index 97ba832..0000000 --- a/modules/dev-env/scripts/bootstrap.sh +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env bash -# dev-env-bootstrap: idempotent materialization of bare repos and worktrees -# -# Reads /etc/dev-env/projects.json (rendered by config.nix) and ensures -# every declared project has: -# -# 1. A bare repo at ${REPOS_DIR}/.git with the declared remotes -# (origin, optional upstream, optional fork). -# 2. A worktree at the declared path for each entry in `worktrees`. -# 3. (Optional) A `.envrc` containing `use flake` if the worktree has -# a flake.nix and DEVENV_WRITE_DIRENV_HINTS=1. -# -# Safety: -# - Never clobbers an existing worktree whose current branch differs -# from the declared one — prints a warning and skips. -# - Never silently rewrites a remote URL — prints the diff and asks. -# - --dry-run shows everything that would happen without touching anything. -# - Re-running is a no-op when the tree already matches the spec. - -set -euo pipefail - -# --- Config ----------------------------------------------------------- - -if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh -fi - -DEV_ROOT="${DEV_ROOT:-$HOME/dev}" -REPOS_DIR="${REPOS_DIR:-$DEV_ROOT/repos}" -PROJECTS_JSON="${DEVENV_PROJECTS_JSON:-/etc/dev-env/projects.json}" -WRITE_DIRENV="${DEVENV_WRITE_DIRENV_HINTS:-1}" - -# --- Args ------------------------------------------------------------- - -DRY_RUN=false -VERBOSE=false -FORCE_REMOTES=false -ONLY_PROJECT="" - -usage() { - cat <&2; } -trace() { if $VERBOSE; then echo -e "${DIM}\$ $*${NC}"; fi; } - -run() { - trace "$*" - if $DRY_RUN; then - echo " (dry-run) $*" - else - "$@" - fi -} - -# --- Preflight -------------------------------------------------------- - -if [[ ! -r "$PROJECTS_JSON" ]]; then - err "projects.json not found at $PROJECTS_JSON" - err "is the dev-env module enabled in your nixos config?" - exit 1 -fi - -if ! command -v jq >/dev/null; then - err "jq is required (should be in environment.systemPackages)" - exit 1 -fi - -if ! command -v git >/dev/null; then - err "git is required" - exit 1 -fi - -run mkdir -p "$REPOS_DIR" "$DEV_ROOT" - -# --- Per-project work ------------------------------------------------- - -# Returns "remote-nameurl" for the given project, one line per remote. -project_remotes_jq() { - local proj="$1" - jq -r --arg p "$proj" ' - .[$p].remotes - | to_entries[] - | "\(.key)\t\(.value)" - ' "$PROJECTS_JSON" -} - -# Returns "wtnamebranchpathremote" for each declared worktree. -project_worktrees_jq() { - local proj="$1" - jq -r --arg p "$proj" ' - .[$p].worktrees - | to_entries[] - | "\(.key)\t\(.value.branch)\t\(.value.path)\t\(.value.remote)" - ' "$PROJECTS_JSON" -} - -ensure_bare_repo() { - local proj="$1" - local bare_path - bare_path="$(jq -r --arg p "$proj" '.[$p].barePath' "$PROJECTS_JSON")" - - if [[ ! -d "$bare_path" ]]; then - info "creating bare repo $bare_path" - run git init --bare "$bare_path" - fi - - while IFS=$'\t' read -r remote_name url; do - [[ -z "$remote_name" ]] && continue - local current="" - current="$(git -C "$bare_path" remote get-url "$remote_name" 2>/dev/null || echo "")" - if [[ -z "$current" ]]; then - info " + remote $remote_name → $url" - run git -C "$bare_path" remote add "$remote_name" "$url" - elif [[ "$current" != "$url" ]]; then - warn " ! remote $remote_name URL differs:" - warn " have: $current" - warn " want: $url" - if $FORCE_REMOTES; then - info " (--force-remotes) updating" - run git -C "$bare_path" remote set-url "$remote_name" "$url" - else - warn " (skipped — pass --force-remotes to overwrite)" - fi - fi - done < <(project_remotes_jq "$proj") - - # Initial fetch (one-shot, only if origin has never been fetched) - if [[ ! -d "$bare_path/refs/remotes/origin" ]]; then - info " fetching origin (first time)" - run git -C "$bare_path" fetch origin 2>&1 | sed 's/^/ /' || \ - warn " fetch failed — check ssh access to origin" - fi - if git -C "$bare_path" remote get-url upstream &>/dev/null \ - && [[ ! -d "$bare_path/refs/remotes/upstream" ]]; then - info " fetching upstream (first time)" - run git -C "$bare_path" fetch upstream 2>&1 | sed 's/^/ /' || \ - warn " upstream fetch failed" - fi -} - -ensure_worktrees() { - local proj="$1" - local bare_path is_clone - bare_path="$(jq -r --arg p "$proj" '.[$p].barePath' "$PROJECTS_JSON")" - is_clone="$(jq -r --arg p "$proj" '.[$p].isClone' "$PROJECTS_JSON")" - - # Single-clone projects: clone to category/projectname instead of using worktrees - if [[ "$is_clone" == "true" ]]; then - local clone_path - clone_path="$(jq -r --arg p "$proj" '.[$p].clonePath' "$PROJECTS_JSON")" - if [[ -d "$clone_path/.git" ]]; then - ok " clone exists: $clone_path" - return 0 - fi - local origin_url - origin_url="$(jq -r --arg p "$proj" '.[$p].remotes.origin' "$PROJECTS_JSON")" - info " cloning $origin_url → $clone_path" - run mkdir -p "$(dirname "$clone_path")" - run git clone "$origin_url" "$clone_path" || warn " clone failed" - return 0 - fi - - while IFS=$'\t' read -r wt_name branch wt_path remote; do - [[ -z "$wt_name" ]] && continue - [[ "$wt_path" == "null" ]] && wt_path="" - - if [[ -z "$wt_path" ]]; then - warn " worktree $wt_name has no path; skipping" - continue - fi - - if [[ -d "$wt_path/.git" ]] || [[ -f "$wt_path/.git" ]]; then - local current_branch - current_branch="$(git -C "$wt_path" branch --show-current 2>/dev/null || echo '?')" - if [[ "$current_branch" == "$branch" ]]; then - ok " worktree $wt_name @ $branch (exists)" - else - warn " worktree $wt_name @ $current_branch (declared: $branch) — leaving alone" - fi - maybe_write_envrc "$wt_path" - continue - fi - - if [[ -e "$wt_path" ]]; then - warn " $wt_path exists but is not a git worktree; skipping" - continue - fi - - # Need to create the worktree. First make sure the local branch exists. - if ! git -C "$bare_path" show-ref --verify --quiet "refs/heads/$branch"; then - if git -C "$bare_path" show-ref --verify --quiet "refs/remotes/$remote/$branch"; then - info " creating local branch $branch from $remote/$branch" - run git -C "$bare_path" branch "$branch" "$remote/$branch" - else - warn " branch $branch not found on $remote — fetch and retry" - continue - fi - fi - - info " + worktree $wt_name → $wt_path ($branch)" - run mkdir -p "$(dirname "$wt_path")" - run git -C "$bare_path" worktree add "$wt_path" "$branch" - maybe_write_envrc "$wt_path" - done < <(project_worktrees_jq "$proj") -} - -maybe_write_envrc() { - local wt="$1" - [[ "$WRITE_DIRENV" != "1" ]] && return 0 - [[ -f "$wt/flake.nix" ]] || return 0 - [[ -e "$wt/.envrc" ]] && return 0 - info " + .envrc (use flake) → $wt/.envrc" - if ! $DRY_RUN; then - echo "use flake" > "$wt/.envrc" - fi -} - -# --- Main loop -------------------------------------------------------- - -mapfile -t PROJECTS < <(jq -r 'keys[]' "$PROJECTS_JSON") - -if [[ -n "$ONLY_PROJECT" ]]; then - if ! printf '%s\n' "${PROJECTS[@]}" | grep -qx "$ONLY_PROJECT"; then - err "project '$ONLY_PROJECT' not in $PROJECTS_JSON" - exit 1 - fi - PROJECTS=("$ONLY_PROJECT") -fi - -echo "" -echo "dev-env bootstrap" -echo " root: $DEV_ROOT" -echo " projects: ${#PROJECTS[@]}" -if $DRY_RUN; then echo " mode: dry-run"; fi -echo "" - -for proj in "${PROJECTS[@]}"; do - echo "─── $proj ───" - ensure_bare_repo "$proj" - ensure_worktrees "$proj" - echo "" -done - -ok "bootstrap complete" -if $DRY_RUN; then echo "(no changes were made)"; fi diff --git a/modules/dev-env/scripts/deploy.sh b/modules/dev-env/scripts/deploy.sh deleted file mode 100644 index c6eaa62..0000000 --- a/modules/dev-env/scripts/deploy.sh +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env bash -# dev-deploy: thin wrapper around your unified deploy flake -# -# Usage: -# dev-deploy switch on host (uses locked deploy flake input) -# dev-deploy test test build (no switch) -# dev-deploy build local build only -# dev-deploy --local same as switch but with --override-input -# wired up from local worktrees so the deploy -# builds against your in-progress changes -# dev-deploy all [test|build] every host in parallel -# dev-deploy update nix flake update -# -# Deploy host → SSH target mapping comes from /etc/dev-env/config.sh -# (devEnv.deploy.targets). The flake working copy lives at -# ${DEV_ROOT}/deploy[/]. - -set -euo pipefail - -if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh -fi - -DEPLOY_BASE="${DEPLOY_DIR:-$DEV_ROOT/deploy}" -if [[ -n "${DEVENV_DEPLOY_FLAKE_INPUT:-}" ]]; then - DEPLOY_DIR="$DEPLOY_BASE/$DEVENV_DEPLOY_FLAKE_INPUT" -else - DEPLOY_DIR="$DEPLOY_BASE" -fi -PROJECTS_JSON="${DEVENV_PROJECTS_JSON:-/etc/dev-env/projects.json}" - -usage() { - cat < deploy (switch) - dev-deploy test test build (no switch) - dev-deploy build local build only (no target) - dev-deploy --local deploy with --override-input from local worktrees - dev-deploy --local test test with overrides - dev-deploy all [test|build] every host in parallel - dev-deploy update [args...] nix flake update - -Configured targets: -EOF - env | grep '^DEPLOY_TARGET_' | sed 's/^DEPLOY_TARGET_/ /' | sed 's/=/ → /' || true - if [[ -r "$DEPLOY_DIR/flake.nix" ]]; then - echo "" - echo "Backing flake: $DEPLOY_DIR" - fi - exit 1 -} - -[[ $# -lt 1 ]] && usage - -# Pull the host → target map. We accept either: -# 1. DEPLOY_TARGET_= (rendered by config.nix as env vars) -# 2. ${DEPLOY_DIR}/deploy.sh's TARGETS associative array (fallback) -target_for() { - local host="$1" - local var="DEPLOY_TARGET_${host//-/_}" - if [[ -n "${!var:-}" ]]; then - echo "${!var}" - return - fi - # Fallback: parse a deploy.sh TARGETS array (simple regex; brittle - # but enough as a fallback) - if [[ -r "$DEPLOY_DIR/deploy.sh" ]]; then - awk -v h="$host" ' - /^[[:space:]]*\[/ { - if (match($0, /\[([^]]+)\]="([^"]*)"/, m)) { - if (m[1] == h) { print m[2]; exit } - } - } - ' "$DEPLOY_DIR/deploy.sh" - fi -} - -# --- subcommands --- - -cmd_update() { - shift - cd "$DEPLOY_DIR" - nix flake update "$@" -} - -# Build the --override-input flags for a host using projects.json's -# `deployFlakeInput` mapping. For each project that has deployFlakeInput -# set AND a worktree resolved to a real on-disk path, we emit -# --override-input path: -# Picks the first worktree by default; pass DEVENV_OVERRIDE_WORKTREE= -# to choose a specific one. -build_overrides() { - local args=() - local prefer="${DEVENV_OVERRIDE_WORKTREE:-}" - [[ -r "$PROJECTS_JSON" ]] || { printf '%s\n' ""; return; } - - while IFS=$'\t' read -r input wt_path; do - [[ -z "$input" || "$input" == "null" ]] && continue - [[ -d "$wt_path" ]] || continue - args+=(--override-input "$input" "path:$wt_path") - done < <(jq -r --arg prefer "$prefer" ' - to_entries[] - | select(.value.deployFlakeInput != null) - | .value as $v - | (if ($prefer | length) > 0 and ($v.worktrees | has($prefer)) - then $v.worktrees[$prefer].path - else - ($v.worktrees | to_entries | first.value.path // null) - end) as $path - | "\($v.deployFlakeInput)\t\($path // "")" - ' "$PROJECTS_JSON") - - printf '%s\0' "${args[@]}" -} - -run_host() { - local host="$1" action="$2" use_local="$3" - local target - target="$(target_for "$host")" - - if [[ -z "$target" ]] && [[ "$action" != "build" ]]; then - echo "no SSH target for host '$host' (set DEPLOY_TARGET_${host//-/_})" >&2 - return 1 - fi - - cd "$DEPLOY_DIR" - - local overrides=() - if $use_local; then - # Read the null-delimited overrides into an array - while IFS= read -r -d '' arg; do - [[ -n "$arg" ]] && overrides+=("$arg") - done < <(build_overrides) - if (( ${#overrides[@]} > 0 )); then - echo "[$host] using ${#overrides[@]} local override(s):" - local i=0 - while (( i < ${#overrides[@]} )); do - echo " ${overrides[$((i+1))]} -> ${overrides[$((i+2))]}" - i=$((i + 3)) - done - fi - fi - - case "$action" in - deploy) - echo "[$host] switch on $target..." - nixos-rebuild switch \ - --flake ".#$host" \ - --target-host "$target" \ - "${overrides[@]}" - ;; - test) - echo "[$host] test on $target..." - nixos-rebuild test \ - --flake ".#$host" \ - --target-host "$target" \ - "${overrides[@]}" - ;; - build) - echo "[$host] local build..." - nix build \ - ".#nixosConfigurations.$host.config.system.build.toplevel" \ - "${overrides[@]}" - ;; - *) - echo "unknown action: $action" >&2 - return 1 - ;; - esac -} - -USE_LOCAL=false -if [[ "$1" == "--local" ]]; then - USE_LOCAL=true - shift -fi - -case "$1" in - update) - cmd_update "$@" - ;; - all) - action="${2:-deploy}" - # iterate every DEPLOY_TARGET_* env var - pids=() - hosts=() - for var in $(env | grep -o '^DEPLOY_TARGET_[A-Za-z0-9_]*' || true); do - host="${var#DEPLOY_TARGET_}" - host="${host//_/-}" - run_host "$host" "$action" "$USE_LOCAL" & - pids+=($!) - hosts+=("$host") - done - failed=() - for i in "${!pids[@]}"; do - if ! wait "${pids[$i]}"; then - failed+=("${hosts[$i]}") - fi - done - if (( ${#failed[@]} > 0 )); then - echo "FAILED: ${failed[*]}" - exit 1 - fi - echo "All hosts deployed successfully." - ;; - -h|--help) - usage - ;; - *) - host="$1" - action="${2:-deploy}" - run_host "$host" "$action" "$USE_LOCAL" - ;; -esac diff --git a/modules/dev-env/scripts/dev.sh b/modules/dev-env/scripts/dev.sh new file mode 100755 index 0000000..2c98e17 --- /dev/null +++ b/modules/dev-env/scripts/dev.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# dev — single entry point for spinning up an LNbits dev environment. +# +# dev up [--fakewallet|--regtest] start lnbits (default: --fakewallet) +# dev down tear down whatever's running +# dev logs follow lnbits (and docker, if regtest) logs +# dev shell drop into the lnbits venv (or regtest container) +# +# Modes: +# --fakewallet (default) LNBITS_BACKEND_WALLET_CLASS=FakeWallet — no docker, +# no chains, instant. Good for extension / UI work. +# --regtest docker-compose up the multi-node regtest stack +# (LND + CLN + Eclair + bitcoind + electrs), then +# start lnbits pointed at the LND-rest endpoint. +# Wraps lnbits/legend-regtest-enviroment. +# +# Skeleton — no real wiring yet. The substantive pass will: +# - locate the lnbits checkout via inputs.lnbits-src (or a configurable path) +# - locate the regtest docker repo via config.lnbits-sensei.devEnv.regtest.repoUrl +# - bring up containers, wait-for-it, populate LND credentials, exec lnbits + +set -euo pipefail + +cmd="${1:-up}" +shift || true + +mode="fakewallet" +while [[ $# -gt 0 ]]; do + case "$1" in + --fakewallet) mode="fakewallet"; shift ;; + --regtest) mode="regtest"; shift ;; + *) echo "dev: unknown flag: $1" >&2; exit 1 ;; + esac +done + +case "$cmd" in + up) + echo "dev up --$mode: TODO — wire substantive startup in next pass." + ;; + down) + echo "dev down: TODO — wire teardown in next pass." + ;; + logs) + echo "dev logs: TODO — tail lnbits + (docker logs if regtest) here." + ;; + shell) + echo "dev shell: TODO — drop into venv / regtest container here." + ;; + *) + echo "Usage: dev {up|down|logs|shell} [--fakewallet|--regtest]" >&2 + exit 1 + ;; +esac diff --git a/modules/dev-env/scripts/nav.sh b/modules/dev-env/scripts/nav.sh deleted file mode 100644 index c910927..0000000 --- a/modules/dev-env/scripts/nav.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env bash -# dev-env: navigation helpers -# -# Sourced by user shells (not invoked as a script) via the loader in -# /etc/profile.d/dev-env-functions.sh. Every function reads -# /etc/dev-env/config.sh at call time, so adding a new worktree on disk -# is immediately visible without a nixos-rebuild. - -# Load runtime config (idempotent). -_devenv_load_config() { - if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh - fi -} - -# Navigate to the dev root. -dev() { - _devenv_load_config - cd "${DEV_ROOT:-$HOME/dev}" || return 1 -} - -# Navigate to an lnbits worktree. -# Usage: lb [worktree] (worktree ∈ whatever the filesystem shows, e.g. dev/main) -lb() { - _devenv_load_config - local env="${1:-}" - local lnbits_dir="${LNBITS_DIR:-$DEV_ROOT/lnbits}" - - if [[ -z "$env" ]]; then - echo "Usage: lb " - echo "" - echo "Current lnbits worktrees:" - if [[ -d "$lnbits_dir" ]]; then - for d in "$lnbits_dir"/*/; do - [[ -d "$d" ]] || continue - local name branch - name="$(basename "$d")" - branch="$(git -C "$d" branch --show-current 2>/dev/null || echo '?')" - printf " %-12s (%s)\n" "$name" "$branch" - done - fi - return 0 - fi - - if [[ -d "$lnbits_dir/$env" ]]; then - cd "$lnbits_dir/$env" || return 1 - else - echo "Unknown lnbits worktree: $env" - return 1 - fi -} - -# Navigate within a project-group folder (a `category` directory holding -# one or more sub-repos). Sub-repos may be single clones or bare-repo -# worktree sets; worktree sets accept an optional worktree name. -# Usage: g [repo] [worktree] -_dev_group_nav() { - local group_dir="$1" repo="${2:-}" worktree="${3:-dev}" - - if [[ -z "$repo" ]]; then - echo "repos under $(basename "$group_dir")/:" - if [[ -d "$group_dir" ]]; then - for d in "$group_dir"/*/; do - [[ -d "$d" ]] && echo " $(basename "$d")" - done - fi - return 0 - fi - - # Single-clone sub-repo (has its own .git at the top level) - if [[ -d "$group_dir/$repo/.git" ]]; then - cd "$group_dir/$repo" || return 1 - return 0 - fi - - # Worktree-based sub-repo - if [[ -d "$group_dir/$repo/$worktree" ]]; then - cd "$group_dir/$repo/$worktree" || return 1 - elif [[ -d "$group_dir/$repo" ]]; then - cd "$group_dir/$repo" || return 1 - else - echo "Unknown repo under $(basename "$group_dir")/: $repo" - return 1 - fi -} - -# Navigate into a project category directory. -# Usage: g [repo] [worktree] -g() { - _devenv_load_config - local category="${1:-}" - if [[ -z "$category" ]]; then - echo "Usage: g [repo] [worktree]" - echo "" - echo "Categories under $DEV_ROOT:" - for d in "$DEV_ROOT"/*/; do - [[ -d "$d" ]] && echo " $(basename "$d")" - done - return 0 - fi - _dev_group_nav "$DEV_ROOT/$category" "${2:-}" "${3:-dev}" -} - -# Navigate to an extension under shared/extensions. -ext() { - _devenv_load_config - local extension="${1:-}" - local ext_root="${SHARED_DIR:-$DEV_ROOT/shared}/extensions" - - if [[ -z "$extension" ]]; then - echo "Available extensions:" - [[ -d "$ext_root" ]] && ls -1 "$ext_root" - return 0 - fi - if [[ -d "$ext_root/$extension" ]]; then - cd "$ext_root/$extension" || return 1 - else - echo "Unknown extension: $extension" - [[ -d "$ext_root" ]] && ls -1 "$ext_root" - return 1 - fi -} - -# Navigate to a deploy host config inside the deploy flake working copy. -deploy_nav() { - _devenv_load_config - local host="${1:-}" - local deploy_root="${DEPLOY_DIR:-$DEV_ROOT/deploy}" - - if [[ -z "$host" ]]; then - echo "Usage: dep " - echo "" - echo "Deploy targets (from /etc/dev-env/config.sh):" - env | grep '^DEPLOY_TARGET_' | sed 's/^DEPLOY_TARGET_/ /' | sed 's/=/ → /' || true - return 1 - fi - - if [[ -d "$deploy_root/hosts/$host" ]]; then - cd "$deploy_root/hosts/$host" || return 1 - elif [[ -d "$deploy_root/$host" ]]; then - cd "$deploy_root/$host" || return 1 - elif [[ -d "$deploy_root" ]]; then - cd "$deploy_root" || return 1 - else - echo "Deploy path not found: $deploy_root" - return 1 - fi -} -alias dep=deploy_nav - -# Navigate to the shared repos directory. -shared() { - _devenv_load_config - cd "${SHARED_DIR:-$DEV_ROOT/shared}" || return 1 -} - -# Navigate to the bare-repos directory. -repos() { - _devenv_load_config - cd "${REPOS_DIR:-$DEV_ROOT/repos}" || return 1 -} - -# Navigate to the upstream-prs directory. -prs() { - _devenv_load_config - cd "${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}" || return 1 -} diff --git a/modules/dev-env/scripts/pr-helpers.sh b/modules/dev-env/scripts/pr-helpers.sh deleted file mode 100644 index 46763d5..0000000 --- a/modules/dev-env/scripts/pr-helpers.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -# dev-env: upstream PR worktree helpers -# -# Sourced into interactive shells. Provides prb / prc / prl. Each PR gets -# a throwaway worktree at ${UPSTREAM_PRS_DIR}/- based on -# upstream/main (or master), ready to push to the `fork` remote and open -# a PR. Reads paths from /etc/dev-env/config.sh. - -_devenv_load_config() { - if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh - fi -} - -# Create a PR worktree at ${UPSTREAM_PRS_DIR}/-. -# -# Usage: git-pr-branch -git-pr-branch() { - _devenv_load_config - local repo_name="${1:-}" - local branch_name="${2:-}" - local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}" - local prs_dir="${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}" - - if [[ -z "$repo_name" || -z "$branch_name" ]]; then - echo "Usage: git-pr-branch " - echo "Example: git-pr-branch lnbits fix-invoice-bug" - echo "" - echo "Creates a worktree at $prs_dir/-" - echo "based on upstream/main, ready for an upstream PR." - return 1 - fi - - local bare_repo="$repos_dir/${repo_name}.git" - local pr_path="$prs_dir/${repo_name}-${branch_name}" - - if [[ ! -d "$bare_repo" ]]; then - echo "Repo not found: $bare_repo" - echo "Available repos:" - ls -1 "$repos_dir"/*.git 2>/dev/null | xargs -n1 basename | sed 's/\.git$//' - return 1 - fi - - if [[ -d "$pr_path" ]]; then - echo "PR worktree already exists: $pr_path" - echo "To remove it: git-pr-cleanup $repo_name $branch_name" - return 1 - fi - - # Fetch upstream - echo "Fetching upstream..." - git -C "$bare_repo" fetch upstream 2>/dev/null || { - echo "No 'upstream' remote on $repo_name — cannot create PR branch" - return 1 - } - - # Determine base branch (main or master) - local base_branch="main" - git -C "$bare_repo" show-ref --verify --quiet "refs/remotes/upstream/main" \ - || base_branch="master" - - echo "Creating branch '$branch_name' from upstream/$base_branch..." - git -C "$bare_repo" branch "$branch_name" "upstream/$base_branch" 2>/dev/null \ - || git -C "$bare_repo" branch -f "$branch_name" "upstream/$base_branch" - - mkdir -p "$prs_dir" - git -C "$bare_repo" worktree add "$pr_path" "$branch_name" - - if ! git -C "$bare_repo" remote get-url fork &>/dev/null; then - echo "" - echo "Note: 'fork' remote not configured for $repo_name." - echo "Add it with:" - echo " git -C $bare_repo remote add fork git@github.com:${GITHUB_FORK_USER:-}/${repo_name}.git" - fi - - cat < " - echo "" - echo "Active PR worktrees:" - ls -1 "$prs_dir" 2>/dev/null || echo " (none)" - return 1 - fi - - local bare_repo="$repos_dir/${repo_name}.git" - local pr_path="$prs_dir/${repo_name}-${branch_name}" - - [[ -d "$pr_path" ]] || { echo "PR worktree not found: $pr_path"; return 1; } - - echo "Removing worktree: $pr_path" - git -C "$bare_repo" worktree remove "$pr_path" - - echo "Deleting branch: $branch_name" - git -C "$bare_repo" branch -d "$branch_name" 2>/dev/null \ - || git -C "$bare_repo" branch -D "$branch_name" - - echo "Done." -} - -# List all active PR worktrees. -git-pr-list() { - _devenv_load_config - local prs_dir="${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}" - - echo "=== Active PR Worktrees ===" - if [[ -d "$prs_dir" && -n "$(ls -A "$prs_dir" 2>/dev/null)" ]]; then - for pr_dir in "$prs_dir"/*; do - [[ -d "$pr_dir" ]] || continue - local name branch status - name="$(basename "$pr_dir")" - branch="$(git -C "$pr_dir" branch --show-current 2>/dev/null || echo '?')" - status="$(git -C "$pr_dir" status -sb 2>/dev/null | head -1 || echo '?')" - printf " %-40s %s %s\n" "$name" "$branch" "$status" - done - else - echo " (none)" - fi -} - -alias prb='git-pr-branch' -alias prc='git-pr-cleanup' -alias prl='git-pr-list' diff --git a/modules/dev-env/scripts/rebase.sh b/modules/dev-env/scripts/rebase.sh deleted file mode 100644 index 60078ca..0000000 --- a/modules/dev-env/scripts/rebase.sh +++ /dev/null @@ -1,409 +0,0 @@ -#!/usr/bin/env bash -# dev-env: safe fork-onto-upstream rebase helper -# -# Walks every worktree under $DEV_ROOT that has an `upstream` remote and -# rebases your fork commits onto upstream, with backups and a -# force-with-lease push gated on confirmation. Reads paths from -# /etc/dev-env/config.sh; the rebase log lives under the user's XDG -# state dir. -# -# Workflow for a single rebase: -# 1. Safety checks (no uncommitted changes, upstream remote exists) -# 2. Create backup branch `backup/pre-rebase-YYYYMMDD-HHMMSS` -# 3. Show incoming + outgoing commits -# 4. Rebase and force-with-lease push (with confirmation) -# 5. On conflict, print resolution guide and preserve backup - -set -e - -# Load runtime config (sets DEV_ROOT, REPOS_DIR, SHARED_DIR, …) -if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh -else - echo "Error: /etc/dev-env/config.sh not found (is the dev-env module enabled?)" >&2 - exit 1 -fi - -DEV_ROOT="${DEV_ROOT:-$HOME/dev}" -REPOS_DIR="${REPOS_DIR:-$DEV_ROOT/repos}" -SHARED_DIR="${SHARED_DIR:-$DEV_ROOT/shared}" - -BACKUP_PREFIX="backup/pre-rebase" -LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/dev-env/rebase.log" -mkdir -p "$(dirname "$LOG_FILE")" - -#------------------------------------------------------------------------------- -# Colors -#------------------------------------------------------------------------------- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -info() { echo -e "${BLUE}[INFO]${NC} $1"; } -success() { echo -e "${GREEN}[OK]${NC} $1"; } -warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -error() { echo -e "${RED}[ERROR]${NC} $1"; } -header() { echo -e "\n${BOLD}${CYAN}═══ $1 ═══${NC}\n"; } - -log_action() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" -} - -confirm() { - local prompt="${1:-Continue?}" - read -r -p "$prompt (y/N) " -n 1 REPLY - echo - [[ $REPLY =~ ^[Yy]$ ]] -} - -#------------------------------------------------------------------------------- -# Generic repo helpers -#------------------------------------------------------------------------------- -get_upstream_branch() { - local repo_path="$1" - (cd "$repo_path" - for branch in main master; do - if git rev-parse "upstream/$branch" &>/dev/null; then - echo "$branch" - return 0 - fi - done - echo "main") -} - -check_repo_clean() { - local repo_path="$1" - [[ -z "$(git -C "$repo_path" status --porcelain)" ]] -} - -has_upstream() { - git -C "$1" remote get-url upstream &>/dev/null -} - -create_backup() { - local repo_path="$1" - local branch - branch="$(git -C "$repo_path" branch --show-current)" - local backup_name="${BACKUP_PREFIX}-$(date +%Y%m%d-%H%M%S)" - git -C "$repo_path" branch -f "$backup_name" "$branch" - echo "$backup_name" -} - -show_divergence() { - local repo_path="$1" upstream_branch="$2" - local behind ahead - behind=$(git -C "$repo_path" rev-list --count "HEAD..upstream/$upstream_branch" 2>/dev/null || echo 0) - ahead=$(git -C "$repo_path" rev-list --count "upstream/$upstream_branch..HEAD" 2>/dev/null || echo 0) - echo -e " ${CYAN}Behind upstream:${NC} $behind commits" - echo -e " ${GREEN}Ahead (your work):${NC} $ahead commits" -} - -#------------------------------------------------------------------------------- -# Core rebase -#------------------------------------------------------------------------------- -rebase_single_repo() { - local repo_path="$1" repo_name="$2" - local upstream_branch="${3:-}" - local auto_mode="${4:-false}" - - header "Rebasing: $repo_name" - - if [[ ! -d "$repo_path/.git" ]] && [[ ! -f "$repo_path/.git" ]]; then - error "Not a git repository: $repo_path" - return 1 - fi - - if ! has_upstream "$repo_path"; then - warn "No upstream remote configured. Skipping." - return 0 - fi - - if ! check_repo_clean "$repo_path"; then - error "Uncommitted changes detected!" - echo " Stash or commit first: git stash / git commit" - return 1 - fi - - [[ -z "$upstream_branch" ]] && upstream_branch="$(get_upstream_branch "$repo_path")" - - local current_branch - current_branch="$(git -C "$repo_path" branch --show-current)" - info "Current branch: $current_branch" - info "Upstream branch: upstream/$upstream_branch" - - info "Fetching upstream..." - git -C "$repo_path" fetch upstream - - show_divergence "$repo_path" "$upstream_branch" - - local behind - behind=$(git -C "$repo_path" rev-list --count "HEAD..upstream/$upstream_branch" 2>/dev/null || echo 0) - if [[ "$behind" -eq 0 ]]; then - success "Already up to date with upstream!" - return 0 - fi - - echo "" - echo -e "${BOLD}New commits from upstream:${NC}" - git -C "$repo_path" log --oneline --graph "HEAD..upstream/$upstream_branch" | head -15 - local total_upstream - total_upstream="$(git -C "$repo_path" rev-list --count "HEAD..upstream/$upstream_branch")" - (( total_upstream > 15 )) && echo " ... and $((total_upstream - 15)) more commits" - - echo "" - echo -e "${BOLD}Your commits to replay:${NC}" - git -C "$repo_path" log --oneline "upstream/$upstream_branch..HEAD" | head -15 - local total_yours - total_yours="$(git -C "$repo_path" rev-list --count "upstream/$upstream_branch..HEAD")" - (( total_yours > 15 )) && echo " ... and $((total_yours - 15)) more commits" - - echo "" - if [[ "$auto_mode" != "true" ]] && ! confirm "Proceed with rebase?"; then - warn "Skipped." - return 0 - fi - - local backup_branch - backup_branch="$(create_backup "$repo_path")" - success "Backup created: $backup_branch" - - info "Rebasing onto upstream/$upstream_branch..." - if git -C "$repo_path" rebase "upstream/$upstream_branch"; then - success "Rebase completed successfully!" - echo "" - if confirm "Push to origin with --force-with-lease?"; then - git -C "$repo_path" push origin "$current_branch" --force-with-lease - success "Pushed to origin!" - log_action "REBASE SUCCESS: $repo_name onto upstream/$upstream_branch" - else - warn "Not pushed. When ready: git push origin $current_branch --force-with-lease" - fi - echo "" - info "Backup branch '$backup_branch' preserved." - echo " Delete later: git branch -D $backup_branch" - return 0 - else - error "Rebase encountered conflicts!" - cat <>>>>>> - 3. Stage: ${CYAN}git add ${NC} - 4. Continue: ${CYAN}git rebase --continue${NC} - 5. Skip empty: ${CYAN}git rebase --skip${NC} - 6. Abort: ${CYAN}git rebase --abort${NC} - 7. After success: ${CYAN}git push origin $current_branch --force-with-lease${NC} - - Backup: ${GREEN}$backup_branch${NC} -EOF - log_action "REBASE CONFLICT: $repo_name - manual resolution required" - return 1 - fi -} - -#------------------------------------------------------------------------------- -# Discovery: walk actual worktrees under $DEV_ROOT instead of the old -# $PROJECTS_DIR layout that no longer exists. -#------------------------------------------------------------------------------- -find_forked_repos() { - # Print one ":" per rebase-eligible repo (has - # upstream remote). We look at every git dir (.git dir or .git file - # for worktrees) under $DEV_ROOT, excluding $REPOS_DIR bare repos. - find "$DEV_ROOT" -type d -name '.git' -prune 2>/dev/null | while read -r gitdir; do - local repo_path - repo_path="$(dirname "$gitdir")" - # Skip bare repos under REPOS_DIR - [[ "$repo_path" == "$REPOS_DIR"* ]] && continue - # Skip node_modules - [[ "$repo_path" == *"/node_modules/"* ]] && continue - if has_upstream "$repo_path"; then - local display - display="${repo_path#$DEV_ROOT/}" - echo "$repo_path:$display" - fi - done - # Also bare repos under REPOS_DIR (the .git file case) - find "$DEV_ROOT" -type f -name '.git' 2>/dev/null | while read -r gitfile; do - local repo_path - repo_path="$(dirname "$gitfile")" - [[ "$repo_path" == *"/node_modules/"* ]] && continue - if has_upstream "$repo_path"; then - local display - display="${repo_path#$DEV_ROOT/}" - echo "$repo_path:$display" - fi - done -} - -#------------------------------------------------------------------------------- -# Batch modes -#------------------------------------------------------------------------------- -rebase_all() { - header "Rebasing ALL Forks with Upstreams" - warn "This walks $DEV_ROOT for every worktree with an 'upstream' remote." - echo "" - if ! confirm "Continue?"; then info "Aborted."; return 0; fi - - local failed=() - while IFS=: read -r path name; do - if ! rebase_single_repo "$path" "$name"; then - failed+=("$name") - fi - done < <(find_forked_repos) - - echo "" - header "Summary" - if (( ${#failed[@]} == 0 )); then - success "All repositories rebased successfully!" - else - error "Failed (${#failed[@]}):" - printf ' - %s\n' "${failed[@]}" - return 1 - fi -} - -show_all_status() { - header "Repository Rebase Status" - while IFS=: read -r path name; do - local upstream_branch behind ahead - upstream_branch="$(get_upstream_branch "$path")" - git -C "$path" fetch upstream --quiet 2>/dev/null || true - behind=$(git -C "$path" rev-list --count "HEAD..upstream/$upstream_branch" 2>/dev/null || echo ?) - ahead=$(git -C "$path" rev-list --count "upstream/$upstream_branch..HEAD" 2>/dev/null || echo ?) - printf " %-40s ↓%-3s ↑%-3s\n" "$name" "$behind" "$ahead" - done < <(find_forked_repos) - echo "" - echo -e "${CYAN}Legend:${NC} ↓=behind upstream ↑=ahead (your commits)" -} - -view_log() { - header "Rebase Log" - if [[ -f "$LOG_FILE" ]]; then tail -50 "$LOG_FILE" - else info "No rebase log yet at $LOG_FILE"; fi -} - -cleanup_backups() { - header "Cleanup Backup Branches" - echo "Options:" - echo " 1) Delete backups older than 7 days" - echo " 2) Delete backups older than 30 days" - echo " 3) Delete ALL backup branches" - echo " 4) Cancel" - read -r -p "Select option: " -n 1 REPLY; echo "" - - local days=0 - case $REPLY in - 1) days=7 ;; - 2) days=30 ;; - 3) days=0 ;; - *) info "Cancelled."; return 0 ;; - esac - - local cutoff="" - (( days > 0 )) && cutoff=$(date -d "$days days ago" +%Y%m%d 2>/dev/null || date -v-"${days}d" +%Y%m%d) - - local deleted=0 - while IFS=: read -r path _; do - cd "$path" || continue - git branch --list "backup/*" 2>/dev/null | while read -r branch; do - branch="$(echo "$branch" | tr -d ' *')" - local bdate - bdate="$(echo "$branch" | grep -oE '[0-9]{8}' | head -1)" - if [[ $days -eq 0 ]] \ - || { [[ -n "$bdate" ]] && [[ "$bdate" < "$cutoff" ]]; }; then - git branch -D "$branch" 2>/dev/null && deleted=$((deleted + 1)) || true - fi - done - done < <(find_forked_repos) - success "Deleted $deleted backup branch(es)." -} - -#------------------------------------------------------------------------------- -# Interactive / CLI -#------------------------------------------------------------------------------- -interactive_menu() { - while true; do - header "Rebase Helper" - echo " 1) Rebase a single repository (by path)" - echo " 2) Rebase ALL forks with upstream" - echo " s) Status of all repos" - echo " l) View rebase log" - echo " c) Clean up old backup branches" - echo " q) Quit" - echo "" - read -r -p "Select: " -n 1 REPLY; echo "" - case $REPLY in - 1) read -r -p "Repo path: " p - [[ -d "$p/.git" || -f "$p/.git" ]] && rebase_single_repo "$p" "$(basename "$p")" || warn "Not a repo: $p" ;; - 2) rebase_all ;; - s) show_all_status ;; - l) view_log ;; - c) cleanup_backups ;; - q) echo "Bye!"; exit 0 ;; - *) warn "Invalid option" ;; - esac - echo "" - read -r -p "Press Enter to continue..." - done -} - -usage() { - cat < Rebase a single repository - rebase all Rebase all forks with upstream remote - rebase status Show status of all forks - rebase log Show rebase log - rebase cleanup Clean up old backup branches - -OPTIONS: - -b, --branch Upstream branch to rebase onto - -y, --yes Auto-confirm prompts - -h, --help Show this help - -Backups are branched automatically as backup/pre-rebase-YYYYMMDD-HHMMSS -and never deleted without explicit cleanup. -EOF -} - -main() { - local command="" repo_path="" upstream_branch="" auto_yes=false - while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) usage; exit 0 ;; - -b|--branch) upstream_branch="$2"; shift 2 ;; - -y|--yes) auto_yes=true; shift ;; - single|all|status|log|cleanup) command="$1"; shift ;; - *) - if [[ -z "$command" ]] && [[ -d "$1/.git" || -f "$1/.git" ]]; then - command="single"; repo_path="$1" - else - repo_path="$1" - fi - shift - ;; - esac - done - - case $command in - "") interactive_menu ;; - single) [[ -z "$repo_path" ]] && { error "Need path"; exit 1; } - rebase_single_repo "$repo_path" "$(basename "$repo_path")" "$upstream_branch" "$auto_yes" ;; - all) rebase_all ;; - status) show_all_status ;; - log) view_log ;; - cleanup) cleanup_backups ;; - *) error "Unknown command: $command"; usage; exit 1 ;; - esac -} - -main "$@" diff --git a/modules/dev-env/scripts/regtest.sh b/modules/dev-env/scripts/regtest.sh deleted file mode 100644 index 16c94e9..0000000 --- a/modules/dev-env/scripts/regtest.sh +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/env bash -# dev-env: Bitcoin/Lightning regtest docker environment -# -# Provides: -# regtest-start [worktree|pr:] [--path ] [--seed|--keep] -# regtest-stop -# regtest-status -# regtest-logs [service] -# regtest-cli # source CLI helpers -# regtest # cd to regtest dir -# -# Reads paths from /etc/dev-env/config.sh. Wraps the -# `lnbits/legend-regtest-enviroment` compose stack (or your fork of it, -# set via devEnv.regtest.repoUrl). Container names and ports below assume -# that stack's layout — adjust if you've customised it. -# -# This file is sourced into interactive shells (function definitions) AND -# dropped into the system path as standalone wrapper scripts so -# `regtest-start` works from a fresh non-interactive shell. - -_devenv_load_config() { - if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh - fi - REGTEST_DIR="${LOCAL_DIR:-$DEV_ROOT/local}/docker/regtest" - LNBITS_DIR="${LNBITS_DIR:-$DEV_ROOT/lnbits}" - UPSTREAM_PRS_DIR="${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}" - # Node used for readiness/balance checks. Override if your stack - # names containers differently. - REGTEST_LND="${REGTEST_LND:-lnbits-lnd-4-1}" -} - -#------------------------------------------------------------------------------- -# Status helpers -#------------------------------------------------------------------------------- - -_regtest_is_running() { - docker ps --filter "name=$REGTEST_LND" --format '{{.Names}}' 2>/dev/null \ - | grep -q "$REGTEST_LND" -} - -#------------------------------------------------------------------------------- -# Start -#------------------------------------------------------------------------------- -# Usage: regtest-start [worktree|pr:] [--path ] [--seed|--keep] -# -# worktree an lnbits worktree name (under ~/dev/lnbits/) -# pr: ~/dev/upstream-prs/lnbits- -# --path arbitrary lnbits directory -# --seed copy lnbits-seed/ to lnbits/ before starting -# --keep keep existing data -regtest-start() { - _devenv_load_config - - local env="" data_mode="fresh" custom_path="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --seed) data_mode="seed" ;; - --keep) data_mode="keep" ;; - --path) shift; custom_path="$1" ;; - pr:*) - local pr_branch="${1#pr:}" - custom_path="$UPSTREAM_PRS_DIR/lnbits-$pr_branch" - ;; - -*) - cat <] [--path ] [--seed|--keep] - - worktree an lnbits worktree name (under ~/dev/lnbits/) - pr: ~/dev/upstream-prs/lnbits- - --path arbitrary lnbits directory -USAGE - return 1 - ;; - *) env="$1" ;; - esac - shift - done - - if [[ ! -d "$REGTEST_DIR" ]]; then - echo "Regtest environment not found at $REGTEST_DIR" - echo "Run dev-env-bootstrap to clone it." - return 1 - fi - - # Determine lnbits source dir - local lnbits_path="" - if [[ -n "$custom_path" ]]; then - lnbits_path="$custom_path" - [[ -d "$lnbits_path" ]] || { echo "lnbits dir not found: $lnbits_path"; return 1; } - else - [[ -z "$env" ]] && env="dev" - lnbits_path="$LNBITS_DIR/$env" - [[ -d "$lnbits_path" ]] || { echo "lnbits worktree not found: $env"; return 1; } - fi - - local lnbits_data="$REGTEST_DIR/data/lnbits" - local lnbits_seed="$REGTEST_DIR/data/lnbits-seed" - - case "$data_mode" in - fresh) - if [[ -d "$lnbits_data" ]] && [[ -n "$(ls -A "$lnbits_data" 2>/dev/null)" ]]; then - echo "Existing lnbits data at $lnbits_data" - read -r -p "Wipe it? [y/N] " -n 1 REPLY; echo - [[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; return 1; } - docker run --rm -v "$lnbits_data":/data alpine sh -c "rm -rf /data/* /data/.*" 2>/dev/null || true - rmdir "$lnbits_data" 2>/dev/null || true - fi - mkdir -p "$lnbits_data" - ;; - seed) - [[ -d "$lnbits_seed" ]] || { echo "Seed data not found at $lnbits_seed"; return 1; } - if [[ -d "$lnbits_data" ]] && [[ -n "$(ls -A "$lnbits_data" 2>/dev/null)" ]]; then - read -r -p "Overwrite with seed? [y/N] " -n 1 REPLY; echo - [[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; return 1; } - docker run --rm -v "$lnbits_data":/data alpine sh -c "rm -rf /data/* /data/.*" 2>/dev/null || true - rmdir "$lnbits_data" 2>/dev/null || true - fi - cp -r "$lnbits_seed" "$lnbits_data" - ;; - keep) - mkdir -p "$lnbits_data" - ;; - esac - - echo "" - echo "Starting regtest..." - echo " lnbits path: $lnbits_path" - echo " data mode: $data_mode" - echo "" - - echo "Building lnbits Docker image from $lnbits_path..." - docker build -t lnbits/lnbits "$lnbits_path" - - (cd "$REGTEST_DIR" && ./start-regtest) - - cat </dev/null \ - | grep -oP '"total_balance":\s*"\K[0-9]+' || echo 0)" - echo "" - echo "$REGTEST_LND balance: $balance sats" - else - echo "Status: STOPPED" - echo "Start with: regtest-start" - fi -} - -regtest-logs() { - _devenv_load_config - local service="${1:-}" - [[ -d "$REGTEST_DIR" ]] || { echo "Regtest not found"; return 1; } - if [[ -n "$service" ]]; then - (cd "$REGTEST_DIR" && docker compose logs -f "$service") - else - (cd "$REGTEST_DIR" && docker compose logs -f) - fi -} - -regtest-cli() { - _devenv_load_config - [[ -f "$REGTEST_DIR/docker-scripts.sh" ]] || { echo "docker-scripts.sh missing"; return 1; } - echo "Sourcing regtest CLI helpers..." - # shellcheck disable=SC1091 - source "$REGTEST_DIR/docker-scripts.sh" - cat < LND node CLI - lightning-cli-sim <1-3> C-Lightning node CLI - boltzcli-sim Boltz client CLI - elements-cli-sim Elements/Liquid CLI - -Examples: - bitcoin-cli-sim -generate 1 - lncli-sim 1 getinfo -EOF -} - -regtest() { - _devenv_load_config - [[ -d "$REGTEST_DIR" ]] && cd "$REGTEST_DIR" || { echo "Regtest not found at $REGTEST_DIR"; return 1; } -} - -#------------------------------------------------------------------------------- -# Rebuild / restart the lnbits service (docker-compose.dev.yml workflow) -#------------------------------------------------------------------------------- -# regtest-lnbits-rebuild build (cached) + recreate the lnbits container -# regtest-lnbits-rebuild --clean build --no-cache + recreate (truly fresh image) -# regtest-lnbits-restart restart the container without rebuilding -# -# The cached build is the happy path: docker invalidates the source COPY layer -# when LNBITS_SRC content changes, so most rebuilds are fast. Use --clean when -# you've been mucking with the image itself. - -_regtest_lnbits_compose_file() { - echo "$REGTEST_DIR/docker-compose.dev.yml" -} - -regtest-lnbits-rebuild() { - _devenv_load_config - local compose_file build_args=() - compose_file="$(_regtest_lnbits_compose_file)" - [[ -f "$compose_file" ]] || { echo "Compose file not found: $compose_file"; return 1; } - - if [[ "${1:-}" == "--clean" ]]; then - build_args+=(--no-cache) - fi - - (cd "$REGTEST_DIR" \ - && docker compose -f "$compose_file" build "${build_args[@]}" lnbits \ - && docker compose -f "$compose_file" up -d --force-recreate lnbits) -} - -regtest-lnbits-restart() { - _devenv_load_config - local compose_file - compose_file="$(_regtest_lnbits_compose_file)" - [[ -f "$compose_file" ]] || { echo "Compose file not found: $compose_file"; return 1; } - docker compose -f "$compose_file" restart lnbits -} diff --git a/modules/dev-env/scripts/status.sh b/modules/dev-env/scripts/status.sh deleted file mode 100644 index 2a7341f..0000000 --- a/modules/dev-env/scripts/status.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env bash -# dev-status: show divergence and dirty state of every dev-env worktree -# -# Reads /etc/dev-env/projects.json and walks every declared worktree, -# reporting dirty state and ahead/behind counts vs origin (and upstream, -# for projects that declare one). - -set -euo pipefail - -if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh -fi - -PROJECTS_JSON="${DEVENV_PROJECTS_JSON:-/etc/dev-env/projects.json}" - -if [[ ! -r "$PROJECTS_JSON" ]]; then - echo "projects.json not found at $PROJECTS_JSON" >&2 - exit 1 -fi - -if ! command -v jq >/dev/null; then - echo "jq required" >&2 - exit 1 -fi - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -issues=() - -header() { - echo "" - echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════${NC}" - echo -e "${BOLD}${CYAN} $1${NC}" - echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════${NC}" -} - -check_one() { - local label="$1" path="$2" has_upstream="${3:-false}" - - if [[ ! -d "$path/.git" ]] && [[ ! -f "$path/.git" ]]; then - printf " ${YELLOW}⚠${NC} %s: not present\n" "$label" - issues+=("$label: not present") - return - fi - - git -C "$path" fetch --all --quiet 2>/dev/null || true - - local branch icons="" status_str="" - branch="$(git -C "$path" branch --show-current 2>/dev/null || echo '?')" - - [[ -n "$(git -C "$path" status --porcelain)" ]] && { - icons+="${YELLOW}●${NC} " - status_str+="dirty " - } - - local behind ahead - behind=$(git -C "$path" rev-list --count "HEAD..origin/$branch" 2>/dev/null || echo 0) - ahead=$(git -C "$path" rev-list --count "origin/$branch..HEAD" 2>/dev/null || echo 0) - (( behind > 0 )) && icons+="${RED}↓${NC}$behind " - (( ahead > 0 )) && icons+="${GREEN}↑${NC}$ahead " - - if [[ "$has_upstream" == "true" ]] && git -C "$path" remote get-url upstream &>/dev/null; then - local ub="main" - git -C "$path" rev-parse upstream/main &>/dev/null || ub="master" - local ub_behind - ub_behind=$(git -C "$path" rev-list --count "HEAD..upstream/$ub" 2>/dev/null || echo 0) - if (( ub_behind > 0 )); then - icons+="${CYAN}⇣${NC}$ub_behind " - issues+=("$label: $ub_behind behind upstream/$ub") - fi - fi - - [[ -z "$icons" ]] && icons="${GREEN}✓${NC}" - printf " %-45s %s (%s)\n" "$label" "$icons" "$branch" -} - -header "Development Environment Status" -echo -e " ${BLUE}Date:${NC} $(date '+%Y-%m-%d %H:%M')" -echo -e " ${BLUE}Host:${NC} $(hostname)" -echo -e " ${BLUE}Root:${NC} ${DEV_ROOT:-?}" - -# Walk every project and worktree from the JSON. -mapfile -t PROJECTS < <(jq -r 'keys[]' "$PROJECTS_JSON") - -for proj in "${PROJECTS[@]}"; do - echo "" - echo -e "${BLUE}─── $proj ───${NC}" - - is_clone="$(jq -r --arg p "$proj" '.[$p].isClone' "$PROJECTS_JSON")" - has_upstream_decl="$(jq -r --arg p "$proj" '.[$p].remotes.upstream != null' "$PROJECTS_JSON")" - - if [[ "$is_clone" == "true" ]]; then - clone_path="$(jq -r --arg p "$proj" '.[$p].clonePath' "$PROJECTS_JSON")" - check_one "$proj" "$clone_path" "$has_upstream_decl" - else - while IFS=$'\t' read -r wt_name wt_path; do - [[ -z "$wt_name" || "$wt_path" == "null" ]] && continue - check_one "$proj/$wt_name" "$wt_path" "$has_upstream_decl" - done < <(jq -r --arg p "$proj" ' - .[$p].worktrees - | to_entries[] - | "\(.key)\t\(.value.path)" - ' "$PROJECTS_JSON") - fi -done - -# Docker -echo "" -echo -e "${BLUE}─── Docker ───${NC}" -if command -v docker &>/dev/null; then - running="$(docker ps --format '{{.Names}}' 2>/dev/null | wc -l)" - echo -e " ${BLUE}Containers running:${NC} $running" -fi - -header "Summary" -if (( ${#issues[@]} == 0 )); then - echo -e " ${GREEN}✓ All worktrees are clean and in sync!${NC}" -else - echo -e " ${YELLOW}Issues found:${NC}" - for issue in "${issues[@]}"; do - echo -e " ${YELLOW}•${NC} $issue" - done -fi - -cat < fetch upstream + show divergence - rebase status which forks need rebasing onto upstream - dev-env-bootstrap materialize missing worktrees - -EOF diff --git a/modules/dev-env/scripts/tmux-launch.sh b/modules/dev-env/scripts/tmux-launch.sh deleted file mode 100644 index 031b468..0000000 --- a/modules/dev-env/scripts/tmux-launch.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -# dev-tm: launch a declared tmux session -# -# Reads /etc/dev-env/tmux-sessions.json (rendered by config.nix) and -# either attaches to an existing session or creates one with the -# declared windows. Window cwds are resolved relative to $DEV_ROOT. -# -# This is a single generic launcher; the window layouts come from Nix -# (or a runtime override at ~/.config/dev-env/tmux-sessions.json). - -set -euo pipefail - -if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh -fi - -DEV_ROOT="${DEV_ROOT:-$HOME/dev}" - -# Prefer a user override if present -SESSIONS_JSON="" -if [[ -r "$HOME/.config/dev-env/tmux-sessions.json" ]]; then - SESSIONS_JSON="$HOME/.config/dev-env/tmux-sessions.json" -elif [[ -r /etc/dev-env/tmux-sessions.json ]]; then - SESSIONS_JSON=/etc/dev-env/tmux-sessions.json -else - echo "No tmux-sessions.json found" >&2 - exit 1 -fi - -usage() { - cat < start or attach to - dev-tm -k kill session - dev-tm -k all kill all dev sessions - dev-tm -l list available sessions -EOF -} - -list_sessions() { - echo "Defined sessions (from $SESSIONS_JSON):" - jq -r 'keys[]' "$SESSIONS_JSON" | sed 's/^/ /' - echo "" - echo "Active tmux sessions:" - tmux list-sessions 2>/dev/null | sed 's/^/ /' || echo " (none)" -} - -resolve_path() { - local p="$1" - [[ -z "$p" || "$p" == "null" ]] && { echo "$DEV_ROOT"; return; } - [[ "$p" = /* ]] && { echo "$p"; return; } - echo "$DEV_ROOT/$p" -} - -start_session() { - local name="$1" - local session_name="dev-$name" - - if tmux has-session -t "$session_name" 2>/dev/null; then - tmux attach-session -t "$session_name" - return - fi - - local session_cwd - session_cwd="$(resolve_path "$(jq -r --arg s "$name" '.[$s].cwd // empty' "$SESSIONS_JSON")")" - - local nwindows - nwindows="$(jq --arg s "$name" '.[$s].windows | length' "$SESSIONS_JSON")" - if (( nwindows == 0 )); then - echo "Session '$name' has no windows defined" >&2 - exit 1 - fi - - # First window - local first_name first_cwd first_cmd - first_name="$(jq -r --arg s "$name" '.[$s].windows[0].name' "$SESSIONS_JSON")" - first_cwd="$(resolve_path "$(jq -r --arg s "$name" '.[$s].windows[0].cwd // empty' "$SESSIONS_JSON")")" - first_cmd="$(jq -r --arg s "$name" '.[$s].windows[0].cmd // empty' "$SESSIONS_JSON")" - - tmux new-session -d -s "$session_name" -n "$first_name" -c "$first_cwd" - [[ -n "$first_cmd" && "$first_cmd" != "null" ]] && \ - tmux send-keys -t "$session_name:0" "$first_cmd" C-m - - # Remaining windows - for ((i = 1; i < nwindows; i++)); do - local wn wcwd wcmd - wn="$(jq -r --arg s "$name" --argjson i "$i" '.[$s].windows[$i].name' "$SESSIONS_JSON")" - wcwd="$(resolve_path "$(jq -r --arg s "$name" --argjson i "$i" '.[$s].windows[$i].cwd // empty' "$SESSIONS_JSON")")" - wcmd="$(jq -r --arg s "$name" --argjson i "$i" '.[$s].windows[$i].cmd // empty' "$SESSIONS_JSON")" - tmux new-window -t "$session_name" -n "$wn" -c "$wcwd" - [[ -n "$wcmd" && "$wcmd" != "null" ]] && \ - tmux send-keys -t "$session_name:$i" "$wcmd" C-m - done - - tmux select-window -t "$session_name:0" - tmux attach-session -t "$session_name" -} - -kill_session() { - local target="$1" - if [[ "$target" == "all" ]]; then - tmux list-sessions -F '#{session_name}' 2>/dev/null \ - | grep '^dev-' \ - | xargs -r -n1 tmux kill-session -t \ - || echo "No dev sessions to kill" - return - fi - tmux kill-session -t "dev-$target" 2>/dev/null || echo "Session 'dev-$target' not found" -} - -# --- entry --- -case "${1:-}" in - -h|--help) usage ;; - -l|--list|"") list_sessions ;; - -k|--kill) - [[ -z "${2:-}" ]] && { echo "Usage: dev-tm -k "; exit 1; } - kill_session "$2" - ;; - *) - if ! jq -e --arg s "$1" 'has($s)' "$SESSIONS_JSON" >/dev/null; then - echo "Unknown session: $1" >&2 - list_sessions - exit 1 - fi - start_session "$1" - ;; -esac diff --git a/modules/dev-env/scripts/worktree.sh b/modules/dev-env/scripts/worktree.sh deleted file mode 100644 index 69ad317..0000000 --- a/modules/dev-env/scripts/worktree.sh +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env bash -# dev-env: git worktree helpers -# -# Sourced into interactive shells. Provides: -# - wt / wts / wtu generic worktree listing / sync / upstream-fetch -# - wtn / worktree-spawn create a new branch + worktree from any project -# - lnbits-status show dev/main divergence vs upstream -# - lnbits-sync-dev merge upstream/dev into the dev worktree -# - lnbits-sync-main merge upstream/main into the main worktree -# -# The bare-repo path comes from /etc/dev-env/config.sh. - -_devenv_load_config() { - if [[ -r /etc/dev-env/config.sh ]]; then - # shellcheck disable=SC1091 - source /etc/dev-env/config.sh - fi -} - -#------------------------------------------------------------------------------- -# Generic worktree helpers -#------------------------------------------------------------------------------- - -worktree-list() { - _devenv_load_config - local repo_name="${1:-}" - local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}" - - if [[ -n "$repo_name" ]]; then - local bare_repo="$repos_dir/${repo_name}.git" - if [[ -d "$bare_repo" ]]; then - git -C "$bare_repo" worktree list - else - echo "Repo not found: $repo_name" - return 1 - fi - else - echo "=== All Worktrees ===" - for repo in "$repos_dir"/*.git; do - [[ -d "$repo" ]] || continue - echo "" - echo "--- $(basename "$repo" .git) ---" - git -C "$repo" worktree list - done - fi -} - -worktree-sync() { - _devenv_load_config - local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}" - echo "=== Syncing Worktrees ===" - for repo in "$repos_dir"/*.git; do - [[ -d "$repo" ]] || continue - local name - name="$(basename "$repo" .git)" - echo "" - echo "--- $name ---" - git -C "$repo" fetch --all --quiet 2>/dev/null || true - git -C "$repo" worktree list | while read -r line; do - # format: [] - local path branch status - path="$(awk '{print $1}' <<<"$line")" - branch="$(awk '{print $3}' <<<"$line" | tr -d '[]')" - if [[ -d "$path" ]]; then - status="$(git -C "$path" status -sb 2>/dev/null | head -1)" - printf " %-20s %s\n" "${branch:-?}" "$status" - fi - done - done -} - -worktree-update-upstream() { - _devenv_load_config - local repo_name="${1:-}" - local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}" - - if [[ -z "$repo_name" ]]; then - echo "Usage: wtu " - return 1 - fi - - local bare_repo="$repos_dir/${repo_name}.git" - if [[ ! -d "$bare_repo" ]]; then - echo "Repo not found: $bare_repo" - return 1 - fi - - if ! git -C "$bare_repo" remote get-url upstream &>/dev/null; then - echo "No upstream remote configured for $repo_name" - return 1 - fi - - echo "Fetching upstream..." - git -C "$bare_repo" fetch upstream - - echo "" - echo "=== Upstream Status: $repo_name ===" - - # Determine upstream base branch (main or master). - local base_branch="main" - git -C "$bare_repo" show-ref --verify --quiet refs/remotes/upstream/main || base_branch="master" - - # Show each local branch's divergence vs the upstream base. - git -C "$bare_repo" for-each-ref --format='%(refname:short)' refs/heads | while read -r br; do - local behind ahead - behind=$(git -C "$bare_repo" rev-list --count "$br..upstream/$base_branch" 2>/dev/null || echo ?) - ahead=$(git -C "$bare_repo" rev-list --count "upstream/$base_branch..$br" 2>/dev/null || echo ?) - printf " %-24s behind:%s ahead:%s\n" "$br" "$behind" "$ahead" - done -} - -#------------------------------------------------------------------------------- -# Spawn a new worktree (== branch) from an existing project. -#------------------------------------------------------------------------------- -# Usage: worktree-spawn [base-branch] -# -# Locates the bare repo, creates from (default -# main, else master), and adds a worktree at the project root next to the -# base worktree. - -worktree-spawn() { - _devenv_load_config - local repo_name="${1:-}" - local new_branch="${2:-}" - local base_branch="${3:-}" - - if [[ -z "$repo_name" || -z "$new_branch" ]]; then - echo "Usage: worktree-spawn [base-branch]" - return 1 - fi - - local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}" - local bare_repo="$repos_dir/${repo_name}.git" - - [[ -d "$bare_repo" ]] || { echo "Repo not found: $bare_repo"; return 1; } - - if [[ -z "$base_branch" ]]; then - if git -C "$bare_repo" show-ref --verify --quiet refs/heads/main; then - base_branch="main" - elif git -C "$bare_repo" show-ref --verify --quiet refs/heads/master; then - base_branch="master" - else - echo "No main/master in $repo_name; pass base branch explicitly." - return 1 - fi - fi - - local base_path - base_path="$(git -C "$bare_repo" worktree list --porcelain | awk -v t="refs/heads/$base_branch" ' - /^worktree / { wt = $2 } - /^branch / && $2 == t { print wt; exit } - ')" - [[ -n "$base_path" ]] || { echo "No worktree for $repo_name@$base_branch"; return 1; } - - local project_root new_path - project_root="$(dirname "$base_path")" - new_path="$project_root/$new_branch" - - [[ -e "$new_path" ]] && { echo "Path already exists: $new_path"; return 1; } - - echo "Creating branch '$new_branch' from '$base_branch' in $repo_name..." - git -C "$bare_repo" branch "$new_branch" "$base_branch" || return 1 - - echo "Adding worktree at $new_path..." - git -C "$bare_repo" worktree add "$new_path" "$new_branch" || return 1 - - echo "" - echo "Ready: $new_path" - [[ -f "$new_path/package.json" ]] && echo "Next: cd $new_path && pnpm install" -} - -#------------------------------------------------------------------------------- -# lnbits fork workflow helpers -#------------------------------------------------------------------------------- -# Mental model: -# Your `dev` and `main` worktrees track a personal fork of lnbits that -# diverges from upstream and carries your own commits on top. The sync -# helpers below `merge upstream/` to vendor upstream changes — -# they don't fast-forward. `lnbits-status` reports divergence vs -# upstream so you know how stale the vendor base is. -# -# Adjust the `dev`/`main` worktree names to whatever your lnbits -# project declares in devEnv.projects.lnbits.worktrees. - -_lnbits_paths() { - _devenv_load_config - BARE="${REPOS_DIR:-$DEV_ROOT/repos}/lnbits.git" - LNBITS_ROOT="${LNBITS_DIR:-$DEV_ROOT/lnbits}" -} - -lnbits-status() { - _lnbits_paths - if [[ ! -d "$BARE" ]]; then - echo "lnbits bare repo not found: $BARE" - return 1 - fi - - git -C "$BARE" fetch upstream --quiet 2>/dev/null || true - git -C "$BARE" fetch origin --quiet 2>/dev/null || true - - echo "=== lnbits Branch Status ===" - echo "" - echo "Branch layout (your branches diverge from upstream; sync via merge):" - echo " dev = your staging (vendored from upstream/dev)" - echo " main = your stable (vendored from upstream/main)" - echo "" - - for env in dev main; do - local wt="$LNBITS_ROOT/$env" - if [[ ! -d "$wt" ]]; then - printf " %-6s: (worktree not created)\n" "$env" - continue - fi - local branch behind_u ahead_u - branch="$(git -C "$wt" branch --show-current)" - case "$env" in - dev) - behind_u=$(git -C "$BARE" rev-list --count "$branch..upstream/dev" 2>/dev/null || echo ?) - ahead_u=$(git -C "$BARE" rev-list --count "upstream/dev..$branch" 2>/dev/null || echo ?) - printf " dev: %-3s behind upstream/dev %-3s ahead\n" "$behind_u" "$ahead_u" - ;; - main) - behind_u=$(git -C "$BARE" rev-list --count "$branch..upstream/main" 2>/dev/null || echo ?) - ahead_u=$(git -C "$BARE" rev-list --count "upstream/main..$branch" 2>/dev/null || echo ?) - printf " main: %-3s behind upstream/main %-3s ahead\n" "$behind_u" "$ahead_u" - ;; - esac - done - - echo "" - echo "Commands:" - echo " lnbits-sync-dev # merge upstream/dev into dev" - echo " lnbits-sync-main # merge upstream/main into main worktree" -} - -lnbits-sync-dev() { - _lnbits_paths - local dev_dir="$LNBITS_ROOT/dev" - [[ -d "$BARE" ]] || { echo "bare repo missing: $BARE"; return 1; } - [[ -d "$dev_dir" ]] || { echo "dev worktree missing: $dev_dir"; return 1; } - - echo "Fetching upstream..." - git -C "$BARE" fetch upstream - - echo "Merging upstream/dev into dev..." - git -C "$dev_dir" merge upstream/dev - - echo "" - echo "Dev synced. Review, then: lb dev && git push" -} - -lnbits-sync-main() { - _lnbits_paths - local main_dir="$LNBITS_ROOT/main" - [[ -d "$BARE" ]] || { echo "bare repo missing: $BARE"; return 1; } - [[ -d "$main_dir" ]] || { echo "main worktree missing: $main_dir"; return 1; } - - echo "Fetching upstream..." - git -C "$BARE" fetch upstream - - echo "Merging upstream/main into main worktree..." - git -C "$main_dir" merge upstream/main - - echo "" - echo "Main synced. Review, then: lb main && git push" -} - -# Aliases keep the short two-letter shortcuts. -alias wt='worktree-list' -alias wts='worktree-sync' -alias wtu='worktree-update-upstream' -alias wtn='worktree-spawn' -alias lbs='lnbits-status' -alias lbsd='lnbits-sync-dev' -alias lbsm='lnbits-sync-main' diff --git a/modules/dev-env/tests/smoke.nix b/modules/dev-env/tests/smoke.nix deleted file mode 100644 index 6c07270..0000000 --- a/modules/dev-env/tests/smoke.nix +++ /dev/null @@ -1,153 +0,0 @@ -# Standalone smoke test for the dev-env module. -# -# Builds a minimal nixosConfiguration that imports the dev-env module -# (plus core.nix for the `lnbits-sensei.*` option namespace and a -# minimum-bootable stub) and exercises a representative slice of the -# option schema: -# -# - worktree-based project with upstream + fork (3 remotes) -# - worktree-based project without upstream (origin only) -# - isClone project with upstream -# - pure-upstream project (origin falls back to upstream) -# - minimal project (mkProject defaults) -# - tmux session schema -# - deploy targets -# -# Consumed by the top-level flake.nix as `checks.${system}.dev-env-smoke` -# so `nix flake check` catches schema regressions without building a -# full system. -# -# This is NOT bootable as a real system — the fileSystem/bootloader -# stubs only exist to satisfy module evaluation. Do not try to deploy. -{ nixpkgs, home-manager }: - -nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - modules = [ - home-manager.nixosModules.home-manager - - # The option namespace (lnbits-sensei.*) and the dev-env module. - ../../core.nix - ../default.nix - - # Minimal host stub + dev-env exercise. - ( - { config, ... }: - { - # ---- Minimum bootable stubs (never actually booted) ---- - - fileSystems."/" = { - device = "none"; - fsType = "tmpfs"; - }; - boot.loader.systemd-boot.enable = true; - boot.loader.efi.canTouchEfiVariables = true; - nixpkgs.hostPlatform = "x86_64-linux"; - system.stateVersion = "24.11"; - - # home-manager wants these set even with zero real users. - home-manager.useGlobalPkgs = true; - home-manager.useUserPackages = true; - - # ---- Exercise the dev-env schema ---- - - lnbits-sensei = { - enable = true; - user = "smoke"; - - devEnv = { - enable = true; - scaffoldPath = "/home/smoke/dev/lnbits-sensei"; - - # Explicit root — don't depend on the user-derived default. - root = "/tmp/dev-env-smoke"; - - github.forkUser = "testuser"; - - deploy.targets = { - host-a = "root@10.0.0.1"; - host-b = "root@10.0.0.2"; - }; - - # Disable regtest to avoid dragging docker into the eval. - regtest.enable = false; - - tmux = { - enable = true; - sessions.dev = { - cwd = "lnbits"; - windows = [ - { - name = "dev"; - cwd = "lnbits/dev"; - cmd = "nvim ."; - } - { - name = "term"; - cwd = "lnbits/dev"; - cmd = null; - } - ]; - }; - }; - - # Use mkProject so we exercise the lib helper in addition to - # the raw submodule schema. - projects = - let - mk = config.lnbits-sensei.devEnv.lib.mkProject; - in - { - # Worktree-based with upstream + fork (3 remotes). - worktree-with-upstream = mk { - name = "worktree-with-upstream"; - category = "shared"; - url = "git@github.com:testuser/worktree-with-upstream.git"; - upstream = "https://github.com/upstream-org/worktree-with-upstream"; - worktrees = { - main.branch = "main"; - dev.branch = "dev"; - feature.branch = "feature/x"; - }; - }; - - # Worktree-based, no upstream (origin only). - worktree-no-upstream = mk { - name = "worktree-no-upstream"; - category = "shared"; - url = "git@github.com:testuser/worktree-no-upstream.git"; - worktrees = { - alpha.branch = "alpha"; - beta.branch = "beta"; - }; - }; - - # Single-clone with upstream (rebase helper target). - clone-with-upstream = mk { - name = "clone-with-upstream"; - category = "test"; - url = "git@github.com:testuser/clone-with-upstream.git"; - upstream = "https://github.com/upstream-org/clone-with-upstream"; - isClone = true; - }; - - # Pure-upstream — origin falls back to upstream. - pure-upstream = mk { - name = "pure-upstream"; - category = "refs"; - upstream = "https://github.com/upstream-org/pure-upstream"; - worktrees.master.branch = "master"; - }; - - # Minimal project — mkProject defaults fill everything. - minimal = mk { - name = "minimal"; - url = "git@github.com:testuser/minimal.git"; - }; - }; - }; - }; - } - ) - ]; -}