feat(dev-env): backport matured dev-env implementation from /etc/nixos
Replace the stub dev-env with the real, working implementation that grew
in the reference machine config — de-identified for the public scaffold.
Nix layer:
- options.nix: full project schema (url/upstream/fork/category/
worktreeRoot/worktrees{branch,path,remote}/isClone/deployFlakeInput),
deploy.targets, github.forkUser, writeDirenvHints. Drops the
forgejo-URL block + deploy-flake auto-derivation (incoherent in a
scaffold that uses explicit per-project urls).
- lib.nix: mkProject + worktreePath/bareRepoPath/projectRemotes,
generalized to the explicit-url model (origin falls back to upstream).
- config.nix: renders /etc/dev-env/{config.sh,projects.json,
tmux-sessions.json}, installs helpers via writeShellScriptBin, loads
shell functions into interactive shells, wires the git pre-commit hook.
Scripts (config-driven, read /etc/dev-env at runtime):
- bootstrap.sh, nav.sh, worktree.sh, pr-helpers.sh, rebase.sh,
status.sh, deploy.sh, regtest.sh, tmux-launch.sh.
- Stripped aiolabs/forgejo/bitspire/lamassu/webapp hardcoding; the
github-fork remote is renamed 'fork' to match git.remotes vocabulary.
- Removes the dev.sh stub (the matured impl uses discrete commands +
shell functions, not a unified 'dev' CLI).
presets/example.nix: a worked, generic project list replacing the
identity-specific aiolabs preset. tests/smoke.nix + flake checks
exercise the schema; 'nix flake check' is green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
773632562e
commit
e38d313db2
17 changed files with 2925 additions and 147 deletions
15
flake.nix
15
flake.nix
|
|
@ -49,8 +49,23 @@
|
||||||
# `settings` (single source of truth — user, host, identity,
|
# `settings` (single source of truth — user, host, identity,
|
||||||
# remote topology) is threaded into every NixOS module and into
|
# remote topology) is threaded into every NixOS module and into
|
||||||
# the home-manager module set, so neither has to re-import it.
|
# 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
|
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 {
|
nixosConfigurations.${settings.hostName} = nixpkgs.lib.nixosSystem {
|
||||||
inherit system;
|
inherit system;
|
||||||
|
|
||||||
|
|
|
||||||
112
modules/dev-env/README.md
Normal file
112
modules/dev-env/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# 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 .#<host>
|
||||||
|
|
||||||
|
# 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 <name> # → ~/dev/shared/extensions/<name>
|
||||||
|
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 <session>` | Launch a declarative tmux session. |
|
||||||
|
| `dev-deploy <host>` | `nixos-rebuild` against your deploy flake. |
|
||||||
|
| `rebase [status\|all\|<path>]` | 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.
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
# lnbits-sensei dev-env — wire-up.
|
# lnbits-sensei dev-env — wire-up.
|
||||||
#
|
#
|
||||||
# Skeleton-only. The substantive pass will:
|
# Nix owns the configuration; bash owns the runtime. This file renders
|
||||||
# - install the regtest.sh / fakewallet.sh wrappers on PATH
|
# the machine-readable config (/etc/dev-env/config.sh + projects.json +
|
||||||
# - render a /etc/dev-env/config.sh consumed by the loose bash
|
# tmux-sessions.json), installs the helper scripts on PATH, loads the
|
||||||
# helpers (worktree nav, upstream-PR helper)
|
# shell-function modules into interactive shells, and wires the shared
|
||||||
# - emit systemd.user units for any long-running pieces
|
# git pre-commit hook. It never materializes repos on disk — that is the
|
||||||
# - hook into config.lnbits-sensei.git.remotes to drive the
|
# job of the user-invoked `dev-env-bootstrap` script.
|
||||||
# bootstrap script's remote reconciliation
|
|
||||||
#
|
|
||||||
# Empty body for now so the module composes cleanly.
|
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
|
@ -20,17 +17,186 @@ let
|
||||||
inherit (lib) mkIf mkMerge;
|
inherit (lib) mkIf mkMerge;
|
||||||
cfg = config.lnbits-sensei.devEnv;
|
cfg = config.lnbits-sensei.devEnv;
|
||||||
user = config.lnbits-sensei.user;
|
user = config.lnbits-sensei.user;
|
||||||
|
helpers = cfg.lib;
|
||||||
|
|
||||||
|
# Resolve a project's complete shape (paths + remotes) once, so the
|
||||||
|
# JSON renderer and the bash scripts both see identical data.
|
||||||
|
resolveProject =
|
||||||
|
name: project:
|
||||||
|
let
|
||||||
|
bare = helpers.bareRepoPath name project;
|
||||||
|
remotes = helpers.projectRemotes project;
|
||||||
|
|
||||||
|
cloneCategoryDir =
|
||||||
|
if project.category != null then "${cfg.root}/${project.category}" else cfg.root;
|
||||||
|
clonePath = "${cloneCategoryDir}/${project.worktreeRoot}";
|
||||||
|
|
||||||
|
resolvedWorktrees = lib.mapAttrs (wtName: wt: {
|
||||||
|
inherit (wt) branch remote;
|
||||||
|
path = helpers.worktreePath name project wtName;
|
||||||
|
}) project.worktrees;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit (project)
|
||||||
|
upstream
|
||||||
|
fork
|
||||||
|
category
|
||||||
|
worktreeRoot
|
||||||
|
isClone
|
||||||
|
deployFlakeInput
|
||||||
|
;
|
||||||
|
barePath = bare;
|
||||||
|
clonePath = clonePath;
|
||||||
|
remotes = remotes;
|
||||||
|
worktrees = resolvedWorktrees;
|
||||||
|
};
|
||||||
|
|
||||||
|
resolvedProjects = lib.mapAttrs resolveProject cfg.projects;
|
||||||
|
|
||||||
|
projectsJson = pkgs.writeText "dev-env-projects.json" (builtins.toJSON resolvedProjects);
|
||||||
|
|
||||||
|
tmuxSessionsJson = pkgs.writeText "dev-env-tmux-sessions.json" (builtins.toJSON cfg.tmux.sessions);
|
||||||
|
|
||||||
|
# Render /etc/dev-env/config.sh — the bash-readable runtime config.
|
||||||
|
# Provides DEV_ROOT, REPOS_DIR, etc. and per-host DEPLOY_TARGET_<HOST>
|
||||||
|
# env vars (the format dev-deploy looks for).
|
||||||
|
renderConfigSh = pkgs.writeText "dev-env-config.sh" ''
|
||||||
|
# Auto-generated by the lnbits-sensei dev-env module — do not edit.
|
||||||
|
# Source from /etc/dev-env/config.sh
|
||||||
|
|
||||||
|
export DEV_ROOT="${cfg.root}"
|
||||||
|
export REPOS_DIR="${cfg.root}/repos"
|
||||||
|
export LNBITS_DIR="${cfg.root}/lnbits"
|
||||||
|
export DEPLOY_DIR="${cfg.root}/deploy"
|
||||||
|
export SHARED_DIR="${cfg.root}/shared"
|
||||||
|
export LOCAL_DIR="${cfg.root}/local"
|
||||||
|
export DOCS_DIR="${cfg.root}/docs"
|
||||||
|
export UPSTREAM_PRS_DIR="${cfg.root}/upstream-prs"
|
||||||
|
|
||||||
|
export GITHUB_SSH="git@github.com"
|
||||||
|
${lib.optionalString (cfg.github.forkUser != null) ''
|
||||||
|
export GITHUB_FORK_USER="${cfg.github.forkUser}"
|
||||||
|
''}
|
||||||
|
|
||||||
|
export DEVENV_PROJECTS_JSON="/etc/dev-env/projects.json"
|
||||||
|
export DEVENV_WRITE_DIRENV_HINTS="${if cfg.writeDirenvHints then "1" else "0"}"
|
||||||
|
${lib.optionalString (cfg.deploy.flakeInput != null) ''
|
||||||
|
export DEVENV_DEPLOY_FLAKE_INPUT="${cfg.deploy.flakeInput}"
|
||||||
|
''}
|
||||||
|
|
||||||
|
# Deploy targets — one env var per host
|
||||||
|
${lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
host: target: ''export DEPLOY_TARGET_${lib.replaceStrings [ "-" ] [ "_" ] host}="${target}"''
|
||||||
|
) cfg.deploy.targets
|
||||||
|
)}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Bash script wrappers — load source verbatim from ./scripts/*.sh.
|
||||||
|
# Using readFile keeps editor tooling/shellcheck working on the .sh
|
||||||
|
# files.
|
||||||
|
mkScriptBin = name: src: pkgs.writeShellScriptBin name (builtins.readFile src);
|
||||||
|
|
||||||
|
# Sourceable bash modules (functions only) loaded by
|
||||||
|
# /etc/profile.d/dev-env-functions.sh into every interactive shell.
|
||||||
|
shellFnSources = [
|
||||||
|
./scripts/nav.sh
|
||||||
|
./scripts/worktree.sh
|
||||||
|
./scripts/pr-helpers.sh
|
||||||
|
]
|
||||||
|
++ lib.optional cfg.regtest.enable ./scripts/regtest.sh;
|
||||||
|
|
||||||
|
shellFnLoader = pkgs.writeText "dev-env-functions.sh" ''
|
||||||
|
# Auto-generated by the lnbits-sensei dev-env module.
|
||||||
|
# Sources every dev-env shell-function module into the current shell.
|
||||||
|
${lib.concatMapStringsSep "\n" (src: ''
|
||||||
|
if [[ -r ${src} ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source ${src}
|
||||||
|
fi
|
||||||
|
'') shellFnSources}
|
||||||
|
'';
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = mkIf cfg.enable (mkMerge [
|
config = mkIf cfg.enable (mkMerge [
|
||||||
# TODO(skeleton): wire scripts, systemd units, and the
|
{
|
||||||
# /etc/dev-env/config.sh render here. See omnixy
|
# 1) /etc/dev-env/* config files (machine-readable)
|
||||||
# modules/dev-env/config.nix for the reference shape.
|
environment.etc = {
|
||||||
|
"dev-env/config.sh".source = renderConfigSh;
|
||||||
|
"dev-env/projects.json".source = projectsJson;
|
||||||
|
"dev-env/tmux-sessions.json".source = tmuxSessionsJson;
|
||||||
|
};
|
||||||
|
|
||||||
# Shared pre-commit hook via core.hooksPath. Installs the
|
# 2) Loader so interactive shells (login OR non-login) get the
|
||||||
# secret-scanner under ~/.local/share/lnbits-sensei/git-hooks/
|
# functions. NixOS only sources /etc/profile.d/*.sh from
|
||||||
# and points the consumer's git config at that directory, so
|
# /etc/profile (login shells); GUI-launched terminals are
|
||||||
# every repo on the machine picks it up automatically.
|
# interactive non-login shells. `interactiveShellInit` is
|
||||||
|
# sourced by both /etc/bashrc and /etc/zshrc on every
|
||||||
|
# interactive shell, which is what we want.
|
||||||
|
environment.etc."profile.d/dev-env-functions.sh".source = shellFnLoader;
|
||||||
|
environment.interactiveShellInit = ''
|
||||||
|
if [[ -r /etc/profile.d/dev-env-functions.sh ]]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /etc/profile.d/dev-env-functions.sh
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
# 3) System packages — every standalone helper.
|
||||||
|
environment.systemPackages = [
|
||||||
|
# core deps used by every script
|
||||||
|
pkgs.git
|
||||||
|
pkgs.jq
|
||||||
|
|
||||||
|
# standalone helpers
|
||||||
|
(mkScriptBin "dev-env-bootstrap" ./scripts/bootstrap.sh)
|
||||||
|
(mkScriptBin "dev-status" ./scripts/status.sh)
|
||||||
|
(mkScriptBin "dev-tm" ./scripts/tmux-launch.sh)
|
||||||
|
(mkScriptBin "dev-deploy" ./scripts/deploy.sh)
|
||||||
|
(mkScriptBin "rebase" ./scripts/rebase.sh)
|
||||||
|
]
|
||||||
|
++ lib.optionals cfg.regtest.enable [
|
||||||
|
(mkScriptBin "regtest-start" (
|
||||||
|
pkgs.writeShellScript "rs" ''
|
||||||
|
source ${./scripts/regtest.sh}
|
||||||
|
regtest-start "$@"
|
||||||
|
''
|
||||||
|
))
|
||||||
|
(mkScriptBin "regtest-stop" (
|
||||||
|
pkgs.writeShellScript "rs2" ''
|
||||||
|
source ${./scripts/regtest.sh}
|
||||||
|
regtest-stop "$@"
|
||||||
|
''
|
||||||
|
))
|
||||||
|
(mkScriptBin "regtest-status" (
|
||||||
|
pkgs.writeShellScript "rs3" ''
|
||||||
|
source ${./scripts/regtest.sh}
|
||||||
|
regtest-status "$@"
|
||||||
|
''
|
||||||
|
))
|
||||||
|
(mkScriptBin "regtest-lnbits-rebuild" (
|
||||||
|
pkgs.writeShellScript "rs5" ''
|
||||||
|
source ${./scripts/regtest.sh}
|
||||||
|
regtest-lnbits-rebuild "$@"
|
||||||
|
''
|
||||||
|
))
|
||||||
|
(mkScriptBin "regtest-lnbits-restart" (
|
||||||
|
pkgs.writeShellScript "rs6" ''
|
||||||
|
source ${./scripts/regtest.sh}
|
||||||
|
regtest-lnbits-restart "$@"
|
||||||
|
''
|
||||||
|
))
|
||||||
|
];
|
||||||
|
|
||||||
|
# 4) tmpfiles to ensure the rebase-log state dir exists. Everything
|
||||||
|
# else is created by dev-env-bootstrap on demand.
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /home/${user}/.local/state/dev-env 0755 ${user} ${user} -"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5) Shared git pre-commit via core.hooksPath, applied per-user via
|
||||||
|
# home-manager so the user's git config picks it up.
|
||||||
(mkIf cfg.gitHooks.enable {
|
(mkIf cfg.gitHooks.enable {
|
||||||
home-manager.users.${user} =
|
home-manager.users.${user} =
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,111 @@
|
||||||
# lnbits-sensei dev-env — helpers.
|
# lnbits-sensei dev-env — helpers.
|
||||||
#
|
#
|
||||||
# Skeleton-only. Place dev-env-internal helpers here (project path
|
# dev-env-internal helpers scoped to this module (project path
|
||||||
# resolution, worktree-path expansion, remote-URL canonicalisation)
|
# resolution, worktree-path expansion, remote construction) rather than
|
||||||
# rather than in the global `lnbits-sensei.lib` so they're scoped to
|
# the global `lnbits-sensei.lib` namespace. Exposed as
|
||||||
# the dev-env module and don't pollute the public helper namespace.
|
# `config.lnbits-sensei.devEnv.lib` so config.nix and the example preset
|
||||||
|
# can build project entries with less boilerplate.
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib) mkOption types;
|
inherit (lib) mkOption types;
|
||||||
|
cfg = config.lnbits-sensei.devEnv;
|
||||||
|
|
||||||
|
# mkProject: shorthand constructor for `devEnv.projects.<name>`.
|
||||||
|
#
|
||||||
|
# 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}/<worktree>; otherwise
|
||||||
|
# ${root}/${worktreeRoot}/<worktree>.
|
||||||
|
worktreePath =
|
||||||
|
_projectName: project: worktreeName:
|
||||||
|
let
|
||||||
|
root = cfg.root;
|
||||||
|
sub =
|
||||||
|
if project.category != null then
|
||||||
|
"${project.category}/${project.worktreeRoot}"
|
||||||
|
else
|
||||||
|
project.worktreeRoot;
|
||||||
|
leaf =
|
||||||
|
let
|
||||||
|
wt = project.worktrees.${worktreeName} or null;
|
||||||
|
in
|
||||||
|
if wt != null && wt.path != null then wt.path else worktreeName;
|
||||||
|
in
|
||||||
|
"${root}/${sub}/${leaf}";
|
||||||
|
|
||||||
|
# Bare repo path (always ${root}/repos/<basename>.git). 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 = {
|
helpers = {
|
||||||
# Resolve a project's on-disk root given a project name.
|
inherit
|
||||||
# projectRoot = name: "${config.lnbits-sensei.devEnv.root}/${name}";
|
mkProject
|
||||||
projectRoot = _name: throw "dev-env.lib.projectRoot: not yet implemented";
|
worktreePath
|
||||||
|
bareRepoPath
|
||||||
# Resolve a worktree path: <root>/<project>/<worktree>.
|
projectRemotes
|
||||||
worktreePath =
|
;
|
||||||
_project: _worktree: throw "dev-env.lib.worktreePath: not yet implemented";
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
# lnbits-sensei dev-env — option schema.
|
# lnbits-sensei dev-env — option schema.
|
||||||
#
|
#
|
||||||
# Skeleton-only. Declares the option surface so consumers can wire
|
# Declares the full option surface for the dev environment: projects
|
||||||
# values today and the substantive implementation can land later
|
# (bare repos + worktrees), the regtest stack, declarative tmux
|
||||||
# without churning the public API.
|
# sessions, deploy targets, and the shared git hooks. Modules in this
|
||||||
|
# directory consume these via `config.lnbits-sensei.devEnv.*`.
|
||||||
|
#
|
||||||
|
# Identity model: this scaffold is git-host-agnostic. A project's
|
||||||
|
# `origin` is the explicit `url` you give it (any forgejo / gitea /
|
||||||
|
# codeberg / github URL) — there is no hardcoded host. The only
|
||||||
|
# github-specific knob is `github.forkUser`, used to derive a personal
|
||||||
|
# fork remote for the upstream-PR workflow.
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib) mkEnableOption mkOption types;
|
inherit (lib) mkEnableOption mkOption types;
|
||||||
|
|
||||||
# One entry in dev-env.projects. A project is a git repo with one or
|
# One entry in devEnv.projects.
|
||||||
# more worktrees (or a single clone if `isClone = true`). Worktrees
|
#
|
||||||
# are derived from a bare repo at `${dev-env.root}/repos/<name>.git`
|
# A project is a git repo with one or more worktrees (or a single
|
||||||
# by default.
|
# clone if `isClone = true`). Worktrees are derived from a bare repo
|
||||||
|
# at `${devEnv.root}/repos/<basename>.git` by default; that path is
|
||||||
|
# materialized by the bootstrap script, never by `nixos-rebuild`.
|
||||||
projectType = types.submodule (
|
projectType = types.submodule (
|
||||||
{ name, ... }:
|
{ name, ... }:
|
||||||
{
|
{
|
||||||
|
|
@ -20,14 +29,59 @@ let
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Origin URL for this project. May be an upstream URL, a
|
Origin remote URL (ssh or https). This is the repo the
|
||||||
personal fork, or a private mirror — the dev-env bootstrap
|
bootstrap script clones/fetches as `origin`. May be null for
|
||||||
script reconciles the rest of the remote topology from
|
a pure-upstream tracking project — in that case `origin`
|
||||||
`lnbits-sensei.git.remotes`.
|
falls back to `upstream`.
|
||||||
|
'';
|
||||||
|
example = "git@git.example.com:you/lnbits.git";
|
||||||
|
};
|
||||||
|
|
||||||
|
upstream = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
HTTPS URL of the upstream OSS repo. Set to null for projects
|
||||||
|
with no upstream (divergent branches, original work). When
|
||||||
|
set, the `upstream` remote is added to the bare repo and the
|
||||||
|
`prb`/`rebase` helpers know how to sync from it.
|
||||||
'';
|
'';
|
||||||
example = "https://github.com/lnbits/lnbits";
|
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 {
|
worktrees = mkOption {
|
||||||
type = types.attrsOf (
|
type = types.attrsOf (
|
||||||
types.submodule {
|
types.submodule {
|
||||||
|
|
@ -36,14 +90,31 @@ let
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "Branch to check out in this worktree.";
|
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 = { };
|
default = { };
|
||||||
description = ''
|
description = ''
|
||||||
Worktrees to materialize for this project, keyed by
|
Worktrees to materialize for this project, keyed by worktree
|
||||||
worktree name. Each becomes a directory under the project
|
name. Each becomes a directory at
|
||||||
root.
|
`''${devEnv.root}/''${category}/''${worktreeRoot}/<key>` (or
|
||||||
|
under root if category is null).
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
{
|
{
|
||||||
|
|
@ -54,22 +125,36 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
isClone = mkEnableOption ''
|
isClone = mkEnableOption ''
|
||||||
Treat as a plain clone rather than a bare-repo + worktrees
|
Treat this project as a regular clone rather than a bare-repo
|
||||||
set. Use for projects you won't have multiple simultaneous
|
worktree set. Use for projects you won't have multiple
|
||||||
branches checked out for
|
simultaneous branches checked out for
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
deployFlakeInput = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Name of the flake input in your deploy flake that this
|
||||||
|
project corresponds to. Used by `dev-deploy --local` to know
|
||||||
|
which `--override-input` to pass so a deploy builds against
|
||||||
|
your in-progress worktree.
|
||||||
|
'';
|
||||||
|
example = "lnbits";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
# One entry in dev-env.tmux.sessions. Mirrors the omnixy shape so
|
# One entry in devEnv.tmux.sessions.
|
||||||
# the `dev-tm <name>` launcher can ship later with a familiar API.
|
|
||||||
tmuxSessionType = types.submodule {
|
tmuxSessionType = types.submodule {
|
||||||
options = {
|
options = {
|
||||||
cwd = mkOption {
|
cwd = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Session-default cwd, relative to dev-env.root.";
|
description = ''
|
||||||
|
Session-default cwd, relative to `devEnv.root`. Windows can
|
||||||
|
override with their own cwd.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
windows = mkOption {
|
windows = mkOption {
|
||||||
type = types.listOf (
|
type = types.listOf (
|
||||||
|
|
@ -79,6 +164,7 @@ let
|
||||||
cwd = mkOption {
|
cwd = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
description = "Window cwd, relative to devEnv.root.";
|
||||||
};
|
};
|
||||||
cmd = mkOption {
|
cmd = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
|
|
@ -101,9 +187,9 @@ in
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = ''
|
description = ''
|
||||||
Absolute path to your lnbits-sensei checkout on this machine.
|
Absolute path to your lnbits-sensei checkout on this machine.
|
||||||
Used to source the seedable CLAUDE.md files (and, later, the
|
Used to source the seedable CLAUDE.md files via
|
||||||
dev-env scripts) via `mkOutOfStoreSymlink` so edits in your
|
`mkOutOfStoreSymlink` so edits in your checkout take effect
|
||||||
checkout take effect without a rebuild.
|
without a rebuild.
|
||||||
|
|
||||||
Required when any `claude.*` integration is enabled. Type is
|
Required when any `claude.*` integration is enabled. Type is
|
||||||
`str` (not `path`) intentionally — `path` would copy the file
|
`str` (not `path`) intentionally — `path` would copy the file
|
||||||
|
|
@ -117,22 +203,114 @@ in
|
||||||
default = "/home/${config.lnbits-sensei.user or "user"}/dev";
|
default = "/home/${config.lnbits-sensei.user or "user"}/dev";
|
||||||
defaultText = "/home/\${config.lnbits-sensei.user}/dev";
|
defaultText = "/home/\${config.lnbits-sensei.user}/dev";
|
||||||
description = ''
|
description = ''
|
||||||
Root directory for the dev environment. Worktrees and project
|
Root directory for the dev environment. Bare repos live under
|
||||||
clones live under this prefix; bare repos under
|
`''${root}/repos/`, worktrees under
|
||||||
`''${root}/repos/`.
|
`''${root}/<category>/<project>/`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
gitHooks = {
|
github = {
|
||||||
|
forkUser = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Personal github username used for upstream PR forks (the
|
||||||
|
`fork` remote). When set, projects that declare an `upstream`
|
||||||
|
but no explicit `fork` automatically get a fork remote derived
|
||||||
|
from `git@github.com:<forkUser>/<basename>.git`.
|
||||||
|
'';
|
||||||
|
example = "octocat";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
projects = mkOption {
|
||||||
|
type = types.attrsOf projectType;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Hand-authored project list. Consumed by `dev-env-bootstrap` to
|
||||||
|
materialize bare repos + worktrees on disk. Use the `devEnv.lib.
|
||||||
|
mkProject` helper to cut boilerplate, or write raw submodule
|
||||||
|
attrs. See `presets/example.nix` for a worked example.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
deploy = {
|
||||||
|
flakeInput = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Sub-directory under `''${root}/deploy/` holding your unified
|
||||||
|
deploy flake working copy. `dev-deploy` uses it as the
|
||||||
|
`--flake` source. null means `''${root}/deploy` itself.
|
||||||
|
'';
|
||||||
|
example = "unified";
|
||||||
|
};
|
||||||
|
|
||||||
|
targets = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Map from hostname to SSH target used by `dev-deploy`. Each
|
||||||
|
entry is rendered to a `DEPLOY_TARGET_<HOST>` env var in
|
||||||
|
/etc/dev-env/config.sh.
|
||||||
|
'';
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
prod = "root@prod-host";
|
||||||
|
staging = "root@staging-host";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
regtest = {
|
||||||
enable = mkEnableOption ''
|
enable = mkEnableOption ''
|
||||||
|
Bitcoin/Lightning regtest docker stack. Wraps an upstream fork
|
||||||
|
of `lnbits/legend-regtest-enviroment` (LND + CLN + Eclair +
|
||||||
|
bitcoind + electrs). Installs `regtest-*` helpers. Requires a
|
||||||
|
container engine (docker) — install it separately; this flag
|
||||||
|
does not pull docker in for you
|
||||||
|
'';
|
||||||
|
|
||||||
|
repoUrl = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "https://github.com/lnbits/legend-regtest-enviroment";
|
||||||
|
description = ''
|
||||||
|
Git URL of the regtest docker-compose repo. Cloned to
|
||||||
|
`''${root}/local/docker/regtest` by the bootstrap script.
|
||||||
|
Point at your own fork if you've customised the stack.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
tmux = {
|
||||||
|
enable = mkEnableOption "declarative tmux session launcher (dev-tm)";
|
||||||
|
|
||||||
|
sessions = mkOption {
|
||||||
|
type = types.attrsOf tmuxSessionType;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Named tmux session layouts. The `dev-tm <name>` launcher reads
|
||||||
|
these from /etc/dev-env/tmux-sessions.json at runtime and
|
||||||
|
recreates the session.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
gitHooks = {
|
||||||
|
enable =
|
||||||
|
mkEnableOption ''
|
||||||
Shared pre-commit hook via `core.hooksPath`. Installs a single
|
Shared pre-commit hook via `core.hooksPath`. Installs a single
|
||||||
secret-scanner hook under
|
secret-scanner hook under
|
||||||
`~/.local/share/lnbits-sensei/git-hooks/pre-commit` and points
|
`~/.local/share/lnbits-sensei/git-hooks/pre-commit` and points
|
||||||
the consumer's git config there — so every repo on the machine
|
the consumer's git config there — so every repo on the machine
|
||||||
picks it up without per-repo wiring. Refuses to commit obvious
|
picks it up without per-repo wiring. Refuses to commit obvious
|
||||||
secrets and unencrypted sops files; false positives are handled
|
secrets and unencrypted sops files; false positives are
|
||||||
via `# pragma: allowlist secret` markers
|
handled via `# pragma: allowlist secret` markers
|
||||||
'';
|
''
|
||||||
|
// {
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
claude = {
|
claude = {
|
||||||
|
|
@ -152,46 +330,14 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
projects = mkOption {
|
writeDirenvHints = mkOption {
|
||||||
type = types.attrsOf projectType;
|
type = types.bool;
|
||||||
default = { };
|
default = true;
|
||||||
description = ''
|
description = ''
|
||||||
Hand-authored project list. Consumed by the bootstrap script
|
When true, the bootstrap script writes a default `.envrc`
|
||||||
(later pass) to materialize repos and worktrees on disk.
|
containing `use flake` into each worktree that has a flake.nix
|
||||||
|
but no existing .envrc. Never clobbers existing files.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
regtest = {
|
|
||||||
enable = mkEnableOption ''
|
|
||||||
Bitcoin/Lightning regtest docker stack. Wraps an upstream
|
|
||||||
fork of `lnbits/legend-regtest-enviroment` (LND + CLN +
|
|
||||||
Eclair + bitcoind + electrs). Brought up via
|
|
||||||
`dev up --regtest`. Implies a container engine — the
|
|
||||||
substantive pass will gate this on a containers feature
|
|
||||||
'';
|
|
||||||
|
|
||||||
repoUrl = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "https://github.com/lnbits/legend-regtest-enviroment";
|
|
||||||
description = ''
|
|
||||||
Git URL of the regtest docker-compose repo. Cloned by the
|
|
||||||
dev-env bootstrap script. Point at your own fork if you've
|
|
||||||
customised the stack.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
tmux = {
|
|
||||||
enable = mkEnableOption "declarative tmux session launcher";
|
|
||||||
|
|
||||||
sessions = mkOption {
|
|
||||||
type = types.attrsOf tmuxSessionType;
|
|
||||||
default = { };
|
|
||||||
description = ''
|
|
||||||
Named tmux session layouts. The launcher script (later pass)
|
|
||||||
reads these at runtime and recreates the session.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
104
modules/dev-env/presets/example.nix
Normal file
104
modules/dev-env/presets/example.nix
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# 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";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
290
modules/dev-env/scripts/bootstrap.sh
Normal file
290
modules/dev-env/scripts/bootstrap.sh
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
#!/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}/<name>.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 <<EOF
|
||||||
|
dev-env-bootstrap — materialize bare repos + worktrees from /etc/dev-env/projects.json
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
dev-env-bootstrap [OPTIONS] [project-name]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-n, --dry-run show what would happen, do not change anything
|
||||||
|
-v, --verbose print every git command
|
||||||
|
-f, --force-remotes silently rewrite remote URLs that differ from spec
|
||||||
|
-h, --help show this help
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
dev-env-bootstrap --dry-run # full preview
|
||||||
|
dev-env-bootstrap # bring everything up to spec
|
||||||
|
dev-env-bootstrap lnbits # only operate on the lnbits project
|
||||||
|
|
||||||
|
Reads from: $PROJECTS_JSON
|
||||||
|
Writes to: $DEV_ROOT
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-n|--dry-run) DRY_RUN=true ;;
|
||||||
|
-v|--verbose) VERBOSE=true ;;
|
||||||
|
-f|--force-remotes) FORCE_REMOTES=true ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
-*) echo "unknown option: $1"; usage; exit 1 ;;
|
||||||
|
*) ONLY_PROJECT="$1" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Output -----------------------------------------------------------
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}[..]${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[XX]${NC} $*" >&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-name<TAB>url" 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 "wtname<TAB>branch<TAB>path<TAB>remote" 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
|
||||||
217
modules/dev-env/scripts/deploy.sh
Normal file
217
modules/dev-env/scripts/deploy.sh
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# dev-deploy: thin wrapper around your unified deploy flake
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# dev-deploy <host> switch on host (uses locked deploy flake input)
|
||||||
|
# dev-deploy <host> test test build (no switch)
|
||||||
|
# dev-deploy <host> build local build only
|
||||||
|
# dev-deploy --local <host> 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[/<flakeInput>].
|
||||||
|
|
||||||
|
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 <<EOF
|
||||||
|
dev-deploy — wraps nixos-rebuild against your unified deploy flake
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
dev-deploy <host> deploy (switch)
|
||||||
|
dev-deploy <host> test test build (no switch)
|
||||||
|
dev-deploy <host> build local build only (no target)
|
||||||
|
dev-deploy --local <host> deploy with --override-input from local worktrees
|
||||||
|
dev-deploy --local <host> 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_<HOST>=<ssh> (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 <input> path:<worktree-path>
|
||||||
|
# Picks the first worktree by default; pass DEVENV_OVERRIDE_WORKTREE=<name>
|
||||||
|
# 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
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
#!/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
|
|
||||||
168
modules/dev-env/scripts/nav.sh
Normal file
168
modules/dev-env/scripts/nav.sh
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
#!/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 <worktree>"
|
||||||
|
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 <category> [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 <category> [repo] [worktree]
|
||||||
|
g() {
|
||||||
|
_devenv_load_config
|
||||||
|
local category="${1:-}"
|
||||||
|
if [[ -z "$category" ]]; then
|
||||||
|
echo "Usage: g <category> [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 <host>"
|
||||||
|
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
|
||||||
|
}
|
||||||
144
modules/dev-env/scripts/pr-helpers.sh
Normal file
144
modules/dev-env/scripts/pr-helpers.sh
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
#!/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}/<repo>-<branch> 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}/<repo>-<branch>.
|
||||||
|
#
|
||||||
|
# Usage: git-pr-branch <repo-name> <branch-name>
|
||||||
|
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 <repo-name> <branch-name>"
|
||||||
|
echo "Example: git-pr-branch lnbits fix-invoice-bug"
|
||||||
|
echo ""
|
||||||
|
echo "Creates a worktree at $prs_dir/<repo>-<branch>"
|
||||||
|
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:-<user>}/${repo_name}.git"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Ready! Your PR worktree is at:
|
||||||
|
cd $pr_path
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. cd $pr_path
|
||||||
|
2. Edit, test, commit
|
||||||
|
3. git push fork $branch_name
|
||||||
|
4. Open the PR on GitHub (against upstream/$base_branch)
|
||||||
|
5. After merge: git-pr-cleanup $repo_name $branch_name
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove a PR worktree after the PR is merged (or abandoned).
|
||||||
|
git-pr-cleanup() {
|
||||||
|
_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-cleanup <repo-name> <branch-name>"
|
||||||
|
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'
|
||||||
409
modules/dev-env/scripts/rebase.sh
Normal file
409
modules/dev-env/scripts/rebase.sh
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
#!/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 <<EOF
|
||||||
|
|
||||||
|
${BOLD}${YELLOW}Conflict Resolution Guide:${NC}
|
||||||
|
1. Check conflicts: ${CYAN}git status${NC}
|
||||||
|
2. Resolve markers <<<<<<< ======= >>>>>>>
|
||||||
|
3. Stage: ${CYAN}git add <file>${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 "<path>:<display-name>" 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 <<EOF
|
||||||
|
${BOLD}rebase${NC} — Safely rebase forks onto upstream
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
rebase Interactive mode
|
||||||
|
rebase single <path> 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 <name> 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 "$@"
|
||||||
267
modules/dev-env/scripts/regtest.sh
Normal file
267
modules/dev-env/scripts/regtest.sh
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# dev-env: Bitcoin/Lightning regtest docker environment
|
||||||
|
#
|
||||||
|
# Provides:
|
||||||
|
# regtest-start [worktree|pr:<branch>] [--path <dir>] [--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:<branch>] [--path <dir>] [--seed|--keep]
|
||||||
|
#
|
||||||
|
# worktree an lnbits worktree name (under ~/dev/lnbits/<name>)
|
||||||
|
# pr:<branch> ~/dev/upstream-prs/lnbits-<branch>
|
||||||
|
# --path <dir> 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 <<USAGE
|
||||||
|
Unknown argument: $1
|
||||||
|
Usage: regtest-start [worktree|pr:<branch>] [--path <dir>] [--seed|--keep]
|
||||||
|
|
||||||
|
worktree an lnbits worktree name (under ~/dev/lnbits/<name>)
|
||||||
|
pr:<branch> ~/dev/upstream-prs/lnbits-<branch>
|
||||||
|
--path <dir> 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 <<EOF
|
||||||
|
|
||||||
|
Regtest running:
|
||||||
|
LNbits: http://localhost:5001/
|
||||||
|
Mempool: http://localhost:8080/
|
||||||
|
Boltz: http://localhost:9001/
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Commands: regtest-stop, regtest-logs, regtest-cli, regtest-status"
|
||||||
|
}
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Stop
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
regtest-stop() {
|
||||||
|
_devenv_load_config
|
||||||
|
|
||||||
|
if [[ ! -d "$REGTEST_DIR" ]]; then
|
||||||
|
echo "Regtest environment not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Stopping regtest environment..."
|
||||||
|
(cd "$REGTEST_DIR"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source ./docker-scripts.sh
|
||||||
|
docker compose down -v)
|
||||||
|
echo "Regtest stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Status / logs / CLI
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
regtest-status() {
|
||||||
|
_devenv_load_config
|
||||||
|
|
||||||
|
echo "=== Regtest Environment ==="
|
||||||
|
if _regtest_is_running; then
|
||||||
|
echo "Status: RUNNING"
|
||||||
|
echo ""
|
||||||
|
docker ps --filter "name=lnbits-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | head -15
|
||||||
|
|
||||||
|
local balance
|
||||||
|
balance="$(docker exec "$REGTEST_LND" lncli --network=regtest walletbalance 2>/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 <<EOF
|
||||||
|
|
||||||
|
Available commands:
|
||||||
|
bitcoin-cli-sim Bitcoin Core CLI
|
||||||
|
lncli-sim <1-4> 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
|
||||||
|
}
|
||||||
145
modules/dev-env/scripts/status.sh
Normal file
145
modules/dev-env/scripts/status.sh
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
#!/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 <<EOF
|
||||||
|
|
||||||
|
${BLUE}Legend:${NC}
|
||||||
|
${GREEN}✓${NC} clean ${YELLOW}●${NC} dirty ${GREEN}↑${NC} ahead origin
|
||||||
|
${RED}↓${NC} behind origin ${CYAN}⇣${NC} behind upstream
|
||||||
|
|
||||||
|
${BLUE}Quick actions:${NC}
|
||||||
|
wts sync all worktrees with origin
|
||||||
|
wtu <repo> fetch upstream + show divergence
|
||||||
|
rebase status which forks need rebasing onto upstream
|
||||||
|
dev-env-bootstrap materialize missing worktrees
|
||||||
|
|
||||||
|
EOF
|
||||||
131
modules/dev-env/scripts/tmux-launch.sh
Normal file
131
modules/dev-env/scripts/tmux-launch.sh
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
#!/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 <<EOF
|
||||||
|
dev-tm — declarative tmux session launcher
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
dev-tm list available sessions
|
||||||
|
dev-tm <session> start or attach to <session>
|
||||||
|
dev-tm -k <session> 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 <session|all>"; 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
|
||||||
275
modules/dev-env/scripts/worktree.sh
Normal file
275
modules/dev-env/scripts/worktree.sh
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
#!/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: <path> <sha> [<branch>]
|
||||||
|
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 <repo-name>"
|
||||||
|
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 <repo> <new-branch> [base-branch]
|
||||||
|
#
|
||||||
|
# Locates the bare repo, creates <new-branch> from <base-branch> (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 <repo-name> <new-branch-name> [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/<branch>` 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'
|
||||||
153
modules/dev-env/tests/smoke.nix
Normal file
153
modules/dev-env/tests/smoke.nix
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
# 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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in a new issue