@kirrosh/zond 0.16.0 → 0.17.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/CHANGELOG.md +132 -112
- package/README.md +3 -10
- package/package.json +2 -3
- package/src/cli/commands/export.ts +144 -0
- package/src/cli/commands/generate.ts +31 -0
- package/src/cli/commands/run.ts +22 -5
- package/src/cli/commands/sync.ts +240 -0
- package/src/cli/index.ts +54 -10
- package/src/core/diagnostics/db-analysis.ts +79 -7
- package/src/core/diagnostics/failure-hints.ts +39 -0
- package/src/core/exporter/postman.ts +963 -0
- package/src/core/generator/data-factory.ts +38 -3
- package/src/core/generator/index.ts +1 -1
- package/src/core/generator/openapi-reader.ts +6 -0
- package/src/core/generator/serializer.ts +17 -2
- package/src/core/generator/suite-generator.ts +163 -14
- package/src/core/generator/types.ts +1 -0
- package/src/core/meta/meta-store.ts +78 -0
- package/src/core/meta/types.ts +21 -0
- package/src/core/parser/schema.ts +12 -2
- package/src/core/parser/types.ts +12 -1
- package/src/core/parser/variables.ts +3 -0
- package/src/core/parser/yaml-parser.ts +2 -1
- package/src/core/runner/assertions.ts +44 -20
- package/src/core/runner/execute-run.ts +31 -8
- package/src/core/runner/executor.ts +34 -8
- package/src/core/runner/http-client.ts +1 -1
- package/src/core/runner/types.ts +1 -0
- package/src/core/sync/spec-differ.ts +38 -0
- package/src/cli/commands/mcp.ts +0 -16
- package/src/mcp/descriptions.ts +0 -47
- package/src/mcp/server.ts +0 -38
- package/src/mcp/tools/ci-init.ts +0 -54
- package/src/mcp/tools/coverage-analysis.ts +0 -141
- package/src/mcp/tools/describe-endpoint.ts +0 -27
- package/src/mcp/tools/manage-server.ts +0 -86
- package/src/mcp/tools/query-db.ts +0 -84
- package/src/mcp/tools/run-tests.ts +0 -116
- package/src/mcp/tools/send-request.ts +0 -51
- package/src/mcp/tools/setup-api.ts +0 -88
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
4
|
-
let serverInstance: ReturnType<typeof Bun.serve> | null = null;
|
|
5
|
-
let serverPort: number = 0;
|
|
6
|
-
|
|
7
|
-
export function registerManageServerTool(server: McpServer, dbPath?: string) {
|
|
8
|
-
server.registerTool("manage_server", {
|
|
9
|
-
description: TOOL_DESCRIPTIONS.manage_server,
|
|
10
|
-
inputSchema: {
|
|
11
|
-
action: z.enum(["start", "stop", "restart", "status"]).describe("Action to perform"),
|
|
12
|
-
port: z.optional(z.number().int().min(1).max(65535)).describe("Port number (default: 8080, only for start/restart)"),
|
|
13
|
-
},
|
|
14
|
-
}, async ({ action, port }) => {
|
|
15
|
-
const targetPort = port ?? 8080;
|
|
16
|
-
|
|
17
|
-
switch (action) {
|
|
18
|
-
case "start": {
|
|
19
|
-
if (serverInstance) {
|
|
20
|
-
return result({ running: true, port: serverPort, url: `http://localhost:${serverPort}`, message: "Server already running" });
|
|
21
|
-
}
|
|
22
|
-
return await startServer(targetPort, dbPath);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
case "stop": {
|
|
26
|
-
if (!serverInstance) {
|
|
27
|
-
return result({ running: false, message: "Server is not running" });
|
|
28
|
-
}
|
|
29
|
-
serverInstance.stop();
|
|
30
|
-
serverInstance = null;
|
|
31
|
-
const stoppedPort = serverPort;
|
|
32
|
-
serverPort = 0;
|
|
33
|
-
return result({ running: false, message: `Server stopped (was on port ${stoppedPort})` });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
case "restart": {
|
|
37
|
-
if (serverInstance) {
|
|
38
|
-
serverInstance.stop();
|
|
39
|
-
serverInstance = null;
|
|
40
|
-
serverPort = 0;
|
|
41
|
-
}
|
|
42
|
-
return await startServer(targetPort, dbPath);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
case "status": {
|
|
46
|
-
if (serverInstance) {
|
|
47
|
-
return result({ running: true, port: serverPort, url: `http://localhost:${serverPort}` });
|
|
48
|
-
}
|
|
49
|
-
return result({ running: false });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function startServer(port: number, dbPath?: string) {
|
|
56
|
-
try {
|
|
57
|
-
const { getDb } = await import("../../db/schema.ts");
|
|
58
|
-
const { createApp } = await import("../../web/server.ts");
|
|
59
|
-
|
|
60
|
-
getDb(dbPath);
|
|
61
|
-
const app = createApp();
|
|
62
|
-
|
|
63
|
-
serverInstance = Bun.serve({
|
|
64
|
-
fetch: app.fetch,
|
|
65
|
-
port,
|
|
66
|
-
hostname: "0.0.0.0",
|
|
67
|
-
});
|
|
68
|
-
serverPort = port;
|
|
69
|
-
|
|
70
|
-
return result({ running: true, port, url: `http://localhost:${port}`, message: "Server started" });
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return {
|
|
73
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
74
|
-
running: false,
|
|
75
|
-
error: (err as Error).message,
|
|
76
|
-
}, null, 2) }],
|
|
77
|
-
isError: true,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function result(data: Record<string, unknown>) {
|
|
83
|
-
return {
|
|
84
|
-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
4
|
-
import { getCollections, getRuns, getRunDetail, diagnoseRun, compareRuns } from "../../core/diagnostics/db-analysis.ts";
|
|
5
|
-
|
|
6
|
-
export function registerQueryDbTool(server: McpServer, dbPath?: string) {
|
|
7
|
-
server.registerTool("query_db", {
|
|
8
|
-
description: TOOL_DESCRIPTIONS.query_db,
|
|
9
|
-
inputSchema: {
|
|
10
|
-
action: z.enum(["list_collections", "list_runs", "get_run_results", "diagnose_failure", "compare_runs"])
|
|
11
|
-
.describe("Query action to perform"),
|
|
12
|
-
runId: z.optional(z.number().int())
|
|
13
|
-
.describe("Run ID (required for get_run_results and diagnose_failure)"),
|
|
14
|
-
runIdB: z.optional(z.number().int())
|
|
15
|
-
.describe("Second run ID (required for compare_runs — this is the newer run)"),
|
|
16
|
-
limit: z.optional(z.number().int().min(1).max(100))
|
|
17
|
-
.describe("Max number of runs to return (default: 20, only for list_runs)"),
|
|
18
|
-
verbose: z.optional(z.boolean())
|
|
19
|
-
.describe("Show full error messages and stack traces (default: false, truncates long traces)"),
|
|
20
|
-
},
|
|
21
|
-
}, async ({ action, runId, runIdB, limit, verbose }) => {
|
|
22
|
-
try {
|
|
23
|
-
switch (action) {
|
|
24
|
-
case "list_collections": {
|
|
25
|
-
const collections = getCollections(dbPath);
|
|
26
|
-
return {
|
|
27
|
-
content: [{ type: "text" as const, text: JSON.stringify(collections, null, 2) }],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
case "list_runs": {
|
|
32
|
-
const runs = getRuns(limit ?? 20, dbPath);
|
|
33
|
-
return {
|
|
34
|
-
content: [{ type: "text" as const, text: JSON.stringify(runs, null, 2) }],
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
case "get_run_results": {
|
|
39
|
-
if (runId == null) {
|
|
40
|
-
return {
|
|
41
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: "runId is required for get_run_results" }, null, 2) }],
|
|
42
|
-
isError: true,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const detail = getRunDetail(runId, verbose, dbPath);
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: "text" as const, text: JSON.stringify(detail, null, 2) }],
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
case "diagnose_failure": {
|
|
52
|
-
if (runId == null) {
|
|
53
|
-
return {
|
|
54
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: "runId is required for diagnose_failure" }, null, 2) }],
|
|
55
|
-
isError: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
const result = diagnoseRun(runId, verbose, dbPath);
|
|
59
|
-
return {
|
|
60
|
-
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
case "compare_runs": {
|
|
65
|
-
if (runId == null || runIdB == null) {
|
|
66
|
-
return {
|
|
67
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: "Both runId (run A) and runIdB (run B) are required for compare_runs" }, null, 2) }],
|
|
68
|
-
isError: true,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const compareResult = compareRuns(runId, runIdB, dbPath);
|
|
72
|
-
return {
|
|
73
|
-
content: [{ type: "text" as const, text: JSON.stringify(compareResult, null, 2) }],
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch (err) {
|
|
78
|
-
return {
|
|
79
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
80
|
-
isError: true,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { executeRun } from "../../core/runner/execute-run.ts";
|
|
5
|
-
import { getDb } from "../../db/schema.ts";
|
|
6
|
-
import { getResultsByRunId, findCollectionByTestPath } from "../../db/queries.ts";
|
|
7
|
-
import { readOpenApiSpec, extractEndpoints, scanCoveredEndpoints, filterUncoveredEndpoints } from "../../core/generator/index.ts";
|
|
8
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
9
|
-
|
|
10
|
-
export function registerRunTestsTool(server: McpServer, dbPath?: string) {
|
|
11
|
-
server.registerTool("run_tests", {
|
|
12
|
-
description: TOOL_DESCRIPTIONS.run_tests,
|
|
13
|
-
inputSchema: {
|
|
14
|
-
testPath: z.string().describe("Path to test YAML file or directory"),
|
|
15
|
-
envName: z.optional(z.string()).describe("Environment name (loads .env.<name>.yaml)"),
|
|
16
|
-
safe: z.optional(z.boolean()).describe("Run only GET tests (read-only, safe mode)"),
|
|
17
|
-
tag: z.optional(z.array(z.string())).describe("Filter suites by tag (OR logic)"),
|
|
18
|
-
envVars: z.optional(z.record(z.string(), z.string())).describe("Environment variables to inject (override env file, e.g. {\"TOKEN\": \"xxx\"})"),
|
|
19
|
-
dryRun: z.optional(z.boolean()).describe("Show requests without sending them (always exits 0)"),
|
|
20
|
-
rerunFrom: z.optional(z.number().int()).describe("Re-run only tests that failed/errored in this run ID"),
|
|
21
|
-
},
|
|
22
|
-
}, async ({ testPath, envName, safe, tag, envVars, dryRun, rerunFrom }) => {
|
|
23
|
-
// Build filter from previous failed run
|
|
24
|
-
let rerunFilter: Set<string> | undefined;
|
|
25
|
-
if (rerunFrom != null) {
|
|
26
|
-
getDb(dbPath);
|
|
27
|
-
const prevResults = getResultsByRunId(rerunFrom);
|
|
28
|
-
const failed = prevResults.filter(r => r.status === "fail" || r.status === "error");
|
|
29
|
-
if (failed.length === 0) {
|
|
30
|
-
return {
|
|
31
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: `Run ${rerunFrom} has no failures to rerun` }, null, 2) }],
|
|
32
|
-
isError: true,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
rerunFilter = new Set(failed.map(r => `${r.suite_name}::${r.test_name}`));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const { runId, results } = await executeRun({
|
|
39
|
-
testPath,
|
|
40
|
-
envName,
|
|
41
|
-
trigger: "mcp",
|
|
42
|
-
dbPath,
|
|
43
|
-
safe,
|
|
44
|
-
tag,
|
|
45
|
-
envVars,
|
|
46
|
-
dryRun,
|
|
47
|
-
rerunFilter,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const total = results.reduce((s, r) => s + r.total, 0);
|
|
51
|
-
const passed = results.reduce((s, r) => s + r.passed, 0);
|
|
52
|
-
const failed = results.reduce((s, r) => s + r.failed, 0);
|
|
53
|
-
const skipped = results.reduce((s, r) => s + r.skipped, 0);
|
|
54
|
-
|
|
55
|
-
const failedSteps = results.flatMap(r =>
|
|
56
|
-
r.steps.filter(s => s.status === "fail" || s.status === "error").map(s => ({
|
|
57
|
-
suite: r.suite_name,
|
|
58
|
-
test: s.name,
|
|
59
|
-
...(r.suite_file ? { file: r.suite_file } : {}),
|
|
60
|
-
status: s.status,
|
|
61
|
-
error: s.error,
|
|
62
|
-
assertions: s.assertions.filter(a => !a.passed).map(a => ({
|
|
63
|
-
field: a.field,
|
|
64
|
-
expected: a.expected,
|
|
65
|
-
actual: a.actual,
|
|
66
|
-
})),
|
|
67
|
-
}))
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
// Best-effort coverage calculation
|
|
71
|
-
let coverage: { covered: number; total: number; percentage: number } | undefined;
|
|
72
|
-
try {
|
|
73
|
-
const resolvedPath = resolve(testPath);
|
|
74
|
-
const collection = findCollectionByTestPath(resolvedPath);
|
|
75
|
-
if (collection?.openapi_spec) {
|
|
76
|
-
const doc = await readOpenApiSpec(collection.openapi_spec);
|
|
77
|
-
const allEndpoints = extractEndpoints(doc);
|
|
78
|
-
const coveredEps = await scanCoveredEndpoints(collection.test_path);
|
|
79
|
-
const uncovered = filterUncoveredEndpoints(allEndpoints, coveredEps);
|
|
80
|
-
const coveredCount = allEndpoints.length - uncovered.length;
|
|
81
|
-
coverage = {
|
|
82
|
-
covered: coveredCount,
|
|
83
|
-
total: allEndpoints.length,
|
|
84
|
-
percentage: allEndpoints.length > 0 ? Math.round((coveredCount / allEndpoints.length) * 100) : 100,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
} catch { /* coverage is best-effort, don't fail run */ }
|
|
88
|
-
|
|
89
|
-
const hints: string[] = [];
|
|
90
|
-
if (failedSteps.length > 0) {
|
|
91
|
-
hints.push("Use query_db(action: 'diagnose_failure', runId: " + runId + ") for detailed failure analysis");
|
|
92
|
-
const hasAssertionFailures = failedSteps.some(s => s.assertions.length > 0);
|
|
93
|
-
if (hasAssertionFailures) {
|
|
94
|
-
hints.push(
|
|
95
|
-
"Some tests have assertion failures — use describe_endpoint(specPath, method, path) to verify expected schemas"
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const summary = {
|
|
100
|
-
runId,
|
|
101
|
-
total,
|
|
102
|
-
passed,
|
|
103
|
-
failed,
|
|
104
|
-
skipped,
|
|
105
|
-
suites: results.length,
|
|
106
|
-
status: failed === 0 ? "all_passed" : "has_failures",
|
|
107
|
-
...(failedSteps.length > 0 ? { failures: failedSteps } : {}),
|
|
108
|
-
...(coverage ? { coverage } : {}),
|
|
109
|
-
hints,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
content: [{ type: "text" as const, text: JSON.stringify(summary, null, 2) }],
|
|
114
|
-
};
|
|
115
|
-
});
|
|
116
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { sendAdHocRequest } from "../../core/runner/send-request.ts";
|
|
4
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
5
|
-
|
|
6
|
-
export function registerSendRequestTool(server: McpServer, dbPath?: string) {
|
|
7
|
-
server.registerTool("send_request", {
|
|
8
|
-
description: TOOL_DESCRIPTIONS.send_request,
|
|
9
|
-
inputSchema: {
|
|
10
|
-
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
|
|
11
|
-
url: z.string().describe("Request URL (supports {{variable}} interpolation)"),
|
|
12
|
-
headers: z.optional(z.string()).describe("Request headers as JSON string (e.g. '{\"Content-Type\": \"application/json\"}')"),
|
|
13
|
-
body: z.optional(z.string()).describe("Request body (JSON string)"),
|
|
14
|
-
timeout: z.optional(z.number().int().positive()).describe("Request timeout in ms"),
|
|
15
|
-
envName: z.optional(z.string()).describe("Environment name for variable interpolation"),
|
|
16
|
-
collectionName: z.optional(z.string()).describe("Collection name to load env from its base_dir (e.g. 'petstore'). Required for {{variable}} interpolation."),
|
|
17
|
-
jsonPath: z.optional(z.string()).describe("Simple dot-notation path to extract from response body (e.g. '[0].code', 'data.items', 'id'). Supports array indices."),
|
|
18
|
-
maxResponseChars: z.optional(z.number().int().positive()).describe("Truncate response body to this many characters"),
|
|
19
|
-
},
|
|
20
|
-
}, async ({ method, url, headers, body, timeout, envName, collectionName, jsonPath, maxResponseChars }) => {
|
|
21
|
-
try {
|
|
22
|
-
const parsedHeaders = headers ? JSON.parse(headers) as Record<string, string> : undefined;
|
|
23
|
-
|
|
24
|
-
const result = await sendAdHocRequest({
|
|
25
|
-
method,
|
|
26
|
-
url,
|
|
27
|
-
headers: parsedHeaders,
|
|
28
|
-
body: body ?? undefined,
|
|
29
|
-
timeout,
|
|
30
|
-
envName,
|
|
31
|
-
collectionName,
|
|
32
|
-
jsonPath,
|
|
33
|
-
dbPath,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
let text = JSON.stringify(result, null, 2);
|
|
37
|
-
if (maxResponseChars && text.length > maxResponseChars) {
|
|
38
|
-
text = text.slice(0, maxResponseChars) + '\n\u2026[truncated]';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: "text" as const, text }],
|
|
43
|
-
};
|
|
44
|
-
} catch (err) {
|
|
45
|
-
return {
|
|
46
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
47
|
-
isError: true,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { setupApi } from "../../core/setup-api.ts";
|
|
6
|
-
import { resetDb } from "../../db/schema.ts";
|
|
7
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
8
|
-
|
|
9
|
-
function findProjectRoot(fromPath: string): string | null {
|
|
10
|
-
let current = existsSync(fromPath) ? fromPath : dirname(fromPath);
|
|
11
|
-
while (true) {
|
|
12
|
-
if (existsSync(join(current, ".git")) || existsSync(join(current, "package.json"))) {
|
|
13
|
-
return current;
|
|
14
|
-
}
|
|
15
|
-
const parent = dirname(current);
|
|
16
|
-
if (parent === current) return null;
|
|
17
|
-
current = parent;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function registerSetupApiTool(server: McpServer, dbPath?: string) {
|
|
22
|
-
server.registerTool("setup_api", {
|
|
23
|
-
description: TOOL_DESCRIPTIONS.setup_api,
|
|
24
|
-
inputSchema: {
|
|
25
|
-
name: z.optional(z.string()).describe("API name (auto-detected from spec title if omitted)"),
|
|
26
|
-
specPath: z.optional(z.string()).describe("Path or URL to OpenAPI spec"),
|
|
27
|
-
dir: z.optional(z.string()).describe("Base directory (default: ./apis/<name>/)"),
|
|
28
|
-
envVars: z.optional(z.string()).describe("Environment variables as JSON string (e.g. '{\"base_url\": \"...\", \"token\": \"...\"}')"),
|
|
29
|
-
force: z.optional(z.boolean()).describe("If true, delete existing API with same name and recreate from scratch"),
|
|
30
|
-
insecure: z.optional(z.boolean()).describe("Skip TLS certificate verification when fetching spec over HTTPS (for self-signed certs)"),
|
|
31
|
-
},
|
|
32
|
-
}, async ({ name, specPath, dir, envVars, force, insecure }) => {
|
|
33
|
-
try {
|
|
34
|
-
let parsedEnvVars: Record<string, string> | undefined;
|
|
35
|
-
if (envVars) {
|
|
36
|
-
try {
|
|
37
|
-
parsedEnvVars = JSON.parse(envVars);
|
|
38
|
-
} catch {
|
|
39
|
-
return {
|
|
40
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: "envVars must be a valid JSON string" }, null, 2) }],
|
|
41
|
-
isError: true,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Auto-chdir to project root when dir is an absolute path
|
|
47
|
-
if (dir && isAbsolute(resolve(dir))) {
|
|
48
|
-
const resolvedDir = resolve(dir);
|
|
49
|
-
const root = findProjectRoot(resolvedDir);
|
|
50
|
-
if (root && root !== process.cwd()) {
|
|
51
|
-
process.chdir(root);
|
|
52
|
-
resetDb();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const result = await setupApi({
|
|
57
|
-
name,
|
|
58
|
-
spec: specPath,
|
|
59
|
-
dir,
|
|
60
|
-
envVars: parsedEnvVars,
|
|
61
|
-
dbPath,
|
|
62
|
-
force,
|
|
63
|
-
insecure,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const envFilePath = join(result.baseDir, ".env.yaml");
|
|
67
|
-
const warningSteps = result.warnings?.map(w => `WARNING: ${w}`) ?? [];
|
|
68
|
-
const response = {
|
|
69
|
-
...result,
|
|
70
|
-
nextSteps: [
|
|
71
|
-
...warningSteps,
|
|
72
|
-
`Edit ${envFilePath} to add credentials (auth_token, api_key, base_url, etc.)`,
|
|
73
|
-
`File is already git-ignored via .gitignore`,
|
|
74
|
-
`Then run: run_tests(testPath: "${result.testPath}")`,
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
|
|
80
|
-
};
|
|
81
|
-
} catch (err) {
|
|
82
|
-
return {
|
|
83
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
84
|
-
isError: true,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|