@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
package/src/cli/btest.ts
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `centrs btest` CLI surface: the `client <router>` and `server` sub-verbs, arg
|
|
3
|
+
* parsing, option-grammar dispatch, and live (text/csv) vs final (json/yaml)
|
|
4
|
+
* output. Flag names mirror `/tool/bandwidth-test` and `/tool/bandwidth-server`
|
|
5
|
+
* verbatim; centrs adds only `--bind` (server safety) and the shared `--format`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
BTEST_CLIENT_CSV_HEADER,
|
|
10
|
+
BTEST_SERVER_CSV_HEADER,
|
|
11
|
+
type BtestClientRequest,
|
|
12
|
+
type BtestOutputFormat,
|
|
13
|
+
type BtestServerRequest,
|
|
14
|
+
btestClient,
|
|
15
|
+
btestClientCsvRow,
|
|
16
|
+
btestClientSummaryLine,
|
|
17
|
+
btestOutputFormats,
|
|
18
|
+
btestServer,
|
|
19
|
+
btestServerCsvRow,
|
|
20
|
+
btestServerSummaryLine,
|
|
21
|
+
formatBtestReportText,
|
|
22
|
+
formatBtestSessionText,
|
|
23
|
+
renderBtestClientEnvelope,
|
|
24
|
+
renderBtestServerEnvelope,
|
|
25
|
+
} from "../btest.ts";
|
|
26
|
+
import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
|
|
27
|
+
import { describeCentrs, parseDuration } from "../index.ts";
|
|
28
|
+
import type { BtestDirection, BtestProtocol } from "../protocols/btest.ts";
|
|
29
|
+
import {
|
|
30
|
+
type CliCommandMetadata,
|
|
31
|
+
expectValue,
|
|
32
|
+
renderCommandHelp,
|
|
33
|
+
} from "./common.ts";
|
|
34
|
+
|
|
35
|
+
export const btestCommand: CliCommandMetadata = {
|
|
36
|
+
name: "btest",
|
|
37
|
+
usage: "centrs btest <client <router>|server> [flags]",
|
|
38
|
+
summary:
|
|
39
|
+
"Run MikroTik's bandwidth test as a client or server (peer measurement).",
|
|
40
|
+
options: [
|
|
41
|
+
{
|
|
42
|
+
flag: "--protocol",
|
|
43
|
+
valueName: "<udp|tcp>",
|
|
44
|
+
description: "(client) Transport. Default udp.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
flag: "--direction",
|
|
48
|
+
valueName: "<receive|transmit|both>",
|
|
49
|
+
description: "(client) Test direction. Default receive.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
flag: "--duration",
|
|
53
|
+
valueName: "<dur>",
|
|
54
|
+
description: "Bound the run (e.g. 15s). Omit for open-ended (Ctrl-C).",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
flag: "--interval",
|
|
58
|
+
valueName: "<dur>",
|
|
59
|
+
description: "Report cadence (20ms..5s). Default 1s.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
flag: "--connection-count",
|
|
63
|
+
valueName: "<n>",
|
|
64
|
+
description: "(client, TCP only) Parallel TCP data connections (1..255).",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
flag: "--local-udp-tx-size",
|
|
68
|
+
valueName: "<n>",
|
|
69
|
+
description: "(client, UDP only) Client→server packet size (28..64000).",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
flag: "--remote-udp-tx-size",
|
|
73
|
+
valueName: "<n>",
|
|
74
|
+
description: "(client, UDP only) Server→client packet size (28..64000).",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
flag: "--local-tx-speed",
|
|
78
|
+
valueName: "<bps>",
|
|
79
|
+
description: "(client) Cap on client→server rate (e.g. 100M).",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
flag: "--remote-tx-speed",
|
|
83
|
+
valueName: "<bps>",
|
|
84
|
+
description: "(client) Cap on server→client rate (e.g. 100M).",
|
|
85
|
+
},
|
|
86
|
+
{ flag: "--random-data", description: "(client) Incompressible payload." },
|
|
87
|
+
{
|
|
88
|
+
flag: "--authenticate[=false]",
|
|
89
|
+
description: "(server) Require EC-SRP5 auth. Default true.",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
flag: "--bind",
|
|
93
|
+
valueName: "<addr>",
|
|
94
|
+
description: "(server) Listen address. Default 127.0.0.1.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
flag: "--allocate-udp-ports-from",
|
|
98
|
+
valueName: "<n>",
|
|
99
|
+
description: "(server) Base of the UDP data-port range. Default 2001.",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
flag: "--max-sessions",
|
|
103
|
+
valueName: "<n>",
|
|
104
|
+
description: "(server) Concurrent test cap (1..1000). Default 100.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
flag: "--user / --username / -u",
|
|
108
|
+
valueName: "<name>",
|
|
109
|
+
description:
|
|
110
|
+
"Credential (aliases --username, -u; falls back to CDB / CENTRS_USERNAME).",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
flag: "--password",
|
|
114
|
+
valueName: "<secret>",
|
|
115
|
+
description: "Credential (falls back to CDB / CENTRS_PASSWORD).",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
flag: "--port",
|
|
119
|
+
valueName: "<port>",
|
|
120
|
+
description: "Control port. Default 2000.",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
flag: "--format",
|
|
124
|
+
valueName: `<${btestOutputFormats.join("|")}>`,
|
|
125
|
+
description: "Output format. Default text.",
|
|
126
|
+
},
|
|
127
|
+
{ flag: "--csv", description: "Shortcut for `--format csv`." },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Parse a bandwidth value: a bare integer or a `K`/`M`/`G` suffix (bits/sec). */
|
|
132
|
+
function parseBandwidth(value: string, flag: string): number {
|
|
133
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)([kKmMgG]?)$/);
|
|
134
|
+
if (!match) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${flag} must be a number with optional K/M/G; got ${value}.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const scale: Record<string, number> = {
|
|
140
|
+
"": 1,
|
|
141
|
+
k: 1e3,
|
|
142
|
+
m: 1e6,
|
|
143
|
+
g: 1e9,
|
|
144
|
+
};
|
|
145
|
+
const result = Math.round(
|
|
146
|
+
Number(match[1]) * (scale[(match[2] ?? "").toLowerCase()] ?? 1),
|
|
147
|
+
);
|
|
148
|
+
if (result > 0xffffffff) {
|
|
149
|
+
throw new Error(`${flag} exceeds the 32-bit wire field (max ~4.29G).`);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseIntFlag(value: string, flag: string): number {
|
|
155
|
+
const n = Number.parseInt(value, 10);
|
|
156
|
+
if (!Number.isInteger(n))
|
|
157
|
+
throw new Error(`${flag} must be an integer; got ${value}.`);
|
|
158
|
+
return n;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseFormat(value: string): BtestOutputFormat {
|
|
162
|
+
if (!btestOutputFormats.includes(value as BtestOutputFormat)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`--format must be one of ${btestOutputFormats.join(", ")}; got ${value}.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return value as BtestOutputFormat;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface CommonCliArgs {
|
|
171
|
+
help?: boolean;
|
|
172
|
+
format?: BtestOutputFormat;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface ClientCliArgs extends CommonCliArgs {
|
|
176
|
+
request: BtestClientRequest;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ServerCliArgs extends CommonCliArgs {
|
|
180
|
+
request: BtestServerRequest;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseClientArgs(args: readonly string[]): ClientCliArgs {
|
|
184
|
+
const request: BtestClientRequest = { env: Bun.env };
|
|
185
|
+
const parsed: ClientCliArgs = { request };
|
|
186
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
187
|
+
const arg = args[i];
|
|
188
|
+
if (arg === undefined) continue;
|
|
189
|
+
switch (arg) {
|
|
190
|
+
case "--help":
|
|
191
|
+
case "-h":
|
|
192
|
+
parsed.help = true;
|
|
193
|
+
break;
|
|
194
|
+
case "--protocol": {
|
|
195
|
+
const v = expectValue(args, ++i, arg);
|
|
196
|
+
if (v !== "udp" && v !== "tcp")
|
|
197
|
+
throw new Error(`--protocol must be udp|tcp; got ${v}.`);
|
|
198
|
+
request.protocol = v as BtestProtocol;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "--direction": {
|
|
202
|
+
const v = expectValue(args, ++i, arg);
|
|
203
|
+
if (v !== "receive" && v !== "transmit" && v !== "both")
|
|
204
|
+
throw new Error(
|
|
205
|
+
`--direction must be receive|transmit|both; got ${v}.`,
|
|
206
|
+
);
|
|
207
|
+
request.direction = v as BtestDirection;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "--duration":
|
|
211
|
+
request.durationMs = parseDuration(expectValue(args, ++i, arg));
|
|
212
|
+
break;
|
|
213
|
+
case "--interval":
|
|
214
|
+
request.intervalMs = parseDuration(expectValue(args, ++i, arg));
|
|
215
|
+
break;
|
|
216
|
+
case "--connection-count":
|
|
217
|
+
request.connectionCount = parseIntFlag(
|
|
218
|
+
expectValue(args, ++i, arg),
|
|
219
|
+
arg,
|
|
220
|
+
);
|
|
221
|
+
break;
|
|
222
|
+
case "--local-udp-tx-size":
|
|
223
|
+
request.localUdpTxSize = parseIntFlag(expectValue(args, ++i, arg), arg);
|
|
224
|
+
break;
|
|
225
|
+
case "--remote-udp-tx-size":
|
|
226
|
+
request.remoteUdpTxSize = parseIntFlag(
|
|
227
|
+
expectValue(args, ++i, arg),
|
|
228
|
+
arg,
|
|
229
|
+
);
|
|
230
|
+
break;
|
|
231
|
+
case "--local-tx-speed":
|
|
232
|
+
request.localTxSpeed = parseBandwidth(expectValue(args, ++i, arg), arg);
|
|
233
|
+
break;
|
|
234
|
+
case "--remote-tx-speed":
|
|
235
|
+
request.remoteTxSpeed = parseBandwidth(
|
|
236
|
+
expectValue(args, ++i, arg),
|
|
237
|
+
arg,
|
|
238
|
+
);
|
|
239
|
+
break;
|
|
240
|
+
case "--random-data":
|
|
241
|
+
request.randomData = true;
|
|
242
|
+
break;
|
|
243
|
+
case "--nat-mode":
|
|
244
|
+
request.natMode = true;
|
|
245
|
+
break;
|
|
246
|
+
case "--username":
|
|
247
|
+
case "-u":
|
|
248
|
+
case "--user":
|
|
249
|
+
request.username = expectValue(args, ++i, arg);
|
|
250
|
+
break;
|
|
251
|
+
case "--password":
|
|
252
|
+
request.password = expectValue(args, ++i, arg);
|
|
253
|
+
break;
|
|
254
|
+
case "--port":
|
|
255
|
+
request.controlPort = parseIntFlag(expectValue(args, ++i, arg), arg);
|
|
256
|
+
break;
|
|
257
|
+
case "--cdb-file":
|
|
258
|
+
request.cdbFile = expectValue(args, ++i, arg);
|
|
259
|
+
break;
|
|
260
|
+
case "--cdb-password":
|
|
261
|
+
request.cdbPassword = expectValue(args, ++i, arg);
|
|
262
|
+
break;
|
|
263
|
+
case "--csv":
|
|
264
|
+
parsed.format = "csv";
|
|
265
|
+
break;
|
|
266
|
+
case "--format":
|
|
267
|
+
parsed.format = parseFormat(expectValue(args, ++i, arg));
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
if (arg.startsWith("-"))
|
|
271
|
+
throw new Error(`Unknown btest client flag: ${arg}`);
|
|
272
|
+
if (request.targetInput !== undefined)
|
|
273
|
+
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
274
|
+
request.targetInput = arg;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return parsed;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseServerArgs(args: readonly string[]): ServerCliArgs {
|
|
281
|
+
const request: BtestServerRequest = { env: Bun.env };
|
|
282
|
+
const parsed: ServerCliArgs = { request };
|
|
283
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
284
|
+
const arg = args[i];
|
|
285
|
+
if (arg === undefined) continue;
|
|
286
|
+
if (arg === "--authenticate" || arg === "--authenticate=true") {
|
|
287
|
+
request.authenticate = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (arg === "--authenticate=false" || arg === "--no-authenticate") {
|
|
291
|
+
request.authenticate = false;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
switch (arg) {
|
|
295
|
+
case "--help":
|
|
296
|
+
case "-h":
|
|
297
|
+
parsed.help = true;
|
|
298
|
+
break;
|
|
299
|
+
case "--bind":
|
|
300
|
+
request.bind = expectValue(args, ++i, arg);
|
|
301
|
+
break;
|
|
302
|
+
case "--allocate-udp-ports-from":
|
|
303
|
+
request.allocateUdpPortsFrom = parseIntFlag(
|
|
304
|
+
expectValue(args, ++i, arg),
|
|
305
|
+
arg,
|
|
306
|
+
);
|
|
307
|
+
break;
|
|
308
|
+
case "--max-sessions":
|
|
309
|
+
request.maxSessions = parseIntFlag(expectValue(args, ++i, arg), arg);
|
|
310
|
+
break;
|
|
311
|
+
case "--interval":
|
|
312
|
+
request.intervalMs = parseDuration(expectValue(args, ++i, arg));
|
|
313
|
+
break;
|
|
314
|
+
case "--duration":
|
|
315
|
+
request.durationMs = parseDuration(expectValue(args, ++i, arg));
|
|
316
|
+
break;
|
|
317
|
+
case "--username":
|
|
318
|
+
case "-u":
|
|
319
|
+
case "--user":
|
|
320
|
+
request.username = expectValue(args, ++i, arg);
|
|
321
|
+
break;
|
|
322
|
+
case "--password":
|
|
323
|
+
request.password = expectValue(args, ++i, arg);
|
|
324
|
+
break;
|
|
325
|
+
case "--port":
|
|
326
|
+
request.port = parseIntFlag(expectValue(args, ++i, arg), arg);
|
|
327
|
+
break;
|
|
328
|
+
case "--csv":
|
|
329
|
+
parsed.format = "csv";
|
|
330
|
+
break;
|
|
331
|
+
case "--format":
|
|
332
|
+
parsed.format = parseFormat(expectValue(args, ++i, arg));
|
|
333
|
+
break;
|
|
334
|
+
default:
|
|
335
|
+
if (arg.startsWith("-"))
|
|
336
|
+
throw new Error(`Unknown btest server flag: ${arg}`);
|
|
337
|
+
throw new Error(
|
|
338
|
+
`\`centrs btest server\` takes no positional arguments; got: ${arg}.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return parsed;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Install a SIGINT→AbortController so an open-ended run stops on Ctrl-C. */
|
|
346
|
+
function sigintSignal(): AbortSignal {
|
|
347
|
+
const controller = new AbortController();
|
|
348
|
+
process.once("SIGINT", () => controller.abort());
|
|
349
|
+
return controller.signal;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function runClient(args: readonly string[]): Promise<number> {
|
|
353
|
+
const { help, format = "text", request } = parseClientArgs(args);
|
|
354
|
+
if (help) {
|
|
355
|
+
console.log(renderCommandHelp(describeCentrs(), btestCommand));
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
const streaming = format === "text" || format === "csv";
|
|
359
|
+
let csvHeader = false;
|
|
360
|
+
if (streaming) {
|
|
361
|
+
request.onReport = (record) => {
|
|
362
|
+
if (format === "csv") {
|
|
363
|
+
if (!csvHeader) {
|
|
364
|
+
console.log(BTEST_CLIENT_CSV_HEADER);
|
|
365
|
+
csvHeader = true;
|
|
366
|
+
}
|
|
367
|
+
console.log(btestClientCsvRow(record));
|
|
368
|
+
} else {
|
|
369
|
+
console.log(formatBtestReportText(record));
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
request.signal = sigintSignal();
|
|
373
|
+
}
|
|
374
|
+
const envelope = await btestClient(request);
|
|
375
|
+
|
|
376
|
+
if (format === "json" || format === "yaml") {
|
|
377
|
+
const rendered = renderBtestClientEnvelope(envelope, format);
|
|
378
|
+
if (envelope.ok) console.log(rendered);
|
|
379
|
+
else console.error(rendered);
|
|
380
|
+
return envelope.ok ? 0 : 1;
|
|
381
|
+
}
|
|
382
|
+
// text / csv already streamed live; emit the trailing summary or error.
|
|
383
|
+
if (envelope.ok) {
|
|
384
|
+
if (format === "csv" && !csvHeader) console.log(BTEST_CLIENT_CSV_HEADER);
|
|
385
|
+
if (format === "text") console.log(btestClientSummaryLine(envelope.data));
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
if (format === "csv" && !csvHeader) console.log(BTEST_CLIENT_CSV_HEADER);
|
|
389
|
+
console.error(formatCentrsErrorText(envelope.error));
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function runServer(args: readonly string[]): Promise<number> {
|
|
394
|
+
const { help, format = "text", request } = parseServerArgs(args);
|
|
395
|
+
if (help) {
|
|
396
|
+
console.log(renderCommandHelp(describeCentrs(), btestCommand));
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
const streaming = format === "text" || format === "csv";
|
|
400
|
+
let csvHeader = false;
|
|
401
|
+
if (streaming) {
|
|
402
|
+
request.onSession = (record) => {
|
|
403
|
+
if (format === "csv") {
|
|
404
|
+
if (!csvHeader) {
|
|
405
|
+
console.log(BTEST_SERVER_CSV_HEADER);
|
|
406
|
+
csvHeader = true;
|
|
407
|
+
}
|
|
408
|
+
console.log(btestServerCsvRow(record));
|
|
409
|
+
} else {
|
|
410
|
+
console.log(formatBtestSessionText(record));
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
request.signal = sigintSignal();
|
|
415
|
+
const envelope = await btestServer(request);
|
|
416
|
+
|
|
417
|
+
if (format === "json" || format === "yaml") {
|
|
418
|
+
const rendered = renderBtestServerEnvelope(envelope, format);
|
|
419
|
+
if (envelope.ok) console.log(rendered);
|
|
420
|
+
else console.error(rendered);
|
|
421
|
+
return envelope.ok ? 0 : 1;
|
|
422
|
+
}
|
|
423
|
+
if (envelope.ok) {
|
|
424
|
+
if (format === "csv" && !csvHeader) console.log(BTEST_SERVER_CSV_HEADER);
|
|
425
|
+
if (format === "text") console.log(btestServerSummaryLine(envelope.data));
|
|
426
|
+
for (const w of envelope.warnings)
|
|
427
|
+
console.error(`warning: [${w.code}] ${w.message}`);
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
console.error(formatCentrsErrorText(envelope.error));
|
|
431
|
+
return 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function runBtestCli(args: readonly string[]): Promise<number> {
|
|
435
|
+
try {
|
|
436
|
+
const [sub, ...rest] = args;
|
|
437
|
+
if (sub === undefined || sub === "--help" || sub === "-h") {
|
|
438
|
+
console.log(renderCommandHelp(describeCentrs(), btestCommand));
|
|
439
|
+
return sub === undefined ? 1 : 0;
|
|
440
|
+
}
|
|
441
|
+
if (sub === "client") return await runClient(rest);
|
|
442
|
+
if (sub === "server") return await runServer(rest);
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Unknown btest sub-command: ${sub}. Use \`client <router>\` or \`server\`.`,
|
|
445
|
+
);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const centrsError = asCentrsError(error, {
|
|
448
|
+
code: "input/invalid-command",
|
|
449
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
450
|
+
remediation: "Use `centrs btest --help` to inspect the supported flags.",
|
|
451
|
+
});
|
|
452
|
+
console.error(formatCentrsErrorText(centrsError));
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI plumbing: command metadata shape, help rendering, and small arg
|
|
3
|
+
* helpers used by every `centrs <command>` runner. Per-command parsing,
|
|
4
|
+
* dispatch, and error-to-envelope handling live in the sibling modules
|
|
5
|
+
* (`./retrieve.ts`, `./devices.ts`, `./execute.ts`); `../cli.ts` wires them
|
|
6
|
+
* together.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface CliCommandOption {
|
|
10
|
+
flag: string;
|
|
11
|
+
valueName?: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CliCommandMetadata {
|
|
16
|
+
name: string;
|
|
17
|
+
usage: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
options: readonly CliCommandOption[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function expectValue(
|
|
23
|
+
args: readonly string[],
|
|
24
|
+
index: number,
|
|
25
|
+
flag: string,
|
|
26
|
+
): string {
|
|
27
|
+
const value = args[index];
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderCommandHelp(
|
|
35
|
+
describe: string,
|
|
36
|
+
command: CliCommandMetadata,
|
|
37
|
+
): string {
|
|
38
|
+
return [
|
|
39
|
+
describe,
|
|
40
|
+
"",
|
|
41
|
+
command.summary,
|
|
42
|
+
"",
|
|
43
|
+
`Usage: ${command.usage}`,
|
|
44
|
+
"",
|
|
45
|
+
"Options:",
|
|
46
|
+
...command.options.map((option) => {
|
|
47
|
+
const flag = option.valueName
|
|
48
|
+
? `${option.flag} ${option.valueName}`
|
|
49
|
+
: option.flag;
|
|
50
|
+
return ` ${flag.padEnd(28)} ${option.description}`;
|
|
51
|
+
}),
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|