@kirrosh/apitool 0.4.3

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 (191) hide show
  1. package/.github/workflows/ci.yml +27 -0
  2. package/.github/workflows/release.yml +97 -0
  3. package/.mcp.json +9 -0
  4. package/APITOOL.md +195 -0
  5. package/BACKLOG.md +62 -0
  6. package/CHANGELOG.md +88 -0
  7. package/LICENSE +21 -0
  8. package/README.md +105 -0
  9. package/bun.lock +291 -0
  10. package/docs/GLOSSARY.md +182 -0
  11. package/docs/INDEX.md +21 -0
  12. package/docs/agent.md +135 -0
  13. package/docs/archive/APITOOL-pre-M22.md +831 -0
  14. package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
  15. package/docs/archive/M1-M2-parser-runner.md +216 -0
  16. package/docs/archive/M4-M7-reporter-cli.md +179 -0
  17. package/docs/archive/M5-M7-storage-junit.md +300 -0
  18. package/docs/archive/M6-webui.md +339 -0
  19. package/docs/ci.md +274 -0
  20. package/docs/generation-issues.md +67 -0
  21. package/generated/.env.yaml +3 -0
  22. package/install.ps1 +80 -0
  23. package/install.sh +113 -0
  24. package/package.json +46 -0
  25. package/scripts/run-mocked-tests.ts +45 -0
  26. package/seed-demo.ts +53 -0
  27. package/self-tests/auth.yaml +18 -0
  28. package/self-tests/collections-crud.yaml +46 -0
  29. package/self-tests/environments-crud.yaml +48 -0
  30. package/self-tests/export.yaml +32 -0
  31. package/self-tests/runs.yaml +16 -0
  32. package/src/bun-types.d.ts +5 -0
  33. package/src/cli/commands/add-api.ts +51 -0
  34. package/src/cli/commands/ai-generate.ts +106 -0
  35. package/src/cli/commands/chat.ts +43 -0
  36. package/src/cli/commands/ci-init.ts +126 -0
  37. package/src/cli/commands/collections.ts +41 -0
  38. package/src/cli/commands/coverage.ts +65 -0
  39. package/src/cli/commands/doctor.ts +127 -0
  40. package/src/cli/commands/envs.ts +218 -0
  41. package/src/cli/commands/init.ts +84 -0
  42. package/src/cli/commands/mcp.ts +16 -0
  43. package/src/cli/commands/run.ts +137 -0
  44. package/src/cli/commands/runs.ts +108 -0
  45. package/src/cli/commands/serve.ts +22 -0
  46. package/src/cli/commands/update.ts +142 -0
  47. package/src/cli/commands/validate.ts +18 -0
  48. package/src/cli/index.ts +500 -0
  49. package/src/cli/output.ts +24 -0
  50. package/src/cli/runtime.ts +7 -0
  51. package/src/core/agent/agent-loop.ts +116 -0
  52. package/src/core/agent/context-manager.ts +41 -0
  53. package/src/core/agent/system-prompt.ts +33 -0
  54. package/src/core/agent/tools/diagnose-failure.ts +51 -0
  55. package/src/core/agent/tools/explore-api.ts +40 -0
  56. package/src/core/agent/tools/index.ts +48 -0
  57. package/src/core/agent/tools/manage-environment.ts +40 -0
  58. package/src/core/agent/tools/query-results.ts +40 -0
  59. package/src/core/agent/tools/run-tests.ts +38 -0
  60. package/src/core/agent/tools/send-request.ts +44 -0
  61. package/src/core/agent/tools/validate-tests.ts +23 -0
  62. package/src/core/agent/types.ts +22 -0
  63. package/src/core/generator/ai/ai-generator.ts +61 -0
  64. package/src/core/generator/ai/llm-client.ts +159 -0
  65. package/src/core/generator/ai/output-parser.ts +307 -0
  66. package/src/core/generator/ai/prompt-builder.ts +153 -0
  67. package/src/core/generator/ai/types.ts +56 -0
  68. package/src/core/generator/coverage-scanner.ts +87 -0
  69. package/src/core/generator/data-factory.ts +115 -0
  70. package/src/core/generator/index.ts +10 -0
  71. package/src/core/generator/openapi-reader.ts +142 -0
  72. package/src/core/generator/schema-utils.ts +52 -0
  73. package/src/core/generator/serializer.ts +189 -0
  74. package/src/core/generator/types.ts +47 -0
  75. package/src/core/parser/filter.ts +14 -0
  76. package/src/core/parser/index.ts +21 -0
  77. package/src/core/parser/schema.ts +175 -0
  78. package/src/core/parser/types.ts +50 -0
  79. package/src/core/parser/variables.ts +146 -0
  80. package/src/core/parser/yaml-parser.ts +85 -0
  81. package/src/core/reporter/console.ts +175 -0
  82. package/src/core/reporter/index.ts +23 -0
  83. package/src/core/reporter/json.ts +9 -0
  84. package/src/core/reporter/junit.ts +78 -0
  85. package/src/core/reporter/types.ts +12 -0
  86. package/src/core/runner/assertions.ts +172 -0
  87. package/src/core/runner/execute-run.ts +75 -0
  88. package/src/core/runner/executor.ts +150 -0
  89. package/src/core/runner/http-client.ts +69 -0
  90. package/src/core/runner/index.ts +12 -0
  91. package/src/core/runner/types.ts +48 -0
  92. package/src/core/setup-api.ts +97 -0
  93. package/src/core/utils.ts +9 -0
  94. package/src/db/queries.ts +868 -0
  95. package/src/db/schema.ts +215 -0
  96. package/src/mcp/server.ts +47 -0
  97. package/src/mcp/tools/ci-init.ts +57 -0
  98. package/src/mcp/tools/coverage-analysis.ts +58 -0
  99. package/src/mcp/tools/explore-api.ts +84 -0
  100. package/src/mcp/tools/generate-missing-tests.ts +80 -0
  101. package/src/mcp/tools/generate-tests-guide.ts +353 -0
  102. package/src/mcp/tools/manage-environment.ts +123 -0
  103. package/src/mcp/tools/manage-server.ts +87 -0
  104. package/src/mcp/tools/query-db.ts +141 -0
  105. package/src/mcp/tools/run-tests.ts +66 -0
  106. package/src/mcp/tools/save-test-suite.ts +164 -0
  107. package/src/mcp/tools/send-request.ts +53 -0
  108. package/src/mcp/tools/setup-api.ts +49 -0
  109. package/src/mcp/tools/validate-tests.ts +42 -0
  110. package/src/tui/chat-ui.ts +150 -0
  111. package/src/web/routes/api.ts +234 -0
  112. package/src/web/routes/dashboard.ts +348 -0
  113. package/src/web/routes/runs.ts +64 -0
  114. package/src/web/schemas.ts +121 -0
  115. package/src/web/server.ts +134 -0
  116. package/src/web/static/htmx.min.js +1 -0
  117. package/src/web/static/style.css +265 -0
  118. package/src/web/views/layout.ts +46 -0
  119. package/src/web/views/results.ts +209 -0
  120. package/tests/agent/agent-loop.test.ts +61 -0
  121. package/tests/agent/context-manager.test.ts +59 -0
  122. package/tests/agent/system-prompt.test.ts +42 -0
  123. package/tests/agent/tools/diagnose-failure.test.ts +85 -0
  124. package/tests/agent/tools/explore-api.test.ts +59 -0
  125. package/tests/agent/tools/manage-environment.test.ts +78 -0
  126. package/tests/agent/tools/query-results.test.ts +77 -0
  127. package/tests/agent/tools/run-tests.test.ts +89 -0
  128. package/tests/agent/tools/send-request.test.ts +78 -0
  129. package/tests/agent/tools/validate-tests.test.ts +59 -0
  130. package/tests/ai/ai-generator.integration.test.ts +131 -0
  131. package/tests/ai/llm-client.test.ts +145 -0
  132. package/tests/ai/output-parser.test.ts +132 -0
  133. package/tests/ai/prompt-builder.test.ts +67 -0
  134. package/tests/ai/types.test.ts +55 -0
  135. package/tests/cli/args.test.ts +63 -0
  136. package/tests/cli/chat.test.ts +38 -0
  137. package/tests/cli/ci-init.test.ts +112 -0
  138. package/tests/cli/commands.test.ts +316 -0
  139. package/tests/cli/coverage.test.ts +58 -0
  140. package/tests/cli/doctor.test.ts +39 -0
  141. package/tests/cli/envs.test.ts +181 -0
  142. package/tests/cli/init.test.ts +80 -0
  143. package/tests/cli/runs.test.ts +94 -0
  144. package/tests/cli/safe-run.test.ts +103 -0
  145. package/tests/cli/update.test.ts +32 -0
  146. package/tests/core/generator/schema-utils.test.ts +108 -0
  147. package/tests/core/parser/nested-assertions.test.ts +80 -0
  148. package/tests/core/runner/root-body-assertions.test.ts +70 -0
  149. package/tests/db/chat-queries.test.ts +88 -0
  150. package/tests/db/chat-schema.test.ts +37 -0
  151. package/tests/db/environments.test.ts +131 -0
  152. package/tests/db/queries.test.ts +409 -0
  153. package/tests/db/schema.test.ts +141 -0
  154. package/tests/fixtures/.env.yaml +3 -0
  155. package/tests/fixtures/auth-token-test.yaml +8 -0
  156. package/tests/fixtures/bail/suite-a.yaml +6 -0
  157. package/tests/fixtures/bail/suite-b.yaml +6 -0
  158. package/tests/fixtures/crud.yaml +35 -0
  159. package/tests/fixtures/invalid-missing-name.yaml +5 -0
  160. package/tests/fixtures/invalid-no-method.yaml +6 -0
  161. package/tests/fixtures/petstore-auth.json +295 -0
  162. package/tests/fixtures/petstore-simple.json +151 -0
  163. package/tests/fixtures/post-only.yaml +12 -0
  164. package/tests/fixtures/simple.yaml +6 -0
  165. package/tests/fixtures/valid/.env.yaml +1 -0
  166. package/tests/fixtures/valid/a.yaml +5 -0
  167. package/tests/fixtures/valid/b.yml +5 -0
  168. package/tests/generator/coverage-scanner.test.ts +129 -0
  169. package/tests/generator/data-factory.test.ts +133 -0
  170. package/tests/generator/openapi-reader.test.ts +131 -0
  171. package/tests/integration/auth-flow.test.ts +217 -0
  172. package/tests/mcp/coverage-analysis.test.ts +64 -0
  173. package/tests/mcp/explore-api-schemas.test.ts +105 -0
  174. package/tests/mcp/explore-api.test.ts +49 -0
  175. package/tests/mcp/generate-missing-tests.test.ts +69 -0
  176. package/tests/mcp/manage-environment.test.ts +89 -0
  177. package/tests/mcp/save-test-suite.test.ts +116 -0
  178. package/tests/mcp/send-request.test.ts +79 -0
  179. package/tests/mcp/setup-api.test.ts +106 -0
  180. package/tests/mcp/tools.test.ts +248 -0
  181. package/tests/parser/schema.test.ts +134 -0
  182. package/tests/parser/variables.test.ts +227 -0
  183. package/tests/parser/yaml-parser.test.ts +69 -0
  184. package/tests/reporter/console.test.ts +256 -0
  185. package/tests/reporter/json.test.ts +98 -0
  186. package/tests/reporter/junit.test.ts +284 -0
  187. package/tests/runner/assertions.test.ts +262 -0
  188. package/tests/runner/executor.test.ts +310 -0
  189. package/tests/runner/http-client.test.ts +138 -0
  190. package/tests/web/routes.test.ts +160 -0
  191. package/tsconfig.json +31 -0
@@ -0,0 +1,18 @@
1
+ name: Auth
2
+ base_url: "http://localhost:8080"
3
+ tests:
4
+ - name: Proxy login request and extract token
5
+ POST: /api/authorize
6
+ headers:
7
+ Content-Type: application/json
8
+ Accept: application/json
9
+ json:
10
+ base_url: "{{$randomString}}"
11
+ path: "{{$randomString}}"
12
+ username: "{{$randomString}}"
13
+ password: "{{$randomString}}"
14
+ expect:
15
+ status: 200
16
+ body:
17
+ token:
18
+ type: string
@@ -0,0 +1,46 @@
1
+ name: collections CRUD
2
+ base_url: "http://localhost:8080"
3
+ tests:
4
+ - name: Create collection
5
+ POST: /api/collections
6
+ headers:
7
+ Content-Type: application/json
8
+ json:
9
+ name: "{{$randomName}}"
10
+ test_path: "{{$randomString}}"
11
+ openapi_spec: "{{$randomString}}"
12
+ expect:
13
+ status: 201
14
+ body:
15
+ id:
16
+ capture: collection_id
17
+ type: number
18
+ name:
19
+ type: string
20
+ test_path:
21
+ type: string
22
+ openapi_spec:
23
+ type: string
24
+ - name: Get created collection
25
+ GET: /api/collections/{{collection_id}}
26
+ expect:
27
+ status: 200
28
+ body:
29
+ id:
30
+ equals: "{{collection_id}}"
31
+ name:
32
+ type: string
33
+ test_path:
34
+ type: string
35
+ openapi_spec:
36
+ type: string
37
+ created_at:
38
+ type: string
39
+ - name: Delete collection
40
+ DELETE: /api/collections/{{collection_id}}
41
+ expect:
42
+ status: 204
43
+ - name: Verify collection deleted
44
+ GET: /api/collections/{{collection_id}}
45
+ expect:
46
+ status: 404
@@ -0,0 +1,48 @@
1
+ name: environments CRUD
2
+ base_url: "http://localhost:8080"
3
+ tests:
4
+ - name: Create environment
5
+ POST: /api/environments
6
+ headers:
7
+ Content-Type: application/json
8
+ json:
9
+ name: "{{$randomName}}"
10
+ expect:
11
+ status: 201
12
+ body:
13
+ id:
14
+ capture: environment_id
15
+ type: number
16
+ name:
17
+ type: string
18
+ variables:
19
+ type: object
20
+ - name: Get created environment
21
+ GET: /api/environments/{{environment_id}}
22
+ expect:
23
+ status: 200
24
+ body:
25
+ id:
26
+ equals: "{{environment_id}}"
27
+ name:
28
+ type: string
29
+ variables:
30
+ type: object
31
+ - name: Update environment
32
+ PUT: /api/environments/{{environment_id}}
33
+ headers:
34
+ Content-Type: application/json
35
+ json:
36
+ variables:
37
+ key1: "{{$randomString}}"
38
+ key2: "{{$randomString}}"
39
+ expect:
40
+ status: 200
41
+ - name: Delete environment
42
+ DELETE: /api/environments/{{environment_id}}
43
+ expect:
44
+ status: 204
45
+ - name: Verify environment deleted
46
+ GET: /api/environments/{{environment_id}}
47
+ expect:
48
+ status: 404
@@ -0,0 +1,32 @@
1
+ name: Export
2
+ base_url: "http://localhost:8080"
3
+ tests:
4
+ - name: Export run results as JSON
5
+ GET: /api/export/{{$randomInt}}/json
6
+ headers:
7
+ Accept: application/json
8
+ expect:
9
+ status: 200
10
+ body:
11
+ suite_name:
12
+ type: string
13
+ started_at:
14
+ type: string
15
+ finished_at:
16
+ type: string
17
+ total:
18
+ type: number
19
+ passed:
20
+ type: number
21
+ failed:
22
+ type: number
23
+ skipped:
24
+ type: number
25
+ steps:
26
+ type: array
27
+ - name: Export run results as JUnit XML
28
+ GET: /api/export/{{$randomInt}}/junit
29
+ headers:
30
+ Accept: application/json
31
+ expect:
32
+ status: 200
@@ -0,0 +1,16 @@
1
+ name: Runs
2
+ base_url: "http://localhost:8080"
3
+ tests:
4
+ - name: Run tests
5
+ POST: /api/run
6
+ headers:
7
+ Content-Type: application/json
8
+ Accept: application/json
9
+ json:
10
+ path: "{{$randomString}}"
11
+ env: "{{$randomString}}"
12
+ expect:
13
+ status: 200
14
+ body:
15
+ runId:
16
+ type: number
@@ -0,0 +1,5 @@
1
+ declare module "*.css" {
2
+ const path: string;
3
+ export default path;
4
+ }
5
+
@@ -0,0 +1,51 @@
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
+ }
11
+
12
+ export async function addApiCommand(options: AddApiOptions): Promise<number> {
13
+ const { name, spec, envPairs, dbPath, dir } = options;
14
+
15
+ // Parse --env key=value pairs into a record
16
+ const envVars: Record<string, string> = {};
17
+ if (envPairs) {
18
+ for (const pair of envPairs) {
19
+ const idx = pair.indexOf("=");
20
+ if (idx === -1) continue;
21
+ const key = pair.slice(0, idx).trim();
22
+ const value = pair.slice(idx + 1).trim();
23
+ if (key) envVars[key] = value;
24
+ }
25
+ }
26
+
27
+ try {
28
+ const result = await setupApi({
29
+ name,
30
+ spec,
31
+ dir,
32
+ envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
33
+ dbPath,
34
+ });
35
+
36
+ printSuccess(`API '${name}' created (id=${result.collectionId})`);
37
+ console.log(` Directory: ${result.testPath.replace(/\/tests$/, "")}`);
38
+ console.log(` Tests: ${result.testPath}/`);
39
+ if (spec) console.log(` Spec: ${spec}`);
40
+ if (result.baseUrl) console.log(` Base URL: ${result.baseUrl}`);
41
+ console.log();
42
+ console.log("Next steps:");
43
+ console.log(` apitool ai-generate --api ${name} --prompt "test the user endpoints"`);
44
+ console.log(` apitool run --api ${name}`);
45
+
46
+ return 0;
47
+ } catch (err) {
48
+ printError((err as Error).message);
49
+ return 1;
50
+ }
51
+ }
@@ -0,0 +1,106 @@
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.APITOOL_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
+ }
@@ -0,0 +1,43 @@
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["APITOOL_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
+ }
@@ -0,0 +1,126 @@
1
+ import { resolve, dirname } from "path";
2
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
3
+ import { printSuccess, printError } from "../output.ts";
4
+
5
+ export interface CiInitOptions {
6
+ platform?: "github" | "gitlab";
7
+ force: boolean;
8
+ dir?: string;
9
+ }
10
+
11
+ const GH_ACTIONS_TEMPLATE = `name: API Tests
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ pull_request:
16
+ schedule:
17
+ - cron: "0 */6 * * *"
18
+ workflow_dispatch:
19
+ repository_dispatch:
20
+ types: [api-updated]
21
+
22
+ permissions:
23
+ contents: read
24
+ checks: write
25
+ pull-requests: write
26
+
27
+ jobs:
28
+ test:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - name: Install apitool
34
+ run: curl -fsSL https://raw.githubusercontent.com/kirrosh/apitool/master/install.sh | sh
35
+
36
+ - name: Run tests
37
+ run: |
38
+ mkdir -p test-results
39
+ apitool run apis/ --report junit --no-db > test-results/junit.xml
40
+ # Add --env <name> to load .env.<name>.yaml from test directory
41
+ continue-on-error: true
42
+
43
+ - name: Publish test results
44
+ uses: EnricoMi/publish-unit-test-result-action@v2
45
+ if: always()
46
+ with:
47
+ files: test-results/junit.xml
48
+
49
+ - uses: actions/upload-artifact@v4
50
+ if: always()
51
+ with:
52
+ name: test-results
53
+ path: test-results/junit.xml
54
+ `;
55
+
56
+ const GITLAB_CI_TEMPLATE = `# Trigger via API: curl -X POST --form ref=main --form token=TRIGGER_TOKEN $CI_API_V4_URL/projects/$CI_PROJECT_ID/trigger/pipeline
57
+
58
+ api-tests:
59
+ image: ubuntu:latest
60
+ before_script:
61
+ - apt-get update -qq && apt-get install -y -qq curl
62
+ - curl -fsSL https://raw.githubusercontent.com/kirrosh/apitool/master/install.sh | sh
63
+ script:
64
+ - mkdir -p test-results
65
+ - apitool run apis/ --report junit --no-db > test-results/junit.xml
66
+ # Add --env <name> to load .env.<name>.yaml from test directory
67
+ allow_failure:
68
+ exit_codes: 1
69
+ artifacts:
70
+ when: always
71
+ reports:
72
+ junit: test-results/junit.xml
73
+ `;
74
+
75
+ function writeIfMissing(filePath: string, content: string, force: boolean): boolean {
76
+ if (!force && existsSync(filePath)) {
77
+ console.log(` Skipped ${filePath} (already exists, use --force to overwrite)`);
78
+ return false;
79
+ }
80
+ const dir = dirname(filePath);
81
+ if (!existsSync(dir)) {
82
+ mkdirSync(dir, { recursive: true });
83
+ }
84
+ writeFileSync(filePath, content, "utf-8");
85
+ console.log(` Created ${filePath}`);
86
+ return true;
87
+ }
88
+
89
+ function detectPlatform(cwd: string): "github" | "gitlab" | null {
90
+ if (existsSync(resolve(cwd, ".github"))) return "github";
91
+ if (existsSync(resolve(cwd, ".gitlab-ci.yml"))) return "gitlab";
92
+ return null;
93
+ }
94
+
95
+ export async function ciInitCommand(options: CiInitOptions): Promise<number> {
96
+ const cwd = options.dir ? resolve(options.dir) : process.cwd();
97
+ let platform = options.platform;
98
+
99
+ if (!platform) {
100
+ platform = detectPlatform(cwd);
101
+ if (!platform) {
102
+ platform = "github";
103
+ console.log("No CI platform detected, defaulting to GitHub Actions.\n");
104
+ } else {
105
+ console.log(`Detected ${platform === "github" ? "GitHub Actions" : "GitLab CI"}.\n`);
106
+ }
107
+ }
108
+
109
+ console.log(`Generating ${platform === "github" ? "GitHub Actions" : "GitLab CI"} workflow...\n`);
110
+
111
+ let created = false;
112
+
113
+ if (platform === "github") {
114
+ const targetPath = resolve(cwd, ".github/workflows/api-tests.yml");
115
+ created = writeIfMissing(targetPath, GH_ACTIONS_TEMPLATE, options.force);
116
+ } else {
117
+ const targetPath = resolve(cwd, ".gitlab-ci.yml");
118
+ created = writeIfMissing(targetPath, GITLAB_CI_TEMPLATE, options.force);
119
+ }
120
+
121
+ if (created) {
122
+ printSuccess("CI workflow created. Commit and push to activate.");
123
+ }
124
+
125
+ return 0;
126
+ }
@@ -0,0 +1,41 @@
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 `apitool 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
+ }
@@ -0,0 +1,65 @@
1
+ import { readOpenApiSpec, extractEndpoints, scanCoveredEndpoints, filterUncoveredEndpoints } from "../../core/generator/index.ts";
2
+ import { printError, printSuccess } from "../output.ts";
3
+
4
+ export interface CoverageOptions {
5
+ spec: string;
6
+ tests: string;
7
+ }
8
+
9
+ const RESET = "\x1b[0m";
10
+ const GREEN = "\x1b[32m";
11
+ const RED = "\x1b[31m";
12
+ const YELLOW = "\x1b[33m";
13
+
14
+ function useColor(): boolean {
15
+ return process.stdout.isTTY ?? false;
16
+ }
17
+
18
+ export async function coverageCommand(options: CoverageOptions): Promise<number> {
19
+ const { spec, tests } = options;
20
+
21
+ try {
22
+ const doc = await readOpenApiSpec(spec);
23
+ const allEndpoints = extractEndpoints(doc);
24
+
25
+ if (allEndpoints.length === 0) {
26
+ printError("No endpoints found in the OpenAPI spec");
27
+ return 1;
28
+ }
29
+
30
+ const covered = await scanCoveredEndpoints(tests);
31
+ const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
32
+ const coveredCount = allEndpoints.length - uncovered.length;
33
+ const percentage = Math.round((coveredCount / allEndpoints.length) * 100);
34
+
35
+ const color = useColor();
36
+
37
+ // Summary
38
+ console.log(`Coverage: ${coveredCount}/${allEndpoints.length} endpoints (${percentage}%)`);
39
+ console.log("");
40
+
41
+ // Covered endpoints
42
+ if (coveredCount > 0) {
43
+ console.log(`${color ? GREEN : ""}Covered:${color ? RESET : ""}`);
44
+ for (const ep of allEndpoints) {
45
+ if (!uncovered.includes(ep)) {
46
+ console.log(` ${color ? GREEN : ""}✓${color ? RESET : ""} ${ep.method.padEnd(7)} ${ep.path}`);
47
+ }
48
+ }
49
+ console.log("");
50
+ }
51
+
52
+ // Uncovered endpoints
53
+ if (uncovered.length > 0) {
54
+ console.log(`${color ? RED : ""}Uncovered:${color ? RESET : ""}`);
55
+ for (const ep of uncovered) {
56
+ console.log(` ${color ? RED : ""}✗${color ? RESET : ""} ${ep.method.padEnd(7)} ${ep.path}`);
57
+ }
58
+ }
59
+
60
+ return uncovered.length > 0 ? 1 : 0;
61
+ } catch (err) {
62
+ printError(err instanceof Error ? err.message : String(err));
63
+ return 2;
64
+ }
65
+ }