@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
@@ -13,6 +13,8 @@
13
13
  * re-couples `explain`'s browser entry to the transport graph.
14
14
  */
15
15
 
16
+ import { isKnownMenuPath } from "../explain/is-known-menu.ts";
17
+
16
18
  export interface CanonicalExecuteCommand {
17
19
  mode: "structured" | "script";
18
20
  input: string;
@@ -24,8 +26,10 @@ export interface CanonicalExecuteCommand {
24
26
 
25
27
  /**
26
28
  * The **script-vs-structured execution gate** — centrs's load-bearing
27
- * discriminator (with {@link isWriteShaped}) for which validation runs and
28
- * whether the write-confirmation prompt fires. centrs owns this gate; the shared,
29
+ * discriminator for which validation and transport path runs. Write-shape
30
+ * detection is intentionally independent: a raw-script fallback must not bypass
31
+ * confirmation merely because it cannot be represented as attributes. centrs
32
+ * owns this gate; the shared,
29
33
  * prose-tolerant, multi-command canonicalizer that `rosetta` / `lsp-routeros-ts`
30
34
  * publish is for *canonicalization*, never the structured-mode predicate —
31
35
  * widening what counts as `structured` is a product regression. Behavior is
@@ -96,10 +100,313 @@ export function canonicalizeExecuteCommand(
96
100
  }
97
101
 
98
102
  export function isWriteShaped(command: CanonicalExecuteCommand): boolean {
99
- return (
100
- command.mode === "structured" &&
101
- ["add", "set", "remove"].includes(command.verb)
102
- );
103
+ if (command.mode === "structured") {
104
+ if (FILE_OUTPUT_ATTRIBUTE in command.attributes) {
105
+ return true;
106
+ }
107
+ return !READ_ONLY_EXECUTE_VERBS.has(command.verb.toLowerCase());
108
+ }
109
+ return scriptContainsWriteShapedCommand(command.input);
110
+ }
111
+
112
+ /**
113
+ * The attribute that turns an otherwise read-only verb into a device write.
114
+ *
115
+ * RouterOS routes command output to a file on the device when `file=` is
116
+ * supplied, so a read verb creates a file: `/ip/address/export file=address`
117
+ * writes `address.rsc`, and `/file/print file=test` writes `test.txt` (both
118
+ * from the RouterOS manual — "Configuration Export" and "Scripting Tips and
119
+ * Tricks", where that idiom is the documented way to create a file and needs
120
+ * the script `write` policy). The verb allowlist cannot see that, so the
121
+ * attribute is checked independently of it.
122
+ */
123
+ const FILE_OUTPUT_ATTRIBUTE = "file";
124
+
125
+ /** True for a `file=<name>` token — the write-shaping output redirect. */
126
+ function isFileOutputToken(token: string): boolean {
127
+ return token.startsWith(`${FILE_OUTPUT_ATTRIBUTE}=`);
128
+ }
129
+
130
+ /**
131
+ * Commands that are safe to execute without write confirmation.
132
+ *
133
+ * This is deliberately an allowlist: RouterOS publishes no mutation bit for a
134
+ * command, and treating a newly introduced verb as read-only would silently
135
+ * widen the execution boundary. False positives require `--yes`; false
136
+ * negatives mutate a router. API uses the same conservative rule (only
137
+ * print/get/listen are read-classed).
138
+ */
139
+ const READ_ONLY_EXECUTE_VERBS: ReadonlySet<string> = new Set([
140
+ "check",
141
+ "check-installation",
142
+ "export",
143
+ "find",
144
+ "get",
145
+ "monitor",
146
+ "monitor-traffic",
147
+ "ping",
148
+ "print",
149
+ "scan",
150
+ "sniff",
151
+ "torch",
152
+ "traceroute",
153
+ ]);
154
+
155
+ /** Known mutators disambiguate the space-separated CLI spelling from a menu. */
156
+ const KNOWN_WRITE_EXECUTE_VERBS: ReadonlySet<string> = new Set([
157
+ "add",
158
+ "comment",
159
+ "delete",
160
+ "disable",
161
+ "edit",
162
+ "enable",
163
+ "flush",
164
+ "format",
165
+ "format-drive",
166
+ "import",
167
+ "install",
168
+ "move",
169
+ "password",
170
+ "reboot",
171
+ "redo",
172
+ "remove",
173
+ "reset",
174
+ "reset-configuration",
175
+ "reset-counters",
176
+ "reset-counters-all",
177
+ "run",
178
+ "set",
179
+ "shutdown",
180
+ "sign",
181
+ "start",
182
+ "stop",
183
+ "unset",
184
+ "upgrade",
185
+ "undo",
186
+ ]);
187
+
188
+ /** Script directives that can launch a command outside the visible statement. */
189
+ const WRITE_SHAPED_SCRIPT_DIRECTIVES: ReadonlySet<string> = new Set([
190
+ ":execute",
191
+ ]);
192
+
193
+ /**
194
+ * Script directives that are safe to run without device-write confirmation:
195
+ * output, control flow and pure value functions, all members of `explain`'s
196
+ * grounded root-cmd vocabulary (`ROOT_CMDS` in `src/explain/write.ts`). A
197
+ * directive's `do={…}` or `[…]` body is its own command head and is checked on
198
+ * its own. Deliberately absent: `:execute` and `:parse` (run built code),
199
+ * `:global` and `:set` (outlive the script), and `:import`, `:password`,
200
+ * `:undo`, `:redo` (configuration changes).
201
+ */
202
+ const READ_ONLY_SCRIPT_DIRECTIVES: ReadonlySet<string> = new Set([
203
+ ":break",
204
+ ":continue",
205
+ ":do",
206
+ ":error",
207
+ ":for",
208
+ ":foreach",
209
+ ":if",
210
+ ":len",
211
+ ":local",
212
+ ":onerror",
213
+ ":pick",
214
+ ":put",
215
+ ":return",
216
+ ":tostr",
217
+ ":typeof",
218
+ ":while",
219
+ ]);
220
+
221
+ function scriptContainsWriteShapedCommand(input: string): boolean {
222
+ for (const statement of splitUnquotedStatements(input)) {
223
+ const tokens = tokenizeRouterOsCli(statement);
224
+ // `file=` redirects output to a device file, so it is a write even under a
225
+ // read verb. Checked before the verb rules for the same reason the
226
+ // structured branch checks it independently of the allowlist.
227
+ if (
228
+ tokens.some((token) => isFileOutputToken(trimGroupingPunctuation(token)))
229
+ ) {
230
+ return true;
231
+ }
232
+ // A script may navigate to a menu on one line and use a relative verb on
233
+ // the next. Recognize grounded mutators even when no slash appears in that
234
+ // statement; quoted words were masked before tokenization.
235
+ if (
236
+ tokens.some((token) =>
237
+ KNOWN_WRITE_EXECUTE_VERBS.has(trimGroupingPunctuation(token)),
238
+ )
239
+ ) {
240
+ return true;
241
+ }
242
+ if (
243
+ tokens.some((token) =>
244
+ WRITE_SHAPED_SCRIPT_DIRECTIVES.has(token.replace(/^[{[(]+/, "")),
245
+ )
246
+ ) {
247
+ return true;
248
+ }
249
+ // Every command the statement runs must be proven read-only, not just the
250
+ // first: `:foreach i in=[/ip address find] do={/user expire-password $i}`
251
+ // reads with `find` and writes with an unlearned mutator.
252
+ if (!commandHeads(statement).every(isReadOnlyCommand)) {
253
+ return true;
254
+ }
255
+ }
256
+ return false;
257
+ }
258
+
259
+ /**
260
+ * The commands a statement runs: one at its start and one after every `[` or
261
+ * `{`, wherever that opener sits — an argument value (`in=[/ip address find]`,
262
+ * `do={/user expire-password $i}`), a sigil (`$[…]`), a concatenation
263
+ * (`"x".[…]`) or a token of its own (`do={ /user … }`). Parentheses group
264
+ * expressions, not commands, so they do not start one. Each head reads only up
265
+ * to the next opener, which begins its own head.
266
+ */
267
+ function commandHeads(statement: string): string[][] {
268
+ const heads: string[][] = [];
269
+ const starts = [0];
270
+ for (let index = 0; index < statement.length; index += 1) {
271
+ const char = statement[index];
272
+ if (char === "[" || char === "{") starts.push(index + 1);
273
+ }
274
+ for (const start of starts) {
275
+ const nextOpener = statement.slice(start).search(/[[{(]/);
276
+ const segment =
277
+ nextOpener < 0
278
+ ? statement.slice(start)
279
+ : statement.slice(start, start + nextOpener);
280
+ const tokens = tokenizeRouterOsCli(segment);
281
+ const head = tokens[0];
282
+ if (head === undefined) continue;
283
+ const words = commandWords(head, tokens.slice(1));
284
+ // A lone closer (`}` ending a multi-line block) runs nothing.
285
+ if (words.length > 0) heads.push(words);
286
+ }
287
+ return heads;
288
+ }
289
+
290
+ /**
291
+ * The path segments and verb of one command, from its head token and the bare
292
+ * words after it. The space-separated spelling puts menu segments and the verb
293
+ * in those words (`/ip firewall filter print`, `/interface 6to4 print`, `/ip
294
+ * firewall/filter print`). A word opening a bracket starts a nested command
295
+ * (`new-mutator [find]`) and a word trailing a closer ends this one (`:put [/ip
296
+ * address find]`), so the run stops at either.
297
+ */
298
+ function commandWords(head: string, following: readonly string[]): string[] {
299
+ const words: string[] = [];
300
+ const headWord = trimGroupingPunctuation(head);
301
+ if (headWord.startsWith(":")) return [headWord];
302
+ words.push(...headWord.split("/").filter(Boolean));
303
+ if (headWord !== head) return words;
304
+ for (const token of following) {
305
+ if (/^[{[(]/.test(token)) break;
306
+ const word = trimGroupingPunctuation(token);
307
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]*(?:\/[A-Za-z0-9][A-Za-z0-9-]*)*$/.test(word))
308
+ break;
309
+ words.push(...word.split("/"));
310
+ if (word !== token) break;
311
+ }
312
+ return words;
313
+ }
314
+
315
+ /**
316
+ * A command is read-only when it is a read-only script directive, or when the
317
+ * first word past the longest known menu prefix is a read verb. Walking the
318
+ * menu table rather than taking the first known verb keeps an unlearned
319
+ * command from borrowing a later argument: `/user expire-password print` names
320
+ * `expire-password` (a published command, absent from every allowlist) with
321
+ * the username `print`. The menu table is a floor, so an unknown menu fails
322
+ * closed here, as does a run of menus with no verb (navigation for a later
323
+ * relative command).
324
+ */
325
+ function isReadOnlyCommand(words: readonly string[]): boolean {
326
+ const head = words[0] ?? "";
327
+ if (head.startsWith(":")) return READ_ONLY_SCRIPT_DIRECTIVES.has(head);
328
+ let depth = 0;
329
+ while (depth < words.length && isKnownMenuPath(words.slice(0, depth + 1))) {
330
+ depth += 1;
331
+ }
332
+ const verb = words[depth];
333
+ return verb !== undefined && READ_ONLY_EXECUTE_VERBS.has(verb.toLowerCase());
334
+ }
335
+
336
+ function trimGroupingPunctuation(value: string): string {
337
+ let start = 0;
338
+ let end = value.length;
339
+ while (start < end && "{[(".includes(value[start] ?? "")) start += 1;
340
+ while (end > start && "}])".includes(value[end - 1] ?? "")) end -= 1;
341
+ return value.slice(start, end);
342
+ }
343
+
344
+ /**
345
+ * Split at real statement boundaries and blank quoted text, which does not
346
+ * execute — except a `$[…]` or `$(…)` substitution inside a double-quoted
347
+ * string, which is code: `:put "$[/system identity set name=x]"` runs the set.
348
+ * Its body is kept (with its opener, so it becomes a command head) and may nest
349
+ * strings of its own. The frame grammar is `explain`'s (`stepFrame` in
350
+ * `src/explain/quoted-string.ts`).
351
+ */
352
+ function splitUnquotedStatements(input: string): string[] {
353
+ const statements: string[] = [];
354
+ let current = "";
355
+ // `"` / `'` are string frames; `]` / `)` are the closer a code frame inside
356
+ // a string is waiting for. An empty stack is top-level code.
357
+ const frames: string[] = [];
358
+ let escaped = false;
359
+ for (let index = 0; index < input.length; index += 1) {
360
+ const char = input[index] ?? "";
361
+ const top = frames.at(-1);
362
+ const inString = top === '"' || top === "'";
363
+ if (escaped) {
364
+ current += inString ? " " : char;
365
+ escaped = false;
366
+ continue;
367
+ }
368
+ if (char === "\\") {
369
+ escaped = true;
370
+ current += inString ? " " : char;
371
+ continue;
372
+ }
373
+ if (inString) {
374
+ if (char === top) {
375
+ frames.pop();
376
+ } else if (
377
+ top === '"' &&
378
+ char === "$" &&
379
+ (input[index + 1] === "[" || input[index + 1] === "(")
380
+ ) {
381
+ const opener = input[index + 1] ?? "";
382
+ frames.push(opener === "[" ? "]" : ")");
383
+ current += ` ${opener}`;
384
+ index += 1;
385
+ continue;
386
+ }
387
+ current += " ";
388
+ continue;
389
+ }
390
+ if (char === '"' || char === "'") {
391
+ frames.push(char);
392
+ current += " ";
393
+ continue;
394
+ }
395
+ if (top !== undefined) {
396
+ // Inside a string's code frame: track nesting to find its closer.
397
+ if (char === top) frames.pop();
398
+ else if (char === "[") frames.push("]");
399
+ else if (char === "(") frames.push(")");
400
+ }
401
+ if (char === ";" || char === "\n" || char === "\r") {
402
+ statements.push(current);
403
+ current = "";
404
+ continue;
405
+ }
406
+ current += char;
407
+ }
408
+ statements.push(current);
409
+ return statements;
103
410
  }
104
411
 
105
412
  function tokenizeRouterOsCli(input: string): string[] {
@@ -30,16 +30,21 @@ import { CentrsError } from "../errors.ts";
30
30
  * protocol layer (no import cycle).
31
31
  */
32
32
  export interface InspectBackend {
33
- inspect(request: "child" | "completion", path: string): Promise<unknown[]>;
33
+ inspect(
34
+ request: "child" | "completion",
35
+ path: string,
36
+ input?: string,
37
+ ): Promise<unknown[]>;
34
38
  }
35
39
 
36
40
  /**
37
41
  * The `/console/inspect` request modes (lsp's `InspectRequest`). Only `child`
38
42
  * and `completion` are wired today (the two the {@link InspectBackend} seam
39
- * exposes); `highlight` (token-level error detection) and `syntax` (parse tree),
40
- * plus the `input`/`.query`/`.proplist` request fields, are the documented
41
- * Phase-2 extension point for value-level validation — widen the adapter seam
42
- * when adding them.
43
+ * exposes), plus `input` for completion over CLI text
44
+ * ({@link inspectWhereBooleans}); `highlight` (token-level error detection),
45
+ * `syntax` (parse tree) and the `.query`/`.proplist` request fields are the
46
+ * documented extension point for value-level validation — widen the adapter
47
+ * seam when adding them.
43
48
  */
44
49
  export type InspectRequestKind =
45
50
  | "child"
@@ -55,12 +60,20 @@ export interface InspectChildItem {
55
60
  }
56
61
 
57
62
  /** A row from `request=completion`: a candidate token for a path/argument. */
63
+ /**
64
+ * One `request=completion` row. These are the only fields RouterOS sends, on
65
+ * every build recorded from 7.9.2 to 7.25beta3 (lsp-routeros-ts
66
+ * `test-data/inspect-shapes.v*.json`). `text` is the candidate's help string,
67
+ * never its name.
68
+ */
58
69
  export interface InspectCompletionItem {
59
70
  type?: string;
60
- name?: string;
61
71
  completion?: string;
62
- value?: string;
72
+ show?: string;
73
+ style?: string;
63
74
  text?: string;
75
+ offset?: string;
76
+ preference?: string;
64
77
  }
65
78
 
66
79
  /**
@@ -99,31 +112,23 @@ export function isCommandNode(child: InspectChildItem, name: string): boolean {
99
112
  }
100
113
 
101
114
  /**
102
- * Flatten completion rows into attribute names. Reads every name-like field a
103
- * RouterOS build might populate (`completion`/`name`/`value`/`text`), strips
104
- * everything from the first `=` onward (the `name=value` completion form), and
105
- * drops blanks. Returns the names in row order **without** de-duplication or
106
- * sorting — callers that need a stable set wrap with `[...new Set(names)].sort()`
107
- * (both current callers do).
115
+ * The candidate names from completion rows: `completion` of each
116
+ * `show: "true"` row that is not `syntax-meta`. `show: "false"` rows are
117
+ * syntax completions (`[`, `(`, `$`, `"`, `*`, `<value>`), not properties
118
+ * (CHR 7.23.7, on both `print,proplist` and `get,value-name`; #380). At an
119
+ * argument boundary RouterOS also offers a shown `=` ("argument value
120
+ * separator", `style: "syntax-meta"`) on every recorded build, 7.9.2 →
121
+ * 7.25beta3; no build offers a `name=value` completion. Returns
122
+ * the names in row order **without** de-duplication or sorting; callers that
123
+ * need a stable set wrap with `[...new Set(names)].sort()`.
108
124
  */
109
125
  export function extractCompletionNames(
110
126
  rows: readonly InspectCompletionItem[],
111
127
  ): string[] {
112
128
  return rows
113
- .flatMap((row) => [row.completion, row.name, row.value, row.text])
114
- .filter((value): value is string => typeof value === "string")
115
- .map((value) => nameBeforeEquals(value).trim())
116
- .filter((value) => value.length > 0);
117
- }
118
-
119
- /**
120
- * The token before the first `=` in a RouterOS completion row. Plain string
121
- * slicing (no regex) — `value.replace(/=.*$/, "")` triggers a CodeQL
122
- * polynomial-ReDoS flag on this exported function's caller-supplied input.
123
- */
124
- function nameBeforeEquals(value: string): string {
125
- const eq = value.indexOf("=");
126
- return eq === -1 ? value : value.slice(0, eq);
129
+ .filter((row) => row.show === "true" && row.style !== "syntax-meta")
130
+ .map((row) => row.completion?.trim() ?? "")
131
+ .filter((name) => name.length > 0);
127
132
  }
128
133
 
129
134
  /** `request=child` for a token path. */
@@ -200,3 +205,36 @@ export async function inspectChildrenOrEmpty(
200
205
  throw error;
201
206
  }
202
207
  }
208
+
209
+ /**
210
+ * Which of `names` are booleans in `<menu>/print where`, asked of the device:
211
+ * completion after `where <name>=` offers exactly `no` and `yes` as `arg` rows
212
+ * for a boolean (`disabled`, `dynamic`, `running`), and `none`-styled values
213
+ * or `<value>` otherwise (enums such as `protocol` or `arp`, strings, numbers;
214
+ * CHR 7.24.5, #397). A `--query` bare name needs this: RouterOS reads
215
+ * `where disabled` as `disabled=yes` but `where comment` as "comment is set",
216
+ * and only the device knows which a property is. One request per name.
217
+ */
218
+ export async function inspectWhereBooleans(
219
+ backend: InspectBackend,
220
+ menuPath: string,
221
+ names: readonly string[],
222
+ ): Promise<Set<string>> {
223
+ const booleans = new Set<string>();
224
+ const menu = menuPath.replace(/\/$/, "");
225
+ for (const name of names) {
226
+ const rows = (await backend.inspect(
227
+ "completion",
228
+ "",
229
+ `${menu}/print where ${name}=`,
230
+ )) as InspectCompletionItem[];
231
+ const values = rows
232
+ .filter((row) => row.style !== "syntax-meta")
233
+ .map((row) => `${row.style}:${row.completion}`)
234
+ .sort();
235
+ if (values.join(" ") === "arg:no arg:yes") {
236
+ booleans.add(name);
237
+ }
238
+ }
239
+ return booleans;
240
+ }