@tikoci/centrs 0.1.1 → 0.1.3
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 +107 -2
- package/GLOSSARY.txt +87 -0
- package/README.md +25 -9
- package/SECURITY.md +3 -3
- package/commands/AGENTS.md +3 -2
- package/commands/api/AGENTS.md +212 -0
- package/commands/api/README.md +177 -0
- package/commands/api/examples.md +428 -0
- package/commands/btest/README.md +56 -31
- package/commands/btest/examples.md +35 -10
- package/commands/check/README.md +371 -53
- package/commands/check/examples.md +390 -0
- package/commands/devices/README.md +177 -37
- package/commands/devices/examples.md +148 -0
- package/commands/execute/README.md +48 -7
- package/commands/execute/examples.md +90 -6
- package/commands/retrieve/README.md +37 -51
- package/commands/retrieve/examples.md +40 -9
- package/commands/settings/README.md +286 -22
- package/commands/settings/examples.md +408 -0
- package/commands/terminal/README.md +6 -0
- package/commands/transfer/README.md +32 -2
- package/commands/transfer/examples.md +66 -0
- package/docs/CONSTITUTION.md +40 -15
- package/docs/MATRIX.md +69 -142
- package/docs/errors/README.md +2 -1
- package/docs/errors/cdb/empty-selection.md +19 -0
- package/docs/errors/input/incomplete-gps.md +9 -0
- package/docs/errors/input/invalid-altitude.md +9 -0
- package/docs/errors/input/invalid-bbox.md +9 -0
- package/docs/errors/input/invalid-coordinate.md +9 -0
- package/docs/errors/input/invalid-radius.md +9 -0
- package/docs/errors/internal/settings-failed.md +9 -0
- package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
- package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
- package/docs/errors/settings/consequential-value.md +26 -0
- package/docs/errors/settings/invalid-boolean.md +21 -3
- package/docs/errors/settings/invalid-format.md +18 -3
- package/docs/errors/settings/invalid-integer.md +18 -3
- package/docs/errors/settings/invalid-timeout.md +20 -3
- package/docs/errors/settings/invalid-via.md +22 -3
- package/docs/errors/settings/reserved-key.md +29 -0
- package/docs/errors/settings/skip-env-file-active.md +25 -0
- package/docs/errors/settings/unknown-key.md +25 -0
- package/docs/errors/usage/fanout-not-supported.md +16 -0
- package/docs/errors/usage/invalid-method.md +23 -0
- package/package.json +9 -9
- package/src/api-fanout.ts +454 -0
- package/src/api.ts +1627 -0
- package/src/btest.ts +61 -1
- package/src/cli/api.ts +660 -0
- package/src/cli/btest.ts +20 -2
- package/src/cli/common.ts +75 -0
- package/src/cli/devices.ts +235 -6
- package/src/cli/discover.ts +3 -1
- package/src/cli/execute.ts +107 -6
- package/src/cli/mcp.ts +8 -1
- package/src/cli/retrieve.ts +135 -90
- package/src/cli/selection.ts +200 -0
- package/src/cli/settings.ts +286 -0
- package/src/cli/terminal.ts +22 -7
- package/src/cli/transfer.ts +171 -47
- package/src/cli.ts +29 -0
- package/src/core/error-catalog.ts +69 -0
- package/src/core/fanout.ts +301 -0
- package/src/core/inspect.ts +170 -0
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/execute-fanout.ts +438 -0
- package/src/execute.ts +134 -70
- package/src/index.ts +166 -9
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +357 -9
- package/src/protocols/btest-session.ts +279 -46
- package/src/protocols/btest.ts +6 -2
- package/src/protocols/index.ts +11 -5
- package/src/protocols/native-api.ts +223 -30
- package/src/resolver/cdb.ts +10 -2
- package/src/resolver/comment-kv.ts +58 -4
- package/src/resolver/config-file.ts +136 -0
- package/src/resolver/facts.ts +78 -0
- package/src/resolver/geo.ts +453 -0
- package/src/resolver/index.ts +46 -0
- package/src/resolver/mac.ts +12 -3
- package/src/resolver/selection.ts +304 -0
- package/src/resolver/settings.ts +54 -5
- package/src/resolver/target.ts +20 -2
- package/src/retrieve-fanout.ts +232 -268
- package/src/retrieve.ts +69 -94
- package/src/settings.ts +1078 -0
- package/src/terminal.ts +11 -1
- package/src/transfer-fanout.ts +527 -0
- package/src/transfer.ts +61 -11
- package/commands/stream/README.md +0 -70
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# api
|
|
2
|
+
|
|
3
|
+
A structured RouterOS **API passthrough**, modeled on `gh api`. You give it a
|
|
4
|
+
REST-style endpoint path and an HTTP method; centrs fills in credentials from the
|
|
5
|
+
CDB / env / flags, validates the request through `/console/inspect`, runs the
|
|
6
|
+
single REST or native-API operation, and returns the result in the standard
|
|
7
|
+
envelope.
|
|
8
|
+
|
|
9
|
+
Status: `CHR-passed` over `rest-api` and `native-api`, including multi-target
|
|
10
|
+
fan-out and open-ended `--stream` follow (native-api only), per
|
|
11
|
+
`docs/MATRIX.md`. This file describes intent and flags; the matrix holds the
|
|
12
|
+
cell states. Load-bearing rules — envelope, errors, settings precedence,
|
|
13
|
+
identity, validation, protocol selection — live in
|
|
14
|
+
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md); the `(constitution: …)`
|
|
15
|
+
notes below point there rather than restating them.
|
|
16
|
+
|
|
17
|
+
`api` absorbs the former `stream` command: open-ended follow is `api <router>
|
|
18
|
+
<endpoint> --stream` (or the `/listen` endpoint form). `--listen` is an accepted
|
|
19
|
+
alias of `--stream`. Streaming is single-session — it cannot combine with
|
|
20
|
+
multi-target fan-out (see below).
|
|
21
|
+
|
|
22
|
+
## Where `api` sits — the verb trichotomy
|
|
23
|
+
|
|
24
|
+
centrs has three RouterOS-touching verbs; pick by shape of work, not by habit:
|
|
25
|
+
|
|
26
|
+
- **`execute`** — the CLI-like runner. The full RouterOS console surface
|
|
27
|
+
(multi-command / script blocks, console-text output, `:put [:parse]` gate). Most
|
|
28
|
+
powerful and lowest-level, but the caller authors a RouterOS CLI string and the
|
|
29
|
+
output is console text.
|
|
30
|
+
- **`retrieve`** — read-only data extraction. Discoverable, structured-out,
|
|
31
|
+
always read.
|
|
32
|
+
- **`api`** (this command) — the structured middle. Enforces the
|
|
33
|
+
*one-command-per-operation* semantics of the REST / native API (no code
|
|
34
|
+
blocks), **can write**, and forces structured input **and** output. For an AI
|
|
35
|
+
agent this is usually the friendliest write path: no CLI-string authoring,
|
|
36
|
+
structured in/out. It does **not** replace `execute` — console-only and
|
|
37
|
+
multi-command work still needs `execute`.
|
|
38
|
+
|
|
39
|
+
## Synopsis
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
centrs api <router> <endpoint> [flags]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `<router>` — IP, DNS, MAC, or CDB-resolved identity (constitution: identity).
|
|
46
|
+
- `<endpoint>` — a REST-style path, leniently normalized. All of
|
|
47
|
+
`ip/address`, `/ip/address`, `rest/ip/address`, `/rest/ip/address`,
|
|
48
|
+
`"ip address"`, `'ip address'` canonicalize to `/ip/address`. A trailing id
|
|
49
|
+
segment (`ip/address/*1`) addresses one row. A trailing `/listen` segment
|
|
50
|
+
infers `--stream` + `--via native-api`.
|
|
51
|
+
|
|
52
|
+
The HTTP method (`-X`, default `GET`) is honored **literally** against RouterOS's
|
|
53
|
+
REST mapping — `GET`→print/get, **`PUT`→add**, `PATCH`→set, `DELETE`→remove,
|
|
54
|
+
`POST`→run-any-command. Note the trap: **`PUT` is RouterOS's create**, not
|
|
55
|
+
`POST`. centrs never rewrites your method, but warns (`tip/rest-verb-mapping`)
|
|
56
|
+
when a bare-collection `POST` carries create-looking fields.
|
|
57
|
+
|
|
58
|
+
## Flags
|
|
59
|
+
|
|
60
|
+
| Flag | Behavior |
|
|
61
|
+
| ---- | -------- |
|
|
62
|
+
| `-X` / `--method <verb>` | HTTP method, default `GET`, case-insensitive. |
|
|
63
|
+
| `-f <key=value>` | String field, repeatable; assembled into the JSON body. Values pass through verbatim (no type-guessing). |
|
|
64
|
+
| `-d` / `--data <json>` | Raw JSON request body. Collides with `-f` → `usage/conflicting-flags`. |
|
|
65
|
+
| `--input <file\|->` | Read the raw JSON body from a file or stdin (`-`). |
|
|
66
|
+
| `--query` / `--filter <expr>` | RouterOS-side row filter, AND-combined, repeatable. `name=value` (eq), `name!=value` (ne), `name>value`/`name<value` (cmp), `name` (has-property). Maps to REST `.query` words / native `?` words. |
|
|
67
|
+
| `--raw-query <word>` | Verbatim RouterOS query word (repeatable) for OR / absence / stack expressions — e.g. `--raw-query type=ether --raw-query type=vlan --raw-query '#\|'` (OR). Emitted as-is; the caller owns the stack. |
|
|
68
|
+
| `--attribute` / `--proplist <a,b>` | Property projection → `.proplist`. |
|
|
69
|
+
| `--raw` | Strip the envelope; emit bare RouterOS JSON. Implies `--validate=false`; does **not** imply `--yes`. |
|
|
70
|
+
| `--yes` | Confirm a mutating (non-read) request in non-interactive runs. |
|
|
71
|
+
| `--stream` (alias `--listen`) | Native-api-only open-ended follow → NDJSON stream of envelopes, ending with a summary envelope. Inferred from a `/listen` endpoint. `--via rest-api --stream` errors `transport/capability-unsupported` (REST's 60 s cap cannot follow). |
|
|
72
|
+
| `--duration <dur>` / `--count <n>` | Bound a `--stream`: stop after a wall-clock window / after N frames. |
|
|
73
|
+
| `--validate[=false]` | Default `true`. Gate is `/console/inspect` (see Validation). |
|
|
74
|
+
| `--via <protocol>` | `rest-api` (default) or `native-api`. No silent downgrade. |
|
|
75
|
+
| `--format <json\|yaml\|text>` | Output format. Defaults to `json` for `api` (machine-first); `CENTRS_FORMAT` overrides. Under `--stream`, `json`/`yaml` emit one compact envelope per line (NDJSON); `text` emits a concise row per frame. |
|
|
76
|
+
| `--group <name>` / `--where <attr>=<value>` / `--near <lat>,<lon>,<radius>` / `--bbox <south>,<west>,<north>,<east>` / `--all` / `--default` / `--concurrency <n>` | Multi-target fan-out (see below). Repeatable `--group`/`--where`; `--near`/`--bbox` select by device GPS (lat-first; see constitution: target selection). The union de-dupes by CDB record index. |
|
|
77
|
+
| target/auth | `--host`, `--port`, `--username`/`--user`/`-u`, `--password`, `--insecure`, `--timeout`, `--cdb-file`, `--cdb-password`, `--resolve <none\|arp>` — same single-target resolver as `retrieve`/`execute`. |
|
|
78
|
+
|
|
79
|
+
## Fan-out (multi-target)
|
|
80
|
+
|
|
81
|
+
Selecting more than one router — any selector flag (`--group` / `--where` /
|
|
82
|
+
`--near` / `--bbox` / `--all` / `--default`) or more than one positional target — switches `api` into
|
|
83
|
+
**fan-out mode** (`src/api-fanout.ts`, on the shared `src/core/fanout.ts` engine
|
|
84
|
+
and the `src/resolver/selection.ts` grammar). A plain single-positional call
|
|
85
|
+
stays the single-target envelope. Output is the locked `FanoutData` envelope
|
|
86
|
+
(`data = { summary, targets[] }`; outer `ok` = orchestration success; per-target
|
|
87
|
+
failures are inner `ok:false`), and the process exit code is granular: `0`
|
|
88
|
+
all-ok, `2` partial, `1` orchestration error or every target failed. See
|
|
89
|
+
`docs/CONSTITUTION.md` (Target selection) for the shared grammar and `--where`
|
|
90
|
+
vs `--query` distinction.
|
|
91
|
+
|
|
92
|
+
- **Writes fan out under `--yes`**, confirmed once up front (not per target).
|
|
93
|
+
Without `--yes`, the error names the blast radius (how many routers) and that
|
|
94
|
+
`--yes` is required.
|
|
95
|
+
- **`--listen`/`--stream` is single-session** → `usage/fanout-not-supported` in
|
|
96
|
+
fan-out mode. **`--raw` strips the envelope** → `usage/conflicting-flags` in
|
|
97
|
+
fan-out mode (per-target envelopes can't be bare).
|
|
98
|
+
|
|
99
|
+
## Validation
|
|
100
|
+
|
|
101
|
+
Default on. Because the input is a structured path (not a CLI string), the gate is
|
|
102
|
+
`/console/inspect` — **not** `:put [:parse]`:
|
|
103
|
+
|
|
104
|
+
- Path existence + per-verb attribute validity via `request=child`
|
|
105
|
+
(`validation/unknown-path`, `validation/unknown-attribute`).
|
|
106
|
+
- Value-level advice via `request=completion` (+`input=`) feeding `tips`/`warnings`.
|
|
107
|
+
- **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
|
|
108
|
+
so the inspect gate is `not-applicable` (`meta.validation.semantic`); RouterOS
|
|
109
|
+
re-validates on the run.
|
|
110
|
+
|
|
111
|
+
`--validate=false` (and `--raw`, which implies it) skip the preflight; RouterOS
|
|
112
|
+
still re-validates writes server-side. Disabling validation to make a call pass is
|
|
113
|
+
forbidden (constitution: validation is the product).
|
|
114
|
+
|
|
115
|
+
## Write confirmation
|
|
116
|
+
|
|
117
|
+
Read-only **iff** the method is `GET`, or the endpoint terminal verb is
|
|
118
|
+
`print`/`get`, or it is `listen`. Everything else is treated as a write and needs
|
|
119
|
+
confirmation (`--yes` non-interactively, or a TTY prompt) → otherwise
|
|
120
|
+
`usage/confirmation-required`. Two consequences:
|
|
121
|
+
|
|
122
|
+
- A `POST …/print` paged read does **not** prompt (keyed on the verb, not the
|
|
123
|
+
wire HTTP method).
|
|
124
|
+
- Streaming does **not** imply read-only: an async-but-mutating command like
|
|
125
|
+
`/system/license/renew --stream` still confirms.
|
|
126
|
+
|
|
127
|
+
`--raw` does not bypass this gate.
|
|
128
|
+
|
|
129
|
+
## Output
|
|
130
|
+
|
|
131
|
+
Always rest-style JSON in the standard envelope (constitution: result envelope).
|
|
132
|
+
Over native-api the records are re-mapped to the same rest-style shape, with string
|
|
133
|
+
values (the binary API carries no JSON scalar types).
|
|
134
|
+
|
|
135
|
+
`--raw` is the bare-passthrough escape hatch (the envelope-lossless rule is waived
|
|
136
|
+
for it): success prints only the RouterOS body on stdout; a RouterOS error prints
|
|
137
|
+
the RouterOS error payload to stderr with a nonzero exit; a centrs-side failure
|
|
138
|
+
with no RouterOS response prints a compact `{code,message}` to stderr. Exit code is
|
|
139
|
+
`0` iff `ok`.
|
|
140
|
+
|
|
141
|
+
## Multi-frame results: bounded vs open-ended
|
|
142
|
+
|
|
143
|
+
RouterOS produces multi-frame output two ways, and `api` treats them differently:
|
|
144
|
+
|
|
145
|
+
- **Bounded `duration=` / `monitor` commands are ordinary `api` calls.** Both
|
|
146
|
+
transports return the accumulated frames as a normal **array** (REST regularizes
|
|
147
|
+
the native `!re` frames into `.section`-keyed records; native's reader collects
|
|
148
|
+
them until the command completes). No flag needed — e.g.
|
|
149
|
+
`centrs api $R interface/monitor-traffic -f interface=ether1 -f duration=5s`
|
|
150
|
+
returns a `.section` array. REST bounds these at the **60 s** cap (a longer
|
|
151
|
+
`duration=` terminates early with an error); native has no cap. This is **not**
|
|
152
|
+
NDJSON.
|
|
153
|
+
- **Open-ended follow is `--stream` (alias `--listen`), native-api only.** Only
|
|
154
|
+
the native API `/listen` command follows indefinitely (REST cannot — the 60 s
|
|
155
|
+
cap). Each `!re` becomes one rest-style envelope frame (one NDJSON line; a
|
|
156
|
+
deletion's `.dead=true` flag is preserved); the stream ends with a summary
|
|
157
|
+
envelope (`data.stopReason` ∈ `count-reached`/`duration-elapsed`/`interrupted`/
|
|
158
|
+
`transport-error`, plus `frames` and `durationMs`). `--duration`/`--count`
|
|
159
|
+
bound it; Ctrl-C stops and still emits the summary. `--via rest-api --stream` →
|
|
160
|
+
`transport/capability-unsupported`. The exit code reflects whether the stream
|
|
161
|
+
*started* cleanly, not whether every frame was `ok`.
|
|
162
|
+
|
|
163
|
+
## MCP (deferred — forward guidance)
|
|
164
|
+
|
|
165
|
+
`api` is **not** exposed over the MCP frontend yet: the MCP server is the
|
|
166
|
+
least-mature surface, and a raw passthrough sits awkwardly against its "scoped
|
|
167
|
+
verbs, never one tool per RouterOS command" model. But precisely because `api` is
|
|
168
|
+
structured and agent-friendly, it is the strongest *future* MCP candidate for an
|
|
169
|
+
agent driving RouterOS — to be revisited behind the per-device `mcp=ro|rw` +
|
|
170
|
+
`confirm:true` write gate (method-aware: read verbs `ro`, everything else `rw`) and
|
|
171
|
+
the CDB-as-allowlist authorization model (constitution: MCP surface).
|
|
172
|
+
|
|
173
|
+
## Definition of done
|
|
174
|
+
|
|
175
|
+
`CHR-passed` only when every example in `examples.md` runs green against a real CHR
|
|
176
|
+
through `bun run test:integration`. See
|
|
177
|
+
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md) for the full done rule.
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# api — examples
|
|
2
|
+
|
|
3
|
+
Each numbered example is an executable spec asserted by CHR integration tests
|
|
4
|
+
(booted by `@tikoci/quickchr`), example N ↔ assertion N:
|
|
5
|
+
`test/integration/api.test.ts` (rest-api examples),
|
|
6
|
+
`test/integration/api-native.test.ts` (native-api examples, N…),
|
|
7
|
+
`test/integration/api-listen.test.ts` (streaming examples, L…), and
|
|
8
|
+
`test/integration/api-fanout.test.ts` (fan-out examples, F…). If a line here is not
|
|
9
|
+
exercised by a test, the test file is wrong; if a line passes only with
|
|
10
|
+
`--validate=false`, the implementation is wrong (see
|
|
11
|
+
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md)).
|
|
12
|
+
|
|
13
|
+
`$R` is `<host>:<rest-port>` resolved by quickchr. `$A` is `<host>` and
|
|
14
|
+
`$API_PORT` is the native API port (`chr.ports.api`). `$U` / `$P` are CHR
|
|
15
|
+
credentials provided by the harness. `$ID` is the `.id` returned by the preceding
|
|
16
|
+
add in the same transport section. All write-shaped examples pass `--yes`.
|
|
17
|
+
|
|
18
|
+
## rest-api (`--via rest-api`, the default)
|
|
19
|
+
|
|
20
|
+
### 1. GET a list
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
centrs api $R ip/address --username $U --password $P
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Envelope: `ok: true`, `data` is an array of rest-style records,
|
|
27
|
+
`meta.via=rest-api`, `meta.validation.source` includes `/console/inspect request=child`.
|
|
28
|
+
|
|
29
|
+
### 2. Endpoint normalization variants resolve to one path
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
centrs api $R "/rest/ip/address" --username $U --password $P
|
|
33
|
+
centrs api $R "rest/ip/address" --username $U --password $P
|
|
34
|
+
centrs api $R "ip address" --username $U --password $P
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each resolves to the same canonical path; `meta.request.path == "/ip/address"`
|
|
38
|
+
and the `data` array matches example 1.
|
|
39
|
+
|
|
40
|
+
### 3. GET a singleton
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
centrs api $R system/resource --username $U --password $P
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Envelope: `ok: true`, `data` is a single object (not an array) with `uptime`,
|
|
47
|
+
`version`, etc.
|
|
48
|
+
|
|
49
|
+
### 4. PUT add (RouterOS create; documents the PUT≠POST trap)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
centrs api $R ip/address -X PUT -f address=198.51.100.10/32 -f interface=ether1 -f comment=centrs-api-rest --username $U --password $P --yes
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Envelope: `ok: true`, `data[".id"]` matches `/^\*[0-9A-F]+$/`, `meta.via=rest-api`.
|
|
56
|
+
A subsequent GET of `/ip/address` contains the new row.
|
|
57
|
+
|
|
58
|
+
### 5. PATCH set by id-in-path
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
centrs api $R ip/address/$ID -X PATCH -f comment=centrs-api-rest-set --username $U --password $P --yes
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Envelope: `ok: true`; re-reading `ip/address/$ID` shows the new comment.
|
|
65
|
+
|
|
66
|
+
### 6. DELETE remove by id-in-path
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
centrs api $R ip/address/$ID -X DELETE --username $U --password $P --yes
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Envelope: `ok: true`; a subsequent GET of `/ip/address` does not contain `$ID`.
|
|
73
|
+
|
|
74
|
+
### 7. Raw JSON body with `-d`
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
centrs api $R ip/address -X PUT -d '{"address":"198.51.100.11/32","interface":"ether1"}' --username $U --password $P --yes
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Envelope: `ok: true`, `data[".id"]` present.
|
|
81
|
+
|
|
82
|
+
### 8. Body from stdin with `--input -`
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
echo '{"address":"198.51.100.12/32","interface":"ether1"}' | centrs api $R ip/address -X PUT --input - --username $U --password $P --yes
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Envelope: `ok: true`, `data[".id"]` present.
|
|
89
|
+
|
|
90
|
+
### 9. Server-side filter with `--query`
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
centrs api $R interface --query type=ether --username $U --password $P
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Envelope: `ok: true`, every returned record has `type=ether`; the request carried
|
|
97
|
+
a REST `.query` (not client-side filtering).
|
|
98
|
+
|
|
99
|
+
### 10. Projection with `--proplist`
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
centrs api $R ip/address --proplist address,interface --username $U --password $P
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Envelope: `ok: true`, each record has only the requested properties (plus `.id`).
|
|
106
|
+
|
|
107
|
+
### 11. Validation rejects an unknown attribute before the write
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
centrs api $R ip/address -X PUT -f address=198.51.100.13/32 -f interface=ether1 -f no-such-arg=x --username $U --password $P --yes
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Envelope: `ok: false`, `error.code=validation/unknown-attribute`,
|
|
114
|
+
`meta.validation.source=/console/inspect`, and no address is added.
|
|
115
|
+
|
|
116
|
+
### 12. Validation rejects an unknown path
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
centrs api $R ip/no-such-menu --username $U --password $P
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Envelope: `ok: false`, `error.code=validation/unknown-path`, no request issued.
|
|
123
|
+
|
|
124
|
+
### 13. `--raw` success prints the bare RouterOS body
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
centrs api $R ip/address --raw --username $U --password $P
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
stdout is the bare RouterOS JSON array (no `meta`/`warnings`); validation is
|
|
131
|
+
skipped; exit code 0.
|
|
132
|
+
|
|
133
|
+
### 14. `--raw` RouterOS error → stderr + nonzero exit
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
centrs api $R ip/address -X PUT -f address=not-an-ip -f interface=ether1 --raw --username $U --password $P --yes
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
stdout is empty; stderr carries the RouterOS error payload as JSON; exit code is
|
|
140
|
+
nonzero.
|
|
141
|
+
|
|
142
|
+
### 15. Mutating + non-TTY + no `--yes` is refused
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
centrs api $R ip/address -X PUT -f address=198.51.100.14/32 -f interface=ether1 --username $U --password $P </dev/null
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Envelope: `ok: false`, `error.code=usage/confirmation-required`, no request
|
|
149
|
+
issued, no address added. (Holds under `--raw` too.)
|
|
150
|
+
|
|
151
|
+
### 16. POST run a console command via `/rest/execute`
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --username $U --password $P --yes
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
|
|
158
|
+
`meta.validation.semantic=not-applicable` (script, not a path).
|
|
159
|
+
|
|
160
|
+
### 17. `--via rest-api --listen` is rejected
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
centrs api $R ip/address --listen --via rest-api --username $U --password $P
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Envelope: `ok: false`, `error.code=transport/capability-unsupported` (REST cannot
|
|
167
|
+
follow); no silent open-ended poll.
|
|
168
|
+
|
|
169
|
+
### 18. `--query` not-equal maps to a negated stack word
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
centrs api $R interface --query type!=ether --username $U --password $P
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Envelope: `ok: true`, no returned record has `type=ether`; the request carried
|
|
176
|
+
REST `.query` words `["type=ether","#!"]` (eq then NOT-top).
|
|
177
|
+
|
|
178
|
+
### 19. `--raw-query` expresses OR
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
centrs api $R interface --raw-query type=ether --raw-query type=loopback --raw-query '#|' --username $U --password $P
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Envelope: `ok: true`, returns the union of `ether` and `loopback` interfaces; the
|
|
185
|
+
raw words are passed through verbatim as `.query` (REST) / `?` words (native).
|
|
186
|
+
|
|
187
|
+
### 20. Bounded `duration=` command returns a `.section` array (not a stream)
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
centrs api $R interface/monitor-traffic -X POST -f interface=ether1 -f duration=5s --username $U --password $P --yes
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Envelope: `ok: true`, `data` is an **array** of `.section`-keyed records (one per
|
|
194
|
+
~second), `meta.via=rest-api`. This is an ordinary bounded call — no `--listen`,
|
|
195
|
+
no NDJSON. `monitor-traffic` is a command (not `print`/`get`), so it is a
|
|
196
|
+
`POST` and write-classed (needs `--yes`); centrs never relaxes that to read-only.
|
|
197
|
+
(REST bounds it at the 60 s cap; native has no cap.)
|
|
198
|
+
|
|
199
|
+
### 21. GET one row by id together with `--proplist`
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
centrs api $R ip/address/$ID --proplist address --username $U --password $P
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Envelope: `ok: true`, `data` is a **single object** (not an array) carrying only
|
|
206
|
+
the projected property `address` (RouterOS omits `.id` unless it is in the
|
|
207
|
+
proplist). Combining an id with `--query`/`--proplist` folds the id into the REST
|
|
208
|
+
`.query` (`.id=$ID`) and rides a `POST …/print`, then unwraps the one row —
|
|
209
|
+
matching the native `?.id=` read. (Plain id-only reads still use the `GET …/$ID`
|
|
210
|
+
URL form; see example 5's re-read.)
|
|
211
|
+
|
|
212
|
+
## native-api (`--via native-api`)
|
|
213
|
+
|
|
214
|
+
The same contract over the binary API. Validation still runs through
|
|
215
|
+
`/console/inspect`, issued as native commands. Values are strings.
|
|
216
|
+
|
|
217
|
+
### N1. GET a list
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
centrs api $A interface -X GET --via native-api --port $API_PORT --username $U --password $P
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Envelope: `ok: true`, `data` is an array of rest-style records (string values),
|
|
224
|
+
`meta.via=native-api`.
|
|
225
|
+
|
|
226
|
+
### N2. PUT add (returns `.id`)
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
centrs api $A ip/address -X PUT -f address=198.51.100.20/32 -f interface=ether1 --via native-api --port $API_PORT --username $U --password $P --yes
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Envelope: `ok: true`, `data[".id"]` present.
|
|
233
|
+
|
|
234
|
+
### N3. PATCH set by id (id → `=.id=` word)
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
centrs api $A ip/address/$ID -X PATCH -f comment=centrs-api-native-set --via native-api --port $API_PORT --username $U --password $P --yes
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Envelope: `ok: true`; re-reading shows the new comment.
|
|
241
|
+
|
|
242
|
+
### N4. DELETE remove by id (id → `=.id=` word)
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
centrs api $A ip/address/$ID -X DELETE --via native-api --port $API_PORT --username $U --password $P --yes
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Envelope: `ok: true`; the row is gone.
|
|
249
|
+
|
|
250
|
+
### N5. GET one by id (→ `print ?.id=`)
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
centrs api $A ip/address/$ID2 -X GET --via native-api --port $API_PORT --username $U --password $P
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Envelope: `ok: true`, `data` is a single object whose `.id` is `$ID2`.
|
|
257
|
+
|
|
258
|
+
### N6. Validation rejects an unknown attribute over native
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
centrs api $A ip/address -X PUT -f address=198.51.100.21/32 -f interface=ether1 -f no-such-arg=x --via native-api --port $API_PORT --username $U --password $P --yes
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Envelope: `ok: false`, `error.code=validation/unknown-attribute`, no row added —
|
|
265
|
+
caught by the inspect gate, not a native `!trap`.
|
|
266
|
+
|
|
267
|
+
### N7. `--raw` over native
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
centrs api $A interface --raw --via native-api --port $API_PORT --username $U --password $P
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
stdout is the bare rest-style array (string values); exit code 0.
|
|
274
|
+
|
|
275
|
+
### N8. POST run a console command via native `/execute`
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
centrs api $A execute -X POST -f script=':put [/system/identity/get name]' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Envelope: `ok: true`, `data` contains the CHR identity.
|
|
282
|
+
|
|
283
|
+
## listen / `--stream` (native-api only)
|
|
284
|
+
|
|
285
|
+
Open-ended follow. `--stream` is the primary flag; `--listen` is an accepted
|
|
286
|
+
alias and a trailing `/listen` endpoint segment infers it. Output is NDJSON: one
|
|
287
|
+
envelope per change frame, then a terminating summary envelope.
|
|
288
|
+
|
|
289
|
+
### L1. `--stream` streams a change as an NDJSON frame, then a summary
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
centrs api $A ip/address --stream --count 1 --via native-api --port $API_PORT --username $U --password $P
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
While listening, the harness adds an address over REST. stdout is NDJSON: at least
|
|
296
|
+
one envelope frame for the new row (`meta.operation.stream.kind=frame`), then a
|
|
297
|
+
final summary envelope (`meta.operation.stream.kind=summary`,
|
|
298
|
+
`data.stopReason=count-reached`, `data.frames>=1`). Exit code 0 (stream started
|
|
299
|
+
cleanly).
|
|
300
|
+
|
|
301
|
+
### L2. A deletion frame carries `.dead`
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
centrs api $A ip/address --stream --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The harness removes a pre-seeded address; an emitted frame's record carries
|
|
308
|
+
`.dead=true` (a minimal `{ ".id", ".dead" }` record, per the CHR-grounded
|
|
309
|
+
`commands/api/AGENTS.md`).
|
|
310
|
+
|
|
311
|
+
### L3. `/listen` endpoint infers native + streaming
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
centrs api $A "ip/address/listen" --count 1 --port $API_PORT --username $U --password $P
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
No `--stream` / `--via` given; the `/listen` endpoint infers `--stream` and
|
|
318
|
+
`--via native-api` (`meta.via=native-api`). Same NDJSON shape as L1.
|
|
319
|
+
|
|
320
|
+
### L4. Bounded `--duration` reports its stop reason
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
centrs api $A ip/address --stream --duration 2s --via native-api --port $API_PORT --username $U --password $P
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
With no change during the window, the stream ends after ~2 s with a summary
|
|
327
|
+
envelope whose `data.stopReason=duration-elapsed`. Exit code 0.
|
|
328
|
+
|
|
329
|
+
## fanout (multi-target, F…)
|
|
330
|
+
|
|
331
|
+
These run against a CDB fixture with two records sharing group `$G`: record 0 is
|
|
332
|
+
the live CHR (comment `board=chr`), record 1 is an unreachable host (comment
|
|
333
|
+
`board=dead`). Fan-out output is the locked `FanoutData` envelope
|
|
334
|
+
(`data = { summary, targets[] }`; outer `ok` = orchestration success; per-target
|
|
335
|
+
failures are inner `ok:false`), with the granular exit code (0 all-ok / 2 partial /
|
|
336
|
+
1 all-failed or orchestration error). See `test/integration/api-fanout.test.ts`.
|
|
337
|
+
The `--raw`/`--listen` + fan-out guards are network-free and validated in
|
|
338
|
+
`test/integration/cli-smoke.test.ts`.
|
|
339
|
+
|
|
340
|
+
### F1. `--group` fans a GET out; a dead target is an inner failure
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
centrs api --group $G ip/address --cdb-file $CDB --json
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`, `data.targets[]` in
|
|
347
|
+
record-index order (`[0, 1]`): target 0 is an inner success (`meta.via=rest-api`),
|
|
348
|
+
target 1 is inner `ok:false` with `error.code=transport/connection-refused`.
|
|
349
|
+
`meta.operation.kind=fanout`. Exit code 2.
|
|
350
|
+
|
|
351
|
+
### F2. `--where` selects only the matching device
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
centrs api --where board=chr ip/address --cdb-file $CDB --json
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The device-class selector matches only record 0, so `data.summary = { total: 1,
|
|
358
|
+
ok: 1, failed: 0 }`, one inner success. Exit code 0.
|
|
359
|
+
|
|
360
|
+
### F3. An empty selection is `ok:true` with summary 0/0/0
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
centrs api --group no-such-group ip/address --cdb-file $CDB --json
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`,
|
|
367
|
+
and a `cdb/empty-group` warning. Exit code 0.
|
|
368
|
+
|
|
369
|
+
### F4. A mutating fan-out without `--yes` is rejected, naming the blast radius
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
centrs api -X PUT ip/address -f address=10.99.0.1/32 -f interface=ether1 --group $G --cdb-file $CDB --json
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Non-interactive and unconfirmed: outer `ok:false`,
|
|
376
|
+
`error.code=usage/confirmation-required`, and the message names the router count
|
|
377
|
+
(`2 router(s)`) and that `--yes` is required. No write is attempted. Exit code 1.
|
|
378
|
+
|
|
379
|
+
### F5. A mutating fan-out with `--yes` writes across the selected devices
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
centrs api -X PUT ip/address -f address=10.99.0.7/32 -f interface=ether1 --where board=chr --yes --cdb-file $CDB --json
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Confirmed once up front; the add runs on record 0 only (selected by `--where`).
|
|
386
|
+
`data.summary = { total: 1, ok: 1, failed: 0 }`, the inner success carries the
|
|
387
|
+
created `.id`. Exit code 0.
|
|
388
|
+
|
|
389
|
+
The fixture for F6–F9 adds record 2 — the reserved `__default__` credential record.
|
|
390
|
+
|
|
391
|
+
### F6. `--all` fans across every record except `__default__`
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
centrs api --all ip/address --cdb-file $CDB --json
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`data.summary = { total: 2, ok: 1, failed: 1 }`, `data.targets[]` record indices
|
|
398
|
+
`[0, 1]` (record 2 `__default__` is excluded). Exit code 2.
|
|
399
|
+
|
|
400
|
+
### F7. A positional + `--group` union de-dupes by record index
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
centrs api $R0 --group $G ip/address --cdb-file $CDB --json
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`$R0` is record 0's target, also a member of `$G`; the union is `{0, 1}` — record
|
|
407
|
+
indices `[0, 1]`, `total = 2` (not 3). Exit code 2.
|
|
408
|
+
|
|
409
|
+
### F8. `--concurrency` bounds the worker pool
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
centrs api --group $G --concurrency 1 ip/address --cdb-file $CDB --json
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Runs one target at a time; `meta.operation.concurrency = 1`, still
|
|
416
|
+
`data.summary = { total: 2, ok: 1, failed: 1 }`. (`--concurrency 2abc` / `1.5` are
|
|
417
|
+
rejected at parse time with `usage/invalid-concurrency`.) Exit code 2.
|
|
418
|
+
|
|
419
|
+
### F9. `--default` selects `__default__`, which fails the connectable guard
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
centrs api --default ip/address --cdb-file $CDB --json
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The reserved record is a credential fallback, not a connectable router, so its one
|
|
426
|
+
target fails deterministically: inner `ok:false` with
|
|
427
|
+
`error.code=target/unresolved` (never a `transport/dns` attempt on `"__default__"`).
|
|
428
|
+
`data.summary = { total: 1, ok: 0, failed: 1 }`. Exit code 1 (every target failed).
|