specli 0.0.21 → 0.0.23
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 +58 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +114 -0
- package/package.json +22 -16
- package/dist/ai/tools.test.d.ts +0 -1
- package/dist/ai/tools.test.js +0 -49
- package/dist/cli/model/capabilities.test.d.ts +0 -1
- package/dist/cli/model/capabilities.test.js +0 -84
- package/dist/cli/model/command-id.test.d.ts +0 -1
- package/dist/cli/model/command-id.test.js +0 -27
- package/dist/cli/model/command-model.test.d.ts +0 -1
- package/dist/cli/model/command-model.test.js +0 -40
- package/dist/cli/model/naming.test.d.ts +0 -1
- package/dist/cli/model/naming.test.js +0 -75
- package/dist/cli/parse/auth-requirements.test.d.ts +0 -1
- package/dist/cli/parse/auth-requirements.test.js +0 -16
- package/dist/cli/parse/auth-schemes.test.d.ts +0 -1
- package/dist/cli/parse/auth-schemes.test.js +0 -56
- package/dist/cli/parse/operations.test.d.ts +0 -1
- package/dist/cli/parse/operations.test.js +0 -51
- package/dist/cli/parse/params.test.d.ts +0 -1
- package/dist/cli/parse/params.test.js +0 -62
- package/dist/cli/parse/positional.test.d.ts +0 -1
- package/dist/cli/parse/positional.test.js +0 -60
- package/dist/cli/parse/request-body.test.d.ts +0 -1
- package/dist/cli/parse/request-body.test.js +0 -31
- package/dist/cli/parse/servers.test.d.ts +0 -1
- package/dist/cli/parse/servers.test.js +0 -49
- package/dist/cli/runtime/body-flags.test.d.ts +0 -1
- package/dist/cli/runtime/body-flags.test.js +0 -192
- package/dist/cli/runtime/request.test.d.ts +0 -1
- package/dist/cli/runtime/request.test.js +0 -332
- package/dist/cli/runtime/validate/coerce.test.d.ts +0 -1
- package/dist/cli/runtime/validate/coerce.test.js +0 -75
package/README.md
CHANGED
|
@@ -310,6 +310,64 @@ Prints equivalent curl command without sending the request.
|
|
|
310
310
|
|
|
311
311
|
Prints method, URL, headers, and body without sending.
|
|
312
312
|
|
|
313
|
+
## Programmatic API
|
|
314
|
+
|
|
315
|
+
Use specli as a library to execute OpenAPI operations programmatically:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { specli } from "specli";
|
|
319
|
+
|
|
320
|
+
const api = await specli({
|
|
321
|
+
spec: "https://api.example.com/openapi.json",
|
|
322
|
+
bearerToken: process.env.API_TOKEN,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// List available resources and actions
|
|
326
|
+
const resources = api.list();
|
|
327
|
+
|
|
328
|
+
// Get help for a specific action
|
|
329
|
+
const help = api.help("users", "get");
|
|
330
|
+
|
|
331
|
+
// Execute an API call
|
|
332
|
+
const result = await api.exec("users", "get", ["123"], { include: "profile" });
|
|
333
|
+
if (result.ok) {
|
|
334
|
+
console.log(result.body);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Options
|
|
339
|
+
|
|
340
|
+
| Option | Description |
|
|
341
|
+
|--------|-------------|
|
|
342
|
+
| `spec` | OpenAPI spec URL or file path (required) |
|
|
343
|
+
| `server` | Override server/base URL |
|
|
344
|
+
| `serverVars` | Server URL template variables (`Record<string, string>`) |
|
|
345
|
+
| `bearerToken` | Bearer token for authentication |
|
|
346
|
+
| `apiKey` | API key for authentication |
|
|
347
|
+
| `basicAuth` | Basic auth credentials (`{ username, password }`) |
|
|
348
|
+
| `authScheme` | Auth scheme to use (if multiple are available) |
|
|
349
|
+
|
|
350
|
+
### Methods
|
|
351
|
+
|
|
352
|
+
| Method | Description |
|
|
353
|
+
|--------|-------------|
|
|
354
|
+
| `list()` | Returns all resources and their actions |
|
|
355
|
+
| `help(resource, action)` | Get detailed info about an action |
|
|
356
|
+
| `exec(resource, action, args?, flags?)` | Execute an API call |
|
|
357
|
+
|
|
358
|
+
### ExecuteResult
|
|
359
|
+
|
|
360
|
+
The `exec()` method returns:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
{
|
|
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
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
313
371
|
## AI SDK Integration
|
|
314
372
|
|
|
315
373
|
specli exports an AI SDK tool for use with LLM agents:
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specli - Convert OpenAPI specs to executables, built to be agent-first
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { specli } from "specli";
|
|
7
|
+
*
|
|
8
|
+
* const api = await specli({ spec: "https://api.example.com/openapi.json" });
|
|
9
|
+
*
|
|
10
|
+
* // List available resources and actions
|
|
11
|
+
* const resources = api.list();
|
|
12
|
+
*
|
|
13
|
+
* // Execute an API call
|
|
14
|
+
* const result = await api.exec("users", "get", ["123"]);
|
|
15
|
+
* console.log(result.body);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { AuthScheme } from "./cli/parse/auth-schemes.js";
|
|
19
|
+
import type { ServerInfo } from "./cli/parse/servers.js";
|
|
20
|
+
import { type ExecuteResult } from "./cli/runtime/execute.js";
|
|
21
|
+
export type SpecliOptions = {
|
|
22
|
+
/** The OpenAPI spec URL or file path */
|
|
23
|
+
spec: string;
|
|
24
|
+
/** Override the server/base URL */
|
|
25
|
+
server?: string;
|
|
26
|
+
/** Server URL template variables */
|
|
27
|
+
serverVars?: Record<string, string>;
|
|
28
|
+
/** Bearer token for authentication */
|
|
29
|
+
bearerToken?: string;
|
|
30
|
+
/** API key for authentication */
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
/** Basic auth credentials */
|
|
33
|
+
basicAuth?: {
|
|
34
|
+
username: string;
|
|
35
|
+
password: string;
|
|
36
|
+
};
|
|
37
|
+
/** Auth scheme to use (if multiple are available) */
|
|
38
|
+
authScheme?: string;
|
|
39
|
+
};
|
|
40
|
+
export type ResourceInfo = {
|
|
41
|
+
name: string;
|
|
42
|
+
actions: ActionInfo[];
|
|
43
|
+
};
|
|
44
|
+
export type ActionInfo = {
|
|
45
|
+
name: string;
|
|
46
|
+
summary?: string;
|
|
47
|
+
method: string;
|
|
48
|
+
path: string;
|
|
49
|
+
args: string[];
|
|
50
|
+
requiredFlags: string[];
|
|
51
|
+
optionalFlags: string[];
|
|
52
|
+
};
|
|
53
|
+
export type ActionDetail = {
|
|
54
|
+
action: string;
|
|
55
|
+
method: string;
|
|
56
|
+
path: string;
|
|
57
|
+
summary?: string;
|
|
58
|
+
args: Array<{
|
|
59
|
+
name: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
}>;
|
|
62
|
+
flags: Array<{
|
|
63
|
+
name: string;
|
|
64
|
+
type: string;
|
|
65
|
+
required: boolean;
|
|
66
|
+
description?: string;
|
|
67
|
+
}>;
|
|
68
|
+
};
|
|
69
|
+
export type SpecliClient = {
|
|
70
|
+
/** List all available resources and their actions */
|
|
71
|
+
list(): ResourceInfo[];
|
|
72
|
+
/** Get detailed help for a specific action */
|
|
73
|
+
help(resource: string, action: string): ActionDetail | undefined;
|
|
74
|
+
/** Execute an API action */
|
|
75
|
+
exec(resource: string, action: string, args?: string[], flags?: Record<string, unknown>): Promise<ExecuteResult>;
|
|
76
|
+
/** Get server information */
|
|
77
|
+
servers: ServerInfo[];
|
|
78
|
+
/** Get authentication schemes */
|
|
79
|
+
authSchemes: AuthScheme[];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Create a specli client for interacting with an OpenAPI spec.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { specli } from "specli";
|
|
87
|
+
*
|
|
88
|
+
* const api = await specli({
|
|
89
|
+
* spec: "https://api.example.com/openapi.json",
|
|
90
|
+
* bearerToken: process.env.API_TOKEN,
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* // List resources
|
|
94
|
+
* const resources = api.list();
|
|
95
|
+
*
|
|
96
|
+
* // Execute a call
|
|
97
|
+
* const result = await api.exec("users", "list");
|
|
98
|
+
* if (result.ok) {
|
|
99
|
+
* console.log(result.body);
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function specli(options: SpecliOptions): Promise<SpecliClient>;
|
|
104
|
+
export type { AuthScheme } from "./cli/parse/auth-schemes.js";
|
|
105
|
+
export type { ServerInfo } from "./cli/parse/servers.js";
|
|
106
|
+
export type { ExecuteResult } from "./cli/runtime/execute.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specli - Convert OpenAPI specs to executables, built to be agent-first
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { specli } from "specli";
|
|
7
|
+
*
|
|
8
|
+
* const api = await specli({ spec: "https://api.example.com/openapi.json" });
|
|
9
|
+
*
|
|
10
|
+
* // List available resources and actions
|
|
11
|
+
* const resources = api.list();
|
|
12
|
+
*
|
|
13
|
+
* // Execute an API call
|
|
14
|
+
* const result = await api.exec("users", "get", ["123"]);
|
|
15
|
+
* console.log(result.body);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { buildRuntimeContext } from "./cli/runtime/context.js";
|
|
19
|
+
import { execute } from "./cli/runtime/execute.js";
|
|
20
|
+
function findAction(ctx, resource, action) {
|
|
21
|
+
const r = ctx.commands.resources.find((r) => r.resource.toLowerCase() === resource.toLowerCase());
|
|
22
|
+
return r?.actions.find((a) => a.action.toLowerCase() === action.toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a specli client for interacting with an OpenAPI spec.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { specli } from "specli";
|
|
30
|
+
*
|
|
31
|
+
* const api = await specli({
|
|
32
|
+
* spec: "https://api.example.com/openapi.json",
|
|
33
|
+
* bearerToken: process.env.API_TOKEN,
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // List resources
|
|
37
|
+
* const resources = api.list();
|
|
38
|
+
*
|
|
39
|
+
* // Execute a call
|
|
40
|
+
* const result = await api.exec("users", "list");
|
|
41
|
+
* if (result.ok) {
|
|
42
|
+
* console.log(result.body);
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export async function specli(options) {
|
|
47
|
+
const { spec, server, serverVars, bearerToken, apiKey, basicAuth, authScheme, } = options;
|
|
48
|
+
const ctx = await buildRuntimeContext({ spec });
|
|
49
|
+
const globals = {
|
|
50
|
+
server,
|
|
51
|
+
serverVar: serverVars
|
|
52
|
+
? Object.entries(serverVars).map(([k, v]) => `${k}=${v}`)
|
|
53
|
+
: undefined,
|
|
54
|
+
auth: authScheme,
|
|
55
|
+
bearerToken,
|
|
56
|
+
apiKey,
|
|
57
|
+
username: basicAuth?.username,
|
|
58
|
+
password: basicAuth?.password,
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
list() {
|
|
62
|
+
return ctx.commands.resources.map((r) => ({
|
|
63
|
+
name: r.resource,
|
|
64
|
+
actions: r.actions.map((a) => ({
|
|
65
|
+
name: a.action,
|
|
66
|
+
summary: a.summary,
|
|
67
|
+
method: a.method,
|
|
68
|
+
path: a.path,
|
|
69
|
+
args: a.positionals.map((p) => p.name),
|
|
70
|
+
requiredFlags: a.flags.filter((f) => f.required).map((f) => f.flag),
|
|
71
|
+
optionalFlags: a.flags.filter((f) => !f.required).map((f) => f.flag),
|
|
72
|
+
})),
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
help(resource, action) {
|
|
76
|
+
const actionDef = findAction(ctx, resource, action);
|
|
77
|
+
if (!actionDef)
|
|
78
|
+
return undefined;
|
|
79
|
+
return {
|
|
80
|
+
action: actionDef.action,
|
|
81
|
+
method: actionDef.method,
|
|
82
|
+
path: actionDef.path,
|
|
83
|
+
summary: actionDef.summary,
|
|
84
|
+
args: actionDef.positionals.map((p) => ({
|
|
85
|
+
name: p.name,
|
|
86
|
+
description: p.description,
|
|
87
|
+
})),
|
|
88
|
+
flags: actionDef.flags.map((f) => ({
|
|
89
|
+
name: f.flag,
|
|
90
|
+
type: f.type,
|
|
91
|
+
required: f.required,
|
|
92
|
+
description: f.description,
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
async exec(resource, action, args = [], flags = {}) {
|
|
97
|
+
const actionDef = findAction(ctx, resource, action);
|
|
98
|
+
if (!actionDef) {
|
|
99
|
+
throw new Error(`Unknown action: ${resource} ${action}`);
|
|
100
|
+
}
|
|
101
|
+
return execute({
|
|
102
|
+
specId: ctx.loaded.id,
|
|
103
|
+
action: actionDef,
|
|
104
|
+
positionalValues: args,
|
|
105
|
+
flagValues: flags,
|
|
106
|
+
globals,
|
|
107
|
+
servers: ctx.servers,
|
|
108
|
+
authSchemes: ctx.authSchemes,
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
servers: ctx.servers,
|
|
112
|
+
authSchemes: ctx.authSchemes,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
|
+
"description": "Run any OpenAPI spec as a CLI. Built for Agents.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/AndrewBarba/specli.git"
|
|
8
|
+
},
|
|
4
9
|
"type": "module",
|
|
5
10
|
"module": "./dist/index.js",
|
|
6
11
|
"types": "./dist/index.d.ts",
|
|
7
|
-
"bin": {
|
|
8
|
-
"specli": "./bin/cli.sh"
|
|
9
|
-
},
|
|
10
12
|
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
},
|
|
11
17
|
"./ai/tools": {
|
|
12
18
|
"import": "./dist/ai/tools.js",
|
|
13
19
|
"types": "./dist/ai/tools.d.ts"
|
|
14
20
|
}
|
|
15
21
|
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"specli": "./bin/cli.sh"
|
|
24
|
+
},
|
|
16
25
|
"files": [
|
|
17
26
|
"bin",
|
|
18
27
|
"dist",
|
|
19
28
|
"README.md",
|
|
20
|
-
"package.json"
|
|
29
|
+
"package.json",
|
|
30
|
+
"!**/*.test.*"
|
|
21
31
|
],
|
|
22
32
|
"scripts": {
|
|
23
33
|
"build": "tsc",
|
|
@@ -27,14 +37,6 @@
|
|
|
27
37
|
"lint:format": "biome format --write",
|
|
28
38
|
"typecheck": "tsgo"
|
|
29
39
|
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@biomejs/biome": "^2.3.11",
|
|
32
|
-
"@tsconfig/node22": "^22.0.5",
|
|
33
|
-
"@types/bun": "^1.3.6",
|
|
34
|
-
"@types/node": "^22.19.7",
|
|
35
|
-
"@typescript/native-preview": "^7.0.0-dev.20260122.3",
|
|
36
|
-
"typescript": "^5.9.3"
|
|
37
|
-
},
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
40
42
|
"ajv": "^8.17.1",
|
|
@@ -47,8 +49,12 @@
|
|
|
47
49
|
"ai": "^6.0.0",
|
|
48
50
|
"zod": "^4.0.0"
|
|
49
51
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@biomejs/biome": "^2.3.11",
|
|
54
|
+
"@tsconfig/node22": "^22.0.5",
|
|
55
|
+
"@types/bun": "^1.3.6",
|
|
56
|
+
"@types/node": "^22.19.7",
|
|
57
|
+
"@typescript/native-preview": "^7.0.0-dev.20260122.3",
|
|
58
|
+
"typescript": "^5.9.3"
|
|
53
59
|
}
|
|
54
60
|
}
|
package/dist/ai/tools.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/ai/tools.test.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { specli } from "./tools.js";
|
|
3
|
-
const mockOptions = {
|
|
4
|
-
toolCallId: "test-call-id",
|
|
5
|
-
abortSignal: new AbortController().signal,
|
|
6
|
-
messages: [],
|
|
7
|
-
};
|
|
8
|
-
describe("specli tool", () => {
|
|
9
|
-
test("creates a tool with correct structure", async () => {
|
|
10
|
-
const tool = await specli({
|
|
11
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
12
|
-
});
|
|
13
|
-
expect(tool).toHaveProperty("description");
|
|
14
|
-
expect(tool).toHaveProperty("inputSchema");
|
|
15
|
-
expect(tool).toHaveProperty("execute");
|
|
16
|
-
expect(typeof tool.execute).toBe("function");
|
|
17
|
-
});
|
|
18
|
-
test("list command returns resources", async () => {
|
|
19
|
-
const tool = await specli({
|
|
20
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
21
|
-
});
|
|
22
|
-
const result = (await tool.execute?.({ command: "list" }, mockOptions));
|
|
23
|
-
expect(result).toHaveProperty("resources");
|
|
24
|
-
expect(Array.isArray(result.resources)).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
test("help command returns action details", async () => {
|
|
27
|
-
const tool = await specli({
|
|
28
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
29
|
-
});
|
|
30
|
-
const result = (await tool.execute?.({ command: "help", resource: "pets", action: "get" }, mockOptions));
|
|
31
|
-
expect(result).toHaveProperty("action");
|
|
32
|
-
expect(result.action).toBe("get");
|
|
33
|
-
});
|
|
34
|
-
test("help command with missing resource returns error", async () => {
|
|
35
|
-
const tool = await specli({
|
|
36
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
37
|
-
});
|
|
38
|
-
const result = (await tool.execute?.({ command: "help" }, mockOptions));
|
|
39
|
-
expect(result).toHaveProperty("error");
|
|
40
|
-
});
|
|
41
|
-
test("exec command with missing args returns error", async () => {
|
|
42
|
-
const tool = await specli({
|
|
43
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
44
|
-
});
|
|
45
|
-
const result = (await tool.execute?.({ command: "exec", resource: "pets", action: "get" }, mockOptions));
|
|
46
|
-
expect(result).toHaveProperty("error");
|
|
47
|
-
expect(result.error).toContain("Missing args");
|
|
48
|
-
});
|
|
49
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { deriveCapabilities } from "./capabilities.js";
|
|
3
|
-
describe("deriveCapabilities", () => {
|
|
4
|
-
test("reports requestBody + server vars", () => {
|
|
5
|
-
const doc = {
|
|
6
|
-
openapi: "3.0.3",
|
|
7
|
-
security: [{ bearerAuth: [] }],
|
|
8
|
-
};
|
|
9
|
-
const servers = [
|
|
10
|
-
{
|
|
11
|
-
url: "https://{region}.api.example.com",
|
|
12
|
-
variables: [],
|
|
13
|
-
variableNames: ["region"],
|
|
14
|
-
},
|
|
15
|
-
];
|
|
16
|
-
const authSchemes = [
|
|
17
|
-
{ key: "bearerAuth", kind: "http-bearer" },
|
|
18
|
-
];
|
|
19
|
-
const operations = [
|
|
20
|
-
{
|
|
21
|
-
key: "POST /contacts",
|
|
22
|
-
method: "POST",
|
|
23
|
-
path: "/contacts",
|
|
24
|
-
tags: [],
|
|
25
|
-
parameters: [],
|
|
26
|
-
requestBody: {
|
|
27
|
-
required: true,
|
|
28
|
-
contentTypes: ["application/json"],
|
|
29
|
-
schemasByContentType: { "application/json": { type: "object" } },
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
];
|
|
33
|
-
const commands = {
|
|
34
|
-
resources: [
|
|
35
|
-
{
|
|
36
|
-
resource: "contacts",
|
|
37
|
-
actions: [
|
|
38
|
-
{
|
|
39
|
-
id: "x",
|
|
40
|
-
key: "POST /contacts",
|
|
41
|
-
action: "create",
|
|
42
|
-
pathArgs: [],
|
|
43
|
-
method: "POST",
|
|
44
|
-
path: "/contacts",
|
|
45
|
-
tags: [],
|
|
46
|
-
style: "rest",
|
|
47
|
-
positionals: [],
|
|
48
|
-
flags: [],
|
|
49
|
-
params: [],
|
|
50
|
-
auth: { alternatives: [] },
|
|
51
|
-
requestBody: {
|
|
52
|
-
required: true,
|
|
53
|
-
content: [
|
|
54
|
-
{
|
|
55
|
-
contentType: "application/json",
|
|
56
|
-
required: true,
|
|
57
|
-
schemaType: "object",
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
hasJson: true,
|
|
61
|
-
hasFormUrlEncoded: false,
|
|
62
|
-
hasMultipart: false,
|
|
63
|
-
bodyFlags: ["--data", "--file"],
|
|
64
|
-
preferredContentType: "application/json",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
const caps = deriveCapabilities({
|
|
72
|
-
doc,
|
|
73
|
-
servers,
|
|
74
|
-
authSchemes,
|
|
75
|
-
operations,
|
|
76
|
-
commands,
|
|
77
|
-
});
|
|
78
|
-
expect(caps.servers.hasVariables).toBe(true);
|
|
79
|
-
expect(caps.operations.hasRequestBodies).toBe(true);
|
|
80
|
-
expect(caps.commands.hasRequestBodies).toBe(true);
|
|
81
|
-
expect(caps.auth.hasSecurityRequirements).toBe(true);
|
|
82
|
-
expect(caps.auth.kinds).toEqual(["http-bearer"]);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildCommandId } from "./command-id.js";
|
|
3
|
-
describe("buildCommandId", () => {
|
|
4
|
-
test("includes spec/resource/action/op", () => {
|
|
5
|
-
expect(buildCommandId({
|
|
6
|
-
specId: "contacts-api",
|
|
7
|
-
resource: "contacts",
|
|
8
|
-
action: "get",
|
|
9
|
-
operationKey: "GET /contacts/{id}",
|
|
10
|
-
})).toBe("contacts-api:contacts:get:get-contacts-id");
|
|
11
|
-
});
|
|
12
|
-
test("disambiguates by operationKey", () => {
|
|
13
|
-
const a = buildCommandId({
|
|
14
|
-
specId: "x",
|
|
15
|
-
resource: "contacts",
|
|
16
|
-
action: "list",
|
|
17
|
-
operationKey: "GET /contacts",
|
|
18
|
-
});
|
|
19
|
-
const b = buildCommandId({
|
|
20
|
-
specId: "x",
|
|
21
|
-
resource: "contacts",
|
|
22
|
-
action: "list",
|
|
23
|
-
operationKey: "GET /contacts/search",
|
|
24
|
-
});
|
|
25
|
-
expect(a).not.toBe(b);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildCommandModel } from "./command-model.js";
|
|
3
|
-
describe("buildCommandModel", () => {
|
|
4
|
-
test("groups operations by resource", () => {
|
|
5
|
-
const planned = [
|
|
6
|
-
{
|
|
7
|
-
key: "GET /contacts",
|
|
8
|
-
method: "GET",
|
|
9
|
-
path: "/contacts",
|
|
10
|
-
tags: ["Contacts"],
|
|
11
|
-
parameters: [],
|
|
12
|
-
resource: "contacts",
|
|
13
|
-
action: "list",
|
|
14
|
-
canonicalAction: "list",
|
|
15
|
-
pathArgs: [],
|
|
16
|
-
style: "rest",
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
key: "GET /contacts/{id}",
|
|
20
|
-
method: "GET",
|
|
21
|
-
path: "/contacts/{id}",
|
|
22
|
-
tags: ["Contacts"],
|
|
23
|
-
parameters: [],
|
|
24
|
-
resource: "contacts",
|
|
25
|
-
action: "get",
|
|
26
|
-
canonicalAction: "get",
|
|
27
|
-
pathArgs: ["id"],
|
|
28
|
-
style: "rest",
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
const model = buildCommandModel(planned, { specId: "contacts-api" });
|
|
32
|
-
expect(model.resources).toHaveLength(1);
|
|
33
|
-
expect(model.resources[0]?.resource).toBe("contacts");
|
|
34
|
-
expect(model.resources[0]?.actions).toHaveLength(2);
|
|
35
|
-
expect(model.resources[0]?.actions.map((a) => a.action)).toEqual([
|
|
36
|
-
"get",
|
|
37
|
-
"list",
|
|
38
|
-
]);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { planOperation } from "./naming.js";
|
|
3
|
-
describe("planOperation", () => {
|
|
4
|
-
test("REST: GET /contacts -> contacts list", () => {
|
|
5
|
-
const op = {
|
|
6
|
-
key: "GET /contacts",
|
|
7
|
-
method: "GET",
|
|
8
|
-
path: "/contacts",
|
|
9
|
-
operationId: "Contacts.List",
|
|
10
|
-
tags: ["Contacts"],
|
|
11
|
-
parameters: [],
|
|
12
|
-
};
|
|
13
|
-
const planned = planOperation(op);
|
|
14
|
-
expect(planned.style).toBe("rest");
|
|
15
|
-
expect(planned.resource).toBe("contacts");
|
|
16
|
-
expect(planned.action).toBe("list");
|
|
17
|
-
expect(planned.pathArgs).toEqual([]);
|
|
18
|
-
});
|
|
19
|
-
test("REST: singleton /ping stays ping and prefers operationId action", () => {
|
|
20
|
-
const op = {
|
|
21
|
-
key: "GET /ping",
|
|
22
|
-
method: "GET",
|
|
23
|
-
path: "/ping",
|
|
24
|
-
operationId: "Ping.Get",
|
|
25
|
-
tags: [],
|
|
26
|
-
parameters: [],
|
|
27
|
-
};
|
|
28
|
-
const planned = planOperation(op);
|
|
29
|
-
expect(planned.style).toBe("rest");
|
|
30
|
-
expect(planned.resource).toBe("ping");
|
|
31
|
-
expect(planned.action).toBe("get");
|
|
32
|
-
});
|
|
33
|
-
test("REST: singular path pluralizes to contacts", () => {
|
|
34
|
-
const op = {
|
|
35
|
-
key: "GET /contact/{id}",
|
|
36
|
-
method: "GET",
|
|
37
|
-
path: "/contact/{id}",
|
|
38
|
-
tags: [],
|
|
39
|
-
parameters: [],
|
|
40
|
-
};
|
|
41
|
-
const planned = planOperation(op);
|
|
42
|
-
expect(planned.style).toBe("rest");
|
|
43
|
-
expect(planned.resource).toBe("contacts");
|
|
44
|
-
expect(planned.action).toBe("get");
|
|
45
|
-
expect(planned.pathArgs).toEqual(["id"]);
|
|
46
|
-
});
|
|
47
|
-
test("RPC: POST /Contacts.List -> contacts list", () => {
|
|
48
|
-
const op = {
|
|
49
|
-
key: "POST /Contacts.List",
|
|
50
|
-
method: "POST",
|
|
51
|
-
path: "/Contacts.List",
|
|
52
|
-
operationId: "Contacts.List",
|
|
53
|
-
tags: [],
|
|
54
|
-
parameters: [],
|
|
55
|
-
};
|
|
56
|
-
const planned = planOperation(op);
|
|
57
|
-
expect(planned.style).toBe("rpc");
|
|
58
|
-
expect(planned.resource).toBe("contacts");
|
|
59
|
-
expect(planned.action).toBe("list");
|
|
60
|
-
});
|
|
61
|
-
test("RPC: Retrieve canonicalizes to get", () => {
|
|
62
|
-
const op = {
|
|
63
|
-
key: "POST /Contacts.Retrieve",
|
|
64
|
-
method: "POST",
|
|
65
|
-
path: "/Contacts.Retrieve",
|
|
66
|
-
operationId: "Contacts.Retrieve",
|
|
67
|
-
tags: [],
|
|
68
|
-
parameters: [],
|
|
69
|
-
};
|
|
70
|
-
const planned = planOperation(op);
|
|
71
|
-
expect(planned.style).toBe("rpc");
|
|
72
|
-
expect(planned.resource).toBe("contacts");
|
|
73
|
-
expect(planned.action).toBe("get");
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { summarizeAuth } from "./auth-requirements.js";
|
|
3
|
-
describe("summarizeAuth", () => {
|
|
4
|
-
test("uses operation-level security when present", () => {
|
|
5
|
-
const schemes = [{ key: "oauth", kind: "oauth2" }];
|
|
6
|
-
const summary = summarizeAuth([{ oauth: ["read:ping"] }], [{ oauth: ["read:other"] }], schemes);
|
|
7
|
-
expect(summary.alternatives).toEqual([
|
|
8
|
-
[{ key: "oauth", scopes: ["read:ping"] }],
|
|
9
|
-
]);
|
|
10
|
-
});
|
|
11
|
-
test("empty operation security disables auth", () => {
|
|
12
|
-
const schemes = [{ key: "oauth", kind: "oauth2" }];
|
|
13
|
-
const summary = summarizeAuth([], [{ oauth: ["read:other"] }], schemes);
|
|
14
|
-
expect(summary.alternatives).toEqual([]);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|