@tikoci/centrs 0.1.9-next.0 → 0.1.9-next.23
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 +177 -40
- package/GLOSSARY.txt +32 -0
- package/README.md +12 -8
- package/SECURITY.md +11 -3
- package/commands/api/README.md +39 -15
- package/commands/api/examples.md +44 -6
- package/commands/execute/README.md +108 -14
- package/commands/execute/examples.md +137 -3
- package/commands/explain/README.md +430 -129
- package/commands/explain/examples.md +260 -1
- package/commands/mcp/README.md +11 -0
- package/commands/mcp/examples.md +6 -0
- package/commands/retrieve/README.md +30 -13
- package/commands/retrieve/examples.md +10 -6
- package/commands/transfer/examples.md +9 -0
- package/docs/CLI.md +8 -8
- package/docs/CONSTITUTION.md +29 -2
- package/docs/MATRIX.md +26 -13
- 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/empty-selection.md +17 -3
- package/docs/errors/cmr/rule-selects-every-device.md +32 -0
- package/docs/errors/cmr/selection-includes-controller.md +27 -0
- package/docs/errors/usage/flag-after-separator.md +34 -0
- package/package.json +10 -5
- package/src/api-fanout.ts +11 -1
- package/src/api.ts +205 -65
- 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 +44 -20
- 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 +78 -0
- package/src/core/envelope.ts +32 -3
- package/src/core/error-catalog.ts +25 -3
- package/src/core/execute-command.ts +144 -19
- package/src/core/fanout.ts +130 -6
- package/src/errors.ts +2 -1
- package/src/execute-cmr.ts +95 -78
- package/src/execute-fanout.ts +11 -1
- package/src/execute.ts +105 -23
- package/src/explain/args.ts +69 -8
- package/src/explain/blocks.ts +3 -4
- package/src/explain/brace-slots.ts +5 -0
- package/src/explain/catalog.ts +13 -6
- package/src/explain/defects.ts +1 -0
- package/src/explain/group-faults.ts +927 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +20 -19
- package/src/explain/pathresolve.ts +102 -20
- package/src/explain/quoted-string.ts +41 -6
- package/src/explain/run-tokens.ts +223 -0
- package/src/explain/scope-brace.ts +103 -6
- package/src/explain/segment.ts +41 -3
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +17 -1
- package/src/explain/transport.ts +13 -0
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain.ts +378 -18
- package/src/index.ts +18 -1
- package/src/mcp/safety.ts +11 -0
- package/src/mcp/tools.ts +4 -1
- package/src/offline-gate.ts +30 -9
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
- 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-follow.ts
CHANGED
|
@@ -28,17 +28,22 @@ import {
|
|
|
28
28
|
buildRetrieveErrorEnvelope,
|
|
29
29
|
buildRetrieveErrorEnvelopeFromResolved,
|
|
30
30
|
type ResolvedRetrieveRequest,
|
|
31
|
-
type RetrieveEnvelope,
|
|
32
31
|
type RetrieveFollowCounts,
|
|
32
|
+
type RetrieveFollowLine,
|
|
33
33
|
type RetrieveFollowStopReason,
|
|
34
34
|
type RetrieveFollowSummary,
|
|
35
35
|
type RetrieveRequest,
|
|
36
|
+
type RetrieveStreamLineEnvelope,
|
|
36
37
|
type RetrieveStreamMeta,
|
|
37
38
|
type RetrieveSuccessEnvelope,
|
|
38
39
|
resolveRetrieveRequest,
|
|
39
40
|
validateRetrieveRead,
|
|
40
41
|
} from "./retrieve.ts";
|
|
41
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
failedStreamEnvelope,
|
|
44
|
+
streamEnvelope,
|
|
45
|
+
streamLineEnvelope,
|
|
46
|
+
} from "./retrieve-stream.ts";
|
|
42
47
|
|
|
43
48
|
/** Changes allowed to wait unread before the follow ends (`transport/stream-overflow`). */
|
|
44
49
|
export const FOLLOW_BUFFER_LIMIT = 50_000;
|
|
@@ -52,7 +57,8 @@ export interface RetrieveFollowOptions {
|
|
|
52
57
|
bufferLimit?: number;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
/** `at`: when the reply arrived (host clock), stamped by `push` in the wire tap. */
|
|
61
|
+
type FollowEvent = { at?: number } & (
|
|
56
62
|
| { type: "listen"; reply: ProtocolTappedReply; version?: number }
|
|
57
63
|
| { type: "listen-end" }
|
|
58
64
|
| { type: "snapshot-row"; row: Record<string, string> }
|
|
@@ -65,7 +71,8 @@ type FollowEvent =
|
|
|
65
71
|
rows: Map<string, Record<string, string>>;
|
|
66
72
|
}
|
|
67
73
|
| { type: "stop" }
|
|
68
|
-
| { type: "error"; error: unknown }
|
|
74
|
+
| { type: "error"; error: unknown }
|
|
75
|
+
);
|
|
69
76
|
|
|
70
77
|
/**
|
|
71
78
|
* The wire-ordered event queue: every source feeds it from a synchronous
|
|
@@ -144,8 +151,8 @@ class FollowQueue {
|
|
|
144
151
|
type FrameMeta = Extract<RetrieveStreamMeta, { kind: "frame" }>;
|
|
145
152
|
|
|
146
153
|
/**
|
|
147
|
-
* Follow one menu: yields
|
|
148
|
-
* live frames, then exactly one summary (successful, or an error envelope
|
|
154
|
+
* Follow one menu: yields `started` (the full meta and any advice), the
|
|
155
|
+
* snapshot frames, `synced`, live frames, then exactly one summary (successful, or an error envelope
|
|
149
156
|
* carrying the partial counts). Errors before the follow starts (resolution,
|
|
150
157
|
* validation, a non-followable menu, a non-native transport) yield one error
|
|
151
158
|
* envelope without a summary.
|
|
@@ -154,7 +161,7 @@ export async function* retrieveFollow(
|
|
|
154
161
|
request: RetrieveRequest,
|
|
155
162
|
env: Record<string, string | undefined> = Bun.env,
|
|
156
163
|
options: RetrieveFollowOptions = {},
|
|
157
|
-
): AsyncGenerator<
|
|
164
|
+
): AsyncGenerator<RetrieveFollowLine, void, void> {
|
|
158
165
|
const followRequest: RetrieveRequest = { ...request, follow: true };
|
|
159
166
|
let resolved: ResolvedRetrieveRequest;
|
|
160
167
|
try {
|
|
@@ -269,7 +276,7 @@ async function* runFollow(
|
|
|
269
276
|
options: RetrieveFollowOptions,
|
|
270
277
|
startedAt: number,
|
|
271
278
|
query?: readonly string[],
|
|
272
|
-
): AsyncGenerator<
|
|
279
|
+
): AsyncGenerator<RetrieveFollowLine, void, void> {
|
|
273
280
|
const follow = resolved.follow ?? { sweepMs: 0 };
|
|
274
281
|
const bufferLimit = options.bufferLimit ?? FOLLOW_BUFFER_LIMIT;
|
|
275
282
|
const counts: RetrieveFollowCounts = {
|
|
@@ -322,7 +329,7 @@ async function* runFollow(
|
|
|
322
329
|
controller.abort();
|
|
323
330
|
return;
|
|
324
331
|
}
|
|
325
|
-
queue.push(event);
|
|
332
|
+
queue.push({ ...event, at: Date.now() });
|
|
326
333
|
};
|
|
327
334
|
const stop = (reason: RetrieveFollowStopReason): void => {
|
|
328
335
|
stopReason ??= reason;
|
|
@@ -529,14 +536,16 @@ async function* runFollow(
|
|
|
529
536
|
id: string,
|
|
530
537
|
source: FrameMeta["source"],
|
|
531
538
|
row: Record<string, string> | null,
|
|
532
|
-
|
|
539
|
+
at: number | undefined,
|
|
540
|
+
): RetrieveStreamLineEnvelope => {
|
|
533
541
|
counts.frames += 1;
|
|
534
542
|
if (phase === "snapshot") counts.snapshot += 1;
|
|
535
543
|
else counts.changes += 1;
|
|
536
544
|
const data = row === null ? null : projectRow(row, resolved.attributes);
|
|
537
|
-
return
|
|
545
|
+
return streamLineEnvelope(data, {
|
|
538
546
|
kind: "frame",
|
|
539
547
|
index: counts.frames,
|
|
548
|
+
observedAt: new Date(at ?? Date.now()).toISOString(),
|
|
540
549
|
phase,
|
|
541
550
|
change,
|
|
542
551
|
id,
|
|
@@ -547,7 +556,7 @@ async function* runFollow(
|
|
|
547
556
|
const applyListen = (
|
|
548
557
|
event: Extract<FollowEvent, { type: "listen" }>,
|
|
549
558
|
phase: FrameMeta["phase"],
|
|
550
|
-
):
|
|
559
|
+
): RetrieveStreamLineEnvelope | undefined => {
|
|
551
560
|
const { reply } = event;
|
|
552
561
|
// `!empty` on a listen is the cancel acknowledgement, never a table state.
|
|
553
562
|
if (reply.type === "empty") return undefined;
|
|
@@ -568,10 +577,10 @@ async function* runFollow(
|
|
|
568
577
|
if (reply.attributes[".dead"] === "true") {
|
|
569
578
|
// A `.dead` for an id never reported is a coalesced add+delete.
|
|
570
579
|
if (!held.delete(id)) return undefined;
|
|
571
|
-
return frame(phase, "removed", id, "listen", null);
|
|
580
|
+
return frame(phase, "removed", id, "listen", null, event.at);
|
|
572
581
|
}
|
|
573
582
|
held.add(id);
|
|
574
|
-
return frame(phase, "upsert", id, "listen", reply.attributes);
|
|
583
|
+
return frame(phase, "upsert", id, "listen", reply.attributes, event.at);
|
|
575
584
|
};
|
|
576
585
|
/**
|
|
577
586
|
* Between frames of one batch (the A1 replay, a sweep's removals): a stop
|
|
@@ -586,15 +595,7 @@ async function* runFollow(
|
|
|
586
595
|
follow.count !== undefined && counts.changes >= follow.count;
|
|
587
596
|
|
|
588
597
|
try {
|
|
589
|
-
|
|
590
|
-
yield streamEnvelope(
|
|
591
|
-
resolved,
|
|
592
|
-
validation,
|
|
593
|
-
null,
|
|
594
|
-
{ kind: "notice" },
|
|
595
|
-
tips,
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
+
yield streamEnvelope(resolved, validation, null, { kind: "started" }, tips);
|
|
598
599
|
loop: for (;;) {
|
|
599
600
|
const event = await queue.next();
|
|
600
601
|
switch (event.type) {
|
|
@@ -620,7 +621,7 @@ async function* runFollow(
|
|
|
620
621
|
const id = event.row[".id"];
|
|
621
622
|
if (id !== undefined) {
|
|
622
623
|
held.add(id);
|
|
623
|
-
yield frame("snapshot", "upsert", id, "print", event.row);
|
|
624
|
+
yield frame("snapshot", "upsert", id, "print", event.row, event.at);
|
|
624
625
|
}
|
|
625
626
|
break;
|
|
626
627
|
}
|
|
@@ -660,9 +661,9 @@ async function* runFollow(
|
|
|
660
661
|
const phase = counts.synced ? "live" : "snapshot";
|
|
661
662
|
if (row) {
|
|
662
663
|
held.add(id);
|
|
663
|
-
yield frame(phase, "upsert", id, "membership", row);
|
|
664
|
+
yield frame(phase, "upsert", id, "membership", row, event.at);
|
|
664
665
|
} else if (held.delete(id)) {
|
|
665
|
-
yield frame(phase, "removed", id, "membership", null);
|
|
666
|
+
yield frame(phase, "removed", id, "membership", null, event.at);
|
|
666
667
|
}
|
|
667
668
|
if (counts.synced && countReached()) {
|
|
668
669
|
stopReason ??= "count-reached";
|
|
@@ -688,13 +689,8 @@ async function* runFollow(
|
|
|
688
689
|
continue;
|
|
689
690
|
if (haltRequested()) break loop;
|
|
690
691
|
held.delete(id);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
"removed",
|
|
694
|
-
id,
|
|
695
|
-
query ? "membership" : "sweep",
|
|
696
|
-
null,
|
|
697
|
-
);
|
|
692
|
+
// The sweep found it gone, filtered or not (#462).
|
|
693
|
+
yield frame("live", "removed", id, "sweep", null, event.at);
|
|
698
694
|
if (countReached()) {
|
|
699
695
|
stopReason ??= "count-reached";
|
|
700
696
|
break loop;
|
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 {
|