@kirrosh/zond 0.12.7 → 0.14.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 (37) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +2 -6
  3. package/src/cli/index.ts +8 -207
  4. package/src/core/generator/guide-builder.ts +53 -1
  5. package/src/core/generator/index.ts +0 -3
  6. package/src/core/generator/serializer.ts +46 -10
  7. package/src/core/parser/schema.ts +51 -4
  8. package/src/core/parser/types.ts +27 -0
  9. package/src/core/parser/variables.ts +1 -0
  10. package/src/core/runner/assertions.ts +126 -0
  11. package/src/core/runner/executor.ts +120 -2
  12. package/src/core/runner/expr-eval.ts +41 -0
  13. package/src/core/runner/transforms.ts +65 -0
  14. package/src/cli/commands/add-api.ts +0 -53
  15. package/src/cli/commands/ai-generate.ts +0 -106
  16. package/src/cli/commands/chat.ts +0 -43
  17. package/src/cli/commands/collections.ts +0 -41
  18. package/src/cli/commands/compare.ts +0 -129
  19. package/src/cli/commands/doctor.ts +0 -127
  20. package/src/cli/commands/init.ts +0 -84
  21. package/src/cli/commands/runs.ts +0 -108
  22. package/src/cli/commands/update.ts +0 -142
  23. package/src/core/agent/agent-loop.ts +0 -116
  24. package/src/core/agent/context-manager.ts +0 -41
  25. package/src/core/agent/system-prompt.ts +0 -27
  26. package/src/core/agent/tools/diagnose-failure.ts +0 -51
  27. package/src/core/agent/tools/index.ts +0 -42
  28. package/src/core/agent/tools/query-results.ts +0 -40
  29. package/src/core/agent/tools/run-tests.ts +0 -38
  30. package/src/core/agent/tools/send-request.ts +0 -44
  31. package/src/core/agent/types.ts +0 -22
  32. package/src/core/generator/ai/ai-generator.ts +0 -61
  33. package/src/core/generator/ai/llm-client.ts +0 -159
  34. package/src/core/generator/ai/output-parser.ts +0 -307
  35. package/src/core/generator/ai/prompt-builder.ts +0 -153
  36. package/src/core/generator/ai/types.ts +0 -56
  37. package/src/tui/chat-ui.ts +0 -150
@@ -90,6 +90,132 @@ function checkRule(path: string, rule: AssertionRule, actual: unknown): Assertio
90
90
  });
91
91
  }
92
92
 
93
+ if (rule.not_equals !== undefined) {
94
+ results.push({
95
+ field, rule: `not_equals ${JSON.stringify(rule.not_equals)}`,
96
+ passed: !deepEquals(actual, rule.not_equals), actual, expected: rule.not_equals,
97
+ });
98
+ }
99
+
100
+ if (rule.not_contains !== undefined) {
101
+ const passed = typeof actual === "string" && !actual.includes(rule.not_contains);
102
+ results.push({
103
+ field, rule: `not_contains "${rule.not_contains}"`,
104
+ passed, actual, expected: rule.not_contains,
105
+ });
106
+ }
107
+
108
+ if (rule.gte !== undefined) {
109
+ const passed = typeof actual === "number" && actual >= rule.gte;
110
+ results.push({
111
+ field, rule: `gte ${rule.gte}`,
112
+ passed, actual, expected: rule.gte,
113
+ });
114
+ }
115
+
116
+ if (rule.lte !== undefined) {
117
+ const passed = typeof actual === "number" && actual <= rule.lte;
118
+ results.push({
119
+ field, rule: `lte ${rule.lte}`,
120
+ passed, actual, expected: rule.lte,
121
+ });
122
+ }
123
+
124
+ if (rule.length !== undefined) {
125
+ const hasLength = (Array.isArray(actual) || typeof actual === "string");
126
+ const passed = hasLength && (actual as string | unknown[]).length === rule.length;
127
+ results.push({
128
+ field, rule: `length ${rule.length}`,
129
+ passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length,
130
+ });
131
+ }
132
+
133
+ if (rule.length_gt !== undefined) {
134
+ const hasLength = (Array.isArray(actual) || typeof actual === "string");
135
+ const passed = hasLength && (actual as string | unknown[]).length > rule.length_gt;
136
+ results.push({
137
+ field, rule: `length_gt ${rule.length_gt}`,
138
+ passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_gt,
139
+ });
140
+ }
141
+
142
+ if (rule.length_gte !== undefined) {
143
+ const hasLength = (Array.isArray(actual) || typeof actual === "string");
144
+ const passed = hasLength && (actual as string | unknown[]).length >= rule.length_gte;
145
+ results.push({
146
+ field, rule: `length_gte ${rule.length_gte}`,
147
+ passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_gte,
148
+ });
149
+ }
150
+
151
+ if (rule.length_lt !== undefined) {
152
+ const hasLength = (Array.isArray(actual) || typeof actual === "string");
153
+ const passed = hasLength && (actual as string | unknown[]).length < rule.length_lt;
154
+ results.push({
155
+ field, rule: `length_lt ${rule.length_lt}`,
156
+ passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_lt,
157
+ });
158
+ }
159
+
160
+ if (rule.length_lte !== undefined) {
161
+ const hasLength = (Array.isArray(actual) || typeof actual === "string");
162
+ const passed = hasLength && (actual as string | unknown[]).length <= rule.length_lte;
163
+ results.push({
164
+ field, rule: `length_lte ${rule.length_lte}`,
165
+ passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_lte,
166
+ });
167
+ }
168
+
169
+ if (rule.each !== undefined) {
170
+ if (!Array.isArray(actual)) {
171
+ results.push({ field, rule: "each", passed: false, actual: describeType(actual), expected: "array" });
172
+ } else {
173
+ for (let i = 0; i < actual.length; i++) {
174
+ for (const [subPath, subRule] of Object.entries(rule.each)) {
175
+ const subActual = getByPath(actual[i], subPath);
176
+ const subResults = checkRule(`${path}[${i}].${subPath}`, subRule, subActual);
177
+ results.push(...subResults);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ if (rule.contains_item !== undefined) {
184
+ if (!Array.isArray(actual)) {
185
+ results.push({ field, rule: "contains_item", passed: false, actual: describeType(actual), expected: "array" });
186
+ } else {
187
+ const found = actual.some((item) => {
188
+ for (const [subPath, subRule] of Object.entries(rule.contains_item!)) {
189
+ const subActual = getByPath(item, subPath);
190
+ const subResults = checkRule("", subRule, subActual);
191
+ if (subResults.some(r => !r.passed)) return false;
192
+ }
193
+ return true;
194
+ });
195
+ results.push({
196
+ field, rule: "contains_item",
197
+ passed: found, actual: `array(${actual.length})`, expected: "at least one matching item",
198
+ });
199
+ }
200
+ }
201
+
202
+ if (rule.set_equals !== undefined) {
203
+ if (!Array.isArray(actual) || !Array.isArray(rule.set_equals)) {
204
+ results.push({
205
+ field, rule: "set_equals",
206
+ passed: false, actual: describeType(actual), expected: "both must be arrays",
207
+ });
208
+ } else {
209
+ const actualSet = new Set(actual.map(v => JSON.stringify(v)));
210
+ const expectedSet = new Set((rule.set_equals as unknown[]).map(v => JSON.stringify(v)));
211
+ const passed = actualSet.size === expectedSet.size && [...actualSet].every(v => expectedSet.has(v));
212
+ results.push({
213
+ field, rule: "set_equals",
214
+ passed, actual, expected: rule.set_equals,
215
+ });
216
+ }
217
+ }
218
+
93
219
  return results;
94
220
  }
95
221
 
@@ -1,8 +1,10 @@
1
- import type { TestSuite, Environment } from "../parser/types.ts";
1
+ import type { TestSuite, TestStep, Environment } from "../parser/types.ts";
2
2
  import { substituteString, substituteStep, substituteDeep, extractVariableReferences } from "../parser/variables.ts";
3
3
  import type { TestRunResult, StepResult, HttpRequest } from "./types.ts";
4
4
  import { executeRequest, type FetchOptions } from "./http-client.ts";
5
5
  import { checkAssertions, extractCaptures } from "./assertions.ts";
6
+ import { evaluateExpr } from "./expr-eval.ts";
7
+ import { applyTransform } from "./transforms.ts";
6
8
 
7
9
  function buildUrl(baseUrl: string | undefined, path: string, query?: Record<string, string>): string {
8
10
  let url = baseUrl ? `${baseUrl.replace(/\/+$/, "")}${path}` : path;
@@ -38,7 +40,61 @@ export async function runSuite(suite: TestSuite, env: Environment = {}, dryRun =
38
40
  follow_redirects: suite.config.follow_redirects,
39
41
  };
40
42
 
41
- for (const step of suite.tests) {
43
+ // Expand steps lazily (for_each needs current variables)
44
+ let stepIndex = 0;
45
+ const rawSteps = [...suite.tests];
46
+
47
+ while (stepIndex < rawSteps.length) {
48
+ const step = rawSteps[stepIndex]!;
49
+ stepIndex++;
50
+
51
+ // Expand for_each: insert expanded steps and skip current
52
+ if (step.for_each) {
53
+ const resolvedIn = substituteDeep(step.for_each.in, variables);
54
+ const items = Array.isArray(resolvedIn) ? resolvedIn : [];
55
+ const expanded: TestStep[] = [];
56
+ for (const item of items) {
57
+ const { for_each: _, ...rest } = step;
58
+ expanded.push({ ...rest, name: `${step.name} [${step.for_each.var}=${JSON.stringify(item)}]` } as TestStep);
59
+ // We'll inject the variable right before executing each expanded step
60
+ // Store the var assignment via a set field
61
+ }
62
+ // Insert expanded steps at current position
63
+ rawSteps.splice(stepIndex, 0, ...expanded);
64
+ // Set the for_each variable for each expanded step
65
+ for (let i = 0; i < items.length; i++) {
66
+ const expandedStep = rawSteps[stepIndex + i]!;
67
+ // Temporarily inject into variables when we reach this step
68
+ // We need a way to pass the variable — use a hidden _for_each_vars
69
+ (expandedStep as Record<string, unknown>).__for_each_var = { key: step.for_each.var, value: items[i] };
70
+ }
71
+ continue;
72
+ }
73
+
74
+ // Inject for_each variable if present
75
+ const forEachData = (step as Record<string, unknown>).__for_each_var as { key: string; value: unknown } | undefined;
76
+ if (forEachData) {
77
+ variables[forEachData.key] = forEachData.value;
78
+ delete (step as Record<string, unknown>).__for_each_var;
79
+ }
80
+
81
+ // Handle set-only steps (no HTTP request)
82
+ if (step.set && step.path === "") {
83
+ for (const [key, rawDirective] of Object.entries(step.set)) {
84
+ const substituted = substituteDeep(rawDirective, variables);
85
+ variables[key] = applyTransform(substituted);
86
+ }
87
+ steps.push({
88
+ name: step.name,
89
+ status: "pass",
90
+ duration_ms: 0,
91
+ request: { method: "", url: "", headers: {} },
92
+ assertions: [],
93
+ captures: {},
94
+ });
95
+ continue;
96
+ }
97
+
42
98
  // Skip check: if step references a failed capture variable, skip it
43
99
  const referencedVars = extractVariableReferences(step);
44
100
  const missingCapture = referencedVars.find((v) => failedCaptures.has(v));
@@ -47,6 +103,15 @@ export async function runSuite(suite: TestSuite, env: Environment = {}, dryRun =
47
103
  continue;
48
104
  }
49
105
 
106
+ // skip_if evaluation
107
+ if (step.skip_if) {
108
+ const exprAfterSubst = String(substituteString(step.skip_if, variables));
109
+ if (evaluateExpr(exprAfterSubst)) {
110
+ steps.push(makeSkippedResult(step.name, `Skipped: ${step.skip_if}`));
111
+ continue;
112
+ }
113
+ }
114
+
50
115
  // Substitute variables
51
116
  const resolved = substituteStep(step, variables);
52
117
 
@@ -104,6 +169,59 @@ export async function runSuite(suite: TestSuite, env: Environment = {}, dryRun =
104
169
  continue;
105
170
  }
106
171
 
172
+ // retry_until wrapper
173
+ if (step.retry_until) {
174
+ const rt = step.retry_until;
175
+ let lastStepResult: StepResult | undefined;
176
+ for (let attempt = 0; attempt < rt.max_attempts; attempt++) {
177
+ try {
178
+ const response = await executeRequest(request, fetchOptions);
179
+ const captures = extractCaptures(resolved.expect.body, response.body_parsed);
180
+ const assertions = checkAssertions(resolved.expect, response);
181
+ const allPassed = assertions.every((a) => a.passed);
182
+
183
+ lastStepResult = {
184
+ name: step.name,
185
+ status: allPassed ? "pass" : "fail",
186
+ duration_ms: response.duration_ms,
187
+ request,
188
+ response,
189
+ assertions,
190
+ captures,
191
+ };
192
+
193
+ // Evaluate condition with response context
194
+ const condVars: Record<string, unknown> = { ...variables, ...captures, status: response.status };
195
+ if (response.body_parsed && typeof response.body_parsed === "object") {
196
+ for (const [k, v] of Object.entries(response.body_parsed as Record<string, unknown>)) {
197
+ condVars[k] = v;
198
+ }
199
+ }
200
+ const condStr = String(substituteString(rt.condition, condVars));
201
+ if (evaluateExpr(condStr)) {
202
+ Object.assign(variables, captures);
203
+ break;
204
+ }
205
+
206
+ if (attempt < rt.max_attempts - 1) {
207
+ await new Promise((resolve) => setTimeout(resolve, rt.delay_ms));
208
+ }
209
+ } catch (err) {
210
+ lastStepResult = {
211
+ name: step.name,
212
+ status: "error",
213
+ duration_ms: 0,
214
+ request,
215
+ assertions: [],
216
+ captures: {},
217
+ error: err instanceof Error ? err.message : String(err),
218
+ };
219
+ }
220
+ }
221
+ if (lastStepResult) steps.push(lastStepResult);
222
+ continue;
223
+ }
224
+
107
225
  try {
108
226
  const response = await executeRequest(request, fetchOptions);
109
227
 
@@ -0,0 +1,41 @@
1
+ const OPERATORS = ["!=", "==", ">=", "<=", ">", "<"] as const;
2
+
3
+ export function evaluateExpr(expr: string): boolean {
4
+ const trimmed = expr.trim();
5
+ if (trimmed === "") return false;
6
+
7
+ for (const op of OPERATORS) {
8
+ const idx = trimmed.indexOf(op);
9
+ if (idx !== -1) {
10
+ const left = trimmed.slice(0, idx).trim();
11
+ const right = trimmed.slice(idx + op.length).trim();
12
+ return compareValues(left, right, op);
13
+ }
14
+ }
15
+
16
+ // No operator — truthiness
17
+ return isTruthy(trimmed);
18
+ }
19
+
20
+ function compareValues(left: string, right: string, op: string): boolean {
21
+ const lNum = Number(left);
22
+ const rNum = Number(right);
23
+ const numeric = !isNaN(lNum) && !isNaN(rNum) && left !== "" && right !== "";
24
+
25
+ switch (op) {
26
+ case "==": return numeric ? lNum === rNum : left === right;
27
+ case "!=": return numeric ? lNum !== rNum : left !== right;
28
+ case ">": return numeric ? lNum > rNum : left > right;
29
+ case "<": return numeric ? lNum < rNum : left < right;
30
+ case ">=": return numeric ? lNum >= rNum : left >= right;
31
+ case "<=": return numeric ? lNum <= rNum : left <= right;
32
+ default: return false;
33
+ }
34
+ }
35
+
36
+ function isTruthy(value: string): boolean {
37
+ if (value === "" || value === "0" || value === "false" || value === "null" || value === "undefined") {
38
+ return false;
39
+ }
40
+ return true;
41
+ }
@@ -0,0 +1,65 @@
1
+ const DIRECTIVES = new Set(["concat", "append", "length", "get", "first", "map_field"]);
2
+
3
+ export function applyTransform(directive: unknown): unknown {
4
+ if (typeof directive !== "object" || directive === null || Array.isArray(directive)) {
5
+ return directive;
6
+ }
7
+
8
+ const obj = directive as Record<string, unknown>;
9
+ const keys = Object.keys(obj);
10
+
11
+ if (keys.length !== 1 || !DIRECTIVES.has(keys[0]!)) {
12
+ return directive;
13
+ }
14
+
15
+ const op = keys[0]!;
16
+ const arg = obj[op];
17
+
18
+ switch (op) {
19
+ case "concat": {
20
+ if (!Array.isArray(arg)) return directive;
21
+ const result: unknown[] = [];
22
+ for (const item of arg) {
23
+ if (Array.isArray(item)) result.push(...item);
24
+ else result.push(item);
25
+ }
26
+ return result;
27
+ }
28
+ case "append": {
29
+ if (!Array.isArray(arg) || arg.length < 2) return directive;
30
+ const arr = Array.isArray(arg[0]) ? [...arg[0]] : [];
31
+ return [...arr, ...arg.slice(1)];
32
+ }
33
+ case "length": {
34
+ if (Array.isArray(arg)) return arg.length;
35
+ if (typeof arg === "string") return arg.length;
36
+ return 0;
37
+ }
38
+ case "get": {
39
+ if (!Array.isArray(arg) || arg.length < 2) return directive;
40
+ const [source, index] = arg;
41
+ if (Array.isArray(source) && typeof index === "number") return source[index];
42
+ if (typeof source === "object" && source !== null && typeof index === "string") {
43
+ return (source as Record<string, unknown>)[index];
44
+ }
45
+ return undefined;
46
+ }
47
+ case "first": {
48
+ if (Array.isArray(arg)) return arg[0];
49
+ return undefined;
50
+ }
51
+ case "map_field": {
52
+ if (!Array.isArray(arg) || arg.length < 2) return directive;
53
+ const [items, field] = arg;
54
+ if (!Array.isArray(items) || typeof field !== "string") return directive;
55
+ return items.map((item) => {
56
+ if (typeof item === "object" && item !== null) {
57
+ return (item as Record<string, unknown>)[field];
58
+ }
59
+ return undefined;
60
+ });
61
+ }
62
+ default:
63
+ return directive;
64
+ }
65
+ }
@@ -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
- }