@tikoci/centrs 0.1.9-next.24 → 0.1.9

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +121 -6
  2. package/GLOSSARY.txt +9 -1
  3. package/README.md +43 -29
  4. package/commands/api/AGENTS.md +3 -2
  5. package/commands/api/README.md +8 -1
  6. package/commands/btest/README.md +9 -0
  7. package/commands/btest/examples.md +7 -0
  8. package/commands/devices/README.md +160 -30
  9. package/commands/devices/examples.md +168 -2
  10. package/commands/execute/README.md +13 -2
  11. package/commands/explain/README.md +223 -3
  12. package/commands/explain/examples.md +8 -1
  13. package/commands/retrieve/README.md +24 -3
  14. package/commands/retrieve/examples.md +84 -0
  15. package/commands/settings/README.md +6 -0
  16. package/commands/settings/examples.md +17 -0
  17. package/commands/transfer/README.md +25 -0
  18. package/commands/transfer/examples.md +46 -0
  19. package/docs/CLI.md +53 -11
  20. package/docs/MATRIX.md +5 -3
  21. package/docs/errors/cdb/encrypt-failed.md +10 -0
  22. package/docs/errors/cdb/password-required.md +5 -4
  23. package/docs/errors/cdb/plaintext-backups.md +11 -0
  24. package/docs/errors/input/empty-stdin.md +27 -0
  25. package/docs/errors/input/invalid-routeros-path.md +25 -3
  26. package/docs/errors/input/invalid-stdin.md +28 -0
  27. package/docs/errors/usage/stdin-is-tty.md +21 -0
  28. package/package.json +11 -7
  29. package/src/api-fanout.ts +2 -0
  30. package/src/api.ts +12 -0
  31. package/src/btest.ts +7 -0
  32. package/src/cli/api.ts +19 -2
  33. package/src/cli/btest.ts +9 -0
  34. package/src/cli/common.ts +1 -0
  35. package/src/cli/devices.ts +248 -15
  36. package/src/cli/execute.ts +12 -3
  37. package/src/cli/retrieve.ts +9 -0
  38. package/src/cli/settings.ts +28 -6
  39. package/src/cli/terminal.ts +9 -0
  40. package/src/cli/transfer.ts +10 -1
  41. package/src/core/error-catalog.ts +28 -2
  42. package/src/core/menu-path.ts +89 -0
  43. package/src/devices.ts +378 -33
  44. package/src/execute-fanout.ts +2 -0
  45. package/src/execute.ts +7 -0
  46. package/src/explain/blocks.ts +2 -2
  47. package/src/explain/catalog.ts +22 -12
  48. package/src/explain/pasted-prompt.ts +198 -0
  49. package/src/explain/pathresolve.ts +2 -2
  50. package/src/explain/segment.ts +2 -2
  51. package/src/explain/symbols.ts +2 -2
  52. package/src/explain/verbsplit.ts +2 -2
  53. package/src/explain/write.ts +2 -2
  54. package/src/explain.ts +2 -0
  55. package/src/index.ts +5 -0
  56. package/src/mcp/safety.ts +7 -6
  57. package/src/mcp/tools.ts +30 -68
  58. package/src/protocols/adapter.ts +2 -0
  59. package/src/protocols/btest-session.ts +10 -30
  60. package/src/protocols/ec-srp5.ts +19 -10
  61. package/src/protocols/mac-telnet-console.ts +2 -2
  62. package/src/resolver/cdb.ts +66 -9
  63. package/src/resolver/index.ts +1 -0
  64. package/src/resolver/quickchr-provider.ts +5 -0
  65. package/src/resolver/selection.ts +15 -4
  66. package/src/retrieve-fanout.ts +2 -0
  67. package/src/retrieve.ts +11 -0
  68. package/src/settings.ts +24 -8
  69. package/src/terminal.ts +7 -0
  70. package/src/transfer-fanout.ts +2 -0
  71. package/src/transfer.ts +79 -14
package/docs/CLI.md CHANGED
@@ -11,7 +11,7 @@ implemented flags live in each command's `commands/<name>/README.md`.
11
11
  | Command | Summary |
12
12
  | ------- | ------- |
13
13
  | [`retrieve`](#retrieve) | Read RouterOS values through the shared core using the selected protocol. |
14
- | [`execute`](#execute) | Run a RouterOS read or write command via native API, REST, or mac-telnet. |
14
+ | [`execute`](#execute) | Run a RouterOS read or write command via native API, REST, SSH, or mac-telnet. |
15
15
  | [`api`](#api) | Structured RouterOS API passthrough (gh-api style) over REST or native API. |
16
16
  | [`transfer`](#transfer) | Copy files to/from a RouterOS device and manage device files (rest/native/sftp). |
17
17
  | [`terminal`](#terminal) | Open an interactive RouterOS console (ssh or mac-telnet). |
@@ -64,13 +64,14 @@ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <targe
64
64
  | `--max-results` | `<bytes>` | Fail instead of printing output larger than the given byte budget. |
65
65
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
66
66
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
67
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
67
68
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
68
69
  | `--validate / --no-validate` | | Enable or disable inspect-backed preflight validation. |
69
70
  | `--verbose` | | Show resolved setting sources in text output, on stderr. |
70
71
 
71
72
  ## execute
72
73
 
73
- Run a RouterOS read or write command via native API, REST, or mac-telnet.
74
+ Run a RouterOS read or write command via native API, REST, SSH, or mac-telnet.
74
75
 
75
76
  ```text
76
77
  Usage: centrs execute <target> <command> [flags] | centrs execute <target...> -- <command> [flags] | centrs execute --host <host|url> [--] <command> [flags] | centrs execute --group <name> <command> [flags]
@@ -78,7 +79,7 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
78
79
 
79
80
  | Flag | Value | Description |
80
81
  | ---- | ----- | ----------- |
81
- | `--via` | &lt;native-api\|rest-api\|mac-telnet&gt; | Pin the protocol selector; no silent downgrade when set. A bare MAC target defaults to mac-telnet. |
82
+ | `--via` | &lt;native-api\|rest-api\|ssh\|mac-telnet&gt; | Pin the protocol selector; no silent downgrade when set. A bare MAC target defaults to mac-telnet. |
82
83
  | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
83
84
  | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
84
85
  | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
@@ -97,9 +98,10 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
97
98
  | `--insecure` | | Disable SSH host-key verification (`--via ssh`: accepts changed/impersonated keys, not just new) or accept a self-signed `api-ssl` TLS cert. Default verifies. |
98
99
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
99
100
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
101
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
100
102
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
101
103
  | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
102
- | `--validate / --no-validate` | | Run RouterOS :parse and /console/inspect validation before execution (default true; `--validate=false` also accepted). |
104
+ | `--validate / --no-validate` | | Run offline analysis, then RouterOS :parse, plus `/console/inspect` for structured commands over native API/REST (default true; `--validate=false` disables both stages). |
103
105
  | `--yes` | | Confirm write-shaped add/set/remove commands in non-interactive runs. |
104
106
  | `--max-results` | `<bytes>` | Fail if the rendered envelope exceeds this byte budget. |
105
107
  | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope. |
@@ -145,9 +147,10 @@ Usage: centrs api <router> <endpoint> [flags] | centrs api --host <host|url> <en
145
147
  | `--insecure` | | Accept a self-signed `api-ssl`/REST TLS cert. Default verifies. |
146
148
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
147
149
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
150
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
148
151
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
149
152
  | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
150
- | `--validate / --no-validate` | | Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted). |
153
+ | `--validate / --no-validate` | | Validate structured requests with `/console/inspect`; `/execute` scripts run offline analysis, then RouterOS :parse (default true; `--validate=false` disables both stages). |
151
154
  | `--format` | &lt;json\|ndjson\|yaml\|text&gt; | Output format. Defaults to json for api; `CENTRS_FORMAT` overrides. |
152
155
  | `--json` | | Shortcut for `--format json`. |
153
156
  | `--verbose` | | Include additional context in text output, on stderr. |
@@ -162,7 +165,7 @@ Usage: centrs transfer <router>|--host <host> upload <local> [remote] | download
162
165
 
163
166
  | Flag | Value | Description |
164
167
  | ---- | ----- | ----------- |
165
- | `--via` | `<method>` | Pin the method: rest, native, or sftp (large transfers); scp/fetch/ftp are not built yet. Auto picks the cheapest by size/direction. |
168
+ | `--via` | `<method>` | Pin the method: rest, native, or sftp (large transfers; `ssh` is an alias); scp/fetch/ftp are not built yet. Auto picks the cheapest by size/direction, after a CDB record's `via=` (ssh→sftp). |
166
169
  | `--ssh-key` | `<path>` | sftp only: explicit private-key path. Falls back to CENTRS_SSH_KEY / the ssh-agent. |
167
170
  | `--insecure` | | Accept a self-signed TLS cert (https/api-ssl) or a new SSH host key. Default verifies. |
168
171
  | `--force / --overwrite` | | Skip the existing-destination check on the device for `upload`/`mkdir`/`copy` (`upload` then overwrites). Default refuses an existing target. `download` always replaces the local file. |
@@ -189,6 +192,7 @@ Usage: centrs transfer <router>|--host <host> upload <local> [remote] | download
189
192
  | `--yaml` | | Shortcut for --format yaml. |
190
193
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
191
194
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
195
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
192
196
  | `--validate / --no-validate` | | Enable or disable the existence-probe preflight. |
193
197
  | `--verbose` | | Show the resolved transport in text output, on stderr. |
194
198
 
@@ -212,6 +216,7 @@ Usage: centrs terminal <router> [flags] | centrs terminal --host <host> [flags]
212
216
  | `--insecure` | | `--via ssh`: disable host-key verification (accepts changed/impersonated keys). Default verifies. |
213
217
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
214
218
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
219
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
215
220
  | `--resolve` | &lt;none\|arp&gt; | `--via ssh` only: turn a MAC target into an IP. CDB-first; `arp` opts into the host ARP cache (default none). The mac-telnet default ignores it. |
216
221
  | `--format` | &lt;text\|json\|yaml&gt; | Error-envelope format on failure. --json / --yaml shortcuts. |
217
222
  | `--json` | | Shortcut for --format json. |
@@ -258,23 +263,27 @@ Usage: centrs explain [<router>] '<input>' [flags]
258
263
  Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB.
259
264
 
260
265
  ```text
261
- Usage: centrs devices <init|list|show|groups|add|edit|set|remove> [args] [flags]
266
+ Usage: centrs devices <init|encrypt|list|show|groups|add|edit|set|remove> [args] [flags]
262
267
  ```
263
268
 
264
269
  | Flag | Value | Description |
265
270
  | ---- | ----- | ----------- |
271
+ | `--encrypt` | | `init` only — require encrypted creation with a non-empty CDB password; refuse an existing open CDB. |
272
+ | `--no-backup` | | `encrypt` only — skip the plaintext pre-conversion snapshot. Without that snapshot, a forgotten password cannot be recovered. |
266
273
  | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
267
- | `--cdb-password` | `<secret>` | Decrypt an encrypted CDB. Falls back to `CENTRS_CDB_PASSWORD`. Also used to re-encrypt on writes. |
274
+ | `--cdb-password` | `<secret>` | CDB password for encrypted reads, writes, explicit encrypted initialization and conversion. Falls back to `CENTRS_CDB_PASSWORD`; a password alone never converts an open CDB. |
268
275
  | `--via` | `<protocol>` | Resolve and report the protocol source for provenance examples; no network IO is performed. |
269
- | `--group` | `<name>` | `list` filter / `add`,`edit` first-class group field for the target entry. |
276
+ | `--group` | `<name>` | `list` filter / `add`,`set` first-class group field for the target entry. |
270
277
  | `--where` | `<attr=value>` | `list` only — repeatable device-class filter over CDB facts (AND-combined), e.g. `--where lat=37.7749`. |
271
278
  | `--near` | `<lat>`,`<lon>`,`<radius>` | `list` only — GPS filter: devices within radius (m/km/mi/ft; bare number = km). Lat-first. |
272
279
  | `--bbox` | `<south>`,`<west>`,`<north>`,`<east>` | `list` only — GPS filter: devices inside the lat-first bounding box. |
273
280
  | `--members` | | `groups` only — expand each group's membership. |
274
- | `--explain` | | `show` only — include the raw `WinBoxCdbRecord` in `data.record`. |
275
- | `--match` | &lt;user=…\|target=…\|record-type&gt; | `show` — disambiguate when `<router>` matches several records: `user=<name>`, `target=<addr>`, or a record-type token. |
281
+ | `--explain` | | `show` only — include the raw `WinBoxCdbRecord` in `data.record` (its password field reads `<redacted>` unless `--show-password`). |
282
+ | `--match` | &lt;user=…\|target=…\|record-type&gt; | `show`,`set`,`remove` — disambiguate when `<router>` matches several records: `user=<name>`, `target=<addr>`, or a record-type token. |
276
283
  | `--user` | `<name>` | `add`,`set` — first-class CDB user field. |
277
284
  | `--password` | `<secret>` | `add`,`set` — first-class CDB password field. |
285
+ | `--password-stdin` | | `add`,`set` — read the record password from stdin (one trailing newline stripped) so it never rides argv. Conflicts with `--password`. |
286
+ | `--show-password` | | `show`,`add`,`set` — include the record password in cleartext. By default output carries `passwordSet` and withholds the password. |
278
287
  | `--profile` | `<name>` | `add`,`set` — named WinBox profile (Workspace). |
279
288
  | `--profile-none` | | `add`,`set` — write the WinBox `<none>` profile sentinel. |
280
289
  | `--profile-own` | | `add`,`set` — write the WinBox `<own>` profile sentinel. |
@@ -291,6 +300,38 @@ Usage: centrs devices <init|list|show|groups|add|edit|set|remove> [args] [flags]
291
300
  | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
292
301
  | `--json` | | Shortcut for `--format json`. |
293
302
 
303
+ ### devices init
304
+
305
+ Create an empty CDB at the resolved path; existing readable files are left unchanged.
306
+
307
+ ```text
308
+ Usage: centrs devices init [flags]
309
+ ```
310
+
311
+ | Flag | Value | Description |
312
+ | ---- | ----- | ----------- |
313
+ | `--encrypt` | | `init` only — require encrypted creation with a non-empty CDB password; refuse an existing open CDB. |
314
+ | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
315
+ | `--cdb-password` | `<secret>` | CDB password for encrypted reads, writes, explicit encrypted initialization and conversion. Falls back to `CENTRS_CDB_PASSWORD`; a password alone never converts an open CDB. |
316
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
317
+ | `--json` | | Shortcut for `--format json`. |
318
+
319
+ ### devices encrypt
320
+
321
+ Encrypt an existing CDB in place, or verify an already-encrypted file; existing backups remain unchanged.
322
+
323
+ ```text
324
+ Usage: centrs devices encrypt [flags]
325
+ ```
326
+
327
+ | Flag | Value | Description |
328
+ | ---- | ----- | ----------- |
329
+ | `--no-backup` | | `encrypt` only — skip the plaintext pre-conversion snapshot. Without that snapshot, a forgotten password cannot be recovered. |
330
+ | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
331
+ | `--cdb-password` | `<secret>` | CDB password for encrypted reads, writes, explicit encrypted initialization and conversion. Falls back to `CENTRS_CDB_PASSWORD`; a password alone never converts an open CDB. |
332
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
333
+ | `--json` | | Shortcut for `--format json`. |
334
+
294
335
  ## discover
295
336
 
296
337
  Discover RouterOS neighbors over MNDP and optionally save them into the CDB.
@@ -340,6 +381,7 @@ Usage: centrs btest <client <router>|server> [flags]
340
381
  | `--port` | `<port>` | Control port. Default 2000. |
341
382
  | `--cdb-file` | `<path>` | (client) Read target credentials from a WinBox CDB file. |
342
383
  | `--cdb-password` | `<password>` | (client) Decrypt an encrypted WinBox CDB file. |
384
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | (client) Pick one of several CDB records saved for `<router>` (as `devices show --match`); `btest server` rejects it. |
343
385
  | `--format` | &lt;text\|csv\|json\|yaml&gt; | Output format. Default text. |
344
386
  | `--csv` | | Shortcut for `--format csv`. |
345
387
 
package/docs/MATRIX.md CHANGED
@@ -189,7 +189,9 @@ Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
189
189
  Live `explain` (#236) is `CHR-passed` on REST and native API: the #491
190
190
  foundation, #492's describe and completion facets, and #225's live value types,
191
191
  with every applicable example green on stable and long-term CHR. Issue #90
192
- owns the remaining sequencing, including #507 context grounding.
192
+ owns the remaining sequencing, including #507's context surface. Its bounded
193
+ submission matrix is grounded (#574); context selection and binding behavior
194
+ remain follow-up work.
193
195
 
194
196
  **First: `explain`** (maintainer decision 2026-10-16 on
195
197
  [#90](https://github.com/tikoci/centrs/issues/90)). In order: make the
@@ -253,8 +255,8 @@ applicable examples pass on CHR via `bun run test:integration`.
253
255
 
254
256
  Open offline follow-ups remain explicit but do not reopen the verified baseline.
255
257
  Issue #211 made an unlisted absolute bare path abstain (rule F, R14); rule
256
- P's closed-world reading is still not adopted (#372). #272 owns
257
- non-authoritative `.scratch/` citation cleanup. Issues #350 (the broad
258
+ P's closed-world reading is still not adopted (#372). The non-authoritative
259
+ scratch citation cleanup is complete (#272, shipped in #568). Issues #350 (the broad
258
260
  optional `=`), #351 (a relative submenu brace read as a command operand)
259
261
  and #355 (`'` is not a RouterOS token) are complete. A submenu block after a navigation statement
260
262
  (`/ip⏎address { … }`) keeps the pre-#351 reading; see
@@ -0,0 +1,10 @@
1
+ # `cdb/encrypt-failed`
2
+
3
+ The existing CDB could not be rewritten with encryption.
4
+
5
+ ## Fix
6
+
7
+ Check directory permissions and available disk space, then retry
8
+ `centrs devices encrypt`. Preserve the original file and any timestamped backups
9
+ before recovery. A failed write may already have created a plaintext backup;
10
+ encryption does not protect that backup.
@@ -1,9 +1,10 @@
1
1
  # `cdb/password-required`
2
2
 
3
- The CDB is encrypted; a --cdb-password is required to read it.
3
+ A CDB password is required for decryption or explicit encryption.
4
4
 
5
5
  ## Fix
6
6
 
7
- See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
8
- contract. This stub will be expanded with the typical trigger and remediation
9
- for `cdb/password-required`.
7
+ Pass `--cdb-password` or set `CENTRS_CDB_PASSWORD` to the CDB password.
8
+ `devices init --encrypt` and `devices encrypt` require a non-empty password,
9
+ even when the file is currently open. A password alone never implicitly
10
+ converts an existing open CDB; use `devices encrypt` explicitly.
@@ -0,0 +1,11 @@
1
+ # `cdb/plaintext-backups`
2
+
3
+ CDB backups may still contain plaintext credentials after encryption.
4
+
5
+ ## Fix
6
+
7
+ Apply your own secure backup and retention policy to the `.bak.*` files beside
8
+ the CDB. `devices encrypt` protects only the active CDB and retains every backup,
9
+ including the plaintext snapshot created during conversion. It does not encrypt
10
+ or delete old copies. This warning also accompanies already-encrypted no-op runs,
11
+ since prior backup protection cannot be inferred from the active file.
@@ -0,0 +1,27 @@
1
+ # `input/empty-stdin`
2
+
3
+ A flag that reads its value from stdin (e.g. `--password-stdin`) received
4
+ nothing.
5
+
6
+ `centrs devices add`/`set --password-stdin` strips one trailing newline (`\n`
7
+ or `\r\n`) and refuses what is left if it is empty. An empty pipe is usually a
8
+ mistake — an unset variable, a missing file, a command that printed nothing —
9
+ so it is not treated as "clear the password".
10
+
11
+ ## Fix
12
+
13
+ Pipe a non-empty value:
14
+
15
+ ```bash
16
+ printf '%s' "$ROUTER_PW" | centrs devices set 192.0.2.5 --password-stdin
17
+ centrs devices set 192.0.2.5 --password-stdin < pw.txt
18
+ ```
19
+
20
+ To clear a stored password on purpose, say so with the argv flag:
21
+
22
+ ```bash
23
+ centrs devices set 192.0.2.5 --password ""
24
+ ```
25
+
26
+ See [`commands/devices/README.md`](https://github.com/tikoci/centrs/blob/main/commands/devices/README.md)
27
+ for the record-password channels.
@@ -1,9 +1,31 @@
1
1
  # `input/invalid-routeros-path`
2
2
 
3
- The RouterOS path is malformed (it must be slash-prefixed).
3
+ The RouterOS path is malformed: not slash-prefixed, a segment that is not a
4
+ menu name, or characters a REST URL would reinterpret.
5
+
6
+ ## Typical triggers
7
+
8
+ - A path without a leading slash: `retrieve <router> ip/address`.
9
+ - A `retrieve` or `api` path segment that is not a RouterOS menu name. Menu
10
+ names are letters, digits and hyphens (case folds) (`/interface/6to4`,
11
+ `/ip/dhcp-server`). The check runs on every transport and even with
12
+ `--validate=false`, because a read must only dispatch a read (#586).
13
+ - A REST request path containing `?`, `#`, `\`, a control character, or a
14
+ `.`/`..` segment, including percent-encoded forms such as `%2e%2e`. The
15
+ URL parser would turn these into a query, a fragment
16
+ or a different path, so the request would not reach the path as written.
17
+ Before this guard, `retrieve '/system/reboot#' --no-validate` posted
18
+ `/rest/system/reboot`.
19
+
20
+ `context.path` names the path; `context.invalidSegments` lists the segments
21
+ that failed the menu grammar.
4
22
 
5
23
  ## Fix
6
24
 
25
+ - Write the path as slash-separated menu names: `/ip/address`,
26
+ `/system/resource`.
27
+ - Select rows with `--query` (for example `--query 'name="a#b"'`), not with
28
+ characters in the path.
29
+
7
30
  See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
8
- contract. This stub will be expanded with the typical trigger and remediation
9
- for `input/invalid-routeros-path`.
31
+ contract.
@@ -0,0 +1,28 @@
1
+ # `input/invalid-stdin`
2
+
3
+ A flag that reads its value from stdin (e.g. `--password-stdin`) received
4
+ bytes that are not valid UTF-8 text, or began with a byte-order mark.
5
+
6
+ `centrs devices add`/`set --password-stdin` strips one trailing newline (`\n`
7
+ or `\r\n`) and decodes the rest as UTF-8. The CDB stores record strings as
8
+ UTF-8 and drops a leading byte-order mark (U+FEFF) when it reads them back, so
9
+ malformed bytes or a BOM would store a credential different from what was
10
+ piped in. centrs refuses instead of guessing. The bytes are never echoed.
11
+
12
+ ## Fix
13
+
14
+ Pipe the password as plain UTF-8 text:
15
+
16
+ ```bash
17
+ printf '%s' "$ROUTER_PW" | centrs devices set 192.0.2.5 --password-stdin
18
+ ```
19
+
20
+ If the value comes from a file an editor saved with a BOM (common on Windows),
21
+ re-save it as UTF-8 without BOM, or strip the first three bytes:
22
+
23
+ ```bash
24
+ tail -c +4 pw.txt | centrs devices set 192.0.2.5 --password-stdin
25
+ ```
26
+
27
+ See [`commands/devices/README.md`](https://github.com/tikoci/centrs/blob/main/commands/devices/README.md)
28
+ for the record-password channels.
@@ -0,0 +1,21 @@
1
+ # `usage/stdin-is-tty`
2
+
3
+ A flag that reads its value from stdin (e.g. `--password-stdin`) was given
4
+ while stdin is a terminal.
5
+
6
+ `--password-stdin` exists for automation: the value arrives on fd 0 so it never
7
+ appears in argv or `ps`. centrs does not prompt for it at a terminal — waiting
8
+ on a keyboard with no prompt would look like a hang, and echoing a typed secret
9
+ would defeat the point. Interactive prompting is tracked separately.
10
+
11
+ ## Fix
12
+
13
+ Pipe or redirect the value:
14
+
15
+ ```bash
16
+ printf '%s' "$ROUTER_PW" | centrs devices add 192.0.2.5 --user admin --password-stdin
17
+ centrs devices add 192.0.2.5 --user admin --password-stdin < pw.txt
18
+ ```
19
+
20
+ See [`commands/devices/README.md`](https://github.com/tikoci/centrs/blob/main/commands/devices/README.md)
21
+ for the record-password channels.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/centrs",
3
- "version": "0.1.9-next.24",
3
+ "version": "0.1.9",
4
4
  "main": "src/index.ts",
5
5
  "module": "src/index.ts",
6
6
  "repository": {
@@ -9,21 +9,21 @@
9
9
  },
10
10
  "homepage": "https://github.com/tikoci/centrs#readme",
11
11
  "bugs": "https://github.com/tikoci/centrs/issues",
12
- "packageManager": "bun@1.4.2",
12
+ "packageManager": "bun@1.4.4",
13
13
  "license": "MIT",
14
14
  "devDependencies": {
15
- "@biomejs/biome": "2.5.15",
15
+ "@biomejs/biome": "2.6.3",
16
16
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.7",
17
- "@types/bun": "1.4.2",
17
+ "@types/bun": "1.4.4",
18
18
  "cloc": "^2.6.0-cloc",
19
19
  "cspell": "^10.3.6",
20
20
  "has-flag": "4.0.0",
21
- "markdownlint-cli2": "^0.23.3",
21
+ "markdownlint-cli2": "^0.24.0",
22
22
  "secretlint": "^13.0.7",
23
23
  "typedoc": "^0.28.20"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@tikoci/quickchr": "^0.4.8"
26
+ "@tikoci/quickchr": "^0.5.2"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "typescript": "^6.0.3"
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "description": "All-in-one TypeScript library/CLI/MCP for interacting with MikroTik RouterOS devices (TUI stub)",
44
44
  "engines": {
45
- "bun": ">=1.3.11 <2"
45
+ "bun": ">=1.4.4 <2"
46
46
  },
47
47
  "private": false,
48
48
  "files": [
@@ -118,6 +118,8 @@
118
118
  "explain:operator-readme:check": "bun run scripts/explain-operator-slice.ts --readme --check",
119
119
  "explain:operator-slice": "bun run scripts/explain-operator-slice.ts",
120
120
  "explain:probe:brace-slots": "bun run scripts/probes/explain-brace-slot-sweep.ts",
121
+ "explain:probe:context-matrix": "bun run scripts/probes/explain-context-matrix.ts",
122
+ "explain:probe:corrected-anchors": "bun run scripts/probes/explain-corrected-anchors.ts",
121
123
  "explain:probe:escapes": "bun run scripts/probes/explain-escape-sweep.ts",
122
124
  "explain:probe:highlight-recapture": "bun run scripts/probes/explain-highlight-recapture.ts",
123
125
  "explain:probe:operators": "bun run scripts/probes/explain-operator-sweep.ts",
@@ -125,6 +127,8 @@
125
127
  "explain:probe:symbol-arms-score": "bun run scripts/probes/explain-symbol-arms-score.ts",
126
128
  "explain:probe:symbol-bad-sigil-replay": "bun run scripts/probes/explain-symbol-bad-sigil-replay.ts",
127
129
  "explain:probe:symbol-classes-replay": "bun run scripts/probes/explain-symbol-classes-replay.ts",
130
+ "explain:probe:import-blocker-validity": "bun run scripts/probes/explain-import-blocker-validity.ts",
131
+ "explain:probe:import-dry-run": "bun run scripts/probes/explain-import-dry-run.ts",
128
132
  "explain:probe:version-floors": "bun run scripts/probes/explain-version-floors.ts",
129
133
  "explain:menus:check": "bun run scripts/gen-explain-menus.ts --check",
130
134
  "explain:value-census": "bun run scripts/explain-value-census.ts",
package/src/api-fanout.ts CHANGED
@@ -62,6 +62,7 @@ import {
62
62
  import { promptForWriteConfirmation } from "./execute.ts";
63
63
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
64
64
  import {
65
+ assertNoMatchInFanout,
65
66
  emptyWriteSelectionError,
66
67
  expandSelection,
67
68
  isDefaultRecordTarget,
@@ -213,6 +214,7 @@ export async function apiFanout(
213
214
  // missing endpoint or an invalid `-X` is an outer error regardless of how many
214
215
  // targets the selection resolves to (so an empty selection cannot mask it).
215
216
  validateApiRequestShape(request);
217
+ assertNoMatchInFanout(request.match);
216
218
  const config = await loadEnvFileDefaults(env);
217
219
  const method = parseApiMethod(request.method);
218
220
  const requestSummary = apiRequestSummaryFromRequest(request, env);
package/src/api.ts CHANGED
@@ -45,6 +45,7 @@ import {
45
45
  isCommandNode,
46
46
  pathTokens,
47
47
  } from "./core/inspect.ts";
48
+ import { assertRouterOsMenuPath } from "./core/menu-path.ts";
48
49
  import {
49
50
  compileQueryWords,
50
51
  type ParsedQuery,
@@ -160,6 +161,12 @@ export interface ApiRequest {
160
161
  verbose?: boolean;
161
162
  cdbFile?: string;
162
163
  cdbPassword?: string;
164
+ /**
165
+ * `--match` selector for a `<router>` several CDB records share
166
+ * (`user=<name>`, `target=<addr>`, or a record type): the same selector
167
+ * `devices show --match` takes. Single-target only (#583).
168
+ */
169
+ match?: string;
163
170
  /** Opt-in host ARP resolution for a MAC target (`none` default, or `arp`). */
164
171
  resolve?: string;
165
172
  stdinIsTty?: boolean;
@@ -937,6 +944,7 @@ export async function resolveApiRequest(
937
944
  targetInput: request.targetInput,
938
945
  cdbFile: request.cdbFile,
939
946
  cdbPassword: request.cdbPassword,
947
+ match: request.match,
940
948
  },
941
949
  env,
942
950
  config,
@@ -2176,6 +2184,10 @@ export function validateApiRequestShape(request: ApiRequest): void {
2176
2184
  "Pass a path with at least one menu segment, such as `ip/address`.",
2177
2185
  });
2178
2186
  }
2187
+ // Before transport choice and regardless of --validate: a `#`/`?` in the
2188
+ // path cut `/print` off the REST URL and a GET read posted to the bare
2189
+ // command, rebooting the router (#586).
2190
+ assertRouterOsMenuPath(normalized.path, { endpoint: request.endpoint });
2179
2191
  }
2180
2192
 
2181
2193
  /**
package/src/btest.ts CHANGED
@@ -113,6 +113,12 @@ export interface BtestClientRequest {
113
113
  natMode?: boolean;
114
114
  cdbFile?: string;
115
115
  cdbPassword?: string;
116
+ /**
117
+ * `--match` selector for a `<router>` several CDB records share
118
+ * (`user=<name>`, `target=<addr>`, or a record type): the same selector
119
+ * `devices show --match` takes. Single-target only (#583).
120
+ */
121
+ match?: string;
116
122
  env?: Record<string, string | undefined>;
117
123
  signal?: AbortSignal;
118
124
  /** Live per-interval frame callback (text/csv streaming). */
@@ -264,6 +270,7 @@ export async function btestClient(
264
270
  targetInput: request.targetInput,
265
271
  cdbFile: request.cdbFile,
266
272
  cdbPassword: request.cdbPassword,
273
+ match: request.match,
267
274
  },
268
275
  env,
269
276
  );
package/src/cli/api.ts CHANGED
@@ -23,7 +23,10 @@ import {
23
23
  formatCentrsErrorText,
24
24
  } from "../errors.ts";
25
25
  import { describeCentrs } from "../index.ts";
26
- import { assertNoQuickchrOverrideConflict } from "../resolver/index.ts";
26
+ import {
27
+ assertNoMatchInFanout,
28
+ assertNoQuickchrOverrideConflict,
29
+ } from "../resolver/index.ts";
27
30
  import {
28
31
  type CliCommandMetadata,
29
32
  expectValue,
@@ -165,6 +168,12 @@ export const apiCommand: CliCommandMetadata = {
165
168
  valueName: "<password>",
166
169
  description: "Decrypt an encrypted WinBox CDB file.",
167
170
  },
171
+ {
172
+ flag: "--match",
173
+ valueName: "<user=name|target=addr|record-type>",
174
+ description:
175
+ "Pick one of several CDB records saved for <router> (as `devices show --match`); single target only.",
176
+ },
168
177
  {
169
178
  flag: "--resolve",
170
179
  valueName: "<none|arp>",
@@ -179,7 +188,7 @@ export const apiCommand: CliCommandMetadata = {
179
188
  {
180
189
  flag: "--validate / --no-validate",
181
190
  description:
182
- "Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted).",
191
+ "Validate structured requests with `/console/inspect`; `/execute` scripts run offline analysis, then RouterOS :parse (default true; `--validate=false` disables both stages).",
183
192
  },
184
193
  {
185
194
  flag: "--format",
@@ -330,6 +339,9 @@ export function parseApiCliArgs(args: readonly string[]): ApiCliArgs {
330
339
  case "--cdb-password":
331
340
  parsed.cdbPassword = expectValue(args, ++index, arg);
332
341
  break;
342
+ case "--match":
343
+ parsed.match = expectValue(args, ++index, arg);
344
+ break;
333
345
  case "--resolve":
334
346
  parsed.resolve = expectValue(args, ++index, arg);
335
347
  break;
@@ -473,6 +485,11 @@ export async function runApiCli(args: readonly string[]): Promise<number> {
473
485
  selectionFlags.quickchr[0] ?? "",
474
486
  );
475
487
  }
488
+ // `--match` + fan-out is refused here too, before `--input -` can block
489
+ // on stdin; `apiFanout` keeps the same check for library callers (#583).
490
+ if (isFanoutMode(selectionFlags, targetPositionals.length)) {
491
+ assertNoMatchInFanout(parsed.match);
492
+ }
476
493
 
477
494
  // Resolve `--input` (file / stdin) before handing the body to the orchestrator.
478
495
  if (parsed.inputPath !== undefined) {
package/src/cli/btest.ts CHANGED
@@ -145,6 +145,12 @@ export const btestCommand: CliCommandMetadata = {
145
145
  valueName: "<password>",
146
146
  description: "(client) Decrypt an encrypted WinBox CDB file.",
147
147
  },
148
+ {
149
+ flag: "--match",
150
+ valueName: "<user=name|target=addr|record-type>",
151
+ description:
152
+ "(client) Pick one of several CDB records saved for <router> (as `devices show --match`); `btest server` rejects it.",
153
+ },
148
154
  {
149
155
  flag: "--format",
150
156
  valueName: `<${btestOutputFormats.join("|")}>`,
@@ -302,6 +308,9 @@ function parseClientArgs(args: readonly string[]): ClientCliArgs {
302
308
  case "--cdb-password":
303
309
  request.cdbPassword = expectValue(args, ++i, arg);
304
310
  break;
311
+ case "--match":
312
+ request.match = expectValue(args, ++i, arg);
313
+ break;
305
314
  case "--csv":
306
315
  parsed.format = "csv";
307
316
  break;
package/src/cli/common.ts CHANGED
@@ -63,6 +63,7 @@ export interface CliCommandMetadata {
63
63
  usage: string;
64
64
  summary: string;
65
65
  options: readonly CliCommandOption[];
66
+ subcommands?: readonly CliCommandMetadata[];
66
67
  }
67
68
 
68
69
  export function expectValue(