@tikoci/centrs 0.1.2 → 0.1.4

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 (134) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/GLOSSARY.txt +284 -0
  3. package/LICENSE +21 -0
  4. package/README.md +55 -11
  5. package/SECURITY.md +3 -3
  6. package/commands/AGENTS.md +7 -1
  7. package/commands/api/README.md +46 -27
  8. package/commands/api/examples.md +124 -6
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +376 -53
  11. package/commands/check/examples.md +390 -0
  12. package/commands/devices/README.md +177 -37
  13. package/commands/devices/examples.md +148 -0
  14. package/commands/discover/README.md +5 -11
  15. package/commands/execute/README.md +73 -26
  16. package/commands/execute/examples.md +131 -9
  17. package/commands/explain/README.md +1716 -0
  18. package/commands/explain/examples.md +606 -0
  19. package/commands/retrieve/README.md +55 -71
  20. package/commands/retrieve/examples.md +56 -9
  21. package/commands/settings/README.md +286 -22
  22. package/commands/settings/examples.md +408 -0
  23. package/commands/terminal/README.md +6 -0
  24. package/commands/transfer/README.md +57 -19
  25. package/commands/transfer/examples.md +95 -0
  26. package/docs/CLI.md +353 -0
  27. package/docs/CONSTITUTION.md +102 -26
  28. package/docs/MATRIX.md +106 -173
  29. package/docs/errors/README.md +5 -1
  30. package/docs/errors/cdb/empty-selection.md +19 -0
  31. package/docs/errors/input/incomplete-gps.md +9 -0
  32. package/docs/errors/input/invalid-altitude.md +9 -0
  33. package/docs/errors/input/invalid-bbox.md +9 -0
  34. package/docs/errors/input/invalid-coordinate.md +9 -0
  35. package/docs/errors/input/invalid-radius.md +9 -0
  36. package/docs/errors/internal/settings-failed.md +9 -0
  37. package/docs/errors/quickchr/machine-not-found.md +22 -0
  38. package/docs/errors/quickchr/machine-stopped.md +20 -0
  39. package/docs/errors/quickchr/package-unavailable.md +27 -0
  40. package/docs/errors/quickchr/unsupported-via.md +35 -0
  41. package/docs/errors/quickchr/unsupported.md +35 -0
  42. package/docs/errors/routeros/version-unsupported.md +26 -0
  43. package/docs/errors/settings/consequential-value.md +26 -0
  44. package/docs/errors/settings/invalid-boolean.md +21 -3
  45. package/docs/errors/settings/invalid-format.md +18 -3
  46. package/docs/errors/settings/invalid-integer.md +18 -3
  47. package/docs/errors/settings/invalid-timeout.md +20 -3
  48. package/docs/errors/settings/invalid-via.md +22 -3
  49. package/docs/errors/settings/reserved-key.md +29 -0
  50. package/docs/errors/settings/skip-env-file-active.md +25 -0
  51. package/docs/errors/settings/unknown-key.md +25 -0
  52. package/docs/errors/transport/provider-trust.md +29 -0
  53. package/docs/errors/usage/fanout-not-supported.md +16 -0
  54. package/docs/errors/usage/stdin-ignored.md +28 -0
  55. package/docs/errors/validation/unknown-attribute.md +37 -4
  56. package/package.json +49 -14
  57. package/src/api-fanout.ts +469 -0
  58. package/src/api.ts +171 -56
  59. package/src/cli/api.ts +156 -13
  60. package/src/cli/btest.ts +38 -4
  61. package/src/cli/common.ts +11 -0
  62. package/src/cli/devices.ts +235 -6
  63. package/src/cli/discover.ts +3 -1
  64. package/src/cli/execute.ts +129 -9
  65. package/src/cli/explain.ts +503 -0
  66. package/src/cli/mcp.ts +8 -1
  67. package/src/cli/retrieve.ts +167 -90
  68. package/src/cli/selection.ts +260 -0
  69. package/src/cli/settings.ts +286 -0
  70. package/src/cli/terminal.ts +22 -7
  71. package/src/cli/transfer.ts +192 -51
  72. package/src/cli.ts +22 -2
  73. package/src/core/envelope.ts +5 -2
  74. package/src/core/error-catalog.ts +95 -0
  75. package/src/core/fanout.ts +143 -2
  76. package/src/core/preflight.ts +306 -0
  77. package/src/core/routeros-string.ts +24 -0
  78. package/src/devices.ts +212 -20
  79. package/src/discover.ts +7 -2
  80. package/src/errors.ts +1 -0
  81. package/src/execute-fanout.ts +453 -0
  82. package/src/execute.ts +216 -68
  83. package/src/explain/arg-tokens.ts +114 -0
  84. package/src/explain/args.ts +1403 -0
  85. package/src/explain/blocks.ts +132 -0
  86. package/src/explain/brace-slots.ts +143 -0
  87. package/src/explain/brace-tokens.ts +38 -0
  88. package/src/explain/catalog.ts +1345 -0
  89. package/src/explain/coordinates.ts +329 -0
  90. package/src/explain/defects.ts +217 -0
  91. package/src/explain/is-known-menu.ts +17 -0
  92. package/src/explain/menus.ts +664 -0
  93. package/src/explain/operator-tokens.ts +310 -0
  94. package/src/explain/operators.ts +458 -0
  95. package/src/explain/path-tokens.ts +150 -0
  96. package/src/explain/pathresolve.ts +1324 -0
  97. package/src/explain/quoted-string.ts +241 -0
  98. package/src/explain/scope-brace.ts +308 -0
  99. package/src/explain/segment.ts +757 -0
  100. package/src/explain/string-tokens.ts +67 -0
  101. package/src/explain/symbol-values.ts +520 -0
  102. package/src/explain/symbols.ts +1597 -0
  103. package/src/explain/token-ranges.ts +72 -0
  104. package/src/explain/transport.ts +497 -0
  105. package/src/explain/value-tokens.ts +80 -0
  106. package/src/explain/values.ts +357 -0
  107. package/src/explain/verbs.ts +64 -0
  108. package/src/explain/verbsplit.ts +813 -0
  109. package/src/explain/write.ts +815 -0
  110. package/src/explain.ts +2152 -0
  111. package/src/index.ts +307 -1
  112. package/src/mcp/config.ts +12 -2
  113. package/src/mcp/tools.ts +137 -2
  114. package/src/protocols/adapter.ts +38 -18
  115. package/src/protocols/index.ts +11 -5
  116. package/src/protocols/mac-telnet-console.ts +61 -17
  117. package/src/resolver/cdb.ts +10 -2
  118. package/src/resolver/comment-kv.ts +58 -4
  119. package/src/resolver/config-file.ts +136 -0
  120. package/src/resolver/facts.ts +78 -0
  121. package/src/resolver/geo.ts +453 -0
  122. package/src/resolver/index.ts +65 -0
  123. package/src/resolver/mac.ts +12 -3
  124. package/src/resolver/quickchr-provider.ts +597 -0
  125. package/src/resolver/selection.ts +391 -0
  126. package/src/resolver/service-endpoint.ts +155 -0
  127. package/src/resolver/settings.ts +54 -5
  128. package/src/resolver/target.ts +21 -3
  129. package/src/retrieve-fanout.ts +239 -133
  130. package/src/retrieve.ts +120 -25
  131. package/src/settings.ts +1078 -0
  132. package/src/terminal.ts +11 -1
  133. package/src/transfer-fanout.ts +544 -0
  134. package/src/transfer.ts +133 -40
package/docs/CLI.md ADDED
@@ -0,0 +1,353 @@
1
+ # centrs CLI reference
2
+
3
+ <!-- GENERATED FILE — do not edit. Regenerate with `bun run docs:cli`. -->
4
+ <!-- Source of truth: the CliCommandMetadata objects in src/cli/*.ts -->
5
+
6
+ Every flag below is generated from the same metadata that renders
7
+ `centrs <command> --help`; CI fails when this file and the metadata drift
8
+ (`bun run docs:cli --check`). Behavior, examples, and designed-but-not-yet
9
+ implemented flags live in each command's `commands/<name>/README.md`.
10
+
11
+ | Command | Summary |
12
+ | ------- | ------- |
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. |
15
+ | [`api`](#api) | Structured RouterOS API passthrough (gh-api style) over REST or native API. |
16
+ | [`transfer`](#transfer) | Copy files to/from a RouterOS device and manage device files (rest/native/sftp). |
17
+ | [`terminal`](#terminal) | Open an interactive RouterOS console (ssh or mac-telnet). |
18
+ | [`explain`](#explain) | Analyze a RouterOS command offline: canonical form, structure, and syntax diagnostics. |
19
+ | [`devices`](#devices) | Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB. |
20
+ | [`discover`](#discover) | Discover RouterOS neighbors over MNDP and optionally save them into the CDB. |
21
+ | [`btest`](#btest) | Run MikroTik's bandwidth test as a client or server (peer measurement). |
22
+ | [`mcp`](#mcp) | Start the centrs MCP server (stdio) — scoped RouterOS tools gated by the CDB allowlist. |
23
+ | [`settings`](#settings) | Manage centrs's own global preferences (`centrs.env`) and inspect the `__default__` CDB record. |
24
+
25
+ ## retrieve
26
+
27
+ Read RouterOS values through the shared core using the selected protocol.
28
+
29
+ ```text
30
+ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <target...> <routeros-path> [flags] | centrs retrieve --group <name> <routeros-path> [flags]
31
+ ```
32
+
33
+ | Flag | Value | Description |
34
+ | ---- | ----- | ----------- |
35
+ | `--via` | `<protocol>` | Pin the protocol selector. Defaults to `rest-api` for retrieve. |
36
+ | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
37
+ | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
38
+ | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
39
+ | `--bbox` | `<south>`,`<west>`,`<north>`,`<east>` | Geo selector: devices whose GPS is inside the lat-first bounding box. |
40
+ | `--all` | | Fan out across every CDB record (excludes `__default__`). |
41
+ | `--default` | | Select the reserved `__default__` record. |
42
+ | `--quickchr` | `<name>` | Target a running quickchr-managed CHR VM by name (repeatable; fans out when repeated). Exclusive of positional targets and CDB selectors; conflicts with direct connection overrides (--host/--port/--username/--password, and --ssh-key where the command has it). |
43
+ | `--concurrency` | `<n>` | Max in-flight targets during fan-out (transport-aware default: rest-api 8, native-api 4). |
44
+ | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL for the target. |
45
+ | `--port` | `<port>` | Override the resolved management port. |
46
+ | `--username / --user / -u` | `<name>` | RouterOS username (aliases `--user`, `-u`). Falls back to `CENTRS_USERNAME`. |
47
+ | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
48
+ | `--timeout` | &lt;ms\|5s&gt; | Operation timeout. REST currently rejects values above 60s. |
49
+ | `--attribute` | `<name>` | Project one attribute. May be repeated. |
50
+ | `--attributes` | `<a,b>` | Project a comma-separated attribute list. |
51
+ | `--all-attributes` | | Request the RouterOS detail/all-attributes shape. |
52
+ | `--list-attributes / --list` | | List inspect-derived attributes without running the data call. |
53
+ | `--query / --filter` | `<expr>` | RouterOS-side row filter (maps to `.query`). Not implemented yet — returns `validation/not-implemented`. |
54
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope. |
55
+ | `--json` | | Shortcut for `--format json`. |
56
+ | `--max-results` | `<bytes>` | Fail instead of printing output larger than the given byte budget. |
57
+ | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
58
+ | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
59
+ | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
60
+ | `--validate / --no-validate` | | Enable or disable inspect-backed preflight validation. |
61
+ | `--verbose` | | Show resolved setting sources in text output. |
62
+
63
+ ## execute
64
+
65
+ Run a RouterOS read or write command via native API, REST, or mac-telnet.
66
+
67
+ ```text
68
+ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> -- <command> [flags] | centrs execute --group <name> <command> [flags]
69
+ ```
70
+
71
+ | Flag | Value | Description |
72
+ | ---- | ----- | ----------- |
73
+ | `--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. |
74
+ | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
75
+ | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
76
+ | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
77
+ | `--bbox` | `<south>`,`<west>`,`<north>`,`<east>` | Geo selector: devices whose GPS is inside the lat-first bounding box. |
78
+ | `--all` | | Fan out across every CDB record (excludes `__default__`). |
79
+ | `--default` | | Select the reserved `__default__` record. |
80
+ | `--quickchr` | `<name>` | Target a running quickchr-managed CHR VM by name (repeatable; fans out when repeated). Exclusive of positional targets and CDB selectors; conflicts with direct connection overrides (--host/--port/--username/--password, and --ssh-key where the command has it). |
81
+ | `--concurrency` | `<n>` | Max in-flight targets during fan-out (transport-aware default: rest-api 8, native-api 4). |
82
+ | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL for the target. |
83
+ | `--port` | `<port>` | Override the resolved management port. |
84
+ | `--username / --user / -u` | `<name>` | RouterOS username (aliases `--user`, `-u`). Falls back to `CENTRS_USERNAME`. |
85
+ | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
86
+ | `--ssh-key` | `<path>` | `--via ssh`: explicit private-key path. Falls back to `CENTRS_SSH_KEY` / the `ssh-agent`. |
87
+ | `--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. |
88
+ | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
89
+ | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
90
+ | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
91
+ | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
92
+ | `--validate / --no-validate` | | Run RouterOS :parse and /console/inspect validation before execution (default true; `--validate=false` also accepted). |
93
+ | `--yes` | | Confirm write-shaped add/set/remove commands in non-interactive runs. |
94
+ | `--max-results` | `<bytes>` | Fail if the rendered envelope exceeds this byte budget. |
95
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope. |
96
+ | `--json` | | Shortcut for `--format json`. |
97
+ | `--verbose` | | Include additional context in text output. |
98
+ | `--` | | End centrs option parsing: every following token is the literal RouterOS command, even flag-shaped ones (e.g. `-- /interface print where disabled=yes`). |
99
+
100
+ ## api
101
+
102
+ Structured RouterOS API passthrough (gh-api style) over REST or native API.
103
+
104
+ ```text
105
+ Usage: centrs api <router> <endpoint> [flags]
106
+ ```
107
+
108
+ | Flag | Value | Description |
109
+ | ---- | ----- | ----------- |
110
+ | `-X / --method` | `<verb>` | HTTP method, default GET (case-insensitive). GET→print, PUT→add, PATCH→set, DELETE→remove, POST→run. |
111
+ | `-f / --field` | `<key=value>` | Body field, repeatable; assembled into the JSON body (verbatim string values). |
112
+ | `-d / --data` | `<json>` | Raw JSON request body. Conflicts with `-f` / `--input`. |
113
+ | `--input` | &lt;file\|-&gt; | Read the raw JSON body from a file or stdin (`-`). |
114
+ | `--query / --filter` | `<expr>` | RouterOS-side row filter, AND-combined, repeatable: name=value, name!=value, name>value, name<value, name. |
115
+ | `--raw-query` | `<word>` | Verbatim RouterOS query word (repeatable) for OR / absence / stack expressions. |
116
+ | `--attribute / --proplist` | `<a,b>` | Property projection → `.proplist`. |
117
+ | `--raw` | | Strip the envelope; emit bare RouterOS JSON. Implies `--validate=false`; does not imply `--yes`. |
118
+ | `--yes` | | Confirm a mutating (non-read) request in non-interactive runs. |
119
+ | `--stream / --listen` | | Follow changes as an NDJSON envelope stream (native-api only; the `/listen` endpoint infers it). Ends with a summary envelope. |
120
+ | `--count` | `<n>` | Stop a `--stream` after N change frames. |
121
+ | `--duration` | `<dur>` | Stop a `--stream` after this wall-clock window (e.g. `5s`). |
122
+ | `--via` | &lt;rest-api\|native-api&gt; | Pin the transport; no silent downgrade. Default rest-api. |
123
+ | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
124
+ | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
125
+ | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
126
+ | `--bbox` | `<south>`,`<west>`,`<north>`,`<east>` | Geo selector: devices whose GPS is inside the lat-first bounding box. |
127
+ | `--all` | | Fan out across every CDB record (excludes `__default__`). |
128
+ | `--default` | | Select the reserved `__default__` record. |
129
+ | `--quickchr` | `<name>` | Target a running quickchr-managed CHR VM by name (repeatable; fans out when repeated). Exclusive of positional targets and CDB selectors; conflicts with direct connection overrides (--host/--port/--username/--password, and --ssh-key where the command has it). |
130
+ | `--concurrency` | `<n>` | Max in-flight targets during fan-out (transport-aware default: rest-api 8, native-api 4). |
131
+ | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL for the target. |
132
+ | `--port` | `<port>` | Override the resolved management port. |
133
+ | `--username / --user / -u` | `<name>` | RouterOS username. Falls back to `CENTRS_USERNAME`. |
134
+ | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
135
+ | `--insecure` | | Accept a self-signed `api-ssl`/REST TLS cert. Default verifies. |
136
+ | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
137
+ | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
138
+ | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
139
+ | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
140
+ | `--validate / --no-validate` | | Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted). |
141
+ | `--format` | &lt;json\|yaml\|text&gt; | Output format. Defaults to json for api; `CENTRS_FORMAT` overrides. |
142
+ | `--json` | | Shortcut for `--format json`. |
143
+ | `--verbose` | | Include additional context in text output. |
144
+
145
+ ## transfer
146
+
147
+ Copy files to/from a RouterOS device and manage device files (rest/native/sftp).
148
+
149
+ ```text
150
+ Usage: centrs transfer <router> upload <local> [remote] | download <remote> [local] | list [path] | remove <remote> | mkdir <remote> | copy <src> <dst> [flags]
151
+ ```
152
+
153
+ | Flag | Value | Description |
154
+ | ---- | ----- | ----------- |
155
+ | `--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. |
156
+ | `--ssh-key` | `<path>` | sftp only: explicit private-key path. Falls back to CENTRS_SSH_KEY / the ssh-agent. |
157
+ | `--insecure` | | Accept a self-signed TLS cert (https/api-ssl) or a new SSH host key. Default verifies. |
158
+ | `--force / --overwrite` | | Replace an existing destination. Default refuses it. |
159
+ | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
160
+ | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
161
+ | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
162
+ | `--bbox` | `<south>`,`<west>`,`<north>`,`<east>` | Geo selector: devices whose GPS is inside the lat-first bounding box. |
163
+ | `--all` | | Fan out across every CDB record (excludes `__default__`). |
164
+ | `--default` | | Select the reserved `__default__` record. |
165
+ | `--quickchr` | `<name>` | Target a running quickchr-managed CHR VM by name (repeatable; fans out when repeated). Exclusive of positional targets and CDB selectors; conflicts with direct connection overrides (--host/--port/--username/--password, and --ssh-key where the command has it). |
166
+ | `--concurrency` | `<n>` | Max in-flight targets during fan-out (transport-aware default: rest-api 8, native-api 4). |
167
+ | `--out-dir` | `<dir>` | `download` fan-out only: write one file per target into `<dir>`, named by CDB identity. Required when downloading across a selection. |
168
+ | `--yes` | | Confirm a mutating fan-out (upload/remove/mkdir/copy) across multiple routers in non-interactive runs. |
169
+ | `--verify / --no-verify` | &lt;size\|checksum\|off&gt; | Post-transfer integrity check. Default size; `--no-verify` skips. |
170
+ | `--type` | &lt;file\|directory\|disk\|package&gt; | list filter: RouterOS /file row type. |
171
+ | `--name` | `<glob>` | list filter: file-name glob. |
172
+ | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL. |
173
+ | `--port` | `<port>` | Override the resolved management port. |
174
+ | `--username / --user / -u` | `<name>` | RouterOS username (aliases --user, -u). Falls back to CENTRS_USERNAME. |
175
+ | `--password` | `<secret>` | RouterOS password. Falls back to CENTRS_PASSWORD. |
176
+ | `--timeout` | &lt;ms\|5s&gt; | Operation timeout. REST rejects values above 60s. |
177
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format. Defaults to text; --json / --yaml shortcuts. |
178
+ | `--json` | | Shortcut for --format json. |
179
+ | `--yaml` | | Shortcut for --format yaml. |
180
+ | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
181
+ | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
182
+ | `--validate / --no-validate` | | Enable or disable the existence-probe preflight. |
183
+ | `--verbose` | | Show the resolved transport in text output. |
184
+
185
+ ## terminal
186
+
187
+ Open an interactive RouterOS console (ssh or mac-telnet).
188
+
189
+ ```text
190
+ Usage: centrs terminal <router> [flags]
191
+ ```
192
+
193
+ | Flag | Value | Description |
194
+ | ---- | ----- | ----------- |
195
+ | `--via` | `<method>` | Transport: `ssh` (host target, default) or `mac-telnet` (MAC target, default for a MAC). rest/native have no terminal capability. |
196
+ | `--host` | `<host>` | Override the target host (ssh) / UDP delivery host (mac-telnet, default L2 broadcast). |
197
+ | `--port` | `<port>` | Override the port: ssh default 22, mac-telnet default 20561. |
198
+ | `--username / --user / -u` | `<name>` | RouterOS username (aliases --user, -u). Falls back to CENTRS_USERNAME. |
199
+ | `--password` | `<secret>` | RouterOS password. Falls back to CENTRS_PASSWORD. |
200
+ | `--source-mac` | `<mac>` | mac-telnet: explicit in-packet source MAC (overrides egress resolution). |
201
+ | `--ssh-key` | `<path>` | `--via ssh`: explicit private-key path. Falls back to `CENTRS_SSH_KEY` / the `ssh-agent`. |
202
+ | `--insecure` | | `--via ssh`: disable host-key verification (accepts changed/impersonated keys). Default verifies. |
203
+ | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
204
+ | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
205
+ | `--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. |
206
+ | `--format` | &lt;text\|json\|yaml&gt; | Error-envelope format on failure. --json / --yaml shortcuts. |
207
+ | `--json` | | Shortcut for --format json. |
208
+ | `--yaml` | | Shortcut for --format yaml. |
209
+ | `--verbose` | | Verbose error output. |
210
+
211
+ ## explain
212
+
213
+ Analyze a RouterOS command offline: canonical form, structure, and syntax diagnostics.
214
+
215
+ ```text
216
+ Usage: centrs explain '<input>' [flags]
217
+ ```
218
+
219
+ | Flag | Value | Description |
220
+ | ---- | ----- | ----------- |
221
+ | `--file` | `<path>` | Read the input from a file instead of the positional (`-` reads stdin). Piped stdin is used automatically when no input positional is given. |
222
+ | `--fail-on` | &lt;error\|warning\|never&gt; | Exit 2 when the verdict reaches this severity. Default `error`; an ambiguous or unknown statement is a warning, never an error. |
223
+ | `--complete` | | Continuation candidates at the cursor. Live evidence — offline emits a tip and enumerates nothing. |
224
+ | `--schema` | | Path enumeration (verbs, args, types, enums). Live evidence — offline emits a tip and enumerates nothing. |
225
+ | `--curl` | | Render a ready-to-edit REST curl for statements covered by a runtime-tested mapping rule. |
226
+ | `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (provisional `class` until #264 B5: every byte not claimed by an analyzer is `unclassified`). |
227
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
228
+ | `--json` | | Shortcut for `--format json`. |
229
+ | `--yaml` | | Shortcut for `--format yaml`. |
230
+ | `--verbose` | | Verbose error output. |
231
+
232
+ ## devices
233
+
234
+ Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB.
235
+
236
+ ```text
237
+ Usage: centrs devices <list|show|groups|add|edit|set|remove> [args] [flags]
238
+ ```
239
+
240
+ | Flag | Value | Description |
241
+ | ---- | ----- | ----------- |
242
+ | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
243
+ | `--cdb-password` | `<secret>` | Decrypt an encrypted CDB. Falls back to `CENTRS_CDB_PASSWORD`. Also used to re-encrypt on writes. |
244
+ | `--via` | `<protocol>` | Resolve and report the protocol source for provenance examples; no network IO is performed. |
245
+ | `--group` | `<name>` | `list` filter / `add`,`edit` first-class group field for the target entry. |
246
+ | `--where` | `<attr=value>` | `list` only — repeatable device-class filter over CDB facts (AND-combined), e.g. `--where lat=37.7749`. |
247
+ | `--near` | `<lat>`,`<lon>`,`<radius>` | `list` only — GPS filter: devices within radius (m/km/mi/ft; bare number = km). Lat-first. |
248
+ | `--bbox` | `<south>`,`<west>`,`<north>`,`<east>` | `list` only — GPS filter: devices inside the lat-first bounding box. |
249
+ | `--members` | | `groups` only — expand each group's membership. |
250
+ | `--explain` | | `show` only — include the raw `WinBoxCdbRecord` in `data.record`. |
251
+ | `--match` | &lt;user=…\|target=…\|record-type&gt; | `show` — disambiguate when `<router>` matches several records: `user=<name>`, `target=<addr>`, or a record-type token. |
252
+ | `--user` | `<name>` | `add`,`set` — first-class CDB user field. |
253
+ | `--password` | `<secret>` | `add`,`set` — first-class CDB password field. |
254
+ | `--profile` | `<name>` | `add`,`set` — named WinBox profile (Workspace). |
255
+ | `--profile-none` | | `add`,`set` — write the WinBox `<none>` profile sentinel. |
256
+ | `--profile-own` | | `add`,`set` — write the WinBox `<own>` profile sentinel. |
257
+ | `--session` | `<name>` | `add`,`set` — first-class CDB session field. |
258
+ | `--comment` | `<text>` | `add` only — base free-form comment (may include kv-soup). |
259
+ | `--record-type` | &lt;macTarget\|ipAdmin\|ipUser\|romonNeighbor\|romonTarget&gt; | `add` only — record type (default `ipAdmin`). |
260
+ | `--lat / --latitude` | `<deg>` | `add`,`set` — latitude in decimal degrees (-90..90); a comment-kv fact, paired with --lon. |
261
+ | `--lon / --lng / --longitude / --long` | `<deg>` | `add`,`set` — longitude in decimal degrees (-180..180); a comment-kv fact, paired with --lat. |
262
+ | `--altitude / --alt / --ele / --elevation` | `<meters>` | `add`,`set` — altitude in meters (may be negative); a comment-kv fact. |
263
+ | `--altitude-type / --alt-type` | &lt;MSL\|AGL&gt; | `add`,`set` — vertical datum for --altitude (default MSL); case-insensitive. |
264
+ | `--gps` | `<lat>`,`<lon>`[,`<altitude>`[,`<altitude-type>`]] | `add`,`set` — combined lat,lon[,altitude[,altitude-type]] convenience (lat-first; missing altitude-type defaults MSL). |
265
+ | `--force` | | `add` only — overwrite the existing (target, user) entry. |
266
+ | `--strict` | | `add`,`set` — reject unknown comment kv keys instead of warning. |
267
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
268
+ | `--json` | | Shortcut for `--format json`. |
269
+
270
+ ## discover
271
+
272
+ Discover RouterOS neighbors over MNDP and optionally save them into the CDB.
273
+
274
+ ```text
275
+ Usage: centrs discover [--timeout 15s] [--save] [flags]
276
+ ```
277
+
278
+ | Flag | Value | Description |
279
+ | ---- | ----- | ----------- |
280
+ | `--timeout` | &lt;ms\|15s&gt; | Listen window before results are returned. Default 15s. |
281
+ | `--save` | | Persist discovered neighbors into the CDB (`group=discovered`, `source=mndp`). |
282
+ | `--group` | `<name>` | `--save` group for new entries. Default `discovered`. |
283
+ | `--port` | `<port>` | UDP port to bind for MNDP. Default 5678. |
284
+ | `--cdb-file` | `<path>` | `--save` CDB path override (default `~/.config/tikoci/winbox.cdb`). |
285
+ | `--cdb-password` | `<secret>` | `--save` password for an encrypted CDB. Used to decrypt the existing CDB and re-encrypt on write. |
286
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
287
+ | `--json` | | Shortcut for `--format json`. |
288
+
289
+ ## btest
290
+
291
+ Run MikroTik's bandwidth test as a client or server (peer measurement).
292
+
293
+ ```text
294
+ Usage: centrs btest <client <router>|server> [flags]
295
+ ```
296
+
297
+ | Flag | Value | Description |
298
+ | ---- | ----- | ----------- |
299
+ | `--protocol` | &lt;udp\|tcp&gt; | (client) Transport. Default udp. |
300
+ | `--direction` | &lt;receive\|transmit\|both&gt; | (client) Test direction. Default receive. |
301
+ | `--duration` | `<dur>` | Bound the run (e.g. 15s). Omit for open-ended (Ctrl-C). |
302
+ | `--interval` | `<dur>` | Report cadence (20ms..5s). Default 1s. |
303
+ | `--connection-count` | `<n>` | (client, TCP only) Parallel TCP data connections (1..255). |
304
+ | `--local-udp-tx-size` | `<n>` | (client, UDP only) Client→server packet size (28..64000). |
305
+ | `--remote-udp-tx-size` | `<n>` | (client, UDP only) Server→client packet size (28..64000). |
306
+ | `--local-tx-speed` | `<bps>` | (client) Cap on client→server rate (e.g. 100M). |
307
+ | `--remote-tx-speed` | `<bps>` | (client) Cap on server→client rate (e.g. 100M). |
308
+ | `--random-data` | | (client) Incompressible payload. |
309
+ | `--nat-mode` | | (client, UDP only) Originate an outbound datagram first so server→client data can traverse NAT. Automatic when the test receives. |
310
+ | `--authenticate / --no-authenticate` | | (server) Require EC-SRP5 auth. Default true; `--authenticate=false` also accepted. |
311
+ | `--bind` | `<addr>` | (server) Listen address. Default 127.0.0.1. |
312
+ | `--allocate-udp-ports-from` | `<n>` | (server) Base of the UDP data-port range. Default 2001. |
313
+ | `--max-sessions` | `<n>` | (server) Concurrent test cap (1..1000). Default 100. |
314
+ | `--user / --username / -u` | `<name>` | Credential (aliases --username, -u; falls back to CDB / CENTRS_USERNAME). |
315
+ | `--password` | `<secret>` | Credential (falls back to CDB / CENTRS_PASSWORD). |
316
+ | `--port` | `<port>` | Control port. Default 2000. |
317
+ | `--cdb-file` | `<path>` | (client) Read target credentials from a WinBox CDB file. |
318
+ | `--cdb-password` | `<password>` | (client) Decrypt an encrypted WinBox CDB file. |
319
+ | `--format` | &lt;text\|csv\|json\|yaml&gt; | Output format. Default text. |
320
+ | `--csv` | | Shortcut for `--format csv`. |
321
+
322
+ ## mcp
323
+
324
+ Start the centrs MCP server (stdio) — scoped RouterOS tools gated by the CDB allowlist.
325
+
326
+ ```text
327
+ Usage: centrs mcp [start] [--cdb-file <path>] [--allow-adhoc-targets]
328
+ ```
329
+
330
+ | Flag | Value | Description |
331
+ | ---- | ----- | ----------- |
332
+ | `--cdb-file` | `<path>` | CDB allowlist path (default `~/.config/tikoci/winbox.cdb`, or CENTRS_CDB_FILE). |
333
+ | `--cdb-password` | `<password>` | Decrypt password for an encrypted CDB (or CENTRS_CDB_PASSWORD). |
334
+ | `--allow-adhoc-targets` | | Reserved unsafe escape hatch for future inline targets; current tools still use CDB-only targets. |
335
+ | `--help` | | Show this help. |
336
+
337
+ ## settings
338
+
339
+ Manage centrs's own global preferences (`centrs.env`) and inspect the `__default__` CDB record.
340
+
341
+ ```text
342
+ Usage: centrs settings [print|get|set|reset] [args] [flags]
343
+ ```
344
+
345
+ | Flag | Value | Description |
346
+ | ---- | ----- | ----------- |
347
+ | `--all` | | `print` only — also list unrecognized CENTRS_* lines found in the file. |
348
+ | `--cdb-file` | `<path>` | `print` only — CDB to probe for the `__default__` record summary (falls back to normal CDB resolution; never centrs.env). |
349
+ | `--cdb-password` | `<secret>` | `print` only — decrypt password for the probed CDB. |
350
+ | `--skip-env-file` | | `print` only — note that other commands in this environment would not see centrs.env (settings itself always reads the real file). |
351
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
352
+ | `--json` | | Shortcut for `--format json`. |
353
+ | `--help` | | Show this help. |
@@ -37,21 +37,29 @@ re-validate-server-side. Validation is not optional polish.
37
37
  attribute inspection. The path syntax (commas, no leading slash, last token
38
38
  is the verb) is part of the validator contract.
39
39
  - `execute` and other CLI-shaped calls validate with `:put [:parse "..."]` plus
40
- a semantic gate. **How `:parse` surfaces an error is transport-specific**
41
- (grounded on CHR 7.23.1): `:parse` *does* reject an unknown attribute
42
- (`bad parameter <name>`) and malformed CLI (`syntax error`), but over REST that
43
- text rides the **HTTP-200 `ret` value** (no error status), and over the
44
- **native API** `:put [:parse]` returns an opaque `*NN` handle that reveals
45
- nothing at all. centrs's REST/native syntax gate runs `:parse` but does not
46
- read the `ret`, so for those transports the unknown-attribute catch comes from
47
- the separate **`/console/inspect`** semantic gate (or the server's own
48
- write re-validation); the `:parse` round-trip there mainly backstops transport
49
- errors and the local quote preflight. Over **mac-telnet** the interactive
50
- console *prints* the `:parse` result, so centrs reads it and a single console
51
- `:parse` covers both syntax and the unknown-attribute (semantic) gate — no
52
- `/console/inspect` table needed. In every case a clean parse is necessary, not
53
- sufficient on its own; semantic validation is a distinct, transport-appropriate
54
- step (`/console/inspect`, the console `:parse` text, or server re-validation).
40
+ a semantic gate. Grounded on CHR 7.23.3 (GH#230), **`:parse` never throws** —
41
+ it returns a script value whose printed form carries the verdict. For a bare
42
+ menu path a clean parse prints `(evl /canonical/path)` with abbreviations
43
+ expanded; for script-shaped input (e.g. `:put …`, `:global …`, `where …`) the
44
+ accepted return echoes the script body and may contain arbitrary text such as
45
+ `expected`. Rejection forms are `(evl bad parameter <name> (line N column M)
46
+ /path)`, `(<%% bad command name <name> (line N column M) …)`, and the
47
+ line-anchored `expected … (line N column M)` (spellings: `expected end of
48
+ command`, `expected command name`, `expected input value`) / `syntax error
49
+ (line N column M)`. That text rides the **HTTP-200 `ret` value** (REST,
50
+ `as-string`), the native `as-string` `ret`, and the console's printed output
51
+ alike — so a single `:put [:parse ...]` covers both the syntax and the
52
+ **unknown-attribute** gate on every transport. centrs sends `as-string` and now
53
+ reads the `ret`/printed value (the previous gate ignored it, so it rejected
54
+ nothing). Because the accepted return echoes the body, the matcher must anchor
55
+ on the diagnostic shape (the `(<%%` wrapper, or a trailing `(line N column M)`)
56
+ and never on a bare English word — `expected` is also a real
57
+ `/ip/firewall/connection` field name.
58
+ That unknown-attribute check is **name-level only**: `:parse` does not validate
59
+ parameter *values* (`/ip/address/add address=not-an-ip` parses clean). So a
60
+ clean parse is necessary, not sufficient, and value-type plus the remaining
61
+ semantic validation stay distinct steps — `/console/inspect` or the server's
62
+ own re-validation on the write round-trip.
55
63
  - **Peer-measurement commands (`btest`) are exempt from this gate** — they issue
56
64
  no RouterOS command, so they validate *option-grammar* + the **EC-SRP5** auth
57
65
  exchange instead (the envelope, identity-resolution, and friendly-error
@@ -221,24 +229,82 @@ must round-trip through the same envelope.
221
229
 
222
230
  One grammar across every non-terminal command. A call selects targets from any
223
231
  mix of: `<router>` positionals (one or more), repeatable `--group <name>`,
224
- `--all` (every CDB record), `--default` (the `__default__` record), and
225
- repeatable `--where <attr>=<value>` — a **device-class** selector that matches
226
- CDB-stored facts/comment-kv (e.g. `--where board=RB5009`), AND-combined across
227
- repeats. The union is **de-duped by CDB record index** and run through the same
228
- per-target pipeline; the `data` array is reassembled in resolved record order,
229
- not completion order, so repeated runs diff cleanly. Destructive multi-target
230
- operations (e.g. `devices remove` across a group) require `--force`. `terminal`
231
- and a single-session `api --stream` follow are not fan-out surfaces and reject
232
- N>1 with `usage/fanout-not-supported`.
232
+ `--all` (every CDB record, **excluding** the reserved `__default__` record),
233
+ `--default` (the `__default__` record), and repeatable `--where <attr>=<value>` —
234
+ a **device-class** selector that matches CDB-stored facts/comment-kv **plus core
235
+ record fields** (`target`/`identity`/`group`/`mac`; core fields win so a comment
236
+ token cannot spoof them), AND-combined across repeats. A call is in **fan-out
237
+ mode** when any selector flag is present **or** more than one positional target is
238
+ given; a plain single-positional call stays single-target. The union is
239
+ **de-duped by CDB record index** (ad-hoc literal targets by host) and run through
240
+ the same per-target pipeline.
241
+
242
+ Fan-out output is the locked **`FanoutData`** envelope: `data = { summary, targets[]
243
+ }`, where the outer `ok` reports **orchestration** success and a per-target failure
244
+ is an inner `ok:false` envelope (output, not metadata). `targets[]` is reassembled
245
+ in resolved record order, not completion order, so repeated runs diff cleanly. The
246
+ **process exit code** is granular and uniform across fan-out commands: `0` every
247
+ target ok (or an empty selection), `2` partial (some ok, some failed), `1`
248
+ orchestration error or every target failed.
249
+
250
+ A multi-target **RouterOS write** (e.g. `execute` / `api` add/set/remove across a
251
+ selection) is gated by **`--yes`**, confirmed **once** up front; when it is missing
252
+ the error names the blast radius (how many routers). The separate **`--force`**
253
+ gate is scoped to destructive **`devices` CDB mutations** (e.g. `devices remove`
254
+ across a group), not RouterOS writes. `terminal` and single-session `stream` /
255
+ `api --stream` are not fan-out surfaces and reject N>1 with
256
+ `usage/fanout-not-supported`; `api --raw` is rejected in fan-out mode with
257
+ `usage/conflicting-flags` (it strips the per-target envelope).
233
258
 
234
259
  `--where` filters *which devices* by CDB-stored facts; keep it distinct from
235
260
  `retrieve`/`execute`'s `--query`/`--filter`, which filter *RouterOS rows* in the
236
- response. Two layers, two flag families.
261
+ response. Two layers, two flag families. `--near <lat>,<lon>,<radius>` and
262
+ `--bbox <south>,<west>,<north>,<east>` are geo union predicates over device GPS
263
+ (**lat-first, decimal degrees**; radius units `m`/`km`/`mi`/`ft`, bare = km);
264
+ geo-less devices never match. They join the same union — a device matches when it
265
+ falls in `--near`'s radius **or** `--bbox`.
237
266
 
238
267
  `--group` is purely a **selector** in this grammar; the group *field* is written
239
268
  by a distinct `--new-group` flag on `devices add`/`set`, so record selection and
240
269
  field-setting never collide on one flag.
241
270
 
271
+ #### Resolution providers
272
+
273
+ Target resolution has **two independent axes**, and centrs keeps them separate so
274
+ the resolver is generalized **once**, not per source (the umbrella plan is #174):
275
+
276
+ - **Axis A — selection** ("which devices"): the target-selection grammar above.
277
+ - **Axis B — connection-fact origin** ("where host/port/auth for a member come
278
+ from"): what a resolved member yields. Today: a **CDB record** or an ad-hoc
279
+ **literal** (+ the `__default__` ladder). Tomorrow: a live descriptor or an
280
+ inventory entry.
281
+
282
+ Sources fall into two kinds, and the distinction is normative:
283
+
284
+ - **Selector-source** — participates in the Axis-A grammar. **CDB** (now), and
285
+ **TikTOML** next (#137). Multiple selector-sources union into one selection;
286
+ the split-brain precedence between them is #137's problem, not resolved here.
287
+ - **Named-live-provider** — addressed *only* by an explicit named flag, **never**
288
+ matched by a selector; fanned out by repeating the flag. Because it owns an
289
+ **ephemeral** endpoint it also **bypasses** the settings/`__default__` ladder and
290
+ **conflicts** with `--host`/`--port`/`--username`/`--password`/`--ssh-key`. These
291
+ are properties of *live/ephemeral* providers, not of any one provider. **quickchr**
292
+ (`--quickchr <name>`, #134) is the first. A named-live-provider selection is
293
+ **exclusive** (v1): mixing it with `<router>` positionals or selector flags is
294
+ `usage/conflicting-flags` — one flag stays single-target, repeating it fans
295
+ out. Relaxing exclusivity later is additive.
296
+
297
+ Connection facts resolve into one neutral **per-service endpoint map**
298
+ (`src/resolver/service-endpoint.ts`: `via → ServiceEndpoint`, each carrying
299
+ availability, host, port, TLS, auth, and provenance). A **selector-source** is the
300
+ *degenerate single-endpoint* case — one record materialized lazily once `--via` is
301
+ known. A **named-live-provider** populates the map directly, since it genuinely
302
+ exposes different facts per transport. Providers must **not** be flattened to a
303
+ single host/port: that shape is the contract quickchr's descriptor and TikTOML's
304
+ `services` block both map onto, and flattening it forces exactly the re-plumb #174
305
+ exists to prevent. A `--via` whose service key a provider does not supply is a
306
+ typed unsupported-provider error, never a silent fallback.
307
+
242
308
  ### MCP surface: the CDB is the allowlist
243
309
 
244
310
  The MCP frontend (`commands/mcp/`) is an adapter over this core, not a new set of
@@ -281,7 +347,11 @@ Rules:
281
347
  - **Never silently downgrade across `--via`.** If the caller pinned a transport
282
348
  that cannot do the operation, error out with a `transport/*` code.
283
349
  Auto-selection (no `--via`) may downgrade *within* the table, but every hop is
284
- reported in `meta.warnings` with its reason.
350
+ reported in `meta.warnings` with its reason. For `api`'s fan-out path, this
351
+ and "Validation is the product" above are also checked structurally by
352
+ CodeQL (`.github/codeql-house-rules/missing-required-gate.ql` —
353
+ `apiFanout` must call `assertApiProtocolSupported`/`validateApiRequestShape`;
354
+ scoped to `apiFanout` only today).
285
355
  - **Execute vs retrieve surfaces:** native-api, rest-api, ssh, mac-telnet (and
286
356
  later RoMON / WinBox Terminal) are execute surfaces; SNMP is **retrieve-only**.
287
357
  mac-telnet is the default execute path for an unresolved-MAC target — IP-level
@@ -415,3 +485,9 @@ canonicalization only, never the structured-mode predicate. The gate contract,
415
485
  its pinning test, and the parser-vendoring preconditions are documented in
416
486
  `commands/execute/README.md` and the `canonicalizeExecuteCommand` doc-comment
417
487
  (`src/execute.ts`).
488
+
489
+ Richer schema-free analysis must distinguish **ambiguous** (multiple plausible
490
+ readings) from **unknown** (no safe reading) and abstain in both cases rather than
491
+ inventing a command, path, symbol class, or transport mapping. These explain-only
492
+ verdicts never widen or reinterpret the execution gate's `mode` or `writeShaped`
493
+ decision.