@tikoci/centrs 0.1.9-next.0 → 0.1.9-next.22
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/CHANGELOG.md +177 -40
- package/GLOSSARY.txt +8 -0
- package/README.md +12 -8
- package/SECURITY.md +11 -3
- package/commands/api/README.md +39 -15
- package/commands/api/examples.md +44 -6
- package/commands/execute/README.md +108 -14
- package/commands/execute/examples.md +118 -3
- package/commands/explain/README.md +2 -1
- package/commands/mcp/README.md +11 -0
- package/commands/mcp/examples.md +6 -0
- package/commands/retrieve/README.md +30 -13
- package/commands/retrieve/examples.md +10 -6
- package/commands/transfer/examples.md +9 -0
- package/docs/CLI.md +8 -8
- package/docs/CONSTITUTION.md +25 -2
- package/docs/MATRIX.md +5 -4
- package/docs/errors/cdb/empty-group.md +19 -3
- package/docs/errors/cdb/empty-selection.md +9 -3
- package/docs/errors/cmr/caller-policy-not-enforced.md +46 -0
- package/docs/errors/cmr/empty-selection.md +17 -3
- package/docs/errors/cmr/rule-selects-every-device.md +32 -0
- package/docs/errors/cmr/selection-includes-controller.md +27 -0
- package/docs/errors/usage/flag-after-separator.md +34 -0
- package/package.json +7 -5
- package/src/api-fanout.ts +11 -1
- package/src/api.ts +205 -65
- package/src/btest.ts +7 -2
- package/src/cli/api.ts +30 -16
- package/src/cli/common.ts +180 -1
- package/src/cli/devices.ts +0 -2
- package/src/cli/discover.ts +0 -1
- package/src/cli/execute.ts +44 -20
- package/src/cli/explain.ts +2 -0
- package/src/cli/mcp.ts +0 -1
- package/src/cli/retrieve.ts +40 -18
- package/src/cli/settings.ts +0 -3
- package/src/cli/terminal.ts +0 -2
- package/src/cli/transfer.ts +10 -8
- package/src/cli/version.ts +87 -0
- package/src/cli.ts +5 -3
- package/src/core/cmr-fleet.ts +486 -0
- package/src/core/cmr.ts +78 -0
- package/src/core/envelope.ts +32 -3
- package/src/core/error-catalog.ts +25 -3
- package/src/core/execute-command.ts +144 -19
- package/src/core/fanout.ts +130 -6
- package/src/errors.ts +2 -1
- package/src/execute-cmr.ts +95 -78
- package/src/execute-fanout.ts +11 -1
- package/src/execute.ts +105 -23
- package/src/index.ts +18 -1
- package/src/mcp/safety.ts +11 -0
- package/src/mcp/tools.ts +4 -1
- package/src/resolver/cdb.ts +13 -1
- package/src/resolver/index.ts +4 -0
- package/src/resolver/selection.ts +37 -1
- package/src/resolver/target.ts +96 -8
- package/src/retrieve-fanout.ts +4 -1
- package/src/retrieve-follow.ts +29 -33
- package/src/retrieve-sample.ts +14 -8
- package/src/retrieve-stream.ts +38 -6
- package/src/retrieve.ts +73 -59
- package/src/transfer-fanout.ts +8 -1
- package/src/transfer.ts +15 -19
|
@@ -47,12 +47,23 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
47
47
|
write-shaped operation. In script mode every command head in a statement
|
|
48
48
|
(the statement start and each `[…]` or `{…}` body) must be a read: its verb
|
|
49
49
|
is the first word past the known menu prefix, so `/ip address print` is a
|
|
50
|
-
read and `/user expire-password print` is not (examples W3).
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
read and `/user expire-password print` is not (examples W3). Script
|
|
51
|
+
directives that only output, branch or compute a value are reads, including
|
|
52
|
+
the pure functions `:serialize`, `:deserialize`, `:convert`, `:delay`,
|
|
53
|
+
`:find`, `:timestamp`, `:rndnum`/`:rndstr` and the `:to*` converters, and the
|
|
54
|
+
`{…}`-bodied `:retry` and `:time` (example W4, #458). The absolute
|
|
55
|
+
`/serialize` spelling counts too; a bare `serialize` keeps the menu-verb rule,
|
|
56
|
+
because after navigation it could name a menu command. RouterOS runs only a
|
|
57
|
+
`{…}` block in a script-typed argument, so every body is visible to the gate.
|
|
58
|
+
Root mutators such as `/import` and indirect
|
|
59
|
+
execution through `:execute` are write-shaped too, as are `:global`, `:set`
|
|
60
|
+
and `:parse`, and so is any command carrying `file=` or `file-name=`
|
|
61
|
+
(including argument prefixes such as `fi=` and `file-n=`, and the spaced
|
|
62
|
+
`file-name value` spelling under print/export/serialize, example W5):
|
|
63
|
+
RouterOS redirects output into a device file, so
|
|
64
|
+
`/ip/address/export file=address` creates `address.rsc`,
|
|
65
|
+
`/file/print file=test` creates `test.txt` and `:serialize … file-name=x`
|
|
66
|
+
creates `x`, even though `export`, `print` and `:serialize` are reads. The shared
|
|
56
67
|
`rosetta`/`lsp-routeros-ts`
|
|
57
68
|
canonicalizer is for canonicalization only — never the structured predicate
|
|
58
69
|
(widening `structured` is a product regression). Pinned by
|
|
@@ -180,7 +191,9 @@ duplicate that table. Behavior notes the generated reference cannot carry:
|
|
|
180
191
|
- `--` — use it when the command contains tokens that would otherwise be
|
|
181
192
|
claimed as centrs flags; otherwise the command must be quoted as a single
|
|
182
193
|
argument. In fan-out, positional targets come BEFORE `--` and the command
|
|
183
|
-
AFTER it (see **Target selection**).
|
|
194
|
+
AFTER it (see **Target selection**). centrs flags go before `--` too: one of
|
|
195
|
+
execute's own long flags after it (a trailing `--json`) is refused before
|
|
196
|
+
dialing as `usage/flag-after-separator`, with the corrected line (#459).
|
|
184
197
|
|
|
185
198
|
### Designed, not implemented
|
|
186
199
|
|
|
@@ -296,8 +309,12 @@ when the target is itself a CMR client.
|
|
|
296
309
|
selects the controller. CDB selectors (`--group`/`--where`/`--all`/
|
|
297
310
|
`--default`/geo), several targets, and `--via mac-telnet|ssh` are
|
|
298
311
|
`usage/conflicting-flags`. `--labels` without `--cmr` is refused the same way.
|
|
299
|
-
Labels that no device has are accepted silently by RouterOS
|
|
300
|
-
selection is `ok: true` with the warning `cmr/empty-selection
|
|
312
|
+
Labels that no device has are accepted silently by RouterOS. An empty
|
|
313
|
+
selection is `ok: true` with the warning `cmr/empty-selection` for a read,
|
|
314
|
+
and the outer error `cmr/empty-selection` for a write-shaped command, with
|
|
315
|
+
or without `--yes` (#460), as RouterOS refuses it (`no devices selected`). A
|
|
316
|
+
mistyped label then exits 1 instead of looking like a successful bulk write.
|
|
317
|
+
A device's
|
|
301
318
|
identity selects it only while the controller's `/cmr auto-labels` includes
|
|
302
319
|
`identity` (the default `all` does). On 7.26beta1, after `auto-labels=version`
|
|
303
320
|
a client's row still *showed* its identity label but no longer matched it.
|
|
@@ -336,16 +353,33 @@ when the target is itself a CMR client.
|
|
|
336
353
|
- **Write gate:** one `--yes`/TTY confirmation, naming how many clients will run
|
|
337
354
|
it, how many were excluded, and through which controller. Clients that failed
|
|
338
355
|
validation are never sent the command.
|
|
356
|
+
- **The controller login is not the limit (#456).** On 7.26beta1, RouterOS
|
|
357
|
+
does not check the caller's policy against a CMR script. `run-script`
|
|
358
|
+
needed only `read` + `api` on the controller login, and
|
|
359
|
+
`/cmr/device/reboot` only `read` + `test` + `api`. A login refused a direct
|
|
360
|
+
write (`not enough permissions (9)`) still wrote to clients and to the
|
|
361
|
+
controller's own row through `run-script`, and rebooted a client through
|
|
362
|
+
both. So a "reader" login on a 7.26beta1 controller is not read-only, and
|
|
363
|
+
centrs' write gate (and `mcp=ro` on MCP) is the only gate. MikroTik
|
|
364
|
+
[says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) 7.26beta2 refuses this for a `read` group; that is not yet
|
|
365
|
+
checked on a beta2 build. For a write,
|
|
366
|
+
centrs reads the login's own `/user` row and `/user/group` once (both are
|
|
367
|
+
readable with just `read` + `api`). When the login lacks `write`, `test` or
|
|
368
|
+
`reboot`, the result and the confirmation error carry
|
|
369
|
+
`cmr/caller-policy-not-enforced` naming what it lacks. A failed read only
|
|
370
|
+
loses the warning.
|
|
339
371
|
- **Dispatch:** one call per validated member, sending the input unchanged with
|
|
340
372
|
`timeout=` set from `--timeout` (capped at 55 s, under CMR's ~60 s per-call
|
|
341
373
|
ceiling). Every call pins `strategy=parallel fail-policy=continue
|
|
342
374
|
freeze-frame-interval=20ms`. **Never retried.**
|
|
343
375
|
- **Outcomes** are a `FanoutData` envelope in inventory order, with exit codes
|
|
344
|
-
0/2/1.
|
|
345
|
-
|
|
376
|
+
0/2/1. The outer `meta.target` is the controller, and the settings,
|
|
377
|
+
request and login every member shares are written once on the outer
|
|
378
|
+
envelope ([constitution](../../docs/CONSTITUTION.md#result-envelope)). Each
|
|
379
|
+
member's `meta.target.cmr` carries `{id, deviceKey, address}`. `success` → `data.ret` holds the client's output; a clipped
|
|
346
380
|
output adds `cmr/output-truncated` and `meta.truncated`. `--max-results`
|
|
347
381
|
applies per member, as in plain execute fan-out: a member whose rendered
|
|
348
|
-
envelope exceeds it (UTF-8 bytes) becomes `input/max-results-exceeded`, and
|
|
382
|
+
envelope (with its full meta, as it would render alone) exceeds it (UTF-8 bytes) becomes `input/max-results-exceeded`, and
|
|
349
383
|
that error says the command already ran (`context.dispatched: true`). A `failed` runtime
|
|
350
384
|
error maps through the usual `routeros/*` table. `skipped` →
|
|
351
385
|
`cmr/client-disconnected`.
|
|
@@ -366,8 +400,15 @@ when the target is itself a CMR client.
|
|
|
366
400
|
- **Validation metadata** reports what ran: `source: ":put [:parse ...] via cmr
|
|
367
401
|
run-script"`, the same claim the console gate makes. There is no client
|
|
368
402
|
`/console/inspect` stage.
|
|
369
|
-
- **
|
|
370
|
-
|
|
403
|
+
- **No progress stream.** `run-script` returns a client's output only when its
|
|
404
|
+
script ends, and no `/cmr` menu records a running script. To watch a long
|
|
405
|
+
CMR job, follow what it changes on a client:
|
|
406
|
+
`centrs retrieve <client> /ip/address --follow` while
|
|
407
|
+
`execute <controller> --cmr` adds addresses. That only works when the client
|
|
408
|
+
can also be reached directly; otherwise re-run a read through `--cmr`.
|
|
409
|
+
- **Not on MCP** until delegated authorization is designed (#449). That design
|
|
410
|
+
must not assume RouterOS refuses a fleet write for a `!write` login (see
|
|
411
|
+
above). `retrieve --cmr` is deferred (#441).
|
|
371
412
|
|
|
372
413
|
`/cmr/device/dashboard` is an interactive console view, refused as
|
|
373
414
|
`cmr/console-only`. A plain path, abbreviated or not (`/cmr/dev/dash`), is
|
|
@@ -382,6 +423,59 @@ Calling `/cmr/device/run-script` yourself, through `api` or plain `execute`,
|
|
|
382
423
|
validates only the controller request. The envelope then carries the warning
|
|
383
424
|
`cmr/client-script-not-validated`.
|
|
384
425
|
|
|
426
|
+
### Fleet commands run directly (#453)
|
|
427
|
+
|
|
428
|
+
A controller's fleet commands are one request to one router that act on many
|
|
429
|
+
devices: `/cmr/device/{reboot,upgrade,pair,run-script}`,
|
|
430
|
+
`/cmr/upgrade/trigger`, `/cmr/upgrade/job/run-next`, and `add`, `set`,
|
|
431
|
+
`remove`, `enable` or `disable` on `/cmr/vlan`, `/cmr/wifi`,
|
|
432
|
+
`/cmr/wifi/radio` and `/cmr/upgrade`. They are write-shaped, so they need
|
|
433
|
+
`--yes`. Before that gate, plain `execute` (and `api`) reads the controller's
|
|
434
|
+
`/cmr/device` inventory, read-only, and resolves which rows the command
|
|
435
|
+
reaches, where it can (see "Not resolved" below). The command still runs
|
|
436
|
+
exactly as typed.
|
|
437
|
+
|
|
438
|
+
- **The gate names the reach.** The prompt and the `usage/confirmation-required`
|
|
439
|
+
summary say how many CMR devices the command reaches and whether the
|
|
440
|
+
controller's own row (`controller=true`) is one of them. `error.context.cmr`
|
|
441
|
+
has `devices`, `connected`, `controllerIncluded`, `identities` and
|
|
442
|
+
`inventory`.
|
|
443
|
+
- **How the selection is resolved.** `numbers` matches a row's `.id` or
|
|
444
|
+
identity. `labels` follows the manual's rules over the row's `labels` and
|
|
445
|
+
`auto-labels`: plain labels are OR, `+label` must match, `-label` excludes,
|
|
446
|
+
`all` selects every row, and an expression with only exclusions starts from
|
|
447
|
+
every row. On a 7.26beta1 lab this matched the device's own selection
|
|
448
|
+
(a `run-script` probe per row) for every expression tried; the lab test
|
|
449
|
+
re-checks it (CMR-D9). It is the controller's current view: a disconnected
|
|
450
|
+
device's `auto-labels` can be stale. `/cmr/upgrade/trigger` reaches the rows
|
|
451
|
+
whose `upgrade-rule` it names. A rule's `set`, `remove`, `enable` or
|
|
452
|
+
`disable` reads the rule's own `labels`; a `set` that changes `labels`
|
|
453
|
+
reaches the old and the new devices. WiFi band labels (`+2ghz`, `+5ghz`,
|
|
454
|
+
`+6ghz`) pick radios, not devices, so they are left out.
|
|
455
|
+
- **Warnings, with or without `--yes`:**
|
|
456
|
+
- `cmr/selection-includes-controller`: the selection includes the
|
|
457
|
+
controller's own row (`labels=all` does, per the manual).
|
|
458
|
+
- `cmr/rule-selects-every-device`: a rule command with no device `labels`
|
|
459
|
+
(for `/cmr/vlan`, no `port-labels` either). The manual warns that such a
|
|
460
|
+
VLAN rule provisions every port of every connected device, the controller
|
|
461
|
+
included, and cuts the fleet's management access.
|
|
462
|
+
- `cmr/empty-selection`: by the controller's inventory now, nothing is
|
|
463
|
+
selected. RouterOS refuses this for a `/cmr/device` command; it accepts
|
|
464
|
+
a rule whose labels match nothing.
|
|
465
|
+
- `cmr/caller-policy-not-enforced` for `run-script` and `reboot`, as for
|
|
466
|
+
`--cmr` above (#456).
|
|
467
|
+
- **Not resolved:** an abbreviated selector argument (`lab=all`),
|
|
468
|
+
`--via ssh` or `mac-telnet`, `/cmr/upgrade/job/run-next` (a job's devices
|
|
469
|
+
are not in the inventory), and a failed read. The gate still names the
|
|
470
|
+
fleet command and says why its reach is unknown, for example `would reach
|
|
471
|
+
the CMR devices it selects through controller ctl (not resolved: an upgrade
|
|
472
|
+
job's devices are not in the inventory)`, and `error.context.cmr` has
|
|
473
|
+
`unresolved` instead of the counts. A failed read never blocks the command,
|
|
474
|
+
and after a transport or login failure the policy read is skipped. Two
|
|
475
|
+
cases are not recognized as fleet commands at all and keep the plain
|
|
476
|
+
wording: script-shaped input (a positional id or `[find …]`) and CDB fan-out
|
|
477
|
+
to several controllers, which has its own gate.
|
|
478
|
+
|
|
385
479
|
## mac-telnet L2 validation
|
|
386
480
|
|
|
387
481
|
Decided (2026-06-06): the real-L2 integration path is `@tikoci/quickchr`'s
|
|
@@ -573,6 +573,47 @@ argument happens to be `print`, so it returns `usage/confirmation-required`.
|
|
|
573
573
|
Every command head in a statement is checked, including `[…]` and `do={…}`
|
|
574
574
|
bodies.
|
|
575
575
|
|
|
576
|
+
### W4. Pure script functions are reads; an output file is not (#458)
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
centrs execute $R ':put [:serialize to=json [/system/resource/print as-value]]' --via rest-api --username $U --password $P </dev/null
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Envelope: `ok: true` without `--yes`; `data.ret` parses as JSON whose
|
|
583
|
+
`version` is the router's. `:delay`, `:convert`, `:deserialize`, `:find`,
|
|
584
|
+
`:timestamp`, `:rndnum`, `:rndstr`, the `:to*` converters, `:retry` and `:time` are reads
|
|
585
|
+
too; any command inside their arguments or `{…}` bodies is still checked.
|
|
586
|
+
RouterOS runs only a `{…}` block in a script-typed argument and refuses a quoted
|
|
587
|
+
string there, so no body hides in text. `:serialize … file-name=centrs-w4`
|
|
588
|
+
creates a device file, so it returns `usage/confirmation-required`, as `file=`
|
|
589
|
+
does under a read verb. `:global`, `:set`, `:execute` and `:parse` stay writes.
|
|
590
|
+
|
|
591
|
+
### W5. Abbreviated output-file arguments keep the write gate
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
centrs execute $R '/system identity export fi=centrs-w5' --via rest-api --username $U --password $P </dev/null
|
|
595
|
+
centrs execute $R ':serialize to=json value=1 file-n=centrs-w5.json' --via rest-api --username $U --password $P </dev/null
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Both return `usage/confirmation-required` and create no file. The same
|
|
599
|
+
commands with `--yes` pass validation and create `centrs-w5.rsc` and
|
|
600
|
+
`centrs-w5.json`, respectively. Native API has the same result. Through
|
|
601
|
+
`centrs_execute`, an `mcp=ro` record returns `cdb/write-not-permitted` with or
|
|
602
|
+
without `confirm: true`, and creates no file.
|
|
603
|
+
|
|
604
|
+
RouterOS also accepts `fi "centrs-w5"` and `file-n "centrs-w5.json"`, without
|
|
605
|
+
`=`. They keep the same confirmation and MCP policy gates, including when
|
|
606
|
+
serialize's file argument follows a nested value expression.
|
|
607
|
+
|
|
608
|
+
`test/integration/execute-write-gate.test.ts` proves the refusal, absence and
|
|
609
|
+
confirmed creation. The export control runs on the selected channel; the
|
|
610
|
+
serialize control pins CHR 7.26beta1, where its file output was reproduced.
|
|
611
|
+
The export uses CLI script spelling: structured attribute validation requires
|
|
612
|
+
full argument names and rejects `fi` as `validation/unknown-attribute`.
|
|
613
|
+
This is a grounding version, not an introduction boundary. Unknown or
|
|
614
|
+
ambiguous output-file prefixes stay write-shaped until the device resolves
|
|
615
|
+
them.
|
|
616
|
+
|
|
576
617
|
### R1. A post-validation `/execute` rejection fails the overall envelope
|
|
577
618
|
|
|
578
619
|
```bash
|
|
@@ -622,6 +663,16 @@ centrs execute --group no-such-group --via rest-api --cdb-file $CDB --json -- /s
|
|
|
622
663
|
`ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
|
|
623
664
|
`warnings` include `cdb/empty-group`. Exit `0`.
|
|
624
665
|
|
|
666
|
+
The same group with a write is an outer error, with or without `--yes`, so a
|
|
667
|
+
misspelled group cannot pass for a successful bulk write (#460):
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
centrs execute --group no-such-group --via rest-api --cdb-file $CDB --yes --json -- '/system/identity/set name=x'
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
`ok: false`, `error.code` `cdb/empty-group`, `error.context.write` `true`.
|
|
674
|
+
Exit `1`. Nothing is dialed.
|
|
675
|
+
|
|
625
676
|
### F3. `--where` device-class selector (subset)
|
|
626
677
|
|
|
627
678
|
```bash
|
|
@@ -763,7 +814,7 @@ transport.
|
|
|
763
814
|
### Q3. Repeated `--quickchr` fans out; an unknown machine is an inner failure
|
|
764
815
|
|
|
765
816
|
```bash
|
|
766
|
-
centrs execute --quickchr $NAME --quickchr no-such-machine -- ':put 1'
|
|
817
|
+
centrs execute --quickchr $NAME --quickchr no-such-machine --json -- ':put 1'
|
|
767
818
|
```
|
|
768
819
|
|
|
769
820
|
Envelope: outer `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`;
|
|
@@ -775,9 +826,9 @@ the unknown machine's inner error is `quickchr/machine-not-found`. Exit code 2
|
|
|
775
826
|
`test/integration/execute-cmr.test.ts` runs these on a two-CHR CMR lab
|
|
776
827
|
(`test/integration/cmr-lab.ts`): `$CTL` is a 7.26beta1 controller with the
|
|
777
828
|
`cmr` package, paired with one client over a socket link. Examples CMR1–CMR7
|
|
778
|
-
and CMR10–
|
|
829
|
+
and CMR10–CMR18 run over **both** `--via native-api` and `--via rest-api` to
|
|
779
830
|
the controller, with validation on. The same test first asserts the device
|
|
780
|
-
behaviour the design depends on (CMR-D1–
|
|
831
|
+
behaviour the design depends on (CMR-D1–D9 in the test file).
|
|
781
832
|
|
|
782
833
|
### CMR0. A router without CMR
|
|
783
834
|
|
|
@@ -856,6 +907,16 @@ centrs execute --quickchr $CTL --cmr --labels no-such-label -- ':put x'
|
|
|
856
907
|
`ok: true`, `data.summary.total` is 0, warning `cmr/empty-selection`, exit
|
|
857
908
|
code 0.
|
|
858
909
|
|
|
910
|
+
The same labels with a write are an outer error, with or without `--yes`, as
|
|
911
|
+
RouterOS itself refuses an empty selection (#460):
|
|
912
|
+
|
|
913
|
+
```bash
|
|
914
|
+
centrs execute --quickchr $CTL --cmr --labels no-such-label --yes -- '/system/note/set note="x"'
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
`ok: false`, `error.code` `cmr/empty-selection`, `error.context.write` `true`,
|
|
918
|
+
exit code 1. No note changes.
|
|
919
|
+
|
|
859
920
|
### CMR8. CLI exit code for a mixed result
|
|
860
921
|
|
|
861
922
|
CMR2 through `runCli` exits `2`.
|
|
@@ -920,6 +981,21 @@ centrs execute --quickchr <client> --cmr --labels all -- ':put x'
|
|
|
920
981
|
`cmr/not-a-controller`: the client runs 7.26beta1 but has no `cmr` package.
|
|
921
982
|
`context.controllerIdentity` names the controller it is paired to.
|
|
922
983
|
|
|
984
|
+
### CMR15. A controller login without `write` or `reboot`
|
|
985
|
+
|
|
986
|
+
The controller has a group with `read,test,api,rest-api,!write,!reboot,!sensitive,!policy`
|
|
987
|
+
and a user `$READER` in it. That login cannot write on the controller
|
|
988
|
+
directly, but:
|
|
989
|
+
|
|
990
|
+
```bash
|
|
991
|
+
centrs execute 127.0.0.1 --port $API --username $READER --password $PW --cmr --labels <client> --yes -- '/system/note/set note="<marker>"'
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
The target is `ok`, the client's note reads `<marker>`, and the envelope warns
|
|
995
|
+
`cmr/caller-policy-not-enforced` with `context.missing` `["write", "reboot"]`:
|
|
996
|
+
CMR ran the script with `read,write,test,reboot` (#456). The same write as the
|
|
997
|
+
lab's admin login has no such warning.
|
|
998
|
+
|
|
923
999
|
### CMR13. The dashboard behind a relative menu change
|
|
924
1000
|
|
|
925
1001
|
```bash
|
|
@@ -931,3 +1007,42 @@ The first is `cmr/console-only`: the controller's `:parse` resolves it to
|
|
|
931
1007
|
`/cmr/device/;(evl /cmr/device/dashboard)`, so it is refused before it runs.
|
|
932
1008
|
The second is `ok` with `data.ret` `CMR device dashboard`: a string that spells
|
|
933
1009
|
the words is not a command.
|
|
1010
|
+
|
|
1011
|
+
### CMR16. A fleet command names the devices it reaches (#453)
|
|
1012
|
+
|
|
1013
|
+
```bash
|
|
1014
|
+
centrs execute --quickchr $CTL -- '/cmr/device/reboot labels=all'
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Without `--yes` (no TTY) this is `usage/confirmation-required`. Its summary
|
|
1018
|
+
says it would reach 2 CMR devices, the controller itself among them, and
|
|
1019
|
+
`error.context.cmr` is `{devices: 2, controllerIncluded: true}` with both
|
|
1020
|
+
identities. The envelope warns `cmr/selection-includes-controller`.
|
|
1021
|
+
Nothing reboots.
|
|
1022
|
+
|
|
1023
|
+
### CMR17. A rule with no labels
|
|
1024
|
+
|
|
1025
|
+
```bash
|
|
1026
|
+
centrs execute --quickchr $CTL --yes -- '/cmr/vlan/add vlan-ids=4001 disabled=yes comment=<marker>'
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
`ok: true` with the warning `cmr/rule-selects-every-device`: the rule has no
|
|
1030
|
+
`labels` and no `port-labels`. It is added disabled, so nothing is
|
|
1031
|
+
provisioned. Removing it by its `.id` warns the same way, read from the rule:
|
|
1032
|
+
|
|
1033
|
+
```bash
|
|
1034
|
+
centrs execute --quickchr $CTL --yes -- '/cmr/vlan/remove numbers=<id>'
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
### CMR18. `run-script` called directly by a login without `write` or `reboot`
|
|
1038
|
+
|
|
1039
|
+
With CMR15's restricted login:
|
|
1040
|
+
|
|
1041
|
+
```bash
|
|
1042
|
+
centrs execute 127.0.0.1 --port $API --username $READER --password $PW --yes -- '/cmr/device/run-script labels=<client> script=":put centrs-cmr18"'
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
`ok: true`, with the warnings `cmr/caller-policy-not-enforced`
|
|
1046
|
+
(`context.missing` `["write", "reboot"]`) and
|
|
1047
|
+
`cmr/client-script-not-validated`. The same command as the lab's admin login
|
|
1048
|
+
has neither the policy warning nor `cmr/selection-includes-controller`.
|
|
@@ -392,7 +392,8 @@ bare menu path. The positional grammar stays **target-first like every other
|
|
|
392
392
|
router-taking command** (amended in PR review — the shared resolver/selection
|
|
393
393
|
helpers assume it): one positional means offline and it *is* the input; two
|
|
394
394
|
positionals mean live with the router first; `--` is accepted before the
|
|
395
|
-
input
|
|
395
|
+
input, and one of explain's own long flags after it is refused as
|
|
396
|
+
`usage/flag-after-separator` (#459); **`--file` replaces the input positional in either form**, so with it
|
|
396
397
|
every positional is a target. Adding a
|
|
397
398
|
router therefore never reinterprets a previously valid offline invocation.
|
|
398
399
|
|
package/commands/mcp/README.md
CHANGED
|
@@ -178,6 +178,17 @@ MCP server applies it as a hard allowlist:
|
|
|
178
178
|
regardless of `confirm`; a local CDB mutation through `centrs_devices` or
|
|
179
179
|
`centrs_discover` `save` fails with `usage/confirmation-required` unless
|
|
180
180
|
explicitly confirmed.
|
|
181
|
+
- **`mcp=ro` does not rest on RouterOS policy.** On a 7.26beta1 CMR
|
|
182
|
+
controller, a login without `write` or `reboot` wrote to a client and to the
|
|
183
|
+
controller's own row through `/cmr/device/run-script`, and rebooted a client
|
|
184
|
+
through `run-script` and `/cmr/device/reboot`. That is the CHR reproduction
|
|
185
|
+
on #456; the integration test re-proves the `run-script` write (CMR15).
|
|
186
|
+
MikroTik [says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) 7.26beta2 will refuse this for a `read` group, but
|
|
187
|
+
centrs' classification does not depend on it: every CMR fleet command
|
|
188
|
+
(`run-script`, `reboot`, `upgrade`, `pair`, `/cmr/upgrade/trigger`, …) is
|
|
189
|
+
write-shaped over every path, pinned by `test/unit/write-gate-tables.test.ts`,
|
|
190
|
+
and `mcp=ro` refuses them whatever the controller login's group is. Any
|
|
191
|
+
future MCP route to CMR clients (#449) must keep that assumption.
|
|
181
192
|
- **`--allow-adhoc-targets`** (off by default) is reserved for one-off lab/power
|
|
182
193
|
user flows. Current RouterOS-facing tools expose no inline host+credential
|
|
183
194
|
schema, so the CDB allowlist remains the only active target source.
|
package/commands/mcp/examples.md
CHANGED
|
@@ -131,6 +131,12 @@ With `$NAME` registered `mcp=ro` (default):
|
|
|
131
131
|
Envelope: `ok: false`, `error.code = "cdb/write-not-permitted"`. The router is not
|
|
132
132
|
mutated even though `confirm` is true — the CDB policy is authoritative.
|
|
133
133
|
|
|
134
|
+
Output-file arguments count as writes even when abbreviated:
|
|
135
|
+
`:serialize to=json value=1 file-n=out.json` and
|
|
136
|
+
`/system identity export fi=out` are refused with or without `confirm`.
|
|
137
|
+
The CHR refusal and file read-back controls are
|
|
138
|
+
[execute example W5](../execute/examples.md#w5-abbreviated-output-file-arguments-keep-the-write-gate).
|
|
139
|
+
|
|
134
140
|
### 8. Write against an `mcp=rw` target succeeds with `confirm: true`
|
|
135
141
|
|
|
136
142
|
With `$NAME` registered `mcp=rw`:
|
|
@@ -262,13 +262,19 @@ target only: a fan-out selector fails `usage/fanout-not-supported`. Follow is
|
|
|
262
262
|
filtered with the same `--query`/`--filter` grammar as a one-shot read (#397).
|
|
263
263
|
|
|
264
264
|
**What each line means.** Every line is one envelope; read
|
|
265
|
-
`meta.operation.stream
|
|
265
|
+
`meta.operation.stream`. Only `started`, `synced` and `summary` carry the full
|
|
266
|
+
`meta` (target, settings, validation, request, auth). A `frame` line carries
|
|
267
|
+
only `meta.operation` (`kind`, `objectCount`, `stream`), because the rest is
|
|
268
|
+
the same on every line. An error line is a full error envelope: the one line
|
|
269
|
+
of a follow that fails before it starts (resolution, validation, a
|
|
270
|
+
non-followable menu, a non-native transport), and a failed `summary`
|
|
271
|
+
([constitution](../../docs/CONSTITUTION.md#result-envelope)).
|
|
266
272
|
|
|
267
273
|
| `kind` | When | `data` | Fields |
|
|
268
274
|
| ------ | ---- | ------ | ------ |
|
|
269
|
-
| `frame` | One row change | the row (`upsert`), `null` (`removed`) | `index`, `phase` (`snapshot`/`live`), `change` (`upsert`/`removed`), `id`, `source` (`print`/`listen`/`sweep`/`membership`) |
|
|
275
|
+
| `frame` | One row change | the row (`upsert`), `null` (`removed`) | `index`, `observedAt`, `phase` (`snapshot`/`live`), `change` (`upsert`/`removed`), `id`, `source` (`print`/`listen`/`sweep`/`membership`) |
|
|
276
|
+
| `started` | First, once validation passed | `null` | — (`tips` carries any advice) |
|
|
270
277
|
| `synced` | Once, after the snapshot | `null` | `rows`: rows held at that point |
|
|
271
|
-
| `notice` | First, only when there is advice | `null` | — (`tips` carries it) |
|
|
272
278
|
| `summary` | Last, exactly once | the counts | `stopReason`, `frames`, `snapshot`, `changes`, `sweeps`, `synced`, `durationMs` |
|
|
273
279
|
|
|
274
280
|
- **Bootstrap (A1).** centrs starts the `listen`, then the `print`, on one
|
|
@@ -279,6 +285,11 @@ filtered with the same `--query`/`--filter` grammar as a one-shot read (#397).
|
|
|
279
285
|
order resurrects deleted rows (#396 round 3). `synced` means the bootstrap
|
|
280
286
|
was processed. It is not an atomic snapshot, a full event history, or
|
|
281
287
|
routing convergence.
|
|
288
|
+
- **`observedAt`** (ISO 8601, host clock) is when centrs received the reply
|
|
289
|
+
the frame is built from: the `listen` change, the snapshot row, the
|
|
290
|
+
membership read or the sweep. A bootstrap change replayed after the
|
|
291
|
+
snapshot keeps its arrival time, so `observedAt` is not ordered by `index`.
|
|
292
|
+
A slow reader does not move it.
|
|
282
293
|
- **`upsert`** carries the latest full row; replace what you hold for `id`.
|
|
283
294
|
**`removed`** carries `data: null`; drop `id`. The row identity is always
|
|
284
295
|
`meta.operation.stream.id`, even when `--attributes` leaves `.id` out of
|
|
@@ -297,8 +308,8 @@ filtered with the same `--query`/`--filter` grammar as a one-shot read (#397).
|
|
|
297
308
|
sweep only checks membership: it does not repair a missed field update or
|
|
298
309
|
report rows `listen` never announced. `--sweep` needs a unit (`10s`,
|
|
299
310
|
`500ms`); a bare number fails `settings/invalid-timeout`, as for `--sample`.
|
|
300
|
-
`--sweep 0` turns it off, with a
|
|
301
|
-
not be reported.
|
|
311
|
+
`--sweep 0` turns it off, with a tip on `started` saying view-menu removals
|
|
312
|
+
will not be reported.
|
|
302
313
|
- **What `listen` never reports.** Hot counters (`rx-byte`, firewall
|
|
303
314
|
`bytes`/`packets`) and some protocol state (BFD sessions accept `listen` but
|
|
304
315
|
stay silent). The follow is correct but quiet there; `--sample` (below)
|
|
@@ -320,7 +331,8 @@ filtered with the same `--query`/`--filter` grammar as a one-shot read (#397).
|
|
|
320
331
|
indefinitely or postpone the first sweep and live `--count`. This may
|
|
321
332
|
coalesce more changes than unfiltered A1 replay.
|
|
322
333
|
The filtered sweep also detects silent exits from a predicate (e.g. a
|
|
323
|
-
decreasing address-list `timeout`)
|
|
334
|
+
decreasing address-list `timeout`), reported as `removed` with
|
|
335
|
+
`source: "sweep"`. It does not discover silent entries or
|
|
324
336
|
repair silent field updates: use `--sample` for such state. With `--sweep 0`,
|
|
325
337
|
silent exits remain held until a listen notification arrives.
|
|
326
338
|
- **Projection.** `--attribute(s)` projects `data`; centrs still asks RouterOS
|
|
@@ -346,9 +358,11 @@ new snapshot. `--sweep` without `--follow`, and `--count` or `--duration`
|
|
|
346
358
|
without `--follow` or `--sample`, fail `usage/conflicting-flags`.
|
|
347
359
|
|
|
348
360
|
**Format.** Under `--follow`, `json`, `yaml` and `ndjson` all print one compact
|
|
349
|
-
envelope per line (as `api --stream` does)
|
|
350
|
-
|
|
351
|
-
|
|
361
|
+
envelope per line (as `api --stream` does), errors included, on stdout.
|
|
362
|
+
`text` prints one short row per `frame` on stdout; the `started` tips, the
|
|
363
|
+
`synced` and summary lines, warnings and errors go to stderr, since text
|
|
364
|
+
stdout is data ([constitution](../../docs/CONSTITUTION.md#result-envelope)).
|
|
365
|
+
The exit code is nonzero for any failure, even after rows. Without `--follow`,
|
|
352
366
|
`--format ndjson` prints the ordinary envelope as one compact line; it never
|
|
353
367
|
starts a follow.
|
|
354
368
|
|
|
@@ -387,10 +401,13 @@ stays available as `api <router> <menu>/print -X POST -f interval=1s --stream`
|
|
|
387
401
|
(`commands/api/README.md`).
|
|
388
402
|
|
|
389
403
|
**What each line means.** Every line is one envelope; read
|
|
390
|
-
`meta.operation.stream
|
|
404
|
+
`meta.operation.stream`. `started` and `summary` carry the full `meta`; a
|
|
405
|
+
`sample` line carries only `meta.operation` (`kind`, `objectCount`, `stream`),
|
|
406
|
+
as a follow `frame` does:
|
|
391
407
|
|
|
392
408
|
| `kind` | When | `data` | Fields |
|
|
393
409
|
| ------ | ---- | ------ | ------ |
|
|
410
|
+
| `started` | First, once validation passed | `null` | — |
|
|
394
411
|
| `sample` | One complete read | the whole read: a row array (list), the record (singleton), or the bare value (one `--attribute` on a singleton) | `index` (1-based), `at` (when the read was sent, host clock, ISO 8601), `readMs` |
|
|
395
412
|
| `summary` | Last, exactly once | the counts | `stopReason`, `samples`, `durationMs` |
|
|
396
413
|
|
|
@@ -415,9 +432,9 @@ carrying the partial `samples` count; there is no retry. `--list-attributes`, `-
|
|
|
415
432
|
`--sweep` conflict with `--sample` (`usage/conflicting-flags`).
|
|
416
433
|
|
|
417
434
|
**Format.** As for `--follow`: `json`, `yaml` and `ndjson` print one compact
|
|
418
|
-
envelope per line,
|
|
419
|
-
|
|
420
|
-
any failure, even after samples.
|
|
435
|
+
envelope per line, errors included, on stdout. `text` prints `index`, `at` and
|
|
436
|
+
the data on one stdout line per sample, and the summary, warnings and errors
|
|
437
|
+
on stderr. The exit code is nonzero for any failure, even after samples.
|
|
421
438
|
|
|
422
439
|
Recipes (bounded, so an agent can run them unattended):
|
|
423
440
|
|
|
@@ -434,7 +434,9 @@ centrs retrieve $A /ip/address --follow --duration 3s --port $API_PORT --usernam
|
|
|
434
434
|
Every line parses as an envelope. Frames before `synced` have
|
|
435
435
|
`phase: "snapshot"` and `source: "print"`; exactly one `synced`; the last line
|
|
436
436
|
is the summary with `stopReason: "duration-elapsed"`, `synced: true`,
|
|
437
|
-
`changes: 0`. Every frame carries `meta.operation.stream.id
|
|
437
|
+
`changes: 0`. Every frame carries `meta.operation.stream.id` and an ISO 8601
|
|
438
|
+
`observedAt`, and no other meta; `synced` and the summary carry the full
|
|
439
|
+
`meta` (`target`, `via`, `settings`, …).
|
|
438
440
|
|
|
439
441
|
### FL2. A live change, and `--count` that ignores the snapshot
|
|
440
442
|
|
|
@@ -454,7 +456,8 @@ centrs retrieve $A /ip/address --follow --sweep 0 --count 1 --duration 15s --por
|
|
|
454
456
|
|
|
455
457
|
After `synced`, remove an address that existed before the follow. One `live`
|
|
456
458
|
`removed` frame, `source: "listen"`, `data: null`. `--sweep 0` keeps the sweep
|
|
457
|
-
out of the race, and its `tip/follow-sweep-off`
|
|
459
|
+
out of the race, and its `tip/follow-sweep-off` tip rides the first line,
|
|
460
|
+
`started`.
|
|
458
461
|
|
|
459
462
|
### FL4. A removal RouterOS does not send (view menu → sweep)
|
|
460
463
|
|
|
@@ -529,9 +532,10 @@ centrs retrieve $R /ip/address --sample 1s --count 3 --username $U --password $P
|
|
|
529
532
|
centrs retrieve $A /ip/address --via native-api --sample 1s --count 3 --port $API_PORT --username $U --password $P --json
|
|
530
533
|
```
|
|
531
534
|
|
|
532
|
-
Each run prints three `sample` lines (`index` 1–3, `data` a row
|
|
533
|
-
summary with `stopReason: "count-reached"`, `samples: 3`.
|
|
534
|
-
values are at least one second apart. `
|
|
535
|
+
Each run prints `started`, three `sample` lines (`index` 1–3, `data` a row
|
|
536
|
+
array) and a summary with `stopReason: "count-reached"`, `samples: 3`.
|
|
537
|
+
Consecutive `at` values are at least one second apart. The `started` line's
|
|
538
|
+
`meta.via` is the transport used.
|
|
535
539
|
|
|
536
540
|
### SA2. Counters `listen` never reports
|
|
537
541
|
|
|
@@ -687,7 +691,7 @@ centrs retrieve $A /ip/firewall/address-list --via native-api --port $API_PORT -
|
|
|
687
691
|
|
|
688
692
|
The harness adds a row with a 10-second timeout. A sweep removes it from the
|
|
689
693
|
selected set as its remaining timeout crosses 8 seconds, with
|
|
690
|
-
`source: "
|
|
694
|
+
`source: "sweep"`; the row still exists in an unfiltered read.
|
|
691
695
|
|
|
692
696
|
### FQ4. CLI alias and predicate validation
|
|
693
697
|
|
|
@@ -463,6 +463,15 @@ centrs transfer --group no-such-group list --via rest --cdb-file $CDB --json
|
|
|
463
463
|
`ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
|
|
464
464
|
`warnings` include `cdb/empty-group`. Exit `0`.
|
|
465
465
|
|
|
466
|
+
A write verb (`upload`, `remove`, `mkdir`, `copy`) over the same group is an
|
|
467
|
+
outer error, with or without `--yes` (#460):
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
centrs transfer --group no-such-group remove fanout-test.txt --via rest --cdb-file $CDB --yes --json
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`ok: false`, `error.code` `cdb/empty-group`. Exit `1`.
|
|
474
|
+
|
|
466
475
|
### F3. `--where` device-class selector (subset)
|
|
467
476
|
|
|
468
477
|
```bash
|
package/docs/CLI.md
CHANGED
|
@@ -51,8 +51,8 @@ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <targe
|
|
|
51
51
|
| `--all-attributes` | | Request the RouterOS detail/all-attributes shape. |
|
|
52
52
|
| `--list-attributes / --list` | | List inspect-derived attributes without running the data call. |
|
|
53
53
|
| `--query / --filter` | `<expr>` | Row filter written as after `print where` (`disabled`, `mtu>=1500 and !dynamic`), run on the router as an API query; repeatable, AND-ed. No `~` or `in`. |
|
|
54
|
-
| `--follow` | | Keep the menu's rows current over native-api `listen`: snapshot frames, a `synced` line, then a line per change, then a summary (NDJSON for json/yaml/ndjson). Single target, list menus whose print takes follow-only. |
|
|
55
|
-
| `--sample` | `<interval>` | Read the menu again every interval (with a unit, e.g. `5s`): one line per complete read, then a summary. With --wait, sets polling cadence and returns one final envelope. Sees counters and state listen never reports. Single target; rest-api or native-api. |
|
|
54
|
+
| `--follow` | | Keep the menu's rows current over native-api `listen`: a `started` line, snapshot frames, a `synced` line, then a line per change, then a summary (NDJSON for json/yaml/ndjson). Single target, list menus whose print takes follow-only. |
|
|
55
|
+
| `--sample` | `<interval>` | Read the menu again every interval (with a unit, e.g. `5s`): a `started` line, one line per complete read, then a summary. With --wait, sets polling cadence and returns one final envelope. Sees counters and state listen never reports. Single target; rest-api or native-api. |
|
|
56
56
|
| `--wait` | `<deadline>` | Wait for a successful read or a condition until this overall deadline, with a unit (e.g. `2m`). Retries refused, closed, reset and timed-out connections and a down or unreachable host; other errors fail at once. One final envelope; single target. |
|
|
57
57
|
| `--until` | `<expr>` | With --wait: succeed when any selected row matches this --query grammar predicate; repeatable, AND-ed. Returns the matching rows. Already true succeeds on the initial read. |
|
|
58
58
|
| `--until-empty` | | With --wait: succeed on a completed read with zero rows selected by --query. Silence and failed reads never satisfy it. |
|
|
@@ -66,7 +66,7 @@ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <targe
|
|
|
66
66
|
| `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
|
|
67
67
|
| `--resolve` | <none\|arp> | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
|
|
68
68
|
| `--validate / --no-validate` | | Enable or disable inspect-backed preflight validation. |
|
|
69
|
-
| `--verbose` | | Show resolved setting sources in text output. |
|
|
69
|
+
| `--verbose` | | Show resolved setting sources in text output, on stderr. |
|
|
70
70
|
|
|
71
71
|
## execute
|
|
72
72
|
|
|
@@ -104,8 +104,8 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
|
|
|
104
104
|
| `--max-results` | `<bytes>` | Fail if the rendered envelope exceeds this byte budget. |
|
|
105
105
|
| `--format` | <text\|json\|yaml> | Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope. |
|
|
106
106
|
| `--json` | | Shortcut for `--format json`. |
|
|
107
|
-
| `--verbose` | | Include additional context in text output. |
|
|
108
|
-
| `--` | | End centrs option parsing: every following token is the literal RouterOS command
|
|
107
|
+
| `--verbose` | | Include additional context in text output, on stderr. |
|
|
108
|
+
| `--` | | End centrs option parsing: every following token is the literal RouterOS command (e.g. `-- /interface print where disabled=yes`). Put centrs flags before it; one of execute's own long flags after it is refused as `usage/flag-after-separator`. |
|
|
109
109
|
|
|
110
110
|
## api
|
|
111
111
|
|
|
@@ -126,7 +126,7 @@ Usage: centrs api <router> <endpoint> [flags] | centrs api --host <host|url> <en
|
|
|
126
126
|
| `--attribute / --proplist` | `<a,b>` | Property projection → `.proplist`. |
|
|
127
127
|
| `--raw` | | Strip the envelope; emit bare RouterOS JSON. Defaults `--validate` to false (an explicit `--validate=true` still runs the gate); does not imply `--yes`. |
|
|
128
128
|
| `--yes` | | Confirm a mutating (non-read) request in non-interactive runs. |
|
|
129
|
-
| `--stream` | | Stream the command's native replies (`!re` rows and `!empty`) as NDJSON, then a summary envelope. Never changes the command: a GET streams one print. To follow changes, request `<menu>/listen`, which implies it. |
|
|
129
|
+
| `--stream` | | Stream the command's native replies (`!re` rows and `!empty`) as NDJSON: a `started` line, a line per reply, then a summary envelope. Never changes the command: a GET streams one print. To follow changes, request `<menu>/listen`, which implies it. |
|
|
130
130
|
| `--count` | `<n>` | Stop a `--stream` after N `!re` rows; `!empty` replies do not count (not the device count= argument). |
|
|
131
131
|
| `--duration` | `<dur>` | Stop a `--stream` after this wall-clock window (e.g. `5s`). |
|
|
132
132
|
| `--via` | <rest-api\|native-api> | Pin the transport; no silent downgrade. Default rest-api. |
|
|
@@ -150,7 +150,7 @@ Usage: centrs api <router> <endpoint> [flags] | centrs api --host <host|url> <en
|
|
|
150
150
|
| `--validate / --no-validate` | | Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted). |
|
|
151
151
|
| `--format` | <json\|ndjson\|yaml\|text> | Output format. Defaults to json for api; `CENTRS_FORMAT` overrides. |
|
|
152
152
|
| `--json` | | Shortcut for `--format json`. |
|
|
153
|
-
| `--verbose` | | Include additional context in text output. |
|
|
153
|
+
| `--verbose` | | Include additional context in text output, on stderr. |
|
|
154
154
|
|
|
155
155
|
## transfer
|
|
156
156
|
|
|
@@ -190,7 +190,7 @@ Usage: centrs transfer <router>|--host <host> upload <local> [remote] | download
|
|
|
190
190
|
| `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
|
|
191
191
|
| `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
|
|
192
192
|
| `--validate / --no-validate` | | Enable or disable the existence-probe preflight. |
|
|
193
|
-
| `--verbose` | | Show the resolved transport in text output. |
|
|
193
|
+
| `--verbose` | | Show the resolved transport in text output, on stderr. |
|
|
194
194
|
|
|
195
195
|
## terminal
|
|
196
196
|
|