@tikoci/centrs 0.1.8 → 0.1.9-next.0
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 +48 -0
- package/README.md +4 -0
- package/commands/api/README.md +5 -0
- package/commands/execute/README.md +117 -0
- package/commands/execute/examples.md +162 -0
- package/commands/retrieve/README.md +2 -1
- package/docs/CLI.md +2 -0
- package/docs/CONSTITUTION.md +29 -1
- package/docs/MATRIX.md +19 -3
- 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 +19 -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/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/missing-labels.md +19 -0
- package/package.json +1 -1
- package/src/api-fanout.ts +7 -1
- package/src/api.ts +25 -6
- package/src/cli/execute.ts +132 -1
- package/src/core/cmr.ts +104 -0
- package/src/core/envelope.ts +16 -0
- package/src/core/error-catalog.ts +71 -0
- package/src/core/fanout.ts +77 -8
- package/src/errors.ts +1 -0
- package/src/execute-cmr.ts +1425 -0
- package/src/execute-fanout.ts +4 -0
- package/src/execute.ts +68 -17
- package/src/index.ts +17 -0
- package/src/protocols/adapter.ts +54 -12
package/src/execute-fanout.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
buildFanoutResolveFailure,
|
|
34
34
|
commonVia,
|
|
35
35
|
defaultFanoutSleep,
|
|
36
|
+
mayRetryUnsentWrite,
|
|
36
37
|
resolveFanoutConcurrency,
|
|
37
38
|
runFanout,
|
|
38
39
|
summarizeFanout,
|
|
@@ -105,6 +106,7 @@ export interface ExecuteFanoutInternals {
|
|
|
105
106
|
) => Promise<SelectionExpansion>;
|
|
106
107
|
execute?: (
|
|
107
108
|
resolved: ResolvedExecuteRequest,
|
|
109
|
+
trace: ValidationTrace,
|
|
108
110
|
) => Promise<ExecuteSuccessEnvelope>;
|
|
109
111
|
sleep?: (ms: number) => Promise<void>;
|
|
110
112
|
}
|
|
@@ -308,6 +310,8 @@ export async function executeFanout(
|
|
|
308
310
|
),
|
|
309
311
|
member,
|
|
310
312
|
),
|
|
313
|
+
mayRetry: (resolved) =>
|
|
314
|
+
mayRetryUnsentWrite(global.summary.write, traces.get(resolved)),
|
|
311
315
|
sleep,
|
|
312
316
|
});
|
|
313
317
|
|
package/src/execute.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertNotCmrDashboardIl,
|
|
3
|
+
assertNotCmrDashboardPath,
|
|
4
|
+
cmrRunScriptWarnings,
|
|
5
|
+
cmrScriptWarnings,
|
|
6
|
+
} from "./core/cmr.ts";
|
|
1
7
|
import type {
|
|
2
8
|
CentrsEnvelope,
|
|
3
9
|
CentrsErrorEnvelope,
|
|
@@ -16,6 +22,7 @@ import {
|
|
|
16
22
|
canonicalizeExecuteCommand,
|
|
17
23
|
isWriteShaped,
|
|
18
24
|
} from "./core/execute-command.ts";
|
|
25
|
+
import { sentWriteWarnings } from "./core/fanout.ts";
|
|
19
26
|
import { inspectArgumentNames, pathTokens } from "./core/inspect.ts";
|
|
20
27
|
import { mapRouterOsResultError } from "./core/routeros-errors.ts";
|
|
21
28
|
import { routerOsStringLiteral } from "./core/routeros-string.ts";
|
|
@@ -37,6 +44,7 @@ import {
|
|
|
37
44
|
getProtocolPlan,
|
|
38
45
|
type ProtocolAdapter,
|
|
39
46
|
type ProtocolExecuteResult,
|
|
47
|
+
type ProtocolSendOptions,
|
|
40
48
|
plannedProtocols,
|
|
41
49
|
type RouterOsProtocol,
|
|
42
50
|
} from "./protocols/index.ts";
|
|
@@ -288,7 +296,11 @@ export async function runResolvedExecute(
|
|
|
288
296
|
// reports the gate that actually passed rather than a guess.
|
|
289
297
|
trace.validation = validation;
|
|
290
298
|
|
|
291
|
-
const result = await runCommand(resolved, backend
|
|
299
|
+
const result = await runCommand(resolved, backend, {
|
|
300
|
+
onSent: () => {
|
|
301
|
+
trace.dispatched = true;
|
|
302
|
+
},
|
|
303
|
+
});
|
|
292
304
|
const routerOsFailure = routerOsFailureFromResult(
|
|
293
305
|
result,
|
|
294
306
|
resolved.via.value,
|
|
@@ -319,6 +331,7 @@ export async function resolveExecuteRequest(
|
|
|
319
331
|
): Promise<ResolvedExecuteRequest> {
|
|
320
332
|
validateExecuteRequestShape(request);
|
|
321
333
|
const canonical = canonicalizeExecuteCommand(request.command);
|
|
334
|
+
assertNotStructuredDashboard(canonical);
|
|
322
335
|
const config = options.config ?? (await loadEnvFileDefaults(env));
|
|
323
336
|
// A quickchr target is a named-live-provider: it supplies host/port/auth from
|
|
324
337
|
// the live descriptor and bypasses CDB/env resolution for those fields
|
|
@@ -450,9 +463,12 @@ export async function resolveExecuteRequest(
|
|
|
450
463
|
maxResultsBytes,
|
|
451
464
|
yes: request.yes ?? false,
|
|
452
465
|
verbose: request.verbose ?? false,
|
|
453
|
-
warnings:
|
|
454
|
-
?
|
|
455
|
-
|
|
466
|
+
warnings: [
|
|
467
|
+
...(connection ? connection.warnings : (cdbResolution?.warnings ?? [])),
|
|
468
|
+
...(canonical.mode === "structured"
|
|
469
|
+
? cmrRunScriptWarnings(`${canonical.path}/${canonical.verb}`)
|
|
470
|
+
: cmrScriptWarnings(canonical.input)),
|
|
471
|
+
],
|
|
456
472
|
};
|
|
457
473
|
}
|
|
458
474
|
|
|
@@ -477,6 +493,7 @@ export function resolveExecuteGlobalContext(
|
|
|
477
493
|
): ExecuteGlobalContext {
|
|
478
494
|
validateExecuteRequestShape(request);
|
|
479
495
|
const canonical = canonicalizeExecuteCommand(request.command);
|
|
496
|
+
assertNotStructuredDashboard(canonical);
|
|
480
497
|
const via = resolveExecuteProtocol(request, env, undefined, config);
|
|
481
498
|
const format = resolveFormat(request, env, config);
|
|
482
499
|
const validate = resolveBooleanSetting(
|
|
@@ -599,7 +616,14 @@ export function buildExecuteErrorEnvelopeFromResolved(
|
|
|
599
616
|
return {
|
|
600
617
|
ok: false,
|
|
601
618
|
error: serializeCentrsError(centrsError),
|
|
602
|
-
warnings: [
|
|
619
|
+
warnings: [
|
|
620
|
+
...resolved.warnings,
|
|
621
|
+
...sentWriteWarnings(
|
|
622
|
+
isWriteShaped(resolved.canonical),
|
|
623
|
+
centrsError,
|
|
624
|
+
trace,
|
|
625
|
+
),
|
|
626
|
+
],
|
|
603
627
|
tips: resolvedExecuteTips(resolved),
|
|
604
628
|
meta: metaFromResolved(
|
|
605
629
|
resolved,
|
|
@@ -729,7 +753,7 @@ function deviceStageSource(resolved: ResolvedExecuteRequest): string {
|
|
|
729
753
|
* judge. An absent verdict means the caller did not thread a trace, so say
|
|
730
754
|
* nothing extra rather than invent the reason.
|
|
731
755
|
*/
|
|
732
|
-
function offlineStagePassed(
|
|
756
|
+
export function offlineStagePassed(
|
|
733
757
|
verdict: ValidationTrace["offlineVerdict"],
|
|
734
758
|
): EnvelopeValidationStage {
|
|
735
759
|
return {
|
|
@@ -748,7 +772,7 @@ function offlineStagePassed(
|
|
|
748
772
|
* a consumer reading `stages` never has to special-case the disabled shape.
|
|
749
773
|
* `reason` is left off — `enabled: false` already says why.
|
|
750
774
|
*/
|
|
751
|
-
function disabledValidationMeta(): EnvelopeValidationMeta {
|
|
775
|
+
export function disabledValidationMeta(): EnvelopeValidationMeta {
|
|
752
776
|
return {
|
|
753
777
|
enabled: false,
|
|
754
778
|
source: "disabled",
|
|
@@ -888,6 +912,7 @@ async function validateConsoleParseCommand(
|
|
|
888
912
|
});
|
|
889
913
|
// Throws validation/syntax or validation/unknown-attribute on a bad parse.
|
|
890
914
|
classifyParseResult(result.ret ?? "", resolved.command, resolved.via.value);
|
|
915
|
+
assertNotCmrDashboardIl(result.ret ?? "", resolved.command);
|
|
891
916
|
return {
|
|
892
917
|
enabled: true,
|
|
893
918
|
source: `:put [:parse ...] over ${resolved.via.value}`,
|
|
@@ -908,7 +933,7 @@ async function validateConsoleParseCommand(
|
|
|
908
933
|
* `warn` is an abstention, and saying so is the difference between "the analyzer
|
|
909
934
|
* read all of this" and "it declined to judge part of it".
|
|
910
935
|
*/
|
|
911
|
-
function passedStages(
|
|
936
|
+
export function passedStages(
|
|
912
937
|
offline: OfflineGateResult,
|
|
913
938
|
deviceSource: string,
|
|
914
939
|
): EnvelopeValidationStage[] {
|
|
@@ -939,6 +964,7 @@ export async function validateRouterOsScript(
|
|
|
939
964
|
via: RouterOsProtocol,
|
|
940
965
|
): Promise<void> {
|
|
941
966
|
const script = `:put [:parse ${routerOsStringLiteral(command)}]`;
|
|
967
|
+
let il: string;
|
|
942
968
|
try {
|
|
943
969
|
// GH#230: `:parse` never throws (CHR 7.23.3). The diagnostic rides the
|
|
944
970
|
// success value's text — over REST/native the `ret` field (HTTP 200 /
|
|
@@ -947,7 +973,8 @@ export async function validateRouterOsScript(
|
|
|
947
973
|
// Read the return and classify it: one `:parse` covers syntax and the
|
|
948
974
|
// unknown-attribute (name-level) gate.
|
|
949
975
|
const result = await backend.execute({ path: "", command: "", script });
|
|
950
|
-
|
|
976
|
+
il = result.ret ?? "";
|
|
977
|
+
classifyParseResult(il, command, via);
|
|
951
978
|
} catch (error) {
|
|
952
979
|
// The preflight is a syntax gate, but `backend.execute` also surfaces
|
|
953
980
|
// connection and authentication failures (login happens lazily here).
|
|
@@ -984,6 +1011,20 @@ export async function validateRouterOsScript(
|
|
|
984
1011
|
causeData: syntaxCause(error),
|
|
985
1012
|
});
|
|
986
1013
|
}
|
|
1014
|
+
assertNotCmrDashboardIl(il, command);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* The plain spellings of the dashboard are refused before anything is dialed.
|
|
1019
|
+
* Every other spelling (a relative menu change, a block, a substitution) is
|
|
1020
|
+
* found in the `:parse` return, so it is refused by validation.
|
|
1021
|
+
*/
|
|
1022
|
+
function assertNotStructuredDashboard(
|
|
1023
|
+
canonical: CanonicalExecuteCommand,
|
|
1024
|
+
): void {
|
|
1025
|
+
if (canonical.mode === "structured") {
|
|
1026
|
+
assertNotCmrDashboardPath(`${canonical.path}/${canonical.verb}`);
|
|
1027
|
+
}
|
|
987
1028
|
}
|
|
988
1029
|
|
|
989
1030
|
/**
|
|
@@ -998,22 +1039,32 @@ function isPreflightTransportError(error: CentrsError): boolean {
|
|
|
998
1039
|
async function runCommand(
|
|
999
1040
|
resolved: ResolvedExecuteRequest,
|
|
1000
1041
|
backend: ProtocolAdapter,
|
|
1042
|
+
options: ProtocolSendOptions,
|
|
1001
1043
|
): Promise<ProtocolExecuteResult> {
|
|
1002
1044
|
if (resolved.via.value === "mac-telnet" || resolved.via.value === "ssh") {
|
|
1003
1045
|
// Console transports: run the raw CLI line and capture the console output
|
|
1004
1046
|
// (structured path-POST / tagged talk do not apply). ssh runs it as a
|
|
1005
1047
|
// single `ssh host "<command>"`; mac-telnet drives the L2 console.
|
|
1006
|
-
return backend.execute(
|
|
1048
|
+
return backend.execute(
|
|
1049
|
+
{ path: "", command: "", script: resolved.command },
|
|
1050
|
+
options,
|
|
1051
|
+
);
|
|
1007
1052
|
}
|
|
1008
1053
|
if (resolved.canonical.mode === "script") {
|
|
1009
|
-
return backend.execute(
|
|
1054
|
+
return backend.execute(
|
|
1055
|
+
{ path: "", command: "", script: resolved.command },
|
|
1056
|
+
options,
|
|
1057
|
+
);
|
|
1010
1058
|
}
|
|
1011
|
-
return backend.execute(
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1059
|
+
return backend.execute(
|
|
1060
|
+
{
|
|
1061
|
+
path: resolved.canonical.path,
|
|
1062
|
+
command: resolved.canonical.verb,
|
|
1063
|
+
attributes: resolved.canonical.attributes,
|
|
1064
|
+
queries: resolved.canonical.queries,
|
|
1065
|
+
},
|
|
1066
|
+
options,
|
|
1067
|
+
);
|
|
1017
1068
|
}
|
|
1018
1069
|
|
|
1019
1070
|
async function assertWriteConfirmed(
|
package/src/index.ts
CHANGED
|
@@ -286,6 +286,23 @@ export {
|
|
|
286
286
|
runResolvedExecute,
|
|
287
287
|
validateExecuteRequestShape,
|
|
288
288
|
} from "./execute.ts";
|
|
289
|
+
export {
|
|
290
|
+
buildExecuteCmrErrorEnvelope,
|
|
291
|
+
CMR_VALIDATION_SOURCE,
|
|
292
|
+
type CmrController,
|
|
293
|
+
type CmrDevice,
|
|
294
|
+
type CmrScriptCall,
|
|
295
|
+
type CmrScriptOutcome,
|
|
296
|
+
type CmrScriptRow,
|
|
297
|
+
type ExecuteCmrData,
|
|
298
|
+
type ExecuteCmrEnvelope,
|
|
299
|
+
type ExecuteCmrErrorEnvelope,
|
|
300
|
+
type ExecuteCmrInternals,
|
|
301
|
+
type ExecuteCmrOperationMeta,
|
|
302
|
+
type ExecuteCmrRequest,
|
|
303
|
+
executeCmr,
|
|
304
|
+
renderExecuteCmrEnvelope,
|
|
305
|
+
} from "./execute-cmr.ts";
|
|
289
306
|
export {
|
|
290
307
|
buildExecuteFanoutErrorEnvelope,
|
|
291
308
|
type ExecuteFanoutData,
|
package/src/protocols/adapter.ts
CHANGED
|
@@ -180,6 +180,16 @@ export interface ProtocolApiResult {
|
|
|
180
180
|
data: unknown;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Options for a single `execute`/`apiRequest` call. `onSent` fires once the
|
|
185
|
+
* command itself is about to leave for the router: after a native or console
|
|
186
|
+
* login, never during it. A failure before it fired cannot have run the
|
|
187
|
+
* command (#448).
|
|
188
|
+
*/
|
|
189
|
+
export interface ProtocolSendOptions {
|
|
190
|
+
onSent?: () => void;
|
|
191
|
+
}
|
|
192
|
+
|
|
183
193
|
/** Options for incremental native command replies. */
|
|
184
194
|
export interface ProtocolListenOptions {
|
|
185
195
|
/** Abort the listen (sends `/cancel`); the generator then ends cleanly. */
|
|
@@ -237,9 +247,15 @@ export interface ProtocolAdapter {
|
|
|
237
247
|
/** Read a menu as an array of records, optionally projected/detailed. */
|
|
238
248
|
list(path: string, options: RetrieveListOptions): Promise<unknown[]>;
|
|
239
249
|
/** Run a CLI-shaped command (WP-1c write surface). */
|
|
240
|
-
execute(
|
|
250
|
+
execute(
|
|
251
|
+
request: ProtocolExecuteRequest,
|
|
252
|
+
options?: ProtocolSendOptions,
|
|
253
|
+
): Promise<ProtocolExecuteResult>;
|
|
241
254
|
/** Run a normalized structured `api` request (the gh-api passthrough surface). */
|
|
242
|
-
apiRequest(
|
|
255
|
+
apiRequest(
|
|
256
|
+
request: ProtocolApiRequest,
|
|
257
|
+
options?: ProtocolSendOptions,
|
|
258
|
+
): Promise<ProtocolApiResult>;
|
|
243
259
|
/**
|
|
244
260
|
* Incremental native replies to the literal command (the same mapping as
|
|
245
261
|
* `apiRequest`; never rewritten into a `listen`). Yields each `!re` and
|
|
@@ -341,7 +357,10 @@ class RestAdapter implements ProtocolAdapter {
|
|
|
341
357
|
|
|
342
358
|
async execute(
|
|
343
359
|
request: ProtocolExecuteRequest,
|
|
360
|
+
options?: ProtocolSendOptions,
|
|
344
361
|
): Promise<ProtocolExecuteResult> {
|
|
362
|
+
// REST has no session to open first: the request is the command.
|
|
363
|
+
options?.onSent?.();
|
|
345
364
|
if (request.script !== undefined) {
|
|
346
365
|
const data = await this.restPost<unknown>("/execute", {
|
|
347
366
|
script: request.script,
|
|
@@ -369,7 +388,11 @@ class RestAdapter implements ProtocolAdapter {
|
|
|
369
388
|
return normalizeRestExecute(data);
|
|
370
389
|
}
|
|
371
390
|
|
|
372
|
-
async apiRequest(
|
|
391
|
+
async apiRequest(
|
|
392
|
+
request: ProtocolApiRequest,
|
|
393
|
+
options?: ProtocolSendOptions,
|
|
394
|
+
): Promise<ProtocolApiResult> {
|
|
395
|
+
options?.onSent?.();
|
|
373
396
|
const base = request.path.replace(/\/$/, "");
|
|
374
397
|
switch (request.verb) {
|
|
375
398
|
case "print": {
|
|
@@ -765,9 +788,12 @@ class NativeApiAdapter implements ProtocolAdapter {
|
|
|
765
788
|
|
|
766
789
|
async execute(
|
|
767
790
|
request: ProtocolExecuteRequest,
|
|
791
|
+
options?: ProtocolSendOptions,
|
|
768
792
|
): Promise<ProtocolExecuteResult> {
|
|
769
793
|
if (request.script !== undefined) {
|
|
770
|
-
return normalizeNativeExecute(
|
|
794
|
+
return normalizeNativeExecute(
|
|
795
|
+
await this.executeScript(request.script, options),
|
|
796
|
+
);
|
|
771
797
|
}
|
|
772
798
|
const command: NativeApiCommand = {
|
|
773
799
|
command: `${request.path.replace(/\/$/, "")}/${request.command}`,
|
|
@@ -781,7 +807,7 @@ class NativeApiAdapter implements ProtocolAdapter {
|
|
|
781
807
|
if (request.binary) {
|
|
782
808
|
command.binary = true;
|
|
783
809
|
}
|
|
784
|
-
const replies = await this.talk(command);
|
|
810
|
+
const replies = await this.talk(command, options);
|
|
785
811
|
return { records: repliesToRecords(replies) };
|
|
786
812
|
}
|
|
787
813
|
|
|
@@ -794,17 +820,24 @@ class NativeApiAdapter implements ProtocolAdapter {
|
|
|
794
820
|
*/
|
|
795
821
|
private async executeScript(
|
|
796
822
|
script: string,
|
|
823
|
+
options?: ProtocolSendOptions,
|
|
797
824
|
): Promise<Record<string, string>[]> {
|
|
798
|
-
const replies = await this.talk(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
825
|
+
const replies = await this.talk(
|
|
826
|
+
{
|
|
827
|
+
command: "/execute",
|
|
828
|
+
attributes: { script, "as-string": "" },
|
|
829
|
+
},
|
|
830
|
+
options,
|
|
831
|
+
);
|
|
802
832
|
return repliesToRecords(replies);
|
|
803
833
|
}
|
|
804
834
|
|
|
805
|
-
async apiRequest(
|
|
835
|
+
async apiRequest(
|
|
836
|
+
request: ProtocolApiRequest,
|
|
837
|
+
options?: ProtocolSendOptions,
|
|
838
|
+
): Promise<ProtocolApiResult> {
|
|
806
839
|
const records = repliesToRecords(
|
|
807
|
-
await this.talk(nativeCommandFor(request)),
|
|
840
|
+
await this.talk(nativeCommandFor(request), options),
|
|
808
841
|
);
|
|
809
842
|
switch (request.verb) {
|
|
810
843
|
case "print":
|
|
@@ -880,8 +913,13 @@ class NativeApiAdapter implements ProtocolAdapter {
|
|
|
880
913
|
return session;
|
|
881
914
|
}
|
|
882
915
|
|
|
883
|
-
private async talk(
|
|
916
|
+
private async talk(
|
|
917
|
+
command: NativeApiCommand,
|
|
918
|
+
options?: ProtocolSendOptions,
|
|
919
|
+
): Promise<ApiReply[]> {
|
|
920
|
+
// Connecting logs in lazily; only the sentence after `/login` is the command.
|
|
884
921
|
const session = await this.connect();
|
|
922
|
+
options?.onSent?.();
|
|
885
923
|
return this.withTimeout(session.talk(command));
|
|
886
924
|
}
|
|
887
925
|
|
|
@@ -982,6 +1020,7 @@ class MacTelnetAdapter implements ProtocolAdapter {
|
|
|
982
1020
|
|
|
983
1021
|
async execute(
|
|
984
1022
|
request: ProtocolExecuteRequest,
|
|
1023
|
+
options?: ProtocolSendOptions,
|
|
985
1024
|
): Promise<ProtocolExecuteResult> {
|
|
986
1025
|
const cli = request.script;
|
|
987
1026
|
if (cli === undefined) {
|
|
@@ -994,6 +1033,7 @@ class MacTelnetAdapter implements ProtocolAdapter {
|
|
|
994
1033
|
});
|
|
995
1034
|
}
|
|
996
1035
|
const console = await this.open();
|
|
1036
|
+
options?.onSent?.();
|
|
997
1037
|
const { output } = await console.run(cli);
|
|
998
1038
|
return { records: [], ret: output };
|
|
999
1039
|
}
|
|
@@ -1134,6 +1174,7 @@ class SshExecAdapter implements ProtocolAdapter {
|
|
|
1134
1174
|
|
|
1135
1175
|
async execute(
|
|
1136
1176
|
request: ProtocolExecuteRequest,
|
|
1177
|
+
options?: ProtocolSendOptions,
|
|
1137
1178
|
): Promise<ProtocolExecuteResult> {
|
|
1138
1179
|
const cli = request.script;
|
|
1139
1180
|
if (cli === undefined) {
|
|
@@ -1145,6 +1186,7 @@ class SshExecAdapter implements ProtocolAdapter {
|
|
|
1145
1186
|
"Report this bug; runCommand must pass the raw command as `script` for ssh.",
|
|
1146
1187
|
});
|
|
1147
1188
|
}
|
|
1189
|
+
options?.onSent?.();
|
|
1148
1190
|
const output = await this.client.exec(cli);
|
|
1149
1191
|
return { records: [], ret: output };
|
|
1150
1192
|
}
|