@tikoci/quickchr 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bun-version ADDED
@@ -0,0 +1 @@
1
+ 1.4.4
package/DESIGN.md CHANGED
@@ -245,7 +245,9 @@ CI, integration and release all install, enforced by `scripts/check-bun-pin.ts`
245
245
  runtime could change between two identical commits — and did: a six-week gap with no `ci.yml`
246
246
  runs spanned the Bun 1.4.0 release, and the next PR inherited a 1.3.14 → 1.4.2 jump as if it
247
247
  were its own regression. Anything that can turn the build red is a dependency and belongs in
248
- a reviewable diff; "whatever is newest at run time" is not a version.
248
+ a reviewable diff; "whatever is newest at run time" is not a version. The same
249
+ `.bun-version` file ships in the npm package so `doctor` can name the tested
250
+ runtime and warn when the running Bun is older without duplicating the pin.
249
251
 
250
252
  **The 2026-07 refactor (#29)** replaced the organically-grown scheme (integration on every
251
253
  PR push, three parallel copies of the runner logic, `continue-on-error` green-washing)
package/MANUAL.md CHANGED
@@ -64,7 +64,7 @@ by the user (or their AI agent) using `quickchr` as a building block.
64
64
  | Bun ≥ 1.1 | runtime | macOS/Linux: `curl -fsSL https://bun.sh/install \| bash` · Windows: `powershell -c "irm bun.sh/install.ps1 \| iex"` (or `winget install Oven-sh.Bun`) |
65
65
  | `qemu-system-x86_64` | x86 CHR | brew/apt/dnf/pacman · Windows: `winget install SoftwareFreedomConservancy.QEMU` |
66
66
  | `qemu-system-aarch64` | arm64 CHR | brew/apt/dnf/pacman · Windows: bundled in the SFC QEMU package above |
67
- | `qemu-img` | `--boot-size`, `--add-disk`, snapshots | included in all standard QEMU packages (brew, apt, dnf, SFC installer) |
67
+ | `qemu-img` | Default qcow2 creation, setup wizard, `--boot-size`, `--add-disk`, snapshots | included in all standard QEMU packages (brew, apt, dnf, SFC installer) |
68
68
  | UEFI firmware (edk2) | arm64 CHR | `qemu-efi-aarch64` (apt), `edk2-aarch64` (dnf), bundled with `qemu` (brew) |
69
69
  | `socket_vmnet` (optional) | rootless `shared`/`bridged` networking on macOS | `brew install socket_vmnet` |
70
70
  | `socat` (optional) | piping serial console externally | brew/apt |
@@ -116,7 +116,9 @@ The `--json` form emits `{ ok, checks, staleImages }`, where `checks` is an
116
116
  array of `{ label, status, detail }` and `staleImages` lists cached images older
117
117
  than the current long-term release. The exit code still reflects `ok`.
118
118
 
119
- ![quickchr doctor output](./images/doctor-demo.gif)
119
+ `doctor` reports the tested Bun pin and warns when the running runtime is older.
120
+
121
+ ![quickchr doctor output](./docs/images/doctor-demo.gif)
120
122
 
121
123
  Use this whenever a `start` fails — the diagnostic almost always points
122
124
  at a missing dependency, missing firmware, or insufficient disk.
@@ -125,7 +127,7 @@ at a missing dependency, missing firmware, or insufficient disk.
125
127
 
126
128
  ## 3. CLI command reference
127
129
 
128
- The dispatcher lives in `src/cli/index.ts` (lines 121–194). Help text is
130
+ The dispatcher lives in `src/cli/index.ts`. Help text is
129
131
  available for every command via `quickchr help <command>`.
130
132
 
131
133
  ### Lifecycle
@@ -138,14 +140,14 @@ directory, downloads the RouterOS image (cached), and writes
138
140
 
139
141
  | Flag | Default | Notes |
140
142
  |---|---|---|
141
- | `--name <s>` | auto | `host-arch-N` if omitted |
143
+ | `--name <s>` | auto | `version-arch-N` if omitted |
142
144
  | `--version <ver>` | latest in channel | e.g., `7.22.1` |
143
145
  | `--channel <ch>` | `stable` | `stable`, `long-term`, `testing`, `development` |
144
146
  | `--arch <a>` | host native | `arm64`, `x86`, or `auto` |
145
147
  | `--cpu <n>` | `1` | vCPU count |
146
148
  | `--mem <mb>` | `512` | RAM (cross-arch TCG bumps to `1024`) |
147
149
  | `--boot-disk-format <f>` | `qcow2` | `qcow2` or `raw` |
148
- | `--boot-size <size>` | — | Resize boot disk; needs `qemu-img`; auto-converts to qcow2 |
150
+ | `--boot-size <size>` | — | Resize qcow2 boot disk; needs `qemu-img`; explicit raw is refused |
149
151
  | `--add-disk <size>` | — | Extra blank qcow2 disk; repeatable |
150
152
  | `--forward <spec>` | — | Extra hostfwd port or built-in service host-port pin; repeatable. See §Custom port forwards. |
151
153
  | `--add-package <pkg>` | — | Provisioning, repeatable, ≥ 7.20.8 |
@@ -486,7 +488,7 @@ Interactive wizard (`@clack/prompts`) that walks through all machine creation
486
488
  options and starts the CHR. Triggered by `quickchr setup` or automatically
487
489
  when `quickchr` is run with no subcommand on a TTY.
488
490
 
489
- ![quickchr setup wizard walkthrough](./images/wizard-demo.gif)
491
+ ![quickchr setup wizard walkthrough](./docs/images/wizard-demo.gif)
490
492
 
491
493
  **First-run detection**: if no machines exist yet, the wizard opens with a
492
494
  welcome message before the first prompt.
@@ -513,7 +515,10 @@ another machine, view status, or quit.
513
515
  | 12 | Run mode: background / foreground | background | (default) / `--fg` |
514
516
  | 13 | Final confirmation | — | — |
515
517
 
516
- Steps 7–9 are skipped when `qemu-img` is absent (a warning is shown). Step 10
518
+ The wizard requires `qemu-img` and refuses before any prompts or image downloads
519
+ when it is absent, showing the install remedy. For a raw x86 boot without `qemu-img`,
520
+ use `quickchr add --arch x86 --boot-disk-format raw` to create a stopped machine,
521
+ then `quickchr start <name>` with the returned name to boot it. Step 10
517
522
  only appears when the platform supports networks beyond user-mode (macOS or
518
523
  Linux). The provisioning gate (step 11) only appears for RouterOS ≥ 7.20.8;
519
524
  older versions show an upgrade prompt.
@@ -597,7 +602,7 @@ default output form when writing step outputs directly.
597
602
  process that wrote it is alive (an extraction: for 30 minutes after its last
598
603
  write). `prune` removes stale ones before anything else.
599
604
 
600
- `cache list --json` prints one object per entry, including `kind` (`image`, `image-zip`,
605
+ `cache list --json` prints an array of entry objects, including `kind` (`image`, `image-zip`,
601
606
  `packages` or `partial`) and `paths` (every file deleting the entry removes).
602
607
  Files in the cache that quickchr did not write are reported as "Other files"
603
608
  and never deleted. `prune` and `clear` never delete an in-use entry.
@@ -690,7 +695,7 @@ mutate any machine's `machine.json`.
690
695
 
691
696
  | Env var | Effect |
692
697
  |---|---|
693
- | `QUICKCHR_DATA_DIR` | Override the data root (defaults to `$XDG_DATA_HOME/quickchr` or platform equivalent — see §8) |
698
+ | `QUICKCHR_DATA_DIR` | Override the data root (defaults to `~/.local/share/quickchr` on POSIX or the Windows path — see §8) |
694
699
  | `QUICKCHR_NO_PROMPT` | Force non-interactive mode (skips all wizards) |
695
700
  | `QUICKCHR_DEBUG` | Emit `[debug]` lines from the progress logger |
696
701
  | `QUICKCHR_INTEGRATION` | Required to run `test/integration/` |
@@ -1257,10 +1262,10 @@ restrictions, half-open SLiRP under TCG), see `docs/networking.md`.
1257
1262
 
1258
1263
  ## 8. Storage layout
1259
1264
 
1260
- ### macOS / Linux (XDG)
1265
+ ### macOS / Linux
1261
1266
 
1262
1267
  ```text
1263
- $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
1268
+ ~/.local/share/quickchr/
1264
1269
  ├── .allocate.lock/ held while a new machine picks its ports/MACs
1265
1270
  ├── cache/ Downloaded RouterOS images
1266
1271
  │ ├── chr-7.22.1.img.zip
@@ -1283,8 +1288,8 @@ $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
1283
1288
  └── <name>.json
1284
1289
  ```
1285
1290
 
1286
- `$XDG_DATA_HOME` defaults to `~/.local/share/quickchr/`. Override the
1287
- whole tree with `QUICKCHR_DATA_DIR=/some/path`.
1291
+ The data root is `~/.local/share/quickchr/`; `XDG_DATA_HOME` does not
1292
+ change it. Override the whole tree with `QUICKCHR_DATA_DIR=/some/path`.
1288
1293
 
1289
1294
  **Global settings** live separately, under the XDG **config** tier, not this
1290
1295
  data tree: `~/.config/quickchr/quickchr.env` (dotenv-style). See §3's
@@ -1581,12 +1586,16 @@ ceiling.
1581
1586
 
1582
1587
  ### Boot disk
1583
1588
 
1584
- Default format is **raw** (the image MikroTik ships). `--boot-size`
1585
- auto-converts to **qcow2** before the first boot and resizes to the
1586
- requested size. Requires `qemu-img` on the host.
1589
+ The CLI and wizard default to **qcow2**, converted from the raw MikroTik
1590
+ image before first boot. Creating qcow2 requires `qemu-img` even without resize.
1591
+ `--boot-size` resizes qcow2 to the requested size; explicitly selecting raw
1592
+ with a boot size is refused. Without `--boot-size`, qcow2 keeps the original
1593
+ image size and enables snapshots.
1587
1594
 
1588
- `--boot-disk-format=qcow2` (without `--boot-size`) just keeps the
1589
- default size but enables snapshots.
1595
+ Library callers default to **raw** unless `bootSize` implies qcow2. CLI users
1596
+ can select raw explicitly with `--boot-disk-format raw`. On x86, this permits
1597
+ creation without `qemu-img` when no resize or extra disks are requested. ARM64
1598
+ also needs `qemu-img` for its writable UEFI firmware disk.
1590
1599
 
1591
1600
  ### Extra disks
1592
1601
 
package/README.md CHANGED
@@ -1,11 +1,5 @@
1
1
  # quickchr
2
2
 
3
- > **Experimental** — quickchr is under active development. Most testing has been on
4
- > macOS (Apple Silicon + Intel), with CI on Linux (x86_64 + aarch64). Windows support
5
- > is early-stage: unit tests pass on `windows-latest` CI, and a weekly sweep boots a real
6
- > CHR on Windows (TCG, anchor smoke subset only). Expect rough edges on Windows — bug
7
- > reports welcome.
8
-
9
3
  CLI and library to download, launch, and manage MikroTik CHR virtual machines via QEMU.
10
4
 
11
5
  ## Quick Start
@@ -14,7 +8,7 @@ CLI and library to download, launch, and manage MikroTik CHR virtual machines vi
14
8
 
15
9
  - [Bun](https://bun.sh) runtime
16
10
  - [QEMU](https://www.qemu.org) with `qemu-system-x86_64` and/or `qemu-system-aarch64`
17
- - `qemu-img` for disk resize and extra-disk features (`--boot-size`, `--add-disk`, `quickchr disk`)
11
+ - `qemu-img` for default qcow2 creation, the setup wizard, disk resize, extra disks, and snapshots
18
12
  - UEFI firmware (edk2) for arm64 CHR
19
13
 
20
14
  Install QEMU:
@@ -60,7 +54,7 @@ bun add @tikoci/quickchr
60
54
  ### Prerelease QA (0.5.1)
61
55
 
62
56
  0.5.1 is published to npm `next` first; the unqualified install above uses `latest`,
63
- and 0.5.1 is promoted there once QA passes. After 0.5.1 is published, pin it for
57
+ and 0.5.1 is promoted there once QA passes. Pin this published candidate for
64
58
  reproducible CLI or library QA:
65
59
 
66
60
  ```bash
@@ -77,7 +71,8 @@ The [0.5.0 notes](./CHANGELOG.md#050--2026-10-22) cover migration from 0.4.x.
77
71
  ### CLI Usage
78
72
 
79
73
  The easiest way to start is the interactive wizard — it walks through
80
- every option and starts the CHR for you:
74
+ every option and starts the CHR for you. It requires `qemu-img` and reports
75
+ a missing tool before prompting or downloading:
81
76
 
82
77
  ```bash
83
78
  quickchr setup
@@ -201,6 +196,7 @@ reports a forced power cut if the guest does not stop within the grace budget.
201
196
  | `--install-all-packages` | Install all packages from `all_packages.zip` | |
202
197
  | `--add-user <user:pass>` | Create a user after boot | |
203
198
  | `--disable-admin` | Disable the default admin account | |
199
+ | `--secure-login` | Create a managed login; use `--disable-admin` to disable admin | |
204
200
  | `--no-secure-login` | Keep admin with no password (skip managed account creation) | |
205
201
  | `--add-network <spec>` | Add a network NIC (repeatable). Specs: `user`, `shared`, `bridged:<iface>`, `socket::<name>`, `tap:<iface>`. Default: single user NIC. | |
206
202
  | `--no-network` | Start with no NICs (headless) | |
@@ -260,7 +256,8 @@ In **foreground** mode, your terminal becomes the CHR serial console. Use these
260
256
 
261
257
  ### Disk Support
262
258
 
263
- - The CHR boot image starts as raw. Using `--boot-size` converts it to qcow2 and resizes it before first boot.
259
+ - CLI and wizard boot disks default to qcow2, converted from the raw MikroTik image using `qemu-img`. Library callers default to raw unless `bootSize` implies qcow2.
260
+ - `--boot-size` resizes qcow2 before first boot; explicitly combining raw with a boot size is refused.
264
261
  - Extra disks from `--add-disk` are always created as blank qcow2 images.
265
262
  - `quickchr clean <name>` removes any resized boot disk and extra disks, then recreates them from the saved machine config.
266
263
  - `quickchr disk <name>` shows the stored disk layout. If `qemu-img` is installed, it also shows virtual and actual sizes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/quickchr",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "CLI and library to download, launch, and manage MikroTik CHR virtual machines via QEMU",
5
5
  "module": "src/index.ts",
6
6
  "main": "src/index.ts",
@@ -15,6 +15,7 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
+ ".bun-version",
18
19
  "src",
19
20
  "README.md",
20
21
  "DESIGN.md",
package/src/cli/index.ts CHANGED
@@ -3233,7 +3233,7 @@ options still work on older versions when the required host tools are installed.
3233
3233
  ${ROUTEROS6_HELP}
3234
3234
 
3235
3235
  Options:
3236
- --name <name> Instance name (required)
3236
+ --name <name> Instance name (auto-generated if omitted)
3237
3237
  --version <ver> RouterOS version (e.g., 7.22.1)
3238
3238
  --channel <ch> Channel: stable, long-term, testing, development
3239
3239
  --arch <arch> Architecture: arm64, x86, auto (default: host native)
@@ -3257,6 +3257,7 @@ Options:
3257
3257
  --install-all-packages Install all packages on first boot
3258
3258
  --add-user <u:p> Create user on first boot (name:password)
3259
3259
  --disable-admin Disable the default admin account on first boot
3260
+ --secure-login Create a managed login; use --disable-admin to disable admin
3260
3261
  --no-secure-login Keep admin with no password (skip managed account)
3261
3262
  --port-base <port> Starting port number (default: auto-allocated from 9100)
3262
3263
  --no-winbox Exclude WinBox port mapping
@@ -3319,7 +3320,7 @@ ${CENTRS_SEE_ALSO}`);
3319
3320
  case "start":
3320
3321
  console.log(`quickchr start [<name>] [options]
3321
3322
 
3322
- <name> Restart an existing stopped machine by name.
3323
+ <name> Restart a stopped machine; an unknown name creates a fresh machine.
3323
3324
  Omit to see a list of stopped machines.
3324
3325
 
3325
3326
  Creation flags below apply when using 'start' to create a new machine in one step.
@@ -3367,6 +3368,7 @@ Options:
3367
3368
  btest:9200-9210:2000-2010/udp.
3368
3369
  --add-user <u:p> Create user with name:password
3369
3370
  --disable-admin Disable the default admin account
3371
+ --secure-login Create a managed login; use --disable-admin to disable admin
3370
3372
  --no-secure-login Keep admin with no password (skip managed account)
3371
3373
  --port-base <port> Starting port number (default: auto-allocated from 9100)
3372
3374
  --no-winbox Exclude WinBox port mapping
package/src/cli/wizard.ts CHANGED
@@ -37,6 +37,9 @@ export function resolveUserChoiceOptions(
37
37
 
38
38
  /** Run the interactive wizard using @clack/prompts. */
39
39
  export async function runWizard(wizardOpts?: { firstRun?: boolean }): Promise<void> {
40
+ const { requireQemuImg } = await import("../lib/platform.ts");
41
+ requireQemuImg();
42
+
40
43
  // Dynamic import — only loaded when wizard is actually used
41
44
  const clack = await import("@clack/prompts");
42
45
  const { QuickCHR } = await import("../lib/quickchr.ts");
@@ -148,75 +151,68 @@ export async function runWizard(wizardOpts?: { firstRun?: boolean }): Promise<vo
148
151
  });
149
152
  if (clack.isCancel(mem)) { clack.cancel("Cancelled."); process.exit(0); }
150
153
 
151
- // 4b. Disk configuration — requires qemu-img; skip gracefully if absent
154
+ // 4b. Disk configuration — qemu-img was checked before any prompts
152
155
  let bootSize: string | undefined;
153
156
  let bootDiskFormat: "qcow2" | "raw" = "qcow2";
154
157
  const extraDisks: string[] = [];
155
158
 
156
- const { findQemuImg, getQemuInstallHint } = await import("../lib/platform.ts");
157
159
  const { isValidDiskSize } = await import("../lib/disk.ts");
158
- const qemuImg = findQemuImg();
160
+ const diskFormatChoice = await clack.select({
161
+ message: "Boot disk format:",
162
+ options: [
163
+ { value: "qcow2", label: "qcow2 (recommended)", hint: "supports snapshots and resize" },
164
+ { value: "raw", label: "raw", hint: "no resize support" },
165
+ ],
166
+ initialValue: "qcow2",
167
+ });
168
+ if (clack.isCancel(diskFormatChoice)) { clack.cancel("Cancelled."); process.exit(0); }
169
+ bootDiskFormat = diskFormatChoice as "qcow2" | "raw";
159
170
 
160
- if (!qemuImg) {
161
- clack.log.warn(`Disk features (boot resize, extra disks) require qemu-img. Run 'quickchr doctor' to confirm setup. ${getQemuInstallHint()}`);
162
- } else {
163
- const diskFormatChoice = await clack.select({
164
- message: "Boot disk format:",
165
- options: [
166
- { value: "qcow2", label: "qcow2 (recommended)", hint: "supports snapshots and resize" },
167
- { value: "raw", label: "raw", hint: "no resize support" },
168
- ],
169
- initialValue: "qcow2",
171
+ if (bootDiskFormat === "qcow2") {
172
+ const wantBootResize = await clack.confirm({
173
+ message: "Resize the boot disk? (default is ~128 MB)",
174
+ initialValue: false,
170
175
  });
171
- if (clack.isCancel(diskFormatChoice)) { clack.cancel("Cancelled."); process.exit(0); }
172
- bootDiskFormat = diskFormatChoice as "qcow2" | "raw";
173
-
174
- if (bootDiskFormat === "qcow2") {
175
- const wantBootResize = await clack.confirm({
176
- message: "Resize the boot disk? (default is ~128 MB)",
177
- initialValue: false,
176
+ if (clack.isCancel(wantBootResize)) { clack.cancel("Cancelled."); process.exit(0); }
177
+
178
+ if (wantBootResize) {
179
+ const size = await clack.text({
180
+ message: "Boot disk size (e.g., 512M, 1G, 2G):",
181
+ validate: (v) => {
182
+ if (!isValidDiskSize(v)) return "Size like 512M, 1G, or 2048";
183
+ },
178
184
  });
179
- if (clack.isCancel(wantBootResize)) { clack.cancel("Cancelled."); process.exit(0); }
180
-
181
- if (wantBootResize) {
182
- const size = await clack.text({
183
- message: "Boot disk size (e.g., 512M, 1G, 2G):",
184
- validate: (v) => {
185
- if (!isValidDiskSize(v)) return "Size like 512M, 1G, or 2048";
186
- },
187
- });
188
- if (clack.isCancel(size)) { clack.cancel("Cancelled."); process.exit(0); }
189
- bootSize = size.trim();
190
- }
191
- } else {
192
- clack.log.info("Raw boot disk selected — boot resize is disabled.");
185
+ if (clack.isCancel(size)) { clack.cancel("Cancelled."); process.exit(0); }
186
+ bootSize = size.trim();
193
187
  }
188
+ } else {
189
+ clack.log.info("Raw boot disk selected — boot resize is disabled.");
190
+ }
194
191
 
195
- const wantExtraDisks = await clack.confirm({
196
- message: "Add extra blank disks?",
197
- initialValue: false,
198
- });
199
- if (clack.isCancel(wantExtraDisks)) { clack.cancel("Cancelled."); process.exit(0); }
200
-
201
- if (wantExtraDisks) {
202
- let addMore = true;
203
- while (addMore) {
204
- const diskSize = await clack.text({
205
- message: `Extra disk ${extraDisks.length + 1} size (e.g., 64M, 512M, 1G):`,
206
- validate: (v) => {
207
- if (!isValidDiskSize(v)) return "Size like 64M, 512M, or 1G";
208
- },
209
- });
210
- if (clack.isCancel(diskSize)) { clack.cancel("Cancelled."); process.exit(0); }
211
- extraDisks.push(diskSize.trim());
192
+ const wantExtraDisks = await clack.confirm({
193
+ message: "Add extra blank disks?",
194
+ initialValue: false,
195
+ });
196
+ if (clack.isCancel(wantExtraDisks)) { clack.cancel("Cancelled."); process.exit(0); }
197
+
198
+ if (wantExtraDisks) {
199
+ let addMore = true;
200
+ while (addMore) {
201
+ const diskSize = await clack.text({
202
+ message: `Extra disk ${extraDisks.length + 1} size (e.g., 64M, 512M, 1G):`,
203
+ validate: (v) => {
204
+ if (!isValidDiskSize(v)) return "Size like 64M, 512M, or 1G";
205
+ },
206
+ });
207
+ if (clack.isCancel(diskSize)) { clack.cancel("Cancelled."); process.exit(0); }
208
+ extraDisks.push(diskSize.trim());
212
209
 
213
- const another = await clack.confirm({
214
- message: "Add another disk?",
215
- initialValue: false,
216
- });
217
- if (clack.isCancel(another)) { clack.cancel("Cancelled."); process.exit(0); }
218
- addMore = another;
219
- }
210
+ const another = await clack.confirm({
211
+ message: "Add another disk?",
212
+ initialValue: false,
213
+ });
214
+ if (clack.isCancel(another)) { clack.cancel("Cancelled."); process.exit(0); }
215
+ addMore = another;
220
216
  }
221
217
  }
222
218
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readdirSync, statSync } from "node:fs";
6
- import type { AccelMode, EfiFirmwarePaths, HostInterface, PackageManager, PlatformInfo, SocketVmnetInfo } from "./types.ts";
6
+ import type { AccelMode, EfiFirmwarePaths, HostInterface, PackageManager, PlatformInfo, SocketVmnetInfo, DoctorCheck } from "./types.ts";
7
7
  import { QuickCHRError as QError, parseAccelMode } from "./types.ts";
8
8
  // settings-file.ts, not settings.ts: the latter's graph (cache → state → network
9
9
  // → platform) is a cycle that would load eagerly on every CLI start.
@@ -590,3 +590,15 @@ export function resolveInterfaceAlias(
590
590
  `No interface found for alias '${alias}' — run 'quickchr doctor' to see available interfaces`,
591
591
  );
592
592
  }
593
+
594
+ /** Compare the running Bun against the same pin CI installs. */
595
+ export async function bunRuntimeCheck(version = Bun.version): Promise<DoctorCheck> {
596
+ const pin = (await Bun.file(new URL("../../.bun-version", import.meta.url)).text()).trim();
597
+ // Runtimes predating the Semver API are also older than the tested pin.
598
+ const older = typeof Bun.semver?.order !== "function" || Bun.semver.order(version, pin) < 0;
599
+ return {
600
+ label: "Bun runtime",
601
+ status: older ? "warn" : "ok",
602
+ detail: `bun ${version}; tested with Bun ${pin}${older ? ` — upgrade to ${pin} or newer` : ""}`,
603
+ };
604
+ }
@@ -30,7 +30,7 @@ import type {
30
30
  import { QuickCHRError, ARCHES, CHANNELS, SERVICE_IDS, QUICKCHR_DESCRIPTOR_VERSION, HOST_GATEWAY_IP, PORTS_PER_BLOCK } from "./types.ts";
31
31
  import { assertValidResourceName, assertPathSafeName, shellQuote } from "./names.ts";
32
32
  import packageJson from "../../package.json";
33
- import { detectPlatform, requireQemu, requireFirmware, getQemuVersion, getQemuInstallHint, isCrossArchEmulation, accelTimeoutFactor, detectAccel, accelNote, resolveAccelOverrideWithSource, accelSourceLabel, findQemuImg, qgaKvmWarning, detectSocketVmnet, isSocketVmnetDaemonRunning, findCommandOnPath } from "./platform.ts";
33
+ import { bunRuntimeCheck, detectPlatform, requireQemu, requireFirmware, getQemuVersion, getQemuInstallHint, isCrossArchEmulation, accelTimeoutFactor, detectAccel, accelNote, resolveAccelOverrideWithSource, accelSourceLabel, findQemuImg, qgaKvmWarning, detectSocketVmnet, isSocketVmnetDaemonRunning, findCommandOnPath } from "./platform.ts";
34
34
  import {
35
35
  resolveVersion,
36
36
  isValidVersion,
@@ -2997,12 +2997,7 @@ export class QuickCHR {
2997
2997
  static async doctor(): Promise<DoctorResult> {
2998
2998
  const checks: DoctorResult["checks"] = [];
2999
2999
 
3000
- // Bun version
3001
- checks.push({
3002
- label: "Bun runtime",
3003
- status: "ok",
3004
- detail: `bun ${Bun.version}`,
3005
- });
3000
+ checks.push(await bunRuntimeCheck());
3006
3001
 
3007
3002
  // QEMU for each arch
3008
3003
  for (const arch of ARCHES) {
@@ -3125,7 +3120,7 @@ export class QuickCHR {
3125
3120
  });
3126
3121
  }
3127
3122
 
3128
- // qemu-img (optional, for disk resize and extra disks)
3123
+ // qemu-img (default qcow2 creation and disk features)
3129
3124
  const qemuImgPath = findQemuImg();
3130
3125
  if (qemuImgPath) {
3131
3126
  checks.push({
@@ -3137,7 +3132,7 @@ export class QuickCHR {
3137
3132
  checks.push({
3138
3133
  label: "qemu-img",
3139
3134
  status: "warn",
3140
- detail: `not found — required for --boot-size and --add-disk (${getQemuInstallHint()})`,
3135
+ detail: `not found — required for default qcow2 creation, setup, --boot-size and --add-disk (${getQemuInstallHint()})`,
3141
3136
  });
3142
3137
  }
3143
3138