@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.27
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 +163 -0
- package/GLOSSARY.txt +23 -1
- package/README.md +30 -18
- package/commands/api/AGENTS.md +21 -3
- package/commands/api/README.md +18 -7
- package/commands/api/examples.md +1 -1
- package/commands/btest/README.md +9 -0
- package/commands/btest/examples.md +7 -0
- package/commands/devices/README.md +72 -3
- package/commands/devices/examples.md +56 -1
- package/commands/execute/README.md +37 -6
- package/commands/execute/examples.md +24 -5
- package/commands/explain/README.md +675 -64
- package/commands/explain/examples.md +199 -42
- package/commands/retrieve/README.md +35 -3
- package/commands/retrieve/examples.md +114 -1
- package/commands/transfer/README.md +33 -4
- package/commands/transfer/examples.md +46 -0
- package/docs/CLI.md +30 -8
- package/docs/CONSTITUTION.md +14 -9
- package/docs/MATRIX.md +37 -22
- package/docs/errors/cdb/encrypt-failed.md +10 -0
- package/docs/errors/cdb/password-required.md +5 -4
- package/docs/errors/cdb/plaintext-backups.md +11 -0
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/input/invalid-routeros-path.md +25 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +9 -5
- package/src/api-fanout.ts +2 -0
- package/src/api.ts +46 -39
- package/src/btest.ts +7 -0
- package/src/cli/api.ts +18 -1
- package/src/cli/btest.ts +9 -0
- package/src/cli/devices.ts +76 -8
- package/src/cli/execute.ts +9 -0
- package/src/cli/explain.ts +206 -47
- package/src/cli/retrieve.ts +9 -0
- package/src/cli/terminal.ts +9 -0
- package/src/cli/transfer.ts +10 -1
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +28 -2
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/menu-path.ts +89 -0
- package/src/core/routeros-errors.ts +25 -0
- package/src/devices.ts +154 -9
- package/src/execute-cmr.ts +2 -8
- package/src/execute-fanout.ts +2 -0
- package/src/execute.ts +55 -20
- package/src/explain/args.ts +115 -10
- package/src/explain/blocks.ts +2 -2
- package/src/explain/catalog.ts +22 -12
- package/src/explain/group-faults.ts +162 -29
- package/src/explain/operators.ts +2 -1
- package/src/explain/pasted-prompt.ts +198 -0
- package/src/explain/pathresolve.ts +2 -2
- package/src/explain/scope-brace.ts +226 -11
- package/src/explain/segment.ts +138 -3
- package/src/explain/symbols.ts +148 -8
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +2 -2
- package/src/explain/write.ts +2 -2
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +397 -43
- package/src/index.ts +10 -0
- package/src/protocols/adapter.ts +90 -10
- package/src/protocols/btest-session.ts +10 -30
- package/src/protocols/ec-srp5.ts +19 -10
- package/src/protocols/mac-telnet-console.ts +2 -2
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/resolver/cdb.ts +58 -2
- package/src/resolver/index.ts +1 -0
- package/src/resolver/quickchr-provider.ts +5 -0
- package/src/resolver/selection.ts +11 -0
- package/src/retrieve-fanout.ts +2 -0
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +88 -27
- package/src/settings.ts +1 -0
- package/src/terminal.ts +7 -0
- package/src/transfer-fanout.ts +2 -0
- package/src/transfer.ts +117 -27
package/src/api.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
isCommandNode,
|
|
46
46
|
pathTokens,
|
|
47
47
|
} from "./core/inspect.ts";
|
|
48
|
+
import { assertRouterOsMenuPath } from "./core/menu-path.ts";
|
|
48
49
|
import {
|
|
49
50
|
compileQueryWords,
|
|
50
51
|
type ParsedQuery,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
promptForWriteConfirmation,
|
|
63
64
|
validateRouterOsScript,
|
|
64
65
|
} from "./execute.ts";
|
|
66
|
+
import { countExecutableStatements } from "./explain/segment.ts";
|
|
65
67
|
import {
|
|
66
68
|
assertOfflineSyntax,
|
|
67
69
|
isOfflineGateRejection,
|
|
@@ -159,6 +161,12 @@ export interface ApiRequest {
|
|
|
159
161
|
verbose?: boolean;
|
|
160
162
|
cdbFile?: string;
|
|
161
163
|
cdbPassword?: string;
|
|
164
|
+
/**
|
|
165
|
+
* `--match` selector for a `<router>` several CDB records share
|
|
166
|
+
* (`user=<name>`, `target=<addr>`, or a record type): the same selector
|
|
167
|
+
* `devices show --match` takes. Single-target only (#583).
|
|
168
|
+
*/
|
|
169
|
+
match?: string;
|
|
162
170
|
/** Opt-in host ARP resolution for a MAC target (`none` default, or `arp`). */
|
|
163
171
|
resolve?: string;
|
|
164
172
|
stdinIsTty?: boolean;
|
|
@@ -541,7 +549,7 @@ async function* streamResolvedApi(
|
|
|
541
549
|
trace.validation = validation;
|
|
542
550
|
resolved = await withQueryBooleans(resolved, backend, result.queryMenu);
|
|
543
551
|
} else {
|
|
544
|
-
validation =
|
|
552
|
+
validation = disabledApiValidationMeta(resolved);
|
|
545
553
|
}
|
|
546
554
|
} catch (error) {
|
|
547
555
|
await backend.close();
|
|
@@ -824,16 +832,6 @@ function adapterForResolved(resolved: ResolvedApiRequest): ProtocolAdapter {
|
|
|
824
832
|
});
|
|
825
833
|
}
|
|
826
834
|
|
|
827
|
-
function disabledValidationMeta(): EnvelopeValidationMeta {
|
|
828
|
-
return {
|
|
829
|
-
enabled: false,
|
|
830
|
-
source: "disabled",
|
|
831
|
-
result: "skipped",
|
|
832
|
-
syntax: false,
|
|
833
|
-
semantic: false,
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
835
|
export async function resolveApiRequest(
|
|
838
836
|
request: ApiRequest,
|
|
839
837
|
env: Record<string, string | undefined>,
|
|
@@ -946,6 +944,7 @@ export async function resolveApiRequest(
|
|
|
946
944
|
targetInput: request.targetInput,
|
|
947
945
|
cdbFile: request.cdbFile,
|
|
948
946
|
cdbPassword: request.cdbPassword,
|
|
947
|
+
match: request.match,
|
|
949
948
|
},
|
|
950
949
|
env,
|
|
951
950
|
config,
|
|
@@ -1347,7 +1346,8 @@ interface ApiValidationResult {
|
|
|
1347
1346
|
* validator is `/console/inspect`, never `:put [:parse]`. Path existence and
|
|
1348
1347
|
* add/set attribute validity both come from `request=child` — command-level
|
|
1349
1348
|
* `request=completion` is unsafe on RouterOS 7.12.2 (`core/inspect.ts`). A
|
|
1350
|
-
* `/execute` script is a CLI string
|
|
1349
|
+
* `/execute` script is a CLI string, so it has no `semantic` summary; a
|
|
1350
|
+
* structured request runs no `:parse`, so it has no `syntax` summary (#535).
|
|
1351
1351
|
*/
|
|
1352
1352
|
async function validateApiRequest(
|
|
1353
1353
|
resolved: ResolvedApiRequest,
|
|
@@ -1383,8 +1383,7 @@ async function validateApiRequest(
|
|
|
1383
1383
|
? "not-applicable"
|
|
1384
1384
|
: API_SCRIPT_DEVICE_STAGE_SOURCE,
|
|
1385
1385
|
result: "passed",
|
|
1386
|
-
syntax:
|
|
1387
|
-
semantic: "not-applicable",
|
|
1386
|
+
...(script !== undefined ? { syntax: true } : {}),
|
|
1388
1387
|
stages: [
|
|
1389
1388
|
{
|
|
1390
1389
|
stage: "offline",
|
|
@@ -1468,13 +1467,7 @@ async function validateApiRequest(
|
|
|
1468
1467
|
}
|
|
1469
1468
|
}
|
|
1470
1469
|
return {
|
|
1471
|
-
validation:
|
|
1472
|
-
enabled: true,
|
|
1473
|
-
source: "/console/inspect request=child",
|
|
1474
|
-
result: "passed",
|
|
1475
|
-
syntax: false,
|
|
1476
|
-
semantic: true,
|
|
1477
|
-
},
|
|
1470
|
+
validation: structuredValidationPassed(resolved),
|
|
1478
1471
|
tips,
|
|
1479
1472
|
queryMenu: isCommand ? `/${parent.join("/")}` : resolved.path,
|
|
1480
1473
|
};
|
|
@@ -1513,11 +1506,7 @@ async function validateApiRequest(
|
|
|
1513
1506
|
}
|
|
1514
1507
|
return {
|
|
1515
1508
|
validation: {
|
|
1516
|
-
|
|
1517
|
-
source: "/console/inspect request=child",
|
|
1518
|
-
result: "passed",
|
|
1519
|
-
syntax: false,
|
|
1520
|
-
semantic: true,
|
|
1509
|
+
...structuredValidationPassed(resolved),
|
|
1521
1510
|
availableAttributes: available,
|
|
1522
1511
|
},
|
|
1523
1512
|
tips,
|
|
@@ -1526,13 +1515,7 @@ async function validateApiRequest(
|
|
|
1526
1515
|
}
|
|
1527
1516
|
|
|
1528
1517
|
return {
|
|
1529
|
-
validation:
|
|
1530
|
-
enabled: true,
|
|
1531
|
-
source: "/console/inspect request=child",
|
|
1532
|
-
result: "passed",
|
|
1533
|
-
syntax: false,
|
|
1534
|
-
semantic: true,
|
|
1535
|
-
},
|
|
1518
|
+
validation: structuredValidationPassed(resolved),
|
|
1536
1519
|
tips,
|
|
1537
1520
|
queryMenu: resolved.path,
|
|
1538
1521
|
};
|
|
@@ -1857,6 +1840,12 @@ export function buildApiErrorEnvelopeFromResolved(
|
|
|
1857
1840
|
trace?: ValidationTrace,
|
|
1858
1841
|
): ApiErrorEnvelope {
|
|
1859
1842
|
const centrsError = asApiError(error);
|
|
1843
|
+
// A structured request is one operation; only a `/execute` script can
|
|
1844
|
+
// carry several statements into one dispatch (#549), at the top level or
|
|
1845
|
+
// inside an executed control scope.
|
|
1846
|
+
const script = resolved.scriptMode ? scriptOf(resolved) : undefined;
|
|
1847
|
+
const multiStatement =
|
|
1848
|
+
script !== undefined && countExecutableStatements(script) > 1;
|
|
1860
1849
|
return {
|
|
1861
1850
|
ok: false,
|
|
1862
1851
|
error: serializeCentrsError(centrsError),
|
|
@@ -1866,6 +1855,7 @@ export function buildApiErrorEnvelopeFromResolved(
|
|
|
1866
1855
|
isApiMutating(resolved.method, resolved.path),
|
|
1867
1856
|
centrsError,
|
|
1868
1857
|
trace,
|
|
1858
|
+
multiStatement,
|
|
1869
1859
|
),
|
|
1870
1860
|
],
|
|
1871
1861
|
tips: [],
|
|
@@ -1877,6 +1867,22 @@ export function buildApiErrorEnvelopeFromResolved(
|
|
|
1877
1867
|
};
|
|
1878
1868
|
}
|
|
1879
1869
|
|
|
1870
|
+
/** A structured request the device stage passed; stage 1 does not apply to it. */
|
|
1871
|
+
function structuredValidationPassed(
|
|
1872
|
+
resolved: ResolvedApiRequest,
|
|
1873
|
+
): EnvelopeValidationMeta {
|
|
1874
|
+
return {
|
|
1875
|
+
enabled: true,
|
|
1876
|
+
source: API_DEVICE_STAGE_SOURCE,
|
|
1877
|
+
result: "passed",
|
|
1878
|
+
semantic: true,
|
|
1879
|
+
stages: [
|
|
1880
|
+
offlineApiStage(resolved),
|
|
1881
|
+
{ stage: "device", source: API_DEVICE_STAGE_SOURCE, result: "passed" },
|
|
1882
|
+
],
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1880
1886
|
/**
|
|
1881
1887
|
* `meta.validation` for an api error envelope. A stage-1 rejection opened no
|
|
1882
1888
|
* connection, so the meta must not report `/console/inspect` as the validator
|
|
@@ -1896,7 +1902,6 @@ function failedApiValidationMeta(
|
|
|
1896
1902
|
source: OFFLINE_GATE_SOURCE,
|
|
1897
1903
|
result: "failed",
|
|
1898
1904
|
syntax: false,
|
|
1899
|
-
semantic: "not-applicable",
|
|
1900
1905
|
stages: [
|
|
1901
1906
|
{ stage: "offline", source: OFFLINE_GATE_SOURCE, result: "failed" },
|
|
1902
1907
|
{
|
|
@@ -1911,14 +1916,14 @@ function failedApiValidationMeta(
|
|
|
1911
1916
|
// Only a `validation/*` error identifies the device stage as the rejecter.
|
|
1912
1917
|
// Everything downstream of a passed gate — a RouterOS fault on the request,
|
|
1913
1918
|
// a transport drop — is served by `trace.validation` before this function is
|
|
1914
|
-
// ever reached.
|
|
1919
|
+
// ever reached. A script is refused by `:parse` (syntax); a structured
|
|
1920
|
+
// request by `/console/inspect` (names) (#535).
|
|
1915
1921
|
if (error.code.startsWith("validation/")) {
|
|
1916
1922
|
return {
|
|
1917
1923
|
enabled: true,
|
|
1918
1924
|
source: apiDeviceStageSource(resolved),
|
|
1919
1925
|
result: "failed",
|
|
1920
|
-
syntax: false,
|
|
1921
|
-
semantic: resolved.scriptMode ? "not-applicable" : false,
|
|
1926
|
+
...(resolved.scriptMode ? { syntax: false } : { semantic: false }),
|
|
1922
1927
|
stages: [offlineApiStage(resolved, trace), deviceStageFailed(resolved)],
|
|
1923
1928
|
};
|
|
1924
1929
|
}
|
|
@@ -2012,8 +2017,6 @@ function disabledApiValidationMeta(
|
|
|
2012
2017
|
enabled: false,
|
|
2013
2018
|
source: "disabled",
|
|
2014
2019
|
result: "skipped",
|
|
2015
|
-
syntax: false,
|
|
2016
|
-
semantic: resolved.scriptMode ? "not-applicable" : false,
|
|
2017
2020
|
stages: [
|
|
2018
2021
|
{ stage: "offline", source: OFFLINE_GATE_SOURCE, result: "skipped" },
|
|
2019
2022
|
{
|
|
@@ -2181,6 +2184,10 @@ export function validateApiRequestShape(request: ApiRequest): void {
|
|
|
2181
2184
|
"Pass a path with at least one menu segment, such as `ip/address`.",
|
|
2182
2185
|
});
|
|
2183
2186
|
}
|
|
2187
|
+
// Before transport choice and regardless of --validate: a `#`/`?` in the
|
|
2188
|
+
// path cut `/print` off the REST URL and a GET read posted to the bare
|
|
2189
|
+
// command, rebooting the router (#586).
|
|
2190
|
+
assertRouterOsMenuPath(normalized.path, { endpoint: request.endpoint });
|
|
2184
2191
|
}
|
|
2185
2192
|
|
|
2186
2193
|
/**
|
package/src/btest.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface BtestClientRequest {
|
|
|
113
113
|
natMode?: boolean;
|
|
114
114
|
cdbFile?: string;
|
|
115
115
|
cdbPassword?: string;
|
|
116
|
+
/**
|
|
117
|
+
* `--match` selector for a `<router>` several CDB records share
|
|
118
|
+
* (`user=<name>`, `target=<addr>`, or a record type): the same selector
|
|
119
|
+
* `devices show --match` takes. Single-target only (#583).
|
|
120
|
+
*/
|
|
121
|
+
match?: string;
|
|
116
122
|
env?: Record<string, string | undefined>;
|
|
117
123
|
signal?: AbortSignal;
|
|
118
124
|
/** Live per-interval frame callback (text/csv streaming). */
|
|
@@ -264,6 +270,7 @@ export async function btestClient(
|
|
|
264
270
|
targetInput: request.targetInput,
|
|
265
271
|
cdbFile: request.cdbFile,
|
|
266
272
|
cdbPassword: request.cdbPassword,
|
|
273
|
+
match: request.match,
|
|
267
274
|
},
|
|
268
275
|
env,
|
|
269
276
|
);
|
package/src/cli/api.ts
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
formatCentrsErrorText,
|
|
24
24
|
} from "../errors.ts";
|
|
25
25
|
import { describeCentrs } from "../index.ts";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
assertNoMatchInFanout,
|
|
28
|
+
assertNoQuickchrOverrideConflict,
|
|
29
|
+
} from "../resolver/index.ts";
|
|
27
30
|
import {
|
|
28
31
|
type CliCommandMetadata,
|
|
29
32
|
expectValue,
|
|
@@ -165,6 +168,12 @@ export const apiCommand: CliCommandMetadata = {
|
|
|
165
168
|
valueName: "<password>",
|
|
166
169
|
description: "Decrypt an encrypted WinBox CDB file.",
|
|
167
170
|
},
|
|
171
|
+
{
|
|
172
|
+
flag: "--match",
|
|
173
|
+
valueName: "<user=name|target=addr|record-type>",
|
|
174
|
+
description:
|
|
175
|
+
"Pick one of several CDB records saved for <router> (as `devices show --match`); single target only.",
|
|
176
|
+
},
|
|
168
177
|
{
|
|
169
178
|
flag: "--resolve",
|
|
170
179
|
valueName: "<none|arp>",
|
|
@@ -330,6 +339,9 @@ export function parseApiCliArgs(args: readonly string[]): ApiCliArgs {
|
|
|
330
339
|
case "--cdb-password":
|
|
331
340
|
parsed.cdbPassword = expectValue(args, ++index, arg);
|
|
332
341
|
break;
|
|
342
|
+
case "--match":
|
|
343
|
+
parsed.match = expectValue(args, ++index, arg);
|
|
344
|
+
break;
|
|
333
345
|
case "--resolve":
|
|
334
346
|
parsed.resolve = expectValue(args, ++index, arg);
|
|
335
347
|
break;
|
|
@@ -473,6 +485,11 @@ export async function runApiCli(args: readonly string[]): Promise<number> {
|
|
|
473
485
|
selectionFlags.quickchr[0] ?? "",
|
|
474
486
|
);
|
|
475
487
|
}
|
|
488
|
+
// `--match` + fan-out is refused here too, before `--input -` can block
|
|
489
|
+
// on stdin; `apiFanout` keeps the same check for library callers (#583).
|
|
490
|
+
if (isFanoutMode(selectionFlags, targetPositionals.length)) {
|
|
491
|
+
assertNoMatchInFanout(parsed.match);
|
|
492
|
+
}
|
|
476
493
|
|
|
477
494
|
// Resolve `--input` (file / stdin) before handing the body to the orchestrator.
|
|
478
495
|
if (parsed.inputPath !== undefined) {
|
package/src/cli/btest.ts
CHANGED
|
@@ -145,6 +145,12 @@ export const btestCommand: CliCommandMetadata = {
|
|
|
145
145
|
valueName: "<password>",
|
|
146
146
|
description: "(client) Decrypt an encrypted WinBox CDB file.",
|
|
147
147
|
},
|
|
148
|
+
{
|
|
149
|
+
flag: "--match",
|
|
150
|
+
valueName: "<user=name|target=addr|record-type>",
|
|
151
|
+
description:
|
|
152
|
+
"(client) Pick one of several CDB records saved for <router> (as `devices show --match`); `btest server` rejects it.",
|
|
153
|
+
},
|
|
148
154
|
{
|
|
149
155
|
flag: "--format",
|
|
150
156
|
valueName: `<${btestOutputFormats.join("|")}>`,
|
|
@@ -302,6 +308,9 @@ function parseClientArgs(args: readonly string[]): ClientCliArgs {
|
|
|
302
308
|
case "--cdb-password":
|
|
303
309
|
request.cdbPassword = expectValue(args, ++i, arg);
|
|
304
310
|
break;
|
|
311
|
+
case "--match":
|
|
312
|
+
request.match = expectValue(args, ++i, arg);
|
|
313
|
+
break;
|
|
305
314
|
case "--csv":
|
|
306
315
|
parsed.format = "csv";
|
|
307
316
|
break;
|
package/src/cli/devices.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
describeCentrs,
|
|
23
23
|
devicesOutputFormats,
|
|
24
24
|
editInteractiveOnlyError,
|
|
25
|
+
encryptCdb,
|
|
25
26
|
initCdb,
|
|
26
27
|
listDevices,
|
|
27
28
|
listGroups,
|
|
@@ -48,10 +49,20 @@ import {
|
|
|
48
49
|
export const devicesCommand: CliCommandMetadata = {
|
|
49
50
|
name: "devices",
|
|
50
51
|
usage:
|
|
51
|
-
"centrs devices <init|list|show|groups|add|edit|set|remove> [args] [flags]",
|
|
52
|
+
"centrs devices <init|encrypt|list|show|groups|add|edit|set|remove> [args] [flags]",
|
|
52
53
|
summary:
|
|
53
54
|
"Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB.",
|
|
54
55
|
options: [
|
|
56
|
+
{
|
|
57
|
+
flag: "--encrypt",
|
|
58
|
+
description:
|
|
59
|
+
"`init` only — require encrypted creation with a non-empty CDB password; refuse an existing open CDB.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
flag: "--no-backup",
|
|
63
|
+
description:
|
|
64
|
+
"`encrypt` only — skip the plaintext pre-conversion snapshot. Without that snapshot, a forgotten password cannot be recovered.",
|
|
65
|
+
},
|
|
55
66
|
{
|
|
56
67
|
flag: "--cdb-file",
|
|
57
68
|
valueName: "<path>",
|
|
@@ -62,7 +73,7 @@ export const devicesCommand: CliCommandMetadata = {
|
|
|
62
73
|
flag: "--cdb-password",
|
|
63
74
|
valueName: "<secret>",
|
|
64
75
|
description:
|
|
65
|
-
"Decrypt an encrypted CDB. Falls back to `CENTRS_CDB_PASSWORD`. Also used to re-encrypt on writes.",
|
|
76
|
+
"Decrypt an encrypted CDB. Falls back to `CENTRS_CDB_PASSWORD`. Also used for `init --encrypt`, `encrypt`, and to re-encrypt on writes.",
|
|
66
77
|
},
|
|
67
78
|
{
|
|
68
79
|
flag: "--via",
|
|
@@ -200,6 +211,7 @@ export const devicesCommand: CliCommandMetadata = {
|
|
|
200
211
|
|
|
201
212
|
type DevicesSubcommand =
|
|
202
213
|
| "init"
|
|
214
|
+
| "encrypt"
|
|
203
215
|
| "list"
|
|
204
216
|
| "show"
|
|
205
217
|
| "groups"
|
|
@@ -226,6 +238,8 @@ interface DevicesCliArgs {
|
|
|
226
238
|
subcommand?: DevicesSubcommand;
|
|
227
239
|
cdbFile?: string;
|
|
228
240
|
cdbPassword?: string;
|
|
241
|
+
encrypt?: boolean;
|
|
242
|
+
noBackup?: boolean;
|
|
229
243
|
group?: string;
|
|
230
244
|
where?: string[];
|
|
231
245
|
via?: string;
|
|
@@ -254,20 +268,44 @@ interface DevicesCliArgs {
|
|
|
254
268
|
bbox?: BboxPredicate;
|
|
255
269
|
}
|
|
256
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Flags `init`/`encrypt` honor. They touch only the CDB file, never a record,
|
|
273
|
+
* so record, selection, and provenance flags would be silently ignored there.
|
|
274
|
+
*/
|
|
275
|
+
const CDB_FILE_FLAGS: Readonly<Record<"init" | "encrypt", readonly string[]>> =
|
|
276
|
+
{
|
|
277
|
+
init: ["--cdb-file", "--cdb-password", "--format", "--json", "--encrypt"],
|
|
278
|
+
encrypt: [
|
|
279
|
+
"--cdb-file",
|
|
280
|
+
"--cdb-password",
|
|
281
|
+
"--format",
|
|
282
|
+
"--json",
|
|
283
|
+
"--no-backup",
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
|
|
257
287
|
function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
258
288
|
const parsed: DevicesCliArgs = {};
|
|
259
289
|
const positional: string[] = [];
|
|
290
|
+
const suppliedFlags: string[] = [];
|
|
260
291
|
|
|
261
292
|
for (let index = 0; index < args.length; index += 1) {
|
|
262
293
|
const arg = args[index];
|
|
263
294
|
if (arg === undefined) {
|
|
264
295
|
continue;
|
|
265
296
|
}
|
|
297
|
+
if (arg.startsWith("-")) suppliedFlags.push(arg);
|
|
266
298
|
switch (arg) {
|
|
267
299
|
case "--help":
|
|
268
300
|
case "-h":
|
|
269
301
|
parsed.help = true;
|
|
270
302
|
break;
|
|
303
|
+
case "--encrypt":
|
|
304
|
+
parsed.encrypt = true;
|
|
305
|
+
break;
|
|
306
|
+
case "--no-backup":
|
|
307
|
+
parsed.noBackup = true;
|
|
308
|
+
break;
|
|
271
309
|
case "--cdb-file":
|
|
272
310
|
parsed.cdbFile = expectValue(args, ++index, arg);
|
|
273
311
|
break;
|
|
@@ -382,6 +420,7 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
382
420
|
|
|
383
421
|
const subcommands: readonly DevicesSubcommand[] = [
|
|
384
422
|
"init",
|
|
423
|
+
"encrypt",
|
|
385
424
|
"list",
|
|
386
425
|
"show",
|
|
387
426
|
"groups",
|
|
@@ -393,17 +432,23 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
393
432
|
const [rawSub, ...rest] = positional;
|
|
394
433
|
if (rawSub === undefined) {
|
|
395
434
|
throw new Error(
|
|
396
|
-
"`centrs devices` requires a subcommand: init, list, show, groups, add, edit, set, or remove.",
|
|
435
|
+
"`centrs devices` requires a subcommand: init, encrypt, list, show, groups, add, edit, set, or remove.",
|
|
397
436
|
);
|
|
398
437
|
}
|
|
399
438
|
// Resolve aliases (print/get/rm/delete) to the canonical verb before validating.
|
|
400
439
|
const sub = SUBCOMMAND_ALIASES[rawSub] ?? (rawSub as DevicesSubcommand);
|
|
401
440
|
if (!subcommands.includes(sub)) {
|
|
402
441
|
throw new Error(
|
|
403
|
-
`Unknown devices subcommand: ${rawSub}. Use init, list, show, groups, add, edit, set, or remove (aliases: print, get, rm, delete).`,
|
|
442
|
+
`Unknown devices subcommand: ${rawSub}. Use init, encrypt, list, show, groups, add, edit, set, or remove (aliases: print, get, rm, delete).`,
|
|
404
443
|
);
|
|
405
444
|
}
|
|
406
445
|
parsed.subcommand = sub;
|
|
446
|
+
if (parsed.encrypt && sub !== "init")
|
|
447
|
+
throw new Error(
|
|
448
|
+
"--encrypt is supported only by devices init; use devices encrypt for conversion.",
|
|
449
|
+
);
|
|
450
|
+
if (parsed.noBackup && sub !== "encrypt")
|
|
451
|
+
throw new Error("--no-backup is supported only by devices encrypt.");
|
|
407
452
|
|
|
408
453
|
// Reject flags that don't apply to the resolved subcommand instead of
|
|
409
454
|
// silently ignoring the user's input: `--where`/`--near`/`--bbox` select
|
|
@@ -446,6 +491,18 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
446
491
|
context: { subcommand: sub, flag: geoFlag },
|
|
447
492
|
});
|
|
448
493
|
}
|
|
494
|
+
if (sub === "init" || sub === "encrypt") {
|
|
495
|
+
const allowed = CDB_FILE_FLAGS[sub];
|
|
496
|
+
const ignored = suppliedFlags.find((flag) => !allowed.includes(flag));
|
|
497
|
+
if (ignored !== undefined) {
|
|
498
|
+
throw new CentrsError({
|
|
499
|
+
code: "input/invalid-command",
|
|
500
|
+
summary: `\`${ignored}\` is not supported by \`centrs devices ${sub}\`.`,
|
|
501
|
+
remediation: `\`centrs devices ${sub}\` accepts only ${allowed.map((flag) => `\`${flag}\``).join(", ")}. Edit records with \`centrs devices add\`/\`set\`.`,
|
|
502
|
+
context: { subcommand: sub, flag: ignored },
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
449
506
|
|
|
450
507
|
if (sub === "show" || sub === "edit" || sub === "remove") {
|
|
451
508
|
if (rest.length !== 1) {
|
|
@@ -511,7 +568,14 @@ function parseKvArg(token: string): CommentKvUpdate {
|
|
|
511
568
|
// canonical key a `--lon`/`--lat`/`--altitude` flag would use. A no-op for
|
|
512
569
|
// every non-geo key.
|
|
513
570
|
const key = canonicalizeGeoKey(token.slice(0, eq));
|
|
514
|
-
|
|
571
|
+
const value = parseKvValue(token.slice(eq + 1));
|
|
572
|
+
// An empty value removes the token (`devices set <t> via=` deletes the
|
|
573
|
+
// `via` override) instead of storing a literally-empty value, per
|
|
574
|
+
// `commands/devices/README.md` (Comment kv-soup). The core upsert/remove
|
|
575
|
+
// model lives in `applyCommentKv` (`src/resolver/comment-kv.ts`), which
|
|
576
|
+
// treats `value: null` as removal; the CLI lowers `key=` to that form
|
|
577
|
+
// here. There is currently no need to store a literally-empty value.
|
|
578
|
+
return { key, value: value === "" ? null : value };
|
|
515
579
|
}
|
|
516
580
|
|
|
517
581
|
/**
|
|
@@ -610,12 +674,16 @@ export async function runDevicesCli(args: readonly string[]): Promise<number> {
|
|
|
610
674
|
}
|
|
611
675
|
|
|
612
676
|
envSnapshot = Bun.env;
|
|
613
|
-
if (parsed.subcommand === "init") {
|
|
614
|
-
const
|
|
677
|
+
if (parsed.subcommand === "init" || parsed.subcommand === "encrypt") {
|
|
678
|
+
const options = {
|
|
615
679
|
cdbFile: parsed.cdbFile,
|
|
616
680
|
cdbPassword: parsed.cdbPassword,
|
|
617
681
|
env: envSnapshot,
|
|
618
|
-
}
|
|
682
|
+
};
|
|
683
|
+
const envelope =
|
|
684
|
+
parsed.subcommand === "init"
|
|
685
|
+
? await initCdb({ ...options, encrypt: parsed.encrypt })
|
|
686
|
+
: await encryptCdb({ ...options, skipBackup: parsed.noBackup });
|
|
619
687
|
console.log(renderDevicesEnvelope(envelope, parsed.format ?? "text"));
|
|
620
688
|
return 0;
|
|
621
689
|
}
|
package/src/cli/execute.ts
CHANGED
|
@@ -121,6 +121,12 @@ export const executeCommand: CliCommandMetadata = {
|
|
|
121
121
|
valueName: "<password>",
|
|
122
122
|
description: "Decrypt an encrypted WinBox CDB file.",
|
|
123
123
|
},
|
|
124
|
+
{
|
|
125
|
+
flag: "--match",
|
|
126
|
+
valueName: "<user=name|target=addr|record-type>",
|
|
127
|
+
description:
|
|
128
|
+
"Pick one of several CDB records saved for <router> (as `devices show --match`); single target only.",
|
|
129
|
+
},
|
|
124
130
|
{
|
|
125
131
|
flag: "--resolve",
|
|
126
132
|
valueName: "<none|arp>",
|
|
@@ -262,6 +268,9 @@ export function parseExecuteCliArgs(args: readonly string[]): ExecuteCliArgs {
|
|
|
262
268
|
case "--cdb-password":
|
|
263
269
|
parsed.cdbPassword = expectValue(args, ++index, arg);
|
|
264
270
|
break;
|
|
271
|
+
case "--match":
|
|
272
|
+
parsed.match = expectValue(args, ++index, arg);
|
|
273
|
+
break;
|
|
265
274
|
case "--resolve":
|
|
266
275
|
parsed.resolve = expectValue(args, ++index, arg);
|
|
267
276
|
break;
|