Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9ffb447
feat(upgrade): snapshot and rollback library for upgrade steps (HACK-…
roodboi Nov 13, 2026
1cdf679
feat(upgrade): read-only hack upgrade check, snapshot and rollback co…
roodboi Nov 13, 2026
b011e0c
fix(upgrade): a project with only hack.toml or a Compose file is live…
roodboi Nov 13, 2026
f77d7b0
fix(upgrade): check linked worktrees, keep the migration write set, p…
roodboi Nov 13, 2026
ab1e187
fix(upgrade): refuse a snapshot root that resolves into a Git working…
roodboi Nov 13, 2026
3118605
fix(upgrade): restore keeps every displaced file, readiness needs eve…
roodboi Nov 13, 2026
0ff8cec
test(upgrade): route fixture git through the shared hermetic environm…
roodboi Nov 13, 2026
0733d15
fix(upgrade): put a displaced file back when it cannot be kept; a fai…
roodboi Nov 13, 2026
049249e
fix(upgrade): decide the store blocker by each reader's own key (HACK…
roodboi Nov 13, 2026
77d100c
fix(upgrade): check every reader's key; host-probe children end with …
roodboi Nov 13, 2026
6c46055
fix(upgrade): keep displaced files as themselves, no overwriting fall…
roodboi Nov 13, 2026
a565f5d
fix(upgrade): when publication fails, put the displaced file back or …
roodboi Nov 13, 2026
d8702b4
fix(upgrade): a failed proxy ports probe is incomplete, not omitted (…
roodboi Nov 13, 2026
19e2900
test(upgrade): pass the check only the variables it reads, never the …
roodboi Nov 13, 2026
9ff7679
fix(upgrade): a dead registry entry is not store-reader uncertainty (…
roodboi Nov 13, 2026
ddf4482
Merge next into claude/hack-2189-upgrade-check (HACK-2189)
roodboi Nov 13, 2026
b5e16f8
fix(upgrade): snapshot each project's own secret store and key (HACK-…
roodboi Nov 13, 2026
665d9e1
fix(upgrade): keep every restore temporary inside the snapshot, never…
roodboi Nov 13, 2026
844191d
fix(upgrade): check project-selected secret stores; a failed containe…
roodboi Nov 13, 2026
21bd149
fix(upgrade): compare a symlinked parent by where it leads; quote the…
roodboi Nov 13, 2026
40639fc
fix(upgrade): snapshot the configured default overlay whatever the se…
roodboi Nov 13, 2026
b64c4a9
fix(upgrade): resolve the migration's project name as doctor does, le…
roodboi Nov 13, 2026
0614bc4
fix(upgrade): a project directory that cannot be listed is a snapshot…
roodboi Nov 13, 2026
64fa471
fix(upgrade): env layer secrets must open under the project env key (…
roodboi Nov 13, 2026
b97c9f0
fix(upgrade): a regular file that cannot be read after displacement g…
roodboi Nov 13, 2026
e68e029
fix(upgrade): fail closed when Git is selected through the environmen…
roodboi Nov 13, 2026
1d2803e
fix(upgrade): any defined GIT_DIR or GIT_WORK_TREE refuses, whitespac…
roodboi Nov 13, 2026
dd47bb6
Merge next (292d3127) into claude/hack-2189-upgrade-check (HACK-2189)
roodboi Nov 13, 2026
c0f7d1e
Merge next (b1e123bb) into claude/hack-2189-upgrade-check (HACK-2189)
roodboi Nov 13, 2026
d5a1587
Merge next (bb6a7d17) into claude/hack-2189-upgrade-check (HACK-2189)
roodboi Nov 14, 2026
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
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ terminal).
- `hack exec <service> -- <cmd...>` — command in an already-running service container
- `hack session` — persistent project workspaces (tmux-first)
- `hack doctor` / `hack doctor --fix` — validate and repair local setup
- `hack upgrade check` — read-only upgrade check of this machine and every registered project, ending in an ordered plan and the next command; `hack upgrade snapshot` and `hack upgrade rollback --rollback-manifest <manifest>` keep and restore the files the upgrade changes
- `hack daemon` — optional local daemon for faster JSON project status; `hack daemon install` starts it at login (launchd on macOS, a systemd user unit on Linux). On Linux without linger it still stops at your last logout and does not start at boot; `loginctl enable-linger` changes that

Candidate `hack-next doctor` and `hack-v5 doctor` report a missing hackd as optional
Expand Down
102 changes: 102 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,108 @@ Notes:
| `--version, -v` | Show version |


## `hack upgrade`

Guided upgrade: check, snapshot and roll back

### Usage

```bash
hack upgrade <subcommand> [options]
```

### Subcommands

| Command | Summary |
| --- | --- |
| `hack upgrade check` | Check this machine and every registered project for the upgrade (read-only) |
| `hack upgrade snapshot` | Keep a private copy of the files the upgrade changes |
| `hack upgrade rollback` | Put back the files kept by an upgrade snapshot |

### Options

| Option | Description |
| --- | --- |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack upgrade check`

Check this machine and every registered project for the upgrade (read-only)

### Usage

```bash
hack upgrade check [options]
```

Reads the installed versions, the daemon's service, the projects registry (dead entries apart
from unreadable ones), Hack's global services, proxy ports and DNS, the global secret store, and
for each live project its env format and key, tracked files, ignore rules, agent integrations
and (Apple Silicon) native preflight. Ends with an ordered plan and the next command. Changes
nothing and prints no value. Exits 1 when something blocks the upgrade, naming it.

### Options

| Option | Description |
| --- | --- |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack upgrade snapshot`

Keep a private copy of the files the upgrade changes

### Usage

```bash
hack upgrade snapshot [options]
```

Copies the global Hack config, the projects registry, the encrypted secret store and its key
(when used), and each live project's .hack config, env files, env layers, .hack/.gitignore and
env key files into a private directory under ~/.hack/upgrade/snapshots (0700, files 0600).
Prints the rollback command. The manifest records paths, sizes, modes and digests, never values.

### Options

| Option | Description |
| --- | --- |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack upgrade rollback`

Put back the files kept by an upgrade snapshot

### Usage

```bash
hack upgrade rollback [options]
```

Restores each file in the manifest with its exact bytes and mode. A file that changed since the
snapshot (or since the step that recorded it) is left as it is unless --overwrite is passed;
whatever a restore replaces or removes is kept in the snapshot directory, never deleted.
Without --rollback-manifest, lists the snapshots and changes nothing.

### Options

| Option | Description |
| --- | --- |
| `--rollback-manifest <manifest>` | The snapshot manifest to restore (from hack upgrade snapshot) |
| `--overwrite` | Also restore files that changed since the snapshot (the current copies are kept in the snapshot) |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |


## `hack version`

Print version
Expand Down
72 changes: 72 additions & 0 deletions docs/versions/v5/guides/upgrade-from-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,78 @@ Sources: [candidate channel contract](https://github.com/hack-dance/hack/blob/26

## 2. Before you start

### Check first

v5 builds that include the upgrade assistant can check the machine before you
change anything. Run the v5 CLI (on macOS, `"$HOME/.hack-next/bin/hack-next"`
with `HACK_RUNTIME_BACKEND=compose`):

```sh
hack upgrade check # add --json for a machine-readable report
```

It reads the installed versions, the daemon's login service, the projects
registry, Hack's global services, proxy ports and DNS, and the global secret
store. For each live project it reads the env format and key, tracked
machine-local files, ignore rules, agent integration files and, on Apple
Silicon, the native preflight. It changes nothing and prints no secret value.

- **Dead registry entries** (the checkout or its config is gone) are counted
apart from live projects that cannot be read. Dead entries only need
`hack projects prune`; unreadable ones need repair.
- It ends with an ordered plan and the next command. A step either has a
command or is marked manual, with the action to take; manual text is never
printed as a command. Commands run the CLI you ran the check with (the
`hack-next` launcher during the prerelease).
- It exits 1 when something blocks the upgrade, naming the blockers. Examples
are a global secret store whose key is gone or refused while a project
reads it, a tracked secret key, an env layer conflict, or a live project
whose config cannot be read. It also exits 1 when a check the upgrade
depends on could not run, listed under "Not fully checked". Examples are a
check that timed out, or a key file that is missing while the OS keychain
may hold the key; the check never asks the keychain, because that can
prompt.

Before you follow the plan, keep a private copy of the files it changes:

```sh
hack upgrade snapshot
```

This copies the global Hack config, the projects registry, the encrypted
secret store and its key (when used), and each live project's `.hack/`
config, env files, env layers, `.hack/.gitignore` and env key files into
`~/.hack/upgrade/snapshots/<id>/` (directories 0700, files 0600). The
manifest records paths, sizes, modes and SHA-256 digests, never values. It
prints the command that puts the files back:

```sh
hack upgrade rollback --rollback-manifest <manifest>
```

Rollback restores each file's exact bytes and mode. A file that changed since
the snapshot is left as it is unless you add `--overwrite`. Whatever rollback
replaces or removes is kept as itself (not as a copy) in the snapshot directory
and named in the output; nothing is deleted. A file that appears at a path while
rollback runs is left alone and reported as a conflict. Rollback keeps every
temporary and replaced file inside the snapshot directory, never beside the
file in your checkout, so Git cannot pick up their contents. It therefore
needs the file and the snapshot on the same filesystem; a project on another
disk than `~/.hack` is reported for you to restore by hand.
`hack upgrade rollback` without a manifest lists the snapshots and changes
nothing.

The snapshot also records, as absent, the env layers the migration would
create, so a rollback moves them aside. This includes layers for overlays held
only in the secret store. When the snapshot cannot cover everything, it says
so and lists what it misses (`complete: false` in `--json`). Examples are a
projects registry it could not read, a live project it could not read, a file
it skipped because it is a link, and overlays it could not list without asking
the OS keychain. A rollback cannot put back what a snapshot does not hold. The snapshot does not cover the Git index, the daemon's login
service or Docker state; the steps below still apply.

### Record the current state

Keep the records and backups private: registry and doctor output can identify
local projects, and ignored env files can contain plaintext secrets. Do not paste
them into an issue. Use a new backup directory and record its location yourself:
Expand Down
2 changes: 2 additions & 0 deletions src/cli/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { sshCommand } from "../commands/ssh.ts";
import { theCommand } from "../commands/the.ts";
import { tuiCommand } from "../commands/tui.ts";
import { updateCommand } from "../commands/update.ts";
import { upgradeCommand } from "../commands/upgrade.ts";
import { usageCommand } from "../commands/usage.ts";
import { versionCommand } from "../commands/version.ts";
import { xCommand } from "../commands/x.ts";
Expand Down Expand Up @@ -105,6 +106,7 @@ export const CLI_SPEC = defineCli({
internalCommand,
xCommand,
updateCommand,
upgradeCommand,
versionCommand,
helpCommand,
],
Expand Down
30 changes: 23 additions & 7 deletions src/commands/doctor-native-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ export async function runNativePreflightCommand(opts: {
readonly profiles: readonly string[];
readonly env?: Readonly<Record<string, string | undefined>>;
}): Promise<number> {
const report = await collectProjectNativePreflight(opts);
if (opts.json) {
emitCliResult({ result: okResult({ data: report }) });
} else {
process.stdout.write(renderNativePreflightReport(report));
}
return report.blockers.length === 0 ? 0 : 1;
}

/**
* The native preflight report for the project at `startDir`, without
* printing it. `runtimeReview: false` skips the native runtime's own review
* (it runs the native binary and writes a temporary directory), leaving
* only the in-process checks (HACK-2189 upgrade check).
*/
export async function collectProjectNativePreflight(opts: {
readonly startDir: string;
readonly profiles: readonly string[];
readonly env?: Readonly<Record<string, string | undefined>>;
readonly runtimeReview?: boolean;
}): Promise<Awaited<ReturnType<typeof collectNativePreflight>>> {
const env = opts.env ?? process.env;
const project = await findProjectContext(opts.startDir).catch(
(error: unknown) => {
Expand Down Expand Up @@ -205,12 +226,7 @@ export async function runNativePreflightCommand(opts: {
hasDefaultDependencyFile,
effectiveEnvName: effectiveEnv,
profiles: opts.profiles,
runtime: selectedRuntime(env),
runtime: opts.runtimeReview === false ? null : selectedRuntime(env),
});
if (opts.json) {
emitCliResult({ result: okResult({ data: report }) });
} else {
process.stdout.write(renderNativePreflightReport(report));
}
return report.blockers.length === 0 ? 0 : 1;
return report;
}
Loading
Loading