@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.
- package/CHANGELOG.md +256 -0
- package/GLOSSARY.txt +32 -0
- package/README.md +2 -2
- package/SECURITY.md +1 -1
- package/commands/AGENTS.md +2 -1
- package/commands/api/AGENTS.md +55 -30
- package/commands/api/README.md +123 -41
- package/commands/api/examples.md +205 -17
- package/commands/devices/README.md +11 -3
- package/commands/execute/README.md +31 -3
- package/commands/execute/examples.md +136 -1
- package/commands/explain/README.md +30 -4
- package/commands/mcp/README.md +5 -1
- package/commands/mcp/examples.md +18 -1
- package/commands/retrieve/README.md +348 -4
- package/commands/retrieve/examples.md +422 -5
- package/commands/terminal/README.md +14 -4
- package/commands/terminal/examples.md +22 -3
- package/commands/transfer/README.md +55 -9
- package/commands/transfer/examples.md +106 -7
- package/docs/CLI.md +28 -20
- package/docs/CONSTITUTION.md +30 -10
- package/docs/MATRIX.md +38 -1
- package/docs/errors/README.md +3 -1
- package/docs/errors/input/invalid-query.md +36 -0
- package/docs/errors/input/local-destination.md +20 -0
- package/docs/errors/input/unsupported-query.md +19 -0
- package/docs/errors/internal/byte-string.md +13 -0
- package/docs/errors/settings/invalid-timeout.md +6 -0
- package/docs/errors/transport/stream-overflow.md +21 -0
- package/docs/errors/usage/removed-flag.md +20 -0
- package/docs/errors/validation/menu-unavailable.md +22 -0
- package/docs/errors/validation/not-followable.md +23 -0
- package/docs/errors/validation/package-missing.md +29 -0
- package/docs/errors/validation/unknown-path.md +16 -4
- package/docs/errors/wait/deadline-exceeded.md +14 -0
- package/docs/errors/wait/interrupted.md +10 -0
- package/package.json +14 -11
- package/src/absent-menu.ts +280 -0
- package/src/api-fanout.ts +22 -12
- package/src/api.ts +511 -186
- package/src/btest.ts +19 -17
- package/src/cli/api.ts +126 -34
- package/src/cli/execute.ts +18 -5
- package/src/cli/retrieve.ts +223 -22
- package/src/cli/terminal.ts +10 -5
- package/src/cli/transfer.ts +23 -8
- package/src/core/envelope.ts +14 -0
- package/src/core/error-catalog.ts +56 -1
- package/src/core/inspect.ts +43 -5
- package/src/core/query.ts +508 -0
- package/src/core/routeros-errors.ts +1 -1
- package/src/devices.ts +6 -9
- package/src/discover.ts +7 -8
- package/src/errors.ts +116 -7
- package/src/execute-fanout.ts +13 -8
- package/src/execute.ts +106 -65
- package/src/explain/catalog.ts +58 -10
- package/src/explain/menus.ts +23 -2
- package/src/explain.ts +6 -6
- package/src/index.ts +29 -1
- package/src/mcp/safety.ts +43 -18
- package/src/mcp/tools.ts +14 -17
- package/src/protocols/adapter.ts +224 -118
- package/src/protocols/btest-session.ts +3 -2
- package/src/protocols/endpoint.ts +10 -0
- package/src/protocols/mac-telnet-console.ts +6 -3
- package/src/protocols/native-api.ts +223 -70
- package/src/protocols/sftp.ts +44 -14
- package/src/protocols/ssh.ts +42 -14
- package/src/resolver/cdb.ts +28 -7
- package/src/resolver/index.ts +1 -0
- package/src/resolver/target.ts +53 -7
- package/src/retrieve-fanout.ts +51 -12
- package/src/retrieve-follow.ts +862 -0
- package/src/retrieve-sample.ts +244 -0
- package/src/retrieve-stream.ts +115 -0
- package/src/retrieve-wait.ts +286 -0
- package/src/retrieve.ts +732 -108
- package/src/settings.ts +15 -15
- package/src/transfer-fanout.ts +40 -15
- 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 =
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
lines.push(JSON.stringify(serialized.context, null, 2));
|
|
148
|
-
}
|
|
191
|
+
return lines;
|
|
192
|
+
}
|
|
149
193
|
|
|
150
|
-
|
|
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 {
|
package/src/execute-fanout.ts
CHANGED
|
@@ -38,7 +38,12 @@ import {
|
|
|
38
38
|
summarizeFanout,
|
|
39
39
|
} from "./core/fanout.ts";
|
|
40
40
|
import { toYaml } from "./core/yaml.ts";
|
|
41
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
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.
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
:
|
|
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 =
|
|
1026
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
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();
|
package/src/explain/catalog.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
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` |
|
|
30
|
-
* | `command` |
|
|
31
|
-
* | `settings` |
|
|
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,
|
|
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, !
|
|
1068
|
-
/system/routerboard/usb/power-reset|command|both||!i386, !i386, !
|
|
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
|