@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/execute.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
pathTokens,
|
|
18
18
|
} from "./core/inspect.ts";
|
|
19
19
|
import { mapRouterOsError } from "./core/routeros-errors.ts";
|
|
20
|
+
import { routerOsStringLiteral } from "./core/routeros-string.ts";
|
|
20
21
|
import { CentrsError, serializeCentrsError } from "./errors.ts";
|
|
21
22
|
import {
|
|
22
23
|
createProtocolAdapter,
|
|
@@ -31,12 +32,15 @@ import {
|
|
|
31
32
|
parseScriptFor,
|
|
32
33
|
} from "./protocols/mac-telnet-console.ts";
|
|
33
34
|
import {
|
|
35
|
+
assertNoQuickchrOverrideConflict,
|
|
34
36
|
type CdbResolution,
|
|
35
37
|
effectiveHostCandidate,
|
|
36
38
|
isIpTransport,
|
|
37
39
|
isMacAddress,
|
|
40
|
+
loadEnvFileDefaults,
|
|
38
41
|
parseDuration,
|
|
39
42
|
parseResolvePolicy,
|
|
43
|
+
quickchrConnection,
|
|
40
44
|
type ResolvedAuth,
|
|
41
45
|
type ResolvedSetting,
|
|
42
46
|
type ResolvedTarget,
|
|
@@ -45,6 +49,7 @@ import {
|
|
|
45
49
|
resolveCdb,
|
|
46
50
|
resolveMacTarget,
|
|
47
51
|
resolveOptionalIntegerSetting,
|
|
52
|
+
resolveQuickchrTarget,
|
|
48
53
|
resolveStringSetting,
|
|
49
54
|
resolveTarget,
|
|
50
55
|
toCoreSource,
|
|
@@ -56,6 +61,12 @@ export type ExecuteOutputFormat = (typeof executeOutputFormats)[number];
|
|
|
56
61
|
|
|
57
62
|
export interface ExecuteRequest {
|
|
58
63
|
targetInput?: string;
|
|
64
|
+
/**
|
|
65
|
+
* quickchr machine name (`--quickchr <name>`): resolve host/port/auth from
|
|
66
|
+
* the live VM descriptor instead of CDB/env (`docs/CONSTITUTION.md` →
|
|
67
|
+
* Resolution providers). Conflicts with host/port/username/password/sshKey.
|
|
68
|
+
*/
|
|
69
|
+
quickchr?: string;
|
|
59
70
|
command: string;
|
|
60
71
|
via?: string;
|
|
61
72
|
host?: string;
|
|
@@ -277,19 +288,40 @@ export async function runResolvedExecute(
|
|
|
277
288
|
export async function resolveExecuteRequest(
|
|
278
289
|
request: ExecuteRequest,
|
|
279
290
|
env: Record<string, string | undefined>,
|
|
291
|
+
options: {
|
|
292
|
+
cdbResolution?: CdbResolution;
|
|
293
|
+
config?: Record<string, string | undefined>;
|
|
294
|
+
} = {},
|
|
280
295
|
): Promise<ResolvedExecuteRequest> {
|
|
281
296
|
validateExecuteRequestShape(request);
|
|
282
297
|
const canonical = canonicalizeExecuteCommand(request.command);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
|
|
298
|
+
const config = options.config ?? (await loadEnvFileDefaults(env));
|
|
299
|
+
// A quickchr target is a named-live-provider: it supplies host/port/auth from
|
|
300
|
+
// the live descriptor and bypasses CDB/env resolution for those fields
|
|
301
|
+
// entirely (no CDB load, no `__default__` ladder, no MAC resolution).
|
|
302
|
+
let quickchrResolution: Awaited<
|
|
303
|
+
ReturnType<typeof resolveQuickchrTarget>
|
|
304
|
+
> | null = null;
|
|
305
|
+
if (request.quickchr !== undefined) {
|
|
306
|
+
assertNoQuickchrOverrideConflict(request, request.quickchr);
|
|
307
|
+
quickchrResolution = await resolveQuickchrTarget(request.quickchr);
|
|
308
|
+
}
|
|
309
|
+
// Fan-out passes the pre-resolved CDB record for a member (the CDB is loaded
|
|
310
|
+
// once in `expandSelection`); single-target resolves it here.
|
|
311
|
+
const cdbResolution = quickchrResolution
|
|
312
|
+
? undefined
|
|
313
|
+
: (options.cdbResolution ??
|
|
314
|
+
(await resolveCdb(
|
|
315
|
+
{
|
|
316
|
+
targetInput: request.targetInput,
|
|
317
|
+
cdbFile: request.cdbFile,
|
|
318
|
+
cdbPassword: request.cdbPassword,
|
|
319
|
+
},
|
|
320
|
+
env,
|
|
321
|
+
config,
|
|
322
|
+
)));
|
|
323
|
+
const via = resolveExecuteProtocol(request, env, cdbResolution, config);
|
|
324
|
+
const format = resolveFormat(request, env, config);
|
|
293
325
|
const validate = resolveBooleanSetting(
|
|
294
326
|
request.validate,
|
|
295
327
|
env,
|
|
@@ -297,57 +329,89 @@ export async function resolveExecuteRequest(
|
|
|
297
329
|
true,
|
|
298
330
|
"validate",
|
|
299
331
|
cdbResolution?.overrides.validate,
|
|
332
|
+
config,
|
|
300
333
|
);
|
|
301
334
|
const timeoutMs = resolveTimeoutSetting(
|
|
302
335
|
request.timeout,
|
|
303
336
|
env,
|
|
304
337
|
via.value,
|
|
305
338
|
cdbResolution?.overrides.timeoutMs,
|
|
339
|
+
config,
|
|
306
340
|
);
|
|
307
341
|
const maxResultsBytes = resolveOptionalIntegerSetting(
|
|
308
342
|
request.maxResultsBytes,
|
|
309
343
|
env,
|
|
310
344
|
"CENTRS_MAX_RESULTS",
|
|
311
345
|
"max-results",
|
|
346
|
+
undefined,
|
|
347
|
+
config,
|
|
312
348
|
);
|
|
313
|
-
const macResolution =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
349
|
+
const macResolution =
|
|
350
|
+
quickchrResolution === null && isIpTransport(via.value)
|
|
351
|
+
? await resolveMacTarget({
|
|
352
|
+
host: request.host,
|
|
353
|
+
targetInput: request.targetInput,
|
|
354
|
+
cdbTarget: cdbResolution?.target,
|
|
355
|
+
env,
|
|
356
|
+
config,
|
|
357
|
+
policy: parseResolvePolicy(
|
|
358
|
+
request.resolve ??
|
|
359
|
+
env["CENTRS_RESOLVE"] ??
|
|
360
|
+
config["CENTRS_RESOLVE"],
|
|
361
|
+
),
|
|
362
|
+
operation: "execute",
|
|
363
|
+
})
|
|
364
|
+
: undefined;
|
|
365
|
+
const connection = quickchrResolution
|
|
366
|
+
? quickchrConnection(quickchrResolution, via.value)
|
|
367
|
+
: null;
|
|
368
|
+
const target = connection
|
|
369
|
+
? connection.target
|
|
370
|
+
: resolveTarget(
|
|
371
|
+
{
|
|
372
|
+
targetInput: request.targetInput,
|
|
373
|
+
host: request.host,
|
|
374
|
+
port: request.port,
|
|
375
|
+
macResolution,
|
|
376
|
+
},
|
|
318
377
|
env,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
378
|
+
via.value,
|
|
379
|
+
cdbResolution,
|
|
380
|
+
config,
|
|
381
|
+
);
|
|
382
|
+
const auth = connection
|
|
383
|
+
? connection.auth
|
|
384
|
+
: resolveAuth(
|
|
385
|
+
{
|
|
386
|
+
username: request.username,
|
|
387
|
+
password: request.password,
|
|
388
|
+
sshKey: request.sshKey,
|
|
389
|
+
},
|
|
390
|
+
env,
|
|
391
|
+
cdbResolution,
|
|
392
|
+
config,
|
|
393
|
+
);
|
|
394
|
+
// A quickchr TLS/SSH endpoint is trusted by provenance (self-signed VM cert /
|
|
395
|
+
// ephemeral host key) — `insecure` is forced with provider provenance and the
|
|
396
|
+
// provider-trust warning rides the envelope (see QuickchrConnection.insecure).
|
|
397
|
+
const insecure =
|
|
398
|
+
connection?.insecure === true
|
|
399
|
+
? {
|
|
400
|
+
value: true,
|
|
401
|
+
source: {
|
|
402
|
+
kind: "provider" as const,
|
|
403
|
+
key: `quickchr:${request.quickchr}`,
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
: resolveBooleanSetting(
|
|
407
|
+
request.insecure,
|
|
408
|
+
env,
|
|
409
|
+
"CENTRS_INSECURE",
|
|
410
|
+
false,
|
|
411
|
+
"insecure",
|
|
412
|
+
cdbResolution?.overrides.insecure,
|
|
413
|
+
config,
|
|
414
|
+
);
|
|
351
415
|
|
|
352
416
|
return {
|
|
353
417
|
command: request.command,
|
|
@@ -362,7 +426,82 @@ export async function resolveExecuteRequest(
|
|
|
362
426
|
maxResultsBytes,
|
|
363
427
|
yes: request.yes ?? false,
|
|
364
428
|
verbose: request.verbose ?? false,
|
|
365
|
-
warnings:
|
|
429
|
+
warnings: connection
|
|
430
|
+
? [...connection.warnings]
|
|
431
|
+
: (cdbResolution?.warnings ?? []),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export interface ExecuteGlobalContext {
|
|
436
|
+
via: RouterOsProtocol;
|
|
437
|
+
write: boolean;
|
|
438
|
+
summary: ExecuteRequestSummary;
|
|
439
|
+
settings: CommonSettingsMeta;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Resolve the target-independent settings (protocol, format, validate, timeout,
|
|
444
|
+
* max-results) for a fan-out's outer `meta.operation.request` summary and its
|
|
445
|
+
* transport-aware concurrency default. No CDB record is consulted; the global
|
|
446
|
+
* `--via` (default `native-api`) decides the summary and concurrency base. Also
|
|
447
|
+
* canonicalizes the command once so write-ness is known before any target runs.
|
|
448
|
+
*/
|
|
449
|
+
export function resolveExecuteGlobalContext(
|
|
450
|
+
request: ExecuteRequest,
|
|
451
|
+
env: Record<string, string | undefined>,
|
|
452
|
+
config: Record<string, string | undefined> = {},
|
|
453
|
+
): ExecuteGlobalContext {
|
|
454
|
+
validateExecuteRequestShape(request);
|
|
455
|
+
const canonical = canonicalizeExecuteCommand(request.command);
|
|
456
|
+
const via = resolveExecuteProtocol(request, env, undefined, config);
|
|
457
|
+
const format = resolveFormat(request, env, config);
|
|
458
|
+
const validate = resolveBooleanSetting(
|
|
459
|
+
request.validate,
|
|
460
|
+
env,
|
|
461
|
+
"CENTRS_VALIDATE",
|
|
462
|
+
true,
|
|
463
|
+
"validate",
|
|
464
|
+
undefined,
|
|
465
|
+
config,
|
|
466
|
+
);
|
|
467
|
+
const timeoutMs = resolveTimeoutSetting(
|
|
468
|
+
request.timeout,
|
|
469
|
+
env,
|
|
470
|
+
via.value,
|
|
471
|
+
undefined,
|
|
472
|
+
config,
|
|
473
|
+
);
|
|
474
|
+
const maxResultsBytes = resolveOptionalIntegerSetting(
|
|
475
|
+
request.maxResultsBytes,
|
|
476
|
+
env,
|
|
477
|
+
"CENTRS_MAX_RESULTS",
|
|
478
|
+
"max-results",
|
|
479
|
+
undefined,
|
|
480
|
+
config,
|
|
481
|
+
);
|
|
482
|
+
return {
|
|
483
|
+
via: via.value,
|
|
484
|
+
write: isWriteShaped(canonical),
|
|
485
|
+
summary: {
|
|
486
|
+
command: request.command,
|
|
487
|
+
canonical,
|
|
488
|
+
write: isWriteShaped(canonical),
|
|
489
|
+
yes: request.yes ?? false,
|
|
490
|
+
validate: validate.value,
|
|
491
|
+
verbose: request.verbose ?? false,
|
|
492
|
+
timeoutMs: timeoutMs.value,
|
|
493
|
+
format: format.value,
|
|
494
|
+
maxResultsBytes: maxResultsBytes?.value,
|
|
495
|
+
},
|
|
496
|
+
settings: {
|
|
497
|
+
via: toCoreSource(via.source),
|
|
498
|
+
timeoutMs: toCoreSource(timeoutMs.source),
|
|
499
|
+
format: toCoreSource(format.source),
|
|
500
|
+
validate: toCoreSource(validate.source),
|
|
501
|
+
maxResultsBytes: maxResultsBytes
|
|
502
|
+
? toCoreSource(maxResultsBytes.source)
|
|
503
|
+
: undefined,
|
|
504
|
+
},
|
|
366
505
|
};
|
|
367
506
|
}
|
|
368
507
|
|
|
@@ -600,7 +739,7 @@ async function validateExecuteCommand(
|
|
|
600
739
|
context: {
|
|
601
740
|
path: resolved.canonical.path,
|
|
602
741
|
verb: resolved.canonical.verb,
|
|
603
|
-
|
|
742
|
+
parameter: missing[0],
|
|
604
743
|
requestedAttributes: requested,
|
|
605
744
|
availableAttributes,
|
|
606
745
|
validationSource: "/console/inspect request=child+completion",
|
|
@@ -622,7 +761,7 @@ async function validateExecuteCommand(
|
|
|
622
761
|
* Validate a command over a console transport (mac-telnet or ssh) with a single
|
|
623
762
|
* `:put [:parse ...]`. Unlike REST/native (a syntax gate plus a `/console/inspect`
|
|
624
763
|
* semantic gate), one console `:parse` reports both `syntax error` and `bad
|
|
625
|
-
* parameter <name>`, so it covers syntax and the unknown-attribute (
|
|
764
|
+
* parameter <name>`, so it covers syntax and the unknown-attribute (name-level)
|
|
626
765
|
* gate at once — identical strings over mac-telnet and ssh (CHR 7.23.1 grounded;
|
|
627
766
|
* see `mac-telnet-console.ts` and `ssh.ts`).
|
|
628
767
|
*/
|
|
@@ -651,7 +790,7 @@ async function validateConsoleParseCommand(
|
|
|
651
790
|
script: parseScriptFor(resolved.command),
|
|
652
791
|
});
|
|
653
792
|
// Throws validation/syntax or validation/unknown-attribute on a bad parse.
|
|
654
|
-
classifyParseResult(result.ret ?? "", resolved.command);
|
|
793
|
+
classifyParseResult(result.ret ?? "", resolved.command, resolved.via.value);
|
|
655
794
|
return {
|
|
656
795
|
enabled: true,
|
|
657
796
|
source: `:put [:parse ...] over ${resolved.via.value}`,
|
|
@@ -683,7 +822,14 @@ async function runSyntaxGate(
|
|
|
683
822
|
}
|
|
684
823
|
const script = `:put [:parse ${routerOsStringLiteral(resolved.command)}]`;
|
|
685
824
|
try {
|
|
686
|
-
|
|
825
|
+
// GH#230: `:parse` never throws (CHR 7.23.3). The diagnostic rides the
|
|
826
|
+
// success value's text — over REST/native the `ret` field (HTTP 200 /
|
|
827
|
+
// `as-string`), over console the printed output. The old code only watched
|
|
828
|
+
// for a thrown transport/RouterOS error and so never rejected anything.
|
|
829
|
+
// Read the return and classify it: one `:parse` covers syntax and the
|
|
830
|
+
// unknown-attribute (name-level) gate.
|
|
831
|
+
const result = await backend.execute({ path: "", command: "", script });
|
|
832
|
+
classifyParseResult(result.ret ?? "", resolved.command, resolved.via.value);
|
|
687
833
|
} catch (error) {
|
|
688
834
|
// The preflight is a syntax gate, but `backend.execute` also surfaces
|
|
689
835
|
// connection and authentication failures (login happens lazily here).
|
|
@@ -694,6 +840,9 @@ async function runSyntaxGate(
|
|
|
694
840
|
if (error instanceof CentrsError && isPreflightTransportError(error)) {
|
|
695
841
|
throw error;
|
|
696
842
|
}
|
|
843
|
+
if (error instanceof CentrsError && error.code.startsWith("validation/")) {
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
697
846
|
// If the mapped RouterOS fault carried a `(line N column M)` byte offset,
|
|
698
847
|
// keep it on the wrapped syntax error rather than dropping it.
|
|
699
848
|
const position = error instanceof CentrsError ? error.position : undefined;
|
|
@@ -756,16 +905,6 @@ async function runCommand(
|
|
|
756
905
|
return backend.execute({ path: "", command: "", script: resolved.command });
|
|
757
906
|
}
|
|
758
907
|
if (resolved.canonical.mode === "script") {
|
|
759
|
-
if (resolved.via.value === "native-api") {
|
|
760
|
-
throw new CentrsError({
|
|
761
|
-
code: "transport/unsupported-operation",
|
|
762
|
-
summary:
|
|
763
|
-
"The native API execute path requires a slash-prefixed RouterOS command.",
|
|
764
|
-
remediation:
|
|
765
|
-
"Use a path-shaped command such as `/system/identity/set name=x`, or pin `--via rest-api` for `/rest/execute` script mode.",
|
|
766
|
-
context: { via: resolved.via.value, command: resolved.command },
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
908
|
return backend.execute({ path: "", command: "", script: resolved.command });
|
|
770
909
|
}
|
|
771
910
|
return backend.execute({
|
|
@@ -821,8 +960,9 @@ function resolveExecuteProtocol(
|
|
|
821
960
|
request: ExecuteRequest,
|
|
822
961
|
env: Record<string, string | undefined>,
|
|
823
962
|
cdb?: CdbResolution,
|
|
963
|
+
config: Record<string, string | undefined> = {},
|
|
824
964
|
): ResolvedSetting<RouterOsProtocol> {
|
|
825
|
-
const defaultVia = isUnresolvedMacTarget(request, env, cdb)
|
|
965
|
+
const defaultVia = isUnresolvedMacTarget(request, env, cdb, config)
|
|
826
966
|
? "mac-telnet"
|
|
827
967
|
: "native-api";
|
|
828
968
|
const via = resolveStringSetting(
|
|
@@ -833,6 +973,7 @@ function resolveExecuteProtocol(
|
|
|
833
973
|
"via",
|
|
834
974
|
undefined,
|
|
835
975
|
cdb?.overrides.via,
|
|
976
|
+
config,
|
|
836
977
|
);
|
|
837
978
|
if (!via) {
|
|
838
979
|
throw new CentrsError({
|
|
@@ -877,8 +1018,13 @@ function isUnresolvedMacTarget(
|
|
|
877
1018
|
request: ExecuteRequest,
|
|
878
1019
|
env: Record<string, string | undefined>,
|
|
879
1020
|
cdb?: CdbResolution,
|
|
1021
|
+
config: Record<string, string | undefined> = {},
|
|
880
1022
|
): boolean {
|
|
881
|
-
if (
|
|
1023
|
+
if (
|
|
1024
|
+
request.via !== undefined ||
|
|
1025
|
+
env["CENTRS_VIA"] !== undefined ||
|
|
1026
|
+
config["CENTRS_VIA"] !== undefined
|
|
1027
|
+
) {
|
|
882
1028
|
return false;
|
|
883
1029
|
}
|
|
884
1030
|
// Decide the default transport from the *effective* target (host >
|
|
@@ -890,6 +1036,7 @@ function isUnresolvedMacTarget(
|
|
|
890
1036
|
targetInput: request.targetInput,
|
|
891
1037
|
cdbTarget: cdb?.target,
|
|
892
1038
|
env,
|
|
1039
|
+
config,
|
|
893
1040
|
});
|
|
894
1041
|
return candidate ? isMacAddress(candidate) : false;
|
|
895
1042
|
}
|
|
@@ -897,6 +1044,7 @@ function isUnresolvedMacTarget(
|
|
|
897
1044
|
function resolveFormat(
|
|
898
1045
|
request: ExecuteRequest,
|
|
899
1046
|
env: Record<string, string | undefined>,
|
|
1047
|
+
config: Record<string, string | undefined> = {},
|
|
900
1048
|
): ResolvedSetting<ExecuteOutputFormat> {
|
|
901
1049
|
return resolveStringSetting(
|
|
902
1050
|
request.format,
|
|
@@ -905,6 +1053,8 @@ function resolveFormat(
|
|
|
905
1053
|
"text",
|
|
906
1054
|
"format",
|
|
907
1055
|
parseOutputFormat,
|
|
1056
|
+
undefined,
|
|
1057
|
+
config,
|
|
908
1058
|
) as ResolvedSetting<ExecuteOutputFormat>;
|
|
909
1059
|
}
|
|
910
1060
|
|
|
@@ -930,6 +1080,7 @@ function resolveTimeoutSetting(
|
|
|
930
1080
|
env: Record<string, string | undefined>,
|
|
931
1081
|
via: RouterOsProtocol,
|
|
932
1082
|
commentKv?: ResolvedSetting<number>,
|
|
1083
|
+
config: Record<string, string | undefined> = {},
|
|
933
1084
|
): ResolvedSetting<number> {
|
|
934
1085
|
const resolved = resolveStringSetting(
|
|
935
1086
|
timeout === undefined ? undefined : String(timeout),
|
|
@@ -950,6 +1101,7 @@ function resolveTimeoutSetting(
|
|
|
950
1101
|
return parsed;
|
|
951
1102
|
},
|
|
952
1103
|
commentKv,
|
|
1104
|
+
config,
|
|
953
1105
|
);
|
|
954
1106
|
if (!resolved) {
|
|
955
1107
|
throw new Error("timeout resolution produced no value");
|
|
@@ -1277,10 +1429,6 @@ function tokenizeRouterOsCli(input: string): string[] {
|
|
|
1277
1429
|
return tokens;
|
|
1278
1430
|
}
|
|
1279
1431
|
|
|
1280
|
-
function routerOsStringLiteral(value: string): string {
|
|
1281
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
1432
|
function syntaxCause(error: unknown): unknown {
|
|
1285
1433
|
if (error instanceof CentrsError) {
|
|
1286
1434
|
return error.causeData ?? error.context ?? error.toJSON();
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B2 arg fill — claims argument names and their `=` on the residual.
|
|
3
|
+
*
|
|
4
|
+
* This is the second B2 fill of #264, added after the operator fill. It runs
|
|
5
|
+
* **after** the proof-only spans (`comment` + `variable-*`) and **before** the
|
|
6
|
+
* operator fill, so the fill order is structural: `spans` claims first, then
|
|
7
|
+
* `arg`, then `operator` sees only what neither wanted. That ordering is why the
|
|
8
|
+
* operator fill can abstain on `, / = -` outside `( )`, on bytes glued after
|
|
9
|
+
* an argument `=`, and on bytes glued into an argument name — those bytes
|
|
10
|
+
* belong to a later fill, not to a smarter operator scanner.
|
|
11
|
+
*
|
|
12
|
+
* The source is `src/explain/args.ts`'s `Argument` — `span` and `valueSpan`
|
|
13
|
+
* already rebased into document analyzed-byte space by `src/explain.ts`. The
|
|
14
|
+
* name run is `[span.start, valueSpan.start - 1)` and the `=` is the single
|
|
15
|
+
* byte at `valueSpan.start - 1`; the value itself is `valueSpan` and is left
|
|
16
|
+
* for the next fill (see #293). A `positional` has no `name`/`valueSpan` and a
|
|
17
|
+
* `query` is a `?` word whose interior `args.ts` explicitly refuses to split
|
|
18
|
+
* (see `Argument.value`'s doc comment), so both are ignored — the `=` is
|
|
19
|
+
* derived from the token shape, never by scanning for the byte.
|
|
20
|
+
*
|
|
21
|
+
* `Argument.value` is not `Argument.text` and is never read: `value` is absent
|
|
22
|
+
* whenever the source spells a substitution or an escape this phase does not
|
|
23
|
+
* decode. This slice claims `span`/`valueSpan` *positions* only.
|
|
24
|
+
*
|
|
25
|
+
* A statement whose bytes were normalized is not addressable: `explain.ts`
|
|
26
|
+
* already refuses those (`its text was normalized`), and this fill inherits the
|
|
27
|
+
* refusal — it only sees tokens from `read === true` statements, which are
|
|
28
|
+
* exactly the addressable ones.
|
|
29
|
+
*
|
|
30
|
+
* Vocabulary is provisional until #264 B5: one `arg` class for both the name
|
|
31
|
+
* bytes and the `=` separator (design decision 2 of #293 — emit first, name
|
|
32
|
+
* later; `highlight`'s `syntax-meta` is a residual merge and never a source).
|
|
33
|
+
* If B5 splits the `=` into its own class, the byte coverage does not move,
|
|
34
|
+
* only the retag.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { ExplainArgumentToken, ExplainToken } from "../explain.ts";
|
|
38
|
+
import { clipToResidual } from "./token-ranges.ts";
|
|
39
|
+
|
|
40
|
+
export type ArgCandidate = ExplainArgumentToken;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Argument name + `=` spans on the residual.
|
|
44
|
+
*
|
|
45
|
+
* `analyzed` is the ASCII-normalized document text (only used for length
|
|
46
|
+
* checks); `residual` is the gap set left by earlier fills (sorted, no
|
|
47
|
+
* overlaps); `candidates` are the already-rebased `Argument` tokens from
|
|
48
|
+
* `read === true` statements. Every emitted span's bytes are fully inside
|
|
49
|
+
* `residual`, sorted by `start`, non-overlapping, and carry
|
|
50
|
+
* `class: "arg"` + `ev: "e11"`.
|
|
51
|
+
*/
|
|
52
|
+
export function argSpans(
|
|
53
|
+
analyzed: string,
|
|
54
|
+
residual: readonly { start: number; end: number }[],
|
|
55
|
+
candidates: readonly ArgCandidate[],
|
|
56
|
+
): ExplainToken[] {
|
|
57
|
+
const len = analyzed.length;
|
|
58
|
+
if (len === 0 || residual.length === 0 || candidates.length === 0) return [];
|
|
59
|
+
|
|
60
|
+
const out: ExplainToken[] = [];
|
|
61
|
+
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (candidate.kind !== "attribute") continue;
|
|
64
|
+
if (candidate.valueSpan === undefined) continue;
|
|
65
|
+
if (candidate.name === undefined) continue;
|
|
66
|
+
const nameStart = candidate.span.start;
|
|
67
|
+
const eqEnd = candidate.valueSpan.start;
|
|
68
|
+
const eqStart = eqEnd - 1;
|
|
69
|
+
const nameEnd = eqStart;
|
|
70
|
+
// Defensive: token shape must give non-empty name and single-byte `=`.
|
|
71
|
+
if (
|
|
72
|
+
!Number.isInteger(nameStart) ||
|
|
73
|
+
!Number.isInteger(nameEnd) ||
|
|
74
|
+
!Number.isInteger(eqStart) ||
|
|
75
|
+
!Number.isInteger(eqEnd)
|
|
76
|
+
)
|
|
77
|
+
continue;
|
|
78
|
+
if (nameStart < 0 || eqEnd > len) continue;
|
|
79
|
+
// Non-empty name. This is also what rejects `valueSpan.start === 0`, where
|
|
80
|
+
// `eqStart` would be -1: `nameEnd` is -1 too, so any `nameStart >= 0` trips
|
|
81
|
+
// it. `analyzed[-1]` on the next line would be `undefined` and continue
|
|
82
|
+
// anyway, so the order is safe either way — but the name check is the one
|
|
83
|
+
// that carries the intent.
|
|
84
|
+
if (nameStart >= nameEnd) continue;
|
|
85
|
+
// The whole token's `=` byte must be `=` in the analyzed text — a cheap
|
|
86
|
+
// shape check that catches a caller that passed un-rebased or misaligned
|
|
87
|
+
// spans. Not a scan for `=`; the position is derived from `valueSpan`.
|
|
88
|
+
if (analyzed[eqStart] !== "=") continue;
|
|
89
|
+
// Also ensure the candidate lies fully inside the analyzed length.
|
|
90
|
+
if (candidate.span.start < 0 || candidate.span.end > len) continue;
|
|
91
|
+
if (candidate.valueSpan.start < 0 || candidate.valueSpan.end > len)
|
|
92
|
+
continue;
|
|
93
|
+
|
|
94
|
+
// The name run and the `=` are contiguous (`nameEnd === eqStart`), so
|
|
95
|
+
// clip them as ONE range per candidate. That yields the maximal `[name=]`
|
|
96
|
+
// run for free — split only where the residual itself has a hole — and
|
|
97
|
+
// makes cross-candidate fusion structurally impossible rather than merely
|
|
98
|
+
// unreachable: coalescing adjacent runs globally would merge two distinct
|
|
99
|
+
// attributes into one token if `lexArguments` ever stopped guaranteeing a
|
|
100
|
+
// separator between them. A `variable-*` span claiming only the `=` still
|
|
101
|
+
// leaves the name run behind, because the residual hole does the splitting.
|
|
102
|
+
for (const r of clipToResidual(nameStart, eqEnd, residual)) {
|
|
103
|
+
out.push({
|
|
104
|
+
start: r.start,
|
|
105
|
+
end: r.end,
|
|
106
|
+
class: "arg" as const,
|
|
107
|
+
ev: "e11",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
out.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
113
|
+
return out;
|
|
114
|
+
}
|