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 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. If you try to use them without a JSON content type, specli errors.
303
- - Required fields in the schema are checked in a "friendly" way:
304
- - `Missing required body field 'name'. Provide --name or use --data/--file.`
305
- - If a body field flag conflicts with an operation parameter flag, the operation parameter takes precedence.
306
- - Numeric coercion uses `Number(...)` / `parseInt(...)`. Today it does not explicitly reject `NaN` (this is an area to harden).
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
@@ -2,7 +2,7 @@
2
2
  "name": "specli",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.0.2",
5
+ "version": "0.0.3",
6
6
  "bin": {
7
7
  "specli": "./cli.ts"
8
8
  },
@@ -34,92 +34,47 @@ export async function executeAction(input: ExecuteInput): Promise<void> {
34
34
  return;
35
35
  }
36
36
 
37
- if (input.globals.dryRun) {
38
- process.stdout.write(`${request.method} ${request.url}\n`);
39
- for (const [k, v] of request.headers.entries()) {
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
- try {
60
- const res = await fetch(request, { signal: controller?.signal });
61
- const contentType = res.headers.get("content-type") ?? "";
62
- const status = res.status;
41
+ const text = await res.text();
42
+ let body: unknown = text;
43
+ let parsedJson: unknown | undefined;
63
44
 
64
- const text = await res.text();
65
- let body: unknown = text;
66
- let parsedJson: unknown | undefined;
67
-
68
- if (contentType.includes("json")) {
69
- try {
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
- const payload: unknown =
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.stdout.write(text);
119
- if (!text.endsWith("\n")) process.stdout.write("\n");
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
- } finally {
122
- if (timeout) clearTimeout(timeout);
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
- const reservedFlags = new Set(action.flags.map((f) => f.flag));
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
- // Common curl-replacement options.
84
- // Only add flags that don't conflict with operation flags.
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 hasExpandedBody = bodyFlagDefs.some((def) => {
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
- if (hasData && hasFile) throw new Error("Use only one of --data or --file");
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 (!hasExpandedBody && !hasData && !hasFile) {
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 if (hasExpandedBody) {
280
+ } else {
318
281
  if (!contentType?.includes("json")) {
319
282
  throw new Error(
320
- "Body field flags are only supported for JSON request bodies. Use --content-type application/json or --data/--file.",
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]} or use --data/--file.`,
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