@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
@@ -1,5 +1,9 @@
1
1
  # retrieve
2
2
 
3
+ Status: `CHR-passed` over `rest-api` and `native-api`, including multi-target
4
+ fan-out (see **Target selection**). `snmp` is `not-started`. Matches
5
+ `docs/MATRIX.md`.
6
+
3
7
  Read RouterOS state. RouterOS menu reads model `<path>/<verb>` where the verb
4
8
  is `print`-style (`print`, `get`, and async POST-shaped reads as they're
5
9
  added). SNMP reads use `retrieve <router> snmp <oid|MIB name>` and resolve
@@ -39,33 +43,32 @@ centrs retrieve <router> snmp <oid|MIB name> [flags]
39
43
 
40
44
  ## Flags
41
45
 
42
- | Flag | Behavior |
46
+ Implemented flags are generated from the CLI metadata into
47
+ [`docs/CLI.md` → retrieve](../../docs/CLI.md#retrieve); this file does not
48
+ duplicate that table. Behavior notes the generated reference cannot carry:
49
+
50
+ - `--format json` (shortcut `--json`): set `CENTRS_FORMAT=json` to make the
51
+ structured envelope the default. `--format yaml` renders the same envelope.
52
+ - `--port`: native-api defaults to 8728; `--port 8729` selects TLS `api-ssl`.
53
+ - `--query`/`--filter` are RouterOS **row** filters and always return an
54
+ array (see Synopsis); `--where` filters *which devices* fan out, not rows.
55
+ - `--all-attributes` conflicts with `--attribute(s)` (see Validation).
56
+ - `--quickchr` resolves host/port/auth from the live `@tikoci/quickchr`
57
+ (0.4.5+, optional dependency) descriptor, bypassing CDB/env resolution for
58
+ those fields.
59
+
60
+ ### Designed, not implemented
61
+
62
+ Spec-tier flags with no implementation yet — today they fail as unknown flags:
63
+
64
+ | Flag | Designed behavior |
43
65
  | ----------------------------------- | ----------------------------------------------------------------------------------------- |
44
- | `--attribute <name>` | Project a single attribute. Repeatable, or comma-separated. |
45
- | `--attributes <a,b,c>` | Alias for `--attribute` with comma-separated input. |
46
- | `--all-attributes` | Equivalent to RouterOS `details=true`. Mutually exclusive with `--attribute(s)`. |
47
- | `--list-attributes` (alias `--list`) | Return the available attribute names for the path. No `print`/`get` is run. |
48
66
  | `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is `api … --stream`. See constitution: protocol selection. |
49
- | `--query <expr>` | RouterOS-side **row** filter (maps to `.query`), repeatable; returns matching rows as an **array**. This is how you read one row by name (`--query name=ether1`). **Not Implemented** yet — returns `validation/not-implemented`. |
50
- | `--filter <expr>` | RouterOS row filter; same `.query` mapping and not-implemented handling as `--query`. |
51
- | `--where <attr>=<value>` | Device-class selector: fan out across CDB records whose stored fact/comment-kv matches (e.g. `--where board=RB5009`). Repeatable (AND). Filters *which devices*, not RouterOS rows. See constitution: target selection. |
52
- | `--max-bytes <n>` | Byte budget for the rendered payload. If the response would exceed it, centrs truncates to fit, keeps `ok: true`, and adds a warning + `meta.truncated`. Not an error. (Renamed from `--max-results`; the legacy flag still parses until the code split lands.) |
67
+ | `--max-bytes <n>` | Byte budget for the rendered payload. If the response would exceed it, centrs truncates to fit, keeps `ok: true`, and adds a warning + `meta.truncated`. Not an error. (Will replace the implemented `--max-results`, which fails instead of truncating.) |
53
68
  | `--max-rows <n>` | Maximum row count for list reads. Excess rows are clipped; `ok: true` with a warning + `meta.truncated`. |
54
- | `--format text` (default) | Human-readable rendering (default). Errors print `[code] summary` + `Fix:` lines. |
55
- | `--format json` (alias `--json`) | Structured JSON envelope (set `CENTRS_FORMAT=json` to make it the default). |
56
- | `--format yaml` (alias `--yaml`) | YAML rendering of the same envelope. |
57
- | `--resolve <none\|arp>` | Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
58
- | `--via <protocol>` | Pin the transport (`rest-api` default, or `native-api`). No silent downgrade. See constitution: protocol selection. |
59
- | `--validate=false` | Escape hatch; default is `true`. See constitution: validation is the product. |
60
- | `--timeout <ms>` | Request timeout. REST: ≤ 60000. native-api allows longer. |
61
- | `--username` (alias `--user`, `-u`) / `--password` | Override CDB-resolved or env credentials. |
62
- | `--host <host\|url>` | Override the resolved host or base URL for the target. |
63
- | `--port <n>` | Override the transport port. native-api defaults to 8728 (TLS api-ssl when `--port 8729`).|
64
- | `--cdb-file` / `--cdb-password` | Override CDB file location / decrypt password. |
69
+ | `--yaml` | Shortcut for `--format yaml`. |
65
70
  | `--ros-version <version>` | SNMP MIB lookup only: pin the MikroTik MIB version to cache/download. |
66
71
  | `--ros-channel <channel>` | SNMP MIB lookup only: `stable`, `long-term`, `testing`, or `development`; default `stable`. |
67
- | `--group <name>` | Fan out across every CDB target in WinBox group `<name>`. Repeatable, and combinable with `<router>` positionals plus `--all`/`--default`; the union is de-duped by CDB record index. See **Group fanout**. |
68
- | `--concurrency <n>` | Max in-flight targets during fanout (integer ≥ 1). Defaults are transport-aware: `rest-api` 8, `native-api` 4. Rejected with `usage/invalid-concurrency` otherwise. |
69
72
 
70
73
  ## Validation
71
74
 
@@ -137,55 +140,36 @@ warning entry — it is not an error. (Earlier text said a byte overflow returns
137
140
  *error* with the needed size; superseded — truncation is an `ok: true` footnote so
138
141
  partial data stays usable.)
139
142
 
140
- ## Group fanout
141
-
142
- `--group <name>` loads the CDB once, expands every matching WinBox record in
143
- that group, de-dupes by CDB record index, and runs the same per-target retrieve
144
- pipeline for each target. Per the shared target-selector grammar, `--group` is
145
- repeatable and combines freely with one or more `<router>` positionals, `--all`
146
- (every CDB record), and `--default` (the `__default__` record); the union is
147
- de-duped by CDB record index. (Earlier drafts made `--group` mutually exclusive
148
- with a positional — superseded by the liberal selector model.)
149
-
150
- Fanout output is one outer envelope whose `ok` reports orchestration success:
151
- `ok: false` is reserved for failures before reliable per-target results exist
152
- (for example invalid flags or CDB decrypt failure). Per-target success/failure
153
- is data, not metadata:
154
-
155
- ```ts
156
- {
157
- ok: true,
158
- data: {
159
- summary: { total: 2, ok: 1, failed: 1 },
160
- targets: [
161
- { ok: true, data: { ... }, meta: { target: { recordIndex: 0 }, ... } },
162
- { ok: false, error: { code: "transport/connection-refused", ... }, meta: { target: { recordIndex: 1 }, ... } }
163
- ]
164
- },
165
- warnings: [],
166
- meta: {
167
- target: {},
168
- via: "rest-api", // null if inner targets disagree on protocol
169
- settings: { ... },
170
- operation: {
171
- kind: "fanout",
172
- group: "prod-edge",
173
- concurrency: 8,
174
- summary: { total: 2, ok: 1, failed: 1 },
175
- request: { path: "/system/resource", validate: true, format: "json", ... }
176
- }
177
- }
178
- }
179
- ```
180
-
181
- An empty or unknown group is `ok: true` with
182
- `data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`, and a
183
- `cdb/empty-group` warning. Validation runs per target because RouterOS schemas
184
- can differ by version. Fanout retries each target up to two times only for
185
- `transport/network` and `transport/connection-closed`; REST 5xx failures are
186
- mapped to `transport/connection-closed`. It does not retry `routeros/*`,
187
- `validation/*`, `auth/*`, `cdb/*`, `target/*`, timeouts, DNS, TLS, or refused
188
- connections.
143
+ ## Target selection
144
+
145
+ retrieve fans out over the **shared target-selection grammar** — multiple
146
+ `<router>` positionals, repeatable `--group`/`--where`, the geo selectors
147
+ `--near`/`--bbox`, `--all`, and `--default`, combined as a set and de-duped by
148
+ CDB record index. The grammar, the locked `FanoutData`
149
+ envelope (`data = { summary, targets[] }`, outer `ok` = orchestration success,
150
+ per-target failure is an inner `ok:false`), the record-order reassembly, and the
151
+ granular **0/2/1 exit code** are all normative in
152
+ [`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar).
153
+ The boundary is intent-keyed: a plain single-positional `retrieve <router> <path>`
154
+ stays the single-target envelope; a selector flag or a second positional target
155
+ switches to fan-out. `--quickchr <name>` (the named-live-provider) behaves like a
156
+ lone positional: one flag stays single-target, repeating it fans out, and mixing
157
+ it with positionals or CDB selectors is `usage/conflicting-flags` (constitution:
158
+ resolution providers).
159
+
160
+ retrieve-specific behavior on top of the shared engine:
161
+
162
+ - The CDB is loaded + decrypted **once** for the whole fan-out; each target is
163
+ then resolved and **validated independently**, because RouterOS schemas can
164
+ differ by version (`/console/inspect` runs per target).
165
+ - An empty or unknown selection is `ok: true` with
166
+ `data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`, a
167
+ `cdb/empty-group` / `cdb/empty-selection` warning, and exit `0`.
168
+ - Each target retries up to two times **only** for `transport/network` and
169
+ `transport/connection-closed` (REST 5xx is mapped to
170
+ `transport/connection-closed`). It does **not** retry `routeros/*`,
171
+ `validation/*`, `auth/*`, `cdb/*`, `target/*`, timeouts, DNS, TLS, or refused
172
+ connections.
189
173
 
190
174
  ## Definition of done
191
175
 
@@ -162,15 +162,16 @@ Call succeeds with a `cdb/password-not-needed` warning in `meta.warnings`.
162
162
  centrs retrieve $R /system/resource --cdb-file $CDB --cdb-password ignored --json
163
163
  ```
164
164
 
165
- ## Group fanout
165
+ ## Target selection (fan-out)
166
166
 
167
- These are exercised by `test/integration/fanout-retrieve.test.ts` when CHR
168
- integration is enabled.
167
+ These exercise the shared target-selection grammar
168
+ ([`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar)).
169
+ `test/integration/fanout-retrieve.test.ts` runs them when CHR integration is
170
+ enabled. `$CDB` contains two records in group `fanout-chr`: record 0 is the live
171
+ CHR (comment fact `role=edge`) and record 1 is an unreachable REST URL (comment
172
+ fact `role=core`).
169
173
 
170
- ### F1. Group fanout with one inner success and one inner failure
171
-
172
- `$CDB` contains two records in group `fanout-chr`: record 0 is the live CHR and
173
- record 1 is an unreachable REST URL.
174
+ ### F1. Group fan-out with one inner success and one inner failure
174
175
 
175
176
  ```bash
176
177
  centrs retrieve --group fanout-chr /system/resource --cdb-file $CDB --json
@@ -179,7 +180,7 @@ centrs retrieve --group fanout-chr /system/resource --cdb-file $CDB --json
179
180
  Outer envelope: `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`,
180
181
  `data.targets` is ordered by CDB `recordIndex`, and `meta.operation.kind` is
181
182
  `fanout`. The unreachable target is an inner `ok: false` envelope with
182
- `transport/connection-refused`.
183
+ `transport/connection-refused`. Process exit `2` (partial).
183
184
 
184
185
  ### F2. Empty / unknown group
185
186
 
@@ -188,7 +189,37 @@ centrs retrieve --group no-such-group /system/resource --cdb-file $CDB --json
188
189
  ```
189
190
 
190
191
  Envelope: `ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
191
- `data.targets = []`, and warnings include `cdb/empty-group`.
192
+ `data.targets = []`, and warnings include `cdb/empty-group`. Exit `0`.
193
+
194
+ ### F3. `--where` device-class selector (subset)
195
+
196
+ ```bash
197
+ centrs retrieve --where role=edge /system/resource --cdb-file $CDB --json
198
+ ```
199
+
200
+ `--where` matches the raw comment fact, selecting only record 0:
201
+ `data.summary = { total: 1, ok: 1, failed: 0 }`,
202
+ `meta.operation.selection.where = ["role=edge"]`, exit `0`.
203
+
204
+ ### F4. `--all` (every CDB record)
205
+
206
+ ```bash
207
+ centrs retrieve --all /system/resource --cdb-file $CDB --json
208
+ ```
209
+
210
+ `--all` fans across every record (excluding `__default__`) — here both, so
211
+ `data.summary = { total: 2, ok: 1, failed: 1 }`,
212
+ `meta.operation.selection.all = true`, exit `2`.
213
+
214
+ ### F5. Multiple positional targets (ad-hoc literals)
215
+
216
+ ```bash
217
+ centrs retrieve $REACHABLE_URL $UNREACHABLE_URL /system/resource --username $U --password $P --json
218
+ ```
219
+
220
+ More than one positional target is fan-out mode without any selector flag. With
221
+ no `--cdb-file`, both are ad-hoc literals, labeled by `meta.target.input` with no
222
+ `recordIndex`: `data.summary = { total: 2, ok: 1, failed: 1 }`, exit `2`.
192
223
 
193
224
  ## Format
194
225
 
@@ -304,3 +335,19 @@ centrs retrieve $A /system/resource --attribute bogus-attr --via native-api --po
304
335
  ```bash
305
336
  centrs retrieve $A /system/resource --via native-api --port $API_PORT --username wrong --password wrong
306
337
  ```
338
+
339
+ ## quickchr targets (#134)
340
+
341
+ `$NAME` is the machine name of a running quickchr-managed CHR
342
+ (`quickchr list`). No `$R`/`$U`/`$P` — host/port/auth come from the live
343
+ descriptor. Covered by `test/integration/quickchr-target.test.ts`.
344
+
345
+ ### Q1. `--quickchr <name>` resolves the VM's REST endpoint
346
+
347
+ ```bash
348
+ centrs retrieve --quickchr $NAME /system/resource --json
349
+ ```
350
+
351
+ Envelope: `ok: true`, `meta.via=rest-api` (normal default; quickchr does not
352
+ change protocol selection), `meta.target.source.kind=provider`,
353
+ `meta.target.identity=$NAME`, no `recordIndex` (the CDB was never consulted).
@@ -3,49 +3,198 @@
3
3
  Read and write centrs's own settings — the global `centrs.env` preferences and
4
4
  the `__default__` fallback record — and run first-time setup.
5
5
 
6
- Status: `designed`. This file describes intent and flags; no implementation yet.
6
+ Status: `CHR-passed` for the surface described by `examples.md` (fixture-backed
7
+ integration tests — `settings` performs no network IO, so no CHR is needed).
7
8
  See `docs/MATRIX.md` for the cell state. `settings` is the front-end to the
8
9
  settings the constitution defines (Settings precedence: the `centrs.env` config
9
10
  file at tier 2; `__default__` fallback creds): it does not invent new settings,
10
- only edits the ones already specified.
11
+ only edits the ones already specified. The interactive first-time-setup flow
12
+ (bare `centrs settings` under a TTY) remains a deferred later slice — see
13
+ "Surface" below.
11
14
 
12
15
  ## Why
13
16
 
14
17
  centrs deliberately has no store of its own for *inventory* — that is the CDB.
15
- But it does have **global, centrs-only preferences** (default discovery window,
16
- default `--format`, concurrency, …) that have no per-device home, and a
18
+ But it does have **global, centrs-only preferences** (default output format,
19
+ default result limit, MCP toggles, …) that have no per-device home, and a
17
20
  **`__default__` fallback record** for credentials. Today both are hand-edited
18
21
  (`centrs.env`, or `devices add __default__`). `settings` makes that ergonomic and
19
22
  is where onboarding flows steer the user.
20
23
 
24
+ (Considered and declined: folding `centrs.env` into the future TikTOML device
25
+ format, #137. TikTOML's locked scope is inventory — devices/groups/links,
26
+ credential-by-reference — and stays that way; see the research posted on #137
27
+ for the reasoning. `centrs.env` stays a standalone file.)
28
+
29
+ ## Scope boundary: which `CENTRS_*` keys `settings` manages
30
+
31
+ Not every `CENTRS_*` env var belongs to `settings`. Two groups, and the split
32
+ matters for what `get`/`set`/`print`/`reset` accept:
33
+
34
+ 1. **Managed — full `settings` support.** Every `CENTRS_*` var that a real
35
+ command currently reads, validated with the same rules the consuming code
36
+ already applies (the shared `parseBoolean`/`parseDuration`, a command's own
37
+ enum, `resolveOptionalIntegerSetting`, …), gets full `get`/`set`/`print`/
38
+ `reset`. This includes both tool-behavior keys (`format`, `max-results`,
39
+ `cdb-file`, `resolve`, `mcp-allow-adhoc`) and keys that also have a
40
+ per-device CDB comment-kv counterpart (`via`, `validate`, `timeout`,
41
+ `port`, `ssh-key`, `insecure`) — an earlier draft of this README tried to
42
+ split those into a second, "unmanaged" tier on the theory that
43
+ `settings` "has no visibility into device context." That theory didn't
44
+ hold up: writing `CENTRS_VIA=ssh` to `centrs.env` is mechanically
45
+ identical to writing `CENTRS_FORMAT=json` — a context-free line write —
46
+ and the existing precedence ladder already places the file **below**
47
+ comment-kv (constitution: tier 2 < tier 3), so a device's own `via=`
48
+ override always wins regardless of how the global default was set.
49
+ There's no real distinction left to justify treating these keys worse.
50
+ See the table below for validation per key, including which ones also
51
+ have a comment-kv form (informational — it doesn't change how `settings`
52
+ treats them).
53
+ 2. **Refused — must not be written by `settings`.** Credential-shaped or
54
+ self-referential keys. `settings set`/`settings reset` on any of these is
55
+ a hard `settings/reserved-key` error with a remediation pointer:
56
+
57
+ | Key | Why refused | Use instead |
58
+ | --- | --- | --- |
59
+ | `CENTRS_PASSWORD`, `CENTRS_USERNAME` | Default credentials belong to the `__default__` CDB record, which routes through the `devices` write layer and is redacted from structured output (constitution: identity and CDB). A parallel copy in a plaintext file is a second source of truth for the same secret, one that is never redacted. | `centrs devices add __default__ --user … --password …` |
60
+ | `CENTRS_CDB_PASSWORD` | This is the CDB's own decryption password. Storing it beside an *encrypted* CDB in a plaintext sibling file defeats the encryption. Security-sensitive surface per `SECURITY.md`. | Pass `--cdb-password` per invocation, export the env var in the calling shell, or use `Bun.secret()` once available — never persist to `centrs.env`. |
61
+ | `CENTRS_SKIP_ENV_FILE` | Self-referential: it means "don't load `centrs.env`." Writing it *into* `centrs.env` is a no-op by construction (a file that says "ignore me" is never read to find out). | Export it in the shell or pass `--skip-env-file` per call. |
62
+ | `CENTRS_RUN_FAST_INTEGRATION` | A CI/test signal, not a user preference. | N/A — set by `bun run test:integration`, never by a human. |
63
+
64
+ `settings get`/`settings print` on a refused key still works as read-only
65
+ inspection, **except the value itself**: `CENTRS_PASSWORD` and
66
+ `CENTRS_CDB_PASSWORD` are always shown as `"(redacted)"` (with an `isSet`
67
+ boolean instead of the value) regardless of whether the real secret came
68
+ from the process environment or — despite `set` refusing to write it —
69
+ was hand-added to the file directly. `settings` cannot stop a human from
70
+ editing `centrs.env` with a text editor, so the redaction has to be a
71
+ read-time defense, not just a write-time refusal. `CENTRS_USERNAME` is
72
+ refused-on-write (it belongs on the `__default__` record, not for secrecy
73
+ reasons) but is not secret and is shown in full on read.
74
+
75
+ ## Known settings keys
76
+
77
+ | Key | Type | Built-in default | Comment-kv counterpart? | Notes |
78
+ | --- | --- | --- | --- | --- |
79
+ | `CENTRS_FORMAT` | enum: `text`\|`json`\|`yaml` | Varies **per command** (`api` defaults `json`; `retrieve`/`execute`/`transfer` default `text`) | No | See "Precedence and what `print`/`get` can actually report" below — `print` cannot show one universal current format. |
80
+ | `CENTRS_CDB_FILE` | path | `~/.config/tikoci/winbox.cdb` (`defaultCdbPath`) | No | Existence is not checked at `settings set` time — pre-configuring a path before the file exists is valid. |
81
+ | `CENTRS_MAX_RESULTS` | positive integer | command-specific (`retrieve`, `execute`) | No | `settings/invalid-integer` on a non-positive or non-integer value (`resolveOptionalIntegerSetting` rules). |
82
+ | `CENTRS_RESOLVE` | enum: `none`\|`arp` | `none` | No | Governs MAC→IP fallback resolution (`src/resolver/mac.ts`, `parseResolvePolicy`). |
83
+ | `CENTRS_MCP_ALLOW_ADHOC` | boolean | `false` | No | Currently gates a **not-yet-built** MCP feature (`src/mcp/config.ts` docstring: "reserved for future work, not yet honored"). Safe to expose in `settings` ahead of the feature — `print` should annotate it `(reserved, not yet active)`. **Its reader checks only the literal string `"1"`** (`test/unit/mcp-config.test.ts`: `"true"` → false), not the shared boolean parser — see the canonicalization rule below. |
84
+ | `CENTRS_VIA` | enum: `rest-api`\|`native-api`\|`ssh`\|`snmp`\|`mndp`\|`mac-telnet`\|`romon`\|`winbox-terminal` | `rest-api` (per `retrieve`; other commands vary the same way `format` does) | Yes — `via=` | `settings/invalid-via` on an unrecognized value. |
85
+ | `CENTRS_VALIDATE` | boolean | `true` | Yes — `validate=` | `settings/invalid-boolean`. |
86
+ | `CENTRS_TIMEOUT` | duration (`500ms`, `5s`, `1m`, or bare ms) | command-specific | Yes — `timeout=` | `settings/invalid-timeout` (`parseDuration` rules). |
87
+ | `CENTRS_PORT` | positive integer | protocol-default (no built-in default; unset falls through to the transport's own port) | Yes — `port=` | `settings/invalid-integer`. |
88
+ | `CENTRS_SSH_KEY` | path | unset | Yes — `ssh-key=` | Path only, never key material — matches the per-device form's own rule. Non-blank check only; existence isn't checked at set time. |
89
+ | `CENTRS_INSECURE` | boolean | `false` | Yes — `insecure=` | Accept RouterOS's self-signed TLS cert. `settings set` should still surface a one-line reminder of what it does (`meta.warnings`), since it's a security-relevant toggle even though it's fully validated like any other boolean key. |
90
+ | `CENTRS_TRANSFER_VIA` | enum: `auto`\|`rest`/`rest-api`\|`native`/`native-api`\|`sftp`\|`scp`\|`fetch`\|`ftp` | `auto` | **No** — `transfer` reads only the env var (`src/transfer.ts`, `selectTransferMethod`); there is no `transfer-via=` comment-kv key today. | `scp`/`fetch` currently error `not-implemented`; `ftp` is blocked unless `ALLOW_UNSAFE_PROTOCOLS` opts in — `settings set transfer-via ftp` should surface that same warning rather than silently accepting a value that will fail at call time. |
91
+ | `CENTRS_HOST` | string | unset (falls back to the target positional) | No (comment-kv has `target`/lookup keys, not a `host` override) | An unusual thing to default globally — most calls pass an explicit target — but it's a real, coded env var, so `settings` exposes it rather than editorializing. Non-blank check only. |
92
+
93
+ **Boolean write canonicalization:** `settings set <bool-key> <value>` accepts
94
+ any spelling the shared `parseBoolean` recognizes (`true`/`false`, `yes`/`no`,
95
+ `on`/`off`, `1`/`0`) as *input*, but always **writes the canonical `1` or
96
+ `0`** to the file, never `true`/`false`. This is driven by
97
+ `CENTRS_MCP_ALLOW_ADHOC`'s strict `=== "1"` reader (a locked, tested
98
+ behavior — changing it is out of scope here) — writing `1`/`0` uniformly
99
+ satisfies that reader *and* is valid input to every `parseBoolean`-based
100
+ reader, so one write rule works for both without touching `mcp/config.ts` or
101
+ its test.
102
+
103
+ *Devices/README.md note:* its comment-kv override-key table does not
104
+ currently list `insecure`, even though `commentKvAllowlist`
105
+ (`src/resolver/comment-kv.ts`) does. That's a pre-existing doc/code drift in
106
+ `devices`' own spec, unrelated to `settings` — flagging it here since this
107
+ research surfaced it, not fixing it in this file.
108
+
109
+ ### Not yet wired — exclude from v1
110
+
111
+ `CENTRS_CONCURRENCY`, `CENTRS_DISCOVER_TIMEOUT`, and `CENTRS_CHECK_IGNORE` are
112
+ named in this file's earlier prose and in the constitution's example list, but
113
+ **no command in `src/` currently reads them**:
114
+
115
+ - Fan-out concurrency is `--concurrency` plus a hardcoded transport-aware
116
+ default (REST 8, native-api 4) — there is no env var read anywhere in
117
+ `src/*-fanout.ts`.
118
+ - `discover` reads only `--timeout` / `DISCOVER_DEFAULT_TIMEOUT_MS` — no
119
+ `CENTRS_DISCOVER_TIMEOUT` read exists.
120
+ - `CENTRS_CHECK_IGNORE` belongs to `check`, which is itself only `designed`
121
+ (not coded).
122
+
123
+ Shipping `settings set concurrency 8` today would silently do nothing —
124
+ succeed, persist a line, and never affect a single fan-out call. That's a
125
+ worse UX than not offering the key. **Recommendation: drop these three from
126
+ the v1 `settings` vocabulary entirely** (not even `get`-able) until the
127
+ consuming command actually reads the env var; add them back to the table
128
+ above in the same change that wires the read. This is a real prerequisite,
129
+ not a documentation nit — pursuing it here would either ship a lying command
130
+ or block #135 on unrelated fan-out/discover changes.
131
+
21
132
  ## Surface
22
133
 
23
134
  ```text
24
135
  centrs settings # interactive setup (TTY)
25
- centrs settings print [<attr>] # show resolved settings + sources
136
+ centrs settings print [<attr>] [--all] [--cdb-file <path>] [--cdb-password <pw>]
26
137
  centrs settings get <attr> # read one value
27
138
  centrs settings set <attr> <value> # write one value to centrs.env
28
139
  centrs settings reset [<attr>] # clear one/all back to built-in default
29
140
  ```
30
141
 
31
- - **Interactive (`centrs settings`, TTY):** prompts (clack-style, like
32
- `@tikoci/quickchr`) to optionally collect a default `user`/`password` (written
33
- to the `__default__` record so a fresh CDB "starts useful"), confirm/update
34
- global preferences, and report where the `centrs.env` lives. Non-TTY runs of
35
- bare `settings` print the resolved settings instead of prompting.
142
+ - **Interactive (`centrs settings`, TTY) — deferred later slice, not built.**
143
+ Eventually: prompts (clack-style, like `@tikoci/quickchr`) to optionally
144
+ collect a default `user`/`password` (written to the `__default__` record so
145
+ a fresh CDB "starts useful"), confirm/update global preferences, and report
146
+ where the `centrs.env` lives. **Implementation prerequisite:** centrs has no
147
+ prompt-library dependency today (`package.json` lists only `zod` and the MCP
148
+ SDK) — this slice needs one added (`@clack/prompts`, matching the
149
+ `@tikoci/quickchr` reference). Until it ships, bare `centrs settings`
150
+ behaves identically to `settings print` **regardless of TTY** — matching
151
+ the issue's own phasing, which explicitly said not to block the
152
+ non-interactive surface (built below) on this later slice.
36
153
  - **Non-interactive (`set`/`get`/`reset`/`print`):** scriptable edits to
37
- `centrs.env`. `print` shows each resolved setting with its winning source
38
- (`default` / `centrs.env` / `env` / `cli`), mirroring `meta.settings`.
154
+ `centrs.env`. Exit code `0` on success, `1` on a validation/refusal error —
155
+ matching the plain (non-fan-out) CLI error convention already used
156
+ elsewhere (`src/cli/api.ts`'s `exitCode`), never a bare non-zero without an
157
+ envelope. `settings` has no fan-out surface, so the constitution's `2`
158
+ ("partial" fan-out result) never applies here.
159
+ - **Agent-facing:** every subcommand emits the standard envelope
160
+ (`ok`/`data`/`meta`/`error`) regardless of TTY — `--format json` is
161
+ honored on `settings` itself exactly like every other command, so an agent
162
+ never needs a TTY-detection special case to consume it.
163
+ - **`--cdb-file <path>` / `--cdb-password <pw>`** (on `print` only): scope
164
+ which CDB `print` probes for the `__default__` record summary (see
165
+ "`__default__` boundary" in the examples). These are **not** settings
166
+ precedence sources — they don't participate in the `default`/`config`/`env`
167
+ vocabulary below, because they're not resolving a `CENTRS_*` value at all;
168
+ they're a one-off read of a different file, reported alongside settings.
169
+ Absent, they fall back to the normal `CENTRS_CDB_FILE`/`CENTRS_CDB_PASSWORD`
170
+ resolution (built-in path / env — never `centrs.env`, since `CDB_PASSWORD`
171
+ is refused there).
39
172
  - Credentials collected for `__default__` route through the `devices` write
40
173
  layer (the only CDB writer) and are redacted from any structured output.
41
174
 
175
+ ### `--all`
176
+
177
+ `print`'s default view lists only the keys `settings` recognizes (the table
178
+ above). `--all` additionally lists every other `CENTRS_*=` line literally
179
+ present in the file — a foreign var a user added for their own shell use, a
180
+ typo of a real key, or (redacted) a refused credential key someone hand-added
181
+ despite `set` refusing to write it — each tagged `(unrecognized)`. This keeps
182
+ the default output focused on what `settings` actually understands while
183
+ still making `print --all` the honest "what's actually in this file" dump for
184
+ debugging.
185
+
42
186
  ## Boundaries
43
187
 
44
188
  - `settings` edits **global** preferences (`centrs.env`) and the **`__default__`**
45
189
  record only. Per-device settings stay in `devices` (CDB comment-kv).
46
190
  - It writes no RouterOS device; it is local-only, like `devices`.
47
191
  - `--skip-env-file` / `CENTRS_SKIP_ENV_FILE=1` makes a run ignore `centrs.env`
48
- (tests, dev-agent usage); `settings` still reports what *would* apply.
192
+ **for every other command's precedence resolution**. It does not affect
193
+ `settings` itself: `settings` always reads and writes the real file
194
+ regardless of this flag, since inspecting/editing the file *is* its job. A
195
+ `settings print` run under `--skip-env-file` should still show the file's
196
+ contents, with a one-line note that the flag is active and other commands
197
+ in this same invocation environment would not see them.
49
198
 
50
199
  ## Onboarding
51
200
 
@@ -55,15 +204,130 @@ creds) and `centrs devices discover` (populate the registry). See
55
204
  `commands/discover/README.md` (save-implied `devices discover`) and
56
205
  `docs/CONSTITUTION.md` (Default-device record).
57
206
 
58
- ## Settings keys
207
+ ## Settings keys: file format and write strategy
59
208
 
60
209
  `centrs.env` holds **canonical `CENTRS_*=value` lines** — identical to the
61
210
  environment-variable spellings, prefix included (`CENTRS_FORMAT=json`,
62
- `CENTRS_DISCOVER_TIMEOUT=15s`, `CENTRS_CONCURRENCY=8`). One vocabulary across
63
- file / env / CLI: the file *is* "just default env vars," so `set -a && source
64
- centrs.env` works and the loader applies each key as a default **only when the
65
- same `CENTRS_*` is not already in the process environment** (it never injects
66
- into `process.env`). `settings set <attr>` accepts the setting name with or
67
- without the prefix for convenience but always writes the canonical prefixed
68
- line. It is **user-global only** — the single XDG path above; there is no
69
- project-local config file discovered from the working directory.
211
+ `CENTRS_MAX_RESULTS=500`). One vocabulary across file / env / CLI: the file
212
+ *is* "just default env vars," so `set -a && source centrs.env` works and the
213
+ loader applies each key as a default **only when the same `CENTRS_*` is not
214
+ already in the process environment** (it never injects into `process.env`).
215
+ `settings set <attr>` accepts the setting name with or without the prefix, and
216
+ in kebab-case or SCREAMING_SNAKE (`format`, `FORMAT`, `centrs_format`,
217
+ `CENTRS_FORMAT` all resolve to the same key), but always **writes** the
218
+ canonical `CENTRS_FORMAT=` line. It is **user-global only** — the single XDG
219
+ path above; there is no project-local config file discovered from the working
220
+ directory.
221
+
222
+ **The `config` precedence layer** — `src/resolver/config-file.ts` resolves
223
+ `${XDG_CONFIG_HOME:-~/.config}/tikoci/centrs.env` (injectable `env` map,
224
+ mirroring `defaultCdbPath`) and `loadEnvFileDefaults()` parses it into a flat
225
+ `Record<string, string>`. `resolveStringSetting`/`resolveBooleanSetting`/
226
+ `resolveOptionalIntegerSetting` (`src/resolver/settings.ts`) each take this as
227
+ a `config` argument, checked *between* `comment-kv` and the built-in default,
228
+ producing `{ value, source: { kind: "config", key: "CENTRS_FORMAT" } }` — a
229
+ genuine new layer, not a value folded into the existing `defaultValue`
230
+ parameter (which would have reported it with `source: "default"`, losing
231
+ provenance). Every command that resolves a managed key (`retrieve`, `execute`,
232
+ `api`, `transfer`, `terminal`, plus `resolveCdb`/`expandCdbSelection` for
233
+ `CENTRS_CDB_FILE` and `mcp/config.ts` for `CENTRS_CDB_FILE`/
234
+ `CENTRS_MCP_ALLOW_ADHOC`) threads this `config` argument through, so a value
235
+ set via `centrs settings set` actually takes effect everywhere, not just in
236
+ `settings print`'s own report. `CENTRS_USERNAME`/`CENTRS_PASSWORD`/
237
+ `CENTRS_CDB_PASSWORD` deliberately never read this tier, even if hand-added to
238
+ the file — a stronger defense than read-time redaction alone, since a
239
+ hand-edited credential line then cannot function as a live source. Fan-out
240
+ via-pinning guards (`retrieve`/`execute`/`api`) also deliberately exclude
241
+ `config` — a `centrs.env` default is the weakest precedence tier and must not
242
+ override a per-device comment-kv `via=` across a mixed fan-out.
243
+
244
+ Write strategy (new; the current file is silent on this):
245
+
246
+ - **Preserve unrecognized content.** A hand-edited `centrs.env` may carry
247
+ blank lines, `#` comments, or foreign (non-`CENTRS_*`) lines. `set`/`reset`
248
+ parse-modify-write, touching only the matched line; everything else
249
+ round-trips byte-for-byte, mirroring the CDB's "unknown fields preserved
250
+ verbatim" principle (`commands/devices/README.md`).
251
+ - **`reset <attr>` deletes the line, never blanks it.** A blank
252
+ `CENTRS_FORMAT=` line is *not* equivalent to "unset" for a file meant to be
253
+ bash-`source`-able — sourcing it would set the shell variable to an empty
254
+ string, which most shells and centrs's own `env[KEY] !== undefined` check
255
+ both treat as "set." Deleting the line entirely is the only way `reset`
256
+ actually restores the built-in default. (This is the one place `settings`
257
+ must **not** copy the CDB comment-kv convention, where an empty value
258
+ removes the token — that convention is safe there because centrs parses
259
+ comment-kv itself; it would be a correctness bug here.)
260
+ - **`reset` with no `<attr>` removes every managed-key line** this file
261
+ currently holds, leaving unrecognized lines and all comments/blank lines
262
+ untouched, and reports `data.cleared: [...]` listing what it removed. No
263
+ confirmation gate — the blast radius is one local file, not N routers, and
264
+ every value is recoverable by re-running `settings set`.
265
+ - **Atomic write:** write to a temp file in the same directory, `fsync`, then
266
+ `rename()` over the original — same pattern as the CDB write layer, scaled
267
+ down (a single `.bak` copy is enough; this file has no multi-KB record set
268
+ to protect, and a user can always re-type a couple of lines, but a crash
269
+ mid-write must never corrupt a file other shells `source`).
270
+ - **First write creates the directory.** `${XDG_CONFIG_HOME:-~/.config}/tikoci/`
271
+ may not exist yet on a fresh machine; `set` creates it (and the file) rather
272
+ than erroring.
273
+
274
+ ## Precedence and what `print`/`get` can actually report
275
+
276
+ The constitution's five-tier precedence (`default < config < comment-kv < env
277
+ < cli`) is a **per-call** resolution — it needs a specific command invocation
278
+ (which CDB record matched, which CLI flags were passed) to produce a `cli` or
279
+ `cdb`/`comment-kv` source. A bare `centrs settings print` has no such call
280
+ context: there is no target, no command, no CLI flags being resolved. So for
281
+ each key, `print`/`get` can only ever report one of three sources —
282
+ `default` (built-in, nothing set), `config` (this file has a line for it —
283
+ the same `SettingSourceKind` every other command's `meta.settings` will use
284
+ once the loader above exists), or `env` (the real process environment
285
+ currently has the var set, which would shadow whatever the file says). It
286
+ should **not** claim `cli` or `comment-kv` sources — those require a call
287
+ `settings` never makes.
288
+
289
+ For `CENTRS_FORMAT` specifically: because each command defines its own
290
+ built-in default (`api`: `json`; others: `text`) independently
291
+ (`retrieveOutputFormats`/`apiOutputFormats`/`executeOutputFormats`/
292
+ `transferOutputFormats` are four separate `const` arrays that currently all
293
+ happen to agree on the same three values), `print` should show the
294
+ `config`/`env` value if set, and when unset show **all** per-command
295
+ defaults rather than a single misleading "default: text" — e.g. `default:
296
+ text (api: json)`.
297
+
298
+ ## Testability
299
+
300
+ `settings` is fixture-testable the same way `devices` is: `defaultSettingsPath`
301
+ (`src/resolver/config-file.ts`) takes an injectable
302
+ `env: Record<string, string | undefined>` map and reads `XDG_CONFIG_HOME`
303
+ from it (falling back to `HOME`, then `os.homedir()`) — mirroring
304
+ `defaultCdbPath`'s pattern. `test/integration/settings.test.ts` sets
305
+ `XDG_CONFIG_HOME` to a per-test temp directory (exactly like `$CDB` in the
306
+ `devices` fixtures), so no test ever touches a real user's
307
+ `~/.config/tikoci/centrs.env`.
308
+
309
+ ## Error and warning codes
310
+
311
+ Per the contributor contract in `docs/errors/README.md`, every code below has
312
+ a catalog entry (`src/core/error-catalog.ts`) and a `docs/errors/<code>.md`
313
+ page:
314
+
315
+ - `settings/reserved-key` — `set`/`reset` refused a credential or
316
+ self-referential key (see Scope boundary table).
317
+ - `settings/unknown-key` — `get`/`set` received a token that is not a
318
+ recognized `CENTRS_*`-shaped name at all — e.g. a typo. (Not needed for
319
+ reads via `print --all`, which tags-and-shows rather than rejecting.)
320
+ - `settings/consequential-value` — a non-fatal `set` warning (resolves the
321
+ former open question below) for exactly two keys: `insecure=true` and
322
+ `transfer-via=ftp`. `meta.warnings`, not an interactive confirm — a confirm
323
+ prompt in non-TTY mode isn't meaningful and would break scripting.
324
+ - `settings/skip-env-file-active` — a non-fatal `print` warning noting that
325
+ `--skip-env-file` is active for this invocation only; `settings` itself
326
+ always reads the real file regardless.
327
+ - `internal/settings-failed` — generic internal-error fallback, left as an
328
+ unenriched stub matching the sibling `internal/devices-failed`.
329
+ - `settings/invalid-boolean`, `settings/invalid-integer`,
330
+ `settings/invalid-timeout`, `settings/invalid-via`, `settings/invalid-format`,
331
+ `settings/unsafe-protocol-blocked` (referenced by the `consequential-value`
332
+ warning for `transfer-via=ftp`) already existed in the catalog (shared with
333
+ other commands' resolvers) and apply unchanged here.