@tikoci/centrs 0.1.2 → 0.1.4
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 +98 -0
- package/GLOSSARY.txt +284 -0
- package/LICENSE +21 -0
- package/README.md +55 -11
- package/SECURITY.md +3 -3
- package/commands/AGENTS.md +7 -1
- package/commands/api/README.md +46 -27
- package/commands/api/examples.md +124 -6
- package/commands/btest/README.md +21 -32
- package/commands/check/README.md +376 -53
- package/commands/check/examples.md +390 -0
- package/commands/devices/README.md +177 -37
- package/commands/devices/examples.md +148 -0
- package/commands/discover/README.md +5 -11
- package/commands/execute/README.md +73 -26
- package/commands/execute/examples.md +131 -9
- package/commands/explain/README.md +1716 -0
- package/commands/explain/examples.md +606 -0
- package/commands/retrieve/README.md +55 -71
- package/commands/retrieve/examples.md +56 -9
- package/commands/settings/README.md +286 -22
- package/commands/settings/examples.md +408 -0
- package/commands/terminal/README.md +6 -0
- package/commands/transfer/README.md +57 -19
- package/commands/transfer/examples.md +95 -0
- package/docs/CLI.md +353 -0
- package/docs/CONSTITUTION.md +102 -26
- package/docs/MATRIX.md +106 -173
- package/docs/errors/README.md +5 -1
- package/docs/errors/cdb/empty-selection.md +19 -0
- package/docs/errors/input/incomplete-gps.md +9 -0
- package/docs/errors/input/invalid-altitude.md +9 -0
- package/docs/errors/input/invalid-bbox.md +9 -0
- package/docs/errors/input/invalid-coordinate.md +9 -0
- package/docs/errors/input/invalid-radius.md +9 -0
- package/docs/errors/internal/settings-failed.md +9 -0
- package/docs/errors/quickchr/machine-not-found.md +22 -0
- package/docs/errors/quickchr/machine-stopped.md +20 -0
- package/docs/errors/quickchr/package-unavailable.md +27 -0
- package/docs/errors/quickchr/unsupported-via.md +35 -0
- package/docs/errors/quickchr/unsupported.md +35 -0
- package/docs/errors/routeros/version-unsupported.md +26 -0
- package/docs/errors/settings/consequential-value.md +26 -0
- package/docs/errors/settings/invalid-boolean.md +21 -3
- package/docs/errors/settings/invalid-format.md +18 -3
- package/docs/errors/settings/invalid-integer.md +18 -3
- package/docs/errors/settings/invalid-timeout.md +20 -3
- package/docs/errors/settings/invalid-via.md +22 -3
- package/docs/errors/settings/reserved-key.md +29 -0
- package/docs/errors/settings/skip-env-file-active.md +25 -0
- package/docs/errors/settings/unknown-key.md +25 -0
- package/docs/errors/transport/provider-trust.md +29 -0
- package/docs/errors/usage/fanout-not-supported.md +16 -0
- package/docs/errors/usage/stdin-ignored.md +28 -0
- package/docs/errors/validation/unknown-attribute.md +37 -4
- package/package.json +49 -14
- package/src/api-fanout.ts +469 -0
- package/src/api.ts +171 -56
- package/src/cli/api.ts +156 -13
- package/src/cli/btest.ts +38 -4
- package/src/cli/common.ts +11 -0
- package/src/cli/devices.ts +235 -6
- package/src/cli/discover.ts +3 -1
- package/src/cli/execute.ts +129 -9
- package/src/cli/explain.ts +503 -0
- package/src/cli/mcp.ts +8 -1
- package/src/cli/retrieve.ts +167 -90
- package/src/cli/selection.ts +260 -0
- package/src/cli/settings.ts +286 -0
- package/src/cli/terminal.ts +22 -7
- package/src/cli/transfer.ts +192 -51
- package/src/cli.ts +22 -2
- package/src/core/envelope.ts +5 -2
- package/src/core/error-catalog.ts +95 -0
- package/src/core/fanout.ts +143 -2
- package/src/core/preflight.ts +306 -0
- package/src/core/routeros-string.ts +24 -0
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/errors.ts +1 -0
- package/src/execute-fanout.ts +453 -0
- package/src/execute.ts +216 -68
- package/src/explain/arg-tokens.ts +114 -0
- package/src/explain/args.ts +1403 -0
- package/src/explain/blocks.ts +132 -0
- package/src/explain/brace-slots.ts +143 -0
- package/src/explain/brace-tokens.ts +38 -0
- package/src/explain/catalog.ts +1345 -0
- package/src/explain/coordinates.ts +329 -0
- package/src/explain/defects.ts +217 -0
- package/src/explain/is-known-menu.ts +17 -0
- package/src/explain/menus.ts +664 -0
- package/src/explain/operator-tokens.ts +310 -0
- package/src/explain/operators.ts +458 -0
- package/src/explain/path-tokens.ts +150 -0
- package/src/explain/pathresolve.ts +1324 -0
- package/src/explain/quoted-string.ts +241 -0
- package/src/explain/scope-brace.ts +308 -0
- package/src/explain/segment.ts +757 -0
- package/src/explain/string-tokens.ts +67 -0
- package/src/explain/symbol-values.ts +520 -0
- package/src/explain/symbols.ts +1597 -0
- package/src/explain/token-ranges.ts +72 -0
- package/src/explain/transport.ts +497 -0
- package/src/explain/value-tokens.ts +80 -0
- package/src/explain/values.ts +357 -0
- package/src/explain/verbs.ts +64 -0
- package/src/explain/verbsplit.ts +813 -0
- package/src/explain/write.ts +815 -0
- package/src/explain.ts +2152 -0
- package/src/index.ts +307 -1
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +38 -18
- package/src/protocols/index.ts +11 -5
- package/src/protocols/mac-telnet-console.ts +61 -17
- package/src/resolver/cdb.ts +10 -2
- package/src/resolver/comment-kv.ts +58 -4
- package/src/resolver/config-file.ts +136 -0
- package/src/resolver/facts.ts +78 -0
- package/src/resolver/geo.ts +453 -0
- package/src/resolver/index.ts +65 -0
- package/src/resolver/mac.ts +12 -3
- package/src/resolver/quickchr-provider.ts +597 -0
- package/src/resolver/selection.ts +391 -0
- package/src/resolver/service-endpoint.ts +155 -0
- package/src/resolver/settings.ts +54 -5
- package/src/resolver/target.ts +21 -3
- package/src/retrieve-fanout.ts +239 -133
- package/src/retrieve.ts +120 -25
- package/src/settings.ts +1078 -0
- package/src/terminal.ts +11 -1
- package/src/transfer-fanout.ts +544 -0
- package/src/transfer.ts +133 -40
package/src/cli/btest.ts
CHANGED
|
@@ -29,8 +29,10 @@ import type { BtestDirection, BtestProtocol } from "../protocols/btest.ts";
|
|
|
29
29
|
import { toYaml } from "../retrieve.ts";
|
|
30
30
|
import {
|
|
31
31
|
type CliCommandMetadata,
|
|
32
|
+
type CliCommandOption,
|
|
32
33
|
expectValue,
|
|
33
34
|
renderCommandHelp,
|
|
35
|
+
unknownFlagError,
|
|
34
36
|
} from "./common.ts";
|
|
35
37
|
import {
|
|
36
38
|
buildTargetSelectionTips,
|
|
@@ -93,8 +95,14 @@ export const btestCommand: CliCommandMetadata = {
|
|
|
93
95
|
},
|
|
94
96
|
{ flag: "--random-data", description: "(client) Incompressible payload." },
|
|
95
97
|
{
|
|
96
|
-
flag: "--
|
|
97
|
-
description:
|
|
98
|
+
flag: "--nat-mode",
|
|
99
|
+
description:
|
|
100
|
+
"(client, UDP only) Originate an outbound datagram first so server→client data can traverse NAT. Automatic when the test receives.",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
flag: "--authenticate / --no-authenticate",
|
|
104
|
+
description:
|
|
105
|
+
"(server) Require EC-SRP5 auth. Default true; `--authenticate=false` also accepted.",
|
|
98
106
|
},
|
|
99
107
|
{
|
|
100
108
|
flag: "--bind",
|
|
@@ -127,6 +135,16 @@ export const btestCommand: CliCommandMetadata = {
|
|
|
127
135
|
valueName: "<port>",
|
|
128
136
|
description: "Control port. Default 2000.",
|
|
129
137
|
},
|
|
138
|
+
{
|
|
139
|
+
flag: "--cdb-file",
|
|
140
|
+
valueName: "<path>",
|
|
141
|
+
description: "(client) Read target credentials from a WinBox CDB file.",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
flag: "--cdb-password",
|
|
145
|
+
valueName: "<password>",
|
|
146
|
+
description: "(client) Decrypt an encrypted WinBox CDB file.",
|
|
147
|
+
},
|
|
130
148
|
{
|
|
131
149
|
flag: "--format",
|
|
132
150
|
valueName: `<${btestOutputFormats.join("|")}>`,
|
|
@@ -136,6 +154,22 @@ export const btestCommand: CliCommandMetadata = {
|
|
|
136
154
|
],
|
|
137
155
|
};
|
|
138
156
|
|
|
157
|
+
/**
|
|
158
|
+
* btest's single option list documents both subcommands. Scope "did you mean?"
|
|
159
|
+
* hints to the active mode so a client typo never suggests a server-only flag
|
|
160
|
+
* (and vice versa). Shared flags carry no `(client)`/`(server)` marker, so they
|
|
161
|
+
* stay in both; if a marker is ever dropped the flag just widens to both
|
|
162
|
+
* modes — it degrades to the old behavior, never a crash.
|
|
163
|
+
*/
|
|
164
|
+
function btestOptionsFor(
|
|
165
|
+
mode: "client" | "server",
|
|
166
|
+
): readonly CliCommandOption[] {
|
|
167
|
+
const other = mode === "client" ? "(server)" : "(client)";
|
|
168
|
+
return btestCommand.options.filter(
|
|
169
|
+
(option) => !option.description.includes(other),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
/** Parse a bandwidth value: a bare integer or a `K`/`M`/`G` suffix (bits/sec). */
|
|
140
174
|
function parseBandwidth(value: string, flag: string): number {
|
|
141
175
|
const match = value.trim().match(/^(\d+(?:\.\d+)?)([kKmMgG]?)$/);
|
|
@@ -276,7 +310,7 @@ function parseClientArgs(args: readonly string[]): ClientCliArgs {
|
|
|
276
310
|
break;
|
|
277
311
|
default:
|
|
278
312
|
if (arg.startsWith("-"))
|
|
279
|
-
throw
|
|
313
|
+
throw unknownFlagError("btest", arg, btestOptionsFor("client"));
|
|
280
314
|
if (request.targetInput !== undefined)
|
|
281
315
|
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
282
316
|
request.targetInput = arg;
|
|
@@ -341,7 +375,7 @@ function parseServerArgs(args: readonly string[]): ServerCliArgs {
|
|
|
341
375
|
break;
|
|
342
376
|
default:
|
|
343
377
|
if (arg.startsWith("-"))
|
|
344
|
-
throw
|
|
378
|
+
throw unknownFlagError("btest", arg, btestOptionsFor("server"));
|
|
345
379
|
throw new Error(
|
|
346
380
|
`\`centrs btest server\` takes no positional arguments; got: ${arg}.`,
|
|
347
381
|
);
|
package/src/cli/common.ts
CHANGED
|
@@ -33,6 +33,17 @@ export function expectValue(
|
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Parse a strictly-integer flag value, or `undefined` when the whole token is
|
|
38
|
+
* not an integer. Rejects numeric prefixes (`2abc` → undefined, not `2`) and
|
|
39
|
+
* decimals (`1.5`), which a bare `Number.parseInt` would silently truncate —
|
|
40
|
+
* the trap shared by `--port` / `--count` / `--concurrency`.
|
|
41
|
+
*/
|
|
42
|
+
export function parseStrictInteger(value: string): number | undefined {
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return /^-?\d+$/.test(trimmed) ? Number.parseInt(trimmed, 10) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
/** Flatten an option's `flag` field ("-X / --method") into individual `-`/`--` tokens. */
|
|
37
48
|
export function knownFlags(options: readonly CliCommandOption[]): string[] {
|
|
38
49
|
const flags: string[] = [];
|
package/src/cli/devices.ts
CHANGED
|
@@ -4,20 +4,31 @@
|
|
|
4
4
|
* unchanged from the former monolithic `cli.ts`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
asCentrsError,
|
|
9
|
+
CentrsError,
|
|
10
|
+
formatCentrsErrorText,
|
|
11
|
+
} from "../errors.ts";
|
|
8
12
|
import {
|
|
9
13
|
addDevice,
|
|
14
|
+
type BboxPredicate,
|
|
10
15
|
buildDevicesErrorEnvelope,
|
|
11
16
|
type CommentKvUpdate,
|
|
17
|
+
canonicalizeGeoKey,
|
|
12
18
|
type DevicesCommand,
|
|
13
19
|
type DevicesEnvelope,
|
|
14
20
|
type DevicesOutputFormat,
|
|
21
|
+
type DevicesWhereClause,
|
|
15
22
|
describeCentrs,
|
|
16
23
|
devicesOutputFormats,
|
|
17
24
|
editInteractiveOnlyError,
|
|
18
25
|
listDevices,
|
|
19
26
|
listGroups,
|
|
20
27
|
loadCdb,
|
|
28
|
+
type NearPredicate,
|
|
29
|
+
parseBbox,
|
|
30
|
+
parseGpsTuple,
|
|
31
|
+
parseNear,
|
|
21
32
|
recordTypeFromName,
|
|
22
33
|
removeDevice,
|
|
23
34
|
renderDevicesEnvelope,
|
|
@@ -30,6 +41,7 @@ import {
|
|
|
30
41
|
type CliCommandMetadata,
|
|
31
42
|
expectValue,
|
|
32
43
|
renderCommandHelp,
|
|
44
|
+
unknownFlagError,
|
|
33
45
|
} from "./common.ts";
|
|
34
46
|
|
|
35
47
|
export const devicesCommand: CliCommandMetadata = {
|
|
@@ -62,6 +74,24 @@ export const devicesCommand: CliCommandMetadata = {
|
|
|
62
74
|
description:
|
|
63
75
|
"`list` filter / `add`,`edit` first-class group field for the target entry.",
|
|
64
76
|
},
|
|
77
|
+
{
|
|
78
|
+
flag: "--where",
|
|
79
|
+
valueName: "<attr=value>",
|
|
80
|
+
description:
|
|
81
|
+
"`list` only — repeatable device-class filter over CDB facts (AND-combined), e.g. `--where lat=37.7749`.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
flag: "--near",
|
|
85
|
+
valueName: "<lat>,<lon>,<radius>",
|
|
86
|
+
description:
|
|
87
|
+
"`list` only — GPS filter: devices within radius (m/km/mi/ft; bare number = km). Lat-first.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
flag: "--bbox",
|
|
91
|
+
valueName: "<south>,<west>,<north>,<east>",
|
|
92
|
+
description:
|
|
93
|
+
"`list` only — GPS filter: devices inside the lat-first bounding box.",
|
|
94
|
+
},
|
|
65
95
|
{
|
|
66
96
|
flag: "--members",
|
|
67
97
|
description: "`groups` only — expand each group's membership.",
|
|
@@ -115,6 +145,36 @@ export const devicesCommand: CliCommandMetadata = {
|
|
|
115
145
|
valueName: `<${Object.keys(winBoxCdbRecordType).join("|")}>`,
|
|
116
146
|
description: "`add` only — record type (default `ipAdmin`).",
|
|
117
147
|
},
|
|
148
|
+
{
|
|
149
|
+
flag: "--lat / --latitude",
|
|
150
|
+
valueName: "<deg>",
|
|
151
|
+
description:
|
|
152
|
+
"`add`,`set` — latitude in decimal degrees (-90..90); a comment-kv fact, paired with --lon.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
flag: "--lon / --lng / --longitude / --long",
|
|
156
|
+
valueName: "<deg>",
|
|
157
|
+
description:
|
|
158
|
+
"`add`,`set` — longitude in decimal degrees (-180..180); a comment-kv fact, paired with --lat.",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
flag: "--altitude / --alt / --ele / --elevation",
|
|
162
|
+
valueName: "<meters>",
|
|
163
|
+
description:
|
|
164
|
+
"`add`,`set` — altitude in meters (may be negative); a comment-kv fact.",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
flag: "--altitude-type / --alt-type",
|
|
168
|
+
valueName: "<MSL|AGL>",
|
|
169
|
+
description:
|
|
170
|
+
"`add`,`set` — vertical datum for --altitude (default MSL); case-insensitive.",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
flag: "--gps",
|
|
174
|
+
valueName: "<lat>,<lon>[,<altitude>[,<altitude-type>]]",
|
|
175
|
+
description:
|
|
176
|
+
"`add`,`set` — combined lat,lon[,altitude[,altitude-type]] convenience (lat-first; missing altitude-type defaults MSL).",
|
|
177
|
+
},
|
|
118
178
|
{
|
|
119
179
|
flag: "--force",
|
|
120
180
|
description: "`add` only — overwrite the existing (target, user) entry.",
|
|
@@ -164,6 +224,7 @@ interface DevicesCliArgs {
|
|
|
164
224
|
cdbFile?: string;
|
|
165
225
|
cdbPassword?: string;
|
|
166
226
|
group?: string;
|
|
227
|
+
where?: string[];
|
|
167
228
|
via?: string;
|
|
168
229
|
members?: boolean;
|
|
169
230
|
explain?: boolean;
|
|
@@ -181,6 +242,13 @@ interface DevicesCliArgs {
|
|
|
181
242
|
format?: DevicesOutputFormat;
|
|
182
243
|
target?: string;
|
|
183
244
|
kvArgs?: string[];
|
|
245
|
+
lat?: string;
|
|
246
|
+
lon?: string;
|
|
247
|
+
altitude?: string;
|
|
248
|
+
altitudeType?: string;
|
|
249
|
+
gps?: string;
|
|
250
|
+
near?: NearPredicate;
|
|
251
|
+
bbox?: BboxPredicate;
|
|
184
252
|
}
|
|
185
253
|
|
|
186
254
|
function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
@@ -206,6 +274,18 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
206
274
|
case "--group":
|
|
207
275
|
parsed.group = expectValue(args, ++index, arg);
|
|
208
276
|
break;
|
|
277
|
+
case "--where":
|
|
278
|
+
parsed.where = [
|
|
279
|
+
...(parsed.where ?? []),
|
|
280
|
+
expectValue(args, ++index, arg),
|
|
281
|
+
];
|
|
282
|
+
break;
|
|
283
|
+
case "--near":
|
|
284
|
+
parsed.near = parseNear(expectValue(args, ++index, arg));
|
|
285
|
+
break;
|
|
286
|
+
case "--bbox":
|
|
287
|
+
parsed.bbox = parseBbox(expectValue(args, ++index, arg));
|
|
288
|
+
break;
|
|
209
289
|
case "--via":
|
|
210
290
|
parsed.via = expectValue(args, ++index, arg);
|
|
211
291
|
break;
|
|
@@ -242,6 +322,29 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
242
322
|
case "--record-type":
|
|
243
323
|
parsed.recordType = expectValue(args, ++index, arg);
|
|
244
324
|
break;
|
|
325
|
+
case "--lat":
|
|
326
|
+
case "--latitude":
|
|
327
|
+
parsed.lat = expectValue(args, ++index, arg);
|
|
328
|
+
break;
|
|
329
|
+
case "--lon":
|
|
330
|
+
case "--lng":
|
|
331
|
+
case "--longitude":
|
|
332
|
+
case "--long":
|
|
333
|
+
parsed.lon = expectValue(args, ++index, arg);
|
|
334
|
+
break;
|
|
335
|
+
case "--altitude":
|
|
336
|
+
case "--alt":
|
|
337
|
+
case "--ele":
|
|
338
|
+
case "--elevation":
|
|
339
|
+
parsed.altitude = expectValue(args, ++index, arg);
|
|
340
|
+
break;
|
|
341
|
+
case "--altitude-type":
|
|
342
|
+
case "--alt-type":
|
|
343
|
+
parsed.altitudeType = expectValue(args, ++index, arg);
|
|
344
|
+
break;
|
|
345
|
+
case "--gps":
|
|
346
|
+
parsed.gps = expectValue(args, ++index, arg);
|
|
347
|
+
break;
|
|
245
348
|
case "--force":
|
|
246
349
|
parsed.force = true;
|
|
247
350
|
break;
|
|
@@ -263,7 +366,7 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
263
366
|
break;
|
|
264
367
|
default:
|
|
265
368
|
if (arg.startsWith("-")) {
|
|
266
|
-
throw
|
|
369
|
+
throw unknownFlagError("devices", arg, devicesCommand.options);
|
|
267
370
|
}
|
|
268
371
|
positional.push(arg);
|
|
269
372
|
break;
|
|
@@ -298,6 +401,48 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
298
401
|
}
|
|
299
402
|
parsed.subcommand = sub;
|
|
300
403
|
|
|
404
|
+
// Reject flags that don't apply to the resolved subcommand instead of
|
|
405
|
+
// silently ignoring the user's input: `--where`/`--near`/`--bbox` select
|
|
406
|
+
// records for `list`; the GPS *setter* flags write via `add`/`set`.
|
|
407
|
+
const selectorFlag =
|
|
408
|
+
(parsed.where?.length ?? 0) > 0
|
|
409
|
+
? "--where"
|
|
410
|
+
: parsed.near !== undefined
|
|
411
|
+
? "--near"
|
|
412
|
+
: parsed.bbox !== undefined
|
|
413
|
+
? "--bbox"
|
|
414
|
+
: undefined;
|
|
415
|
+
if (selectorFlag !== undefined && sub !== "list") {
|
|
416
|
+
throw new CentrsError({
|
|
417
|
+
code: "input/invalid-command",
|
|
418
|
+
summary: `\`${selectorFlag}\` is not supported by \`centrs devices ${sub}\`.`,
|
|
419
|
+
remediation:
|
|
420
|
+
"`--where`/`--near`/`--bbox` select records for `centrs devices list`. For `add`/`set`, name the `<target>` directly.",
|
|
421
|
+
context: { subcommand: sub, flag: selectorFlag },
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const geoFlag =
|
|
425
|
+
parsed.lat !== undefined
|
|
426
|
+
? "--lat"
|
|
427
|
+
: parsed.lon !== undefined
|
|
428
|
+
? "--lon"
|
|
429
|
+
: parsed.altitude !== undefined
|
|
430
|
+
? "--altitude"
|
|
431
|
+
: parsed.altitudeType !== undefined
|
|
432
|
+
? "--altitude-type"
|
|
433
|
+
: parsed.gps !== undefined
|
|
434
|
+
? "--gps"
|
|
435
|
+
: undefined;
|
|
436
|
+
if (geoFlag !== undefined && sub !== "add" && sub !== "set") {
|
|
437
|
+
throw new CentrsError({
|
|
438
|
+
code: "input/invalid-command",
|
|
439
|
+
summary: `GPS flag \`${geoFlag}\` is not supported by \`centrs devices ${sub}\`.`,
|
|
440
|
+
remediation:
|
|
441
|
+
"Store GPS with `centrs devices add`/`set` (e.g. `--gps <lat>,<lon>`); query it with `centrs devices list --where lat=…` / `--near`.",
|
|
442
|
+
context: { subcommand: sub, flag: geoFlag },
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
301
446
|
if (sub === "show" || sub === "edit" || sub === "remove") {
|
|
302
447
|
if (rest.length !== 1) {
|
|
303
448
|
throw new Error(
|
|
@@ -324,6 +469,32 @@ function parseDevicesCliArgs(args: readonly string[]): DevicesCliArgs {
|
|
|
324
469
|
return parsed;
|
|
325
470
|
}
|
|
326
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Parse a `--where attr=value` token for `devices list` (constitution: target
|
|
474
|
+
* selection grammar). Mirrors `src/cli/selection.ts` `parseWhereClause`: a
|
|
475
|
+
* structured `input/invalid-command` (not a bare `Error`) so the surfaced error
|
|
476
|
+
* keeps its remediation/context, consistent with every other CLI surface. Geo
|
|
477
|
+
* alias canonicalization (`latitude=`→`lat=`) is NOT done here — it happens once,
|
|
478
|
+
* centrally, in `matchesWhere` (`src/resolver/facts.ts`), so `devices list` and
|
|
479
|
+
* every fan-out command's `--where` behave identically.
|
|
480
|
+
*/
|
|
481
|
+
function parseWhereClause(token: string): DevicesWhereClause {
|
|
482
|
+
const eq = token.indexOf("=");
|
|
483
|
+
if (eq <= 0) {
|
|
484
|
+
throw new CentrsError({
|
|
485
|
+
code: "input/invalid-command",
|
|
486
|
+
summary: `--where must be \`<attr>=<value>\`; received: ${token}`,
|
|
487
|
+
remediation:
|
|
488
|
+
"Pass a device-class selector like `--where board=RB5009` (matches a CDB fact or core field).",
|
|
489
|
+
context: { where: token },
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
key: token.slice(0, eq),
|
|
494
|
+
value: token.slice(eq + 1),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
327
498
|
function parseKvArg(token: string): CommentKvUpdate {
|
|
328
499
|
const eq = token.indexOf("=");
|
|
329
500
|
if (eq <= 0) {
|
|
@@ -331,7 +502,51 @@ function parseKvArg(token: string): CommentKvUpdate {
|
|
|
331
502
|
`Invalid kv override "${token}"; expected key=value (use key= to clear a value).`,
|
|
332
503
|
);
|
|
333
504
|
}
|
|
334
|
-
|
|
505
|
+
// Alias canonicalization happens in ONE place (`canonicalizeGeoKey`) so a
|
|
506
|
+
// bare `lng=…`/`latitude=…`/`elevation=…` positional lands under the same
|
|
507
|
+
// canonical key a `--lon`/`--lat`/`--altitude` flag would use. A no-op for
|
|
508
|
+
// every non-geo key.
|
|
509
|
+
const key = canonicalizeGeoKey(token.slice(0, eq));
|
|
510
|
+
return { key, value: parseKvValue(token.slice(eq + 1)) };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Lower the per-device GPS flags (`--lat`/`--lon`/`--altitude`/
|
|
515
|
+
* `--altitude-type` and their aliases, plus the combined `--gps`) into
|
|
516
|
+
* comment-kv updates. These are CLI conveniences only — no new first-class CDB
|
|
517
|
+
* tag — so they merge into the same `CommentKvUpdate[]` as bare `k=v`
|
|
518
|
+
* positionals; range/enum/pairing validation happens once, in
|
|
519
|
+
* `validateCommentKvUpdates` (`src/devices.ts`), regardless of which flag
|
|
520
|
+
* produced the update. `--gps` is expanded first so a discrete `--lat`/`--lon`/
|
|
521
|
+
* `--altitude`/`--altitude-type` flag passed alongside it wins (applied later
|
|
522
|
+
* in the array).
|
|
523
|
+
*/
|
|
524
|
+
function buildGeoUpdates(parsed: DevicesCliArgs): CommentKvUpdate[] {
|
|
525
|
+
const updates: CommentKvUpdate[] = [];
|
|
526
|
+
if (parsed.gps !== undefined) {
|
|
527
|
+
const gps = parseGpsTuple(parsed.gps);
|
|
528
|
+
updates.push({ key: "lat", value: gps.raw.lat });
|
|
529
|
+
updates.push({ key: "lon", value: gps.raw.lon });
|
|
530
|
+
if (gps.raw.altitude !== undefined) {
|
|
531
|
+
updates.push({ key: "altitude", value: gps.raw.altitude });
|
|
532
|
+
}
|
|
533
|
+
if (gps.raw.altitudeType !== undefined) {
|
|
534
|
+
updates.push({ key: "altitude-type", value: gps.raw.altitudeType });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (parsed.lat !== undefined) {
|
|
538
|
+
updates.push({ key: "lat", value: parsed.lat });
|
|
539
|
+
}
|
|
540
|
+
if (parsed.lon !== undefined) {
|
|
541
|
+
updates.push({ key: "lon", value: parsed.lon });
|
|
542
|
+
}
|
|
543
|
+
if (parsed.altitude !== undefined) {
|
|
544
|
+
updates.push({ key: "altitude", value: parsed.altitude });
|
|
545
|
+
}
|
|
546
|
+
if (parsed.altitudeType !== undefined) {
|
|
547
|
+
updates.push({ key: "altitude-type", value: parsed.altitudeType });
|
|
548
|
+
}
|
|
549
|
+
return updates;
|
|
335
550
|
}
|
|
336
551
|
|
|
337
552
|
function parseKvValue(value: string): string {
|
|
@@ -400,7 +615,13 @@ export async function runDevicesCli(args: readonly string[]): Promise<number> {
|
|
|
400
615
|
let envelope: DevicesEnvelope<unknown>;
|
|
401
616
|
switch (parsed.subcommand) {
|
|
402
617
|
case "list":
|
|
403
|
-
envelope = listDevices({
|
|
618
|
+
envelope = listDevices({
|
|
619
|
+
cdb,
|
|
620
|
+
group: parsed.group,
|
|
621
|
+
where: (parsed.where ?? []).map(parseWhereClause),
|
|
622
|
+
near: parsed.near,
|
|
623
|
+
bbox: parsed.bbox,
|
|
624
|
+
});
|
|
404
625
|
break;
|
|
405
626
|
case "show":
|
|
406
627
|
if (!parsed.target) {
|
|
@@ -441,7 +662,10 @@ export async function runDevicesCli(args: readonly string[]): Promise<number> {
|
|
|
441
662
|
profile: resolveProfile(parsed),
|
|
442
663
|
session: parsed.session,
|
|
443
664
|
comment: parsed.comment,
|
|
444
|
-
commentKvUpdates:
|
|
665
|
+
commentKvUpdates: [
|
|
666
|
+
...buildGeoUpdates(parsed),
|
|
667
|
+
...(parsed.kvArgs ?? []).map(parseKvArg),
|
|
668
|
+
],
|
|
445
669
|
force: parsed.force,
|
|
446
670
|
strict: parsed.strict,
|
|
447
671
|
});
|
|
@@ -459,7 +683,10 @@ export async function runDevicesCli(args: readonly string[]): Promise<number> {
|
|
|
459
683
|
cdb,
|
|
460
684
|
target: parsed.target,
|
|
461
685
|
match: parsed.match,
|
|
462
|
-
updates:
|
|
686
|
+
updates: [
|
|
687
|
+
...buildGeoUpdates(parsed),
|
|
688
|
+
...(parsed.kvArgs ?? []).map(parseKvArg),
|
|
689
|
+
],
|
|
463
690
|
user: parsed.user,
|
|
464
691
|
password: parsed.password,
|
|
465
692
|
group: parsed.group,
|
|
@@ -506,8 +733,10 @@ export async function runDevicesCli(args: readonly string[]): Promise<number> {
|
|
|
506
733
|
centrsError,
|
|
507
734
|
);
|
|
508
735
|
if (format === "json" || format === "yaml") {
|
|
736
|
+
// 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.
|
|
509
737
|
console.error(renderDevicesEnvelope(envelope, format));
|
|
510
738
|
} else {
|
|
739
|
+
// 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.
|
|
511
740
|
console.error(formatCentrsErrorText(centrsError));
|
|
512
741
|
}
|
|
513
742
|
return 1;
|
package/src/cli/discover.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type CliCommandMetadata,
|
|
21
21
|
expectValue,
|
|
22
22
|
renderCommandHelp,
|
|
23
|
+
unknownFlagError,
|
|
23
24
|
} from "./common.ts";
|
|
24
25
|
|
|
25
26
|
export const discoverCommand: CliCommandMetadata = {
|
|
@@ -134,7 +135,7 @@ function parseDiscoverCliArgs(args: readonly string[]): DiscoverCliArgs {
|
|
|
134
135
|
break;
|
|
135
136
|
default:
|
|
136
137
|
if (arg.startsWith("-")) {
|
|
137
|
-
throw
|
|
138
|
+
throw unknownFlagError("discover", arg, discoverCommand.options);
|
|
138
139
|
}
|
|
139
140
|
throw new Error(
|
|
140
141
|
`\`centrs discover\` does not accept positional arguments; got: ${arg}.`,
|
|
@@ -206,6 +207,7 @@ export async function runDiscoverCli(args: readonly string[]): Promise<number> {
|
|
|
206
207
|
remediation:
|
|
207
208
|
"Use `centrs discover --help` to inspect the supported flags.",
|
|
208
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.
|
|
209
211
|
console.error(formatCentrsErrorText(centrsError));
|
|
210
212
|
return 1;
|
|
211
213
|
}
|