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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,9 @@ come from npm. The generated short alias is the installed identity; red-dev
never also writes the backend-qualified spec into the global config. Existing
machines are migrated only after the aliased replacement is present, with the
original config backed up before the redundant declaration is retired.
The bootstrap download is temporary ownership: after a successful first run
proves mise has installed `red-dev`, the bootstrap removes its own copy. A
cancelled setup keeps that copy so the command never disappears.

On the Ubuntu desktop, red-dev itself also has a tool-level postinstall: it
runs the newly installed binary's `desktop reconcile`. That updates only the
Expand Down
19 changes: 19 additions & 0 deletions boot.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,22 @@ $ProgressPreference = $PreviousProgressPreference
# falls back to a line menu in a narrow one, and prints help when there is
# no terminal at all.
& $Bin
$Status = $LASTEXITCODE

# The downloaded executable is only the bootstrap. Once the successful first
# run has installed the managed mise identity, the child is closed and Windows
# releases the file lock, so this copy can be retired without scheduling a
# second cleanup process. A cancelled setup keeps the bootstrap intact.
if ($Status -eq 0) {
$Mise = Get-Command mise -ErrorAction SilentlyContinue
if ($Mise) {
$ManagedRoot = (& $Mise.Source where red-dev 2>$null | Select-Object -First 1)
$Managed = if ($ManagedRoot) { Join-Path $ManagedRoot 'red-dev.exe' } else { $null }
if ($Managed -and (Test-Path $Managed) -and ($Managed -ne $Bin)) {
Remove-Item $Bin -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $Bin)) { Say "mise owns red-dev now; retired bootstrap $Bin" }
}
}
}

exit $Status
23 changes: 21 additions & 2 deletions boot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,26 @@ export RED_DEV_BOOTSTRAP=1
# /dev/tty is the controlling terminal regardless of what stdin was
# redirected to. When there is none — CI, a container, a cron job — the
# fallback is the current behaviour, which is what should happen there.
set +e
if [ -r /dev/tty ]; then
exec "$BIN" < /dev/tty
"$BIN" < /dev/tty
STATUS=$?
else
"$BIN"
STATUS=$?
fi
exec "$BIN"
set -e

# The downloaded binary is a bootstrap, not a second permanent owner. Once a
# successful first run has installed red-dev through the managed mise alias,
# retire this copy so PATH, updates and disk all have one answer. If setup was
# cancelled before mise acquired it, keep the bootstrap working.
if [ "$STATUS" -eq 0 ] && command -v mise >/dev/null 2>&1; then
MISE_RED_DEV=$(mise where red-dev 2>/dev/null || true)
if [ -n "$MISE_RED_DEV" ] && [ -x "$MISE_RED_DEV/red-dev" ] && [ "$MISE_RED_DEV/red-dev" != "$BIN" ]; then
rm -f "$BIN"
say "mise owns red-dev now; retired bootstrap $BIN"
fi
fi

exit "$STATUS"
32 changes: 32 additions & 0 deletions src/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,38 @@ return {}
log.plain(` original config backed up at ${backup}`);
},
},
{
id: "2026-09-22-suite-binary-handover",
describe: "finish handing bootstrap binaries to their single mise owner",
applies: (p) => staleReleaseBinaries(p).length > 0,
run: async (p) => {
const stale = staleReleaseBinaries(p);
const mise = Bun.which("mise");
if (!mise) throw new Error("mise is not installed yet — bootstrap binaries left intact");

// Prove every replacement before removing any old copy. `mise where`
// asks for the alias's install tree directly; `which` can answer with
// the very ~/.local/bin file this migration is trying to retire.
for (const { name } of stale) {
const where = Bun.spawnSync([mise, "where", name], { stdout: "pipe", stderr: "ignore" });
if (where.exitCode !== 0 || where.stdout.toString().trim() === "") {
throw new Error(`${name} has no mise replacement yet — bootstrap binaries left intact`);
}
}

for (const { name, path } of stale) {
// Linux permits unlinking the image this process is executing; the
// inode stays alive until exit and the next command resolves through
// mise. Windows locks a running .exe, so boot.ps1 removes its bootstrap
// copy after this process returns and the migration retries once.
if (p.os === "windows" && samePath(path, process.execPath)) {
throw new Error(`${path} is still running — boot.ps1 will retire it after exit`);
}
rmSync(path, { force: true });
log.plain(` ${name}: removed ${path}; mise is the only owner`);
}
},
},
];

/**
Expand Down
12 changes: 12 additions & 0 deletions src/mise-handover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ describe("the stale binary an older release left behind", () => {
expect(staleReleaseBinaries(ubuntu).map((s) => s.name)).toEqual(["red"]);
});
});

test("includes red-dev's bootstrap copy so a later run can finish the handover", () => {
const home = temp();
mkdirSync(join(home, ".local", "bin"), { recursive: true });
writeFileSync(join(home, ".local", "bin", "red-dev"), "#!/bin/sh\nexit 0\n");
withEnv({ HOME: home }, () => {
expect(staleReleaseBinaries(ubuntu)).toContainEqual({
name: "red-dev",
path: join(home, ".local", "bin", "red-dev"),
});
});
});
});

describe("mise's shims", () => {
Expand Down
10 changes: 8 additions & 2 deletions src/sudo-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,15 @@ describe("the human install entry points", () => {
// `curl | sh` leaves stdin pointing at the exhausted script pipe. The
// downloaded binary must inherit the controlling terminal or neither
// its sudo prompt nor its fullscreen interface can read a key.
expect(boot).toContain('exec "$BIN" < /dev/tty');
expect(boot).toContain('"$BIN" < /dev/tty');
// The bootstrap must regain control after the child closes so it can
// retire its copy once mise owns red-dev. `exec` would preserve the TTY
// but make the duplicate permanent again.
expect(boot).not.toContain('exec "$BIN" < /dev/tty');
expect(boot).toContain("mise where red-dev");
expect(boot).toContain('rm -f "$BIN"');
expect(boot.indexOf("export RED_DEV_BOOTSTRAP=1")).toBeLessThan(
boot.indexOf('exec "$BIN" < /dev/tty'),
boot.indexOf('"$BIN" < /dev/tty'),
);

// The bootstrap enters the menu rather than `cmdInstall`, so it needs
Expand Down