@tikoci/centrs 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/GLOSSARY.txt +71 -0
- package/README.md +24 -8
- package/SECURITY.md +3 -3
- package/commands/api/README.md +27 -9
- package/commands/api/examples.md +109 -6
- package/commands/check/README.md +371 -53
- package/commands/check/examples.md +390 -0
- package/commands/devices/README.md +177 -37
- package/commands/devices/examples.md +148 -0
- package/commands/execute/README.md +39 -4
- package/commands/execute/examples.md +90 -6
- package/commands/retrieve/README.md +36 -50
- package/commands/retrieve/examples.md +40 -9
- package/commands/settings/README.md +286 -22
- package/commands/settings/examples.md +408 -0
- package/commands/terminal/README.md +6 -0
- package/commands/transfer/README.md +32 -2
- package/commands/transfer/examples.md +66 -0
- package/docs/CONSTITUTION.md +36 -11
- package/docs/MATRIX.md +68 -172
- package/docs/errors/README.md +2 -1
- package/docs/errors/cdb/empty-selection.md +19 -0
- package/docs/errors/input/incomplete-gps.md +9 -0
- package/docs/errors/input/invalid-altitude.md +9 -0
- package/docs/errors/input/invalid-bbox.md +9 -0
- package/docs/errors/input/invalid-coordinate.md +9 -0
- package/docs/errors/input/invalid-radius.md +9 -0
- package/docs/errors/internal/settings-failed.md +9 -0
- package/docs/errors/settings/consequential-value.md +26 -0
- package/docs/errors/settings/invalid-boolean.md +21 -3
- package/docs/errors/settings/invalid-format.md +18 -3
- package/docs/errors/settings/invalid-integer.md +18 -3
- package/docs/errors/settings/invalid-timeout.md +20 -3
- package/docs/errors/settings/invalid-via.md +22 -3
- package/docs/errors/settings/reserved-key.md +29 -0
- package/docs/errors/settings/skip-env-file-active.md +25 -0
- package/docs/errors/settings/unknown-key.md +25 -0
- package/docs/errors/usage/fanout-not-supported.md +16 -0
- package/package.json +6 -6
- package/src/api-fanout.ts +454 -0
- package/src/api.ts +91 -19
- package/src/cli/api.ts +135 -11
- package/src/cli/btest.ts +20 -2
- package/src/cli/common.ts +11 -0
- package/src/cli/devices.ts +235 -6
- package/src/cli/discover.ts +3 -1
- package/src/cli/execute.ts +107 -6
- package/src/cli/mcp.ts +8 -1
- package/src/cli/retrieve.ts +135 -90
- package/src/cli/selection.ts +200 -0
- package/src/cli/settings.ts +286 -0
- package/src/cli/terminal.ts +22 -7
- package/src/cli/transfer.ts +171 -47
- package/src/cli.ts +7 -0
- package/src/core/error-catalog.ts +55 -0
- package/src/core/fanout.ts +143 -2
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/execute-fanout.ts +438 -0
- package/src/execute.ts +120 -23
- package/src/index.ts +105 -1
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +38 -18
- package/src/protocols/index.ts +11 -5
- package/src/resolver/cdb.ts +10 -2
- package/src/resolver/comment-kv.ts +58 -4
- package/src/resolver/config-file.ts +136 -0
- package/src/resolver/facts.ts +78 -0
- package/src/resolver/geo.ts +453 -0
- package/src/resolver/index.ts +46 -0
- package/src/resolver/mac.ts +12 -3
- package/src/resolver/selection.ts +304 -0
- package/src/resolver/settings.ts +54 -5
- package/src/resolver/target.ts +20 -2
- package/src/retrieve-fanout.ts +225 -133
- package/src/retrieve.ts +39 -7
- package/src/settings.ts +1078 -0
- package/src/terminal.ts +11 -1
- package/src/transfer-fanout.ts +527 -0
- package/src/transfer.ts +61 -11
|
@@ -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
|
|
|
@@ -160,7 +161,8 @@ is an explicit `--via scp` escape hatch. The reason is capability, not taste:
|
|
|
160
161
|
| `--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
162
|
| `--type <file\|directory\|disk\|package>` | `list` only: filter by `/file` row type. |
|
|
162
163
|
| `--name <glob>` | `list` only: filter by file name glob. |
|
|
163
|
-
| `--out-dir <dir>` | `download`
|
|
164
|
+
| `--out-dir <dir>` | `download` fan-out only: write one file per target into `<dir>` (named by CDB identity, collision-safe, keeping the remote extension), since N devices cannot share one local path. **Required** for a `download` across a selection. See **Target selection**. |
|
|
165
|
+
| `--yes` | Confirm a mutating fan-out (`upload`/`remove`/`mkdir`/`copy`) across multiple routers in non-interactive runs (confirmed once up front). See **Target selection**. |
|
|
164
166
|
| `--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
167
|
| `--resolve <none\|arp>` | *(deferred — not parsed yet)* Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
|
|
166
168
|
| `--format text` (default) | Human summary: method, bytes, duration, verified. Errors print `[code] summary` + `Fix:` lines. |
|
|
@@ -172,7 +174,35 @@ is an explicit `--via scp` escape hatch. The reason is capability, not taste:
|
|
|
172
174
|
| `--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
175
|
| `--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
176
|
| `--cdb-file` / `--cdb-password` | Override CDB file location / decrypt password. |
|
|
175
|
-
| `--group <name>` / `--where <attr>=<value>` / `--all` / `--default` / `--concurrency <n>` |
|
|
177
|
+
| `--group <name>` / `--where <attr>=<value>` / `--near <lat>,<lon>,<radius>` / `--bbox <south>,<west>,<north>,<east>` / `--all` / `--default` / `--concurrency <n>` | Fan out across CDB targets (e.g. push one firmware file to a fleet). Same selector grammar and per-target envelope as `retrieve`/`execute` (constitution: target selection). `--near`/`--bbox` select by device GPS (lat-first). `download` fan-out requires `--out-dir`; `remove`/`mkdir`/`copy`/`upload` fan out directly and are `--yes`-gated. See **Target selection**. |
|
|
178
|
+
|
|
179
|
+
## Target selection
|
|
180
|
+
|
|
181
|
+
`transfer` runs the **same** verb across a multi-target selection over the shared
|
|
182
|
+
grammar (multiple `<router>` positionals, repeatable `--group`/`--where`, the geo
|
|
183
|
+
selectors `--near`/`--bbox`, `--all`, `--default`). The grammar, the locked
|
|
184
|
+
`FanoutData` envelope, the record-order
|
|
185
|
+
reassembly, and the granular **0/2/1 exit code** are normative in
|
|
186
|
+
[`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar).
|
|
187
|
+
|
|
188
|
+
The positional **boundary is the verb keyword** (`upload`/`download`/`list`/
|
|
189
|
+
`remove`/`mkdir`/`copy` or an alias): positionals **before** it are fan-out
|
|
190
|
+
targets, the verb and its paths follow (`transfer r1 r2 download /flash/log.txt
|
|
191
|
+
--out-dir ./logs`). A single positional target with no selector stays
|
|
192
|
+
single-target. On the top-level `upload`/`download` alias the verb is fixed, so
|
|
193
|
+
its positionals are paths — positional fan-out targets are not expressible there;
|
|
194
|
+
fan-out comes from a selector flag.
|
|
195
|
+
|
|
196
|
+
Two transfer-specific rules:
|
|
197
|
+
|
|
198
|
+
- **`download` fan-out requires `--out-dir <dir>`.** N devices cannot share one
|
|
199
|
+
local path, so each target's file is written into the directory, named by its
|
|
200
|
+
CDB identity (collision-safe — same-label targets are disambiguated by record
|
|
201
|
+
index — and keeping the remote file's extension). Each inner envelope records
|
|
202
|
+
its per-target local path.
|
|
203
|
+
- **`upload`/`remove`/`mkdir`/`copy` are device writes**, so a fan-out of them is
|
|
204
|
+
gated by **`--yes`**, confirmed once up front; without it the error names the
|
|
205
|
+
blast radius (how many routers). `download`/`list` only read the device.
|
|
176
206
|
|
|
177
207
|
## Integrity
|
|
178
208
|
|
|
@@ -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)
|
package/docs/CONSTITUTION.md
CHANGED
|
@@ -221,19 +221,40 @@ must round-trip through the same envelope.
|
|
|
221
221
|
|
|
222
222
|
One grammar across every non-terminal command. A call selects targets from any
|
|
223
223
|
mix of: `<router>` positionals (one or more), repeatable `--group <name>`,
|
|
224
|
-
`--all` (every CDB record
|
|
225
|
-
repeatable `--where <attr>=<value>` —
|
|
226
|
-
CDB-stored facts/comment-kv
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
224
|
+
`--all` (every CDB record, **excluding** the reserved `__default__` record),
|
|
225
|
+
`--default` (the `__default__` record), and repeatable `--where <attr>=<value>` —
|
|
226
|
+
a **device-class** selector that matches CDB-stored facts/comment-kv **plus core
|
|
227
|
+
record fields** (`target`/`identity`/`group`/`mac`; core fields win so a comment
|
|
228
|
+
token cannot spoof them), AND-combined across repeats. A call is in **fan-out
|
|
229
|
+
mode** when any selector flag is present **or** more than one positional target is
|
|
230
|
+
given; a plain single-positional call stays single-target. The union is
|
|
231
|
+
**de-duped by CDB record index** (ad-hoc literal targets by host) and run through
|
|
232
|
+
the same per-target pipeline.
|
|
233
|
+
|
|
234
|
+
Fan-out output is the locked **`FanoutData`** envelope: `data = { summary, targets[]
|
|
235
|
+
}`, where the outer `ok` reports **orchestration** success and a per-target failure
|
|
236
|
+
is an inner `ok:false` envelope (output, not metadata). `targets[]` is reassembled
|
|
237
|
+
in resolved record order, not completion order, so repeated runs diff cleanly. The
|
|
238
|
+
**process exit code** is granular and uniform across fan-out commands: `0` every
|
|
239
|
+
target ok (or an empty selection), `2` partial (some ok, some failed), `1`
|
|
240
|
+
orchestration error or every target failed.
|
|
241
|
+
|
|
242
|
+
A multi-target **RouterOS write** (e.g. `execute` / `api` add/set/remove across a
|
|
243
|
+
selection) is gated by **`--yes`**, confirmed **once** up front; when it is missing
|
|
244
|
+
the error names the blast radius (how many routers). The separate **`--force`**
|
|
245
|
+
gate is scoped to destructive **`devices` CDB mutations** (e.g. `devices remove`
|
|
246
|
+
across a group), not RouterOS writes. `terminal` and single-session `stream` /
|
|
247
|
+
`api --stream` are not fan-out surfaces and reject N>1 with
|
|
248
|
+
`usage/fanout-not-supported`; `api --raw` is rejected in fan-out mode with
|
|
249
|
+
`usage/conflicting-flags` (it strips the per-target envelope).
|
|
233
250
|
|
|
234
251
|
`--where` filters *which devices* by CDB-stored facts; keep it distinct from
|
|
235
252
|
`retrieve`/`execute`'s `--query`/`--filter`, which filter *RouterOS rows* in the
|
|
236
|
-
response. Two layers, two flag families.
|
|
253
|
+
response. Two layers, two flag families. `--near <lat>,<lon>,<radius>` and
|
|
254
|
+
`--bbox <south>,<west>,<north>,<east>` are geo union predicates over device GPS
|
|
255
|
+
(**lat-first, decimal degrees**; radius units `m`/`km`/`mi`/`ft`, bare = km);
|
|
256
|
+
geo-less devices never match. They join the same union — a device matches when it
|
|
257
|
+
falls in `--near`'s radius **or** `--bbox`.
|
|
237
258
|
|
|
238
259
|
`--group` is purely a **selector** in this grammar; the group *field* is written
|
|
239
260
|
by a distinct `--new-group` flag on `devices add`/`set`, so record selection and
|
|
@@ -281,7 +302,11 @@ Rules:
|
|
|
281
302
|
- **Never silently downgrade across `--via`.** If the caller pinned a transport
|
|
282
303
|
that cannot do the operation, error out with a `transport/*` code.
|
|
283
304
|
Auto-selection (no `--via`) may downgrade *within* the table, but every hop is
|
|
284
|
-
reported in `meta.warnings` with its reason.
|
|
305
|
+
reported in `meta.warnings` with its reason. For `api`'s fan-out path, this
|
|
306
|
+
and "Validation is the product" above are also checked structurally by
|
|
307
|
+
CodeQL (`.github/codeql-house-rules/missing-required-gate.ql` —
|
|
308
|
+
`apiFanout` must call `assertApiProtocolSupported`/`validateApiRequestShape`;
|
|
309
|
+
scoped to `apiFanout` only today).
|
|
285
310
|
- **Execute vs retrieve surfaces:** native-api, rest-api, ssh, mac-telnet (and
|
|
286
311
|
later RoMON / WinBox Terminal) are execute surfaces; SNMP is **retrieve-only**.
|
|
287
312
|
mac-telnet is the default execute path for an unresolved-MAC target — IP-level
|