@tikoci/centrs 0.1.9-next.27 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,13 +10,24 @@ Each stable version has one section. Work in progress goes under
10
10
  `## X.Y.Z — next`, which is what `@next` installs (`X.Y.Z-next.N`); the stable
11
11
  release dates that heading. A `-next.N` build has no section of its own.
12
12
 
13
- ## 0.1.9 — next
13
+ ## 0.1.9 — 2026-11-03
14
14
 
15
- In progress on npm tag `next`. The `0.1.9-next.0` cut (2026-10-13) carried
16
- part of this section.
15
+ The `0.1.9-next.0` preview cut (2026-10-13) carried part of this section.
17
16
 
18
17
  ### Added
19
18
 
19
+ - `explain` publishes a syntactic statement tree (#497, #522): statement
20
+ `id`/`parent`, substitution `owner`, and block ownership/index links let
21
+ consumers locate nested source without inferring ancestry from spans.
22
+ Stored script and callback bodies remain opaque; ownership does not claim
23
+ execution or lexical inheritance.
24
+ - `devices add`/`set --password-stdin` reads the record password from stdin,
25
+ so automation can write a credential without it showing in argv or `ps`
26
+ (#582). One trailing `\n` or `\r\n` is stripped. Empty input is
27
+ `input/empty-stdin`, non-UTF-8 bytes or a leading byte-order mark are
28
+ `input/invalid-stdin`, a terminal on stdin is `usage/stdin-is-tty`, and
29
+ combining it with `--password` is `usage/conflicting-flags`. Other `devices`
30
+ subcommands refuse the flag.
20
31
  - `explain` offers a non-applied repair tip for pasted console prompts
21
32
  (#370, #567, #578), including `[user@host] /menu>` and HotLock `>>` forms.
22
33
  The candidate preserves command separators and names the menu context;
@@ -117,6 +128,20 @@ part of this section.
117
128
 
118
129
  ### Changed
119
130
 
131
+ - Bun 1.4.4 is the minimum supported version (`engines.bun: >=1.4.4 <2`, was
132
+ `>=1.3.11 <2`). The CI/release/Pages `packageManager` pin, Docker image and
133
+ `@types/bun` now use 1.4.4 together (0.1.8 shipped on 1.3.14; #438).
134
+ - **Breaking:** `devices show`, `add` and `set` no longer print the record password
135
+ (#551). Text and JSON carry `passwordSet: boolean` instead, and
136
+ `--show-password` on those three subcommands opts back in; other subcommands
137
+ refuse the flag. The `--explain` record's password field reads `<redacted>`
138
+ unless the flag is given, as does an unknown-tcode tail that may hold the
139
+ password. **Library consumers:** exported `showDevice`,
140
+ `addDevice` and `setDevice` now redact too. `DevicesShowItem` has no
141
+ `password` field; pass `showPassword: true` to get
142
+ `DevicesShowItemWithPassword`. MCP output is unchanged (it already redacted)
143
+ and now uses the same core redaction. Dialing with CDB credentials is
144
+ unaffected: the resolver and MCP allowlist read the raw record.
120
145
  - **Breaking:** permission and edge-auth failures get their own error codes
121
146
  (#541). Three codes change for `--json` and library readers:
122
147
  - A REST policy refusal (HTTP 500 `not enough permissions (9)` or `std
@@ -203,9 +228,7 @@ part of this section.
203
228
  `/cmr/upgrade/trigger`, `/cmr/upgrade/job/run-next`) are pinned as writes in
204
229
  every spelling, over `execute`, `api` and MCP `mcp=ro`. On 7.26beta1 a
205
230
  controller login with only `read` + `api` can run them (#456).
206
- - CI, release, Pages and the Docker image run Bun 1.4.2 (was 1.3.14), the
207
- version local development already used; `@types/bun` moves with it (#438).
208
- `engines.bun` stays `>=1.3.11 <2`. The `typescript` peer stays `^6.0.3`:
231
+ - The `typescript` peer stays `^6.0.3`:
209
232
  the source passes `tsc --noEmit` under TypeScript 7.0.2, but typedoc 0.28 (the
210
233
  API reference build) supports TypeScript up to 6.0.x only.
211
234
  - QA records a CHR leg in `qa-history` only when its test step passed or
@@ -223,6 +246,31 @@ part of this section.
223
246
 
224
247
  ### Fixed
225
248
 
249
+ - Help now lists SSH for `execute --via`, the offline analyzer before device
250
+ validation for `execute` and `api /execute`, and `devices --match` on
251
+ `show`/`set`/`remove`. `devices --group` names `add`/`set`, and
252
+ `devices init --help` / `encrypt --help` show only the flags they accept.
253
+ The generated CLI reference includes both file-command help sections.
254
+ - Offline `explain` withdraws unsupported readings of unlisted bare paths
255
+ and space-for-`=` arguments (#211, #350), distinguishes command operands
256
+ from menu blocks (#496), and correctly reads `where` arrays, column-0
257
+ array comments and nested relative submenu blocks (#502, #503, #351).
258
+ - Offline `explain` rejects CHR-grounded group, operator, escape, variable,
259
+ name and glued-token faults (#505, #511, #506, #518), including unquoted
260
+ `?`, backticks, second argument `=` and non-ASCII bytes (#358, #371).
261
+ Quoted values and accepted controls retain their supported readings.
262
+ A stray top-level `}` or `]` warns `discarded-close` and marks the
263
+ unread tail; the execution gate refuses it because RouterOS would discard
264
+ part of the submitted script (#504).
265
+ - `explain` block spans use analyzed UTF-8 byte offsets, matching statements
266
+ and tokens even after Unicode text (#523, #524).
267
+ - `settings get <attr>` works in text mode (#587). It crashed reading
268
+ `typed.source` because the text renderer treated `get`'s single entry as
269
+ `print`'s key-to-entry map; `--json` was unaffected. It now prints one
270
+ `key: value [source: …]` line in `print`'s format, under the canonical attr
271
+ name, and the JSON envelope names that key in `meta.operation.attr`. A fault
272
+ after the arguments parse is now `internal/settings-failed` (exit 1), not
273
+ `input/invalid-command` with `--help` advice.
226
274
  - **`retrieve` and `api` can no longer send a non-read** (#586). A `#` or
227
275
  `?` in the path cut the appended `/print` off the REST URL, so
228
276
  `retrieve '/system/reboot#' --no-validate --attribute uptime` posted
@@ -343,6 +391,16 @@ part of this section.
343
391
 
344
392
  ### Maintenance
345
393
 
394
+ - QA allows 50 minutes per CHR leg so the full integration suite can finish
395
+ (#611).
396
+ - The explain corpus uses schema 5 and one reader, with pinned channel
397
+ sessions, a regenerated highlight slice and a wiresage conformance gate
398
+ (#498, #514, #515). Bounded-execution tests use deterministic structural
399
+ work guards instead of depth-cap timing (#521).
400
+ - The explain contract records proposed topic checks and related live reads
401
+ (#493), the CHR-grounded context submission matrix (#574), and import
402
+ verbose dry-run behavior (#520). These are design/grounding evidence;
403
+ `--context`, topic checks and contextual reads are not shipped CLI options.
346
404
  - Releases are one dispatch from `main` (#484, #485). `@next` publishes
347
405
  `X.Y.Z-next.<run_number>` with no commit, PR or tag, and reuses main's QA
348
406
  run on the commit instead of sweeping every channel again. `@latest` creates
package/README.md CHANGED
@@ -4,21 +4,21 @@
4
4
  CLI (with an MCP server; TUI and HTTP proxy frontends planned) for talking to
5
5
  MikroTik RouterOS devices through a regularized, validated interface.
6
6
 
7
- Preview builds are published on npm. To try the current offline analyzer without
8
- installing centrs or connecting to a router:
7
+ Stable releases are published on npm under `latest`. To try the offline
8
+ analyzer without installing centrs or connecting to a router:
9
9
 
10
10
  ```bash
11
- bunx @tikoci/centrs@next explain '/ip/address print' --json
11
+ bunx @tikoci/centrs@latest explain '/ip/address print' --json
12
12
  ```
13
13
 
14
- Use `bunx @tikoci/centrs@next --help` to inspect the full preview CLI, and
15
- `bunx @tikoci/centrs@next --version` (`-V`; `--json` for an envelope) to print
14
+ Use `bunx @tikoci/centrs@latest --help` to inspect the release CLI, and
15
+ `bunx @tikoci/centrs@latest --version` (`-V`; `--json` for an envelope) to print
16
16
  the exact version for scripts and field reports. The dist-tag follows standard
17
- SemVer semantics: a plain version (`0.1.8`) publishes to `latest` and carries
18
- `next` forward with it, while a `next` build (`0.1.9-next.23`) publishes to
17
+ SemVer semantics: a plain version (`0.1.9`) publishes to `latest` and carries
18
+ `next` forward with it, while a `next` build (`0.1.10-next.23`) publishes to
19
19
  `next` only and never moves `latest`. A `next` build is described by the
20
- `## 0.1.9 — next` section of [`CHANGELOG.md`](CHANGELOG.md). So `@next` is the
21
- leading edge and can equal `@latest` between cuts —
20
+ next version's `## X.Y.Z — next` section of [`CHANGELOG.md`](CHANGELOG.md).
21
+ So `@next` is the leading edge and can equal `@latest` between cuts —
22
22
  `npm view @tikoci/centrs dist-tags` is the authority.
23
23
 
24
24
  The full flag reference for every command is generated from the CLI metadata
@@ -63,7 +63,7 @@ product; without them this would just be a worse `curl`.
63
63
  | Command | Purpose |
64
64
  | ---------- | ------- |
65
65
  | `retrieve` | Read RouterOS state over REST/native API. `--follow` keeps a menu's rows current over native API (snapshot, then a line per change; NDJSON); `--sample <interval>` re-reads it on a timer for counters; `--query` filters on the router with the `print where` grammar; `--wait` retries until a read succeeds (or an optional `--until` condition holds), up to a deadline. SNMP OID/MIB reads are planned. |
66
- | `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API/REST/L2 surfaces. |
66
+ | `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API, REST, SSH or MAC-Telnet. |
67
67
  | `api` | Structured RouterOS API passthrough (gh-api style): one command per operation, structured in/out, can write, over REST or native API. Follow changes with `api … <menu>/listen`; `--stream` streams any command's replies (native API; NDJSON). |
68
68
  | `terminal` | Open an interactive console, primarily SSH or MAC-Telnet. |
69
69
  | `transfer` | Upload, download and manage device files over REST/native API or SFTP; `upload` and `download` are shortcuts. |
@@ -96,7 +96,9 @@ stores:
96
96
  comment carries `identity=edge1`. Every other comment token stays inert prose.
97
97
  See `commands/devices/README.md` (Identity model) for the full resolution rules.
98
98
  - **`user` / `password`** — the credentials centrs connects with. They stay in
99
- the CDB and are redacted from MCP results/resources.
99
+ the CDB. `devices show` / `add` / `set` withhold the password and report
100
+ `passwordSet` (`--show-password` reveals it); MCP results/resources always
101
+ redact it.
100
102
  - **`group`, `profile`, `session`, port** — used as-is.
101
103
 
102
104
  So `centrs retrieve edge1 /ip/address` works with no flags: `edge1` is looked
@@ -223,8 +225,8 @@ GitHub issues it links are the roadmap.
223
225
 
224
226
  Requirements:
225
227
 
226
- - Bun 1.3.11 or newer (CI runs the `packageManager` version in `package.json`;
227
- Bun 2.x not yet validated).
228
+ - Bun 1.4.4 or newer within 1.x (`engines.bun`: `>=1.4.4 <2`; CI runs the
229
+ `packageManager` version in `package.json`; Bun 2.x not yet validated).
228
230
  - Git.
229
231
  - `uv`, only for `bun run lint:actions` (zizmor workflow audit).
230
232
  - For integration tests: QEMU plus `@tikoci/quickchr` (handles CHR image
@@ -310,32 +310,27 @@ is no group-definition record. A group is the set of all entries whose
310
310
  `group` value matches by exact, case-sensitive string compare. An entry with
311
311
  `group=""` belongs to no group.
312
312
 
313
- `--group <G>` on any command resolves to the membership set and fans out.
313
+ `--group <G>` on a router-facing fan-out command resolves to the membership set.
314
314
  `discover --save` writes MNDP records with `group=discovered`
315
315
  unless the caller supplies another group.
316
316
 
317
317
  ### Promotion (discovered → managed)
318
318
 
319
319
  There is no `promote` verb. Moving one device out of `group=discovered` and
320
- giving it credentials is `devices set <t> --new-group prod --user … --password
321
- …`. A **bulk** promotion (a whole discovered class at once) is the same call with
322
- a target selector picking the records:
320
+ giving it credentials uses `set` on that target:
323
321
 
324
322
  ```bash
325
- devices set --group discovered --new-group prod \
326
- --user admin --password … --force
323
+ centrs devices set <target> --group prod --user admin --password …
327
324
  ```
328
325
 
329
- `--group` (and `--where`/`--all`/positionals) always **selects** which records to
330
- modify; `--new-group` always **writes** the group field. The two are distinct
331
- flags, so there is no select-vs-set overload — this resolves the former wrinkle.
332
- (Code follow-up: the CLI still overloads `--group` as the `add`/`set` field
333
- setter today; `examples.md` reflects that current flag until the `--new-group`
334
- split lands.)
326
+ On `devices add`/`set`, `--group` writes the first-class group field; on
327
+ `devices list` it filters. Bulk `set`/`remove` selection and a separate
328
+ `--new-group` flag remain designed, not implemented. Both mutations currently
329
+ require one target, with `--match` to disambiguate saved records.
335
330
 
336
331
  ## Fanout (multi-target invocations)
337
332
 
338
- Any command may receive multiple positional targets, one or more `--group`
333
+ Router-facing fan-out commands may receive multiple positional targets, one or more `--group`
339
334
  flags, `--all`, `--default`, `--where <attr>=<value>` (device-class), and/or the
340
335
  geo selectors `--near <lat>,<lon>,<radius>` / `--bbox <south>,<west>,<north>,<east>`
341
336
  (see "Geo query" above) — see `docs/CONSTITUTION.md` (Target selection grammar).
@@ -365,21 +360,24 @@ centrs devices encrypt [--no-backup] # explicitly encrypt an existing open CD
365
360
  centrs devices list [--group G] [--where attr=value]
366
361
  [--near lat,lon,radius] [--bbox south,west,north,east]
367
362
  [--format json|yaml|text]
368
- centrs devices show <target> [--explain] [--via <protocol>] [--match <type>]
363
+ centrs devices show <target> [--explain] [--via <protocol>] [--match <selector>]
364
+ [--show-password]
369
365
  centrs devices groups [--members]
370
- centrs devices discover [--timeout 15s] [--group G] # = discover --save (save implied)
371
- centrs devices add <target> [--user U] [--password P] [--new-group G]
366
+ centrs devices discover [--timeout 15s] [--group G] # designed, not implemented; use `centrs discover --save`
367
+ centrs devices add <target> [--user U] [--password P|--password-stdin] [--group G]
372
368
  [--profile P|--profile-none|--profile-own]
373
369
  [--session S] [--comment "text k=v"]
374
370
  [--record-type ipAdmin|ipUser|macTarget|...]
375
371
  [--gps lat,lon[,altitude[,altitude-type]]]
376
372
  [--lat D] [--lon D] [--altitude M] [--altitude-type MSL|AGL]
377
- centrs devices set <selector…> [--user U] [--password P] [--new-group G]
373
+ [--show-password]
374
+ centrs devices set <target> [--match <selector>] [--user U] [--password P|--password-stdin] [--group G]
378
375
  [--profile P|--profile-none|--profile-own]
379
376
  [--session S] [k=v ...]
380
377
  [--gps lat,lon[,altitude[,altitude-type]]]
381
378
  [--lat D] [--lon D] [--altitude M] [--altitude-type MSL|AGL]
382
- centrs devices remove <selector…>
379
+ [--show-password]
380
+ centrs devices remove <target> [--match <selector>]
383
381
  centrs devices edit <target> # future: clack/TUI wizard
384
382
  ```
385
383
 
@@ -423,18 +421,79 @@ is no `update`.
423
421
  existing `(target, user)` it errors `cdb/already-exists` unless `--force`
424
422
  overwrites; the same `target` under a *different* `--user` is a new record,
425
423
  not a collision.
426
- - `set` modifies existing record(s). First-class fields change via flags
427
- (`--user`, `--password`, `--new-group`, `--profile[-none|-own]`, `--session`);
428
- comment lookup/override keys change via `k=v` positionals. **Which** records to
429
- modify is the target selector — positionals, `--group`, `--where`, `--all`
430
- (constitution: target selection) — so `--group` here *selects* and
431
- `--new-group` *writes* the group field (no overload). Against a missing target
432
- it errors `cdb/not-found-target`; a multi-record `set` needs `--force`. Writing
424
+ - `set` modifies one matched record. First-class fields change via flags
425
+ (`--user`, `--password`, `--group`, `--profile[-none|-own]`, `--session`);
426
+ comment lookup/override keys change via `k=v` positionals. The positional
427
+ target and optional `--match` select the record. Against a missing target
428
+ it errors `cdb/not-found-target`. Writing
433
429
  a first-class field name as a `k=v` positional (e.g. `user=x`) stays refused
434
430
  with `cdb/reserved-key` — the flag is the only path. Unknown comment keys warn
435
431
  (`cdb/unknown-option`), or error under `--strict`.
436
- - `remove` (aliases `rm`, `delete`) removes a single entry. Group-wide deletes
437
- (`--group G`) require `--force` (writes are large).
432
+ - `remove` (aliases `rm`, `delete`) removes one matched entry. The positional
433
+ target and optional `--match` select the record; group-wide deletion is not
434
+ implemented.
435
+
436
+ ### Record password channels
437
+
438
+ `add` and `set` take the record password one of two ways:
439
+
440
+ - `--password <secret>` — argv. Simple, but `ps` can show the value while the
441
+ process runs, and shell history can keep the command after it exits.
442
+ - `--password-stdin` — read from stdin, so the value never appears in argv. For
443
+ automation and harnesses that spawn centrs (`printf '%s' "$PW" | centrs
444
+ devices set <t> --password-stdin`, or `centrs devices set <t>
445
+ --password-stdin < pw.txt`).
446
+
447
+ `--password-stdin` rules:
448
+
449
+ - Exactly one trailing newline (`\n` or `\r\n`) is stripped, so `echo pw |`
450
+ works. Any other whitespace is part of the password.
451
+ - The rest must be valid UTF-8 with no leading byte-order mark; otherwise it
452
+ is `input/invalid-stdin`. The CDB stores record strings as UTF-8 and drops a
453
+ leading BOM on read, so either would store a different credential than the
454
+ bytes piped in.
455
+ - Empty input (nothing, or only that one newline) is `input/empty-stdin`.
456
+ Clearing a stored password stays a deliberate `--password ""`, never an empty
457
+ pipe.
458
+ - A terminal on stdin is refused with `usage/stdin-is-tty`; centrs does not
459
+ prompt.
460
+ - `--password` together with `--password-stdin` is `usage/conflicting-flags`.
461
+ There is no precedence order between them.
462
+ - Only `add` and `set` accept it; every other subcommand refuses it with
463
+ `input/invalid-command`.
464
+ - The value is never echoed in errors. Success envelopes are the same as with
465
+ `--password`.
466
+
467
+ There is no environment-variable channel for the record password:
468
+ `CENTRS_PASSWORD` is the dial credential, not a CDB field.
469
+
470
+ ### Record password in output
471
+
472
+ `show`, `add` and `set` withhold the stored record password by default, in text
473
+ and JSON (#551). Each entry always carries `passwordSet: boolean` (true when a
474
+ non-empty password is stored); text prints `password: (set, hidden)` or
475
+ `(not set)`. `--show-password` returns the cleartext `password` on those three
476
+ subcommands; every other subcommand refuses it with `input/invalid-command`.
477
+ The `--explain` record's password field reads `<redacted>` without the flag.
478
+ When an unknown-tcode `rawTail` comes before any decoded password field, the
479
+ tail may hold the password, so without the flag its `value` reads
480
+ `<redacted: N bytes, may contain password>` instead of the bytes. Such a
481
+ record decodes to an empty password, so `passwordSet` is `false` (it reports
482
+ the password centrs decoded and would dial with); the `cdb/unknown-field`
483
+ warning then carries `context.mayContainPassword: true`.
484
+ There is no encryption gate: the opt-in works the same on an open or encrypted
485
+ CDB. `passwordSet` is not the WinBox `savedPassword` ("keep password") flag.
486
+
487
+ The library matches the CLI: exported `showDevice` / `addDevice` / `setDevice`
488
+ redact by default and take `showPassword?: boolean` (default `false`); with
489
+ `showPassword: true` the entry type is `DevicesShowItemWithPassword`. MCP
490
+ (`centrs_devices`) never passes it, so MCP output never carries the password.
491
+
492
+ Dialing does not read this envelope. The resolver and the MCP allowlist gate
493
+ read the raw record through an internal lookup (`lookupDeviceRecord`, not
494
+ exported from the package root), so a CDB-credentialed dial still sends the
495
+ stored password. The public entry type has no `password` field, so a caller
496
+ that reached for the envelope instead fails to type-check.
438
497
 
439
498
  `--profile <name>` sets a named WinBox profile; `--profile-none` /
440
499
  `--profile-own` write the WinBox sentinels `<none>` / `<own>`. `profile` and
@@ -498,7 +557,9 @@ be recovered.
498
557
  `init` and `encrypt` act on the CDB file, not on records: they accept only
499
558
  `--cdb-file`, `--cdb-password`, `--format`/`--json`, plus `--encrypt` (`init`)
500
559
  or `--no-backup` (`encrypt`). Any other flag is `input/invalid-command`
501
- instead of being silently ignored.
560
+ instead of being silently ignored. `devices init --help` and
561
+ `devices encrypt --help` show only those accepted flags; the same metadata
562
+ generates their sections in [`docs/CLI.md`](../../docs/CLI.md).
502
563
 
503
564
  Close WinBox and other CDB writers before conversion. If they share this path,
504
565
  they must reopen it using the CDB password; a stale writer can overwrite the
@@ -94,7 +94,8 @@ centrs devices show 192.0.2.5 --cdb-file $CDB --explain
94
94
  ```
95
95
 
96
96
  `data.record` is the raw `WinBoxCdbRecord` (tag/marker/tcode/value). Useful
97
- for diagnosing unknown-field preservation.
97
+ for diagnosing unknown-field preservation. The password field value reads
98
+ `<redacted>` unless `--show-password` is passed (example 60).
98
99
 
99
100
  ## groups
100
101
 
@@ -720,3 +721,113 @@ A fresh path, so it does not depend on example 57 having already converted
720
721
  is retained, so
721
722
  `cdb/plaintext-backups` still warns. Test: `centrs devices encryption (#558)`
722
723
  in `test/integration/devices.test.ts`.
724
+
725
+ ## Record password in output
726
+
727
+ `show`, `add` and `set` withhold the stored record password by default, in text
728
+ and JSON and in the library envelopes (#551). Every entry carries
729
+ `passwordSet: boolean`; `--show-password` (library: `showPassword: true`) opts in.
730
+ `passwordSet` is not the WinBox `savedPassword` flag. MCP never returns the
731
+ password and has no opt-in.
732
+
733
+ ### 60. `show` withholds the password and reports `passwordSet`
734
+
735
+ ```bash
736
+ centrs devices show 192.0.2.5 --cdb-file $CDB --explain --json
737
+ centrs devices show 192.0.2.5 --cdb-file $CDB
738
+ ```
739
+
740
+ `data.entry` has `passwordSet: true` and no `password` key. The `--explain`
741
+ record's password field (tag 3) reads `<redacted>`. Text prints
742
+ `password: (set, hidden)`, or `(not set)` for an empty password.
743
+
744
+ ### 61. `--show-password` returns the password on `show`, `add` and `set`
745
+
746
+ ```bash
747
+ centrs devices show 192.0.2.5 --cdb-file $CDB --show-password --json
748
+ centrs devices add 198.51.100.57 --user writer --password pw57 --cdb-file $CDB --json
749
+ centrs devices set 198.51.100.57 --group lab --cdb-file $CDB --show-password --json
750
+ ```
751
+
752
+ `show` returns `data.entry.password: "secret"`. The `add` without the flag
753
+ returns `passwordSet: true` and no `password`; the `set` with it returns
754
+ `data.entry.password: "pw57"`.
755
+
756
+ ### 62. `--show-password` is refused outside `show` / `add` / `set`
757
+
758
+ ```bash
759
+ centrs devices list --cdb-file $CDB --show-password --json
760
+ ```
761
+
762
+ `input/invalid-command` with `error.context: { subcommand: "list", flag:
763
+ "--show-password" }`, the same refusal other subcommand-scoped flags use.
764
+
765
+ ## Record password on stdin
766
+
767
+ `--password-stdin` (#582) reads the record password from fd 0 so it never rides
768
+ argv. These run as subprocesses (`test/integration/cli-process.ts`), because
769
+ only a child process can be handed real stdin bytes. The newline, empty and TTY
770
+ rules are also unit-tested in `test/unit/devices-password-stdin.test.ts`.
771
+
772
+ ### 63. `add --password-stdin` stores the piped value
773
+
774
+ ```bash
775
+ printf 'piped pw\n' | centrs devices add 192.0.2.77 --user robot --password-stdin --cdb-file $CDB --json
776
+ ```
777
+
778
+ `ok: true`. The stored password is `piped pw`: one trailing newline is
779
+ stripped, the inner space is kept.
780
+
781
+ ### 64. `set --password-stdin` strips a trailing CRLF
782
+
783
+ ```bash
784
+ printf 'rotated\r\n' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
785
+ ```
786
+
787
+ `ok: true`. The stored password is `rotated`; group and comment are unchanged.
788
+
789
+ ### 65. Empty stdin is refused
790
+
791
+ ```bash
792
+ printf '' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
793
+ ```
794
+
795
+ Errors with `input/empty-stdin` (also for a lone `\n`). The CDB is not written
796
+ and no backup is made.
797
+
798
+ ### 66. `--password` with `--password-stdin` conflicts
799
+
800
+ ```bash
801
+ echo stdin-secret | centrs devices set 192.0.2.5 --password argv-secret --password-stdin --cdb-file $CDB --json
802
+ echo stdin-secret | centrs devices set 192.0.2.5 --password --password-stdin --cdb-file $CDB --json
803
+ ```
804
+
805
+ Both error with `usage/conflicting-flags`. Neither value appears in the error;
806
+ `--password` cannot consume `--password-stdin` as a literal credential.
807
+
808
+ ### 67. Non-UTF-8 or BOM-prefixed stdin is refused
809
+
810
+ ```bash
811
+ printf 'pw\377\n' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
812
+ printf '\357\273\277pw\n' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
813
+ ```
814
+
815
+ Both error with `input/invalid-stdin`: the first is not valid UTF-8, the second
816
+ starts with a byte-order mark. The bytes are not echoed, the CDB is not written
817
+ and no backup is made.
818
+
819
+ ## File-command help
820
+
821
+ ### 68. `init` and `encrypt` show only their accepted flags
822
+
823
+ ```bash
824
+ centrs devices init --help
825
+ centrs devices encrypt --help
826
+ ```
827
+
828
+ Both exit 0 without reading or writing a CDB. Each shows its own usage, the
829
+ CDB path/password and output flags, plus `--encrypt` for `init` or
830
+ `--no-backup` for `encrypt`. Record and selection flags are absent, including
831
+ `--user`, `--password-stdin`, `--show-password`, `--group` and `--match`.
832
+ Test: `devices file-command help is scoped without opening a CDB` in
833
+ `test/integration/cli-smoke.test.ts` (network-free).
@@ -146,8 +146,8 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
146
146
  all-strings). Unlike `jo`-style KV builders, there is no per-value
147
147
  type-forcing escape, because the device, not centrs, owns value typing.
148
148
  - On an empty-CDB resolution failure, execute (like retrieve) emits the
149
- `tip/no-devices` tip, steering toward `centrs devices discover` /
150
- `centrs settings`.
149
+ `tip/no-devices` tip, steering toward `centrs discover --save` /
150
+ `centrs devices add`.
151
151
  - Protocol surfaces are native API, REST, mac-telnet, and SSH. RoMON and
152
152
  WinBox Terminal are later. SNMP is retrieve-only and must reject execute.
153
153
  - For a MAC target not resolved from CDB, auto-selection defaults to
@@ -583,5 +583,5 @@ reach green is forbidden. See `docs/CONSTITUTION.md` for the full done rule.
583
583
  - **ssh / mac-telnet / romon / winbox-terminal** — execute surfaces, not
584
584
  retrieve surfaces.
585
585
  - **tips** — when a `<router>` fails to resolve against an empty CDB, retrieve
586
- emits the same `tip/no-devices` advice as `devices list`, steering toward
587
- `centrs devices discover` and `centrs settings`.
586
+ emits `tip/no-devices`, steering toward `centrs discover --save` and
587
+ `centrs devices add`.
@@ -156,6 +156,12 @@ centrs settings reset [<attr>] # clear one/all back to built-in
156
156
  elsewhere (`src/cli/api.ts`'s `exitCode`), never a bare non-zero without an
157
157
  envelope. `settings` has no fan-out surface, so the constitution's `2`
158
158
  ("partial" fan-out result) never applies here.
159
+ - **`get` output:** text mode renders the one entry exactly as `print`
160
+ renders that key — `key: value [source: kind (key)]` under the canonical
161
+ attr name, plus any `Tips:` footer (examples.md #33). `get`'s `data` is the
162
+ bare `{ source, value }` entry, so the JSON/YAML envelope names the key in
163
+ `meta.operation.attr` (canonical name, whichever spelling was asked for).
164
+ A blank `<attr>` is refused at parse time as `input/invalid-command`.
159
165
  - **Agent-facing:** every subcommand emits the standard envelope
160
166
  (`ok`/`data`/`meta`/`error`) regardless of TTY — `--format json` is
161
167
  honored on `settings` itself exactly like every other command, so an agent
@@ -406,3 +406,20 @@ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print --cdb-file /tmp/does-not-exi
406
406
  device configured" rather than `cdb/not-found`, since reporting settings
407
407
  should not require a CDB to exist at all). `data.defaultDevice = {
408
408
  configured: false }`.
409
+
410
+ ## Text output
411
+
412
+ ### 33. Get renders one entry in text mode, like print
413
+
414
+ ```bash
415
+ XDG_CONFIG_HOME=$SETTINGS_DIR CENTRS_MAX_RESULTS=7 centrs settings get CENTRS_MAX_RESULTS
416
+ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings get ssh-key
417
+ ```
418
+
419
+ `ok: true`, exit 0. Text mode prints the single entry as one line in the
420
+ same `key: value [source: kind (key)]` format `print` uses, under the
421
+ canonical attr name whichever spelling was asked for: `max-results: 7
422
+ [source: env (CENTRS_MAX_RESULTS)]`, then `ssh-key: (unset) [source:
423
+ default (ssh-key)]` followed by its `Tips:` footer. `get`'s `data` is the bare
424
+ entry, so the JSON envelope names the key in `meta.operation.attr`. A
425
+ credential-shaped key renders `(redacted)`, never the secret.
package/docs/CLI.md CHANGED
@@ -11,7 +11,7 @@ implemented flags live in each command's `commands/<name>/README.md`.
11
11
  | Command | Summary |
12
12
  | ------- | ------- |
13
13
  | [`retrieve`](#retrieve) | Read RouterOS values through the shared core using the selected protocol. |
14
- | [`execute`](#execute) | Run a RouterOS read or write command via native API, REST, or mac-telnet. |
14
+ | [`execute`](#execute) | Run a RouterOS read or write command via native API, REST, SSH, or mac-telnet. |
15
15
  | [`api`](#api) | Structured RouterOS API passthrough (gh-api style) over REST or native API. |
16
16
  | [`transfer`](#transfer) | Copy files to/from a RouterOS device and manage device files (rest/native/sftp). |
17
17
  | [`terminal`](#terminal) | Open an interactive RouterOS console (ssh or mac-telnet). |
@@ -71,7 +71,7 @@ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <targe
71
71
 
72
72
  ## execute
73
73
 
74
- Run a RouterOS read or write command via native API, REST, or mac-telnet.
74
+ Run a RouterOS read or write command via native API, REST, SSH, or mac-telnet.
75
75
 
76
76
  ```text
77
77
  Usage: centrs execute <target> <command> [flags] | centrs execute <target...> -- <command> [flags] | centrs execute --host <host|url> [--] <command> [flags] | centrs execute --group <name> <command> [flags]
@@ -79,7 +79,7 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
79
79
 
80
80
  | Flag | Value | Description |
81
81
  | ---- | ----- | ----------- |
82
- | `--via` | &lt;native-api\|rest-api\|mac-telnet&gt; | Pin the protocol selector; no silent downgrade when set. A bare MAC target defaults to mac-telnet. |
82
+ | `--via` | &lt;native-api\|rest-api\|ssh\|mac-telnet&gt; | Pin the protocol selector; no silent downgrade when set. A bare MAC target defaults to mac-telnet. |
83
83
  | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
84
84
  | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
85
85
  | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
@@ -101,7 +101,7 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
101
101
  | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
102
102
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
103
103
  | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
104
- | `--validate / --no-validate` | | Run RouterOS :parse and /console/inspect validation before execution (default true; `--validate=false` also accepted). |
104
+ | `--validate / --no-validate` | | Run offline analysis, then RouterOS :parse, plus `/console/inspect` for structured commands over native API/REST (default true; `--validate=false` disables both stages). |
105
105
  | `--yes` | | Confirm write-shaped add/set/remove commands in non-interactive runs. |
106
106
  | `--max-results` | `<bytes>` | Fail if the rendered envelope exceeds this byte budget. |
107
107
  | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope. |
@@ -150,7 +150,7 @@ Usage: centrs api <router> <endpoint> [flags] | centrs api --host <host|url> <en
150
150
  | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
151
151
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
152
152
  | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
153
- | `--validate / --no-validate` | | Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted). |
153
+ | `--validate / --no-validate` | | Validate structured requests with `/console/inspect`; `/execute` scripts run offline analysis, then RouterOS :parse (default true; `--validate=false` disables both stages). |
154
154
  | `--format` | &lt;json\|ndjson\|yaml\|text&gt; | Output format. Defaults to json for api; `CENTRS_FORMAT` overrides. |
155
155
  | `--json` | | Shortcut for `--format json`. |
156
156
  | `--verbose` | | Include additional context in text output, on stderr. |
@@ -271,17 +271,19 @@ Usage: centrs devices <init|encrypt|list|show|groups|add|edit|set|remove> [args]
271
271
  | `--encrypt` | | `init` only — require encrypted creation with a non-empty CDB password; refuse an existing open CDB. |
272
272
  | `--no-backup` | | `encrypt` only — skip the plaintext pre-conversion snapshot. Without that snapshot, a forgotten password cannot be recovered. |
273
273
  | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
274
- | `--cdb-password` | `<secret>` | Decrypt an encrypted CDB. Falls back to `CENTRS_CDB_PASSWORD`. Also used for `init --encrypt`, `encrypt`, and to re-encrypt on writes. |
274
+ | `--cdb-password` | `<secret>` | CDB password for encrypted reads, writes, explicit encrypted initialization and conversion. Falls back to `CENTRS_CDB_PASSWORD`; a password alone never converts an open CDB. |
275
275
  | `--via` | `<protocol>` | Resolve and report the protocol source for provenance examples; no network IO is performed. |
276
- | `--group` | `<name>` | `list` filter / `add`,`edit` first-class group field for the target entry. |
276
+ | `--group` | `<name>` | `list` filter / `add`,`set` first-class group field for the target entry. |
277
277
  | `--where` | `<attr=value>` | `list` only — repeatable device-class filter over CDB facts (AND-combined), e.g. `--where lat=37.7749`. |
278
278
  | `--near` | `<lat>`,`<lon>`,`<radius>` | `list` only — GPS filter: devices within radius (m/km/mi/ft; bare number = km). Lat-first. |
279
279
  | `--bbox` | `<south>`,`<west>`,`<north>`,`<east>` | `list` only — GPS filter: devices inside the lat-first bounding box. |
280
280
  | `--members` | | `groups` only — expand each group's membership. |
281
- | `--explain` | | `show` only — include the raw `WinBoxCdbRecord` in `data.record`. |
282
- | `--match` | &lt;user=…\|target=…\|record-type&gt; | `show` — disambiguate when `<router>` matches several records: `user=<name>`, `target=<addr>`, or a record-type token. |
281
+ | `--explain` | | `show` only — include the raw `WinBoxCdbRecord` in `data.record` (its password field reads `<redacted>` unless `--show-password`). |
282
+ | `--match` | &lt;user=…\|target=…\|record-type&gt; | `show`,`set`,`remove` — disambiguate when `<router>` matches several records: `user=<name>`, `target=<addr>`, or a record-type token. |
283
283
  | `--user` | `<name>` | `add`,`set` — first-class CDB user field. |
284
284
  | `--password` | `<secret>` | `add`,`set` — first-class CDB password field. |
285
+ | `--password-stdin` | | `add`,`set` — read the record password from stdin (one trailing newline stripped) so it never rides argv. Conflicts with `--password`. |
286
+ | `--show-password` | | `show`,`add`,`set` — include the record password in cleartext. By default output carries `passwordSet` and withholds the password. |
285
287
  | `--profile` | `<name>` | `add`,`set` — named WinBox profile (Workspace). |
286
288
  | `--profile-none` | | `add`,`set` — write the WinBox `<none>` profile sentinel. |
287
289
  | `--profile-own` | | `add`,`set` — write the WinBox `<own>` profile sentinel. |
@@ -298,6 +300,38 @@ Usage: centrs devices <init|encrypt|list|show|groups|add|edit|set|remove> [args]
298
300
  | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
299
301
  | `--json` | | Shortcut for `--format json`. |
300
302
 
303
+ ### devices init
304
+
305
+ Create an empty CDB at the resolved path; existing readable files are left unchanged.
306
+
307
+ ```text
308
+ Usage: centrs devices init [flags]
309
+ ```
310
+
311
+ | Flag | Value | Description |
312
+ | ---- | ----- | ----------- |
313
+ | `--encrypt` | | `init` only — require encrypted creation with a non-empty CDB password; refuse an existing open CDB. |
314
+ | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
315
+ | `--cdb-password` | `<secret>` | CDB password for encrypted reads, writes, explicit encrypted initialization and conversion. Falls back to `CENTRS_CDB_PASSWORD`; a password alone never converts an open CDB. |
316
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
317
+ | `--json` | | Shortcut for `--format json`. |
318
+
319
+ ### devices encrypt
320
+
321
+ Encrypt an existing CDB in place, or verify an already-encrypted file; existing backups remain unchanged.
322
+
323
+ ```text
324
+ Usage: centrs devices encrypt [flags]
325
+ ```
326
+
327
+ | Flag | Value | Description |
328
+ | ---- | ----- | ----------- |
329
+ | `--no-backup` | | `encrypt` only — skip the plaintext pre-conversion snapshot. Without that snapshot, a forgotten password cannot be recovered. |
330
+ | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
331
+ | `--cdb-password` | `<secret>` | CDB password for encrypted reads, writes, explicit encrypted initialization and conversion. Falls back to `CENTRS_CDB_PASSWORD`; a password alone never converts an open CDB. |
332
+ | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
333
+ | `--json` | | Shortcut for `--format json`. |
334
+
301
335
  ## discover
302
336
 
303
337
  Discover RouterOS neighbors over MNDP and optionally save them into the CDB.