@zudojs/api 1.1.1 → 1.2.1
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/README.md +162 -6
- package/dist/api/bindings/cli/apiCli.argv.d.ts +36 -0
- package/dist/api/bindings/cli/apiCli.argv.js +78 -0
- package/dist/api/bindings/cli/apiCli.binding.d.ts +45 -0
- package/dist/api/bindings/cli/apiCli.binding.js +77 -0
- package/dist/api/bindings/cli/apiCli.exitCode.d.ts +31 -0
- package/dist/api/bindings/cli/apiCli.exitCode.js +43 -0
- package/dist/api/bindings/cli/apiCli.values.d.ts +24 -0
- package/dist/api/bindings/cli/apiCli.values.js +71 -0
- package/dist/api/bindings/cli/index.d.ts +12 -0
- package/dist/api/bindings/cli/index.js +9 -0
- package/dist/api/bindings/fetch/apiFetch.handler.d.ts +35 -0
- package/dist/api/bindings/fetch/apiFetch.handler.js +99 -0
- package/dist/api/bindings/fetch/apiFetch.input.d.ts +29 -0
- package/dist/api/bindings/fetch/apiFetch.input.js +74 -0
- package/dist/api/bindings/fetch/index.d.ts +11 -0
- package/dist/api/bindings/fetch/index.js +9 -0
- package/dist/api/bindings/index.d.ts +25 -0
- package/dist/api/bindings/index.js +18 -0
- package/dist/api/bindings/openapi/apiOpenAPI.descriptor.d.ts +37 -0
- package/dist/api/bindings/openapi/apiOpenAPI.descriptor.js +67 -0
- package/dist/api/bindings/openapi/apiOpenAPI.schemas.d.ts +32 -0
- package/dist/api/bindings/openapi/apiOpenAPI.schemas.js +62 -0
- package/dist/api/bindings/openapi/index.d.ts +9 -0
- package/dist/api/bindings/openapi/index.js +8 -0
- package/dist/api/bindings/queue/apiQueue.binding.d.ts +39 -0
- package/dist/api/bindings/queue/apiQueue.binding.js +65 -0
- package/dist/api/bindings/queue/index.d.ts +7 -0
- package/dist/api/bindings/queue/index.js +6 -0
- package/dist/api/bindings/route/apiRoute.resolver.d.ts +33 -0
- package/dist/api/bindings/route/apiRoute.resolver.js +109 -0
- package/dist/api/bindings/route/apiRoute.table.d.ts +53 -0
- package/dist/api/bindings/route/apiRoute.table.js +95 -0
- package/dist/api/bindings/route/apiRoute.type.d.ts +65 -0
- package/dist/api/bindings/route/apiRoute.type.js +8 -0
- package/dist/api/bindings/route/index.d.ts +11 -0
- package/dist/api/bindings/route/index.js +9 -0
- package/dist/api/bindings/rpc/apiRpc.binding.d.ts +46 -0
- package/dist/api/bindings/rpc/apiRpc.binding.js +63 -0
- package/dist/api/bindings/rpc/apiRpc.errors.d.ts +20 -0
- package/dist/api/bindings/rpc/apiRpc.errors.js +64 -0
- package/dist/api/bindings/rpc/index.d.ts +9 -0
- package/dist/api/bindings/rpc/index.js +8 -0
- package/dist/api/bindings/shared/apiBinding.helper.d.ts +44 -0
- package/dist/api/bindings/shared/apiBinding.helper.js +66 -0
- package/dist/api/bindings/shared/apiBinding.type.d.ts +37 -0
- package/dist/api/bindings/shared/apiBinding.type.js +2 -0
- package/dist/api/bindings/shared/apiWireResult.helper.d.ts +47 -0
- package/dist/api/bindings/shared/apiWireResult.helper.js +38 -0
- package/dist/api/bindings/shared/index.d.ts +11 -0
- package/dist/api/bindings/shared/index.js +8 -0
- package/dist/api/context/context.type.d.ts +4 -4
- package/dist/api/context/context.type.js +2 -2
- package/dist/api/context/contextKey.type.d.ts +8 -0
- package/dist/api/context/contextKey.type.js +2 -0
- package/dist/api/executor/executor.core.d.ts +20 -63
- package/dist/api/executor/executor.core.js +36 -193
- package/dist/api/executor/executor.deadline.d.ts +40 -0
- package/dist/api/executor/executor.deadline.js +93 -0
- package/dist/api/executor/executor.issues.d.ts +19 -0
- package/dist/api/executor/executor.issues.js +46 -0
- package/dist/api/executor/executor.normalize.d.ts +16 -0
- package/dist/api/executor/executor.normalize.js +45 -0
- package/dist/api/executor/executor.type.d.ts +33 -0
- package/dist/api/executor/executor.type.js +2 -0
- package/dist/api/executor/executor.validation.d.ts +31 -0
- package/dist/api/executor/executor.validation.js +58 -0
- package/dist/api/executor/index.d.ts +9 -2
- package/dist/api/executor/index.js +8 -1
- package/dist/api/handler/handler.type.d.ts +7 -4
- package/dist/api/interceptors/interceptor.type.d.ts +6 -5
- package/dist/api/operation/operation.type.d.ts +18 -1
- package/dist/api/operation/operation.type.js +5 -11
- package/dist/api/operation/operationSchema.type.d.ts +56 -0
- package/dist/api/operation/operationSchema.type.js +2 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -2
- package/package.json +16 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zudojs/api
|
|
2
2
|
|
|
3
|
-
Higher-level API layer — operation definitions, execution context, interceptors,
|
|
3
|
+
Higher-level API layer — operation definitions, execution context, interceptors, a transport-agnostic executor, and bindings that expose one operation over HTTP, RPC, queues and the CLI. `@zudojs/http` mounts it; it never imports `@zudojs/http`.
|
|
4
4
|
|
|
5
5
|
<!-- zudo-docs:start -->
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Higher-level API layer — operation definitions, execution context, interceptor
|
|
|
12
12
|
|
|
13
13
|
Import this when you need:
|
|
14
14
|
|
|
15
|
-
- define an operation once and
|
|
15
|
+
- define an operation once and serve it over HTTP (`createApiFetchHandler`), RPC (`registerApiRpcProcedures`), a queue (`bindApiQueue`), and the CLI (`runApiCli`)
|
|
16
16
|
- apply the same interceptors (auth, logging, retry) regardless of transport
|
|
17
17
|
- typed `APIContext` flowing through every handler
|
|
18
18
|
|
|
@@ -68,6 +68,8 @@ import {
|
|
|
68
68
|
} from "@zudojs/api";
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
The bindings add `createApiFetchHandler`, `describeApiRoutes`, `resolveApiRoute`, `registerApiRpcProcedures`, `createApiRpcProcedure`, `apiErrorToRPCError`, `bindApiQueue`, `createApiQueueProcessor`, `runApiCli`, `parseApiCliArgs`, `APICliExitCode`, `toApiWireResult`, `toApiWireError`, `TransportContextKey` and the types `APIOperationRoute`, `APIOperationHttpOptions`, `APIWireResult`, `APIWireError` and each binding's options — see [Bindings](#bindings-one-operation-many-transports).
|
|
72
|
+
|
|
71
73
|
Every API error class from `@zudojs/errors` (`APIError`, `APIValidationError`, `APIInternalError`, `APIOperationNotFoundError`, …) plus `createAPIError`, `isAPIError` and `ErrorCode` are re-exported for convenience.
|
|
72
74
|
|
|
73
75
|
## Usage
|
|
@@ -120,6 +122,147 @@ if (result.ok) {
|
|
|
120
122
|
|
|
121
123
|
Results are frozen `{ ok: true, data }` / `{ ok: false, error }` objects — `execute` never throws for an operation failure.
|
|
122
124
|
|
|
125
|
+
## Bindings: one operation, many transports
|
|
126
|
+
|
|
127
|
+
Every binding runs calls through an `APIExecutor` you pass as `executor` (so its interceptors apply everywhere), validates input with the operation's schema (after the interceptors), sets `TransportContextKey` (`"http" | "rpc" | "queue" | "cli"`) on the context, and reports each non-exposed failure to `onInternalError(error, requestId)`. The `state` option builds `context.state` from what the transport hands over (the `Request`, the `RPCContext`, the `Job`, the CLI invocation); throw an `APIError` there, such as `APIAuthenticationError`, to refuse the call.
|
|
128
|
+
|
|
129
|
+
Failures take one client-safe shape everywhere, `APIWireError`: `{ code, message, statusCode, requestId, issues? }`. `message` is the error's own only when it is `expose: true`, otherwise `"An internal error occurred."`. Stack traces, causes and metadata never leave the process.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { schema } from "@zudojs/schema";
|
|
133
|
+
import { APIExecutor, APIOperationRegistry, defineOperation } from "@zudojs/api";
|
|
134
|
+
|
|
135
|
+
const getUser = defineOperation({
|
|
136
|
+
name: "users.get",
|
|
137
|
+
input: schema.object({ id: schema.string() }),
|
|
138
|
+
metadata: { http: { method: "GET", path: "/users/:id" } }, // default: POST /users.get
|
|
139
|
+
handler: async (input) => db.findUser(input.id),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const registry = new APIOperationRegistry();
|
|
143
|
+
registry.register(getUser);
|
|
144
|
+
registry.freeze();
|
|
145
|
+
const executor = new APIExecutor({ interceptors: [logging] });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### HTTP: a web-standard fetch handler
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { createApiFetchHandler } from "@zudojs/api";
|
|
152
|
+
|
|
153
|
+
const handle = createApiFetchHandler(registry, {
|
|
154
|
+
executor,
|
|
155
|
+
basePath: "/api",
|
|
156
|
+
state: (request) => ({ user: verify(request.headers.get("authorization")) }),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Any Fetch API server: @zudojs/http, Bun.serve, Deno.serve, an edge runtime.
|
|
160
|
+
const response = await handle(new Request("https://app.test/api/users/u1"));
|
|
161
|
+
// 200 { "ok": true, "data": { ... } }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
On `@zudojs/http`, mount it with no glue code. `mountFetchHandler` strips the mount prefix by default, so leave `basePath` unset:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { mountFetchHandler } from "@zudojs/http";
|
|
168
|
+
|
|
169
|
+
mountFetchHandler(router, "/api", createApiFetchHandler(registry, { executor }));
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`GET` and `DELETE` routes read input from the query string (values are strings, so use coercing schemas); `POST`, `PUT` and `PATCH` read a JSON body (at most `maxBodyBytes`, 1 MiB by default). Path parameters are merged over either and win. A `__proto__`, `constructor` or `prototype` key anywhere in the query or body is refused with 400, and a body route answers 415 to any declared content type other than JSON, even with an empty body, so a cross-site HTML form cannot trigger it. Every binding (RPC, queue, CLI) refuses such keys in its input too, with a validation error. Errors use their status: 422 validation, 404 unknown route, 405 wrong method (with `Allow`), 413/415/400 for a bad body, 500 internal. The request id comes from a safe `x-request-id` header and is echoed back; the call runs under `request.signal`.
|
|
173
|
+
|
|
174
|
+
### Route contract for `@zudojs/http` and OpenAPI
|
|
175
|
+
|
|
176
|
+
`describeApiRoutes(operations, { basePath })` returns one frozen `APIOperationRoute` per operation and throws on invalid or conflicting routes:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
interface APIOperationRoute {
|
|
180
|
+
readonly operationId: string; // operation name
|
|
181
|
+
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
182
|
+
readonly path: string; // "/api/users/:id"
|
|
183
|
+
readonly pathParams: readonly string[]; // ["id"]
|
|
184
|
+
readonly inputSource: "query" | "body";
|
|
185
|
+
readonly input?: unknown; // the operation's input schema
|
|
186
|
+
readonly output?: unknown; // the operation's output schema
|
|
187
|
+
readonly description?: string;
|
|
188
|
+
readonly tags?: readonly string[];
|
|
189
|
+
readonly deprecated?: boolean;
|
|
190
|
+
readonly version?: string;
|
|
191
|
+
readonly successStatus: 200;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### OpenAPI
|
|
196
|
+
|
|
197
|
+
`toOpenAPIRouteDescriptors(operations, { basePath })` turns the same routes into `@zudojs/openapi` descriptors, so one call documents what the fetch handler serves:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { createOpenAPIDocumentFromRoutes } from "@zudojs/openapi";
|
|
201
|
+
import { toOpenAPIRouteDescriptors } from "@zudojs/api";
|
|
202
|
+
|
|
203
|
+
const document = createOpenAPIDocumentFromRoutes(
|
|
204
|
+
toOpenAPIRouteDescriptors(registry, { basePath: "/api" }),
|
|
205
|
+
{ info: { title: "Users", version: "1.0.0" } },
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Each descriptor carries `operationId`, method, path, description, tags and `deprecated`. The input schema becomes `query` (`GET` / `DELETE`) or `body`, with path-bound fields moved to `params`. `responses` documents 200 with the `{ ok: true, data }` envelope around the output schema, and 422, 404, 409, 500 and 504 with the `{ ok: false, error }` body. Only `@zudojs/schema` schemas are converted; with another schema library, input stays undocumented and `data` is `unknown`.
|
|
210
|
+
|
|
211
|
+
### RPC
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { RPCClient, RPCServer, createRPCMemoryTransport } from "@zudojs/rpc";
|
|
215
|
+
import { registerApiRpcProcedures } from "@zudojs/api";
|
|
216
|
+
|
|
217
|
+
const server = new RPCServer();
|
|
218
|
+
registerApiRpcProcedures(server, registry, {
|
|
219
|
+
executor,
|
|
220
|
+
state: (rpc) => ({ user: rpc.auth?.userId }), // transport-verified, never frame metadata
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const client = new RPCClient(createRPCMemoryTransport(server));
|
|
224
|
+
await client.call("users.get", { id: "u1" });
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Procedure names default to the operation name, which must then be a valid RPC name (`"users.get"`); pass `procedureName` to map others. API errors become their RPC equivalents (`RPC_VALIDATION_ERROR` with issues, `RPC_UNAUTHENTICATED`, `RPC_FORBIDDEN`, `RPC_RATE_LIMITED`, `RPC_TIMEOUT`, …); any other keeps its code, such as `ERR_API_CONFLICT`. Serve the same server over HTTP with `createRPCFetchHandler(server)`, for example `mountFetchHandler(router, "/rpc", createRPCFetchHandler(server))` on `@zudojs/http`.
|
|
228
|
+
|
|
229
|
+
### Queues
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { createInMemoryQueue, createQueueName } from "@zudojs/queue";
|
|
233
|
+
import { bindApiQueue } from "@zudojs/api";
|
|
234
|
+
|
|
235
|
+
const queue = createInMemoryQueue(createQueueName("operations"));
|
|
236
|
+
bindApiQueue(queue, registry, { executor });
|
|
237
|
+
|
|
238
|
+
await queue.add("users.get", { id: "u1" }, { attempts: 1 });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The job name is the operation name and `job.data` is the input. Success completes the job with the output. A failure throws an `APIError` carrying the client-safe message, so the queue retries and dead-letters it as usual. Validation failures are retried too, so enqueue unchecked input with `attempts: 1`.
|
|
242
|
+
|
|
243
|
+
### CLI
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
#!/usr/bin/env node
|
|
247
|
+
import { runApiCli } from "@zudojs/api";
|
|
248
|
+
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
process.once("SIGINT", () => controller.abort());
|
|
251
|
+
process.exitCode = await runApiCli(registry, process.argv.slice(2), {
|
|
252
|
+
executor,
|
|
253
|
+
programName: "app",
|
|
254
|
+
signal: controller.signal,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
app users.get --id u1 # prints the output as JSON, exit 0
|
|
260
|
+
app users.create --json '{"name":"Ann"}' --address.city Paris --admin
|
|
261
|
+
app --help # lists operations
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The operation name comes first (or pass `operation` for a single-purpose binary). `--field value` and `--field=value` set fields; kebab-case becomes camelCase, dots nest, repeats collect arrays, `--flag` is `true` and `--no-flag` is `false`. JSON-looking values (numbers, `true`, `false`, `null`, `{…}`, `[…]`) are parsed; anything else stays a string. On failure the `{ ok: false, error }` body goes to stderr and the exit code follows `APICliExitCode`: 64 usage, 65 invalid input, 69 unavailable, 70 internal, 75 timeout, 77 permission, 130 cancelled, 1 otherwise.
|
|
265
|
+
|
|
123
266
|
## Input validation
|
|
124
267
|
|
|
125
268
|
The executor accepts two kinds of schema for `input` and `output`, recognised structurally so the package depends on no validation library:
|
|
@@ -143,7 +286,9 @@ const charge = defineOperation({
|
|
|
143
286
|
});
|
|
144
287
|
```
|
|
145
288
|
|
|
146
|
-
The executor validates the input before the handler runs and passes the schema's _transformed_ value to the handler. Failures return an `APIValidationError` (422).
|
|
289
|
+
The executor validates the input immediately before the handler runs, after every interceptor, and passes the schema's _transformed_ value to the handler. Failures return an `APIValidationError` (422). Because the interceptors run first, an authentication interceptor refuses an anonymous call with its own error (401) before the input is inspected, so a 422 describing the schema never reaches an unauthenticated caller; logging, metrics and rate-limit interceptors see invalid calls (`context.result` is the 422); and an interceptor that replaces `context.input` cannot bypass the schema, since the replacement is what gets validated.
|
|
290
|
+
|
|
291
|
+
Without type arguments, `defineOperation` infers the handler's `input` from the `input` schema: a Standard Schema's declared output type, or the `data` of a `safeParse` schema's success result. That works inline too, as in `registry.register(defineOperation({ input: TodoInput, handler: async (input) => input.title }))` or an operation list passed to a binding. Explicit type arguments (`defineOperation<TInput, TOutput>(...)`) are still honoured as given. `InferAPISchemaOutput<typeof schema>` names the inferred type.
|
|
147
292
|
|
|
148
293
|
Schema issue messages routinely interpolate the value that failed, so by default the executor does **not** copy them into the client-facing error: each issue becomes `"<path>: invalid"` (e.g. `"user.email: invalid"`), naming where validation failed without echoing what was submitted. The list is capped at `MAX_VALIDATION_ISSUES` entries with a trailing `"… and N more issue(s) omitted."` marker.
|
|
149
294
|
|
|
@@ -161,14 +306,25 @@ When `operation.output` is set, the handler's return value is validated too, and
|
|
|
161
306
|
|
|
162
307
|
Every operation has a deadline. `timeout` must be a positive, finite integer of at most `MAX_OPERATION_TIMEOUT` milliseconds; `defineOperation` rejects `0`, negatives, `NaN` and non-integers rather than silently running unbounded. Precedence is `timeout` → `metadata.timeout` → `DEFAULT_OPERATION_TIMEOUT`, resolved by `resolveOperationTimeout`.
|
|
163
308
|
|
|
164
|
-
The handler
|
|
309
|
+
The handler always receives a `context.signal`, even when the caller supplied none. It aborts when the deadline elapses (its `reason` is the `APITimeoutError` the call fails with, 504) or when the caller's signal aborts (its `reason` is the `ErrorCode.OPERATION_CANCELLED` error the call fails with), so pass it into anything that supports it (`fetch`, a driver query, a loop check) and the work stops instead of running on and repeating side effects after the caller has given up. The signal is not aborted when the handler completes normally. Everything else on the handler's context (`requestId`, `state`, `get`/`set`, `metadata`) is the caller's context. Branch on `ErrorCode.OPERATION_CANCELLED` rather than on the (nginx-convention) 499 status.
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const exportReport = defineOperation({
|
|
313
|
+
name: "reports.export",
|
|
314
|
+
timeout: 10_000,
|
|
315
|
+
handler: async (input, context) => {
|
|
316
|
+
const rows = await fetch(reportUrl, { signal: context.signal }); // stops at the deadline
|
|
317
|
+
return rows.json();
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
```
|
|
165
321
|
|
|
166
322
|
## Interceptors
|
|
167
323
|
|
|
168
324
|
```typescript
|
|
169
325
|
const timing: APIInterceptor = {
|
|
170
326
|
async intercept(context, next) {
|
|
171
|
-
context.input = sanitize(context.input); // reaches the handler
|
|
327
|
+
context.input = sanitize(context.input); // validated, then reaches the handler
|
|
172
328
|
const started = Date.now();
|
|
173
329
|
const result = await next();
|
|
174
330
|
// context.result === result, including when a downstream interceptor
|
|
@@ -181,7 +337,7 @@ const timing: APIInterceptor = {
|
|
|
181
337
|
new APIExecutor([timing]); // or new APIExecutor({ interceptors: [timing] })
|
|
182
338
|
```
|
|
183
339
|
|
|
184
|
-
At most `MAX_INTERCEPTORS` interceptors per executor, and each `next()` may be awaited once.
|
|
340
|
+
At most `MAX_INTERCEPTORS` interceptors per executor, and each `next()` may be awaited once. Interceptors run outermost first and wrap input validation: `context.input` is the input as the caller sent it (not yet validated or transformed), and whatever an interceptor leaves there is validated before the handler sees it.
|
|
185
341
|
|
|
186
342
|
## Context
|
|
187
343
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Serializer } from "@zudojs/serialization";
|
|
2
|
+
/**
|
|
3
|
+
* Parsed command line: the operation named on it (when the CLI serves
|
|
4
|
+
* several), the input object built from flags, and whether help was asked
|
|
5
|
+
* for. A malformed command line is reported as `{ ok: false, message }`.
|
|
6
|
+
*/
|
|
7
|
+
export type APICliParseResult = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly operation?: string;
|
|
10
|
+
readonly input: unknown;
|
|
11
|
+
readonly help: boolean;
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Parses CLI arguments into operation input.
|
|
18
|
+
*
|
|
19
|
+
* - `--name value`, `--name=value`: sets `input.name`. Kebab-case becomes
|
|
20
|
+
* camelCase (`--first-name` → `firstName`); dots nest
|
|
21
|
+
* (`--address.city Paris`).
|
|
22
|
+
* - A value that is a JSON number, `true`, `false` or `null`, or starts
|
|
23
|
+
* with `{`, `[` or `"`, is parsed as JSON; anything else stays a string.
|
|
24
|
+
* - `--flag` with no value is `true`; `--no-flag` is `false`.
|
|
25
|
+
* - A repeated flag collects an array.
|
|
26
|
+
* - `--json '<object>'` supplies a base input object the flags merge over,
|
|
27
|
+
* or any JSON input when no other flags are given.
|
|
28
|
+
* - `--help` / `-h` requests help.
|
|
29
|
+
*
|
|
30
|
+
* @param expectOperation Whether the first positional argument names the
|
|
31
|
+
* operation.
|
|
32
|
+
* @param serializer JSON parser for values. Defaults to a size- and
|
|
33
|
+
* depth-limited `@zudojs/serialization` JSON serializer.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseApiCliArgs(argv: readonly string[], expectOperation: boolean, serializer?: Serializer<unknown, string>): APICliParseResult;
|
|
36
|
+
//# sourceMappingURL=apiCli.argv.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createSerializer } from "@zudojs/serialization";
|
|
2
|
+
import { isPlainObject } from "@zudojs/types";
|
|
3
|
+
import { assignOption, camelCase, coerceOptionValue, parseJson } from "./apiCli.values.js";
|
|
4
|
+
const DEFAULT_SERIALIZER = createSerializer("json", { maxSize: 1024 * 1024, maxDepth: 64 });
|
|
5
|
+
const KEY_SEGMENT = /^[A-Za-z_$][\w$-]*$/;
|
|
6
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
7
|
+
/**
|
|
8
|
+
* Parses CLI arguments into operation input.
|
|
9
|
+
*
|
|
10
|
+
* - `--name value`, `--name=value`: sets `input.name`. Kebab-case becomes
|
|
11
|
+
* camelCase (`--first-name` → `firstName`); dots nest
|
|
12
|
+
* (`--address.city Paris`).
|
|
13
|
+
* - A value that is a JSON number, `true`, `false` or `null`, or starts
|
|
14
|
+
* with `{`, `[` or `"`, is parsed as JSON; anything else stays a string.
|
|
15
|
+
* - `--flag` with no value is `true`; `--no-flag` is `false`.
|
|
16
|
+
* - A repeated flag collects an array.
|
|
17
|
+
* - `--json '<object>'` supplies a base input object the flags merge over,
|
|
18
|
+
* or any JSON input when no other flags are given.
|
|
19
|
+
* - `--help` / `-h` requests help.
|
|
20
|
+
*
|
|
21
|
+
* @param expectOperation Whether the first positional argument names the
|
|
22
|
+
* operation.
|
|
23
|
+
* @param serializer JSON parser for values. Defaults to a size- and
|
|
24
|
+
* depth-limited `@zudojs/serialization` JSON serializer.
|
|
25
|
+
*/
|
|
26
|
+
export function parseApiCliArgs(argv, expectOperation, serializer = DEFAULT_SERIALIZER) {
|
|
27
|
+
const fields = {};
|
|
28
|
+
let base;
|
|
29
|
+
let operation;
|
|
30
|
+
let help = false;
|
|
31
|
+
let flagCount = 0;
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
const token = argv[i];
|
|
34
|
+
if (token === "--help" || token === "-h") {
|
|
35
|
+
help = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!token.startsWith("--")) {
|
|
39
|
+
if (expectOperation && operation === undefined) {
|
|
40
|
+
operation = token;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
return { ok: false, message: `Unexpected argument "${token}".` };
|
|
44
|
+
}
|
|
45
|
+
const eq = token.indexOf("=");
|
|
46
|
+
const rawKey = eq === -1 ? token.slice(2) : token.slice(2, eq);
|
|
47
|
+
const next = argv[i + 1];
|
|
48
|
+
let raw = eq === -1 ? undefined : token.slice(eq + 1);
|
|
49
|
+
if (raw === undefined && next !== undefined && !next.startsWith("--")) {
|
|
50
|
+
raw = next;
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
if (rawKey === "json") {
|
|
54
|
+
const parsed = parseJson(raw, serializer);
|
|
55
|
+
if (!parsed.ok) {
|
|
56
|
+
return { ok: false, message: "--json must be followed by valid JSON." };
|
|
57
|
+
}
|
|
58
|
+
base = parsed.value;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const negated = raw === undefined && rawKey.startsWith("no-");
|
|
62
|
+
const path = (negated ? rawKey.slice(3) : rawKey).split(".").map(camelCase);
|
|
63
|
+
if (path.some((segment) => !KEY_SEGMENT.test(segment) || UNSAFE_KEYS.has(segment))) {
|
|
64
|
+
return { ok: false, message: `Invalid option "--${rawKey}".` };
|
|
65
|
+
}
|
|
66
|
+
const value = raw === undefined ? !negated : coerceOptionValue(raw, serializer);
|
|
67
|
+
if (!assignOption(fields, path, value)) {
|
|
68
|
+
return { ok: false, message: `Option "--${rawKey}" conflicts with another option.` };
|
|
69
|
+
}
|
|
70
|
+
flagCount += 1;
|
|
71
|
+
}
|
|
72
|
+
if (flagCount > 0 && base !== undefined && !isPlainObject(base)) {
|
|
73
|
+
return { ok: false, message: "--json must be an object when combined with other options." };
|
|
74
|
+
}
|
|
75
|
+
const input = flagCount === 0 ? (base ?? {}) : { ...(isPlainObject(base) ? base : {}), ...fields };
|
|
76
|
+
return { ok: true, ...(operation !== undefined ? { operation } : {}), input, help };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=apiCli.argv.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { APIBindingOptions, APIOperationSource } from "../shared/apiBinding.type.js";
|
|
2
|
+
import type { APICliExitCodeValue } from "./apiCli.exitCode.js";
|
|
3
|
+
/**
|
|
4
|
+
* One CLI call as the `state` hook sees it.
|
|
5
|
+
*/
|
|
6
|
+
export interface APICliInvocation {
|
|
7
|
+
readonly argv: readonly string[];
|
|
8
|
+
readonly input: unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Where the CLI writes. Defaults to `process.stdout` / `process.stderr`.
|
|
12
|
+
*/
|
|
13
|
+
export interface APICliIO {
|
|
14
|
+
readonly stdout: (text: string) => void;
|
|
15
|
+
readonly stderr: (text: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Options for {@link runApiCli}. `state` receives the invocation.
|
|
19
|
+
*/
|
|
20
|
+
export interface APICliOptions extends APIBindingOptions<APICliInvocation> {
|
|
21
|
+
readonly io?: APICliIO;
|
|
22
|
+
/** Cancels the running operation (wire it to SIGINT). */
|
|
23
|
+
readonly signal?: AbortSignal;
|
|
24
|
+
/**
|
|
25
|
+
* Run this operation instead of reading its name from `argv[0]` — for a
|
|
26
|
+
* single-purpose binary.
|
|
27
|
+
*/
|
|
28
|
+
readonly operation?: string;
|
|
29
|
+
/** Program name shown in help. Defaults to `"cli"`. */
|
|
30
|
+
readonly programName?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Runs an operation from command-line arguments and returns the exit code.
|
|
34
|
+
*
|
|
35
|
+
* `argv` is `process.argv.slice(2)`: the operation name first (unless
|
|
36
|
+
* `options.operation` fixes it), then `--field value` options and/or
|
|
37
|
+
* `--json '{...}'` (see `parseApiCliArgs`). On success the output is
|
|
38
|
+
* printed to stdout as JSON and the exit code is 0. On failure the same
|
|
39
|
+
* client-safe `{ ok: false, error }` body the HTTP binding sends is
|
|
40
|
+
* printed to stderr, and the exit code reflects the failure (see
|
|
41
|
+
* {@link APICliExitCode}). Never calls `process.exit`, and never throws
|
|
42
|
+
* for a failed call — only for an invalid operation list.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runApiCli(operations: APIOperationSource, argv: readonly string[], options?: APICliOptions): Promise<APICliExitCodeValue>;
|
|
45
|
+
//# sourceMappingURL=apiCli.binding.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createSerializer } from "@zudojs/serialization";
|
|
2
|
+
import { createOperationRunner, listOperations } from "../shared/apiBinding.helper.js";
|
|
3
|
+
import { toApiWireResult } from "../shared/apiWireResult.helper.js";
|
|
4
|
+
import { normalizeAPIError } from "../../executor/executor.core.js";
|
|
5
|
+
import { parseApiCliArgs } from "./apiCli.argv.js";
|
|
6
|
+
import { APICliExitCode, apiCliExitCodeForStatus } from "./apiCli.exitCode.js";
|
|
7
|
+
const PRETTY = createSerializer("json", { pretty: true });
|
|
8
|
+
/**
|
|
9
|
+
* Runs an operation from command-line arguments and returns the exit code.
|
|
10
|
+
*
|
|
11
|
+
* `argv` is `process.argv.slice(2)`: the operation name first (unless
|
|
12
|
+
* `options.operation` fixes it), then `--field value` options and/or
|
|
13
|
+
* `--json '{...}'` (see `parseApiCliArgs`). On success the output is
|
|
14
|
+
* printed to stdout as JSON and the exit code is 0. On failure the same
|
|
15
|
+
* client-safe `{ ok: false, error }` body the HTTP binding sends is
|
|
16
|
+
* printed to stderr, and the exit code reflects the failure (see
|
|
17
|
+
* {@link APICliExitCode}). Never calls `process.exit`, and never throws
|
|
18
|
+
* for a failed call — only for an invalid operation list.
|
|
19
|
+
*/
|
|
20
|
+
export async function runApiCli(operations, argv, options = {}) {
|
|
21
|
+
const io = options.io ?? defaultIO();
|
|
22
|
+
const known = listOperations(operations);
|
|
23
|
+
const fixed = options.operation;
|
|
24
|
+
const parsed = parseApiCliArgs(argv, fixed === undefined);
|
|
25
|
+
if (!parsed.ok) {
|
|
26
|
+
io.stderr(`${parsed.message}\n`);
|
|
27
|
+
return APICliExitCode.USAGE;
|
|
28
|
+
}
|
|
29
|
+
const name = fixed ?? parsed.operation;
|
|
30
|
+
if (parsed.help || name === undefined) {
|
|
31
|
+
(parsed.help ? io.stdout : io.stderr)(usage(known, options.programName ?? "cli", fixed));
|
|
32
|
+
return parsed.help ? APICliExitCode.OK : APICliExitCode.USAGE;
|
|
33
|
+
}
|
|
34
|
+
const operation = known.find((candidate) => candidate.name === name);
|
|
35
|
+
if (operation === undefined) {
|
|
36
|
+
io.stderr(`Unknown operation "${name}". Run with --help to list operations.\n`);
|
|
37
|
+
return APICliExitCode.USAGE;
|
|
38
|
+
}
|
|
39
|
+
const run = createOperationRunner(options);
|
|
40
|
+
const { result, requestId } = await run({
|
|
41
|
+
operation,
|
|
42
|
+
input: parsed.input,
|
|
43
|
+
source: { argv, input: parsed.input },
|
|
44
|
+
transport: "cli",
|
|
45
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
46
|
+
});
|
|
47
|
+
const wire = toApiWireResult(result, requestId);
|
|
48
|
+
if (!wire.ok) {
|
|
49
|
+
io.stderr(`${PRETTY.serialize(wire)}\n`);
|
|
50
|
+
return apiCliExitCodeForStatus(wire.error.statusCode);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
if (wire.data !== undefined) {
|
|
54
|
+
io.stdout(`${PRETTY.serialize(wire.data)}\n`);
|
|
55
|
+
}
|
|
56
|
+
return APICliExitCode.OK;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const failure = normalizeAPIError(error, operation.name);
|
|
60
|
+
options.onInternalError?.(failure, requestId);
|
|
61
|
+
io.stderr(`${PRETTY.serialize(toApiWireResult({ ok: false, error: failure }, requestId))}\n`);
|
|
62
|
+
return APICliExitCode.INTERNAL;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function usage(operations, program, fixed) {
|
|
66
|
+
const shown = fixed === undefined ? operations : operations.filter((op) => op.name === fixed);
|
|
67
|
+
const lines = shown.map((op) => ` ${op.name}${op.metadata?.description !== undefined ? ` ${op.metadata.description}` : ""}`);
|
|
68
|
+
const call = fixed === undefined ? `${program} <operation>` : program;
|
|
69
|
+
return `Usage: ${call} [--field value ...] [--json '<input>']\n\nOperations:\n${lines.join("\n")}\n`;
|
|
70
|
+
}
|
|
71
|
+
function defaultIO() {
|
|
72
|
+
return {
|
|
73
|
+
stdout: (text) => void process.stdout.write(text),
|
|
74
|
+
stderr: (text) => void process.stderr.write(text),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=apiCli.binding.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit codes returned by `runApiCli`, following BSD `sysexits.h` where a
|
|
3
|
+
* code fits so shell scripts can branch on the kind of failure.
|
|
4
|
+
*/
|
|
5
|
+
export declare const APICliExitCode: Readonly<{
|
|
6
|
+
/** The operation succeeded. */
|
|
7
|
+
OK: 0;
|
|
8
|
+
/** The operation failed with a client error that has no closer code. */
|
|
9
|
+
FAILURE: 1;
|
|
10
|
+
/** Bad command line: unknown operation, malformed option (EX_USAGE). */
|
|
11
|
+
USAGE: 64;
|
|
12
|
+
/** Input failed validation (EX_DATAERR). */
|
|
13
|
+
INVALID_INPUT: 65;
|
|
14
|
+
/** Rate limited or unavailable; try again later (EX_UNAVAILABLE). */
|
|
15
|
+
UNAVAILABLE: 69;
|
|
16
|
+
/** Internal error (EX_SOFTWARE). */
|
|
17
|
+
INTERNAL: 70;
|
|
18
|
+
/** Timed out (EX_TEMPFAIL). */
|
|
19
|
+
TIMEOUT: 75;
|
|
20
|
+
/** Not authenticated or not permitted (EX_NOPERM). */
|
|
21
|
+
PERMISSION: 77;
|
|
22
|
+
/** Cancelled through the abort signal (128 + SIGINT). */
|
|
23
|
+
CANCELLED: 130;
|
|
24
|
+
}>;
|
|
25
|
+
/** One of the {@link APICliExitCode} values. */
|
|
26
|
+
export type APICliExitCodeValue = (typeof APICliExitCode)[keyof typeof APICliExitCode];
|
|
27
|
+
/**
|
|
28
|
+
* Maps a failed result's HTTP-style status to an exit code.
|
|
29
|
+
*/
|
|
30
|
+
export declare function apiCliExitCodeForStatus(status: number): APICliExitCodeValue;
|
|
31
|
+
//# sourceMappingURL=apiCli.exitCode.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit codes returned by `runApiCli`, following BSD `sysexits.h` where a
|
|
3
|
+
* code fits so shell scripts can branch on the kind of failure.
|
|
4
|
+
*/
|
|
5
|
+
export const APICliExitCode = Object.freeze({
|
|
6
|
+
/** The operation succeeded. */
|
|
7
|
+
OK: 0,
|
|
8
|
+
/** The operation failed with a client error that has no closer code. */
|
|
9
|
+
FAILURE: 1,
|
|
10
|
+
/** Bad command line: unknown operation, malformed option (EX_USAGE). */
|
|
11
|
+
USAGE: 64,
|
|
12
|
+
/** Input failed validation (EX_DATAERR). */
|
|
13
|
+
INVALID_INPUT: 65,
|
|
14
|
+
/** Rate limited or unavailable; try again later (EX_UNAVAILABLE). */
|
|
15
|
+
UNAVAILABLE: 69,
|
|
16
|
+
/** Internal error (EX_SOFTWARE). */
|
|
17
|
+
INTERNAL: 70,
|
|
18
|
+
/** Timed out (EX_TEMPFAIL). */
|
|
19
|
+
TIMEOUT: 75,
|
|
20
|
+
/** Not authenticated or not permitted (EX_NOPERM). */
|
|
21
|
+
PERMISSION: 77,
|
|
22
|
+
/** Cancelled through the abort signal (128 + SIGINT). */
|
|
23
|
+
CANCELLED: 130,
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Maps a failed result's HTTP-style status to an exit code.
|
|
27
|
+
*/
|
|
28
|
+
export function apiCliExitCodeForStatus(status) {
|
|
29
|
+
if (status === 400 || status === 422)
|
|
30
|
+
return APICliExitCode.INVALID_INPUT;
|
|
31
|
+
if (status === 401 || status === 403)
|
|
32
|
+
return APICliExitCode.PERMISSION;
|
|
33
|
+
if (status === 408 || status === 504)
|
|
34
|
+
return APICliExitCode.TIMEOUT;
|
|
35
|
+
if (status === 429 || status === 503)
|
|
36
|
+
return APICliExitCode.UNAVAILABLE;
|
|
37
|
+
if (status === 499)
|
|
38
|
+
return APICliExitCode.CANCELLED;
|
|
39
|
+
if (status >= 500)
|
|
40
|
+
return APICliExitCode.INTERNAL;
|
|
41
|
+
return APICliExitCode.FAILURE;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=apiCli.exitCode.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Serializer } from "@zudojs/serialization";
|
|
2
|
+
/** Converts a kebab-case option segment to camelCase. */
|
|
3
|
+
export declare function camelCase(segment: string): string;
|
|
4
|
+
/** Parses JSON text, reporting failure instead of throwing. */
|
|
5
|
+
export declare function parseJson(raw: string | undefined, serializer: Serializer<unknown, string>): {
|
|
6
|
+
readonly ok: true;
|
|
7
|
+
readonly value: unknown;
|
|
8
|
+
} | {
|
|
9
|
+
readonly ok: false;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Parses an option value that looks like JSON (a number, `true`, `false`,
|
|
13
|
+
* `null`, or text starting with `{`, `[` or `"`); keeps anything else, and
|
|
14
|
+
* any number JSON would round, as the string typed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function coerceOptionValue(raw: string, serializer: Serializer<unknown, string>): unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Sets `value` at a dotted `path`, collecting repeats into an array.
|
|
19
|
+
* Returns `false` when the path collides with an existing scalar or
|
|
20
|
+
* object. Only own properties count, so `--to-string` cannot collide
|
|
21
|
+
* with `Object.prototype`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assignOption(target: Record<string, unknown>, path: readonly string[], value: unknown): boolean;
|
|
24
|
+
//# sourceMappingURL=apiCli.values.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isPlainObject } from "@zudojs/types";
|
|
2
|
+
const JSON_LITERAL = /^(?:-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)$/;
|
|
3
|
+
/** Converts a kebab-case option segment to camelCase. */
|
|
4
|
+
export function camelCase(segment) {
|
|
5
|
+
return segment.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
6
|
+
}
|
|
7
|
+
/** Parses JSON text, reporting failure instead of throwing. */
|
|
8
|
+
export function parseJson(raw, serializer) {
|
|
9
|
+
if (raw === undefined) {
|
|
10
|
+
return { ok: false };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return { ok: true, value: serializer.deserialize(raw) };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ok: false };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parses an option value that looks like JSON (a number, `true`, `false`,
|
|
21
|
+
* `null`, or text starting with `{`, `[` or `"`); keeps anything else, and
|
|
22
|
+
* any number JSON would round, as the string typed.
|
|
23
|
+
*/
|
|
24
|
+
export function coerceOptionValue(raw, serializer) {
|
|
25
|
+
const looksJson = JSON_LITERAL.test(raw) || /^[[{"]/.test(raw);
|
|
26
|
+
if (!looksJson) {
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
const parsed = parseJson(raw, serializer);
|
|
30
|
+
return parsed.ok && !isLossyNumber(parsed.value) ? parsed.value : raw;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A number JSON cannot hand back faithfully — an integer beyond 2^53 or
|
|
34
|
+
* an overflow to Infinity — is kept as the string the user typed.
|
|
35
|
+
*/
|
|
36
|
+
function isLossyNumber(value) {
|
|
37
|
+
return (typeof value === "number" &&
|
|
38
|
+
(!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sets `value` at a dotted `path`, collecting repeats into an array.
|
|
42
|
+
* Returns `false` when the path collides with an existing scalar or
|
|
43
|
+
* object. Only own properties count, so `--to-string` cannot collide
|
|
44
|
+
* with `Object.prototype`.
|
|
45
|
+
*/
|
|
46
|
+
export function assignOption(target, path, value) {
|
|
47
|
+
let node = target;
|
|
48
|
+
for (const segment of path.slice(0, -1)) {
|
|
49
|
+
const child = Object.hasOwn(node, segment) ? node[segment] : undefined;
|
|
50
|
+
if (child === undefined) {
|
|
51
|
+
node[segment] = {};
|
|
52
|
+
}
|
|
53
|
+
else if (!isPlainObject(child)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
node = node[segment];
|
|
57
|
+
}
|
|
58
|
+
const leaf = path[path.length - 1];
|
|
59
|
+
const existing = Object.hasOwn(node, leaf) ? node[leaf] : undefined;
|
|
60
|
+
if (existing === undefined) {
|
|
61
|
+
node[leaf] = value;
|
|
62
|
+
}
|
|
63
|
+
else if (isPlainObject(existing)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
node[leaf] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=apiCli.values.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI binding: parses command-line arguments into operation input, runs
|
|
3
|
+
* the operation, prints the result as JSON, and returns an exit code. No
|
|
4
|
+
* dependency on any CLI framework.
|
|
5
|
+
*/
|
|
6
|
+
export type { APICliInvocation, APICliIO, APICliOptions } from "./apiCli.binding.js";
|
|
7
|
+
export { runApiCli } from "./apiCli.binding.js";
|
|
8
|
+
export type { APICliParseResult } from "./apiCli.argv.js";
|
|
9
|
+
export { parseApiCliArgs } from "./apiCli.argv.js";
|
|
10
|
+
export type { APICliExitCodeValue } from "./apiCli.exitCode.js";
|
|
11
|
+
export { APICliExitCode, apiCliExitCodeForStatus } from "./apiCli.exitCode.js";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI binding: parses command-line arguments into operation input, runs
|
|
3
|
+
* the operation, prints the result as JSON, and returns an exit code. No
|
|
4
|
+
* dependency on any CLI framework.
|
|
5
|
+
*/
|
|
6
|
+
export { runApiCli } from "./apiCli.binding.js";
|
|
7
|
+
export { parseApiCliArgs } from "./apiCli.argv.js";
|
|
8
|
+
export { APICliExitCode, apiCliExitCodeForStatus } from "./apiCli.exitCode.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|