@tikoci/centrs 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +107 -2
  2. package/GLOSSARY.txt +87 -0
  3. package/README.md +25 -9
  4. package/SECURITY.md +3 -3
  5. package/commands/AGENTS.md +3 -2
  6. package/commands/api/AGENTS.md +212 -0
  7. package/commands/api/README.md +177 -0
  8. package/commands/api/examples.md +428 -0
  9. package/commands/btest/README.md +56 -31
  10. package/commands/btest/examples.md +35 -10
  11. package/commands/check/README.md +371 -53
  12. package/commands/check/examples.md +390 -0
  13. package/commands/devices/README.md +177 -37
  14. package/commands/devices/examples.md +148 -0
  15. package/commands/execute/README.md +48 -7
  16. package/commands/execute/examples.md +90 -6
  17. package/commands/retrieve/README.md +37 -51
  18. package/commands/retrieve/examples.md +40 -9
  19. package/commands/settings/README.md +286 -22
  20. package/commands/settings/examples.md +408 -0
  21. package/commands/terminal/README.md +6 -0
  22. package/commands/transfer/README.md +32 -2
  23. package/commands/transfer/examples.md +66 -0
  24. package/docs/CONSTITUTION.md +40 -15
  25. package/docs/MATRIX.md +69 -142
  26. package/docs/errors/README.md +2 -1
  27. package/docs/errors/cdb/empty-selection.md +19 -0
  28. package/docs/errors/input/incomplete-gps.md +9 -0
  29. package/docs/errors/input/invalid-altitude.md +9 -0
  30. package/docs/errors/input/invalid-bbox.md +9 -0
  31. package/docs/errors/input/invalid-coordinate.md +9 -0
  32. package/docs/errors/input/invalid-radius.md +9 -0
  33. package/docs/errors/internal/settings-failed.md +9 -0
  34. package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
  35. package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
  36. package/docs/errors/settings/consequential-value.md +26 -0
  37. package/docs/errors/settings/invalid-boolean.md +21 -3
  38. package/docs/errors/settings/invalid-format.md +18 -3
  39. package/docs/errors/settings/invalid-integer.md +18 -3
  40. package/docs/errors/settings/invalid-timeout.md +20 -3
  41. package/docs/errors/settings/invalid-via.md +22 -3
  42. package/docs/errors/settings/reserved-key.md +29 -0
  43. package/docs/errors/settings/skip-env-file-active.md +25 -0
  44. package/docs/errors/settings/unknown-key.md +25 -0
  45. package/docs/errors/usage/fanout-not-supported.md +16 -0
  46. package/docs/errors/usage/invalid-method.md +23 -0
  47. package/package.json +9 -9
  48. package/src/api-fanout.ts +454 -0
  49. package/src/api.ts +1627 -0
  50. package/src/btest.ts +61 -1
  51. package/src/cli/api.ts +660 -0
  52. package/src/cli/btest.ts +20 -2
  53. package/src/cli/common.ts +75 -0
  54. package/src/cli/devices.ts +235 -6
  55. package/src/cli/discover.ts +3 -1
  56. package/src/cli/execute.ts +107 -6
  57. package/src/cli/mcp.ts +8 -1
  58. package/src/cli/retrieve.ts +135 -90
  59. package/src/cli/selection.ts +200 -0
  60. package/src/cli/settings.ts +286 -0
  61. package/src/cli/terminal.ts +22 -7
  62. package/src/cli/transfer.ts +171 -47
  63. package/src/cli.ts +29 -0
  64. package/src/core/error-catalog.ts +69 -0
  65. package/src/core/fanout.ts +301 -0
  66. package/src/core/inspect.ts +170 -0
  67. package/src/devices.ts +212 -20
  68. package/src/discover.ts +7 -2
  69. package/src/execute-fanout.ts +438 -0
  70. package/src/execute.ts +134 -70
  71. package/src/index.ts +166 -9
  72. package/src/mcp/config.ts +12 -2
  73. package/src/mcp/tools.ts +137 -2
  74. package/src/protocols/adapter.ts +357 -9
  75. package/src/protocols/btest-session.ts +279 -46
  76. package/src/protocols/btest.ts +6 -2
  77. package/src/protocols/index.ts +11 -5
  78. package/src/protocols/native-api.ts +223 -30
  79. package/src/resolver/cdb.ts +10 -2
  80. package/src/resolver/comment-kv.ts +58 -4
  81. package/src/resolver/config-file.ts +136 -0
  82. package/src/resolver/facts.ts +78 -0
  83. package/src/resolver/geo.ts +453 -0
  84. package/src/resolver/index.ts +46 -0
  85. package/src/resolver/mac.ts +12 -3
  86. package/src/resolver/selection.ts +304 -0
  87. package/src/resolver/settings.ts +54 -5
  88. package/src/resolver/target.ts +20 -2
  89. package/src/retrieve-fanout.ts +232 -268
  90. package/src/retrieve.ts +69 -94
  91. package/src/settings.ts +1078 -0
  92. package/src/terminal.ts +11 -1
  93. package/src/transfer-fanout.ts +527 -0
  94. package/src/transfer.ts +61 -11
  95. package/commands/stream/README.md +0 -70
@@ -6,16 +6,20 @@
6
6
  * `../cli.ts`. Mirrors the shape of `./retrieve.ts`.
7
7
  */
8
8
 
9
+ import { fanoutExitCode } from "../core/fanout.ts";
9
10
  import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
10
11
  import {
11
12
  buildTransferErrorEnvelope,
13
+ buildTransferFanoutErrorEnvelope,
12
14
  CentrsError,
13
15
  describeCentrs,
14
16
  renderTransferEnvelope,
17
+ renderTransferFanoutEnvelope,
15
18
  type TransferOutputFormat,
16
19
  type TransferRequest,
17
20
  type TransferVerb,
18
21
  transfer,
22
+ transferFanout,
19
23
  transferOutputFormats,
20
24
  transferVerbs,
21
25
  } from "../index.ts";
@@ -23,6 +27,7 @@ import {
23
27
  type CliCommandMetadata,
24
28
  expectValue,
25
29
  renderCommandHelp,
30
+ unknownFlagError,
26
31
  } from "./common.ts";
27
32
  import {
28
33
  buildTargetSelectionTips,
@@ -32,6 +37,14 @@ import {
32
37
  missingTargetError,
33
38
  withTips,
34
39
  } from "./missing-target.ts";
40
+ import {
41
+ buildTargetSelection,
42
+ consumeSelectionFlag,
43
+ emptySelectionFlags,
44
+ isFanoutMode,
45
+ type SelectionFlags,
46
+ selectionCommandOptions,
47
+ } from "./selection.ts";
35
48
 
36
49
  /** Sub-verb aliases, resolved to canonical verbs silently (commands/AGENTS.md). */
37
50
  const VERB_ALIASES: Record<string, TransferVerb> = {
@@ -73,6 +86,18 @@ export const transferCommand: CliCommandMetadata = {
73
86
  valueName: "(--overwrite)",
74
87
  description: "Replace an existing destination. Default refuses it.",
75
88
  },
89
+ ...selectionCommandOptions,
90
+ {
91
+ flag: "--out-dir",
92
+ valueName: "<dir>",
93
+ description:
94
+ "`download` fan-out only: write one file per target into <dir>, named by CDB identity. Required when downloading across a selection.",
95
+ },
96
+ {
97
+ flag: "--yes",
98
+ description:
99
+ "Confirm a mutating fan-out (upload/remove/mkdir/copy) across multiple routers in non-interactive runs.",
100
+ },
76
101
  {
77
102
  flag: "--verify",
78
103
  valueName: "<size|checksum|off>",
@@ -159,6 +184,17 @@ export async function runTransferCli(
159
184
  console.log(renderCommandHelp(describeCentrs(), transferCommand));
160
185
  return 0;
161
186
  }
187
+
188
+ // Fan-out mode (a selector flag, or >1 positional target before the verb)
189
+ // has its own envelope shape, single up-front write-confirm, and exit code.
190
+ if (isFanoutMode(parsed.selectionFlags, parsed.targetPositionals.length)) {
191
+ return await runTransferFanoutCli(
192
+ parsed.request,
193
+ parsed.selectionFlags,
194
+ parsed.targetPositionals,
195
+ args,
196
+ );
197
+ }
162
198
  request = parsed.request;
163
199
 
164
200
  const envelope = await transfer(request);
@@ -196,12 +232,14 @@ export async function runTransferCli(
196
232
  tips,
197
233
  );
198
234
  console.error(
235
+ // codeql[js/clear-text-logging] The transfer error path redacts password/cdbPassword before any renderer (redactTransferRequest), and buildTransferErrorEnvelope only emits targetInput.
199
236
  renderTransferEnvelope(envelope, format, {
200
237
  verbose: safeRequest?.verbose ?? false,
201
238
  }),
202
239
  );
203
240
  } else {
204
241
  console.error(
242
+ // codeql[js/clear-text-logging] Credentials are redacted from the request before rendering (redactTransferRequest) and never reach the error/stderr; same known false positive dismissed across all CLI commands.
205
243
  formatCentrsErrorText(
206
244
  asCentrsError(error, {
207
245
  code: "input/invalid-command",
@@ -217,6 +255,46 @@ export async function runTransferCli(
217
255
  }
218
256
  }
219
257
 
258
+ async function runTransferFanoutCli(
259
+ request: TransferRequest,
260
+ selectionFlags: SelectionFlags,
261
+ targetPositionals: readonly string[],
262
+ args: readonly string[],
263
+ ): Promise<number> {
264
+ const selection = buildTargetSelection(selectionFlags, targetPositionals);
265
+ const format = inferRequestedFormat(args, request);
266
+ try {
267
+ const envelope = await transferFanout(
268
+ { ...request, stdinIsTty: process.stdin.isTTY },
269
+ selection,
270
+ Bun.env,
271
+ {},
272
+ { concurrency: selectionFlags.concurrency, allowAdhoc: true },
273
+ );
274
+ console.log(
275
+ renderTransferFanoutEnvelope(envelope, format, {
276
+ verbose: request.verbose ?? false,
277
+ }),
278
+ );
279
+ // Granular exit contract: 0 all-ok / 2 partial / 1 all-failed.
280
+ return fanoutExitCode(envelope);
281
+ } catch (error) {
282
+ // Strip credentials before any renderer touches the request (stderr/CI logs):
283
+ // pass an explicitly redacted request, with no raw-request fallback path.
284
+ const envelope = buildTransferFanoutErrorEnvelope(
285
+ { ...request, password: undefined, cdbPassword: undefined },
286
+ error,
287
+ );
288
+ console.error(
289
+ // codeql[js/clear-text-logging] No secret logged: request is redactTransferRequest'd (password/cdbPassword stripped) before the builder; fan-out error meta is {target:{},via,settings:{}}.
290
+ renderTransferFanoutEnvelope(envelope, format, {
291
+ verbose: request.verbose ?? false,
292
+ }),
293
+ );
294
+ return 1;
295
+ }
296
+ }
297
+
220
298
  /**
221
299
  * Strip credential fields before a failed request is handed to the error
222
300
  * envelope/text renderers. Those outputs land on stderr (and CI logs), so the
@@ -237,13 +315,16 @@ function redactTransferRequest(
237
315
  interface ParsedTransfer {
238
316
  help?: boolean;
239
317
  request: TransferRequest;
318
+ selectionFlags: SelectionFlags;
319
+ targetPositionals: readonly string[];
240
320
  }
241
321
 
242
- function parseTransferCliArgs(
322
+ export function parseTransferCliArgs(
243
323
  args: readonly string[],
244
324
  fixedVerb?: TransferVerb,
245
325
  ): ParsedTransfer {
246
326
  const flags: Partial<TransferRequest> & { verbose?: boolean } = {};
327
+ const selectionFlags = emptySelectionFlags();
247
328
  const positional: string[] = [];
248
329
  let help = false;
249
330
  // `--verify <mode>` and `--no-verify` both map to the single `verify` field,
@@ -326,28 +407,39 @@ function parseTransferCliArgs(
326
407
  case "--no-validate":
327
408
  flags.validate = false;
328
409
  break;
410
+ case "--out-dir":
411
+ flags.outDir = expectValue(args, ++index, arg);
412
+ break;
413
+ case "--yes":
414
+ flags.yes = true;
415
+ break;
329
416
  case "--verbose":
330
417
  flags.verbose = true;
331
418
  break;
332
- default:
419
+ default: {
420
+ const consumed = consumeSelectionFlag(args, index, selectionFlags);
421
+ if (consumed !== null) {
422
+ index = consumed;
423
+ break;
424
+ }
333
425
  // A lone `-` is the stdin/stdout positional (upload from / download
334
426
  // to a pipe), not a flag.
335
427
  if (arg !== "-" && arg.startsWith("-")) {
336
- throw new CentrsError({
337
- code: "input/invalid-command",
338
- summary: `Unknown transfer flag: ${arg}`,
339
- remediation:
340
- "Remove the flag or run `centrs transfer --help` for the supported options.",
341
- context: { flag: arg },
342
- });
428
+ throw unknownFlagError("transfer", arg, transferCommand.options);
343
429
  }
344
430
  positional.push(arg);
345
431
  break;
432
+ }
346
433
  }
347
434
  }
348
435
 
349
436
  if (help) {
350
- return { help: true, request: { verb: fixedVerb ?? "list" } };
437
+ return {
438
+ help: true,
439
+ request: { verb: fixedVerb ?? "list" },
440
+ selectionFlags,
441
+ targetPositionals: [],
442
+ };
351
443
  }
352
444
 
353
445
  if (sawNoVerify && explicitVerify !== undefined && explicitVerify !== "off") {
@@ -359,67 +451,101 @@ function parseTransferCliArgs(
359
451
  });
360
452
  }
361
453
 
362
- const request = assemblePositionals(positional, flags, fixedVerb);
363
- return { request };
454
+ const { request, targetPositionals } = assemblePositionals(
455
+ positional,
456
+ flags,
457
+ selectionFlags,
458
+ fixedVerb,
459
+ );
460
+ return { request, selectionFlags, targetPositionals };
461
+ }
462
+
463
+ /** Whether a positional token is a transfer verb keyword (canonical or alias). */
464
+ function isTransferVerbToken(token: string): boolean {
465
+ return transferVerbs.includes(token as TransferVerb) || token in VERB_ALIASES;
364
466
  }
365
467
 
366
468
  /**
367
- * Map positionals to the per-verb request shape. For the top-level alias the
368
- * verb is fixed and the first positional is `<router>`; for `centrs transfer`
369
- * the positionals are `<router> <verb> …`.
469
+ * Map positionals to the per-verb request shape and split fan-out targets. The
470
+ * boundary is the **verb keyword**: positionals before it are targets, the verb +
471
+ * paths follow. The top-level `upload`/`download` alias fixes the verb, so its
472
+ * positionals are paths; with a selector present, positional fan-out targets are
473
+ * not expressible there (fan-out comes from the selector). See
474
+ * `docs/CONSTITUTION.md` (Target selection).
370
475
  */
371
476
  function assemblePositionals(
372
477
  positional: readonly string[],
373
478
  flags: Partial<TransferRequest> & { verbose?: boolean },
479
+ selectionFlags: SelectionFlags,
374
480
  fixedVerb?: TransferVerb,
375
- ): TransferRequest {
376
- let targetInput: string | undefined;
481
+ ): { request: TransferRequest; targetPositionals: readonly string[] } {
482
+ const hasSelector = isFanoutMode(selectionFlags, 0);
377
483
  let verb: TransferVerb;
484
+ let targetPositionals: readonly string[];
378
485
  let rest: readonly string[];
379
486
 
380
487
  if (fixedVerb) {
381
- targetInput = positional[0];
382
488
  verb = fixedVerb;
383
- rest = positional.slice(1);
384
- } else {
385
- targetInput = positional[0];
386
- const rawVerb = positional[1];
387
- // A missing target gets the tagged missing-target error (CDB picker tip);
388
- // a present target with no verb is a distinct "which verb?" usage error.
389
- if (!targetInput) {
390
- throw missingTargetError({
391
- command: "transfer",
392
- summary:
393
- "`centrs transfer` requires a <router> target and a verb (upload|download|list|remove|mkdir|copy).",
394
- remediation:
395
- "Pass the target then a verb, e.g. `centrs transfer <router> list`; run `--help` for the full shape.",
396
- });
489
+ if (hasSelector) {
490
+ // alias + selector: targets come from the selector; positionals are paths.
491
+ targetPositionals = [];
492
+ rest = positional;
493
+ } else {
494
+ targetPositionals = positional[0] !== undefined ? [positional[0]] : [];
495
+ rest = positional.slice(1);
397
496
  }
398
- if (!rawVerb) {
497
+ } else {
498
+ const verbIndex = positional.findIndex(isTransferVerbToken);
499
+ if (verbIndex === -1) {
500
+ if (positional.length === 0 && !hasSelector) {
501
+ throw missingTargetError({
502
+ command: "transfer",
503
+ summary:
504
+ "`centrs transfer` requires a <router> target and a verb (upload|download|list|remove|mkdir|copy).",
505
+ remediation:
506
+ "Pass the target then a verb, e.g. `centrs transfer <router> list`; run `--help` for the full shape.",
507
+ });
508
+ }
399
509
  throw new CentrsError({
400
510
  code: "input/invalid-command",
401
511
  summary:
402
- "`centrs transfer` requires a verb after the <router> (upload|download|list|remove|mkdir|copy).",
512
+ "`centrs transfer` requires a verb (upload|download|list|remove|mkdir|copy).",
403
513
  remediation:
404
514
  "Pass a verb, e.g. `centrs transfer <router> list`; run `--help` for the full shape.",
405
- context: { targetInput, verb: rawVerb },
515
+ context: { positionals: positional },
406
516
  });
407
517
  }
408
- verb = canonicalVerb(rawVerb);
409
- rest = positional.slice(2);
518
+ targetPositionals = positional.slice(0, verbIndex);
519
+ verb = canonicalVerb(positional[verbIndex] as string);
520
+ rest = positional.slice(verbIndex + 1);
410
521
  }
411
522
 
412
- if (!targetInput) {
413
- throw missingTargetError({
414
- command: "transfer",
415
- summary: "`centrs transfer` requires a <router> target.",
416
- remediation:
417
- "Pass the router host/identity as the first argument; run `centrs transfer --help` for the command shape.",
418
- });
523
+ const request: TransferRequest = { verb, ...flags };
524
+ applyVerbPaths(request, verb, rest);
525
+
526
+ // Single-target (not fan-out mode) needs exactly one resolved target.
527
+ if (!isFanoutMode(selectionFlags, targetPositionals.length)) {
528
+ const targetInput = targetPositionals[0];
529
+ if (!targetInput) {
530
+ throw missingTargetError({
531
+ command: "transfer",
532
+ summary: "`centrs transfer` requires a <router> target.",
533
+ remediation:
534
+ "Pass the router host/identity as the first argument; run `centrs transfer --help` for the command shape.",
535
+ });
536
+ }
537
+ request.targetInput = targetInput;
419
538
  }
420
539
 
421
- const request: TransferRequest = { verb, targetInput, ...flags };
540
+ return { request, targetPositionals };
541
+ }
422
542
 
543
+ /** Map the post-verb positionals onto the per-verb request fields. */
544
+ function applyVerbPaths(
545
+ request: TransferRequest,
546
+ verb: TransferVerb,
547
+ rest: readonly string[],
548
+ ): void {
423
549
  switch (verb) {
424
550
  case "upload":
425
551
  request.local = rest[0];
@@ -441,8 +567,6 @@ function assemblePositionals(
441
567
  request.remoteDest = rest[1];
442
568
  break;
443
569
  }
444
-
445
- return request;
446
570
  }
447
571
 
448
572
  function canonicalVerb(raw: string): TransferVerb {
package/src/cli.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { apiCommand, runApiCli } from "./cli/api.ts";
3
4
  import { btestCommand, runBtestCli } from "./cli/btest.ts";
4
5
  import { devicesCommand, runDevicesCli } from "./cli/devices.ts";
5
6
  import { discoverCommand, runDiscoverCli } from "./cli/discover.ts";
6
7
  import { executeCommand, runExecuteCli } from "./cli/execute.ts";
7
8
  import { mcpCommand, runMcpCli } from "./cli/mcp.ts";
8
9
  import { retrieveCommand, runRetrieveCli } from "./cli/retrieve.ts";
10
+ import { runSettingsCli, settingsCommand } from "./cli/settings.ts";
9
11
  import { runTerminalCli, terminalCommand } from "./cli/terminal.ts";
10
12
  import { runTransferCli, transferCommand } from "./cli/transfer.ts";
11
13
  import { asCentrsError, formatCentrsErrorText } from "./errors.ts";
@@ -14,12 +16,14 @@ import { describeCentrs, plannedProtocols, plannedSurfaces } from "./index.ts";
14
16
  const commandSummaries: ReadonlyArray<{ name: string; summary: string }> = [
15
17
  retrieveCommand,
16
18
  executeCommand,
19
+ apiCommand,
17
20
  transferCommand,
18
21
  terminalCommand,
19
22
  devicesCommand,
20
23
  discoverCommand,
21
24
  btestCommand,
22
25
  mcpCommand,
26
+ settingsCommand,
23
27
  ].map((command) => ({ name: command.name, summary: command.summary }));
24
28
 
25
29
  export function renderCliHelp(): string {
@@ -54,6 +58,9 @@ export async function runCli(
54
58
  if (command === "execute") {
55
59
  return await runExecuteCli(rest);
56
60
  }
61
+ if (command === "api") {
62
+ return await runApiCli(rest);
63
+ }
57
64
  if (command === "transfer") {
58
65
  return await runTransferCli(rest);
59
66
  }
@@ -80,8 +87,29 @@ export async function runCli(
80
87
  if (command === "mcp") {
81
88
  return await runMcpCli(rest);
82
89
  }
90
+ if (command === "settings") {
91
+ return await runSettingsCli(rest);
92
+ }
93
+ // `stream` (and the `tail` shorthand) folded into `api --stream`.
94
+ if (command === "stream" || command === "tail") {
95
+ console.error(
96
+ formatCentrsErrorText(
97
+ asCentrsError(
98
+ new Error(`\`centrs ${command}\` has been folded into \`api\`.`),
99
+ {
100
+ code: "input/invalid-command",
101
+ summary: `\`centrs ${command}\` is no longer a command — open-ended follow is now \`api … --stream\`.`,
102
+ remediation:
103
+ "Use `centrs api <router> <endpoint> --stream` (native-api only), e.g. `centrs api edge1 ip/address --stream`.",
104
+ },
105
+ ),
106
+ ),
107
+ );
108
+ return 1;
109
+ }
83
110
 
84
111
  console.error(
112
+ // codeql[js/clear-text-logging] password reaches here only as SettingSource provenance ({kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
85
113
  formatCentrsErrorText(
86
114
  asCentrsError(new Error(`Unknown centrs command: ${command}`), {
87
115
  code: "input/invalid-command",
@@ -96,6 +124,7 @@ export async function runCli(
96
124
  // to a structured envelope, so reaching here means an unconverted throw
97
125
  // escaped. Never let a raw stack trace reach the user.
98
126
  console.error(
127
+ // codeql[js/clear-text-logging] password reaches here only as SettingSource provenance ({kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
99
128
  formatCentrsErrorText(
100
129
  asCentrsError(error, {
101
130
  code: "internal/unhandled",
@@ -53,6 +53,10 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
53
53
  code: "cdb/empty-group",
54
54
  summary: "The named CDB group contains no records.",
55
55
  },
56
+ {
57
+ code: "cdb/empty-selection",
58
+ summary: "The target selection matched no CDB records.",
59
+ },
56
60
  {
57
61
  code: "cdb/invalid-option",
58
62
  summary: "A comment kv-soup option carried an invalid value.",
@@ -129,10 +133,27 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
129
133
  },
130
134
 
131
135
  // input/*
136
+ {
137
+ code: "input/incomplete-gps",
138
+ summary: "lat/lon (or a --gps tuple) were not given as a complete pair.",
139
+ },
140
+ {
141
+ code: "input/invalid-altitude",
142
+ summary: "The altitude is not a number, or altitude-type is not MSL/AGL.",
143
+ },
144
+ {
145
+ code: "input/invalid-bbox",
146
+ summary:
147
+ "The --bbox is not four lat-first corners with south<=north and west<=east.",
148
+ },
132
149
  {
133
150
  code: "input/invalid-command",
134
151
  summary: "The request shape was invalid (bad or missing arguments).",
135
152
  },
153
+ {
154
+ code: "input/invalid-coordinate",
155
+ summary: "The latitude or longitude is not a number in its valid range.",
156
+ },
136
157
  {
137
158
  code: "input/invalid-match",
138
159
  summary:
@@ -143,6 +164,11 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
143
164
  summary:
144
165
  "A file path contains a character the transport cannot quote (e.g. a quote or newline).",
145
166
  },
167
+ {
168
+ code: "input/invalid-radius",
169
+ summary:
170
+ "The --near radius is not a number with an optional m/km/mi/ft suffix.",
171
+ },
146
172
  {
147
173
  code: "input/invalid-routeros-path",
148
174
  summary: "The RouterOS path is malformed (it must be slash-prefixed).",
@@ -181,6 +207,10 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
181
207
  code: "internal/native-api-length",
182
208
  summary: "A native-API word length could not be encoded.",
183
209
  },
210
+ {
211
+ code: "internal/settings-failed",
212
+ summary: "The settings command failed with an unexpected internal error.",
213
+ },
184
214
  {
185
215
  code: "internal/unhandled",
186
216
  summary: "An unexpected internal error occurred (this is a centrs bug).",
@@ -218,6 +248,11 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
218
248
  summary:
219
249
  "The RouterOS native API returned a !trap with no more specific mapping.",
220
250
  },
251
+ {
252
+ code: "routeros/btest-connection-count-single-stream",
253
+ summary:
254
+ "Fewer TCP data connections were opened than --connection-count requested (e.g. authenticated multi-connection is not yet supported), so throughput will not scale to the full count.",
255
+ },
221
256
  {
222
257
  code: "routeros/btest-protocol",
223
258
  summary: "A bandwidth-test (btest) packet could not be decoded.",
@@ -227,6 +262,11 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
227
262
  summary:
228
263
  "The btest server refused a client because max-sessions is reached.",
229
264
  },
265
+ {
266
+ code: "routeros/btest-udp-tx-size-ignored",
267
+ summary:
268
+ "For UDP --direction both, the btest wire protocol carries a single tx-size; --remote-udp-tx-size is ignored and --local-udp-tx-size is used for both directions.",
269
+ },
230
270
  {
231
271
  code: "routeros/command-failed",
232
272
  summary: "RouterOS reported a command failure.",
@@ -285,6 +325,11 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
285
325
  },
286
326
 
287
327
  // settings/*
328
+ {
329
+ code: "settings/consequential-value",
330
+ summary:
331
+ "`settings set` accepted a syntactically valid value with a real, non-obvious consequence (e.g. a global --insecure default, or a transfer-via value gated elsewhere).",
332
+ },
288
333
  {
289
334
  code: "settings/invalid-boolean",
290
335
  summary: "A boolean-like setting received a non-boolean value.",
@@ -305,6 +350,21 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
305
350
  code: "settings/invalid-via",
306
351
  summary: "An unsupported protocol identifier was supplied to --via.",
307
352
  },
353
+ {
354
+ code: "settings/reserved-key",
355
+ summary:
356
+ "`settings set`/`reset` refused a credential-shaped or self-referential CENTRS_* key.",
357
+ },
358
+ {
359
+ code: "settings/skip-env-file-active",
360
+ summary:
361
+ "`settings print` ran under --skip-env-file; the shown centrs.env values would not apply to other commands run in the same environment.",
362
+ },
363
+ {
364
+ code: "settings/unknown-key",
365
+ summary:
366
+ "`settings get`/`set` received a token that is not a recognized CENTRS_*-shaped setting.",
367
+ },
308
368
  {
309
369
  code: "settings/unsafe-protocol-blocked",
310
370
  summary:
@@ -415,6 +475,15 @@ export const errorCatalog: readonly ErrorCatalogEntry[] = [
415
475
  code: "usage/invalid-concurrency",
416
476
  summary: "The --concurrency value must be an integer >= 1.",
417
477
  },
478
+ {
479
+ code: "usage/fanout-not-supported",
480
+ summary:
481
+ "The command or mode cannot fan out across multiple targets (e.g. terminal, --listen/--stream).",
482
+ },
483
+ {
484
+ code: "usage/invalid-method",
485
+ summary: "The -X/--method value is not a supported HTTP method.",
486
+ },
418
487
  {
419
488
  code: "usage/missing-group",
420
489
  summary: "The operation requires a non-empty --group value.",