@tikoci/centrs 0.1.7 → 0.1.8

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 (82) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +2 -2
  4. package/SECURITY.md +1 -1
  5. package/commands/AGENTS.md +2 -1
  6. package/commands/api/AGENTS.md +55 -30
  7. package/commands/api/README.md +123 -41
  8. package/commands/api/examples.md +205 -17
  9. package/commands/devices/README.md +11 -3
  10. package/commands/execute/README.md +31 -3
  11. package/commands/execute/examples.md +136 -1
  12. package/commands/explain/README.md +30 -4
  13. package/commands/mcp/README.md +5 -1
  14. package/commands/mcp/examples.md +18 -1
  15. package/commands/retrieve/README.md +348 -4
  16. package/commands/retrieve/examples.md +422 -5
  17. package/commands/terminal/README.md +14 -4
  18. package/commands/terminal/examples.md +22 -3
  19. package/commands/transfer/README.md +55 -9
  20. package/commands/transfer/examples.md +106 -7
  21. package/docs/CLI.md +28 -20
  22. package/docs/CONSTITUTION.md +30 -10
  23. package/docs/MATRIX.md +38 -1
  24. package/docs/errors/README.md +3 -1
  25. package/docs/errors/input/invalid-query.md +36 -0
  26. package/docs/errors/input/local-destination.md +20 -0
  27. package/docs/errors/input/unsupported-query.md +19 -0
  28. package/docs/errors/internal/byte-string.md +13 -0
  29. package/docs/errors/settings/invalid-timeout.md +6 -0
  30. package/docs/errors/transport/stream-overflow.md +21 -0
  31. package/docs/errors/usage/removed-flag.md +20 -0
  32. package/docs/errors/validation/menu-unavailable.md +22 -0
  33. package/docs/errors/validation/not-followable.md +23 -0
  34. package/docs/errors/validation/package-missing.md +29 -0
  35. package/docs/errors/validation/unknown-path.md +16 -4
  36. package/docs/errors/wait/deadline-exceeded.md +14 -0
  37. package/docs/errors/wait/interrupted.md +10 -0
  38. package/package.json +14 -11
  39. package/src/absent-menu.ts +280 -0
  40. package/src/api-fanout.ts +22 -12
  41. package/src/api.ts +511 -186
  42. package/src/btest.ts +19 -17
  43. package/src/cli/api.ts +126 -34
  44. package/src/cli/execute.ts +18 -5
  45. package/src/cli/retrieve.ts +223 -22
  46. package/src/cli/terminal.ts +10 -5
  47. package/src/cli/transfer.ts +23 -8
  48. package/src/core/envelope.ts +14 -0
  49. package/src/core/error-catalog.ts +56 -1
  50. package/src/core/inspect.ts +43 -5
  51. package/src/core/query.ts +508 -0
  52. package/src/core/routeros-errors.ts +1 -1
  53. package/src/devices.ts +6 -9
  54. package/src/discover.ts +7 -8
  55. package/src/errors.ts +116 -7
  56. package/src/execute-fanout.ts +13 -8
  57. package/src/execute.ts +106 -65
  58. package/src/explain/catalog.ts +58 -10
  59. package/src/explain/menus.ts +23 -2
  60. package/src/explain.ts +6 -6
  61. package/src/index.ts +29 -1
  62. package/src/mcp/safety.ts +43 -18
  63. package/src/mcp/tools.ts +14 -17
  64. package/src/protocols/adapter.ts +224 -118
  65. package/src/protocols/btest-session.ts +3 -2
  66. package/src/protocols/endpoint.ts +10 -0
  67. package/src/protocols/mac-telnet-console.ts +6 -3
  68. package/src/protocols/native-api.ts +223 -70
  69. package/src/protocols/sftp.ts +44 -14
  70. package/src/protocols/ssh.ts +42 -14
  71. package/src/resolver/cdb.ts +28 -7
  72. package/src/resolver/index.ts +1 -0
  73. package/src/resolver/target.ts +53 -7
  74. package/src/retrieve-fanout.ts +51 -12
  75. package/src/retrieve-follow.ts +862 -0
  76. package/src/retrieve-sample.ts +244 -0
  77. package/src/retrieve-stream.ts +115 -0
  78. package/src/retrieve-wait.ts +286 -0
  79. package/src/retrieve.ts +732 -108
  80. package/src/settings.ts +15 -15
  81. package/src/transfer-fanout.ts +40 -15
  82. package/src/transfer.ts +234 -32
package/src/errors.ts CHANGED
@@ -5,7 +5,16 @@
5
5
  */
6
6
  export const ERROR_DETAILS_BASE_URL = "https://tikoci.github.io/centrs/errors/";
7
7
 
8
+ /**
9
+ * Remediation for a device-stage `:parse` rejection. It does not claim the
10
+ * syntax is wrong: RouterOS rejects an absent menu (unknown path, package not
11
+ * installed) in the same words, and #361 tracks telling those apart.
12
+ */
13
+ export const SYNTAX_REJECTED_REMEDIATION =
14
+ "RouterOS refused to parse this command; its own words are in `Device said`. Check the syntax with `centrs explain '<command>'`. A menu that does not exist on this device (an unknown path, or a package that is not installed) is rejected the same way.";
15
+
8
16
  export type CentrsErrorCode =
17
+ | `wait/${string}`
9
18
  | `auth/${string}`
10
19
  | `cdb/${string}`
11
20
  | `discover/${string}`
@@ -120,34 +129,134 @@ export function asCentrsError(
120
129
  });
121
130
  }
122
131
 
132
+ /**
133
+ * The one text renderer for an error, shared by every command (GH#362).
134
+ *
135
+ * Order: header, where (`At:`), the device's own words (`Device said:`), `Fix:`,
136
+ * `Details:`. Where and the device's words print without `--verbose`: they are
137
+ * what lets a caller recover from a remediation that names the wrong cause.
138
+ * `--verbose` adds the full `context` object. A command that has more to say
139
+ * (warnings, tips) appends to this output; it never re-derives the header.
140
+ */
123
141
  export function formatCentrsErrorText(
124
142
  error: CentrsError | SerializedCentrsError,
125
143
  options: { verbose?: boolean } = {},
126
144
  ): string {
127
145
  const serialized = serializeCentrsError(error);
128
- const lines = [`[${serialized.code}] ${serialized.summary}`];
146
+ const lines = errorFields(serialized);
147
+ if (options.verbose && serialized.context) {
148
+ lines.push("");
149
+ lines.push(terminalSafe(JSON.stringify(serialized.context, null, 2)));
150
+ }
151
+ return lines.join("\n");
152
+ }
153
+
154
+ /**
155
+ * The same fields as `formatCentrsErrorText`, on one line joined by ` — `, for
156
+ * output that must stay line-per-record (`api --stream --format text`).
157
+ * `--verbose` adds nothing here: a context block cannot be one line.
158
+ */
159
+ export function formatCentrsErrorLine(
160
+ error: CentrsError | SerializedCentrsError,
161
+ ): string {
162
+ return errorFields(serializeCentrsError(error)).map(oneLine).join(" — ");
163
+ }
164
+
165
+ function errorFields(serialized: SerializedCentrsError): string[] {
166
+ const lines = [`[${serialized.code}] ${terminalSafe(serialized.summary)}`];
129
167
 
130
168
  if (serialized.position) {
131
169
  // RouterOS's authoritative byte column (1-based); see RouterOsErrorPosition.
132
170
  lines.push(
133
171
  `At: line ${serialized.position.line}, column ${serialized.position.column} (RouterOS byte offset)`,
134
172
  );
173
+ } else {
174
+ const offline = offlineSpanText(serialized.context);
175
+ if (offline) lines.push(offline);
176
+ }
177
+
178
+ const deviceSaid = deviceSaidText(serialized);
179
+ if (deviceSaid) {
180
+ lines.push(`Device said: ${terminalSafe(deviceSaid)}`);
135
181
  }
136
182
 
137
183
  if (serialized.remediation) {
138
- lines.push(`Fix: ${serialized.remediation}`);
184
+ lines.push(`Fix: ${terminalSafe(serialized.remediation)}`);
139
185
  }
140
186
 
141
187
  if (serialized.detailsUrl) {
142
188
  lines.push(`Details: ${serialized.detailsUrl}`);
143
189
  }
144
190
 
145
- if (options.verbose && serialized.context) {
146
- lines.push("");
147
- lines.push(JSON.stringify(serialized.context, null, 2));
148
- }
191
+ return lines;
192
+ }
149
193
 
150
- return lines.join("\n");
194
+ const DEVICE_SAID_MAX = 240;
195
+
196
+ /**
197
+ * Control characters other than tab and newline, C1 included. Summaries and
198
+ * `detail` carry router-supplied text; an ESC or CR in it would drive the
199
+ * terminal. `JSON.stringify` escapes C0 but not C1, so serialized excerpts need
200
+ * this too. `--json` keeps the raw value.
201
+ */
202
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching them is the point.
203
+ const TERMINAL_CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
204
+
205
+ function terminalSafe(text: string): string {
206
+ return text.replace(TERMINAL_CONTROL, " ");
207
+ }
208
+
209
+ /** Compact records also replace tab/newline so external text cannot add rows. */
210
+ export function terminalSafeLine(text: string): string {
211
+ return terminalSafe(text).replace(/[\t\n]/g, " ");
212
+ }
213
+
214
+ function oneLine(text: string): string {
215
+ return text.replace(/\s+/g, " ").trim();
216
+ }
217
+
218
+ /**
219
+ * RouterOS's own verdict, from `context.detail`: the raw `:parse` return or the
220
+ * trap / REST message, as `mapRouterOsError` and `classifyParseResult` store it.
221
+ * Collapsed to one line and capped. Omitted when the summary already quotes it,
222
+ * so a trap message is not printed twice. `--json` keeps it unabridged.
223
+ */
224
+ function deviceSaidText(error: SerializedCentrsError): string | undefined {
225
+ const detail = error.context?.["detail"];
226
+ if (typeof detail !== "string") return undefined;
227
+ const said = oneLine(detail);
228
+ if (said.length === 0 || oneLine(error.summary).includes(said))
229
+ return undefined;
230
+ return said.length > DEVICE_SAID_MAX
231
+ ? `${said.slice(0, DEVICE_SAID_MAX)}…`
232
+ : said;
233
+ }
234
+
235
+ /**
236
+ * The offline gate's rejected byte range (`context.span`, 0-based, end-exclusive,
237
+ * into `context.command`). Labelled as offline analysis so it is never mistaken
238
+ * for a RouterOS-reported position (see `src/offline-gate.ts`).
239
+ */
240
+ function offlineSpanText(
241
+ context: Record<string, unknown> | undefined,
242
+ ): string | undefined {
243
+ const span = context?.["span"];
244
+ if (
245
+ !span ||
246
+ typeof span !== "object" ||
247
+ typeof (span as { start?: unknown }).start !== "number" ||
248
+ typeof (span as { end?: unknown }).end !== "number"
249
+ ) {
250
+ return undefined;
251
+ }
252
+ const { start, end } = span as { start: number; end: number };
253
+ const command = context?.["command"];
254
+ const excerpt =
255
+ typeof command === "string" && end > start
256
+ ? `: ${terminalSafe(JSON.stringify(new TextDecoder().decode(new TextEncoder().encode(command).subarray(start, end))))}`
257
+ : "";
258
+ // Half-open, exactly as `context.span` carries it.
259
+ return `At: bytes [${start}, ${end}) (offline analysis)${excerpt}`;
151
260
  }
152
261
 
153
262
  export function serializeUnknownError(error: unknown): unknown {
@@ -38,7 +38,12 @@ import {
38
38
  summarizeFanout,
39
39
  } from "./core/fanout.ts";
40
40
  import { toYaml } from "./core/yaml.ts";
41
- import { CentrsError, serializeCentrsError } from "./errors.ts";
41
+ import {
42
+ CentrsError,
43
+ formatCentrsErrorText,
44
+ serializeCentrsError,
45
+ terminalSafeLine,
46
+ } from "./errors.ts";
42
47
  import {
43
48
  buildExecuteErrorEnvelopeFromResolved,
44
49
  type ExecuteEnvelope,
@@ -428,11 +433,7 @@ export function renderExecuteFanoutEnvelope(
428
433
  return toYaml(envelope);
429
434
  }
430
435
  if (!envelope.ok) {
431
- const lines = [`[${envelope.error.code}] ${envelope.error.summary}`];
432
- if (envelope.error.remediation) {
433
- lines.push(`Fix: ${envelope.error.remediation}`);
434
- }
435
- return lines.join("\n");
436
+ return formatCentrsErrorText(envelope.error, options);
436
437
  }
437
438
  const operation = envelope.meta.operation;
438
439
  const { total, ok, failed } = envelope.data.summary;
@@ -453,10 +454,14 @@ export function renderExecuteFanoutEnvelope(
453
454
  );
454
455
  } else {
455
456
  lines.push(
456
- ` [${index}] FAIL ${label} [${target.error.code}] ${target.error.summary}`,
457
+ terminalSafeLine(
458
+ ` [${index}] FAIL ${label} [${target.error.code}] ${target.error.summary}`,
459
+ ),
457
460
  );
458
461
  if (options.verbose && target.error.remediation) {
459
- lines.push(` Fix: ${target.error.remediation}`);
462
+ lines.push(
463
+ terminalSafeLine(` Fix: ${target.error.remediation}`),
464
+ );
460
465
  }
461
466
  }
462
467
  }
package/src/execute.ts CHANGED
@@ -10,7 +10,7 @@ import type {
10
10
  ValidationTrace,
11
11
  Warning,
12
12
  } from "./core/envelope.ts";
13
- import { buildTip } from "./core/envelope.ts";
13
+ import { buildTip, notReachedReason } from "./core/envelope.ts";
14
14
  import {
15
15
  type CanonicalExecuteCommand,
16
16
  canonicalizeExecuteCommand,
@@ -20,7 +20,12 @@ import { inspectArgumentNames, pathTokens } from "./core/inspect.ts";
20
20
  import { mapRouterOsResultError } from "./core/routeros-errors.ts";
21
21
  import { routerOsStringLiteral } from "./core/routeros-string.ts";
22
22
  import { toYaml } from "./core/yaml.ts";
23
- import { CentrsError, serializeCentrsError } from "./errors.ts";
23
+ import {
24
+ CentrsError,
25
+ formatCentrsErrorText,
26
+ SYNTAX_REJECTED_REMEDIATION,
27
+ serializeCentrsError,
28
+ } from "./errors.ts";
24
29
  import {
25
30
  assertOfflineSyntax,
26
31
  isOfflineGateRejection,
@@ -623,18 +628,21 @@ function failedValidationMeta(
623
628
  return disabledValidationMeta();
624
629
  }
625
630
  const deviceSource = deviceStageSource(resolved);
626
- // Script-shaped input has no `/console/inspect` half to fail, on either
627
- // branch below. Reporting `semantic: false` for it would say a semantic gate
628
- // ran and said no, which is a different claim from "there was none".
629
- const semantic: boolean | "not-applicable" =
630
- resolved.canonical.mode === "structured" ? false : "not-applicable";
631
+ const structured = resolved.canonical.mode === "structured";
632
+ // A flat summary is present only when its check gave a verdict (GH#435).
633
+ // Script-shaped input has no semantic half at all: `not-applicable`. For
634
+ // structured input, a syntax rejection means the name-level check was
635
+ // never asked, so `semantic` is left out rather than reported `false`.
636
+ const semanticNotReached = structured
637
+ ? {}
638
+ : { semantic: "not-applicable" as const };
631
639
  if (isOfflineGateRejection(error)) {
632
640
  return {
633
641
  enabled: true,
634
642
  source: OFFLINE_GATE_SOURCE,
635
643
  result: "failed",
636
644
  syntax: false,
637
- semantic,
645
+ ...semanticNotReached,
638
646
  stages: [
639
647
  { stage: "offline", source: OFFLINE_GATE_SOURCE, result: "failed" },
640
648
  {
@@ -648,31 +656,60 @@ function failedValidationMeta(
648
656
  }
649
657
  // Only a `validation/*` error tells us the DEVICE stage rejected it — and
650
658
  // only then do we know stage 1 passed, because stage 1 runs first and would
651
- // have thrown its own (offline-tagged) error otherwise. This builder also
652
- // catches everything downstream — a RouterOS fault on the run, a transport
653
- // drop — which passed both stages and is served by `trace.validation`. When
654
- // neither applies, `stages` is omitted rather than asserting a verdict this
655
- // function cannot know.
656
- const deviceRejected =
657
- error instanceof CentrsError && error.code.startsWith("validation/");
659
+ // have thrown its own (offline-tagged) error otherwise. Everything downstream
660
+ // of a passed gate — a RouterOS fault on the run, a transport drop — is
661
+ // served by `trace.validation` before this function is reached.
662
+ if (error instanceof CentrsError && error.code.startsWith("validation/")) {
663
+ // Only a structured `unknown-attribute` (inspect, or `:parse`'s `bad
664
+ // parameter`) says the syntax was fine and a name was wrong. Every other
665
+ // device rejection — `syntax`, `unknown-path`, `package-missing`,
666
+ // `menu-unavailable` — is `:parse` refusing the command itself.
667
+ const nameRejected =
668
+ structured && error.code === "validation/unknown-attribute";
669
+ return {
670
+ enabled: true,
671
+ source: deviceSource,
672
+ result: "failed",
673
+ ...(nameRejected
674
+ ? { syntax: true, semantic: false }
675
+ : { syntax: false, ...semanticNotReached }),
676
+ stages: [
677
+ offlineStagePassed(trace?.offlineVerdict),
678
+ { stage: "device", source: deviceSource, result: "failed" },
679
+ ],
680
+ };
681
+ }
682
+ // Anything else stopped the gate before it produced a verdict: a transport or
683
+ // auth failure, a device error that is not a parse answer, or a refusal
684
+ // before the gate ran (write confirmation). No `result`, no `syntax` /
685
+ // `semantic` — no stage said either (GH#435). Without a trace this builder
686
+ // cannot know whether stage 1 ran, so it leaves that stage out.
687
+ const reason = notReachedReason(
688
+ error instanceof CentrsError ? error.code : "internal/unhandled",
689
+ );
690
+ const device: EnvelopeValidationStage = {
691
+ stage: "device",
692
+ source: deviceSource,
693
+ result: "skipped",
694
+ reason,
695
+ };
696
+ if (!trace) {
697
+ return { enabled: true, source: deviceSource, stages: [device] };
698
+ }
658
699
  return {
659
700
  enabled: true,
660
701
  source: deviceSource,
661
- result: "failed",
662
- syntax: true,
663
- semantic,
664
- ...(deviceRejected
665
- ? {
666
- stages: [
667
- offlineStagePassed(trace?.offlineVerdict),
668
- {
669
- stage: "device" as const,
670
- source: deviceSource,
671
- result: "failed" as const,
672
- },
673
- ],
674
- }
675
- : {}),
702
+ stages: [
703
+ trace.offlineVerdict
704
+ ? offlineStagePassed(trace.offlineVerdict)
705
+ : {
706
+ stage: "offline",
707
+ source: OFFLINE_GATE_SOURCE,
708
+ result: "skipped",
709
+ reason,
710
+ },
711
+ device,
712
+ ],
676
713
  };
677
714
  }
678
715
 
@@ -738,7 +775,7 @@ export function renderExecuteEnvelope(
738
775
  case "text":
739
776
  return envelope.ok
740
777
  ? renderExecuteSuccessText(envelope, options)
741
- : renderExecuteErrorText(envelope, options);
778
+ : formatCentrsErrorText(envelope.error, options);
742
779
  default:
743
780
  return exhaustiveOutputFormat(format);
744
781
  }
@@ -931,12 +968,16 @@ export async function validateRouterOsScript(
931
968
  code: "validation/syntax",
932
969
  summary:
933
970
  "RouterOS rejected the command syntax during `:parse` preflight.",
934
- remediation:
935
- "Fix the RouterOS CLI syntax, especially quotes and bracketed expressions, then retry.",
971
+ remediation: SYNTAX_REJECTED_REMEDIATION,
936
972
  context: {
937
973
  command,
938
974
  validationSource: ":put [:parse ...]",
939
975
  via,
976
+ // Keep RouterOS's own words from the mapped fault (GH#362).
977
+ ...(error instanceof CentrsError &&
978
+ typeof error.context?.["detail"] === "string"
979
+ ? { detail: error.context["detail"] }
980
+ : {}),
940
981
  },
941
982
  ...(position ? { position } : {}),
942
983
  cause: error,
@@ -1022,10 +1063,8 @@ function resolveExecuteProtocol(
1022
1063
  cdb?: CdbResolution,
1023
1064
  config: Record<string, string | undefined> = {},
1024
1065
  ): ResolvedSetting<RouterOsProtocol> {
1025
- const defaultVia = isUnresolvedMacTarget(request, env, cdb, config)
1026
- ? "mac-telnet"
1027
- : "native-api";
1028
- const via = resolveStringSetting(
1066
+ const defaultVia = defaultExecuteVia(request, env, cdb, config);
1067
+ const resolvedVia = resolveStringSetting(
1029
1068
  request.via,
1030
1069
  env,
1031
1070
  "CENTRS_VIA",
@@ -1035,12 +1074,20 @@ function resolveExecuteProtocol(
1035
1074
  cdb?.overrides.via,
1036
1075
  config,
1037
1076
  );
1077
+ // A URL-derived default names its reason, since native-api is the documented one.
1078
+ const via =
1079
+ resolvedVia?.source.kind === "default" && resolvedVia.value === "rest-api"
1080
+ ? {
1081
+ ...resolvedVia,
1082
+ source: { kind: "default" as const, key: "via:target-url" },
1083
+ }
1084
+ : resolvedVia;
1038
1085
  if (!via) {
1039
1086
  throw new CentrsError({
1040
1087
  code: "internal/unhandled",
1041
1088
  summary: "Failed to resolve the default execute protocol.",
1042
1089
  remediation:
1043
- "Report this bug; execute should default to native-api or mac-telnet.",
1090
+ "Report this bug; execute should default to native-api, rest-api, or mac-telnet.",
1044
1091
  });
1045
1092
  }
1046
1093
  if (!plannedProtocols.includes(via.value as RouterOsProtocol)) {
@@ -1074,23 +1121,25 @@ function resolveExecuteProtocol(
1074
1121
  return via as ResolvedSetting<RouterOsProtocol>;
1075
1122
  }
1076
1123
 
1077
- function isUnresolvedMacTarget(
1124
+ /**
1125
+ * Execute's default transport when no tier pins `via`, decided from the
1126
+ * *effective* target (host > CENTRS_HOST > CDB target > positional), not the
1127
+ * positional alone — an IP supplied via --host/CENTRS_HOST must default to
1128
+ * native-api even when the positional was a MAC. An unresolved MAC defaults to
1129
+ * mac-telnet, and an explicit `http(s)://` base URL names the REST service, so
1130
+ * its scheme and port are honored rather than dropped for native-api (#426).
1131
+ */
1132
+ function defaultExecuteVia(
1078
1133
  request: ExecuteRequest,
1079
1134
  env: Record<string, string | undefined>,
1080
1135
  cdb?: CdbResolution,
1081
1136
  config: Record<string, string | undefined> = {},
1082
- ): boolean {
1083
- if (
1084
- request.via !== undefined ||
1085
- env["CENTRS_VIA"] !== undefined ||
1086
- config["CENTRS_VIA"] !== undefined
1087
- ) {
1088
- return false;
1137
+ ): RouterOsProtocol {
1138
+ // A quickchr target's endpoint comes from its descriptor, so no host
1139
+ // setting may steer its transport.
1140
+ if (request.quickchr !== undefined) {
1141
+ return "native-api";
1089
1142
  }
1090
- // Decide the default transport from the *effective* target (host >
1091
- // CENTRS_HOST > CDB target > positional), not the positional alone — an IP
1092
- // supplied via --host/CENTRS_HOST must default to native-api even when the
1093
- // positional was a MAC.
1094
1143
  const candidate = effectiveHostCandidate({
1095
1144
  host: request.host,
1096
1145
  targetInput: request.targetInput,
@@ -1098,7 +1147,13 @@ function isUnresolvedMacTarget(
1098
1147
  env,
1099
1148
  config,
1100
1149
  });
1101
- return candidate ? isMacAddress(candidate) : false;
1150
+ if (candidate === undefined) {
1151
+ return "native-api";
1152
+ }
1153
+ if (isMacAddress(candidate)) {
1154
+ return "mac-telnet";
1155
+ }
1156
+ return /^https?:\/\//i.test(candidate) ? "rest-api" : "native-api";
1102
1157
  }
1103
1158
 
1104
1159
  function resolveFormat(
@@ -1367,20 +1422,6 @@ function renderExecuteSuccessText(
1367
1422
  return lines.join("\n");
1368
1423
  }
1369
1424
 
1370
- function renderExecuteErrorText(
1371
- envelope: ExecuteErrorEnvelope,
1372
- options: { verbose?: boolean },
1373
- ): string {
1374
- const lines = [`[${envelope.error.code}] ${envelope.error.summary}`];
1375
- if (envelope.error.remediation) {
1376
- lines.push(`Fix: ${envelope.error.remediation}`);
1377
- }
1378
- if (options.verbose && envelope.error.context) {
1379
- lines.push("", JSON.stringify(envelope.error.context, null, 2));
1380
- }
1381
- return lines.join("\n");
1382
- }
1383
-
1384
1425
  function syntaxCause(error: unknown): unknown {
1385
1426
  if (error instanceof CentrsError) {
1386
1427
  return error.causeData ?? error.context ?? error.toJSON();
@@ -9,10 +9,10 @@
9
9
  * Union of two first-order sources (#228):
10
10
  *
11
11
  * 1. MikroTik's published CLI Reference (`https://manual.mikrotik.com/docs/cli-reference/`),
12
- * 1,055 pages, 1,062 entries — first-order about the definition
12
+ * 1,099 pages, 1,106 entries — first-order about the definition
13
13
  * structs and their build-time gates. Since #285 every page is a leaf whose
14
14
  * slug is the CLI path, so no spelling has to be rewritten to be looked up.
15
- * 2. 6 pinned restraml `/console/inspect` trees
15
+ * 2. 8 pinned restraml `/console/inspect` trees
16
16
  * (`https://tikoci.github.io/restraml/`) — first-order about the CLI surface:
17
17
  *
18
18
  * | Tree | Arch | RouterOS | Nodes |
@@ -23,12 +23,14 @@
23
23
  * | `7.24.4/extra/deep-inspect.arm64.json` | arm64 | 7.24.4 | 42,691 |
24
24
  * | `7.25beta5/extra/deep-inspect.x86.json` | x86 | 7.25beta5 | 41,794 |
25
25
  * | `7.25beta5/extra/deep-inspect.arm64.json` | arm64 | 7.25beta5 | 43,150 |
26
+ * | `7.26beta1/extra/deep-inspect.x86.json` | x86 | 7.26beta1 | 43,294 |
27
+ * | `7.26beta1/extra/deep-inspect.arm64.json` | arm64 | 7.26beta1 | 44,650 |
26
28
  *
27
29
  * | Kind | Total | `both` | `inspect` | `published` |
28
30
  * | ---- | ----- | ------ | --------- | ----------- |
29
- * | `menu` | 554 | 445 | 65 | 44 |
30
- * | `command` | 450 | 409 | 0 | 41 |
31
- * | `settings` | 116 | 107 | 0 | 9 |
31
+ * | `menu` | 568 | 457 | 67 | 44 |
32
+ * | `command` | 477 | 436 | 0 | 41 |
33
+ * | `settings` | 121 | 112 | 0 | 9 |
32
34
  *
33
35
  * Zero navigation-vs-command contradictions between the two sources. Generation
34
36
  * aborts if that ever stops holding, rather than picking a winner. Where a path
@@ -89,7 +91,7 @@ export interface CatalogEntry {
89
91
  * `path|kind|provenance|package|conditions|syscap` — with trailing empty
90
92
  * columns trimmed. Lower-cased, slash-led and sorted by path.
91
93
  *
92
- * Text rather than ~1,120 object literals so that one path is one line in a
94
+ * Text rather than ~1,166 object literals so that one path is one line in a
93
95
  * review diff, and so the formatter has nothing to re-wrap.
94
96
  */
95
97
  const ROWS = `
@@ -154,6 +156,38 @@ const ROWS = `
154
156
  /certificate/settings|settings|both
155
157
  /certificate/sign|command|both
156
158
  /certificate/sign-certificate-request|command|both
159
+ /cmr|settings|both|cmr
160
+ /cmr/alert|menu|both|cmr
161
+ /cmr/alert/show-devices|command|both|cmr
162
+ /cmr/alert/test|command|both|cmr
163
+ /cmr/client|settings|both||!smips, !powerpc
164
+ /cmr/client/forget|command|both||!smips, !powerpc
165
+ /cmr/client/pair|command|both||!smips, !powerpc
166
+ /cmr/client/push-button|command|both
167
+ /cmr/device|menu|both|cmr
168
+ /cmr/device/apptraffic|command|both|cmr
169
+ /cmr/device/dashboard|command|both|cmr
170
+ /cmr/device/pair|command|both|cmr
171
+ /cmr/device/reboot|command|both|cmr
172
+ /cmr/device/run-script|command|both|cmr
173
+ /cmr/device/upgrade|command|both|cmr
174
+ /cmr/device/wifi-logs|command|both|cmr
175
+ /cmr/layout|menu|both|cmr
176
+ /cmr/layout/add-devices|command|both|cmr
177
+ /cmr/layout/link|menu|both|cmr
178
+ /cmr/layout/node|menu|both|cmr
179
+ /cmr/layout/rebuild-links|command|both|cmr
180
+ /cmr/push-button|command|both
181
+ /cmr/upgrade|menu|both|cmr
182
+ /cmr/upgrade/job|menu|both|cmr
183
+ /cmr/upgrade/job/run-next|command|both|cmr
184
+ /cmr/upgrade/job/show-devices|command|both|cmr
185
+ /cmr/upgrade/show-devices|command|both|cmr
186
+ /cmr/upgrade/trigger|command|both|cmr
187
+ /cmr/upgrade/version-check|command|both|cmr
188
+ /cmr/vlan|menu|both|cmr
189
+ /cmr/wifi|menu|both|cmr
190
+ /cmr/wifi/radio|menu|both|cmr
157
191
  /console|menu|inspect
158
192
  /console/inspect|command|both
159
193
  /console/settings|settings|both
@@ -286,6 +320,7 @@ const ROWS = `
286
320
  /interface/ethernet/poe/power-cycle|command|published|||(poe or poe-in)
287
321
  /interface/ethernet/poe/settings|settings|published|||(poe or poe-in) and poesettings
288
322
  /interface/ethernet/pon|menu|both
323
+ /interface/ethernet/pon/monitor|command|both
289
324
  /interface/ethernet/reset-counters|command|both
290
325
  /interface/ethernet/reset-mac-address|command|both
291
326
  /interface/ethernet/switch|menu|both
@@ -327,7 +362,8 @@ const ROWS = `
327
362
  /interface/ethernet/switch/reserved-fdb|menu|published|||musicswitch
328
363
  /interface/ethernet/switch/reset-counters|command|both
329
364
  /interface/ethernet/switch/rule|menu|both|||rbswitch
330
- /interface/ethernet/switch/rule/reset-counters|command|both|||rbswitch
365
+ /interface/ethernet/switch/rule/reset-counters|command|both|||rbswitch and crs_prestera
366
+ /interface/ethernet/switch/rule/reset-counters-all|command|published|||rbswitch and crs_prestera
331
367
  /interface/ethernet/switch/shaper|menu|published|||musicswitch
332
368
  /interface/ethernet/switch/stats|settings|published||!smips|musicswitch
333
369
  /interface/ethernet/switch/trunk|menu|published|||musicswitch
@@ -442,6 +478,11 @@ const ROWS = `
442
478
  /interface/wifi/capsman/remote-cap/upgrade|command|both
443
479
  /interface/wifi/channel|menu|both
444
480
  /interface/wifi/configuration|menu|both
481
+ /interface/wifi/dashboard|menu|inspect
482
+ /interface/wifi/dashboard/settings|settings|both
483
+ /interface/wifi/dashboard/show|command|both
484
+ /interface/wifi/dashboard/show-clients|command|both
485
+ /interface/wifi/dashboard/show-logs|command|both
445
486
  /interface/wifi/datapath|menu|both
446
487
  /interface/wifi/devel|command|both
447
488
  /interface/wifi/flat-snoop|command|both
@@ -525,6 +566,7 @@ const ROWS = `
525
566
  /interface/wireless/sniffer/sniff|command|both|wireless-rep
526
567
  /interface/wireless/snooper|settings|both|wireless-rep
527
568
  /interface/wireless/snooper/flat-snoop|command|both|wireless-rep
569
+ /interface/wireless/snooper/snoop|command|both|wireless-rep
528
570
  /interface/wireless/spectral-scan|command|both|wireless-rep
529
571
  /interface/wireless/wds|menu|both|wireless-rep
530
572
  /interface/wireless/wds/monitor|command|both|wireless-rep
@@ -984,6 +1026,9 @@ const ROWS = `
984
1026
  /system/clock/manual|settings|both
985
1027
  /system/console|menu|both
986
1028
  /system/console/screen|settings|both||i386
1029
+ /system/dashboard|menu|inspect
1030
+ /system/dashboard/settings|settings|both
1031
+ /system/dashboard/show|command|both
987
1032
  /system/default-configuration|settings|both
988
1033
  /system/default-configuration/caps-mode-script|settings|both
989
1034
  /system/default-configuration/custom-script|settings|both
@@ -1062,10 +1107,9 @@ const ROWS = `
1062
1107
  /system/routerboard/mode-button|settings|both||!i386
1063
1108
  /system/routerboard/reset-button|settings|both||!i386
1064
1109
  /system/routerboard/settings|settings|both||!i386
1065
- /system/routerboard/settings/keep-frequency|command|published||!i386, mipsel
1066
1110
  /system/routerboard/upgrade|command|both||!i386
1067
- /system/routerboard/usb|settings|both||!i386, !i386, !mipsel, !powerpc
1068
- /system/routerboard/usb/power-reset|command|both||!i386, !i386, !mipsel, !powerpc
1111
+ /system/routerboard/usb|settings|both||!i386, !i386, !powerpc
1112
+ /system/routerboard/usb/power-reset|command|both||!i386, !i386, !powerpc
1069
1113
  /system/routerboard/wps-button|settings|both||!i386
1070
1114
  /system/rtrace|settings|both
1071
1115
  /system/rtrace/start|command|both
@@ -1101,6 +1145,10 @@ const ROWS = `
1101
1145
  /task/terminate|command|both
1102
1146
  /terminal|menu|inspect
1103
1147
  /tool|menu|inspect
1148
+ /tool/apptraffic|settings|both||!mmips, !powerpc, !smips
1149
+ /tool/apptraffic/stats|menu|both||!mmips, !powerpc, !smips
1150
+ /tool/apptraffic/stats/categories|menu|both||!mmips, !powerpc, !smips
1151
+ /tool/apptraffic/stats/clear|command|both||!mmips, !powerpc, !smips
1104
1152
  /tool/bandwidth-server|settings|both
1105
1153
  /tool/bandwidth-server/session|menu|both
1106
1154
  /tool/bandwidth-test|command|both