Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,60 @@ created: 2026-06-16
pr: null
---

# Devloop Upgrade Command
Add an explicit upgrade command and an interactive startup prompt so installed Devloop users can move to the latest release without uninstalling first.
# Devloop Update Command
Add an explicit update command and an interactive startup prompt so installed Devloop users can move to the latest release without uninstalling first.

```mermaid
flowchart LR
Current["Installed Devloop"] --> Check["Check latest release"]
Check --> Upgrade["Run existing remote installer"]
Upgrade --> Result["Current symlink points at latest release"]
Check --> Update["Run existing remote installer"]
Update --> Result["Current symlink points at latest release"]
```

## Problem
Updating an already installed Devloop currently requires remembering the uninstall script and rerunning the curl installer manually. This hurts maintainers and users whenever a new release is available, because the update path is outside the CLI they already use.

## Outcome
Users can run `devloop upgrade` to install the latest released Devloop. When a user opens the interactive Devloop entry point and a newer release exists, Devloop prompts once in that session to upgrade or skip before continuing.
Users can run `devloop update` to install the latest released Devloop. When a user opens the interactive Devloop entry point and a newer release exists, Devloop prompts once in that session to update or skip before continuing.

## Scope
- In: `devloop`, `scripts/install.remote.sh`, `README.md`, `scripts/devloop_test.sh`, and generated site install parity if the remote installer contract changes.
- In: `devloop upgrade`, help/usage text, release version comparison, interactive prompt behavior for `devloop` with no args and `devloop menu`.
- In: `devloop update`, help/usage text, release version comparison, interactive prompt behavior for `devloop` with no args and `devloop menu`.
- In: test fixtures that avoid real network calls by using existing `DEVLOOP_GITHUB_API_URL`, `DEVLOOP_RELEASE_BASE_URL`, `DEVLOOP_INSTALL_DIR`, and `DEVLOOP_BIN_DIR` overrides.
- Out: background update daemons, automatic upgrades without confirmation, package manager integration, self-updating from git `main`, and update prompts for non-interactive or scripted commands.
- Out: background update daemons, automatic installs without confirmation, package manager integration, self-updating from git `main`, and update prompts for non-interactive or scripted commands.

## Behavior
### Happy path
1. User runs `devloop upgrade`.
1. User runs `devloop update`.
2. Devloop resolves the latest released version using the same release metadata source as the remote installer.
3. If the latest version is newer than the current `VERSION`, Devloop invokes the bundled remote installer flow to download, verify, install, relink `devloop`, and refresh bundled skills.
4. User sees output that names the current version, target version, and completed install.
5. User later runs `devloop --version` and sees the upgraded version.
5. User later runs `devloop --version` and sees the updated version.

### Edge cases
- Already current: `devloop upgrade` exits 0, prints that the current version is already latest, and does not reinstall.
- Version check fails: `devloop upgrade` exits nonzero with a concise error that the latest version could not be resolved.
- Already current: `devloop update` exits 0, prints that the current version is already latest, and does not reinstall.
- Version check fails: `devloop update` exits nonzero with a concise error that the latest version could not be resolved.
- Check finds an invalid version: Devloop exits nonzero and reports the invalid version instead of installing.
- Non-interactive command: `devloop <spec.md>`, `devloop status`, `devloop clean`, `devloop continue`, `devloop doctor`, `devloop spec`, `devloop --version`, and `devloop --help` do not perform an automatic upgrade check or prompt.
- Interactive startup has newer version: `devloop` with no args and `devloop menu` prompt before showing the menu; accepting runs the same upgrade flow as `devloop upgrade`, and declining continues normally.
- Non-interactive command: `devloop <spec.md>`, `devloop status`, `devloop clean`, `devloop continue`, `devloop doctor`, `devloop spec`, `devloop --version`, and `devloop --help` do not perform an automatic update check or prompt.
- Interactive startup has newer version: `devloop` with no args and `devloop menu` prompt before showing the menu; accepting runs the same update flow as `devloop update`, and declining continues normally.
- Interactive startup is current or check fails: Devloop does not block the menu; failures are quiet or dim informational output, not fatal.
- Prompt environment has no TTY: Devloop skips the automatic prompt.
- Source checkout install: the command still upgrades to the latest released archive using the remote installer, replacing the active symlink just like the curl install path.
- Source checkout install: the command still updates to the latest released archive using the remote installer, replacing the active symlink just like the curl install path.

## Acceptance criteria
1. `devloop upgrade` appears in plain help, TUI help, README command table, and command dispatch.
2. `devloop upgrade` installs a newer release through the existing remote installer behavior when fixture metadata reports a greater version.
3. `devloop upgrade` exits 0 without reinstalling when the latest fixture version equals the current `VERSION`.
4. `devloop upgrade` exits nonzero with a clear error when the latest version cannot be resolved.
5. `devloop` with no args prompts to upgrade before the menu when fixture metadata reports a greater version and stdin/stdout are TTYs.
6. `devloop menu` prompts to upgrade before the menu when fixture metadata reports a greater version and stdin/stdout are TTYs.
1. `devloop update` appears in plain help, TUI help, README command table, and command dispatch.
2. `devloop update` installs a newer release through the existing remote installer behavior when fixture metadata reports a greater version.
3. `devloop update` exits 0 without reinstalling when the latest fixture version equals the current `VERSION`.
4. `devloop update` exits nonzero with a clear error when the latest version cannot be resolved.
5. `devloop` with no args prompts to update before the menu when fixture metadata reports a greater version and stdin/stdout are TTYs.
6. `devloop menu` prompts to update before the menu when fixture metadata reports a greater version and stdin/stdout are TTYs.
7. Declining the automatic prompt continues to the normal interactive menu without changing the installed symlink.
8. Non-interactive commands and commands with explicit work do not run the automatic upgrade check.
9. The implementation has Bash test coverage in `scripts/devloop_test.sh` for upgrade command success, already-current behavior, resolver failure, prompt accept, prompt decline, and prompt skip paths.
8. Non-interactive commands and commands with explicit work do not run the automatic update check.
9. The implementation has Bash test coverage in `scripts/devloop_test.sh` for update command success, already-current behavior, resolver failure, prompt accept, prompt decline, and prompt skip paths.
10. The implementation preserves the existing remote installer checksum verification and skill installation behavior.

## Test plan
- Red: Add failing Bash tests in `scripts/devloop_test.sh` for `devloop upgrade` using local release fixtures and for automatic prompt routing on interactive entry points.
- Red: Add failing Bash tests in `scripts/devloop_test.sh` for `devloop update` using local release fixtures and for automatic prompt routing on interactive entry points.
- Green: `bash scripts/devloop_test.sh`
- Full: `bash scripts/devloop_test.sh`
- Coverage: `bash scripts/devloop_test.sh` must keep the existing project function coverage gate passing at 100%.
Expand All @@ -67,10 +67,10 @@ Users can run `devloop upgrade` to install the latest released Devloop. When a u
- Must: keep Devloop as a Bash CLI and reuse the release archive, checksum, install root, bin dir, and skill install conventions already present in `scripts/install.remote.sh`.
- Must: honor existing test override environment variables so tests do not hit GitHub or `devloop.sh`.
- Must: avoid blocking scripted commands on network checks or prompts.
- Must: compare semantic versions using a deterministic Bash-compatible helper before deciding that an upgrade is available.
- Must: compare semantic versions using a deterministic Bash-compatible helper before deciding that an update is available.
- Avoid: introducing Python, Node, Homebrew, cron, launch agents, telemetry, or a new package distribution path.
- Avoid: duplicating archive download and checksum installation logic in `devloop` when the remote installer can perform the install.
- Existing convention: `VERSION` is the single local version source, `scripts/devloop_test.sh` is the shell test suite, and user-visible CLI changes are documented in `README.md`.

## Notes
Use the current remote installer as the installation engine. A minimal shape is to add shared helpers in `devloop` that resolve the latest version, compare it with `DEVLOOP_VERSION`, and call `$ROOT_DIR/scripts/install.remote.sh --yes --version <latest>` when upgrading. The existing remote installer already supports `DEVLOOP_GITHUB_API_URL`, `DEVLOOP_RELEASE_BASE_URL`, `DEVLOOP_INSTALL_DIR`, `DEVLOOP_BIN_DIR`, `--yes`, archive verification, and skill installation, which should keep the implementation small.
Use the current remote installer as the installation engine. A minimal shape is to add shared helpers in `devloop` that resolve the latest version, compare it with `DEVLOOP_VERSION`, and call `$ROOT_DIR/scripts/install.remote.sh --yes --version <latest>` when updating. The existing remote installer already supports `DEVLOOP_GITHUB_API_URL`, `DEVLOOP_RELEASE_BASE_URL`, `DEVLOOP_INSTALL_DIR`, `DEVLOOP_BIN_DIR`, `--yes`, archive verification, and skill installation, which should keep the implementation small.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Uninstall with `./scripts/uninstall.sh` (`--dry-run` to preview).
| `devloop spec "..."` | Have an agent interview you and write a spec |
| `devloop <spec.md>` | Run a spec |
| `devloop --create-pr <spec.md>` | Run a spec and maintain a draft PR (requires `gh`) |
| `devloop upgrade` | Install the latest released Devloop |
| `devloop update` | Install the latest released Devloop |
| `devloop continue` | Resume a tracked run |
| `devloop status` | Show run status |
| `devloop clean` | Remove run artifacts |
Expand Down
42 changes: 21 additions & 21 deletions devloop
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ UI_DIM_COLOR="244"
UI_BORDER_COLOR="141"
UI_BACK=false
UI_NOTICE=""
DEVLOOP_UPGRADE_PROMPTED=false
DEVLOOP_UPDATE_PROMPTED=false

EVENT_IDS=()
EVENT_TITLES=()
Expand Down Expand Up @@ -88,9 +88,9 @@ main() {
return 0
fi

if [ "${1:-}" = "upgrade" ]; then
if [ "${1:-}" = "update" ]; then
shift
upgrade_command "$@"
update_command "$@"
return $?
fi

Expand All @@ -102,7 +102,7 @@ main() {

if [ "${1:-}" = "menu" ]; then
shift
if [ "$#" -eq 0 ]; then maybe_prompt_upgrade; fi
if [ "$#" -eq 0 ]; then maybe_prompt_update; fi
interactive_menu "$@"
return $?
fi
Expand Down Expand Up @@ -143,7 +143,7 @@ main() {

if [ "$#" -eq 0 ] || has_arg "-h" "$@" || has_arg "--help" "$@"; then
if [ "$#" -eq 0 ] && [ "$USE_TUI" = true ]; then
maybe_prompt_upgrade
maybe_prompt_update
interactive_menu
return $?
fi
Expand Down Expand Up @@ -181,7 +181,7 @@ Usage:

Common commands:
devloop doctor
devloop upgrade
devloop update
devloop reports
devloop status
devloop clean
Expand Down Expand Up @@ -220,7 +220,7 @@ welcome_tui() {
printf ' devloop [options] <spec.md> [max=5]\n\n'
gum style --foreground "$UI_ACCENT_COLOR" --bold "Common commands"
printf ' %-42s %s\n' "devloop doctor" "check required tools"
printf ' %-42s %s\n' "devloop upgrade" "install the latest release"
printf ' %-42s %s\n' "devloop update" "install the latest release"
printf ' %-42s %s\n' "devloop reports" "open previous run reports"
printf ' %-42s %s\n' "devloop status" "summarize tracked runs"
printf ' %-42s %s\n' "devloop clean" "show safe cleanup candidates"
Expand Down Expand Up @@ -386,28 +386,28 @@ devloop_resolve_latest_version() {
devloop_normalize_version "$version"
}

devloop_upgrade_install() {
devloop_update_install() {
local latest="$1"
local current="$2"
local installer="$ROOT_DIR/scripts/install.remote.sh"
if [ ! -f "$installer" ]; then
printf 'error: remote installer not found: %s\n' "$installer" >&2
return 1
fi
printf 'upgrading devloop %s -> %s\n' "$current" "$latest"
printf 'updating devloop %s -> %s\n' "$current" "$latest"
bash "$installer" --yes --version "$latest"
}

upgrade_command() {
update_command() {
local resolved latest current
if [ "$#" -gt 0 ]; then
case "$1" in
-h|--help)
printf '%s\n' "usage: devloop upgrade"
printf '%s\n' "usage: devloop update"
return 0
;;
*)
printf '%s\n' "usage: devloop upgrade" >&2
printf '%s\n' "usage: devloop update" >&2
return 2
;;
esac
Expand All @@ -423,29 +423,29 @@ upgrade_command() {
printf 'devloop %s is already latest\n' "$current"
return 0
fi
devloop_upgrade_install "$latest" "$current"
devloop_update_install "$latest" "$current"
}

devloop_prompt_tty_ready() {
[ -t 0 ] && [ -t 1 ]
}

maybe_prompt_upgrade() {
maybe_prompt_update() {
local resolved latest current
if [ "$DEVLOOP_UPGRADE_PROMPTED" = true ]; then return 0; fi
DEVLOOP_UPGRADE_PROMPTED=true
if [ "$DEVLOOP_UPDATE_PROMPTED" = true ]; then return 0; fi
DEVLOOP_UPDATE_PROMPTED=true
devloop_prompt_tty_ready || return 0
current="$(devloop_normalize_version "$DEVLOOP_VERSION" 2>/dev/null || printf '%s\n' "$DEVLOOP_VERSION")"
resolved="$(devloop_resolve_latest_version 2>/dev/null)" || return 0
latest="$resolved"
devloop_version_gt "$latest" "$current" || return 0
printf 'upgrade available: devloop %s -> %s\n' "$current" "$latest" >&2
if ui_confirm "Upgrade Devloop to $latest now?"; then
if ! devloop_upgrade_install "$latest" "$current"; then
UI_NOTICE="upgrade failed; continuing without upgrade"
printf 'update available: devloop %s -> %s\n' "$current" "$latest" >&2
if ui_confirm "Update Devloop to $latest now?"; then
if ! devloop_update_install "$latest" "$current"; then
UI_NOTICE="update failed; continuing without update"
fi
else
UI_NOTICE="upgrade skipped"
UI_NOTICE="update skipped"
fi
}

Expand Down
Loading
Loading