@tikoci/centrs 0.1.3 → 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 (90) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/GLOSSARY.txt +213 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -8
  5. package/commands/AGENTS.md +7 -1
  6. package/commands/api/README.md +19 -18
  7. package/commands/api/examples.md +15 -0
  8. package/commands/btest/README.md +21 -32
  9. package/commands/check/README.md +6 -1
  10. package/commands/discover/README.md +5 -11
  11. package/commands/execute/README.md +41 -29
  12. package/commands/execute/examples.md +41 -3
  13. package/commands/explain/README.md +1716 -0
  14. package/commands/explain/examples.md +606 -0
  15. package/commands/retrieve/README.md +25 -27
  16. package/commands/retrieve/examples.md +16 -0
  17. package/commands/transfer/README.md +28 -20
  18. package/commands/transfer/examples.md +29 -0
  19. package/docs/CLI.md +353 -0
  20. package/docs/CONSTITUTION.md +66 -15
  21. package/docs/MATRIX.md +38 -1
  22. package/docs/errors/README.md +3 -0
  23. package/docs/errors/quickchr/machine-not-found.md +22 -0
  24. package/docs/errors/quickchr/machine-stopped.md +20 -0
  25. package/docs/errors/quickchr/package-unavailable.md +27 -0
  26. package/docs/errors/quickchr/unsupported-via.md +35 -0
  27. package/docs/errors/quickchr/unsupported.md +35 -0
  28. package/docs/errors/routeros/version-unsupported.md +26 -0
  29. package/docs/errors/transport/provider-trust.md +29 -0
  30. package/docs/errors/usage/stdin-ignored.md +28 -0
  31. package/docs/errors/validation/unknown-attribute.md +37 -4
  32. package/package.json +45 -10
  33. package/src/api-fanout.ts +29 -14
  34. package/src/api.ts +97 -54
  35. package/src/cli/api.ts +25 -6
  36. package/src/cli/btest.ts +18 -2
  37. package/src/cli/execute.ts +25 -6
  38. package/src/cli/explain.ts +503 -0
  39. package/src/cli/retrieve.ts +33 -1
  40. package/src/cli/selection.ts +71 -11
  41. package/src/cli/settings.ts +2 -2
  42. package/src/cli/transfer.ts +23 -6
  43. package/src/cli.ts +15 -2
  44. package/src/core/envelope.ts +5 -2
  45. package/src/core/error-catalog.ts +40 -0
  46. package/src/core/preflight.ts +306 -0
  47. package/src/core/routeros-string.ts +24 -0
  48. package/src/errors.ts +1 -0
  49. package/src/execute-fanout.ts +29 -14
  50. package/src/execute.ts +114 -63
  51. package/src/explain/arg-tokens.ts +114 -0
  52. package/src/explain/args.ts +1403 -0
  53. package/src/explain/blocks.ts +132 -0
  54. package/src/explain/brace-slots.ts +143 -0
  55. package/src/explain/brace-tokens.ts +38 -0
  56. package/src/explain/catalog.ts +1345 -0
  57. package/src/explain/coordinates.ts +329 -0
  58. package/src/explain/defects.ts +217 -0
  59. package/src/explain/is-known-menu.ts +17 -0
  60. package/src/explain/menus.ts +664 -0
  61. package/src/explain/operator-tokens.ts +310 -0
  62. package/src/explain/operators.ts +458 -0
  63. package/src/explain/path-tokens.ts +150 -0
  64. package/src/explain/pathresolve.ts +1324 -0
  65. package/src/explain/quoted-string.ts +241 -0
  66. package/src/explain/scope-brace.ts +308 -0
  67. package/src/explain/segment.ts +757 -0
  68. package/src/explain/string-tokens.ts +67 -0
  69. package/src/explain/symbol-values.ts +520 -0
  70. package/src/explain/symbols.ts +1597 -0
  71. package/src/explain/token-ranges.ts +72 -0
  72. package/src/explain/transport.ts +497 -0
  73. package/src/explain/value-tokens.ts +80 -0
  74. package/src/explain/values.ts +357 -0
  75. package/src/explain/verbs.ts +64 -0
  76. package/src/explain/verbsplit.ts +813 -0
  77. package/src/explain/write.ts +815 -0
  78. package/src/explain.ts +2152 -0
  79. package/src/index.ts +206 -4
  80. package/src/protocols/mac-telnet-console.ts +61 -17
  81. package/src/resolver/facts.ts +1 -1
  82. package/src/resolver/index.ts +23 -4
  83. package/src/resolver/quickchr-provider.ts +597 -0
  84. package/src/resolver/selection.ts +99 -12
  85. package/src/resolver/service-endpoint.ts +155 -0
  86. package/src/resolver/target.ts +1 -1
  87. package/src/retrieve-fanout.ts +26 -12
  88. package/src/retrieve.ts +81 -18
  89. package/src/transfer-fanout.ts +42 -25
  90. package/src/transfer.ts +87 -44
package/src/api-fanout.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * `ApiEnvelope`s into the locked `FanoutData` shape (`src/core/envelope.ts`).
8
8
  *
9
9
  * Boundaries (see `commands/api/README.md`, `docs/CONSTITUTION.md`):
10
- * - The CDB is loaded ONCE (`expandCdbSelection`); a cdb member is resolved
10
+ * - The CDB is loaded ONCE (`expandSelection`); a cdb member is resolved
11
11
  * with its pre-resolved record, so `resolveCdb`'s `__default__` synthetic
12
12
  * fallback never collides ad-hoc literals.
13
13
  * - A `--default` / `__default__` member is guarded from being dialed as the
@@ -53,12 +53,12 @@ import { CentrsError, serializeCentrsError } from "./errors.ts";
53
53
  import { promptForWriteConfirmation } from "./execute.ts";
54
54
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
55
55
  import {
56
- type CdbSelectionExpansion,
57
- type CdbSelectionMember,
58
- type CdbSelectionResolveInput,
59
- expandCdbSelection,
56
+ expandSelection,
60
57
  isDefaultRecordTarget,
61
58
  loadEnvFileDefaults,
59
+ type SelectionExpansion,
60
+ type SelectionMember,
61
+ type SelectionResolveInput,
62
62
  type TargetSelection,
63
63
  } from "./resolver/index.ts";
64
64
  import { toYaml } from "./retrieve.ts";
@@ -70,6 +70,8 @@ export interface ApiSelectionSummary {
70
70
  all: boolean;
71
71
  default: boolean;
72
72
  positionals: readonly string[];
73
+ /** quickchr machine names (`--quickchr`, repeatable; exclusive of the rest). */
74
+ quickchr?: readonly string[];
73
75
  }
74
76
 
75
77
  export interface ApiFanoutOperationMeta {
@@ -92,10 +94,10 @@ export type ApiFanoutErrorEnvelope =
92
94
  export interface ApiFanoutInternals {
93
95
  expand?: (
94
96
  selection: TargetSelection,
95
- input: CdbSelectionResolveInput,
97
+ input: SelectionResolveInput,
96
98
  env: Record<string, string | undefined>,
97
99
  config?: Record<string, string | undefined>,
98
- ) => Promise<CdbSelectionExpansion>;
100
+ ) => Promise<SelectionExpansion>;
99
101
  execute?: (resolved: ResolvedApiRequest) => Promise<ApiSuccessEnvelope>;
100
102
  sleep?: (ms: number) => Promise<void>;
101
103
  }
@@ -113,6 +115,7 @@ function summarizeSelection(selection: TargetSelection): ApiSelectionSummary {
113
115
  all: selection.all,
114
116
  default: selection.default,
115
117
  positionals: selection.positionals,
118
+ quickchr: selection.quickchr,
116
119
  };
117
120
  }
118
121
 
@@ -120,7 +123,7 @@ function summarizeSelection(selection: TargetSelection): ApiSelectionSummary {
120
123
  function fanoutVia(
121
124
  request: ApiRequest,
122
125
  env: Record<string, string | undefined>,
123
- expansion: CdbSelectionExpansion,
126
+ expansion: SelectionExpansion,
124
127
  ): RouterOsProtocol {
125
128
  // Deliberately CLI/env only, not `config` — see the note above this
126
129
  // function's caller.
@@ -139,7 +142,7 @@ function fanoutVia(
139
142
  return anyNative ? "native-api" : "rest-api";
140
143
  }
141
144
 
142
- function memberTargetMeta(member: CdbSelectionMember): {
145
+ function memberTargetMeta(member: SelectionMember): {
143
146
  input?: string;
144
147
  identity?: string;
145
148
  recordIndex?: number;
@@ -151,6 +154,9 @@ function memberTargetMeta(member: CdbSelectionMember): {
151
154
  recordIndex: member.recordIndex,
152
155
  };
153
156
  }
157
+ if (member.kind === "quickchr") {
158
+ return { input: member.name, identity: member.name };
159
+ }
154
160
  return { input: member.input };
155
161
  }
156
162
 
@@ -163,7 +169,7 @@ function memberTargetMeta(member: CdbSelectionMember): {
163
169
  */
164
170
  function relabelAdhocMember(
165
171
  envelope: ApiEnvelope,
166
- member: CdbSelectionMember,
172
+ member: SelectionMember,
167
173
  ): ApiEnvelope {
168
174
  if (member.kind !== "literal") {
169
175
  return envelope;
@@ -209,7 +215,7 @@ export async function apiFanout(
209
215
  assertApiProtocolSupported(pinnedVia);
210
216
  }
211
217
 
212
- const expand = internals.expand ?? expandCdbSelection;
218
+ const expand = internals.expand ?? expandSelection;
213
219
  const expansion = await expand(
214
220
  selection,
215
221
  {
@@ -243,7 +249,7 @@ export async function apiFanout(
243
249
  const sleep = internals.sleep ?? defaultFanoutSleep;
244
250
 
245
251
  const targets = await runFanout<
246
- CdbSelectionMember,
252
+ SelectionMember,
247
253
  ResolvedApiRequest,
248
254
  ApiEnvelope
249
255
  >({
@@ -267,6 +273,15 @@ export async function apiFanout(
267
273
  { cdbResolution: member.resolution, config },
268
274
  );
269
275
  }
276
+ if (member.kind === "quickchr") {
277
+ // The live descriptor resolves here, per member — a stopped/unknown
278
+ // machine is an inner per-target failure, not an expansion error.
279
+ return resolveApiRequest(
280
+ { ...request, targetInput: undefined, quickchr: member.name },
281
+ env,
282
+ { config },
283
+ );
284
+ }
270
285
  return resolveApiRequest({ ...request, targetInput: member.input }, env, {
271
286
  config,
272
287
  });
@@ -302,7 +317,7 @@ async function assertFanoutWriteConfirmed(
302
317
  request: ApiRequest,
303
318
  summary: ApiRequestSummary,
304
319
  method: string,
305
- expansion: CdbSelectionExpansion,
320
+ expansion: SelectionExpansion,
306
321
  ): Promise<void> {
307
322
  if (!summary.write || request.yes) {
308
323
  return;
@@ -329,7 +344,7 @@ async function assertFanoutWriteConfirmed(
329
344
  interface ApiFanoutSuccessInput {
330
345
  summary: FanoutSummary;
331
346
  targets: readonly ApiEnvelope[];
332
- warnings: CdbSelectionExpansion["warnings"];
347
+ warnings: SelectionExpansion["warnings"];
333
348
  via: RouterOsProtocol | null;
334
349
  concurrency: number;
335
350
  requestSummary: ApiRequestSummary;
package/src/api.ts CHANGED
@@ -45,11 +45,13 @@ import {
45
45
  type RouterOsProtocol,
46
46
  } from "./protocols/index.ts";
47
47
  import {
48
+ assertNoQuickchrOverrideConflict,
48
49
  type CdbResolution,
49
50
  isIpTransport,
50
51
  loadEnvFileDefaults,
51
52
  parseDuration,
52
53
  parseResolvePolicy,
54
+ quickchrConnection,
53
55
  type ResolvedAuth,
54
56
  type ResolvedSetting,
55
57
  type ResolvedTarget,
@@ -57,6 +59,7 @@ import {
57
59
  resolveBooleanSetting,
58
60
  resolveCdb,
59
61
  resolveMacTarget,
62
+ resolveQuickchrTarget,
60
63
  resolveStringSetting,
61
64
  resolveTarget,
62
65
  toCoreSource,
@@ -72,6 +75,12 @@ export type ApiMethod = (typeof apiMethods)[number];
72
75
 
73
76
  export interface ApiRequest {
74
77
  targetInput?: string;
78
+ /**
79
+ * quickchr machine name (`--quickchr <name>`): resolve host/port/auth from
80
+ * the live VM descriptor instead of CDB/env (`docs/CONSTITUTION.md` →
81
+ * Resolution providers). Conflicts with host/port/username/password.
82
+ */
83
+ quickchr?: string;
75
84
  /** REST-style endpoint path, leniently normalized (`ip/address`, `/rest/ip/address`, `"ip address"`, `ip/address/*1`, `ip/address/listen`). */
76
85
  endpoint: string;
77
86
  /** HTTP method (`-X`), default `GET`, case-insensitive. */
@@ -542,17 +551,27 @@ export async function resolveApiRequest(
542
551
  const query = buildApiQuery(request);
543
552
  const proplist = buildApiProplist(request);
544
553
 
545
- const cdbResolution =
546
- override?.cdbResolution ??
547
- (await resolveCdb(
548
- {
549
- targetInput: request.targetInput,
550
- cdbFile: request.cdbFile,
551
- cdbPassword: request.cdbPassword,
552
- },
553
- env,
554
- config,
555
- ));
554
+ // A quickchr target is a named-live-provider: host/port/auth come from the
555
+ // live descriptor, bypassing CDB, `__default__`, and MAC resolution.
556
+ let quickchrResolution: Awaited<
557
+ ReturnType<typeof resolveQuickchrTarget>
558
+ > | null = null;
559
+ if (request.quickchr !== undefined) {
560
+ assertNoQuickchrOverrideConflict(request, request.quickchr);
561
+ quickchrResolution = await resolveQuickchrTarget(request.quickchr);
562
+ }
563
+ const cdbResolution = quickchrResolution
564
+ ? undefined
565
+ : (override?.cdbResolution ??
566
+ (await resolveCdb(
567
+ {
568
+ targetInput: request.targetInput,
569
+ cdbFile: request.cdbFile,
570
+ cdbPassword: request.cdbPassword,
571
+ },
572
+ env,
573
+ config,
574
+ )));
556
575
  const via = resolveApiProtocol(request, env, listen, cdbResolution, config);
557
576
  const format = resolveApiFormat(request, env, config);
558
577
  // `--raw` forces validation off (constitution: --raw waiver); otherwise the
@@ -575,48 +594,70 @@ export async function resolveApiRequest(
575
594
  cdbResolution?.overrides.timeoutMs,
576
595
  config,
577
596
  );
578
- const macResolution = isIpTransport(via.value)
579
- ? await resolveMacTarget({
580
- host: request.host,
581
- targetInput: request.targetInput,
582
- cdbTarget: cdbResolution?.target,
597
+ const macResolution =
598
+ quickchrResolution === null && isIpTransport(via.value)
599
+ ? await resolveMacTarget({
600
+ host: request.host,
601
+ targetInput: request.targetInput,
602
+ cdbTarget: cdbResolution?.target,
603
+ env,
604
+ config,
605
+ policy: parseResolvePolicy(
606
+ request.resolve ??
607
+ env["CENTRS_RESOLVE"] ??
608
+ config["CENTRS_RESOLVE"],
609
+ ),
610
+ // api has no Layer-2 transport (rest-api/native-api only), so MAC
611
+ // remediation matches retrieve's (no mac-telnet suggestion).
612
+ operation: "retrieve",
613
+ })
614
+ : undefined;
615
+ const connection = quickchrResolution
616
+ ? quickchrConnection(quickchrResolution, via.value)
617
+ : null;
618
+ const target = connection
619
+ ? connection.target
620
+ : resolveTarget(
621
+ {
622
+ targetInput: request.targetInput,
623
+ host: request.host,
624
+ port: request.port,
625
+ macResolution,
626
+ },
583
627
  env,
628
+ via.value,
629
+ cdbResolution,
584
630
  config,
585
- policy: parseResolvePolicy(
586
- request.resolve ?? env["CENTRS_RESOLVE"] ?? config["CENTRS_RESOLVE"],
587
- ),
588
- // api has no Layer-2 transport (rest-api/native-api only), so MAC
589
- // remediation matches retrieve's (no mac-telnet suggestion).
590
- operation: "retrieve",
591
- })
592
- : undefined;
593
- const target = resolveTarget(
594
- {
595
- targetInput: request.targetInput,
596
- host: request.host,
597
- port: request.port,
598
- macResolution,
599
- },
600
- env,
601
- via.value,
602
- cdbResolution,
603
- config,
604
- );
605
- const auth = resolveAuth(
606
- { username: request.username, password: request.password },
607
- env,
608
- cdbResolution,
609
- config,
610
- );
611
- const insecure = resolveBooleanSetting(
612
- request.insecure,
613
- env,
614
- "CENTRS_INSECURE",
615
- false,
616
- "insecure",
617
- cdbResolution?.overrides.insecure,
618
- config,
619
- );
631
+ );
632
+ const auth = connection
633
+ ? connection.auth
634
+ : resolveAuth(
635
+ { username: request.username, password: request.password },
636
+ env,
637
+ cdbResolution,
638
+ config,
639
+ );
640
+ // A quickchr TLS endpoint is trusted by provenance (self-signed VM cert) —
641
+ // `insecure` is forced with provider provenance and the provider-trust
642
+ // warning rides the envelope (see QuickchrConnection.insecure).
643
+ const insecure =
644
+ connection?.insecure === true
645
+ ? {
646
+ value: true,
647
+ source: {
648
+ kind: "provider" as const,
649
+ key: `quickchr:${request.quickchr}`,
650
+ },
651
+ }
652
+ : resolveBooleanSetting(
653
+ request.insecure,
654
+ env,
655
+ "CENTRS_INSECURE",
656
+ false,
657
+ "insecure",
658
+ cdbResolution?.overrides.insecure,
659
+ config,
660
+ );
620
661
 
621
662
  return {
622
663
  endpoint: request.endpoint,
@@ -644,7 +685,9 @@ export async function resolveApiRequest(
644
685
  insecure,
645
686
  yes: request.yes ?? false,
646
687
  verbose: request.verbose ?? false,
647
- warnings: cdbResolution?.warnings ?? [],
688
+ warnings: connection
689
+ ? [...connection.warnings]
690
+ : (cdbResolution?.warnings ?? []),
648
691
  };
649
692
  }
650
693
 
@@ -942,7 +985,7 @@ async function validateApiRequest(
942
985
  context: {
943
986
  path: resolved.path,
944
987
  verb: resolved.verb,
945
- attribute: missing[0],
988
+ parameter: missing[0],
946
989
  requestedAttributes: requested,
947
990
  availableAttributes: available,
948
991
  validationSource: "/console/inspect request=child+completion",
@@ -987,7 +1030,7 @@ async function validateApiRequest(
987
1030
  context: {
988
1031
  path: resolved.path,
989
1032
  verb: resolved.verb,
990
- attribute: missing[0],
1033
+ parameter: missing[0],
991
1034
  requestedAttributes: requested,
992
1035
  availableAttributes: available,
993
1036
  validationSource: "/console/inspect request=child+completion",
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).",
@@ -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
@@ -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.",