@tikoci/quickchr 0.5.0 → 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
@@ -144,7 +144,8 @@ Current consumers are all first-party tikoci projects (centrs, donny, restraml),
144
144
  ├── machines/
145
145
  │ └── 7.22.1-arm64-1/
146
146
  │ ├── machine.json # Config + state
147
- │ ├── disk.img # Working copy
147
+ │ ├── boot.qcow2 # Boot disk, qcow2 — or disk.img when raw (see below)
148
+ │ ├── disk<N>.qcow2 # Extra disks
148
149
  │ ├── efi-vars.qcow2 # UEFI vars (arm64; qcow2 so savevm works — #31)
149
150
  │ ├── monitor.sock # QEMU monitor
150
151
  │ ├── serial.sock # Serial console
@@ -156,12 +157,60 @@ Current consumers are all first-party tikoci projects (centrs, donny, restraml),
156
157
  └── boot-failure-<machine>-<iso8601>.json
157
158
  ```
158
159
 
160
+ **One boot disk per machine.** A raw machine boots `disk.img`, a copy of the cached
161
+ image. A qcow2 machine boots `boot.qcow2`, converted straight from the cached image
162
+ into a `.tmp` file, checked to be a standalone qcow2 (no backing file), then renamed
163
+ into place. Machines created before 0.5.1 also kept the raw `disk.img` the qcow2 came
164
+ from: a full-size copy (128 MiB x86) that nothing reads. Restart no longer requires it
165
+ (#200). `clean` removes it, and nothing else deletes it implicitly (#199). The shared
166
+ cache image stays: `clean` and a failed first boot's rollback rebuild from it.
167
+
168
+ A missing disk is rebuilt only while the machine has never booted. A booted machine's
169
+ disks hold guest state, so a missing `boot.qcow2`, `disk.img` or extra disk is
170
+ `MACHINE_NOT_FOUND`, never a silent rebuild from factory. The way out is to restore the
171
+ file or run `quickchr clean`.
172
+
173
+ **Storage admission (#199).** An operation that writes new data (creating a machine,
174
+ an image or package download) is admitted when **each filesystem it writes to** has
175
+ `estimate + storage-reserve` free there. It replaced a flat 10 GiB floor that refused
176
+ small hosts which had room. The operation's writes are parts with paths (boot disk →
177
+ `machines/`, downloads → the cache it writes to), grouped by `stat().dev`. The estimate
178
+ is a per-stage constant (`STORAGE_ESTIMATES` in `storage.ts`), rounded up from one
179
+ host's measurements and written down there. The reserve (default 2 GiB) absorbs the
180
+ estimate's error and keeps the host usable. Restart is never gated. Virtual disk
181
+ capacity is not charged: a sparse disk's growth is the guest's, after admission.
182
+
183
+ Under pressure, eligible cache entries are evicted, but only from a cache on the short
184
+ filesystem, and only when everything eligible could cover the gap. Free space is then
185
+ **measured again**, in passes, until the measurement admits or nothing more can be
186
+ evicted. Logical sizes are not proof of room (sparse files, APFS clones).
187
+
188
+ **Coordination** (`storage-lease.ts`, after Codex's review of #203 reproduced both
189
+ races). One data-dir lock, held for milliseconds: a check-and-record, or an eviction
190
+ pass. Under it, an operation records a **lease** naming the cache entries it is about
191
+ to read (pins) and the bytes it is about to write, per filesystem. Admission subtracts
192
+ other live leases' bytes from measured free space, so two creators cannot spend the
193
+ same bytes. Eviction skips pinned entries. A pin is recorded before the reader checks
194
+ the entry exists, so an eviction either sees it or has already finished, and then the
195
+ reader fetches the entry again. A create's lease lasts until its `machine.json` is
196
+ saved, which protects the image from then on. A download's lease lasts the download.
197
+ A dead pid's lease is ignored and deleted. A create's own downloads are checked up
198
+ front, so it refuses early, but are reserved only by the download itself, so they are
199
+ not counted twice. Automatic eviction also skips anything used in the last 30
200
+ minutes (a cache hit bumps its mtime). That rule is not what protects a reader; pins
201
+ are. It spares what a finished `cache add` fetched for a later process.
202
+
203
+ The cache cap (`cache-max-size`) is retention, separate from admission. It counts
204
+ images, package archives with their extractions (one entry: the extraction is only a
205
+ hit while its archive exists) and interrupted downloads. Files quickchr did not write
206
+ are reported and never deleted. A failed delete is reported as failed, not freed.
207
+
159
208
  **Global settings** live separately, under the XDG **config** tier (not the data tree
160
209
  above): `~/.config/quickchr/quickchr.env`, dotenv-style (`QUICKCHR_KEY=value` lines).
161
210
  Managed via `quickchr settings get|set|print|reset`. Precedence per key: CLI flag >
162
- `QUICKCHR_<KEY>` env var > `quickchr.env` > built-in default. The 6 managed keys:
163
- `default-channel`, `default-arch`, `accel`, `cache-max-size`, `timeout-extra`,
164
- `secure-login`.
211
+ `QUICKCHR_<KEY>` env var > `quickchr.env` > built-in default. The 7 managed keys:
212
+ `default-channel`, `default-arch`, `accel`, `cache-max-size`, `storage-reserve`,
213
+ `timeout-extra`, `secure-login`.
165
214
  See MANUAL.md's CLI reference and environment-variables sections for the full surface.
166
215
 
167
216
  ## Platform Support
@@ -196,7 +245,9 @@ CI, integration and release all install, enforced by `scripts/check-bun-pin.ts`
196
245
  runtime could change between two identical commits — and did: a six-week gap with no `ci.yml`
197
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
198
247
  were its own regression. Anything that can turn the build red is a dependency and belongs in
199
- 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.
200
251
 
201
252
  **The 2026-07 refactor (#29)** replaced the organically-grown scheme (integration on every
202
253
  PR push, three parallel copies of the runner logic, `continue-on-error` green-washing)
@@ -229,9 +280,10 @@ with a layered design:
229
280
  - **`release.yml`** — one-click publish from committed release state: freshness gate
230
281
  (no suite re-run — main is kept continuously release-able) + `package.json` version
231
282
  with a matching non-empty CHANGELOG section → tag + GitHub Release + `npm publish
232
- --provenance` (odd minor → `next`, even → `latest`; a stable release is then also
233
- tagged `next`, since there is no separately maintained pre-release line and `next`
234
- must not point at something older than `latest`). CI never bumps versions or pushes
283
+ --provenance` under the dispatch-time `npm-tag` (default `next`; promoted to `latest`
284
+ after QA — not derived from the version number; a `latest` release is also tagged
285
+ `next`, since there is no separately maintained pre-release line and `next` must not
286
+ point at something older than `latest`). CI never bumps versions or pushes
235
287
  to protected `main`.
236
288
  - **`ros-versions.yml`** — daily: new RouterOS versions (per channel) with no linux-x86
237
289
  record in `ci-data/tested-versions.json` ride the `routeros-targets` matrix of a
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 |
@@ -72,31 +72,40 @@ by the user (or their AI agent) using `quickchr` as a building block.
72
72
 
73
73
  ### Install quickchr
74
74
 
75
- The first GitHub release is **0.1.1** — pre-release per the changelog's
76
- odd/even policy. Not yet on npm.
75
+ Install the stable npm channel:
77
76
 
78
77
  ```bash
79
- # Library
80
- bun add github:tikoci/quickchr
78
+ bun install -g @tikoci/quickchr
79
+ bun add @tikoci/quickchr
80
+ ```
81
81
 
82
- # CLI from source (until npm publish)
83
- git clone https://github.com/tikoci/quickchr
84
- cd quickchr
85
- bun install
86
- bun run dev -- doctor
82
+ For prerelease QA, use `@next` for the current candidate or, after publication,
83
+ `@0.5.1` to pin this candidate. A release is published to `next` first and promoted
84
+ to `latest` once QA passes; `next` is never older than `latest`. The quickchr package
85
+ channel is independent of `--channel`, which selects a RouterOS firmware channel.
86
+
87
+ ```bash
88
+ bun install -g @tikoci/quickchr@0.5.1
89
+ bun add @tikoci/quickchr@0.5.1
90
+ quickchr --version
87
91
  ```
88
92
 
89
- Once `0.2.0` is published to npm:
93
+ To run an unpublished checkout:
90
94
 
91
95
  ```bash
92
- bun install -g @tikoci/quickchr
96
+ git clone https://github.com/tikoci/quickchr
97
+ cd quickchr
98
+ bun install
99
+ bun run dev -- doctor
93
100
  ```
94
101
 
95
102
  ### `quickchr doctor`
96
103
 
97
104
  Reports prerequisite status, accelerator availability, image cache
98
105
  size, machine count, free disk space, and shell-completion install
99
- state. Exit code is `0` when no checks return `error` (warnings are OK).
106
+ state. The disk check measures the data dir's filesystem and warns when free
107
+ space is under `storage-reserve` + 2 GiB. Exit code is `0` when no checks return
108
+ `error` (warnings are OK).
100
109
 
101
110
  ```bash
102
111
  quickchr doctor # human-readable
@@ -107,7 +116,9 @@ The `--json` form emits `{ ok, checks, staleImages }`, where `checks` is an
107
116
  array of `{ label, status, detail }` and `staleImages` lists cached images older
108
117
  than the current long-term release. The exit code still reflects `ok`.
109
118
 
110
- ![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)
111
122
 
112
123
  Use this whenever a `start` fails — the diagnostic almost always points
113
124
  at a missing dependency, missing firmware, or insufficient disk.
@@ -116,7 +127,7 @@ at a missing dependency, missing firmware, or insufficient disk.
116
127
 
117
128
  ## 3. CLI command reference
118
129
 
119
- 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
120
131
  available for every command via `quickchr help <command>`.
121
132
 
122
133
  ### Lifecycle
@@ -129,14 +140,14 @@ directory, downloads the RouterOS image (cached), and writes
129
140
 
130
141
  | Flag | Default | Notes |
131
142
  |---|---|---|
132
- | `--name <s>` | auto | `host-arch-N` if omitted |
143
+ | `--name <s>` | auto | `version-arch-N` if omitted |
133
144
  | `--version <ver>` | latest in channel | e.g., `7.22.1` |
134
145
  | `--channel <ch>` | `stable` | `stable`, `long-term`, `testing`, `development` |
135
146
  | `--arch <a>` | host native | `arm64`, `x86`, or `auto` |
136
147
  | `--cpu <n>` | `1` | vCPU count |
137
148
  | `--mem <mb>` | `512` | RAM (cross-arch TCG bumps to `1024`) |
138
149
  | `--boot-disk-format <f>` | `qcow2` | `qcow2` or `raw` |
139
- | `--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 |
140
151
  | `--add-disk <size>` | — | Extra blank qcow2 disk; repeatable |
141
152
  | `--forward <spec>` | — | Extra hostfwd port or built-in service host-port pin; repeatable. See §Custom port forwards. |
142
153
  | `--add-package <pkg>` | — | Provisioning, repeatable, ≥ 7.20.8 |
@@ -150,6 +161,7 @@ directory, downloads the RouterOS image (cached), and writes
150
161
  | `--device-mode <m>` | — | `rose`, `advanced`, `basic`, `home`, `auto`, `skip`, ≥ 7.20.8 |
151
162
  | `--device-mode-enable <f,...>` | — | Repeatable, comma-separated |
152
163
  | `--device-mode-disable <f,...>` | — | Repeatable, comma-separated |
164
+ | `--no-device-mode` | — | Skip device-mode provisioning entirely |
153
165
  | `--add-network <spec>` | `user` | See §7. Repeatable. |
154
166
  | `--no-network` | false | Headless, no NICs |
155
167
  | `--no-winbox` | false | Exclude WinBox port |
@@ -168,15 +180,19 @@ stdio to your terminal (`--fg`). Accepts the same provisioning flags as `add`.
168
180
  QEMU runs detached in its own session either way, so it survives a signal to the
169
181
  shell that started it.
170
182
 
183
+ When `<name>` matches no machine (a typo, or one removed earlier), `start` creates a
184
+ factory CHR under that name and says so: `<name> created and started`, or
185
+ `created — not waiting for RouterOS` with `--no-wait`. A machine that already
186
+ existed, including one made by `add`, prints `started`.
187
+
171
188
  Additional flags:
172
189
 
173
190
  | Flag | Notes |
174
191
  |---|---|
175
- | `--all` | Start every stopped machine in background; accepts readiness, acceleration, timeout and secure-login options only. Other options are refused rather than ignored |
192
+ | `--all` | Start every stopped machine in background; accepts readiness, acceleration, timeout and secure-login options only. Other options are refused rather than ignored. `--secure-login` is resolved per machine: one that has never booted creates the managed login; one that already booted accepts it as a no-op only if it already has one, and is refused otherwise. With `--no-wait`, a never-booted machine refuses it (provisioning needs the wait). Machines start one at a time, so a refusal stops the batch after the earlier machines have started |
176
193
  | `--no-wait` | Return as soon as QEMU is running (past the 1.5 s early-exit check), with state and ports persisted; do not wait for RouterOS. Refused with any provisioning flag, including first-boot options `add` recorded, and with `--fg` |
177
194
  | `--fg` / `--foreground` (alias `--no-bg`, `--no-background`) | Serial console on stdio |
178
195
  | `--bg` / `--background` | **Deprecated, does nothing.** Background is the default, and the flag never made `start` return early — it only ever chose where the serial console went. Prints a warning; use `--no-wait` |
179
- | `--install-deps` | Auto-install missing host deps (calls package manager) |
180
196
  | `--timeout-extra <s>` / `-T <s>` | Add extra seconds to computed boot timeout |
181
197
  | `--dry-run` | Print resolved options and QEMU command, exit |
182
198
 
@@ -394,7 +410,8 @@ the Library API reference.
394
410
  #### `exec <name> <command> [--via=auto|rest|qga|console|ssh] [--user u] [--password p] [--timeout=ms]`
395
411
 
396
412
  Run a RouterOS CLI command. See §12 for transport details and JSON
397
- output trick.
413
+ output trick. Exit 0 means the command was delivered, not that RouterOS
414
+ accepted it — see §12 "Exit status" for the pass/fail route.
398
415
 
399
416
  #### `console <name>`
400
417
 
@@ -471,7 +488,7 @@ Interactive wizard (`@clack/prompts`) that walks through all machine creation
471
488
  options and starts the CHR. Triggered by `quickchr setup` or automatically
472
489
  when `quickchr` is run with no subcommand on a TTY.
473
490
 
474
- ![quickchr setup wizard walkthrough](./images/wizard-demo.gif)
491
+ ![quickchr setup wizard walkthrough](./docs/images/wizard-demo.gif)
475
492
 
476
493
  **First-run detection**: if no machines exist yet, the wizard opens with a
477
494
  welcome message before the first prompt.
@@ -498,7 +515,10 @@ another machine, view status, or quit.
498
515
  | 12 | Run mode: background / foreground | background | (default) / `--fg` |
499
516
  | 13 | Final confirmation | — | — |
500
517
 
501
- 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
502
522
  only appears when the platform supports networks beyond user-mode (macOS or
503
523
  Linux). The provisioning gate (step 11) only appears for RouterOS ≥ 7.20.8;
504
524
  older versions show an upgrade prompt.
@@ -567,6 +587,64 @@ appended directly to `$GITHUB_OUTPUT`.
567
587
  `--json` intentionally emits one JSON object for programmatic parsing; use the
568
588
  default output form when writing step outputs directly.
569
589
 
590
+ `cache list [--json]` shows each cache entry with its version, arch, size, age
591
+ (time since it was cached or last used) and whether it is in use. Entries are:
592
+
593
+ - **images** (`chr-<ver>[-arm64].img`, and a `.img.zip` not yet extracted). An
594
+ image is **in use** while any machine, running or stopped, was created from
595
+ that version and arch: `clean` and a failed first boot rebuild from it.
596
+ - **package archives** (`packages-<arch>-<ver>`): the `all_packages` ZIP and its
597
+ extraction, sized and deleted together, since the extraction is only a cache
598
+ hit while its ZIP is there. In use while a machine that asks for packages has
599
+ not finished its first boot.
600
+ - **partial** entries: an interrupted download (`*.<pid>-<id>.part`) or
601
+ extraction (`.quickchr-image-*`, `.quickchr-packages-*`). In use while the
602
+ process that wrote it is alive (an extraction: for 30 minutes after its last
603
+ write). `prune` removes stale ones before anything else.
604
+
605
+ `cache list --json` prints an array of entry objects, including `kind` (`image`, `image-zip`,
606
+ `packages` or `partial`) and `paths` (every file deleting the entry removes).
607
+ Files in the cache that quickchr did not write are reported as "Other files"
608
+ and never deleted. `prune` and `clear` never delete an in-use entry.
609
+
610
+ ```bash
611
+ quickchr cache list
612
+ quickchr cache prune --older-than 7.22 --dry-run # versions below 7.22
613
+ quickchr cache prune --max-age 30d # not used for more than 30 days
614
+ quickchr cache prune --max-size 2G # oldest versions first, until <= 2 GiB
615
+ quickchr cache clear --all # every entry not in use
616
+ ```
617
+
618
+ `prune` needs at least one of `--older-than <ver>`, `--max-age <dur>` or
619
+ `--max-size <size>`, and applies them in that order when combined. `--max-size`
620
+ evicts the oldest RouterOS version first (oldest cache time breaks ties) until
621
+ the total, in-use entries included, fits. Durations are `<N>(s|m|h|d|w)`; sizes
622
+ are `<N>(K|M|G|T)` with an optional `i`/`B`, read as IEC binary. `--dry-run`
623
+ lists what `prune` or `clear` would delete without deleting it. `clear` refuses
624
+ without `--all`. A delete that fails is listed on stderr, is not counted as
625
+ freed, and makes the command exit 1. A cache hit counts as a use, so `--max-age`
626
+ measures time since an entry was last used.
627
+
628
+ Separately, a successful `start` prunes to the `cache-max-size` setting (default
629
+ `2G`; see `settings` below). That automatic prune, and the one below, skip any
630
+ entry used in the last 30 minutes, which another create may be reading.
631
+
632
+ **Disk space.** Creating a machine (`add`, or `start` with a new name) and
633
+ filling the cache (an image or package download) are admitted when each
634
+ filesystem the operation writes to has room for what it writes there plus the
635
+ `storage-reserve` setting (default `2G`), less what other quickchr operations
636
+ in progress have already claimed. The estimate is the boot disk
637
+ (192 MiB), plus an image download (256 MiB) and a package download (192 MiB)
638
+ when those are not cached yet, so a cold create needs about 2.6 GiB free and a
639
+ warm one about 2.2 GiB. When there is not enough, quickchr first evicts what the
640
+ cache can spare on that filesystem (oldest version first; never entries a
641
+ machine or an operation in progress needs, nor recently used ones), measures
642
+ again until there is room or nothing more can go, and only then refuses with `INSUFFICIENT_DISK_SPACE`,
643
+ naming the free space, what it needed, what it evicted and what it kept.
644
+ Restarting an existing machine, `stop`, `remove` and read-only commands are
645
+ never gated. The estimate covers creation, not a guest that later fills its
646
+ disk: extra disks and `--boot-size` are sparse and grow as the guest writes.
647
+
570
648
  #### `settings [print|get|set|reset] [key] [value] [--json]`
571
649
 
572
650
  Manage quickchr's own global preferences — repeated flags/defaults that
@@ -583,6 +661,7 @@ Precedence per key (highest wins): CLI flag > `QUICKCHR_<KEY>` env var >
583
661
  | `default-arch` | `add`/`start`'s `--arch`, wizard's arch prompt | `arm64`\|`x86`\|`auto` | `auto` (host native) |
584
662
  | `accel` | `add`/`start`'s `--accel`; every `detectAccel()` call in the run | `auto`\|`tcg`\|`hvf`\|`kvm` | `auto` (detect) |
585
663
  | `cache-max-size` | Auto-prune cap applied after each successful `start` | size string (e.g. `2G`, `512M`) | `2G` |
664
+ | `storage-reserve` | Free space creating a machine or filling the cache must leave, beyond what it writes | size string | `2G` |
586
665
  | `timeout-extra` | `start`'s `--timeout-extra`/`-T` when omitted | non-negative integer seconds | `0` |
587
666
  | `secure-login` | `add`/`start`'s `--secure-login` when neither `--secure-login` nor `--no-secure-login` is passed | boolean | `unset` (reported as `(unset)` — see note) |
588
667
 
@@ -601,7 +680,7 @@ the wizard's own recommended "managed login" highlighted). Giving it a real
601
680
  the setting resolves to exactly `true`.
602
681
 
603
682
  ```bash
604
- quickchr settings print # all 6 keys, resolved value + source
683
+ quickchr settings print # all 7 keys, resolved value + source
605
684
  quickchr settings set default-channel testing # write one key to quickchr.env
606
685
  quickchr settings get default-arch # print one key's resolved value
607
686
  quickchr settings reset default-channel # delete one managed line
@@ -609,14 +688,14 @@ quickchr settings reset # clear all managed lines
609
688
  ```
610
689
 
611
690
  Credentials (`--add-user`, `MIKROTIK_WEB_ACCOUNT`/`MIKROTIK_WEB_PASSWORD`) are
612
- never stored here — `settings` only manages the 6 keys above, and does not
691
+ never stored here — `settings` only manages the 7 keys above, and does not
613
692
  mutate any machine's `machine.json`.
614
693
 
615
694
  ### Global behavior
616
695
 
617
696
  | Env var | Effect |
618
697
  |---|---|
619
- | `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) |
620
699
  | `QUICKCHR_NO_PROMPT` | Force non-interactive mode (skips all wizards) |
621
700
  | `QUICKCHR_DEBUG` | Emit `[debug]` lines from the progress logger |
622
701
  | `QUICKCHR_INTEGRATION` | Required to run `test/integration/` |
@@ -824,14 +903,14 @@ redact before logging or attaching to bug reports.
824
903
  | Field | Type | Default | Notes |
825
904
  |---|---|---|---|
826
905
  | `name` | string | auto | Must not start with `-` |
827
- | `version` | string | — | Mutually exclusive with `channel` |
906
+ | `version` | string | — | Wins over `channel`: passing both warns and uses `version` |
828
907
  | `channel` | Channel | `"stable"` | |
829
908
  | `arch` | `"arm64" \| "x86"` | host | |
830
909
  | `cpu` | number | `1` | |
831
910
  | `mem` | number | `512` (1024 cross-arch TCG) | MiB |
832
911
  | `background` | boolean | `true` | Where the serial console goes — not whether `start()` waits |
833
912
  | `wait` | boolean | `true` | `false` resolves once QEMU is running and leaves readiness to the caller (`instance.waitForBoot()`); refused with provisioning or `background: false` (`INVALID_ARGUMENT`) |
834
- | `bootDiskFormat` | `"raw" \| "qcow2"` | `"raw"` (auto-`qcow2` if `bootSize`) | |
913
+ | `bootDiskFormat` | `"raw" \| "qcow2"` | `"raw"` (auto-`qcow2` if `bootSize`) | The CLI always passes a format, defaulting to `qcow2`; the `"raw"` default applies to library callers only |
835
914
  | `bootSize` | string | — | e.g., `"512M"`, `"2G"`. Needs `qemu-img` |
836
915
  | `extraDisks` | string[] | `[]` | Each entry is a size string |
837
916
  | `networks` | NetworkSpecifier[] | `["user"]` | See §7 |
@@ -845,7 +924,7 @@ redact before logging or attaching to bug reports.
845
924
  | `secureLogin` | boolean | `false` | **`true` is the explicit trigger** for managed-account creation |
846
925
  | `license` | `LicenseInput` | — | `"p1"` / `"p10"` / `"unlimited"` or `LicenseOptions` |
847
926
  | `deviceMode` | `DeviceModeOptions` | — | `{mode?, enable?[], disable?[]}` |
848
- | `installDeps` | boolean | — | Auto-install missing OS deps |
927
+ | `installDeps` | boolean | — | **Deprecated, ignored.** Never installed anything; run `quickchr doctor` for install hints |
849
928
  | `dryRun` | boolean | `false` | Print QEMU command and exit |
850
929
  | `timeoutExtra` | number | `0` | Extra **milliseconds** added to boot timeout |
851
930
  | `onProgress` | `(msg) => void` | `console.log` | `[debug]` lines only when `QUICKCHR_DEBUG=1` |
@@ -1183,10 +1262,10 @@ restrictions, half-open SLiRP under TCG), see `docs/networking.md`.
1183
1262
 
1184
1263
  ## 8. Storage layout
1185
1264
 
1186
- ### macOS / Linux (XDG)
1265
+ ### macOS / Linux
1187
1266
 
1188
1267
  ```text
1189
- $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
1268
+ ~/.local/share/quickchr/
1190
1269
  ├── .allocate.lock/ held while a new machine picks its ports/MACs
1191
1270
  ├── cache/ Downloaded RouterOS images
1192
1271
  │ ├── chr-7.22.1.img.zip
@@ -1195,8 +1274,8 @@ $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
1195
1274
  ├── machines/
1196
1275
  │ └── <name>/
1197
1276
  │ ├── machine.json Persisted MachineState
1198
- │ ├── disk.img | disk.qcow2
1199
- │ ├── extra-disk-*.qcow2
1277
+ │ ├── boot.qcow2 | disk.img boot disk: qcow2 (CLI default) or raw — one, not both
1278
+ │ ├── disk<N>.qcow2 extra disks
1200
1279
  │ ├── efi-vars.fd arm64 only (UEFI vars)
1201
1280
  │ ├── monitor.sock QEMU monitor
1202
1281
  │ ├── serial.sock serial console
@@ -1209,8 +1288,8 @@ $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
1209
1288
  └── <name>.json
1210
1289
  ```
1211
1290
 
1212
- `$XDG_DATA_HOME` defaults to `~/.local/share/quickchr/`. Override the
1213
- 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`.
1214
1293
 
1215
1294
  **Global settings** live separately, under the XDG **config** tier, not this
1216
1295
  data tree: `~/.config/quickchr/quickchr.env` (dotenv-style). See §3's
@@ -1224,7 +1303,7 @@ var list.
1224
1303
  ├── cache\
1225
1304
  ├── machines\<name>\
1226
1305
  │ ├── machine.json
1227
- │ ├── disk.img
1306
+ │ ├── boot.qcow2 | disk.img
1228
1307
  │ ├── monitor (named pipe handle, no on-disk file)
1229
1308
  │ └── …
1230
1309
  └── …
@@ -1370,7 +1449,7 @@ managed via `quickchr settings get|set|print|reset` (§3, Meta). Atomic writes
1370
1449
  preserve every other line (comments, blank lines, foreign vars) byte-for-byte.
1371
1450
  `reset <key>` deletes the line entirely rather than blanking it, since a blank
1372
1451
  `KEY=` line is still "set" to a bash-sourced file. Never stores credentials —
1373
- the 6 managed keys are all structural preferences, not secrets.
1452
+ the 7 managed keys are all structural preferences, not secrets.
1374
1453
 
1375
1454
  ### Environment variables consumed
1376
1455
 
@@ -1387,6 +1466,7 @@ the 6 managed keys are all structural preferences, not secrets.
1387
1466
  | `QUICKCHR_DEFAULT_ARCH` | `settings.ts` — env tier for the `default-arch` setting |
1388
1467
  | `QUICKCHR_ACCEL` | `settings.ts` / `platform.ts` — env tier for the `accel` setting; forces `-accel` and bypasses detection |
1389
1468
  | `QUICKCHR_CACHE_MAX_SIZE` | `settings.ts` — env tier for the `cache-max-size` setting |
1469
+ | `QUICKCHR_STORAGE_RESERVE` | `settings.ts` — env tier for the `storage-reserve` setting |
1390
1470
  | `QUICKCHR_SHUTDOWN_TIMEOUT` | CLI `shutdown` grace budget in positive integer seconds; default 10, overridden by `--timeout` |
1391
1471
  | `QUICKCHR_TIMEOUT_EXTRA` | `settings.ts` — env tier for the `timeout-extra` setting |
1392
1472
  | `QUICKCHR_SECURE_LOGIN` | `settings.ts` — env tier for the `secure-login` setting |
@@ -1436,6 +1516,29 @@ funnel through the same transport selector.
1436
1516
  > (see [`docs/centrs-interface.md`](./docs/centrs-interface.md)) — it never reads
1437
1517
  > `machine.json`, and neither should you.
1438
1518
 
1519
+ ### Exit status
1520
+
1521
+ `quickchr exec` exits 0 when the command was **delivered**, not when RouterOS
1522
+ accepted it. `/rest/execute` returns HTTP 200 with the rejection text in the same
1523
+ envelope as a success, so a refused command looks like this:
1524
+
1525
+ ```console
1526
+ $ quickchr exec my-chr "/user/add name=-2 group=read password=x"; echo "rc=$?"
1527
+ failure: invalid user name (/user/add; line 1)
1528
+ rc=0
1529
+ ```
1530
+
1531
+ The library is the same: `chr.exec()` resolves, and `ExecResult` has no failure
1532
+ field. quickchr does not pattern-match RouterOS error text, whose wording changes
1533
+ between versions (#142). When a script needs a pass/fail exit code, run the
1534
+ command through centrs, which validates it first and exits non-zero on a rejection.
1535
+ A write command needs `--yes` outside a TTY; without it centrs refuses with
1536
+ `usage/confirmation-required` (also exit 1) and sends nothing:
1537
+
1538
+ ```bash
1539
+ centrs execute --quickchr my-chr --yes "/user/add name=-2 group=read password=x"
1540
+ ```
1541
+
1439
1542
  ### Transports
1440
1543
 
1441
1544
  | `--via` | Source | Notes |
@@ -1483,12 +1586,16 @@ ceiling.
1483
1586
 
1484
1587
  ### Boot disk
1485
1588
 
1486
- Default format is **raw** (the image MikroTik ships). `--boot-size`
1487
- auto-converts to **qcow2** before the first boot and resizes to the
1488
- 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.
1489
1594
 
1490
- `--boot-disk-format=qcow2` (without `--boot-size`) just keeps the
1491
- 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.
1492
1599
 
1493
1600
  ### Extra disks
1494
1601
 
@@ -1605,7 +1712,7 @@ present, then exits `1`.
1605
1712
  | `DOWNLOAD_FAILED` | image fetch failed | retry; check connectivity to MikroTik download server |
1606
1713
  | `INVALID_VERSION` / `INVALID_ARCH` / `INVALID_NAME` / `INVALID_DISK_SIZE` | argument parse | fix the value |
1607
1714
  | `MACHINE_EXISTS` | name already in `machines/` | choose another name or `QuickCHR.get(name)` |
1608
- | `MACHINE_NOT_FOUND` | name not in `machines/` | `quickchr list` |
1715
+ | `MACHINE_NOT_FOUND` | name not in `machines/`, or a booted machine's disk file is missing | `quickchr list`; for a missing disk, restore it or `quickchr clean <name>` |
1609
1716
  | `MACHINE_RUNNING` / `MACHINE_STOPPED` | wrong lifecycle state | `start` or `stop` first |
1610
1717
  | `MACHINE_LOCKED` | another operation holds the machine (the message names it, its pid and age) | wait for it; a `foreground` holder must be quit from its console |
1611
1718
  | `EXEC_FAILED` | RouterOS rejected the command (auth, syntax) | check creds, command syntax |
@@ -1618,7 +1725,7 @@ present, then exits `1`.
1618
1725
  | `PROVISIONING_VERSION_UNSUPPORTED` | provisioning option on RouterOS < 7.20.8 (including any 6.x) | use `--channel long-term` or pin a `7.20.8+` version |
1619
1726
  | `REST_UNAVAILABLE` | REST operation (`exec`, `get`, `rest()`) on a RouterOS 6 machine | use the native API — `centrs … --via native-api --no-validate` |
1620
1727
  | `INVALID_ARGUMENT` | a flag combination quickchr will not honour — e.g. `--no-wait` with provisioning | drop one of the two |
1621
- | `INSUFFICIENT_DISK_SPACE` | host data dir full | free space or move via `QUICKCHR_DATA_DIR` |
1728
+ | `INSUFFICIENT_DISK_SPACE` | data dir's filesystem has less than the operation's estimate plus `storage-reserve`, even after evicting what the cache could spare | free space, remove machines, lower `storage-reserve`, or move via `QUICKCHR_DATA_DIR` |
1622
1729
  | `STATE_ERROR` | snapshot on raw boot disk | recreate with `--boot-disk-format=qcow2` |
1623
1730
  | `INVALID_SIZE_STRING` | invalid cache/settings size string | use size syntax like `512M`, `2G`, or `1.5G` |
1624
1731
  | `INVALID_SETTING_KEY` | `settings get`/`set`/`reset` received an unrecognized key | `quickchr settings print` to see the managed keys |
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:
@@ -57,10 +51,28 @@ Library:
57
51
  bun add @tikoci/quickchr
58
52
  ```
59
53
 
54
+ ### Prerelease QA (0.5.1)
55
+
56
+ 0.5.1 is published to npm `next` first; the unqualified install above uses `latest`,
57
+ and 0.5.1 is promoted there once QA passes. Pin this published candidate for
58
+ reproducible CLI or library QA:
59
+
60
+ ```bash
61
+ bun install -g @tikoci/quickchr@0.5.1
62
+ bun add @tikoci/quickchr@0.5.1
63
+ quickchr --version
64
+ ```
65
+
66
+ Use `@next` instead of `@0.5.1` to follow later candidates. See the
67
+ [0.5.1 changelog](./CHANGELOG.md#051--2026-10-22) for the changes since 0.5.0,
68
+ including storage admission, cache accounting, and disk preservation fixes.
69
+ The [0.5.0 notes](./CHANGELOG.md#050--2026-10-22) cover migration from 0.4.x.
70
+
60
71
  ### CLI Usage
61
72
 
62
73
  The easiest way to start is the interactive wizard — it walks through
63
- 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:
64
76
 
65
77
  ```bash
66
78
  quickchr setup
@@ -184,6 +196,7 @@ reports a forced power cut if the guest does not stop within the grace budget.
184
196
  | `--install-all-packages` | Install all packages from `all_packages.zip` | |
185
197
  | `--add-user <user:pass>` | Create a user after boot | |
186
198
  | `--disable-admin` | Disable the default admin account | |
199
+ | `--secure-login` | Create a managed login; use `--disable-admin` to disable admin | |
187
200
  | `--no-secure-login` | Keep admin with no password (skip managed account creation) | |
188
201
  | `--add-network <spec>` | Add a network NIC (repeatable). Specs: `user`, `shared`, `bridged:<iface>`, `socket::<name>`, `tap:<iface>`. Default: single user NIC. | |
189
202
  | `--no-network` | Start with no NICs (headless) | |
@@ -195,6 +208,7 @@ reports a forced power cut if the guest does not stop within the grace budget.
195
208
  | `--device-mode <m>` | Configure device-mode: rose\|advanced\|basic\|home\|auto\|skip | |
196
209
  | `--device-mode-enable <f>` | Set one or more device-mode flags to yes | |
197
210
  | `--device-mode-disable <f>` | Set one or more device-mode flags to no | |
211
+ | `--no-device-mode` | Skip device-mode provisioning entirely | |
198
212
  | `--port-base <port>` | Starting port number | Auto (9100+) |
199
213
  | `--timeout-extra <s>` | Add extra seconds to the auto-computed boot timeout | |
200
214
  | `--dry-run` | Print what would run without executing | |
@@ -238,11 +252,12 @@ In **foreground** mode, your terminal becomes the CHR serial console. Use these
238
252
  | `Ctrl-A C` | Toggle QEMU monitor (`quit` to force-stop) |
239
253
  | `Ctrl-A H` | List all key shortcuts |
240
254
 
241
- > **Note:** Background QEMU processes are true OS-level orphans — `quickchr` does not use shell job control (`&`). After the command returns you can close the terminal and QEMU keeps running. Use `quickchr stop <name>` to shut it down cleanly.
255
+ > **Note:** Background QEMU processes are true OS-level orphans — `quickchr` does not use shell job control (`&`). After the command returns you can close the terminal and QEMU keeps running. `quickchr stop <name>` ends it with a power cut; `quickchr shutdown <name>` asks the guest to shut down first, and still falls back to a power cut if it does not exit in time.
242
256
 
243
257
  ### Disk Support
244
258
 
245
- - 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.
246
261
  - Extra disks from `--add-disk` are always created as blank qcow2 images.
247
262
  - `quickchr clean <name>` removes any resized boot disk and extra disks, then recreates them from the saved machine config.
248
263
  - `quickchr disk <name>` shows the stored disk layout. If `qemu-img` is installed, it also shows virtual and actual sizes.
@@ -437,7 +452,7 @@ afterAll(async () => {
437
452
  |----------|-------------|
438
453
  | `MIKROTIK_WEB_ACCOUNT` | MikroTik.com account email (for license renewal) |
439
454
  | `MIKROTIK_WEB_PASSWORD` | MikroTik.com password (for license renewal) |
440
- | `QUICKCHR_DEFAULT_CHANNEL`, `QUICKCHR_DEFAULT_ARCH`, `QUICKCHR_CACHE_MAX_SIZE`, `QUICKCHR_TIMEOUT_EXTRA`, `QUICKCHR_SECURE_LOGIN` | Env-tier overrides for the 5 `quickchr settings` keys — see [MANUAL.md §3 `settings`](./MANUAL.md#3-cli-command-reference) |
455
+ | `QUICKCHR_DEFAULT_CHANNEL`, `QUICKCHR_DEFAULT_ARCH`, `QUICKCHR_ACCEL`, `QUICKCHR_CACHE_MAX_SIZE`, `QUICKCHR_STORAGE_RESERVE`, `QUICKCHR_TIMEOUT_EXTRA`, `QUICKCHR_SECURE_LOGIN` | Env-tier overrides for the 7 `quickchr settings` keys — see [MANUAL.md §3 `settings`](./MANUAL.md#3-cli-command-reference) |
441
456
  | `QUICKCHR_INTEGRATION` | Set to `1` to run integration tests |
442
457
  | `QUICKCHR_TEST_TARGET` | Integration tests only: RouterOS channel (`stable`/`long-term`/`testing`/`development`) or pinned version (e.g. `7.22.1`) to boot. Empty = `stable`. The Integration workflow resolves each `routeros-target` once, then sets this to that exact pin across platforms |
443
458
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/quickchr",
3
- "version": "0.5.0",
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",
@@ -25,7 +26,7 @@
25
26
  "lint": "biome check . && tsc --noEmit",
26
27
  "lint:biome": "biome check .",
27
28
  "lint:typecheck": "tsc --noEmit",
28
- "lint:markdown": "bunx markdownlint-cli2 '**/*.md'",
29
+ "lint:markdown": "markdownlint-cli2 '**/*.md'",
29
30
  "lint:cspell": "cspell lint . --no-progress",
30
31
  "lint:examples": "bun run scripts/validate-examples.ts",
31
32
  "lint:specifiers": "bun run scripts/validate-network-specifiers.ts",