@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
@@ -0,0 +1,408 @@
1
+ # settings — examples
2
+
3
+ Each numbered example is an executable spec, in the same style as
4
+ `commands/devices/examples.md`. `settings` performs no network IO, so its
5
+ integration tests run against a fixture `centrs.env` rather than a live CHR.
6
+
7
+ `$SETTINGS_DIR` is a per-test temporary directory passed as `XDG_CONFIG_HOME`
8
+ for the invocation, so the resolved file is `$SETTINGS_DIR/tikoci/centrs.env`
9
+ — there is no checked-in fixture file; each test starts from either an absent
10
+ file or one written in-test. This mirrors `$CDB` in the `devices` fixtures.
11
+ Some examples also reference `$CDB` (a fixture WinBox CDB, built the same way
12
+ as `devices`' fixtures) to exercise the `__default__` boundary between the two
13
+ files.
14
+
15
+ Every example below is a named test in `test/integration/settings.test.ts`
16
+ ("example N: ..."), green via `bun run test:integration:settings`.
17
+
18
+ ## print
19
+
20
+ ### 1. Print with no file present
21
+
22
+ ```bash
23
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print
24
+ ```
25
+
26
+ No `centrs.env` exists yet. `data` lists every settings-managed key with
27
+ `source: { kind: "default", key: <attr> }`. For `format`, since the built-in
28
+ default varies per command, `data.format = { value: null, source: { kind:
29
+ "default", key: "format" }, perCommandDefault: { retrieve: "text", execute:
30
+ "text", transfer: "text", api: "json" } }` instead of a single scalar.
31
+
32
+ ### 2. Print after a value is set
33
+
34
+ ```bash
35
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set format json
36
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print
37
+ ```
38
+
39
+ `data.format = { value: "json", source: { kind: "config", key:
40
+ "CENTRS_FORMAT" } }`. No `perCommandDefault` block once an override exists —
41
+ the file's value applies uniformly.
42
+
43
+ ### 3. Print --all surfaces unrecognized lines
44
+
45
+ The fixture file has a hand-added `# my note` comment and a
46
+ `CENTRS_QUUX=5s` line (not a real key) before any `set` call in this test.
47
+
48
+ ```bash
49
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print --all
50
+ ```
51
+
52
+ `data` includes an `unrecognized` array: `[{ key: "CENTRS_QUUX", value:
53
+ "5s" }]`, tagged `(unrecognized)`. Bare `print` (no `--all`) from the same
54
+ fixture omits it entirely — it isn't a key `settings` knows, managed or
55
+ refused.
56
+
57
+ ### 4. Print a single attribute
58
+
59
+ ```bash
60
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print max-results
61
+ ```
62
+
63
+ `data` contains only the `max-results` entry, not the full set.
64
+
65
+ ### 5. Real env overrides the file
66
+
67
+ ```bash
68
+ XDG_CONFIG_HOME=$SETTINGS_DIR CENTRS_FORMAT=yaml centrs settings print format
69
+ ```
70
+
71
+ The file (from example 2) holds `CENTRS_FORMAT=json`, but the real process
72
+ environment wins per precedence: `data.format = { value: "yaml", source: {
73
+ kind: "env", key: "CENTRS_FORMAT" } }`.
74
+
75
+ ### 6. Print under --skip-env-file still shows the file
76
+
77
+ ```bash
78
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print --skip-env-file
79
+ ```
80
+
81
+ `ok: true`. Values and sources are unchanged from example 2 — `settings`
82
+ itself always reads the real file. `meta.warnings` includes a note that
83
+ `--skip-env-file` is active and other commands run in this same environment
84
+ would not see these values.
85
+
86
+ ## get
87
+
88
+ ### 7. Get a key with no override and a per-command default
89
+
90
+ ```bash
91
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings get format
92
+ ```
93
+
94
+ Fresh file. `data.value = null`, `data.source = { kind: "default", key:
95
+ "format" }`, `data.perCommandDefault` populated as in example 1 above. This
96
+ is `get`'s one deliberate exception to "returns one value" — there truly
97
+ isn't one until an override exists.
98
+
99
+ ### 8. Get after set returns the resolved value
100
+
101
+ ```bash
102
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set max-results 250
103
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings get max-results
104
+ ```
105
+
106
+ `data = { value: 250, source: { kind: "config", key: "CENTRS_MAX_RESULTS" } }`.
107
+
108
+ ### 9. Get an unknown key
109
+
110
+ ```bash
111
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings get totally-bogus-key
112
+ ```
113
+
114
+ Errors `settings/unknown-key` — the token isn't a recognized `CENTRS_*`-shaped
115
+ name at all (not a typo close enough to suggest, not a plausible env name).
116
+ `details_url` populated.
117
+
118
+ ### 10. Get a credential-shaped key redacts the value
119
+
120
+ ```bash
121
+ XDG_CONFIG_HOME=$SETTINGS_DIR CENTRS_CDB_PASSWORD=hunter2 centrs settings get cdb-password
122
+ ```
123
+
124
+ `ok: true` — read-only inspection is allowed even for refused-on-write keys
125
+ (see README Scope boundary). `data = { value: "(redacted)", isSet: true,
126
+ source: { kind: "env", key: "CENTRS_CDB_PASSWORD" } }`. The literal secret
127
+ `hunter2` must not appear anywhere in the envelope, including
128
+ `meta`/`error.context` on any code path — this example exists specifically to
129
+ pin that.
130
+
131
+ ### 11. Get a key that also has a per-device comment-kv form
132
+
133
+ ```bash
134
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set via ssh
135
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings get via
136
+ ```
137
+
138
+ `ok: true`. `data = { value: "ssh", source: { kind: "config", key:
139
+ "CENTRS_VIA" } }` — fully validated and managed like any other key (see
140
+ README: there is no second-class "unmanaged" tier). `tips` (the envelope's
141
+ existing informational channel, not `warnings`) carries a
142
+ `tip/comment-kv-may-override` entry noting that a device's own `via=`
143
+ comment-kv override, if set, takes precedence over this global default —
144
+ since `settings` has no target/device context here, it cannot say whether
145
+ any specific device actually has one.
146
+
147
+ ## set
148
+
149
+ ### 12. Set a known key (happy path)
150
+
151
+ ```bash
152
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set format json
153
+ ```
154
+
155
+ `ok: true`. `data = { key: "format", previous: null, value: "json" }`. Reading
156
+ the file directly shows a single `CENTRS_FORMAT=json` line.
157
+
158
+ ### 13. Prefix and case are normalized on input, canonical on write
159
+
160
+ ```bash
161
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set CENTRS_MAX_RESULTS 250
162
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set max-results 250
163
+ ```
164
+
165
+ Both calls resolve to the same key and produce the identical
166
+ `CENTRS_MAX_RESULTS=250` line — the second call is a no-op mutation (file
167
+ unchanged, `data.previous == data.value == 250`, still `ok: true`).
168
+
169
+ ### 14. Set an invalid value for a typed key
170
+
171
+ ```bash
172
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set format xml
173
+ ```
174
+
175
+ Errors `settings/invalid-format` (the shared `{text, json, yaml}` union — see
176
+ README). No file mutation; a failed `set` never partially writes.
177
+
178
+ ### 15. First write creates the directory and file
179
+
180
+ ```bash
181
+ XDG_CONFIG_HOME=$SETTINGS_DIR/does-not-exist-yet centrs settings set format text
182
+ ```
183
+
184
+ `ok: true`. `$SETTINGS_DIR/does-not-exist-yet/tikoci/` is created, along with
185
+ `centrs.env` inside it.
186
+
187
+ ### 16. Set preserves unrelated file content
188
+
189
+ Fixture file (written in-test, not via `settings`) contains:
190
+
191
+ ```env
192
+ # personal note, keep me
193
+ CENTRS_VIA=ssh
194
+ CENTRS_FORMAT=text
195
+ ```
196
+
197
+ ```bash
198
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set format json
199
+ ```
200
+
201
+ Re-reading the file shows the comment and the `CENTRS_VIA=ssh` line
202
+ byte-for-byte unchanged; only the `CENTRS_FORMAT` line's value changed to
203
+ `json`. (`via` is a fully managed key too — this example is about `set` only
204
+ touching the one line it was asked to touch, not about `via` being
205
+ second-class.)
206
+
207
+ ### 17. Set a refused credential key
208
+
209
+ ```bash
210
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set cdb-password hunter2
211
+ ```
212
+
213
+ Errors `settings/reserved-key`. `remediation` points at `--cdb-password` /
214
+ `CENTRS_CDB_PASSWORD` / `Bun.secret()`. No file mutation — re-reading the
215
+ file (or a fresh `print`) shows no `CENTRS_CDB_PASSWORD` line was written.
216
+ The literal value `hunter2` does not appear anywhere in the error envelope.
217
+
218
+ ### 18. Set a comment-kv-shadowed key is a normal validated write
219
+
220
+ ```bash
221
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set validate false
222
+ ```
223
+
224
+ `ok: true`. `data = { key: "validate", previous: null, value: false }`. No
225
+ warning — `validate` is validated with the same `parseBoolean` rule as any
226
+ other boolean key; the fact that a device's own `validate=` comment-kv can
227
+ override it per-device is normal precedence layering, not a reason to treat
228
+ this write differently from `set format json`.
229
+
230
+ ### 19. Set an unrecognized key name
231
+
232
+ ```bash
233
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set totally-bogus-key value
234
+ ```
235
+
236
+ Errors `settings/unknown-key`, same as `get` (example 9). No file mutation.
237
+
238
+ ### 20. Set a typed integer key out of range
239
+
240
+ ```bash
241
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set max-results 0
242
+ ```
243
+
244
+ Errors `settings/invalid-integer` (positive-integer rule, matching
245
+ `resolveOptionalIntegerSetting`'s existing validation used elsewhere). `-1`
246
+ and non-numeric input (`abc`) behave the same.
247
+
248
+ ### 21. Set an invalid protocol identifier
249
+
250
+ ```bash
251
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set via carrier-pigeon
252
+ ```
253
+
254
+ Errors `settings/invalid-via` — same enum and code `retrieve.ts` already uses
255
+ for `--via`/`CENTRS_VIA` (`rest-api`, `native-api`, `ssh`, `snmp`, `mndp`,
256
+ `mac-telnet`, `romon`, `winbox-terminal`). No file mutation.
257
+
258
+ ### 22. Set an invalid duration
259
+
260
+ ```bash
261
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set timeout not-a-duration
262
+ ```
263
+
264
+ Errors `settings/invalid-timeout` (`parseDuration` rules: bare milliseconds
265
+ or a `ms`/`s`/`m` suffix). No file mutation.
266
+
267
+ ### 23. Boolean values are written in canonical 1/0 form
268
+
269
+ ```bash
270
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set mcp-allow-adhoc true
271
+ ```
272
+
273
+ `ok: true`. Reading the file directly shows `CENTRS_MCP_ALLOW_ADHOC=1`, not
274
+ `=true` — `settings` accepted the `parseBoolean`-recognized spelling `true`
275
+ as input but always writes the canonical `1`/`0` (see README: this is
276
+ required because `src/mcp/config.ts` checks the literal string `"1"`, not the
277
+ shared boolean parser). `settings get mcp-allow-adhoc` afterward returns
278
+ `{ value: true, source: { kind: "config", ... } }` — `get` still returns a
279
+ real boolean, only the on-disk serialization is canonicalized.
280
+
281
+ ### 24. A value that will fail later still gets a warning now
282
+
283
+ ```bash
284
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set transfer-via ftp
285
+ ```
286
+
287
+ `ok: true` — `ftp` is a recognized `CENTRS_TRANSFER_VIA` value syntactically,
288
+ so `set` writes it. `meta.warnings` contains a note that `transfer` will
289
+ currently reject `ftp` at call time unless `ALLOW_UNSAFE_PROTOCOLS` includes
290
+ it (`settings/unsafe-protocol-blocked`, `src/transfer.ts`) — `settings` warns
291
+ rather than silently accepting a value it knows will not work, without
292
+ duplicating `transfer`'s own runtime gate.
293
+
294
+ ### 25. Re-setting the same value is idempotent
295
+
296
+ ```bash
297
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set format json
298
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set format json
299
+ ```
300
+
301
+ Both calls return `ok: true`; the second reports `data.previous ==
302
+ data.value`. The file has exactly one `CENTRS_FORMAT=` line afterward, not
303
+ two.
304
+
305
+ ## reset
306
+
307
+ ### 26. Reset removes the line, not just its value
308
+
309
+ ```bash
310
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings set format json
311
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings reset format
312
+ ```
313
+
314
+ `ok: true`. The `CENTRS_FORMAT=` line is **absent** from the file afterward —
315
+ not present with an empty value (see README: an empty value would be
316
+ "set to empty string" for a bash-sourced file, not "unset"). A follow-up
317
+ `settings get format` shows `source: { kind: "default" }` again, matching
318
+ example 7.
319
+
320
+ ### 27. Reset on a key that was never set is a no-op success
321
+
322
+ ```bash
323
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings reset max-results
324
+ ```
325
+
326
+ Fresh file, `CENTRS_MAX_RESULTS` never written. `ok: true`,
327
+ `data.wasSet: false` — resetting nothing is not an error.
328
+
329
+ ### 28. Reset with no attribute clears every managed key, leaves the rest
330
+
331
+ Fixture file:
332
+
333
+ ```env
334
+ # keep me
335
+ SOME_OTHER_VAR=untouched
336
+ CENTRS_FORMAT=json
337
+ CENTRS_MAX_RESULTS=250
338
+ CENTRS_VIA=ssh
339
+ ```
340
+
341
+ ```bash
342
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings reset
343
+ ```
344
+
345
+ `ok: true`. `data.cleared = ["format", "max-results", "via"]` — all three
346
+ recognized keys, `via` included, since it's fully managed like the others.
347
+ Re-reading the file shows the comment and `SOME_OTHER_VAR=untouched`
348
+ unchanged; the three managed lines are gone. No confirmation was required
349
+ (see README: local-file blast radius, always recoverable via `set`).
350
+
351
+ ### 29. Reset also refuses credential keys
352
+
353
+ ```bash
354
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings reset cdb-password
355
+ ```
356
+
357
+ Errors `settings/reserved-key`, same family as `set` (example 17) — `reset`
358
+ is a mutating verb and refuses the same key list.
359
+
360
+ ## Interactive / non-TTY
361
+
362
+ ### 30. Bare `settings` under non-TTY behaves like `print`
363
+
364
+ ```bash
365
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings </dev/null
366
+ ```
367
+
368
+ `ok: true`, identical shape to `settings print` (example 1/2 depending on
369
+ fixture state). No prompt is emitted and the call does not hang.
370
+
371
+ The interactive TTY flow (clack-style prompts collecting `__default__`
372
+ credentials and confirming preferences) is explicitly deferred — see the
373
+ README's "later slice" note and its `@clack/prompts` dependency prerequisite.
374
+ It has no example here because a genuine TTY session isn't exercisable in the
375
+ fixture-backed integration tier (the same reason `devices` has no interactive
376
+ example either, only the `</dev/null` disambiguation case). When that slice
377
+ ships, its examples land as a new numbered range appended here, not inserted
378
+ into the middle of this list.
379
+
380
+ ## `__default__` boundary
381
+
382
+ ### 31. Print reports `__default__` presence without leaking secrets
383
+
384
+ Fixture CDB `$CDB` has a `__default__` record with `user=admin` and a
385
+ password set.
386
+
387
+ ```bash
388
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print --cdb-file $CDB
389
+ ```
390
+
391
+ `ok: true`. `data.defaultDevice = { configured: true, user: "admin",
392
+ passwordSet: true }` — the password value itself never appears. This is the
393
+ one place `settings print` reaches into the CDB rather than `centrs.env`;
394
+ `--cdb-file` here is a probe-only flag (see README Surface), not a settings
395
+ precedence source. It's read-only (no CDB mutation), matching the README's
396
+ "the `__default__` record only" boundary.
397
+
398
+ ### 32. Print with no `__default__` record
399
+
400
+ ```bash
401
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print --cdb-file /tmp/does-not-exist.cdb
402
+ ```
403
+
404
+ `ok: true` (a missing/absent CDB is not an error for `settings`, unlike
405
+ `devices list` in devices' example 3 — `settings` degrades to "no default
406
+ device configured" rather than `cdb/not-found`, since reporting settings
407
+ should not require a CDB to exist at all). `data.defaultDevice = {
408
+ configured: false }`.
@@ -2,6 +2,12 @@
2
2
 
3
3
  Open an interactive RouterOS console.
4
4
 
5
+ `terminal` is **single-session**: it is not a fan-out surface. A target-selection
6
+ flag (`--group` / `--where` / `--all` / `--default` / `--concurrency`) is rejected
7
+ with `usage/fanout-not-supported` — open a console to one router at a time; use
8
+ `execute` / `api` for multi-target commands. See `docs/CONSTITUTION.md`
9
+ (Target selection grammar).
10
+
5
11
  Status: `terminal / mac-telnet` **and** `terminal / ssh` are both **`CHR-passed`**
6
12
  (see `docs/MATRIX.md` and `examples.md` T1–T3 / TS1–TS2). The two transports take
7
13
  different paths: **mac-telnet** is the in-process raw passthrough over
@@ -17,6 +17,7 @@ sftp-seeded >60 KB file) — confirming the `/file`
17
17
  RouterOS. Unit coverage is `test/unit/transfer.test.ts`. **sftp** is the first SSH
18
18
  consumer — a self-contained SFTP transfer client over the host OpenSSH `sftp`
19
19
  subsystem. `scp` and `fetch` are **designed but deferred** past this pass.
20
+ Multi-target fan-out is built (see **Target selection**, examples F1–F5).
20
21
 
21
22
  ## Intent
22
23
 
@@ -153,26 +154,63 @@ is an explicit `--via scp` escape hatch. The reason is capability, not taste:
153
154
 
154
155
  ## Flags
155
156
 
156
- | Flag | Behavior |
157
- | ---- | -------- |
158
- | `--via <method>` | Pin the method: `sftp` (built; secure default for large transfers), `rest`, `native`, or `scp` *(deferred)* / `fetch` *(deferred)* / `ftp` *(gated)*. No silent downgrade. See *Method selection* and constitution: protocol selection. |
159
- | `--force` (alias `--overwrite`) | Replace an existing target. Default **refuses** an existing target with `usage/target-exists` (mirrors how `add` refuses an existing record). Applies to the destination side: the local file for `download`, the remote file for `upload`. |
160
- | `--verify <size\|checksum\|off>` | Post-transfer integrity check. Default `size` (compare the byte count centrs sent/received against the settled `/file` size). `off` (alias `--no-verify`) skips it. See *Integrity*. |
161
- | `--type <file\|directory\|disk\|package>` | `list` only: filter by `/file` row type. |
162
- | `--name <glob>` | `list` only: filter by file name glob. |
163
- | `--out-dir <dir>` | `download` fanout only *(deferred — fanout not built yet)*: write one file per target into `<dir>` (named by CDB target), since N devices cannot share one local path. |
157
+ Implemented flags are generated from the CLI metadata into
158
+ [`docs/CLI.md` → transfer](../../docs/CLI.md#transfer); this file does not
159
+ duplicate that table. Behavior notes the generated reference cannot carry:
160
+
161
+ - `--force`/`--overwrite` applies to the destination side (the local file for
162
+ `download`, the remote file for `upload`); the default refuses an existing
163
+ target with `usage/target-exists` (see *Validation*).
164
+ - `--verify` details and the sftp size caveat are in *Integrity*.
165
+ - `--timeout`: `rest`/`native` are per-request ≤ 60000 ms (a chunked read is
166
+ many short requests, each capped); `sftp` accepts longer for a single large
167
+ transfer.
168
+ - `--ssh-key` is the same `sshKey` setting as `terminal`/`execute`
169
+ (`CENTRS_SSH_KEY`, CDB `ssh-key=`); when unset, the ssh-agent /
170
+ `~/.ssh/config` is used. See `commands/terminal/README.md`.
171
+ - `--insecure` adds a `transport/insecure-trust` warning (constitution:
172
+ transport trust).
173
+ - `--quickchr` resolves host/port/auth from the live `@tikoci/quickchr`
174
+ (0.4.5+, optional dependency) descriptor. `--via sftp` additionally
175
+ requires the descriptor's SSH endpoint to advertise a batch-capable auth
176
+ mode, else `quickchr/unsupported-via` (never a password prompt).
177
+
178
+ ### Designed, not implemented
179
+
180
+ Spec-tier flags with no implementation yet — today they fail as unknown flags:
181
+
182
+ | Flag | Designed behavior |
183
+ | ---- | ----------------- |
164
184
  | `--advertise-host <host>` / `--advertise-port <n>` / `--bind <addr>` | `fetch` only *(deferred)*: the host/IP/port centrs advertises in the fetch URL and the local bind address. Default: auto-detect the local IP on the route to the router; ephemeral port; single-use random URL token. |
165
- | `--resolve <none\|arp>` | *(deferred — not parsed yet)* Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
166
- | `--format text` (default) | Human summary: method, bytes, duration, verified. Errors print `[code] summary` + `Fix:` lines. |
167
- | `--format json` (alias `--json`) / `--format yaml` (alias `--yaml`) | Structured envelope. `CENTRS_FORMAT=json` makes JSON the default. |
168
- | `--validate=false` | Escape hatch; default `true`. See constitution: validation is the product. |
169
- | `--timeout <ms>` | Request timeout. `rest`/`native` per-request ≤ 60000 (a chunked read is many short requests, each capped); `sftp`/`scp`/`fetch` accept longer for a single large transfer. |
170
- | `--username` / `--password` | Override CDB-resolved or env credentials. |
171
- | `--host <host\|url>` / `--port <n>` | Override the resolved host / transport port. |
172
- | `--ssh-key <path>` | `sftp` (and future `scp`): explicit private-key path. Same `sshKey` setting as `terminal`/`execute` (`CENTRS_SSH_KEY`, CDB `ssh-key=`). When unset, the ssh-agent / `~/.ssh/config` is used. See `commands/terminal/README.md`. |
173
- | `--insecure` | Accept a self-signed TLS cert (`https`/`api-ssl`) or a new SSH host key (TOFU). Default verifies; see constitution: transport trust. Adds a `transport/insecure-trust` warning. |
174
- | `--cdb-file` / `--cdb-password` | Override CDB file location / decrypt password. |
175
- | `--group <name>` / `--where <attr>=<value>` / `--all` / `--default` / `--concurrency <n>` | *(deferred — fanout not built for `transfer` yet)* Fan out across CDB targets (e.g. push one firmware file to a fleet). Same selector grammar and per-target envelope as `retrieve` (constitution: target selection). `download` fanout requires `--out-dir`; `remove`/`mkdir`/`copy`/`upload` fan out directly. |
185
+ | `--resolve <none\|arp>` | Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
186
+
187
+ ## Target selection
188
+
189
+ `transfer` runs the **same** verb across a multi-target selection over the shared
190
+ grammar (multiple `<router>` positionals, repeatable `--group`/`--where`, the geo
191
+ selectors `--near`/`--bbox`, `--all`, `--default`). The grammar, the locked
192
+ `FanoutData` envelope, the record-order
193
+ reassembly, and the granular **0/2/1 exit code** are normative in
194
+ [`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar).
195
+
196
+ The positional **boundary is the verb keyword** (`upload`/`download`/`list`/
197
+ `remove`/`mkdir`/`copy` or an alias): positionals **before** it are fan-out
198
+ targets, the verb and its paths follow (`transfer r1 r2 download /flash/log.txt
199
+ --out-dir ./logs`). A single positional target with no selector stays
200
+ single-target. On the top-level `upload`/`download` alias the verb is fixed, so
201
+ its positionals are paths — positional fan-out targets are not expressible there;
202
+ fan-out comes from a selector flag.
203
+
204
+ Two transfer-specific rules:
205
+
206
+ - **`download` fan-out requires `--out-dir <dir>`.** N devices cannot share one
207
+ local path, so each target's file is written into the directory, named by its
208
+ CDB identity (collision-safe — same-label targets are disambiguated by record
209
+ index — and keeping the remote file's extension). Each inner envelope records
210
+ its per-target local path.
211
+ - **`upload`/`remove`/`mkdir`/`copy` are device writes**, so a fan-out of them is
212
+ gated by **`--yes`**, confirmed once up front; without it the error names the
213
+ blast radius (how many routers). `download`/`list` only read the device.
176
214
 
177
215
  ## Integrity
178
216
 
@@ -336,6 +336,72 @@ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $K
336
336
  Both `ok: true`. On-device `copy` has no SFTP primitive — `--via sftp copy` returns
337
337
  `transport/unsupported-operation` (it stays on rest/native).
338
338
 
339
+ ## Target selection (fan-out)
340
+
341
+ These exercise the shared target-selection grammar
342
+ ([`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar)),
343
+ run by `test/integration/transfer-fanout.test.ts`. `$CDB` has two records in group
344
+ `fanout-chr`: record 0 is the live CHR (comment fact `role=edge`, seeded with
345
+ `fanout-test.txt`) and record 1 is an unreachable REST URL (`role=core`). Note the
346
+ **verb-keyword boundary**: targets/selectors precede the verb.
347
+
348
+ ### F1. Group fan-out of a read verb (`list`)
349
+
350
+ ```bash
351
+ centrs transfer --group fanout-chr list --via rest --cdb-file $CDB --json
352
+ ```
353
+
354
+ `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`, targets ordered by
355
+ `recordIndex`, `meta.operation.kind = fanout`; the unreachable target is an inner
356
+ `ok: false`. Exit `2`.
357
+
358
+ ### F2. Empty / unknown group
359
+
360
+ ```bash
361
+ centrs transfer --group no-such-group list --via rest --cdb-file $CDB --json
362
+ ```
363
+
364
+ `ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
365
+ `warnings` include `cdb/empty-group`. Exit `0`.
366
+
367
+ ### F3. `--where` device-class selector (subset)
368
+
369
+ ```bash
370
+ centrs transfer --where role=edge list --via rest --cdb-file $CDB --json
371
+ ```
372
+
373
+ Selects only record 0: `data.summary = { total: 1, ok: 1, failed: 0 }`,
374
+ `meta.operation.selection.where = ["role=edge"]`, exit `0`.
375
+
376
+ ### F4. `download` fan-out into `--out-dir`
377
+
378
+ ```bash
379
+ centrs transfer --group fanout-chr download fanout-test.txt --out-dir ./downloads --via rest --cdb-file $CDB --json
380
+ ```
381
+
382
+ Each reachable target writes one file into `./downloads`, named by its CDB identity
383
+ (collision-safe, keeping the remote extension). `data.summary = { total: 2, ok: 1,
384
+ failed: 1 }`, exit `2`; the ok target's inner `data.local` is the written path.
385
+
386
+ ### F5. Mutating fan-out (`remove`) without `--yes` is refused
387
+
388
+ ```bash
389
+ centrs transfer --group fanout-chr remove fanout-test.txt --via rest --cdb-file $CDB --json
390
+ ```
391
+
392
+ A mutating fan-out is gated once up front: `ok: false`,
393
+ `error.code = usage/confirmation-required`, the summary names the blast radius
394
+ (`2 router(s)`), exit `1`. Nothing is removed. Add `--yes` to fan the write out.
395
+
396
+ ### F6. `download` fan-out without `--out-dir` is refused
397
+
398
+ ```bash
399
+ centrs transfer --group fanout-chr download fanout-test.txt --via rest --cdb-file $CDB --json
400
+ ```
401
+
402
+ N devices cannot share one local path: `ok: false`,
403
+ `error.code = usage/conflicting-flags`, exit `1`. Pass `--out-dir <dir>`.
404
+
339
405
  ## Pending transports (contract defined; flips when the transport lands)
340
406
 
341
407
  ### P2. `--via scp` (follow-on pass after sftp)
@@ -366,3 +432,32 @@ centrs transfer $R upload $SRC centrs-up.txt --via ftp --username $U --password
366
432
  Envelope: `ok: false`, the error reports the protocol is gated (the cleartext-FTP
367
433
  opt-in is absent). When `ALLOW_UNSAFE_PROTOCOLS=ftp` is set but the method is not
368
434
  yet built, it is `usage/not-implemented` instead.
435
+
436
+ ## quickchr targets (#134)
437
+
438
+ `$NAME` is the machine name of a running quickchr-managed CHR. Host/port/auth
439
+ come from the live descriptor. Covered by
440
+ `test/integration/quickchr-target.test.ts`.
441
+
442
+ ### Q1. `list` over the descriptor's REST service
443
+
444
+ ```bash
445
+ centrs transfer --quickchr $NAME list --json
446
+ ```
447
+
448
+ Envelope: `ok: true`, `meta.target.source.kind=provider`.
449
+
450
+ ### Q2. `--via sftp` gates on the SSH endpoint's batch auth
451
+
452
+ ```bash
453
+ centrs transfer --quickchr $NAME list --via sftp --json
454
+ ```
455
+
456
+ When the descriptor's SSH endpoint advertises a **usable** batch-capable auth
457
+ mode (`agent-or-config`, or `private-key` together with a verified
458
+ `privateKeyPath` — a bare `private-key` mode with no path does not count), the
459
+ list succeeds over sftp: `meta.via=sftp`, `meta.target.source.kind=provider`,
460
+ `meta.settings.insecure` provider-sourced, and the key path (when one is
461
+ handed over) carried as a provider-sourced `sshKey`. Otherwise the envelope is
462
+ `ok: false` with `error.code=quickchr/unsupported-via` and exit code 1 — a
463
+ typed gate, **never** a password prompt and never a silent fallback to REST.