@tikoci/centrs 0.1.7 → 0.1.8
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 +256 -0
- package/GLOSSARY.txt +32 -0
- package/README.md +2 -2
- package/SECURITY.md +1 -1
- package/commands/AGENTS.md +2 -1
- package/commands/api/AGENTS.md +55 -30
- package/commands/api/README.md +123 -41
- package/commands/api/examples.md +205 -17
- package/commands/devices/README.md +11 -3
- package/commands/execute/README.md +31 -3
- package/commands/execute/examples.md +136 -1
- package/commands/explain/README.md +30 -4
- package/commands/mcp/README.md +5 -1
- package/commands/mcp/examples.md +18 -1
- package/commands/retrieve/README.md +348 -4
- package/commands/retrieve/examples.md +422 -5
- package/commands/terminal/README.md +14 -4
- package/commands/terminal/examples.md +22 -3
- package/commands/transfer/README.md +55 -9
- package/commands/transfer/examples.md +106 -7
- package/docs/CLI.md +28 -20
- package/docs/CONSTITUTION.md +30 -10
- package/docs/MATRIX.md +38 -1
- package/docs/errors/README.md +3 -1
- package/docs/errors/input/invalid-query.md +36 -0
- package/docs/errors/input/local-destination.md +20 -0
- package/docs/errors/input/unsupported-query.md +19 -0
- package/docs/errors/internal/byte-string.md +13 -0
- package/docs/errors/settings/invalid-timeout.md +6 -0
- package/docs/errors/transport/stream-overflow.md +21 -0
- package/docs/errors/usage/removed-flag.md +20 -0
- package/docs/errors/validation/menu-unavailable.md +22 -0
- package/docs/errors/validation/not-followable.md +23 -0
- package/docs/errors/validation/package-missing.md +29 -0
- package/docs/errors/validation/unknown-path.md +16 -4
- package/docs/errors/wait/deadline-exceeded.md +14 -0
- package/docs/errors/wait/interrupted.md +10 -0
- package/package.json +14 -11
- package/src/absent-menu.ts +280 -0
- package/src/api-fanout.ts +22 -12
- package/src/api.ts +511 -186
- package/src/btest.ts +19 -17
- package/src/cli/api.ts +126 -34
- package/src/cli/execute.ts +18 -5
- package/src/cli/retrieve.ts +223 -22
- package/src/cli/terminal.ts +10 -5
- package/src/cli/transfer.ts +23 -8
- package/src/core/envelope.ts +14 -0
- package/src/core/error-catalog.ts +56 -1
- package/src/core/inspect.ts +43 -5
- package/src/core/query.ts +508 -0
- package/src/core/routeros-errors.ts +1 -1
- package/src/devices.ts +6 -9
- package/src/discover.ts +7 -8
- package/src/errors.ts +116 -7
- package/src/execute-fanout.ts +13 -8
- package/src/execute.ts +106 -65
- package/src/explain/catalog.ts +58 -10
- package/src/explain/menus.ts +23 -2
- package/src/explain.ts +6 -6
- package/src/index.ts +29 -1
- package/src/mcp/safety.ts +43 -18
- package/src/mcp/tools.ts +14 -17
- package/src/protocols/adapter.ts +224 -118
- package/src/protocols/btest-session.ts +3 -2
- package/src/protocols/endpoint.ts +10 -0
- package/src/protocols/mac-telnet-console.ts +6 -3
- package/src/protocols/native-api.ts +223 -70
- package/src/protocols/sftp.ts +44 -14
- package/src/protocols/ssh.ts +42 -14
- package/src/resolver/cdb.ts +28 -7
- package/src/resolver/index.ts +1 -0
- package/src/resolver/target.ts +53 -7
- package/src/retrieve-fanout.ts +51 -12
- package/src/retrieve-follow.ts +862 -0
- package/src/retrieve-sample.ts +244 -0
- package/src/retrieve-stream.ts +115 -0
- package/src/retrieve-wait.ts +286 -0
- package/src/retrieve.ts +732 -108
- package/src/settings.ts +15 -15
- package/src/transfer-fanout.ts +40 -15
- package/src/transfer.ts +234 -32
package/commands/api/README.md
CHANGED
|
@@ -7,17 +7,19 @@ single REST or native-API operation, and returns the result in the standard
|
|
|
7
7
|
envelope.
|
|
8
8
|
|
|
9
9
|
Status: `CHR-passed` over `rest-api` and `native-api`, including multi-target
|
|
10
|
-
fan-out and
|
|
10
|
+
fan-out and incremental `--stream` replies (native-api only), per
|
|
11
11
|
`docs/MATRIX.md`. This file describes intent and flags; the matrix holds the
|
|
12
12
|
cell states. Load-bearing rules — envelope, errors, settings precedence,
|
|
13
13
|
identity, validation, protocol selection — live in
|
|
14
14
|
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md); the `(constitution: …)`
|
|
15
15
|
notes below point there rather than restating them.
|
|
16
16
|
|
|
17
|
-
`api` absorbs the former `stream` command
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
`api` absorbs the former `stream` command. `api` sends the command as typed:
|
|
18
|
+
following changes is the menu's own `listen` (`api <router> ip/address/listen`),
|
|
19
|
+
and `--stream` delivers any command's replies incrementally. `--listen` was
|
|
20
|
+
removed in #402 (`usage/removed-flag`, naming the exact replacement): it also
|
|
21
|
+
turned a GET into a listen. Streaming is single-session — it cannot combine
|
|
22
|
+
with multi-target fan-out (see below).
|
|
21
23
|
|
|
22
24
|
## Where `api` sits — the verb trichotomy
|
|
23
25
|
|
|
@@ -46,8 +48,10 @@ centrs api <router> <endpoint> [flags]
|
|
|
46
48
|
- `<endpoint>` — a REST-style path, leniently normalized. All of
|
|
47
49
|
`ip/address`, `/ip/address`, `rest/ip/address`, `/rest/ip/address`,
|
|
48
50
|
`"ip address"`, `'ip address'` canonicalize to `/ip/address`. A trailing id
|
|
49
|
-
segment (`ip/address/*1`) addresses one row
|
|
50
|
-
|
|
51
|
+
segment (`ip/address/*1`) addresses one row for GET/PATCH/DELETE; PUT and
|
|
52
|
+
POST reject it with `input/invalid-path`. A trailing `/listen` segment is
|
|
53
|
+
the menu's `listen` command, which implies `--stream` + `--via native-api`
|
|
54
|
+
and requires GET (`ip/address/*1/listen` follows one row).
|
|
51
55
|
|
|
52
56
|
The HTTP method (`-X`, default `GET`) is honored **literally** against RouterOS's
|
|
53
57
|
REST mapping — `GET`→print/get, **`PUT`→add**, `PATCH`→set, `DELETE`→remove,
|
|
@@ -64,18 +68,31 @@ the generated reference cannot carry:
|
|
|
64
68
|
|
|
65
69
|
- `-f` values pass through verbatim (no type-guessing); `-d`/`--input`
|
|
66
70
|
collide with `-f` → `usage/conflicting-flags`.
|
|
67
|
-
- `--query`/`--filter`
|
|
71
|
+
- `--query`/`--filter` take the same `print where` grammar as
|
|
72
|
+
`retrieve --query` ([retrieve README → Query](../retrieve/README.md#query---query))
|
|
73
|
+
and compile to REST `.query` words / native `?` words. A bare name is
|
|
74
|
+
`name=yes` for a boolean and "has a value" otherwise, which the router is
|
|
75
|
+
asked during validation; with `--validate=false` (or `--raw`) a bare name
|
|
76
|
+
fails `input/invalid-query`. Unlike `retrieve`, `api` does not check the
|
|
77
|
+
property names.
|
|
68
78
|
- `--raw-query` is emitted as-is; the caller owns the stack — e.g.
|
|
69
|
-
`--raw-query type=ether --raw-query type=vlan --raw-query '#|'` (OR)
|
|
79
|
+
`--raw-query type=ether --raw-query type=vlan --raw-query '#|'` (OR), or the
|
|
80
|
+
has-property `--raw-query comment` / absence `--raw-query -comment` words.
|
|
70
81
|
- `--via rest-api --stream` errors `transport/capability-unsupported`
|
|
71
82
|
(REST's 60 s cap cannot follow).
|
|
72
|
-
-
|
|
73
|
-
|
|
83
|
+
- `--format ndjson` prints every envelope as one compact line, one-shot or
|
|
84
|
+
streamed; it never starts a stream by itself. Under `--stream`,
|
|
85
|
+
`json`/`yaml` also emit NDJSON (unchanged for existing callers); `text`
|
|
86
|
+
emits a concise row per frame.
|
|
74
87
|
- The target/auth flags (`--host`, `--port`, `--username`, `--password`,
|
|
75
88
|
`--insecure`, `--timeout`, `--cdb-file`, `--cdb-password`, `--resolve`)
|
|
76
89
|
use the same single-target resolver as `retrieve`/`execute`; `--quickchr`
|
|
77
90
|
resolves host/port/auth from the live `@tikoci/quickchr` descriptor
|
|
78
91
|
instead (constitution: resolution providers).
|
|
92
|
+
- The `--host` **flag** stands in for a missing `<router>`:
|
|
93
|
+
`api --host http://192.0.2.10:8080 system/resource` targets that host with
|
|
94
|
+
no CDB lookup. `CENTRS_HOST` never does; `api <endpoint>` with it set is
|
|
95
|
+
still a missing `<router>`.
|
|
79
96
|
|
|
80
97
|
## Fan-out (multi-target)
|
|
81
98
|
|
|
@@ -93,7 +110,7 @@ vs `--query` distinction.
|
|
|
93
110
|
- **Writes fan out under `--yes`**, confirmed once up front (not per target).
|
|
94
111
|
Without `--yes`, the error names the blast radius (how many routers) and that
|
|
95
112
|
`--yes` is required.
|
|
96
|
-
- **`--
|
|
113
|
+
- **`--stream` (or a `/listen` endpoint) is single-session** → `usage/fanout-not-supported` in
|
|
97
114
|
fan-out mode. **`--raw` strips the envelope** → `usage/conflicting-flags` in
|
|
98
115
|
fan-out mode (per-target envelopes can't be bare).
|
|
99
116
|
|
|
@@ -171,35 +188,100 @@ the RouterOS error payload to stderr with a nonzero exit; a centrs-side failure
|
|
|
171
188
|
with no RouterOS response prints a compact `{code,message}` to stderr. Exit code is
|
|
172
189
|
`0` iff `ok`.
|
|
173
190
|
|
|
174
|
-
## Multi-frame results
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`
|
|
202
|
-
|
|
191
|
+
## Multi-frame results and incremental replies
|
|
192
|
+
|
|
193
|
+
Without `--stream`, bounded commands return their accumulated rows as an ordinary
|
|
194
|
+
array. REST retains its **60 s** cap; native API collects replies until `!done`.
|
|
195
|
+
|
|
196
|
+
`--stream` emits the literal command's native replies incrementally, whether
|
|
197
|
+
the command is finite or open-ended (#402: one wire reply, one line).
|
|
198
|
+
`--via rest-api --stream` fails with `transport/capability-unsupported`;
|
|
199
|
+
centrs never substitutes polling.
|
|
200
|
+
|
|
201
|
+
- **Following changes is a command you ask for**: the `<menu>/listen`
|
|
202
|
+
endpoint (GET; implies `--stream` and native-api), or POST `<menu>/print`
|
|
203
|
+
with `-f follow-only=` / `-f follow=` to pass print arguments.
|
|
204
|
+
`ip/address/*1/listen` follows one row (`?.id=*1`).
|
|
205
|
+
- **GET menu + `--stream`** streams one literal print and completes; it does
|
|
206
|
+
not follow. Its summary carries `tip/stream-print` naming the `/listen`
|
|
207
|
+
form (see the notice below). A streamed GET (or a `/listen`) rejects `-f` fields rather than
|
|
208
|
+
silently dropping them.
|
|
209
|
+
- **POST command + `--stream`** runs the literal command with its attributes:
|
|
210
|
+
`api $R /tool/ping -X POST -f address=10.0.2.2 -f count=3 --stream --yes`.
|
|
211
|
+
POST `/system/resource/print -f interval=1 --stream --count 2` samples twice.
|
|
212
|
+
PUT/PATCH/DELETE retain their add/set/remove mapping and confirmation gates.
|
|
213
|
+
Query/projection flags on these mutations or `/execute` scripts fail with
|
|
214
|
+
`usage/conflicting-flags` instead of being ignored. `/execute` retains
|
|
215
|
+
synchronous `as-string` script semantics. Streaming never makes a command
|
|
216
|
+
read-only.
|
|
217
|
+
- Each wire reply emits one envelope with `meta.operation.stream.kind=frame`
|
|
218
|
+
and `reply`: **`re`** (a row; `data` is the row, device `.section`,
|
|
219
|
+
`.dead`, `.nextid` and `.about` verbatim) or **`empty`** (RouterOS `!empty`:
|
|
220
|
+
zero rows; `data` is `null`). RouterOS sends `!empty` for an empty finite
|
|
221
|
+
read, for **each zero-row tick** of `print interval=`, and for a listen
|
|
222
|
+
cancelled before it sent anything. A frame received after centrs stopped the
|
|
223
|
+
stream locally carries `afterStop: true` — that cancellation `!empty` is not
|
|
224
|
+
evidence of an empty table. A terminal `!done` is not a frame: its
|
|
225
|
+
attributes, when received, appear in successful summary `data.done`,
|
|
226
|
+
including a terminal result received during local cancellation. The stop
|
|
227
|
+
reason still reports the first local bound.
|
|
228
|
+
- Read outcomes from **`meta.operation.stream`** on both success and failure;
|
|
229
|
+
successful `data.stopReason` is a compatibility copy.
|
|
230
|
+
- Exactly one terminal summary follows a started stream. Its marker is
|
|
231
|
+
`meta.operation.stream.kind=summary`, with `frames` (every reply line),
|
|
232
|
+
`rows` (`!re`), `empty` (`!empty`), `durationMs`, and `stopReason`:
|
|
233
|
+
`completed`, `count-reached`, `duration-elapsed`, `interrupted`,
|
|
234
|
+
`routeros-error`, or `transport-error`. `meta.operation.objectCount` is
|
|
235
|
+
`rows`. A RouterOS/transport failure (`!trap`, `!fatal`, a dropped
|
|
236
|
+
connection) has `ok:false` and a structured `error`; summary counts/reason
|
|
237
|
+
are in metadata (error envelopes have no `data`). Previously emitted rows
|
|
238
|
+
remain partial results. A failed summary retains any
|
|
239
|
+
`transport/cancel-unacknowledged` warning as well. Preflight failures emit
|
|
240
|
+
one error envelope without starting a stream.
|
|
241
|
+
- Structured streams put **all** envelopes, including errors, on stdout as
|
|
242
|
+
NDJSON and exit nonzero for any failure, even after successful rows. Text
|
|
243
|
+
mode uses the same outcome/exit rules. `--raw` retains bare payloads and
|
|
244
|
+
stderr errors; it does not provide the structured stream contract.
|
|
245
|
+
|
|
246
|
+
`api` never edits a change feed to make it safer; it says what RouterOS will
|
|
247
|
+
not report as tips (CHR 7.23.7 + 7.24.5). Because that advice depends only on
|
|
248
|
+
the request, it arrives **first**: a leading envelope with
|
|
249
|
+
`meta.operation.stream.kind=notice`, `data: null` and the `tips`, before any
|
|
250
|
+
reply — so a filtered watch that never gets a reply still shows it. A notice is
|
|
251
|
+
not a reply and is not counted in `frames`/`rows`; a stream with no advice has
|
|
252
|
+
no notice. The same tips are repeated on the terminal summary, successful or
|
|
253
|
+
failed. `--raw` omits the notice.
|
|
254
|
+
|
|
255
|
+
- `tip/filtered-follow` — a `listen` (or `follow`) with a query reports
|
|
256
|
+
neither rows leaving the filter nor deletes. Follow unfiltered and filter
|
|
257
|
+
on the client, or follow one row (`<menu>/*id/listen`). Stateful filtered
|
|
258
|
+
follow belongs to `retrieve --follow` (#396/#397).
|
|
259
|
+
- `tip/follow-proplist` — a `--proplist` without `.id,.dead` makes RouterOS
|
|
260
|
+
strip both from a delete frame, even under `?.id=`: the delete arrives as an
|
|
261
|
+
`!re` with no attributes. Add `.id,.dead` to see deletes.
|
|
262
|
+
|
|
263
|
+
`--count N` stops after N **`!re` rows**; `!empty` replies never count, so an
|
|
264
|
+
empty tick cannot use up the count before the data arrives.
|
|
265
|
+
`-f count=3` and `-f duration=2s` are **device arguments**; `--count` and
|
|
266
|
+
`--duration` are **centrs bounds**. The duration window starts after validation,
|
|
267
|
+
excluding its connection/login. With validation disabled, connect/login happen
|
|
268
|
+
inside the window. Duration expiry or Ctrl-C sends `/cancel` and waits up to
|
|
269
|
+
`--timeout` for acknowledgement; otherwise centrs closes locally and reports
|
|
270
|
+
`transport/cancel-unacknowledged`. Count stop sends `/cancel` best-effort and
|
|
271
|
+
closes without waiting (#385, example L5). A local bound is not a device `!done`.
|
|
272
|
+
|
|
273
|
+
For the next wire change on a connected router:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
centrs api $A /ip/address/listen --port $API_PORT --stream --count 1 --duration 60s
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
This counts replies rather than asserting readiness or a predicate. Use
|
|
280
|
+
`retrieve --wait` / `--until` / `--until-empty` instead when the router may
|
|
281
|
+
still be booting, when the condition may already be true, when you are on REST,
|
|
282
|
+
or when the state you need is one that `listen` never reports (counters,
|
|
283
|
+
`running`, timeouts)
|
|
284
|
+
([retrieve → Wait](../retrieve/README.md#wait---wait---until---until-empty)).
|
|
203
285
|
|
|
204
286
|
## MCP (deferred — forward guidance)
|
|
205
287
|
|
package/commands/api/examples.md
CHANGED
|
@@ -5,7 +5,8 @@ Each numbered example is an executable spec asserted by CHR integration tests
|
|
|
5
5
|
`test/integration/api.test.ts` (rest-api examples),
|
|
6
6
|
`test/integration/api-native.test.ts` (native-api examples, N…),
|
|
7
7
|
`test/integration/api-listen.test.ts` (streaming examples, L…), and
|
|
8
|
-
`test/integration/api-fanout.test.ts` (fan-out examples, F…)
|
|
8
|
+
`test/integration/api-fanout.test.ts` (fan-out examples, F…), and
|
|
9
|
+
`test/integration/retrieve-query.test.ts` (query grammar examples, QA…). If a line here is not
|
|
9
10
|
exercised by a test, the test file is wrong; if a line passes only with
|
|
10
11
|
`--validate=false`, the implementation is wrong (see
|
|
11
12
|
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md)).
|
|
@@ -199,10 +200,21 @@ command was rejected at run, not at validation. Ordinary output that merely
|
|
|
199
200
|
mentions a fault string (`:put "status: no such item appears in help"`) stays
|
|
200
201
|
`ok: true`.
|
|
201
202
|
|
|
202
|
-
###
|
|
203
|
+
### 16e. Wrong credentials: the device stage is not reached (GH#435)
|
|
203
204
|
|
|
204
205
|
```bash
|
|
205
|
-
centrs api $R
|
|
206
|
+
centrs api $R system/identity --username centrs-wrong --password centrs-wrong --json
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Envelope: `ok: false`, `error.code=transport/auth-failed`. `meta.validation`
|
|
210
|
+
has no `result`. Its device stage is `skipped` with
|
|
211
|
+
`reason: "not reached: transport/auth-failed"`, because RouterOS answered 401
|
|
212
|
+
before `/console/inspect` could give a verdict.
|
|
213
|
+
|
|
214
|
+
### 17. `--via rest-api --stream` is rejected
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
centrs api $R ip/address --stream --via rest-api --username $U --password $P
|
|
206
218
|
```
|
|
207
219
|
|
|
208
220
|
Envelope: `ok: false`, `error.code=transport/capability-unsupported` (REST cannot
|
|
@@ -233,7 +245,7 @@ centrs api $R interface/monitor-traffic -X POST -f interface=ether1 -f duration=
|
|
|
233
245
|
```
|
|
234
246
|
|
|
235
247
|
Envelope: `ok: true`, `data` is an **array** of `.section`-keyed records (one per
|
|
236
|
-
~second), `meta.via=rest-api`. This is an ordinary bounded call — no `--
|
|
248
|
+
~second), `meta.via=rest-api`. This is an ordinary bounded call — no `--stream`,
|
|
237
249
|
no NDJSON. `monitor-traffic` is a command (not `print`/`get`), so it is a
|
|
238
250
|
`POST` and write-classed (needs `--yes`); centrs never relaxes that to read-only.
|
|
239
251
|
(REST bounds it at the 60 s cap; native has no cap.)
|
|
@@ -265,6 +277,15 @@ key. Contrast example 13, where bare `--raw` skips the preflight entirely. This
|
|
|
265
277
|
pair is how you tell a RouterOS rejection from a centrs bug while staying on the
|
|
266
278
|
bare-passthrough surface.
|
|
267
279
|
|
|
280
|
+
### H1. `--host` stands in for `<router>` (#428)
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
centrs api --host $R system/identity --username $U --password $P --json
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`ok: true`, with `data.name` the router's identity. The lone positional is the
|
|
287
|
+
endpoint, and there is no CDB lookup.
|
|
288
|
+
|
|
268
289
|
## native-api (`--via native-api`)
|
|
269
290
|
|
|
270
291
|
The same contract over the binary API. Validation still runs through
|
|
@@ -359,26 +380,29 @@ surface the `as-string` console text as `data`.
|
|
|
359
380
|
|
|
360
381
|
## listen / `--stream` (native-api only)
|
|
361
382
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
383
|
+
`api` sends the command as typed (#402). Following changes is the menu's
|
|
384
|
+
`listen`, requested as a `<menu>/listen` endpoint, which implies `--stream` and
|
|
385
|
+
native-api. `--stream` delivers any command's replies incrementally: one NDJSON
|
|
386
|
+
envelope per wire reply (`!re` row or `!empty`), then a terminating summary.
|
|
387
|
+
`--listen` is removed (`usage/removed-flag`, unit- and smoke-tested with its
|
|
388
|
+
exact replacement).
|
|
365
389
|
|
|
366
|
-
### L1.
|
|
390
|
+
### L1. A `/listen` stream emits a change as an NDJSON frame, then a summary
|
|
367
391
|
|
|
368
392
|
```bash
|
|
369
|
-
centrs api $A ip/address --
|
|
393
|
+
centrs api $A ip/address/listen --count 1 --via native-api --port $API_PORT --username $U --password $P
|
|
370
394
|
```
|
|
371
395
|
|
|
372
396
|
While listening, the harness adds an address over REST. stdout is NDJSON: at least
|
|
373
|
-
one envelope frame for the new row (`meta.operation.stream.kind=frame
|
|
374
|
-
final summary envelope (`meta.operation.stream.kind=summary`,
|
|
375
|
-
`
|
|
376
|
-
|
|
397
|
+
one envelope frame for the new row (`meta.operation.stream.kind=frame`,
|
|
398
|
+
`reply=re`), then a final summary envelope (`meta.operation.stream.kind=summary`,
|
|
399
|
+
`meta.operation.stream.stopReason=count-reached`,
|
|
400
|
+
`meta.operation.stream.rows>=1`). Exit code 0 (successful local count stop).
|
|
377
401
|
|
|
378
402
|
### L2. A deletion frame carries `.dead`
|
|
379
403
|
|
|
380
404
|
```bash
|
|
381
|
-
centrs api $A ip/address --
|
|
405
|
+
centrs api $A ip/address/listen --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
382
406
|
```
|
|
383
407
|
|
|
384
408
|
The harness removes a pre-seeded address; an emitted frame's record carries
|
|
@@ -397,11 +421,14 @@ No `--stream` / `--via` given; the `/listen` endpoint infers `--stream` and
|
|
|
397
421
|
### L4. Bounded `--duration` reports its stop reason
|
|
398
422
|
|
|
399
423
|
```bash
|
|
400
|
-
centrs api $A ip/address --
|
|
424
|
+
centrs api $A ip/address/listen --duration 2s --via native-api --port $API_PORT --username $U --password $P
|
|
401
425
|
```
|
|
402
426
|
|
|
403
427
|
With no change during the window, the stream ends after ~2 s with a summary
|
|
404
|
-
envelope whose `
|
|
428
|
+
envelope whose `meta.operation.stream.stopReason=duration-elapsed`, `rows=0`,
|
|
429
|
+
`empty=1`. Exit code 0. RouterOS answers the cancel of a listen that sent
|
|
430
|
+
nothing with interrupted, `!empty`, `!done`: that `!empty` is the one frame,
|
|
431
|
+
with `reply=empty`, `data=null` and `afterStop: true`.
|
|
405
432
|
|
|
406
433
|
### L5. A router that ignores `/cancel` cannot hold the process open
|
|
407
434
|
|
|
@@ -424,6 +451,145 @@ loopback native-API peer that ignores it, as a real CLI process:
|
|
|
424
451
|
chatty peer, trap without `!done`, close after cancel) are in
|
|
425
452
|
`test/unit/native-api.test.ts`.
|
|
426
453
|
|
|
454
|
+
### L6. Finite ping completes naturally
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
centrs api $A tool/ping -X POST -f address=10.0.2.2 -f count=3 --stream --yes --via native-api --port $API_PORT --username $U --password $P
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Three reply frames, then a successful summary with `meta.operation.stream.stopReason=completed`.
|
|
461
|
+
The device's `count=3` completes the command; no centrs `--count` is needed.
|
|
462
|
+
|
|
463
|
+
### L7. Monitor replies preserve `.section`
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
centrs api $A interface/monitor-traffic -X POST -f interface=ether1 -f duration=2s --stream --yes --via native-api --port $API_PORT --username $U --password $P
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
One or more rows carrying device `.section` values, then `completed`.
|
|
470
|
+
|
|
471
|
+
### L8. Explicit print interval streams instead of listening
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
centrs api $A system/resource/print -X POST -f interval=1 --stream --count 2 --via native-api --port $API_PORT --username $U --password $P
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Two reply frames and a `count-reached` summary. POST print stays read-only.
|
|
478
|
+
|
|
479
|
+
### L9. Projection is sent as typed; the tip says what it hides
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
centrs api $A ip/address/listen --proplist address --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
483
|
+
centrs api $A ip/address/listen --proplist address,.id,.dead --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The harness deletes a seeded address during each. With `address` alone no
|
|
487
|
+
frame carries `.dead` for it, and the leading notice and the summary carry
|
|
488
|
+
`tip/follow-proplist`.
|
|
489
|
+
With `address,.id,.dead` the delete frame is `{ ".id", ".dead": "true" }` and
|
|
490
|
+
there is no tip.
|
|
491
|
+
|
|
492
|
+
### L10. A filtered listen is sent as typed, with a tip
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
centrs api $A ip/address/listen --query interface=ether1 --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
The harness deletes a seeded `ether1` address while listening. RouterOS sends
|
|
499
|
+
nothing for it: the only frame is the cancellation `!empty` (`afterStop`).
|
|
500
|
+
The first line is a `notice` with `tip/filtered-follow`; the summary is `ok`
|
|
501
|
+
and repeats it. `--raw-query` is sent
|
|
502
|
+
the same way.
|
|
503
|
+
|
|
504
|
+
### L11. Command attributes are validated
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
centrs api $A tool/ping -X POST -f no-such-arg=x --stream --yes --via native-api --port $API_PORT --username $U --password $P
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
`validation/unknown-attribute`; no ping stream starts.
|
|
511
|
+
|
|
512
|
+
### L12. Streaming keeps the mutator confirmation gate
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
centrs api $A system/license/renew -X POST --stream --via native-api --port $API_PORT --username $U --password $P
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Non-interactively, `usage/confirmation-required`; no renew operation is sent.
|
|
519
|
+
|
|
520
|
+
### L13. Mutation streams retain method mapping and terminal `ret`
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
centrs api $A ip/address -X PUT -f address=198.51.100.34/32 -f interface=ether1 --stream --yes --via native-api --port $API_PORT --username $U --password $P
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
A successful zero-row summary with `data.done.ret` naming the created address.
|
|
527
|
+
The harness retrieves that ID to check its address, then streams DELETE on that
|
|
528
|
+
ID and requires natural completion. Neither mutation becomes a listen.
|
|
529
|
+
|
|
530
|
+
### L14. Streamed `/execute` runtime errors fail the terminal summary
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
centrs api $A execute -X POST --stream --via native-api --yes --json -f 'script=/ip/service/set www-ssl certificate=nope' --port $API_PORT --username $U --password $P
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
With validation enabled and no `nope` certificate, syntax validation passes but
|
|
537
|
+
RouterOS rejects the value at runtime. The sole terminal envelope is `ok: false`,
|
|
538
|
+
`routeros/invalid-value`, and `meta.operation.stream.stopReason=routeros-error`; the CLI exits nonzero.
|
|
539
|
+
Ordinary stdout such as `:put "status: no such item appears in ordinary output"`
|
|
540
|
+
still completes successfully with its text in `data.done.ret`.
|
|
541
|
+
|
|
542
|
+
### L15. An addressed listen reports its deletion; a projection hides it
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
centrs api $A "ip/address/$ID/listen" --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
546
|
+
centrs api $A "ip/address/$ID/listen" --duration 3s --proplist address --via native-api --port $API_PORT --username $U --password $P
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
For each variant the harness seeds an address, starts the addressed listen
|
|
550
|
+
(`?.id=$ID`), and deletes it after the listening barrier. Without projection
|
|
551
|
+
the stream emits `.id=$ID` with `.dead=true` and no tip. With `--proplist
|
|
552
|
+
address` RouterOS strips both, so the delete is one `!re` with no attributes
|
|
553
|
+
(`data={}`) and the summary carries `tip/follow-proplist`.
|
|
554
|
+
|
|
555
|
+
### L16. POST print preserves query/projection in either delivery mode
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
centrs api $A ip/address/print -X POST --query address=198.51.100.34/32 --proplist address --via native-api --port $API_PORT --username $U --password $P
|
|
559
|
+
centrs api $A ip/address/print -X POST --query address=198.51.100.34/32 --proplist address --stream --via native-api --port $API_PORT --username $U --password $P
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
The harness seeds the L13 address. Both modes return only that address and only
|
|
563
|
+
the requested `address` field; the streamed form then completes naturally. The
|
|
564
|
+
one-shot REST equivalent is also checked, using the same query/projection.
|
|
565
|
+
|
|
566
|
+
### L17. A GET stream is one literal print
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
centrs api $A ip/address --stream --via native-api --port $API_PORT --username $U --password $P
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
The print completes on its own (`stopReason=completed`) with one `re` frame
|
|
573
|
+
per row (`rows` equals a one-shot GET's row count); a leading notice and the
|
|
574
|
+
summary carry `tip/stream-print` naming `ip/address/listen`. It never becomes a listen.
|
|
575
|
+
|
|
576
|
+
### L18. Zero-row ticks are `!empty` frames and do not use up `--count`
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
centrs api $A ip/firewall/raw/print -X POST -f interval=1 --stream --count 1 --duration 2500ms --via native-api --port $API_PORT --username $U --password $P
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
On the CHR's empty raw table, each tick is a frame with `reply=empty` and
|
|
583
|
+
`data=null`; at least two arrive. They do not count toward `--count 1`, so the
|
|
584
|
+
stream ends on `duration-elapsed` with `rows=0`.
|
|
585
|
+
|
|
586
|
+
Loopback regressions in `test/integration/api-stream-command.test.ts` also
|
|
587
|
+
exercise natural/empty completion, `!empty` frames, row-only `--count`,
|
|
588
|
+
`afterStop`, the stream tips, `--format ndjson`, terminal `ret`,
|
|
589
|
+
first/midstream trap, unsolicited interruption, fatal/close, and nonzero
|
|
590
|
+
process exit with the failure summary on stdout. Device-dependent examples
|
|
591
|
+
L6–L18 run in `test/integration/api-listen.test.ts` with validation enabled.
|
|
592
|
+
|
|
427
593
|
## fanout (multi-target, F…)
|
|
428
594
|
|
|
429
595
|
These run against a CDB fixture with two records sharing group `$G`: record 0 is
|
|
@@ -432,7 +598,7 @@ the live CHR (comment `board=chr`), record 1 is an unreachable host (comment
|
|
|
432
598
|
(`data = { summary, targets[] }`; outer `ok` = orchestration success; per-target
|
|
433
599
|
failures are inner `ok:false`), with the granular exit code (0 all-ok / 2 partial /
|
|
434
600
|
1 all-failed or orchestration error). See `test/integration/api-fanout.test.ts`.
|
|
435
|
-
The `--raw`/`--
|
|
601
|
+
The `--raw`/`--stream` + fan-out guards are network-free and validated in
|
|
436
602
|
`test/integration/cli-smoke.test.ts`.
|
|
437
603
|
|
|
438
604
|
### F1. `--group` fans a GET out; a dead target is an inner failure
|
|
@@ -525,6 +691,28 @@ target fails deterministically: inner `ok:false` with
|
|
|
525
691
|
`error.code=target/unresolved` (never a `transport/dns` attempt on `"__default__"`).
|
|
526
692
|
`data.summary = { total: 1, ok: 0, failed: 1 }`. Exit code 1 (every target failed).
|
|
527
693
|
|
|
694
|
+
## `--query` grammar (#397)
|
|
695
|
+
|
|
696
|
+
`--query` takes the `print where` grammar shared with `retrieve --query`. The
|
|
697
|
+
test seeds `qa-397` address-list rows, one of them disabled with comment `x>y`.
|
|
698
|
+
|
|
699
|
+
### QA1. A bare boolean is `=yes`, on either transport
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
centrs api $R ip/firewall/address-list --query 'list=qa-397 and disabled' --proplist address --username $U --password $P
|
|
703
|
+
centrs api $A ip/firewall/address-list --via native-api --query 'list=qa-397 and disabled' --proplist address --port $API_PORT --username $U --password $P
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Both return only the disabled row's address.
|
|
707
|
+
|
|
708
|
+
### QA2. A value containing an operator stays one value
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
centrs api $R ip/firewall/address-list --query 'comment=x>y' --proplist address --username $U --password $P
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Returns the row whose comment is `x>y`; the query word sent is `comment=x>y`.
|
|
715
|
+
|
|
528
716
|
## quickchr targets (#134)
|
|
529
717
|
|
|
530
718
|
`$NAME` is the machine name of a running quickchr-managed CHR. Host/port/auth
|
|
@@ -65,7 +65,15 @@ inert. (Earlier drafts said "the comment is not a lookup key" — superseded.)
|
|
|
65
65
|
When `<router>` matches an entry (by `target` or a lookup key), credentials and
|
|
66
66
|
other CDB-resident fields load from that entry. If nothing matches, the call
|
|
67
67
|
still proceeds but `--username` / `--password` (or `CENTRS_USERNAME` /
|
|
68
|
-
`CENTRS_PASSWORD`, or a `__default__` record) become mandatory.
|
|
68
|
+
`CENTRS_PASSWORD`, or a `__default__` record) become mandatory. The one
|
|
69
|
+
exception is a **pinned** CDB (`--cdb-file`, `CENTRS_CDB_FILE`,
|
|
70
|
+
`CENTRS_CDB_PASSWORD`, or a `centrs.env` CDB path) with no `__default__`
|
|
71
|
+
record: a DNS name or identity that matches nothing fails closed with
|
|
72
|
+
`cdb/not-found-target`, because it can't be told apart from a mistyped record.
|
|
73
|
+
An address that matches nothing still proceeds as a literal host: an IPv4 or
|
|
74
|
+
IPv6 literal (optionally with a port, IPv6 as `[…]:port`), an `http(s)://` URL,
|
|
75
|
+
or a MAC. On MCP every target must still be registered
|
|
76
|
+
(`cdb/target-not-registered`).
|
|
69
77
|
|
|
70
78
|
### Ambiguity
|
|
71
79
|
|
|
@@ -326,8 +334,8 @@ runs produce stable diffs.
|
|
|
326
334
|
- A multi-target **write** is gated by `--yes`, confirmed once up front (not per
|
|
327
335
|
target); without it the error/tip names the blast radius (how many routers).
|
|
328
336
|
`--force` stays scoped to destructive `devices` CDB mutations.
|
|
329
|
-
- Commands that are not safe to fan out (`terminal`, and `api --stream`
|
|
330
|
-
|
|
337
|
+
- Commands that are not safe to fan out (`terminal`, and `api --stream` or a
|
|
338
|
+
`/listen` endpoint) reject a multi-target selection with `usage/fanout-not-supported`.
|
|
331
339
|
|
|
332
340
|
## Subcommands
|
|
333
341
|
|
|
@@ -6,7 +6,7 @@ CLI-shaped commands — there is no separate `update` command.
|
|
|
6
6
|
|
|
7
7
|
Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
|
|
8
8
|
`docs/MATRIX.md` and `commands/execute/examples.md`, examples 1–11 over REST,
|
|
9
|
-
12–19 over the native API, 20–22 over mac-telnet, and S1–
|
|
9
|
+
12–19 over the native API, 20–22 over mac-telnet, and S1–S5 over ssh, green via
|
|
10
10
|
`bun run test:integration`), including multi-target fan-out (see **Target
|
|
11
11
|
selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
12
12
|
`not-started`. SNMP is retrieve-only and rejects `execute`.
|
|
@@ -136,14 +136,28 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
136
136
|
mac-telnet. Callers that want IP-level execution may opt into ARP-based
|
|
137
137
|
MAC → IP resolution with `--resolve arp` (plus `--via native-api`/`rest-api`)
|
|
138
138
|
before protocol selection.
|
|
139
|
+
- An explicit `http(s)://host[:port]` target (positional, `--host`, or
|
|
140
|
+
`CENTRS_HOST`) names the REST service: with no `via` pinned anywhere,
|
|
141
|
+
auto-selection picks `rest-api` and keeps the URL's scheme and port
|
|
142
|
+
(`meta.settings.via` source `default` / `via:target-url`). Otherwise
|
|
143
|
+
auto-selection picks native-api. On the IP transports (native-api, REST,
|
|
144
|
+
ssh), a port typed into the target (`host:port`, `http://host:port`,
|
|
145
|
+
including an explicit `:80`/`:443`) is honored. The port settings ladder
|
|
146
|
+
(`--port`, `CENTRS_PORT`, a CDB `port=` comment, config) wins over it, and
|
|
147
|
+
the transport default comes last. The port carried by a matched CDB
|
|
148
|
+
record's `target` is not applied to native-api. mac-telnet addresses the MAC,
|
|
149
|
+
so its `--host`/`--port` are only the UDP delivery endpoint.
|
|
139
150
|
- When the resolved transport is mac-telnet and **no** username or password is
|
|
140
151
|
resolved from any source, execute emits a `tip/mac-telnet-no-credentials`
|
|
141
152
|
pointer: mac-telnet logs in with RouterOS credentials over **MTWEI** (MD5 is
|
|
142
153
|
refused by current RouterOS), so an empty login is rejected at connect time.
|
|
143
154
|
It is a tip, not an error — the call is still attempted.
|
|
144
155
|
- Default output is human-readable `text`; pass `--json`/`--format json`
|
|
145
|
-
(or set `CENTRS_FORMAT=json`) for the structured envelope.
|
|
146
|
-
|
|
156
|
+
(or set `CENTRS_FORMAT=json`) for the structured envelope. In text mode every
|
|
157
|
+
command renders errors the same way (`formatCentrsErrorText`): `[code] summary`,
|
|
158
|
+
then `At:` (RouterOS position or offline byte span), `Device said:` (RouterOS's
|
|
159
|
+
own words), `Fix:` and `Details:` when present. `--verbose` adds the full
|
|
160
|
+
`context`.
|
|
147
161
|
- CDB comment-kv metadata may provide per-target `via`, `port`, and `ssh-key`
|
|
148
162
|
overrides; CLI/API arguments still win.
|
|
149
163
|
- RoMON and WinBox Terminal are lower priority than mac-telnet until their
|
|
@@ -189,6 +203,10 @@ with the first SSH consumer, `transfer`/sftp; `execute / ssh` (this command) and
|
|
|
189
203
|
`terminal / ssh` followed as separate transports over the same host-`ssh`
|
|
190
204
|
plumbing (`src/protocols/ssh.ts`) — all three SSH cells are `CHR-passed`.
|
|
191
205
|
|
|
206
|
+
A selected key adds `IdentitiesOnly=yes`, preventing fallback to unrelated
|
|
207
|
+
agent keys. Explicit OpenSSH `IdentityFile` configuration still applies; see
|
|
208
|
+
[`terminal` → SSH key selection](../terminal/README.md#ssh-key-selection).
|
|
209
|
+
|
|
192
210
|
Over SSH — like mac-telnet — `execute` is a **console transport**: it runs a
|
|
193
211
|
CLI line and returns text (`SshExecClient`, one `ssh user@host "<command>"` per
|
|
194
212
|
command; validation reuses the same `:put [:parse …]` gate), so structured
|
|
@@ -229,6 +247,16 @@ many tokens):
|
|
|
229
247
|
the command; no `--` needed). One `--quickchr` stays single-target; repeating
|
|
230
248
|
it fans out; mixing it with positionals or CDB selectors is
|
|
231
249
|
`usage/conflicting-flags` (constitution: resolution providers).
|
|
250
|
+
- `execute --host http://192.0.2.10:8080 '/system/resource/print'` (or
|
|
251
|
+
`execute --host <host|url> -- <command>`) — the `--host` **flag** stands in
|
|
252
|
+
for a missing `<target>`: a lone positional is the command, and nothing
|
|
253
|
+
before `--` is fine. There is no `<target>`, so there is no CDB lookup and no
|
|
254
|
+
record credential can be picked up; auth comes from flags, env or config.
|
|
255
|
+
Two or more positionals without `--` keep the legacy split, so
|
|
256
|
+
`execute <mac> <command> --via mac-telnet --host <udp-host>` (the mac-telnet
|
|
257
|
+
delivery override) is unchanged. `CENTRS_HOST` never changes positional
|
|
258
|
+
parsing: `execute '<command>'` with it set still reads the lone positional as
|
|
259
|
+
the target.
|
|
232
260
|
|
|
233
261
|
Because `execute` is the primary **write** path, a write-shaped fan-out is gated
|
|
234
262
|
by **`--yes`**, confirmed **once** up front; without it
|