@tikoci/centrs 0.1.8 → 0.1.9-next.22
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 +216 -31
- package/GLOSSARY.txt +8 -0
- package/README.md +16 -8
- package/SECURITY.md +11 -3
- package/commands/api/README.md +44 -15
- package/commands/api/examples.md +44 -6
- package/commands/execute/README.md +218 -7
- package/commands/execute/examples.md +278 -1
- package/commands/explain/README.md +2 -1
- package/commands/mcp/README.md +11 -0
- package/commands/mcp/examples.md +6 -0
- package/commands/retrieve/README.md +32 -14
- package/commands/retrieve/examples.md +10 -6
- package/commands/transfer/examples.md +9 -0
- package/docs/CLI.md +10 -8
- package/docs/CONSTITUTION.md +54 -3
- package/docs/MATRIX.md +20 -3
- package/docs/errors/cdb/empty-group.md +19 -3
- package/docs/errors/cdb/empty-selection.md +9 -3
- package/docs/errors/cmr/caller-policy-not-enforced.md +46 -0
- package/docs/errors/cmr/cancelled.md +15 -0
- package/docs/errors/cmr/client-disconnected.md +19 -0
- package/docs/errors/cmr/client-script-not-validated.md +22 -0
- package/docs/errors/cmr/console-only.md +29 -0
- package/docs/errors/cmr/empty-selection.md +33 -0
- package/docs/errors/cmr/membership-changed.md +20 -0
- package/docs/errors/cmr/not-a-controller.md +26 -0
- package/docs/errors/cmr/outcome-unknown.md +29 -0
- package/docs/errors/cmr/output-truncated.md +16 -0
- package/docs/errors/cmr/policy-denied.md +31 -0
- package/docs/errors/cmr/rule-selects-every-device.md +32 -0
- package/docs/errors/cmr/selection-includes-controller.md +27 -0
- package/docs/errors/cmr/selection-incomplete.md +18 -0
- package/docs/errors/cmr/validation-incomplete.md +23 -0
- package/docs/errors/routeros/version-unsupported.md +4 -0
- package/docs/errors/transport/write-outcome-unknown.md +24 -0
- package/docs/errors/usage/flag-after-separator.md +34 -0
- package/docs/errors/usage/missing-labels.md +19 -0
- package/package.json +7 -5
- package/src/api-fanout.ts +18 -2
- package/src/api.ts +230 -71
- package/src/btest.ts +7 -2
- package/src/cli/api.ts +30 -16
- package/src/cli/common.ts +180 -1
- package/src/cli/devices.ts +0 -2
- package/src/cli/discover.ts +0 -1
- package/src/cli/execute.ts +176 -21
- package/src/cli/explain.ts +2 -0
- package/src/cli/mcp.ts +0 -1
- package/src/cli/retrieve.ts +40 -18
- package/src/cli/settings.ts +0 -3
- package/src/cli/terminal.ts +0 -2
- package/src/cli/transfer.ts +10 -8
- package/src/cli/version.ts +87 -0
- package/src/cli.ts +5 -3
- package/src/core/cmr-fleet.ts +486 -0
- package/src/core/cmr.ts +182 -0
- package/src/core/envelope.ts +46 -1
- package/src/core/error-catalog.ts +95 -2
- package/src/core/execute-command.ts +144 -19
- package/src/core/fanout.ts +207 -14
- package/src/errors.ts +3 -1
- package/src/execute-cmr.ts +1442 -0
- package/src/execute-fanout.ts +15 -1
- package/src/execute.ts +173 -40
- package/src/index.ts +35 -1
- package/src/mcp/safety.ts +11 -0
- package/src/mcp/tools.ts +4 -1
- package/src/protocols/adapter.ts +54 -12
- package/src/resolver/cdb.ts +13 -1
- package/src/resolver/index.ts +4 -0
- package/src/resolver/selection.ts +37 -1
- package/src/resolver/target.ts +96 -8
- package/src/retrieve-fanout.ts +4 -1
- package/src/retrieve-follow.ts +29 -33
- package/src/retrieve-sample.ts +14 -8
- package/src/retrieve-stream.ts +38 -6
- package/src/retrieve.ts +73 -59
- package/src/transfer-fanout.ts +8 -1
- package/src/transfer.ts +15 -19
package/src/retrieve-sample.ts
CHANGED
|
@@ -17,15 +17,19 @@ import {
|
|
|
17
17
|
buildRetrieveErrorEnvelopeFromResolved,
|
|
18
18
|
executeRetrieve,
|
|
19
19
|
type ResolvedRetrieveRequest,
|
|
20
|
-
type RetrieveEnvelope,
|
|
21
20
|
type RetrieveRequest,
|
|
22
21
|
type RetrieveSampleStopReason,
|
|
23
22
|
type RetrieveSampleSummary,
|
|
23
|
+
type RetrieveStreamLine,
|
|
24
24
|
type RetrieveSuccessEnvelope,
|
|
25
25
|
resolveRetrieveRequest,
|
|
26
26
|
validateRetrieveRead,
|
|
27
27
|
} from "./retrieve.ts";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
failedStreamEnvelope,
|
|
30
|
+
streamEnvelope,
|
|
31
|
+
streamLineEnvelope,
|
|
32
|
+
} from "./retrieve-stream.ts";
|
|
29
33
|
|
|
30
34
|
export interface RetrieveSampleOptions {
|
|
31
35
|
/** Ctrl-C: ends the sample with `stopReason: "interrupted"`. */
|
|
@@ -33,16 +37,17 @@ export interface RetrieveSampleOptions {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
|
-
* Sample one menu: yields a `sample` line per
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* envelope without a
|
|
40
|
+
* Sample one menu: yields `started` (the full meta), a `sample` line per
|
|
41
|
+
* complete read (only its `meta.operation`), then exactly one summary
|
|
42
|
+
* (successful, or an error envelope carrying the partial count). Errors before
|
|
43
|
+
* the first read (resolution, validation) yield one error envelope without a
|
|
44
|
+
* summary.
|
|
40
45
|
*/
|
|
41
46
|
export async function* retrieveSample(
|
|
42
47
|
request: RetrieveRequest,
|
|
43
48
|
env: Record<string, string | undefined> = Bun.env,
|
|
44
49
|
options: RetrieveSampleOptions = {},
|
|
45
|
-
): AsyncGenerator<
|
|
50
|
+
): AsyncGenerator<RetrieveStreamLine, void, void> {
|
|
46
51
|
if (request.wait !== undefined) {
|
|
47
52
|
yield buildRetrieveErrorEnvelope(
|
|
48
53
|
request,
|
|
@@ -126,6 +131,7 @@ export async function* retrieveSample(
|
|
|
126
131
|
sample.durationMs,
|
|
127
132
|
);
|
|
128
133
|
}
|
|
134
|
+
yield streamEnvelope(resolved, validation, null, { kind: "started" });
|
|
129
135
|
try {
|
|
130
136
|
while (stopReason === undefined) {
|
|
131
137
|
const sentAt = Date.now();
|
|
@@ -141,7 +147,7 @@ export async function* retrieveSample(
|
|
|
141
147
|
// A read still in flight at the stop is not a complete sample.
|
|
142
148
|
if (result === undefined) break;
|
|
143
149
|
samples += 1;
|
|
144
|
-
yield
|
|
150
|
+
yield streamLineEnvelope(result.data, {
|
|
145
151
|
kind: "sample",
|
|
146
152
|
index: samples,
|
|
147
153
|
at: new Date(sentAt).toISOString(),
|
package/src/retrieve-stream.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Line helpers shared by the two streaming forms of `retrieve`: `--follow`
|
|
3
3
|
* (`src/retrieve-follow.ts`) and `--sample` (`src/retrieve-sample.ts`). Each
|
|
4
4
|
* line is one envelope whose `meta.operation.stream` says what it is
|
|
5
|
-
* (`commands/retrieve/README.md` → Follow, Sample).
|
|
5
|
+
* (`commands/retrieve/README.md` → Follow, Sample). A stream opens with a
|
|
6
|
+
* full-meta `started` line and ends with a full-meta `summary`; the data lines
|
|
7
|
+
* between carry only `meta.operation` (#462, #473).
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { EnvelopeValidationMeta, Tip } from "./core/envelope.ts";
|
|
@@ -11,15 +13,16 @@ import {
|
|
|
11
13
|
buildRetrieveErrorEnvelopeFromResolved,
|
|
12
14
|
metaFromResolved,
|
|
13
15
|
type ResolvedRetrieveRequest,
|
|
14
|
-
type RetrieveEnvelope,
|
|
15
16
|
type RetrieveErrorEnvelope,
|
|
16
17
|
type RetrieveOutputFormat,
|
|
18
|
+
type RetrieveStreamLine,
|
|
19
|
+
type RetrieveStreamLineEnvelope,
|
|
17
20
|
type RetrieveStreamMeta,
|
|
18
21
|
type RetrieveSuccessEnvelope,
|
|
19
22
|
retrieveRequestSummary,
|
|
20
23
|
} from "./retrieve.ts";
|
|
21
24
|
|
|
22
|
-
/**
|
|
25
|
+
/** A full-meta stream line (`started`, `synced`, `summary`): `data` plus its marker. */
|
|
23
26
|
export function streamEnvelope(
|
|
24
27
|
resolved: ResolvedRetrieveRequest,
|
|
25
28
|
validation: EnvelopeValidationMeta,
|
|
@@ -45,6 +48,27 @@ export function streamEnvelope(
|
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** A `frame` or `sample` line: only its `meta.operation` (#462, #473). */
|
|
52
|
+
export function streamLineEnvelope(
|
|
53
|
+
data: unknown,
|
|
54
|
+
stream: RetrieveStreamMeta,
|
|
55
|
+
tips: Tip[] = [],
|
|
56
|
+
): RetrieveStreamLineEnvelope {
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
data,
|
|
60
|
+
warnings: [],
|
|
61
|
+
tips,
|
|
62
|
+
meta: {
|
|
63
|
+
operation: {
|
|
64
|
+
kind: "data",
|
|
65
|
+
objectCount: data === null ? 0 : Array.isArray(data) ? data.length : 1,
|
|
66
|
+
stream,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
48
72
|
/**
|
|
49
73
|
* The failed summary that ends a stream after it started: the error, plus the
|
|
50
74
|
* partial counts as a `summary` marker. `stopReason` is `routeros-error` when
|
|
@@ -77,17 +101,19 @@ export function failedStreamEnvelope(
|
|
|
77
101
|
* One stream envelope as one line: compact NDJSON for every structured format
|
|
78
102
|
* (a multi-line YAML document cannot be a stream line), and a short row for
|
|
79
103
|
* `text`. Router text is JSON-escaped so it cannot carry terminal control
|
|
80
|
-
* bytes.
|
|
104
|
+
* bytes. In text, only `frame` and `sample` rows are data
|
|
105
|
+
* ({@link isStreamDataLine}); the CLI writes the rest to stderr, and an empty
|
|
106
|
+
* string (a `started` line without tips) prints nothing.
|
|
81
107
|
*/
|
|
82
108
|
export function renderRetrieveStreamLine(
|
|
83
|
-
envelope:
|
|
109
|
+
envelope: RetrieveStreamLine,
|
|
84
110
|
format: RetrieveOutputFormat,
|
|
85
111
|
): string {
|
|
86
112
|
if (format !== "text") return JSON.stringify(envelope);
|
|
87
113
|
if (!envelope.ok) return formatCentrsErrorLine(envelope.error);
|
|
88
114
|
const stream = envelope.meta.operation?.stream;
|
|
89
115
|
switch (stream?.kind) {
|
|
90
|
-
case "
|
|
116
|
+
case "started":
|
|
91
117
|
return envelope.tips
|
|
92
118
|
.map((tip) => `tip: ${tip.message}${tip.fix ? ` ${tip.fix}` : ""}`)
|
|
93
119
|
.join("\n");
|
|
@@ -110,6 +136,12 @@ export function renderRetrieveStreamLine(
|
|
|
110
136
|
}
|
|
111
137
|
}
|
|
112
138
|
|
|
139
|
+
/** Whether a stream line is a data row: text stdout carries only these (#473). */
|
|
140
|
+
export function isStreamDataLine(envelope: RetrieveStreamLine): boolean {
|
|
141
|
+
const kind = envelope.ok ? envelope.meta.operation?.stream?.kind : undefined;
|
|
142
|
+
return kind === "frame" || kind === "sample";
|
|
143
|
+
}
|
|
144
|
+
|
|
113
145
|
function escaped(text: string): string {
|
|
114
146
|
return JSON.stringify(text).slice(1, -1);
|
|
115
147
|
}
|
package/src/retrieve.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CommonSettingsMeta,
|
|
6
6
|
SettingSource as CoreSettingSource,
|
|
7
7
|
EnvelopeValidationMeta,
|
|
8
|
+
StreamLineEnvelope,
|
|
8
9
|
ValidationTrace,
|
|
9
10
|
} from "./core/envelope.ts";
|
|
10
11
|
import { notReachedReason } from "./core/envelope.ts";
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
type ResolvedAuth,
|
|
52
53
|
type ResolvedSetting,
|
|
53
54
|
type ResolvedTarget,
|
|
55
|
+
redactTargetInput,
|
|
54
56
|
resolveAuth,
|
|
55
57
|
resolveBooleanSetting,
|
|
56
58
|
resolveCdb,
|
|
@@ -230,15 +232,20 @@ export interface RetrieveSampleSummary {
|
|
|
230
232
|
* (`commands/retrieve/README.md` → Follow, Sample). A `frame` is one row
|
|
231
233
|
* change: `upsert` carries the latest full row, `removed` carries
|
|
232
234
|
* `data: null`; `id` is the row identity even when the projection leaves `.id`
|
|
233
|
-
* out of `data`.
|
|
235
|
+
* out of `data`. `observedAt` is when centrs received the reply the frame is
|
|
236
|
+
* built from (host clock, ISO 8601; a replayed bootstrap change keeps its
|
|
237
|
+
* arrival time). A `sample` is one complete read; `at` is when it was sent
|
|
234
238
|
* (host clock) and `readMs` how long it took. The two summaries are told apart
|
|
235
|
-
* by their counts (`frames` vs `samples`).
|
|
239
|
+
* by their counts (`frames` vs `samples`). `started` is the first line of
|
|
240
|
+
* every stream that got past validation: the full meta, `data: null`, and any
|
|
241
|
+
* advice in `tips` (#473).
|
|
236
242
|
*/
|
|
237
243
|
export type RetrieveStreamMeta =
|
|
238
|
-
| { kind: "
|
|
244
|
+
| { kind: "started" }
|
|
239
245
|
| {
|
|
240
246
|
kind: "frame";
|
|
241
247
|
index: number;
|
|
248
|
+
observedAt: string;
|
|
242
249
|
phase: "snapshot" | "live";
|
|
243
250
|
change: "upsert" | "removed";
|
|
244
251
|
id: string;
|
|
@@ -264,6 +271,23 @@ export interface RetrieveOperationMeta {
|
|
|
264
271
|
}
|
|
265
272
|
|
|
266
273
|
export type RetrieveEnvelope = CentrsEnvelope<unknown, RetrieveOperationMeta>;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A `--follow` frame or `--sample` sample line: only its own `meta.operation`
|
|
277
|
+
* (#462, #473). The rest of the meta is on the stream's `started` and
|
|
278
|
+
* `summary` lines (and `synced` for `--follow`).
|
|
279
|
+
*/
|
|
280
|
+
export type RetrieveStreamLineEnvelope = StreamLineEnvelope<
|
|
281
|
+
unknown,
|
|
282
|
+
Pick<RetrieveOperationMeta, "kind" | "objectCount"> & {
|
|
283
|
+
stream: RetrieveStreamMeta;
|
|
284
|
+
}
|
|
285
|
+
>;
|
|
286
|
+
|
|
287
|
+
/** One `retrieve --follow` or `--sample` line: a full envelope or a data line. */
|
|
288
|
+
export type RetrieveStreamLine = RetrieveEnvelope | RetrieveStreamLineEnvelope;
|
|
289
|
+
/** One `retrieve --follow` line. */
|
|
290
|
+
export type RetrieveFollowLine = RetrieveStreamLine;
|
|
267
291
|
export type RetrieveSuccessEnvelope = CentrsSuccessEnvelope<
|
|
268
292
|
unknown,
|
|
269
293
|
RetrieveOperationMeta
|
|
@@ -352,7 +376,7 @@ export async function retrieve(
|
|
|
352
376
|
summary:
|
|
353
377
|
"`follow` yields a stream of envelopes instead of one `retrieve` result.",
|
|
354
378
|
remediation:
|
|
355
|
-
"Consume it with `retrieveFollow()` (library) or `centrs retrieve … --follow` (CLI): snapshot frames, `synced`, live frames, then a summary.",
|
|
379
|
+
"Consume it with `retrieveFollow()` (library) or `centrs retrieve … --follow` (CLI): `started`, snapshot frames, `synced`, live frames, then a summary.",
|
|
356
380
|
context: { capability: "follow" },
|
|
357
381
|
});
|
|
358
382
|
}
|
|
@@ -362,7 +386,7 @@ export async function retrieve(
|
|
|
362
386
|
summary:
|
|
363
387
|
"`sample` yields a stream of envelopes instead of one `retrieve` result.",
|
|
364
388
|
remediation:
|
|
365
|
-
"Consume it with `retrieveSample()` (library) or `centrs retrieve … --sample <interval>` (CLI): one line per complete read, then a summary.",
|
|
389
|
+
"Consume it with `retrieveSample()` (library) or `centrs retrieve … --sample <interval>` (CLI): `started`, one line per complete read, then a summary.",
|
|
366
390
|
context: { capability: "sample" },
|
|
367
391
|
});
|
|
368
392
|
}
|
|
@@ -547,7 +571,7 @@ export function buildRetrieveErrorEnvelope(
|
|
|
547
571
|
warnings: [],
|
|
548
572
|
tips: [],
|
|
549
573
|
meta: {
|
|
550
|
-
target: { input: request.targetInput },
|
|
574
|
+
target: { input: redactTargetInput(request.targetInput) },
|
|
551
575
|
via: requestedVia,
|
|
552
576
|
settings: {},
|
|
553
577
|
},
|
|
@@ -653,7 +677,7 @@ export function renderRetrieveEnvelope(
|
|
|
653
677
|
case "text":
|
|
654
678
|
return (
|
|
655
679
|
(envelope.ok
|
|
656
|
-
? renderRetrieveSuccessText(envelope
|
|
680
|
+
? renderRetrieveSuccessText(envelope)
|
|
657
681
|
: formatCentrsErrorText(envelope.error, options)) +
|
|
658
682
|
(envelope.meta.operation?.wait
|
|
659
683
|
? `\nwait: ${envelope.meta.operation.wait.stopReason}; ${envelope.meta.operation.wait.elapsedMs}ms; ${envelope.meta.operation.wait.attempts} attempt(s), ${envelope.meta.operation.wait.observations} observation(s)`
|
|
@@ -664,64 +688,42 @@ export function renderRetrieveEnvelope(
|
|
|
664
688
|
}
|
|
665
689
|
}
|
|
666
690
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
): string {
|
|
671
|
-
const lines: string[] = [];
|
|
672
|
-
const verbose = options.verbose ?? false;
|
|
673
|
-
const meta = envelope.meta;
|
|
674
|
-
const operation = meta.operation;
|
|
675
|
-
|
|
676
|
-
if (verbose) {
|
|
677
|
-
const target = meta.target;
|
|
678
|
-
lines.push(`target: ${target.input ?? target.host} -> ${target.baseUrl}`);
|
|
679
|
-
lines.push(`via: ${meta.via}`);
|
|
680
|
-
lines.push(
|
|
681
|
-
`sources: via=${formatCoreSource(meta.settings.via)}, host=${formatCoreSource(
|
|
682
|
-
meta.settings.host,
|
|
683
|
-
)}, port=${formatCoreSource(meta.settings.port)}, timeout=${formatCoreSource(
|
|
684
|
-
meta.settings.timeoutMs,
|
|
685
|
-
)}, format=${formatCoreSource(meta.settings.format)}, validate=${formatCoreSource(
|
|
686
|
-
meta.settings.validate,
|
|
687
|
-
)}`,
|
|
688
|
-
);
|
|
689
|
-
if (meta.validation?.enabled) {
|
|
690
|
-
lines.push(`validation: ${meta.validation.source}`);
|
|
691
|
-
}
|
|
692
|
-
lines.push("");
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (operation?.kind === "attributes") {
|
|
691
|
+
/** Text mode renders only the data; the CLI writes the rest to stderr (#470). */
|
|
692
|
+
function renderRetrieveSuccessText(envelope: RetrieveSuccessEnvelope): string {
|
|
693
|
+
if (envelope.meta.operation?.kind === "attributes") {
|
|
696
694
|
const attributes = Array.isArray(envelope.data)
|
|
697
695
|
? (envelope.data as readonly string[])
|
|
698
696
|
: [];
|
|
699
|
-
|
|
700
|
-
} else if (typeof envelope.data === "string") {
|
|
701
|
-
lines.push(envelope.data);
|
|
702
|
-
} else {
|
|
703
|
-
lines.push(JSON.stringify(envelope.data, null, 2));
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (envelope.warnings.length > 0) {
|
|
707
|
-
lines.push("");
|
|
708
|
-
for (const warning of envelope.warnings) {
|
|
709
|
-
lines.push(`warning [${warning.code}]: ${warning.message}`);
|
|
710
|
-
}
|
|
697
|
+
return attributes.join("\n");
|
|
711
698
|
}
|
|
699
|
+
return typeof envelope.data === "string"
|
|
700
|
+
? envelope.data
|
|
701
|
+
: JSON.stringify(envelope.data, null, 2);
|
|
702
|
+
}
|
|
712
703
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
704
|
+
/**
|
|
705
|
+
* The `--verbose` header of a text-mode retrieve result (stderr, #470); none
|
|
706
|
+
* for a failure before resolution (`via: null`).
|
|
707
|
+
*/
|
|
708
|
+
export function retrieveVerboseLines(envelope: RetrieveEnvelope): string[] {
|
|
709
|
+
const meta = envelope.meta;
|
|
710
|
+
if (meta.via === null) return [];
|
|
711
|
+
const target = meta.target;
|
|
712
|
+
const lines = [
|
|
713
|
+
`target: ${target.input ?? target.host}${target.baseUrl ? ` -> ${target.baseUrl}` : ""}`,
|
|
714
|
+
`via: ${meta.via}`,
|
|
715
|
+
`sources: via=${formatCoreSource(meta.settings.via)}, host=${formatCoreSource(
|
|
716
|
+
meta.settings.host,
|
|
717
|
+
)}, port=${formatCoreSource(meta.settings.port)}, timeout=${formatCoreSource(
|
|
718
|
+
meta.settings.timeoutMs,
|
|
719
|
+
)}, format=${formatCoreSource(meta.settings.format)}, validate=${formatCoreSource(
|
|
720
|
+
meta.settings.validate,
|
|
721
|
+
)}`,
|
|
722
|
+
];
|
|
723
|
+
if (meta.validation?.enabled) {
|
|
724
|
+
lines.push(`validation: ${meta.validation.source}`);
|
|
722
725
|
}
|
|
723
|
-
|
|
724
|
-
return lines.join("\n");
|
|
726
|
+
return lines;
|
|
725
727
|
}
|
|
726
728
|
|
|
727
729
|
export function retrieveRequestSummary(
|
|
@@ -1445,6 +1447,18 @@ function resolveProtocol(
|
|
|
1445
1447
|
return via as ResolvedSetting<RouterOsProtocol>;
|
|
1446
1448
|
}
|
|
1447
1449
|
|
|
1450
|
+
/**
|
|
1451
|
+
* The output format a request resolves to (config < env < CLI; the CDB never
|
|
1452
|
+
* sets it). The `--follow` CLI needs it before the first line, because frame
|
|
1453
|
+
* lines carry no `meta.operation.request` (#462).
|
|
1454
|
+
*/
|
|
1455
|
+
export async function resolveRetrieveOutputFormat(
|
|
1456
|
+
request: RetrieveRequest,
|
|
1457
|
+
env: Record<string, string | undefined> = Bun.env,
|
|
1458
|
+
): Promise<RetrieveOutputFormat> {
|
|
1459
|
+
return resolveFormat(request, env, await loadEnvFileDefaults(env)).value;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1448
1462
|
function resolveFormat(
|
|
1449
1463
|
request: RetrieveRequest,
|
|
1450
1464
|
env: Record<string, string | undefined>,
|
package/src/transfer-fanout.ts
CHANGED
|
@@ -48,9 +48,11 @@ import {
|
|
|
48
48
|
import { promptForWriteConfirmation } from "./execute.ts";
|
|
49
49
|
import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
|
|
50
50
|
import {
|
|
51
|
+
emptyWriteSelectionError,
|
|
51
52
|
expandSelection,
|
|
52
53
|
isDefaultRecordTarget,
|
|
53
54
|
loadEnvFileDefaults,
|
|
55
|
+
redactTargetInput,
|
|
54
56
|
type SelectionExpansion,
|
|
55
57
|
type SelectionMember,
|
|
56
58
|
type SelectionResolveInput,
|
|
@@ -95,6 +97,8 @@ export interface TransferFanoutOperationMeta {
|
|
|
95
97
|
concurrency: number;
|
|
96
98
|
summary: FanoutSummary;
|
|
97
99
|
request: TransferFanoutRequestSummary;
|
|
100
|
+
/** Hoisted from the targets when every target used the same login (#461). */
|
|
101
|
+
auth?: { username?: string; passwordProvided: boolean };
|
|
98
102
|
selection: TransferSelectionSummary;
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -135,7 +139,7 @@ function summarizeSelection(
|
|
|
135
139
|
where: selection.where.map((clause) => `${clause.key}=${clause.value}`),
|
|
136
140
|
all: selection.all,
|
|
137
141
|
default: selection.default,
|
|
138
|
-
positionals: selection.positionals,
|
|
142
|
+
positionals: selection.positionals.map((value) => redactTargetInput(value)),
|
|
139
143
|
quickchr: selection.quickchr,
|
|
140
144
|
};
|
|
141
145
|
}
|
|
@@ -299,6 +303,9 @@ export async function transferFanout(
|
|
|
299
303
|
const concurrency = resolveFanoutConcurrency(options.concurrency, "rest-api");
|
|
300
304
|
|
|
301
305
|
if (expansion.empty) {
|
|
306
|
+
if (isTransferWriteVerb(request.verb)) {
|
|
307
|
+
throw emptyWriteSelectionError(expansion, `\`${request.verb}\``);
|
|
308
|
+
}
|
|
302
309
|
return transferFanoutEnvelope({
|
|
303
310
|
selection: selectionSummary,
|
|
304
311
|
concurrency,
|
package/src/transfer.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
type ResolvedAuth,
|
|
61
61
|
type ResolvedSetting,
|
|
62
62
|
type ResolvedTarget,
|
|
63
|
+
redactTargetInput,
|
|
63
64
|
resolveAuth,
|
|
64
65
|
resolveBooleanSetting,
|
|
65
66
|
resolveCdb,
|
|
@@ -1594,7 +1595,7 @@ export function buildTransferErrorEnvelope(
|
|
|
1594
1595
|
warnings: [],
|
|
1595
1596
|
tips: [],
|
|
1596
1597
|
meta: {
|
|
1597
|
-
target: { input: request.targetInput },
|
|
1598
|
+
target: { input: redactTargetInput(request.targetInput) },
|
|
1598
1599
|
via: null,
|
|
1599
1600
|
settings: {},
|
|
1600
1601
|
},
|
|
@@ -1615,19 +1616,16 @@ export function renderTransferEnvelope(
|
|
|
1615
1616
|
return toYaml(envelope);
|
|
1616
1617
|
case "text":
|
|
1617
1618
|
return envelope.ok
|
|
1618
|
-
? renderSuccessText(envelope
|
|
1619
|
+
? renderSuccessText(envelope)
|
|
1619
1620
|
: formatCentrsErrorText(envelope.error, options);
|
|
1620
1621
|
default:
|
|
1621
1622
|
return exhaustiveFormat(format);
|
|
1622
1623
|
}
|
|
1623
1624
|
}
|
|
1624
1625
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
options: { verbose?: boolean },
|
|
1628
|
-
): string {
|
|
1626
|
+
/** Text mode renders only the data; the CLI writes the rest to stderr (#470). */
|
|
1627
|
+
function renderSuccessText(envelope: TransferSuccessEnvelope): string {
|
|
1629
1628
|
const lines: string[] = [];
|
|
1630
|
-
const operation = envelope.meta.operation;
|
|
1631
1629
|
|
|
1632
1630
|
if (Array.isArray(envelope.data)) {
|
|
1633
1631
|
// list
|
|
@@ -1653,21 +1651,19 @@ function renderSuccessText(
|
|
|
1653
1651
|
lines.push(parts.join(" "));
|
|
1654
1652
|
}
|
|
1655
1653
|
|
|
1656
|
-
if (options.verbose && operation) {
|
|
1657
|
-
lines.push("");
|
|
1658
|
-
lines.push(`via: ${envelope.meta.via}`);
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
if (envelope.warnings.length > 0) {
|
|
1662
|
-
lines.push("");
|
|
1663
|
-
for (const warning of envelope.warnings) {
|
|
1664
|
-
lines.push(`warning [${warning.code}]: ${warning.message}`);
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
1654
|
return lines.join("\n");
|
|
1669
1655
|
}
|
|
1670
1656
|
|
|
1657
|
+
/**
|
|
1658
|
+
* The `--verbose` header of a text-mode transfer result (stderr, #470); none
|
|
1659
|
+
* for a failure before resolution.
|
|
1660
|
+
*/
|
|
1661
|
+
export function transferVerboseLines(envelope: TransferEnvelope): string[] {
|
|
1662
|
+
return envelope.meta.operation && envelope.meta.via !== null
|
|
1663
|
+
? [`via: ${envelope.meta.via}`]
|
|
1664
|
+
: [];
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1671
1667
|
// ── Small helpers ────────────────────────────────────────────────────────────
|
|
1672
1668
|
|
|
1673
1669
|
function uploadSizeHint(request: TransferRequest): number | undefined {
|