@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.
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/release.yml +97 -0
- package/.mcp.json +9 -0
- package/APITOOL.md +195 -0
- package/BACKLOG.md +62 -0
- package/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/bun.lock +291 -0
- package/docs/GLOSSARY.md +182 -0
- package/docs/INDEX.md +21 -0
- package/docs/agent.md +135 -0
- package/docs/archive/APITOOL-pre-M22.md +831 -0
- package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
- package/docs/archive/M1-M2-parser-runner.md +216 -0
- package/docs/archive/M4-M7-reporter-cli.md +179 -0
- package/docs/archive/M5-M7-storage-junit.md +300 -0
- package/docs/archive/M6-webui.md +339 -0
- package/docs/ci.md +274 -0
- package/docs/generation-issues.md +67 -0
- package/generated/.env.yaml +3 -0
- package/install.ps1 +80 -0
- package/install.sh +113 -0
- package/package.json +46 -0
- package/scripts/run-mocked-tests.ts +45 -0
- package/seed-demo.ts +53 -0
- package/self-tests/auth.yaml +18 -0
- package/self-tests/collections-crud.yaml +46 -0
- package/self-tests/environments-crud.yaml +48 -0
- package/self-tests/export.yaml +32 -0
- package/self-tests/runs.yaml +16 -0
- package/src/bun-types.d.ts +5 -0
- package/src/cli/commands/add-api.ts +51 -0
- package/src/cli/commands/ai-generate.ts +106 -0
- package/src/cli/commands/chat.ts +43 -0
- package/src/cli/commands/ci-init.ts +126 -0
- package/src/cli/commands/collections.ts +41 -0
- package/src/cli/commands/coverage.ts +65 -0
- package/src/cli/commands/doctor.ts +127 -0
- package/src/cli/commands/envs.ts +218 -0
- package/src/cli/commands/init.ts +84 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/run.ts +137 -0
- package/src/cli/commands/runs.ts +108 -0
- package/src/cli/commands/serve.ts +22 -0
- package/src/cli/commands/update.ts +142 -0
- package/src/cli/commands/validate.ts +18 -0
- package/src/cli/index.ts +500 -0
- package/src/cli/output.ts +24 -0
- package/src/cli/runtime.ts +7 -0
- package/src/core/agent/agent-loop.ts +116 -0
- package/src/core/agent/context-manager.ts +41 -0
- package/src/core/agent/system-prompt.ts +33 -0
- package/src/core/agent/tools/diagnose-failure.ts +51 -0
- package/src/core/agent/tools/explore-api.ts +40 -0
- package/src/core/agent/tools/index.ts +48 -0
- package/src/core/agent/tools/manage-environment.ts +40 -0
- package/src/core/agent/tools/query-results.ts +40 -0
- package/src/core/agent/tools/run-tests.ts +38 -0
- package/src/core/agent/tools/send-request.ts +44 -0
- package/src/core/agent/tools/validate-tests.ts +23 -0
- package/src/core/agent/types.ts +22 -0
- package/src/core/generator/ai/ai-generator.ts +61 -0
- package/src/core/generator/ai/llm-client.ts +159 -0
- package/src/core/generator/ai/output-parser.ts +307 -0
- package/src/core/generator/ai/prompt-builder.ts +153 -0
- package/src/core/generator/ai/types.ts +56 -0
- package/src/core/generator/coverage-scanner.ts +87 -0
- package/src/core/generator/data-factory.ts +115 -0
- package/src/core/generator/index.ts +10 -0
- package/src/core/generator/openapi-reader.ts +142 -0
- package/src/core/generator/schema-utils.ts +52 -0
- package/src/core/generator/serializer.ts +189 -0
- package/src/core/generator/types.ts +47 -0
- package/src/core/parser/filter.ts +14 -0
- package/src/core/parser/index.ts +21 -0
- package/src/core/parser/schema.ts +175 -0
- package/src/core/parser/types.ts +50 -0
- package/src/core/parser/variables.ts +146 -0
- package/src/core/parser/yaml-parser.ts +85 -0
- package/src/core/reporter/console.ts +175 -0
- package/src/core/reporter/index.ts +23 -0
- package/src/core/reporter/json.ts +9 -0
- package/src/core/reporter/junit.ts +78 -0
- package/src/core/reporter/types.ts +12 -0
- package/src/core/runner/assertions.ts +172 -0
- package/src/core/runner/execute-run.ts +75 -0
- package/src/core/runner/executor.ts +150 -0
- package/src/core/runner/http-client.ts +69 -0
- package/src/core/runner/index.ts +12 -0
- package/src/core/runner/types.ts +48 -0
- package/src/core/setup-api.ts +97 -0
- package/src/core/utils.ts +9 -0
- package/src/db/queries.ts +868 -0
- package/src/db/schema.ts +215 -0
- package/src/mcp/server.ts +47 -0
- package/src/mcp/tools/ci-init.ts +57 -0
- package/src/mcp/tools/coverage-analysis.ts +58 -0
- package/src/mcp/tools/explore-api.ts +84 -0
- package/src/mcp/tools/generate-missing-tests.ts +80 -0
- package/src/mcp/tools/generate-tests-guide.ts +353 -0
- package/src/mcp/tools/manage-environment.ts +123 -0
- package/src/mcp/tools/manage-server.ts +87 -0
- package/src/mcp/tools/query-db.ts +141 -0
- package/src/mcp/tools/run-tests.ts +66 -0
- package/src/mcp/tools/save-test-suite.ts +164 -0
- package/src/mcp/tools/send-request.ts +53 -0
- package/src/mcp/tools/setup-api.ts +49 -0
- package/src/mcp/tools/validate-tests.ts +42 -0
- package/src/tui/chat-ui.ts +150 -0
- package/src/web/routes/api.ts +234 -0
- package/src/web/routes/dashboard.ts +348 -0
- package/src/web/routes/runs.ts +64 -0
- package/src/web/schemas.ts +121 -0
- package/src/web/server.ts +134 -0
- package/src/web/static/htmx.min.js +1 -0
- package/src/web/static/style.css +265 -0
- package/src/web/views/layout.ts +46 -0
- package/src/web/views/results.ts +209 -0
- package/tests/agent/agent-loop.test.ts +61 -0
- package/tests/agent/context-manager.test.ts +59 -0
- package/tests/agent/system-prompt.test.ts +42 -0
- package/tests/agent/tools/diagnose-failure.test.ts +85 -0
- package/tests/agent/tools/explore-api.test.ts +59 -0
- package/tests/agent/tools/manage-environment.test.ts +78 -0
- package/tests/agent/tools/query-results.test.ts +77 -0
- package/tests/agent/tools/run-tests.test.ts +89 -0
- package/tests/agent/tools/send-request.test.ts +78 -0
- package/tests/agent/tools/validate-tests.test.ts +59 -0
- package/tests/ai/ai-generator.integration.test.ts +131 -0
- package/tests/ai/llm-client.test.ts +145 -0
- package/tests/ai/output-parser.test.ts +132 -0
- package/tests/ai/prompt-builder.test.ts +67 -0
- package/tests/ai/types.test.ts +55 -0
- package/tests/cli/args.test.ts +63 -0
- package/tests/cli/chat.test.ts +38 -0
- package/tests/cli/ci-init.test.ts +112 -0
- package/tests/cli/commands.test.ts +316 -0
- package/tests/cli/coverage.test.ts +58 -0
- package/tests/cli/doctor.test.ts +39 -0
- package/tests/cli/envs.test.ts +181 -0
- package/tests/cli/init.test.ts +80 -0
- package/tests/cli/runs.test.ts +94 -0
- package/tests/cli/safe-run.test.ts +103 -0
- package/tests/cli/update.test.ts +32 -0
- package/tests/core/generator/schema-utils.test.ts +108 -0
- package/tests/core/parser/nested-assertions.test.ts +80 -0
- package/tests/core/runner/root-body-assertions.test.ts +70 -0
- package/tests/db/chat-queries.test.ts +88 -0
- package/tests/db/chat-schema.test.ts +37 -0
- package/tests/db/environments.test.ts +131 -0
- package/tests/db/queries.test.ts +409 -0
- package/tests/db/schema.test.ts +141 -0
- package/tests/fixtures/.env.yaml +3 -0
- package/tests/fixtures/auth-token-test.yaml +8 -0
- package/tests/fixtures/bail/suite-a.yaml +6 -0
- package/tests/fixtures/bail/suite-b.yaml +6 -0
- package/tests/fixtures/crud.yaml +35 -0
- package/tests/fixtures/invalid-missing-name.yaml +5 -0
- package/tests/fixtures/invalid-no-method.yaml +6 -0
- package/tests/fixtures/petstore-auth.json +295 -0
- package/tests/fixtures/petstore-simple.json +151 -0
- package/tests/fixtures/post-only.yaml +12 -0
- package/tests/fixtures/simple.yaml +6 -0
- package/tests/fixtures/valid/.env.yaml +1 -0
- package/tests/fixtures/valid/a.yaml +5 -0
- package/tests/fixtures/valid/b.yml +5 -0
- package/tests/generator/coverage-scanner.test.ts +129 -0
- package/tests/generator/data-factory.test.ts +133 -0
- package/tests/generator/openapi-reader.test.ts +131 -0
- package/tests/integration/auth-flow.test.ts +217 -0
- package/tests/mcp/coverage-analysis.test.ts +64 -0
- package/tests/mcp/explore-api-schemas.test.ts +105 -0
- package/tests/mcp/explore-api.test.ts +49 -0
- package/tests/mcp/generate-missing-tests.test.ts +69 -0
- package/tests/mcp/manage-environment.test.ts +89 -0
- package/tests/mcp/save-test-suite.test.ts +116 -0
- package/tests/mcp/send-request.test.ts +79 -0
- package/tests/mcp/setup-api.test.ts +106 -0
- package/tests/mcp/tools.test.ts +248 -0
- package/tests/parser/schema.test.ts +134 -0
- package/tests/parser/variables.test.ts +227 -0
- package/tests/parser/yaml-parser.test.ts +69 -0
- package/tests/reporter/console.test.ts +256 -0
- package/tests/reporter/json.test.ts +98 -0
- package/tests/reporter/junit.test.ts +284 -0
- package/tests/runner/assertions.test.ts +262 -0
- package/tests/runner/executor.test.ts +310 -0
- package/tests/runner/http-client.test.ts +138 -0
- package/tests/web/routes.test.ts +160 -0
- 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,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
|
+
}
|