@tikoci/centrs 0.1.9-next.0 → 0.1.9-next.23
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 +177 -40
- package/GLOSSARY.txt +32 -0
- package/README.md +12 -8
- package/SECURITY.md +11 -3
- package/commands/api/README.md +39 -15
- package/commands/api/examples.md +44 -6
- package/commands/execute/README.md +108 -14
- package/commands/execute/examples.md +137 -3
- package/commands/explain/README.md +430 -129
- package/commands/explain/examples.md +260 -1
- package/commands/mcp/README.md +11 -0
- package/commands/mcp/examples.md +6 -0
- package/commands/retrieve/README.md +30 -13
- package/commands/retrieve/examples.md +10 -6
- package/commands/transfer/examples.md +9 -0
- package/docs/CLI.md +8 -8
- package/docs/CONSTITUTION.md +29 -2
- package/docs/MATRIX.md +26 -13
- package/docs/errors/cdb/empty-group.md +19 -3
- package/docs/errors/cdb/empty-selection.md +9 -3
- package/docs/errors/cmr/caller-policy-not-enforced.md +46 -0
- package/docs/errors/cmr/empty-selection.md +17 -3
- package/docs/errors/cmr/rule-selects-every-device.md +32 -0
- package/docs/errors/cmr/selection-includes-controller.md +27 -0
- package/docs/errors/usage/flag-after-separator.md +34 -0
- package/package.json +10 -5
- package/src/api-fanout.ts +11 -1
- package/src/api.ts +205 -65
- package/src/btest.ts +7 -2
- package/src/cli/api.ts +30 -16
- package/src/cli/common.ts +180 -1
- package/src/cli/devices.ts +0 -2
- package/src/cli/discover.ts +0 -1
- package/src/cli/execute.ts +44 -20
- package/src/cli/explain.ts +2 -0
- package/src/cli/mcp.ts +0 -1
- package/src/cli/retrieve.ts +40 -18
- package/src/cli/settings.ts +0 -3
- package/src/cli/terminal.ts +0 -2
- package/src/cli/transfer.ts +10 -8
- package/src/cli/version.ts +87 -0
- package/src/cli.ts +5 -3
- package/src/core/cmr-fleet.ts +486 -0
- package/src/core/cmr.ts +78 -0
- package/src/core/envelope.ts +32 -3
- package/src/core/error-catalog.ts +25 -3
- package/src/core/execute-command.ts +144 -19
- package/src/core/fanout.ts +130 -6
- package/src/errors.ts +2 -1
- package/src/execute-cmr.ts +95 -78
- package/src/execute-fanout.ts +11 -1
- package/src/execute.ts +105 -23
- package/src/explain/args.ts +69 -8
- package/src/explain/blocks.ts +3 -4
- package/src/explain/brace-slots.ts +5 -0
- package/src/explain/catalog.ts +13 -6
- package/src/explain/defects.ts +1 -0
- package/src/explain/group-faults.ts +927 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +20 -19
- package/src/explain/pathresolve.ts +102 -20
- package/src/explain/quoted-string.ts +41 -6
- package/src/explain/run-tokens.ts +223 -0
- package/src/explain/scope-brace.ts +103 -6
- package/src/explain/segment.ts +41 -3
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +17 -1
- package/src/explain/transport.ts +13 -0
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain.ts +378 -18
- package/src/index.ts +18 -1
- package/src/mcp/safety.ts +11 -0
- package/src/mcp/tools.ts +4 -1
- package/src/offline-gate.ts +30 -9
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
- package/src/resolver/cdb.ts +13 -1
- package/src/resolver/index.ts +4 -0
- package/src/resolver/selection.ts +37 -1
- package/src/resolver/target.ts +96 -8
- package/src/retrieve-fanout.ts +4 -1
- package/src/retrieve-follow.ts +29 -33
- package/src/retrieve-sample.ts +14 -8
- package/src/retrieve-stream.ts +38 -6
- package/src/retrieve.ts +73 -59
- package/src/transfer-fanout.ts +8 -1
- package/src/transfer.ts +15 -19
package/src/explain/args.ts
CHANGED
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
} from "./segment.ts";
|
|
88
88
|
|
|
89
89
|
/** A RouterOS argument name: bare word, optionally dotted (`.id`, `.proplist`). */
|
|
90
|
-
const ARGUMENT_NAME = /^\.?[A-Za-z][A-Za-z0-9._-]*$/;
|
|
90
|
+
export const ARGUMENT_NAME = /^\.?[A-Za-z][A-Za-z0-9._-]*$/;
|
|
91
91
|
|
|
92
92
|
/** What one argument token is. */
|
|
93
93
|
export type ArgumentKind = "attribute" | "positional" | "query";
|
|
@@ -327,6 +327,12 @@ interface WalkOptions {
|
|
|
327
327
|
* see {@link braceSlotTakesArray}.
|
|
328
328
|
*/
|
|
329
329
|
directiveVerb?: string;
|
|
330
|
+
/**
|
|
331
|
+
* Where a command's top-level `where` positional ends, set by the walk itself
|
|
332
|
+
* once it reads one. From there on a brace is an expression value
|
|
333
|
+
* (see {@link comparesNamedValue}).
|
|
334
|
+
*/
|
|
335
|
+
whereAt?: number;
|
|
330
336
|
}
|
|
331
337
|
|
|
332
338
|
/** How deep member scanning descends before it stops and keeps only the shape. */
|
|
@@ -363,7 +369,11 @@ function braceOpensArray(
|
|
|
363
369
|
options: WalkOptions,
|
|
364
370
|
): boolean {
|
|
365
371
|
const verb = options.directiveVerb;
|
|
366
|
-
if (verb === undefined)
|
|
372
|
+
if (verb === undefined)
|
|
373
|
+
return (
|
|
374
|
+
options.whereAt !== undefined &&
|
|
375
|
+
!comparesNamedValue(text, at, options.whereAt)
|
|
376
|
+
);
|
|
367
377
|
const owner = braceValueOwner(text, tokenStart, at);
|
|
368
378
|
if (owner === undefined) return false;
|
|
369
379
|
return braceSlotTakesArray(
|
|
@@ -390,6 +400,7 @@ function* walkArguments(
|
|
|
390
400
|
let i = Math.max(0, from);
|
|
391
401
|
// How many positionals have been read, which is the NEXT positional's index.
|
|
392
402
|
let positionals = 0;
|
|
403
|
+
let walk = options;
|
|
393
404
|
while (i < text.length) {
|
|
394
405
|
// The MASKED view decides whitespace, because `maskComments` blanks a
|
|
395
406
|
// continuation comment that `text` still spells with a `#`. Reading `text`
|
|
@@ -412,7 +423,7 @@ function* walkArguments(
|
|
|
412
423
|
i += continuation;
|
|
413
424
|
continue;
|
|
414
425
|
}
|
|
415
|
-
const token = scanToken(text, structural, i, positionals,
|
|
426
|
+
const token = scanToken(text, structural, i, positionals, walk);
|
|
416
427
|
if (typeof token === "string") {
|
|
417
428
|
yield token;
|
|
418
429
|
return;
|
|
@@ -430,7 +441,7 @@ function* walkArguments(
|
|
|
430
441
|
// the refusal exists to prevent. Shape only, never a value.
|
|
431
442
|
const read =
|
|
432
443
|
token.why === undefined
|
|
433
|
-
? readToken(text, structural, i, token.end,
|
|
444
|
+
? readToken(text, structural, i, token.end, walk)
|
|
434
445
|
: undecidedToken(text, i, token.end, token.why);
|
|
435
446
|
if (typeof read === "string") {
|
|
436
447
|
if (options.tolerant !== true) {
|
|
@@ -446,6 +457,13 @@ function* walkArguments(
|
|
|
446
457
|
continue;
|
|
447
458
|
}
|
|
448
459
|
if (read.kind === "positional") positionals++;
|
|
460
|
+
if (
|
|
461
|
+
read.kind === "positional" &&
|
|
462
|
+
read.text === "where" &&
|
|
463
|
+
walk.directiveVerb === undefined &&
|
|
464
|
+
walk.whereAt === undefined
|
|
465
|
+
)
|
|
466
|
+
walk = { ...walk, whereAt: token.end };
|
|
449
467
|
yield read;
|
|
450
468
|
i = token.end;
|
|
451
469
|
}
|
|
@@ -1661,13 +1679,51 @@ export function lexValueAnchors(
|
|
|
1661
1679
|
return { complete: true, anchors };
|
|
1662
1680
|
}
|
|
1663
1681
|
|
|
1682
|
+
/**
|
|
1683
|
+
* Whether the brace at `at` is the right side of a `where` comparison that
|
|
1684
|
+
* RouterOS reads with the named property's own value parser (#502).
|
|
1685
|
+
*
|
|
1686
|
+
* A `where` clause is expression context, so a brace there is an array value —
|
|
1687
|
+
* except straight after `<name>=`, `<name>!=` or `<name>==`, spaced or not.
|
|
1688
|
+
* On CHR 7.24.5 and 7.23.8, `:parse` rejects `/ip/route/print where
|
|
1689
|
+
* comment={1;2}` with `expected comment value`, and `where disabled={…}` with
|
|
1690
|
+
* `expected yes or no`. It accepts the brace after `in`, `~`, `<`, `>`, `<=`
|
|
1691
|
+
* and `>=`, on the left of `=`, and after `!interface=`, `$x=` and `"a"=`.
|
|
1692
|
+
*
|
|
1693
|
+
* Only a property name the menu KNOWS is read this way: `where foo-bar={…}` is
|
|
1694
|
+
* accepted. Offline has no per-menu property list (decision 3), so every bare
|
|
1695
|
+
* name counts, and an unknown one is a pinned over-strict reading.
|
|
1696
|
+
*
|
|
1697
|
+
* The name may open a group, `where (interface={"x"})` and `or(comment={…})`,
|
|
1698
|
+
* which `group-faults.ts` asks about (#509).
|
|
1699
|
+
*/
|
|
1700
|
+
export function comparesNamedValue(
|
|
1701
|
+
text: string,
|
|
1702
|
+
at: number,
|
|
1703
|
+
floor: number,
|
|
1704
|
+
): boolean {
|
|
1705
|
+
let j = at - 1;
|
|
1706
|
+
while (j > floor && /\s/.test(text[j] as string)) j--;
|
|
1707
|
+
if (text[j] !== "=") return false;
|
|
1708
|
+
while (j > floor && text[j] === "=") j--;
|
|
1709
|
+
if (text[j] === "<" || text[j] === ">") return false;
|
|
1710
|
+
if (text[j] === "!") j--;
|
|
1711
|
+
while (j > floor && /\s/.test(text[j] as string)) j--;
|
|
1712
|
+
const nameEnd = j + 1;
|
|
1713
|
+
while (j > floor && /[A-Za-z0-9._-]/.test(text[j] as string)) j--;
|
|
1714
|
+
const name = text.slice(j + 1, nameEnd);
|
|
1715
|
+
return /^[A-Za-z.]/.test(name) && /[\s(]/.test(text[j] as string);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1664
1718
|
/**
|
|
1665
1719
|
* Locate top-level `{...}` values that RouterOS rejects in command arguments.
|
|
1666
1720
|
*
|
|
1667
1721
|
* Parentheses and substitutions restore expression context, where a brace
|
|
1668
|
-
* literal is legal
|
|
1669
|
-
*
|
|
1670
|
-
*
|
|
1722
|
+
* literal is legal, and so does a `where` clause, apart from the comparisons
|
|
1723
|
+
* {@link comparesNamedValue} names. Stored-script attributes are also
|
|
1724
|
+
* excluded: their brace is a script body, not an array. The caller applies
|
|
1725
|
+
* this only to non-root commands; root scripting directives use the
|
|
1726
|
+
* `(verb, slot)` table instead.
|
|
1671
1727
|
*/
|
|
1672
1728
|
export function invalidCommandBraceOffsets(
|
|
1673
1729
|
text: string,
|
|
@@ -1681,6 +1737,7 @@ export function invalidCommandBraceOffsets(
|
|
|
1681
1737
|
let i = Math.max(0, from);
|
|
1682
1738
|
let tokenStart: number | null = null;
|
|
1683
1739
|
let positionals = 0;
|
|
1740
|
+
let whereAt: number | null = null;
|
|
1684
1741
|
while (i < structural.length) {
|
|
1685
1742
|
const c = structural[i] as string;
|
|
1686
1743
|
if (
|
|
@@ -1689,6 +1746,7 @@ export function invalidCommandBraceOffsets(
|
|
|
1689
1746
|
) {
|
|
1690
1747
|
if (tokenStart !== null) {
|
|
1691
1748
|
const token = structural.slice(tokenStart, i);
|
|
1749
|
+
if (whereAt === null && token === "where") whereAt = i;
|
|
1692
1750
|
if (
|
|
1693
1751
|
!token.startsWith("?") &&
|
|
1694
1752
|
unquotedEquals(token, 0, token.length) === null
|
|
@@ -1737,7 +1795,10 @@ export function invalidCommandBraceOffsets(
|
|
|
1737
1795
|
: braceStartsStatements(range.masked, range.start + i, range.start)) ||
|
|
1738
1796
|
(directiveVerb !== undefined &&
|
|
1739
1797
|
slot !== undefined &&
|
|
1740
|
-
braceSlotAcceptsBrace(directiveVerb, slot))
|
|
1798
|
+
braceSlotAcceptsBrace(directiveVerb, slot)) ||
|
|
1799
|
+
(directiveVerb === undefined &&
|
|
1800
|
+
whereAt !== null &&
|
|
1801
|
+
!comparesNamedValue(structural, i, whereAt))
|
|
1741
1802
|
) {
|
|
1742
1803
|
i = end ?? structural.length;
|
|
1743
1804
|
continue;
|
package/src/explain/blocks.ts
CHANGED
|
@@ -13,10 +13,9 @@
|
|
|
13
13
|
* name (and, for the one head-dependent case, the leading directive) off the
|
|
14
14
|
* statement itself.
|
|
15
15
|
*
|
|
16
|
-
* This module ships
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* traversal of its own.
|
|
16
|
+
* This module ships the single-pass, non-recursive classification primitives
|
|
17
|
+
* the path resolver needs. `explain.ts` publishes source ownership from that
|
|
18
|
+
* resolver's existing bounded traversal; `scopeBlocks` remains a shallow scan.
|
|
20
19
|
*
|
|
21
20
|
* Every string skip here goes through `segment.ts`'s `scanQuotedString` rather
|
|
22
21
|
* than a local quote-to-quote loop, so this module and the segmenter cannot
|
|
@@ -134,6 +134,11 @@ export function braceSlotTakesArray(verb: string, slot: string): boolean {
|
|
|
134
134
|
return BRACE_ARRAY_SLOTS.get(verb)?.has(slot) === true;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** Whether a `{…}` in this root-directive slot is code or script text. */
|
|
138
|
+
export function braceSlotTakesProgram(verb: string, slot: string): boolean {
|
|
139
|
+
return BRACE_PROGRAM_SLOTS.get(verb)?.has(slot) === true;
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
/** Whether the device sweep proved this root-directive slot accepts `{...}`. */
|
|
138
143
|
export function braceSlotAcceptsBrace(verb: string, slot: string): boolean {
|
|
139
144
|
return (
|
package/src/explain/catalog.ts
CHANGED
|
@@ -1317,6 +1317,11 @@ export function lookupPath(
|
|
|
1317
1317
|
return PATH_CATALOG.get(`/${segments.join("/").toLowerCase()}`);
|
|
1318
1318
|
}
|
|
1319
1319
|
|
|
1320
|
+
/** Segments in the deepest catalog path; no lookup past it can match. */
|
|
1321
|
+
const MAX_PATH_DEPTH = Math.max(
|
|
1322
|
+
...Array.from(PATH_CATALOG.keys(), (path) => path.split("/").length - 1),
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1320
1325
|
/**
|
|
1321
1326
|
* Where a published COMMAND ends inside a path run: the index of the run
|
|
1322
1327
|
* segment that is the VERB, or `null` when no prefix of `segments` is one.
|
|
@@ -1340,12 +1345,14 @@ export function lookupPath(
|
|
|
1340
1345
|
* question; what the segment IS is not.
|
|
1341
1346
|
*/
|
|
1342
1347
|
export function commandVerbIndex(segments: readonly string[]): number | null {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1348
|
+
// Bounded by the deepest row and built incrementally: a run of thousands of
|
|
1349
|
+
// bare words would otherwise re-join every prefix (#496).
|
|
1350
|
+
const limit = Math.min(segments.length, MAX_PATH_DEPTH);
|
|
1351
|
+
let path = "";
|
|
1352
|
+
for (let depth = 0; depth < limit; depth++) {
|
|
1353
|
+
path += `/${(segments[depth] as string).toLowerCase()}`;
|
|
1354
|
+
if (PATH_CATALOG.get(path)?.kind === "command") return depth;
|
|
1355
|
+
}
|
|
1349
1356
|
return null;
|
|
1350
1357
|
}
|
|
1351
1358
|
|