@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/GLOSSARY.txt +252 -0
  3. package/LICENSE +21 -0
  4. package/README.md +44 -12
  5. package/commands/AGENTS.md +14 -5
  6. package/commands/api/AGENTS.md +13 -1
  7. package/commands/api/README.md +56 -26
  8. package/commands/api/examples.md +46 -1
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +6 -1
  11. package/commands/discover/README.md +5 -11
  12. package/commands/execute/README.md +77 -36
  13. package/commands/execute/examples.md +120 -10
  14. package/commands/explain/README.md +2756 -0
  15. package/commands/explain/examples.md +702 -0
  16. package/commands/mcp/README.md +7 -4
  17. package/commands/mcp/examples.md +2 -2
  18. package/commands/retrieve/README.md +25 -27
  19. package/commands/retrieve/examples.md +16 -0
  20. package/commands/terminal/examples.md +7 -1
  21. package/commands/transfer/README.md +28 -20
  22. package/commands/transfer/examples.md +29 -0
  23. package/docs/CLI.md +353 -0
  24. package/docs/CONSTITUTION.md +138 -30
  25. package/docs/MATRIX.md +92 -18
  26. package/docs/errors/README.md +3 -0
  27. package/docs/errors/quickchr/machine-not-found.md +22 -0
  28. package/docs/errors/quickchr/machine-stopped.md +20 -0
  29. package/docs/errors/quickchr/package-unavailable.md +27 -0
  30. package/docs/errors/quickchr/unsupported-via.md +35 -0
  31. package/docs/errors/quickchr/unsupported.md +35 -0
  32. package/docs/errors/routeros/version-unsupported.md +26 -0
  33. package/docs/errors/transport/provider-trust.md +29 -0
  34. package/docs/errors/usage/stdin-ignored.md +28 -0
  35. package/docs/errors/validation/syntax.md +29 -4
  36. package/docs/errors/validation/unknown-attribute.md +37 -4
  37. package/package.json +56 -10
  38. package/src/api-fanout.ts +46 -18
  39. package/src/api.ts +351 -121
  40. package/src/cli/api.ts +26 -7
  41. package/src/cli/btest.ts +19 -3
  42. package/src/cli/execute.ts +25 -6
  43. package/src/cli/explain.ts +503 -0
  44. package/src/cli/retrieve.ts +33 -1
  45. package/src/cli/selection.ts +71 -11
  46. package/src/cli/settings.ts +2 -2
  47. package/src/cli/terminal.ts +1 -1
  48. package/src/cli/transfer.ts +23 -6
  49. package/src/cli.ts +15 -2
  50. package/src/core/envelope.ts +58 -2
  51. package/src/core/error-catalog.ts +42 -1
  52. package/src/core/execute-command.ts +148 -0
  53. package/src/core/inspect.ts +36 -4
  54. package/src/core/preflight.ts +306 -0
  55. package/src/core/routeros-string.ts +24 -0
  56. package/src/core/version.ts +4 -0
  57. package/src/core/yaml.ts +86 -0
  58. package/src/errors.ts +1 -0
  59. package/src/execute-fanout.ts +47 -18
  60. package/src/execute.ts +339 -307
  61. package/src/explain/arg-tokens.ts +151 -0
  62. package/src/explain/args.ts +1749 -0
  63. package/src/explain/blocks.ts +175 -0
  64. package/src/explain/brace-slots.ts +143 -0
  65. package/src/explain/brace-tokens.ts +39 -0
  66. package/src/explain/catalog.ts +1343 -0
  67. package/src/explain/coordinates.ts +488 -0
  68. package/src/explain/defects.ts +218 -0
  69. package/src/explain/escape-tokens.ts +77 -0
  70. package/src/explain/highlight-projection.ts +308 -0
  71. package/src/explain/is-known-menu.ts +17 -0
  72. package/src/explain/menus.ts +668 -0
  73. package/src/explain/operator-tokens.ts +316 -0
  74. package/src/explain/operators.ts +465 -0
  75. package/src/explain/path-tokens.ts +150 -0
  76. package/src/explain/pathresolve.ts +1569 -0
  77. package/src/explain/quoted-string.ts +331 -0
  78. package/src/explain/scope-brace.ts +461 -0
  79. package/src/explain/segment.ts +1165 -0
  80. package/src/explain/separator.ts +187 -0
  81. package/src/explain/split-owner.ts +194 -0
  82. package/src/explain/string-tokens.ts +77 -0
  83. package/src/explain/symbol-values.ts +556 -0
  84. package/src/explain/symbols.ts +1617 -0
  85. package/src/explain/token-ranges.ts +72 -0
  86. package/src/explain/transport.ts +514 -0
  87. package/src/explain/value-tokens.ts +83 -0
  88. package/src/explain/values.ts +357 -0
  89. package/src/explain/verbs.ts +64 -0
  90. package/src/explain/verbsplit.ts +955 -0
  91. package/src/explain/write.ts +856 -0
  92. package/src/explain.ts +2602 -0
  93. package/src/index.ts +225 -6
  94. package/src/mcp/server.ts +2 -1
  95. package/src/offline-gate.ts +148 -0
  96. package/src/protocols/mac-telnet-console.ts +61 -17
  97. package/src/resolver/facts.ts +1 -1
  98. package/src/resolver/index.ts +23 -4
  99. package/src/resolver/quickchr-provider.ts +597 -0
  100. package/src/resolver/selection.ts +99 -12
  101. package/src/resolver/service-endpoint.ts +155 -0
  102. package/src/resolver/target.ts +1 -1
  103. package/src/retrieve-fanout.ts +27 -13
  104. package/src/retrieve.ts +82 -77
  105. package/src/settings.ts +2 -1
  106. package/src/transfer-fanout.ts +43 -26
  107. package/src/transfer.ts +88 -45
package/src/cli/api.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  formatCentrsErrorText,
22
22
  } from "../errors.ts";
23
23
  import { describeCentrs } from "../index.ts";
24
+ import { assertNoQuickchrOverrideConflict } from "../resolver/index.ts";
24
25
  import {
25
26
  type CliCommandMetadata,
26
27
  expectValue,
@@ -36,6 +37,7 @@ import {
36
37
  withTips,
37
38
  } from "./missing-target.ts";
38
39
  import {
40
+ assertQuickchrExclusive,
39
41
  buildTargetSelection,
40
42
  consumeSelectionFlag,
41
43
  emptySelectionFlags,
@@ -57,7 +59,7 @@ export const apiCommand: CliCommandMetadata = {
57
59
  "HTTP method, default GET (case-insensitive). GET→print, PUT→add, PATCH→set, DELETE→remove, POST→run.",
58
60
  },
59
61
  {
60
- flag: "-f",
62
+ flag: "-f / --field",
61
63
  valueName: "<key=value>",
62
64
  description:
63
65
  "Body field, repeatable; assembled into the JSON body (verbatim string values).",
@@ -92,7 +94,7 @@ export const apiCommand: CliCommandMetadata = {
92
94
  {
93
95
  flag: "--raw",
94
96
  description:
95
- "Strip the envelope; emit bare RouterOS JSON. Implies `--validate=false`; does not imply `--yes`.",
97
+ "Strip the envelope; emit bare RouterOS JSON. Defaults `--validate` to false (an explicit `--validate=true` still runs the gate); does not imply `--yes`.",
96
98
  },
97
99
  {
98
100
  flag: "--yes",
@@ -168,9 +170,9 @@ export const apiCommand: CliCommandMetadata = {
168
170
  description: "Per-request timeout (for REST, max 60s).",
169
171
  },
170
172
  {
171
- flag: "--validate[=false]",
173
+ flag: "--validate / --no-validate",
172
174
  description:
173
- "Run `/console/inspect` validation before the request (default true).",
175
+ "Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted).",
174
176
  },
175
177
  {
176
178
  flag: "--format",
@@ -366,6 +368,20 @@ export async function runApiCli(args: readonly string[]): Promise<number> {
366
368
  console.log(renderCommandHelp(describeCentrs(), apiCommand));
367
369
  return 0;
368
370
  }
371
+ // Selection-shape usage errors fire before `--input` is read: a bad
372
+ // `--quickchr` combination must not block on stdin (`--input -`) or
373
+ // degrade into per-member fan-out failures. The resolver re-checks the
374
+ // override conflict for library callers.
375
+ const selectionFlags = parsed.selectionFlags ?? emptySelectionFlags();
376
+ const targetPositionals = parsed.targetPositionals ?? [];
377
+ assertQuickchrExclusive(selectionFlags, targetPositionals.length);
378
+ if (selectionFlags.quickchr.length > 0) {
379
+ assertNoQuickchrOverrideConflict(
380
+ parsed,
381
+ selectionFlags.quickchr[0] ?? "",
382
+ );
383
+ }
384
+
369
385
  // Resolve `--input` (file / stdin) before handing the body to the orchestrator.
370
386
  if (parsed.inputPath !== undefined) {
371
387
  const inputPath = parsed.inputPath;
@@ -389,8 +405,6 @@ export async function runApiCli(args: readonly string[]): Promise<number> {
389
405
  // Fan-out mode (selector flag present, or >1 positional target) has its own
390
406
  // envelope shape, guards, and granular exit code. It runs first and rejects
391
407
  // `--stream`/`--listen` + fan-out itself (single-session is exclusive).
392
- const selectionFlags = parsed.selectionFlags ?? emptySelectionFlags();
393
- const targetPositionals = parsed.targetPositionals ?? [];
394
408
  if (isFanoutMode(selectionFlags, targetPositionals.length)) {
395
409
  return await runApiFanoutCli(
396
410
  parsed,
@@ -399,6 +413,11 @@ export async function runApiCli(args: readonly string[]): Promise<number> {
399
413
  args,
400
414
  );
401
415
  }
416
+ // A single `--quickchr <name>` is single-target mode (streaming included):
417
+ // the machine name is the target, resolved from the live descriptor.
418
+ if (selectionFlags.quickchr.length === 1) {
419
+ parsed.quickchr = selectionFlags.quickchr[0];
420
+ }
402
421
 
403
422
  // Open-ended follow (`--stream`/`--listen`, or a `/listen` endpoint) consumes
404
423
  // the NDJSON envelope stream instead of a single one-shot envelope.
@@ -410,7 +429,7 @@ export async function runApiCli(args: readonly string[]): Promise<number> {
410
429
  return await runApiListenCli(parsed, args);
411
430
  }
412
431
 
413
- if (!parsed.targetInput) {
432
+ if (!parsed.targetInput && parsed.quickchr === undefined) {
414
433
  throw missingTargetError({
415
434
  command: "api",
416
435
  summary: "`centrs api` requires a <router> and an <endpoint>.",
package/src/cli/btest.ts CHANGED
@@ -23,10 +23,10 @@ import {
23
23
  renderBtestClientEnvelope,
24
24
  renderBtestServerEnvelope,
25
25
  } from "../btest.ts";
26
+ import { toYaml } from "../core/yaml.ts";
26
27
  import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
27
28
  import { describeCentrs, parseDuration } from "../index.ts";
28
29
  import type { BtestDirection, BtestProtocol } from "../protocols/btest.ts";
29
- import { toYaml } from "../retrieve.ts";
30
30
  import {
31
31
  type CliCommandMetadata,
32
32
  type CliCommandOption,
@@ -95,8 +95,14 @@ export const btestCommand: CliCommandMetadata = {
95
95
  },
96
96
  { flag: "--random-data", description: "(client) Incompressible payload." },
97
97
  {
98
- flag: "--authenticate[=false]",
99
- description: "(server) Require EC-SRP5 auth. Default true.",
98
+ flag: "--nat-mode",
99
+ description:
100
+ "(client, UDP only) Originate an outbound datagram first so server→client data can traverse NAT. Automatic when the test receives.",
101
+ },
102
+ {
103
+ flag: "--authenticate / --no-authenticate",
104
+ description:
105
+ "(server) Require EC-SRP5 auth. Default true; `--authenticate=false` also accepted.",
100
106
  },
101
107
  {
102
108
  flag: "--bind",
@@ -129,6 +135,16 @@ export const btestCommand: CliCommandMetadata = {
129
135
  valueName: "<port>",
130
136
  description: "Control port. Default 2000.",
131
137
  },
138
+ {
139
+ flag: "--cdb-file",
140
+ valueName: "<path>",
141
+ description: "(client) Read target credentials from a WinBox CDB file.",
142
+ },
143
+ {
144
+ flag: "--cdb-password",
145
+ valueName: "<password>",
146
+ description: "(client) Decrypt an encrypted WinBox CDB file.",
147
+ },
132
148
  {
133
149
  flag: "--format",
134
150
  valueName: `<${btestOutputFormats.join("|")}>`,
@@ -14,6 +14,7 @@ import {
14
14
  renderExecuteFanoutEnvelope,
15
15
  } from "../execute-fanout.ts";
16
16
  import { describeCentrs } from "../index.ts";
17
+ import { assertNoQuickchrOverrideConflict } from "../resolver/index.ts";
17
18
  import {
18
19
  type CliCommandMetadata,
19
20
  expectValue,
@@ -29,9 +30,11 @@ import {
29
30
  withTips,
30
31
  } from "./missing-target.ts";
31
32
  import {
33
+ assertQuickchrExclusive,
32
34
  buildTargetSelection,
33
35
  consumeSelectionFlag,
34
36
  emptySelectionFlags,
37
+ hasTargetSelector,
35
38
  isFanoutMode,
36
39
  type SelectionFlags,
37
40
  selectionCommandOptions,
@@ -105,9 +108,9 @@ export const executeCommand: CliCommandMetadata = {
105
108
  description: "Per-request timeout (for REST, max 60s).",
106
109
  },
107
110
  {
108
- flag: "--validate[=false]",
111
+ flag: "--validate / --no-validate",
109
112
  description:
110
- "Run RouterOS :parse and /console/inspect validation before execution (default true).",
113
+ "Run RouterOS :parse and /console/inspect validation before execution (default true; `--validate=false` also accepted).",
111
114
  },
112
115
  {
113
116
  flag: "--yes",
@@ -265,12 +268,12 @@ export function parseExecuteCliArgs(args: readonly string[]): ExecuteCliArgs {
265
268
  parsed.selectionFlags = selectionFlags;
266
269
  // Positional boundary for execute (see `docs/CONSTITUTION.md` → Target selection):
267
270
  // - `--` present: targets are the positionals BEFORE it, command is AFTER it.
268
- // - selector flag present, no `--`: every positional is the command (targets
269
- // come from the selector).
271
+ // - selector flag (or `--quickchr`) present, no `--`: every positional is the
272
+ // command (targets come from the flag).
270
273
  // - otherwise (no `--`, no selector): legacy single-target split — first
271
274
  // positional is the target, the rest is the command. Multiple positional
272
275
  // targets therefore REQUIRE `--`.
273
- const hasSelector = isFanoutMode(selectionFlags, 0);
276
+ const hasSelector = hasTargetSelector(selectionFlags);
274
277
  if (endOfOptions) {
275
278
  parsed.targetPositionals = positional;
276
279
  parsed.command = afterDashDash.join(" ");
@@ -300,6 +303,17 @@ export async function runExecuteCli(args: readonly string[]): Promise<number> {
300
303
  // own envelope shape, single up-front write-confirm, and granular exit code.
301
304
  const selectionFlags = parsed.selectionFlags ?? emptySelectionFlags();
302
305
  const targetPositionals = parsed.targetPositionals ?? [];
306
+ assertQuickchrExclusive(selectionFlags, targetPositionals.length);
307
+ // Direct connection overrides conflict with `--quickchr` globally: reject
308
+ // before fan-out dispatch so a repeated `--quickchr` gets one usage error
309
+ // (exit 1), never per-member failures. The resolver re-checks for library
310
+ // callers.
311
+ if (selectionFlags.quickchr.length > 0) {
312
+ assertNoQuickchrOverrideConflict(
313
+ parsed,
314
+ selectionFlags.quickchr[0] ?? "",
315
+ );
316
+ }
303
317
  if (isFanoutMode(selectionFlags, targetPositionals.length)) {
304
318
  return await runExecuteFanoutCli(
305
319
  parsed,
@@ -308,12 +322,17 @@ export async function runExecuteCli(args: readonly string[]): Promise<number> {
308
322
  args,
309
323
  );
310
324
  }
325
+ // A single `--quickchr <name>` is single-target mode: the machine name is
326
+ // the target (resolved from the live descriptor inside the resolver).
327
+ if (selectionFlags.quickchr.length === 1) {
328
+ parsed.quickchr = selectionFlags.quickchr[0];
329
+ }
311
330
 
312
331
  // No positional at all: lead with the missing-target guidance (CDB picker /
313
332
  // discover) rather than the missing-command error — there is nothing to run
314
333
  // without a device. A lone positional is treated as the target, so the
315
334
  // "requires a command" path still fires for `execute <target>`.
316
- if (!parsed.targetInput) {
335
+ if (!parsed.targetInput && parsed.quickchr === undefined) {
317
336
  throw missingTargetError({
318
337
  command: "execute",
319
338
  summary: "`centrs execute` requires a <target> and a RouterOS command.",