@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/cli/common.ts
CHANGED
|
@@ -6,7 +6,51 @@
|
|
|
6
6
|
* together.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import type { Tip, Warning } from "../core/envelope.ts";
|
|
10
|
+
import { CentrsError, terminalSafeLine } from "../errors.ts";
|
|
11
|
+
import { redactTargetInput } from "../resolver/target.ts";
|
|
12
|
+
import { formatTipsText } from "./missing-target.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Warnings that a single-target text-mode CLI leaves out (#470).
|
|
16
|
+
* `transport/provider-trust` rides every quickchr run over TLS or SSH, so
|
|
17
|
+
* printing it would be noise. `--json` keeps every warning.
|
|
18
|
+
*/
|
|
19
|
+
const TEXT_QUIET_WARNING_CODES: readonly string[] = [
|
|
20
|
+
"transport/provider-trust",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One `warning [<code>]: <message>` line per envelope warning, in the same form
|
|
25
|
+
* as fan-out text, minus {@link TEXT_QUIET_WARNING_CODES}.
|
|
26
|
+
*/
|
|
27
|
+
export function textWarningLines(warnings: readonly Warning[]): string[] {
|
|
28
|
+
return warnings
|
|
29
|
+
.filter((warning) => !TEXT_QUIET_WARNING_CODES.includes(warning.code))
|
|
30
|
+
.map((warning) =>
|
|
31
|
+
terminalSafeLine(`warning [${warning.code}]: ${warning.message}`),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* #470: in text mode a single-target command's stdout is its data and nothing
|
|
37
|
+
* else. The notes around the data go to stderr: the `--verbose` header before
|
|
38
|
+
* it ({@link printTextHeader}), then warnings and tips after it. This applies
|
|
39
|
+
* on success and on failure, so a write whose outcome is unknown still says so.
|
|
40
|
+
*/
|
|
41
|
+
export function printTextNotes(envelope: {
|
|
42
|
+
warnings: readonly Warning[];
|
|
43
|
+
tips: readonly Tip[];
|
|
44
|
+
}): void {
|
|
45
|
+
for (const line of textWarningLines(envelope.warnings)) console.error(line);
|
|
46
|
+
const tips = formatTipsText(envelope.tips);
|
|
47
|
+
if (tips) console.error(tips);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The `--verbose` header of a text-mode result, on stderr (#470). */
|
|
51
|
+
export function printTextHeader(lines: readonly string[]): void {
|
|
52
|
+
if (lines.length > 0) console.error(lines.join("\n"));
|
|
53
|
+
}
|
|
10
54
|
|
|
11
55
|
export interface CliCommandOption {
|
|
12
56
|
flag: string;
|
|
@@ -106,6 +150,141 @@ export function unknownFlagError(
|
|
|
106
150
|
});
|
|
107
151
|
}
|
|
108
152
|
|
|
153
|
+
/** Single-quote a token for a shell line a user can copy, unless it is plain. */
|
|
154
|
+
function shellQuote(token: string): string {
|
|
155
|
+
return /^[\w/:=.,@%+-]+$/.test(token)
|
|
156
|
+
? token
|
|
157
|
+
: `'${token.replaceAll("'", `'\\''`)}'`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A centrs flag written after a bare `--` (#459).
|
|
162
|
+
*
|
|
163
|
+
* `--` makes every later token RouterOS input, so a trailing `--json` (the
|
|
164
|
+
* habitual position: `execute r1 -- '/cmd' --json`) went to the router and
|
|
165
|
+
* failed there as `unknown-attribute`. The RouterOS command is one quoted
|
|
166
|
+
* argument in every documented form, so a later token that is exactly one of
|
|
167
|
+
* this command's LONG flags (or its `--flag=value` spelling) is this mistake.
|
|
168
|
+
* Short aliases (`-u`, `-h`) are left alone: a lone `-x` is plausible
|
|
169
|
+
* RouterOS input, `--json` is not. A command that really needs such a token
|
|
170
|
+
* can carry it inside the quoted command.
|
|
171
|
+
*
|
|
172
|
+
* Throws `usage/flag-after-separator` with the command line rewritten with the
|
|
173
|
+
* flags (and their values) moved before `--`. Password values are never
|
|
174
|
+
* echoed. A no-op when there is no `--`.
|
|
175
|
+
*/
|
|
176
|
+
export function rejectFlagsAfterSeparator(
|
|
177
|
+
command: string,
|
|
178
|
+
args: readonly string[],
|
|
179
|
+
options: readonly CliCommandOption[],
|
|
180
|
+
): void {
|
|
181
|
+
const optionByFlag = new Map<string, CliCommandOption>();
|
|
182
|
+
for (const option of options) {
|
|
183
|
+
for (const token of option.flag.split("/")) {
|
|
184
|
+
const trimmed = token.trim();
|
|
185
|
+
if (trimmed.startsWith("-") && trimmed !== "--") {
|
|
186
|
+
optionByFlag.set(trimmed, option);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let separator = -1;
|
|
191
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
192
|
+
const token = args[index] ?? "";
|
|
193
|
+
if (token === "--") {
|
|
194
|
+
separator = index;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
// A flag's value may itself be `--`; only the parser's separator counts.
|
|
198
|
+
if (optionByFlag.get(token)?.valueName !== undefined) index += 1;
|
|
199
|
+
}
|
|
200
|
+
if (separator === -1) return;
|
|
201
|
+
const secret = (flag: string) =>
|
|
202
|
+
flag.startsWith("-") && flag.includes("password");
|
|
203
|
+
const before: string[] = [];
|
|
204
|
+
for (let index = 0; index < separator; index += 1) {
|
|
205
|
+
const token = args[index] ?? "";
|
|
206
|
+
const [name = ""] = token.split("=");
|
|
207
|
+
const equal = token.indexOf("=");
|
|
208
|
+
if (equal >= 0 && optionByFlag.get(name)?.valueName !== undefined) {
|
|
209
|
+
before.push(
|
|
210
|
+
name,
|
|
211
|
+
secret(name) ? "<password>" : redactTargetInput(token.slice(equal + 1)),
|
|
212
|
+
);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (equal >= 0 && secret(name)) {
|
|
216
|
+
before.push(`${name}=<password>`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
before.push(redactTargetInput(token));
|
|
220
|
+
if (optionByFlag.get(name)?.valueName !== undefined || secret(name)) {
|
|
221
|
+
const value = args[index + 1];
|
|
222
|
+
if (index + 1 < separator && value !== undefined) {
|
|
223
|
+
before.push(secret(name) ? "<password>" : redactTargetInput(value));
|
|
224
|
+
index += 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const unsupportedInlineBooleans: string[] = [];
|
|
229
|
+
const moved: string[] = [];
|
|
230
|
+
const kept: string[] = [];
|
|
231
|
+
const flags: string[] = [];
|
|
232
|
+
const after = args.slice(separator + 1);
|
|
233
|
+
for (let index = 0; index < after.length; index += 1) {
|
|
234
|
+
const token = after[index] ?? "";
|
|
235
|
+
const [name = "", ...valueParts] = token.split("=");
|
|
236
|
+
const option = name.startsWith("--") ? optionByFlag.get(name) : undefined;
|
|
237
|
+
if (option === undefined) {
|
|
238
|
+
kept.push(token);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
flags.push(name);
|
|
242
|
+
// The parsers take a value flag's value as the next token (`--timeout 30s`,
|
|
243
|
+
// never `--timeout=30s`), so the fixed line splits it; only a boolean's
|
|
244
|
+
// supported `=` form (`--validate=no`) is kept as typed.
|
|
245
|
+
if (valueParts.length > 0) {
|
|
246
|
+
if (option.valueName === undefined) {
|
|
247
|
+
if (command !== "execute" || name !== "--validate") {
|
|
248
|
+
unsupportedInlineBooleans.push(name);
|
|
249
|
+
}
|
|
250
|
+
moved.push(token);
|
|
251
|
+
} else {
|
|
252
|
+
moved.push(name, secret(name) ? "<password>" : valueParts.join("="));
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
moved.push(name);
|
|
257
|
+
const value = after[index + 1];
|
|
258
|
+
if (option.valueName !== undefined && value !== undefined) {
|
|
259
|
+
moved.push(secret(name) ? "<password>" : value);
|
|
260
|
+
index += 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (flags.length === 0) return;
|
|
264
|
+
// A moved --host value needs the same redaction as a positional target.
|
|
265
|
+
// Keep the RouterOS command itself verbatim; it is not a target.
|
|
266
|
+
const fixed = [
|
|
267
|
+
"centrs",
|
|
268
|
+
command,
|
|
269
|
+
...before,
|
|
270
|
+
...moved.map((token) => redactTargetInput(token)),
|
|
271
|
+
"--",
|
|
272
|
+
...kept,
|
|
273
|
+
]
|
|
274
|
+
.map(shellQuote)
|
|
275
|
+
.join(" ");
|
|
276
|
+
const list = flags.map((flag) => `\`${flag}\``).join(", ");
|
|
277
|
+
throw new CentrsError({
|
|
278
|
+
code: "usage/flag-after-separator",
|
|
279
|
+
summary: `${list} ${flags.length === 1 ? "comes" : "come"} after \`--\`, so ${flags.length === 1 ? "it" : "they"} would be sent to RouterOS as part of the command.`,
|
|
280
|
+
remediation:
|
|
281
|
+
unsupportedInlineBooleans.length > 0
|
|
282
|
+
? `Put centrs flags before \`--\`. These flags accept no value: ${unsupportedInlineBooleans.map((flag) => `\`${flag}\``).join(", ")}. Use a bare flag to enable it, or omit it to keep the default.`
|
|
283
|
+
: `Put centrs flags before \`--\`: \`${fixed}\``,
|
|
284
|
+
context: { flags },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
109
288
|
export function renderCommandHelp(
|
|
110
289
|
describe: string,
|
|
111
290
|
command: CliCommandMetadata,
|
package/src/cli/devices.ts
CHANGED
|
@@ -746,10 +746,8 @@ export async function runDevicesCli(args: readonly string[]): Promise<number> {
|
|
|
746
746
|
centrsError,
|
|
747
747
|
);
|
|
748
748
|
if (format === "json" || format === "yaml") {
|
|
749
|
-
// codeql[js/clear-text-logging] password reaches here only as SettingSource provenance ({kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
|
|
750
749
|
console.error(renderDevicesEnvelope(envelope, format));
|
|
751
750
|
} else {
|
|
752
|
-
// codeql[js/clear-text-logging] password reaches here only as SettingSource provenance ({kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
|
|
753
751
|
console.error(formatCentrsErrorText(centrsError));
|
|
754
752
|
}
|
|
755
753
|
return 1;
|
package/src/cli/discover.ts
CHANGED
|
@@ -207,7 +207,6 @@ export async function runDiscoverCli(args: readonly string[]): Promise<number> {
|
|
|
207
207
|
remediation:
|
|
208
208
|
"Use `centrs discover --help` to inspect the supported flags.",
|
|
209
209
|
});
|
|
210
|
-
// codeql[js/clear-text-logging] password reaches here only as SettingSource provenance ({kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
|
|
211
210
|
console.error(formatCentrsErrorText(centrsError));
|
|
212
211
|
return 1;
|
|
213
212
|
}
|
package/src/cli/execute.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ExecuteRequest,
|
|
11
11
|
executeEnvelope,
|
|
12
12
|
executeOutputFormats,
|
|
13
|
+
executeVerboseLines,
|
|
13
14
|
renderExecuteEnvelope,
|
|
14
15
|
} from "../execute.ts";
|
|
15
16
|
import {
|
|
@@ -27,6 +28,9 @@ import { assertNoQuickchrOverrideConflict } from "../resolver/index.ts";
|
|
|
27
28
|
import {
|
|
28
29
|
type CliCommandMetadata,
|
|
29
30
|
expectValue,
|
|
31
|
+
printTextHeader,
|
|
32
|
+
printTextNotes,
|
|
33
|
+
rejectFlagsAfterSeparator,
|
|
30
34
|
renderCommandHelp,
|
|
31
35
|
unknownFlagError,
|
|
32
36
|
} from "./common.ts";
|
|
@@ -155,12 +159,12 @@ export const executeCommand: CliCommandMetadata = {
|
|
|
155
159
|
},
|
|
156
160
|
{
|
|
157
161
|
flag: "--verbose",
|
|
158
|
-
description: "Include additional context in text output.",
|
|
162
|
+
description: "Include additional context in text output, on stderr.",
|
|
159
163
|
},
|
|
160
164
|
{
|
|
161
165
|
flag: "--",
|
|
162
166
|
description:
|
|
163
|
-
"End centrs option parsing: every following token is the literal RouterOS command
|
|
167
|
+
"End centrs option parsing: every following token is the literal RouterOS command (e.g. `-- /interface print where disabled=yes`). Put centrs flags before it; one of execute's own long flags after it is refused as `usage/flag-after-separator`.",
|
|
164
168
|
},
|
|
165
169
|
],
|
|
166
170
|
};
|
|
@@ -175,6 +179,7 @@ interface ExecuteCliArgs extends ExecuteRequest {
|
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
export function parseExecuteCliArgs(args: readonly string[]): ExecuteCliArgs {
|
|
182
|
+
rejectFlagsAfterSeparator("execute", args, executeCommand.options);
|
|
178
183
|
const parsed: ExecuteCliArgs = { command: "" };
|
|
179
184
|
const selectionFlags = emptySelectionFlags();
|
|
180
185
|
const positional: string[] = [];
|
|
@@ -399,15 +404,20 @@ export async function runExecuteCli(args: readonly string[]): Promise<number> {
|
|
|
399
404
|
)?.request?.format ??
|
|
400
405
|
parsed.format ??
|
|
401
406
|
"text";
|
|
407
|
+
const text = resolvedFormat === "text";
|
|
408
|
+
if (text && parsed.verbose) {
|
|
409
|
+
printTextHeader(executeVerboseLines(envelope));
|
|
410
|
+
}
|
|
402
411
|
const rendered = renderExecuteEnvelope(envelope, resolvedFormat, {
|
|
403
412
|
verbose: parsed.verbose,
|
|
404
413
|
});
|
|
405
414
|
if (envelope.ok) {
|
|
406
415
|
console.log(rendered);
|
|
407
|
-
|
|
416
|
+
} else {
|
|
417
|
+
console.error(rendered);
|
|
408
418
|
}
|
|
409
|
-
|
|
410
|
-
return 1;
|
|
419
|
+
if (text) printTextNotes(envelope);
|
|
420
|
+
return envelope.ok ? 0 : 1;
|
|
411
421
|
} catch (error) {
|
|
412
422
|
// Parse/usage errors land here too: surface them through the same typed
|
|
413
423
|
// envelope as every other runner (no raw message, no exit code 2).
|
|
@@ -424,14 +434,12 @@ export async function runExecuteCli(args: readonly string[]): Promise<number> {
|
|
|
424
434
|
tips,
|
|
425
435
|
);
|
|
426
436
|
console.error(
|
|
427
|
-
// codeql[js/clear-text-logging] No secret reaches this sink. The tainted 'password' flows in only as provenance (SettingSource={kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
|
|
428
437
|
renderExecuteEnvelope(envelope, format, {
|
|
429
438
|
verbose: parsed?.verbose ?? false,
|
|
430
439
|
}),
|
|
431
440
|
);
|
|
432
441
|
} else {
|
|
433
442
|
console.error(
|
|
434
|
-
// codeql[js/clear-text-logging] Same false-positive pattern: password parsed from args never reaches error.message in the text-format catch path.
|
|
435
443
|
formatCentrsErrorText(
|
|
436
444
|
asCentrsError(error, {
|
|
437
445
|
code: "input/invalid-command",
|
|
@@ -477,7 +485,6 @@ async function runExecuteFanoutCli(
|
|
|
477
485
|
} catch (error) {
|
|
478
486
|
const envelope = buildExecuteFanoutErrorEnvelope(parsed, error);
|
|
479
487
|
console.error(
|
|
480
|
-
// codeql[js/clear-text-logging] Twin of #80/#81/#83/#84. No secret logged: buildExecuteFanoutErrorEnvelope meta is {target:{},via,settings:{}}; CodeQL taints the whole request object but the builder projects the password away.
|
|
481
488
|
renderExecuteFanoutEnvelope(envelope, format, {
|
|
482
489
|
verbose: parsed.verbose ?? false,
|
|
483
490
|
}),
|
|
@@ -581,6 +588,12 @@ async function runExecuteCmrCli(
|
|
|
581
588
|
}
|
|
582
589
|
}
|
|
583
590
|
|
|
591
|
+
/**
|
|
592
|
+
* The output format for an error that may have struck before parsing finished.
|
|
593
|
+
* Reads the argv the way the parser does: only tokens before `--` (after it is
|
|
594
|
+
* RouterOS input), last format flag wins, and a value-taking flag's value is
|
|
595
|
+
* skipped, so `--format yaml -- '/x' --json` renders YAML.
|
|
596
|
+
*/
|
|
584
597
|
function inferExecuteFormat(
|
|
585
598
|
args: readonly string[],
|
|
586
599
|
parsed?: ExecuteCliArgs,
|
|
@@ -588,22 +601,33 @@ function inferExecuteFormat(
|
|
|
588
601
|
if (parsed?.format) {
|
|
589
602
|
return parsed.format;
|
|
590
603
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (
|
|
598
|
-
value
|
|
599
|
-
executeOutputFormats.includes(value as ExecuteOutputFormat)
|
|
600
|
-
|
|
601
|
-
|
|
604
|
+
let format: ExecuteOutputFormat = "text";
|
|
605
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
606
|
+
const arg = args[index];
|
|
607
|
+
if (arg === undefined || arg === "--") break;
|
|
608
|
+
if (arg === "--json") {
|
|
609
|
+
format = "json";
|
|
610
|
+
} else if (arg === "--format") {
|
|
611
|
+
const value = args[index + 1];
|
|
612
|
+
if (executeOutputFormats.includes(value as ExecuteOutputFormat)) {
|
|
613
|
+
format = value as ExecuteOutputFormat;
|
|
614
|
+
}
|
|
615
|
+
index += 1;
|
|
616
|
+
} else if (EXECUTE_VALUE_FLAGS.has(arg)) {
|
|
617
|
+
index += 1;
|
|
602
618
|
}
|
|
603
619
|
}
|
|
604
|
-
return
|
|
620
|
+
return format;
|
|
605
621
|
}
|
|
606
622
|
|
|
623
|
+
const EXECUTE_VALUE_FLAGS: ReadonlySet<string> = new Set(
|
|
624
|
+
executeCommand.options.flatMap((option) =>
|
|
625
|
+
option.valueName === undefined
|
|
626
|
+
? []
|
|
627
|
+
: option.flag.split("/").map((token) => token.trim()),
|
|
628
|
+
),
|
|
629
|
+
);
|
|
630
|
+
|
|
607
631
|
function parseIntegerFlag(value: string, flag: string): number {
|
|
608
632
|
const parsed = Number.parseInt(value, 10);
|
|
609
633
|
if (!Number.isInteger(parsed)) {
|
package/src/cli/explain.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type { ResolvedSetting } from "../resolver/settings.ts";
|
|
|
51
51
|
import {
|
|
52
52
|
type CliCommandMetadata,
|
|
53
53
|
expectValue,
|
|
54
|
+
rejectFlagsAfterSeparator,
|
|
54
55
|
renderCommandHelp,
|
|
55
56
|
unknownFlagError,
|
|
56
57
|
} from "./common.ts";
|
|
@@ -118,6 +119,7 @@ export interface ExplainCliArgs {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
export function parseExplainCliArgs(args: readonly string[]): ExplainCliArgs {
|
|
122
|
+
rejectFlagsAfterSeparator("explain", args, explainCliCommand.options);
|
|
121
123
|
const parsed: ExplainCliArgs = {
|
|
122
124
|
positionals: [],
|
|
123
125
|
failOn: "error",
|
package/src/cli/mcp.ts
CHANGED
|
@@ -101,7 +101,6 @@ export async function runMcpCli(args: readonly string[]): Promise<number> {
|
|
|
101
101
|
return 0;
|
|
102
102
|
} catch (error) {
|
|
103
103
|
console.error(
|
|
104
|
-
// codeql[js/clear-text-logging] password reaches here only as SettingSource provenance ({kind,key}), never the value — see CommonSettingsMeta.password in src/core/envelope.ts.
|
|
105
104
|
formatCentrsErrorText(
|
|
106
105
|
asCentrsError(error, {
|
|
107
106
|
code: "internal/mcp-start",
|
package/src/cli/retrieve.ts
CHANGED
|
@@ -15,17 +15,20 @@ import {
|
|
|
15
15
|
buildRetrieveFanoutErrorEnvelope,
|
|
16
16
|
describeCentrs,
|
|
17
17
|
fanoutExitCode,
|
|
18
|
+
isStreamDataLine,
|
|
18
19
|
type ResolvedRetrieveRequest,
|
|
19
20
|
type RetrieveOutputFormat,
|
|
20
21
|
type RetrieveRequest,
|
|
21
22
|
renderRetrieveEnvelope,
|
|
22
23
|
renderRetrieveFanoutEnvelope,
|
|
23
24
|
renderRetrieveStreamLine,
|
|
25
|
+
resolveRetrieveOutputFormat,
|
|
24
26
|
retrieve,
|
|
25
27
|
retrieveFanout,
|
|
26
28
|
retrieveFollow,
|
|
27
29
|
retrieveOutputFormats,
|
|
28
30
|
retrieveSample,
|
|
31
|
+
retrieveVerboseLines,
|
|
29
32
|
retrieveWait,
|
|
30
33
|
type ValidationTrace,
|
|
31
34
|
validateRetrieveRequestShape,
|
|
@@ -34,6 +37,8 @@ import { assertNoQuickchrOverrideConflict } from "../resolver/index.ts";
|
|
|
34
37
|
import {
|
|
35
38
|
type CliCommandMetadata,
|
|
36
39
|
expectValue,
|
|
40
|
+
printTextHeader,
|
|
41
|
+
printTextNotes,
|
|
37
42
|
renderCommandHelp,
|
|
38
43
|
unknownFlagError,
|
|
39
44
|
} from "./common.ts";
|
|
@@ -125,13 +130,13 @@ export const retrieveCommand: CliCommandMetadata = {
|
|
|
125
130
|
{
|
|
126
131
|
flag: "--follow",
|
|
127
132
|
description:
|
|
128
|
-
"Keep the menu's rows current over native-api `listen`: snapshot frames, a `synced` line, then a line per change, then a summary (NDJSON for json/yaml/ndjson). Single target, list menus whose print takes follow-only.",
|
|
133
|
+
"Keep the menu's rows current over native-api `listen`: a `started` line, snapshot frames, a `synced` line, then a line per change, then a summary (NDJSON for json/yaml/ndjson). Single target, list menus whose print takes follow-only.",
|
|
129
134
|
},
|
|
130
135
|
{
|
|
131
136
|
flag: "--sample",
|
|
132
137
|
valueName: "<interval>",
|
|
133
138
|
description:
|
|
134
|
-
"Read the menu again every interval (with a unit, e.g. `5s`): one line per complete read, then a summary. With --wait, sets polling cadence and returns one final envelope. Sees counters and state listen never reports. Single target; rest-api or native-api.",
|
|
139
|
+
"Read the menu again every interval (with a unit, e.g. `5s`): a `started` line, one line per complete read, then a summary. With --wait, sets polling cadence and returns one final envelope. Sees counters and state listen never reports. Single target; rest-api or native-api.",
|
|
135
140
|
},
|
|
136
141
|
{
|
|
137
142
|
flag: "--wait",
|
|
@@ -206,7 +211,7 @@ export const retrieveCommand: CliCommandMetadata = {
|
|
|
206
211
|
},
|
|
207
212
|
{
|
|
208
213
|
flag: "--verbose",
|
|
209
|
-
description: "Show resolved setting sources in text output.",
|
|
214
|
+
description: "Show resolved setting sources in text output, on stderr.",
|
|
210
215
|
},
|
|
211
216
|
],
|
|
212
217
|
};
|
|
@@ -283,6 +288,10 @@ export async function runRetrieveCli(args: readonly string[]): Promise<number> {
|
|
|
283
288
|
request?: { format?: RetrieveOutputFormat };
|
|
284
289
|
}
|
|
285
290
|
)?.request?.format ?? "text";
|
|
291
|
+
const text = resolvedFormat === "text";
|
|
292
|
+
if (text && request.verbose) {
|
|
293
|
+
printTextHeader(retrieveVerboseLines(envelope));
|
|
294
|
+
}
|
|
286
295
|
console.log(
|
|
287
296
|
renderRetrieveEnvelope(
|
|
288
297
|
envelope as Parameters<typeof renderRetrieveEnvelope>[0],
|
|
@@ -290,6 +299,7 @@ export async function runRetrieveCli(args: readonly string[]): Promise<number> {
|
|
|
290
299
|
{ verbose: request.verbose },
|
|
291
300
|
),
|
|
292
301
|
);
|
|
302
|
+
if (text) printTextNotes(envelope);
|
|
293
303
|
return 0;
|
|
294
304
|
} catch (error) {
|
|
295
305
|
const format = inferRequestedFormat(args, request);
|
|
@@ -313,14 +323,12 @@ export async function runRetrieveCli(args: readonly string[]): Promise<number> {
|
|
|
313
323
|
tips,
|
|
314
324
|
);
|
|
315
325
|
console.error(
|
|
316
|
-
// codeql[js/clear-text-logging] Twin of the established redacted-request dismissals: error builders carry only targetInput (host)+via (protocol); the raw password never reaches this render.
|
|
317
326
|
renderRetrieveEnvelope(envelope, format, {
|
|
318
327
|
verbose: request?.verbose ?? false,
|
|
319
328
|
}),
|
|
320
329
|
);
|
|
321
330
|
} else {
|
|
322
331
|
console.error(
|
|
323
|
-
// codeql[js/clear-text-logging] Same false-positive pattern: password parsed from args never reaches error.message in the text-format catch path.
|
|
324
332
|
formatCentrsErrorText(
|
|
325
333
|
asCentrsError(error, {
|
|
326
334
|
code: "input/invalid-command",
|
|
@@ -340,14 +348,21 @@ export async function runRetrieveCli(args: readonly string[]): Promise<number> {
|
|
|
340
348
|
|
|
341
349
|
/**
|
|
342
350
|
* Every `--follow`/`--sample` envelope goes to stdout, one per line, errors
|
|
343
|
-
* included; any failure sets exit 1, even after successful lines.
|
|
344
|
-
*
|
|
351
|
+
* included; any failure sets exit 1, even after successful lines. In text,
|
|
352
|
+
* stdout is data (#470, #473): only `frame`/`sample` rows go there, and the
|
|
353
|
+
* status lines, tips, warnings and errors go to stderr. Ctrl-C asks for a
|
|
354
|
+
* bounded stop that still ends with a summary.
|
|
345
355
|
*/
|
|
346
356
|
async function runRetrieveStreamCli(
|
|
347
357
|
request: RetrieveCliArgs,
|
|
348
358
|
args: readonly string[],
|
|
349
359
|
): Promise<number> {
|
|
350
|
-
|
|
360
|
+
// Resolved once, up front: a config-file `CENTRS_FORMAT` is not in `args`,
|
|
361
|
+
// and follow frame lines carry no request (#462). An invalid format is
|
|
362
|
+
// reported by the stream's own error line, rendered as the args ask.
|
|
363
|
+
const format = await resolveRetrieveOutputFormat(request, Bun.env).catch(() =>
|
|
364
|
+
inferRequestedFormat(args, request),
|
|
365
|
+
);
|
|
351
366
|
const controller = new AbortController();
|
|
352
367
|
const onSigint = (): void => controller.abort();
|
|
353
368
|
process.on("SIGINT", onSigint);
|
|
@@ -357,9 +372,14 @@ async function runRetrieveStreamCli(
|
|
|
357
372
|
for await (const envelope of stream(request, Bun.env, {
|
|
358
373
|
signal: controller.signal,
|
|
359
374
|
})) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
375
|
+
const line = renderRetrieveStreamLine(envelope, format);
|
|
376
|
+
if (format !== "text" || isStreamDataLine(envelope)) {
|
|
377
|
+
console.log(line);
|
|
378
|
+
} else {
|
|
379
|
+
// `started` renders its own tips; the summary repeats them.
|
|
380
|
+
if (line) console.error(line);
|
|
381
|
+
printTextNotes({ warnings: envelope.warnings, tips: [] });
|
|
382
|
+
}
|
|
363
383
|
if (!envelope.ok) exitCode = 1;
|
|
364
384
|
}
|
|
365
385
|
} finally {
|
|
@@ -379,14 +399,17 @@ async function runRetrieveWaitCli(
|
|
|
379
399
|
const envelope = await retrieveWait(request, Bun.env, {
|
|
380
400
|
signal: controller.signal,
|
|
381
401
|
});
|
|
402
|
+
const format =
|
|
403
|
+
envelope.meta.operation?.request.format ??
|
|
404
|
+
inferRequestedFormat(args, request);
|
|
405
|
+
const text = format === "text";
|
|
406
|
+
if (text && request.verbose) {
|
|
407
|
+
printTextHeader(retrieveVerboseLines(envelope));
|
|
408
|
+
}
|
|
382
409
|
console.log(
|
|
383
|
-
renderRetrieveEnvelope(
|
|
384
|
-
envelope,
|
|
385
|
-
envelope.meta.operation?.request.format ??
|
|
386
|
-
inferRequestedFormat(args, request),
|
|
387
|
-
{ verbose: request.verbose },
|
|
388
|
-
),
|
|
410
|
+
renderRetrieveEnvelope(envelope, format, { verbose: request.verbose }),
|
|
389
411
|
);
|
|
412
|
+
if (text) printTextNotes(envelope);
|
|
390
413
|
return envelope.ok ? 0 : 1;
|
|
391
414
|
} finally {
|
|
392
415
|
process.off("SIGINT", onSigint);
|
|
@@ -423,7 +446,6 @@ async function runRetrieveFanoutCli(
|
|
|
423
446
|
} catch (error) {
|
|
424
447
|
const envelope = buildRetrieveFanoutErrorEnvelope(request, error);
|
|
425
448
|
console.error(
|
|
426
|
-
// codeql[js/clear-text-logging] Twin of #80/#81/#83. No secret logged: error builders carry only targetInput (host)+via (protocol); serializeCentrsError holds code/summary/remediation/context.
|
|
427
449
|
renderRetrieveFanoutEnvelope(envelope, format, {
|
|
428
450
|
verbose: request.verbose ?? false,
|
|
429
451
|
}),
|
package/src/cli/settings.ts
CHANGED
|
@@ -244,7 +244,6 @@ export async function runSettingsCli(args: readonly string[]): Promise<number> {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
const format = parsed.format ?? "text";
|
|
247
|
-
// codeql[js/clear-text-logging] passwordSet is a has-password boolean, never the secret; the property-NAME heuristic (maybePassword) matches the field name, not its value type.
|
|
248
247
|
console.log(renderSettingsEnvelope(envelope, format));
|
|
249
248
|
return 0;
|
|
250
249
|
} catch (error) {
|
|
@@ -263,10 +262,8 @@ export async function runSettingsCli(args: readonly string[]): Promise<number> {
|
|
|
263
262
|
centrsError,
|
|
264
263
|
);
|
|
265
264
|
if (format === "json" || format === "yaml") {
|
|
266
|
-
// codeql[js/clear-text-logging] sink renders only a literal error string (e.g. "Missing value for --password."), never the real value; CodeQL over-taints via shared expectValue/CentrsError code.
|
|
267
265
|
console.error(renderSettingsEnvelope(envelope, format));
|
|
268
266
|
} else {
|
|
269
|
-
// codeql[js/clear-text-logging] sink renders only a literal error string (e.g. "Missing value for --password."), never the real value; CodeQL over-taints via shared expectValue/CentrsError code.
|
|
270
267
|
console.error(formatCentrsErrorText(centrsError));
|
|
271
268
|
}
|
|
272
269
|
return 1;
|
package/src/cli/terminal.ts
CHANGED
|
@@ -188,12 +188,10 @@ export async function runTerminalCli(args: readonly string[]): Promise<number> {
|
|
|
188
188
|
if (format === "json" || format === "yaml") {
|
|
189
189
|
const envelope = withTips(buildTerminalErrorEnvelope(error), tips);
|
|
190
190
|
console.error(
|
|
191
|
-
// codeql[js/clear-text-logging] CLI error renderer false positive; password reaches neither the error nor the stderr renderers. Dismissed across all CLI commands.
|
|
192
191
|
format === "yaml" ? toYaml(envelope) : JSON.stringify(envelope),
|
|
193
192
|
);
|
|
194
193
|
} else {
|
|
195
194
|
console.error(
|
|
196
|
-
// codeql[js/clear-text-logging] Password parsed from args never reaches error.message in the text-format catch path; the parsed request (which holds the raw password) is never rendered.
|
|
197
195
|
formatCentrsErrorText(
|
|
198
196
|
asCentrsError(error, {
|
|
199
197
|
code: "input/invalid-command",
|
package/src/cli/transfer.ts
CHANGED
|
@@ -21,12 +21,15 @@ import {
|
|
|
21
21
|
transferEnvelope,
|
|
22
22
|
transferFanout,
|
|
23
23
|
transferOutputFormats,
|
|
24
|
+
transferVerboseLines,
|
|
24
25
|
transferVerbs,
|
|
25
26
|
} from "../index.ts";
|
|
26
27
|
import { assertNoQuickchrOverrideConflict } from "../resolver/index.ts";
|
|
27
28
|
import {
|
|
28
29
|
type CliCommandMetadata,
|
|
29
30
|
expectValue,
|
|
31
|
+
printTextHeader,
|
|
32
|
+
printTextNotes,
|
|
30
33
|
renderCommandHelp,
|
|
31
34
|
unknownFlagError,
|
|
32
35
|
} from "./common.ts";
|
|
@@ -167,7 +170,7 @@ export const transferCommand: CliCommandMetadata = {
|
|
|
167
170
|
},
|
|
168
171
|
{
|
|
169
172
|
flag: "--verbose",
|
|
170
|
-
description: "Show the resolved transport in text output.",
|
|
173
|
+
description: "Show the resolved transport in text output, on stderr.",
|
|
171
174
|
},
|
|
172
175
|
],
|
|
173
176
|
};
|
|
@@ -206,11 +209,12 @@ export async function runTransferCli(
|
|
|
206
209
|
const rendered = renderTransferEnvelope(envelope, format, {
|
|
207
210
|
verbose: request.verbose,
|
|
208
211
|
});
|
|
212
|
+
if (format === "text" && request.verbose) {
|
|
213
|
+
printTextHeader(transferVerboseLines(envelope));
|
|
214
|
+
}
|
|
209
215
|
if (!envelope.ok) {
|
|
210
|
-
console.error(
|
|
211
|
-
|
|
212
|
-
format === "text" ? rendered + formatTipsText(envelope.tips) : rendered,
|
|
213
|
-
);
|
|
216
|
+
console.error(rendered);
|
|
217
|
+
if (format === "text") printTextNotes(envelope);
|
|
214
218
|
return 1;
|
|
215
219
|
}
|
|
216
220
|
// A download to stdout (`local === "-"`) has already streamed the file bytes
|
|
@@ -221,6 +225,7 @@ export async function runTransferCli(
|
|
|
221
225
|
} else {
|
|
222
226
|
console.log(rendered);
|
|
223
227
|
}
|
|
228
|
+
if (format === "text") printTextNotes(envelope);
|
|
224
229
|
return 0;
|
|
225
230
|
} catch (error) {
|
|
226
231
|
// Drop credential fields before any renderer touches the request: error
|
|
@@ -243,14 +248,12 @@ export async function runTransferCli(
|
|
|
243
248
|
tips,
|
|
244
249
|
);
|
|
245
250
|
console.error(
|
|
246
|
-
// codeql[js/clear-text-logging] The transfer error path redacts password/cdbPassword before any renderer (redactTransferRequest), and buildTransferErrorEnvelope only emits targetInput.
|
|
247
251
|
renderTransferEnvelope(envelope, format, {
|
|
248
252
|
verbose: safeRequest?.verbose ?? false,
|
|
249
253
|
}),
|
|
250
254
|
);
|
|
251
255
|
} else {
|
|
252
256
|
console.error(
|
|
253
|
-
// codeql[js/clear-text-logging] Credentials are redacted from the request before rendering (redactTransferRequest) and never reach the error/stderr; same known false positive dismissed across all CLI commands.
|
|
254
257
|
formatCentrsErrorText(
|
|
255
258
|
asCentrsError(error, {
|
|
256
259
|
code: "input/invalid-command",
|
|
@@ -297,7 +300,6 @@ async function runTransferFanoutCli(
|
|
|
297
300
|
error,
|
|
298
301
|
);
|
|
299
302
|
console.error(
|
|
300
|
-
// codeql[js/clear-text-logging] No secret logged: request is redactTransferRequest'd (password/cdbPassword stripped) before the builder; fan-out error meta is {target:{},via,settings:{}}.
|
|
301
303
|
renderTransferFanoutEnvelope(envelope, format, {
|
|
302
304
|
verbose: request.verbose ?? false,
|
|
303
305
|
}),
|