From 30c72976c1ac89e09788e799dac3aa4571871203 Mon Sep 17 00:00:00 2001 From: satyaborg Date: Sat, 30 May 2026 10:46:01 +1000 Subject: [PATCH] feat: add release versioning --- AGENTS.md | 5 +- CHANGELOG.md | 56 ++++++++++++++++ README.md | 29 +++++++- VERSION | 1 + cliff.toml | 36 ++++++++++ devloop | 25 ++++++- release.sh | 151 ++++++++++++++++++++++++++++++++++++++++++ tests/devloop_test.sh | 28 +++++++- 8 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 VERSION create mode 100644 cliff.toml create mode 100755 release.sh diff --git a/AGENTS.md b/AGENTS.md index c54c24d..d18eacc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,14 @@ ## Project Structure & Module Organization -This is a Bash CLI project. The active runtime is the root `devloop` executable. `install.sh` links it into a local bin directory and installs bundled skills into `~/.agents/skills`, `tests/devloop_test.sh` covers the shell runtime, `skills/devloop-spec/SKILL.md` is the spec-generation skill, `skills/devloop-review/SKILL.md` is the review skill, and `skills/devloop-spec/references/spec-template.md` is the starter spec. Generated runtime output belongs under `.codex/` in target repositories and should not be committed here. +This is a Bash CLI project. The active runtime is the root `devloop` executable. `VERSION` is the single version source, `release.sh` cuts local release commits and annotated tags, `install.sh` links the CLI into a local bin directory and installs bundled skills into `~/.agents/skills` and `~/.claude/skills`, `tests/devloop_test.sh` covers the shell runtime, `skills/devloop-spec/SKILL.md` is the spec-generation skill, `skills/devloop-review/SKILL.md` is the review skill, and `skills/devloop-spec/references/spec-template.md` is the starter spec. Generated runtime output belongs under `.codex/` in target repositories and should not be committed here. ## Build, Test, and Development Commands - `bash tests/devloop_test.sh`: run the shell test suite. - `./install.sh`: link `devloop` into `~/.local/bin` or `DEVLOOP_BIN_DIR`. - `./devloop --plain .specs/change.md`: example local CLI invocation from a target git worktree. +- `./release.sh 0.1.0 --dry-run`: validate the release path without changing files. ## Coding Style & Naming Conventions @@ -24,4 +25,4 @@ Git history follows Conventional Commits, for example `fix: surface devloop comm ## Agent-Specific Instructions -Keep changes narrow and regression-first. Do not commit `.codex/`, coverage artifacts, local specs, or dependency caches unless explicitly requested. When modifying acceptance or reporting behavior, update both tests and `README.md` examples if user-facing output changes. +Keep changes narrow and regression-first. Do not commit `.codex/`, coverage artifacts, local specs, or dependency caches unless explicitly requested. When modifying acceptance, reporting, versioning, or release behavior, update tests and `README.md` examples if user-facing output changes. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f6a76b2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.0](https://github.com/satyaborg/devloop/releases/tag/v0.1.0) - 2026-05-30 + +### Added + +- initial implementation ([96901ee](https://github.com/satyaborg/devloop/commit/96901ee67cbe8e01e4fe25cea9807d100dc94f59)) +- reuse agent sessions in devloop ([6cf72c5](https://github.com/satyaborg/devloop/commit/6cf72c59df7acf8ae95e66e882e7e05898365abd)) +- port devloop to bun opentui ([38cf9bd](https://github.com/satyaborg/devloop/commit/38cf9bde799ec7e8c5b47b261e0ff986a14dd67f)) +- show default cli welcome ([4e55181](https://github.com/satyaborg/devloop/commit/4e551816a2446ac041100b22c988eaa0e3e5a398)) +- run devloop in isolated worktrees ([17b7fd5](https://github.com/satyaborg/devloop/commit/17b7fd5ac76ee90feb601efd11ac4b902bd73c99)) +- derive semantic work names ([0bb4bf0](https://github.com/satyaborg/devloop/commit/0bb4bf0411c854f872038c0ca22826582dd65b40)) +- show tui step progress ([fcfe52a](https://github.com/satyaborg/devloop/commit/fcfe52af09db5c6b6d3dbecb3e170a9849a860ac)) +- bundle spec generation skill ([a4ae43d](https://github.com/satyaborg/devloop/commit/a4ae43d45cb9e8e3cbfb01ebde03e94bf67fd3a2)) +- configure coder and reviewer agents ([f00c2ae](https://github.com/satyaborg/devloop/commit/f00c2aee76b55f15a98c806e20a527c0062420f5)) +- require evidence-rich review matrices ([1f31f70](https://github.com/satyaborg/devloop/commit/1f31f7077da79fcca2ec1219252c3d0e7ce37373)) +- automate pass commits and prs ([4c222a7](https://github.com/satyaborg/devloop/commit/4c222a72eaa73535777ed0dfbe789902f704589b)) +- npm-release-readiness ([057aef0](https://github.com/satyaborg/devloop/commit/057aef0fc8e715952c394af4be8ba3fc6ba59b1d)) +- force maximum agent effort ([51af634](https://github.com/satyaborg/devloop/commit/51af634d573774b6a88d722927f3460c4081d854)) +- add bash devloop runtime ([f6611e0](https://github.com/satyaborg/devloop/commit/f6611e00c480058014b73708ab7bfdae6eefc5cc)) +- add adversarial review gates ([5143efd](https://github.com/satyaborg/devloop/commit/5143efd46713e0b95b6e550d9e702fb2a814fba5)) +- overhaul interactive cli ui ([3d0f033](https://github.com/satyaborg/devloop/commit/3d0f0332592b1a68a4e3606249ca82013b7ba506)) +- configure spec directories ([c8a7cbd](https://github.com/satyaborg/devloop/commit/c8a7cbdc1ee272f8baae9e146a180d271d55de94)) +- add scoped devloop config ([00868e6](https://github.com/satyaborg/devloop/commit/00868e6532e87e1663340c187bee34872a2db38e)) +- simplify spec path settings ([1e1213c](https://github.com/satyaborg/devloop/commit/1e1213cafabe8f984e45ee8212e3585bd768d15f)) +- polish interactive tui ([a4485ae](https://github.com/satyaborg/devloop/commit/a4485ae3022a827275bed4870128a6ebeda2ff2c)) + + +### Breaking changes + +- install skills for codex and claude ([eb2cb6d](https://github.com/satyaborg/devloop/commit/eb2cb6de9f988ea2acac988f7f1fb782a25a39ba)) + + +### Documentation + +- add repository guidelines ([447dd0c](https://github.com/satyaborg/devloop/commit/447dd0ce63264b1fc9cc124eaa975fa179ee3e76)) +- link claude guide to agents ([70be06e](https://github.com/satyaborg/devloop/commit/70be06eebb326baaa54ff5eb4d8c6199f074d029)) +- clarify worktree cleanup ([3dc7b1b](https://github.com/satyaborg/devloop/commit/3dc7b1b239684e17683c04f8e0f3ba15c2d16b0e)) +- add license section ([ac82872](https://github.com/satyaborg/devloop/commit/ac8287290a2f7dae956248fc46411e9ac9cacf8b)) + + +### Fixed + +- surface devloop commit failures ([493031a](https://github.com/satyaborg/devloop/commit/493031ab90ba593ad2bc904ec3d8c0ba1fd92b96)) +- clarify worktree outputs ([6e1067a](https://github.com/satyaborg/devloop/commit/6e1067acf5cf0298a3e3c825176f190cbb346bbb)) +- clean up naming temp logs ([424a502](https://github.com/satyaborg/devloop/commit/424a502fe78441e6b9e9877f719bc876acbd2210)) +- align tui navigation labels ([3f74b90](https://github.com/satyaborg/devloop/commit/3f74b90b0d87294c5224f33888f6c05346ba1397)) +- derive report subtitles from specs ([1984ca6](https://github.com/satyaborg/devloop/commit/1984ca6b8e82dfdd3689df864fc1108d38ec2517)) +- add topical report haiku ([2cd843d](https://github.com/satyaborg/devloop/commit/2cd843df292217f676e59b3631d7f6d922d3a1db)) +- key sessions by configured agent ([16ef419](https://github.com/satyaborg/devloop/commit/16ef419de2429bc4569fc7789fb4fb1998aefc0f)) +- align release readiness tests ([8d9f0bf](https://github.com/satyaborg/devloop/commit/8d9f0bf14d5ca4be0096e01cca8ca19d140f270b)) +- harden bash runtime helpers ([7fb28cc](https://github.com/satyaborg/devloop/commit/7fb28cc7f7da3788d7ef3a233242465ad4a8e2c9)) +- preserve interactive run status ([48cadbb](https://github.com/satyaborg/devloop/commit/48cadbb140662919f6ae8ce6597274cf755d13a1)) + diff --git a/README.md b/README.md index 7b86d0a..d9e3a58 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ cd devloop ./install.sh ``` +Check the installed version: + +```sh +devloop --version +``` + Run without installing: ```sh @@ -104,6 +110,7 @@ devloop [options] [max=5] | `--no-strict` | Weaken strict review gates | | `--no-shell`, `--stay` | Do not open a shell in the generated worktree after completion | | `--shell`, `--enter-worktree` | Open a shell in the generated worktree after completion | +| `-V`, `--version` | Show version | ## Interactive UI @@ -137,8 +144,9 @@ Nested menu screens keep `Back` as the final option so you can return to the pre ## Development ```sh -bash -n devloop install.sh +bash -n devloop install.sh release.sh skill_helpers.sh ./devloop --help +./devloop --version tmp="$(mktemp -d)" DEVLOOP_BIN_DIR="$tmp/bin" HOME="$tmp/home" ./install.sh PATH="$tmp/bin:$PATH" HOME="$tmp/home" devloop doctor @@ -146,6 +154,25 @@ PATH="$tmp/bin:$PATH" HOME="$tmp/home" devloop doctor The supported runtime is the root [`devloop`](devloop) Bash script. +## Versioning and Release + +`devloop` uses [Semantic Versioning](https://semver.org/) and stores the current version in [`VERSION`](VERSION). `0.x` releases are initial public API releases, so breaking changes can happen between minor versions. + +Release notes in [`CHANGELOG.md`](CHANGELOG.md) are generated from Conventional Commit history with [`git-cliff`](https://git-cliff.org/). Install it before cutting a real release: + +```sh +brew install git-cliff +``` + +Cut a release from a clean tree: + +```sh +./release.sh 0.1.0 --dry-run +./release.sh 0.1.0 +``` + +That updates `VERSION` and [`CHANGELOG.md`](CHANGELOG.md), runs `bash tests/devloop_test.sh`, commits `chore: release 0.1.0`, and creates an annotated `v0.1.0` tag. Add `--push` to push the release branch and tag. By default, pushed releases must run from `main`. + ## License MIT. See [LICENSE](LICENSE). diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..106aefd --- /dev/null +++ b/cliff.toml @@ -0,0 +1,36 @@ +[changelog] +header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n" +body = """ +{% if version -%} +## [{{ version | trim_start_matches(pat="v") }}](https://github.com/satyaborg/devloop/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} +## [Unreleased] +{% endif -%} +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} +{% for commit in commits %} +- {{ commit.message | split(pat="\n") | first | trim }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/satyaborg/devloop/commit/{{ commit.id }})) +{%- endfor %} + +{% endfor %} +""" +trim = true + +[git] +conventional_commits = true +filter_unconventional = false +split_commits = false +commit_parsers = [ + { message = "^chore: release", skip = true }, + { message = "^Merge pull request", skip = true }, + { message = "^.+!:", group = "Breaking changes" }, + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^docs", group = "Documentation" }, + { message = "^test", skip = true }, + { message = "^chore", skip = true }, + { message = "^refactor", skip = true }, + { message = "^.*", skip = true }, +] +tag_pattern = "v[0-9].*" +sort_commits = "oldest" diff --git a/devloop b/devloop index 67914a6..796945c 100755 --- a/devloop +++ b/devloop @@ -17,6 +17,8 @@ while [ -L "$SCRIPT_PATH" ]; do done ROOT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)" source "$ROOT_DIR/skill_helpers.sh" +DEVLOOP_VERSION="$(sed -n '1p' "$ROOT_DIR/VERSION" 2>/dev/null || true)" +if [ -z "$DEVLOOP_VERSION" ]; then DEVLOOP_VERSION="0.0.0-dev"; fi USE_TUI=false if [ -t 1 ]; then USE_TUI=true; fi @@ -67,6 +69,11 @@ main() { if has_arg "--plain" "$@"; then USE_TUI=false; fi if has_arg "--tui" "$@"; then USE_TUI=true; fi + if version_args_only "$@"; then + printf 'devloop %s\n' "$DEVLOOP_VERSION" + return 0 + fi + if [ "${1:-}" = "spec" ]; then shift spec_command "$@" @@ -127,6 +134,7 @@ devloop_logo() { ░█░█░█▀▀░▀▄▀░█░░░█░█░█░█░█▀▀ ░▀▀░░▀▀▀░░▀░░▀▀▀░▀▀▀░▀▀▀░▀░░ EOF + printf 'v%s\n' "$DEVLOOP_VERSION" } welcome_plain() { @@ -162,6 +170,7 @@ Options: --create-pr, --pr push accepted branch and open a PR --no-shell, --stay do not open a shell in the run worktree after completion --shell, --enter-worktree open a shell in the run worktree after completion + -V, --version show version -h, --help show this screen EOF } @@ -192,11 +201,12 @@ welcome_tui() { printf ' %-30s %s\n' "--create-pr, --pr" "push accepted branch and open a PR" printf ' %-30s %s\n' "--no-shell, --stay" "do not open a shell after completion" printf ' %-30s %s\n' "--shell, --enter-worktree" "open a shell after completion" + printf ' %-30s %s\n' "-V, --version" "show version" printf ' %-30s %s\n' "-h, --help" "show this screen" } usage() { - printf '%s\n' "usage: devloop [--plain|--tui] [--in-place] [--no-strict] [--create-pr|--pr] [--no-shell|--stay|--shell|--enter-worktree] [--coder codex|claude] [--reviewer codex|claude] [--report-format html|markdown] [max=5]" + printf '%s\n' "usage: devloop [--version] [--plain|--tui] [--in-place] [--no-strict] [--create-pr|--pr] [--no-shell|--stay|--shell|--enter-worktree] [--coder codex|claude] [--reviewer codex|claude] [--report-format html|markdown] [max=5]" printf '%s\n' "agents: Codex or Claude Code" } @@ -222,6 +232,19 @@ has_arg() { return 1 } +version_args_only() { + local item saw_version=false + [ "$#" -gt 0 ] || return 1 + for item in "$@"; do + case "$item" in + --version|-V) saw_version=true ;; + --plain|--tui) ;; + *) return 1 ;; + esac + done + [ "$saw_version" = true ] +} + trim_string() { printf '%s\n' "$1" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' } diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..f54c256 --- /dev/null +++ b/release.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_PATH="${BASH_SOURCE[0]}" +while [ -L "$SCRIPT_PATH" ]; do + SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)" + SCRIPT_TARGET="$(readlink "$SCRIPT_PATH")" + case "$SCRIPT_TARGET" in + /*) SCRIPT_PATH="$SCRIPT_TARGET" ;; + *) SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_TARGET" ;; + esac +done + +ROOT="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)" + +release_usage() { + cat <<'EOF' +usage: ./release.sh [--dry-run] [--push] + +Creates a release commit and annotated tag from a SemVer version. + +Examples: + ./release.sh 0.1.0 --dry-run + ./release.sh 0.1.0 + ./release.sh 0.1.0 --push +EOF +} + +release_version_valid() { + local version="$1" + printf '%s\n' "$version" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$' +} + +release_tag_for_version() { + printf 'v%s\n' "$1" +} + +release_require_command() { + local command="$1" + if command -v "$command" >/dev/null 2>&1; then return 0; fi + printf 'missing required command: %s\n' "$command" >&2 + return 1 +} + +release_assert_clean_tree() { + if [ -z "$(git -C "$ROOT" status --porcelain)" ]; then return 0; fi + printf '%s\n' "working tree must be clean before release" >&2 + return 1 +} + +release_assert_tag_available() { + local tag="$1" + if git -C "$ROOT" rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + printf 'tag already exists: %s\n' "$tag" >&2 + return 1 + fi +} + +release_current_branch() { + git -C "$ROOT" branch --show-current +} + +release_assert_push_branch() { + local branch + branch="$(release_current_branch)" + if [ -z "$branch" ]; then + printf '%s\n' "refusing to push release from detached HEAD" >&2 + return 1 + fi + if [ "$branch" = "main" ] || [ "${DEVLOOP_RELEASE_ALLOW_BRANCH:-0}" = "1" ]; then return 0; fi + printf 'refusing to push release from branch: %s\n' "$branch" >&2 + printf '%s\n' "checkout main, or set DEVLOOP_RELEASE_ALLOW_BRANCH=1" >&2 + return 1 +} + +release_main() { + local version="" + local dry_run=false + local push=false + local tag branch + + while [ "$#" -gt 0 ]; do + case "$1" in + --dry-run) dry_run=true ;; + --push) push=true ;; + -h|--help) release_usage; return 0 ;; + --*) + printf 'unknown option: %s\n' "$1" >&2 + release_usage >&2 + return 2 + ;; + *) + if [ -n "$version" ]; then + release_usage >&2 + return 2 + fi + version="$1" + ;; + esac + shift + done + + if [ -z "$version" ]; then + release_usage >&2 + return 2 + fi + if ! release_version_valid "$version"; then + printf 'invalid SemVer version: %s\n' "$version" >&2 + return 2 + fi + + tag="$(release_tag_for_version "$version")" + release_require_command git + if [ "$dry_run" = true ]; then + release_assert_tag_available "$tag" + printf 'release: %s\n' "$tag" + if [ -n "$(git -C "$ROOT" status --porcelain)" ]; then + printf '%s\n' "note: actual release requires a clean working tree" + fi + printf '%s\n' "would run bash tests/devloop_test.sh" + printf '%s\n' "would update VERSION and CHANGELOG.md" + printf 'would commit: chore: release %s\n' "$version" + printf 'would tag: %s\n' "$tag" + if [ "$push" = true ]; then printf '%s\n' "would push branch and tag"; fi + return 0 + fi + + release_require_command git-cliff + release_assert_tag_available "$tag" + release_assert_clean_tree + if [ "$push" = true ]; then release_assert_push_branch; fi + + bash "$ROOT/tests/devloop_test.sh" + printf '%s\n' "$version" > "$ROOT/VERSION" + git-cliff --config "$ROOT/cliff.toml" --workdir "$ROOT" --tag "$tag" --output "$ROOT/CHANGELOG.md" + git -C "$ROOT" add VERSION CHANGELOG.md + git -C "$ROOT" commit -m "chore: release $version" + git -C "$ROOT" tag -a "$tag" -m "devloop $version" + + if [ "$push" = true ]; then + branch="$(release_current_branch)" + git -C "$ROOT" push origin "$branch" + git -C "$ROOT" push origin "$tag" + fi + + printf 'released %s\n' "$tag" +} + +if [ "${DEVLOOP_RELEASE_LIB:-}" != "1" ]; then + release_main "$@" +fi diff --git a/tests/devloop_test.sh b/tests/devloop_test.sh index 4da0257..92d64b8 100755 --- a/tests/devloop_test.sh +++ b/tests/devloop_test.sh @@ -26,7 +26,7 @@ equals() { [[ "$actual" == "$expected" ]] || fail "$label expected [$expected], got [$actual]" } -bash -n "$ROOT/devloop" "$ROOT/install.sh" "$ROOT/skill_helpers.sh" +bash -n "$ROOT/devloop" "$ROOT/install.sh" "$ROOT/skill_helpers.sh" "$ROOT/release.sh" ok "bash syntax" DEVLOOP_LIB=1 @@ -35,6 +35,11 @@ unset DEVLOOP_LIB equals "${CODEX_MODEL_ARGS[*]}" "-m gpt-5.5" "codex model args" equals "${CLAUDE_MODEL_ARGS[*]}" "--model claude-opus-4-8" "claude model args" +version="$(sed -n '1p' "$ROOT/VERSION")" +equals "$("$ROOT/devloop" --version)" "devloop $version" "version output" +equals "$("$ROOT/devloop" -V)" "devloop $version" "short version output" +equals "$("$ROOT/devloop" --plain --version)" "devloop $version" "version after global flag" + help="$("$ROOT/devloop" --help)" contains "$help" "Common commands:" "help" contains "$help" "devloop doctor" "help" @@ -42,6 +47,8 @@ contains "$help" "devloop reports" "help" contains "$help" "--create-pr" "help" contains "$help" "--no-shell" "help" contains "$help" "--enter-worktree" "help" +contains "$help" "--version" "help" +contains "$help" "v$version" "help" ok "help output" skill_path="$("$ROOT/devloop" spec --skill-path)" @@ -262,6 +269,7 @@ session_output=$'unrelated 11111111-1111-4111-8111-111111111111\nTo continue thi equals "$(extract_session_id "$session_output")" "22222222-2222-4222-8222-222222222222" "extract_session_id uses session marker" contains "$(devloop_logo)" "░█▀▄░█▀▀" "devloop logo" +contains "$(devloop_logo)" "v$version" "devloop logo version" equals "$(ui_color_code accent)" "38;5;141" "accent color" equals "$(ui_color_code rec)" "38;5;135" "run color" equals "$(ui_color_code ok)" "38;5;141" "ok color" @@ -288,6 +296,24 @@ PATH="$old_path" contains "$uuid_one" "00000000-0000-4000-8000-" "new_uuid fallback format" ok "pure helpers" +( + DEVLOOP_RELEASE_LIB=1 + source "$ROOT/release.sh" + release_version_valid "0.1.0" || fail "release version rejected valid patch" + release_version_valid "1.2.3-alpha.1+build.7" || fail "release version rejected valid prerelease" + if release_version_valid "01.2.3"; then fail "release version accepted leading zero"; fi + if release_version_valid "1.2"; then fail "release version accepted missing patch"; fi + if release_version_valid "1.2.3-alpha.01"; then fail "release version accepted leading zero prerelease"; fi + equals "$(release_tag_for_version "1.2.3")" "v1.2.3" "release tag" + release_require_command() { + if [ "$1" = "git-cliff" ]; then return 1; fi + return 0 + } + dry_run_output="$(release_main "9.9.9" --dry-run)" || fail "release dry-run required git-cliff" + contains "$dry_run_output" "would tag: v9.9.9" "release dry-run" +) +ok "release helpers" + bin_dir="$work/bin" install_home="$work/install-home" DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$ROOT/install.sh" >/tmp/devloop-install-test.out