@tzurp/openapi-skills 1.2.5
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/LICENSE +201 -0
- package/README.md +230 -0
- package/bin/openapi-skills +4 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.js +1646 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-schema-builder.d.ts +35 -0
- package/dist/client-schema-builder.js +268 -0
- package/dist/client-schema-builder.js.map +1 -0
- package/dist/helper/base64url-json.d.ts +3 -0
- package/dist/helper/base64url-json.js +28 -0
- package/dist/helper/base64url-json.js.map +1 -0
- package/dist/helper/dotNotation.d.ts +3 -0
- package/dist/helper/dotNotation.js +34 -0
- package/dist/helper/dotNotation.js.map +1 -0
- package/dist/helper/endpoint-filter.d.ts +16 -0
- package/dist/helper/endpoint-filter.js +165 -0
- package/dist/helper/endpoint-filter.js.map +1 -0
- package/dist/helper/endpoint-utils.d.ts +2 -0
- package/dist/helper/endpoint-utils.js +10 -0
- package/dist/helper/endpoint-utils.js.map +1 -0
- package/dist/helper/error-codes.d.ts +27 -0
- package/dist/helper/error-codes.js +116 -0
- package/dist/helper/error-codes.js.map +1 -0
- package/dist/helper/error-formatter.d.ts +46 -0
- package/dist/helper/error-formatter.js +72 -0
- package/dist/helper/error-formatter.js.map +1 -0
- package/dist/helper/get-operation-filter.d.ts +17 -0
- package/dist/helper/get-operation-filter.js +26 -0
- package/dist/helper/get-operation-filter.js.map +1 -0
- package/dist/helper/graphql.d.ts +40 -0
- package/dist/helper/graphql.js +967 -0
- package/dist/helper/graphql.js.map +1 -0
- package/dist/helper/http-client.d.ts +28 -0
- package/dist/helper/http-client.js +90 -0
- package/dist/helper/http-client.js.map +1 -0
- package/dist/helper/json-updater.d.ts +17 -0
- package/dist/helper/json-updater.js +195 -0
- package/dist/helper/json-updater.js.map +1 -0
- package/dist/helper/logger.d.ts +13 -0
- package/dist/helper/logger.js +56 -0
- package/dist/helper/logger.js.map +1 -0
- package/dist/helper/output-size.d.ts +5 -0
- package/dist/helper/output-size.js +7 -0
- package/dist/helper/output-size.js.map +1 -0
- package/dist/helper/parameter-schema.d.ts +14 -0
- package/dist/helper/parameter-schema.js +36 -0
- package/dist/helper/parameter-schema.js.map +1 -0
- package/dist/helper/paths.d.ts +11 -0
- package/dist/helper/paths.js +47 -0
- package/dist/helper/paths.js.map +1 -0
- package/dist/helper/prompt-delete.d.ts +1 -0
- package/dist/helper/prompt-delete.js +13 -0
- package/dist/helper/prompt-delete.js.map +1 -0
- package/dist/helper/request-artifacts.d.ts +14 -0
- package/dist/helper/request-artifacts.js +60 -0
- package/dist/helper/request-artifacts.js.map +1 -0
- package/dist/helper/request-preparation.d.ts +23 -0
- package/dist/helper/request-preparation.js +85 -0
- package/dist/helper/request-preparation.js.map +1 -0
- package/dist/helper/sanitizer.d.ts +1 -0
- package/dist/helper/sanitizer.js +17 -0
- package/dist/helper/sanitizer.js.map +1 -0
- package/dist/helper/update-check.d.ts +1 -0
- package/dist/helper/update-check.js +36 -0
- package/dist/helper/update-check.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/install-skill.d.ts +5 -0
- package/dist/install-skill.js +139 -0
- package/dist/install-skill.js.map +1 -0
- package/dist/parser.d.ts +12 -0
- package/dist/parser.js +393 -0
- package/dist/parser.js.map +1 -0
- package/dist/validate-response.d.ts +47 -0
- package/dist/validate-response.js +1017 -0
- package/dist/validate-response.js.map +1 -0
- package/package.json +63 -0
- package/skill-templates/SKILL.md +204 -0
- package/skill-templates/create-endpoint-test.md +413 -0
- package/skill-templates/http-client.md +141 -0
- package/skill-templates/write-client-code.md +219 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1646 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { parseSchemaSource, validateSchema } from "./parser.js";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { getOpenapiToSkillsDir, getProjectRoot, getEndpointsPath, getOperationArtifactPath } from "./helper/paths.js";
|
|
7
|
+
import { ensureConfig, updateConfig, listApis, getConfigValue, deleteApi, loadConfig } from "./index.js";
|
|
8
|
+
import { buildClientCodeSchema } from "./client-schema-builder.js";
|
|
9
|
+
import { validateResponse, makeRequest, ensureResponseSchema, prepareRequestTemplate, collectRequestUpdateTypeWarnings, getSchemaType } from "./validate-response.js";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import { promptInstallLocation, installSkillBundle } from "./install-skill.js";
|
|
12
|
+
import { ensureEndpointSchemaFile } from "./parser.js";
|
|
13
|
+
import { logger, toErrorMessage } from "./helper/logger.js";
|
|
14
|
+
import { filterEndpoints, filterResolvedEndpoints, sliceEndpointsByIndex } from "./helper/endpoint-filter.js";
|
|
15
|
+
import { getSanitizedOperationId } from "./helper/endpoint-utils.js";
|
|
16
|
+
import { checkForUpdateOncePerTerminalSession } from "./helper/update-check.js";
|
|
17
|
+
import { getJsonOutputSize, MEDIUM_OUTPUT_MAX_BYTES } from "./helper/output-size.js";
|
|
18
|
+
import { promptDeleteConfirmation } from "./helper/prompt-delete.js";
|
|
19
|
+
import { collectRequestJsonPaths, collectUpdateRequestKeys, resolveSelectedArtifact } from "./helper/request-artifacts.js";
|
|
20
|
+
import { prepareMultiOperationRequests, getRequestResponseMetadata } from "./helper/request-preparation.js";
|
|
21
|
+
import { filterArray, getByPath } from "./helper/dotNotation.js";
|
|
22
|
+
import { parseGetOperationFilter } from "./helper/get-operation-filter.js";
|
|
23
|
+
import { decodeBase64UrlJsonObject } from "./helper/base64url-json.js";
|
|
24
|
+
import { buildError, buildSuccess } from "./helper/error-formatter.js";
|
|
25
|
+
import { ErrorCode } from "./helper/error-codes.js";
|
|
26
|
+
import { isTypeScriptUnavailableError, typescriptInstallCommand } from "./helper/graphql.js";
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const pkg = require("../package.json");
|
|
29
|
+
const url = pkg.repository.url;
|
|
30
|
+
await checkForUpdateOncePerTerminalSession(pkg.version);
|
|
31
|
+
const openapiToSkillsDir = getOpenapiToSkillsDir();
|
|
32
|
+
const program = new Command();
|
|
33
|
+
const silentFlagRequested = process.argv.includes("--silent");
|
|
34
|
+
program.name("openapi-skills")
|
|
35
|
+
.description("A command‑line tool for working with OpenAPI/Swagger and GraphQL specs. Use it to parse specs into artifacts, explore API endpoints and root fields, validate requests, generate typed client metadata, and teach AI agents how to operate the CLI and write API tests and client code.")
|
|
36
|
+
.version(pkg.version);
|
|
37
|
+
async function guardApiName(apiName) {
|
|
38
|
+
const apiNames = await listApis().catch(() => []);
|
|
39
|
+
if (apiNames.includes(apiName)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
logger.result(buildError(ErrorCode.UNKNOWN_API, {
|
|
43
|
+
summary: `API '${apiName}' is not installed.`,
|
|
44
|
+
message: `No API named '${apiName}' was found. Run 'openapi-skills get-api-names' to list installed APIs, or run 'openapi-skills generate <openapi-source>' first.`,
|
|
45
|
+
context: { api_name: apiName },
|
|
46
|
+
nextCommand: "openapi-skills get-api-names",
|
|
47
|
+
}));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const banner = `
|
|
52
|
+
\u001b[32m
|
|
53
|
+
▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄▄▄ ▄▄ ▄▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄▄
|
|
54
|
+
██▀██ ██▄█▀ ██▄▄ ███▄██ ██▀██ ██▄█▀ ██ ▄▄▄ ███▄▄ ██▄█▀ ██ ██ ██ ███▄▄
|
|
55
|
+
▀███▀ ██ ██▄▄▄ ██ ▀██ ██▀██ ██ ██ ▄▄██▀ ██ ██ ██ ██▄▄▄ ██▄▄▄ ▄▄██▀
|
|
56
|
+
\u001b[0m
|
|
57
|
+
`;
|
|
58
|
+
program.option("--silent", "Suppress the banner in help output for agent-friendly usage.");
|
|
59
|
+
if (!silentFlagRequested) {
|
|
60
|
+
program.addHelpText("before", banner);
|
|
61
|
+
}
|
|
62
|
+
program.addHelpText("before", `
|
|
63
|
+
===================
|
|
64
|
+
openapi-skills CLI
|
|
65
|
+
===================
|
|
66
|
+
`);
|
|
67
|
+
const startYear = 2026;
|
|
68
|
+
const currentYear = new Date().getFullYear();
|
|
69
|
+
const yearRange = `${startYear}-${currentYear}`;
|
|
70
|
+
program.addHelpText("after", `
|
|
71
|
+
-------------------------------------------------------------------------------
|
|
72
|
+
Copyright © ${yearRange} Tzur Paldi — Powered by Bedekbyte® All rights reserved
|
|
73
|
+
Support: tzur.paldi@outlook.com
|
|
74
|
+
Project: ${url}
|
|
75
|
+
-------------------------------------------------------------------------------
|
|
76
|
+
`);
|
|
77
|
+
program
|
|
78
|
+
.command("install")
|
|
79
|
+
.description("Install SKILL.md and scenario markdowns for agent frameworks like Claude and GitHub Copilot. Supports: --skills [--global].")
|
|
80
|
+
.option("--skills", "Install SKILL.md files for agent frameworks.")
|
|
81
|
+
.option("--global", "Install the skill bundle under the user's home directory instead of the current project.")
|
|
82
|
+
.action(async (options) => {
|
|
83
|
+
if (options.global && !options.skills) {
|
|
84
|
+
logger.result(buildError(ErrorCode.CONFIG_ERROR, {
|
|
85
|
+
summary: "--global requires --skills.",
|
|
86
|
+
message: "Use `openapi-skills install --skills --global` to install the skill bundle under your home directory.",
|
|
87
|
+
context: {},
|
|
88
|
+
nextCommand: "openapi-skills install --skills --global",
|
|
89
|
+
}));
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!options.skills) {
|
|
94
|
+
await ensureConfig();
|
|
95
|
+
logger.info(".openapi-skills directory and configuration installed. Use --skills to install SKILL.md files for agent frameworks.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const rootDir = options.global ? os.homedir() : getProjectRoot();
|
|
99
|
+
const srcDir = getProjectRoot();
|
|
100
|
+
const defaultInstallPath = path.join(rootDir, "installed-skills");
|
|
101
|
+
logger.info(`Source directory (copied): ${srcDir}`);
|
|
102
|
+
const installPath = await promptInstallLocation(rootDir, defaultInstallPath);
|
|
103
|
+
logger.info(`Target install directory: ${installPath}`);
|
|
104
|
+
try {
|
|
105
|
+
const result = await installSkillBundle(srcDir, installPath);
|
|
106
|
+
logger.info('openapi-skills skill bundle installed');
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
logger.result(buildError(ErrorCode.CONFIG_ERROR, {
|
|
110
|
+
summary: "Failed to install skill bundle.",
|
|
111
|
+
message: err instanceof Error ? err.message : String(err),
|
|
112
|
+
context: { install_path: installPath },
|
|
113
|
+
nextCommand: "openapi-skills install --skills",
|
|
114
|
+
}));
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
const generateCmd = program
|
|
119
|
+
.command("generate [openapi-source]")
|
|
120
|
+
.description("Parse an OpenAPI or GraphQL source (file path or URL) and generate endpoints.json, schemas/, and config.json. Run this command first for a new spec. Supports: --validate, --base-url, --dereference, --no-progress.")
|
|
121
|
+
.option("--validate <schema>", "Validate an OpenAPI or GraphQL schema (file path or URL) and exit")
|
|
122
|
+
.option("--base-url <url>", "Base URL for the API")
|
|
123
|
+
.option("--dereference", "Fully dereference the entire OpenAPI document before processing endpoints")
|
|
124
|
+
.option("--no-progress", "Disable the progress indicator")
|
|
125
|
+
.option("--rename <newName>", "Optional new name for the generated schema.")
|
|
126
|
+
.action(async (openapiSource, options) => {
|
|
127
|
+
try {
|
|
128
|
+
if (typeof options.validate === "string" && options.validate.length > 0) {
|
|
129
|
+
try {
|
|
130
|
+
await validateSchema(options.validate);
|
|
131
|
+
logger.result(buildSuccess({
|
|
132
|
+
valid: true,
|
|
133
|
+
schemaSource: options.validate,
|
|
134
|
+
message: "Schema is valid",
|
|
135
|
+
}, { kind: "schema-validation" }));
|
|
136
|
+
process.exitCode = 0;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.result(buildError(ErrorCode.SCHEMA_VALIDATION_FAILED, {
|
|
140
|
+
summary: "OpenAPI or GraphQL schema is invalid.",
|
|
141
|
+
message: `Schema validation failed: ${toErrorMessage(error)}`,
|
|
142
|
+
context: { schema_source: options.validate },
|
|
143
|
+
nextCommand: `openapi-skills generate ${options.validate}`,
|
|
144
|
+
}));
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!openapiSource) {
|
|
150
|
+
throw new Error("An OpenAPI source is required unless --validate is used.");
|
|
151
|
+
}
|
|
152
|
+
const providedBaseUrl = typeof options.baseUrl === "string" ? options.baseUrl.trim() : "";
|
|
153
|
+
const baseUrlProvided = providedBaseUrl.length > 0;
|
|
154
|
+
const baseUrl = baseUrlProvided ? providedBaseUrl : "";
|
|
155
|
+
const apiName = await parseSchemaSource(openapiSource, baseUrl, { dereference: options.dereference === true, progress: options.progress !== false, rename: options.rename });
|
|
156
|
+
if (!baseUrlProvided) {
|
|
157
|
+
const warningMessage = [
|
|
158
|
+
`You haven't included --base-url for API "${apiName}".`,
|
|
159
|
+
`If you need to make requests and validations, add baseUrl directly to .openapi-skills/config.json under apis.${apiName}.baseUrl.`,
|
|
160
|
+
`Example:`,
|
|
161
|
+
`{
|
|
162
|
+
"apis": {
|
|
163
|
+
"${apiName}": {
|
|
164
|
+
"baseUrl": "https://petstore3.swagger.io/api/v3",
|
|
165
|
+
"openapi-sorce": "https://petstore3.swagger.io/api/v3/openapi.json"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}`,
|
|
169
|
+
].join("\n");
|
|
170
|
+
logger.warn(warningMessage);
|
|
171
|
+
logger.result(buildSuccess({
|
|
172
|
+
apiName,
|
|
173
|
+
openapiSource,
|
|
174
|
+
warning: warningMessage,
|
|
175
|
+
}, { kind: "generate-warning" }));
|
|
176
|
+
}
|
|
177
|
+
logger.result(buildSuccess({
|
|
178
|
+
apiName,
|
|
179
|
+
openapiSource,
|
|
180
|
+
fileCount: 3,
|
|
181
|
+
}, { kind: "generate-result" }));
|
|
182
|
+
process.exitCode = 0;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (isTypeScriptUnavailableError(error)) {
|
|
186
|
+
logger.result(buildError(ErrorCode.API_PARSE_ERROR, {
|
|
187
|
+
summary: "TypeScript is required to analyze builder GraphQL schemas.",
|
|
188
|
+
message: `TypeScript is not available in this environment. Install it with \`${typescriptInstallCommand}\` and rerun the command.`,
|
|
189
|
+
context: { openapi_source: openapiSource },
|
|
190
|
+
nextCommand: typescriptInstallCommand,
|
|
191
|
+
reason: "GraphQL builder parsing requires TypeScript to be installed in the current workspace.",
|
|
192
|
+
}));
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
logger.result(buildError(options.validate ? ErrorCode.SCHEMA_VALIDATION_FAILED : ErrorCode.API_PARSE_ERROR, {
|
|
197
|
+
summary: options.validate ? "OpenAPI or GraphQL schema is invalid." : "Failed to parse the provided schema.",
|
|
198
|
+
message: toErrorMessage(error),
|
|
199
|
+
context: { openapi_source: openapiSource },
|
|
200
|
+
nextCommand: options.validate ? `openapi-skills generate ${options.validate}` : `openapi-skills generate ${openapiSource ?? "<openapi-source>"}`,
|
|
201
|
+
}));
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
generateCmd.agentMeta = {
|
|
206
|
+
name: "generate",
|
|
207
|
+
category: "Generation",
|
|
208
|
+
usage: "openapi-skills generate [openapi-source] [--validate <schema>] [--base-url <url>] [--dereference] [--no-progress] [--rename <newName>]",
|
|
209
|
+
description: "Parse a source file or URL and generate the API artifacts used by the rest of the CLI. Use --validate to check a schema and exit, --base-url to set the API base URL, --dereference when full dereferencing is appropriate, --rename to override the generated API name, and --no-progress to suppress progress output.",
|
|
210
|
+
arguments: [
|
|
211
|
+
{ name: "openapi-source", type: "path|url", required: false, positional: true, description: "OpenAPI source file path or URL." },
|
|
212
|
+
{ name: "validate", type: "path|url", required: false, flag: true, description: "Validate an OpenAPI schema and exit." },
|
|
213
|
+
{ name: "base-url", type: "string", required: false, flag: true, description: "Base URL for the generated API." },
|
|
214
|
+
{ name: "dereference", type: "boolean", required: false, flag: true, description: "Fully dereference the OpenAPI document before processing endpoints." },
|
|
215
|
+
{ name: "no-progress", type: "boolean", required: false, flag: true, description: "Disable the progress indicator." },
|
|
216
|
+
{ name: "rename", type: "string", required: false, flag: true, description: "Optional new name for the generated schema." }
|
|
217
|
+
],
|
|
218
|
+
examples: [
|
|
219
|
+
"openapi-skills generate ./petstore.yaml --base-url https://api.example.com",
|
|
220
|
+
"openapi-skills generate https://example.com/openapi.yaml",
|
|
221
|
+
"openapi-skills generate ./petstore.yaml --validate ./petstore.yaml",
|
|
222
|
+
"openapi-skills generate ./petstore.yaml --dereference",
|
|
223
|
+
"openapi-skills generate ./petstore.yaml --rename petstore-v2",
|
|
224
|
+
"openapi-skills generate ./petstore.yaml --no-progress"
|
|
225
|
+
],
|
|
226
|
+
returns: {
|
|
227
|
+
type: "json",
|
|
228
|
+
description: "Prints output directory and config.json path."
|
|
229
|
+
},
|
|
230
|
+
sideEffects: {
|
|
231
|
+
writesFiles: true,
|
|
232
|
+
readsFiles: true,
|
|
233
|
+
network: true
|
|
234
|
+
},
|
|
235
|
+
constraints: {
|
|
236
|
+
destructive: false,
|
|
237
|
+
idempotent: false,
|
|
238
|
+
requiresParsedApi: false
|
|
239
|
+
},
|
|
240
|
+
filesWritten: ["endpoints.json", "schemas/", "config.json"]
|
|
241
|
+
};
|
|
242
|
+
const listCmd = program
|
|
243
|
+
.command("list")
|
|
244
|
+
.description("List summarized operation objects for the specified API as JSON. Supports --filter, --tag, --resolved/--dereferenced, --index slicing, and --count for operation totals. Also supports OpenAPI‑only options (--path, --method) and the GraphQL‑only option (--root-type). At least one filter is required unless --index : is used intentionally. GraphQL APIs reject --method, --path, and --tag, and OpenAPI APIs reject --root-type.")
|
|
245
|
+
.requiredOption("--api <apiName>", "API name to use")
|
|
246
|
+
.option("--count", "Return the number of operations after applying any list filters and index slicing. With no filters, returns the total operation count.")
|
|
247
|
+
.option("--resolved, --dereferenced", "Show only operations that already have generated schema details saved.")
|
|
248
|
+
.option("--path <path>", [
|
|
249
|
+
"Filter operations by path structure. Repeatable; multiple values are ANDed.",
|
|
250
|
+
"Supports:",
|
|
251
|
+
" - Path prefix: /users (matches operations whose path begins with /users)",
|
|
252
|
+
" - Parameter detection: :param (matches operations with any {param} in the path)",
|
|
253
|
+
" - Segment matching: 'store order' (matches operations whose path contains BOTH 'store' AND 'order' as segments, in any order)",
|
|
254
|
+
" - OR within a clause: 'store|shop' (matches operations whose path contains EITHER 'store' OR 'shop')",
|
|
255
|
+
" - Combined AND+OR: 'store order|shop item' (matches endpoints with both 'store' and 'order', OR both 'shop' and 'item')",
|
|
256
|
+
" - Multiple --path flags are ANDed: --path /store --path order (matches endpoints starting with /store AND containing 'order')",
|
|
257
|
+
"",
|
|
258
|
+
"Examples:",
|
|
259
|
+
" --path /users",
|
|
260
|
+
" --path :param",
|
|
261
|
+
" --path 'store order'",
|
|
262
|
+
" --path 'store|shop'",
|
|
263
|
+
" --path /store --path order"
|
|
264
|
+
].join("\n"), (value, previous) => {
|
|
265
|
+
const values = Array.isArray(previous) ? previous : previous ? [previous] : [];
|
|
266
|
+
return [...values, value];
|
|
267
|
+
})
|
|
268
|
+
.option("--filter <filterPattern>", "Filter operations by any of their searchable properties. Supports AND/OR (use spaces for AND, | for OR), e.g. --filter 'create account|register user'.")
|
|
269
|
+
.option("--tag <tag>", "Filter OpenAPI operations by an exact tag, case-insensitive.")
|
|
270
|
+
.option("--method <method>", "Filter operations by HTTP method (GET, POST, etc). Use this for OpenAPI operations.")
|
|
271
|
+
.option("--root-type <rootType>", "Filter GraphQL root fields by root type (query, mutation, or subscription).")
|
|
272
|
+
.option("--index <range>", "Slice the filtered results with inclusive Python-like range syntax, e.g. 0:10, 5:, :10, -1, or :")
|
|
273
|
+
.action(async (options) => {
|
|
274
|
+
const apiName = options.api;
|
|
275
|
+
if (!(await guardApiName(apiName))) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const endpointsPath = getEndpointsPath(apiName);
|
|
280
|
+
const schemaType = await getSchemaType(apiName);
|
|
281
|
+
const resolveRequested = options.resolved === true || options.dereferenced === true;
|
|
282
|
+
const hasFilter = Boolean(options.path || options.filter || options.tag || options.method || options.rootType || options.index || resolveRequested);
|
|
283
|
+
const filterOpts = {};
|
|
284
|
+
if (typeof options.path === "string" || Array.isArray(options.path))
|
|
285
|
+
filterOpts.path = options.path;
|
|
286
|
+
if (typeof options.filter === "string")
|
|
287
|
+
filterOpts.filter = options.filter;
|
|
288
|
+
if (typeof options.tag === "string")
|
|
289
|
+
filterOpts.tag = options.tag;
|
|
290
|
+
if (typeof options.method === "string")
|
|
291
|
+
filterOpts.method = options.method;
|
|
292
|
+
if (typeof options.rootType === "string")
|
|
293
|
+
filterOpts.rootType = options.rootType;
|
|
294
|
+
if (schemaType === "graphql" && (typeof options.method === "string" || typeof options.path === "string" || typeof options.tag === "string")) {
|
|
295
|
+
logger.result(buildError(ErrorCode.SCHEMA_TYPE_MISMATCH, {
|
|
296
|
+
summary: "--method, --path, and --tag are only valid for OpenAPI APIs.",
|
|
297
|
+
message: "This API is GraphQL. Use --root-type with --filter instead.",
|
|
298
|
+
context: { api_name: apiName, schema_type: schemaType },
|
|
299
|
+
nextCommand: `openapi-skills list --api ${apiName} --root-type query`,
|
|
300
|
+
}));
|
|
301
|
+
process.exitCode = 2;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (schemaType === "openapi" && typeof options.rootType === "string") {
|
|
305
|
+
logger.result(buildError(ErrorCode.SCHEMA_TYPE_MISMATCH, {
|
|
306
|
+
summary: "--root-type is only valid for GraphQL APIs.",
|
|
307
|
+
message: "This API is OpenAPI. Use --method and --path or --filter instead.",
|
|
308
|
+
context: { api_name: apiName, schema_type: schemaType },
|
|
309
|
+
nextCommand: `openapi-skills list --api ${apiName} --method GET`,
|
|
310
|
+
}));
|
|
311
|
+
process.exitCode = 2;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!options.count && !hasFilter) {
|
|
315
|
+
logger.result(buildError(ErrorCode.MISSING_FILTER_ARGUMENT, {
|
|
316
|
+
summary: "No filter provided. Use --path, --filter, --tag, --method, --root-type, or --index.",
|
|
317
|
+
message: "The list command requires at least one filter to avoid returning large, unbounded result sets.",
|
|
318
|
+
context: { api_name: apiName },
|
|
319
|
+
nextCommand: `openapi-skills list --api ${apiName} --index :`,
|
|
320
|
+
severity: "warning",
|
|
321
|
+
}));
|
|
322
|
+
process.exitCode = 0;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (fs.existsSync(endpointsPath) === false) {
|
|
326
|
+
if (options.count) {
|
|
327
|
+
logger.result(buildSuccess({
|
|
328
|
+
count: 0,
|
|
329
|
+
message: `No endpoints.json found for API "${apiName}".`,
|
|
330
|
+
}, { kind: "endpoint-count" }));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
logger.result(buildError(ErrorCode.NO_ENDPOINTS_FOUND, {
|
|
334
|
+
summary: `No endpoints.json found for API "${apiName}".`,
|
|
335
|
+
message: `The API has not been parsed yet. Run 'openapi-skills generate' to create it.`,
|
|
336
|
+
context: { api_name: apiName },
|
|
337
|
+
nextCommand: `openapi-skills generate <openapi-source> --rename ${apiName}`,
|
|
338
|
+
reason: "The API artifacts have not been generated.",
|
|
339
|
+
}));
|
|
340
|
+
logger.warn(`No endpoints found for "${apiName}". This API name may be misspelled or not generated yet. (openapi-skills generate <openapi-source> [options])`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const endpoints = await fs.readJson(endpointsPath);
|
|
344
|
+
let filtered = filterEndpoints(endpoints, filterOpts);
|
|
345
|
+
if (resolveRequested) {
|
|
346
|
+
filtered = await filterResolvedEndpoints(apiName, filtered);
|
|
347
|
+
}
|
|
348
|
+
filtered = sliceEndpointsByIndex(filtered, options.index);
|
|
349
|
+
const outputEndpoints = filtered.map(({ tags, ...endpoint }) => endpoint);
|
|
350
|
+
if (options.count) {
|
|
351
|
+
logger.result(buildSuccess({
|
|
352
|
+
count: filtered.length,
|
|
353
|
+
apiName,
|
|
354
|
+
}, { kind: "endpoint-count" }));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (filtered.length === 0) {
|
|
358
|
+
logger.result(buildSuccess({
|
|
359
|
+
apiName,
|
|
360
|
+
items: [],
|
|
361
|
+
}, { kind: "endpoint-list" }));
|
|
362
|
+
if (options.path || options.filter || options.tag || options.method || options.rootType || options.index || resolveRequested) {
|
|
363
|
+
const pathValue = Array.isArray(options.path) ? options.path.join(", ") : options.path;
|
|
364
|
+
const pathMsg = pathValue ? `path "${pathValue}"` : "";
|
|
365
|
+
const filterMsg = options.filter ? `filter \"${options.filter}\"` : "";
|
|
366
|
+
const tagMsg = options.tag ? `tag \"${options.tag}\"` : "";
|
|
367
|
+
const methodMsg = options.method ? `method \"${options.method}\"` : "";
|
|
368
|
+
const rootTypeMsg = options.rootType ? `rootType \"${options.rootType}\"` : "";
|
|
369
|
+
const resolveMsg = resolveRequested ? `resolve` : "";
|
|
370
|
+
const indexMsg = options.index ? `index \"${options.index}\"` : "";
|
|
371
|
+
const msg = [pathMsg, filterMsg, tagMsg, methodMsg, rootTypeMsg, resolveMsg, indexMsg].filter(Boolean).join(", ");
|
|
372
|
+
logger.warn(`No endpoints matched the ${msg}.`);
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
logger.result(buildSuccess({
|
|
377
|
+
apiName,
|
|
378
|
+
items: outputEndpoints,
|
|
379
|
+
}, { kind: "endpoint-list" }));
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
logger.result(buildError(ErrorCode.API_PARSE_ERROR, {
|
|
383
|
+
summary: "Error listing endpoints.",
|
|
384
|
+
message: toErrorMessage(error),
|
|
385
|
+
context: { api_name: apiName },
|
|
386
|
+
}));
|
|
387
|
+
process.exitCode = 1;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
listCmd.agentMeta = {
|
|
391
|
+
name: "list",
|
|
392
|
+
category: "Navigation",
|
|
393
|
+
usage: "openapi-skills list --api <apiName> [--count] [--resolved|--dereferenced] [--path <path>]... [--filter <pattern>] [--tag <tag>] [--method <method>] [--root-type <rootType>] [--index <range>]",
|
|
394
|
+
description: [
|
|
395
|
+
"List operation summaries for a parsed API as JSON.",
|
|
396
|
+
"At least one filter is required. --index is treated as a filter input, so the command can run when only a slice is requested.",
|
|
397
|
+
"Use --count to return the number of operations after filtering and slicing, or --resolved to show only operations that already have generated schema details saved.",
|
|
398
|
+
"Filter usage differs by schema type: use --method, --path, and --tag for OpenAPI operations, use --root-type for GraphQL root fields, and use --filter and --index with either schema. Filtering is case-insensitive and supports:",
|
|
399
|
+
"- Path prefix: --path '/users' (matches operations whose path begins with the prefix)",
|
|
400
|
+
"- Parameter detection: --path :param (matches operations that contain at least one '{...}' path placeholder)",
|
|
401
|
+
"- Segment matching: --path 'store order' (matches endpoints whose path contains both segments)",
|
|
402
|
+
"- OR within a single path clause: --path 'store|shop' (matches either segment)",
|
|
403
|
+
"- Multiple path flags are ANDed: --path /store --path order",
|
|
404
|
+
"- Exact tag matching: --tag pet (matches OpenAPI operations whose tag is exactly 'pet', ignoring case)",
|
|
405
|
+
"- Simple substring filtering: --filter 'user account' (matches operations containing both 'user' and 'account' in any field, including GraphQL name/rootType fields)",
|
|
406
|
+
"- OR filtering: --filter 'create|register|signup' (matches any operation containing any of the words)",
|
|
407
|
+
"- Combined AND+OR: --filter 'create account|register user' (matches operations containing both 'create' and 'account', OR both 'register' and 'user')",
|
|
408
|
+
"- Path substring search: --filter '/users' (matches operations whose path contains the substring)",
|
|
409
|
+
"- OperationId: --filter 'getUser' (matches operationId field)",
|
|
410
|
+
"- Summary/description: --filter 'delete permanently'",
|
|
411
|
+
"- Method filtering: --method GET (OpenAPI only; can be combined with --path and/or --filter)",
|
|
412
|
+
"- GraphQL root type filtering: --root-type query (query, mutation, or subscription)",
|
|
413
|
+
"- Index slicing: --index 0:10, --index 5:, --index :10, --index -1, --index :",
|
|
414
|
+
" Slices the filtered results using inclusive Python-like range syntax:",
|
|
415
|
+
" N = only the Nth item (0-based, negative counts from end)",
|
|
416
|
+
" start:end = all items from start to end, inclusive",
|
|
417
|
+
" start: = from start to last",
|
|
418
|
+
" :end = from first to end",
|
|
419
|
+
" : = all items",
|
|
420
|
+
" Negative indices count from the end (e.g., -1 is the last item, -2 is second-to-last).",
|
|
421
|
+
" Examples:",
|
|
422
|
+
" --index 0:2 (first three items)",
|
|
423
|
+
" --index 5: (from 6th to last)",
|
|
424
|
+
" --index :3 (first four items)",
|
|
425
|
+
" --index -1 (last item)",
|
|
426
|
+
" --index : (all items)",
|
|
427
|
+
" --count (return the filtered/sliced endpoint count, or the total count when no filters are used)",
|
|
428
|
+
"\n",
|
|
429
|
+
"Use --path for path-aware matching. Use --filter for substring matching across path, operationId/name, rootType, summary, or description.",
|
|
430
|
+
"If no operations match, outputs [] and prints a message to stderr."
|
|
431
|
+
].join("\n"),
|
|
432
|
+
arguments: [
|
|
433
|
+
{ name: "api", type: "string", required: true, flag: true, description: "API name to use." },
|
|
434
|
+
{ name: "count", type: "flag", required: false, flag: true, description: "Return the number of operations after filtering and slicing." },
|
|
435
|
+
{ name: "resolved", type: "flag", required: false, flag: true, description: "Show only operations that already have generated schema details saved. Alias: --dereferenced." },
|
|
436
|
+
{ name: "path", type: "string[]", required: false, flag: true, description: "Filter operations by path structure." },
|
|
437
|
+
{ name: "filter", type: "string", required: false, flag: true, description: "Filter operations by keywords, operationId/name, rootType, path, summary, or description." },
|
|
438
|
+
{ name: "tag", type: "string", required: false, flag: true, description: "Filter OpenAPI operations by an exact tag, case-insensitive." },
|
|
439
|
+
{ name: "method", type: "string", required: false, flag: true, description: "Filter OpenAPI operations by HTTP method." },
|
|
440
|
+
{ name: "rootType", type: "string", required: false, flag: true, description: "Filter GraphQL operations by root type (query, mutation, or subscription)." },
|
|
441
|
+
{ name: "index", type: "string", required: false, flag: true, description: "Slice the filtered results with inclusive Python-like range syntax." }
|
|
442
|
+
],
|
|
443
|
+
examples: [
|
|
444
|
+
"openapi-skills list --api petstore",
|
|
445
|
+
"openapi-skills list --api petstore --count",
|
|
446
|
+
"openapi-skills list --api petstore --resolved",
|
|
447
|
+
"openapi-skills list --api petstore --tag pet",
|
|
448
|
+
"openapi-skills list --api petstore --path /users",
|
|
449
|
+
"openapi-skills list --api petstore --path /store --path order",
|
|
450
|
+
"openapi-skills list --api petstore --path 'store order'",
|
|
451
|
+
"openapi-skills list --api petstore --path :param",
|
|
452
|
+
"openapi-skills list --api petstore --filter addpet",
|
|
453
|
+
"openapi-skills list --api petstore --filter 'create|register' --method POST",
|
|
454
|
+
"openapi-skills list --api countries --root-type query",
|
|
455
|
+
"openapi-skills list --api petstore --resolved --count",
|
|
456
|
+
"openapi-skills list --api petstore --filter '/users'",
|
|
457
|
+
"openapi-skills list --api petstore --index 0:10"
|
|
458
|
+
],
|
|
459
|
+
returns: {
|
|
460
|
+
type: "json",
|
|
461
|
+
description: "Returns filtered operation summaries as JSON, or a count object when --count is used. When no filters are provided, returns a structured warning object."
|
|
462
|
+
},
|
|
463
|
+
sideEffects: {
|
|
464
|
+
writesFiles: false,
|
|
465
|
+
readsFiles: true,
|
|
466
|
+
network: false
|
|
467
|
+
},
|
|
468
|
+
constraints: {
|
|
469
|
+
destructive: false,
|
|
470
|
+
idempotent: true,
|
|
471
|
+
requiresParsedApi: true
|
|
472
|
+
},
|
|
473
|
+
filesWritten: []
|
|
474
|
+
};
|
|
475
|
+
const genClientSchemaCmd = program
|
|
476
|
+
.command("generate-client-schema <operationId>")
|
|
477
|
+
.requiredOption("--api <apiName>", "API name to use")
|
|
478
|
+
.option("--force", "Overwrite the cached <operationId> artifact file before generating metadata.")
|
|
479
|
+
.description("Return structured metadata for client code generation for a specific operation. Use --force to overwrite the cached <operationId> artifact file before reading it.")
|
|
480
|
+
.action(async (operationId, options) => {
|
|
481
|
+
const apiName = options.api;
|
|
482
|
+
if (!(await guardApiName(apiName))) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
|
|
487
|
+
const schema = await buildClientCodeSchema(apiName, operationId, sanitizedOperationId, options.force === true);
|
|
488
|
+
logger.result(buildSuccess(schema, { kind: "client-schema" }));
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
492
|
+
logger.result(buildError(ErrorCode.API_PARSE_ERROR, {
|
|
493
|
+
summary: "Error generating client code metadata.",
|
|
494
|
+
message,
|
|
495
|
+
context: { api_name: apiName, operation_id: operationId },
|
|
496
|
+
}));
|
|
497
|
+
process.exitCode = 1;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
genClientSchemaCmd.agentMeta = {
|
|
501
|
+
name: "generate-client-schema",
|
|
502
|
+
category: "Code Generation",
|
|
503
|
+
usage: "openapi-skills generate-client-schema <operationId> --api <apiName> [--force]",
|
|
504
|
+
description: "Print structured metadata optimized for client code generation as pretty-printed JSON. Returns `response: null` for operations with no JSON response body, in which case the generated client should return `Promise<void>`. The cached artifact is reused unless --force is provided.",
|
|
505
|
+
arguments: [
|
|
506
|
+
{ name: "operationId", type: "string", required: true, positional: true, description: "The operationId of the operation to inspect." },
|
|
507
|
+
{ name: "api", type: "string", required: true, flag: true, description: "The API name as defined in .openapi-skills/config.json." },
|
|
508
|
+
{ name: "force", type: "flag", required: false, flag: true, description: "Overwrite the cached artifact file before generating metadata." }
|
|
509
|
+
],
|
|
510
|
+
examples: [
|
|
511
|
+
"openapi-skills generate-client-schema addPet --api petstore",
|
|
512
|
+
"openapi-skills generate-client-schema addPet --api petstore --force"
|
|
513
|
+
],
|
|
514
|
+
returns: {
|
|
515
|
+
type: "json",
|
|
516
|
+
description: "Returns metadata as pretty-printed JSON."
|
|
517
|
+
},
|
|
518
|
+
sideEffects: {
|
|
519
|
+
writesFiles: false,
|
|
520
|
+
readsFiles: true,
|
|
521
|
+
network: false
|
|
522
|
+
},
|
|
523
|
+
constraints: {
|
|
524
|
+
destructive: false,
|
|
525
|
+
idempotent: true,
|
|
526
|
+
requiresParsedApi: true
|
|
527
|
+
},
|
|
528
|
+
filesWritten: []
|
|
529
|
+
};
|
|
530
|
+
const describeCmd = program
|
|
531
|
+
.command("describe <operationId>")
|
|
532
|
+
.requiredOption("--api <apiName>", "API name to use")
|
|
533
|
+
.option("--force", "Overwrite the cached <operationId> artifact file before printing the raw schema.")
|
|
534
|
+
.description("describe → fallback for generate-client-schema. Use generate-client-schema first. Prints the complete raw schema for a specific operation as JSON, including all parameters, request body, and all response codes. The cached <operationId> artifact file is reused unless --force is provided, which overwrites the cached schema from the bundled API document before output.")
|
|
535
|
+
.action(async (operationId, options) => {
|
|
536
|
+
const apiName = options.api;
|
|
537
|
+
if (!(await guardApiName(apiName))) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
|
|
542
|
+
const schema = await ensureEndpointSchemaFile(apiName, operationId, sanitizedOperationId, options.force === true);
|
|
543
|
+
logger.result(buildSuccess({
|
|
544
|
+
operationId,
|
|
545
|
+
warnings: ["describe → fallback for generate-client-schema. Use openapi-skills generate-client-schema first for client code generation."],
|
|
546
|
+
schema,
|
|
547
|
+
}, { kind: "describe-result" }));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
552
|
+
logger.result(buildError(ErrorCode.API_PARSE_ERROR, {
|
|
553
|
+
summary: "Error describing operation.",
|
|
554
|
+
message,
|
|
555
|
+
context: { api_name: apiName, operation_id: operationId },
|
|
556
|
+
}));
|
|
557
|
+
process.exitCode = 1;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
describeCmd.agentMeta = {
|
|
562
|
+
name: "describe",
|
|
563
|
+
category: "Navigation",
|
|
564
|
+
usage: "openapi-skills describe <operationId> --api <apiName> [--force]",
|
|
565
|
+
description: [
|
|
566
|
+
"Fallback for generate-client-schema. Prints the complete raw schema for a specific operation as JSON, including all parameters, request body, and response codes. The cached artifact is reused unless --force is provided.",
|
|
567
|
+
"",
|
|
568
|
+
"**Agents MUST use `generate-client-schema` for client code generation. Use `describe` only when you need full raw schema detail beyond what `generate-client-schema` provides.**"
|
|
569
|
+
].join("\n"),
|
|
570
|
+
arguments: [
|
|
571
|
+
{ name: "operationId", type: "string", required: true, positional: true, description: "The operationId of the endpoint to describe." },
|
|
572
|
+
{ name: "api", type: "string", required: true, flag: true, description: "The API name as defined in .openapi-skills/config.json." },
|
|
573
|
+
{ name: "force", type: "flag", required: false, flag: true, description: "Overwrite the cached schema file before printing the raw schema." }
|
|
574
|
+
],
|
|
575
|
+
examples: [
|
|
576
|
+
"openapi-skills describe getPetById --api petstore",
|
|
577
|
+
"openapi-skills describe deletePet --api petstore",
|
|
578
|
+
"openapi-skills describe getPetById --api petstore --force"
|
|
579
|
+
],
|
|
580
|
+
returns: {
|
|
581
|
+
type: "json",
|
|
582
|
+
description: "Structured JSON with a fallback warning and the raw OpenAPI schema nested under `schema`."
|
|
583
|
+
},
|
|
584
|
+
sideEffects: {
|
|
585
|
+
writesFiles: false,
|
|
586
|
+
readsFiles: true,
|
|
587
|
+
network: false
|
|
588
|
+
},
|
|
589
|
+
constraints: {
|
|
590
|
+
destructive: false,
|
|
591
|
+
idempotent: true,
|
|
592
|
+
requiresParsedApi: true
|
|
593
|
+
},
|
|
594
|
+
filesWritten: []
|
|
595
|
+
};
|
|
596
|
+
const getOperationCmd = program
|
|
597
|
+
.command("get-operation <operationId>")
|
|
598
|
+
.alias("get-operation-artifact")
|
|
599
|
+
.requiredOption("--api <apiName>", "API name to use")
|
|
600
|
+
.option("--request", "Return request artifact for the operation.")
|
|
601
|
+
.option("--response", "Return response artifact for the operation.")
|
|
602
|
+
.option("--response-schema", "Return response-schema artifact for the operation. Inspect this first when the response shape is unknown.")
|
|
603
|
+
.option("--get <path>", "Select a nested value by dot-notation path first. Inspect --response-schema first so the path matches the actual response shape.")
|
|
604
|
+
.option("--filter <expr>", "Filter the selected value using count, index, range, or <path>=<value> syntax (e.g. count, 0, 0:10, status=sold). Select path first with --get, then filter or slice the selected value.")
|
|
605
|
+
.description("Return one stored operation artifact as JSON. Run `request` first for the same operationId so the artifact exists. Exactly one of --request, --response, or --response-schema is required. Select path first with --get, then apply --filter to the selected value. Use --response-schema first when the response shape is unknown.")
|
|
606
|
+
.action(async (operationId, options) => {
|
|
607
|
+
const apiName = options.api;
|
|
608
|
+
if (!(await guardApiName(apiName))) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const selection = resolveSelectedArtifact(options);
|
|
612
|
+
const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
|
|
613
|
+
if (selection.error || !selection.artifactName) {
|
|
614
|
+
logger.result(buildError(ErrorCode.INVALID_REQUEST_UPDATE, {
|
|
615
|
+
summary: "Invalid operation artifact selection.",
|
|
616
|
+
message: selection.error ?? "Invalid operation artifact selection.",
|
|
617
|
+
context: { api_name: apiName, operation_id: operationId },
|
|
618
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --request`,
|
|
619
|
+
}));
|
|
620
|
+
process.exitCode = 1;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const artifactPath = getOperationArtifactPath(apiName, sanitizedOperationId, selection.artifactName);
|
|
624
|
+
if (!(await fs.pathExists(artifactPath))) {
|
|
625
|
+
if (selection.artifactName === "response-schema") {
|
|
626
|
+
logger.result(buildError(ErrorCode.NO_RESPONSE_SCHEMA, {
|
|
627
|
+
summary: "No response schema artifact exists for this operation.",
|
|
628
|
+
message: "The response schema has not been created yet.",
|
|
629
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: "response-schema" },
|
|
630
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName}`,
|
|
631
|
+
reason: "Make a request first to generate the response-schema artifact.",
|
|
632
|
+
}));
|
|
633
|
+
process.exitCode = 1;
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
logger.result(buildError(ErrorCode.REQUEST_TEMPLATE_STALE, {
|
|
637
|
+
summary: `Artifact '${selection.artifactName}' is missing for this operation.`,
|
|
638
|
+
message: `The '${selection.artifactName}' artifact does not exist yet.`,
|
|
639
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName },
|
|
640
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --force`,
|
|
641
|
+
}));
|
|
642
|
+
process.exitCode = 1;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
let artifact = await fs.readJson(artifactPath);
|
|
647
|
+
const getPath = typeof options.get === "string" ? options.get : undefined;
|
|
648
|
+
const filterExpr = typeof options.filter === "string" ? options.filter : undefined;
|
|
649
|
+
const hasGet = typeof getPath === "string" && getPath.length > 0;
|
|
650
|
+
const hasFilter = typeof filterExpr === "string" && filterExpr.length > 0;
|
|
651
|
+
const selectedValue = hasGet ? getByPath(artifact, getPath) : artifact;
|
|
652
|
+
if (hasGet && !hasFilter) {
|
|
653
|
+
const { sizeBytes } = getJsonOutputSize(selectedValue);
|
|
654
|
+
if (sizeBytes >= MEDIUM_OUTPUT_MAX_BYTES) {
|
|
655
|
+
logger.result(buildError(ErrorCode.OVERSIZED_OUTPUT, {
|
|
656
|
+
summary: "Output is too large to display.",
|
|
657
|
+
message: `The selected value is ${sizeBytes} bytes, exceeding the limit of ${MEDIUM_OUTPUT_MAX_BYTES} bytes.`,
|
|
658
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, size_bytes: sizeBytes },
|
|
659
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName} --get ${getPath} --filter count`,
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
logger.result(buildSuccess({
|
|
664
|
+
apiName,
|
|
665
|
+
value: selectedValue,
|
|
666
|
+
}, { kind: "artifact-value" }));
|
|
667
|
+
}
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (hasFilter) {
|
|
671
|
+
let artifactToFilter = selectedValue;
|
|
672
|
+
let arrayFields = [];
|
|
673
|
+
if (!Array.isArray(artifactToFilter) &&
|
|
674
|
+
artifactToFilter !== null &&
|
|
675
|
+
typeof artifactToFilter === "object") {
|
|
676
|
+
arrayFields = Object.values(artifactToFilter)
|
|
677
|
+
.filter(v => Array.isArray(v));
|
|
678
|
+
if (arrayFields.length === 1) {
|
|
679
|
+
artifactToFilter = arrayFields[0];
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const parsedFilter = parseGetOperationFilter(filterExpr);
|
|
683
|
+
let result;
|
|
684
|
+
if (parsedFilter.kind === "count" || parsedFilter.kind === "index" || parsedFilter.kind === "range") {
|
|
685
|
+
if (!Array.isArray(artifactToFilter)) {
|
|
686
|
+
if (arrayFields.length === 1) {
|
|
687
|
+
artifactToFilter = arrayFields[0];
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
logger.result(buildError(ErrorCode.INVALID_FILTER_SYNTAX, {
|
|
691
|
+
summary: "--filter requires the target to be an array.",
|
|
692
|
+
message: `The selected value is not an array and contains no single array field to unwrap.`,
|
|
693
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, filter: filterExpr, selected_path: hasGet ? getPath : undefined },
|
|
694
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName} --response-schema`,
|
|
695
|
+
}));
|
|
696
|
+
process.exitCode = 1;
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (!Array.isArray(artifactToFilter)) {
|
|
701
|
+
logger.result(buildError(ErrorCode.INVALID_FILTER_SYNTAX, {
|
|
702
|
+
summary: "--filter requires the target to be an array.",
|
|
703
|
+
message: `The selected value is not an array.`,
|
|
704
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, filter: filterExpr, selected_path: hasGet ? getPath : undefined },
|
|
705
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName}`,
|
|
706
|
+
}));
|
|
707
|
+
process.exitCode = 1;
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (parsedFilter.kind === "count") {
|
|
711
|
+
result = artifactToFilter.length;
|
|
712
|
+
}
|
|
713
|
+
else if (parsedFilter.kind === "index") {
|
|
714
|
+
const idx = parsedFilter.index < 0 ? artifactToFilter.length + parsedFilter.index : parsedFilter.index;
|
|
715
|
+
result = artifactToFilter[idx];
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
result = artifactToFilter.slice(parsedFilter.start, parsedFilter.end);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
else if (parsedFilter.kind === "path") {
|
|
722
|
+
const filterPath = parsedFilter.path;
|
|
723
|
+
const rawValue = parsedFilter.value;
|
|
724
|
+
let expectedValue = rawValue;
|
|
725
|
+
if (hasGet && !Array.isArray(artifactToFilter)) {
|
|
726
|
+
logger.result(buildError(ErrorCode.INVALID_FILTER_SYNTAX, {
|
|
727
|
+
summary: "--filter requires the --get result to be an array.",
|
|
728
|
+
message: `The value at path '${getPath}' is not an array.`,
|
|
729
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, filter: filterExpr, selected_path: getPath },
|
|
730
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName} --response-schema`,
|
|
731
|
+
}));
|
|
732
|
+
process.exitCode = 1;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (!Array.isArray(artifactToFilter)) {
|
|
736
|
+
const { sizeBytes } = getJsonOutputSize(artifactToFilter);
|
|
737
|
+
if (sizeBytes >= MEDIUM_OUTPUT_MAX_BYTES) {
|
|
738
|
+
logger.result(buildError(ErrorCode.OVERSIZED_OUTPUT, {
|
|
739
|
+
summary: "Output is too large to display.",
|
|
740
|
+
message: `The selected value is ${sizeBytes} bytes, exceeding the limit of ${MEDIUM_OUTPUT_MAX_BYTES} bytes.`,
|
|
741
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, size_bytes: sizeBytes },
|
|
742
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName} --filter count`,
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
logger.result(buildSuccess({
|
|
747
|
+
apiName,
|
|
748
|
+
value: artifactToFilter,
|
|
749
|
+
}, { kind: "artifact-value" }));
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
result = filterArray(artifactToFilter, filterPath, expectedValue);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
logger.result(buildError(ErrorCode.INVALID_FILTER_SYNTAX, {
|
|
757
|
+
summary: "Invalid --filter syntax.",
|
|
758
|
+
message: "--filter must use count, index, range, or <path>=<value> syntax.",
|
|
759
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, filter: filterExpr },
|
|
760
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName} --filter 0`,
|
|
761
|
+
}));
|
|
762
|
+
process.exitCode = 1;
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const { sizeBytes } = getJsonOutputSize(result);
|
|
766
|
+
if (sizeBytes >= MEDIUM_OUTPUT_MAX_BYTES) {
|
|
767
|
+
logger.result(buildError(ErrorCode.OVERSIZED_OUTPUT, {
|
|
768
|
+
summary: "Output is too large to display.",
|
|
769
|
+
message: `The filtered result is ${sizeBytes} bytes, exceeding the limit of ${MEDIUM_OUTPUT_MAX_BYTES} bytes.`,
|
|
770
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, size_bytes: sizeBytes },
|
|
771
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName} --filter count`,
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
logger.result(buildSuccess({
|
|
776
|
+
apiName,
|
|
777
|
+
value: result,
|
|
778
|
+
}, { kind: "artifact-value" }));
|
|
779
|
+
}
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const { sizeBytes } = getJsonOutputSize(artifact);
|
|
783
|
+
if (sizeBytes >= MEDIUM_OUTPUT_MAX_BYTES) {
|
|
784
|
+
logger.result(buildError(ErrorCode.OVERSIZED_OUTPUT, {
|
|
785
|
+
summary: "Output is too large to display.",
|
|
786
|
+
message: `The artifact is ${sizeBytes} bytes, exceeding the limit of ${MEDIUM_OUTPUT_MAX_BYTES} bytes.`,
|
|
787
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName, size_bytes: sizeBytes },
|
|
788
|
+
nextCommand: `openapi-skills get-operation ${operationId} --api ${apiName} --${selection.artifactName} --get <path> --filter count`,
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
logger.result(buildSuccess({
|
|
793
|
+
apiName,
|
|
794
|
+
operationId,
|
|
795
|
+
artifact,
|
|
796
|
+
}, { kind: "artifact" }));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
logger.result(buildError(ErrorCode.API_PARSE_ERROR, {
|
|
801
|
+
summary: `Failed to read artifact '${selection.artifactName}'.`,
|
|
802
|
+
message: toErrorMessage(error),
|
|
803
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: selection.artifactName },
|
|
804
|
+
}));
|
|
805
|
+
process.exitCode = 1;
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
getOperationCmd.agentMeta = {
|
|
809
|
+
name: "get-operation",
|
|
810
|
+
category: "Navigation",
|
|
811
|
+
usage: "openapi-skills get-operation|get-operation-artifact <operationId> --api <apiName> [--request] [--response] [--response-schema] [--get <path>] [--filter count|<index>|<range>|<path>=<value>]",
|
|
812
|
+
description: "Return a stored operation artifact created by `openapi-skills request` as raw JSON. Run `request` first so the artifact exists. Use --get to select a nested value, then --filter to count, slice, or match values. Use --response-schema first when the response shape is unknown. Exactly one of --request, --response, or --response-schema is required. Alias: `get-operation-artifact`.",
|
|
813
|
+
arguments: [
|
|
814
|
+
{ name: "operationId", type: "string", required: true, positional: true, description: "The operationId whose stored artifact should be returned." },
|
|
815
|
+
{ name: "api", type: "string", required: true, flag: true, description: "The API name as defined in .openapi-skills/config.json." },
|
|
816
|
+
{ name: "request", type: "flag", required: false, flag: true, description: "Return request artifact for the operation." },
|
|
817
|
+
{ name: "response", type: "flag", required: false, flag: true, description: "Return response artifact for the operation." },
|
|
818
|
+
{ name: "response-schema", type: "flag", required: false, flag: true, description: "Return response-schema artifact for the operation. Inspect this first when the response shape is unknown." },
|
|
819
|
+
{ name: "get", type: "string", required: false, flag: true, description: "Select a nested value by dot-notation path first. Inspect get-operation --response-schema first so the path matches the actual response shape." },
|
|
820
|
+
{ name: "filter", type: "string", required: false, flag: true, description: "Filter the selected value using count, index, range, or <path>=<value> syntax. Examples: --filter count, --filter 0, --filter 0:10, --filter status=sold. Select path first with --get, then filter or slice the selected value." }
|
|
821
|
+
],
|
|
822
|
+
examples: [
|
|
823
|
+
"openapi-skills request getPetById --api petstore --force",
|
|
824
|
+
"openapi-skills get-operation getPetById --api petstore --request",
|
|
825
|
+
"openapi-skills get-operation-artifact getPetById --api petstore --request",
|
|
826
|
+
"openapi-skills get-operation getPetById --api petstore --response-schema",
|
|
827
|
+
"openapi-skills get-operation-artifact getPetById --api petstore --response-schema",
|
|
828
|
+
"openapi-skills get-operation getPetById --api petstore --response",
|
|
829
|
+
"openapi-skills get-operation-artifact getPetById --api petstore --response",
|
|
830
|
+
"openapi-skills get-operation getPetById --api petstore --response --get name",
|
|
831
|
+
"openapi-skills get-operation getPetById --api petstore --response --get body --filter id=555",
|
|
832
|
+
"openapi-skills get-operation getPetById --api petstore --response --filter id=5555",
|
|
833
|
+
"openapi-skills get-operation getPetById --api petstore --response --filter count",
|
|
834
|
+
"openapi-skills get-operation getPetById --api petstore --response --filter 0",
|
|
835
|
+
"openapi-skills get-operation getPetById --api petstore --response --filter 0:10"
|
|
836
|
+
],
|
|
837
|
+
returns: {
|
|
838
|
+
type: "json",
|
|
839
|
+
description: "Returns the selected artifact as raw JSON, or a nested value / filtered array when --get or --filter is supplied. When both are used, --get runs first and --filter applies to the narrowed result. Inspect --response-schema first when the response shape is unknown so --get and --filter can be applied in the right order. --filter can also return an array count, single item, or slice when the selected target is an array."
|
|
840
|
+
},
|
|
841
|
+
sideEffects: {
|
|
842
|
+
writesFiles: false,
|
|
843
|
+
readsFiles: true,
|
|
844
|
+
network: false
|
|
845
|
+
},
|
|
846
|
+
constraints: {
|
|
847
|
+
destructive: false,
|
|
848
|
+
idempotent: true,
|
|
849
|
+
requiresParsedApi: true
|
|
850
|
+
},
|
|
851
|
+
filesWritten: []
|
|
852
|
+
};
|
|
853
|
+
const requestCmd = program
|
|
854
|
+
.command("request <operationId...>")
|
|
855
|
+
.description("Make a live HTTP request for a specific operation, or prepare a multi-step request scenario without executing requests. Supports: --validate (validate only the response against the schema after the request is sent; it does not validate request bodies or guarantee a response exists), --force (regenerate request artifact; use it when you want the original schema-shaped template), --update-request (patch request artifact using flattened dot-notation keys; binary fields can be file paths or {kind:'file', path,...} descriptors), --update-request-encoded (patch request artifact using base64url-encoded JSON), --header (add headers), --header-encoded (add headers using base64url-encoded JSON).")
|
|
856
|
+
.requiredOption("--api <apiName>", "API name to use")
|
|
857
|
+
.option("--validate", "Validate only the response against the schema after the request is sent. Does not validate the request body or guarantee a response exists.")
|
|
858
|
+
.option("--force", "Force overwrite request artifact with default values. Use this when you want the original schema-shaped template; omit it if you want to keep previous request values.")
|
|
859
|
+
.option("--update-request <json>", [
|
|
860
|
+
"Update request artifact before making the request using a single-quoted JSON string that represents a flattened object with dot-notation keys.",
|
|
861
|
+
"Nested JSON objects are supported (they will be flattened and issue a warning), but the top-level value must be a JSON object. Invalid JSON will cause the command to fail.",
|
|
862
|
+
"To delete a field, set its value to \"__delete__\".",
|
|
863
|
+
"Binary request fields may be patched with a file path string or an object like {\"kind\":\"file\",\"path\":\"c:/path/file.pdf\",\"fileName\":\"file.pdf\",\"mimeType\":\"application/pdf\"}.",
|
|
864
|
+
"Format (POSIX shells): --update-request '{\"field.path\":value,...}'",
|
|
865
|
+
"Format (PowerShell): --update-request \"{\"field.path\":value,...}\" (escape inner quotes as needed)",
|
|
866
|
+
" - Only flattened object dot-notation keys are recommended (e.g. 'items.0.name').",
|
|
867
|
+
"Examples:",
|
|
868
|
+
" --update-request '{\"person.id\":\"2\"}'",
|
|
869
|
+
" --update-request '{\"items.0.name\":\"Alice\",\"items.1.value\":42}'",
|
|
870
|
+
" --update-request '{\"parameters.0\":\"__delete__\"}'",
|
|
871
|
+
" --update-request '{\"address.street\":\"Main St\",\"address.zip\":12345}'",
|
|
872
|
+
"Note: The CLI runs JSON.parse on the provided value; if parsing fails the command exits with an error."
|
|
873
|
+
].join("\n"))
|
|
874
|
+
.option("--update-request-encoded <base64url>", [
|
|
875
|
+
"Patch request artifact before making the request using base64url-encoded JSON.",
|
|
876
|
+
"Use this when a shell or agent may strip quotes from literal JSON.",
|
|
877
|
+
"Example:",
|
|
878
|
+
" --update-request-encoded eyJyZXF1ZXN0Qm9keS5pZCI6MTJ9"
|
|
879
|
+
].join("\n"))
|
|
880
|
+
.option("--header <json>", [
|
|
881
|
+
"Additional headers as a JSON string (merged with config and defaults).",
|
|
882
|
+
"Format: --header '{\"Header-Name\":\"value\"}'",
|
|
883
|
+
"Example:",
|
|
884
|
+
" --header '{\"X-Api-Key\":\"abc\"}'"
|
|
885
|
+
].join("\n"))
|
|
886
|
+
.option("--header-encoded <base64url>", [
|
|
887
|
+
"Additional headers as base64url-encoded JSON (merged with config and defaults).",
|
|
888
|
+
"Use this when a shell or agent may strip quotes from literal JSON.",
|
|
889
|
+
"Example:",
|
|
890
|
+
" --header-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9"
|
|
891
|
+
].join("\n"))
|
|
892
|
+
.action(async (operationIds, options) => {
|
|
893
|
+
const apiName = options.api;
|
|
894
|
+
if (!(await guardApiName(apiName))) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (operationIds.length > 1) {
|
|
898
|
+
try {
|
|
899
|
+
const { summaryText, payload } = await prepareMultiOperationRequests(apiName, operationIds, options.force === true);
|
|
900
|
+
logger.result(buildSuccess({
|
|
901
|
+
summary: summaryText,
|
|
902
|
+
operationIds,
|
|
903
|
+
apiName,
|
|
904
|
+
}, { kind: "request-prepare" }));
|
|
905
|
+
process.exitCode = 0;
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
const message = toErrorMessage(error);
|
|
909
|
+
logger.result(buildError(ErrorCode.API_PARSE_ERROR, {
|
|
910
|
+
summary: "Failed to prepare multi-operation request scenario.",
|
|
911
|
+
message,
|
|
912
|
+
context: { api_name: apiName, operation_ids: operationIds },
|
|
913
|
+
}));
|
|
914
|
+
process.exitCode = 1;
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const operationId = operationIds[0];
|
|
919
|
+
if (!operationId) {
|
|
920
|
+
logger.result(buildError(ErrorCode.INVALID_REQUEST_UPDATE, {
|
|
921
|
+
summary: "An operationId is required.",
|
|
922
|
+
message: "Provide at least one operationId to invoke.",
|
|
923
|
+
context: { api_name: apiName },
|
|
924
|
+
nextCommand: `openapi-skills request <operationId> --api ${apiName}`,
|
|
925
|
+
}));
|
|
926
|
+
process.exitCode = 1;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
let requestJsonUpdates;
|
|
930
|
+
let requestJsonWarnings;
|
|
931
|
+
if (options.updateRequest && options.updateRequestEncoded) {
|
|
932
|
+
logger.result(buildError(ErrorCode.INVALID_REQUEST_UPDATE, {
|
|
933
|
+
summary: "Specify only one of --update-request or --update-request-encoded.",
|
|
934
|
+
message: "Choose the raw JSON form or the base64url-encoded form, not both.",
|
|
935
|
+
context: { api_name: apiName, operation_id: operationId },
|
|
936
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --update-request-encoded eyJyZXF1ZXN0Qm9keS5pZCI6MTJ9`,
|
|
937
|
+
}));
|
|
938
|
+
process.exitCode = 2;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (options.updateRequest) {
|
|
942
|
+
let updates;
|
|
943
|
+
try {
|
|
944
|
+
updates = JSON.parse(options.updateRequest);
|
|
945
|
+
}
|
|
946
|
+
catch (err) {
|
|
947
|
+
const message = `Invalid JSON for --update-request: ${err instanceof Error ? err.message : String(err)}`;
|
|
948
|
+
logger.result(buildError(ErrorCode.INVALID_UPDATE_REQUEST_JSON, {
|
|
949
|
+
summary: "--update-request JSON is malformed.",
|
|
950
|
+
message,
|
|
951
|
+
context: { api_name: apiName, operation_id: operationId, update_request: options.updateRequest },
|
|
952
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --update-request '{\"field.path\":\"value\"}'`,
|
|
953
|
+
}));
|
|
954
|
+
process.exitCode = 2;
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (typeof updates !== "object" || Array.isArray(updates) || !updates || Object.keys(updates).length === 0) {
|
|
958
|
+
logger.result(buildError(ErrorCode.INVALID_REQUEST_UPDATE, {
|
|
959
|
+
summary: "--update-request must be a non-empty JSON object.",
|
|
960
|
+
message: "Use flattened dot-notation keys and provide at least one update.",
|
|
961
|
+
context: { api_name: apiName, operation_id: operationId, update_request: options.updateRequest },
|
|
962
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --update-request '{\"field.path\":\"value\"}'`,
|
|
963
|
+
}));
|
|
964
|
+
process.exitCode = 2;
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
|
|
968
|
+
const requestJsonPath = getOperationArtifactPath(apiName, sanitizedOperationId, "request");
|
|
969
|
+
if (options.force === true) {
|
|
970
|
+
const config = await loadConfig();
|
|
971
|
+
const configuredSchemaType = config.apis?.[apiName]?.schemaType;
|
|
972
|
+
let isGraphQLSchema = configuredSchemaType === "graphql";
|
|
973
|
+
if (!isGraphQLSchema && configuredSchemaType !== "openapi") {
|
|
974
|
+
try {
|
|
975
|
+
const endpoints = await fs.readJson(getEndpointsPath(apiName));
|
|
976
|
+
isGraphQLSchema = Array.isArray(endpoints) && endpoints.some((endpoint) => typeof endpoint?.rootType === "string");
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
isGraphQLSchema = false;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!isGraphQLSchema) {
|
|
983
|
+
await ensureEndpointSchemaFile(apiName, operationId, sanitizedOperationId);
|
|
984
|
+
}
|
|
985
|
+
await prepareRequestTemplate(apiName, sanitizedOperationId, true);
|
|
986
|
+
}
|
|
987
|
+
else if (!(await fs.pathExists(requestJsonPath))) {
|
|
988
|
+
logger.result(buildError(ErrorCode.REQUEST_TEMPLATE_STALE, {
|
|
989
|
+
summary: "request.json does not exist for this operation.",
|
|
990
|
+
message: "--update-request requires an existing request.json artifact. Use --force to regenerate the request template.",
|
|
991
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: "request" },
|
|
992
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --force --update-request '{\"field.path\":\"value\"}'`,
|
|
993
|
+
}));
|
|
994
|
+
process.exitCode = 2;
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const requestJson = await fs.readJson(requestJsonPath);
|
|
998
|
+
const requestJsonKeys = collectRequestJsonPaths(requestJson);
|
|
999
|
+
const updateKeys = collectUpdateRequestKeys(updates);
|
|
1000
|
+
const missingKeys = Array.from(updateKeys).filter(key => !requestJsonKeys.has(key));
|
|
1001
|
+
if (missingKeys.length > 0) {
|
|
1002
|
+
logger.result(buildError(ErrorCode.INVALID_REQUEST_UPDATE, {
|
|
1003
|
+
summary: "--update-request contains keys not present in request.json.",
|
|
1004
|
+
message: `The following keys are missing from request.json: ${missingKeys.join(", ")}.`,
|
|
1005
|
+
context: { api_name: apiName, operation_id: operationId, update_request: options.updateRequest, missing_keys: missingKeys },
|
|
1006
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --force`,
|
|
1007
|
+
}));
|
|
1008
|
+
process.exitCode = 2;
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (options.force === true) {
|
|
1012
|
+
const typeWarnings = collectRequestUpdateTypeWarnings(requestJson, updates);
|
|
1013
|
+
if (typeWarnings.length > 0) {
|
|
1014
|
+
requestJsonWarnings = typeWarnings;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
requestJsonUpdates = updates;
|
|
1018
|
+
}
|
|
1019
|
+
if (options.updateRequestEncoded) {
|
|
1020
|
+
let updates;
|
|
1021
|
+
try {
|
|
1022
|
+
updates = decodeBase64UrlJsonObject(options.updateRequestEncoded, "--update-request-encoded");
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
const message = `Invalid base64url for --update-request-encoded: ${err instanceof Error ? err.message : String(err)}`;
|
|
1026
|
+
logger.result(buildError(ErrorCode.INVALID_UPDATE_REQUEST_JSON, {
|
|
1027
|
+
summary: "--update-request-encoded value is malformed.",
|
|
1028
|
+
message,
|
|
1029
|
+
context: { api_name: apiName, operation_id: operationId, update_request_encoded: options.updateRequestEncoded },
|
|
1030
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --update-request-encoded eyJyZXF1ZXN0Qm9keS5pZCI6MTJ9`,
|
|
1031
|
+
}));
|
|
1032
|
+
process.exitCode = 2;
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (typeof updates !== "object" || Array.isArray(updates) || !updates || Object.keys(updates).length === 0) {
|
|
1036
|
+
logger.result(buildError(ErrorCode.INVALID_REQUEST_UPDATE, {
|
|
1037
|
+
summary: "--update-request-encoded must decode to a non-empty JSON object.",
|
|
1038
|
+
message: "The decoded payload must contain at least one flattened dot-notation key.",
|
|
1039
|
+
context: { api_name: apiName, operation_id: operationId, update_request_encoded: options.updateRequestEncoded },
|
|
1040
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --update-request-encoded eyJyZXF1ZXN0Qm9keS5pZCI6MTJ9`,
|
|
1041
|
+
}));
|
|
1042
|
+
process.exitCode = 2;
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const sanitizedOperationId = await getSanitizedOperationId(apiName, operationId);
|
|
1046
|
+
const requestJsonPath = getOperationArtifactPath(apiName, sanitizedOperationId, "request");
|
|
1047
|
+
if (options.force === true) {
|
|
1048
|
+
const config = await loadConfig();
|
|
1049
|
+
const configuredSchemaType = config.apis?.[apiName]?.schemaType;
|
|
1050
|
+
let isGraphQLSchema = configuredSchemaType === "graphql";
|
|
1051
|
+
if (!isGraphQLSchema && configuredSchemaType !== "openapi") {
|
|
1052
|
+
try {
|
|
1053
|
+
const endpoints = await fs.readJson(getEndpointsPath(apiName));
|
|
1054
|
+
isGraphQLSchema = Array.isArray(endpoints) && endpoints.some((endpoint) => typeof endpoint?.rootType === "string");
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
isGraphQLSchema = false;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (!isGraphQLSchema) {
|
|
1061
|
+
await ensureEndpointSchemaFile(apiName, operationId, sanitizedOperationId);
|
|
1062
|
+
}
|
|
1063
|
+
await prepareRequestTemplate(apiName, sanitizedOperationId, true);
|
|
1064
|
+
}
|
|
1065
|
+
else if (!(await fs.pathExists(requestJsonPath))) {
|
|
1066
|
+
logger.result(buildError(ErrorCode.REQUEST_TEMPLATE_STALE, {
|
|
1067
|
+
summary: "request.json does not exist for this operation.",
|
|
1068
|
+
message: "--update-request-encoded requires an existing request.json artifact. Use --force to regenerate the request template.",
|
|
1069
|
+
context: { api_name: apiName, operation_id: operationId, artifact_type: "request" },
|
|
1070
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --force --update-request-encoded eyJyZXF1ZXN0Qm9keS5pZCI6MTJ9`,
|
|
1071
|
+
}));
|
|
1072
|
+
process.exitCode = 2;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const requestJson = await fs.readJson(requestJsonPath);
|
|
1076
|
+
const requestJsonKeys = collectRequestJsonPaths(requestJson);
|
|
1077
|
+
const updateKeys = collectUpdateRequestKeys(updates);
|
|
1078
|
+
const missingKeys = Array.from(updateKeys).filter(key => !requestJsonKeys.has(key));
|
|
1079
|
+
if (missingKeys.length > 0) {
|
|
1080
|
+
logger.result(buildError(ErrorCode.INVALID_REQUEST_UPDATE, {
|
|
1081
|
+
summary: "--update-request-encoded contains keys not present in request.json.",
|
|
1082
|
+
message: `The following keys are missing from request.json: ${missingKeys.join(", ")}.`,
|
|
1083
|
+
context: { api_name: apiName, operation_id: operationId, update_request_encoded: options.updateRequestEncoded, missing_keys: missingKeys },
|
|
1084
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --force`,
|
|
1085
|
+
}));
|
|
1086
|
+
process.exitCode = 2;
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (options.force === true) {
|
|
1090
|
+
const typeWarnings = collectRequestUpdateTypeWarnings(requestJson, updates);
|
|
1091
|
+
if (typeWarnings.length > 0) {
|
|
1092
|
+
requestJsonWarnings = typeWarnings;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
requestJsonUpdates = updates;
|
|
1096
|
+
}
|
|
1097
|
+
if (options.header && options.headerEncoded) {
|
|
1098
|
+
logger.result(buildError(ErrorCode.INVALID_JSON_ARGUMENT, {
|
|
1099
|
+
summary: "Provide only one of --header or --header-encoded.",
|
|
1100
|
+
message: "--header and --header-encoded are mutually exclusive.",
|
|
1101
|
+
context: { api_name: apiName, operation_id: operationId, header: options.header, header_encoded: options.headerEncoded },
|
|
1102
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --header-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9`,
|
|
1103
|
+
}));
|
|
1104
|
+
process.exitCode = 2;
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
let cliHeaders = undefined;
|
|
1108
|
+
if (options.header) {
|
|
1109
|
+
try {
|
|
1110
|
+
cliHeaders = JSON.parse(options.header);
|
|
1111
|
+
if (typeof cliHeaders !== "object" || Array.isArray(cliHeaders) || !cliHeaders) {
|
|
1112
|
+
throw new Error("--header must be a JSON object");
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
catch (err) {
|
|
1116
|
+
const message = "Invalid --header JSON. Example: --header '{\"X-Api-Key\":\"abc\"}'";
|
|
1117
|
+
logger.result(buildError(ErrorCode.INVALID_JSON_ARGUMENT, {
|
|
1118
|
+
summary: "--header JSON is malformed.",
|
|
1119
|
+
message,
|
|
1120
|
+
context: { header: options.header },
|
|
1121
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --header '{"Header-Name":"value"}'`,
|
|
1122
|
+
}));
|
|
1123
|
+
process.exitCode = 2;
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (options.headerEncoded) {
|
|
1128
|
+
try {
|
|
1129
|
+
cliHeaders = decodeBase64UrlJsonObject(options.headerEncoded, "--header-encoded");
|
|
1130
|
+
}
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
const message = `Invalid base64url for --header-encoded: ${err instanceof Error ? err.message : String(err)}`;
|
|
1133
|
+
logger.result(buildError(ErrorCode.INVALID_JSON_ARGUMENT, {
|
|
1134
|
+
summary: "--header-encoded value is malformed.",
|
|
1135
|
+
message,
|
|
1136
|
+
context: { header_encoded: options.headerEncoded },
|
|
1137
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --header-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9`,
|
|
1138
|
+
}));
|
|
1139
|
+
process.exitCode = 2;
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (options.validate) {
|
|
1144
|
+
try {
|
|
1145
|
+
const result = await validateResponse(apiName, operationId, options.force, cliHeaders, requestJsonUpdates, requestJsonWarnings);
|
|
1146
|
+
const metadata = getRequestResponseMetadata(apiName, operationId);
|
|
1147
|
+
if (!result.valid) {
|
|
1148
|
+
logger.result(buildError(ErrorCode.VALIDATION_FAILED, {
|
|
1149
|
+
summary: "Response validation failed.",
|
|
1150
|
+
message: (result.errors ?? []).join("; ") || "The response did not match the expected schema.",
|
|
1151
|
+
context: {
|
|
1152
|
+
api_name: apiName,
|
|
1153
|
+
operation_id: operationId,
|
|
1154
|
+
validation_warnings: result.warnings ?? [],
|
|
1155
|
+
validation_errors: result.errors ?? [],
|
|
1156
|
+
operation_artifact_count: metadata.fileCount,
|
|
1157
|
+
},
|
|
1158
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --validate`,
|
|
1159
|
+
}));
|
|
1160
|
+
process.exitCode = 1;
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
logger.result(buildSuccess({
|
|
1164
|
+
apiName,
|
|
1165
|
+
operationId,
|
|
1166
|
+
valid: result.valid,
|
|
1167
|
+
warnings: result.warnings ?? [],
|
|
1168
|
+
errors: result.errors ?? [],
|
|
1169
|
+
operationArtifactCount: metadata.fileCount,
|
|
1170
|
+
retrieveArtifacts: "Use `get-operation-artifact [--request|--response|--response-schema] <operationId>` to view this operation's artifacts."
|
|
1171
|
+
}, { kind: "validation-result" }));
|
|
1172
|
+
process.exitCode = 0;
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
logger.result(buildError(ErrorCode.VALIDATION_FAILED, {
|
|
1176
|
+
summary: "Validation failed.",
|
|
1177
|
+
message: toErrorMessage(error),
|
|
1178
|
+
context: { api_name: apiName, operation_id: operationId },
|
|
1179
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName} --validate`,
|
|
1180
|
+
}));
|
|
1181
|
+
process.exitCode = 2;
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
try {
|
|
1187
|
+
const { request, response, warnings } = await makeRequest(apiName, operationId, options.force, cliHeaders, requestJsonUpdates, requestJsonWarnings);
|
|
1188
|
+
await ensureResponseSchema(apiName, operationId);
|
|
1189
|
+
const metadata = getRequestResponseMetadata(apiName, operationId);
|
|
1190
|
+
logger.result(buildSuccess({
|
|
1191
|
+
apiName,
|
|
1192
|
+
operationId,
|
|
1193
|
+
warnings: warnings ?? [],
|
|
1194
|
+
fileCount: metadata.fileCount,
|
|
1195
|
+
retrieveArtifacts: "Use `get-operation-artifact [--request|--response|--response-schema] <operationId>` to view this operation's artifacts."
|
|
1196
|
+
}, { kind: "request-result" }));
|
|
1197
|
+
process.exitCode = 0;
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
logger.result(buildError(ErrorCode.REQUEST_FAILED, {
|
|
1201
|
+
summary: "Request failed.",
|
|
1202
|
+
message: toErrorMessage(error),
|
|
1203
|
+
context: { api_name: apiName, operation_id: operationId },
|
|
1204
|
+
nextCommand: `openapi-skills request ${operationId} --api ${apiName}`,
|
|
1205
|
+
}));
|
|
1206
|
+
process.exitCode = 2;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
requestCmd.agentMeta = {
|
|
1212
|
+
name: "request",
|
|
1213
|
+
category: "Validation",
|
|
1214
|
+
usage: "openapi-skills request <operationId...> --api <apiName> [--validate] [--force] [--update-request <json>] [--update-request-encoded <base64url>] [--header <json>] [--header-encoded <base64url>]",
|
|
1215
|
+
description: [
|
|
1216
|
+
"Make a live HTTP request for an operation, or prepare a multi-step request scenario without executing requests.",
|
|
1217
|
+
"When multiple operationIds are supplied, the command enters prepare-only mode and refreshes request artifact templates for the scenario.",
|
|
1218
|
+
"With --validate, validate only the response against the schema after the request is sent.",
|
|
1219
|
+
"With --force, regenerate the request artifact from schema defaults.",
|
|
1220
|
+
"With --update-request, patch the request artifact using flattened dot-notation keys. Nested JSON objects are accepted, but the provided value must be valid JSON. With --update-request-encoded, pass base64url-encoded JSON instead. With --header, merge JSON headers into the request. With --header-encoded, pass base64url-encoded JSON headers instead. Binary fields can be set to a file path string or a file descriptor object. Set a field to \"__delete__\" to remove it."
|
|
1221
|
+
].join(" "),
|
|
1222
|
+
arguments: [
|
|
1223
|
+
{ name: "operationId", type: "string[]", required: true, positional: true, description: "One or more operationIds to invoke. Multiple values switch the command into prepare-only mode for a multi-step scenario." },
|
|
1224
|
+
{ name: "api", type: "string", required: true, flag: true, description: "The API name as defined in .openapi-skills/config.json." },
|
|
1225
|
+
{ name: "validate", type: "flag", required: false, flag: true, description: "Validate only the response against the schema after the request is sent. Does not validate the request body or guarantee a response exists." },
|
|
1226
|
+
{ name: "force", type: "flag", required: false, flag: true, description: "Force overwrite request artifact with default values. Use this when you want the original schema-shaped template; omit it if you want to keep previous request values." },
|
|
1227
|
+
{ name: "update-request", type: "json", required: false, flag: true, description: "Patch request artifact before making the request. Only flattened object dot-notation keys are allowed. Binary fields can be set to a file path string or file descriptor object. Set a field to \"__delete__\" to remove it. Use with --force to rebuild defaults first." },
|
|
1228
|
+
{ name: "update-request-encoded", type: "string", required: false, flag: true, description: "Patch request artifact before making the request using base64url-encoded JSON. Use this when quoting is unreliable." },
|
|
1229
|
+
{ name: "header", type: "json", required: false, flag: true, description: "Additional headers as a JSON string." },
|
|
1230
|
+
{ name: "header-encoded", type: "string", required: false, flag: true, description: "Additional headers as base64url-encoded JSON. Use this when quoting is unreliable." }
|
|
1231
|
+
],
|
|
1232
|
+
examples: [
|
|
1233
|
+
"openapi-skills request getPetById --api petstore",
|
|
1234
|
+
"openapi-skills request getPetById --api petstore --validate",
|
|
1235
|
+
"openapi-skills request getPetById --api petstore --force --update-request '{\"user.profile.name\":\"Ada\"}'",
|
|
1236
|
+
"openapi-skills request getPetById --api petstore --force --update-request-encoded eyJyZXF1ZXN0Qm9keS5pZCI6MTJ9",
|
|
1237
|
+
"openapi-skills request getPetById --api petstore --update-request '{\"user.profile.name\":\"Ada\"}'",
|
|
1238
|
+
"openapi-skills request getPetById --api petstore --update-request '{\"parameters.0.id\":1}'",
|
|
1239
|
+
"openapi-skills request getPetById --api petstore --header-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9",
|
|
1240
|
+
"openapi-skills request getPetById --api petstore --update-request '{\"parameters.0\":\"__delete__\"}'",
|
|
1241
|
+
"openapi-skills request operationId1 operationId2 --api petstore"
|
|
1242
|
+
],
|
|
1243
|
+
returns: {
|
|
1244
|
+
type: "json",
|
|
1245
|
+
description: "Returns raw request and response for single-operation requests, or a structured batch preparation summary when multiple operationIds are supplied."
|
|
1246
|
+
},
|
|
1247
|
+
sideEffects: {
|
|
1248
|
+
writesFiles: true,
|
|
1249
|
+
readsFiles: true,
|
|
1250
|
+
network: true
|
|
1251
|
+
},
|
|
1252
|
+
constraints: {
|
|
1253
|
+
destructive: false,
|
|
1254
|
+
idempotent: false,
|
|
1255
|
+
requiresParsedApi: true
|
|
1256
|
+
},
|
|
1257
|
+
filesWritten: ["request artifact", "response artifact", "response-schema artifact (when response has JSON body)"]
|
|
1258
|
+
};
|
|
1259
|
+
const setEnvCmd = program
|
|
1260
|
+
.command("set-env")
|
|
1261
|
+
.requiredOption("--api <apiName>", "API name to use")
|
|
1262
|
+
.option("--base-url <url>", "Base URL for the API environment.")
|
|
1263
|
+
.option("--auth <json>", "Authentication headers as a JSON string.")
|
|
1264
|
+
.option("--auth-encoded <base64url>", "Authentication headers as base64url-encoded JSON. Use this when a shell or agent may strip quotes from literal JSON.")
|
|
1265
|
+
.option("--var <key=value>", "Set a runtime variable for the API environment. Repeatable.", (value, previous) => {
|
|
1266
|
+
const values = Array.isArray(previous) ? previous : previous ? [previous] : [];
|
|
1267
|
+
return [...values, value];
|
|
1268
|
+
})
|
|
1269
|
+
.description("Set or update the runtime environment for a parsed API.")
|
|
1270
|
+
.addHelpText('after', [
|
|
1271
|
+
"Use this command to set baseUrl, auth headers, and runtime vars for a parsed API.",
|
|
1272
|
+
"Pass auth headers with --auth as JSON or --auth-encoded as base64url-encoded JSON when quoting is unreliable.",
|
|
1273
|
+
"",
|
|
1274
|
+
"Examples:",
|
|
1275
|
+
" set-env --api petstore --base-url https://dev.example.com --auth '{\"Authorization\":\"Bearer abc\"}'",
|
|
1276
|
+
" set-env --api petstore --auth-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9",
|
|
1277
|
+
" set-env --api petstore --auth '{\"Authorization\":\"Bearer abc\"}'",
|
|
1278
|
+
" set-env --api petstore --var userId=123"
|
|
1279
|
+
].join("\n"))
|
|
1280
|
+
.action(async (options) => {
|
|
1281
|
+
const apiName = options.api;
|
|
1282
|
+
if (!(await guardApiName(apiName))) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const providedBaseUrl = typeof options.baseUrl === "string" ? options.baseUrl.trim() : "";
|
|
1286
|
+
const baseUrl = providedBaseUrl.length > 0 ? providedBaseUrl : undefined;
|
|
1287
|
+
let auth;
|
|
1288
|
+
if (typeof options.auth === "string" && typeof options.authEncoded === "string") {
|
|
1289
|
+
logger.result(buildError(ErrorCode.INVALID_JSON_ARGUMENT, {
|
|
1290
|
+
summary: "Provide only one of --auth or --auth-encoded.",
|
|
1291
|
+
message: "--auth and --auth-encoded are mutually exclusive.",
|
|
1292
|
+
context: { api_name: apiName, auth: options.auth, auth_encoded: options.authEncoded },
|
|
1293
|
+
nextCommand: `openapi-skills set-env --api ${apiName} --auth-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9`,
|
|
1294
|
+
}));
|
|
1295
|
+
process.exitCode = 1;
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
if (typeof options.auth === "string") {
|
|
1299
|
+
try {
|
|
1300
|
+
const parsedAuth = JSON.parse(options.auth);
|
|
1301
|
+
if (typeof parsedAuth !== "object" || Array.isArray(parsedAuth) || !parsedAuth) {
|
|
1302
|
+
throw new Error("Auth must be a JSON object");
|
|
1303
|
+
}
|
|
1304
|
+
auth = parsedAuth;
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
logger.result(buildError(ErrorCode.INVALID_JSON_ARGUMENT, {
|
|
1308
|
+
summary: "--auth JSON is malformed.",
|
|
1309
|
+
message: "Invalid --auth JSON. Example: '{\"Authorization\":\"Bearer abc\"}'",
|
|
1310
|
+
context: { api_name: apiName, auth: options.auth ?? null },
|
|
1311
|
+
nextCommand: `openapi-skills set-env --api ${apiName} --auth '{\"Authorization\":\"Bearer abc\"}'`,
|
|
1312
|
+
}));
|
|
1313
|
+
process.exitCode = 1;
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (typeof options.authEncoded === "string") {
|
|
1318
|
+
try {
|
|
1319
|
+
const decodedAuth = decodeBase64UrlJsonObject(options.authEncoded, "--auth-encoded");
|
|
1320
|
+
const normalizedAuth = {};
|
|
1321
|
+
for (const [key, value] of Object.entries(decodedAuth)) {
|
|
1322
|
+
if (typeof value !== "string") {
|
|
1323
|
+
throw new Error(`Auth header "${key}" must be a string`);
|
|
1324
|
+
}
|
|
1325
|
+
normalizedAuth[key] = value;
|
|
1326
|
+
}
|
|
1327
|
+
auth = normalizedAuth;
|
|
1328
|
+
}
|
|
1329
|
+
catch (err) {
|
|
1330
|
+
logger.result(buildError(ErrorCode.INVALID_JSON_ARGUMENT, {
|
|
1331
|
+
summary: "--auth-encoded value is malformed.",
|
|
1332
|
+
message: `Invalid base64url for --auth-encoded: ${err instanceof Error ? err.message : String(err)}`,
|
|
1333
|
+
context: { api_name: apiName, auth_encoded: options.authEncoded },
|
|
1334
|
+
nextCommand: `openapi-skills set-env --api ${apiName} --auth-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9`,
|
|
1335
|
+
}));
|
|
1336
|
+
process.exitCode = 1;
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const rawVars = options.var ? (Array.isArray(options.var) ? options.var : [options.var]) : [];
|
|
1341
|
+
const vars = {};
|
|
1342
|
+
for (const entry of rawVars) {
|
|
1343
|
+
const separatorIndex = entry.indexOf("=");
|
|
1344
|
+
if (separatorIndex <= 0) {
|
|
1345
|
+
logger.result(buildError(ErrorCode.INVALID_VARIABLE_SYNTAX, {
|
|
1346
|
+
summary: "Invalid --var syntax.",
|
|
1347
|
+
message: `Invalid --var value "${entry}". Use key=value.`,
|
|
1348
|
+
context: { api_name: apiName, variable: entry },
|
|
1349
|
+
nextCommand: `openapi-skills set-env --api ${apiName} --var userId=123`,
|
|
1350
|
+
}));
|
|
1351
|
+
process.exitCode = 1;
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const key = entry.slice(0, separatorIndex).trim();
|
|
1355
|
+
const value = entry.slice(separatorIndex + 1);
|
|
1356
|
+
if (!key) {
|
|
1357
|
+
logger.result(buildError(ErrorCode.INVALID_VARIABLE_SYNTAX, {
|
|
1358
|
+
summary: "Invalid --var syntax.",
|
|
1359
|
+
message: `Invalid --var value "${entry}". Use key=value.`,
|
|
1360
|
+
context: { api_name: apiName, variable: entry },
|
|
1361
|
+
nextCommand: `openapi-skills set-env --api ${apiName} --var userId=123`,
|
|
1362
|
+
}));
|
|
1363
|
+
process.exitCode = 1;
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
vars[key] = value;
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const configUpdates = {};
|
|
1370
|
+
if (baseUrl !== undefined)
|
|
1371
|
+
configUpdates.baseUrl = baseUrl;
|
|
1372
|
+
if (auth !== undefined)
|
|
1373
|
+
configUpdates.auth = auth;
|
|
1374
|
+
if (Object.keys(vars).length > 0)
|
|
1375
|
+
configUpdates.vars = vars;
|
|
1376
|
+
await updateConfig(apiName, configUpdates);
|
|
1377
|
+
logger.result(buildSuccess({
|
|
1378
|
+
apiName,
|
|
1379
|
+
baseUrl,
|
|
1380
|
+
hasAuth: auth !== undefined,
|
|
1381
|
+
varCount: Object.keys(vars).length,
|
|
1382
|
+
}, { kind: "set-env-result" }));
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
logger.result(buildError(ErrorCode.CONFIG_ERROR, {
|
|
1386
|
+
summary: "Failed to update API environment.",
|
|
1387
|
+
message: toErrorMessage(error),
|
|
1388
|
+
context: { api_name: apiName, base_url: baseUrl ?? null, var_count: Object.keys(vars).length },
|
|
1389
|
+
nextCommand: `openapi-skills set-env --api ${apiName}`,
|
|
1390
|
+
}));
|
|
1391
|
+
process.exitCode = 1;
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
setEnvCmd.agentMeta = {
|
|
1395
|
+
name: "set-env",
|
|
1396
|
+
category: "Configuration",
|
|
1397
|
+
usage: "openapi-skills set-env --api <apiName> [--base-url <url>] [--auth <json>] [--auth-encoded <base64url>] [--var key=value]",
|
|
1398
|
+
description: "Set or update the runtime environment for a parsed API. Persists baseUrl, auth headers, and named vars in config.json.",
|
|
1399
|
+
arguments: [
|
|
1400
|
+
{ name: "api", type: "string", required: true, flag: true, description: "The API name as defined in .openapi-skills/config.json." },
|
|
1401
|
+
{ name: "base-url", type: "string", required: false, flag: true, description: "Base URL for the API environment." },
|
|
1402
|
+
{ name: "auth", type: "json", required: false, flag: true, description: "Authentication headers as a JSON object." },
|
|
1403
|
+
{ name: "auth-encoded", type: "string", required: false, flag: true, description: "Authentication headers as base64url-encoded JSON." },
|
|
1404
|
+
{ name: "var", type: "string", required: false, flag: true, description: "Runtime variable in key=value form. Repeatable." }
|
|
1405
|
+
],
|
|
1406
|
+
examples: [
|
|
1407
|
+
"openapi-skills set-env --api petstore --base-url https://dev.example.com --auth '{\"Authorization\":\"Bearer abc\"}'",
|
|
1408
|
+
"openapi-skills set-env --api petstore --auth-encoded eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIGFiYyJ9",
|
|
1409
|
+
"openapi-skills set-env --api petstore --auth '{\"Authorization\":\"Bearer abc\"}'",
|
|
1410
|
+
"openapi-skills set-env --api petstore --var userId=123 --var env=staging"
|
|
1411
|
+
],
|
|
1412
|
+
returns: {
|
|
1413
|
+
type: "json",
|
|
1414
|
+
description: "Returns success or error message."
|
|
1415
|
+
},
|
|
1416
|
+
sideEffects: {
|
|
1417
|
+
writesFiles: true,
|
|
1418
|
+
readsFiles: true,
|
|
1419
|
+
network: false
|
|
1420
|
+
},
|
|
1421
|
+
constraints: {
|
|
1422
|
+
destructive: false,
|
|
1423
|
+
idempotent: false,
|
|
1424
|
+
requiresParsedApi: false
|
|
1425
|
+
},
|
|
1426
|
+
filesWritten: ["config.json"]
|
|
1427
|
+
};
|
|
1428
|
+
const getEnvCmd = program
|
|
1429
|
+
.command("get-env")
|
|
1430
|
+
.requiredOption("--api <apiName>", "API name to use")
|
|
1431
|
+
.option("--base-url", "Include baseUrl in output")
|
|
1432
|
+
.option("--auth", "Include auth headers in output")
|
|
1433
|
+
.option("--var <key=value>", "Request one or more runtime vars. Repeatable; only the key part is used to filter output.", (value, previous) => {
|
|
1434
|
+
const values = Array.isArray(previous) ? previous : previous ? [previous] : [];
|
|
1435
|
+
return [...values, value];
|
|
1436
|
+
})
|
|
1437
|
+
.description("Read API environment configuration from `.openapi-skills/config.json`. Returns all values unless a specific field is requested via flags.")
|
|
1438
|
+
.action(async (options) => {
|
|
1439
|
+
const apiName = options.api;
|
|
1440
|
+
if (!(await guardApiName(apiName))) {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
const wantBase = options.baseUrl === true;
|
|
1445
|
+
const wantAuth = options.auth === true;
|
|
1446
|
+
const rawVars = options.var ? (Array.isArray(options.var) ? options.var : [options.var]) : [];
|
|
1447
|
+
const wantAny = wantBase || wantAuth || rawVars.length > 0;
|
|
1448
|
+
const baseUrl = await getConfigValue(apiName, "baseUrl");
|
|
1449
|
+
const authHeaders = await getConfigValue(apiName, "authHeaders");
|
|
1450
|
+
const varsEntriesRaw = await getConfigValue(apiName, "vars");
|
|
1451
|
+
const varsEntries = Array.isArray(varsEntriesRaw) ? varsEntriesRaw : [];
|
|
1452
|
+
const varsObj = Object.fromEntries(varsEntries);
|
|
1453
|
+
const resultPayload = { apiName };
|
|
1454
|
+
if (!wantAny || wantBase)
|
|
1455
|
+
resultPayload.baseUrl = baseUrl;
|
|
1456
|
+
if (!wantAny || wantAuth)
|
|
1457
|
+
resultPayload.authHeaders = authHeaders;
|
|
1458
|
+
if (!wantAny) {
|
|
1459
|
+
resultPayload.vars = varsEntries;
|
|
1460
|
+
}
|
|
1461
|
+
else if (rawVars.length > 0) {
|
|
1462
|
+
const keys = rawVars.map(entry => {
|
|
1463
|
+
const idx = entry.indexOf("=");
|
|
1464
|
+
return idx >= 0 ? entry.slice(0, idx).trim() : entry.trim();
|
|
1465
|
+
}).filter(Boolean);
|
|
1466
|
+
resultPayload.vars = keys.map(k => [k, varsObj[k]]).filter(([k, v]) => v !== undefined);
|
|
1467
|
+
}
|
|
1468
|
+
logger.result(buildSuccess(resultPayload, { kind: "get-env-result" }));
|
|
1469
|
+
process.exitCode = 0;
|
|
1470
|
+
}
|
|
1471
|
+
catch (err) {
|
|
1472
|
+
logger.result(buildError(ErrorCode.CONFIG_ERROR, {
|
|
1473
|
+
summary: "Failed to read API environment.",
|
|
1474
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1475
|
+
context: { api_name: apiName },
|
|
1476
|
+
nextCommand: `openapi-skills get-env --api ${apiName}`,
|
|
1477
|
+
}));
|
|
1478
|
+
process.exitCode = 1;
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
getEnvCmd.agentMeta = {
|
|
1482
|
+
name: "get-env",
|
|
1483
|
+
category: "Configuration",
|
|
1484
|
+
usage: "openapi-skills get-env --api <apiName>",
|
|
1485
|
+
description: "Read environment configuration for an API. Returns all values by default, or only a specific field when a flag is provided.",
|
|
1486
|
+
arguments: [
|
|
1487
|
+
{ name: "api", type: "string", required: true, flag: true, description: "The API name as defined in .openapi-skills/config.json." }
|
|
1488
|
+
],
|
|
1489
|
+
examples: [
|
|
1490
|
+
"openapi-skills get-env --api petstore",
|
|
1491
|
+
"openapi-skills get-env --api petstore --base-url",
|
|
1492
|
+
"openapi-skills get-env --api petstore --var userId",
|
|
1493
|
+
],
|
|
1494
|
+
returns: {
|
|
1495
|
+
type: "json",
|
|
1496
|
+
description: "Prints { apiName, baseUrl, authHeaders, vars } where vars is an entries array."
|
|
1497
|
+
},
|
|
1498
|
+
sideEffects: {
|
|
1499
|
+
writesFiles: false,
|
|
1500
|
+
readsFiles: true,
|
|
1501
|
+
network: false
|
|
1502
|
+
},
|
|
1503
|
+
constraints: {
|
|
1504
|
+
destructive: false,
|
|
1505
|
+
idempotent: true,
|
|
1506
|
+
requiresParsedApi: false
|
|
1507
|
+
},
|
|
1508
|
+
filesWritten: []
|
|
1509
|
+
};
|
|
1510
|
+
const getApiNamesCmd = program
|
|
1511
|
+
.command("get-api-names")
|
|
1512
|
+
.description("List all available API names (parsed OpenAPI bundles) in the project.")
|
|
1513
|
+
.action(async () => {
|
|
1514
|
+
try {
|
|
1515
|
+
const apiNames = await listApis();
|
|
1516
|
+
logger.result(buildSuccess({
|
|
1517
|
+
apiNames,
|
|
1518
|
+
}, { kind: "api-list" }));
|
|
1519
|
+
if (!apiNames || apiNames.length === 0) {
|
|
1520
|
+
logger.warn("No APIs were generated yet. Run first: openapi-skills generate <openapi-source> [options]");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
catch (error) {
|
|
1524
|
+
logger.result(buildError(ErrorCode.CONFIG_ERROR, {
|
|
1525
|
+
summary: "Failed to list APIs.",
|
|
1526
|
+
message: "Run `openapi-skills generate [options] [openapi-source]` to parse APIs first.",
|
|
1527
|
+
context: {},
|
|
1528
|
+
nextCommand: "openapi-skills generate <openapi-source>",
|
|
1529
|
+
}));
|
|
1530
|
+
process.exitCode = 1;
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
getApiNamesCmd.agentMeta = {
|
|
1534
|
+
name: "get-api-names",
|
|
1535
|
+
category: "Navigation",
|
|
1536
|
+
usage: "openapi-skills get-api-names",
|
|
1537
|
+
description: "List all available parsed API names in the project. Output is always JSON: { kind: 'api-list', apiNames: [<apiName>, ...] }.",
|
|
1538
|
+
arguments: [],
|
|
1539
|
+
examples: [
|
|
1540
|
+
"openapi-skills get-api-names"
|
|
1541
|
+
],
|
|
1542
|
+
returns: {
|
|
1543
|
+
type: "json",
|
|
1544
|
+
description: "Returns { kind: 'api-list', apiNames: [<apiName>, ...] } as JSON."
|
|
1545
|
+
},
|
|
1546
|
+
sideEffects: {
|
|
1547
|
+
writesFiles: false,
|
|
1548
|
+
readsFiles: true,
|
|
1549
|
+
network: false
|
|
1550
|
+
},
|
|
1551
|
+
constraints: {
|
|
1552
|
+
destructive: false,
|
|
1553
|
+
idempotent: true,
|
|
1554
|
+
requiresParsedApi: false
|
|
1555
|
+
},
|
|
1556
|
+
filesWritten: []
|
|
1557
|
+
};
|
|
1558
|
+
const removeApiCmd = program
|
|
1559
|
+
.command("remove-api <apiName>")
|
|
1560
|
+
.description("Remove an API from config.json and delete its .openapi-skills directory after confirmation.")
|
|
1561
|
+
.option("-y, --yes", "Confirm removal automatically without prompting.")
|
|
1562
|
+
.action(async (apiName, options) => {
|
|
1563
|
+
const targetApiName = apiName;
|
|
1564
|
+
try {
|
|
1565
|
+
const apiNames = await listApis();
|
|
1566
|
+
if (!apiNames.includes(targetApiName)) {
|
|
1567
|
+
logger.result(buildError(ErrorCode.UNKNOWN_API, {
|
|
1568
|
+
summary: `API '${targetApiName}' is not installed.`,
|
|
1569
|
+
message: `API '${targetApiName}' is not installed.`,
|
|
1570
|
+
context: { api_name: targetApiName },
|
|
1571
|
+
nextCommand: "openapi-skills get-api-names",
|
|
1572
|
+
}));
|
|
1573
|
+
process.exitCode = 1;
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const confirmed = options?.yes === true ? true : await promptDeleteConfirmation(targetApiName);
|
|
1577
|
+
if (!confirmed) {
|
|
1578
|
+
logger.result(buildSuccess({ cancelled: true, apiName: targetApiName }, { kind: "remove-api-result" }));
|
|
1579
|
+
process.exitCode = 0;
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const result = await deleteApi(targetApiName);
|
|
1583
|
+
if (result.ok) {
|
|
1584
|
+
logger.result(buildSuccess({
|
|
1585
|
+
removedApi: result.data.removedApi,
|
|
1586
|
+
message: result.message,
|
|
1587
|
+
}, { kind: "remove-api-result" }));
|
|
1588
|
+
process.exitCode = 0;
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const errorCode = /config/i.test(result.error.message)
|
|
1592
|
+
? ErrorCode.CONFIG_ERROR
|
|
1593
|
+
: result.error.type === "ApiNotFound"
|
|
1594
|
+
? ErrorCode.UNKNOWN_API
|
|
1595
|
+
: ErrorCode.REQUEST_FAILED;
|
|
1596
|
+
logger.result(buildError(errorCode, {
|
|
1597
|
+
summary: `Remove API error for ${targetApiName}.`,
|
|
1598
|
+
message: result.error.message,
|
|
1599
|
+
context: { api_name: targetApiName },
|
|
1600
|
+
nextCommand: errorCode === ErrorCode.UNKNOWN_API ? "openapi-skills get-api-names" : "None",
|
|
1601
|
+
}));
|
|
1602
|
+
process.exitCode = 1;
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
catch (error) {
|
|
1606
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1607
|
+
logger.result(buildError(/config/i.test(message) ? ErrorCode.CONFIG_ERROR : ErrorCode.REQUEST_FAILED, {
|
|
1608
|
+
summary: `Remove API error for ${targetApiName}.`,
|
|
1609
|
+
message,
|
|
1610
|
+
context: { api_name: targetApiName },
|
|
1611
|
+
nextCommand: /config/i.test(message) ? "None" : `openapi-skills remove-api ${targetApiName} --yes`,
|
|
1612
|
+
}));
|
|
1613
|
+
process.exitCode = 1;
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
removeApiCmd.agentMeta = {
|
|
1617
|
+
name: "remove-api",
|
|
1618
|
+
category: "Configuration",
|
|
1619
|
+
usage: "openapi-skills remove-api <apiName> [--yes|-y]",
|
|
1620
|
+
description: "Remove a parsed API after confirmation. The command deletes the API entry from config.json and removes the API directory under .openapi-skills.",
|
|
1621
|
+
arguments: [
|
|
1622
|
+
{ name: "apiName", type: "string", required: true, positional: true, description: "The API name to remove." },
|
|
1623
|
+
{ name: "yes", type: "flag", required: false, flag: true, description: "Confirm removal automatically without prompting." }
|
|
1624
|
+
],
|
|
1625
|
+
examples: [
|
|
1626
|
+
"openapi-skills remove-api petstore --yes"
|
|
1627
|
+
],
|
|
1628
|
+
returns: {
|
|
1629
|
+
type: "json",
|
|
1630
|
+
description: "Returns a structured success or error result for the deletion operation."
|
|
1631
|
+
},
|
|
1632
|
+
sideEffects: {
|
|
1633
|
+
writesFiles: true,
|
|
1634
|
+
readsFiles: true,
|
|
1635
|
+
network: false
|
|
1636
|
+
},
|
|
1637
|
+
constraints: {
|
|
1638
|
+
destructive: true,
|
|
1639
|
+
idempotent: false,
|
|
1640
|
+
requiresParsedApi: true
|
|
1641
|
+
},
|
|
1642
|
+
filesWritten: ["config.json"]
|
|
1643
|
+
};
|
|
1644
|
+
export { program };
|
|
1645
|
+
program.parse(process.argv);
|
|
1646
|
+
//# sourceMappingURL=cli.js.map
|