@kirrosh/zond 0.13.0 → 0.16.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -7
  4. package/src/cli/commands/ci-init.ts +12 -1
  5. package/src/cli/commands/coverage.ts +21 -1
  6. package/src/cli/commands/db.ts +121 -0
  7. package/src/cli/commands/describe.ts +60 -0
  8. package/src/cli/commands/generate.ts +127 -0
  9. package/src/cli/commands/guide.ts +127 -0
  10. package/src/cli/commands/init.ts +50 -77
  11. package/src/cli/commands/request.ts +57 -0
  12. package/src/cli/commands/run.ts +53 -10
  13. package/src/cli/commands/serve.ts +62 -3
  14. package/src/cli/commands/validate.ts +18 -2
  15. package/src/cli/index.ts +213 -215
  16. package/src/cli/json-envelope.ts +19 -0
  17. package/src/core/diagnostics/db-analysis.ts +351 -0
  18. package/src/core/diagnostics/failure-hints.ts +1 -0
  19. package/src/core/generator/data-factory.ts +19 -8
  20. package/src/core/generator/describe.ts +250 -0
  21. package/src/core/generator/guide-builder.ts +20 -0
  22. package/src/core/generator/index.ts +0 -3
  23. package/src/core/generator/suite-generator.ts +133 -20
  24. package/src/core/runner/executor.ts +1 -0
  25. package/src/core/runner/send-request.ts +94 -0
  26. package/src/core/runner/types.ts +1 -0
  27. package/src/db/queries.ts +4 -2
  28. package/src/db/schema.ts +11 -3
  29. package/src/mcp/descriptions.ts +0 -24
  30. package/src/mcp/server.ts +1 -8
  31. package/src/mcp/tools/describe-endpoint.ts +3 -218
  32. package/src/mcp/tools/query-db.ts +6 -222
  33. package/src/mcp/tools/run-tests.ts +1 -0
  34. package/src/mcp/tools/send-request.ts +15 -61
  35. package/src/web/views/suites-tab.ts +1 -1
  36. package/src/cli/commands/add-api.ts +0 -53
  37. package/src/cli/commands/ai-generate.ts +0 -106
  38. package/src/cli/commands/chat.ts +0 -43
  39. package/src/cli/commands/collections.ts +0 -41
  40. package/src/cli/commands/compare.ts +0 -129
  41. package/src/cli/commands/doctor.ts +0 -127
  42. package/src/cli/commands/runs.ts +0 -108
  43. package/src/cli/commands/update.ts +0 -142
  44. package/src/core/agent/agent-loop.ts +0 -116
  45. package/src/core/agent/context-manager.ts +0 -41
  46. package/src/core/agent/system-prompt.ts +0 -27
  47. package/src/core/agent/tools/diagnose-failure.ts +0 -51
  48. package/src/core/agent/tools/index.ts +0 -42
  49. package/src/core/agent/tools/query-results.ts +0 -40
  50. package/src/core/agent/tools/run-tests.ts +0 -38
  51. package/src/core/agent/tools/send-request.ts +0 -44
  52. package/src/core/agent/types.ts +0 -22
  53. package/src/core/generator/ai/ai-generator.ts +0 -61
  54. package/src/core/generator/ai/llm-client.ts +0 -159
  55. package/src/core/generator/ai/output-parser.ts +0 -307
  56. package/src/core/generator/ai/prompt-builder.ts +0 -153
  57. package/src/core/generator/ai/types.ts +0 -56
  58. package/src/mcp/tools/generate-and-save.ts +0 -202
  59. package/src/mcp/tools/save-test-suite.ts +0 -218
  60. package/src/mcp/tools/set-work-dir.ts +0 -35
  61. package/src/tui/chat-ui.ts +0 -150
@@ -1,63 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { getDb } from "../../db/schema.ts";
4
- import { listCollections, listRuns, getRunById, getResultsByRunId, getCollectionById } from "../../db/queries.ts";
5
- import { join } from "node:path";
6
3
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
7
- import { statusHint, classifyFailure, envHint, envCategory, schemaHint } from "../../core/diagnostics/failure-hints.ts";
8
-
9
- function truncateErrorMessage(raw: string | null | undefined, verbose?: boolean): string | undefined {
10
- if (!raw) return undefined;
11
- if (verbose || raw.length < 500) return raw;
12
- const lines = raw.split(/\r?\n/);
13
- // First line is the error message itself
14
- const msgLines = [lines[0]!];
15
- // Grab up to 3 stack-trace lines (indented or starting with "at ")
16
- let traceCount = 0;
17
- for (let i = 1; i < lines.length && traceCount < 3; i++) {
18
- const line = lines[i]!;
19
- if (/^\s+/.test(line) || /^\s*at\s/.test(line)) {
20
- msgLines.push(line);
21
- traceCount++;
22
- }
23
- }
24
- const remaining = lines.length - msgLines.length;
25
- if (remaining > 0) {
26
- msgLines.push(`...[truncated ${remaining} lines]`);
27
- }
28
- return msgLines.join("\n");
29
- }
30
-
31
- function parseBodySafe(raw: string | null | undefined): unknown {
32
- if (!raw) return undefined;
33
- const truncated = raw.length > 2000 ? raw.slice(0, 2000) + "…[truncated]" : raw;
34
- try {
35
- return JSON.parse(raw);
36
- } catch {
37
- return truncated;
38
- }
39
- }
40
-
41
- const USEFUL_HEADERS = new Set([
42
- "content-type", "content-length", "location", "retry-after",
43
- "www-authenticate", "allow",
44
- ]);
45
- const USEFUL_PREFIXES = ["x-", "ratelimit"];
46
-
47
- function filterHeaders(raw: string | null | undefined): Record<string, string> | undefined {
48
- if (!raw) return undefined;
49
- try {
50
- const h = JSON.parse(raw) as Record<string, string>;
51
- const out: Record<string, string> = {};
52
- for (const [k, v] of Object.entries(h)) {
53
- const l = k.toLowerCase();
54
- if (USEFUL_HEADERS.has(l) || USEFUL_PREFIXES.some(p => l.startsWith(p))) {
55
- out[k] = v;
56
- }
57
- }
58
- return Object.keys(out).length > 0 ? out : undefined;
59
- } catch { return undefined; }
60
- }
4
+ import { getCollections, getRuns, getRunDetail, diagnoseRun, compareRuns } from "../../core/diagnostics/db-analysis.ts";
61
5
 
62
6
  export function registerQueryDbTool(server: McpServer, dbPath?: string) {
63
7
  server.registerTool("query_db", {
@@ -76,18 +20,16 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
76
20
  },
77
21
  }, async ({ action, runId, runIdB, limit, verbose }) => {
78
22
  try {
79
- getDb(dbPath);
80
-
81
23
  switch (action) {
82
24
  case "list_collections": {
83
- const collections = listCollections();
25
+ const collections = getCollections(dbPath);
84
26
  return {
85
27
  content: [{ type: "text" as const, text: JSON.stringify(collections, null, 2) }],
86
28
  };
87
29
  }
88
30
 
89
31
  case "list_runs": {
90
- const runs = listRuns(limit ?? 20);
32
+ const runs = getRuns(limit ?? 20, dbPath);
91
33
  return {
92
34
  content: [{ type: "text" as const, text: JSON.stringify(runs, null, 2) }],
93
35
  };
@@ -100,39 +42,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
100
42
  isError: true,
101
43
  };
102
44
  }
103
- const run = getRunById(runId);
104
- if (!run) {
105
- return {
106
- content: [{ type: "text" as const, text: JSON.stringify({ error: `Run ${runId} not found` }, null, 2) }],
107
- isError: true,
108
- };
109
- }
110
- const results = getResultsByRunId(runId);
111
- const detail = {
112
- run: {
113
- id: run.id,
114
- started_at: run.started_at,
115
- finished_at: run.finished_at,
116
- total: run.total,
117
- passed: run.passed,
118
- failed: run.failed,
119
- skipped: run.skipped,
120
- trigger: run.trigger,
121
- environment: run.environment,
122
- duration_ms: run.duration_ms,
123
- },
124
- results: results.map(r => ({
125
- suite_name: r.suite_name,
126
- test_name: r.test_name,
127
- status: r.status,
128
- duration_ms: r.duration_ms,
129
- request_method: r.request_method,
130
- request_url: r.request_url,
131
- response_status: r.response_status,
132
- error_message: truncateErrorMessage(r.error_message, verbose),
133
- assertions: r.assertions,
134
- })),
135
- };
45
+ const detail = getRunDetail(runId, verbose, dbPath);
136
46
  return {
137
47
  content: [{ type: "text" as const, text: JSON.stringify(detail, null, 2) }],
138
48
  };
@@ -145,81 +55,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
145
55
  isError: true,
146
56
  };
147
57
  }
148
- const diagRun = getRunById(runId);
149
- if (!diagRun) {
150
- return {
151
- content: [{ type: "text" as const, text: JSON.stringify({ error: `Run ${runId} not found` }, null, 2) }],
152
- isError: true,
153
- };
154
- }
155
-
156
- // Resolve env file path from collection for actionable hints
157
- let envFilePath: string | undefined;
158
- if (diagRun.collection_id) {
159
- const collection = getCollectionById(diagRun.collection_id);
160
- if (collection?.base_dir) {
161
- envFilePath = join(collection.base_dir, ".env.yaml").replace(/\\/g, "/");
162
- }
163
- }
164
-
165
- const allResults = getResultsByRunId(runId);
166
- const failures = allResults
167
- .filter(r => r.status === "fail" || r.status === "error")
168
- .map(r => {
169
- // env issues take priority over generic status hints
170
- const hint = envHint(r.request_url, r.error_message, envFilePath) ?? statusHint(r.response_status);
171
- const failure_type = classifyFailure(r.status, r.response_status);
172
- const sHint = schemaHint(failure_type, r.response_status);
173
- return {
174
- suite_name: r.suite_name,
175
- test_name: r.test_name,
176
- status: r.status,
177
- failure_type,
178
- error_message: truncateErrorMessage(r.error_message, verbose),
179
- request_method: r.request_method,
180
- request_url: r.request_url,
181
- response_status: r.response_status,
182
- ...(hint ? { hint } : {}),
183
- ...(sHint ? { schema_hint: sHint } : {}),
184
- response_body: parseBodySafe(r.response_body),
185
- response_headers: filterHeaders(r.response_headers),
186
- assertions: r.assertions,
187
- duration_ms: r.duration_ms,
188
- };
189
- });
190
-
191
- // Top-level env_issue when all failures have the same env problem category
192
- const categories = new Set(failures.map(f => envCategory(f.hint)).filter(Boolean));
193
- const sharedEnvHint = categories.size === 1
194
- ? categories.has("base_url_missing")
195
- ? `All failures: base_url is not set — add base_url to ${envFilePath ?? ".env.yaml"}`
196
- : categories.has("unresolved_variable")
197
- ? `All failures: some variables are not substituted — check variable names in ${envFilePath ?? ".env.yaml"}`
198
- : [...failures.map(f => f.hint).filter(Boolean)][0]
199
- : undefined;
200
-
201
- const apiErrors = failures.filter(f => f.failure_type === "api_error").length;
202
- const assertionFailures = failures.filter(f => f.failure_type === "assertion_failed").length;
203
- const networkErrors = failures.filter(f => f.failure_type === "network_error").length;
204
-
205
- const result = {
206
- run: {
207
- id: diagRun.id,
208
- started_at: diagRun.started_at,
209
- environment: diagRun.environment,
210
- duration_ms: diagRun.duration_ms,
211
- },
212
- summary: {
213
- total: diagRun.total,
214
- passed: diagRun.passed,
215
- failed: diagRun.failed,
216
- api_errors: apiErrors,
217
- assertion_failures: assertionFailures,
218
- network_errors: networkErrors,
219
- },
220
- ...(sharedEnvHint ? { env_issue: sharedEnvHint } : {}),
221
- failures,
222
- };
58
+ const result = diagnoseRun(runId, verbose, dbPath);
223
59
  return {
224
60
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
225
61
  };
@@ -232,59 +68,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
232
68
  isError: true,
233
69
  };
234
70
  }
235
- const runARecord = getRunById(runId);
236
- const runBRecord = getRunById(runIdB);
237
- if (!runARecord) {
238
- return {
239
- content: [{ type: "text" as const, text: JSON.stringify({ error: `Run #${runId} not found` }, null, 2) }],
240
- isError: true,
241
- };
242
- }
243
- if (!runBRecord) {
244
- return {
245
- content: [{ type: "text" as const, text: JSON.stringify({ error: `Run #${runIdB} not found` }, null, 2) }],
246
- isError: true,
247
- };
248
- }
249
-
250
- const resultsA = getResultsByRunId(runId);
251
- const resultsB = getResultsByRunId(runIdB);
252
-
253
- const mapA = new Map<string, string>();
254
- const mapB = new Map<string, string>();
255
- for (const r of resultsA) mapA.set(`${r.suite_name}::${r.test_name}`, r.status);
256
- for (const r of resultsB) mapB.set(`${r.suite_name}::${r.test_name}`, r.status);
257
-
258
- const regressions: Array<{ suite: string; test: string; before: string; after: string }> = [];
259
- const fixes: Array<{ suite: string; test: string; before: string; after: string }> = [];
260
- let unchanged = 0;
261
- let newTests = 0;
262
- let removedTests = 0;
263
-
264
- for (const [key, statusB] of mapB) {
265
- const statusA = mapA.get(key);
266
- if (statusA === undefined) { newTests++; continue; }
267
- const [suite, test] = key.split("::") as [string, string];
268
- const wasPass = statusA === "pass";
269
- const isPass = statusB === "pass";
270
- const wasFail = statusA === "fail" || statusA === "error";
271
- const isFail = statusB === "fail" || statusB === "error";
272
- if (wasPass && isFail) regressions.push({ suite, test, before: statusA, after: statusB });
273
- else if (wasFail && isPass) fixes.push({ suite, test, before: statusA, after: statusB });
274
- else unchanged++;
275
- }
276
- for (const key of mapA.keys()) {
277
- if (!mapB.has(key)) removedTests++;
278
- }
279
-
280
- const compareResult = {
281
- runA: { id: runId, started_at: runARecord.started_at },
282
- runB: { id: runIdB, started_at: runBRecord.started_at },
283
- summary: { regressions: regressions.length, fixes: fixes.length, unchanged, newTests, removedTests },
284
- regressions,
285
- fixes,
286
- hasRegressions: regressions.length > 0,
287
- };
71
+ const compareResult = compareRuns(runId, runIdB, dbPath);
288
72
  return {
289
73
  content: [{ type: "text" as const, text: JSON.stringify(compareResult, null, 2) }],
290
74
  };
@@ -56,6 +56,7 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
56
56
  r.steps.filter(s => s.status === "fail" || s.status === "error").map(s => ({
57
57
  suite: r.suite_name,
58
58
  test: s.name,
59
+ ...(r.suite_file ? { file: r.suite_file } : {}),
59
60
  status: s.status,
60
61
  error: s.error,
61
62
  assertions: s.assertions.filter(a => !a.passed).map(a => ({
@@ -1,29 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { executeRequest } from "../../core/runner/http-client.ts";
4
- import { loadEnvironment, substituteString, substituteDeep } from "../../core/parser/variables.ts";
5
- import { getDb } from "../../db/schema.ts";
6
- import { findCollectionByNameOrId } from "../../db/queries.ts";
3
+ import { sendAdHocRequest } from "../../core/runner/send-request.ts";
7
4
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
8
5
 
9
- function extractByPath(obj: unknown, path: string): unknown {
10
- const segments = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
11
- let current: unknown = obj;
12
- for (const seg of segments) {
13
- if (current === null || current === undefined) return undefined;
14
- if (Array.isArray(current)) {
15
- const idx = parseInt(seg, 10);
16
- if (isNaN(idx)) return undefined;
17
- current = current[idx];
18
- } else if (typeof current === 'object') {
19
- current = (current as Record<string, unknown>)[seg];
20
- } else {
21
- return undefined;
22
- }
23
- }
24
- return current;
25
- }
26
-
27
6
  export function registerSendRequestTool(server: McpServer, dbPath?: string) {
28
7
  server.registerTool("send_request", {
29
8
  description: TOOL_DESCRIPTIONS.send_request,
@@ -40,48 +19,23 @@ export function registerSendRequestTool(server: McpServer, dbPath?: string) {
40
19
  },
41
20
  }, async ({ method, url, headers, body, timeout, envName, collectionName, jsonPath, maxResponseChars }) => {
42
21
  try {
43
- let searchDir = process.cwd();
44
- if (collectionName) {
45
- getDb(dbPath);
46
- const col = findCollectionByNameOrId(collectionName);
47
- if (col?.base_dir) searchDir = col.base_dir;
48
- }
49
- const vars = await loadEnvironment(envName, searchDir);
50
-
51
- const resolvedUrl = substituteString(url, vars) as string;
52
- const parsedHeaders = headers ? JSON.parse(headers) as Record<string, string> : {};
53
- const resolvedHeaders = Object.keys(parsedHeaders).length > 0 ? substituteDeep(parsedHeaders, vars) : {};
54
- const resolvedBody = body ? substituteString(body, vars) as string : undefined;
55
-
56
- const response = await executeRequest(
57
- {
58
- method,
59
- url: resolvedUrl,
60
- headers: resolvedHeaders,
61
- body: resolvedBody,
62
- },
63
- timeout ? { timeout } : undefined,
64
- );
65
-
66
- let responseBody: unknown = response.body_parsed ?? response.body;
67
-
68
- // Apply jsonPath filter
69
- if (jsonPath && responseBody !== undefined) {
70
- responseBody = extractByPath(responseBody, jsonPath);
71
- }
72
-
73
- const result = {
74
- status: response.status,
75
- headers: response.headers,
76
- body: responseBody,
77
- duration_ms: response.duration_ms,
78
- };
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
+ });
79
35
 
80
36
  let text = JSON.stringify(result, null, 2);
81
-
82
- // Apply maxResponseChars truncation
83
37
  if (maxResponseChars && text.length > maxResponseChars) {
84
- text = text.slice(0, maxResponseChars) + '\n[truncated]';
38
+ text = text.slice(0, maxResponseChars) + '\n\u2026[truncated]';
85
39
  }
86
40
 
87
41
  return {
@@ -8,7 +8,7 @@ import { basename } from "node:path";
8
8
 
9
9
  export function renderSuitesTab(state: CollectionState): string {
10
10
  if (state.suites.length === 0) {
11
- return `<div class="tab-empty">No test suites found on disk. Generate tests with <code>generate_and_save</code>.</div>`;
11
+ return `<div class="tab-empty">No test suites found on disk. Generate tests with <code>zond guide</code> or use the test-generation skill.</div>`;
12
12
  }
13
13
 
14
14
  const rows = state.suites.map((s, i) => renderSuiteRow(s, i)).join("");
@@ -1,53 +0,0 @@
1
- import { setupApi } from "../../core/setup-api.ts";
2
- import { printError, printSuccess } from "../output.ts";
3
-
4
- export interface AddApiOptions {
5
- name: string;
6
- spec?: string;
7
- dir?: string;
8
- envPairs?: string[];
9
- dbPath?: string;
10
- insecure?: boolean;
11
- }
12
-
13
- export async function addApiCommand(options: AddApiOptions): Promise<number> {
14
- const { name, spec, envPairs, dbPath, dir, insecure } = options;
15
-
16
- // Parse --env key=value pairs into a record
17
- const envVars: Record<string, string> = {};
18
- if (envPairs) {
19
- for (const pair of envPairs) {
20
- const idx = pair.indexOf("=");
21
- if (idx === -1) continue;
22
- const key = pair.slice(0, idx).trim();
23
- const value = pair.slice(idx + 1).trim();
24
- if (key) envVars[key] = value;
25
- }
26
- }
27
-
28
- try {
29
- const result = await setupApi({
30
- name,
31
- spec,
32
- dir,
33
- envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
34
- dbPath,
35
- insecure,
36
- });
37
-
38
- printSuccess(`API '${name}' created (id=${result.collectionId})`);
39
- console.log(` Directory: ${result.testPath.replace(/\/tests$/, "")}`);
40
- console.log(` Tests: ${result.testPath}/`);
41
- if (spec) console.log(` Spec: ${spec}`);
42
- if (result.baseUrl) console.log(` Base URL: ${result.baseUrl}`);
43
- console.log();
44
- console.log("Next steps:");
45
- console.log(` zond ai-generate --api ${name} --prompt "test the user endpoints"`);
46
- console.log(` zond run --api ${name}`);
47
-
48
- return 0;
49
- } catch (err) {
50
- printError((err as Error).message);
51
- return 1;
52
- }
53
- }
@@ -1,106 +0,0 @@
1
- import { resolve, dirname } from "path";
2
- import { generateWithAI } from "../../core/generator/ai/ai-generator.ts";
3
- import { resolveProviderConfig } from "../../core/generator/ai/types.ts";
4
- import type { AIProviderConfig } from "../../core/generator/ai/types.ts";
5
- import { printError, printSuccess } from "../output.ts";
6
-
7
- export interface AIGenerateCommandOptions {
8
- from: string;
9
- prompt: string;
10
- provider: string;
11
- model?: string;
12
- apiKey?: string;
13
- baseUrl?: string;
14
- output?: string;
15
- }
16
-
17
- export async function aiGenerateCommand(options: AIGenerateCommandOptions): Promise<number> {
18
- try {
19
- const providerName = options.provider as AIProviderConfig["provider"];
20
- if (!["ollama", "openai", "anthropic", "custom"].includes(providerName)) {
21
- printError(`Unknown provider: ${options.provider}. Use: ollama, openai, anthropic, custom`);
22
- return 2;
23
- }
24
-
25
- const provider = resolveProviderConfig({
26
- provider: providerName,
27
- model: options.model,
28
- baseUrl: options.baseUrl,
29
- apiKey: options.apiKey ?? process.env.ZOND_AI_KEY,
30
- });
31
-
32
- console.log(`Provider: ${provider.provider} (${provider.model})`);
33
- console.log(`Spec: ${options.from}`);
34
- console.log(`Prompt: ${options.prompt}`);
35
- console.log(`Generating...`);
36
-
37
- const startTime = Date.now();
38
- const result = await generateWithAI({
39
- specPath: options.from,
40
- prompt: options.prompt,
41
- provider,
42
- });
43
- const durationMs = Date.now() - startTime;
44
-
45
- console.log(`Done in ${(durationMs / 1000).toFixed(1)}s (model: ${result.model})`);
46
- if (result.promptTokens) {
47
- console.log(`Tokens: ${result.promptTokens} prompt + ${result.completionTokens} completion`);
48
- }
49
-
50
- // Write output
51
- const outputDir = options.output ?? "./generated/ai/";
52
- const { mkdir } = await import("node:fs/promises");
53
- await mkdir(outputDir, { recursive: true });
54
-
55
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
56
- const fileName = `ai-generated-${timestamp}.yaml`;
57
- const filePath = resolve(outputDir, fileName);
58
-
59
- await Bun.write(filePath, result.yaml);
60
- printSuccess(`Written: ${filePath}`);
61
-
62
- // Auto-create collection if DB is available
63
- try {
64
- const { getDb } = await import("../../db/schema.ts");
65
- getDb();
66
- const { findCollectionByTestPath, findCollectionBySpec, createCollection, normalizePath, saveAIGeneration } = await import("../../db/queries.ts");
67
- const { resolveSpecPath } = await import("../../core/generator/serializer.ts");
68
- const normalizedOutput = normalizePath(outputDir);
69
- const resolvedSpec = resolveSpecPath(options.from);
70
-
71
- let collectionId: number | undefined;
72
- const existing = findCollectionByTestPath(normalizedOutput) ?? findCollectionBySpec(resolvedSpec);
73
- if (existing) {
74
- collectionId = existing.id;
75
- } else {
76
- const specName = `AI Tests (${new Date().toLocaleDateString()})`;
77
- collectionId = createCollection({
78
- name: specName,
79
- test_path: normalizedOutput,
80
- openapi_spec: resolvedSpec,
81
- });
82
- printSuccess(`Created collection "${specName}" (id: ${collectionId})`);
83
- }
84
-
85
- saveAIGeneration({
86
- collection_id: collectionId,
87
- prompt: options.prompt,
88
- model: result.model,
89
- provider: providerName,
90
- generated_yaml: result.yaml,
91
- output_path: filePath,
92
- status: "success",
93
- prompt_tokens: result.promptTokens,
94
- completion_tokens: result.completionTokens,
95
- duration_ms: durationMs,
96
- });
97
- } catch {
98
- // DB not critical
99
- }
100
-
101
- return 0;
102
- } catch (err) {
103
- printError(err instanceof Error ? err.message : String(err));
104
- return 2;
105
- }
106
- }
@@ -1,43 +0,0 @@
1
- import { resolveProviderConfig, PROVIDER_DEFAULTS } from "../../core/generator/ai/types.ts";
2
- import type { AIProviderConfig } from "../../core/generator/ai/types.ts";
3
- import { printError } from "../output.ts";
4
-
5
- export interface ChatCommandOptions {
6
- provider?: string;
7
- model?: string;
8
- apiKey?: string;
9
- baseUrl?: string;
10
- safe?: boolean;
11
- dbPath?: string;
12
- }
13
-
14
- const VALID_PROVIDERS = new Set(["ollama", "openai", "anthropic", "custom"]);
15
-
16
- export async function chatCommand(options: ChatCommandOptions): Promise<number> {
17
- const providerName = options.provider ?? "ollama";
18
-
19
- if (!VALID_PROVIDERS.has(providerName)) {
20
- printError(`Unknown provider: ${providerName}. Available: ollama, openai, anthropic, custom`);
21
- return 2;
22
- }
23
-
24
- const providerConfig = resolveProviderConfig({
25
- provider: providerName as AIProviderConfig["provider"],
26
- model: options.model,
27
- apiKey: options.apiKey ?? process.env["ZOND_AI_KEY"],
28
- baseUrl: options.baseUrl,
29
- });
30
-
31
- try {
32
- const { startChatUI } = await import("../../tui/chat-ui.ts");
33
- await startChatUI({
34
- provider: providerConfig,
35
- safeMode: options.safe,
36
- dbPath: options.dbPath,
37
- });
38
- return 0;
39
- } catch (err) {
40
- printError(`Chat error: ${(err as Error).message}`);
41
- return 2;
42
- }
43
- }
@@ -1,41 +0,0 @@
1
- import { getDb } from "../../db/schema.ts";
2
- import { listCollections } from "../../db/queries.ts";
3
- import { formatDuration } from "../../core/reporter/console.ts";
4
-
5
- export function collectionsCommand(dbPath?: string): number {
6
- getDb(dbPath);
7
- const collections = listCollections();
8
-
9
- if (collections.length === 0) {
10
- console.log("No collections found.");
11
- console.log("Hint: use `zond generate --from <spec>` to create a collection automatically.");
12
- return 0;
13
- }
14
-
15
- // Print table header
16
- const header = [
17
- "ID".padEnd(5),
18
- "Name".padEnd(30),
19
- "Runs".padEnd(6),
20
- "Pass Rate".padEnd(11),
21
- "Last Run".padEnd(20),
22
- ].join(" ");
23
-
24
- console.log(header);
25
- console.log("-".repeat(header.length));
26
-
27
- for (const c of collections) {
28
- const passRate = c.total_runs > 0 ? `${c.pass_rate}%` : "-";
29
- const lastRun = c.last_run_at ?? "-";
30
- const row = [
31
- String(c.id).padEnd(5),
32
- c.name.slice(0, 30).padEnd(30),
33
- String(c.total_runs).padEnd(6),
34
- passRate.padEnd(11),
35
- lastRun.slice(0, 20).padEnd(20),
36
- ].join(" ");
37
- console.log(row);
38
- }
39
-
40
- return 0;
41
- }