@tikoci/centrs 0.1.0 → 0.1.2
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 +73 -2
- package/GLOSSARY.txt +16 -0
- package/README.md +1 -1
- package/commands/AGENTS.md +3 -2
- package/commands/api/AGENTS.md +212 -0
- package/commands/api/README.md +159 -0
- package/commands/api/examples.md +325 -0
- package/commands/btest/README.md +56 -31
- package/commands/btest/examples.md +35 -10
- package/commands/devices/README.md +26 -0
- package/commands/discover/README.md +17 -8
- package/commands/discover/examples.md +10 -5
- package/commands/execute/README.md +9 -3
- package/commands/retrieve/README.md +1 -1
- package/docs/CONSTITUTION.md +6 -6
- package/docs/MATRIX.md +46 -15
- package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
- package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
- package/docs/errors/usage/invalid-method.md +23 -0
- package/package.json +5 -5
- package/src/api.ts +1555 -0
- package/src/btest.ts +61 -1
- package/src/cli/api.ts +536 -0
- package/src/cli/btest.ts +45 -1
- package/src/cli/common.ts +64 -0
- package/src/cli/discover.ts +26 -0
- package/src/cli/execute.ts +30 -4
- package/src/cli/missing-target.ts +171 -0
- package/src/cli/retrieve.ts +32 -14
- package/src/cli/terminal.ts +18 -4
- package/src/cli/transfer.ts +37 -9
- package/src/cli.ts +22 -0
- package/src/core/error-catalog.ts +14 -0
- package/src/core/fanout.ts +160 -0
- package/src/core/inspect.ts +170 -0
- package/src/discover.ts +96 -23
- package/src/execute.ts +14 -47
- package/src/index.ts +61 -8
- package/src/protocols/adapter.ts +328 -0
- package/src/protocols/btest-session.ts +279 -46
- package/src/protocols/btest.ts +6 -2
- package/src/protocols/native-api.ts +223 -30
- package/src/retrieve-fanout.ts +13 -141
- package/src/retrieve.ts +30 -87
- package/commands/stream/README.md +0 -70
package/src/index.ts
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
export const centrsVersion = "0.1.0";
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
type ApiEnvelope,
|
|
5
|
+
type ApiErrorEnvelope,
|
|
6
|
+
type ApiMethod,
|
|
7
|
+
type ApiOperationMeta,
|
|
8
|
+
type ApiOutputFormat,
|
|
9
|
+
type ApiRequest,
|
|
10
|
+
type ApiRequestSummary,
|
|
11
|
+
type ApiStreamMeta,
|
|
12
|
+
type ApiStreamStopReason,
|
|
13
|
+
type ApiStreamSummary,
|
|
14
|
+
type ApiSuccessEnvelope,
|
|
15
|
+
api,
|
|
16
|
+
apiEnvelope,
|
|
17
|
+
apiListen,
|
|
18
|
+
apiMethods,
|
|
19
|
+
apiOutputFormats,
|
|
20
|
+
buildApiBody,
|
|
21
|
+
buildApiErrorEnvelope,
|
|
22
|
+
buildApiErrorEnvelopeFromResolved,
|
|
23
|
+
buildApiQuery,
|
|
24
|
+
buildProtocolApiRequest,
|
|
25
|
+
isApiMutating,
|
|
26
|
+
mapMethodToVerb,
|
|
27
|
+
type NormalizedApiEndpoint,
|
|
28
|
+
normalizeApiEndpoint,
|
|
29
|
+
type ResolvedApiRequest,
|
|
30
|
+
renderApiEnvelope,
|
|
31
|
+
renderApiStreamLine,
|
|
32
|
+
resolveApiRequest,
|
|
33
|
+
runResolvedApi,
|
|
34
|
+
validateApiRequestShape,
|
|
35
|
+
} from "./api.ts";
|
|
3
36
|
export {
|
|
4
37
|
BTEST_CLIENT_CSV_HEADER,
|
|
5
38
|
BTEST_SERVER_CSV_HEADER,
|
|
@@ -41,6 +74,31 @@ export {
|
|
|
41
74
|
type SettingSourceKind,
|
|
42
75
|
type Warning,
|
|
43
76
|
} from "./core/envelope.ts";
|
|
77
|
+
export {
|
|
78
|
+
FANOUT_BACKOFF_BASE_MS,
|
|
79
|
+
FANOUT_CONCURRENCY_DEFAULTS,
|
|
80
|
+
FANOUT_MAX_RETRIES,
|
|
81
|
+
FANOUT_RETRYABLE_CODES,
|
|
82
|
+
isRetryableFanoutError,
|
|
83
|
+
resolveFanoutConcurrency,
|
|
84
|
+
runBoundedPool,
|
|
85
|
+
runWithRetry,
|
|
86
|
+
summarizeFanout,
|
|
87
|
+
} from "./core/fanout.ts";
|
|
88
|
+
export {
|
|
89
|
+
extractCompletionNames,
|
|
90
|
+
type InspectBackend,
|
|
91
|
+
type InspectChildItem,
|
|
92
|
+
type InspectCompletionItem,
|
|
93
|
+
type InspectRequestKind,
|
|
94
|
+
inspectChildren,
|
|
95
|
+
inspectChildrenOrEmpty,
|
|
96
|
+
inspectCompletions,
|
|
97
|
+
inspectPath,
|
|
98
|
+
isArgumentNode,
|
|
99
|
+
isCommandNode,
|
|
100
|
+
pathTokens,
|
|
101
|
+
} from "./core/inspect.ts";
|
|
44
102
|
export {
|
|
45
103
|
encodeMndpPacket,
|
|
46
104
|
MNDP_BROADCAST_ADDRESS,
|
|
@@ -185,11 +243,14 @@ export {
|
|
|
185
243
|
validateExecuteRequestShape,
|
|
186
244
|
} from "./execute.ts";
|
|
187
245
|
export {
|
|
246
|
+
type ApiVerb,
|
|
188
247
|
createProtocolAdapter,
|
|
189
248
|
getProtocolPlan,
|
|
190
249
|
type ProtocolAdapter,
|
|
191
250
|
type ProtocolAdapterCapabilities,
|
|
192
251
|
type ProtocolAdapterConfig,
|
|
252
|
+
type ProtocolApiRequest,
|
|
253
|
+
type ProtocolApiResult,
|
|
193
254
|
type ProtocolCapability,
|
|
194
255
|
type ProtocolExecuteRequest,
|
|
195
256
|
type ProtocolExecuteResult,
|
|
@@ -262,21 +323,13 @@ export {
|
|
|
262
323
|
} from "./retrieve.ts";
|
|
263
324
|
export {
|
|
264
325
|
buildRetrieveFanoutErrorEnvelope,
|
|
265
|
-
isRetryableFanoutError,
|
|
266
|
-
RETRIEVE_FANOUT_BACKOFF_BASE_MS,
|
|
267
|
-
RETRIEVE_FANOUT_CONCURRENCY_DEFAULTS,
|
|
268
|
-
RETRIEVE_FANOUT_MAX_RETRIES,
|
|
269
|
-
RETRIEVE_FANOUT_RETRYABLE_CODES,
|
|
270
326
|
type RetrieveFanoutData,
|
|
271
327
|
type RetrieveFanoutEnvelope,
|
|
272
328
|
type RetrieveFanoutErrorEnvelope,
|
|
273
329
|
type RetrieveFanoutOperationMeta,
|
|
274
330
|
type RetrieveGroupInternals,
|
|
275
331
|
renderRetrieveFanoutEnvelope,
|
|
276
|
-
resolveFanoutConcurrency,
|
|
277
332
|
retrieveGroup,
|
|
278
|
-
runBoundedPool,
|
|
279
|
-
summarizeFanout,
|
|
280
333
|
} from "./retrieve-fanout.ts";
|
|
281
334
|
export {
|
|
282
335
|
buildSshTerminalArgv,
|
package/src/protocols/adapter.ts
CHANGED
|
@@ -106,6 +106,54 @@ export interface ProtocolExecuteResult {
|
|
|
106
106
|
ret?: string;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* RouterOS verb the `api` orchestrator resolved from the HTTP method (the gh-api
|
|
111
|
+
* `-X` map: `GET`→print, `PUT`→add, `PATCH`→set, `DELETE`→remove, `POST`→run).
|
|
112
|
+
* `print` covers list, get-singleton, and get-one (disambiguated by `id`).
|
|
113
|
+
*/
|
|
114
|
+
export type ApiVerb = "print" | "add" | "set" | "remove" | "run";
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A normalized `api` request the orchestrator hands to a transport. It is
|
|
118
|
+
* transport-agnostic: the REST adapter maps the verb to an HTTP method + URL
|
|
119
|
+
* (id-in-path, `.query`/`.proplist` body), the native adapter maps it to a
|
|
120
|
+
* tagged `talk` (`=.id=`, `?`-prefixed queries, `=.proplist=`). The orchestrator
|
|
121
|
+
* owns method→verb, validation, and the write gate; the adapter only executes.
|
|
122
|
+
*/
|
|
123
|
+
export interface ProtocolApiRequest {
|
|
124
|
+
verb: ApiVerb;
|
|
125
|
+
/** Slash path without id, e.g. `/ip/address`. For `run`, the command target (`/interface/monitor-traffic`); ignored when `script` is set. */
|
|
126
|
+
path: string;
|
|
127
|
+
/** Object id (`*1`) for get-one/set/remove — REST puts it in the URL, native in `=.id=`. */
|
|
128
|
+
id?: string;
|
|
129
|
+
/** Body fields (string values), the JSON object for add/set or the args for a `run` command. */
|
|
130
|
+
attributes?: Record<string, string>;
|
|
131
|
+
/** Row filter in REST `.query` form (no leading `?`), e.g. `["type=ether", "#!"]`; native prefixes each with `?`. */
|
|
132
|
+
query?: readonly string[];
|
|
133
|
+
/** Property projection → REST `.proplist` / native `=.proplist=`. */
|
|
134
|
+
proplist?: readonly string[];
|
|
135
|
+
/** Raw console line for the `/execute` script form (`run`); when set, `path`/`attributes` are ignored. */
|
|
136
|
+
script?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Result of an `apiRequest` round-trip: the response body shaped for the envelope. */
|
|
140
|
+
export interface ProtocolApiResult {
|
|
141
|
+
/** REST returns the body verbatim (array / object / scalar); native re-maps `!re` records to the same rest-style shape. */
|
|
142
|
+
data: unknown;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Options for an open-ended `listen` follow. */
|
|
146
|
+
export interface ProtocolListenOptions {
|
|
147
|
+
/** Abort the listen (sends `/cancel`); the generator then ends cleanly. */
|
|
148
|
+
signal?: AbortSignal;
|
|
149
|
+
/**
|
|
150
|
+
* Fired once the subscription is on the wire (connection up, listen sentence
|
|
151
|
+
* written) — a real "listening" barrier for callers/tests that must act only
|
|
152
|
+
* after the follow is established, instead of a blind timer.
|
|
153
|
+
*/
|
|
154
|
+
onListening?: () => void;
|
|
155
|
+
}
|
|
156
|
+
|
|
109
157
|
/**
|
|
110
158
|
* Runtime transport seam. The orchestrator drives validation and data fetch
|
|
111
159
|
* through these operations so REST and native-api share the same pipeline and
|
|
@@ -124,6 +172,18 @@ export interface ProtocolAdapter {
|
|
|
124
172
|
list(path: string, options: RetrieveListOptions): Promise<unknown[]>;
|
|
125
173
|
/** Run a CLI-shaped command (WP-1c write surface). */
|
|
126
174
|
execute(request: ProtocolExecuteRequest): Promise<ProtocolExecuteResult>;
|
|
175
|
+
/** Run a normalized structured `api` request (the gh-api passthrough surface). */
|
|
176
|
+
apiRequest(request: ProtocolApiRequest): Promise<ProtocolApiResult>;
|
|
177
|
+
/**
|
|
178
|
+
* Open-ended `/listen` follow (native-api only). Yields one rest-style change
|
|
179
|
+
* record per `!re` frame (deletions carry `.dead`), until cancelled via
|
|
180
|
+
* `options.signal` or the consumer stopping. REST and console transports
|
|
181
|
+
* reject with `transport/capability-unsupported`.
|
|
182
|
+
*/
|
|
183
|
+
listen(
|
|
184
|
+
request: ProtocolApiRequest,
|
|
185
|
+
options: ProtocolListenOptions,
|
|
186
|
+
): AsyncIterable<Record<string, unknown>>;
|
|
127
187
|
/** Release any underlying connection. Safe to call when never connected. */
|
|
128
188
|
close(): Promise<void>;
|
|
129
189
|
}
|
|
@@ -207,10 +267,105 @@ class RestAdapter implements ProtocolAdapter {
|
|
|
207
267
|
return normalizeRestExecute(data);
|
|
208
268
|
}
|
|
209
269
|
|
|
270
|
+
async apiRequest(request: ProtocolApiRequest): Promise<ProtocolApiResult> {
|
|
271
|
+
const base = request.path.replace(/\/$/, "");
|
|
272
|
+
switch (request.verb) {
|
|
273
|
+
case "print": {
|
|
274
|
+
const hasQuery = (request.query?.length ?? 0) > 0;
|
|
275
|
+
const hasProplist = (request.proplist?.length ?? 0) > 0;
|
|
276
|
+
if (request.id && !hasQuery && !hasProplist) {
|
|
277
|
+
// GET one by id: RouterOS REST addresses a single object in the URL.
|
|
278
|
+
return {
|
|
279
|
+
data: await this.requestRest("GET", `${base}/${request.id}`),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (request.id || hasQuery || hasProplist) {
|
|
283
|
+
// A GET cannot carry a body, so `.query`/`.proplist` projection (and an
|
|
284
|
+
// id, folded into `.query` as `.id=`) rides a POST to the `/print`
|
|
285
|
+
// sub-endpoint — the documented REST idiom, matching native's `?`-words.
|
|
286
|
+
const data = await this.requestRest(
|
|
287
|
+
"POST",
|
|
288
|
+
`${base}/print`,
|
|
289
|
+
restQueryBody(request),
|
|
290
|
+
);
|
|
291
|
+
// An id addresses one row → unwrap to a single object, matching the
|
|
292
|
+
// GET-by-id shape and native's `?.id=` read.
|
|
293
|
+
if (request.id && Array.isArray(data)) {
|
|
294
|
+
return { data: data[0] ?? null };
|
|
295
|
+
}
|
|
296
|
+
return { data };
|
|
297
|
+
}
|
|
298
|
+
return { data: await this.requestRest("GET", base) };
|
|
299
|
+
}
|
|
300
|
+
case "add":
|
|
301
|
+
return {
|
|
302
|
+
data: await this.requestRest("PUT", base, request.attributes ?? {}),
|
|
303
|
+
};
|
|
304
|
+
case "set":
|
|
305
|
+
return {
|
|
306
|
+
data: await this.requestRest(
|
|
307
|
+
"PATCH",
|
|
308
|
+
`${base}/${request.id}`,
|
|
309
|
+
request.attributes ?? {},
|
|
310
|
+
),
|
|
311
|
+
};
|
|
312
|
+
case "remove":
|
|
313
|
+
return {
|
|
314
|
+
data: await this.requestRest("DELETE", `${base}/${request.id}`),
|
|
315
|
+
};
|
|
316
|
+
case "run": {
|
|
317
|
+
if (request.script !== undefined) {
|
|
318
|
+
// Sync script run: `as-string` makes `/rest/execute` block and return the
|
|
319
|
+
// captured output rather than scheduling an async job (CHR-grounded).
|
|
320
|
+
const body = await this.requestRest("POST", "/execute", {
|
|
321
|
+
script: request.script,
|
|
322
|
+
"as-string": "",
|
|
323
|
+
});
|
|
324
|
+
const result = normalizeRestExecute(body);
|
|
325
|
+
return { data: result.ret ?? result.records };
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
data: await this.requestRest("POST", base, request.attributes ?? {}),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
default:
|
|
332
|
+
return exhaustiveApiVerb(request.verb);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// REST cannot hold an open-ended follow (the 60s cap); listen is native-only.
|
|
337
|
+
// biome-ignore lint/correctness/useYield: a throw-only async generator yields nothing.
|
|
338
|
+
async *listen(): AsyncGenerator<Record<string, unknown>> {
|
|
339
|
+
throw new CentrsError({
|
|
340
|
+
code: "transport/capability-unsupported",
|
|
341
|
+
summary: "REST cannot follow an open-ended `--stream` (60s cap).",
|
|
342
|
+
remediation:
|
|
343
|
+
"Open-ended follow is native-api only: use `--via native-api`, or drop `--stream` for a bounded one-shot.",
|
|
344
|
+
context: { protocol: this.protocol, capability: "listen" },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
210
348
|
async close(): Promise<void> {
|
|
211
349
|
// REST is stateless; nothing to release.
|
|
212
350
|
}
|
|
213
351
|
|
|
352
|
+
private async requestRest(
|
|
353
|
+
method: string,
|
|
354
|
+
path: string,
|
|
355
|
+
body?: Record<string, unknown>,
|
|
356
|
+
): Promise<unknown> {
|
|
357
|
+
const init: RequestInit =
|
|
358
|
+
body === undefined
|
|
359
|
+
? { method }
|
|
360
|
+
: {
|
|
361
|
+
method,
|
|
362
|
+
body: JSON.stringify(body),
|
|
363
|
+
headers: { "Content-Type": "application/json" },
|
|
364
|
+
};
|
|
365
|
+
const response = await this.fetchRest(path, init);
|
|
366
|
+
return response.data;
|
|
367
|
+
}
|
|
368
|
+
|
|
214
369
|
private async restGet(path: string): Promise<unknown> {
|
|
215
370
|
const response = await this.fetchRest(path, { method: "GET" });
|
|
216
371
|
return response.data;
|
|
@@ -472,6 +627,110 @@ class NativeApiAdapter implements ProtocolAdapter {
|
|
|
472
627
|
return { records: repliesToRecords(replies) };
|
|
473
628
|
}
|
|
474
629
|
|
|
630
|
+
async apiRequest(request: ProtocolApiRequest): Promise<ProtocolApiResult> {
|
|
631
|
+
const base = request.path.replace(/\/$/, "");
|
|
632
|
+
switch (request.verb) {
|
|
633
|
+
case "print": {
|
|
634
|
+
const command: NativeApiCommand = { command: `${base}/print` };
|
|
635
|
+
const queries: string[] = [];
|
|
636
|
+
// REST `.query` words map 1:1 to native `?`-prefixed words (CHR-grounded).
|
|
637
|
+
for (const word of request.query ?? []) {
|
|
638
|
+
queries.push(`?${word}`);
|
|
639
|
+
}
|
|
640
|
+
// No native get-one-by-id shorthand: address a single row with `?.id=`.
|
|
641
|
+
if (request.id) {
|
|
642
|
+
queries.push(`?.id=${request.id}`);
|
|
643
|
+
}
|
|
644
|
+
if (queries.length > 0) {
|
|
645
|
+
command.queries = queries;
|
|
646
|
+
}
|
|
647
|
+
if (request.proplist && request.proplist.length > 0) {
|
|
648
|
+
command.proplist = request.proplist;
|
|
649
|
+
}
|
|
650
|
+
const records = repliesToRecords(await this.talk(command));
|
|
651
|
+
return { data: request.id ? (records[0] ?? null) : records };
|
|
652
|
+
}
|
|
653
|
+
case "add": {
|
|
654
|
+
const records = repliesToRecords(
|
|
655
|
+
await this.talk({
|
|
656
|
+
command: `${base}/add`,
|
|
657
|
+
attributes: request.attributes ?? {},
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
return { data: restStyleMutationData(records) };
|
|
661
|
+
}
|
|
662
|
+
case "set": {
|
|
663
|
+
const records = repliesToRecords(
|
|
664
|
+
await this.talk({
|
|
665
|
+
command: `${base}/set`,
|
|
666
|
+
attributes: {
|
|
667
|
+
...(request.attributes ?? {}),
|
|
668
|
+
".id": request.id ?? "",
|
|
669
|
+
},
|
|
670
|
+
}),
|
|
671
|
+
);
|
|
672
|
+
return { data: restStyleMutationData(records) };
|
|
673
|
+
}
|
|
674
|
+
case "remove": {
|
|
675
|
+
const records = repliesToRecords(
|
|
676
|
+
await this.talk({
|
|
677
|
+
command: `${base}/remove`,
|
|
678
|
+
attributes: { ".id": request.id ?? "" },
|
|
679
|
+
}),
|
|
680
|
+
);
|
|
681
|
+
return { data: restStyleMutationData(records) };
|
|
682
|
+
}
|
|
683
|
+
case "run": {
|
|
684
|
+
if (request.script !== undefined) {
|
|
685
|
+
// `as-string` makes `/execute` run synchronously and return the captured
|
|
686
|
+
// output; without it the native API schedules a job and returns its id
|
|
687
|
+
// (CHR-grounded — mirrors REST `/rest/execute`).
|
|
688
|
+
const records = repliesToRecords(
|
|
689
|
+
await this.talk({
|
|
690
|
+
command: "/execute",
|
|
691
|
+
attributes: { script: request.script, "as-string": "" },
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
return { data: restStyleRunData(records) };
|
|
695
|
+
}
|
|
696
|
+
const command: NativeApiCommand = { command: base };
|
|
697
|
+
if (request.attributes && Object.keys(request.attributes).length > 0) {
|
|
698
|
+
command.attributes = request.attributes;
|
|
699
|
+
}
|
|
700
|
+
return { data: repliesToRecords(await this.talk(command)) };
|
|
701
|
+
}
|
|
702
|
+
default:
|
|
703
|
+
return exhaustiveApiVerb(request.verb);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async *listen(
|
|
708
|
+
request: ProtocolApiRequest,
|
|
709
|
+
options: ProtocolListenOptions,
|
|
710
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
711
|
+
const base = request.path.replace(/\/$/, "");
|
|
712
|
+
const command: NativeApiCommand = { command: `${base}/listen` };
|
|
713
|
+
// A listen can filter which changes it follows with the same `?`-words /
|
|
714
|
+
// `=.proplist=` as print (REST `.query` word == native `?`-word minus `?`).
|
|
715
|
+
const queries = (request.query ?? []).map((word) => `?${word}`);
|
|
716
|
+
// An addressed row (`ip/address/*1 --stream`) follows just that row, the
|
|
717
|
+
// same `?.id=` mapping one-shot native reads use.
|
|
718
|
+
if (request.id) {
|
|
719
|
+
queries.push(`?.id=${request.id}`);
|
|
720
|
+
}
|
|
721
|
+
if (queries.length > 0) {
|
|
722
|
+
command.queries = queries;
|
|
723
|
+
}
|
|
724
|
+
if (request.proplist && request.proplist.length > 0) {
|
|
725
|
+
command.proplist = request.proplist;
|
|
726
|
+
}
|
|
727
|
+
const session = await this.connect();
|
|
728
|
+
for await (const reply of session.listen(command, options)) {
|
|
729
|
+
// Each `!re` becomes a rest-style record (string values; `.dead` preserved).
|
|
730
|
+
yield { ...reply.attributes };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
475
734
|
async close(): Promise<void> {
|
|
476
735
|
this.session?.close();
|
|
477
736
|
this.session = undefined;
|
|
@@ -614,6 +873,15 @@ class MacTelnetAdapter implements ProtocolAdapter {
|
|
|
614
873
|
return { records: [], ret: output };
|
|
615
874
|
}
|
|
616
875
|
|
|
876
|
+
apiRequest(): Promise<ProtocolApiResult> {
|
|
877
|
+
return Promise.reject(this.unsupported("structured api requests"));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// biome-ignore lint/correctness/useYield: a reject-only async generator yields nothing.
|
|
881
|
+
async *listen(): AsyncGenerator<Record<string, unknown>> {
|
|
882
|
+
throw this.unsupported("open-ended listen streaming");
|
|
883
|
+
}
|
|
884
|
+
|
|
617
885
|
async close(): Promise<void> {
|
|
618
886
|
this.console?.close();
|
|
619
887
|
this.transport?.close();
|
|
@@ -756,11 +1024,71 @@ class SshExecAdapter implements ProtocolAdapter {
|
|
|
756
1024
|
return { records: [], ret: output };
|
|
757
1025
|
}
|
|
758
1026
|
|
|
1027
|
+
apiRequest(): Promise<ProtocolApiResult> {
|
|
1028
|
+
return Promise.reject(this.unsupported("structured api requests"));
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// biome-ignore lint/correctness/useYield: a reject-only async generator yields nothing.
|
|
1032
|
+
async *listen(): AsyncGenerator<Record<string, unknown>> {
|
|
1033
|
+
throw this.unsupported("open-ended listen streaming");
|
|
1034
|
+
}
|
|
1035
|
+
|
|
759
1036
|
close(): Promise<void> {
|
|
760
1037
|
return Promise.resolve();
|
|
761
1038
|
}
|
|
762
1039
|
}
|
|
763
1040
|
|
|
1041
|
+
/** REST `.query` / `.proplist` projection body for a POST `/print`. */
|
|
1042
|
+
function restQueryBody(request: ProtocolApiRequest): Record<string, unknown> {
|
|
1043
|
+
const body: Record<string, unknown> = {};
|
|
1044
|
+
// Fold an id into `.query` as `.id=<id>` so id+query/proplist reads work over
|
|
1045
|
+
// REST exactly as they do over native (`?.id=` plus the `?`-words).
|
|
1046
|
+
const query = [
|
|
1047
|
+
...(request.id ? [`.id=${request.id}`] : []),
|
|
1048
|
+
...(request.query ?? []),
|
|
1049
|
+
];
|
|
1050
|
+
if (query.length > 0) {
|
|
1051
|
+
body[".query"] = query;
|
|
1052
|
+
}
|
|
1053
|
+
if (request.proplist && request.proplist.length > 0) {
|
|
1054
|
+
body[".proplist"] = request.proplist;
|
|
1055
|
+
}
|
|
1056
|
+
return body;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Re-map a native add/set/remove reply to the rest-style shape. `add` returns the
|
|
1061
|
+
* new id in `=ret=`; surface it as `{".id": ret}` (REST returns the created
|
|
1062
|
+
* object with `.id`). `set`/`remove` reply with a bare `!done` (no records) → no
|
|
1063
|
+
* meaningful body, mirrored as `null`.
|
|
1064
|
+
*/
|
|
1065
|
+
function restStyleMutationData(
|
|
1066
|
+
records: readonly Record<string, string>[],
|
|
1067
|
+
): unknown {
|
|
1068
|
+
if (records.length === 0) {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
const record = records[0] ?? {};
|
|
1072
|
+
const ret = record["ret"];
|
|
1073
|
+
if (typeof ret === "string" && /^\*[0-9A-F]+$/i.test(ret)) {
|
|
1074
|
+
return { ".id": ret };
|
|
1075
|
+
}
|
|
1076
|
+
return records.length === 1 ? record : records;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Re-map a native `/execute` reply: prefer the captured `ret` string, else the records. */
|
|
1080
|
+
function restStyleRunData(records: readonly Record<string, string>[]): unknown {
|
|
1081
|
+
const ret = records.length === 1 ? records[0]?.["ret"] : undefined;
|
|
1082
|
+
if (typeof ret === "string") {
|
|
1083
|
+
return ret;
|
|
1084
|
+
}
|
|
1085
|
+
return records.length === 1 ? records[0] : records;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function exhaustiveApiVerb(verb: never): never {
|
|
1089
|
+
throw new Error(`Unhandled api verb: ${String(verb)}`);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
764
1092
|
function normalizeRestExecute(data: unknown): ProtocolExecuteResult {
|
|
765
1093
|
if (Array.isArray(data)) {
|
|
766
1094
|
return { records: data.filter(isPlainObject) as Record<string, string>[] };
|