@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/src/cli/index.ts
CHANGED
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
* quickchr CLI entry point — command router.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { StartOptions, Arch, Channel, ServiceName, NetworkSpecifier } from "../lib/types.ts";
|
|
6
|
+
import type { StartOptions, Arch, Channel, DeviceModeOptions, ServiceName, NetworkSpecifier } from "../lib/types.ts";
|
|
7
7
|
import { parseNetworkSpecifier } from "../lib/network.ts";
|
|
8
8
|
import { expandForwardSpec } from "../lib/forward-spec.ts";
|
|
9
|
-
import { ADD_FLAGS, REMOVED_FLAGS, START_FLAGS, VALUE_FLAGS, unknownFlags, unknownFlagMessage, valuelessFlags, valuelessFlagMessage } from "./flags.ts";
|
|
9
|
+
import { ADD_FLAGS, REMOVED_FLAGS, SET_FLAGS, START_FLAGS, VALUE_FLAGS, unknownFlags, unknownFlagMessage, valuelessFlags, valuelessFlagMessage } from "./flags.ts";
|
|
10
10
|
import { QuickCHRError } from "../lib/types.ts";
|
|
11
11
|
import { CENTRS_EXEC_TIP, CENTRS_SEE_ALSO, tip, tipsForError } from "./tips.ts";
|
|
12
12
|
import {
|
|
13
|
+
isRouterOs6,
|
|
13
14
|
MIN_PROVISION_VERSION,
|
|
14
15
|
PROVISIONING_BOOT_ONLY_SUMMARY,
|
|
15
16
|
PROVISIONING_FEATURE_SUMMARY,
|
|
16
17
|
provisioningSupportHint,
|
|
17
18
|
provisioningSupportSummary,
|
|
19
|
+
ROUTEROS6_BOOT_ONLY,
|
|
18
20
|
} from "../lib/versions.ts";
|
|
19
21
|
|
|
20
22
|
const args = process.argv.slice(2);
|
|
@@ -103,6 +105,20 @@ function flagBool(flags: Record<string, string | boolean | string[]>, key: strin
|
|
|
103
105
|
return flags[key] === true || flags[key] === "true";
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/** A boolean flag that keeps "not given" apart from `--no-<key>`: `true`, `false`, or
|
|
109
|
+
* `undefined` when the flag is absent.
|
|
110
|
+
*
|
|
111
|
+
* `flagBool()` collapses absent into `false`, and that is the wrong answer for any
|
|
112
|
+
* option `start()` merges with stored intent (`opts.x ?? existing.x`): `false ?? true`
|
|
113
|
+
* is `false`, so `quickchr add --install-all-packages` followed by a plain
|
|
114
|
+
* `quickchr start <name>` booted without the packages and said nothing (#185). Same
|
|
115
|
+
* rule as `--no-device-mode`: silence and refusal are different instructions, and only
|
|
116
|
+
* silence may fall through to what `add` recorded. */
|
|
117
|
+
export function flagOptionalBool(flags: Record<string, string | boolean | string[]>, key: string): boolean | undefined {
|
|
118
|
+
if (flags[key] === undefined) return undefined;
|
|
119
|
+
return flags[key] !== false && flags[key] !== "false";
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
function flagList(flags: Record<string, string | boolean | string[]>, key: string): string[] {
|
|
107
123
|
const val = flags[key];
|
|
108
124
|
if (Array.isArray(val)) return val;
|
|
@@ -114,6 +130,49 @@ function csvList(values: string[]): string[] {
|
|
|
114
130
|
return [...new Set(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean))];
|
|
115
131
|
}
|
|
116
132
|
|
|
133
|
+
/** The device-mode request the `--device-mode*` flags describe, or `undefined` when
|
|
134
|
+
* they describe nothing.
|
|
135
|
+
*
|
|
136
|
+
* One reader for all three verbs that take these flags. `add` and `start` had
|
|
137
|
+
* separate copies and they had already drifted: only `start` honoured
|
|
138
|
+
* `--no-device-mode`, so `quickchr add --no-device-mode --device-mode-enable container`
|
|
139
|
+
* enabled container while the same flags on `start` skipped device-mode with a
|
|
140
|
+
* warning. Two spellings of one intent producing opposite results is the silent-drop
|
|
141
|
+
* failure in another costume (#176).
|
|
142
|
+
*
|
|
143
|
+
* `--no-device-mode` means *skip device-mode entirely*, so it wins over the feature
|
|
144
|
+
* lists and says so rather than quietly discarding them.
|
|
145
|
+
*
|
|
146
|
+
* It returns an explicit `{ mode: "skip" }` rather than `undefined`, because
|
|
147
|
+
* `undefined` means *the user said nothing* and the two are not the same instruction.
|
|
148
|
+
* `start()` resolves a silent request against stored intent — `opts.deviceMode ??
|
|
149
|
+
* existing.deviceMode` — so a machine created with `add --device-mode-enable
|
|
150
|
+
* container` and then started with `--no-device-mode` provisioned container anyway,
|
|
151
|
+
* power cycle included. Measured on CHR 7.24.4: 48 s and `container=yes`, from a flag
|
|
152
|
+
* whose documented job is to skip the step. A flag that is accepted and does nothing
|
|
153
|
+
* is the defect this whole area exists to remove (#176). */
|
|
154
|
+
export function deviceModeFromFlags(
|
|
155
|
+
flags: Record<string, string | boolean | string[]>,
|
|
156
|
+
): DeviceModeOptions | undefined {
|
|
157
|
+
const mode = flag(flags, "device-mode");
|
|
158
|
+
const enable = csvList(flagList(flags, "device-mode-enable"));
|
|
159
|
+
const disable = csvList(flagList(flags, "device-mode-disable"));
|
|
160
|
+
|
|
161
|
+
if (flags["device-mode"] === false) {
|
|
162
|
+
if (enable.length > 0 || disable.length > 0) {
|
|
163
|
+
console.warn("Warning: --device-mode-enable/--device-mode-disable ignored because --no-device-mode was set.");
|
|
164
|
+
}
|
|
165
|
+
return { mode: "skip" };
|
|
166
|
+
}
|
|
167
|
+
if (mode === undefined && enable.length === 0 && disable.length === 0) return undefined;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
mode: mode ?? "auto",
|
|
171
|
+
enable: enable.length > 0 ? enable : undefined,
|
|
172
|
+
disable: disable.length > 0 ? disable : undefined,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
117
176
|
/** Build networks array from --add-network and --no-network. */
|
|
118
177
|
function buildNetworks(flags: Record<string, string | boolean | string[]>): NetworkSpecifier[] | undefined {
|
|
119
178
|
const specs: NetworkSpecifier[] = [];
|
|
@@ -222,10 +281,27 @@ export async function applyAccelFlag(flags: Record<string, string | boolean | st
|
|
|
222
281
|
|
|
223
282
|
/** Resolve --secure-login/--no-secure-login, falling back to the secure-login setting
|
|
224
283
|
* only when neither flag was passed. Shared by cmdAdd, cmdStart, and cmdStart's
|
|
225
|
-
* --all bulk-restart path.
|
|
226
|
-
|
|
284
|
+
* --all bulk-restart path.
|
|
285
|
+
*
|
|
286
|
+
* `applySetting` is **required**, and deliberately so. The setting is a *first-boot*
|
|
287
|
+
* default: carrying it into the start of a machine whose provisioning window has
|
|
288
|
+
* closed turns `quickchr start <name>` — and every such machine in `start --all` —
|
|
289
|
+
* into a request to provision a booted guest, which now refuses out loud (#176).
|
|
290
|
+
* Callers pass the target's own window state, not "does this machine exist": a
|
|
291
|
+
* machine that was `add`ed and never started is still on its first boot and keeps
|
|
292
|
+
* the setting. An explicit flag always wins over both.
|
|
293
|
+
*
|
|
294
|
+
* Making the argument required is the guard. An earlier pass left `cmdStart`'s
|
|
295
|
+
* single-target call on the defaulted form while believing it had been updated, so
|
|
296
|
+
* the restart regression stayed live behind a fix that had landed in `cmdAdd`
|
|
297
|
+
* instead. A missing argument is now a type error rather than a silent default. */
|
|
298
|
+
export async function resolveSecureLoginFlag(
|
|
299
|
+
flags: Record<string, string | boolean | string[]>,
|
|
300
|
+
applySetting: boolean,
|
|
301
|
+
): Promise<boolean | undefined> {
|
|
227
302
|
if (flags["secure-login"] === false) return false;
|
|
228
303
|
if (flagBool(flags, "secure-login")) return true;
|
|
304
|
+
if (!applySetting) return undefined;
|
|
229
305
|
const { resolveSetting } = await import("../lib/settings.ts");
|
|
230
306
|
return resolveSetting("secure-login").value === true ? true : undefined;
|
|
231
307
|
}
|
|
@@ -264,6 +340,9 @@ async function main() {
|
|
|
264
340
|
case "stop":
|
|
265
341
|
await cmdStop(args.slice(1));
|
|
266
342
|
break;
|
|
343
|
+
case "shutdown":
|
|
344
|
+
await cmdShutdown(args.slice(1));
|
|
345
|
+
break;
|
|
267
346
|
case "list":
|
|
268
347
|
case "ls":
|
|
269
348
|
case "status":
|
|
@@ -360,7 +439,9 @@ async function main() {
|
|
|
360
439
|
console.error(` Hint: ${err.installHint}`);
|
|
361
440
|
}
|
|
362
441
|
for (const line of tipsForError(err.code, command)) tip(line);
|
|
363
|
-
|
|
442
|
+
// The 7.x baseline explanation; a RouterOS 6 refusal already says why, and
|
|
443
|
+
// "for older 7.x" would point a 6.x user at the wrong fix.
|
|
444
|
+
if (err.code === "PROVISIONING_VERSION_UNSUPPORTED" && !err.message.includes(ROUTEROS6_BOOT_ONLY)) {
|
|
364
445
|
console.error(` Why this happened: ${provisioningSupportSummary(MIN_PROVISION_VERSION)}`);
|
|
365
446
|
console.error(` ${PROVISIONING_BOOT_ONLY_SUMMARY}`);
|
|
366
447
|
console.error(" Try this:");
|
|
@@ -375,6 +456,12 @@ async function main() {
|
|
|
375
456
|
|
|
376
457
|
// --- Commands ---
|
|
377
458
|
|
|
459
|
+
/** RouterOS 6 in `add`/`start --help` (#182): what it is for, and what it is not. */
|
|
460
|
+
const ROUTEROS6_HELP = `RouterOS 6.x is a best-effort boot-only target (x86 only): quickchr downloads,
|
|
461
|
+
boots and tracks it, and reports its ports and login. It is there for testing v6→v7
|
|
462
|
+
migration tooling, not as supported RouterOS. Readiness is a native-API login, every
|
|
463
|
+
provisioning flag is refused, and REST-based commands (exec, get, set) say so.`;
|
|
464
|
+
|
|
378
465
|
/** Print QEMU keyboard shortcuts before foreground launch. Screen clears after QEMU starts. */
|
|
379
466
|
function printForegroundTips(hasProvisioning = false) {
|
|
380
467
|
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
@@ -626,13 +713,15 @@ async function cmdAdd(argv: string[]) {
|
|
|
626
713
|
|
|
627
714
|
const opts: StartOptions = {
|
|
628
715
|
version: flag(flags, "version"),
|
|
629
|
-
|
|
716
|
+
// The default channel only stands in for a missing --version; applied alongside
|
|
717
|
+
// one, it reads to start()/add() as a caller passing both (#178).
|
|
718
|
+
channel: (flag(flags, "channel") as Channel | undefined) ?? (flag(flags, "version") ? undefined : resolveSetting("default-channel").value as Channel | undefined),
|
|
630
719
|
arch: (flag(flags, "arch") as Arch | "auto" | undefined) ?? (resolveSetting("default-arch").value as Arch | "auto" | undefined),
|
|
631
720
|
name: flag(flags, "name") ?? positional[0],
|
|
632
721
|
cpu: flag(flags, "cpu") ? Number(flag(flags, "cpu")) : undefined,
|
|
633
722
|
mem: flag(flags, "mem") ? Number(flag(flags, "mem")) : undefined,
|
|
634
723
|
packages: csvList(flagList(flags, "add-package")),
|
|
635
|
-
installAllPackages:
|
|
724
|
+
installAllPackages: flagOptionalBool(flags, "install-all-packages"),
|
|
636
725
|
portBase: flag(flags, "port-base") ? Number(flag(flags, "port-base")) : undefined,
|
|
637
726
|
excludePorts: [
|
|
638
727
|
...(flags.winbox === false ? ["winbox" as ServiceName] : []),
|
|
@@ -645,24 +734,18 @@ async function cmdAdd(argv: string[]) {
|
|
|
645
734
|
extraPorts: flagList(flags, "forward").flatMap(expandForwardSpec),
|
|
646
735
|
};
|
|
647
736
|
|
|
648
|
-
|
|
649
|
-
const deviceModeEnable = csvList(flagList(flags, "device-mode-enable"));
|
|
650
|
-
const deviceModeDisable = csvList(flagList(flags, "device-mode-disable"));
|
|
651
|
-
if (deviceModeValue !== undefined || deviceModeEnable.length > 0 || deviceModeDisable.length > 0) {
|
|
652
|
-
opts.deviceMode = {
|
|
653
|
-
mode: deviceModeValue ?? "auto",
|
|
654
|
-
enable: deviceModeEnable.length > 0 ? deviceModeEnable : undefined,
|
|
655
|
-
disable: deviceModeDisable.length > 0 ? deviceModeDisable : undefined,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
737
|
+
opts.deviceMode = deviceModeFromFlags(flags);
|
|
658
738
|
|
|
659
739
|
const userStr = flag(flags, "add-user");
|
|
660
740
|
if (userStr) {
|
|
661
741
|
const [uname = "", password] = userStr.split(":");
|
|
662
742
|
opts.user = { name: uname, password: password ?? "" };
|
|
663
743
|
}
|
|
664
|
-
opts.disableAdmin =
|
|
665
|
-
|
|
744
|
+
opts.disableAdmin = flagOptionalBool(flags, "disable-admin");
|
|
745
|
+
// `add` always creates a machine, so its provisioning window is open by definition.
|
|
746
|
+
// A RouterOS 6 machine takes no provisioning at all, so the setting — a default, not
|
|
747
|
+
// a request — stays out of it; only an explicit --secure-login is refused.
|
|
748
|
+
opts.secureLogin = await resolveSecureLoginFlag(flags, !isRouterOs6(opts.version ?? ""));
|
|
666
749
|
|
|
667
750
|
const state = await QuickCHR.add(opts);
|
|
668
751
|
|
|
@@ -673,10 +756,26 @@ async function cmdAdd(argv: string[]) {
|
|
|
673
756
|
console.log(` CPU/Mem: ${state.cpu} vCPU${state.cpu > 1 ? "s" : ""}, ${state.mem} MB`);
|
|
674
757
|
console.log(` Network: ${formatNetworks(state.networks ?? [])}`);
|
|
675
758
|
console.log(` Ports: ${formatPorts(state.ports)}`);
|
|
676
|
-
|
|
759
|
+
if (isRouterOs6(state.version)) {
|
|
760
|
+
console.log(` API: 127.0.0.1:${ports.api} ${dim("(after start; RouterOS 6 is boot-only and has no REST)")}`);
|
|
761
|
+
} else {
|
|
762
|
+
console.log(` REST: http://127.0.0.1:${ports.http} ${dim("(after start)")}`);
|
|
763
|
+
}
|
|
677
764
|
console.log(` Dir: ${dim(state.machineDir)}`);
|
|
678
|
-
if (state.
|
|
679
|
-
if (state.
|
|
765
|
+
if (state.installAllPackages) console.log(` Packages: all extra packages ${dim("(applied on first start)")}`);
|
|
766
|
+
else if (state.packages.length > 0) console.log(` Packages: ${state.packages.join(", ")} ${dim("(applied on first start)")}`);
|
|
767
|
+
const login = [
|
|
768
|
+
...(state.user ? [`add user "${state.user.name}"`] : []),
|
|
769
|
+
...(state.secureLogin === true ? ["managed quickchr account"] : []),
|
|
770
|
+
...(state.disableAdmin ? ["disable admin"] : []),
|
|
771
|
+
];
|
|
772
|
+
if (login.length > 0) console.log(` Login: ${login.join(", ")} ${dim("(applied on first start)")}`);
|
|
773
|
+
if (state.deviceMode) {
|
|
774
|
+
const { shouldApplyDeviceMode: willApplyDm, resolveDeviceModeOptions: resolveDm } = await import("../lib/device-mode.ts");
|
|
775
|
+
console.log(willApplyDm(resolveDm(state.deviceMode))
|
|
776
|
+
? ` Device-mode: ${state.deviceMode.mode ?? "auto"} ${dim("(applied on first start)")}`
|
|
777
|
+
: ` Device-mode: ${dim("skipped (--no-device-mode)")}`);
|
|
778
|
+
}
|
|
680
779
|
console.log();
|
|
681
780
|
console.log(`${dim("tip:")} quickchr start ${state.name}`);
|
|
682
781
|
}
|
|
@@ -1261,29 +1360,75 @@ async function cmdSetup() {
|
|
|
1261
1360
|
}
|
|
1262
1361
|
}
|
|
1263
1362
|
|
|
1363
|
+
/** Whether the `secure-login` *setting* reaches this start. The setting is a default,
|
|
1364
|
+
* not a request, so it stays out of every start that cannot provision: a closed
|
|
1365
|
+
* window, a `--no-wait` start (#159), and a RouterOS 6 machine (#182). Otherwise a
|
|
1366
|
+
* user with `secure-login=true` configured could never use either. An explicit
|
|
1367
|
+
* `--secure-login` is still passed through, and refused by name. */
|
|
1368
|
+
export function secureLoginSettingApplies(
|
|
1369
|
+
flags: Record<string, string | boolean | string[]>,
|
|
1370
|
+
windowOpen: boolean,
|
|
1371
|
+
version: string | undefined,
|
|
1372
|
+
): boolean {
|
|
1373
|
+
return windowOpen && flags.wait !== false && !isRouterOs6(version ?? "");
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** `--bg` never did what it reads as (#159): background is the default, and the flag
|
|
1377
|
+
* only ever chose where the serial console went. It stays accepted so existing
|
|
1378
|
+
* scripts keep working, and says once that it does nothing. */
|
|
1379
|
+
function warnDeprecatedBackgroundFlag(flags: Record<string, string | boolean | string[]>): void {
|
|
1380
|
+
const given = flags.bg === true ? "--bg" : flags.background === true ? "--background" : undefined;
|
|
1381
|
+
if (!given) return;
|
|
1382
|
+
console.error(`Warning: ${given} is deprecated and does nothing — background is already the default, and it never made start return early. Use --no-wait to return as soon as QEMU is running.`);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/** After a `--no-wait` start the caller owns readiness; name the command that does it. */
|
|
1386
|
+
function printReadinessTip(name: string, version: string): void {
|
|
1387
|
+
// 6.x: native API, and no validation — centrs checks paths against the 7.x tree
|
|
1388
|
+
// and rejects /system/resource on 6.49 (checked 2026-10-21).
|
|
1389
|
+
const via = isRouterOs6(version) ? " --via native-api --no-validate" : "";
|
|
1390
|
+
tip(`wait for readiness with: centrs retrieve --quickchr ${name} /system/resource${via} --wait 5m`);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1264
1393
|
async function cmdStart(argv: string[]) {
|
|
1265
1394
|
const { flags, positional } = parseFlags(argv);
|
|
1266
1395
|
applyTimeoutExtraShortFlag(argv, flags, positional);
|
|
1267
1396
|
rejectUnknownFlags(flags, START_FLAGS, "start");
|
|
1268
1397
|
// Before the --all branch: both start paths must see the same override.
|
|
1269
1398
|
await applyAccelFlag(flags);
|
|
1399
|
+
warnDeprecatedBackgroundFlag(flags);
|
|
1400
|
+
const wait = flags.wait === false ? false : undefined;
|
|
1270
1401
|
|
|
1271
1402
|
// --all: start every stopped machine in background
|
|
1272
1403
|
if (flagBool(flags, "all")) {
|
|
1404
|
+
// Batch starts only accept options this branch actually forwards. In particular,
|
|
1405
|
+
// a provisioning flag must never disappear on a --no-wait start.
|
|
1406
|
+
const batchFlags = new Set(["all", "wait", "bg", "background", "accel", "timeout-extra", "secure-login"]);
|
|
1407
|
+
const unsupported = Object.keys(flags).filter((key) => !batchFlags.has(key));
|
|
1408
|
+
if (unsupported.length > 0 || flags.bg === false || flags.background === false) {
|
|
1409
|
+
throw new QuickCHRError("INVALID_ARGUMENT", "start --all runs in background and accepts only --no-wait, --bg, --background, --accel, --timeout-extra, --secure-login and --no-secure-login. Start an individual machine to use other options.");
|
|
1410
|
+
}
|
|
1273
1411
|
const { QuickCHR } = await import("../lib/quickchr.ts");
|
|
1274
1412
|
const { statusIcon, link, bold, resolveDisplayCredentials, formatRestUrl, formatSshCommand } = await import("./format.ts");
|
|
1275
1413
|
const timeoutExtra = await resolveTimeoutExtraMs(flags);
|
|
1276
|
-
const secureLogin = await resolveSecureLoginFlag(flags);
|
|
1277
1414
|
const stopped = QuickCHR.list().filter((m) => m.status !== "running");
|
|
1278
1415
|
if (stopped.length === 0) {
|
|
1279
1416
|
console.log("No stopped instances.");
|
|
1280
1417
|
return;
|
|
1281
1418
|
}
|
|
1419
|
+
const { isProvisioningWindowOpen } = await import("../lib/provisioning-window.ts");
|
|
1282
1420
|
for (const m of stopped) {
|
|
1283
1421
|
console.log(`Starting ${bold(m.name)}...`);
|
|
1284
|
-
|
|
1422
|
+
// Per machine, not per invocation: a never-started machine in this batch is
|
|
1423
|
+
// still entitled to the secure-login setting on its first boot.
|
|
1424
|
+
const secureLogin = await resolveSecureLoginFlag(flags, secureLoginSettingApplies(flags, isProvisioningWindowOpen(m), m.version));
|
|
1425
|
+
const instance = await QuickCHR.start({ name: m.name, background: true, wait, timeoutExtra, secureLogin });
|
|
1285
1426
|
const creds = await resolveDisplayCredentials(instance.state);
|
|
1286
|
-
|
|
1427
|
+
const endpoint = isRouterOs6(instance.state.version)
|
|
1428
|
+
? `API: 127.0.0.1:${instance.ports.api}`
|
|
1429
|
+
: `REST: ${link(formatRestUrl(instance.ports.http, creds.user, creds.password))}`;
|
|
1430
|
+
console.log(`${statusIcon("running")} ${bold(instance.name)} ${endpoint} SSH: ${formatSshCommand(creds.user, instance.sshPort)}${wait === false ? " (not waited for)" : ""}`);
|
|
1431
|
+
if (wait === false) printReadinessTip(instance.name, instance.state.version);
|
|
1287
1432
|
}
|
|
1288
1433
|
return;
|
|
1289
1434
|
}
|
|
@@ -1310,13 +1455,15 @@ async function cmdStart(argv: string[]) {
|
|
|
1310
1455
|
|
|
1311
1456
|
const opts: StartOptions = {
|
|
1312
1457
|
version: flag(flags, "version"),
|
|
1313
|
-
|
|
1458
|
+
// The default channel only stands in for a missing --version; applied alongside
|
|
1459
|
+
// one, it reads to start()/add() as a caller passing both (#178).
|
|
1460
|
+
channel: (flag(flags, "channel") as Channel | undefined) ?? (flag(flags, "version") ? undefined : resolveSetting("default-channel").value as Channel | undefined),
|
|
1314
1461
|
arch: (flag(flags, "arch") as Arch | "auto" | undefined) ?? (resolveSetting("default-arch").value as Arch | "auto" | undefined),
|
|
1315
1462
|
name: flag(flags, "name") ?? positional[0],
|
|
1316
1463
|
cpu: flag(flags, "cpu") ? Number(flag(flags, "cpu")) : undefined,
|
|
1317
1464
|
mem: flag(flags, "mem") ? Number(flag(flags, "mem")) : undefined,
|
|
1318
|
-
packages: flagList(flags, "add-package"),
|
|
1319
|
-
installAllPackages:
|
|
1465
|
+
packages: csvList(flagList(flags, "add-package")),
|
|
1466
|
+
installAllPackages: flagOptionalBool(flags, "install-all-packages"),
|
|
1320
1467
|
portBase: flag(flags, "port-base") ? Number(flag(flags, "port-base")) : undefined,
|
|
1321
1468
|
excludePorts: [
|
|
1322
1469
|
...(flags.winbox === false ? ["winbox" as ServiceName] : []),
|
|
@@ -1332,21 +1479,7 @@ async function cmdStart(argv: string[]) {
|
|
|
1332
1479
|
timeoutExtra: await resolveTimeoutExtraMs(flags),
|
|
1333
1480
|
};
|
|
1334
1481
|
|
|
1335
|
-
|
|
1336
|
-
const deviceModeEnable = csvList(flagList(flags, "device-mode-enable"));
|
|
1337
|
-
const deviceModeDisable = csvList(flagList(flags, "device-mode-disable"));
|
|
1338
|
-
const noDeviceMode = flags["device-mode"] === false;
|
|
1339
|
-
if (noDeviceMode) {
|
|
1340
|
-
if (deviceModeEnable.length > 0 || deviceModeDisable.length > 0) {
|
|
1341
|
-
console.warn("Warning: --device-mode-enable/--device-mode-disable ignored because --no-device-mode was set.");
|
|
1342
|
-
}
|
|
1343
|
-
} else if (deviceModeValue !== undefined || deviceModeEnable.length > 0 || deviceModeDisable.length > 0) {
|
|
1344
|
-
opts.deviceMode = {
|
|
1345
|
-
mode: deviceModeValue ?? "auto",
|
|
1346
|
-
enable: deviceModeEnable.length > 0 ? deviceModeEnable : undefined,
|
|
1347
|
-
disable: deviceModeDisable.length > 0 ? deviceModeDisable : undefined,
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1482
|
+
opts.deviceMode = deviceModeFromFlags(flags);
|
|
1350
1483
|
|
|
1351
1484
|
// --license-* flags (credentials from env if not supplied)
|
|
1352
1485
|
const licenseLevel = flag(flags, "license-level");
|
|
@@ -1371,10 +1504,25 @@ async function cmdStart(argv: string[]) {
|
|
|
1371
1504
|
opts.user = { name, password: password ?? "" };
|
|
1372
1505
|
}
|
|
1373
1506
|
|
|
1374
|
-
opts.disableAdmin =
|
|
1375
|
-
|
|
1507
|
+
opts.disableAdmin = flagOptionalBool(flags, "disable-admin");
|
|
1508
|
+
{
|
|
1509
|
+
const { QuickCHR } = await import("../lib/quickchr.ts");
|
|
1510
|
+
const { isProvisioningWindowOpen } = await import("../lib/provisioning-window.ts");
|
|
1511
|
+
const target = opts.name ? QuickCHR.get(opts.name) : null;
|
|
1512
|
+
// No machine yet, or one that was added and never booted: still a first boot,
|
|
1513
|
+
// so the setting applies. Only a closed window suppresses it — otherwise a
|
|
1514
|
+
// plain `quickchr start <booted-name>` would ask to provision a booted guest
|
|
1515
|
+
// and be refused.
|
|
1516
|
+
opts.secureLogin = await resolveSecureLoginFlag(flags, secureLoginSettingApplies(
|
|
1517
|
+
flags,
|
|
1518
|
+
!target || isProvisioningWindowOpen(target.state),
|
|
1519
|
+
target?.state.version ?? opts.version,
|
|
1520
|
+
));
|
|
1521
|
+
}
|
|
1522
|
+
opts.wait = wait;
|
|
1376
1523
|
|
|
1377
1524
|
// Background default: true. Explicitly foreground only with --fg / --foreground / --no-background / --no-bg.
|
|
1525
|
+
// `--bg` / `--background` are accepted and change nothing (see warnDeprecatedBackgroundFlag).
|
|
1378
1526
|
const wantFg =
|
|
1379
1527
|
flagBool(flags, "fg") ||
|
|
1380
1528
|
flagBool(flags, "foreground") ||
|
|
@@ -1387,8 +1535,9 @@ async function cmdStart(argv: string[]) {
|
|
|
1387
1535
|
return;
|
|
1388
1536
|
}
|
|
1389
1537
|
|
|
1390
|
-
// --fg on already-running machine: attach to its serial console
|
|
1391
|
-
|
|
1538
|
+
// --fg on already-running machine: attach to its serial console.
|
|
1539
|
+
// `--fg --no-wait` skips both foreground blocks so start() refuses the pair before any I/O.
|
|
1540
|
+
if (!opts.background && opts.wait !== false && opts.name) {
|
|
1392
1541
|
const runningMachine = QuickCHR.list().find((m) => m.name === opts.name && m.status === "running");
|
|
1393
1542
|
if (runningMachine) {
|
|
1394
1543
|
await attachSerial(runningMachine.name, runningMachine.machineDir);
|
|
@@ -1396,7 +1545,7 @@ async function cmdStart(argv: string[]) {
|
|
|
1396
1545
|
}
|
|
1397
1546
|
}
|
|
1398
1547
|
|
|
1399
|
-
if (!opts.background) {
|
|
1548
|
+
if (!opts.background && opts.wait !== false) {
|
|
1400
1549
|
const wantsDeviceMode = !!opts.deviceMode && !["skip", "none", "off", "disabled"].includes((opts.deviceMode.mode ?? "auto").toLowerCase());
|
|
1401
1550
|
const hasProv = !!(
|
|
1402
1551
|
opts.installAllPackages ||
|
|
@@ -1423,13 +1572,19 @@ async function cmdStart(argv: string[]) {
|
|
|
1423
1572
|
console.log(` ${dim("Tip: run background")} quickchr start ${instance.name}`);
|
|
1424
1573
|
} else {
|
|
1425
1574
|
const creds = await resolveDisplayCredentials(instance.state);
|
|
1426
|
-
|
|
1427
|
-
console.log(
|
|
1575
|
+
const v6 = isRouterOs6(instance.state.version);
|
|
1576
|
+
console.log(`${statusIcon("running")} ${bold(instance.name)} ${opts.wait === false ? "launched — not waiting for RouterOS (--no-wait)" : "started"}`);
|
|
1577
|
+
console.log(` Version: ${instance.state.version} (${instance.state.arch})${v6 ? ` ${dim("boot-only: no REST, no provisioning")}` : ""}`);
|
|
1428
1578
|
console.log(` Network: ${formatNetworks(instance.state.networks ?? [])}`);
|
|
1429
1579
|
console.log(` Ports: ${formatPorts(instance.state.ports)}`);
|
|
1430
|
-
|
|
1580
|
+
if (v6) {
|
|
1581
|
+
console.log(` API: 127.0.0.1:${instance.ports.api} ${dim(`(native API, user ${creds.user})`)}`);
|
|
1582
|
+
} else {
|
|
1583
|
+
console.log(` REST: ${link(formatRestUrl(instance.ports.http, creds.user, creds.password))}`);
|
|
1584
|
+
}
|
|
1431
1585
|
console.log(` SSH: ${formatSshCommand(creds.user, instance.sshPort)}`);
|
|
1432
1586
|
console.log(` WinBox: 127.0.0.1:${instance.ports.winbox}`);
|
|
1587
|
+
if (opts.wait === false) printReadinessTip(instance.name, instance.state.version);
|
|
1433
1588
|
if (creds.user !== "admin") {
|
|
1434
1589
|
console.log(` Login: ${creds.user} / ${creds.password || "(no password)"}`);
|
|
1435
1590
|
console.log(` Run: quickchr exec ${instance.name} /system/resource/print`);
|
|
@@ -1437,6 +1592,32 @@ async function cmdStart(argv: string[]) {
|
|
|
1437
1592
|
}
|
|
1438
1593
|
}
|
|
1439
1594
|
|
|
1595
|
+
async function cmdShutdown(argv: string[]) {
|
|
1596
|
+
const { flags, positional } = parseFlags(argv);
|
|
1597
|
+
const invalid = unknownFlags(flags, ["timeout"]);
|
|
1598
|
+
if (invalid.length) throw new QuickCHRError("INVALID_ARGUMENT", "shutdown accepts only <name> and --timeout <seconds>");
|
|
1599
|
+
if (positional.length !== 1 || positional[0] === undefined) throw new QuickCHRError("INVALID_ARGUMENT", "Usage: quickchr shutdown <name> [--timeout <seconds>]");
|
|
1600
|
+
const raw = flags.timeout === undefined ? (process.env.QUICKCHR_SHUTDOWN_TIMEOUT ?? "10") : flag(flags, "timeout");
|
|
1601
|
+
if (raw === undefined || !/^[0-9]+$/.test(raw) || Number(raw) <= 0) {
|
|
1602
|
+
throw new QuickCHRError("INVALID_ARGUMENT", "shutdown timeout must be a positive integer number of seconds");
|
|
1603
|
+
}
|
|
1604
|
+
const { validateShutdownTimeout } = await import("../lib/shutdown.ts");
|
|
1605
|
+
const timeoutMs = validateShutdownTimeout(Number(raw) * 1000);
|
|
1606
|
+
const { QuickCHR } = await import("../lib/quickchr.ts");
|
|
1607
|
+
const name = positional[0];
|
|
1608
|
+
const instance = QuickCHR.get(name);
|
|
1609
|
+
if (!instance) throw new QuickCHRError("MACHINE_NOT_FOUND", `Machine "${name}" not found`);
|
|
1610
|
+
const result = await instance.shutdown({ timeoutMs });
|
|
1611
|
+
if (result.outcome === "forced") {
|
|
1612
|
+
console.log(`${name}: forced power cut — guest shutdown not confirmed`);
|
|
1613
|
+
if (result.requestError) console.error(`Shutdown request: ${result.requestError}`);
|
|
1614
|
+
} else if (result.outcome === "already-stopped") {
|
|
1615
|
+
console.log(`${name}: already stopped`);
|
|
1616
|
+
} else {
|
|
1617
|
+
console.log(`${name}: guest exited after shutdown request`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1440
1621
|
async function cmdStop(argv: string[]) {
|
|
1441
1622
|
const { flags, positional } = parseFlags(argv);
|
|
1442
1623
|
const { QuickCHR } = await import("../lib/quickchr.ts");
|
|
@@ -1700,6 +1881,11 @@ async function cmdGet(argv: string[]) {
|
|
|
1700
1881
|
return;
|
|
1701
1882
|
}
|
|
1702
1883
|
|
|
1884
|
+
// Every live group is a REST read. Refused here rather than left to fetchGroup(),
|
|
1885
|
+
// which swallows errors and would print an empty report for a v6 machine (#182).
|
|
1886
|
+
const { assertRestAvailable } = await import("../lib/versions.ts");
|
|
1887
|
+
assertRestAvailable(s, "'quickchr get' (live properties)");
|
|
1888
|
+
|
|
1703
1889
|
const { resolveAuth } = await import("../lib/auth.ts");
|
|
1704
1890
|
const auth = resolveAuth(s);
|
|
1705
1891
|
const base = `http://127.0.0.1:${instance.ports.http}/rest`;
|
|
@@ -1747,14 +1933,20 @@ async function cmdGet(argv: string[]) {
|
|
|
1747
1933
|
}
|
|
1748
1934
|
|
|
1749
1935
|
if (results["device-mode"]) {
|
|
1936
|
+
const { DEVICE_MODE_NON_FEATURE_KEYS, isDeviceModeFeatureEnabled } = await import("../lib/device-mode.ts");
|
|
1750
1937
|
const dm = results["device-mode"] as Record<string, unknown>;
|
|
1751
1938
|
const mode = dm.mode ?? "(not set)";
|
|
1752
1939
|
console.log(`\n ${bold("Device Mode")}`);
|
|
1753
1940
|
console.log(` Mode: ${mode}`);
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1941
|
+
// RouterOS reports these as strings ("true"/"false"), so the old `v === true`
|
|
1942
|
+
// test matched nothing and the feature lines never printed — `get` could not
|
|
1943
|
+
// read back what `set` had just written. Both lines are shown: after
|
|
1944
|
+
// `--device-mode-disable smb` the interesting answer is the one that is off.
|
|
1945
|
+
const features = Object.entries(dm).filter(([k]) => !DEVICE_MODE_NON_FEATURE_KEYS.has(k));
|
|
1946
|
+
const enabled = features.filter(([, v]) => isDeviceModeFeatureEnabled(v)).map(([k]) => k);
|
|
1947
|
+
const disabled = features.filter(([, v]) => !isDeviceModeFeatureEnabled(v)).map(([k]) => k);
|
|
1948
|
+
if (enabled.length > 0) console.log(` Enabled: ${enabled.join(", ")}`);
|
|
1949
|
+
if (disabled.length > 0) console.log(` Disabled: ${disabled.join(", ")}`);
|
|
1758
1950
|
}
|
|
1759
1951
|
|
|
1760
1952
|
if (results.admin) {
|
|
@@ -1883,6 +2075,10 @@ async function applyLicense(argv: string[]) {
|
|
|
1883
2075
|
console.error(`Machine "${name}" is not running.`);
|
|
1884
2076
|
process.exit(1);
|
|
1885
2077
|
}
|
|
2078
|
+
// Before any credential prompt: instance.license() refuses RouterOS 6 too, but only
|
|
2079
|
+
// after the user has typed a MikroTik password for nothing.
|
|
2080
|
+
const { assertProvisioningSupportedVersion } = await import("../lib/versions.ts");
|
|
2081
|
+
assertProvisioningSupportedVersion(instance.state.version, "apply a license");
|
|
1886
2082
|
|
|
1887
2083
|
let account = flag(flags, "account") ?? process.env.MIKROTIK_WEB_ACCOUNT;
|
|
1888
2084
|
let password = flag(flags, "password") ?? process.env.MIKROTIK_WEB_PASSWORD;
|
|
@@ -1928,28 +2124,100 @@ async function applyLicense(argv: string[]) {
|
|
|
1928
2124
|
console.log(`License applied: ${before} → ${level}`);
|
|
1929
2125
|
}
|
|
1930
2126
|
|
|
2127
|
+
/** `set` applies a change to a machine that has already booted.
|
|
2128
|
+
*
|
|
2129
|
+
* It is the post-boot half of the provisioning flags, and deliberately narrow: only
|
|
2130
|
+
* the steps that are safe to run against a guest whose configuration quickchr no
|
|
2131
|
+
* longer knows get a home here (#176). `--license` is account-level. `--device-mode`
|
|
2132
|
+
* is a capability flag — it gates whether a feature *can* run and describes nothing
|
|
2133
|
+
* about how the guest is configured, so applying it later cannot clobber working
|
|
2134
|
+
* config; the config it was blocking could not have been applied while it was off.
|
|
2135
|
+
*
|
|
2136
|
+
* `--add-user`, `--disable-admin` and `--secure-login` are excluded on purpose. They
|
|
2137
|
+
* are working config rather than a capability, they collide with whatever has been
|
|
2138
|
+
* created since, and they are the steps that drift. Their route stays
|
|
2139
|
+
* `quickchr clean <name>` and a fresh provisioning run. */
|
|
1931
2140
|
async function cmdSet(argv: string[]) {
|
|
1932
2141
|
const { flags, positional } = parseFlags(argv);
|
|
1933
|
-
|
|
2142
|
+
// Positional first, `--name` as the fallback — the same order `applyLicense` uses,
|
|
2143
|
+
// which `set --license` delegates to. Without this `quickchr set --name lab
|
|
2144
|
+
// --license` printed a usage error while `quickchr license --name lab` worked.
|
|
2145
|
+
const name = positional[0] ?? flag(flags, "name");
|
|
1934
2146
|
|
|
2147
|
+
const usage = "Usage: quickchr set <name> [--license [--level=p1|p10|unlimited]] [--device-mode <mode>] [--device-mode-enable <features>] [--device-mode-disable <features>]";
|
|
1935
2148
|
if (!name) {
|
|
1936
|
-
console.error(
|
|
2149
|
+
console.error(usage);
|
|
2150
|
+
process.exit(1);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const valueless = valuelessFlags(flags, SET_FLAGS);
|
|
2154
|
+
if (valueless.length > 0) {
|
|
2155
|
+
console.error(valuelessFlagMessage(valueless, "set"));
|
|
2156
|
+
process.exit(1);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
if (flags["device-mode"] === false) {
|
|
2160
|
+
// `--no-device-mode` means "skip device-mode provisioning" on add/start. There is
|
|
2161
|
+
// nothing to skip in an imperative set, so honouring it would be accepting a flag
|
|
2162
|
+
// and doing nothing — the failure this whole area exists to stop.
|
|
2163
|
+
console.error("--no-device-mode is a provisioning flag for 'add'/'start' (skip the step); 'set' applies a change, so pass a mode or a feature list instead.");
|
|
1937
2164
|
process.exit(1);
|
|
1938
2165
|
}
|
|
1939
2166
|
|
|
1940
2167
|
const wantsLicense = flagBool(flags, "license");
|
|
2168
|
+
const deviceMode = deviceModeFromFlags(flags);
|
|
1941
2169
|
|
|
1942
|
-
if (!wantsLicense) {
|
|
1943
|
-
console.error("Nothing to set. Available flags: --license");
|
|
2170
|
+
if (!wantsLicense && !deviceMode) {
|
|
2171
|
+
console.error("Nothing to set. Available flags: --license, --device-mode, --device-mode-enable, --device-mode-disable");
|
|
1944
2172
|
process.exit(1);
|
|
1945
2173
|
}
|
|
1946
2174
|
|
|
2175
|
+
// Device-mode first: it power-cycles the guest, and a license applied beforehand
|
|
2176
|
+
// would be read back across a reboot it did not expect.
|
|
2177
|
+
if (deviceMode) await applyDeviceModeChange(name, deviceMode);
|
|
1947
2178
|
if (wantsLicense) {
|
|
1948
2179
|
const subcmdArgs = [name, ...Object.entries(flags).flatMap(([k, v]) => v === true ? [`--${k}`] : [`--${k}=${v}`]), ...positional.slice(1)];
|
|
1949
2180
|
await applyLicense(subcmdArgs);
|
|
1950
2181
|
}
|
|
1951
2182
|
}
|
|
1952
2183
|
|
|
2184
|
+
/** `set <name> --device-mode…` — the post-boot route the refusal names.
|
|
2185
|
+
*
|
|
2186
|
+
* Device-mode needs a **power cycle**: RouterOS will not apply it otherwise, and
|
|
2187
|
+
* `/system/device-mode/update` issued from inside the guest simply never returns,
|
|
2188
|
+
* because it is waiting for a power cycle it cannot perform on itself. quickchr owns
|
|
2189
|
+
* the power button, which is why this lives here and not in a tool that talks to a
|
|
2190
|
+
* router over the network. */
|
|
2191
|
+
async function applyDeviceModeChange(name: string, deviceMode: DeviceModeOptions) {
|
|
2192
|
+
const { QuickCHR } = await import("../lib/quickchr.ts");
|
|
2193
|
+
const { bold, dim, machineNotFoundMessage } = await import("./format.ts");
|
|
2194
|
+
const { formatDeviceModeSelection, resolveDeviceModeOptions } = await import("../lib/device-mode.ts");
|
|
2195
|
+
const { shellQuote } = await import("../lib/names.ts");
|
|
2196
|
+
|
|
2197
|
+
const instance = QuickCHR.get(name);
|
|
2198
|
+
if (!instance) {
|
|
2199
|
+
console.error(machineNotFoundMessage(name));
|
|
2200
|
+
process.exit(1);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// Before announcing a change that cannot happen (setDeviceMode() refuses it as well).
|
|
2204
|
+
const { assertProvisioningSupportedVersion } = await import("../lib/versions.ts");
|
|
2205
|
+
assertProvisioningSupportedVersion(instance.state.version, "set device-mode");
|
|
2206
|
+
|
|
2207
|
+
const selection = formatDeviceModeSelection(resolveDeviceModeOptions(deviceMode));
|
|
2208
|
+
console.log(`Setting device-mode on ${bold(name)}: ${selection}`);
|
|
2209
|
+
// Hedged on purpose: a request the guest already satisfies costs nothing, and
|
|
2210
|
+
// setDeviceMode() skips the power cycle in that case. Promising a restart before
|
|
2211
|
+
// reading the guest would make the quiet no-op look like a failure to restart.
|
|
2212
|
+
console.log(dim(" RouterOS only applies device-mode across a power cycle, so any setting that moves restarts the machine."));
|
|
2213
|
+
// setDeviceMode() checks the preconditions (running, user-mode NIC), prints the
|
|
2214
|
+
// settings that will actually move, and skips the power cycle when none would.
|
|
2215
|
+
await instance.setDeviceMode(deviceMode);
|
|
2216
|
+
// Quoted for the same reason the refusal routes are: a legacy machine name can
|
|
2217
|
+
// contain a space, and an unquoted read-back hint would name a different machine.
|
|
2218
|
+
console.log(`Device-mode set. Read it back with: quickchr get ${shellQuote(name)} device-mode`);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
1953
2221
|
async function cmdLicense(argv: string[]) {
|
|
1954
2222
|
await applyLicense(argv);
|
|
1955
2223
|
}
|
|
@@ -2897,7 +3165,8 @@ Commands:
|
|
|
2897
3165
|
setup Interactive setup wizard (TTY only)
|
|
2898
3166
|
add [options] Create a new CHR machine (without starting)
|
|
2899
3167
|
start [<name>|options] Start or restart a CHR instance
|
|
2900
|
-
stop [<name>|--all]
|
|
3168
|
+
stop [<name>|--all] Power cut — print list if no name
|
|
3169
|
+
shutdown <name> Request guest shutdown, then bounded hard-stop fallback
|
|
2901
3170
|
list [<name>] [--json] List all instances, or detail for one
|
|
2902
3171
|
status Alias for list
|
|
2903
3172
|
inspect <name> [--json] Print running machine descriptor as JSON
|
|
@@ -2907,8 +3176,8 @@ Commands:
|
|
|
2907
3176
|
(raw /rest/execute — see 'quickchr exec --help' for centrs)
|
|
2908
3177
|
remove [<name>|--all] Remove instance(s) and disk
|
|
2909
3178
|
clean [<name>|--all] Reset instance disk to fresh image
|
|
2910
|
-
get <name> [group]
|
|
2911
|
-
set <name>
|
|
3179
|
+
get <name> [group] Show machine config (license, device-mode, admin)
|
|
3180
|
+
set <name> [flags] Change a booted machine (license, device-mode)
|
|
2912
3181
|
license <name> Deprecated — use 'set <name> --license'
|
|
2913
3182
|
disk <name> Show disk details for an instance
|
|
2914
3183
|
snapshot <name> [cmd] Manage snapshots (list/save/load/delete)
|
|
@@ -2946,6 +3215,8 @@ Post-boot provisioning flags in this command (--add-package, --install-all-packa
|
|
|
2946
3215
|
RouterOS ${MIN_PROVISION_VERSION}+ only. Older 7.x remains boot-only. Disk and network
|
|
2947
3216
|
options still work on older versions when the required host tools are installed.
|
|
2948
3217
|
|
|
3218
|
+
${ROUTEROS6_HELP}
|
|
3219
|
+
|
|
2949
3220
|
Options:
|
|
2950
3221
|
--name <name> Instance name (required)
|
|
2951
3222
|
--version <ver> RouterOS version (e.g., 7.22.1)
|
|
@@ -2956,9 +3227,9 @@ Options:
|
|
|
2956
3227
|
--accel <mode> Force QEMU accelerator: auto|tcg|hvf|kvm (default: auto).
|
|
2957
3228
|
Anything but 'auto' bypasses detection, including the
|
|
2958
3229
|
arm64-on-Apple-Silicon TCG fallback (tikoci/quickchr#97).
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3230
|
+
--boot-disk-format <f> Boot disk format: qcow2|raw (default: qcow2)
|
|
3231
|
+
--boot-size <size> Resize boot disk (e.g., 512M, 2G) — qcow2 only, requires qemu-img
|
|
3232
|
+
--add-disk <size> Add an extra blank qcow2 disk (repeatable, requires qemu-img)
|
|
2962
3233
|
--add-package <pkg> Extra package to install on first boot (repeatable)
|
|
2963
3234
|
--forward <spec> Add an extra hostfwd port (repeatable). Spec:
|
|
2964
3235
|
name[:host[:guest]][/proto], or a port range
|
|
@@ -2976,9 +3247,18 @@ Options:
|
|
|
2976
3247
|
--no-winbox Exclude WinBox port mapping
|
|
2977
3248
|
--no-api-ssl Exclude API-SSL port mapping
|
|
2978
3249
|
--device-mode <m> Set device-mode on first boot: rose|advanced|basic|home|auto|skip
|
|
3250
|
+
--device-mode-enable <f> Set one or more device-mode flags to yes (e.g. container)
|
|
3251
|
+
--device-mode-disable <f> Set one or more device-mode flags to no
|
|
3252
|
+
--no-device-mode Skip device-mode provisioning entirely
|
|
2979
3253
|
--add-network <spec> Add a network NIC (repeatable). Specs: user, shared, bridged:<if>,
|
|
2980
3254
|
socket::<name>, tap:<if>. Default: single user NIC.
|
|
2981
|
-
--no-network Start with no NICs (headless)
|
|
3255
|
+
--no-network Start with no NICs (headless)
|
|
3256
|
+
|
|
3257
|
+
Provisioning flags apply on the machine's first boot — whether they are given
|
|
3258
|
+
here or on that first 'start'. After the guest has booted, a flag the machine already
|
|
3259
|
+
has is accepted as a no-op; any other is refused rather than ignored, naming where the
|
|
3260
|
+
change can still be made (often 'quickchr set <name>'). 'quickchr clean <name>' also
|
|
3261
|
+
reopens the window, but resets the disk to factory and erases the guest's configuration.`);
|
|
2982
3262
|
break;
|
|
2983
3263
|
case "setup":
|
|
2984
3264
|
console.log(`quickchr setup
|
|
@@ -3028,10 +3308,23 @@ validated on RouterOS ${MIN_PROVISION_VERSION}+ only. Older 7.x remains boot-onl
|
|
|
3028
3308
|
Disk and network options still work on older versions when the required host tools
|
|
3029
3309
|
are installed.
|
|
3030
3310
|
|
|
3311
|
+
${ROUTEROS6_HELP}
|
|
3312
|
+
|
|
3313
|
+
By default 'start' returns once RouterOS is ready (REST answers; on 6.x, a native-API
|
|
3314
|
+
login). --no-wait returns as soon as QEMU is running and leaves readiness to you:
|
|
3315
|
+
centrs retrieve --quickchr <name> /system/resource --wait 5m
|
|
3316
|
+
QEMU runs detached either way. --no-wait is refused with --fg, and with provisioning
|
|
3317
|
+
flags, including ones 'add' recorded for the first boot — provisioning needs a booted
|
|
3318
|
+
guest.
|
|
3319
|
+
|
|
3031
3320
|
Options:
|
|
3032
|
-
--all Start all stopped machines
|
|
3033
|
-
|
|
3321
|
+
--all Start all stopped machines, in background. Accepts only
|
|
3322
|
+
--no-wait, --accel, --timeout-extra and --[no-]secure-login
|
|
3323
|
+
(plus the deprecated no-op --bg / --background); any other
|
|
3324
|
+
option is refused rather than ignored
|
|
3325
|
+
--no-wait Return once QEMU is running; do not wait for RouterOS (not with --fg)
|
|
3034
3326
|
--fg / --foreground Run in foreground — serial console on stdio
|
|
3327
|
+
--bg / --background Deprecated, does nothing: background is already the default
|
|
3035
3328
|
--version <ver> RouterOS version (e.g., 7.22.1)
|
|
3036
3329
|
--channel <ch> Channel: stable, long-term, testing, development
|
|
3037
3330
|
--arch <arch> Architecture: arm64, x86, auto (default: host native)
|
|
@@ -3041,9 +3334,9 @@ Options:
|
|
|
3041
3334
|
--accel <mode> Force QEMU accelerator: auto|tcg|hvf|kvm (default: auto).
|
|
3042
3335
|
Anything but 'auto' bypasses detection, including the
|
|
3043
3336
|
arm64-on-Apple-Silicon TCG fallback (tikoci/quickchr#97).
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3337
|
+
--boot-disk-format <f> Boot disk format: qcow2|raw (default: qcow2)
|
|
3338
|
+
--boot-size <size> Resize boot disk (e.g., 512M, 2G) — qcow2 only, requires qemu-img
|
|
3339
|
+
--add-disk <size> Add an extra blank qcow2 disk (repeatable, requires qemu-img)
|
|
3047
3340
|
--add-package <pkg> Extra package to install (repeatable)
|
|
3048
3341
|
--forward <spec> Add an extra hostfwd port (repeatable). Spec:
|
|
3049
3342
|
name[:host[:guest]][/proto], or a range
|
|
@@ -3068,15 +3361,35 @@ Options:
|
|
|
3068
3361
|
--device-mode-disable <f> Set one or more device-mode flags to no
|
|
3069
3362
|
--no-device-mode Skip device-mode provisioning entirely
|
|
3070
3363
|
--timeout-extra <s>, -T <s> Add extra seconds to the auto-computed boot timeout
|
|
3071
|
-
--dry-run Print what would run without starting
|
|
3364
|
+
--dry-run Print what would run without starting
|
|
3365
|
+
|
|
3366
|
+
Provisioning flags (packages, login, device-mode, license) apply only on a machine's
|
|
3367
|
+
first boot. On a machine that has booted, a flag the machine already has (same
|
|
3368
|
+
packages, user, license, device-mode) is accepted as a no-op. Any other is refused,
|
|
3369
|
+
naming where the change can still be made — often 'quickchr set <name>'. Only reach for
|
|
3370
|
+
'quickchr clean <name>' if you mean it: it resets the disk to factory, erasing the
|
|
3371
|
+
guest's configuration, and reopens the first-boot window.`);
|
|
3072
3372
|
break;
|
|
3073
3373
|
case "stop":
|
|
3074
3374
|
console.log(`quickchr stop [<name>] [--all]
|
|
3075
3375
|
|
|
3376
|
+
Power cut: QEMU SIGTERM, then SIGKILL if needed. Pending guest changes may be lost.
|
|
3377
|
+
Use shutdown to request /system/shutdown first.
|
|
3378
|
+
|
|
3076
3379
|
<name> Stop a specific running instance.
|
|
3077
3380
|
Omit to see a list of running instances.
|
|
3078
3381
|
--all Stop all running instances.`);
|
|
3079
3382
|
break;
|
|
3383
|
+
case "shutdown":
|
|
3384
|
+
console.log(`quickchr shutdown <name> [--timeout <seconds>]
|
|
3385
|
+
|
|
3386
|
+
Request /system/shutdown over REST, then wait for QEMU to exit.
|
|
3387
|
+
If it is still running at the deadline, force a power cut and report it.
|
|
3388
|
+
--timeout <seconds> Positive integer seconds (default 10).
|
|
3389
|
+
Overrides QUICKCHR_SHUTDOWN_TIMEOUT.
|
|
3390
|
+
Lock wait and hard-stop escalation are separate bounded waits.
|
|
3391
|
+
Preserves disks and credentials. Use stop for immediate teardown.`);
|
|
3392
|
+
break;
|
|
3080
3393
|
case "status":
|
|
3081
3394
|
console.log(`quickchr status [<name>] [--json]
|
|
3082
3395
|
|
|
@@ -3195,14 +3508,30 @@ When no group is given, all properties are shown.
|
|
|
3195
3508
|
The machine must be running to query live properties.`);
|
|
3196
3509
|
break;
|
|
3197
3510
|
case "set":
|
|
3198
|
-
console.log(`quickchr set <name> --license [options]
|
|
3511
|
+
console.log(`quickchr set <name> [--license | --device-mode...] [options]
|
|
3199
3512
|
|
|
3200
|
-
|
|
3513
|
+
Change a property on a machine that has already booted. This is the post-boot
|
|
3514
|
+
half of the provisioning flags: 'add'/'start' apply them at first boot, 'set'
|
|
3515
|
+
applies them now.
|
|
3201
3516
|
|
|
3202
3517
|
<name> Name of a running CHR instance (required).
|
|
3203
3518
|
|
|
3204
3519
|
Flags:
|
|
3205
3520
|
--license Apply or renew a CHR trial license.
|
|
3521
|
+
--device-mode <m> Set device-mode: rose|advanced|basic|home|auto
|
|
3522
|
+
--device-mode-enable <f> Set one or more device-mode flags to yes (e.g. container)
|
|
3523
|
+
--device-mode-disable <f> Set one or more device-mode flags to no
|
|
3524
|
+
|
|
3525
|
+
Device-mode notes:
|
|
3526
|
+
RouterOS only applies device-mode across a power cycle, so 'set' restarts the
|
|
3527
|
+
machine (roughly double a plain start). Enabling a feature without naming a
|
|
3528
|
+
mode resolves the mode to 'rose'; quickchr prints every setting that moves
|
|
3529
|
+
before it restarts. The machine must be running and have a user-mode NIC.
|
|
3530
|
+
Read it back with 'quickchr get <name> device-mode'.
|
|
3531
|
+
|
|
3532
|
+
Not available here: --add-user, --disable-admin and --secure-login. Those rewrite
|
|
3533
|
+
working config rather than unlock a capability, so their route stays
|
|
3534
|
+
'quickchr clean <name>' followed by the original add/start command.
|
|
3206
3535
|
|
|
3207
3536
|
License options (used with --license):
|
|
3208
3537
|
--level <level> License level: p1 (1 Gbps), p10 (10 Gbps), unlimited (default: p1)
|
|
@@ -3213,7 +3542,7 @@ Credential resolution order:
|
|
|
3213
3542
|
1. --account / --password flags
|
|
3214
3543
|
2. MIKROTIK_WEB_ACCOUNT / MIKROTIK_WEB_PASSWORD environment variables
|
|
3215
3544
|
3. OS native secret store (macOS Keychain / Linux Keyring)
|
|
3216
|
-
4. ~/.config/quickchr/
|
|
3545
|
+
4. ~/.config/quickchr/com.quickchr.mikrotik-web.json (fallback)`);
|
|
3217
3546
|
break;
|
|
3218
3547
|
case "license":
|
|
3219
3548
|
console.log(`quickchr license <name> [options]
|
|
@@ -3235,7 +3564,7 @@ Credential resolution order (highest priority first):
|
|
|
3235
3564
|
1. --account / --password flags
|
|
3236
3565
|
2. MIKROTIK_WEB_ACCOUNT / MIKROTIK_WEB_PASSWORD environment variables
|
|
3237
3566
|
3. OS native secret store (macOS Keychain, Linux GNOME Keyring, Windows Credential Manager)
|
|
3238
|
-
4. ~/.config/quickchr/
|
|
3567
|
+
4. ~/.config/quickchr/com.quickchr.mikrotik-web.json (fallback)`);
|
|
3239
3568
|
break;
|
|
3240
3569
|
case "qga":
|
|
3241
3570
|
console.log(`quickchr qga <name> <operation> [options]
|