specli 0.0.2 → 0.0.3
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 +5 -49
- package/package.json +1 -1
- package/src/cli/runtime/execute.ts +34 -79
- package/src/cli/runtime/generated.ts +6 -40
- package/src/cli/runtime/request.ts +7 -85
package/README.md
CHANGED
|
@@ -201,50 +201,6 @@ Implementation notes:
|
|
|
201
201
|
|
|
202
202
|
## Request Bodies
|
|
203
203
|
|
|
204
|
-
### Selecting the Body Input
|
|
205
|
-
|
|
206
|
-
If an operation has a `requestBody`, you may provide a body via:
|
|
207
|
-
|
|
208
|
-
- `--data <string>`
|
|
209
|
-
- `--file <path>`
|
|
210
|
-
- Body field flags (JSON-only; see below)
|
|
211
|
-
|
|
212
|
-
Rules:
|
|
213
|
-
|
|
214
|
-
- `--data` and `--file` are mutually exclusive.
|
|
215
|
-
- Body field flags cannot be used with `--data` or `--file`.
|
|
216
|
-
- If `requestBody.required` is true and you provide none of the above, the command fails with:
|
|
217
|
-
- `Missing request body. Provide --data, --file, or body field flags.`
|
|
218
|
-
|
|
219
|
-
### Content-Type
|
|
220
|
-
|
|
221
|
-
`Content-Type` is chosen as:
|
|
222
|
-
|
|
223
|
-
1. `--content-type` (explicit override)
|
|
224
|
-
2. The preferred content type derived from the OpenAPI requestBody (prefers `application/json` when present)
|
|
225
|
-
|
|
226
|
-
### JSON Parsing + Normalization
|
|
227
|
-
|
|
228
|
-
If the selected `Content-Type` includes `json`:
|
|
229
|
-
|
|
230
|
-
- `--data`/`--file` content is parsed as either JSON or YAML
|
|
231
|
-
- the request is sent as normalized JSON (`JSON.stringify(parsed)`)
|
|
232
|
-
|
|
233
|
-
If `Content-Type` does not include `json`:
|
|
234
|
-
|
|
235
|
-
- the body is treated as a raw string
|
|
236
|
-
|
|
237
|
-
### Schema Validation (Ajv)
|
|
238
|
-
|
|
239
|
-
specli uses Ajv (best-effort, `strict: false`) to validate:
|
|
240
|
-
|
|
241
|
-
- query/header/cookie params
|
|
242
|
-
- JSON request bodies when a requestBody schema is available
|
|
243
|
-
|
|
244
|
-
Validation errors are formatted into a readable multiline message. For `required` errors, the message is normalized to:
|
|
245
|
-
|
|
246
|
-
- `/<path> missing required property '<name>'`
|
|
247
|
-
|
|
248
204
|
### Body Field Flags
|
|
249
205
|
|
|
250
206
|
When an operation has a `requestBody` and the preferred schema is a JSON object, specli generates convenience flags that match the property names:
|
|
@@ -299,11 +255,11 @@ Which produces:
|
|
|
299
255
|
|
|
300
256
|
Notes / edge cases:
|
|
301
257
|
|
|
302
|
-
- Body field flags are only supported for JSON bodies.
|
|
303
|
-
- Required fields in the schema are checked
|
|
304
|
-
- `Missing required body field 'name'. Provide --name
|
|
305
|
-
- If a body field flag
|
|
306
|
-
- Numeric coercion uses `Number(...)` / `parseInt(...)`.
|
|
258
|
+
- Body field flags are only supported for JSON bodies.
|
|
259
|
+
- Required fields in the schema are checked with friendly error messages:
|
|
260
|
+
- `Missing required body field 'name'. Provide --name.`
|
|
261
|
+
- If a body field flag would conflict with an operation parameter flag or `--curl`, the operation parameter takes precedence.
|
|
262
|
+
- Numeric coercion uses `Number(...)` / `parseInt(...)`.
|
|
307
263
|
|
|
308
264
|
## Servers
|
|
309
265
|
|
package/package.json
CHANGED
|
@@ -34,92 +34,47 @@ export async function executeAction(input: ExecuteInput): Promise<void> {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
process.stdout.write(`${k}: ${v}\n`);
|
|
41
|
-
}
|
|
42
|
-
if (request.body) {
|
|
43
|
-
const text = await request.clone().text();
|
|
44
|
-
if (text) process.stdout.write(`\n${text}\n`);
|
|
45
|
-
}
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const timeoutMs = input.globals.timeout
|
|
50
|
-
? Number(input.globals.timeout)
|
|
51
|
-
: undefined;
|
|
52
|
-
let timeout: Timer | undefined;
|
|
53
|
-
let controller: AbortController | undefined;
|
|
54
|
-
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
55
|
-
controller = new AbortController();
|
|
56
|
-
timeout = setTimeout(() => controller?.abort(), timeoutMs);
|
|
57
|
-
}
|
|
37
|
+
const res = await fetch(request);
|
|
38
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
39
|
+
const status = res.status;
|
|
58
40
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const status = res.status;
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
let body: unknown = text;
|
|
43
|
+
let parsedJson: unknown | undefined;
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
parsedJson = text ? JSON.parse(text) : null;
|
|
71
|
-
body = parsedJson;
|
|
72
|
-
} catch {
|
|
73
|
-
// keep as text
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!res.ok) {
|
|
78
|
-
if (input.globals.json) {
|
|
79
|
-
process.stdout.write(
|
|
80
|
-
`${JSON.stringify({
|
|
81
|
-
status,
|
|
82
|
-
body,
|
|
83
|
-
headers: input.globals.headers
|
|
84
|
-
? Object.fromEntries(res.headers.entries())
|
|
85
|
-
: undefined,
|
|
86
|
-
})}\n`,
|
|
87
|
-
);
|
|
88
|
-
} else {
|
|
89
|
-
process.stderr.write(`HTTP ${status}\n`);
|
|
90
|
-
process.stderr.write(
|
|
91
|
-
`${typeof body === "string" ? body : JSON.stringify(body, null, 2)}\n`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
process.exitCode = 1;
|
|
95
|
-
return;
|
|
45
|
+
if (contentType.includes("json")) {
|
|
46
|
+
try {
|
|
47
|
+
parsedJson = text ? JSON.parse(text) : null;
|
|
48
|
+
body = parsedJson;
|
|
49
|
+
} catch {
|
|
50
|
+
// keep as text
|
|
96
51
|
}
|
|
52
|
+
}
|
|
97
53
|
|
|
54
|
+
if (!res.ok) {
|
|
98
55
|
if (input.globals.json) {
|
|
99
|
-
|
|
100
|
-
input.globals.status || input.globals.headers
|
|
101
|
-
? {
|
|
102
|
-
status: input.globals.status ? status : undefined,
|
|
103
|
-
headers: input.globals.headers
|
|
104
|
-
? Object.fromEntries(res.headers.entries())
|
|
105
|
-
: undefined,
|
|
106
|
-
body,
|
|
107
|
-
}
|
|
108
|
-
: body;
|
|
109
|
-
|
|
110
|
-
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// default (human + agent readable)
|
|
115
|
-
if (typeof parsedJson !== "undefined") {
|
|
116
|
-
process.stdout.write(`${JSON.stringify(parsedJson, null, 2)}\n`);
|
|
56
|
+
process.stdout.write(`${JSON.stringify({ status, body })}\n`);
|
|
117
57
|
} else {
|
|
118
|
-
process.
|
|
119
|
-
|
|
58
|
+
process.stderr.write(`HTTP ${status}\n`);
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`${typeof body === "string" ? body : JSON.stringify(body, null, 2)}\n`,
|
|
61
|
+
);
|
|
120
62
|
}
|
|
121
|
-
|
|
122
|
-
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (input.globals.json) {
|
|
68
|
+
process.stdout.write(`${JSON.stringify(body)}\n`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// default (human + agent readable)
|
|
73
|
+
if (typeof parsedJson !== "undefined") {
|
|
74
|
+
process.stdout.write(`${JSON.stringify(parsedJson, null, 2)}\n`);
|
|
75
|
+
} else {
|
|
76
|
+
process.stdout.write(text);
|
|
77
|
+
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
123
78
|
}
|
|
124
79
|
} catch (err) {
|
|
125
80
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -5,7 +5,6 @@ import type { CommandModel } from "../command-model.ts";
|
|
|
5
5
|
import type { ServerInfo } from "../server.ts";
|
|
6
6
|
|
|
7
7
|
import { type BodyFlagDef, generateBodyFlags } from "./body-flags.ts";
|
|
8
|
-
import { collectRepeatable } from "./collect.ts";
|
|
9
8
|
import { executeAction } from "./execute.ts";
|
|
10
9
|
import type { EmbeddedDefaults } from "./request.ts";
|
|
11
10
|
import { coerceArrayInput, coerceValue } from "./validate/index.ts";
|
|
@@ -78,54 +77,21 @@ export function addGeneratedCommands(
|
|
|
78
77
|
else cmd.option(key, desc, parser);
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
// Collect reserved flags: operation params + --curl
|
|
81
|
+
const operationFlags = new Set(action.flags.map((f) => f.flag));
|
|
82
|
+
const reservedFlags = new Set([...operationFlags, "--curl"]);
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
if (!reservedFlags.has("--header")) {
|
|
86
|
-
cmd.option(
|
|
87
|
-
"--header <header>",
|
|
88
|
-
"Extra header (repeatable)",
|
|
89
|
-
collectRepeatable,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
if (!reservedFlags.has("--accept")) {
|
|
93
|
-
cmd.option("--accept <type>", "Override Accept header");
|
|
94
|
-
}
|
|
95
|
-
if (!reservedFlags.has("--status")) {
|
|
96
|
-
cmd.option("--status", "Include status in --json output");
|
|
97
|
-
}
|
|
98
|
-
if (!reservedFlags.has("--headers")) {
|
|
99
|
-
cmd.option("--headers", "Include headers in --json output");
|
|
100
|
-
}
|
|
101
|
-
if (!reservedFlags.has("--dry-run")) {
|
|
102
|
-
cmd.option("--dry-run", "Print request without sending");
|
|
103
|
-
}
|
|
104
|
-
if (!reservedFlags.has("--curl")) {
|
|
84
|
+
// Only --curl is a built-in flag (for debugging)
|
|
85
|
+
if (!operationFlags.has("--curl")) {
|
|
105
86
|
cmd.option("--curl", "Print curl command without sending");
|
|
106
87
|
}
|
|
107
|
-
if (!reservedFlags.has("--timeout")) {
|
|
108
|
-
cmd.option("--timeout <ms>", "Request timeout in milliseconds");
|
|
109
|
-
}
|
|
110
88
|
|
|
111
89
|
// Track body flag definitions for this action
|
|
112
90
|
let bodyFlagDefs: BodyFlagDef[] = [];
|
|
113
91
|
|
|
114
92
|
if (action.requestBody) {
|
|
115
|
-
if (!reservedFlags.has("--data")) {
|
|
116
|
-
cmd.option("--data <data>", "Inline request body");
|
|
117
|
-
}
|
|
118
|
-
if (!reservedFlags.has("--file")) {
|
|
119
|
-
cmd.option("--file <path>", "Request body from file");
|
|
120
|
-
}
|
|
121
|
-
if (!reservedFlags.has("--content-type")) {
|
|
122
|
-
cmd.option(
|
|
123
|
-
"--content-type <type>",
|
|
124
|
-
"Override Content-Type (defaults from OpenAPI)",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
93
|
// Generate body flags from schema (recursive with dot notation)
|
|
94
|
+
// Pass reserved flags to avoid conflicts with operation params and --curl
|
|
129
95
|
bodyFlagDefs = generateBodyFlags(
|
|
130
96
|
action.requestBodySchema,
|
|
131
97
|
reservedFlags,
|
|
@@ -2,8 +2,6 @@ import type { AuthScheme } from "../auth-schemes.ts";
|
|
|
2
2
|
import type { CommandAction } from "../command-model.ts";
|
|
3
3
|
|
|
4
4
|
import { resolveAuthScheme } from "./auth/resolve.ts";
|
|
5
|
-
import { loadBody, parseBodyAsJsonOrYaml } from "./body.ts";
|
|
6
|
-
import { parseHeaderInput } from "./headers.ts";
|
|
7
5
|
import { getToken } from "./profile/secrets.ts";
|
|
8
6
|
import { getProfile, readProfiles } from "./profile/store.ts";
|
|
9
7
|
import { resolveServerUrl } from "./server-url.ts";
|
|
@@ -19,18 +17,7 @@ export type RuntimeGlobals = {
|
|
|
19
17
|
server?: string;
|
|
20
18
|
serverVar?: string[];
|
|
21
19
|
|
|
22
|
-
// Common runtime flags.
|
|
23
|
-
header?: string[];
|
|
24
|
-
accept?: string;
|
|
25
|
-
contentType?: string;
|
|
26
|
-
data?: string;
|
|
27
|
-
file?: string;
|
|
28
|
-
timeout?: string;
|
|
29
|
-
dryRun?: boolean;
|
|
30
20
|
curl?: boolean;
|
|
31
|
-
status?: boolean;
|
|
32
|
-
headers?: boolean;
|
|
33
|
-
|
|
34
21
|
json?: boolean;
|
|
35
22
|
|
|
36
23
|
auth?: string;
|
|
@@ -207,8 +194,6 @@ export async function buildRequest(
|
|
|
207
194
|
);
|
|
208
195
|
|
|
209
196
|
const headers = new Headers();
|
|
210
|
-
const accept = input.globals.accept;
|
|
211
|
-
if (accept) headers.set("Accept", accept);
|
|
212
197
|
|
|
213
198
|
// Collect declared params for validation.
|
|
214
199
|
const queryValues: Record<string, unknown> = {};
|
|
@@ -273,51 +258,29 @@ export async function buildRequest(
|
|
|
273
258
|
headers.set("Cookie", existing ? `${existing}; ${part}` : part);
|
|
274
259
|
}
|
|
275
260
|
|
|
276
|
-
const extraHeaders = (input.globals.header ?? []).map(parseHeaderInput);
|
|
277
|
-
for (const { name, value } of extraHeaders) {
|
|
278
|
-
headers.set(name, value);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
261
|
let body: string | undefined;
|
|
282
262
|
if (input.action.requestBody) {
|
|
283
|
-
const data = input.globals.data;
|
|
284
|
-
const file = input.globals.file;
|
|
285
|
-
|
|
286
|
-
const hasData = typeof data === "string";
|
|
287
|
-
const hasFile = typeof file === "string";
|
|
288
|
-
|
|
289
263
|
// Check if any body flags were provided using the flag definitions
|
|
290
264
|
const bodyFlagDefs = input.bodyFlagDefs ?? [];
|
|
291
|
-
const
|
|
265
|
+
const hasBodyFlags = bodyFlagDefs.some((def) => {
|
|
292
266
|
// Commander keeps dots in option names: --address.street -> "address.street"
|
|
293
267
|
const dotKey = def.path.join(".");
|
|
294
268
|
return input.flagValues[dotKey] !== undefined;
|
|
295
269
|
});
|
|
296
270
|
|
|
297
|
-
|
|
298
|
-
if (hasExpandedBody && (hasData || hasFile)) {
|
|
299
|
-
throw new Error(
|
|
300
|
-
"Use either --data/--file or body field flags (not both)",
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const contentType =
|
|
305
|
-
input.globals.contentType ??
|
|
306
|
-
input.action.requestBody.preferredContentType;
|
|
271
|
+
const contentType = input.action.requestBody.preferredContentType;
|
|
307
272
|
if (contentType) headers.set("Content-Type", contentType);
|
|
308
273
|
|
|
309
274
|
const schema = input.action.requestBodySchema;
|
|
310
275
|
|
|
311
|
-
if (!
|
|
276
|
+
if (!hasBodyFlags) {
|
|
312
277
|
if (input.action.requestBody.required) {
|
|
313
|
-
throw new Error(
|
|
314
|
-
"Missing request body. Provide --data, --file, or body field flags.",
|
|
315
|
-
);
|
|
278
|
+
throw new Error("Missing required request body fields.");
|
|
316
279
|
}
|
|
317
|
-
} else
|
|
280
|
+
} else {
|
|
318
281
|
if (!contentType?.includes("json")) {
|
|
319
282
|
throw new Error(
|
|
320
|
-
"Body field flags are only supported for JSON request bodies.
|
|
283
|
+
"Body field flags are only supported for JSON request bodies.",
|
|
321
284
|
);
|
|
322
285
|
}
|
|
323
286
|
|
|
@@ -328,7 +291,7 @@ export async function buildRequest(
|
|
|
328
291
|
const missing = findMissingRequired(input.flagValues, bodyFlagDefs);
|
|
329
292
|
if (missing.length > 0) {
|
|
330
293
|
throw new Error(
|
|
331
|
-
`Missing required body field '${missing[0]}'. Provide --${missing[0]}
|
|
294
|
+
`Missing required body field '${missing[0]}'. Provide --${missing[0]}.`,
|
|
332
295
|
);
|
|
333
296
|
}
|
|
334
297
|
|
|
@@ -343,47 +306,6 @@ export async function buildRequest(
|
|
|
343
306
|
}
|
|
344
307
|
|
|
345
308
|
body = JSON.stringify(built);
|
|
346
|
-
} else if (hasData) {
|
|
347
|
-
if (contentType?.includes("json")) {
|
|
348
|
-
const parsed = parseBodyAsJsonOrYaml(data as string);
|
|
349
|
-
|
|
350
|
-
if (schema) {
|
|
351
|
-
const validate = ajv.compile(schema);
|
|
352
|
-
if (!validate(parsed)) {
|
|
353
|
-
throw new Error(formatAjvErrors(validate.errors));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
body = JSON.stringify(parsed);
|
|
358
|
-
} else {
|
|
359
|
-
body = data as string;
|
|
360
|
-
}
|
|
361
|
-
} else if (hasFile) {
|
|
362
|
-
const loaded = await loadBody({
|
|
363
|
-
kind: "file",
|
|
364
|
-
path: file as string,
|
|
365
|
-
});
|
|
366
|
-
if (contentType?.includes("json")) {
|
|
367
|
-
const parsed = parseBodyAsJsonOrYaml(loaded?.raw ?? "");
|
|
368
|
-
|
|
369
|
-
if (schema) {
|
|
370
|
-
const validate = ajv.compile(schema);
|
|
371
|
-
if (!validate(parsed)) {
|
|
372
|
-
throw new Error(formatAjvErrors(validate.errors));
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
body = JSON.stringify(parsed);
|
|
377
|
-
} else {
|
|
378
|
-
body = loaded?.raw;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
} else {
|
|
382
|
-
if (
|
|
383
|
-
typeof input.globals.data === "string" ||
|
|
384
|
-
typeof input.globals.file === "string"
|
|
385
|
-
) {
|
|
386
|
-
throw new Error("This operation does not accept a request body");
|
|
387
309
|
}
|
|
388
310
|
}
|
|
389
311
|
|