specli 0.0.35 → 0.0.36

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
@@ -1,7 +1,23 @@
1
1
  # specli
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/specli.svg)](https://www.npmjs.com/package/specli)
4
+
3
5
  Turn any OpenAPI spec into a CLI.
4
6
 
7
+ ## Demo
8
+
9
+ Compile this weather OpenAPI spec to an executable:
10
+
11
+ ```sh
12
+ npx specli compile https://raw.githubusercontent.com/open-meteo/open-meteo/refs/heads/main/openapi.yml --name weather
13
+ ```
14
+
15
+ Ask an agent what the current weather is:
16
+
17
+ ```sh
18
+ opencode run 'Using ./out/weather what is the current weather in new york city'
19
+ ```
20
+
5
21
  ## Install
6
22
 
7
23
  ```bash
@@ -330,8 +346,8 @@ const help = api.help("users", "get");
330
346
 
331
347
  // Execute an API call
332
348
  const result = await api.exec("users", "get", ["123"], { include: "profile" });
333
- if (result.ok) {
334
- console.log(result.body);
349
+ if (result.type === "success" && result.response.ok) {
350
+ console.log(result.response.body);
335
351
  }
336
352
  ```
337
353
 
@@ -355,16 +371,40 @@ if (result.ok) {
355
371
  | `help(resource, action)` | Get detailed info about an action |
356
372
  | `exec(resource, action, args?, flags?)` | Execute an API call |
357
373
 
358
- ### ExecuteResult
374
+ ### CommandResult
359
375
 
360
- The `exec()` method returns:
376
+ The `exec()` method returns a `CommandResult` which is a discriminated union:
361
377
 
362
378
  ```typescript
379
+ // Success
363
380
  {
364
- ok: boolean; // true if status 2xx
365
- status: number; // HTTP status code
366
- body: unknown; // Parsed response body
367
- curl: string; // Equivalent curl command
381
+ type: "success";
382
+ request: PreparedRequest;
383
+ response: {
384
+ status: number;
385
+ ok: boolean;
386
+ headers: Record<string, string>;
387
+ body: unknown;
388
+ rawBody: string;
389
+ };
390
+ timing: { startedAt: string; durationMs: number };
391
+ }
392
+
393
+ // Error
394
+ {
395
+ type: "error";
396
+ message: string;
397
+ response?: ResponseData; // If HTTP error
398
+ }
399
+ ```
400
+
401
+ Type guards are available for convenience:
402
+
403
+ ```typescript
404
+ import { isSuccess, isError } from "specli";
405
+
406
+ if (isSuccess(result)) {
407
+ console.log(result.response.body);
368
408
  }
369
409
  ```
370
410
 
@@ -413,19 +453,3 @@ bun test
413
453
  bun run lint
414
454
  bun run typecheck
415
455
  ```
416
-
417
- ## Demos
418
-
419
- ### Weather
420
-
421
- Compile the weather spec to an executable:
422
-
423
- ```sh
424
- npx specli compile https://raw.githubusercontent.com/open-meteo/open-meteo/refs/heads/main/openapi.yml --name weather
425
- ```
426
-
427
- Ask an agent what the current weather is:
428
-
429
- ```sh
430
- opencode run 'Using ./out/weather what is the current weather in new york city'
431
- ```
@@ -30,38 +30,6 @@ export declare function specliTool(options: SpecliOptions): Promise<import("ai")
30
30
  action?: string | undefined;
31
31
  args?: string[] | undefined;
32
32
  flags?: Record<string, unknown> | undefined;
33
- }, import("../client/index.js").ActionDetail | {
34
- resources: import("../client/index.js").ResourceInfo[];
35
- error?: undefined;
36
- resource?: undefined;
37
- actions?: undefined;
38
- status?: undefined;
39
- ok?: undefined;
40
- body?: undefined;
41
- } | {
42
- error: string;
43
- resources?: undefined;
44
- resource?: undefined;
45
- actions?: undefined;
46
- status?: undefined;
47
- ok?: undefined;
48
- body?: undefined;
49
- } | {
50
- resource: string;
51
- actions: string[];
52
- resources?: undefined;
53
- error?: undefined;
54
- status?: undefined;
55
- ok?: undefined;
56
- body?: undefined;
57
- } | {
58
- status: number;
59
- ok: boolean;
60
- body: unknown;
61
- resources?: undefined;
62
- error?: undefined;
63
- resource?: undefined;
64
- actions?: undefined;
65
- }>>;
33
+ }, Record<string, unknown> | import("../client/index.js").ActionDetail>>;
66
34
  export { specliTool as specli };
67
35
  export type { SpecliOptions as SpecliToolOptions } from "../client/index.js";
package/dist/ai/tools.js CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { tool } from "ai";
21
21
  import { z } from "zod";
22
+ import { toJSON } from "../cli/runtime/render.js";
22
23
  import { createClient } from "../client/index.js";
23
24
  /**
24
25
  * Create an AI SDK tool for interacting with an OpenAPI spec.
@@ -69,13 +70,8 @@ export async function specliTool(options) {
69
70
  if (command === "exec") {
70
71
  if (!resource || !action)
71
72
  return { error: "Missing resource or action" };
72
- try {
73
- const result = await client.exec(resource, action, args ?? [], flags ?? {});
74
- return { status: result.status, ok: result.ok, body: result.body };
75
- }
76
- catch (err) {
77
- return { error: err instanceof Error ? err.message : String(err) };
78
- }
73
+ const result = await client.exec(resource, action, args ?? [], flags ?? {});
74
+ return toJSON(result);
79
75
  }
80
76
  return { error: `Unknown command: ${command}` };
81
77
  },
@@ -9,7 +9,10 @@ export type CommandAction = {
9
9
  id: string;
10
10
  key: string;
11
11
  action: string;
12
+ /** CLI-friendly path arg names (kebab-case) for display */
12
13
  pathArgs: string[];
14
+ /** Original path template variable names (for URL substitution) */
15
+ rawPathArgs: string[];
13
16
  method: string;
14
17
  path: string;
15
18
  operationId?: string;
@@ -20,6 +20,7 @@ export function buildCommandModel(planned, options) {
20
20
  key: op.key,
21
21
  action: op.action,
22
22
  pathArgs: op.pathArgs,
23
+ rawPathArgs: op.rawPathArgs,
23
24
  method: op.method,
24
25
  path: op.path,
25
26
  operationId: op.operationId,
@@ -2,7 +2,10 @@ import type { NormalizedOperation } from "../core/types.js";
2
2
  export type PlannedOperation = NormalizedOperation & {
3
3
  resource: string;
4
4
  action: string;
5
+ /** CLI-friendly path arg names (kebab-case) */
5
6
  pathArgs: string[];
7
+ /** Original path template variable names (for URL substitution) */
8
+ rawPathArgs: string[];
6
9
  style: "rest" | "rpc";
7
10
  canonicalAction: string;
8
11
  aliasOf?: string;
@@ -234,6 +234,7 @@ export function planOperation(op) {
234
234
  const style = inferStyle(op);
235
235
  const resource = inferResource(op);
236
236
  const action = style === "rpc" ? inferRpcAction(op) : inferRestAction(op);
237
+ const rawPathArgs = getPathArgs(op.path);
237
238
  return {
238
239
  ...op,
239
240
  key: op.key,
@@ -241,7 +242,8 @@ export function planOperation(op) {
241
242
  resource,
242
243
  action,
243
244
  canonicalAction: action,
244
- pathArgs: getPathArgs(op.path).map((a) => kebabCase(a)),
245
+ pathArgs: rawPathArgs.map((a) => kebabCase(a)),
246
+ rawPathArgs,
245
247
  };
246
248
  }
247
249
  export function planOperations(ops) {
@@ -3,6 +3,7 @@ import type { AuthScheme } from "../parse/auth-schemes.js";
3
3
  import type { ServerInfo } from "../parse/servers.js";
4
4
  import type { BodyFlagDef } from "./body-flags.js";
5
5
  import { type EmbeddedDefaults, type RuntimeGlobals } from "./request.js";
6
+ import type { CommandResult } from "./result.js";
6
7
  export type ExecuteInput = {
7
8
  action: CommandAction;
8
9
  positionalValues: string[];
@@ -16,17 +17,16 @@ export type ExecuteInput = {
16
17
  /** Resource name for error messages (e.g. "plans") */
17
18
  resourceName?: string;
18
19
  };
19
- export type ExecuteResult = {
20
- ok: boolean;
21
- status: number;
22
- body: unknown;
23
- curl: string;
24
- };
25
20
  /**
26
- * Execute an action and return the result as data.
21
+ * Build a prepared request without executing it.
22
+ * Returns a PreparedResult or ErrorResult (for validation failures).
23
+ */
24
+ export declare function prepare(input: Omit<ExecuteInput, "resourceName">): Promise<CommandResult>;
25
+ /**
26
+ * Execute an action and return the result as a CommandResult.
27
27
  * This is the core execution function used by both CLI and programmatic API.
28
28
  */
29
- export declare function execute(input: Omit<ExecuteInput, "resourceName">): Promise<ExecuteResult>;
29
+ export declare function execute(input: Omit<ExecuteInput, "resourceName">): Promise<CommandResult>;
30
30
  /**
31
31
  * Execute an action and write output to stdout/stderr.
32
32
  * This is the CLI-facing wrapper around execute().
@@ -1,47 +1,133 @@
1
+ import { getExitCode, getOutputStream, renderToString } from "./render.js";
1
2
  import { buildRequest, } from "./request.js";
2
3
  /**
3
- * Format an error message with a help hint.
4
+ * Build a prepared request without executing it.
5
+ * Returns a PreparedResult or ErrorResult (for validation failures).
4
6
  */
5
- function formatError(message, resourceName, actionName) {
6
- const helpCmd = resourceName
7
- ? `${resourceName} ${actionName} --help`
8
- : `${actionName} --help`;
9
- return `${message}\n\nRun '${helpCmd}' to see available options.`;
7
+ export async function prepare(input) {
8
+ try {
9
+ const { request, curl, body } = await buildRequest({
10
+ specId: input.specId,
11
+ action: input.action,
12
+ positionalValues: input.positionalValues,
13
+ flagValues: input.flagValues,
14
+ globals: input.globals,
15
+ servers: input.servers,
16
+ authSchemes: input.authSchemes,
17
+ embeddedDefaults: input.embeddedDefaults,
18
+ bodyFlagDefs: input.bodyFlagDefs,
19
+ });
20
+ const headers = {};
21
+ for (const [key, value] of request.headers.entries()) {
22
+ headers[key] = value;
23
+ }
24
+ const prepared = {
25
+ method: request.method,
26
+ url: request.url,
27
+ headers,
28
+ body,
29
+ curl,
30
+ };
31
+ return {
32
+ type: "prepared",
33
+ request: prepared,
34
+ };
35
+ }
36
+ catch (err) {
37
+ return {
38
+ type: "error",
39
+ message: err instanceof Error ? err.message : String(err),
40
+ };
41
+ }
10
42
  }
11
43
  /**
12
- * Execute an action and return the result as data.
44
+ * Execute an action and return the result as a CommandResult.
13
45
  * This is the core execution function used by both CLI and programmatic API.
14
46
  */
15
47
  export async function execute(input) {
16
- const { request, curl } = await buildRequest({
17
- specId: input.specId,
18
- action: input.action,
19
- positionalValues: input.positionalValues,
20
- flagValues: input.flagValues,
21
- globals: input.globals,
22
- servers: input.servers,
23
- authSchemes: input.authSchemes,
24
- embeddedDefaults: input.embeddedDefaults,
25
- bodyFlagDefs: input.bodyFlagDefs,
26
- });
27
- const res = await fetch(request);
28
- const contentType = res.headers.get("content-type") ?? "";
29
- const text = await res.text();
30
- let body = text;
31
- if (contentType.includes("json") && text) {
32
- try {
33
- body = JSON.parse(text);
48
+ const startTime = Date.now();
49
+ const startedAt = new Date().toISOString();
50
+ try {
51
+ const { request, curl, body } = await buildRequest({
52
+ specId: input.specId,
53
+ action: input.action,
54
+ positionalValues: input.positionalValues,
55
+ flagValues: input.flagValues,
56
+ globals: input.globals,
57
+ servers: input.servers,
58
+ authSchemes: input.authSchemes,
59
+ embeddedDefaults: input.embeddedDefaults,
60
+ bodyFlagDefs: input.bodyFlagDefs,
61
+ });
62
+ // Build PreparedRequest before fetch (since body gets consumed)
63
+ const headers = {};
64
+ for (const [key, value] of request.headers.entries()) {
65
+ headers[key] = value;
34
66
  }
35
- catch {
36
- // keep as text
67
+ const preparedRequest = {
68
+ method: request.method,
69
+ url: request.url,
70
+ headers,
71
+ body,
72
+ curl,
73
+ };
74
+ // Handle --curl mode
75
+ if (input.globals.curl) {
76
+ const result = {
77
+ type: "curl",
78
+ curl,
79
+ request: preparedRequest,
80
+ };
81
+ return result;
37
82
  }
83
+ // Execute the request
84
+ const res = await fetch(request);
85
+ const durationMs = Date.now() - startTime;
86
+ const contentType = res.headers.get("content-type") ?? "";
87
+ const rawBody = await res.text();
88
+ let parsedBody = rawBody;
89
+ if (contentType.includes("json") && rawBody) {
90
+ try {
91
+ parsedBody = JSON.parse(rawBody);
92
+ }
93
+ catch {
94
+ // keep as text
95
+ }
96
+ }
97
+ // Build response headers
98
+ const responseHeaders = {};
99
+ for (const [key, value] of res.headers.entries()) {
100
+ responseHeaders[key] = value;
101
+ }
102
+ const result = {
103
+ type: "success",
104
+ request: preparedRequest,
105
+ response: {
106
+ status: res.status,
107
+ ok: res.ok,
108
+ headers: responseHeaders,
109
+ body: parsedBody,
110
+ rawBody,
111
+ },
112
+ timing: {
113
+ startedAt,
114
+ durationMs,
115
+ },
116
+ };
117
+ return result;
118
+ }
119
+ catch (err) {
120
+ const durationMs = Date.now() - startTime;
121
+ const result = {
122
+ type: "error",
123
+ message: err instanceof Error ? err.message : String(err),
124
+ timing: {
125
+ startedAt,
126
+ durationMs,
127
+ },
128
+ };
129
+ return result;
38
130
  }
39
- return {
40
- ok: res.ok,
41
- status: res.status,
42
- body,
43
- curl,
44
- };
45
131
  }
46
132
  /**
47
133
  * Execute an action and write output to stdout/stderr.
@@ -50,57 +136,24 @@ export async function execute(input) {
50
136
  export async function executeAction(input) {
51
137
  const actionName = input.action.action;
52
138
  const resourceName = input.resourceName;
53
- try {
54
- if (input.globals.curl) {
55
- const { curl } = await buildRequest({
56
- specId: input.specId,
57
- action: input.action,
58
- positionalValues: input.positionalValues,
59
- flagValues: input.flagValues,
60
- globals: input.globals,
61
- servers: input.servers,
62
- authSchemes: input.authSchemes,
63
- embeddedDefaults: input.embeddedDefaults,
64
- bodyFlagDefs: input.bodyFlagDefs,
65
- });
66
- process.stdout.write(`${curl}\n`);
67
- return;
68
- }
69
- const result = await execute(input);
70
- if (!result.ok) {
71
- if (input.globals.json) {
72
- process.stdout.write(`${JSON.stringify({ status: result.status, body: result.body })}\n`);
73
- }
74
- else {
75
- process.stderr.write(`HTTP ${result.status}\n`);
76
- process.stderr.write(`${typeof result.body === "string" ? result.body : JSON.stringify(result.body, null, 2)}\n`);
77
- }
78
- process.exitCode = 1;
79
- return;
80
- }
81
- if (input.globals.json) {
82
- process.stdout.write(`${JSON.stringify(result.body)}\n`);
83
- return;
84
- }
85
- // default (human + agent readable)
86
- if (typeof result.body === "string") {
87
- process.stdout.write(result.body);
88
- if (!result.body.endsWith("\n"))
89
- process.stdout.write("\n");
90
- }
91
- else {
92
- process.stdout.write(`${JSON.stringify(result.body, null, 2)}\n`);
93
- }
139
+ // Execute and get the result
140
+ const result = await execute(input);
141
+ // Add context for error messages
142
+ if (result.type === "error" || result.type === "validation") {
143
+ result.resource = resourceName;
144
+ result.action = actionName;
94
145
  }
95
- catch (err) {
96
- const rawMessage = err instanceof Error ? err.message : String(err);
97
- const message = formatError(rawMessage, resourceName, actionName);
98
- if (input.globals.json) {
99
- process.stdout.write(`${JSON.stringify({ error: rawMessage })}\n`);
100
- }
101
- else {
102
- process.stderr.write(`error: ${message}\n`);
103
- }
104
- process.exitCode = 1;
146
+ // Render the result
147
+ const format = input.globals.json ? "json" : "text";
148
+ const output = renderToString(result, { format });
149
+ // Write to appropriate stream
150
+ const stream = getOutputStream(result);
151
+ if (stream === "stderr") {
152
+ process.stderr.write(output);
153
+ }
154
+ else {
155
+ process.stdout.write(output);
105
156
  }
157
+ // Set exit code
158
+ process.exitCode = getExitCode(result);
106
159
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Render functions for CommandResult.
3
+ *
4
+ * These functions convert the IR to output formats suitable for
5
+ * CLI display or programmatic consumption.
6
+ */
7
+ import type { CommandResult } from "./result.js";
8
+ export type RenderOptions = {
9
+ /** Output format: "text" (human readable) or "json" (machine readable) */
10
+ format?: "text" | "json";
11
+ /** Include timing information in output */
12
+ showTiming?: boolean;
13
+ /** Pretty-print JSON output (default: true for text, false for json format) */
14
+ prettyPrint?: boolean;
15
+ };
16
+ /**
17
+ * Render a CommandResult to a string for CLI output.
18
+ *
19
+ * @param result - The command result to render
20
+ * @param options - Rendering options
21
+ * @returns A string ready to be output (includes trailing newline)
22
+ */
23
+ export declare function renderToString(result: CommandResult, options?: RenderOptions): string;
24
+ /**
25
+ * Render a CommandResult to JSON string.
26
+ */
27
+ export declare function renderToJSON(result: CommandResult, options?: RenderOptions): string;
28
+ /**
29
+ * Convert a CommandResult to a plain JSON-serializable object.
30
+ */
31
+ export declare function toJSON(result: CommandResult): Record<string, unknown>;
32
+ /**
33
+ * Render a CommandResult to human-readable text.
34
+ */
35
+ export declare function renderToText(result: CommandResult, options?: RenderOptions): string;
36
+ /**
37
+ * Determine the exit code for a CommandResult.
38
+ * Returns 0 for success, 1 for errors.
39
+ */
40
+ export declare function getExitCode(result: CommandResult): number;
41
+ /**
42
+ * Determine which output stream to use for a CommandResult.
43
+ * Returns "stdout" for success, "stderr" for errors.
44
+ */
45
+ export declare function getOutputStream(result: CommandResult): "stdout" | "stderr";
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Render functions for CommandResult.
3
+ *
4
+ * These functions convert the IR to output formats suitable for
5
+ * CLI display or programmatic consumption.
6
+ */
7
+ // ----------------------------------------------------------------------------
8
+ // Main Render Function
9
+ // ----------------------------------------------------------------------------
10
+ /**
11
+ * Render a CommandResult to a string for CLI output.
12
+ *
13
+ * @param result - The command result to render
14
+ * @param options - Rendering options
15
+ * @returns A string ready to be output (includes trailing newline)
16
+ */
17
+ export function renderToString(result, options = {}) {
18
+ const { format = "text" } = options;
19
+ if (format === "json") {
20
+ return renderToJSON(result, options);
21
+ }
22
+ return renderToText(result, options);
23
+ }
24
+ // ----------------------------------------------------------------------------
25
+ // JSON Rendering
26
+ // ----------------------------------------------------------------------------
27
+ /**
28
+ * Render a CommandResult to JSON string.
29
+ */
30
+ export function renderToJSON(result, options = {}) {
31
+ const { prettyPrint = false } = options;
32
+ const indent = prettyPrint ? 2 : undefined;
33
+ const json = toJSON(result);
34
+ return `${JSON.stringify(json, null, indent)}\n`;
35
+ }
36
+ /**
37
+ * Convert a CommandResult to a plain JSON-serializable object.
38
+ */
39
+ export function toJSON(result) {
40
+ switch (result.type) {
41
+ case "success":
42
+ return {
43
+ ok: true,
44
+ status: result.response.status,
45
+ body: result.response.body,
46
+ };
47
+ case "error":
48
+ return {
49
+ ok: false,
50
+ error: result.message,
51
+ ...(result.response && {
52
+ status: result.response.status,
53
+ body: result.response.body,
54
+ }),
55
+ };
56
+ case "validation":
57
+ return {
58
+ ok: false,
59
+ error: "Validation failed",
60
+ errors: result.errors,
61
+ };
62
+ case "prepared":
63
+ return {
64
+ ok: true,
65
+ request: result.request,
66
+ };
67
+ case "curl":
68
+ return {
69
+ ok: true,
70
+ curl: result.curl,
71
+ };
72
+ case "data":
73
+ return {
74
+ ok: true,
75
+ data: result.data,
76
+ };
77
+ }
78
+ }
79
+ // ----------------------------------------------------------------------------
80
+ // Text Rendering
81
+ // ----------------------------------------------------------------------------
82
+ /**
83
+ * Render a CommandResult to human-readable text.
84
+ */
85
+ export function renderToText(result, options = {}) {
86
+ switch (result.type) {
87
+ case "success":
88
+ return renderSuccessText(result, options);
89
+ case "error":
90
+ return renderErrorText(result, options);
91
+ case "validation":
92
+ return renderValidationText(result, options);
93
+ case "prepared":
94
+ return renderPreparedText(result, options);
95
+ case "curl":
96
+ return renderCurlText(result, options);
97
+ case "data":
98
+ return renderDataText(result, options);
99
+ }
100
+ }
101
+ function renderSuccessText(result, _options) {
102
+ const body = result.response.body;
103
+ if (typeof body === "string") {
104
+ return body.endsWith("\n") ? body : `${body}\n`;
105
+ }
106
+ return `${JSON.stringify(body, null, 2)}\n`;
107
+ }
108
+ function renderErrorText(result, _options) {
109
+ const lines = [];
110
+ // If we have an HTTP response, show status first
111
+ if (result.response) {
112
+ lines.push(`HTTP ${result.response.status}`);
113
+ const body = result.response.body;
114
+ if (typeof body === "string") {
115
+ lines.push(body);
116
+ }
117
+ else if (body !== undefined && body !== null) {
118
+ lines.push(JSON.stringify(body, null, 2));
119
+ }
120
+ }
121
+ else {
122
+ // No HTTP response - just an error message
123
+ lines.push(`error: ${result.message}`);
124
+ // Add help hint if we have resource/action context
125
+ if (result.action) {
126
+ const helpCmd = result.resource
127
+ ? `${result.resource} ${result.action} --help`
128
+ : `${result.action} --help`;
129
+ lines.push("");
130
+ lines.push(`Run '${helpCmd}' to see available options.`);
131
+ }
132
+ }
133
+ return `${lines.join("\n")}\n`;
134
+ }
135
+ function renderValidationText(result, _options) {
136
+ const lines = ["Validation errors:"];
137
+ for (const error of result.errors) {
138
+ lines.push(` ${error.path}: ${error.message}`);
139
+ }
140
+ // Add help hint if we have resource/action context
141
+ if (result.action) {
142
+ const helpCmd = result.resource
143
+ ? `${result.resource} ${result.action} --help`
144
+ : `${result.action} --help`;
145
+ lines.push("");
146
+ lines.push(`Run '${helpCmd}' to see available options.`);
147
+ }
148
+ return `${lines.join("\n")}\n`;
149
+ }
150
+ function renderPreparedText(result, _options) {
151
+ const { request } = result;
152
+ const lines = [`${request.method} ${request.url}`, "", "Headers:"];
153
+ for (const [key, value] of Object.entries(request.headers)) {
154
+ lines.push(` ${key}: ${value}`);
155
+ }
156
+ if (request.body) {
157
+ lines.push("");
158
+ lines.push("Body:");
159
+ lines.push(request.body);
160
+ }
161
+ return `${lines.join("\n")}\n`;
162
+ }
163
+ function renderCurlText(result, _options) {
164
+ return `${result.curl}\n`;
165
+ }
166
+ function renderDataText(result, _options) {
167
+ const { data } = result;
168
+ if (typeof data === "string") {
169
+ return data.endsWith("\n") ? data : `${data}\n`;
170
+ }
171
+ return `${JSON.stringify(data, null, 2)}\n`;
172
+ }
173
+ // ----------------------------------------------------------------------------
174
+ // Exit Code Helper
175
+ // ----------------------------------------------------------------------------
176
+ /**
177
+ * Determine the exit code for a CommandResult.
178
+ * Returns 0 for success, 1 for errors.
179
+ */
180
+ export function getExitCode(result) {
181
+ switch (result.type) {
182
+ case "success":
183
+ return result.response.ok ? 0 : 1;
184
+ case "error":
185
+ case "validation":
186
+ return 1;
187
+ case "prepared":
188
+ case "curl":
189
+ case "data":
190
+ return 0;
191
+ }
192
+ }
193
+ // ----------------------------------------------------------------------------
194
+ // Output Stream Helper
195
+ // ----------------------------------------------------------------------------
196
+ /**
197
+ * Determine which output stream to use for a CommandResult.
198
+ * Returns "stdout" for success, "stderr" for errors.
199
+ */
200
+ export function getOutputStream(result) {
201
+ switch (result.type) {
202
+ case "success":
203
+ return result.response.ok ? "stdout" : "stderr";
204
+ case "error":
205
+ case "validation":
206
+ return "stderr";
207
+ case "prepared":
208
+ case "curl":
209
+ case "data":
210
+ return "stdout";
211
+ }
212
+ }
@@ -33,4 +33,5 @@ export type BuildRequestInput = {
33
33
  export declare function buildRequest(input: BuildRequestInput): Promise<{
34
34
  request: Request;
35
35
  curl: string;
36
+ body?: string;
36
37
  }>;
@@ -103,18 +103,14 @@ export async function buildRequest(input) {
103
103
  servers: input.servers,
104
104
  serverVars,
105
105
  });
106
- // Path params: action.positionals order matches templated params order.
106
+ // Path params: positionals order matches templated params order.
107
+ // Use rawPathArgs (original template variable names) for URL substitution.
107
108
  const pathVars = {};
108
109
  for (let i = 0; i < input.action.positionals.length; i++) {
109
- const pos = input.action.positionals[i];
110
- const raw = input.action.pathArgs[i];
110
+ const rawName = input.action.rawPathArgs[i];
111
111
  const value = input.positionalValues[i];
112
- if (typeof raw === "string" && typeof value === "string") {
113
- pathVars[raw] = value;
114
- }
115
- // Use cli name too as fallback
116
- if (pos?.name && typeof value === "string") {
117
- pathVars[pos.name] = value;
112
+ if (typeof rawName === "string" && typeof value === "string") {
113
+ pathVars[rawName] = value;
118
114
  }
119
115
  }
120
116
  const path = applyTemplate(input.action.path, pathVars, { encode: true });
@@ -259,7 +255,7 @@ export async function buildRequest(input) {
259
255
  body,
260
256
  });
261
257
  const curl = buildCurl(req, body);
262
- return { request: req, curl };
258
+ return { request: req, curl, body };
263
259
  }
264
260
  function buildCurl(req, body) {
265
261
  const parts = ["curl", "-sS", "-X", req.method];
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Intermediate Representation (IR) for command results.
3
+ *
4
+ * All specli operations return a CommandResult which can be:
5
+ * - Rendered to string for CLI output
6
+ * - Serialized to JSON for programmatic use
7
+ * - Inspected/modified before execution (PreparedRequest)
8
+ */
9
+ /**
10
+ * A request that has been built but not yet executed.
11
+ * Can be inspected, modified, or converted to curl.
12
+ */
13
+ export type PreparedRequest = {
14
+ /** HTTP method */
15
+ method: string;
16
+ /** Full URL including query params */
17
+ url: string;
18
+ /** Request headers */
19
+ headers: Record<string, string>;
20
+ /** Request body (if any) */
21
+ body?: string;
22
+ /** Curl command equivalent */
23
+ curl: string;
24
+ };
25
+ /**
26
+ * HTTP response data.
27
+ */
28
+ export type ResponseData = {
29
+ /** HTTP status code */
30
+ status: number;
31
+ /** Whether status is 2xx */
32
+ ok: boolean;
33
+ /** Response headers */
34
+ headers: Record<string, string>;
35
+ /** Parsed response body */
36
+ body: unknown;
37
+ /** Raw response body as string */
38
+ rawBody: string;
39
+ };
40
+ /**
41
+ * A single validation error.
42
+ */
43
+ export type ValidationError = {
44
+ /** Path to the invalid field (e.g., "body.name", "query.limit") */
45
+ path: string;
46
+ /** Error message */
47
+ message: string;
48
+ /** The invalid value (if available) */
49
+ value?: unknown;
50
+ };
51
+ /**
52
+ * Request timing information.
53
+ */
54
+ export type Timing = {
55
+ /** When the request started (ISO string) */
56
+ startedAt: string;
57
+ /** Total duration in milliseconds */
58
+ durationMs: number;
59
+ };
60
+ /**
61
+ * Base fields shared by all result types.
62
+ */
63
+ type ResultBase = {
64
+ /** Resource name (e.g., "users") */
65
+ resource?: string;
66
+ /** Action name (e.g., "list", "get") */
67
+ action?: string;
68
+ };
69
+ /**
70
+ * Successful execution result.
71
+ */
72
+ export type SuccessResult = ResultBase & {
73
+ type: "success";
74
+ /** The prepared request that was sent */
75
+ request: PreparedRequest;
76
+ /** The response received */
77
+ response: ResponseData;
78
+ /** Request timing */
79
+ timing: Timing;
80
+ };
81
+ /**
82
+ * Error result (HTTP error or execution error).
83
+ */
84
+ export type ErrorResult = ResultBase & {
85
+ type: "error";
86
+ /** Error message */
87
+ message: string;
88
+ /** The prepared request (if available) */
89
+ request?: PreparedRequest;
90
+ /** The response (if HTTP error) */
91
+ response?: ResponseData;
92
+ /** Request timing (if request was made) */
93
+ timing?: Timing;
94
+ };
95
+ /**
96
+ * Validation failure result.
97
+ */
98
+ export type ValidationResult = ResultBase & {
99
+ type: "validation";
100
+ /** Validation errors */
101
+ errors: ValidationError[];
102
+ /** The request that failed validation (partial) */
103
+ request?: Partial<PreparedRequest>;
104
+ };
105
+ /**
106
+ * Prepared request result (dry-run mode).
107
+ */
108
+ export type PreparedResult = ResultBase & {
109
+ type: "prepared";
110
+ /** The prepared request ready to execute */
111
+ request: PreparedRequest;
112
+ };
113
+ /**
114
+ * Curl output result (--curl mode).
115
+ */
116
+ export type CurlResult = ResultBase & {
117
+ type: "curl";
118
+ /** The curl command */
119
+ curl: string;
120
+ /** The full prepared request */
121
+ request: PreparedRequest;
122
+ };
123
+ /**
124
+ * Data result (for list, help, schema commands).
125
+ */
126
+ export type DataResult = ResultBase & {
127
+ type: "data";
128
+ /** The data payload */
129
+ data: unknown;
130
+ };
131
+ /**
132
+ * All possible command result types.
133
+ */
134
+ export type CommandResult = SuccessResult | ErrorResult | ValidationResult | PreparedResult | CurlResult | DataResult;
135
+ export declare function isSuccess(result: CommandResult): result is SuccessResult;
136
+ export declare function isError(result: CommandResult): result is ErrorResult;
137
+ export declare function isValidation(result: CommandResult): result is ValidationResult;
138
+ export declare function isPrepared(result: CommandResult): result is PreparedResult;
139
+ export declare function isCurl(result: CommandResult): result is CurlResult;
140
+ export declare function isData(result: CommandResult): result is DataResult;
141
+ /**
142
+ * Returns true if the result represents a successful operation.
143
+ * For HTTP requests, this means a 2xx status code.
144
+ */
145
+ export declare function isOk(result: CommandResult): boolean;
146
+ /**
147
+ * Extract the response body from a result (if available).
148
+ */
149
+ export declare function getBody(result: CommandResult): unknown;
150
+ /**
151
+ * Extract the HTTP status code from a result (if available).
152
+ */
153
+ export declare function getStatus(result: CommandResult): number | undefined;
154
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Intermediate Representation (IR) for command results.
3
+ *
4
+ * All specli operations return a CommandResult which can be:
5
+ * - Rendered to string for CLI output
6
+ * - Serialized to JSON for programmatic use
7
+ * - Inspected/modified before execution (PreparedRequest)
8
+ */
9
+ // ----------------------------------------------------------------------------
10
+ // Type guards
11
+ // ----------------------------------------------------------------------------
12
+ export function isSuccess(result) {
13
+ return result.type === "success";
14
+ }
15
+ export function isError(result) {
16
+ return result.type === "error";
17
+ }
18
+ export function isValidation(result) {
19
+ return result.type === "validation";
20
+ }
21
+ export function isPrepared(result) {
22
+ return result.type === "prepared";
23
+ }
24
+ export function isCurl(result) {
25
+ return result.type === "curl";
26
+ }
27
+ export function isData(result) {
28
+ return result.type === "data";
29
+ }
30
+ // ----------------------------------------------------------------------------
31
+ // Helper to check if result represents a successful operation
32
+ // ----------------------------------------------------------------------------
33
+ /**
34
+ * Returns true if the result represents a successful operation.
35
+ * For HTTP requests, this means a 2xx status code.
36
+ */
37
+ export function isOk(result) {
38
+ switch (result.type) {
39
+ case "success":
40
+ return result.response.ok;
41
+ case "error":
42
+ case "validation":
43
+ return false;
44
+ case "prepared":
45
+ case "curl":
46
+ case "data":
47
+ return true;
48
+ }
49
+ }
50
+ // ----------------------------------------------------------------------------
51
+ // Helper to extract body from result
52
+ // ----------------------------------------------------------------------------
53
+ /**
54
+ * Extract the response body from a result (if available).
55
+ */
56
+ export function getBody(result) {
57
+ switch (result.type) {
58
+ case "success":
59
+ return result.response.body;
60
+ case "error":
61
+ return result.response?.body;
62
+ case "data":
63
+ return result.data;
64
+ default:
65
+ return undefined;
66
+ }
67
+ }
68
+ // ----------------------------------------------------------------------------
69
+ // Helper to extract status from result
70
+ // ----------------------------------------------------------------------------
71
+ /**
72
+ * Extract the HTTP status code from a result (if available).
73
+ */
74
+ export function getStatus(result) {
75
+ switch (result.type) {
76
+ case "success":
77
+ return result.response.status;
78
+ case "error":
79
+ return result.response?.status;
80
+ default:
81
+ return undefined;
82
+ }
83
+ }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { AuthScheme } from "../cli/parse/auth-schemes.js";
5
5
  import type { ServerInfo } from "../cli/parse/servers.js";
6
- import { type ExecuteResult } from "../cli/runtime/execute.js";
6
+ import type { CommandResult } from "../cli/runtime/result.js";
7
7
  export type SpecliOptions = {
8
8
  /** The OpenAPI spec URL or file path */
9
9
  spec: string;
@@ -57,8 +57,15 @@ export type SpecliClient = {
57
57
  list(): ResourceInfo[];
58
58
  /** Get detailed help for a specific action */
59
59
  help(resource: string, action: string): ActionDetail | undefined;
60
- /** Execute an API action */
61
- exec(resource: string, action: string, args?: string[], flags?: Record<string, unknown>): Promise<ExecuteResult>;
60
+ /**
61
+ * Execute an API action and return the full CommandResult.
62
+ */
63
+ exec(resource: string, action: string, args?: string[], flags?: Record<string, unknown>): Promise<CommandResult>;
64
+ /**
65
+ * Prepare a request without executing it.
66
+ * Returns a PreparedResult or ErrorResult.
67
+ */
68
+ prepare(resource: string, action: string, args?: string[], flags?: Record<string, unknown>): Promise<CommandResult>;
62
69
  /** Get server information */
63
70
  servers: ServerInfo[];
64
71
  /** Get authentication schemes */
@@ -70,4 +77,4 @@ export type SpecliClient = {
70
77
  export declare function createClient(options: SpecliOptions): Promise<SpecliClient>;
71
78
  export type { AuthScheme } from "../cli/parse/auth-schemes.js";
72
79
  export type { ServerInfo } from "../cli/parse/servers.js";
73
- export type { ExecuteResult } from "../cli/runtime/execute.js";
80
+ export type { CommandResult, CurlResult, DataResult, ErrorResult, PreparedRequest, PreparedResult, ResponseData, SuccessResult, Timing, ValidationError, ValidationResult, } from "../cli/runtime/result.js";
@@ -2,7 +2,7 @@
2
2
  * Specli Client - Core programmatic API for OpenAPI specs
3
3
  */
4
4
  import { buildRuntimeContext } from "../cli/runtime/context.js";
5
- import { execute } from "../cli/runtime/execute.js";
5
+ import { execute, prepare } from "../cli/runtime/execute.js";
6
6
  function findAction(ctx, resource, action) {
7
7
  const r = ctx.commands.resources.find((r) => r.resource.toLowerCase() === resource.toLowerCase());
8
8
  return r?.actions.find((a) => a.action.toLowerCase() === action.toLowerCase());
@@ -63,9 +63,14 @@ export async function createClient(options) {
63
63
  async exec(resource, action, args = [], flags = {}) {
64
64
  const actionDef = findAction(ctx, resource, action);
65
65
  if (!actionDef) {
66
- throw new Error(`Unknown action: ${resource} ${action}`);
66
+ return {
67
+ type: "error",
68
+ message: `Unknown action: ${resource} ${action}`,
69
+ resource,
70
+ action,
71
+ };
67
72
  }
68
- return execute({
73
+ const result = await execute({
69
74
  specId: ctx.loaded.id,
70
75
  action: actionDef,
71
76
  positionalValues: args,
@@ -74,6 +79,34 @@ export async function createClient(options) {
74
79
  servers: ctx.servers,
75
80
  authSchemes: ctx.authSchemes,
76
81
  });
82
+ // Add context to the result
83
+ result.resource = resource;
84
+ result.action = action;
85
+ return result;
86
+ },
87
+ async prepare(resource, action, args = [], flags = {}) {
88
+ const actionDef = findAction(ctx, resource, action);
89
+ if (!actionDef) {
90
+ return {
91
+ type: "error",
92
+ message: `Unknown action: ${resource} ${action}`,
93
+ resource,
94
+ action,
95
+ };
96
+ }
97
+ const result = await prepare({
98
+ specId: ctx.loaded.id,
99
+ action: actionDef,
100
+ positionalValues: args,
101
+ flagValues: flags,
102
+ globals,
103
+ servers: ctx.servers,
104
+ authSchemes: ctx.authSchemes,
105
+ });
106
+ // Add context to the result
107
+ result.resource = resource;
108
+ result.action = action;
109
+ return result;
77
110
  },
78
111
  servers: ctx.servers,
79
112
  authSchemes: ctx.authSchemes,
package/dist/index.d.ts CHANGED
@@ -10,9 +10,11 @@
10
10
  * // List available resources and actions
11
11
  * const resources = api.list();
12
12
  *
13
- * // Execute an API call
13
+ * // Execute an API call and get the full result
14
14
  * const result = await api.exec("users", "get", ["123"]);
15
- * console.log(result.body);
15
+ * if (result.type === "success") {
16
+ * console.log(result.response.body);
17
+ * }
16
18
  * ```
17
19
  */
18
20
  import { type SpecliClient, type SpecliOptions } from "./client/index.js";
@@ -33,10 +35,12 @@ import { type SpecliClient, type SpecliOptions } from "./client/index.js";
33
35
  *
34
36
  * // Execute a call
35
37
  * const result = await api.exec("users", "list");
36
- * if (result.ok) {
37
- * console.log(result.body);
38
+ * if (result.type === "success" && result.response.ok) {
39
+ * console.log(result.response.body);
38
40
  * }
39
41
  * ```
40
42
  */
41
43
  export declare function specli(options: SpecliOptions): Promise<SpecliClient>;
42
- export type { ActionDetail, ActionInfo, AuthScheme, ExecuteResult, ResourceInfo, ServerInfo, SpecliClient, SpecliOptions, } from "./client/index.js";
44
+ export { getExitCode, getOutputStream, type RenderOptions, renderToJSON, renderToString, toJSON, } from "./cli/runtime/render.js";
45
+ export { getBody, getStatus, isCurl, isData, isError, isOk, isPrepared, isSuccess, isValidation, } from "./cli/runtime/result.js";
46
+ export type { ActionDetail, ActionInfo, AuthScheme, CommandResult, CurlResult, DataResult, ErrorResult, PreparedRequest, PreparedResult, ResourceInfo, ResponseData, ServerInfo, SpecliClient, SpecliOptions, SuccessResult, Timing, ValidationError, ValidationResult, } from "./client/index.js";
package/dist/index.js CHANGED
@@ -10,9 +10,11 @@
10
10
  * // List available resources and actions
11
11
  * const resources = api.list();
12
12
  *
13
- * // Execute an API call
13
+ * // Execute an API call and get the full result
14
14
  * const result = await api.exec("users", "get", ["123"]);
15
- * console.log(result.body);
15
+ * if (result.type === "success") {
16
+ * console.log(result.response.body);
17
+ * }
16
18
  * ```
17
19
  */
18
20
  import { createClient, } from "./client/index.js";
@@ -33,11 +35,15 @@ import { createClient, } from "./client/index.js";
33
35
  *
34
36
  * // Execute a call
35
37
  * const result = await api.exec("users", "list");
36
- * if (result.ok) {
37
- * console.log(result.body);
38
+ * if (result.type === "success" && result.response.ok) {
39
+ * console.log(result.response.body);
38
40
  * }
39
41
  * ```
40
42
  */
41
43
  export async function specli(options) {
42
44
  return createClient(options);
43
45
  }
46
+ // Re-export render utilities for advanced usage
47
+ export { getExitCode, getOutputStream, renderToJSON, renderToString, toJSON, } from "./cli/runtime/render.js";
48
+ // Re-export type guards for convenience
49
+ export { getBody, getStatus, isCurl, isData, isError, isOk, isPrepared, isSuccess, isValidation, } from "./cli/runtime/result.js";
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "specli",
3
- "version": "0.0.35",
4
- "description": "Run any OpenAPI spec as a CLI. Built for Agents.",
3
+ "version": "0.0.36",
4
+ "description": "Run any OpenAPI spec as an Agent optimized executable",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/AndrewBarba/specli.git"
7
+ "url": "https://github.com/vercel-labs/specli.git"
8
8
  },
9
9
  "type": "module",
10
10
  "module": "./dist/index.js",