@tikoci/quickchr 0.4.8 → 0.5.0
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 +162 -7
- package/MANUAL.md +273 -46
- package/README.md +13 -6
- package/package.json +1 -1
- package/src/cli/flags.ts +15 -1
- package/src/cli/index.ts +405 -76
- package/src/cli/wizard.ts +10 -2
- package/src/index.ts +2 -0
- package/src/lib/api-login.ts +138 -0
- package/src/lib/completions.ts +20 -11
- package/src/lib/device-mode.ts +149 -10
- package/src/lib/diagnostics.ts +13 -6
- package/src/lib/lock.ts +497 -0
- package/src/lib/names.ts +24 -0
- package/src/lib/network.ts +16 -16
- package/src/lib/provision.ts +3 -2
- package/src/lib/provisioning-window.ts +356 -0
- package/src/lib/qemu.ts +54 -2
- package/src/lib/quickchr.ts +888 -327
- package/src/lib/shutdown.ts +49 -0
- package/src/lib/socket-registry.ts +2 -2
- package/src/lib/state.ts +64 -2
- package/src/lib/types.ts +96 -20
- package/src/lib/versions.ts +54 -0
package/MANUAL.md
CHANGED
|
@@ -41,6 +41,10 @@ by the user (or their AI agent) using `quickchr` as a building block.
|
|
|
41
41
|
- ❌ Multi-router topology orchestration, test-matrix runners,
|
|
42
42
|
configuration-as-code DSLs. Use `examples/` as references and compose
|
|
43
43
|
yourself.
|
|
44
|
+
- ⚠️ **RouterOS 6** is a best-effort, boot-only target (x86, CLI only): quickchr
|
|
45
|
+
downloads, boots and tracks a 6.x CHR, and does nothing else to it. It exists so
|
|
46
|
+
v6→v7 migration tooling has a disposable 6.x router to test against — it is not
|
|
47
|
+
RouterOS 6 support. See [RouterOS 6](#routeros-6-boot-only) below.
|
|
44
48
|
|
|
45
49
|
**Two ways to use it:**
|
|
46
50
|
|
|
@@ -159,20 +163,32 @@ an error naming its replacement, not a silently ignored flag.
|
|
|
159
163
|
#### `start [name]`
|
|
160
164
|
|
|
161
165
|
Boot a stopped machine, or create-and-boot in one command. Blocks until
|
|
162
|
-
|
|
163
|
-
terminal (`--fg`). Accepts the same provisioning flags as `add`.
|
|
166
|
+
RouterOS is ready (REST answers; on 6.x, a native-API login), or attaches QEMU
|
|
167
|
+
stdio to your terminal (`--fg`). Accepts the same provisioning flags as `add`.
|
|
168
|
+
QEMU runs detached in its own session either way, so it survives a signal to the
|
|
169
|
+
shell that started it.
|
|
164
170
|
|
|
165
171
|
Additional flags:
|
|
166
172
|
|
|
167
173
|
| Flag | Notes |
|
|
168
174
|
|---|---|
|
|
169
|
-
| `--all` | Start every stopped machine in background |
|
|
170
|
-
| `--
|
|
175
|
+
| `--all` | Start every stopped machine in background; accepts readiness, acceleration, timeout and secure-login options only. Other options are refused rather than ignored |
|
|
176
|
+
| `--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` |
|
|
171
177
|
| `--fg` / `--foreground` (alias `--no-bg`, `--no-background`) | Serial console on stdio |
|
|
178
|
+
| `--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` |
|
|
172
179
|
| `--install-deps` | Auto-install missing host deps (calls package manager) |
|
|
173
180
|
| `--timeout-extra <s>` / `-T <s>` | Add extra seconds to computed boot timeout |
|
|
174
181
|
| `--dry-run` | Print resolved options and QEMU command, exit |
|
|
175
182
|
|
|
183
|
+
After `--no-wait`, readiness is yours. centrs (0.1.8+) waits for it in one command:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
quickchr start lab --no-wait
|
|
187
|
+
centrs retrieve --quickchr lab /system/resource --wait 5m
|
|
188
|
+
# RouterOS 6: centrs validates against the 7.x command tree, so skip validation
|
|
189
|
+
centrs retrieve --quickchr lab6 /system/resource --via native-api --no-validate --wait 5m
|
|
190
|
+
```
|
|
191
|
+
|
|
176
192
|
Foreground mode shortcuts (QEMU mux):
|
|
177
193
|
|
|
178
194
|
| Key | Action |
|
|
@@ -188,9 +204,74 @@ machine state.
|
|
|
188
204
|
|
|
189
205
|
#### `stop [name | --all]`
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
Power cut: sends SIGTERM to QEMU, waits up to 5 seconds, then SIGKILL if
|
|
208
|
+
needed. This does not request a guest shutdown; pending RouterOS changes
|
|
209
|
+
may be lost. Updates `state.status = "stopped"` and unregisters socket-named
|
|
210
|
+
networks. Listing-mode if no name and not `--all`.
|
|
211
|
+
|
|
212
|
+
Use `stop` for normal teardown and device-mode power-cycle confirmation.
|
|
213
|
+
Use `shutdown` when the caller wants to request a guest shutdown first.
|
|
214
|
+
|
|
215
|
+
#### `shutdown <name> [--timeout <seconds>]`
|
|
216
|
+
|
|
217
|
+
Sends `/system/shutdown` once through REST, independently waits for QEMU to
|
|
218
|
+
exit, then falls back to a power cut if it remains running. Default grace
|
|
219
|
+
budget: **10 seconds**. `--timeout` takes positive integer seconds and
|
|
220
|
+
overrides `QUICKCHR_SHUTDOWN_TIMEOUT`. Neither changes the boot timeout.
|
|
221
|
+
The maximum is 2,147,483 seconds (the HTTP timer's limit).
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
quickchr shutdown lab
|
|
225
|
+
quickchr shutdown lab --timeout 20
|
|
226
|
+
QUICKCHR_SHUTDOWN_TIMEOUT=20 quickchr shutdown lab
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The CLI reports `guest exited`, `already stopped`, or **forced power cut —
|
|
230
|
+
guest shutdown not confirmed**. Forced fallback exits successfully if QEMU
|
|
231
|
+
is confirmed gone; invalid input, lock failure, or inability to stop QEMU
|
|
232
|
+
is an error. Disks, credentials and restart configuration survive.
|
|
233
|
+
|
|
234
|
+
The guest request and exit observation share one grace budget. Lifecycle
|
|
235
|
+
lock acquisition, the existing SIGTERM/SIGKILL escalation, and final exit
|
|
236
|
+
verification have separate bounded waits. A 10-second grace budget does
|
|
237
|
+
not bound the entire CLI invocation to 10 seconds.
|
|
238
|
+
|
|
239
|
+
An HTTP error does not establish failure: on CHR 7.24.5, both x86/HVF and
|
|
240
|
+
arm64/TCG returned HTTP 400 `interrupted`, then exited. Shutdown uses
|
|
241
|
+
process exit as its completion signal. Observed exit does not guarantee
|
|
242
|
+
application or storage durability. Unavailable REST (including RouterOS 6),
|
|
243
|
+
failed authentication and a guest that does not halt all use the same
|
|
244
|
+
explicit hard-stop fallback; no console/QGA/ACPI replay is attempted.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const result = await chr.shutdown({ timeoutMs: 20_000 });
|
|
248
|
+
// outcome: "guest-exited" | "forced" | "already-stopped"
|
|
249
|
+
// requestError can accompany guest-exited (e.g. HTTP 400 interrupted).
|
|
250
|
+
console.log(result.outcome);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Library `timeoutMs` values are positive integer milliseconds, maximum
|
|
254
|
+
2,147,483,647, with a 10,000 ms default. Library callers supply options
|
|
255
|
+
explicitly; the CLI environment override is not read by `shutdown()`.
|
|
256
|
+
|
|
257
|
+
For consumers of an older quickchr version, the manual workaround needs
|
|
258
|
+
both request-error handling and independent exit observation:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const pid = chr.state.pid;
|
|
262
|
+
if (pid) {
|
|
263
|
+
try { await chr.exec("/system/shutdown", { via: "rest", timeout: 10_000 }); }
|
|
264
|
+
catch { /* HTTP 400 interrupted may accompany successful guest shutdown. */ }
|
|
265
|
+
const exited = await chr.waitFor(async () => {
|
|
266
|
+
try { process.kill(pid, 0); return false; } catch { return true; }
|
|
267
|
+
}, 10_000);
|
|
268
|
+
if (!exited) console.warn("Guest shutdown not confirmed; forcing a power cut");
|
|
269
|
+
}
|
|
270
|
+
await chr.stop(); // reconcile state/network membership, or hard-stop if still alive
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
This older-version workaround has separate request and wait budgets;
|
|
274
|
+
`shutdown()` provides one shared budget and structured fallback reporting.
|
|
194
275
|
|
|
195
276
|
#### `remove [name | --all]` (alias `rm`)
|
|
196
277
|
|
|
@@ -207,10 +288,17 @@ flag. The next `start` therefore logs in as factory `admin` with an empty
|
|
|
207
288
|
password. EFI vars are deliberately **kept** (they store boot order, not
|
|
208
289
|
guest state; wiping them costs an arm64 device rescan).
|
|
209
290
|
|
|
210
|
-
`clean`
|
|
211
|
-
(
|
|
212
|
-
|
|
213
|
-
|
|
291
|
+
`clean` itself never provisions — it only resets the disk. What it does do is
|
|
292
|
+
**reopen the provisioning window** (#176), so the *next* `start` re-applies the
|
|
293
|
+
provisioning intent `machine.json` still holds: `packages`, `deviceMode`,
|
|
294
|
+
`secureLogin`. Budget for that start accordingly; it costs what a first boot costs,
|
|
295
|
+
not what a restart costs.
|
|
296
|
+
|
|
297
|
+
What does **not** come back on its own is anything `clean` had to clear because it
|
|
298
|
+
described the erased guest rather than an intent — `user`, `disableAdmin`,
|
|
299
|
+
`licenseLevel` — and any option from a request that was *refused*, since a refused
|
|
300
|
+
request is never persisted. Pass those flags again, which is why the refusal says to
|
|
301
|
+
repeat the original command rather than to run a bare `start`.
|
|
214
302
|
|
|
215
303
|
### Inspection
|
|
216
304
|
|
|
@@ -238,6 +326,9 @@ machine as a directory to delete rather than state to load.
|
|
|
238
326
|
Live REST query. Hits the running CHR and returns the requested group
|
|
239
327
|
(or all if no group specified). Requires the machine to be running.
|
|
240
328
|
|
|
329
|
+
`device-mode` lists the mode plus every feature RouterOS reports, split into
|
|
330
|
+
enabled and disabled — the read-back for `quickchr set <name> --device-mode…`.
|
|
331
|
+
|
|
241
332
|
#### `inspect <name> [--json]`
|
|
242
333
|
|
|
243
334
|
Stable, versioned (`descriptorVersion: 1`) connection descriptor for a
|
|
@@ -328,11 +419,43 @@ Apply or renew a CHR trial license on a running instance. Resolves
|
|
|
328
419
|
MikroTik.com credentials from `--license-account`/`--license-password`,
|
|
329
420
|
the env vars, or the secret store. Requires RouterOS ≥ 7.20.8.
|
|
330
421
|
|
|
331
|
-
#### `set <name> <
|
|
422
|
+
#### `set <name> [--license | --device-mode <m>] [--device-mode-enable <f>] [--device-mode-disable <f>]`
|
|
423
|
+
|
|
424
|
+
Change a property on a machine that has **already booted** — the post-boot half
|
|
425
|
+
of the provisioning flags. `add`/`start` apply them at first boot; `set` applies
|
|
426
|
+
them now. It is the route `PROVISIONING_WINDOW_CLOSED` names, printed with the
|
|
427
|
+
flags that were asked for so it can be run as given.
|
|
332
428
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
429
|
+
| flag | what it does |
|
|
430
|
+
|---|---|
|
|
431
|
+
| `--license` | Apply or renew a CHR trial license (§ `license` above for credential resolution). |
|
|
432
|
+
| `--device-mode <m>` | `rose`, `advanced`, `basic`, `home`, `auto`. |
|
|
433
|
+
| `--device-mode-enable <f,…>` | Set one or more device-mode flags to yes. Repeatable, comma-separated. |
|
|
434
|
+
| `--device-mode-disable <f,…>` | Set one or more to no. |
|
|
435
|
+
|
|
436
|
+
**Device-mode restarts the machine.** RouterOS only applies a device-mode change
|
|
437
|
+
across a power cycle, and quickchr owns the power button — which is why this lives
|
|
438
|
+
here rather than in a tool that reaches a router over the network: the same request
|
|
439
|
+
issued from inside the guest never returns, because it is waiting for a power cycle
|
|
440
|
+
it cannot perform on itself. quickchr prints every setting that will move before it
|
|
441
|
+
restarts, and skips the restart entirely when the guest already matches.
|
|
442
|
+
|
|
443
|
+
Naming a feature without a mode resolves the mode to `rose` — so
|
|
444
|
+
`set lab --device-mode-enable container` moves `mode` as well. That move is printed
|
|
445
|
+
before it happens, not discovered afterwards.
|
|
446
|
+
|
|
447
|
+
Preconditions: the machine must be **running** and have a **user-mode NIC**
|
|
448
|
+
(device-mode is applied over localhost REST), on RouterOS ≥ 7.20.8. Each is a
|
|
449
|
+
named refusal rather than a timeout.
|
|
450
|
+
|
|
451
|
+
The change is folded into `machine.json` rather than overwriting it — device-mode
|
|
452
|
+
updates move only the settings they name, so the record stays cumulative — and
|
|
453
|
+
recorded in `provisioning.steps`, which makes a later `start` passing the same
|
|
454
|
+
device-mode a recognised no-op instead of a refusal.
|
|
455
|
+
|
|
456
|
+
**Not available here:** `--add-user`, `--disable-admin`, `--secure-login`. Those
|
|
457
|
+
rewrite working config rather than unlock a capability, and they are the steps that
|
|
458
|
+
drift; their route stays `quickchr clean <name>` followed by the original command.
|
|
336
459
|
|
|
337
460
|
#### `snapshot <name> <list | save [snapname] | load <snapname> | delete <snapname>>` (alias `snap`)
|
|
338
461
|
|
|
@@ -372,7 +495,7 @@ another machine, view status, or quit.
|
|
|
372
495
|
| 9 | Extra blank disks (repeatable) | none | `--add-disk` |
|
|
373
496
|
| 10 | Additional networks (macOS/Linux) | user-mode only | `--add-network` |
|
|
374
497
|
| 11 | Provisioning (packages, device-mode, user, license) | optional | various |
|
|
375
|
-
| 12 | Run mode: background / foreground | background |
|
|
498
|
+
| 12 | Run mode: background / foreground | background | (default) / `--fg` |
|
|
376
499
|
| 13 | Final confirmation | — | — |
|
|
377
500
|
|
|
378
501
|
Steps 7–9 are skipped when `qemu-img` is absent (a warning is shown). Step 10
|
|
@@ -497,7 +620,7 @@ mutate any machine's `machine.json`.
|
|
|
497
620
|
| `QUICKCHR_NO_PROMPT` | Force non-interactive mode (skips all wizards) |
|
|
498
621
|
| `QUICKCHR_DEBUG` | Emit `[debug]` lines from the progress logger |
|
|
499
622
|
| `QUICKCHR_INTEGRATION` | Required to run `test/integration/` |
|
|
500
|
-
| `QUICKCHR_PRESERVE_ON_FAILURE` | `=1`
|
|
623
|
+
| `QUICKCHR_PRESERVE_ON_FAILURE` | `=1` skips removal/reset after a first-boot timeout or failed first-boot provisioning run (QEMU is still stopped). Previously booted machines always survive a restart timeout |
|
|
501
624
|
| `QUICKCHR_SERIAL_LOG` | `=1` tees the guest serial console to `<machineDir>/serial.log`. **Secret-bearing** — serial provisioning types the generated password in cleartext |
|
|
502
625
|
| `QUICKCHR_DEEP_BOOT_DIAGNOSTICS` | `=1` lets a boot-timeout capture write to the guest (a mangle counting rule + host probes) to localize a silent packet drop. Skipped when `QUICKCHR_PRESERVE_ON_FAILURE=1` |
|
|
503
626
|
| `MIKROTIK_WEB_ACCOUNT`, `MIKROTIK_WEB_PASSWORD` | License credentials fallback |
|
|
@@ -552,10 +675,29 @@ QuickCHR.resolveVersion(channel: Channel): Promise<string>
|
|
|
552
675
|
```
|
|
553
676
|
|
|
554
677
|
`start()` creates the machine if needed, downloads the image, allocates
|
|
555
|
-
a port block, spawns QEMU, waits for REST
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
678
|
+
a port block, spawns QEMU, waits for readiness (REST on 7.x, a native-API
|
|
679
|
+
`/login` on RouterOS 6), and runs all provisioning steps before returning.
|
|
680
|
+
With `wait: false` (`--no-wait`) it returns once QEMU is running and leaves
|
|
681
|
+
readiness to the caller; provisioning options are refused in that mode.
|
|
682
|
+
On a boot timeout it stops QEMU, then removes a machine this call created,
|
|
683
|
+
resets an existing never-booted machine to its factory disk, and preserves a
|
|
684
|
+
previously booted one — see [Auto-cleanup on boot timeout](#auto-cleanup-on-boot-timeout).
|
|
685
|
+
|
|
686
|
+
**Concurrency.** Machines may be created and started in parallel, from one
|
|
687
|
+
`Promise.all` or from separate processes. Each creation chooses its port
|
|
688
|
+
block and NIC MACs under a data-dir-wide allocation lock and reserves them
|
|
689
|
+
until `machine.json` is written, so concurrent auto-allocated machines never
|
|
690
|
+
share a block. An explicit `portBase` is never moved: one that overlaps
|
|
691
|
+
another machine's block fails with `PORT_CONFLICT`. Per machine, every
|
|
692
|
+
lifecycle operation takes the machine lock (`machines/<name>/.lock/`):
|
|
693
|
+
|
|
694
|
+
- `stop()`, `clean()` and `remove()` **wait** for an operation already in
|
|
695
|
+
progress (up to the machine's boot timeout), then act on the state it left.
|
|
696
|
+
- A second `start()` of the same machine, `setDeviceMode()` and removing a
|
|
697
|
+
leftover directory **fail at once** with `MACHINE_LOCKED`, naming the
|
|
698
|
+
operation, pid and age of the holder.
|
|
699
|
+
- A machine running in the **foreground** holds the lock until its VM exits.
|
|
700
|
+
Nothing waits for it. Quit it from its console.
|
|
559
701
|
|
|
560
702
|
`add()` is the same minus the QEMU spawn — useful when you want to
|
|
561
703
|
materialize a machine and start it later (or hand the config to another
|
|
@@ -598,7 +740,7 @@ throws `INVALID_NAME` for a name that is not a usable path segment.
|
|
|
598
740
|
|---|---|---|
|
|
599
741
|
| Identity | `name`, `state`, `ports`, `restUrl`, `sshPort`, `portBase` | Static metadata persisted in `machine.json` |
|
|
600
742
|
| Capture | `captureInterface`, `hostGatewayIp` | Platform-correct values for TZSP capture (`tshark -i …`) and the guest→host address any UDP emitter targets |
|
|
601
|
-
| Lifecycle | `waitForBoot()`, `waitFor(fn, ms?)`, `stop()`, `remove()`, `clean()`, `destroy()` | Boot readiness, custom polling, teardown |
|
|
743
|
+
| Lifecycle | `waitForBoot()`, `waitFor(fn, ms?)`, `stop()`, `shutdown(opts?)`, `remove()`, `clean()`, `destroy()` | Boot readiness, custom polling, power-cut or guest-shutdown teardown |
|
|
602
744
|
| Comms | `rest()`, `exec()`, `monitor()`, `serial()`, `qga()` | REST, RouterOS CLI, QEMU monitor, serial console, QGA (x86 only) |
|
|
603
745
|
| Provisioning | `license()`, `setDeviceMode()`, `availablePackages()`, `installPackage()` | Post-boot configuration |
|
|
604
746
|
| Files | `upload()`, `download()` | SCP transfer to/from the CHR |
|
|
@@ -615,11 +757,11 @@ interface ChrInstance {
|
|
|
615
757
|
portBase: number; // start of this instance's reserved port block
|
|
616
758
|
captureInterface: string; // "lo0" on macOS, "any" on Linux — for TZSP capture
|
|
617
759
|
hostGatewayIp: string; // "10.0.2.2" — the host, as seen from inside the guest
|
|
618
|
-
tzspGatewayIp: string; // deprecated alias for hostGatewayIp (#26)
|
|
619
760
|
|
|
620
761
|
waitForBoot(timeoutMs?: number): Promise<boolean>;
|
|
621
762
|
waitFor(condition: () => Promise<boolean>, timeoutMs?: number): Promise<boolean>;
|
|
622
|
-
stop(): Promise<void>;
|
|
763
|
+
stop(): Promise<void>; // power cut: SIGTERM, then SIGKILL
|
|
764
|
+
shutdown(opts?: ShutdownOptions): Promise<ShutdownResult>; // /system/shutdown, bounded fallback
|
|
623
765
|
remove(): Promise<void>;
|
|
624
766
|
clean(): Promise<void>;
|
|
625
767
|
destroy(): Promise<void>; // stop + remove
|
|
@@ -687,7 +829,8 @@ redact before logging or attaching to bug reports.
|
|
|
687
829
|
| `arch` | `"arm64" \| "x86"` | host | |
|
|
688
830
|
| `cpu` | number | `1` | |
|
|
689
831
|
| `mem` | number | `512` (1024 cross-arch TCG) | MiB |
|
|
690
|
-
| `background` | boolean | `true` | |
|
|
832
|
+
| `background` | boolean | `true` | Where the serial console goes — not whether `start()` waits |
|
|
833
|
+
| `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`) |
|
|
691
834
|
| `bootDiskFormat` | `"raw" \| "qcow2"` | `"raw"` (auto-`qcow2` if `bootSize`) | |
|
|
692
835
|
| `bootSize` | string | — | e.g., `"512M"`, `"2G"`. Needs `qemu-img` |
|
|
693
836
|
| `extraDisks` | string[] | `[]` | Each entry is a size string |
|
|
@@ -843,10 +986,43 @@ port mappings work on any 7.x.
|
|
|
843
986
|
Disabling admin **does not** lock the REST API — RouterOS's `expired`
|
|
844
987
|
flag only gates CLI/SSH/Winbox login, not REST.
|
|
845
988
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
989
|
+
A failure in any step fails the whole first boot, and quickchr rolls it back
|
|
990
|
+
rather than leave a half-provisioned guest: a machine `start` created in that
|
|
991
|
+
call is removed (as a boot timeout removes it), and a machine created by `add`
|
|
992
|
+
has its disk reset to the factory image and `machine.json` restored to what
|
|
993
|
+
`add` recorded. Either way the retry is **the same command again**. With
|
|
994
|
+
`QUICKCHR_PRESERVE_ON_FAILURE=1` the half-provisioned machine is kept, stopped,
|
|
995
|
+
for inspection — `quickchr clean <name>` then resets it.
|
|
996
|
+
|
|
997
|
+
On the first `start` of a machine created by `add`, a provisioning option you
|
|
998
|
+
leave out keeps what `add` recorded, and an explicit negation
|
|
999
|
+
(`--no-install-all-packages`, `--no-disable-admin`, `--no-secure-login`,
|
|
1000
|
+
`--no-device-mode`) overrides it.
|
|
1001
|
+
|
|
1002
|
+
**The provisioning window closes at the machine's first boot** — not at
|
|
1003
|
+
`add`. A machine created with no provisioning options can still take them
|
|
1004
|
+
on its first `start`, which is where they are applied from `machine.json`.
|
|
1005
|
+
After that boot the guest may hold configuration quickchr did not put
|
|
1006
|
+
there, so provisioning cannot run against it: an option passed anyway is
|
|
1007
|
+
**refused**, never ignored, with `PROVISIONING_WINDOW_CLOSED` naming where
|
|
1008
|
+
the change can still be made. An option `machine.json` already records is
|
|
1009
|
+
reported as already applied rather than failing, so passing the same flags
|
|
1010
|
+
on every start is fine.
|
|
1011
|
+
|
|
1012
|
+
Routes that still work after the window has closed:
|
|
1013
|
+
|
|
1014
|
+
| change | route |
|
|
1015
|
+
|---|---|
|
|
1016
|
+
| license | `quickchr set <name> --license` |
|
|
1017
|
+
| device-mode | `quickchr set <name> --device-mode…` / `instance.setDeviceMode()` (power-cycles the VM) |
|
|
1018
|
+
| packages | `instance.installPackage()` (library; reboots the guest) |
|
|
1019
|
+
| anything else | `quickchr clean <name>` resets the disk to the factory image and reopens the window, at first-boot cost |
|
|
1020
|
+
|
|
1021
|
+
`clean()` retains provisioning *intent* — packages, device-mode, managed login — so the
|
|
1022
|
+
next `start` re-applies those on its own. It clears `user`, `disableAdmin` and
|
|
1023
|
+
`licenseLevel`, which are facts about the erased guest rather than intent, and a refused
|
|
1024
|
+
request is never persisted. **Repeat the original command after the reset** rather than
|
|
1025
|
+
a bare `quickchr start <name>`, or those options are silently left out.
|
|
850
1026
|
|
|
851
1027
|
**Always read back what we wrote.** This catches version-specific drift
|
|
852
1028
|
in REST responses and surfaces actionable errors instead of silent
|
|
@@ -1011,6 +1187,7 @@ restrictions, half-open SLiRP under TCG), see `docs/networking.md`.
|
|
|
1011
1187
|
|
|
1012
1188
|
```text
|
|
1013
1189
|
$XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
|
|
1190
|
+
├── .allocate.lock/ held while a new machine picks its ports/MACs
|
|
1014
1191
|
├── cache/ Downloaded RouterOS images
|
|
1015
1192
|
│ ├── chr-7.22.1.img.zip
|
|
1016
1193
|
│ ├── chr-7.22.1.img
|
|
@@ -1026,7 +1203,7 @@ $XDG_DATA_HOME/quickchr/ (defaults to ~/.local/share/quickchr/)
|
|
|
1026
1203
|
│ ├── qga.sock QGA (x86 only)
|
|
1027
1204
|
│ ├── qemu.pid
|
|
1028
1205
|
│ ├── qemu.log
|
|
1029
|
-
│ ├──
|
|
1206
|
+
│ ├── .lock/ lifecycle lock + in-flight port/MAC reservation
|
|
1030
1207
|
│ └── ssh/id_ed25519[.pub] SSH key (if managed user provisioned)
|
|
1031
1208
|
└── networks/ Named L2 socket reservations
|
|
1032
1209
|
└── <name>.json
|
|
@@ -1059,11 +1236,11 @@ is unset.
|
|
|
1059
1236
|
### Credentials (separate from data dir)
|
|
1060
1237
|
|
|
1061
1238
|
- **Per-instance CHR credentials**: file in the user config dir
|
|
1062
|
-
(`~/.config/quickchr/
|
|
1239
|
+
(`~/.config/quickchr/com.quickchr.instance.json`; not the OS keychain — a
|
|
1063
1240
|
Keychain dialog hangs in non-TTY contexts like tests and CI).
|
|
1064
1241
|
- **MikroTik.com web account** (for license operations): tries
|
|
1065
1242
|
`Bun.secrets` (OS keychain) when interactive, falls back to the same
|
|
1066
|
-
config-file store otherwise.
|
|
1243
|
+
config-file store otherwise (`~/.config/quickchr/com.quickchr.mikrotik-web.json`).
|
|
1067
1244
|
|
|
1068
1245
|
Both honor explicit CLI flags / env vars first.
|
|
1069
1246
|
|
|
@@ -1095,8 +1272,10 @@ If an extra mapping reuses a built-in service name such as `winbox`, it
|
|
|
1095
1272
|
pins/replaces that service's host port rather than creating a second
|
|
1096
1273
|
`ports.winbox` entry.
|
|
1097
1274
|
|
|
1098
|
-
If port `9100` is in use, the next instance gets `9110`, etc.
|
|
1099
|
-
|
|
1275
|
+
If port `9100` is in use, the next instance gets `9110`, etc. Concurrent
|
|
1276
|
+
creations get distinct blocks (see **Concurrency** under [Static methods on `QuickCHR`](#static-methods-on-quickchr)).
|
|
1277
|
+
Manual override via `--port-base`, which is never remapped: a base that
|
|
1278
|
+
overlaps another machine's block is a `PORT_CONFLICT`.
|
|
1100
1279
|
|
|
1101
1280
|
---
|
|
1102
1281
|
|
|
@@ -1136,8 +1315,12 @@ Native (HVF/KVM): factor ~1 → 120s base.
|
|
|
1136
1315
|
Cross-arch TCG: factor ~15 → up to 1800s.
|
|
1137
1316
|
Package install adds a 2× multiplier (extra reboot cycle).
|
|
1138
1317
|
|
|
1139
|
-
If boot times out,
|
|
1140
|
-
|
|
1318
|
+
If boot times out, QEMU is stopped. A previously booted machine is **preserved**
|
|
1319
|
+
with its disks, credentials and configuration; retry with `quickchr start <name>`
|
|
1320
|
+
or explicitly remove it. A machine created by this `start()` is removed, and an
|
|
1321
|
+
existing never-booted machine is reset to its factory disk and pre-launch record.
|
|
1322
|
+
`QUICKCHR_PRESERVE_ON_FAILURE=1` skips removal/reset for those first-boot cases.
|
|
1323
|
+
See the [cleanup table](#auto-cleanup-on-boot-timeout) for all three cases. Use `--timeout-extra` to add a buffer if you know your host is slow, or
|
|
1141
1324
|
persist a default via `quickchr settings set timeout-extra <seconds>` so
|
|
1142
1325
|
every `start` picks it up without repeating the flag.
|
|
1143
1326
|
|
|
@@ -1154,7 +1337,8 @@ against:
|
|
|
1154
1337
|
- Accidental credential leakage in commits, CI logs, CLI output.
|
|
1155
1338
|
|
|
1156
1339
|
We **don't** protect against same-user code reading
|
|
1157
|
-
`
|
|
1340
|
+
the `~/.config/quickchr/*.json` credential files, `ps`-visible CLI args, or memory
|
|
1341
|
+
inspection.
|
|
1158
1342
|
|
|
1159
1343
|
### Auth resolution (`resolveAuth`)
|
|
1160
1344
|
|
|
@@ -1196,13 +1380,14 @@ the 6 managed keys are all structural preferences, not secrets.
|
|
|
1196
1380
|
| `QUICKCHR_NO_PROMPT` | CLI dispatcher — forces non-interactive mode |
|
|
1197
1381
|
| `QUICKCHR_DEBUG` | progress logger — emits `[debug]` lines |
|
|
1198
1382
|
| `QUICKCHR_INTEGRATION` | `bun:test` gate for `test/integration/` |
|
|
1199
|
-
| `QUICKCHR_PRESERVE_ON_FAILURE` | `quickchr.ts` — skip `remove()` on `BOOT_TIMEOUT
|
|
1383
|
+
| `QUICKCHR_PRESERVE_ON_FAILURE` | `quickchr.ts` — skip `remove()` on `BOOT_TIMEOUT`, and the rollback of a failed provisioning run, so the machine dir survives for diagnosis |
|
|
1200
1384
|
| `QUICKCHR_SERIAL_LOG` | `qemu.ts` — adds `logfile=` to the serial chardev (writes cleartext provisioning passwords) |
|
|
1201
1385
|
| `QUICKCHR_DEEP_BOOT_DIAGNOSTICS` | `guest-snapshot.ts` — allows the guest-mutating counting-rule probe during boot-failure capture |
|
|
1202
1386
|
| `QUICKCHR_DEFAULT_CHANNEL` | `settings.ts` — env tier for the `default-channel` setting |
|
|
1203
1387
|
| `QUICKCHR_DEFAULT_ARCH` | `settings.ts` — env tier for the `default-arch` setting |
|
|
1204
1388
|
| `QUICKCHR_ACCEL` | `settings.ts` / `platform.ts` — env tier for the `accel` setting; forces `-accel` and bypasses detection |
|
|
1205
1389
|
| `QUICKCHR_CACHE_MAX_SIZE` | `settings.ts` — env tier for the `cache-max-size` setting |
|
|
1390
|
+
| `QUICKCHR_SHUTDOWN_TIMEOUT` | CLI `shutdown` grace budget in positive integer seconds; default 10, overridden by `--timeout` |
|
|
1206
1391
|
| `QUICKCHR_TIMEOUT_EXTRA` | `settings.ts` — env tier for the `timeout-extra` setting |
|
|
1207
1392
|
| `QUICKCHR_SECURE_LOGIN` | `settings.ts` — env tier for the `secure-login` setting |
|
|
1208
1393
|
| `MIKROTIK_WEB_ACCOUNT` | license-renewal account (fallback) |
|
|
@@ -1366,7 +1551,13 @@ stored instance credentials, `state.user`, the managed SSH keypair under
|
|
|
1366
1551
|
`<machineDir>/ssh/`, and `disableAdmin` — because the accounts they named
|
|
1367
1552
|
went with the disk. The machine comes back as a factory CHR: `admin` with
|
|
1368
1553
|
an empty password, which is what `rest()`, `exec()`, and `inspect` resolve
|
|
1369
|
-
to afterwards.
|
|
1554
|
+
to afterwards.
|
|
1555
|
+
|
|
1556
|
+
`clean()` performs no provisioning of its own, but it does reopen the provisioning
|
|
1557
|
+
window: the next `start` re-applies the retained intent (`packages`, `deviceMode`,
|
|
1558
|
+
`secureLogin`) as a first boot would. The credential facts above are cleared rather
|
|
1559
|
+
than retained, so an explicit `--add-user` / `--disable-admin` — and anything from a
|
|
1560
|
+
request that was refused, which is never persisted — has to be passed again.
|
|
1370
1561
|
|
|
1371
1562
|
### Snapshots
|
|
1372
1563
|
|
|
@@ -1410,13 +1601,13 @@ present, then exits `1`.
|
|
|
1410
1601
|
| `MISSING_FIRMWARE` | edk2 UEFI firmware not installed (arm64) | install `qemu-efi-aarch64` / `edk2-aarch64` |
|
|
1411
1602
|
| `MISSING_UNZIP` | image extraction needs `unzip` | install OS package |
|
|
1412
1603
|
| `PORT_CONFLICT` | no free 10-port block | stop a machine or set `--port-base` |
|
|
1413
|
-
| `BOOT_TIMEOUT` | CHR didn't respond within timeout | check `qemu.log`; under TCG, raise `--timeout-extra`;
|
|
1604
|
+
| `BOOT_TIMEOUT` | CHR didn't respond within timeout | check `qemu.log`; under TCG, raise `--timeout-extra`; previously booted machines are **preserved**; new machines are removed, existing first boots reset, unless `QUICKCHR_PRESERVE_ON_FAILURE=1` |
|
|
1414
1605
|
| `DOWNLOAD_FAILED` | image fetch failed | retry; check connectivity to MikroTik download server |
|
|
1415
1606
|
| `INVALID_VERSION` / `INVALID_ARCH` / `INVALID_NAME` / `INVALID_DISK_SIZE` | argument parse | fix the value |
|
|
1416
1607
|
| `MACHINE_EXISTS` | name already in `machines/` | choose another name or `QuickCHR.get(name)` |
|
|
1417
1608
|
| `MACHINE_NOT_FOUND` | name not in `machines/` | `quickchr list` |
|
|
1418
1609
|
| `MACHINE_RUNNING` / `MACHINE_STOPPED` | wrong lifecycle state | `start` or `stop` first |
|
|
1419
|
-
| `MACHINE_LOCKED` | another
|
|
1610
|
+
| `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 |
|
|
1420
1611
|
| `EXEC_FAILED` | RouterOS rejected the command (auth, syntax) | check creds, command syntax |
|
|
1421
1612
|
| `PROCESS_FAILED` | QEMU spawn / reboot / verify failed | check `qemu.log`, host resources |
|
|
1422
1613
|
| `SPAWN_FAILED` | exec/spawn of host binary failed | check binary exists + executable |
|
|
@@ -1424,7 +1615,9 @@ present, then exits `1`.
|
|
|
1424
1615
|
| `QGA_TIMEOUT` | QGA daemon unreachable | KVM-only; not on macOS HVF or TCG |
|
|
1425
1616
|
| `NETWORK_UNAVAILABLE` | provisioning needs `user` NIC, none present | add `--add-network user` |
|
|
1426
1617
|
| `INVALID_NETWORK` | unknown alias / unresolvable iface / dead socket daemon | `quickchr networks` |
|
|
1427
|
-
| `PROVISIONING_VERSION_UNSUPPORTED` | provisioning option on RouterOS < 7.20.8 | use `--channel long-term` or pin a `7.20.8+` version |
|
|
1618
|
+
| `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
|
+
| `REST_UNAVAILABLE` | REST operation (`exec`, `get`, `rest()`) on a RouterOS 6 machine | use the native API — `centrs … --via native-api --no-validate` |
|
|
1620
|
+
| `INVALID_ARGUMENT` | a flag combination quickchr will not honour — e.g. `--no-wait` with provisioning | drop one of the two |
|
|
1428
1621
|
| `INSUFFICIENT_DISK_SPACE` | host data dir full | free space or move via `QUICKCHR_DATA_DIR` |
|
|
1429
1622
|
| `STATE_ERROR` | snapshot on raw boot disk | recreate with `--boot-disk-format=qcow2` |
|
|
1430
1623
|
| `INVALID_SIZE_STRING` | invalid cache/settings size string | use size syntax like `512M`, `2G`, or `1.5G` |
|
|
@@ -1433,10 +1626,44 @@ present, then exits `1`.
|
|
|
1433
1626
|
|
|
1434
1627
|
### Auto-cleanup on boot timeout
|
|
1435
1628
|
|
|
1436
|
-
If `start()` hits `BOOT_TIMEOUT`, quickchr
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1629
|
+
If `start()` hits `BOOT_TIMEOUT`, quickchr stops the failing instance and then:
|
|
1630
|
+
|
|
1631
|
+
| Machine at launch entry | Timeout cleanup | Recovery |
|
|
1632
|
+
| --- | --- | --- |
|
|
1633
|
+
| Created by this `start()` call | Remove automatically | Repeat the original command |
|
|
1634
|
+
| Existing, never booted, provisioning window open | Reset factory disk and restore the pre-launch record, window open | `quickchr start <name>` |
|
|
1635
|
+
| Existing, provisioning window already closed | Preserve disks, credentials and persistent configuration; clear PID and release socket memberships | `quickchr start <name>`, or explicit `quickchr remove <name>` |
|
|
1636
|
+
|
|
1637
|
+
`QUICKCHR_PRESERVE_ON_FAILURE=1` skips removal/reset for the first two cases and
|
|
1638
|
+
keeps the machine as it failed. Previously booted machines survive without the
|
|
1639
|
+
flag, including older records whose only boot-history marker is `lastStartedAt`.
|
|
1640
|
+
The diagnostic report is written outside the machine directory before cleanup;
|
|
1641
|
+
QEMU logs in a preserved machine also remain available. A configured guest does
|
|
1642
|
+
not need `clean` to retry: that command deliberately erases its configuration.
|
|
1643
|
+
|
|
1644
|
+
### RouterOS 6 (boot-only)
|
|
1645
|
+
|
|
1646
|
+
A best-effort target for v6→v7 migration testing (tikoci/centrs#386), CLI only —
|
|
1647
|
+
the setup wizard does not offer it.
|
|
1648
|
+
|
|
1649
|
+
```sh
|
|
1650
|
+
quickchr add --name lab6 --version 6.49.23 --arch x86
|
|
1651
|
+
quickchr start lab6
|
|
1652
|
+
centrs retrieve --quickchr lab6 /system/resource --via native-api --no-validate
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
- **x86 only.** MikroTik publishes no arm64 CHR for 6.x; `--arch arm64` is refused
|
|
1656
|
+
before download (`INVALID_ARCH`).
|
|
1657
|
+
- **Readiness is a native-API `/login`** on guest port 8728 (two consecutive
|
|
1658
|
+
answers), since 6.x has no REST. A 6.49.23 guest is ready in ~25 s under HVF.
|
|
1659
|
+
- **No provisioning.** Every provisioning flag, and the post-boot `set` routes, are
|
|
1660
|
+
refused before download where possible (`PROVISIONING_VERSION_UNSUPPORTED`). The
|
|
1661
|
+
`secure-login` setting is a default, not a request, so it does not apply to 6.x.
|
|
1662
|
+
- **No REST.** `exec` (REST/auto transport), `get`'s live groups and
|
|
1663
|
+
`ChrInstance.rest()` fail with `REST_UNAVAILABLE` rather than time out.
|
|
1664
|
+
`inspect` reports `rest-api` unavailable and `native-api` available.
|
|
1665
|
+
- `list`, `inspect`, `stop`, `remove` and `start --no-wait` work as for 7.x
|
|
1666
|
+
(checked live on 6.49.23).
|
|
1440
1667
|
|
|
1441
1668
|
### Where to look
|
|
1442
1669
|
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> **Experimental** — quickchr is under active development. Most testing has been on
|
|
4
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,
|
|
6
|
-
>
|
|
7
|
-
>
|
|
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
8
|
|
|
9
9
|
CLI and library to download, launch, and manage MikroTik CHR virtual machines via QEMU.
|
|
10
10
|
|
|
@@ -158,6 +158,11 @@ quickchr remove my-chr
|
|
|
158
158
|
quickchr doctor
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
+
`quickchr stop <name>` terminates QEMU (a power cut). For an explicit guest
|
|
162
|
+
shutdown request, use `quickchr shutdown <name> [--timeout <seconds>]` (default
|
|
163
|
+
10 seconds, or `QUICKCHR_SHUTDOWN_TIMEOUT`). It waits for QEMU to exit and clearly
|
|
164
|
+
reports a forced power cut if the guest does not stop within the grace budget.
|
|
165
|
+
|
|
161
166
|
### Create / Start Options
|
|
162
167
|
|
|
163
168
|
| Flag | Description | Default |
|
|
@@ -172,8 +177,9 @@ quickchr doctor
|
|
|
172
177
|
| `--boot-size <size>` | Resize boot disk (e.g., 512M, 2G). Requires `qemu-img`. | |
|
|
173
178
|
| `--add-disk <size>` | Attach an extra blank qcow2 disk. Repeatable. Requires `qemu-img`. | |
|
|
174
179
|
| `--forward <spec>` | Add or pin a QEMU SLiRP hostfwd mapping. Repeatable. | |
|
|
175
|
-
| `--
|
|
180
|
+
| `--no-wait` | Return once QEMU is running; readiness is up to the caller. Not with provisioning flags or `--fg` | |
|
|
176
181
|
| `--fg` / `--foreground` | Run in foreground — serial console on stdio | |
|
|
182
|
+
| `--bg` / `--background` | **Deprecated, does nothing** — background is already the default. Warns; use `--no-wait` to return early | |
|
|
177
183
|
| `--add-package <pkg>` | Extra package to install (repeatable) | |
|
|
178
184
|
| `--install-all-packages` | Install all packages from `all_packages.zip` | |
|
|
179
185
|
| `--add-user <user:pass>` | Create a user after boot | |
|
|
@@ -204,7 +210,7 @@ quickchr separates **boot-only** machine management from **post-boot provisionin
|
|
|
204
210
|
|
|
205
211
|
| Feature | Minimum RouterOS | Notes |
|
|
206
212
|
|---------|------------------|-------|
|
|
207
|
-
| Boot / start / stop | Any RouterOS 7.x | Boot-only path; no post-boot RouterOS mutations |
|
|
213
|
+
| Boot / start / stop | Any RouterOS 7.x; RouterOS 6 on x86 (best-effort) | Boot-only path; no post-boot RouterOS mutations. 6.x has no REST: readiness is a native-API login, and provisioning is refused — see MANUAL "RouterOS 6 (boot-only)" |
|
|
208
214
|
| Disk resize / extra disks / disk inspection | Any RouterOS 7.x | Requires host `qemu-img`; QEMU-local only |
|
|
209
215
|
| Network attachment / port mappings | Any RouterOS 7.x | QEMU networking setup; not provisioning |
|
|
210
216
|
| Managed login / custom user / disable-admin | 7.20.8+ | Post-boot provisioning |
|
|
@@ -359,7 +365,8 @@ const result = await chr.exec("/system/resource/print");
|
|
|
359
365
|
console.log(result);
|
|
360
366
|
|
|
361
367
|
// Stop (or remove)
|
|
362
|
-
await chr.stop();
|
|
368
|
+
await chr.stop(); // power cut for normal teardown
|
|
369
|
+
// Alternatively, replace stop() above with: await chr.shutdown({ timeoutMs: 10_000 });
|
|
363
370
|
await chr.remove();
|
|
364
371
|
|
|
365
372
|
console.log(chr.state.bootDiskFormat); // "qcow2"
|
package/package.json
CHANGED
package/src/cli/flags.ts
CHANGED
|
@@ -24,13 +24,27 @@ const CREATE_FLAGS = [
|
|
|
24
24
|
|
|
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
|
-
"all", "dry-run", "background", "bg", "fg", "foreground",
|
|
27
|
+
"all", "dry-run", "wait", "background", "bg", "fg", "foreground",
|
|
28
28
|
"install-deps", "timeout-extra",
|
|
29
29
|
"license-level", "license-account", "license-password",
|
|
30
30
|
] as const;
|
|
31
31
|
|
|
32
|
+
/** `set` applies a change to a machine that has already booted, so it takes the
|
|
33
|
+
* post-boot subset of the provisioning flags plus `--license`'s credential flags.
|
|
34
|
+
*
|
|
35
|
+
* It is listed here for the **valueless** check, not for strictness: `set` still
|
|
36
|
+
* tolerates an unknown flag like every other non-creating command. The check that
|
|
37
|
+
* matters is arity — `quickchr set lab --device-mode` with no value parses as a
|
|
38
|
+
* boolean and would otherwise reach "Nothing to set", which blames the user for the
|
|
39
|
+
* wrong thing. */
|
|
40
|
+
const SET_ONLY_FLAGS = [
|
|
41
|
+
"name", "license", "level", "account", "password",
|
|
42
|
+
"device-mode", "device-mode-enable", "device-mode-disable",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
32
45
|
export const ADD_FLAGS: readonly string[] = CREATE_FLAGS;
|
|
33
46
|
export const START_FLAGS: readonly string[] = [...CREATE_FLAGS, ...START_ONLY_FLAGS];
|
|
47
|
+
export const SET_FLAGS: readonly string[] = SET_ONLY_FLAGS;
|
|
34
48
|
|
|
35
49
|
/** Every flag that takes a value, across every command — `parseFlags`'s arity table.
|
|
36
50
|
*
|