specli 0.0.18 → 0.0.19
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/dist/cli/compile.js +137 -61
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -1
- package/package.json +1 -2
- package/src/ai/tools.test.ts +0 -83
- package/src/ai/tools.ts +0 -211
- package/src/cli/auth-requirements.test.ts +0 -27
- package/src/cli/auth-requirements.ts +0 -91
- package/src/cli/auth-schemes.test.ts +0 -66
- package/src/cli/auth-schemes.ts +0 -187
- package/src/cli/capabilities.test.ts +0 -94
- package/src/cli/capabilities.ts +0 -88
- package/src/cli/command-id.test.ts +0 -32
- package/src/cli/command-id.ts +0 -16
- package/src/cli/command-index.ts +0 -19
- package/src/cli/command-model.test.ts +0 -44
- package/src/cli/command-model.ts +0 -128
- package/src/cli/compile.ts +0 -109
- package/src/cli/crypto.ts +0 -9
- package/src/cli/derive-name.ts +0 -101
- package/src/cli/exec.ts +0 -72
- package/src/cli/main.ts +0 -255
- package/src/cli/naming.test.ts +0 -86
- package/src/cli/naming.ts +0 -224
- package/src/cli/operations.test.ts +0 -57
- package/src/cli/operations.ts +0 -152
- package/src/cli/params.test.ts +0 -70
- package/src/cli/params.ts +0 -71
- package/src/cli/pluralize.ts +0 -41
- package/src/cli/positional.test.ts +0 -65
- package/src/cli/positional.ts +0 -75
- package/src/cli/request-body.test.ts +0 -35
- package/src/cli/request-body.ts +0 -94
- package/src/cli/runtime/argv.ts +0 -14
- package/src/cli/runtime/auth/resolve.ts +0 -59
- package/src/cli/runtime/body-flags.test.ts +0 -261
- package/src/cli/runtime/body-flags.ts +0 -176
- package/src/cli/runtime/body.ts +0 -24
- package/src/cli/runtime/collect.ts +0 -6
- package/src/cli/runtime/compat.ts +0 -89
- package/src/cli/runtime/context.ts +0 -62
- package/src/cli/runtime/execute.ts +0 -147
- package/src/cli/runtime/generated.ts +0 -242
- package/src/cli/runtime/headers.ts +0 -37
- package/src/cli/runtime/index.ts +0 -3
- package/src/cli/runtime/profile/secrets.ts +0 -83
- package/src/cli/runtime/profile/store.ts +0 -100
- package/src/cli/runtime/request.test.ts +0 -375
- package/src/cli/runtime/request.ts +0 -390
- package/src/cli/runtime/server-url.ts +0 -45
- package/src/cli/runtime/template.ts +0 -26
- package/src/cli/runtime/validate/ajv.ts +0 -13
- package/src/cli/runtime/validate/coerce.test.ts +0 -98
- package/src/cli/runtime/validate/coerce.ts +0 -71
- package/src/cli/runtime/validate/error.ts +0 -29
- package/src/cli/runtime/validate/index.ts +0 -4
- package/src/cli/runtime/validate/schema.ts +0 -54
- package/src/cli/schema-shape.ts +0 -36
- package/src/cli/schema.ts +0 -76
- package/src/cli/server.test.ts +0 -55
- package/src/cli/server.ts +0 -167
- package/src/cli/spec-id.ts +0 -12
- package/src/cli/spec-loader.ts +0 -58
- package/src/cli/stable-json.ts +0 -35
- package/src/cli/strings.ts +0 -21
- package/src/cli/types.ts +0 -59
- package/src/cli.ts +0 -94
- package/src/compiled.ts +0 -24
- package/src/macros/env.ts +0 -21
- package/src/macros/spec.ts +0 -17
- package/src/macros/version.ts +0 -14
package/dist/cli/compile.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
1
3
|
import path from "node:path";
|
|
2
4
|
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { deriveBinaryName } from "./derive-name.js";
|
|
4
|
-
// Resolve the
|
|
5
|
-
// At runtime this file is at dist/cli/compile.js, so we go up two levels to package root
|
|
6
|
-
// then into src/compiled.ts (which must be included in the published package)
|
|
6
|
+
// Resolve the package root directory (at runtime this file is at dist/cli/compile.js)
|
|
7
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "../..");
|
|
9
9
|
function parseKeyValue(input) {
|
|
10
10
|
const idx = input.indexOf("=");
|
|
11
11
|
if (idx === -1)
|
|
@@ -16,6 +16,72 @@ function parseKeyValue(input) {
|
|
|
16
16
|
throw new Error(`Invalid --define '${input}', missing key`);
|
|
17
17
|
return { key, value };
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Loads the OpenAPI spec from a URL or file path.
|
|
21
|
+
*/
|
|
22
|
+
async function loadSpec(spec) {
|
|
23
|
+
if (!spec)
|
|
24
|
+
throw new Error("Missing spec path/URL");
|
|
25
|
+
if (/^https?:\/\//i.test(spec)) {
|
|
26
|
+
const res = await fetch(spec);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
return await res.text();
|
|
31
|
+
}
|
|
32
|
+
return await fs.promises.readFile(spec, "utf-8");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reads the package version from package.json.
|
|
36
|
+
*/
|
|
37
|
+
function getPackageVersion() {
|
|
38
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
39
|
+
const content = fs.readFileSync(packageJsonPath, "utf-8");
|
|
40
|
+
const packageJson = JSON.parse(content);
|
|
41
|
+
return packageJson.version;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generates a temporary entrypoint file with all values hardcoded.
|
|
45
|
+
* This avoids the Bun macro security restriction in node_modules.
|
|
46
|
+
*/
|
|
47
|
+
function generateEntrypoint(options) {
|
|
48
|
+
const mainImportPath = path.join(packageRoot, "dist/cli/main.js");
|
|
49
|
+
// Escape the spec text for embedding as a string literal
|
|
50
|
+
const escapedSpec = JSON.stringify(options.specText);
|
|
51
|
+
const escapedName = options.cliName
|
|
52
|
+
? JSON.stringify(options.cliName)
|
|
53
|
+
: "undefined";
|
|
54
|
+
const escapedServer = options.server
|
|
55
|
+
? JSON.stringify(options.server)
|
|
56
|
+
: "undefined";
|
|
57
|
+
const escapedServerVars = options.serverVars
|
|
58
|
+
? JSON.stringify(options.serverVars)
|
|
59
|
+
: "undefined";
|
|
60
|
+
const escapedAuth = options.auth ? JSON.stringify(options.auth) : "undefined";
|
|
61
|
+
const escapedVersion = JSON.stringify(options.version);
|
|
62
|
+
return `#!/usr/bin/env bun
|
|
63
|
+
// Auto-generated entrypoint for specli compile
|
|
64
|
+
// This file embeds all configuration at build time
|
|
65
|
+
|
|
66
|
+
import { main } from ${JSON.stringify(mainImportPath)};
|
|
67
|
+
|
|
68
|
+
const embeddedSpecText = ${escapedSpec};
|
|
69
|
+
const cliName = ${escapedName};
|
|
70
|
+
const server = ${escapedServer};
|
|
71
|
+
const serverVars = ${escapedServerVars};
|
|
72
|
+
const auth = ${escapedAuth};
|
|
73
|
+
const embeddedVersion = ${escapedVersion};
|
|
74
|
+
|
|
75
|
+
await main(process.argv, {
|
|
76
|
+
embeddedSpecText,
|
|
77
|
+
cliName,
|
|
78
|
+
server,
|
|
79
|
+
serverVars: serverVars ? serverVars.split(",") : undefined,
|
|
80
|
+
auth,
|
|
81
|
+
version: embeddedVersion,
|
|
82
|
+
});
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
19
85
|
export async function compileCommand(spec, options) {
|
|
20
86
|
// Derive name from spec if not provided
|
|
21
87
|
const name = options.name ?? (await deriveBinaryName(spec));
|
|
@@ -23,64 +89,74 @@ export async function compileCommand(spec, options) {
|
|
|
23
89
|
const target = options.target
|
|
24
90
|
? options.target
|
|
25
91
|
: `bun-${process.platform}-${process.arch}`;
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
92
|
+
// Load the spec content
|
|
93
|
+
process.stdout.write(`Loading spec: ${spec}\n`);
|
|
94
|
+
const specText = await loadSpec(spec);
|
|
95
|
+
// Get package version
|
|
96
|
+
const version = getPackageVersion();
|
|
97
|
+
// Generate temporary entrypoint file
|
|
98
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "specli-"));
|
|
99
|
+
const tempEntrypoint = path.join(tempDir, "entrypoint.ts");
|
|
100
|
+
const entrypointCode = generateEntrypoint({
|
|
101
|
+
specText,
|
|
102
|
+
cliName: name,
|
|
103
|
+
server: options.server,
|
|
104
|
+
serverVars: options.serverVar?.join(","),
|
|
105
|
+
auth: options.auth,
|
|
106
|
+
version,
|
|
107
|
+
});
|
|
108
|
+
fs.writeFileSync(tempEntrypoint, entrypointCode);
|
|
109
|
+
try {
|
|
110
|
+
// Parse --define pairs
|
|
111
|
+
const define = {};
|
|
112
|
+
if (options.define) {
|
|
113
|
+
for (const pair of options.define) {
|
|
114
|
+
const { key, value } = parseKeyValue(pair);
|
|
115
|
+
define[key] = JSON.stringify(value);
|
|
116
|
+
}
|
|
32
117
|
}
|
|
118
|
+
// Build command args
|
|
119
|
+
const buildArgs = [
|
|
120
|
+
"build",
|
|
121
|
+
"--compile",
|
|
122
|
+
`--outfile=${outfile}`,
|
|
123
|
+
`--target=${target}`,
|
|
124
|
+
];
|
|
125
|
+
if (options.minify)
|
|
126
|
+
buildArgs.push("--minify");
|
|
127
|
+
if (options.bytecode)
|
|
128
|
+
buildArgs.push("--bytecode");
|
|
129
|
+
for (const [k, v] of Object.entries(define)) {
|
|
130
|
+
buildArgs.push("--define", `${k}=${v}`);
|
|
131
|
+
}
|
|
132
|
+
if (options.dotenv === false)
|
|
133
|
+
buildArgs.push("--no-compile-autoload-dotenv");
|
|
134
|
+
if (options.bunfig === false)
|
|
135
|
+
buildArgs.push("--no-compile-autoload-bunfig");
|
|
136
|
+
buildArgs.push(tempEntrypoint);
|
|
137
|
+
const proc = Bun.spawn({
|
|
138
|
+
cmd: ["bun", ...buildArgs],
|
|
139
|
+
stdout: "pipe",
|
|
140
|
+
stderr: "pipe",
|
|
141
|
+
env: process.env,
|
|
142
|
+
});
|
|
143
|
+
const output = await new Response(proc.stdout).text();
|
|
144
|
+
const error = await new Response(proc.stderr).text();
|
|
145
|
+
const code = await proc.exited;
|
|
146
|
+
if (output)
|
|
147
|
+
process.stdout.write(output);
|
|
148
|
+
if (error)
|
|
149
|
+
process.stderr.write(error);
|
|
150
|
+
if (code !== 0) {
|
|
151
|
+
process.exitCode = code;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
process.stdout.write(`ok: built ${outfile}\n`);
|
|
155
|
+
process.stdout.write(`target: ${target}\n`);
|
|
156
|
+
process.stdout.write(`name: ${name}\n`);
|
|
33
157
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"--compile",
|
|
38
|
-
`--outfile=${outfile}`,
|
|
39
|
-
`--target=${target}`,
|
|
40
|
-
];
|
|
41
|
-
if (options.minify)
|
|
42
|
-
buildArgs.push("--minify");
|
|
43
|
-
if (options.bytecode)
|
|
44
|
-
buildArgs.push("--bytecode");
|
|
45
|
-
for (const [k, v] of Object.entries(define)) {
|
|
46
|
-
buildArgs.push("--define", `${k}=${v}`);
|
|
47
|
-
}
|
|
48
|
-
if (options.dotenv === false)
|
|
49
|
-
buildArgs.push("--no-compile-autoload-dotenv");
|
|
50
|
-
if (options.bunfig === false)
|
|
51
|
-
buildArgs.push("--no-compile-autoload-bunfig");
|
|
52
|
-
buildArgs.push(compiledEntrypoint);
|
|
53
|
-
// Only set env vars that have actual values - avoid empty strings
|
|
54
|
-
// because the macros will embed them and they will override defaults.
|
|
55
|
-
const buildEnv = {
|
|
56
|
-
...process.env,
|
|
57
|
-
SPECLI_SPEC: spec,
|
|
58
|
-
SPECLI_NAME: name,
|
|
59
|
-
};
|
|
60
|
-
if (options.server)
|
|
61
|
-
buildEnv.SPECLI_SERVER = options.server;
|
|
62
|
-
if (options.serverVar?.length)
|
|
63
|
-
buildEnv.SPECLI_SERVER_VARS = options.serverVar.join(",");
|
|
64
|
-
if (options.auth)
|
|
65
|
-
buildEnv.SPECLI_AUTH = options.auth;
|
|
66
|
-
const proc = Bun.spawn({
|
|
67
|
-
cmd: ["bun", ...buildArgs],
|
|
68
|
-
stdout: "pipe",
|
|
69
|
-
stderr: "pipe",
|
|
70
|
-
env: buildEnv,
|
|
71
|
-
});
|
|
72
|
-
const output = await new Response(proc.stdout).text();
|
|
73
|
-
const error = await new Response(proc.stderr).text();
|
|
74
|
-
const code = await proc.exited;
|
|
75
|
-
if (output)
|
|
76
|
-
process.stdout.write(output);
|
|
77
|
-
if (error)
|
|
78
|
-
process.stderr.write(error);
|
|
79
|
-
if (code !== 0) {
|
|
80
|
-
process.exitCode = code;
|
|
81
|
-
return;
|
|
158
|
+
finally {
|
|
159
|
+
// Clean up temporary files
|
|
160
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
82
161
|
}
|
|
83
|
-
process.stdout.write(`ok: built ${outfile}\n`);
|
|
84
|
-
process.stdout.write(`target: ${target}\n`);
|
|
85
|
-
process.stdout.write(`name: ${name}\n`);
|
|
86
162
|
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|
|
18
18
|
"dist",
|
|
19
|
-
"src",
|
|
20
19
|
"README.md",
|
|
21
20
|
"package.json"
|
|
22
21
|
],
|
package/src/ai/tools.test.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { clearSpecliCache, specli } from "./tools.js";
|
|
3
|
-
|
|
4
|
-
const mockOptions = {
|
|
5
|
-
toolCallId: "test-call-id",
|
|
6
|
-
abortSignal: new AbortController().signal,
|
|
7
|
-
messages: [],
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
describe("specli tool", () => {
|
|
11
|
-
test("creates a tool with correct structure", () => {
|
|
12
|
-
const tool = specli({
|
|
13
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
expect(tool).toHaveProperty("description");
|
|
17
|
-
expect(tool).toHaveProperty("inputSchema");
|
|
18
|
-
expect(tool).toHaveProperty("execute");
|
|
19
|
-
expect(typeof tool.execute).toBe("function");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("list command returns resources", async () => {
|
|
23
|
-
const tool = specli({
|
|
24
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const result = (await tool.execute?.({ command: "list" }, mockOptions)) as {
|
|
28
|
-
resources: unknown[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
expect(result).toHaveProperty("resources");
|
|
32
|
-
expect(Array.isArray(result.resources)).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("help command returns action details", async () => {
|
|
36
|
-
const tool = specli({
|
|
37
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const result = (await tool.execute?.(
|
|
41
|
-
{ command: "help", resource: "pets", action: "get" },
|
|
42
|
-
mockOptions,
|
|
43
|
-
)) as { action: string };
|
|
44
|
-
|
|
45
|
-
expect(result).toHaveProperty("action");
|
|
46
|
-
expect(result.action).toBe("get");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("help command with missing resource returns error", async () => {
|
|
50
|
-
const tool = specli({
|
|
51
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const result = (await tool.execute?.({ command: "help" }, mockOptions)) as {
|
|
55
|
-
error: string;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
expect(result).toHaveProperty("error");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("exec command with missing args returns error", async () => {
|
|
62
|
-
const tool = specli({
|
|
63
|
-
spec: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const result = (await tool.execute?.(
|
|
67
|
-
{ command: "exec", resource: "pets", action: "get" },
|
|
68
|
-
mockOptions,
|
|
69
|
-
)) as { error: string };
|
|
70
|
-
|
|
71
|
-
expect(result).toHaveProperty("error");
|
|
72
|
-
expect(result.error).toContain("Missing args");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("clearCache works", async () => {
|
|
76
|
-
const spec = "https://petstore3.swagger.io/api/v3/openapi.json";
|
|
77
|
-
const tool = specli({ spec });
|
|
78
|
-
|
|
79
|
-
await tool.execute?.({ command: "list" }, mockOptions);
|
|
80
|
-
clearSpecliCache(spec);
|
|
81
|
-
clearSpecliCache();
|
|
82
|
-
});
|
|
83
|
-
});
|
package/src/ai/tools.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI SDK tools for specli
|
|
3
|
-
*
|
|
4
|
-
* Provides tools for AI agents to explore and execute OpenAPI specs.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```ts
|
|
8
|
-
* import { specli } from "specli/ai";
|
|
9
|
-
* import { generateText } from "ai";
|
|
10
|
-
*
|
|
11
|
-
* const result = await generateText({
|
|
12
|
-
* model: yourModel,
|
|
13
|
-
* tools: {
|
|
14
|
-
* api: specli({ spec: "https://api.example.com/openapi.json" }),
|
|
15
|
-
* },
|
|
16
|
-
* prompt: "List all users",
|
|
17
|
-
* });
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { tool } from "ai";
|
|
22
|
-
import { z } from "zod";
|
|
23
|
-
|
|
24
|
-
import type { CommandAction } from "../cli/command-model.js";
|
|
25
|
-
import { buildRuntimeContext } from "../cli/runtime/context.js";
|
|
26
|
-
import { execute } from "../cli/runtime/execute.js";
|
|
27
|
-
import type { RuntimeGlobals } from "../cli/runtime/request.js";
|
|
28
|
-
|
|
29
|
-
export type SpecliToolOptions = {
|
|
30
|
-
/** The OpenAPI spec URL or file path */
|
|
31
|
-
spec: string;
|
|
32
|
-
/** Override the server/base URL */
|
|
33
|
-
server?: string;
|
|
34
|
-
/** Server URL template variables */
|
|
35
|
-
serverVars?: Record<string, string>;
|
|
36
|
-
/** Bearer token for authentication */
|
|
37
|
-
bearerToken?: string;
|
|
38
|
-
/** API key for authentication */
|
|
39
|
-
apiKey?: string;
|
|
40
|
-
/** Basic auth credentials */
|
|
41
|
-
basicAuth?: { username: string; password: string };
|
|
42
|
-
/** Auth scheme to use (if multiple are available) */
|
|
43
|
-
authScheme?: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Cache contexts to avoid reloading spec on every call
|
|
47
|
-
const contextCache = new Map<
|
|
48
|
-
string,
|
|
49
|
-
Awaited<ReturnType<typeof buildRuntimeContext>>
|
|
50
|
-
>();
|
|
51
|
-
|
|
52
|
-
async function getContext(spec: string) {
|
|
53
|
-
let ctx = contextCache.get(spec);
|
|
54
|
-
if (!ctx) {
|
|
55
|
-
ctx = await buildRuntimeContext({ spec });
|
|
56
|
-
contextCache.set(spec, ctx);
|
|
57
|
-
}
|
|
58
|
-
return ctx;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function findAction(
|
|
62
|
-
ctx: Awaited<ReturnType<typeof buildRuntimeContext>>,
|
|
63
|
-
resource: string,
|
|
64
|
-
action: string,
|
|
65
|
-
): CommandAction | undefined {
|
|
66
|
-
const r = ctx.commands.resources.find(
|
|
67
|
-
(r) => r.resource.toLowerCase() === resource.toLowerCase(),
|
|
68
|
-
);
|
|
69
|
-
return r?.actions.find(
|
|
70
|
-
(a) => a.action.toLowerCase() === action.toLowerCase(),
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Create an AI SDK tool for interacting with an OpenAPI spec.
|
|
76
|
-
*/
|
|
77
|
-
export function specli(options: SpecliToolOptions) {
|
|
78
|
-
const {
|
|
79
|
-
spec,
|
|
80
|
-
server,
|
|
81
|
-
serverVars,
|
|
82
|
-
bearerToken,
|
|
83
|
-
apiKey,
|
|
84
|
-
basicAuth,
|
|
85
|
-
authScheme,
|
|
86
|
-
} = options;
|
|
87
|
-
|
|
88
|
-
return tool({
|
|
89
|
-
description: `Execute API operations. Commands: "list" (show resources/actions), "help" (action details), "exec" (call API).`,
|
|
90
|
-
inputSchema: z.object({
|
|
91
|
-
command: z.enum(["list", "help", "exec"]).describe("Command to run"),
|
|
92
|
-
resource: z.string().optional().describe("Resource name (e.g. users)"),
|
|
93
|
-
action: z
|
|
94
|
-
.string()
|
|
95
|
-
.optional()
|
|
96
|
-
.describe("Action name (e.g. list, get, create)"),
|
|
97
|
-
args: z.array(z.string()).optional().describe("Positional arguments"),
|
|
98
|
-
flags: z
|
|
99
|
-
.record(z.string(), z.unknown())
|
|
100
|
-
.optional()
|
|
101
|
-
.describe("Named flags"),
|
|
102
|
-
}),
|
|
103
|
-
execute: async ({ command, resource, action, args, flags }) => {
|
|
104
|
-
const ctx = await getContext(spec);
|
|
105
|
-
|
|
106
|
-
if (command === "list") {
|
|
107
|
-
return {
|
|
108
|
-
resources: ctx.commands.resources.map((r) => ({
|
|
109
|
-
name: r.resource,
|
|
110
|
-
actions: r.actions.map((a) => ({
|
|
111
|
-
name: a.action,
|
|
112
|
-
summary: a.summary,
|
|
113
|
-
method: a.method,
|
|
114
|
-
path: a.path,
|
|
115
|
-
args: a.positionals.map((p) => p.name),
|
|
116
|
-
requiredFlags: a.flags
|
|
117
|
-
.filter((f) => f.required)
|
|
118
|
-
.map((f) => f.flag),
|
|
119
|
-
})),
|
|
120
|
-
})),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (command === "help") {
|
|
125
|
-
if (!resource) return { error: "Missing resource" };
|
|
126
|
-
const r = ctx.commands.resources.find(
|
|
127
|
-
(r) => r.resource.toLowerCase() === resource.toLowerCase(),
|
|
128
|
-
);
|
|
129
|
-
if (!r) return { error: `Unknown resource: ${resource}` };
|
|
130
|
-
if (!action) {
|
|
131
|
-
return {
|
|
132
|
-
resource: r.resource,
|
|
133
|
-
actions: r.actions.map((a) => a.action),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const a = r.actions.find(
|
|
137
|
-
(a) => a.action.toLowerCase() === action.toLowerCase(),
|
|
138
|
-
);
|
|
139
|
-
if (!a) return { error: `Unknown action: ${action}` };
|
|
140
|
-
return {
|
|
141
|
-
action: a.action,
|
|
142
|
-
method: a.method,
|
|
143
|
-
path: a.path,
|
|
144
|
-
summary: a.summary,
|
|
145
|
-
args: a.positionals.map((p) => ({
|
|
146
|
-
name: p.name,
|
|
147
|
-
description: p.description,
|
|
148
|
-
})),
|
|
149
|
-
flags: a.flags.map((f) => ({
|
|
150
|
-
name: f.flag,
|
|
151
|
-
type: f.type,
|
|
152
|
-
required: f.required,
|
|
153
|
-
description: f.description,
|
|
154
|
-
})),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (command === "exec") {
|
|
159
|
-
if (!resource || !action)
|
|
160
|
-
return { error: "Missing resource or action" };
|
|
161
|
-
const actionDef = findAction(ctx, resource, action);
|
|
162
|
-
if (!actionDef) return { error: `Unknown: ${resource} ${action}` };
|
|
163
|
-
|
|
164
|
-
const positionalValues = args ?? [];
|
|
165
|
-
if (positionalValues.length < actionDef.positionals.length) {
|
|
166
|
-
return {
|
|
167
|
-
error: `Missing args: ${actionDef.positionals
|
|
168
|
-
.slice(positionalValues.length)
|
|
169
|
-
.map((p) => p.name)
|
|
170
|
-
.join(", ")}`,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const globals: RuntimeGlobals = {
|
|
175
|
-
server,
|
|
176
|
-
serverVar: serverVars
|
|
177
|
-
? Object.entries(serverVars).map(([k, v]) => `${k}=${v}`)
|
|
178
|
-
: undefined,
|
|
179
|
-
auth: authScheme,
|
|
180
|
-
bearerToken,
|
|
181
|
-
apiKey,
|
|
182
|
-
username: basicAuth?.username,
|
|
183
|
-
password: basicAuth?.password,
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const result = await execute({
|
|
188
|
-
specId: ctx.loaded.id,
|
|
189
|
-
action: actionDef,
|
|
190
|
-
positionalValues,
|
|
191
|
-
flagValues: flags ?? {},
|
|
192
|
-
globals,
|
|
193
|
-
servers: ctx.servers,
|
|
194
|
-
authSchemes: ctx.authSchemes,
|
|
195
|
-
});
|
|
196
|
-
return { status: result.status, ok: result.ok, body: result.body };
|
|
197
|
-
} catch (err) {
|
|
198
|
-
return { error: err instanceof Error ? err.message : String(err) };
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return { error: `Unknown command: ${command}` };
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Clear cached spec context */
|
|
208
|
-
export function clearSpecliCache(spec?: string): void {
|
|
209
|
-
if (spec) contextCache.delete(spec);
|
|
210
|
-
else contextCache.clear();
|
|
211
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { summarizeAuth } from "./auth-requirements.js";
|
|
4
|
-
import type { AuthScheme } from "./auth-schemes.js";
|
|
5
|
-
|
|
6
|
-
describe("summarizeAuth", () => {
|
|
7
|
-
test("uses operation-level security when present", () => {
|
|
8
|
-
const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
|
|
9
|
-
|
|
10
|
-
const summary = summarizeAuth(
|
|
11
|
-
[{ oauth: ["read:ping"] }],
|
|
12
|
-
[{ oauth: ["read:other"] }],
|
|
13
|
-
schemes,
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
expect(summary.alternatives).toEqual([
|
|
17
|
-
[{ key: "oauth", scopes: ["read:ping"] }],
|
|
18
|
-
]);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("empty operation security disables auth", () => {
|
|
22
|
-
const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
|
|
23
|
-
|
|
24
|
-
const summary = summarizeAuth([], [{ oauth: ["read:other"] }], schemes);
|
|
25
|
-
expect(summary.alternatives).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { AuthScheme } from "./auth-schemes.js";
|
|
2
|
-
import type { SecurityRequirement } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export type AuthRequirement = {
|
|
5
|
-
key: string;
|
|
6
|
-
scopes: string[];
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export type AuthSummary = {
|
|
10
|
-
// Alternatives: any one of these sets is sufficient.
|
|
11
|
-
alternatives: AuthRequirement[][];
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function isSecurityRequirement(value: unknown): value is SecurityRequirement {
|
|
15
|
-
if (!value || typeof value !== "object") return false;
|
|
16
|
-
if (Array.isArray(value)) return false;
|
|
17
|
-
|
|
18
|
-
for (const [k, v] of Object.entries(value)) {
|
|
19
|
-
if (typeof k !== "string") return false;
|
|
20
|
-
if (!Array.isArray(v)) return false;
|
|
21
|
-
if (!v.every((s) => typeof s === "string")) return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeSecurity(value: unknown): {
|
|
28
|
-
requirements: SecurityRequirement[];
|
|
29
|
-
source: "none" | "empty" | "non-empty";
|
|
30
|
-
} {
|
|
31
|
-
if (value == null) return { requirements: [], source: "none" };
|
|
32
|
-
if (!Array.isArray(value)) return { requirements: [], source: "none" };
|
|
33
|
-
|
|
34
|
-
const reqs = value.filter(isSecurityRequirement);
|
|
35
|
-
if (reqs.length === 0) return { requirements: [], source: "empty" };
|
|
36
|
-
return { requirements: reqs, source: "non-empty" };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function summarizeAuth(
|
|
40
|
-
operationSecurity: unknown,
|
|
41
|
-
globalSecurity: unknown,
|
|
42
|
-
knownSchemes: AuthScheme[],
|
|
43
|
-
): AuthSummary {
|
|
44
|
-
// Per spec:
|
|
45
|
-
// - operation security overrides root
|
|
46
|
-
// - empty array [] means "no auth"
|
|
47
|
-
const op = normalizeSecurity(operationSecurity);
|
|
48
|
-
if (op.source === "non-empty") {
|
|
49
|
-
return { alternatives: toAlternatives(op.requirements, knownSchemes) };
|
|
50
|
-
}
|
|
51
|
-
if (op.source === "empty") {
|
|
52
|
-
return { alternatives: [] };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const global = normalizeSecurity(globalSecurity);
|
|
56
|
-
if (global.source === "non-empty") {
|
|
57
|
-
return { alternatives: toAlternatives(global.requirements, knownSchemes) };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { alternatives: [] };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function toAlternatives(
|
|
64
|
-
requirements: SecurityRequirement[],
|
|
65
|
-
knownSchemes: AuthScheme[],
|
|
66
|
-
): AuthRequirement[][] {
|
|
67
|
-
const known = new Set(knownSchemes.map((s) => s.key));
|
|
68
|
-
|
|
69
|
-
return requirements.map((req) => {
|
|
70
|
-
const out: AuthRequirement[] = [];
|
|
71
|
-
for (const [key, scopes] of Object.entries(req)) {
|
|
72
|
-
out.push({
|
|
73
|
-
key,
|
|
74
|
-
scopes: Array.isArray(scopes) ? scopes : [],
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Stable order.
|
|
79
|
-
out.sort((a, b) => a.key.localeCompare(b.key));
|
|
80
|
-
|
|
81
|
-
// Prefer known schemes first.
|
|
82
|
-
out.sort((a, b) => {
|
|
83
|
-
const ak = known.has(a.key) ? 0 : 1;
|
|
84
|
-
const bk = known.has(b.key) ? 0 : 1;
|
|
85
|
-
if (ak !== bk) return ak - bk;
|
|
86
|
-
return a.key.localeCompare(b.key);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return out;
|
|
90
|
-
});
|
|
91
|
-
}
|