@tikoci/centrs 0.1.2 → 0.1.4

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 (134) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/GLOSSARY.txt +284 -0
  3. package/LICENSE +21 -0
  4. package/README.md +55 -11
  5. package/SECURITY.md +3 -3
  6. package/commands/AGENTS.md +7 -1
  7. package/commands/api/README.md +46 -27
  8. package/commands/api/examples.md +124 -6
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +376 -53
  11. package/commands/check/examples.md +390 -0
  12. package/commands/devices/README.md +177 -37
  13. package/commands/devices/examples.md +148 -0
  14. package/commands/discover/README.md +5 -11
  15. package/commands/execute/README.md +73 -26
  16. package/commands/execute/examples.md +131 -9
  17. package/commands/explain/README.md +1716 -0
  18. package/commands/explain/examples.md +606 -0
  19. package/commands/retrieve/README.md +55 -71
  20. package/commands/retrieve/examples.md +56 -9
  21. package/commands/settings/README.md +286 -22
  22. package/commands/settings/examples.md +408 -0
  23. package/commands/terminal/README.md +6 -0
  24. package/commands/transfer/README.md +57 -19
  25. package/commands/transfer/examples.md +95 -0
  26. package/docs/CLI.md +353 -0
  27. package/docs/CONSTITUTION.md +102 -26
  28. package/docs/MATRIX.md +106 -173
  29. package/docs/errors/README.md +5 -1
  30. package/docs/errors/cdb/empty-selection.md +19 -0
  31. package/docs/errors/input/incomplete-gps.md +9 -0
  32. package/docs/errors/input/invalid-altitude.md +9 -0
  33. package/docs/errors/input/invalid-bbox.md +9 -0
  34. package/docs/errors/input/invalid-coordinate.md +9 -0
  35. package/docs/errors/input/invalid-radius.md +9 -0
  36. package/docs/errors/internal/settings-failed.md +9 -0
  37. package/docs/errors/quickchr/machine-not-found.md +22 -0
  38. package/docs/errors/quickchr/machine-stopped.md +20 -0
  39. package/docs/errors/quickchr/package-unavailable.md +27 -0
  40. package/docs/errors/quickchr/unsupported-via.md +35 -0
  41. package/docs/errors/quickchr/unsupported.md +35 -0
  42. package/docs/errors/routeros/version-unsupported.md +26 -0
  43. package/docs/errors/settings/consequential-value.md +26 -0
  44. package/docs/errors/settings/invalid-boolean.md +21 -3
  45. package/docs/errors/settings/invalid-format.md +18 -3
  46. package/docs/errors/settings/invalid-integer.md +18 -3
  47. package/docs/errors/settings/invalid-timeout.md +20 -3
  48. package/docs/errors/settings/invalid-via.md +22 -3
  49. package/docs/errors/settings/reserved-key.md +29 -0
  50. package/docs/errors/settings/skip-env-file-active.md +25 -0
  51. package/docs/errors/settings/unknown-key.md +25 -0
  52. package/docs/errors/transport/provider-trust.md +29 -0
  53. package/docs/errors/usage/fanout-not-supported.md +16 -0
  54. package/docs/errors/usage/stdin-ignored.md +28 -0
  55. package/docs/errors/validation/unknown-attribute.md +37 -4
  56. package/package.json +49 -14
  57. package/src/api-fanout.ts +469 -0
  58. package/src/api.ts +171 -56
  59. package/src/cli/api.ts +156 -13
  60. package/src/cli/btest.ts +38 -4
  61. package/src/cli/common.ts +11 -0
  62. package/src/cli/devices.ts +235 -6
  63. package/src/cli/discover.ts +3 -1
  64. package/src/cli/execute.ts +129 -9
  65. package/src/cli/explain.ts +503 -0
  66. package/src/cli/mcp.ts +8 -1
  67. package/src/cli/retrieve.ts +167 -90
  68. package/src/cli/selection.ts +260 -0
  69. package/src/cli/settings.ts +286 -0
  70. package/src/cli/terminal.ts +22 -7
  71. package/src/cli/transfer.ts +192 -51
  72. package/src/cli.ts +22 -2
  73. package/src/core/envelope.ts +5 -2
  74. package/src/core/error-catalog.ts +95 -0
  75. package/src/core/fanout.ts +143 -2
  76. package/src/core/preflight.ts +306 -0
  77. package/src/core/routeros-string.ts +24 -0
  78. package/src/devices.ts +212 -20
  79. package/src/discover.ts +7 -2
  80. package/src/errors.ts +1 -0
  81. package/src/execute-fanout.ts +453 -0
  82. package/src/execute.ts +216 -68
  83. package/src/explain/arg-tokens.ts +114 -0
  84. package/src/explain/args.ts +1403 -0
  85. package/src/explain/blocks.ts +132 -0
  86. package/src/explain/brace-slots.ts +143 -0
  87. package/src/explain/brace-tokens.ts +38 -0
  88. package/src/explain/catalog.ts +1345 -0
  89. package/src/explain/coordinates.ts +329 -0
  90. package/src/explain/defects.ts +217 -0
  91. package/src/explain/is-known-menu.ts +17 -0
  92. package/src/explain/menus.ts +664 -0
  93. package/src/explain/operator-tokens.ts +310 -0
  94. package/src/explain/operators.ts +458 -0
  95. package/src/explain/path-tokens.ts +150 -0
  96. package/src/explain/pathresolve.ts +1324 -0
  97. package/src/explain/quoted-string.ts +241 -0
  98. package/src/explain/scope-brace.ts +308 -0
  99. package/src/explain/segment.ts +757 -0
  100. package/src/explain/string-tokens.ts +67 -0
  101. package/src/explain/symbol-values.ts +520 -0
  102. package/src/explain/symbols.ts +1597 -0
  103. package/src/explain/token-ranges.ts +72 -0
  104. package/src/explain/transport.ts +497 -0
  105. package/src/explain/value-tokens.ts +80 -0
  106. package/src/explain/values.ts +357 -0
  107. package/src/explain/verbs.ts +64 -0
  108. package/src/explain/verbsplit.ts +813 -0
  109. package/src/explain/write.ts +815 -0
  110. package/src/explain.ts +2152 -0
  111. package/src/index.ts +307 -1
  112. package/src/mcp/config.ts +12 -2
  113. package/src/mcp/tools.ts +137 -2
  114. package/src/protocols/adapter.ts +38 -18
  115. package/src/protocols/index.ts +11 -5
  116. package/src/protocols/mac-telnet-console.ts +61 -17
  117. package/src/resolver/cdb.ts +10 -2
  118. package/src/resolver/comment-kv.ts +58 -4
  119. package/src/resolver/config-file.ts +136 -0
  120. package/src/resolver/facts.ts +78 -0
  121. package/src/resolver/geo.ts +453 -0
  122. package/src/resolver/index.ts +65 -0
  123. package/src/resolver/mac.ts +12 -3
  124. package/src/resolver/quickchr-provider.ts +597 -0
  125. package/src/resolver/selection.ts +391 -0
  126. package/src/resolver/service-endpoint.ts +155 -0
  127. package/src/resolver/settings.ts +54 -5
  128. package/src/resolver/target.ts +21 -3
  129. package/src/retrieve-fanout.ts +239 -133
  130. package/src/retrieve.ts +120 -25
  131. package/src/settings.ts +1078 -0
  132. package/src/terminal.ts +11 -1
  133. package/src/transfer-fanout.ts +544 -0
  134. package/src/transfer.ts +133 -40
@@ -1,3 +1,4 @@
1
+ import { fanoutExitCode } from "../core/fanout.ts";
1
2
  import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
2
3
  import {
3
4
  buildExecuteErrorEnvelope,
@@ -7,11 +8,18 @@ import {
7
8
  executeOutputFormats,
8
9
  renderExecuteEnvelope,
9
10
  } from "../execute.ts";
11
+ import {
12
+ buildExecuteFanoutErrorEnvelope,
13
+ executeFanout,
14
+ renderExecuteFanoutEnvelope,
15
+ } from "../execute-fanout.ts";
10
16
  import { describeCentrs } from "../index.ts";
17
+ import { assertNoQuickchrOverrideConflict } from "../resolver/index.ts";
11
18
  import {
12
19
  type CliCommandMetadata,
13
20
  expectValue,
14
21
  renderCommandHelp,
22
+ unknownFlagError,
15
23
  } from "./common.ts";
16
24
  import {
17
25
  buildTargetSelectionTips,
@@ -21,10 +29,21 @@ import {
21
29
  missingTargetError,
22
30
  withTips,
23
31
  } from "./missing-target.ts";
32
+ import {
33
+ assertQuickchrExclusive,
34
+ buildTargetSelection,
35
+ consumeSelectionFlag,
36
+ emptySelectionFlags,
37
+ hasTargetSelector,
38
+ isFanoutMode,
39
+ type SelectionFlags,
40
+ selectionCommandOptions,
41
+ } from "./selection.ts";
24
42
 
25
43
  export const executeCommand: CliCommandMetadata = {
26
44
  name: "execute",
27
- usage: "centrs execute <target> <command> [flags]",
45
+ usage:
46
+ "centrs execute <target> <command> [flags] | centrs execute <target...> -- <command> [flags] | centrs execute --group <name> <command> [flags]",
28
47
  summary:
29
48
  "Run a RouterOS read or write command via native API, REST, or mac-telnet.",
30
49
  options: [
@@ -34,6 +53,7 @@ export const executeCommand: CliCommandMetadata = {
34
53
  description:
35
54
  "Pin the protocol selector; no silent downgrade when set. A bare MAC target defaults to mac-telnet.",
36
55
  },
56
+ ...selectionCommandOptions,
37
57
  {
38
58
  flag: "--host",
39
59
  valueName: "<host|url>",
@@ -88,9 +108,9 @@ export const executeCommand: CliCommandMetadata = {
88
108
  description: "Per-request timeout (for REST, max 60s).",
89
109
  },
90
110
  {
91
- flag: "--validate[=false]",
111
+ flag: "--validate / --no-validate",
92
112
  description:
93
- "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).",
94
114
  },
95
115
  {
96
116
  flag: "--yes",
@@ -127,11 +147,15 @@ export const executeCommand: CliCommandMetadata = {
127
147
  interface ExecuteCliArgs extends ExecuteRequest {
128
148
  help?: boolean;
129
149
  format?: ExecuteOutputFormat;
150
+ selectionFlags?: SelectionFlags;
151
+ targetPositionals?: readonly string[];
130
152
  }
131
153
 
132
154
  export function parseExecuteCliArgs(args: readonly string[]): ExecuteCliArgs {
133
155
  const parsed: ExecuteCliArgs = { command: "" };
156
+ const selectionFlags = emptySelectionFlags();
134
157
  const positional: string[] = [];
158
+ const afterDashDash: string[] = [];
135
159
  let endOfOptions = false;
136
160
 
137
161
  for (let index = 0; index < args.length; index += 1) {
@@ -144,7 +168,7 @@ export function parseExecuteCliArgs(args: readonly string[]): ExecuteCliArgs {
144
168
  // tokens like `--` / `-foo` (or a value that looks like a flag) without the
145
169
  // parser claiming them. `--` itself is consumed, not added to the command.
146
170
  if (endOfOptions) {
147
- positional.push(arg);
171
+ afterDashDash.push(arg);
148
172
  continue;
149
173
  }
150
174
  if (arg === "--") {
@@ -226,17 +250,42 @@ export function parseExecuteCliArgs(args: readonly string[]): ExecuteCliArgs {
226
250
  parsed.format = value as ExecuteOutputFormat;
227
251
  break;
228
252
  }
229
- default:
253
+ default: {
254
+ const consumed = consumeSelectionFlag(args, index, selectionFlags);
255
+ if (consumed !== null) {
256
+ index = consumed;
257
+ break;
258
+ }
230
259
  if (arg.startsWith("-")) {
231
- throw new Error(`Unknown execute flag: ${arg}`);
260
+ throw unknownFlagError("execute", arg, executeCommand.options);
232
261
  }
233
262
  positional.push(arg);
234
263
  break;
264
+ }
235
265
  }
236
266
  }
237
267
 
238
- parsed.targetInput = positional[0];
239
- parsed.command = positional.slice(1).join(" ");
268
+ parsed.selectionFlags = selectionFlags;
269
+ // Positional boundary for execute (see `docs/CONSTITUTION.md` → Target selection):
270
+ // - `--` present: targets are the positionals BEFORE it, command is AFTER it.
271
+ // - selector flag (or `--quickchr`) present, no `--`: every positional is the
272
+ // command (targets come from the flag).
273
+ // - otherwise (no `--`, no selector): legacy single-target split — first
274
+ // positional is the target, the rest is the command. Multiple positional
275
+ // targets therefore REQUIRE `--`.
276
+ const hasSelector = hasTargetSelector(selectionFlags);
277
+ if (endOfOptions) {
278
+ parsed.targetPositionals = positional;
279
+ parsed.command = afterDashDash.join(" ");
280
+ } else if (hasSelector) {
281
+ parsed.targetPositionals = [];
282
+ parsed.command = positional.join(" ");
283
+ } else {
284
+ parsed.targetPositionals =
285
+ positional[0] !== undefined ? [positional[0]] : [];
286
+ parsed.command = positional.slice(1).join(" ");
287
+ }
288
+ parsed.targetInput = parsed.targetPositionals[0];
240
289
  parsed.stdinIsTty = process.stdin.isTTY;
241
290
  return parsed;
242
291
  }
@@ -249,11 +298,41 @@ export async function runExecuteCli(args: readonly string[]): Promise<number> {
249
298
  console.log(renderCommandHelp(describeCentrs(), executeCommand));
250
299
  return 0;
251
300
  }
301
+
302
+ // Fan-out mode (a selector flag, or >1 positional target before `--`) has its
303
+ // own envelope shape, single up-front write-confirm, and granular exit code.
304
+ const selectionFlags = parsed.selectionFlags ?? emptySelectionFlags();
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
+ }
317
+ if (isFanoutMode(selectionFlags, targetPositionals.length)) {
318
+ return await runExecuteFanoutCli(
319
+ parsed,
320
+ selectionFlags,
321
+ targetPositionals,
322
+ args,
323
+ );
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
+ }
330
+
252
331
  // No positional at all: lead with the missing-target guidance (CDB picker /
253
332
  // discover) rather than the missing-command error — there is nothing to run
254
333
  // without a device. A lone positional is treated as the target, so the
255
334
  // "requires a command" path still fires for `execute <target>`.
256
- if (!parsed.targetInput) {
335
+ if (!parsed.targetInput && parsed.quickchr === undefined) {
257
336
  throw missingTargetError({
258
337
  command: "execute",
259
338
  summary: "`centrs execute` requires a <target> and a RouterOS command.",
@@ -296,12 +375,14 @@ export async function runExecuteCli(args: readonly string[]): Promise<number> {
296
375
  tips,
297
376
  );
298
377
  console.error(
378
+ // codeql[js/clear-text-logging] No secret reaches this sink. The tainted 'password' flows in only as provenance (SettingSource={kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
299
379
  renderExecuteEnvelope(envelope, format, {
300
380
  verbose: parsed?.verbose ?? false,
301
381
  }),
302
382
  );
303
383
  } else {
304
384
  console.error(
385
+ // codeql[js/clear-text-logging] Same false-positive pattern: password parsed from args never reaches error.message in the text-format catch path.
305
386
  formatCentrsErrorText(
306
387
  asCentrsError(error, {
307
388
  code: "input/invalid-command",
@@ -317,6 +398,45 @@ export async function runExecuteCli(args: readonly string[]): Promise<number> {
317
398
  }
318
399
  }
319
400
 
401
+ async function runExecuteFanoutCli(
402
+ parsed: ExecuteCliArgs,
403
+ selectionFlags: SelectionFlags,
404
+ targetPositionals: readonly string[],
405
+ args: readonly string[],
406
+ ): Promise<number> {
407
+ const selection = buildTargetSelection(selectionFlags, targetPositionals);
408
+ const format = inferExecuteFormat(args, parsed);
409
+ try {
410
+ const envelope = await executeFanout(
411
+ parsed,
412
+ selection,
413
+ Bun.env,
414
+ {},
415
+ {
416
+ concurrency: selectionFlags.concurrency,
417
+ allowAdhoc: true,
418
+ },
419
+ );
420
+ const resolvedFormat = envelope.meta.operation?.request.format ?? format;
421
+ console.log(
422
+ renderExecuteFanoutEnvelope(envelope, resolvedFormat, {
423
+ verbose: parsed.verbose ?? false,
424
+ }),
425
+ );
426
+ // Granular exit contract: 0 all-ok / 2 partial / 1 all-failed.
427
+ return fanoutExitCode(envelope);
428
+ } catch (error) {
429
+ const envelope = buildExecuteFanoutErrorEnvelope(parsed, error);
430
+ console.error(
431
+ // codeql[js/clear-text-logging] Twin of #80/#81/#83/#84. No secret logged: buildExecuteFanoutErrorEnvelope meta is {target:{},via,settings:{}}; CodeQL taints the whole request object but the builder projects the password away.
432
+ renderExecuteFanoutEnvelope(envelope, format, {
433
+ verbose: parsed.verbose ?? false,
434
+ }),
435
+ );
436
+ return 1;
437
+ }
438
+ }
439
+
320
440
  function inferExecuteFormat(
321
441
  args: readonly string[],
322
442
  parsed?: ExecuteCliArgs,