@tikoci/centrs 0.1.6 → 0.1.8-next.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +342 -0
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +10 -6
  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 +135 -42
  8. package/commands/api/examples.md +275 -19
  9. package/commands/devices/README.md +21 -4
  10. package/commands/devices/examples.md +33 -2
  11. package/commands/execute/README.md +60 -18
  12. package/commands/execute/examples.md +224 -0
  13. package/commands/explain/README.md +42 -5
  14. package/commands/mcp/README.md +2 -1
  15. package/commands/retrieve/README.md +348 -4
  16. package/commands/retrieve/examples.md +442 -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 +29 -21
  22. package/docs/CONSTITUTION.md +41 -16
  23. package/docs/MATRIX.md +38 -1
  24. package/docs/errors/README.md +3 -1
  25. package/docs/errors/cdb/create-failed.md +20 -0
  26. package/docs/errors/cdb/file-exists.md +23 -0
  27. package/docs/errors/input/invalid-query.md +36 -0
  28. package/docs/errors/input/invalid-target.md +25 -3
  29. package/docs/errors/input/local-destination.md +20 -0
  30. package/docs/errors/input/unsupported-query.md +19 -0
  31. package/docs/errors/internal/byte-string.md +13 -0
  32. package/docs/errors/routeros/invalid-value.md +3 -2
  33. package/docs/errors/settings/invalid-timeout.md +6 -0
  34. package/docs/errors/transport/cancel-unacknowledged.md +24 -0
  35. package/docs/errors/transport/stream-overflow.md +21 -0
  36. package/docs/errors/usage/removed-flag.md +20 -0
  37. package/docs/errors/validation/menu-unavailable.md +22 -0
  38. package/docs/errors/validation/not-followable.md +23 -0
  39. package/docs/errors/validation/package-missing.md +29 -0
  40. package/docs/errors/validation/unknown-path.md +16 -4
  41. package/docs/errors/wait/deadline-exceeded.md +14 -0
  42. package/docs/errors/wait/interrupted.md +10 -0
  43. package/package.json +14 -11
  44. package/src/absent-menu.ts +280 -0
  45. package/src/api-fanout.ts +22 -12
  46. package/src/api.ts +603 -194
  47. package/src/btest.ts +19 -17
  48. package/src/cli/api.ts +126 -34
  49. package/src/cli/devices.ts +16 -3
  50. package/src/cli/execute.ts +18 -5
  51. package/src/cli/retrieve.ts +216 -22
  52. package/src/cli/terminal.ts +10 -5
  53. package/src/cli/transfer.ts +23 -8
  54. package/src/core/envelope.ts +14 -0
  55. package/src/core/error-catalog.ts +70 -1
  56. package/src/core/execute-command.ts +313 -6
  57. package/src/core/inspect.ts +65 -27
  58. package/src/core/query.ts +508 -0
  59. package/src/core/routeros-errors.ts +67 -1
  60. package/src/data/winbox-cdb-write.ts +22 -8
  61. package/src/devices.ts +148 -37
  62. package/src/discover.ts +7 -8
  63. package/src/errors.ts +116 -7
  64. package/src/execute-fanout.ts +14 -9
  65. package/src/execute.ts +128 -116
  66. package/src/explain/catalog.ts +64 -16
  67. package/src/explain/menus.ts +27 -6
  68. package/src/explain.ts +6 -6
  69. package/src/index.ts +31 -1
  70. package/src/mcp/tools.ts +10 -15
  71. package/src/protocols/adapter.ts +235 -118
  72. package/src/protocols/btest-session.ts +3 -2
  73. package/src/protocols/endpoint.ts +10 -0
  74. package/src/protocols/mac-telnet-console.ts +12 -7
  75. package/src/protocols/native-api.ts +252 -73
  76. package/src/protocols/sftp.ts +44 -14
  77. package/src/protocols/ssh.ts +42 -14
  78. package/src/resolver/cdb.ts +28 -7
  79. package/src/resolver/index.ts +1 -0
  80. package/src/resolver/target.ts +84 -18
  81. package/src/retrieve-fanout.ts +51 -12
  82. package/src/retrieve-follow.ts +862 -0
  83. package/src/retrieve-sample.ts +244 -0
  84. package/src/retrieve-stream.ts +115 -0
  85. package/src/retrieve-wait.ts +286 -0
  86. package/src/retrieve.ts +741 -109
  87. package/src/settings.ts +15 -15
  88. package/src/transfer-fanout.ts +40 -15
  89. package/src/transfer.ts +234 -32
package/src/execute.ts CHANGED
@@ -10,17 +10,22 @@ 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,
17
17
  isWriteShaped,
18
18
  } from "./core/execute-command.ts";
19
19
  import { inspectArgumentNames, pathTokens } from "./core/inspect.ts";
20
- import { mapRouterOsError } from "./core/routeros-errors.ts";
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
  }
@@ -892,7 +929,16 @@ async function runSyntaxGate(
892
929
  resolved: ResolvedExecuteRequest,
893
930
  backend: ProtocolAdapter,
894
931
  ): Promise<void> {
895
- const script = `:put [:parse ${routerOsStringLiteral(resolved.command)}]`;
932
+ await validateRouterOsScript(resolved.command, backend, resolved.via.value);
933
+ }
934
+
935
+ /** Shared live `:parse` stage for every surface that carries a CLI string. */
936
+ export async function validateRouterOsScript(
937
+ command: string,
938
+ backend: ProtocolAdapter,
939
+ via: RouterOsProtocol,
940
+ ): Promise<void> {
941
+ const script = `:put [:parse ${routerOsStringLiteral(command)}]`;
896
942
  try {
897
943
  // GH#230: `:parse` never throws (CHR 7.23.3). The diagnostic rides the
898
944
  // success value's text — over REST/native the `ret` field (HTTP 200 /
@@ -901,7 +947,7 @@ async function runSyntaxGate(
901
947
  // Read the return and classify it: one `:parse` covers syntax and the
902
948
  // unknown-attribute (name-level) gate.
903
949
  const result = await backend.execute({ path: "", command: "", script });
904
- classifyParseResult(result.ret ?? "", resolved.command, resolved.via.value);
950
+ classifyParseResult(result.ret ?? "", command, via);
905
951
  } catch (error) {
906
952
  // The preflight is a syntax gate, but `backend.execute` also surfaces
907
953
  // connection and authentication failures (login happens lazily here).
@@ -922,12 +968,16 @@ async function runSyntaxGate(
922
968
  code: "validation/syntax",
923
969
  summary:
924
970
  "RouterOS rejected the command syntax during `:parse` preflight.",
925
- remediation:
926
- "Fix the RouterOS CLI syntax, especially quotes and bracketed expressions, then retry.",
971
+ remediation: SYNTAX_REJECTED_REMEDIATION,
927
972
  context: {
928
- command: resolved.command,
973
+ command,
929
974
  validationSource: ":put [:parse ...]",
930
- via: resolved.via.value,
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
+ : {}),
931
981
  },
932
982
  ...(position ? { position } : {}),
933
983
  cause: error,
@@ -1013,10 +1063,8 @@ function resolveExecuteProtocol(
1013
1063
  cdb?: CdbResolution,
1014
1064
  config: Record<string, string | undefined> = {},
1015
1065
  ): ResolvedSetting<RouterOsProtocol> {
1016
- const defaultVia = isUnresolvedMacTarget(request, env, cdb, config)
1017
- ? "mac-telnet"
1018
- : "native-api";
1019
- const via = resolveStringSetting(
1066
+ const defaultVia = defaultExecuteVia(request, env, cdb, config);
1067
+ const resolvedVia = resolveStringSetting(
1020
1068
  request.via,
1021
1069
  env,
1022
1070
  "CENTRS_VIA",
@@ -1026,12 +1074,20 @@ function resolveExecuteProtocol(
1026
1074
  cdb?.overrides.via,
1027
1075
  config,
1028
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;
1029
1085
  if (!via) {
1030
1086
  throw new CentrsError({
1031
1087
  code: "internal/unhandled",
1032
1088
  summary: "Failed to resolve the default execute protocol.",
1033
1089
  remediation:
1034
- "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.",
1035
1091
  });
1036
1092
  }
1037
1093
  if (!plannedProtocols.includes(via.value as RouterOsProtocol)) {
@@ -1065,23 +1121,25 @@ function resolveExecuteProtocol(
1065
1121
  return via as ResolvedSetting<RouterOsProtocol>;
1066
1122
  }
1067
1123
 
1068
- 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(
1069
1133
  request: ExecuteRequest,
1070
1134
  env: Record<string, string | undefined>,
1071
1135
  cdb?: CdbResolution,
1072
1136
  config: Record<string, string | undefined> = {},
1073
- ): boolean {
1074
- if (
1075
- request.via !== undefined ||
1076
- env["CENTRS_VIA"] !== undefined ||
1077
- config["CENTRS_VIA"] !== undefined
1078
- ) {
1079
- 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";
1080
1142
  }
1081
- // Decide the default transport from the *effective* target (host >
1082
- // CENTRS_HOST > CDB target > positional), not the positional alone — an IP
1083
- // supplied via --host/CENTRS_HOST must default to native-api even when the
1084
- // positional was a MAC.
1085
1143
  const candidate = effectiveHostCandidate({
1086
1144
  host: request.host,
1087
1145
  targetInput: request.targetInput,
@@ -1089,7 +1147,13 @@ function isUnresolvedMacTarget(
1089
1147
  env,
1090
1148
  config,
1091
1149
  });
1092
- 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";
1093
1157
  }
1094
1158
 
1095
1159
  function resolveFormat(
@@ -1298,52 +1362,14 @@ function routerOsFailureFromResult(
1298
1362
  result: ProtocolExecuteResult,
1299
1363
  via: RouterOsProtocol,
1300
1364
  ): CentrsError | undefined {
1301
- const candidates = [
1302
- result.ret,
1303
- ...result.records.flatMap((record) => [
1304
- record["detail"],
1305
- record["message"],
1306
- record["failure"],
1307
- record["error"],
1308
- record["ret"],
1309
- ]),
1310
- ].filter((value): value is string => typeof value === "string");
1311
- const failure = candidates.find((candidate) =>
1312
- isRouterOsFailureString(candidate),
1313
- );
1314
- return failure
1315
- ? mapRouterOsError(failure, {
1316
- transport:
1317
- via === "rest-api" || via === "mac-telnet" || via === "ssh"
1318
- ? via
1319
- : "native-api",
1320
- context: { via },
1321
- })
1322
- : undefined;
1323
- }
1324
-
1325
- function isRouterOsFailureString(value: string): boolean {
1326
- const trimmed = value.trim();
1327
- // REST/native fault strings (unchanged).
1328
- if (
1329
- /^(failure:|error:)|unknown parameter|invalid value|session closed|\(:error; line \d+\)/i.test(
1330
- trimmed,
1331
- )
1332
- ) {
1333
- return true;
1334
- }
1335
- // Console (mac-telnet) error forms. `ret` here is ordinary stdout (every
1336
- // mac-telnet command and REST `/execute`), so anchor to the grounded RouterOS
1337
- // error *shapes* — the `(line N column M)` source location for parse rejections
1338
- // — rather than bare phrases, so `:put "syntax error"` is not misread as a
1339
- // failure.
1340
- return (
1341
- /(?:^|\n)\s*(?:bad parameter \S+|syntax error)\b[^\n]*\(line \d+ column \d+\)/i.test(
1342
- trimmed,
1343
- ) ||
1344
- /(?:^|\n)\s*bad command name\b/i.test(trimmed) ||
1345
- /(?:^|\n)\s*expected end of command\b/i.test(trimmed)
1346
- );
1365
+ if (typeof result.ret !== "string") return undefined;
1366
+ return mapRouterOsResultError(result.ret, {
1367
+ transport:
1368
+ via === "rest-api" || via === "mac-telnet" || via === "ssh"
1369
+ ? via
1370
+ : "native-api",
1371
+ context: { via },
1372
+ });
1347
1373
  }
1348
1374
 
1349
1375
  function applyMaxResultsBudget(
@@ -1396,20 +1422,6 @@ function renderExecuteSuccessText(
1396
1422
  return lines.join("\n");
1397
1423
  }
1398
1424
 
1399
- function renderExecuteErrorText(
1400
- envelope: ExecuteErrorEnvelope,
1401
- options: { verbose?: boolean },
1402
- ): string {
1403
- const lines = [`[${envelope.error.code}] ${envelope.error.summary}`];
1404
- if (envelope.error.remediation) {
1405
- lines.push(`Fix: ${envelope.error.remediation}`);
1406
- }
1407
- if (options.verbose && envelope.error.context) {
1408
- lines.push("", JSON.stringify(envelope.error.context, null, 2));
1409
- }
1410
- return lines.join("\n");
1411
- }
1412
-
1413
1425
  function syntaxCause(error: unknown): unknown {
1414
1426
  if (error instanceof CentrsError) {
1415
1427
  return error.causeData ?? error.context ?? error.toJSON();
@@ -9,26 +9,28 @@
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,053 pages, 1,060 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 |
19
19
  * | ---- | ---- | -------- | ----- |
20
20
  * | `7.10.2/extra/inspect.json` | x86 | 7.10.2 | 29,086 |
21
21
  * | `7.16/extra/inspect.json` | x86 | 7.16 | 34,876 |
22
- * | `7.24.2/extra/deep-inspect.x86.json` | x86 | 7.24.2 | 41,345 |
23
- * | `7.24.2/extra/deep-inspect.arm64.json` | arm64 | 7.24.2 | 42,691 |
24
- * | `7.25beta3/extra/deep-inspect.x86.json` | x86 | 7.25beta3 | 41,798 |
25
- * | `7.25beta3/extra/deep-inspect.arm64.json` | arm64 | 7.25beta3 | 43,148 |
22
+ * | `7.24.4/extra/deep-inspect.x86.json` | x86 | 7.24.4 | 41,345 |
23
+ * | `7.24.4/extra/deep-inspect.arm64.json` | arm64 | 7.24.4 | 42,691 |
24
+ * | `7.25beta5/extra/deep-inspect.x86.json` | x86 | 7.25beta5 | 41,794 |
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 | 443 | 67 | 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
@@ -285,7 +319,8 @@ const ROWS = `
285
319
  /interface/ethernet/poe/monitor|command|published|||(poe or poe-in)
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
- /interface/ethernet/pon|menu|inspect
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
@@ -744,7 +786,7 @@ const ROWS = `
744
786
  /ip/ssh|settings|both
745
787
  /ip/ssh/export-host-key|command|both
746
788
  /ip/ssh/import-host-key|command|both
747
- /ip/ssh/known-hosts|menu|inspect
789
+ /ip/ssh/known-hosts|menu|both
748
790
  /ip/ssh/regenerate-host-key|command|both
749
791
  /ip/tftp|menu|both
750
792
  /ip/tftp/settings|settings|both
@@ -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
@@ -5,20 +5,22 @@
5
5
  * the drift gate. The generator (`scripts/gen-explain-menus.ts`) carries the
6
6
  * full rationale, the source pin, and why absence here is safe.
7
7
  *
8
- * Union of the `dir` and `path` nodes in 6 pinned restraml typed trees
8
+ * Union of the `dir` and `path` nodes in 8 pinned restraml typed trees
9
9
  * (`https://tikoci.github.io/restraml/`), all extra-packages builds:
10
10
  *
11
11
  * | Tree | Arch | RouterOS | Nodes | Containers |
12
12
  * | ---- | ---- | -------- | ----- | ---------- |
13
13
  * | `7.10.2/extra/inspect.json` | x86 | 7.10.2 | 29,086 | 458 |
14
14
  * | `7.16/extra/inspect.json` | x86 | 7.16 | 34,876 | 522 |
15
- * | `7.24.2/extra/deep-inspect.x86.json` | x86 | 7.24.2 | 41,345 | 554 |
16
- * | `7.24.2/extra/deep-inspect.arm64.json` | arm64 | 7.24.2 | 42,691 | 575 |
17
- * | `7.25beta3/extra/deep-inspect.x86.json` | x86 | 7.25beta3 | 41,798 | 557 |
18
- * | `7.25beta3/extra/deep-inspect.arm64.json` | arm64 | 7.25beta3 | 43,148 | 578 |
15
+ * | `7.24.4/extra/deep-inspect.x86.json` | x86 | 7.24.4 | 41,345 | 554 |
16
+ * | `7.24.4/extra/deep-inspect.arm64.json` | arm64 | 7.24.4 | 42,691 | 575 |
17
+ * | `7.25beta5/extra/deep-inspect.x86.json` | x86 | 7.25beta5 | 41,794 | 557 |
18
+ * | `7.25beta5/extra/deep-inspect.arm64.json` | arm64 | 7.25beta5 | 43,150 | 578 |
19
+ * | `7.26beta1/extra/deep-inspect.x86.json` | x86 | 7.26beta1 | 43,294 | 576 |
20
+ * | `7.26beta1/extra/deep-inspect.arm64.json` | arm64 | 7.26beta1 | 44,650 | 597 |
19
21
  *
20
22
  * Zero node-type conflicts across those trees — no `dir`↔`cmd` flip across
21
- * 4 versions or across architectures — which is what makes the union
23
+ * 5 versions or across architectures — which is what makes the union
22
24
  * version-less rather than version-keyed. Generation aborts if that ever stops
23
25
  * holding.
24
26
  *
@@ -57,6 +59,18 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
57
59
  "/certificate/scep-server/ra",
58
60
  "/certificate/scep-server/requests",
59
61
  "/certificate/settings",
62
+ "/cmr",
63
+ "/cmr/alert",
64
+ "/cmr/client",
65
+ "/cmr/device",
66
+ "/cmr/layout",
67
+ "/cmr/layout/link",
68
+ "/cmr/layout/node",
69
+ "/cmr/upgrade",
70
+ "/cmr/upgrade/job",
71
+ "/cmr/vlan",
72
+ "/cmr/wifi",
73
+ "/cmr/wifi/radio",
60
74
  "/console",
61
75
  "/console/settings",
62
76
  "/container",
@@ -183,6 +197,8 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
183
197
  "/interface/wifi/capsman/remote-cap",
184
198
  "/interface/wifi/channel",
185
199
  "/interface/wifi/configuration",
200
+ "/interface/wifi/dashboard",
201
+ "/interface/wifi/dashboard/settings",
186
202
  "/interface/wifi/datapath",
187
203
  "/interface/wifi/interworking",
188
204
  "/interface/wifi/network",
@@ -526,6 +542,8 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
526
542
  "/system/clock/manual",
527
543
  "/system/console",
528
544
  "/system/console/screen",
545
+ "/system/dashboard",
546
+ "/system/dashboard/settings",
529
547
  "/system/default-configuration",
530
548
  "/system/default-configuration/caps-mode-script",
531
549
  "/system/default-configuration/custom-script",
@@ -583,6 +601,9 @@ export const MENU_PATHS: ReadonlySet<string> = new Set([
583
601
  "/task",
584
602
  "/terminal",
585
603
  "/tool",
604
+ "/tool/apptraffic",
605
+ "/tool/apptraffic/stats",
606
+ "/tool/apptraffic/stats/categories",
586
607
  "/tool/bandwidth-server",
587
608
  "/tool/bandwidth-server/session",
588
609
  "/tool/calea",
package/src/explain.ts CHANGED
@@ -108,7 +108,11 @@ import {
108
108
  isWriteShaped,
109
109
  } from "./core/execute-command.ts";
110
110
  import { toYaml } from "./core/yaml.ts";
111
- import { CentrsError, serializeCentrsError } from "./errors.ts";
111
+ import {
112
+ CentrsError,
113
+ formatCentrsErrorText,
114
+ serializeCentrsError,
115
+ } from "./errors.ts";
112
116
  import { argSpans } from "./explain/arg-tokens.ts";
113
117
  import {
114
118
  type ArgumentKind,
@@ -2495,11 +2499,7 @@ function renderExplainText(
2495
2499
  ): string {
2496
2500
  const lines: string[] = [];
2497
2501
  if (!envelope.ok) {
2498
- lines.push(`[${envelope.error.code}] ${envelope.error.summary}`);
2499
- if (envelope.error.remediation)
2500
- lines.push(`Fix: ${envelope.error.remediation}`);
2501
- if (envelope.error.detailsUrl)
2502
- lines.push(`Details: ${envelope.error.detailsUrl}`);
2502
+ lines.push(formatCentrsErrorText(envelope.error));
2503
2503
  lines.push(...tipLines(envelope.tips));
2504
2504
  return lines.join("\n");
2505
2505
  }