@tikoci/quickchr 0.2.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.
@@ -0,0 +1,690 @@
1
+ /**
2
+ * Shared types for quickchr — CHR QEMU Manager.
3
+ */
4
+
5
+ import type { ProgressLogger } from "./log.ts";
6
+
7
+ // --- Version & Architecture ---
8
+
9
+ export type Channel = "stable" | "long-term" | "testing" | "development";
10
+ export const CHANNELS: Channel[] = ["stable", "long-term", "testing", "development"];
11
+
12
+ export type Arch = "arm64" | "x86";
13
+ export const ARCHES: Arch[] = ["arm64", "x86"];
14
+
15
+ // --- Networking ---
16
+
17
+ /** @deprecated Use NetworkSpecifier instead. Kept for backward compatibility with old machine.json files. */
18
+ export type NetworkMode =
19
+ | "user"
20
+ | "vmnet-shared"
21
+ | { type: "vmnet-bridge"; iface: string };
22
+
23
+ /**
24
+ * Network specifier — expresses user intent for a network interface.
25
+ * Generic specifiers (user, socket, shared, bridged) are cross-platform;
26
+ * explicit specifiers (vmnet-shared, vmnet-bridged, tap) bypass platform resolution.
27
+ */
28
+ export type NetworkSpecifier =
29
+ | "user"
30
+ | { type: "socket"; name: string }
31
+ | { type: "socket-listen"; port: number }
32
+ | { type: "socket-connect"; port: number }
33
+ | { type: "socket-mcast"; group: string; port: number }
34
+ | "shared"
35
+ | { type: "bridged"; iface: string }
36
+ | "vmnet-shared"
37
+ | { type: "vmnet-bridged"; iface: string }
38
+ | { type: "tap"; ifname: string };
39
+
40
+ /** Result of resolving a NetworkSpecifier on a specific platform. */
41
+ export interface ResolvedNetwork {
42
+ /** QEMU -netdev and -device argument pairs. */
43
+ qemuNetdevArgs: string[];
44
+ /** Optional wrapper command (e.g. socket_vmnet_client). When set, QEMU is spawned as a child of this wrapper. */
45
+ wrapper?: string[];
46
+ /** If the specifier was downgraded (e.g. shared → user), details are here. */
47
+ downgraded?: { from: string; reason: string };
48
+ /** MAC address override for deterministic addressing (socket_vmnet DHCP). */
49
+ mac?: string;
50
+ }
51
+
52
+ /** A single network interface in a machine's config. */
53
+ export interface NetworkConfig {
54
+ /** The user-specified intent (stored in machine.json for re-resolution on restart). */
55
+ specifier: NetworkSpecifier;
56
+ /** QEMU netdev id (net0, net1, ...). */
57
+ id: string;
58
+ /** Resolved QEMU args (populated at launch time, not persisted). */
59
+ resolved?: ResolvedNetwork;
60
+ }
61
+
62
+ /** Physical network interface detected on the host. */
63
+ export interface HostInterface {
64
+ /** OS device name (e.g. en0, eth0). */
65
+ device: string;
66
+ /** Human-readable port name (e.g. "Wi-Fi", "Thunderbolt 1"). */
67
+ name: string;
68
+ /** MAC address. */
69
+ mac?: string;
70
+ /** Interface alias for --add-network convenience: "wifi", "ethernet", or undefined. */
71
+ alias?: "wifi" | "ethernet";
72
+ }
73
+
74
+ /** socket_vmnet daemon availability on macOS. */
75
+ export interface SocketVmnetInfo {
76
+ /** Path to socket_vmnet_client binary. */
77
+ client: string;
78
+ /** Path to shared daemon socket (if running). */
79
+ sharedSocket?: string;
80
+ /** Map of interface name → bridged daemon socket path (if running). */
81
+ bridgedSockets: Record<string, string>;
82
+ }
83
+
84
+ export interface PortMapping {
85
+ name: string;
86
+ host: number;
87
+ guest: number;
88
+ proto: "tcp" | "udp";
89
+ }
90
+
91
+ /** Default RouterOS service ports and their offset within a port block. */
92
+ export const SERVICE_PORTS = {
93
+ http: { guest: 80, offset: 0, proto: "tcp" as const },
94
+ https: { guest: 443, offset: 1, proto: "tcp" as const },
95
+ ssh: { guest: 22, offset: 2, proto: "tcp" as const },
96
+ api: { guest: 8728, offset: 3, proto: "tcp" as const },
97
+ "api-ssl": { guest: 8729, offset: 4, proto: "tcp" as const },
98
+ winbox: { guest: 8291, offset: 5, proto: "tcp" as const },
99
+ } as const;
100
+
101
+ export type ServiceName = keyof typeof SERVICE_PORTS;
102
+ export const SERVICE_NAMES = Object.keys(SERVICE_PORTS) as ServiceName[];
103
+
104
+ /** Ports per instance block for allocation. */
105
+ export const PORTS_PER_BLOCK = 10;
106
+ export const DEFAULT_PORT_BASE = 9100;
107
+
108
+ /**
109
+ * TCP endpoint for QEMU IPC channels on Windows.
110
+ * Used instead of named pipes when QEMU's Winsock bind() can't handle \\.\pipe\ paths.
111
+ * Offsets within the port block: monitor=+6, serial=+7, qga=+8.
112
+ */
113
+ export type ChannelTcpEndpoint = { host: string; port: number };
114
+
115
+ // --- Disk ---
116
+
117
+ export type BootDiskFormat = "raw" | "qcow2";
118
+
119
+ /** Information about a single QEMU snapshot stored in a qcow2 disk image.
120
+ *
121
+ * Populated by parsing QEMU monitor `info snapshots` output or
122
+ * `qemu-img info --output=json`. The `vmStateSize` field gives a rough
123
+ * indication of how much guest state has changed — larger values typically
124
+ * mean more RAM pages were dirty at snapshot time. */
125
+ export interface SnapshotInfo {
126
+ /** Numeric snapshot ID (assigned by QEMU, monotonically increasing). */
127
+ id: string;
128
+ /** Human-readable tag / name (the string passed to `savevm`). */
129
+ name: string;
130
+ /** Size of the saved VM state in bytes. Gives a rough sense of how "heavy"
131
+ * the snapshot is — proportional to the amount of dirty guest RAM at save time. */
132
+ vmStateSize: number;
133
+ /** When the snapshot was created (ISO 8601 string). */
134
+ date: string;
135
+ /** Guest VM clock at snapshot time, formatted as `HH:MM:SS.mmm`. */
136
+ vmClock: string;
137
+ /** Instruction count at snapshot time, or `undefined` if QEMU reported `--`. */
138
+ icount?: number;
139
+ }
140
+
141
+ // --- Machine State ---
142
+
143
+ export interface MachineConfig {
144
+ name: string;
145
+ version: string;
146
+ arch: Arch;
147
+ cpu: number;
148
+ mem: number;
149
+ /** @deprecated Use `networks` instead. Kept for backward compat with old machine.json. */
150
+ network?: NetworkMode;
151
+ /** Network interfaces for this machine. Default: single user-mode NIC. */
152
+ networks: NetworkConfig[];
153
+ ports: Record<string, PortMapping>;
154
+ packages: string[];
155
+ /** Install all packages from all_packages.zip on first boot. */
156
+ installAllPackages?: boolean;
157
+ deviceMode?: DeviceModeOptions;
158
+ user?: { name: string; password: string };
159
+ disableAdmin?: boolean;
160
+ /** Create a 'quickchr' managed account with a generated password. Defaults to true. */
161
+ secureLogin?: boolean;
162
+ portBase: number;
163
+ excludePorts: ServiceName[];
164
+ extraPorts: PortMapping[];
165
+ licenseLevel?: LicenseLevel;
166
+ /** Boot disk size override (e.g. "512M", "2G"). Requires `qemu-img` on the host.
167
+ * When set, the boot disk is converted from raw to qcow2. */
168
+ bootSize?: string;
169
+ /** Extra blank disk sizes (e.g. ["512M", "1G"]). Requires `qemu-img` on the host.
170
+ * Extra disks are always created as qcow2 images. */
171
+ extraDisks?: string[];
172
+ /** Format of the boot disk — raw by default, qcow2 when resized. */
173
+ bootDiskFormat?: BootDiskFormat;
174
+ }
175
+
176
+ export interface MachineState extends MachineConfig {
177
+ createdAt: string;
178
+ lastStartedAt?: string;
179
+ status: "stopped" | "running" | "error";
180
+ pid?: number;
181
+ /** Path to the machine directory. */
182
+ machineDir: string;
183
+ }
184
+
185
+ // --- Start Options ---
186
+
187
+ /**
188
+ * Options for creating and starting a CHR instance.
189
+ *
190
+ * Boot-only options (version, arch, cpu, mem, networks, disks) work with any RouterOS 7.x.
191
+ * Post-boot provisioning options (packages, user, license, deviceMode) require RouterOS ≥ 7.20.8
192
+ * and will throw if used with an older version.
193
+ *
194
+ * When `installAllPackages` is set, boot time increases significantly (full package install
195
+ * requires SCP upload + reboot cycle, typically 60–120s extra on native acceleration).
196
+ */
197
+ export interface StartOptions {
198
+ /** RouterOS version to use (e.g. "7.22.1"). Mutually exclusive with `channel`. */
199
+ version?: string;
200
+ /** Release channel to resolve latest version from. Default: "stable". */
201
+ channel?: Channel;
202
+ /** Guest architecture. Default: matches host (arm64 on Apple Silicon, x86 on Intel/AMD). */
203
+ arch?: Arch;
204
+ /** Instance name. Auto-generated from version+arch if omitted. Must not start with "-". */
205
+ name?: string;
206
+ /** Number of virtual CPUs. Default: 1. */
207
+ cpu?: number;
208
+ /** Memory in MiB. Default: 512 (1024 for cross-arch TCG emulation). */
209
+ mem?: number;
210
+ /** Run QEMU in background (true, default) or foreground with serial console on stdio (false). */
211
+ background?: boolean;
212
+ /** Extra packages to install after boot.
213
+ * This is quickchr provisioning, so it is validated/tested on RouterOS 7.20.8+ only. */
214
+ packages?: string[];
215
+ /** Install all packages from the all_packages ZIP (overrides packages[]).
216
+ * This is quickchr provisioning, so it is validated/tested on RouterOS 7.20.8+ only. */
217
+ installAllPackages?: boolean;
218
+ /** Create a user after boot.
219
+ * This is quickchr provisioning, so it is validated/tested on RouterOS 7.20.8+ only. */
220
+ user?: { name: string; password: string };
221
+ /** Disable the default admin account after boot.
222
+ * This is quickchr provisioning, so it is validated/tested on RouterOS 7.20.8+ only. */
223
+ disableAdmin?: boolean;
224
+ /** Create a 'quickchr' managed account with a generated password. Defaults to true.
225
+ * Set to false to keep admin with no password (exec still works).
226
+ * Enabling the managed login path is quickchr provisioning, so it is validated/tested
227
+ * on RouterOS 7.20.8+ only. */
228
+ secureLogin?: boolean;
229
+ /** Starting port number for this instance's port block. Auto-allocated if omitted. */
230
+ portBase?: number;
231
+ /** Services to exclude from port mappings (e.g. ["winbox", "api-ssl"]). */
232
+ excludePorts?: ServiceName[];
233
+ /** Additional custom port mappings appended to the default set. */
234
+ extraPorts?: PortMapping[];
235
+ /** @deprecated Use `networks` instead. Kept for backward compatibility. */
236
+ network?: NetworkMode;
237
+ /** Network interfaces. Each entry becomes a NIC on the CHR.
238
+ * Default (when omitted): single user-mode NIC.
239
+ * When specified: count of entries = count of NICs (explicit control). */
240
+ networks?: NetworkSpecifier[];
241
+ /** Install OS-level dependencies (QEMU, firmware) automatically if missing. */
242
+ installDeps?: boolean;
243
+ /** Print QEMU command and config without actually starting the VM. */
244
+ dryRun?: boolean;
245
+ /** Apply a CHR trial license after boot via /system/license/renew.
246
+ * Pass a level string (e.g. "p1") to auto-resolve MikroTik credentials,
247
+ * or a LicenseOptions object to supply credentials explicitly.
248
+ * This is quickchr provisioning, so it is validated/tested on RouterOS 7.20.8+ only. */
249
+ license?: LicenseInput;
250
+ /** Configure /system/device-mode after boot. If omitted, CHR boots with RouterOS defaults (mode=advanced).
251
+ * This is quickchr provisioning, so it is validated/tested on RouterOS 7.20.8+ only. */
252
+ deviceMode?: DeviceModeOptions;
253
+ /** Boot disk size override (e.g. "512M", "2G"). Requires `qemu-img` on the host.
254
+ * Converts the boot disk to qcow2 before first boot. */
255
+ bootSize?: string;
256
+ /** Boot disk format. `qcow2` enables snapshots and resize; `raw` keeps the original raw image.
257
+ * Raw boot disks cannot be resized. */
258
+ bootDiskFormat?: BootDiskFormat;
259
+ /** Extra blank disks to attach, specified as sizes (e.g. ["512M", "1G"]).
260
+ * Requires `qemu-img` on the host. Extra disks are always qcow2. */
261
+ extraDisks?: string[];
262
+ /** Extra milliseconds added to the computed boot timeout.
263
+ * Useful on slow hosts where `defaultBootTimeout` underestimates actual boot time. */
264
+ timeoutExtra?: number;
265
+ /** Progress callback for status messages during start.
266
+ * If not provided, messages go to console.log.
267
+ * Debug-level messages (prefixed [debug]) are only emitted when QUICKCHR_DEBUG=1. */
268
+ onProgress?: (message: string) => void;
269
+ }
270
+
271
+ // --- Instance (runtime handle) ---
272
+
273
+ export interface ChrPorts {
274
+ http: number;
275
+ https: number;
276
+ ssh: number;
277
+ api: number;
278
+ apiSsl: number;
279
+ winbox: number;
280
+ [key: string]: number;
281
+ }
282
+
283
+ /** Return value of ChrInstance.queryLoad(). */
284
+ export interface ChrLoadSample {
285
+ /** Host CPU percent across all virtual CPUs (0–100 × vCPU count). */
286
+ cpuPercent: number;
287
+ /** Balloon-reported guest memory in use, in MiB. */
288
+ memUsedMb: number;
289
+ }
290
+
291
+ /**
292
+ * Runtime handle for a CHR virtual machine.
293
+ *
294
+ * Returned by {@link QuickCHR.start}. Provides methods to interact with the running
295
+ * (or stopped) instance: REST API calls, CLI command execution, QGA, serial console,
296
+ * snapshots, licensing, and lifecycle management.
297
+ *
298
+ * After `QuickCHR.start()` resolves, the instance is REST-ready — all requested
299
+ * provisioning (packages, license, device-mode, user creation) has completed.
300
+ */
301
+ export interface ChrInstance {
302
+ name: string;
303
+ state: MachineState;
304
+ ports: ChrPorts;
305
+ restUrl: string;
306
+ sshPort: number;
307
+
308
+ /** Check or wait for REST API readiness.
309
+ * After `QuickCHR.start()` with provisioning options (installAllPackages, license, etc.),
310
+ * the returned instance is already REST-reachable — this returns immediately.
311
+ * Call this only when starting without provisioning, or as a safety check after a manual stop/restart. */
312
+ waitForBoot(timeoutMs?: number): Promise<boolean>;
313
+ stop(): Promise<void>;
314
+ remove(): Promise<void>;
315
+ clean(): Promise<void>;
316
+ /** Stop and permanently delete this instance (disk + state). Equivalent to stop() then remove(). */
317
+ destroy(): Promise<void>;
318
+
319
+ monitor(command: string): Promise<string>;
320
+ serial(): { readable: ReadableStream; writable: WritableStream };
321
+ /**
322
+ * Execute any raw QGA command. Prefer the typed helpers (`qgaGetOsInfo`,
323
+ * `qgaGetNetworkInterfaces`, etc.) exported from this package for common
324
+ * operations. Use this for advanced / one-off commands.
325
+ *
326
+ * x86_64 only — throws `QGA_UNSUPPORTED` on ARM64 until MikroTik ships
327
+ * guest-agent support for that architecture.
328
+ */
329
+ qga(command: QgaCommand, args?: object): Promise<unknown>;
330
+
331
+ rest(path: string, opts?: RequestInit): Promise<unknown>;
332
+ /** Run a RouterOS CLI command against the instance. */
333
+ exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
334
+ /** Apply or renew a CHR trial license.
335
+ * quickchr validates this provisioning flow on RouterOS 7.20.8+ only. */
336
+ license(opts: LicenseOptions): Promise<void>;
337
+ /** Change device-mode on a running instance (e.g. enable container, rose, etc.).
338
+ * quickchr validates this provisioning flow on RouterOS 7.20.8+ only.
339
+ * This requires a hard QEMU power-cycle to confirm the change — the instance
340
+ * will briefly stop and restart. Wait for the returned promise before using the
341
+ * instance again.
342
+ * @param options Desired device-mode flags (same as StartOptions.deviceMode).
343
+ * @param logger Optional progress logger for status/debug output. */
344
+ setDeviceMode(options: DeviceModeOptions, logger?: ProgressLogger): Promise<void>;
345
+
346
+ /** List extra packages available for this instance's version and arch.
347
+ * Downloads and caches the all_packages ZIP on the first call. */
348
+ availablePackages(): Promise<string[]>;
349
+ /** Install extra package(s), reboot, and wait until REST API is ready again.
350
+ * quickchr validates this provisioning flow on RouterOS 7.20.8+ only.
351
+ * Accepts a single package name or an array. Returns the names that were
352
+ * actually installed (missing packages are skipped with a warning). */
353
+ installPackage(packages: string | string[]): Promise<string[]>;
354
+
355
+ /** Build an env-var map for spawning a subprocess against this instance.
356
+ * Includes QUICKCHR_* prefixed keys plus legacy URLBASE/BASICAUTH for compat.
357
+ * Resolves auth from the instance secret store, provisioned user, or admin default.
358
+ *
359
+ * @example
360
+ * Bun.spawn(["bun", "my-script.ts"], { env: { ...process.env, ...await chr.subprocessEnv() } })
361
+ */
362
+ subprocessEnv(): Promise<Record<string, string>>;
363
+
364
+ /** Sample QEMU guest load from the monitor. Returns null when the monitor is
365
+ * unavailable (machine stopped or running in foreground mode). */
366
+ queryLoad(): Promise<ChrLoadSample | null>;
367
+
368
+ /** Snapshot operations on this instance's boot disk.
369
+ *
370
+ * Requires a qcow2 boot disk (`bootDiskFormat: "qcow2"` in StartOptions).
371
+ * Raw disks do not support snapshots — methods throw `STATE_ERROR` if called
372
+ * on a raw-disk instance.
373
+ *
374
+ * **For running machines**, `save`/`load`/`delete` use the QEMU monitor
375
+ * (`savevm`/`loadvm`/`delvm`). `list` uses the monitor for live state.
376
+ *
377
+ * **For stopped machines**, only `list` works (reads qcow2 metadata directly
378
+ * via `qemu-img info`). Other operations require the machine to be running.
379
+ *
380
+ * @example
381
+ * const snaps = await instance.snapshot.list();
382
+ * await instance.snapshot.save("before-upgrade");
383
+ * // ... do something risky ...
384
+ * await instance.snapshot.load("before-upgrade");
385
+ */
386
+ snapshot: {
387
+ /** List all snapshots on this instance's boot disk.
388
+ * Works on both running and stopped machines. */
389
+ list(): Promise<SnapshotInfo[]>;
390
+ /** Save a snapshot of the current VM state. Requires the machine to be running.
391
+ * @param name - Snapshot tag name (e.g. "before-upgrade"). Auto-generated if omitted. */
392
+ save(name?: string): Promise<SnapshotInfo>;
393
+ /** Load (restore) a previously saved snapshot. Requires the machine to be running.
394
+ * The VM state is replaced; existing runtime state is lost.
395
+ * @param name - Snapshot tag name to restore. */
396
+ load(name: string): Promise<void>;
397
+ /** Delete a snapshot. Requires the machine to be running.
398
+ * @param name - Snapshot tag name to delete. */
399
+ delete(name: string): Promise<void>;
400
+ };
401
+ }
402
+
403
+ // --- Platform ---
404
+
405
+ export interface EfiFirmwarePaths {
406
+ code: string;
407
+ vars: string;
408
+ }
409
+
410
+ export type PackageManager = "brew" | "apt" | "dnf" | "pacman" | "winget" | "unknown";
411
+
412
+ export interface PlatformInfo {
413
+ os: "darwin" | "linux" | "win32";
414
+ hostArch: "x64" | "arm64";
415
+ packageManager: PackageManager;
416
+ qemuBinX86?: string;
417
+ qemuBinArm64?: string;
418
+ qemuImg?: string;
419
+ efiFirmware?: EfiFirmwarePaths;
420
+ accelAvailable: string[];
421
+ socketVmnet?: SocketVmnetInfo;
422
+ }
423
+
424
+ // --- Doctor ---
425
+
426
+ export interface DoctorCheck {
427
+ label: string;
428
+ status: "ok" | "warn" | "error";
429
+ detail: string;
430
+ }
431
+
432
+ export interface DoctorResult {
433
+ checks: DoctorCheck[];
434
+ ok: boolean;
435
+ }
436
+
437
+ // --- Errors ---
438
+
439
+ export type ErrorCode =
440
+ | "MISSING_QEMU"
441
+ | "MISSING_FIRMWARE"
442
+ | "MISSING_BUN"
443
+ | "MISSING_UNZIP"
444
+ | "PORT_CONFLICT"
445
+ | "BOOT_TIMEOUT"
446
+ | "QGA_UNSUPPORTED"
447
+ | "QGA_TIMEOUT"
448
+ | "DOWNLOAD_FAILED"
449
+ | "MACHINE_EXISTS"
450
+ | "MACHINE_NOT_FOUND"
451
+ | "MACHINE_RUNNING"
452
+ | "MACHINE_STOPPED"
453
+ | "INVALID_VERSION"
454
+ | "INVALID_ARCH"
455
+ | "INVALID_NAME"
456
+ | "INVALID_DISK_SIZE"
457
+ | "MACHINE_LOCKED"
458
+ | "EXEC_FAILED"
459
+ | "PROCESS_FAILED"
460
+ | "SPAWN_FAILED"
461
+ | "NETWORK_UNAVAILABLE"
462
+ | "INVALID_NETWORK"
463
+ | "PROVISIONING_VERSION_UNSUPPORTED"
464
+ | "INSUFFICIENT_DISK_SPACE"
465
+ | "STATE_ERROR";
466
+
467
+ export class QuickCHRError extends Error {
468
+ code: ErrorCode;
469
+ installHint?: string;
470
+
471
+ constructor(code: ErrorCode, message: string, installHint?: string) {
472
+ super(message);
473
+ this.name = "QuickCHRError";
474
+ this.code = code;
475
+ this.installHint = installHint;
476
+ }
477
+ }
478
+
479
+ // --- Exec ---
480
+
481
+ /** Transport used to execute a RouterOS CLI command. */
482
+ export type ExecTransport = "auto" | "rest" | "ssh" | "console" | "qga";
483
+
484
+ /** Options for ChrInstance.exec(). */
485
+ export interface ExecOptions {
486
+ /** Transport to use. "auto" tries REST /execute, future: SSH fallback. */
487
+ via?: ExecTransport;
488
+ /** Override username (default: resolved from machine config). */
489
+ user?: string;
490
+ /** Override password (default: resolved from machine config). */
491
+ password?: string;
492
+ /** Request timeout in milliseconds (default: 30 000). */
493
+ timeout?: number;
494
+ }
495
+
496
+ /** Result of a ChrInstance.exec() call. */
497
+ export interface ExecResult {
498
+ /** Command output as a string (or parsed JSON when output="json"). */
499
+ output: string;
500
+ /** Which transport actually carried the command. */
501
+ via: ExecTransport;
502
+ }
503
+
504
+ // --- QGA (QEMU Guest Agent) ---
505
+
506
+ /** Result of a QGA guest-exec command (exitcode + decoded stdout/stderr). */
507
+ export interface QgaExecResult {
508
+ exitcode: number;
509
+ stdout: string;
510
+ stderr: string;
511
+ }
512
+
513
+ /**
514
+ * All QGA commands supported by RouterOS CHR (x86_64 only).
515
+ *
516
+ * Use with {@link ChrInstance.qga} for raw access, or call the typed QGA
517
+ * helper functions (`qgaGetOsInfo`, `qgaGetNetworkInterfaces`, etc.).
518
+ *
519
+ * RouterOS implements these via its own QGA (version "2.10.50") — not stock
520
+ * qemu-ga. The `guest-exec` command only accepts `input-data` (RouterOS script),
521
+ * not `path`. File operations use flat RouterOS filenames only (no directories).
522
+ *
523
+ * QGA requires KVM — RouterOS only starts the guest agent daemon under KVM hypervisors.
524
+ * On macOS (HVF) and Windows, the daemon never starts; calls will time out.
525
+ * ARM64 CHR does not implement QGA at all (MikroTik feature, pending).
526
+ */
527
+ export type QgaCommand =
528
+ | "guest-ping"
529
+ | "guest-info"
530
+ | "guest-get-osinfo"
531
+ | "guest-get-host-name"
532
+ | "guest-get-time"
533
+ | "guest-get-timezone"
534
+ | "guest-network-get-interfaces"
535
+ | "guest-fsfreeze-status"
536
+ | "guest-fsfreeze-freeze"
537
+ | "guest-fsfreeze-thaw"
538
+ | "guest-shutdown"
539
+ | "guest-exec"
540
+ | "guest-exec-status"
541
+ | "guest-file-open"
542
+ | "guest-file-read"
543
+ | "guest-file-write"
544
+ | "guest-file-close"
545
+ | "guest-file-flush";
546
+
547
+ /** OS information returned by {@link qgaGetOsInfo}. */
548
+ export interface QgaOsInfo {
549
+ /** Always "routeros" */
550
+ id: string;
551
+ /** "RouterOS" */
552
+ name: string;
553
+ /** e.g. "RouterOS 7.22" */
554
+ prettyName: string;
555
+ /** Linux kernel release, e.g. "5.6.3-64" */
556
+ kernelRelease: string;
557
+ /** CPU architecture, e.g. "x86_64" */
558
+ machine: string;
559
+ }
560
+
561
+ /** Single IP address on a network interface (from {@link qgaGetNetworkInterfaces}). */
562
+ export interface QgaNetworkIpAddress {
563
+ type: "ipv4" | "ipv6";
564
+ address: string;
565
+ prefix: number;
566
+ }
567
+
568
+ /** Network interface as seen by RouterOS (from {@link qgaGetNetworkInterfaces}). */
569
+ export interface QgaNetworkInterface {
570
+ /** RouterOS interface name, e.g. "ether1" */
571
+ name: string;
572
+ /** MAC address, e.g. "0e:61:47:d8:43:2a" */
573
+ mac?: string;
574
+ ipAddresses: QgaNetworkIpAddress[];
575
+ }
576
+
577
+ /** Timezone information returned by {@link qgaGetTimezone}. */
578
+ export interface QgaTimezone {
579
+ /** Offset from UTC in seconds (positive = east of UTC) */
580
+ offset: number;
581
+ /** IANA timezone name if reported by the guest */
582
+ zone?: string;
583
+ }
584
+
585
+ /** Filesystem freeze state returned by {@link qgaFsFreezeStatus}. */
586
+ export type QgaFsFreezeStatus = "thawed" | "frozen";
587
+
588
+ // --- CHR License ---
589
+
590
+ /** CHR trial license levels (speed caps). free = 1 Mbps, p1 = 1 Gbps, p10 = 10 Gbps, unlimited = no cap. */
591
+ export type LicenseLevel = "p1" | "p10" | "unlimited";
592
+ export const LICENSE_LEVELS: LicenseLevel[] = ["p1", "p10", "unlimited"];
593
+
594
+ /** Options passed to /system/license/renew.
595
+ * `account` and `password` are optional: when omitted, quickchr resolves them
596
+ * from env vars (MIKROTIK_WEB_ACCOUNT / MIKROTIK_WEB_PASSWORD) or the secret store.
597
+ */
598
+ export interface LicenseOptions {
599
+ account?: string;
600
+ password?: string;
601
+ level?: LicenseLevel;
602
+ }
603
+
604
+ /** License shorthand accepted by StartOptions.license.
605
+ * A string value (e.g. "p1") auto-resolves MikroTik credentials from env vars
606
+ * or the secret store. An object lets the caller supply credentials explicitly.
607
+ */
608
+ export type LicenseInput = LicenseLevel | LicenseOptions;
609
+
610
+ // --- Device-mode ---
611
+
612
+ /** Device-mode options for /system/device-mode/update. */
613
+ export interface DeviceModeOptions {
614
+ /** Profile mode. Supports known modes plus unknown future values. */
615
+ mode?: string;
616
+ /** Feature names to set to yes (e.g. container, routerboard). */
617
+ enable?: string[];
618
+ /** Feature names to set to no (e.g. zerotier, iot). */
619
+ disable?: string[];
620
+ }
621
+
622
+ // --- Known extra packages ---
623
+ // Baseline: exact contents of RouterOS 7.22.1 all_packages ZIP files.
624
+ // x86: all_packages-x86-7.22.1.zip
625
+ // arm64: all_packages-arm64-7.22.1.zip
626
+ // Note: some packages are hardware-specific (switch-marvell, wifi-*) or mutually
627
+ // exclusive (wireless vs wifi-qcom vs wifi-qcom-be). All are included here because
628
+ // even on CHR (VM) they register API endpoints useful for schema generation.
629
+
630
+ /** Extra packages in all_packages-x86-7.22.1.zip (sorted). */
631
+ export const KNOWN_PACKAGES_X86 = [
632
+ "calea",
633
+ "container",
634
+ "dude",
635
+ "gps",
636
+ "iot",
637
+ "openflow",
638
+ "rose-storage",
639
+ "tr069-client",
640
+ "ups",
641
+ "user-manager",
642
+ "wireless",
643
+ ] as const;
644
+
645
+ /** Extra packages in all_packages-arm64-7.22.1.zip (sorted). */
646
+ export const KNOWN_PACKAGES_ARM64 = [
647
+ "calea",
648
+ "container",
649
+ "dude",
650
+ "extra-nic",
651
+ "gps",
652
+ "iot",
653
+ "iot-bt-extra",
654
+ "openflow",
655
+ "rose-storage",
656
+ "switch-marvell",
657
+ "tr069-client",
658
+ "ups",
659
+ "user-manager",
660
+ "wifi-qcom",
661
+ "wifi-qcom-be",
662
+ "wireless",
663
+ "zerotier",
664
+ ] as const;
665
+
666
+ /** Union of all known packages across architectures. */
667
+ export const KNOWN_PACKAGES = [
668
+ "calea",
669
+ "container",
670
+ "dude",
671
+ "extra-nic",
672
+ "gps",
673
+ "iot",
674
+ "iot-bt-extra",
675
+ "openflow",
676
+ "rose-storage",
677
+ "switch-marvell",
678
+ "tr069-client",
679
+ "ups",
680
+ "user-manager",
681
+ "wifi-qcom",
682
+ "wifi-qcom-be",
683
+ "wireless",
684
+ "zerotier",
685
+ ] as const;
686
+
687
+ /** Return the known packages for a specific architecture. */
688
+ export function knownPackagesForArch(arch: Arch): readonly string[] {
689
+ return arch === "arm64" ? KNOWN_PACKAGES_ARM64 : KNOWN_PACKAGES_X86;
690
+ }