@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/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
|
}),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `centrs --version` / `-V` (#463): the package version, for scripts, agents and
|
|
3
|
+
* field reports.
|
|
4
|
+
*
|
|
5
|
+
* Text output is the bare version string (`0.1.9-next.0`), so a script can use
|
|
6
|
+
* it as is. `--json` / `--format json|yaml` return the standard envelope with
|
|
7
|
+
* `data.version`. Format comes from flags only, never `CENTRS_FORMAT`, so a
|
|
8
|
+
* plain `centrs --version` always prints the bare string.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CentrsEnvelope } from "../core/envelope.ts";
|
|
12
|
+
import { centrsVersion } from "../core/version.ts";
|
|
13
|
+
import { toYaml } from "../core/yaml.ts";
|
|
14
|
+
import { CentrsError, formatCentrsErrorText } from "../errors.ts";
|
|
15
|
+
|
|
16
|
+
const versionFormats = ["text", "json", "yaml"] as const;
|
|
17
|
+
type VersionFormat = (typeof versionFormats)[number];
|
|
18
|
+
|
|
19
|
+
export type VersionEnvelope = CentrsEnvelope<
|
|
20
|
+
{ version: string },
|
|
21
|
+
{ command: "version" }
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export function versionEnvelope(): VersionEnvelope {
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
data: { version: centrsVersion },
|
|
28
|
+
warnings: [],
|
|
29
|
+
tips: [],
|
|
30
|
+
meta: {
|
|
31
|
+
target: {},
|
|
32
|
+
via: null,
|
|
33
|
+
settings: {},
|
|
34
|
+
operation: { command: "version" },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseVersionFormat(args: readonly string[]): VersionFormat {
|
|
40
|
+
let format: VersionFormat = "text";
|
|
41
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
42
|
+
const arg = args[index];
|
|
43
|
+
if (arg === "--json") {
|
|
44
|
+
format = "json";
|
|
45
|
+
} else if (arg === "--yaml") {
|
|
46
|
+
format = "yaml";
|
|
47
|
+
} else if (arg === "--format") {
|
|
48
|
+
const value = args[++index];
|
|
49
|
+
if (!versionFormats.includes(value as VersionFormat)) {
|
|
50
|
+
throw new CentrsError({
|
|
51
|
+
code: "settings/invalid-format",
|
|
52
|
+
summary: `--format must be one of ${versionFormats.join(", ")}; got ${value ?? "(missing)"}.`,
|
|
53
|
+
remediation: `Choose one of ${versionFormats.join(", ")}.`,
|
|
54
|
+
context: { format: value },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
format = value as VersionFormat;
|
|
58
|
+
} else {
|
|
59
|
+
throw new CentrsError({
|
|
60
|
+
code: "input/invalid-command",
|
|
61
|
+
summary: `\`centrs --version\` takes only an output format; got ${arg}.`,
|
|
62
|
+
remediation:
|
|
63
|
+
"Run `centrs --version`, or `centrs --version --json` for the envelope.",
|
|
64
|
+
context: { arg },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return format;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function runVersionCli(args: readonly string[]): number {
|
|
72
|
+
let format: VersionFormat;
|
|
73
|
+
try {
|
|
74
|
+
format = parseVersionFormat(args);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(formatCentrsErrorText(error as CentrsError));
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
if (format === "json") {
|
|
80
|
+
console.log(JSON.stringify(versionEnvelope(), null, 2));
|
|
81
|
+
} else if (format === "yaml") {
|
|
82
|
+
console.log(toYaml(versionEnvelope()));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(centrsVersion);
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { retrieveCommand, runRetrieveCli } from "./cli/retrieve.ts";
|
|
|
12
12
|
import { runSettingsCli, settingsCommand } from "./cli/settings.ts";
|
|
13
13
|
import { runTerminalCli, terminalCommand } from "./cli/terminal.ts";
|
|
14
14
|
import { runTransferCli, transferCommand } from "./cli/transfer.ts";
|
|
15
|
+
import { runVersionCli } from "./cli/version.ts";
|
|
15
16
|
import { asCentrsError, formatCentrsErrorText } from "./errors.ts";
|
|
16
17
|
import { describeCentrs, plannedProtocols, plannedSurfaces } from "./index.ts";
|
|
17
18
|
|
|
@@ -48,7 +49,7 @@ export function renderCliHelp(): string {
|
|
|
48
49
|
(command) => ` ${command.name.padEnd(12)} ${command.summary}`,
|
|
49
50
|
),
|
|
50
51
|
"",
|
|
51
|
-
"Use `centrs <command> --help` for command-specific options.",
|
|
52
|
+
"Use `centrs <command> --help` for command-specific options, and `centrs --version` (`-V`) for the version alone.",
|
|
52
53
|
].join("\n");
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -60,6 +61,9 @@ export async function runCli(
|
|
|
60
61
|
console.log(renderCliHelp());
|
|
61
62
|
return 0;
|
|
62
63
|
}
|
|
64
|
+
if (args[0] === "--version" || args[0] === "-V") {
|
|
65
|
+
return runVersionCli(args.slice(1));
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
const [command, ...rest] = args;
|
|
65
69
|
if (command === "retrieve") {
|
|
@@ -122,7 +126,6 @@ export async function runCli(
|
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
console.error(
|
|
125
|
-
// 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.
|
|
126
129
|
formatCentrsErrorText(
|
|
127
130
|
asCentrsError(new Error(`Unknown centrs command: ${command}`), {
|
|
128
131
|
code: "input/invalid-command",
|
|
@@ -137,7 +140,6 @@ export async function runCli(
|
|
|
137
140
|
// to a structured envelope, so reaching here means an unconverted throw
|
|
138
141
|
// escaped. Never let a raw stack trace reach the user.
|
|
139
142
|
console.error(
|
|
140
|
-
// 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.
|
|
141
143
|
formatCentrsErrorText(
|
|
142
144
|
asCentrsError(error, {
|
|
143
145
|
code: "internal/unhandled",
|