Merge nucleic/mellow-dewy-falcon-rjhr into main

This commit is contained in:
2026-08-07 03:12:18 -07:00
parent e7163de22f
commit adb7dedd80
5 changed files with 292 additions and 30 deletions
+34 -3
View File
@@ -64,7 +64,12 @@ struct ImageCommand: AsyncParsableCommand {
let store = VMStore(config: config)
try store.ensureLayout()
if try store.image(named: name) != nil {
// Only a *finished* image blocks a rebuild. An installed but
// unprovisioned bundle is an hour of work that `ImageBuilder.build`
// knows how to resume, so it must get the chance to say so.
if let existing = try store.image(named: name),
(try? existing.loadConfig())?.provisioned == true
{
throw ValidationError(
"image '\(name)' already exists — delete it first with `image delete \(name)`"
)
@@ -101,7 +106,10 @@ struct ImageCommand: AsyncParsableCommand {
// it to exit is its own deadlock. See `VZAppRuntime.run`.
VZAppRuntime.flushAndExit(1)
}
printer.finish("done")
// Just seals the line: the builder's own `.done` stage has
// already printed it, and saying it twice down a pipe reads
// like something ran twice.
printer.finish()
print("built image '\(imageName)'")
print("next: gitea-macos-runner vm boot --image \(imageName)")
@@ -281,7 +289,30 @@ struct ImageCommand: AsyncParsableCommand {
if case .note(let text) = stage {
printer.line(text)
} else {
printer.update(describe(stage))
printer.update(describe(stage), group: group(of: stage))
}
}
/// The stage a status line belongs to, ignoring its varying payload.
///
/// Two lines share a group exactly when one is meant to overwrite the
/// other. Crossing a group boundary seals the previous line instead, which
/// is why `installing macOS … 100%` survives into scrollback rather than
/// being replaced by `first boot + guest provisioning…`.
static func group(of stage: ImageBuildStage) -> String {
switch stage {
case .downloadingIPSW: return "download"
case .preparing: return "preparing"
case .loadingRestoreImage: return "loading"
case .creatingBundle: return "bundle"
case .note: return "note"
case .installing: return "install"
case .firstBoot: return "firstBoot"
// Each provisioning step is its own headline — "installing Node.js"
// should not erase "downloading gitea-runner".
case .provisioning(let step): return "provisioning:\(step)"
case .finalizing: return "finalizing"
case .done: return "done"
}
}