@tikoci/centrs 0.1.0
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 +207 -0
- package/GLOSSARY.txt +420 -0
- package/README.md +196 -0
- package/SECURITY.md +30 -0
- package/commands/AGENTS.md +50 -0
- package/commands/btest/README.md +257 -0
- package/commands/btest/examples.md +190 -0
- package/commands/check/README.md +59 -0
- package/commands/devices/README.md +405 -0
- package/commands/devices/examples.md +488 -0
- package/commands/discover/README.md +170 -0
- package/commands/discover/examples.md +98 -0
- package/commands/execute/README.md +208 -0
- package/commands/execute/examples.md +353 -0
- package/commands/mcp/README.md +260 -0
- package/commands/mcp/examples.md +158 -0
- package/commands/retrieve/README.md +209 -0
- package/commands/retrieve/examples.md +306 -0
- package/commands/settings/README.md +69 -0
- package/commands/stream/README.md +70 -0
- package/commands/terminal/README.md +177 -0
- package/commands/terminal/examples.md +144 -0
- package/commands/transfer/README.md +315 -0
- package/commands/transfer/examples.md +368 -0
- package/docs/CONSTITUTION.md +417 -0
- package/docs/MATRIX.md +224 -0
- package/docs/errors/README.md +74 -0
- package/docs/errors/auth/failed.md +9 -0
- package/docs/errors/cdb/already-exists.md +9 -0
- package/docs/errors/cdb/created.md +9 -0
- package/docs/errors/cdb/decrypt-failed.md +9 -0
- package/docs/errors/cdb/empty-group.md +9 -0
- package/docs/errors/cdb/invalid-option.md +9 -0
- package/docs/errors/cdb/invalid-record.md +9 -0
- package/docs/errors/cdb/not-found-target.md +9 -0
- package/docs/errors/cdb/not-found.md +9 -0
- package/docs/errors/cdb/override-applied.md +9 -0
- package/docs/errors/cdb/parse-failed.md +9 -0
- package/docs/errors/cdb/password-not-needed.md +9 -0
- package/docs/errors/cdb/password-required.md +9 -0
- package/docs/errors/cdb/reserved-key.md +9 -0
- package/docs/errors/cdb/reserved-option.md +9 -0
- package/docs/errors/cdb/target-not-registered.md +9 -0
- package/docs/errors/cdb/unknown-field.md +9 -0
- package/docs/errors/cdb/unknown-option.md +9 -0
- package/docs/errors/cdb/write-not-permitted.md +9 -0
- package/docs/errors/discover/broadcast-unavailable.md +9 -0
- package/docs/errors/discover/failed.md +9 -0
- package/docs/errors/identity/ambiguous.md +9 -0
- package/docs/errors/identity/no-match.md +9 -0
- package/docs/errors/input/invalid-command.md +9 -0
- package/docs/errors/input/invalid-match.md +9 -0
- package/docs/errors/input/invalid-path.md +9 -0
- package/docs/errors/input/invalid-routeros-path.md +9 -0
- package/docs/errors/input/invalid-target.md +9 -0
- package/docs/errors/input/local-file-not-found.md +9 -0
- package/docs/errors/input/mac-address.md +9 -0
- package/docs/errors/input/max-results-exceeded.md +9 -0
- package/docs/errors/internal/devices-failed.md +9 -0
- package/docs/errors/internal/mcp-start.md +9 -0
- package/docs/errors/internal/mcp-tool.md +9 -0
- package/docs/errors/internal/native-api-length.md +9 -0
- package/docs/errors/internal/unhandled.md +9 -0
- package/docs/errors/internal/unreachable.md +9 -0
- package/docs/errors/mndp/encode-failed.md +9 -0
- package/docs/errors/mndp/listen-failed.md +9 -0
- package/docs/errors/mndp/malformed.md +9 -0
- package/docs/errors/routeros/api-fatal.md +9 -0
- package/docs/errors/routeros/api-protocol.md +9 -0
- package/docs/errors/routeros/api-trap.md +9 -0
- package/docs/errors/routeros/btest-protocol.md +12 -0
- package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
- package/docs/errors/routeros/command-failed.md +14 -0
- package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
- package/docs/errors/routeros/error.md +9 -0
- package/docs/errors/routeros/invalid-value.md +14 -0
- package/docs/errors/routeros/mac-telnet-error.md +9 -0
- package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
- package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
- package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
- package/docs/errors/routeros/protocol-not-implemented.md +9 -0
- package/docs/errors/routeros/request-failed.md +9 -0
- package/docs/errors/routeros/session-closed.md +13 -0
- package/docs/errors/routeros/unknown-attribute.md +13 -0
- package/docs/errors/routeros/unknown-path.md +15 -0
- package/docs/errors/routeros/unsupported-capability.md +9 -0
- package/docs/errors/settings/invalid-boolean.md +9 -0
- package/docs/errors/settings/invalid-format.md +9 -0
- package/docs/errors/settings/invalid-integer.md +9 -0
- package/docs/errors/settings/invalid-timeout.md +9 -0
- package/docs/errors/settings/invalid-via.md +9 -0
- package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
- package/docs/errors/target/mac-not-in-arp.md +14 -0
- package/docs/errors/target/mac-required.md +23 -0
- package/docs/errors/target/mac-unresolved.md +17 -0
- package/docs/errors/target/unresolved.md +9 -0
- package/docs/errors/transport/auth-failed.md +9 -0
- package/docs/errors/transport/auto-method.md +9 -0
- package/docs/errors/transport/capability-unsupported.md +22 -0
- package/docs/errors/transport/checksum-unavailable.md +9 -0
- package/docs/errors/transport/connection-closed.md +9 -0
- package/docs/errors/transport/connection-refused.md +9 -0
- package/docs/errors/transport/dns.md +9 -0
- package/docs/errors/transport/host-key-mismatch.md +9 -0
- package/docs/errors/transport/incomplete-transfer.md +9 -0
- package/docs/errors/transport/insecure-trust.md +9 -0
- package/docs/errors/transport/local-tool-missing.md +11 -0
- package/docs/errors/transport/network.md +9 -0
- package/docs/errors/transport/timeout.md +9 -0
- package/docs/errors/transport/tls-certificate.md +9 -0
- package/docs/errors/transport/unreachable.md +9 -0
- package/docs/errors/transport/unsupported-operation.md +9 -0
- package/docs/errors/usage/confirmation-required.md +9 -0
- package/docs/errors/usage/conflicting-flags.md +9 -0
- package/docs/errors/usage/invalid-concurrency.md +9 -0
- package/docs/errors/usage/missing-group.md +9 -0
- package/docs/errors/usage/not-implemented.md +9 -0
- package/docs/errors/usage/target-exists.md +9 -0
- package/docs/errors/usage/timeout-out-of-range.md +9 -0
- package/docs/errors/validation/not-implemented.md +9 -0
- package/docs/errors/validation/option.md +9 -0
- package/docs/errors/validation/syntax.md +9 -0
- package/docs/errors/validation/unknown-attribute.md +9 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +96 -0
- package/src/AGENTS.md +7 -0
- package/src/btest.ts +906 -0
- package/src/cli/btest.ts +455 -0
- package/src/cli/common.ts +53 -0
- package/src/cli/devices.ts +527 -0
- package/src/cli/discover.ts +186 -0
- package/src/cli/execute.ts +333 -0
- package/src/cli/mcp.ts +109 -0
- package/src/cli/retrieve.ts +383 -0
- package/src/cli/terminal.ts +303 -0
- package/src/cli/transfer.ts +471 -0
- package/src/cli.ts +115 -0
- package/src/core/envelope.ts +221 -0
- package/src/core/error-catalog.ts +469 -0
- package/src/core/routeros-errors.ts +268 -0
- package/src/data/AGENTS.md +11 -0
- package/src/data/dude-db.ts +0 -0
- package/src/data/mndp-cache.ts +132 -0
- package/src/data/mndp.ts +405 -0
- package/src/data/winbox-cdb-write.ts +200 -0
- package/src/data/winbox-cdb.ts +913 -0
- package/src/devices-schema.ts +107 -0
- package/src/devices.ts +1717 -0
- package/src/discover.ts +802 -0
- package/src/errors.ts +189 -0
- package/src/execute.ts +1350 -0
- package/src/index.ts +353 -0
- package/src/mcp/config.ts +66 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/safety.ts +119 -0
- package/src/mcp/server.ts +225 -0
- package/src/mcp/tools.ts +657 -0
- package/src/mcp.ts +33 -0
- package/src/protocols/AGENTS.md +14 -0
- package/src/protocols/adapter.ts +866 -0
- package/src/protocols/btest-session.ts +1493 -0
- package/src/protocols/btest.ts +447 -0
- package/src/protocols/ec-srp5.ts +466 -0
- package/src/protocols/index.ts +118 -0
- package/src/protocols/mac-telnet-console.ts +655 -0
- package/src/protocols/mac-telnet.ts +1299 -0
- package/src/protocols/mtwei.ts +47 -0
- package/src/protocols/native-api.ts +796 -0
- package/src/protocols/sftp.ts +427 -0
- package/src/protocols/ssh.ts +297 -0
- package/src/resolver/cdb.ts +580 -0
- package/src/resolver/comment-kv.ts +360 -0
- package/src/resolver/index.ts +74 -0
- package/src/resolver/mac.ts +227 -0
- package/src/resolver/settings.ts +228 -0
- package/src/resolver/target.ts +442 -0
- package/src/retrieve-fanout.ts +533 -0
- package/src/retrieve.ts +1215 -0
- package/src/terminal.ts +444 -0
- package/src/transfer.ts +1554 -0
- package/src/tui.ts +22 -0
- package/src/webproxy.ts +16 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# terminal — examples
|
|
2
|
+
|
|
3
|
+
Each numbered example is an executable spec. The integration test under
|
|
4
|
+
`test/integration/terminal-mac-telnet.test.ts` runs the CHR-testable examples
|
|
5
|
+
(T1–T3) against a CHR booted by `@tikoci/quickchr` over a real L2 path, driving
|
|
6
|
+
the **real `centrs terminal` binary** through the subprocess harness
|
|
7
|
+
(`test/integration/cli-process.ts`) pointed at the L2 bridge's UDP relay — the
|
|
8
|
+
same transport the `execute / mac-telnet` command path uses.
|
|
9
|
+
|
|
10
|
+
`terminal` is a **raw passthrough**, not an enveloped command: device terminal
|
|
11
|
+
bytes stream to **stdout**, and any error/summary goes to **stderr** (so the two
|
|
12
|
+
never interleave — the same split as `transfer download -`). On a clean session
|
|
13
|
+
close the exit code is `0`; a pre-stream failure (bad transport, auth, no route)
|
|
14
|
+
emits the standard error envelope on stderr and exits `1`.
|
|
15
|
+
|
|
16
|
+
Two input modes, chosen by whether stdin is a TTY:
|
|
17
|
+
|
|
18
|
+
- **Interactive** (stdin is a TTY): raw-mode stdin → the session, device output →
|
|
19
|
+
stdout, the real terminal size is reported to the device, and `SIGWINCH`
|
|
20
|
+
forwards resizes. This is the human path; it is described in T4 and verified by
|
|
21
|
+
hand (a piped test cannot be a TTY).
|
|
22
|
+
- **Batch** (stdin is a pipe/file): bytes are forwarded as typed and the session
|
|
23
|
+
closes when stdin reaches EOF (after a short output drain). This is what the
|
|
24
|
+
harness drives — pipe a command in, read the device's response off stdout.
|
|
25
|
+
|
|
26
|
+
Conventions (provided by the harness):
|
|
27
|
+
|
|
28
|
+
- `$MAC` is the CHR target interface MAC; `$U` / `$P` are MAC-Telnet credentials.
|
|
29
|
+
- `$HOST` / `$UDP` point the UDP transport at the loopback L2 bridge
|
|
30
|
+
(`--host 127.0.0.1 --port <bridge udpPort>`), exactly as the `execute /
|
|
31
|
+
mac-telnet` command-path test does. A real call omits these and lets the
|
|
32
|
+
adapter discover the L2 route.
|
|
33
|
+
|
|
34
|
+
## CHR-tested (T1–T3)
|
|
35
|
+
|
|
36
|
+
### T1. Run a command over a MAC-Telnet terminal (batch relay)
|
|
37
|
+
|
|
38
|
+
Pipe a CLI line into the terminal; the device's response appears on stdout.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
printf '/system/identity/print\n' | centrs terminal $MAC --via mac-telnet --host $HOST --port $UDP --username $U --password $P
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
stdout contains the device identity (cross-checked against REST). Exit code `0`.
|
|
45
|
+
|
|
46
|
+
### T2. `--via rest-api` has no terminal capability
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
centrs terminal $MAC --via rest-api --username $U --password $P
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Envelope on stderr: `ok: false`, `error.code="transport/capability-unsupported"`.
|
|
53
|
+
Exit code `1`. No bytes on stdout.
|
|
54
|
+
|
|
55
|
+
### T3. `--via native-api` has no terminal capability
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
centrs terminal $MAC --via native-api --username $U --password $P
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Same contract as T2: `transport/capability-unsupported` on stderr, exit `1`.
|
|
62
|
+
|
|
63
|
+
## Interactive (verified by hand, not in CI)
|
|
64
|
+
|
|
65
|
+
### T4. Open an interactive console to a MAC
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
centrs terminal $MAC --username $U --password $P
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A MAC target with no `--via` selects `mac-telnet` (the L2 default). stdin goes
|
|
72
|
+
into raw mode; keystrokes reach the device and its output paints stdout. The real
|
|
73
|
+
terminal rows/cols are reported to the console (so it neither wraps nor paginates)
|
|
74
|
+
and a window resize forwards via `SIGWINCH`. `Ctrl-D` / session end exits `0`.
|
|
75
|
+
|
|
76
|
+
## ssh (`--via ssh`)
|
|
77
|
+
|
|
78
|
+
`terminal / ssh` execs the **host `ssh`** with inherited stdio: RouterOS grants no
|
|
79
|
+
pseudo-tty, but `ssh user@host` opens the interactive console and the OS relays it
|
|
80
|
+
(the no-PTY stream is already clean — no screen emulation). centrs's value is
|
|
81
|
+
resolving the target/key/trust and building the argv; the interactive TTY, raw
|
|
82
|
+
mode, and signals are the inherited terminal's. A **host target defaults to ssh**
|
|
83
|
+
(a MAC target defaults to mac-telnet). Because the session is interactive, the
|
|
84
|
+
terminal argv omits `BatchMode=yes` (the batch sftp/execute clients keep it), so
|
|
85
|
+
the host `ssh` may prompt on the TTY for an encrypted key's passphrase or a
|
|
86
|
+
password — centrs does not forward `--password`, since OpenSSH takes no password
|
|
87
|
+
on the argv.
|
|
88
|
+
|
|
89
|
+
Harness inputs (from `test/integration/terminal-ssh.test.ts`): `$SSH_PORT` is
|
|
90
|
+
`chr.sshPort` (passed via `--port`), and `$KEY` is the ephemeral private key the
|
|
91
|
+
test mints with `ssh-keygen -f <tmp>/id` and passes via `--ssh-key`. **TS1–TS2 are
|
|
92
|
+
the CHR-tested set** (green via `bun run test:integration`, CHR 7.23.1) — the
|
|
93
|
+
matrix's `CHR-passed` claim rests on them. **TS3 is the interactive hand-verified
|
|
94
|
+
analog of T4** (a piped subprocess stdin cannot be a TTY), not in CI.
|
|
95
|
+
|
|
96
|
+
### TS1. Run a command over an SSH terminal (batch relay)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
printf '/system/identity/print\n/quit\n' | centrs terminal 127.0.0.1 --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
stdout contains the device identity (cross-checked against REST). centrs returns
|
|
103
|
+
ssh's exit code (a no-PTY console closed by EOF can exit non-zero — that is the
|
|
104
|
+
device/ssh's result, not a centrs failure; a clean `/quit` from a real TTY exits
|
|
105
|
+
`0`).
|
|
106
|
+
|
|
107
|
+
### TS2. `--via rest-api` has no terminal capability
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
centrs terminal 127.0.0.1 --via rest-api --username $U --password $P
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Envelope on stderr: `ok: false`, `error.code="transport/capability-unsupported"`,
|
|
114
|
+
exit `1` — same gate as mac-telnet's T2/T3, evaluated before any connection.
|
|
115
|
+
|
|
116
|
+
### TS3. Open an interactive console over SSH (verified by hand, not in CI)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
centrs terminal 192.0.2.10 --username $U --ssh-key $KEY
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A host target with no `--via` selects `ssh`. The inherited terminal hands the
|
|
123
|
+
RouterOS console to the user; `/quit` (or `Ctrl-D`) ends the session. RouterOS's
|
|
124
|
+
no-PTY limitation applies (e.g. multi-line brace blocks are not supported over
|
|
125
|
+
SSH — see the SSH page); single-line commands work.
|
|
126
|
+
|
|
127
|
+
## MAC target over `--via ssh` resolution (behavior note — not a numbered CHR example)
|
|
128
|
+
|
|
129
|
+
This path resolves a MAC to an IP and, on the unresolved / no-opt-in branch,
|
|
130
|
+
fails before reaching any router — so it is **not** a numbered CHR integration
|
|
131
|
+
example. It is covered by the network-free smoke tier
|
|
132
|
+
(`test/integration/cli-smoke.test.ts`, JG-01) and `test/unit/{terminal,mac}.test.ts`.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
centrs terminal aa:bb:cc:dd:ee:ff --via ssh --username $U --json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A MAC pinned to the IP transport needs an IP. With no matching CDB record and the
|
|
139
|
+
default `--resolve none`, the envelope on stderr is `ok: false`,
|
|
140
|
+
`error.code="target/mac-unresolved"`, exit `1` — and the remediation leads with
|
|
141
|
+
`--via mac-telnet` (reach it over L2, no IP needed). `--resolve arp` opts into the
|
|
142
|
+
host ARP cache instead; a miss is `target/mac-not-in-arp`. centrs never resolves a
|
|
143
|
+
MAC over ARP, or swaps to another transport, on its own (see
|
|
144
|
+
`commands/terminal/README.md` → *MAC target over SSH*).
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# transfer
|
|
2
|
+
|
|
3
|
+
Copy files to and from a RouterOS device, and manage device files.
|
|
4
|
+
|
|
5
|
+
Status (agrees with `docs/MATRIX.md`): `CHR-passed` for `rest-api` / `native-api`
|
|
6
|
+
and for `ssh`/**sftp** (`src/transfer.ts`, `src/cli/transfer.ts`,
|
|
7
|
+
`src/protocols/sftp.ts`). `test/integration/transfer.test.ts` is green against a
|
|
8
|
+
real CHR 7.23.1 (110 assertions): the rest + native round-trip, list + filters,
|
|
9
|
+
validate-before-write, device file management (mkdir/copy/remove), leading-slash
|
|
10
|
+
normalization, the >60 KB REST-write rejection, the error contract, the
|
|
11
|
+
stdin/stdout/default-local forms (examples 8–10, driven through the real CLI
|
|
12
|
+
binary via the subprocess harness `test/integration/cli-process.ts`), the native
|
|
13
|
+
`N1`–`N4` mirror, the sftp `S1`–`S5` round-trip (key-auth, the >60 KB upload REST
|
|
14
|
+
cannot do, list/mkdir/remove), and example 17 (chunked REST read of an
|
|
15
|
+
sftp-seeded >60 KB file) — confirming the `/file`
|
|
16
|
+
`get`/`set`/`add`/`copy`/`remove` wire shapes and the SFTP subsystem on real
|
|
17
|
+
RouterOS. Unit coverage is `test/unit/transfer.test.ts`. **sftp** is the first SSH
|
|
18
|
+
consumer — a self-contained SFTP transfer client over the host OpenSSH `sftp`
|
|
19
|
+
subsystem. `scp` and `fetch` are **designed but deferred** past this pass.
|
|
20
|
+
|
|
21
|
+
## Intent
|
|
22
|
+
|
|
23
|
+
`transfer` is centrs's file verb. Direction is explicit in the sub-verb —
|
|
24
|
+
`upload` is host → device (the WinBox "upload files" direction), `download` is
|
|
25
|
+
device → host. It honors the same `<router>` resolution, credentials, envelope,
|
|
26
|
+
and settings as every other command (see [`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md)
|
|
27
|
+
for identity/CDB resolution and the result envelope). Large transfers are exempt
|
|
28
|
+
from the REST 60 s timeout cap because they do not ride a single REST request.
|
|
29
|
+
|
|
30
|
+
centrs does not pick one method for you and hope; it picks the **cheapest method
|
|
31
|
+
that can carry this file in this direction**, reports the choice in
|
|
32
|
+
`meta.via`, and never silently crosses an explicit `--via`. See *Method
|
|
33
|
+
selection*.
|
|
34
|
+
|
|
35
|
+
## Synopsis
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
centrs transfer <router> upload <local> [<remote>] [flags]
|
|
39
|
+
centrs transfer <router> download <remote> [<local>] [flags]
|
|
40
|
+
centrs transfer <router> list [<path>] [flags]
|
|
41
|
+
centrs transfer <router> remove <remote> [flags]
|
|
42
|
+
centrs transfer <router> mkdir <remote> [flags]
|
|
43
|
+
centrs transfer <router> copy <remote-src> <remote-dst> [flags] # on-device
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `<router>` — IP, DNS, MAC, or CDB-resolved name. See constitution: identity.
|
|
47
|
+
- `<local>` — a host path, or `-` for stdin (`upload`) / stdout (`download`), so
|
|
48
|
+
`transfer` composes in shell pipelines. If omitted, `download` writes the
|
|
49
|
+
remote basename into the current directory.
|
|
50
|
+
- `<remote>` — a RouterOS file path (e.g. `flash/fw.rsc`). If omitted on
|
|
51
|
+
`upload`, centrs uses the **local basename**. *(Planned: prefer the device's
|
|
52
|
+
`flash` disk — `flash/<basename>` — when one is present.)*
|
|
53
|
+
- Remote paths match RouterOS exactly — no path rewriting. RouterOS file names
|
|
54
|
+
carry **no leading slash** (`/file/print` shows `flash/fw.rsc`), but the
|
|
55
|
+
`/file/add name=/flash/…` form *accepts* one, so centrs accepts a leading `/`
|
|
56
|
+
for ergonomics and **normalizes it away** to the canonical no-slash form used
|
|
57
|
+
on the wire (REST `/file` keys and SFTP paths both want it stripped).
|
|
58
|
+
`flash/fw.rsc` and `/flash/fw.rsc` are the same target.
|
|
59
|
+
- Reboot-persistence: on devices that have a `flash` disk, anything stored
|
|
60
|
+
**outside** `flash/` lives on a RAM disk and is lost on reboot — so prefer an
|
|
61
|
+
explicit `flash/…` remote on such devices. CHR has no `flash` disk —
|
|
62
|
+
everything persists. *(Planned: detect a `type=disk name=flash` row to default
|
|
63
|
+
omitted uploads into `flash/` and warn on an explicit non-`flash/` path;
|
|
64
|
+
centrs never silently relocates an explicit path.)*
|
|
65
|
+
|
|
66
|
+
`list` is thin sugar over `retrieve <router> /file` (a `/file/print` menu read),
|
|
67
|
+
rendered for humans (human-readable sizes, `flash/` tree, `.npk` package
|
|
68
|
+
metadata). For RouterOS-side row queries beyond `--type` / `--name`, use
|
|
69
|
+
`retrieve /file --query` directly.
|
|
70
|
+
|
|
71
|
+
### Top-level shortcuts
|
|
72
|
+
|
|
73
|
+
`upload` and `download` are also promoted to **top-level command aliases** for the
|
|
74
|
+
two highest-frequency operations, so the router stays the first positional:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
centrs upload <router> <local> [<remote>] [flags] → centrs transfer <router> upload <local> [<remote>]
|
|
78
|
+
centrs download <router> <remote> [<local>] [flags] → centrs transfer <router> download <remote> [<local>]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
They are pure aliases (identical flags, envelope, and validation); help and docs
|
|
82
|
+
show the canonical `transfer …` form, and the shared "did you mean?" / level-aware
|
|
83
|
+
help applies. Only `upload`/`download` are promoted — `list`/`remove`/`mkdir`/
|
|
84
|
+
`copy` stay under `transfer` because those verbs would be ambiguous at the top
|
|
85
|
+
level (every command can list or remove something).
|
|
86
|
+
|
|
87
|
+
## Method selection
|
|
88
|
+
|
|
89
|
+
The RouterOS file plumbing is asymmetric, so the default is direction- and
|
|
90
|
+
size-aware. Grounding: [Files](https://help.mikrotik.com/docs/spaces/ROS/pages/2555971/Files),
|
|
91
|
+
[Fetch](https://help.mikrotik.com/docs/spaces/ROS/pages/8978514/Fetch).
|
|
92
|
+
|
|
93
|
+
| Method (`--via`) | Grid transport | download (device → host) | upload (host → device) |
|
|
94
|
+
| ---------------- | -------------- | ------------------------ | ---------------------- |
|
|
95
|
+
| `rest` / `native` | rest-api / native-api | small via `/file/get … contents` (**≤ 60 KB**); **large via chunked `/file/read offset chunk-size≤32768`** | **`/file/set contents=` ≤ 60 KB only** — no chunked write |
|
|
96
|
+
| `sftp` (default secure) | ssh | any size | any size |
|
|
97
|
+
| `scp` | ssh | any size | any size |
|
|
98
|
+
| `fetch` *(deferred)* | rest-api/native-api + inbound HTTP | router PUTs to centrs (`upload=yes http-method=put src-path=…`) | router GETs from centrs (`url=http://centrs/… dst-path=…`) |
|
|
99
|
+
| `ftp` *(gated)* | ftp | any size | any size |
|
|
100
|
+
|
|
101
|
+
Auto-selection (no `--via`):
|
|
102
|
+
|
|
103
|
+
- **download** — `rest`/`native` for everything (chunked `/file/read` scales to
|
|
104
|
+
any size with no SSH service required); fall back to `sftp` only if the REST
|
|
105
|
+
family is unreachable.
|
|
106
|
+
- **upload ≤ 60 KB** — `rest`/`native` (`/file/set contents`); one round trip, no
|
|
107
|
+
SSH needed.
|
|
108
|
+
- **upload > 60 KB** — `sftp`. The REST family cannot write past 60 KB, and
|
|
109
|
+
`fetch` is the only REST-family way to push a large file without SSH — but it
|
|
110
|
+
needs inbound reachability we can't assume, so it is **explicit-only** (see
|
|
111
|
+
below), never auto-selected.
|
|
112
|
+
|
|
113
|
+
Every auto hop is reported in `meta.warnings` with its reason. An explicit
|
|
114
|
+
`--via` is never silently downgraded: if you pin `--via rest` and ask to upload a
|
|
115
|
+
2 MB file, centrs fails with `transport/unsupported-operation` (REST cannot write
|
|
116
|
+
past 60 KB) rather than quietly switching to sftp. `scp`, `fetch`, and `ftp` are
|
|
117
|
+
explicit-only; `ftp` additionally requires `ALLOW_UNSAFE_PROTOCOLS=ftp` because
|
|
118
|
+
it is cleartext.
|
|
119
|
+
|
|
120
|
+
> Future methods documented so callers know the capability exists, behind an
|
|
121
|
+
> explicit `--via`: `/system/smb` (not enabled by default) and `rose-storage`
|
|
122
|
+
> (`rsync`/`nfs`/`nvme-over-tcp`/`iscsi`, needs `rose-storage.npk`).
|
|
123
|
+
|
|
124
|
+
### SFTP vs SCP (why sftp is the SSH default)
|
|
125
|
+
|
|
126
|
+
RouterOS supports both over its SSH server, and MikroTik documents both — their
|
|
127
|
+
container guides use `sftp admin@<router>` to push/pull config files
|
|
128
|
+
([example 1](https://help.mikrotik.com/docs/spaces/ROS/pages/172294200/Container%2B-%2Bfreeradius%2Bserver),
|
|
129
|
+
[example 2](https://help.mikrotik.com/docs/spaces/ROS/pages/169246787/Container%2B-%2Bmosquitto%2BMQTT%2Bserver)),
|
|
130
|
+
while the [CHR-ProxMox guide](https://help.mikrotik.com/docs/spaces/ROS/pages/48660553/CHR%2BProxMox%2Binstallation)
|
|
131
|
+
uses `scp`. centrs makes **sftp the default and the primary investment**; `scp`
|
|
132
|
+
is an explicit `--via scp` escape hatch. The reason is capability, not taste:
|
|
133
|
+
|
|
134
|
+
- **SFTP is a real protocol** — `stat`, `readdir`, partial reads/writes, `mkdir`,
|
|
135
|
+
`remove`, `rename`. Those are exactly what centrs's feature set needs: the
|
|
136
|
+
validate-before-write existence check (`stat` the target) and the
|
|
137
|
+
`list`/`remove`/`mkdir` verbs all fall out of SFTP. (`--verify size` is still
|
|
138
|
+
supported over sftp — it just trusts the SFTP transfer guarantee rather than
|
|
139
|
+
re-reading a settled size, since RouterOS's `ls -l` has no reliable size column;
|
|
140
|
+
see *Integrity*.)
|
|
141
|
+
- **SCP is a dumb byte-stream** — no stat, no listing, no existence check; it just
|
|
142
|
+
blasts bytes and truncates on collision. An `--via scp` upload therefore
|
|
143
|
+
**cannot do the existence check itself** and would need a side-channel REST/SFTP
|
|
144
|
+
`stat` anyway — so `--via scp` skips the refuse-overwrite guard unless REST is
|
|
145
|
+
also reachable, and `--verify` degrades to `off` (warned). scp buys only a
|
|
146
|
+
marginally lighter single-file stream, irrelevant under the CHR 1 Mb/s cap.
|
|
147
|
+
- **Direction of the ecosystem** — OpenSSH deprecated the legacy SCP wire protocol
|
|
148
|
+
(9.0, 2022); modern `scp` rides SFTP underneath, so the reliable SSH file channel
|
|
149
|
+
is the **SFTP subsystem**, not an exec-driven copy. (RouterOS's SSH server has no
|
|
150
|
+
pseudo-tty, but a single-line `ssh user@host "<command>"` *does* run and return
|
|
151
|
+
clean output — that path drives `execute / ssh`, see `commands/execute/README.md`;
|
|
152
|
+
it is not a file-transfer channel.)
|
|
153
|
+
|
|
154
|
+
## Flags
|
|
155
|
+
|
|
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. |
|
|
164
|
+
| `--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. |
|
|
176
|
+
|
|
177
|
+
## Integrity
|
|
178
|
+
|
|
179
|
+
`--verify size` (default) over `rest`/`native` reads the settled `/file` size
|
|
180
|
+
after the transfer and compares it to the byte count centrs moved. Over **sftp**,
|
|
181
|
+
RouterOS's `ls -l` reports no reliable byte size (CHR 7.23.1), so `--verify size`
|
|
182
|
+
instead trusts the SFTP transfer guarantee — a partial `put`/`get` errors — rather
|
|
183
|
+
than re-reading a size. Two RouterOS facts shape the `/file`-size path:
|
|
184
|
+
|
|
185
|
+
- **NAND write-back** delays the flush up to ~40 s on some devices, so the size
|
|
186
|
+
may read short immediately after an upload. centrs does a single post-write
|
|
187
|
+
`/file/print` size probe today. *(Planned: a brief poll/retry to absorb the
|
|
188
|
+
write-back window before declaring a mismatch.)*
|
|
189
|
+
- **Transparent compression**: the `/file` menu reports the *uncompressed* size
|
|
190
|
+
(compression only shows up in `/system/resource` free space), so size-compare
|
|
191
|
+
against the uncompressed byte count we sent stays valid.
|
|
192
|
+
|
|
193
|
+
A confirmed shortfall (clear truncation after the poll window) is
|
|
194
|
+
`transport/incomplete-transfer`. `--verify checksum` is stronger but only where
|
|
195
|
+
RouterOS exposes a usable digest; otherwise it degrades to `size` with a warning.
|
|
196
|
+
|
|
197
|
+
## Output shape
|
|
198
|
+
|
|
199
|
+
The downloaded/uploaded bytes go to disk (or stdout for `download -`); the
|
|
200
|
+
envelope carries transfer *metadata*, not file contents.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
{
|
|
204
|
+
ok: true,
|
|
205
|
+
data: {
|
|
206
|
+
op: "upload", // upload | download | list | remove | mkdir | copy
|
|
207
|
+
remote: "flash/fw.rsc",
|
|
208
|
+
local: "./fw.rsc", // null for list/remove/mkdir/copy
|
|
209
|
+
bytes: 1048576,
|
|
210
|
+
verified: "size", // size | checksum | off
|
|
211
|
+
durationMs: 1840
|
|
212
|
+
},
|
|
213
|
+
warnings: [], // e.g. auto-method hops, insecure-trust
|
|
214
|
+
meta: { target, via: "ssh", settings, validation, timing } // via is the grid transport: rest-api | native-api | ssh
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`list` returns `data` as the `/file` row array (same shape as
|
|
219
|
+
`retrieve /file`). Fanout returns the shared one-outer-envelope form documented
|
|
220
|
+
in `commands/retrieve/README.md` (Group fanout): `ok` reports orchestration
|
|
221
|
+
success, per-target results live in `data.targets`.
|
|
222
|
+
|
|
223
|
+
## Validation
|
|
224
|
+
|
|
225
|
+
`transfer` validates what it can locally first — conflicting flags
|
|
226
|
+
(`usage/conflicting-flags`), a missing local source on `upload`, a `--via rest`
|
|
227
|
+
upload that exceeds 60 KB (`transport/unsupported-operation`). The
|
|
228
|
+
**refuse-overwrite** guard is a real precondition probe, not a local check:
|
|
229
|
+
unless `--force`, centrs `stat`s the destination (SFTP `stat`, or `/file/print`
|
|
230
|
+
for the REST family) and fails with `usage/target-exists` if it is already there.
|
|
231
|
+
This is the "validate before write" gate applied to files — see constitution:
|
|
232
|
+
validation is the product.
|
|
233
|
+
|
|
234
|
+
The `/file` menu path is fixed, so there is no `/console/inspect` path/attribute
|
|
235
|
+
gate as in `retrieve` / `execute`; RouterOS's own menu errors (missing remote
|
|
236
|
+
file, permission denied) map to `routeros/*`, and SFTP/SCP/fetch errors map to
|
|
237
|
+
`transport/*` and `auth/*`.
|
|
238
|
+
|
|
239
|
+
**Error codes reuse existing namespaces — `transfer` adds no namespace of its
|
|
240
|
+
own.** Everything specific to transfer fits `usage/*` (`usage/target-exists`),
|
|
241
|
+
`transport/*` (`transport/unsupported-operation` for the REST 60 KB write cap,
|
|
242
|
+
`transport/incomplete-transfer` for a verified shortfall, `transport/unreachable`
|
|
243
|
+
for fetch inbound-reachability), `routeros/*`, `auth/*`, and `validation/*`. A new
|
|
244
|
+
code is only justified where the *semantics* are genuinely file-transfer-specific
|
|
245
|
+
and not already expressible in those namespaces.
|
|
246
|
+
|
|
247
|
+
## Decided (2026-06-10)
|
|
248
|
+
|
|
249
|
+
- **Verbs are `upload` / `download` / `list`** (+ `remove` / `mkdir` / `copy` for
|
|
250
|
+
device file management). Direction is explicit in the sub-verb; `upload` =
|
|
251
|
+
host → device per WinBox wording. **`copy` needs RouterOS ≥ 7.23beta2**:
|
|
252
|
+
server-side copy rides the REST `/file/copy` endpoint, first seen in 7.23beta2;
|
|
253
|
+
earlier RouterOS (e.g. 7.21.x long-term) returns `no such command` (the
|
|
254
|
+
integration test gates the `copy` example on the running version).
|
|
255
|
+
- **Method selection is size/direction-aware** (table above), superseding the
|
|
256
|
+
earlier "sftp is always the default" decision. REST/native carry small writes
|
|
257
|
+
and all reads (reads scale via chunked `/file/read`); sftp carries large
|
|
258
|
+
uploads. This updates the constitution's protocol-selection row.
|
|
259
|
+
- **sftp is the SSH default, scp is `--via scp` only** — sftp's `stat`/`readdir`/
|
|
260
|
+
partial ops are what enable the existence check, `--verify`, and
|
|
261
|
+
`list`/`remove`/`mkdir`; scp can't do those and degrades the overwrite guard and
|
|
262
|
+
`--verify`. See *SFTP vs SCP*.
|
|
263
|
+
- **Remote paths match RouterOS** (no rewriting); a leading `/` is accepted but
|
|
264
|
+
normalized away to the canonical no-slash wire form. The omitted-`<remote>`
|
|
265
|
+
default uses `flash/` on devices that have a flash disk.
|
|
266
|
+
- **`upload` / `download` are also top-level command aliases** (`centrs upload
|
|
267
|
+
<router> …`), the only sub-verbs promoted to the top level.
|
|
268
|
+
- **Error codes reuse existing namespaces** — no `transfer/*` namespace; new codes
|
|
269
|
+
only where the semantics are genuinely transfer-specific.
|
|
270
|
+
- **`fetch` (centrs-as-HTTP-server + `/tool/fetch`) is explicit-only and
|
|
271
|
+
deferred** — designed here, built after sftp + REST land. It is the only
|
|
272
|
+
REST-family way to push a > 60 KB file without SSH, but needs inbound
|
|
273
|
+
reachability (router → centrs), so it is never auto-selected.
|
|
274
|
+
- **v1 includes** device file management (`remove`/`mkdir`/`copy`), integrity
|
|
275
|
+
verification (`--verify`), and stdout/stdin piping (`-`). **Directory /
|
|
276
|
+
recursive transfer is a later phase.**
|
|
277
|
+
- **CI proof is a small-file round-trip**, not a throughput stress test — the
|
|
278
|
+
free CHR license caps throughput at 1 Mb/s.
|
|
279
|
+
|
|
280
|
+
## Definition of done
|
|
281
|
+
|
|
282
|
+
`CHR-passed` only when every line in `examples.md` runs green against a real CHR
|
|
283
|
+
through `bun run test:integration` (one example ↔ one assertion). Disabling
|
|
284
|
+
validation to reach green is forbidden. See `docs/CONSTITUTION.md` for the full
|
|
285
|
+
done rule. The first coded pass targets `rest`/`native` (no SSH dependency);
|
|
286
|
+
`sftp`/`scp` advance with the SSH transport; `fetch` and `ftp` follow.
|
|
287
|
+
|
|
288
|
+
## Notes for future cells
|
|
289
|
+
|
|
290
|
+
- **fetch design (deferred).** centrs starts an ephemeral single-use HTTP server
|
|
291
|
+
(random URL token, bound to the interface facing the router) and drives the
|
|
292
|
+
router with `/tool/fetch` over `rest`/`native`. **upload**: serve the local
|
|
293
|
+
file, `fetch url=http://<advertise>/<token> dst-path=<remote>` → router GETs.
|
|
294
|
+
**download**: `fetch upload=yes http-method=put url=http://<advertise>/<token>
|
|
295
|
+
src-path=<remote>` → router PUTs to centrs (`http-method` any since 7.21;
|
|
296
|
+
binary REST serialization fixed 7.17). Open questions: advertise-host
|
|
297
|
+
auto-detection behind NAT, HTTP-vs-HTTPS + `check-certificate`, and whether to
|
|
298
|
+
add fetch's `user`/`password` on top of the URL token.
|
|
299
|
+
- **ssh** — `sftp` is **built** (`src/protocols/sftp.ts`, host OpenSSH `sftp`
|
|
300
|
+
subsystem; `scp` is the deferred follow-on). The host SSH client owns
|
|
301
|
+
host-key/agent/algorithm negotiation; the centrs-side trust knob is the unified
|
|
302
|
+
`--insecure` (constitution: transport trust). Two RouterOS auth facts transfer
|
|
303
|
+
inherits, grounded on the [SSH](https://help.mikrotik.com/docs/spaces/ROS/pages/132350014/SSH)
|
|
304
|
+
/ [User](https://help.mikrotik.com/docs/spaces/ROS/pages/8978504/User) pages:
|
|
305
|
+
**by default** a user cannot password-auth over SSH once an SSH key is set for it
|
|
306
|
+
(`/ip/ssh password-authentication=yes-if-no-key`, the default — settable to
|
|
307
|
+
`yes` to allow both), so key auth is the normal path; and `strong-crypto=yes`
|
|
308
|
+
disables ssh-rsa/SHA1, so the client must offer ed25519 / rsa-sha2-256 (the host
|
|
309
|
+
OpenSSH negotiates this natively). The SSH file path is the SFTP **subsystem**
|
|
310
|
+
(a real file protocol), not exec-driven scp; on-device `copy`
|
|
311
|
+
has no SFTP primitive and stays on rest/native (`--via sftp copy` →
|
|
312
|
+
`transport/unsupported-operation`). See `commands/terminal/README.md` (RouterOS
|
|
313
|
+
SSH surface) for the full device-side option alignment.
|
|
314
|
+
- **list ↔ retrieve** — `transfer list` is sugar over `retrieve /file`; keep the
|
|
315
|
+
read logic in one place.
|