@tikoci/quickchr 0.5.0 → 0.5.1

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/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
@@ -229,9 +278,10 @@ with a layered design:
229
278
  - **`release.yml`** — one-click publish from committed release state: freshness gate
230
279
  (no suite re-run — main is kept continuously release-able) + `package.json` version
231
280
  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
281
+ --provenance` under the dispatch-time `npm-tag` (default `next`; promoted to `latest`
282
+ after QA — not derived from the version number; a `latest` release is also tagged
283
+ `next`, since there is no separately maintained pre-release line and `next` must not
284
+ point at something older than `latest`). CI never bumps versions or pushes
235
285
  to protected `main`.
236
286
  - **`ros-versions.yml`** — daily: new RouterOS versions (per channel) with no linux-x86
237
287
  record in `ci-data/tested-versions.json` ride the `routeros-targets` matrix of a
package/MANUAL.md CHANGED
@@ -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
@@ -150,6 +159,7 @@ directory, downloads the RouterOS image (cached), and writes
150
159
  | `--device-mode <m>` | — | `rose`, `advanced`, `basic`, `home`, `auto`, `skip`, ≥ 7.20.8 |
151
160
  | `--device-mode-enable <f,...>` | — | Repeatable, comma-separated |
152
161
  | `--device-mode-disable <f,...>` | — | Repeatable, comma-separated |
162
+ | `--no-device-mode` | — | Skip device-mode provisioning entirely |
153
163
  | `--add-network <spec>` | `user` | See §7. Repeatable. |
154
164
  | `--no-network` | false | Headless, no NICs |
155
165
  | `--no-winbox` | false | Exclude WinBox port |
@@ -168,15 +178,19 @@ stdio to your terminal (`--fg`). Accepts the same provisioning flags as `add`.
168
178
  QEMU runs detached in its own session either way, so it survives a signal to the
169
179
  shell that started it.
170
180
 
181
+ When `<name>` matches no machine (a typo, or one removed earlier), `start` creates a
182
+ factory CHR under that name and says so: `<name> created and started`, or
183
+ `created — not waiting for RouterOS` with `--no-wait`. A machine that already
184
+ existed, including one made by `add`, prints `started`.
185
+
171
186
  Additional flags:
172
187
 
173
188
  | Flag | Notes |
174
189
  |---|---|
175
- | `--all` | Start every stopped machine in background; accepts readiness, acceleration, timeout and secure-login options only. Other options are refused rather than ignored |
190
+ | `--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
191
  | `--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
192
  | `--fg` / `--foreground` (alias `--no-bg`, `--no-background`) | Serial console on stdio |
178
193
  | `--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
194
  | `--timeout-extra <s>` / `-T <s>` | Add extra seconds to computed boot timeout |
181
195
  | `--dry-run` | Print resolved options and QEMU command, exit |
182
196
 
@@ -394,7 +408,8 @@ the Library API reference.
394
408
  #### `exec <name> <command> [--via=auto|rest|qga|console|ssh] [--user u] [--password p] [--timeout=ms]`
395
409
 
396
410
  Run a RouterOS CLI command. See §12 for transport details and JSON
397
- output trick.
411
+ output trick. Exit 0 means the command was delivered, not that RouterOS
412
+ accepted it — see §12 "Exit status" for the pass/fail route.
398
413
 
399
414
  #### `console <name>`
400
415
 
@@ -567,6 +582,64 @@ appended directly to `$GITHUB_OUTPUT`.
567
582
  `--json` intentionally emits one JSON object for programmatic parsing; use the
568
583
  default output form when writing step outputs directly.
569
584
 
585
+ `cache list [--json]` shows each cache entry with its version, arch, size, age
586
+ (time since it was cached or last used) and whether it is in use. Entries are:
587
+
588
+ - **images** (`chr-<ver>[-arm64].img`, and a `.img.zip` not yet extracted). An
589
+ image is **in use** while any machine, running or stopped, was created from
590
+ that version and arch: `clean` and a failed first boot rebuild from it.
591
+ - **package archives** (`packages-<arch>-<ver>`): the `all_packages` ZIP and its
592
+ extraction, sized and deleted together, since the extraction is only a cache
593
+ hit while its ZIP is there. In use while a machine that asks for packages has
594
+ not finished its first boot.
595
+ - **partial** entries: an interrupted download (`*.<pid>-<id>.part`) or
596
+ extraction (`.quickchr-image-*`, `.quickchr-packages-*`). In use while the
597
+ process that wrote it is alive (an extraction: for 30 minutes after its last
598
+ write). `prune` removes stale ones before anything else.
599
+
600
+ `cache list --json` prints one object per entry, including `kind` (`image`, `image-zip`,
601
+ `packages` or `partial`) and `paths` (every file deleting the entry removes).
602
+ Files in the cache that quickchr did not write are reported as "Other files"
603
+ and never deleted. `prune` and `clear` never delete an in-use entry.
604
+
605
+ ```bash
606
+ quickchr cache list
607
+ quickchr cache prune --older-than 7.22 --dry-run # versions below 7.22
608
+ quickchr cache prune --max-age 30d # not used for more than 30 days
609
+ quickchr cache prune --max-size 2G # oldest versions first, until <= 2 GiB
610
+ quickchr cache clear --all # every entry not in use
611
+ ```
612
+
613
+ `prune` needs at least one of `--older-than <ver>`, `--max-age <dur>` or
614
+ `--max-size <size>`, and applies them in that order when combined. `--max-size`
615
+ evicts the oldest RouterOS version first (oldest cache time breaks ties) until
616
+ the total, in-use entries included, fits. Durations are `<N>(s|m|h|d|w)`; sizes
617
+ are `<N>(K|M|G|T)` with an optional `i`/`B`, read as IEC binary. `--dry-run`
618
+ lists what `prune` or `clear` would delete without deleting it. `clear` refuses
619
+ without `--all`. A delete that fails is listed on stderr, is not counted as
620
+ freed, and makes the command exit 1. A cache hit counts as a use, so `--max-age`
621
+ measures time since an entry was last used.
622
+
623
+ Separately, a successful `start` prunes to the `cache-max-size` setting (default
624
+ `2G`; see `settings` below). That automatic prune, and the one below, skip any
625
+ entry used in the last 30 minutes, which another create may be reading.
626
+
627
+ **Disk space.** Creating a machine (`add`, or `start` with a new name) and
628
+ filling the cache (an image or package download) are admitted when each
629
+ filesystem the operation writes to has room for what it writes there plus the
630
+ `storage-reserve` setting (default `2G`), less what other quickchr operations
631
+ in progress have already claimed. The estimate is the boot disk
632
+ (192 MiB), plus an image download (256 MiB) and a package download (192 MiB)
633
+ when those are not cached yet, so a cold create needs about 2.6 GiB free and a
634
+ warm one about 2.2 GiB. When there is not enough, quickchr first evicts what the
635
+ cache can spare on that filesystem (oldest version first; never entries a
636
+ machine or an operation in progress needs, nor recently used ones), measures
637
+ again until there is room or nothing more can go, and only then refuses with `INSUFFICIENT_DISK_SPACE`,
638
+ naming the free space, what it needed, what it evicted and what it kept.
639
+ Restarting an existing machine, `stop`, `remove` and read-only commands are
640
+ never gated. The estimate covers creation, not a guest that later fills its
641
+ disk: extra disks and `--boot-size` are sparse and grow as the guest writes.
642
+
570
643
  #### `settings [print|get|set|reset] [key] [value] [--json]`
571
644
 
572
645
  Manage quickchr's own global preferences — repeated flags/defaults that
@@ -583,6 +656,7 @@ Precedence per key (highest wins): CLI flag > `QUICKCHR_<KEY>` env var >
583
656
  | `default-arch` | `add`/`start`'s `--arch`, wizard's arch prompt | `arm64`\|`x86`\|`auto` | `auto` (host native) |
584
657
  | `accel` | `add`/`start`'s `--accel`; every `detectAccel()` call in the run | `auto`\|`tcg`\|`hvf`\|`kvm` | `auto` (detect) |
585
658
  | `cache-max-size` | Auto-prune cap applied after each successful `start` | size string (e.g. `2G`, `512M`) | `2G` |
659
+ | `storage-reserve` | Free space creating a machine or filling the cache must leave, beyond what it writes | size string | `2G` |
586
660
  | `timeout-extra` | `start`'s `--timeout-extra`/`-T` when omitted | non-negative integer seconds | `0` |
587
661
  | `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
662
 
@@ -601,7 +675,7 @@ the wizard's own recommended "managed login" highlighted). Giving it a real
601
675
  the setting resolves to exactly `true`.
602
676
 
603
677
  ```bash
604
- quickchr settings print # all 6 keys, resolved value + source
678
+ quickchr settings print # all 7 keys, resolved value + source
605
679
  quickchr settings set default-channel testing # write one key to quickchr.env
606
680
  quickchr settings get default-arch # print one key's resolved value
607
681
  quickchr settings reset default-channel # delete one managed line
@@ -609,7 +683,7 @@ quickchr settings reset # clear all managed lines
609
683
  ```
610
684
 
611
685
  Credentials (`--add-user`, `MIKROTIK_WEB_ACCOUNT`/`MIKROTIK_WEB_PASSWORD`) are
612
- never stored here — `settings` only manages the 6 keys above, and does not
686
+ never stored here — `settings` only manages the 7 keys above, and does not
613
687
  mutate any machine's `machine.json`.
614
688
 
615
689
  ### Global behavior
@@ -824,14 +898,14 @@ redact before logging or attaching to bug reports.
824
898
  | Field | Type | Default | Notes |
825
899
  |---|---|---|---|
826
900
  | `name` | string | auto | Must not start with `-` |
827
- | `version` | string | — | Mutually exclusive with `channel` |
901
+ | `version` | string | — | Wins over `channel`: passing both warns and uses `version` |
828
902
  | `channel` | Channel | `"stable"` | |
829
903
  | `arch` | `"arm64" \| "x86"` | host | |
830
904
  | `cpu` | number | `1` | |
831
905
  | `mem` | number | `512` (1024 cross-arch TCG) | MiB |
832
906
  | `background` | boolean | `true` | Where the serial console goes — not whether `start()` waits |
833
907
  | `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`) | |
908
+ | `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
909
  | `bootSize` | string | — | e.g., `"512M"`, `"2G"`. Needs `qemu-img` |
836
910
  | `extraDisks` | string[] | `[]` | Each entry is a size string |
837
911
  | `networks` | NetworkSpecifier[] | `["user"]` | See §7 |
@@ -845,7 +919,7 @@ redact before logging or attaching to bug reports.
845
919
  | `secureLogin` | boolean | `false` | **`true` is the explicit trigger** for managed-account creation |
846
920
  | `license` | `LicenseInput` | — | `"p1"` / `"p10"` / `"unlimited"` or `LicenseOptions` |
847
921
  | `deviceMode` | `DeviceModeOptions` | — | `{mode?, enable?[], disable?[]}` |
848
- | `installDeps` | boolean | — | Auto-install missing OS deps |
922
+ | `installDeps` | boolean | — | **Deprecated, ignored.** Never installed anything; run `quickchr doctor` for install hints |
849
923
  | `dryRun` | boolean | `false` | Print QEMU command and exit |
850
924
  | `timeoutExtra` | number | `0` | Extra **milliseconds** added to boot timeout |
851
925
  | `onProgress` | `(msg) => void` | `console.log` | `[debug]` lines only when `QUICKCHR_DEBUG=1` |
@@ -1195,8 +1269,8 @@ $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
1195
1269
  ├── machines/
1196
1270
  │ └── <name>/
1197
1271
  │ ├── machine.json Persisted MachineState
1198
- │ ├── disk.img | disk.qcow2
1199
- │ ├── extra-disk-*.qcow2
1272
+ │ ├── boot.qcow2 | disk.img boot disk: qcow2 (CLI default) or raw — one, not both
1273
+ │ ├── disk<N>.qcow2 extra disks
1200
1274
  │ ├── efi-vars.fd arm64 only (UEFI vars)
1201
1275
  │ ├── monitor.sock QEMU monitor
1202
1276
  │ ├── serial.sock serial console
@@ -1224,7 +1298,7 @@ var list.
1224
1298
  ├── cache\
1225
1299
  ├── machines\<name>\
1226
1300
  │ ├── machine.json
1227
- │ ├── disk.img
1301
+ │ ├── boot.qcow2 | disk.img
1228
1302
  │ ├── monitor (named pipe handle, no on-disk file)
1229
1303
  │ └── …
1230
1304
  └── …
@@ -1370,7 +1444,7 @@ managed via `quickchr settings get|set|print|reset` (§3, Meta). Atomic writes
1370
1444
  preserve every other line (comments, blank lines, foreign vars) byte-for-byte.
1371
1445
  `reset <key>` deletes the line entirely rather than blanking it, since a blank
1372
1446
  `KEY=` line is still "set" to a bash-sourced file. Never stores credentials —
1373
- the 6 managed keys are all structural preferences, not secrets.
1447
+ the 7 managed keys are all structural preferences, not secrets.
1374
1448
 
1375
1449
  ### Environment variables consumed
1376
1450
 
@@ -1387,6 +1461,7 @@ the 6 managed keys are all structural preferences, not secrets.
1387
1461
  | `QUICKCHR_DEFAULT_ARCH` | `settings.ts` — env tier for the `default-arch` setting |
1388
1462
  | `QUICKCHR_ACCEL` | `settings.ts` / `platform.ts` — env tier for the `accel` setting; forces `-accel` and bypasses detection |
1389
1463
  | `QUICKCHR_CACHE_MAX_SIZE` | `settings.ts` — env tier for the `cache-max-size` setting |
1464
+ | `QUICKCHR_STORAGE_RESERVE` | `settings.ts` — env tier for the `storage-reserve` setting |
1390
1465
  | `QUICKCHR_SHUTDOWN_TIMEOUT` | CLI `shutdown` grace budget in positive integer seconds; default 10, overridden by `--timeout` |
1391
1466
  | `QUICKCHR_TIMEOUT_EXTRA` | `settings.ts` — env tier for the `timeout-extra` setting |
1392
1467
  | `QUICKCHR_SECURE_LOGIN` | `settings.ts` — env tier for the `secure-login` setting |
@@ -1436,6 +1511,29 @@ funnel through the same transport selector.
1436
1511
  > (see [`docs/centrs-interface.md`](./docs/centrs-interface.md)) — it never reads
1437
1512
  > `machine.json`, and neither should you.
1438
1513
 
1514
+ ### Exit status
1515
+
1516
+ `quickchr exec` exits 0 when the command was **delivered**, not when RouterOS
1517
+ accepted it. `/rest/execute` returns HTTP 200 with the rejection text in the same
1518
+ envelope as a success, so a refused command looks like this:
1519
+
1520
+ ```console
1521
+ $ quickchr exec my-chr "/user/add name=-2 group=read password=x"; echo "rc=$?"
1522
+ failure: invalid user name (/user/add; line 1)
1523
+ rc=0
1524
+ ```
1525
+
1526
+ The library is the same: `chr.exec()` resolves, and `ExecResult` has no failure
1527
+ field. quickchr does not pattern-match RouterOS error text, whose wording changes
1528
+ between versions (#142). When a script needs a pass/fail exit code, run the
1529
+ command through centrs, which validates it first and exits non-zero on a rejection.
1530
+ A write command needs `--yes` outside a TTY; without it centrs refuses with
1531
+ `usage/confirmation-required` (also exit 1) and sends nothing:
1532
+
1533
+ ```bash
1534
+ centrs execute --quickchr my-chr --yes "/user/add name=-2 group=read password=x"
1535
+ ```
1536
+
1439
1537
  ### Transports
1440
1538
 
1441
1539
  | `--via` | Source | Notes |
@@ -1605,7 +1703,7 @@ present, then exits `1`.
1605
1703
  | `DOWNLOAD_FAILED` | image fetch failed | retry; check connectivity to MikroTik download server |
1606
1704
  | `INVALID_VERSION` / `INVALID_ARCH` / `INVALID_NAME` / `INVALID_DISK_SIZE` | argument parse | fix the value |
1607
1705
  | `MACHINE_EXISTS` | name already in `machines/` | choose another name or `QuickCHR.get(name)` |
1608
- | `MACHINE_NOT_FOUND` | name not in `machines/` | `quickchr list` |
1706
+ | `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
1707
  | `MACHINE_RUNNING` / `MACHINE_STOPPED` | wrong lifecycle state | `start` or `stop` first |
1610
1708
  | `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
1709
  | `EXEC_FAILED` | RouterOS rejected the command (auth, syntax) | check creds, command syntax |
@@ -1618,7 +1716,7 @@ present, then exits `1`.
1618
1716
  | `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
1717
  | `REST_UNAVAILABLE` | REST operation (`exec`, `get`, `rest()`) on a RouterOS 6 machine | use the native API — `centrs … --via native-api --no-validate` |
1620
1718
  | `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` |
1719
+ | `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
1720
  | `STATE_ERROR` | snapshot on raw boot disk | recreate with `--boot-disk-format=qcow2` |
1623
1721
  | `INVALID_SIZE_STRING` | invalid cache/settings size string | use size syntax like `512M`, `2G`, or `1.5G` |
1624
1722
  | `INVALID_SETTING_KEY` | `settings get`/`set`/`reset` received an unrecognized key | `quickchr settings print` to see the managed keys |
package/README.md CHANGED
@@ -57,6 +57,23 @@ Library:
57
57
  bun add @tikoci/quickchr
58
58
  ```
59
59
 
60
+ ### Prerelease QA (0.5.1)
61
+
62
+ 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
64
+ reproducible CLI or library QA:
65
+
66
+ ```bash
67
+ bun install -g @tikoci/quickchr@0.5.1
68
+ bun add @tikoci/quickchr@0.5.1
69
+ quickchr --version
70
+ ```
71
+
72
+ Use `@next` instead of `@0.5.1` to follow later candidates. See the
73
+ [0.5.1 changelog](./CHANGELOG.md#051--2026-10-22) for the changes since 0.5.0,
74
+ including storage admission, cache accounting, and disk preservation fixes.
75
+ The [0.5.0 notes](./CHANGELOG.md#050--2026-10-22) cover migration from 0.4.x.
76
+
60
77
  ### CLI Usage
61
78
 
62
79
  The easiest way to start is the interactive wizard — it walks through
@@ -195,6 +212,7 @@ reports a forced power cut if the guest does not stop within the grace budget.
195
212
  | `--device-mode <m>` | Configure device-mode: rose\|advanced\|basic\|home\|auto\|skip | |
196
213
  | `--device-mode-enable <f>` | Set one or more device-mode flags to yes | |
197
214
  | `--device-mode-disable <f>` | Set one or more device-mode flags to no | |
215
+ | `--no-device-mode` | Skip device-mode provisioning entirely | |
198
216
  | `--port-base <port>` | Starting port number | Auto (9100+) |
199
217
  | `--timeout-extra <s>` | Add extra seconds to the auto-computed boot timeout | |
200
218
  | `--dry-run` | Print what would run without executing | |
@@ -238,7 +256,7 @@ In **foreground** mode, your terminal becomes the CHR serial console. Use these
238
256
  | `Ctrl-A C` | Toggle QEMU monitor (`quit` to force-stop) |
239
257
  | `Ctrl-A H` | List all key shortcuts |
240
258
 
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.
259
+ > **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
260
 
243
261
  ### Disk Support
244
262
 
@@ -437,7 +455,7 @@ afterAll(async () => {
437
455
  |----------|-------------|
438
456
  | `MIKROTIK_WEB_ACCOUNT` | MikroTik.com account email (for license renewal) |
439
457
  | `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) |
458
+ | `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
459
  | `QUICKCHR_INTEGRATION` | Set to `1` to run integration tests |
442
460
  | `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
461
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/quickchr",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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",
@@ -25,7 +25,7 @@
25
25
  "lint": "biome check . && tsc --noEmit",
26
26
  "lint:biome": "biome check .",
27
27
  "lint:typecheck": "tsc --noEmit",
28
- "lint:markdown": "bunx markdownlint-cli2 '**/*.md'",
28
+ "lint:markdown": "markdownlint-cli2 '**/*.md'",
29
29
  "lint:cspell": "cspell lint . --no-progress",
30
30
  "lint:examples": "bun run scripts/validate-examples.ts",
31
31
  "lint:specifiers": "bun run scripts/validate-network-specifiers.ts",
package/src/cli/flags.ts CHANGED
@@ -25,7 +25,7 @@ const CREATE_FLAGS = [
25
25
  /** `start` also boots, so it owns the run-time flags on top of the create set. */
26
26
  const START_ONLY_FLAGS = [
27
27
  "all", "dry-run", "wait", "background", "bg", "fg", "foreground",
28
- "install-deps", "timeout-extra",
28
+ "timeout-extra",
29
29
  "license-level", "license-account", "license-password",
30
30
  ] as const;
31
31
 
@@ -96,10 +96,15 @@ export const VALUE_FLAGS: ReadonlySet<string> = new Set([
96
96
  * that `--add-network` already expresses in general (#164). Removing them silently would
97
97
  * repeat the defect they were removed for: a flag that is accepted and does nothing. A
98
98
  * removed name stays known for as long as anyone might still type it, and says what
99
- * replaced it. */
99
+ * replaced it.
100
+ *
101
+ * `--install-deps` is the same defect from the other side: `start` accepted it from
102
+ * the beginning and nothing ever read it (#193). It never installed anything, so the
103
+ * replacement is the command that names what to install. */
100
104
  export const REMOVED_FLAGS: ReadonlyMap<string, string> = new Map([
101
105
  ["vmnet-shared", "--add-network shared"],
102
106
  ["vmnet-bridge", "--add-network bridged:<iface>"],
107
+ ["install-deps", "quickchr doctor (it never installed anything; doctor lists missing host dependencies and how to install them)"],
103
108
  ]);
104
109
 
105
110
  /** Value-taking flags with a documented `--no-` spelling, where `false` is a real