@desplega.ai/qa-use 2.8.6 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/package.json +4 -2
- package/dist/src/cli/commands/api/index.d.ts +3 -0
- package/dist/src/cli/commands/api/index.d.ts.map +1 -0
- package/dist/src/cli/commands/api/index.js +6 -0
- package/dist/src/cli/commands/api/index.js.map +1 -0
- package/dist/src/cli/commands/api/lib/http.d.ts +18 -0
- package/dist/src/cli/commands/api/lib/http.d.ts.map +1 -0
- package/dist/src/cli/commands/api/lib/http.js +51 -0
- package/dist/src/cli/commands/api/lib/http.js.map +1 -0
- package/dist/src/cli/commands/api/lib/openapi-cache.d.ts +10 -0
- package/dist/src/cli/commands/api/lib/openapi-cache.d.ts.map +1 -0
- package/dist/src/cli/commands/api/lib/openapi-cache.js +41 -0
- package/dist/src/cli/commands/api/lib/openapi-cache.js.map +1 -0
- package/dist/src/cli/commands/api/lib/openapi-errors.d.ts +8 -0
- package/dist/src/cli/commands/api/lib/openapi-errors.d.ts.map +1 -0
- package/dist/src/cli/commands/api/lib/openapi-errors.js +20 -0
- package/dist/src/cli/commands/api/lib/openapi-errors.js.map +1 -0
- package/dist/src/cli/commands/api/lib/openapi-spec.d.ts +62 -0
- package/dist/src/cli/commands/api/lib/openapi-spec.d.ts.map +1 -0
- package/dist/src/cli/commands/api/lib/openapi-spec.js +166 -0
- package/dist/src/cli/commands/api/lib/openapi-spec.js.map +1 -0
- package/dist/src/cli/commands/api/lib/output.d.ts +8 -0
- package/dist/src/cli/commands/api/lib/output.d.ts.map +1 -0
- package/dist/src/cli/commands/api/lib/output.js +31 -0
- package/dist/src/cli/commands/api/lib/output.js.map +1 -0
- package/dist/src/cli/commands/api/ls.d.ts +3 -0
- package/dist/src/cli/commands/api/ls.d.ts.map +1 -0
- package/dist/src/cli/commands/api/ls.js +76 -0
- package/dist/src/cli/commands/api/ls.js.map +1 -0
- package/dist/src/cli/commands/api/request.d.ts +11 -0
- package/dist/src/cli/commands/api/request.d.ts.map +1 -0
- package/dist/src/cli/commands/api/request.js +225 -0
- package/dist/src/cli/commands/api/request.js.map +1 -0
- package/dist/src/cli/commands/docs.d.ts +6 -0
- package/dist/src/cli/commands/docs.d.ts.map +1 -0
- package/dist/src/cli/commands/docs.js +71 -0
- package/dist/src/cli/commands/docs.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts +1 -1
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +100 -34
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/generated/docs-content.d.ts +15 -0
- package/dist/src/cli/generated/docs-content.d.ts.map +1 -0
- package/dist/src/cli/generated/docs-content.js +2420 -0
- package/dist/src/cli/generated/docs-content.js.map +1 -0
- package/dist/src/cli/index.js +50 -3
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -96,6 +96,27 @@ qa-use browser run # Interactive REPL mode
|
|
|
96
96
|
|
|
97
97
|
Run `qa-use browser --help` for the full list of 29 browser commands.
|
|
98
98
|
|
|
99
|
+
### API Commands
|
|
100
|
+
|
|
101
|
+
Dynamic API access powered by live OpenAPI (`/api/v1/openapi.json`) with local cache fallback.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
qa-use api ls # List endpoints from live/cached OpenAPI
|
|
105
|
+
qa-use api /api/v1/tests # Call endpoint (method inferred)
|
|
106
|
+
qa-use api -X POST /api/v1/tests-actions/run --input body.json
|
|
107
|
+
qa-use api ls --refresh # Force refresh OpenAPI cache
|
|
108
|
+
qa-use api ls --offline # Use cached OpenAPI only
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Command | Description |
|
|
112
|
+
|---------|-------------|
|
|
113
|
+
| `qa-use api ls` | List `/api/v1/*` endpoints from OpenAPI |
|
|
114
|
+
| `qa-use api <path>` | Send API request to endpoint |
|
|
115
|
+
| `qa-use api ... --refresh` | Force OpenAPI spec refresh |
|
|
116
|
+
| `qa-use api ... --offline` | Use cached spec without network |
|
|
117
|
+
|
|
118
|
+
If live spec fetch fails, qa-use falls back to the last cached spec and prints a stale-cache warning.
|
|
119
|
+
|
|
99
120
|
### Setup Commands
|
|
100
121
|
|
|
101
122
|
| Command | Description |
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/qa-use",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"packageManager": "bun@^1.3.4",
|
|
5
5
|
"description": "QA automation tool for browser testing with MCP server support",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,12 @@
|
|
|
37
37
|
"test:unit": "node scripts/test-unit.js",
|
|
38
38
|
"test:integration": "node scripts/test-integration.js",
|
|
39
39
|
"test:coverage": "bun test --coverage --preload ./test-setup.ts",
|
|
40
|
+
"generate:docs": "tsx scripts/generate-docs.ts",
|
|
40
41
|
"generate:readme": "node scripts/generate-readme-tools.js",
|
|
41
42
|
"generate:types": "tsx scripts/generate-types.ts",
|
|
43
|
+
"check:docs": "tsx scripts/generate-docs.ts && git diff --exit-code src/cli/generated/docs-content.ts || (echo '❌ Docs content is stale — run bun run generate:docs and commit the result' && exit 1)",
|
|
42
44
|
"release": "bun scripts/release.ts",
|
|
43
|
-
"prepublishOnly": "bun run build && bun lint && bun typecheck",
|
|
45
|
+
"prepublishOnly": "bun run generate:docs && bun run check:docs && bun run build && bun lint && bun typecheck",
|
|
44
46
|
"cc:validate": "claude plugin validate ."
|
|
45
47
|
},
|
|
46
48
|
"keywords": [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,eAAO,MAAM,UAAU,SAGtB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { lsCommand } from './ls.js';
|
|
3
|
+
import { registerApiRequestAction } from './request.js';
|
|
4
|
+
export const apiCommand = registerApiRequestAction(new Command('api').description('Call desplega.ai API endpoints using OpenAPI metadata'), { endpointRequired: false });
|
|
5
|
+
apiCommand.addCommand(lsCommand);
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/cli/commands/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAExD,MAAM,CAAC,MAAM,UAAU,GAAG,wBAAwB,CAChD,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,uDAAuD,CAAC,EACvF,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAC5B,CAAC;AAEF,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ExecuteApiRequestOptions {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
query?: Record<string, string>;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface ExecuteApiRequestResult {
|
|
11
|
+
status: number;
|
|
12
|
+
statusText: string;
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
data: unknown;
|
|
15
|
+
}
|
|
16
|
+
export declare function formatApiResponseError(status: number, statusText: string, data: unknown): string;
|
|
17
|
+
export declare function executeApiRequest(options: ExecuteApiRequestOptions): Promise<ExecuteApiRequestResult>;
|
|
18
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/http.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,OAAO,CAAC;CACf;AAMD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAYhG;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAkClC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
function normalizeApiUrl(apiUrl) {
|
|
3
|
+
return apiUrl.replace(/\/$/, '');
|
|
4
|
+
}
|
|
5
|
+
export function formatApiResponseError(status, statusText, data) {
|
|
6
|
+
if (data && typeof data === 'object') {
|
|
7
|
+
const payload = data;
|
|
8
|
+
if (payload.message && typeof payload.message === 'string') {
|
|
9
|
+
return `HTTP ${status} ${statusText}: ${payload.message}`;
|
|
10
|
+
}
|
|
11
|
+
if (payload.detail && typeof payload.detail === 'string') {
|
|
12
|
+
return `HTTP ${status} ${statusText}: ${payload.detail}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return `HTTP ${status} ${statusText}`;
|
|
16
|
+
}
|
|
17
|
+
export async function executeApiRequest(options) {
|
|
18
|
+
const requestConfig = {
|
|
19
|
+
method: options.method,
|
|
20
|
+
baseURL: normalizeApiUrl(options.apiUrl),
|
|
21
|
+
url: options.path,
|
|
22
|
+
headers: {
|
|
23
|
+
...(options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {}),
|
|
24
|
+
...options.headers,
|
|
25
|
+
},
|
|
26
|
+
params: options.query,
|
|
27
|
+
data: options.body,
|
|
28
|
+
timeout: 25000,
|
|
29
|
+
validateStatus: () => true,
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const response = await axios.request(requestConfig);
|
|
33
|
+
const headers = {};
|
|
34
|
+
for (const [key, value] of Object.entries(response.headers || {})) {
|
|
35
|
+
headers[key] = Array.isArray(value) ? value.join(', ') : String(value);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
status: response.status,
|
|
39
|
+
statusText: response.statusText,
|
|
40
|
+
headers,
|
|
41
|
+
data: response.data,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (axios.isAxiosError(error)) {
|
|
46
|
+
throw new Error(error.message);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/http.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAmB1B,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAc,EAAE,UAAkB,EAAE,IAAa;IACtF,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAA6C,CAAC;QAC9D,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC3D,OAAO,QAAQ,MAAM,IAAI,UAAU,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;QAC5D,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACzD,OAAO,QAAQ,MAAM,IAAI,UAAU,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,MAAM,IAAI,UAAU,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAiC;IAEjC,MAAM,aAAa,GAAuB;QACxC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC;QACxC,GAAG,EAAE,OAAO,CAAC,IAAI;QACjB,OAAO,EAAE;YACP,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,GAAG,OAAO,CAAC,OAAO;SACnB;QACD,MAAM,EAAE,OAAO,CAAC,KAAK;QACrB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,KAAK;QACd,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI;KAC3B,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACpD,MAAM,OAAO,GAA2B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;YAClE,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACzE,CAAC;QAED,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO;YACP,IAAI,EAAE,QAAQ,CAAC,IAAI;SACpB,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CachedOpenApiSpec {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
fetchedAt: string;
|
|
4
|
+
etag?: string;
|
|
5
|
+
specHash: string;
|
|
6
|
+
spec: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function readOpenApiCache(apiUrl: string): CachedOpenApiSpec | undefined;
|
|
9
|
+
export declare function writeOpenApiCache(entry: CachedOpenApiSpec): void;
|
|
10
|
+
//# sourceMappingURL=openapi-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-cache.d.ts","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/openapi-cache.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;CACf;AA+BD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAG9E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAShE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const CONFIG_PATH = join(homedir(), '.qa-use.json');
|
|
5
|
+
function normalizeApiUrl(apiUrl) {
|
|
6
|
+
return apiUrl.replace(/\/$/, '');
|
|
7
|
+
}
|
|
8
|
+
function readConfig() {
|
|
9
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeConfig(config) {
|
|
20
|
+
try {
|
|
21
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Ignore cache write failures (permissions, disk full, etc.)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function readOpenApiCache(apiUrl) {
|
|
28
|
+
const key = normalizeApiUrl(apiUrl);
|
|
29
|
+
return readConfig().openapi_cache?.[key];
|
|
30
|
+
}
|
|
31
|
+
export function writeOpenApiCache(entry) {
|
|
32
|
+
const key = normalizeApiUrl(entry.apiUrl);
|
|
33
|
+
const config = readConfig();
|
|
34
|
+
config.openapi_cache = config.openapi_cache || {};
|
|
35
|
+
config.openapi_cache[key] = {
|
|
36
|
+
...entry,
|
|
37
|
+
apiUrl: key,
|
|
38
|
+
};
|
|
39
|
+
writeConfig(config);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=openapi-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-cache.js","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/openapi-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;AAepD,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,UAAU;IACjB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAgB,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,MAAmB;IACtC,IAAI,CAAC;QACH,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,6DAA6D;IAC/D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,UAAU,EAAE,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAwB;IACxD,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;IAClD,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG;QAC1B,GAAG,KAAK;QACR,MAAM,EAAE,GAAG;KACZ,CAAC;IACF,WAAW,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class OpenApiError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare function formatMissingSpecError(apiUrl: string, detail?: string): string;
|
|
5
|
+
export declare function formatInvalidSpecError(detail: string): string;
|
|
6
|
+
export declare function formatStaleCacheWarning(apiUrl: string, fetchedAt: string, reason: string): string;
|
|
7
|
+
export declare function formatOfflineCacheMissError(apiUrl: string): string;
|
|
8
|
+
//# sourceMappingURL=openapi-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-errors.d.ts","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/openapi-errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9E;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjG;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class OpenApiError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'OpenApiError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function formatMissingSpecError(apiUrl, detail) {
|
|
8
|
+
const suffix = detail ? `\nReason: ${detail}` : '';
|
|
9
|
+
return `Unable to load OpenAPI spec from ${apiUrl}/api/v1/openapi.json and no cached spec is available.${suffix}\nRun the command again when online or provide credentials with QA_USE_API_KEY.`;
|
|
10
|
+
}
|
|
11
|
+
export function formatInvalidSpecError(detail) {
|
|
12
|
+
return `OpenAPI spec is invalid: ${detail}`;
|
|
13
|
+
}
|
|
14
|
+
export function formatStaleCacheWarning(apiUrl, fetchedAt, reason) {
|
|
15
|
+
return `Warning: using stale cached OpenAPI spec for ${apiUrl} (cached at ${fetchedAt}). Refresh failed: ${reason}`;
|
|
16
|
+
}
|
|
17
|
+
export function formatOfflineCacheMissError(apiUrl) {
|
|
18
|
+
return `Offline mode requested, but no cached OpenAPI spec exists for ${apiUrl}. Run with --refresh while online first.`;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=openapi-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-errors.js","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/openapi-errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAc,EAAE,MAAe;IACpE,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACnD,OAAO,oCAAoC,MAAM,wDAAwD,MAAM,iFAAiF,CAAC;AACnM,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,OAAO,4BAA4B,MAAM,EAAE,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAc,EAAE,SAAiB,EAAE,MAAc;IACvF,OAAO,gDAAgD,MAAM,eAAe,SAAS,sBAAsB,MAAM,EAAE,CAAC;AACtH,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,MAAc;IACxD,OAAO,iEAAiE,MAAM,0CAA0C,CAAC;AAC3H,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type OpenApiRefreshMode = 'default' | 'refresh' | 'offline';
|
|
2
|
+
interface OpenApiPathOperation {
|
|
3
|
+
summary?: string;
|
|
4
|
+
operationId?: string;
|
|
5
|
+
tags?: string[];
|
|
6
|
+
parameters?: Array<{
|
|
7
|
+
name?: string;
|
|
8
|
+
in?: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
schema?: {
|
|
11
|
+
type?: string;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
requestBody?: {
|
|
15
|
+
required?: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
interface OpenApiSpecDocument {
|
|
19
|
+
openapi: string;
|
|
20
|
+
paths: Record<string, Record<string, OpenApiPathOperation>>;
|
|
21
|
+
components?: {
|
|
22
|
+
securitySchemes?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface NormalizedOperation {
|
|
26
|
+
key: string;
|
|
27
|
+
method: string;
|
|
28
|
+
path: string;
|
|
29
|
+
summary?: string;
|
|
30
|
+
operationId?: string;
|
|
31
|
+
tags: string[];
|
|
32
|
+
parameters: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
in: 'path' | 'query' | 'header' | 'cookie' | 'unknown';
|
|
35
|
+
required: boolean;
|
|
36
|
+
schemaType?: string;
|
|
37
|
+
}>;
|
|
38
|
+
parameterCount: number;
|
|
39
|
+
requestBodyRequired: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface OpenApiSpecIndex {
|
|
42
|
+
raw: OpenApiSpecDocument;
|
|
43
|
+
operations: Record<string, NormalizedOperation>;
|
|
44
|
+
}
|
|
45
|
+
export interface LoadedOpenApiSpec {
|
|
46
|
+
source: 'live' | 'cache';
|
|
47
|
+
stale: boolean;
|
|
48
|
+
apiUrl: string;
|
|
49
|
+
fetchedAt: string;
|
|
50
|
+
etag?: string;
|
|
51
|
+
specHash: string;
|
|
52
|
+
index: OpenApiSpecIndex;
|
|
53
|
+
warnings: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface LoadOpenApiSpecOptions {
|
|
56
|
+
apiUrl: string;
|
|
57
|
+
apiKey?: string;
|
|
58
|
+
refreshMode?: OpenApiRefreshMode;
|
|
59
|
+
}
|
|
60
|
+
export declare function loadOpenApiSpec(options: LoadOpenApiSpecOptions): Promise<LoadedOpenApiSpec>;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=openapi-spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-spec.d.ts","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/openapi-spec.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEnE,UAAU,oBAAoB;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,MAAM,CAAC,EAAE;YACP,IAAI,CAAC,EAAE,MAAM,CAAC;SACf,CAAC;KACH,CAAC,CAAC;IACH,WAAW,CAAC,EAAE;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;CACH;AAED,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAC5D,UAAU,CAAC,EAAE;QACX,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC3C,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;QACvD,QAAQ,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,mBAAmB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAkHD,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0FjG"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readOpenApiCache, writeOpenApiCache } from './openapi-cache.js';
|
|
3
|
+
import { formatInvalidSpecError, formatMissingSpecError, formatOfflineCacheMissError, formatStaleCacheWarning, OpenApiError, } from './openapi-errors.js';
|
|
4
|
+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head']);
|
|
5
|
+
function normalizeApiUrl(apiUrl) {
|
|
6
|
+
return apiUrl.replace(/\/$/, '');
|
|
7
|
+
}
|
|
8
|
+
function buildOpenApiUrl(apiUrl) {
|
|
9
|
+
return `${normalizeApiUrl(apiUrl)}/api/v1/openapi.json`;
|
|
10
|
+
}
|
|
11
|
+
function computeSpecHash(spec) {
|
|
12
|
+
return createHash('sha256').update(JSON.stringify(spec)).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
function validateOpenApiDocument(spec) {
|
|
15
|
+
if (!spec || typeof spec !== 'object') {
|
|
16
|
+
throw new OpenApiError(formatInvalidSpecError('expected an object payload'));
|
|
17
|
+
}
|
|
18
|
+
const candidate = spec;
|
|
19
|
+
if (!candidate.openapi || typeof candidate.openapi !== 'string') {
|
|
20
|
+
throw new OpenApiError(formatInvalidSpecError('missing `openapi` version string'));
|
|
21
|
+
}
|
|
22
|
+
if (!candidate.paths || typeof candidate.paths !== 'object' || Array.isArray(candidate.paths)) {
|
|
23
|
+
throw new OpenApiError(formatInvalidSpecError('missing `paths` object'));
|
|
24
|
+
}
|
|
25
|
+
if (candidate.components &&
|
|
26
|
+
candidate.components.securitySchemes !== undefined &&
|
|
27
|
+
(typeof candidate.components.securitySchemes !== 'object' ||
|
|
28
|
+
Array.isArray(candidate.components.securitySchemes))) {
|
|
29
|
+
throw new OpenApiError(formatInvalidSpecError('`components.securitySchemes` must be an object'));
|
|
30
|
+
}
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
function normalizeOperations(spec) {
|
|
34
|
+
const operations = {};
|
|
35
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
36
|
+
if (!pathItem || typeof pathItem !== 'object') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
40
|
+
const normalizedMethod = method.toLowerCase();
|
|
41
|
+
if (!HTTP_METHODS.has(normalizedMethod)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const op = operation || {};
|
|
45
|
+
const key = `${normalizedMethod.toUpperCase()} ${path}`;
|
|
46
|
+
operations[key] = {
|
|
47
|
+
key,
|
|
48
|
+
method: normalizedMethod.toUpperCase(),
|
|
49
|
+
path,
|
|
50
|
+
summary: op.summary,
|
|
51
|
+
operationId: op.operationId,
|
|
52
|
+
tags: Array.isArray(op.tags) ? op.tags.filter((tag) => typeof tag === 'string') : [],
|
|
53
|
+
parameters: Array.isArray(op.parameters)
|
|
54
|
+
? op.parameters
|
|
55
|
+
.filter((parameter) => Boolean(parameter?.name))
|
|
56
|
+
.map((parameter) => ({
|
|
57
|
+
name: String(parameter.name),
|
|
58
|
+
in: parameter.in === 'path' ||
|
|
59
|
+
parameter.in === 'query' ||
|
|
60
|
+
parameter.in === 'header' ||
|
|
61
|
+
parameter.in === 'cookie'
|
|
62
|
+
? parameter.in
|
|
63
|
+
: 'unknown',
|
|
64
|
+
required: Boolean(parameter.required),
|
|
65
|
+
schemaType: parameter.schema?.type,
|
|
66
|
+
}))
|
|
67
|
+
: [],
|
|
68
|
+
parameterCount: Array.isArray(op.parameters) ? op.parameters.length : 0,
|
|
69
|
+
requestBodyRequired: Boolean(op.requestBody?.required),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return operations;
|
|
74
|
+
}
|
|
75
|
+
function createIndex(spec) {
|
|
76
|
+
return {
|
|
77
|
+
raw: spec,
|
|
78
|
+
operations: normalizeOperations(spec),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function fromCache(cache, stale, warning) {
|
|
82
|
+
const spec = validateOpenApiDocument(cache.spec);
|
|
83
|
+
return {
|
|
84
|
+
source: 'cache',
|
|
85
|
+
stale,
|
|
86
|
+
apiUrl: cache.apiUrl,
|
|
87
|
+
fetchedAt: cache.fetchedAt,
|
|
88
|
+
etag: cache.etag,
|
|
89
|
+
specHash: cache.specHash,
|
|
90
|
+
index: createIndex(spec),
|
|
91
|
+
warnings: warning ? [warning] : [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function loadOpenApiSpec(options) {
|
|
95
|
+
const apiUrl = normalizeApiUrl(options.apiUrl);
|
|
96
|
+
const refreshMode = options.refreshMode || 'default';
|
|
97
|
+
const cache = readOpenApiCache(apiUrl);
|
|
98
|
+
if (refreshMode === 'offline') {
|
|
99
|
+
if (!cache) {
|
|
100
|
+
throw new OpenApiError(formatOfflineCacheMissError(apiUrl));
|
|
101
|
+
}
|
|
102
|
+
return fromCache(cache, false);
|
|
103
|
+
}
|
|
104
|
+
const headers = {};
|
|
105
|
+
if (options.apiKey) {
|
|
106
|
+
headers.Authorization = `Bearer ${options.apiKey}`;
|
|
107
|
+
}
|
|
108
|
+
if (refreshMode !== 'refresh' && cache?.etag) {
|
|
109
|
+
headers['If-None-Match'] = cache.etag;
|
|
110
|
+
}
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await fetch(buildOpenApiUrl(apiUrl), {
|
|
114
|
+
method: 'GET',
|
|
115
|
+
headers,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!cache) {
|
|
120
|
+
throw new OpenApiError(formatMissingSpecError(apiUrl, error instanceof Error ? error.message : 'network failure'));
|
|
121
|
+
}
|
|
122
|
+
return fromCache(cache, true, formatStaleCacheWarning(apiUrl, cache.fetchedAt, error instanceof Error ? error.message : 'network failure'));
|
|
123
|
+
}
|
|
124
|
+
if (response.status === 304) {
|
|
125
|
+
if (!cache) {
|
|
126
|
+
throw new OpenApiError(formatMissingSpecError(apiUrl, 'received 304 without cached spec'));
|
|
127
|
+
}
|
|
128
|
+
return fromCache(cache, false);
|
|
129
|
+
}
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const reason = `HTTP ${response.status}`;
|
|
132
|
+
if (!cache) {
|
|
133
|
+
throw new OpenApiError(formatMissingSpecError(apiUrl, reason));
|
|
134
|
+
}
|
|
135
|
+
return fromCache(cache, true, formatStaleCacheWarning(apiUrl, cache.fetchedAt, reason));
|
|
136
|
+
}
|
|
137
|
+
let jsonPayload;
|
|
138
|
+
try {
|
|
139
|
+
jsonPayload = await response.json();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new OpenApiError(formatInvalidSpecError('response body is not valid JSON'));
|
|
143
|
+
}
|
|
144
|
+
const spec = validateOpenApiDocument(jsonPayload);
|
|
145
|
+
const specHash = computeSpecHash(spec);
|
|
146
|
+
const fetchedAt = new Date().toISOString();
|
|
147
|
+
const etag = response.headers.get('etag') ?? undefined;
|
|
148
|
+
writeOpenApiCache({
|
|
149
|
+
apiUrl,
|
|
150
|
+
fetchedAt,
|
|
151
|
+
etag,
|
|
152
|
+
specHash,
|
|
153
|
+
spec,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
source: 'live',
|
|
157
|
+
stale: false,
|
|
158
|
+
apiUrl,
|
|
159
|
+
fetchedAt,
|
|
160
|
+
etag,
|
|
161
|
+
specHash,
|
|
162
|
+
index: createIndex(spec),
|
|
163
|
+
warnings: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=openapi-spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-spec.js","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/openapi-spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NormalizedOperation } from './openapi-spec.js';
|
|
2
|
+
export interface RenderListOptions {
|
|
3
|
+
source: 'live' | 'cache';
|
|
4
|
+
stale: boolean;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function renderOperationList(operations: NormalizedOperation[], options: RenderListOptions): string;
|
|
8
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AASD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,mBAAmB,EAAE,EACjC,OAAO,EAAE,iBAAiB,GACzB,MAAM,CAkCR"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function pad(value, width) {
|
|
2
|
+
if (value.length >= width) {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
return `${value}${' '.repeat(width - value.length)}`;
|
|
6
|
+
}
|
|
7
|
+
export function renderOperationList(operations, options) {
|
|
8
|
+
if (options.json) {
|
|
9
|
+
return JSON.stringify({
|
|
10
|
+
source: options.source,
|
|
11
|
+
stale: options.stale,
|
|
12
|
+
count: operations.length,
|
|
13
|
+
operations,
|
|
14
|
+
}, null, 2);
|
|
15
|
+
}
|
|
16
|
+
if (operations.length === 0) {
|
|
17
|
+
return `No API operations matched your filters.\nSource: ${options.source}${options.stale ? ' (stale cache)' : ''}`;
|
|
18
|
+
}
|
|
19
|
+
const methodWidth = Math.max(6, ...operations.map((operation) => operation.method.length));
|
|
20
|
+
const pathWidth = Math.max(20, ...operations.map((operation) => operation.path.length));
|
|
21
|
+
const lines = [
|
|
22
|
+
`Source: ${options.source}${options.stale ? ' (stale cache)' : ''}`,
|
|
23
|
+
`${pad('METHOD', methodWidth)} ${pad('PATH', pathWidth)} SUMMARY`,
|
|
24
|
+
`${'-'.repeat(methodWidth)} ${'-'.repeat(pathWidth)} ${'-'.repeat(32)}`,
|
|
25
|
+
];
|
|
26
|
+
for (const operation of operations) {
|
|
27
|
+
lines.push(`${pad(operation.method, methodWidth)} ${pad(operation.path, pathWidth)} ${operation.summary || ''}`);
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../../../../../src/cli/commands/api/lib/output.ts"],"names":[],"mappings":"AAQA,SAAS,GAAG,CAAC,KAAa,EAAE,KAAa;IACvC,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,GAAG,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,UAAiC,EACjC,OAA0B;IAE1B,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,SAAS,CACnB;YACE,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,KAAK,EAAE,UAAU,CAAC,MAAM;YACxB,UAAU;SACX,EACD,IAAI,EACJ,CAAC,CACF,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,oDAAoD,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACtH,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3F,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAExF,MAAM,KAAK,GAAG;QACZ,WAAW,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,EAAE;QACnE,GAAG,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,KAAK,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,WAAW;QACnE,GAAG,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE;KAC1E,CAAC;IAEF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CACR,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,SAAS,CAAC,OAAO,IAAI,EAAE,EAAE,CACvG,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/api/ls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,eAAO,MAAM,SAAS,SAwElB,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { loadConfig } from '../../lib/config.js';
|
|
3
|
+
import { error, formatError, warning } from '../../lib/output.js';
|
|
4
|
+
import { loadOpenApiSpec } from './lib/openapi-spec.js';
|
|
5
|
+
import { renderOperationList } from './lib/output.js';
|
|
6
|
+
function getRefreshMode(options) {
|
|
7
|
+
if (options.refresh && options.offline) {
|
|
8
|
+
throw new Error('Cannot use --refresh and --offline together');
|
|
9
|
+
}
|
|
10
|
+
if (options.refresh)
|
|
11
|
+
return 'refresh';
|
|
12
|
+
if (options.offline)
|
|
13
|
+
return 'offline';
|
|
14
|
+
return 'default';
|
|
15
|
+
}
|
|
16
|
+
export const lsCommand = new Command('ls')
|
|
17
|
+
.description('List available /api/v1 endpoints from OpenAPI')
|
|
18
|
+
.option('-X, --method <method>', 'Filter by HTTP method (GET, POST, PUT, PATCH, DELETE)')
|
|
19
|
+
.option('-q, --query <text>', 'Filter by text in path, summary, or operation id')
|
|
20
|
+
.option('--tag <tag>', 'Filter by OpenAPI tag')
|
|
21
|
+
.option('--json', 'Render JSON output')
|
|
22
|
+
.option('--refresh', 'Force refresh OpenAPI spec from server')
|
|
23
|
+
.option('--offline', 'Use cached OpenAPI spec only')
|
|
24
|
+
.action(async (options, command) => {
|
|
25
|
+
try {
|
|
26
|
+
const parentOptions = (command.parent?.opts() || {});
|
|
27
|
+
const effectiveOptions = {
|
|
28
|
+
...parentOptions,
|
|
29
|
+
...options,
|
|
30
|
+
};
|
|
31
|
+
const config = await loadConfig();
|
|
32
|
+
const apiUrl = config.api_url || process.env.QA_USE_API_URL || 'https://api.desplega.ai';
|
|
33
|
+
const apiKey = config.api_key || process.env.QA_USE_API_KEY;
|
|
34
|
+
const refreshMode = getRefreshMode(effectiveOptions);
|
|
35
|
+
const loadedSpec = await loadOpenApiSpec({ apiUrl, apiKey, refreshMode });
|
|
36
|
+
if (loadedSpec.stale) {
|
|
37
|
+
for (const message of loadedSpec.warnings) {
|
|
38
|
+
console.error(warning(message));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let operations = Object.values(loadedSpec.index.operations);
|
|
42
|
+
if (effectiveOptions.method) {
|
|
43
|
+
const expectedMethod = effectiveOptions.method.toUpperCase();
|
|
44
|
+
operations = operations.filter((operation) => operation.method === expectedMethod);
|
|
45
|
+
}
|
|
46
|
+
if (effectiveOptions.query) {
|
|
47
|
+
const query = effectiveOptions.query.toLowerCase();
|
|
48
|
+
operations = operations.filter((operation) => {
|
|
49
|
+
return (operation.path.toLowerCase().includes(query) ||
|
|
50
|
+
(operation.summary || '').toLowerCase().includes(query) ||
|
|
51
|
+
(operation.operationId || '').toLowerCase().includes(query));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (effectiveOptions.tag) {
|
|
55
|
+
const tag = effectiveOptions.tag.toLowerCase();
|
|
56
|
+
operations = operations.filter((operation) => operation.tags.some((operationTag) => operationTag.toLowerCase() === tag));
|
|
57
|
+
}
|
|
58
|
+
operations.sort((a, b) => {
|
|
59
|
+
const pathCompare = a.path.localeCompare(b.path);
|
|
60
|
+
if (pathCompare !== 0) {
|
|
61
|
+
return pathCompare;
|
|
62
|
+
}
|
|
63
|
+
return a.method.localeCompare(b.method);
|
|
64
|
+
});
|
|
65
|
+
console.log(renderOperationList(operations, {
|
|
66
|
+
source: loadedSpec.source,
|
|
67
|
+
stale: loadedSpec.stale,
|
|
68
|
+
json: effectiveOptions.json,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(error(`Failed to list API operations: ${formatError(err)}`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=ls.js.map
|