@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.
- package/CHANGELOG.md +39 -0
- package/GLOSSARY.txt +213 -0
- package/LICENSE +21 -0
- package/README.md +36 -8
- package/commands/AGENTS.md +7 -1
- package/commands/api/README.md +19 -18
- package/commands/api/examples.md +15 -0
- 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 +41 -29
- package/commands/execute/examples.md +41 -3
- package/commands/explain/README.md +1716 -0
- package/commands/explain/examples.md +606 -0
- package/commands/retrieve/README.md +25 -27
- package/commands/retrieve/examples.md +16 -0
- package/commands/transfer/README.md +28 -20
- package/commands/transfer/examples.md +29 -0
- package/docs/CLI.md +353 -0
- package/docs/CONSTITUTION.md +66 -15
- package/docs/MATRIX.md +38 -1
- 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/unknown-attribute.md +37 -4
- package/package.json +45 -10
- package/src/api-fanout.ts +29 -14
- package/src/api.ts +97 -54
- package/src/cli/api.ts +25 -6
- package/src/cli/btest.ts +18 -2
- 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/transfer.ts +23 -6
- package/src/cli.ts +15 -2
- package/src/core/envelope.ts +5 -2
- package/src/core/error-catalog.ts +40 -0
- package/src/core/preflight.ts +306 -0
- package/src/core/routeros-string.ts +24 -0
- package/src/errors.ts +1 -0
- package/src/execute-fanout.ts +29 -14
- package/src/execute.ts +114 -63
- package/src/explain/arg-tokens.ts +114 -0
- package/src/explain/args.ts +1403 -0
- package/src/explain/blocks.ts +132 -0
- package/src/explain/brace-slots.ts +143 -0
- package/src/explain/brace-tokens.ts +38 -0
- package/src/explain/catalog.ts +1345 -0
- package/src/explain/coordinates.ts +329 -0
- package/src/explain/defects.ts +217 -0
- package/src/explain/is-known-menu.ts +17 -0
- package/src/explain/menus.ts +664 -0
- package/src/explain/operator-tokens.ts +310 -0
- package/src/explain/operators.ts +458 -0
- package/src/explain/path-tokens.ts +150 -0
- package/src/explain/pathresolve.ts +1324 -0
- package/src/explain/quoted-string.ts +241 -0
- package/src/explain/scope-brace.ts +308 -0
- package/src/explain/segment.ts +757 -0
- package/src/explain/string-tokens.ts +67 -0
- package/src/explain/symbol-values.ts +520 -0
- package/src/explain/symbols.ts +1597 -0
- package/src/explain/token-ranges.ts +72 -0
- package/src/explain/transport.ts +497 -0
- package/src/explain/value-tokens.ts +80 -0
- package/src/explain/values.ts +357 -0
- package/src/explain/verbs.ts +64 -0
- package/src/explain/verbsplit.ts +813 -0
- package/src/explain/write.ts +815 -0
- package/src/explain.ts +2152 -0
- package/src/index.ts +206 -4
- 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 +26 -12
- package/src/retrieve.ts +81 -18
- package/src/transfer-fanout.ts +42 -25
- 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 (`
|
|
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
|
-
|
|
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:
|
|
97
|
+
input: SelectionResolveInput,
|
|
96
98
|
env: Record<string, string | undefined>,
|
|
97
99
|
config?: Record<string, string | undefined>,
|
|
98
|
-
) => Promise<
|
|
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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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 =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: "--
|
|
99
|
-
description:
|
|
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("|")}>`,
|
package/src/cli/execute.ts
CHANGED
|
@@ -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
|
|
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
|
|
269
|
-
// come from the
|
|
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 =
|
|
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.",
|