@tikoci/centrs 0.1.6 → 0.1.8-next.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/CHANGELOG.md +342 -0
- package/GLOSSARY.txt +32 -0
- package/README.md +10 -6
- package/SECURITY.md +1 -1
- package/commands/AGENTS.md +2 -1
- package/commands/api/AGENTS.md +55 -30
- package/commands/api/README.md +135 -42
- package/commands/api/examples.md +275 -19
- package/commands/devices/README.md +21 -4
- package/commands/devices/examples.md +33 -2
- package/commands/execute/README.md +60 -18
- package/commands/execute/examples.md +224 -0
- package/commands/explain/README.md +42 -5
- package/commands/mcp/README.md +2 -1
- package/commands/retrieve/README.md +348 -4
- package/commands/retrieve/examples.md +442 -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 +29 -21
- package/docs/CONSTITUTION.md +41 -16
- package/docs/MATRIX.md +38 -1
- package/docs/errors/README.md +3 -1
- package/docs/errors/cdb/create-failed.md +20 -0
- package/docs/errors/cdb/file-exists.md +23 -0
- package/docs/errors/input/invalid-query.md +36 -0
- package/docs/errors/input/invalid-target.md +25 -3
- 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/routeros/invalid-value.md +3 -2
- package/docs/errors/settings/invalid-timeout.md +6 -0
- package/docs/errors/transport/cancel-unacknowledged.md +24 -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 +603 -194
- package/src/btest.ts +19 -17
- package/src/cli/api.ts +126 -34
- package/src/cli/devices.ts +16 -3
- package/src/cli/execute.ts +18 -5
- package/src/cli/retrieve.ts +216 -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 +70 -1
- package/src/core/execute-command.ts +313 -6
- package/src/core/inspect.ts +65 -27
- package/src/core/query.ts +508 -0
- package/src/core/routeros-errors.ts +67 -1
- package/src/data/winbox-cdb-write.ts +22 -8
- package/src/devices.ts +148 -37
- package/src/discover.ts +7 -8
- package/src/errors.ts +116 -7
- package/src/execute-fanout.ts +14 -9
- package/src/execute.ts +128 -116
- package/src/explain/catalog.ts +64 -16
- package/src/explain/menus.ts +27 -6
- package/src/explain.ts +6 -6
- package/src/index.ts +31 -1
- package/src/mcp/tools.ts +10 -15
- package/src/protocols/adapter.ts +235 -118
- package/src/protocols/btest-session.ts +3 -2
- package/src/protocols/endpoint.ts +10 -0
- package/src/protocols/mac-telnet-console.ts +12 -7
- package/src/protocols/native-api.ts +252 -73
- 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 +84 -18
- 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 +741 -109
- 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
|
|
|
@@ -112,15 +129,18 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
|
|
|
112
129
|
does) answers normally on the same build. See the 7.12.2 finding in
|
|
113
130
|
`AGENTS.md` (GH#343).
|
|
114
131
|
- **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
|
|
115
|
-
so
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`
|
|
121
|
-
the
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
so structured inspect is `not-applicable` (`meta.validation.semantic`). It
|
|
133
|
+
follows the CLI-string pipeline instead: offline analysis, then live
|
|
134
|
+
`:put [:parse ...]`, then run. An offline rejection is still a byte span with
|
|
135
|
+
no connection; an offline pass or abstention continues to the device stage.
|
|
136
|
+
`meta.validation.stages[]` reports both decisions. A missing or blank `script`
|
|
137
|
+
is still `input/invalid-command`, not a syntax rejection. Validation passing
|
|
138
|
+
is not the run succeeding: `as-string` makes the console's own text the reply
|
|
139
|
+
body, so a runtime rejection arrives as an HTTP-200 `ret`. `/execute` output
|
|
140
|
+
is normalized through the same grounded result table `execute` uses, so
|
|
141
|
+
`no such item` fails the envelope with a `routeros/*` code while the stages
|
|
142
|
+
stay `passed`. Ordinary output that merely contains a fault string is
|
|
143
|
+
unaffected — the result matchers are anchored, not substring.
|
|
124
144
|
- Structured path requests are **not** offline-gated. The input is a path plus a
|
|
125
145
|
body, not a CLI string, and an offline *path* gate collides with GH#211's
|
|
126
146
|
unlisted-path decision. On the success path `meta.validation.stages[]` is
|
|
@@ -168,27 +188,100 @@ the RouterOS error payload to stderr with a nonzero exit; a centrs-side failure
|
|
|
168
188
|
with no RouterOS response prints a compact `{code,message}` to stderr. Exit code is
|
|
169
189
|
`0` iff `ok`.
|
|
170
190
|
|
|
171
|
-
## Multi-frame results
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`
|
|
191
|
-
|
|
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)).
|
|
192
285
|
|
|
193
286
|
## MCP (deferred — forward guidance)
|
|
194
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)).
|
|
@@ -156,7 +157,8 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
|
|
|
156
157
|
|
|
157
158
|
Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
|
|
158
159
|
`meta.validation.semantic=not-applicable` (script, not a path) and
|
|
159
|
-
`meta.validation.stages` is `offline: passed` then `device:
|
|
160
|
+
`meta.validation.stages` is `offline: passed` then live `device: passed` from
|
|
161
|
+
`:put [:parse ...]`.
|
|
160
162
|
|
|
161
163
|
### 16b. The `/execute` script is gated offline (GH#354)
|
|
162
164
|
|
|
@@ -173,10 +175,46 @@ Envelope: `ok: false`, `error.code=validation/syntax`,
|
|
|
173
175
|
`error.context.span={"start":5,"end":6}`, and **not** a `transport/*` code.
|
|
174
176
|
`meta.validation.stages` is `offline: failed` then `device: skipped`.
|
|
175
177
|
|
|
176
|
-
###
|
|
178
|
+
### 16c. Offline pass continues to live `:parse`
|
|
177
179
|
|
|
178
180
|
```bash
|
|
179
|
-
centrs api $R ip/address
|
|
181
|
+
centrs api $R execute -X POST -f script='/ip/address/add no-such-arg=x' --username $U --password $P --yes
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Envelope: `ok: false` with a device validation code
|
|
185
|
+
(`validation/unknown-attribute`, or `validation/syntax` on older RouterOS), and
|
|
186
|
+
`meta.validation.stages` is `offline: passed` then `device: failed`. Offline
|
|
187
|
+
acceptance remains `runtimeAcceptance: not-proven`; it never suppresses the live
|
|
188
|
+
stage.
|
|
189
|
+
|
|
190
|
+
### 16d. A runtime rejection in `/execute` output is not `ok: true`
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
centrs api $R execute -X POST -f script='/ip/service/set www-ssl certificate=nope' --username $U --password $P --yes
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Envelope: `ok: false`, `error.code=routeros/invalid-value`. `as-string` makes
|
|
197
|
+
the console's own text the reply body, so RouterOS carries this rejection in an
|
|
198
|
+
HTTP-200 `ret`; the validation gate stays `passed` because it did pass — the
|
|
199
|
+
command was rejected at run, not at validation. Ordinary output that merely
|
|
200
|
+
mentions a fault string (`:put "status: no such item appears in help"`) stays
|
|
201
|
+
`ok: true`.
|
|
202
|
+
|
|
203
|
+
### 16e. Wrong credentials: the device stage is not reached (GH#435)
|
|
204
|
+
|
|
205
|
+
```bash
|
|
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
|
|
180
218
|
```
|
|
181
219
|
|
|
182
220
|
Envelope: `ok: false`, `error.code=transport/capability-unsupported` (REST cannot
|
|
@@ -207,7 +245,7 @@ centrs api $R interface/monitor-traffic -X POST -f interface=ether1 -f duration=
|
|
|
207
245
|
```
|
|
208
246
|
|
|
209
247
|
Envelope: `ok: true`, `data` is an **array** of `.section`-keyed records (one per
|
|
210
|
-
~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`,
|
|
211
249
|
no NDJSON. `monitor-traffic` is a command (not `print`/`get`), so it is a
|
|
212
250
|
`POST` and write-classed (needs `--yes`); centrs never relaxes that to read-only.
|
|
213
251
|
(REST bounds it at the 60 s cap; native has no cap.)
|
|
@@ -239,6 +277,15 @@ key. Contrast example 13, where bare `--raw` skips the preflight entirely. This
|
|
|
239
277
|
pair is how you tell a RouterOS rejection from a centrs bug while staying on the
|
|
240
278
|
bare-passthrough surface.
|
|
241
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
|
+
|
|
242
289
|
## native-api (`--via native-api`)
|
|
243
290
|
|
|
244
291
|
The same contract over the binary API. Validation still runs through
|
|
@@ -308,30 +355,54 @@ stdout is the bare rest-style array (string values); exit code 0.
|
|
|
308
355
|
centrs api $A execute -X POST -f script=':put [/system/identity/get name]' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
309
356
|
```
|
|
310
357
|
|
|
311
|
-
Envelope: `ok: true`, `data` contains the CHR identity
|
|
358
|
+
Envelope: `ok: true`, `data` contains the CHR identity and validation reports
|
|
359
|
+
offline plus live `:parse` stages passed.
|
|
360
|
+
|
|
361
|
+
### N8b. Native `/execute` also rejects at live `:parse`
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
centrs api $A execute -X POST -f script='/ip/address/add no-such-arg=x' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Envelope: `ok: false`; the accepted version-specific validation code is
|
|
368
|
+
`validation/unknown-attribute` or `validation/syntax`, with stages
|
|
369
|
+
`offline: passed`, `device: failed`.
|
|
370
|
+
|
|
371
|
+
### N8c. Native `/execute` runtime rejection fails the envelope
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
centrs api $A execute -X POST -f script='/ip/service/set www-ssl certificate=nope' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Envelope: `ok: false`, `error.code=routeros/invalid-value`, with the validation
|
|
378
|
+
stages still `passed` — the same normalization as REST, since both adapters
|
|
379
|
+
surface the `as-string` console text as `data`.
|
|
312
380
|
|
|
313
381
|
## listen / `--stream` (native-api only)
|
|
314
382
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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).
|
|
318
389
|
|
|
319
|
-
### L1.
|
|
390
|
+
### L1. A `/listen` stream emits a change as an NDJSON frame, then a summary
|
|
320
391
|
|
|
321
392
|
```bash
|
|
322
|
-
centrs api $A ip/address --
|
|
393
|
+
centrs api $A ip/address/listen --count 1 --via native-api --port $API_PORT --username $U --password $P
|
|
323
394
|
```
|
|
324
395
|
|
|
325
396
|
While listening, the harness adds an address over REST. stdout is NDJSON: at least
|
|
326
|
-
one envelope frame for the new row (`meta.operation.stream.kind=frame
|
|
327
|
-
final summary envelope (`meta.operation.stream.kind=summary`,
|
|
328
|
-
`
|
|
329
|
-
|
|
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).
|
|
330
401
|
|
|
331
402
|
### L2. A deletion frame carries `.dead`
|
|
332
403
|
|
|
333
404
|
```bash
|
|
334
|
-
centrs api $A ip/address --
|
|
405
|
+
centrs api $A ip/address/listen --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
335
406
|
```
|
|
336
407
|
|
|
337
408
|
The harness removes a pre-seeded address; an emitted frame's record carries
|
|
@@ -350,11 +421,174 @@ No `--stream` / `--via` given; the `/listen` endpoint infers `--stream` and
|
|
|
350
421
|
### L4. Bounded `--duration` reports its stop reason
|
|
351
422
|
|
|
352
423
|
```bash
|
|
353
|
-
centrs api $A ip/address --
|
|
424
|
+
centrs api $A ip/address/listen --duration 2s --via native-api --port $API_PORT --username $U --password $P
|
|
354
425
|
```
|
|
355
426
|
|
|
356
427
|
With no change during the window, the stream ends after ~2 s with a summary
|
|
357
|
-
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`.
|
|
432
|
+
|
|
433
|
+
### L5. A router that ignores `/cancel` cannot hold the process open
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
centrs api $A ip/address --stream --duration 200ms --timeout 1s --via native-api --port $API_PORT --username $U --password $P
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
A stream stopped by `--duration` or Ctrl-C sends `/cancel` and waits up to
|
|
440
|
+
`--timeout` for the router's acknowledgement (the interrupted `!trap` and
|
|
441
|
+
`!done`). If none arrives, centrs closes the session itself. The summary still
|
|
442
|
+
reports its stop reason (`duration-elapsed` or `interrupted`), exit code is 0,
|
|
443
|
+
and `warnings` carries `transport/cancel-unacknowledged`. A router that answers
|
|
444
|
+
inside the window gets no warning. `--count` sends `/cancel` without waiting,
|
|
445
|
+
so it exits at once and never warns (#385).
|
|
446
|
+
|
|
447
|
+
RouterOS on CHR always acknowledges `/cancel`, so this example runs against a
|
|
448
|
+
loopback native-API peer that ignores it, as a real CLI process:
|
|
449
|
+
`test/integration/api-stream-cancel.test.ts` covers `--duration`, SIGINT,
|
|
450
|
+
`--count` and the cooperative control. The session-level cases (silent peer,
|
|
451
|
+
chatty peer, trap without `!done`, close after cancel) are in
|
|
452
|
+
`test/unit/native-api.test.ts`.
|
|
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.
|
|
358
592
|
|
|
359
593
|
## fanout (multi-target, F…)
|
|
360
594
|
|
|
@@ -364,7 +598,7 @@ the live CHR (comment `board=chr`), record 1 is an unreachable host (comment
|
|
|
364
598
|
(`data = { summary, targets[] }`; outer `ok` = orchestration success; per-target
|
|
365
599
|
failures are inner `ok:false`), with the granular exit code (0 all-ok / 2 partial /
|
|
366
600
|
1 all-failed or orchestration error). See `test/integration/api-fanout.test.ts`.
|
|
367
|
-
The `--raw`/`--
|
|
601
|
+
The `--raw`/`--stream` + fan-out guards are network-free and validated in
|
|
368
602
|
`test/integration/cli-smoke.test.ts`.
|
|
369
603
|
|
|
370
604
|
### F1. `--group` fans a GET out; a dead target is an inner failure
|
|
@@ -457,6 +691,28 @@ target fails deterministically: inner `ok:false` with
|
|
|
457
691
|
`error.code=target/unresolved` (never a `transport/dns` attempt on `"__default__"`).
|
|
458
692
|
`data.summary = { total: 1, ok: 0, failed: 1 }`. Exit code 1 (every target failed).
|
|
459
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
|
+
|
|
460
716
|
## quickchr targets (#134)
|
|
461
717
|
|
|
462
718
|
`$NAME` is the machine name of a running quickchr-managed CHR. Host/port/auth
|