feat(dev-env): backport matured dev-env implementation from /etc/nixos

Replace the stub dev-env with the real, working implementation that grew
in the reference machine config — de-identified for the public scaffold.

Nix layer:
- options.nix: full project schema (url/upstream/fork/category/
  worktreeRoot/worktrees{branch,path,remote}/isClone/deployFlakeInput),
  deploy.targets, github.forkUser, writeDirenvHints. Drops the
  forgejo-URL block + deploy-flake auto-derivation (incoherent in a
  scaffold that uses explicit per-project urls).
- lib.nix: mkProject + worktreePath/bareRepoPath/projectRemotes,
  generalized to the explicit-url model (origin falls back to upstream).
- config.nix: renders /etc/dev-env/{config.sh,projects.json,
  tmux-sessions.json}, installs helpers via writeShellScriptBin, loads
  shell functions into interactive shells, wires the git pre-commit hook.

Scripts (config-driven, read /etc/dev-env at runtime):
- bootstrap.sh, nav.sh, worktree.sh, pr-helpers.sh, rebase.sh,
  status.sh, deploy.sh, regtest.sh, tmux-launch.sh.
- Stripped aiolabs/forgejo/bitspire/lamassu/webapp hardcoding; the
  github-fork remote is renamed 'fork' to match git.remotes vocabulary.
- Removes the dev.sh stub (the matured impl uses discrete commands +
  shell functions, not a unified 'dev' CLI).

presets/example.nix: a worked, generic project list replacing the
identity-specific aiolabs preset. tests/smoke.nix + flake checks
exercise the schema; 'nix flake check' is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-15 21:18:49 +02:00
commit e38d313db2
17 changed files with 2925 additions and 147 deletions

View file

@ -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'