specli 0.0.34 → 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 +52 -12
- package/dist/ai/tools.d.ts +1 -33
- package/dist/ai/tools.js +3 -7
- package/dist/cli/main.js +2 -17
- package/dist/cli/model/command-model.d.ts +3 -0
- package/dist/cli/model/command-model.js +1 -0
- package/dist/cli/model/naming.d.ts +3 -0
- package/dist/cli/model/naming.js +3 -1
- package/dist/cli/runtime/execute.d.ts +8 -8
- package/dist/cli/runtime/execute.js +137 -84
- package/dist/cli/runtime/render.d.ts +45 -0
- package/dist/cli/runtime/render.js +212 -0
- package/dist/cli/runtime/request.d.ts +1 -0
- package/dist/cli/runtime/request.js +6 -10
- package/dist/cli/runtime/result.d.ts +154 -0
- package/dist/cli/runtime/result.js +83 -0
- package/dist/cli.js +0 -1
- package/dist/client/index.d.ts +11 -4
- package/dist/client/index.js +36 -3
- package/dist/index.d.ts +9 -5
- package/dist/index.js +10 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
# specli
|
|
2
2
|
|
|
3
|
+
[](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
|
|
@@ -63,7 +79,7 @@ specli compile <spec> [options]
|
|
|
63
79
|
| Option | Description |
|
|
64
80
|
|--------|-------------|
|
|
65
81
|
| `--name <name>` | Binary name (default: derived from spec title) |
|
|
66
|
-
| `--outfile <path>` | Output path (default: `./
|
|
82
|
+
| `--outfile <path>` | Output path (default: `./out/<name>`) |
|
|
67
83
|
| `--target <target>` | Cross-compile target (e.g. `bun-linux-x64`) |
|
|
68
84
|
| `--minify` | Enable minification |
|
|
69
85
|
| `--bytecode` | Enable bytecode compilation |
|
|
@@ -78,14 +94,14 @@ specli compile <spec> [options]
|
|
|
78
94
|
```bash
|
|
79
95
|
# Compile with auto-derived name
|
|
80
96
|
specli compile ./openapi.yaml
|
|
81
|
-
# Creates: ./
|
|
97
|
+
# Creates: ./out/my-api
|
|
82
98
|
|
|
83
99
|
# Compile with explicit name
|
|
84
100
|
specli compile ./openapi.yaml --name myapi
|
|
85
|
-
# Creates: ./
|
|
101
|
+
# Creates: ./out/myapi
|
|
86
102
|
|
|
87
103
|
# Cross-compile for Linux
|
|
88
|
-
specli compile ./openapi.json --target bun-linux-x64 --outfile ./
|
|
104
|
+
specli compile ./openapi.json --target bun-linux-x64 --outfile ./out/myapi-linux
|
|
89
105
|
|
|
90
106
|
# Bake in defaults
|
|
91
107
|
specli compile https://api.example.com/openapi.json \
|
|
@@ -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
|
-
###
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
package/dist/ai/tools.d.ts
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
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
|
},
|
package/dist/cli/main.js
CHANGED
|
@@ -19,8 +19,6 @@ function getPackageVersion() {
|
|
|
19
19
|
return "0.0.0";
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
import { stableStringify } from "./core/stable-json.js";
|
|
23
|
-
import { toMinimalSchemaOutput } from "./model/schema.js";
|
|
24
22
|
import { readStdinText } from "./runtime/compat.js";
|
|
25
23
|
import { buildRuntimeContext } from "./runtime/context.js";
|
|
26
24
|
import { addGeneratedCommands } from "./runtime/generated.js";
|
|
@@ -44,7 +42,6 @@ export async function main(argv, options = {}) {
|
|
|
44
42
|
.option("--username <username>", "Basic auth username")
|
|
45
43
|
.option("--password <password>", "Basic auth password")
|
|
46
44
|
.option("--api-key <key>", "API key value")
|
|
47
|
-
.option("--json", "Machine-readable output")
|
|
48
45
|
.showHelpAfterError();
|
|
49
46
|
// If user asks for help and we have no embedded spec and no --spec, show minimal help.
|
|
50
47
|
const spec = getArgValue(argv, "--spec");
|
|
@@ -149,21 +146,10 @@ export async function main(argv, options = {}) {
|
|
|
149
146
|
});
|
|
150
147
|
program
|
|
151
148
|
.command("__schema")
|
|
152
|
-
.description("Print indexed operations
|
|
153
|
-
.option("--pretty", "Pretty-print JSON when used with --json")
|
|
154
|
-
.option("--min", "Minimal JSON output (commands + metadata only)")
|
|
149
|
+
.description("Print indexed operations")
|
|
155
150
|
.option("--commands", "List all <resource> <action> commands (can be large)")
|
|
156
151
|
.action(async (_opts, command) => {
|
|
157
152
|
const flags = command.optsWithGlobals();
|
|
158
|
-
if (flags.json) {
|
|
159
|
-
const pretty = Boolean(flags.pretty);
|
|
160
|
-
const payload = flags.min
|
|
161
|
-
? toMinimalSchemaOutput(ctx.schema)
|
|
162
|
-
: ctx.schema;
|
|
163
|
-
const text = stableStringify(payload, { space: pretty ? 2 : 0 });
|
|
164
|
-
process.stdout.write(`${text}\n`);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
153
|
process.stdout.write(`${ctx.schema.openapi.title ?? "(untitled)"}\n`);
|
|
168
154
|
process.stdout.write(`OpenAPI: ${ctx.schema.openapi.version}\n`);
|
|
169
155
|
process.stdout.write(`Servers: ${ctx.schema.servers.length}\n`);
|
|
@@ -187,8 +173,7 @@ export async function main(argv, options = {}) {
|
|
|
187
173
|
}
|
|
188
174
|
process.stdout.write("\nNext:\n" +
|
|
189
175
|
`- ${program.name()} <resource> --help\n` +
|
|
190
|
-
`- ${program.name()} <resource> <action> --help\n`
|
|
191
|
-
"\nNote: Use --help to discover required flags; avoid __schema --json for agent use (too verbose).\n");
|
|
176
|
+
`- ${program.name()} <resource> <action> --help\n`);
|
|
192
177
|
});
|
|
193
178
|
addGeneratedCommands(program, {
|
|
194
179
|
servers: ctx.servers,
|
|
@@ -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;
|
|
@@ -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;
|
package/dist/cli/model/naming.js
CHANGED
|
@@ -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:
|
|
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
|
-
*
|
|
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<
|
|
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
|
-
*
|
|
4
|
+
* Build a prepared request without executing it.
|
|
5
|
+
* Returns a PreparedResult or ErrorResult (for validation failures).
|
|
4
6
|
*/
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
process.
|
|
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
|
+
}
|
|
@@ -103,18 +103,14 @@ export async function buildRequest(input) {
|
|
|
103
103
|
servers: input.servers,
|
|
104
104
|
serverVars,
|
|
105
105
|
});
|
|
106
|
-
// Path params:
|
|
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
|
|
110
|
-
const raw = input.action.pathArgs[i];
|
|
110
|
+
const rawName = input.action.rawPathArgs[i];
|
|
111
111
|
const value = input.positionalValues[i];
|
|
112
|
-
if (typeof
|
|
113
|
-
pathVars[
|
|
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
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -36,7 +36,6 @@ program
|
|
|
36
36
|
.option("--password <password>", "Basic auth password")
|
|
37
37
|
.option("--api-key <key>", "API key value")
|
|
38
38
|
.option("--profile <name>", "Profile name")
|
|
39
|
-
.option("--json", "Machine-readable output")
|
|
40
39
|
.allowUnknownOption()
|
|
41
40
|
.allowExcessArguments()
|
|
42
41
|
.action(async (spec, options, command) => {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
61
|
-
|
|
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 {
|
|
80
|
+
export type { CommandResult, CurlResult, DataResult, ErrorResult, PreparedRequest, PreparedResult, ResponseData, SuccessResult, Timing, ValidationError, ValidationResult, } from "../cli/runtime/result.js";
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
66
|
+
return {
|
|
67
|
+
type: "error",
|
|
68
|
+
message: `Unknown action: ${resource} ${action}`,
|
|
69
|
+
resource,
|
|
70
|
+
action,
|
|
71
|
+
};
|
|
67
72
|
}
|
|
68
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
4
|
-
"description": "Run any OpenAPI spec as
|
|
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/
|
|
7
|
+
"url": "https://github.com/vercel-labs/specli.git"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"module": "./dist/index.js",
|