@tikoci/centrs 0.1.3 → 0.1.6
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 +348 -0
- package/GLOSSARY.txt +252 -0
- package/LICENSE +21 -0
- package/README.md +44 -12
- package/commands/AGENTS.md +14 -5
- package/commands/api/AGENTS.md +13 -1
- package/commands/api/README.md +56 -26
- package/commands/api/examples.md +46 -1
- package/commands/btest/README.md +21 -32
- package/commands/check/README.md +6 -1
- package/commands/discover/README.md +5 -11
- package/commands/execute/README.md +77 -36
- package/commands/execute/examples.md +120 -10
- package/commands/explain/README.md +2756 -0
- package/commands/explain/examples.md +702 -0
- package/commands/mcp/README.md +7 -4
- package/commands/mcp/examples.md +2 -2
- package/commands/retrieve/README.md +25 -27
- package/commands/retrieve/examples.md +16 -0
- package/commands/terminal/examples.md +7 -1
- package/commands/transfer/README.md +28 -20
- package/commands/transfer/examples.md +29 -0
- package/docs/CLI.md +353 -0
- package/docs/CONSTITUTION.md +138 -30
- package/docs/MATRIX.md +92 -18
- package/docs/errors/README.md +3 -0
- package/docs/errors/quickchr/machine-not-found.md +22 -0
- package/docs/errors/quickchr/machine-stopped.md +20 -0
- package/docs/errors/quickchr/package-unavailable.md +27 -0
- package/docs/errors/quickchr/unsupported-via.md +35 -0
- package/docs/errors/quickchr/unsupported.md +35 -0
- package/docs/errors/routeros/version-unsupported.md +26 -0
- package/docs/errors/transport/provider-trust.md +29 -0
- package/docs/errors/usage/stdin-ignored.md +28 -0
- package/docs/errors/validation/syntax.md +29 -4
- package/docs/errors/validation/unknown-attribute.md +37 -4
- package/package.json +56 -10
- package/src/api-fanout.ts +46 -18
- package/src/api.ts +351 -121
- package/src/cli/api.ts +26 -7
- package/src/cli/btest.ts +19 -3
- package/src/cli/execute.ts +25 -6
- package/src/cli/explain.ts +503 -0
- package/src/cli/retrieve.ts +33 -1
- package/src/cli/selection.ts +71 -11
- package/src/cli/settings.ts +2 -2
- package/src/cli/terminal.ts +1 -1
- package/src/cli/transfer.ts +23 -6
- package/src/cli.ts +15 -2
- package/src/core/envelope.ts +58 -2
- package/src/core/error-catalog.ts +42 -1
- package/src/core/execute-command.ts +148 -0
- package/src/core/inspect.ts +36 -4
- package/src/core/preflight.ts +306 -0
- package/src/core/routeros-string.ts +24 -0
- package/src/core/version.ts +4 -0
- package/src/core/yaml.ts +86 -0
- package/src/errors.ts +1 -0
- package/src/execute-fanout.ts +47 -18
- package/src/execute.ts +339 -307
- package/src/explain/arg-tokens.ts +151 -0
- package/src/explain/args.ts +1749 -0
- package/src/explain/blocks.ts +175 -0
- package/src/explain/brace-slots.ts +143 -0
- package/src/explain/brace-tokens.ts +39 -0
- package/src/explain/catalog.ts +1343 -0
- package/src/explain/coordinates.ts +488 -0
- package/src/explain/defects.ts +218 -0
- package/src/explain/escape-tokens.ts +77 -0
- package/src/explain/highlight-projection.ts +308 -0
- package/src/explain/is-known-menu.ts +17 -0
- package/src/explain/menus.ts +668 -0
- package/src/explain/operator-tokens.ts +316 -0
- package/src/explain/operators.ts +465 -0
- package/src/explain/path-tokens.ts +150 -0
- package/src/explain/pathresolve.ts +1569 -0
- package/src/explain/quoted-string.ts +331 -0
- package/src/explain/scope-brace.ts +461 -0
- package/src/explain/segment.ts +1165 -0
- package/src/explain/separator.ts +187 -0
- package/src/explain/split-owner.ts +194 -0
- package/src/explain/string-tokens.ts +77 -0
- package/src/explain/symbol-values.ts +556 -0
- package/src/explain/symbols.ts +1617 -0
- package/src/explain/token-ranges.ts +72 -0
- package/src/explain/transport.ts +514 -0
- package/src/explain/value-tokens.ts +83 -0
- package/src/explain/values.ts +357 -0
- package/src/explain/verbs.ts +64 -0
- package/src/explain/verbsplit.ts +955 -0
- package/src/explain/write.ts +856 -0
- package/src/explain.ts +2602 -0
- package/src/index.ts +225 -6
- package/src/mcp/server.ts +2 -1
- package/src/offline-gate.ts +148 -0
- package/src/protocols/mac-telnet-console.ts +61 -17
- package/src/resolver/facts.ts +1 -1
- package/src/resolver/index.ts +23 -4
- package/src/resolver/quickchr-provider.ts +597 -0
- package/src/resolver/selection.ts +99 -12
- package/src/resolver/service-endpoint.ts +155 -0
- package/src/resolver/target.ts +1 -1
- package/src/retrieve-fanout.ts +27 -13
- package/src/retrieve.ts +82 -77
- package/src/settings.ts +2 -1
- package/src/transfer-fanout.ts +43 -26
- package/src/transfer.ts +88 -45
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline lexical value-shape hints (#225).
|
|
3
|
+
*
|
|
4
|
+
* These are deliberately not RouterOS types: a spelling is one axis, while the
|
|
5
|
+
* live parser, an argument schema, and runtime casting may each answer
|
|
6
|
+
* differently. The vocabulary still borrows RouterOS's type NAMES, so a shape
|
|
7
|
+
* must never be spelled in a way the device would contradict — `num` is
|
|
8
|
+
* integer-only here because RouterOS numbers are integers, and `2.2` is
|
|
9
|
+
* observed as `ip` (`2.0.0.2`), never as a decimal. `mac` is the deliberate
|
|
10
|
+
* exception: it names the CLI Reference's `macAddr` schema spelling, not a
|
|
11
|
+
* `:typeof` result (a bare MAC is observed as `str`).
|
|
12
|
+
*
|
|
13
|
+
* `op` is deliberately absent from this vocabulary (#288, a bounded #225
|
|
14
|
+
* decision). A deferred expression `(> …)` has no offline shape hint: e.g.
|
|
15
|
+
* `(>[:return 1])` is `op` and `(>{"a"=1})` is `array` at runtime, yet both
|
|
16
|
+
* parse identically — head `>` arity 1 — and `:parse` IL cannot distinguish
|
|
17
|
+
* them; only `:typeof` can. Grounded on CHR 7.21.5, 7.23.3 and 7.24rc4 in
|
|
18
|
+
* `test/fixtures/explain/operators.json` → `sweep.runtime` rows
|
|
19
|
+
* `typeof-deferred` / `deferred-array-typeof` and `sweep.opAxis` `defer-*`.
|
|
20
|
+
* Offline abstains on every such form; a live `observedType` reports the
|
|
21
|
+
* runtime type.
|
|
22
|
+
*
|
|
23
|
+
* ## The two contexts are different lexicons, not one lexicon plus a fallback
|
|
24
|
+
*
|
|
25
|
+
* An argument value is console text the command's schema interprets. An ARRAY
|
|
26
|
+
* MEMBER is parsed as an expression, and that changes the answer for whole
|
|
27
|
+
* classes of spelling. Grounded on CHR 7.23.3, reading each member's own
|
|
28
|
+
* `:typeof` and the `:parse` IL of the enclosing statement:
|
|
29
|
+
*
|
|
30
|
+
* | member | IL | `:typeof` |
|
|
31
|
+
* | ------ | -- | --------- |
|
|
32
|
+
* | `{abc}` | `$abc` | `nothing` — a bare word is a VARIABLE REFERENCE |
|
|
33
|
+
* | `{00:11:22:33:44:55}` | `$00:11:…` | `nothing` — no MAC literal exists |
|
|
34
|
+
* | `{100000w}` | `$100000w` | `nothing` — out of the time range |
|
|
35
|
+
* | `{*1}` | — | hard syntax error; `id` has no expression spelling |
|
|
36
|
+
* | `{0x10}` | `16` | `num` — hex is a number here |
|
|
37
|
+
* | `{Yes}` | `$Yes` | `nothing` — the boolean words are lower-case only |
|
|
38
|
+
* | `{"abc"}` | `abc` | `str` — quoting is how a member becomes text |
|
|
39
|
+
*
|
|
40
|
+
* So the member lexicon drops `str`-by-fallback, `mac` and `id`, adds hex, and
|
|
41
|
+
* range-checks time. Everything it cannot place is a variable reference or an
|
|
42
|
+
* expression, and an abstention says exactly that.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { isIP } from "node:net";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The closed offline vocabulary — an ENUMERATION, not an ordering.
|
|
49
|
+
*
|
|
50
|
+
* Nothing sorts by this list: {@link valueShapeHints} emits in the order it
|
|
51
|
+
* tests spellings, so reordering the members here must not change any result.
|
|
52
|
+
* `array` is emitted by the structured anchor reader rather than
|
|
53
|
+
* {@link valueShapeHints}, which receives decoded scalar values only.
|
|
54
|
+
* `op` is not a member: a deferred `(> …)` form abstains offline and is
|
|
55
|
+
* reported only as a live `observedType` (#288).
|
|
56
|
+
*/
|
|
57
|
+
export const VALUE_SHAPES = [
|
|
58
|
+
"num",
|
|
59
|
+
"ip",
|
|
60
|
+
"ip-prefix",
|
|
61
|
+
"ip6",
|
|
62
|
+
"ip6-prefix",
|
|
63
|
+
"id",
|
|
64
|
+
"time",
|
|
65
|
+
"array",
|
|
66
|
+
"mac",
|
|
67
|
+
"bool",
|
|
68
|
+
"str",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
export type ValueShape = (typeof VALUE_SHAPES)[number];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Where the value sits, because RouterOS reads the same bytes differently.
|
|
75
|
+
*
|
|
76
|
+
* `argument` is a command's `name=value` or positional operand; `array-member`
|
|
77
|
+
* is one member of a `{…}`/`(…)` literal, which the device parses as an
|
|
78
|
+
* expression (see the module header's table).
|
|
79
|
+
*/
|
|
80
|
+
export type ValueContext = "argument" | "array-member";
|
|
81
|
+
|
|
82
|
+
export interface ValueShapeOptions {
|
|
83
|
+
/** A fully enclosing quoted run always has string shape. */
|
|
84
|
+
quoted: boolean;
|
|
85
|
+
/** A named attribute makes an otherwise-generic bare string addressable. */
|
|
86
|
+
allowBareString?: boolean;
|
|
87
|
+
/** Defaults to `argument`, the only context before #225's interior work. */
|
|
88
|
+
context?: ValueContext;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Nanoseconds RouterOS can still hold in a time literal, measured rather than
|
|
93
|
+
* assumed: on 7.23.3 `9223372036s` is `time` and `9223372037s` is a variable
|
|
94
|
+
* reference, and the same cliff sits between `15250w`/`15251w` and
|
|
95
|
+
* `106751d`/`106752d`. That is exactly a signed 64-bit nanosecond count.
|
|
96
|
+
*/
|
|
97
|
+
const MAX_TIME_NS = 9_223_372_036_854_775_807n;
|
|
98
|
+
|
|
99
|
+
const UNIT_NS: Record<string, bigint> = {
|
|
100
|
+
ns: 1n,
|
|
101
|
+
us: 1_000n,
|
|
102
|
+
ms: 1_000_000n,
|
|
103
|
+
s: 1_000_000_000n,
|
|
104
|
+
m: 60_000_000_000n,
|
|
105
|
+
h: 3_600_000_000_000n,
|
|
106
|
+
d: 86_400_000_000_000n,
|
|
107
|
+
w: 604_800_000_000_000n,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function unsignedParts(value: string): bigint[] | null {
|
|
111
|
+
if (value.length > 39) return null;
|
|
112
|
+
if (!/^\d+(?:\.\d+){1,3}$/.test(value)) return null;
|
|
113
|
+
return value.split(".").map((part) => BigInt(part));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* RouterOS's short IPv4 spellings (`1.1`, `2.1.1`, and the full four parts).
|
|
118
|
+
*
|
|
119
|
+
* The shortcut fills the LOW octets — `1.1` is `1.0.0.1` and `2.1.1` is
|
|
120
|
+
* `2.1.0.1` — but it does NOT accept the class-based wide final field this
|
|
121
|
+
* originally allowed (`second <= 16777215`, `third <= 65535`, transcribed from
|
|
122
|
+
* the class-based description). Every part is a single octet on 7.23.3, in both
|
|
123
|
+
* the argument and the member position:
|
|
124
|
+
*
|
|
125
|
+
* | literal | `:typeof` | value |
|
|
126
|
+
* | ------- | --------- | ----- |
|
|
127
|
+
* | `1.255` | `ip` | `1.0.0.255` |
|
|
128
|
+
* | `1.256` | `time` | `00:00:01.256` — a decimal count of seconds |
|
|
129
|
+
* | `1.16777215` | `time` | `00:00:01.167772150`, never `1.255.255.255` |
|
|
130
|
+
* | `1.1.255` | `ip` | `1.1.0.255` |
|
|
131
|
+
* | `1.1.256` | `str` | plain text; as a member, a variable reference |
|
|
132
|
+
*
|
|
133
|
+
* A too-large field therefore does not narrow the address — it stops being an
|
|
134
|
+
* address, which is why the wide bounds produced an `ip` hint the device
|
|
135
|
+
* contradicted.
|
|
136
|
+
*/
|
|
137
|
+
function isIpv4Shortcut(value: string): boolean {
|
|
138
|
+
const parts = unsignedParts(value);
|
|
139
|
+
if (parts === null) return false;
|
|
140
|
+
return parts.every((part) => part <= 255n);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function prefixParts(
|
|
144
|
+
value: string,
|
|
145
|
+
): { address: string; prefix: number } | null {
|
|
146
|
+
const slash = value.lastIndexOf("/");
|
|
147
|
+
if (slash <= 0 || slash === value.length - 1) return null;
|
|
148
|
+
const prefixText = value.slice(slash + 1);
|
|
149
|
+
if (!/^\d+$/.test(prefixText)) return null;
|
|
150
|
+
return { address: value.slice(0, slash), prefix: Number(prefixText) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fragment COVERAGE, deliberately not unit order.
|
|
155
|
+
*
|
|
156
|
+
* On 7.23.3 and 7.24rc3 a time literal is order-independent and additive:
|
|
157
|
+
* `1s1m` is `00:01:01`, `1m1m` sums to `00:02:00`, `1h1h1h` to `03:00:00`, and
|
|
158
|
+
* `1d2d` to `3d00:00:00` — all typed `time`. Rejecting an out-of-order or
|
|
159
|
+
* repeated unit would make the hint disagree with the device, so the only
|
|
160
|
+
* question asked here is whether recognized fragments cover the whole value.
|
|
161
|
+
* Reported as an ordering defect in review of #242; the fixture's
|
|
162
|
+
* `timeOrdering` block is the refutation.
|
|
163
|
+
*/
|
|
164
|
+
function isTimeShape(value: string): boolean {
|
|
165
|
+
if (value.length > 64) return false;
|
|
166
|
+
// RouterOS also accepts its own display form: optional additive week/day
|
|
167
|
+
// fragments followed by H:M or H:M:S, with a fractional final component.
|
|
168
|
+
// The fields are normalized rather than range-rejected (`00:60:00` becomes
|
|
169
|
+
// `01:00:00`). This branch must run before the colon/address abstention guard.
|
|
170
|
+
if (/^(?:\d+[wd])*\d+:\d+(?::\d+)?(?:\.\d+)?$/.test(value)) return true;
|
|
171
|
+
// Longest suffixes must precede their one-letter prefixes: otherwise `ms`
|
|
172
|
+
// could be consumed as minutes plus seconds instead of milliseconds.
|
|
173
|
+
const matches = [...value.matchAll(/(\d+(?:\.\d+)?)(ns|us|ms|w|d|h|m|s)/g)];
|
|
174
|
+
return (
|
|
175
|
+
matches.length > 0 && matches.map((match) => match[0]).join("") === value
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** One `<digits>[.<digits>]` quantity of `unit`, in nanoseconds. */
|
|
180
|
+
function fragmentNanos(quantity: string, unit: string): bigint {
|
|
181
|
+
const scale = UNIT_NS[unit] as bigint;
|
|
182
|
+
const [whole, fraction] = quantity.split(".");
|
|
183
|
+
let total = BigInt(whole as string) * scale;
|
|
184
|
+
if (fraction !== undefined && fraction !== "")
|
|
185
|
+
total += (BigInt(fraction) * scale) / 10n ** BigInt(fraction.length);
|
|
186
|
+
return total;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Total nanoseconds of a time-shaped value, or null when it does not fit.
|
|
191
|
+
*
|
|
192
|
+
* Only the member context asks: an out-of-range literal is a plain variable
|
|
193
|
+
* reference there (`{100000w}` -> IL `$100000w`), whereas an argument value is
|
|
194
|
+
* console text the schema still reads, which is why the argument lexicon keeps
|
|
195
|
+
* the documented `100000w`-is-`str` deviation instead of abstaining.
|
|
196
|
+
*/
|
|
197
|
+
function timeNanos(value: string): bigint | null {
|
|
198
|
+
if (!isTimeShape(value)) return null;
|
|
199
|
+
const colon = value.match(
|
|
200
|
+
/^((?:\d+[wd])*)(\d+):(\d+)(?::(\d+))?(?:\.(\d+))?$/,
|
|
201
|
+
);
|
|
202
|
+
if (colon !== null) {
|
|
203
|
+
let total = 0n;
|
|
204
|
+
for (const [, quantity, unit] of (colon[1] as string).matchAll(
|
|
205
|
+
/(\d+)([wd])/g,
|
|
206
|
+
))
|
|
207
|
+
total += fragmentNanos(quantity as string, unit as string);
|
|
208
|
+
total += fragmentNanos(colon[2] as string, "h");
|
|
209
|
+
total += fragmentNanos(colon[3] as string, "m");
|
|
210
|
+
if (colon[4] !== undefined) total += fragmentNanos(colon[4], "s");
|
|
211
|
+
if (colon[5] !== undefined) total += fragmentNanos(`0.${colon[5]}`, "s");
|
|
212
|
+
return total > MAX_TIME_NS ? null : total;
|
|
213
|
+
}
|
|
214
|
+
let total = 0n;
|
|
215
|
+
for (const [, quantity, unit] of value.matchAll(
|
|
216
|
+
/(\d+(?:\.\d+)?)(ns|us|ms|w|d|h|m|s)/g,
|
|
217
|
+
)) {
|
|
218
|
+
total += fragmentNanos(quantity as string, unit as string);
|
|
219
|
+
if (total > MAX_TIME_NS) return null;
|
|
220
|
+
}
|
|
221
|
+
return total;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Shapes for ONE member of an array literal — an expression position.
|
|
226
|
+
*
|
|
227
|
+
* Every abstention here is a positive device reading rather than a gap: what
|
|
228
|
+
* is not a literal in this position is a variable reference (`{abc}`,
|
|
229
|
+
* `{100000w}`), an expression (`{1+1}`, `{a-b}`), or, for `*1`, a hard syntax
|
|
230
|
+
* error. Nested `{…}`/`(…)` members are shaped by the caller from their
|
|
231
|
+
* delimiters, not from here.
|
|
232
|
+
*/
|
|
233
|
+
function memberShapeHints(value: string, quoted: boolean): ValueShape[] {
|
|
234
|
+
if (quoted) return ["str"];
|
|
235
|
+
if (/^(?:yes|no|true|false)$/.test(value)) return ["bool"];
|
|
236
|
+
// `0x10` is 16 but `0X10` is a variable reference, and `+1` is a syntax
|
|
237
|
+
// error — the accepted spellings are exactly these two.
|
|
238
|
+
if (/^-?\d+$/.test(value) || /^-?0x[0-9a-fA-F]+$/.test(value)) return ["num"];
|
|
239
|
+
const prefix = prefixParts(value);
|
|
240
|
+
if (prefix !== null) {
|
|
241
|
+
if (prefix.prefix <= 32 && isIpv4Shortcut(prefix.address))
|
|
242
|
+
return ["ip-prefix"];
|
|
243
|
+
if (prefix.prefix <= 128 && isIP(prefix.address) === 6)
|
|
244
|
+
return ["ip6-prefix"];
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
if (isIpv4Shortcut(value)) return ["ip"];
|
|
248
|
+
if (isIP(value) === 6) return ["ip6"];
|
|
249
|
+
// A dotted decimal that is NOT a valid address falls through to SECONDS on
|
|
250
|
+
// the device (`{256.1}` is `00:04:16.100`, `{1.256}` is `00:00:01.256`).
|
|
251
|
+
// `isTimeShape` does not spell that form, so it abstains here already —
|
|
252
|
+
// deliberately, because the reading depends on the failed address attempt
|
|
253
|
+
// and an abstention never has to be unsaid.
|
|
254
|
+
if (timeNanos(value) !== null) return ["time"];
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Full six-octet CLI Reference `macAddr` spelling; shorter runs are time/text. */
|
|
259
|
+
function isMacShape(value: string): boolean {
|
|
260
|
+
return /^[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}$/.test(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* IPv6 text always carries at least two colons (`::` counts as its own pair),
|
|
265
|
+
* so a single colon is not an address attempt: `comment=foo:bar` is a string,
|
|
266
|
+
* while `1::1`, a colon time spelling, and a MAC stay conservative.
|
|
267
|
+
*/
|
|
268
|
+
function isColonAddressLike(value: string): boolean {
|
|
269
|
+
if (!value.includes("::") && (value.match(/:/g) ?? []).length < 2)
|
|
270
|
+
return false;
|
|
271
|
+
return /^[0-9a-fA-F:.]+$/.test(value);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A conservative address attempt whose failed validation must not become `str`.
|
|
276
|
+
*
|
|
277
|
+
* The FIRST slash bounds the address here, while {@link prefixParts} reads the
|
|
278
|
+
* LAST one. The asymmetry is deliberate: a two-slash value like `1.1.1.1/24/x`
|
|
279
|
+
* is not a prefix any reading can accept, and splitting on the first slash is
|
|
280
|
+
* the reading that still recognizes the address attempt and abstains.
|
|
281
|
+
*/
|
|
282
|
+
function isAddressLike(value: string): boolean {
|
|
283
|
+
const slash = value.indexOf("/");
|
|
284
|
+
const address = slash < 0 ? value : value.slice(0, slash);
|
|
285
|
+
return /^\d+(?:\.\d+){1,3}$/.test(address) || isColonAddressLike(address);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The bare comma spelling — the one place a spelling has TWO device readings.
|
|
290
|
+
*
|
|
291
|
+
* `{1;2}` is a syntax error in a command argument, but `=1,2,3` is not, and
|
|
292
|
+
* which reading it gets is decided by the ARGUMENT's type, which offline does
|
|
293
|
+
* not have. On 7.23.3 the device splits it for a list-typed attribute and keeps
|
|
294
|
+
* it whole for a single-valued one, with no syntactic difference between them:
|
|
295
|
+
*
|
|
296
|
+
* | input | `:parse` IL |
|
|
297
|
+
* | ----- | ----------- |
|
|
298
|
+
* | `servers=1.1.1.1,8.8.8.8` | `servers=1.1.1.1;8.8.8.8` — a list |
|
|
299
|
+
* | `dst-port=80,443` | `dst-port=;80;443` — a list |
|
|
300
|
+
* | `connection-state=established,related` | `…=;established;related` — a list |
|
|
301
|
+
* | `comment=a,b` | `comment=a,b` — ONE string |
|
|
302
|
+
* | `interface=ether1,ether2` | `interface=ether1,ether2` — ONE string |
|
|
303
|
+
*
|
|
304
|
+
* So a NAMED attribute carries both readings, which is what the hint list is a
|
|
305
|
+
* list for. In an expression position there is no second reading to carry:
|
|
306
|
+
* `:local x 1,2` is a two-member `array` (and so is `a,b`), and no text reading
|
|
307
|
+
* exists — which is exactly the slots where `allowBareString` is already false.
|
|
308
|
+
*
|
|
309
|
+
* No members are located for it. Whether the run splits at all is the schema's
|
|
310
|
+
* answer, so member spans here would be a guess; the `(1,2)` spelling, where
|
|
311
|
+
* the delimiters prove it, is anchored and descended into as usual.
|
|
312
|
+
*/
|
|
313
|
+
function commaSpellingHints(options: ValueShapeOptions): ValueShape[] {
|
|
314
|
+
return options.allowBareString ? ["array", "str"] : ["array"];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Infer non-authoritative shapes from one already-decoded literal.
|
|
319
|
+
*
|
|
320
|
+
* Generic bare positional words abstain: in `:local x 2.2`, `x` is a binding
|
|
321
|
+
* name, not a value. A named attribute can safely fall back to `str`, and a
|
|
322
|
+
* quoted positional is unambiguously string-shaped.
|
|
323
|
+
*/
|
|
324
|
+
export function valueShapeHints(
|
|
325
|
+
value: string,
|
|
326
|
+
options: ValueShapeOptions,
|
|
327
|
+
): ValueShape[] {
|
|
328
|
+
if (options.context === "array-member")
|
|
329
|
+
return memberShapeHints(value, options.quoted);
|
|
330
|
+
if (options.quoted) return ["str"];
|
|
331
|
+
if (value.includes(",")) return commaSpellingHints(options);
|
|
332
|
+
|
|
333
|
+
const hints: ValueShape[] = [];
|
|
334
|
+
const prefix = prefixParts(value);
|
|
335
|
+
if (prefix !== null) {
|
|
336
|
+
if (prefix.prefix <= 32 && isIpv4Shortcut(prefix.address))
|
|
337
|
+
hints.push("ip-prefix");
|
|
338
|
+
else if (prefix.prefix <= 128 && isIP(prefix.address) === 6)
|
|
339
|
+
hints.push("ip6-prefix");
|
|
340
|
+
if (hints.length > 0 || isAddressLike(value)) return hints;
|
|
341
|
+
return options.allowBareString ? ["str"] : [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (/^(?:yes|no|true|false)$/.test(value)) hints.push("bool");
|
|
345
|
+
// Integer-only: a dotted decimal is an IPv4 shortcut on the device, not a
|
|
346
|
+
// number, so `num` here would contradict the observed type it is named after.
|
|
347
|
+
if (/^-?\d+$/.test(value)) hints.push("num");
|
|
348
|
+
if (/^\*[0-9a-fA-F]+$/.test(value)) hints.push("id");
|
|
349
|
+
if (isIpv4Shortcut(value)) hints.push("ip");
|
|
350
|
+
else if (isIP(value) === 6) hints.push("ip6");
|
|
351
|
+
if (isTimeShape(value)) hints.push("time");
|
|
352
|
+
if (isMacShape(value)) hints.push("mac");
|
|
353
|
+
|
|
354
|
+
if (hints.length === 0 && options.allowBareString && !isAddressLike(value))
|
|
355
|
+
hints.push("str");
|
|
356
|
+
return hints;
|
|
357
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frozen console-verb vocabulary for `explain`.
|
|
3
|
+
*
|
|
4
|
+
* A leaf DATA module with no imports, exactly like `menus.ts`, and for the same
|
|
5
|
+
* reason: two modules on opposite sides of the dependency edge both need to ask
|
|
6
|
+
* "is this word a verb?", and they must ask the SAME object. `verbsplit.ts`
|
|
7
|
+
* decides the verb/menu boundary (Q6) and already imports `pathresolve.ts` for
|
|
8
|
+
* the document context walk; `pathresolve.ts`'s R9 (#211) needs the vocabulary
|
|
9
|
+
* to keep `menuNavPath` from claiming `/ip address print` as navigation. Holding
|
|
10
|
+
* the set here rather than in `verbsplit.ts` keeps that a shared leaf instead of
|
|
11
|
+
* an import cycle. `verbsplit.ts` re-exports both names, so the public surface
|
|
12
|
+
* is unchanged.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* FROZEN universal-verb vocabulary: thirteen console CRUD verbs that exist at
|
|
17
|
+
* essentially every configuration menu and have not changed across 7.x. This is
|
|
18
|
+
* deliberately NOT a schema — no menu structure, no per-menu verb list, no
|
|
19
|
+
* version. Do not tune it against scoring output; the whole point is to price a
|
|
20
|
+
* fixed list honestly (it is what ratifies decision 3, "no offline schema
|
|
21
|
+
* snapshot").
|
|
22
|
+
*
|
|
23
|
+
* The entries are lower-case, and the two sides of the R9 seam — `verbsplit`'s
|
|
24
|
+
* `splitRun`/`resolveVerb` and `pathresolve`'s `menuNavPath` — both look up the
|
|
25
|
+
* RAW run-token name, so both are case-sensitive. That agreement is the only
|
|
26
|
+
* thing R9 needs; what matters is that the two use the SAME normalization, not
|
|
27
|
+
* which one they pick.
|
|
28
|
+
*
|
|
29
|
+
* Elsewhere in `explain` the strategy differs: `write.ts`'s `classifyVerb`
|
|
30
|
+
* lower-cases before consulting `READ_VERBS`/`WRITE_VERBS` (a superset of this
|
|
31
|
+
* set — it also carries `monitor`, `reset`, `reset-counters`,
|
|
32
|
+
* `reset-counters-all`), and `menus.ts` lower-cases path segments. So an
|
|
33
|
+
* upper-cased verb is read differently by different modules. That divergence
|
|
34
|
+
* predates R9 and is NOT corpus-reachable — over the whole 911-script frozen
|
|
35
|
+
* corpus, zero run tokens match a verb only after lower-casing — and no claim
|
|
36
|
+
* is made here about which reading the device would give `PRINT`, because that
|
|
37
|
+
* has not been checked on a router.
|
|
38
|
+
*/
|
|
39
|
+
export const VERBS: ReadonlySet<string> = new Set([
|
|
40
|
+
"add",
|
|
41
|
+
"comment",
|
|
42
|
+
"disable",
|
|
43
|
+
"edit",
|
|
44
|
+
"enable",
|
|
45
|
+
"export",
|
|
46
|
+
"find",
|
|
47
|
+
"get",
|
|
48
|
+
"move",
|
|
49
|
+
"print",
|
|
50
|
+
"remove",
|
|
51
|
+
"set",
|
|
52
|
+
"unset",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Root menus idiomatically written with a `:` sigil that take a SUB-MENU rather
|
|
57
|
+
* than a positional operand. Measured, not guessed: over the frozen dev split,
|
|
58
|
+
* directive statements with a 2+ token run split 2,088 at run[0] against 18 at
|
|
59
|
+
* run[1], and every one of the 18 is `:log <level>`. `log` has been a root
|
|
60
|
+
* directory since 7.9.2 (checked against the 7.9.2 / 7.20.8 / 7.23rc1 trees),
|
|
61
|
+
* so the exception is version-stable. It is one word, not a schema — a second
|
|
62
|
+
* entry would mean decision 3's fixed-vocabulary claim needs re-examining.
|
|
63
|
+
*/
|
|
64
|
+
export const SUBMENU_DIRECTIVES: ReadonlySet<string> = new Set(["log"]);
|