@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,22 @@
|
|
|
1
|
+
import { startServer } from "../../web/server.ts";
|
|
2
|
+
|
|
3
|
+
export interface ServeOptions {
|
|
4
|
+
port?: number;
|
|
5
|
+
host?: string;
|
|
6
|
+
openapiSpec?: string;
|
|
7
|
+
testsDir?: string;
|
|
8
|
+
dbPath?: string;
|
|
9
|
+
watch?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function serveCommand(options: ServeOptions): Promise<number> {
|
|
13
|
+
await startServer({
|
|
14
|
+
port: options.port,
|
|
15
|
+
host: options.host,
|
|
16
|
+
dbPath: options.dbPath,
|
|
17
|
+
dev: options.watch,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Keep running — Bun.serve keeps the process alive
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { tmpdir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, unlinkSync, renameSync, copyFileSync, chmodSync } from "fs";
|
|
4
|
+
import { VERSION } from "../index.ts";
|
|
5
|
+
import { isCompiledBinary } from "../runtime.ts";
|
|
6
|
+
|
|
7
|
+
export interface UpdateCommandOptions {
|
|
8
|
+
force?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function detectTarget(): { target: string; archive: "tar.gz" | "zip" } {
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
const arch = process.arch;
|
|
14
|
+
|
|
15
|
+
const os = platform === "win32" ? "win" : platform;
|
|
16
|
+
const archSuffix = arch === "arm64" ? "arm64" : "x64";
|
|
17
|
+
const target = `${os}-${archSuffix}`;
|
|
18
|
+
const archive = platform === "win32" ? "zip" : ("tar.gz" as const);
|
|
19
|
+
|
|
20
|
+
return { target, archive };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseVersion(tag: string): string {
|
|
24
|
+
return tag.replace(/^v/, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function compareVersions(a: string, b: string): number {
|
|
28
|
+
const pa = a.split(".").map(Number);
|
|
29
|
+
const pb = b.split(".").map(Number);
|
|
30
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
31
|
+
const na = pa[i] ?? 0;
|
|
32
|
+
const nb = pb[i] ?? 0;
|
|
33
|
+
if (na !== nb) return na - nb;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function updateCommand(options: UpdateCommandOptions): Promise<number> {
|
|
39
|
+
if (!isCompiledBinary()) {
|
|
40
|
+
console.log("Running from source — use git pull to update.");
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log("Checking for updates...");
|
|
45
|
+
|
|
46
|
+
// Fetch latest release
|
|
47
|
+
const res = await fetch("https://api.github.com/repos/kirrosh/apitool/releases/latest", {
|
|
48
|
+
headers: { "User-Agent": "apitool-updater" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
console.error(`Failed to check for updates: HTTP ${res.status}`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const release = (await res.json()) as { tag_name: string };
|
|
57
|
+
const latestVersion = parseVersion(release.tag_name);
|
|
58
|
+
const currentVersion = VERSION;
|
|
59
|
+
|
|
60
|
+
if (!options.force && compareVersions(currentVersion, latestVersion) >= 0) {
|
|
61
|
+
console.log(`Already up to date (v${currentVersion}).`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`Updating v${currentVersion} → v${latestVersion}...`);
|
|
66
|
+
|
|
67
|
+
const { target, archive } = detectTarget();
|
|
68
|
+
const assetName = `apitool-${target}.${archive}`;
|
|
69
|
+
const downloadUrl = `https://github.com/kirrosh/apitool/releases/download/${release.tag_name}/${assetName}`;
|
|
70
|
+
|
|
71
|
+
// Download artifact
|
|
72
|
+
const dlRes = await fetch(downloadUrl);
|
|
73
|
+
if (!dlRes.ok) {
|
|
74
|
+
console.error(`Failed to download ${assetName}: HTTP ${dlRes.status}`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tempDir = tmpdir();
|
|
79
|
+
const archivePath = join(tempDir, assetName);
|
|
80
|
+
const archiveBytes = new Uint8Array(await dlRes.arrayBuffer());
|
|
81
|
+
await Bun.write(archivePath, archiveBytes);
|
|
82
|
+
|
|
83
|
+
// Extract
|
|
84
|
+
const extractDir = join(tempDir, `apitool-update-${Date.now()}`);
|
|
85
|
+
const mkdirResult = Bun.spawnSync(["mkdir", "-p", extractDir]);
|
|
86
|
+
if (mkdirResult.exitCode !== 0) {
|
|
87
|
+
// Fallback for Windows
|
|
88
|
+
const { mkdirSync } = await import("fs");
|
|
89
|
+
mkdirSync(extractDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (archive === "tar.gz") {
|
|
93
|
+
const tar = Bun.spawnSync(["tar", "-xzf", archivePath, "-C", extractDir]);
|
|
94
|
+
if (tar.exitCode !== 0) {
|
|
95
|
+
console.error("Failed to extract archive.");
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Windows zip — use tar (Windows 10+ includes bsdtar)
|
|
100
|
+
const tar = Bun.spawnSync(["tar", "-xf", archivePath, "-C", extractDir]);
|
|
101
|
+
if (tar.exitCode !== 0) {
|
|
102
|
+
console.error("Failed to extract archive.");
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Find extracted binary
|
|
108
|
+
const binaryName = process.platform === "win32" ? "apitool.exe" : "apitool";
|
|
109
|
+
const newBinary = join(extractDir, binaryName);
|
|
110
|
+
|
|
111
|
+
if (!existsSync(newBinary)) {
|
|
112
|
+
console.error(`Expected binary not found: ${newBinary}`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Replace current binary
|
|
117
|
+
const currentBinary = process.execPath;
|
|
118
|
+
|
|
119
|
+
if (process.platform === "win32") {
|
|
120
|
+
// Windows: can't overwrite running exe — rename current to .old, copy new
|
|
121
|
+
const oldPath = currentBinary + ".old";
|
|
122
|
+
try {
|
|
123
|
+
if (existsSync(oldPath)) unlinkSync(oldPath);
|
|
124
|
+
} catch { /* ignore */ }
|
|
125
|
+
renameSync(currentBinary, oldPath);
|
|
126
|
+
copyFileSync(newBinary, currentBinary);
|
|
127
|
+
} else {
|
|
128
|
+
// Unix: rename new over current (atomic on same filesystem, but we copy across)
|
|
129
|
+
unlinkSync(currentBinary);
|
|
130
|
+
copyFileSync(newBinary, currentBinary);
|
|
131
|
+
chmodSync(currentBinary, 0o755);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Cleanup
|
|
135
|
+
try {
|
|
136
|
+
unlinkSync(archivePath);
|
|
137
|
+
unlinkSync(newBinary);
|
|
138
|
+
} catch { /* best effort */ }
|
|
139
|
+
|
|
140
|
+
console.log(`Updated to v${latestVersion}.`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parse } from "../../core/parser/yaml-parser.ts";
|
|
2
|
+
import { printError, printSuccess } from "../output.ts";
|
|
3
|
+
|
|
4
|
+
export interface ValidateOptions {
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function validateCommand(options: ValidateOptions): Promise<number> {
|
|
9
|
+
try {
|
|
10
|
+
const suites = await parse(options.path);
|
|
11
|
+
const totalSteps = suites.reduce((sum, s) => sum + s.tests.length, 0);
|
|
12
|
+
printSuccess(`OK: ${suites.length} suite(s), ${totalSteps} test(s) validated successfully`);
|
|
13
|
+
return 0;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { runCommand } from "./commands/run.ts";
|
|
4
|
+
import { validateCommand } from "./commands/validate.ts";
|
|
5
|
+
import { serveCommand } from "./commands/serve.ts";
|
|
6
|
+
import { collectionsCommand } from "./commands/collections.ts";
|
|
7
|
+
import { aiGenerateCommand } from "./commands/ai-generate.ts";
|
|
8
|
+
import { mcpCommand } from "./commands/mcp.ts";
|
|
9
|
+
import { initCommand } from "./commands/init.ts";
|
|
10
|
+
import { updateCommand } from "./commands/update.ts";
|
|
11
|
+
import { chatCommand } from "./commands/chat.ts";
|
|
12
|
+
import { envsCommand } from "./commands/envs.ts";
|
|
13
|
+
import { runsCommand } from "./commands/runs.ts";
|
|
14
|
+
import { coverageCommand } from "./commands/coverage.ts";
|
|
15
|
+
import { doctorCommand } from "./commands/doctor.ts";
|
|
16
|
+
import { addApiCommand } from "./commands/add-api.ts";
|
|
17
|
+
import { ciInitCommand } from "./commands/ci-init.ts";
|
|
18
|
+
import { printError } from "./output.ts";
|
|
19
|
+
import { getRuntimeInfo } from "./runtime.ts";
|
|
20
|
+
import { getDb } from "../db/schema.ts";
|
|
21
|
+
import { findCollectionByNameOrId } from "../db/queries.ts";
|
|
22
|
+
import type { ReporterName } from "../core/reporter/types.ts";
|
|
23
|
+
|
|
24
|
+
export const VERSION = "0.3.0";
|
|
25
|
+
|
|
26
|
+
export interface ParsedArgs {
|
|
27
|
+
command: string | undefined;
|
|
28
|
+
positional: string[];
|
|
29
|
+
flags: Record<string, string | boolean>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
33
|
+
// argv: [bunPath, scriptPath, ...userArgs]
|
|
34
|
+
const args = argv.slice(2);
|
|
35
|
+
let command: string | undefined;
|
|
36
|
+
const positional: string[] = [];
|
|
37
|
+
const flags: Record<string, string | boolean> = {};
|
|
38
|
+
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < args.length) {
|
|
41
|
+
const arg = args[i]!;
|
|
42
|
+
|
|
43
|
+
if (arg.startsWith("--")) {
|
|
44
|
+
const eqIndex = arg.indexOf("=");
|
|
45
|
+
if (eqIndex !== -1) {
|
|
46
|
+
// --flag=value
|
|
47
|
+
flags[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1);
|
|
48
|
+
} else {
|
|
49
|
+
const key = arg.slice(2);
|
|
50
|
+
const next = args[i + 1];
|
|
51
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
52
|
+
flags[key] = next;
|
|
53
|
+
i++;
|
|
54
|
+
} else {
|
|
55
|
+
flags[key] = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
59
|
+
// Short flag: -h, -v
|
|
60
|
+
flags[arg.slice(1)] = true;
|
|
61
|
+
} else if (command === undefined) {
|
|
62
|
+
command = arg;
|
|
63
|
+
} else {
|
|
64
|
+
positional.push(arg);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { command, positional, flags };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printUsage(): void {
|
|
74
|
+
console.log(`apitool - API Testing Platform
|
|
75
|
+
|
|
76
|
+
Usage:
|
|
77
|
+
apitool add-api <name> Register a new API (collection)
|
|
78
|
+
apitool run <path> Run API tests
|
|
79
|
+
apitool validate <path> Validate test files without running
|
|
80
|
+
apitool ai-generate --from <spec> --prompt "..." Generate tests with AI
|
|
81
|
+
apitool envs [list|get|set|delete] Manage environments
|
|
82
|
+
apitool runs [id] View test run history
|
|
83
|
+
apitool coverage --spec <path> --tests <dir> Analyze API test coverage
|
|
84
|
+
apitool collections List test collections
|
|
85
|
+
apitool serve Start web dashboard
|
|
86
|
+
apitool init Initialize a new apitool project
|
|
87
|
+
apitool ci init Generate CI/CD workflow (GitHub Actions, GitLab CI)
|
|
88
|
+
apitool mcp Start MCP server (stdio transport for AI agents)
|
|
89
|
+
--dir <path> Set working directory (relative paths resolve here)
|
|
90
|
+
apitool chat Start interactive AI chat for API testing
|
|
91
|
+
apitool doctor Run diagnostic checks
|
|
92
|
+
apitool update Update to latest version
|
|
93
|
+
|
|
94
|
+
Options for 'add-api':
|
|
95
|
+
--spec <path-or-url> OpenAPI spec (extracts base_url from servers[0])
|
|
96
|
+
--dir <directory> Base directory (default: ./apis/<name>/)
|
|
97
|
+
--env key=value Set environment variables (repeatable)
|
|
98
|
+
|
|
99
|
+
Options for 'chat':
|
|
100
|
+
--provider <name> LLM provider: ollama, openai, anthropic, custom (default: ollama)
|
|
101
|
+
--model <name> Model name (default: provider-specific)
|
|
102
|
+
--api-key <key> API key (or set APITOOL_AI_KEY env var)
|
|
103
|
+
--base-url <url> Provider base URL override
|
|
104
|
+
--safe Only allow running GET tests (read-only mode)
|
|
105
|
+
|
|
106
|
+
Options for 'envs':
|
|
107
|
+
envs List all environments
|
|
108
|
+
envs get <name> Show variables in an environment
|
|
109
|
+
envs set <name> K=V Set variables (multiple KEY=VALUE pairs)
|
|
110
|
+
envs delete <name> Delete an environment
|
|
111
|
+
envs import <name> <file> Import environment from YAML file
|
|
112
|
+
envs export <name> Export environment as YAML to stdout
|
|
113
|
+
--api <name> Scope operation to a specific API collection
|
|
114
|
+
|
|
115
|
+
Options for 'runs':
|
|
116
|
+
runs List recent test runs
|
|
117
|
+
runs <id> Show run details with step results
|
|
118
|
+
--limit <n> Number of runs to show (default: 20)
|
|
119
|
+
|
|
120
|
+
Options for 'coverage':
|
|
121
|
+
--api <name> Use API collection (auto-resolves spec and tests dir)
|
|
122
|
+
--spec <path> Path to OpenAPI spec (required unless --api used)
|
|
123
|
+
--tests <dir> Path to test files directory (required unless --api used)
|
|
124
|
+
|
|
125
|
+
Options for 'run':
|
|
126
|
+
--api <name> Use API collection (resolves test path automatically)
|
|
127
|
+
--env <name> Use environment file (.env.<name>.yaml)
|
|
128
|
+
--report <format> Output format: console, json, junit (default: console)
|
|
129
|
+
--timeout <ms> Override request timeout
|
|
130
|
+
--bail Stop on first suite failure
|
|
131
|
+
--no-db Do not save results to apitool.db
|
|
132
|
+
--db <path> Path to SQLite database file (default: apitool.db)
|
|
133
|
+
--auth-token <token> Auth token injected as {{auth_token}} variable
|
|
134
|
+
--safe Run only GET tests (read-only, safe mode)
|
|
135
|
+
--tag <tag> Filter suites by tag (repeatable, comma-separated, OR logic)
|
|
136
|
+
|
|
137
|
+
Options for 'ai-generate':
|
|
138
|
+
--api <name> Use API collection (auto-resolves spec and output dir)
|
|
139
|
+
--from <spec> Path to OpenAPI spec (required unless --api used)
|
|
140
|
+
--prompt <text> Test scenario description (required)
|
|
141
|
+
--provider <name> LLM provider: ollama, openai, anthropic, custom (default: ollama)
|
|
142
|
+
--model <name> Model name (default: provider-specific)
|
|
143
|
+
--api-key <key> API key (or set APITOOL_AI_KEY env var)
|
|
144
|
+
--base-url <url> Provider base URL override
|
|
145
|
+
--output <dir> Output directory (default: ./generated/ai/)
|
|
146
|
+
|
|
147
|
+
Options for 'serve':
|
|
148
|
+
--port <port> Server port (default: 8080)
|
|
149
|
+
--host <host> Server host (default: 0.0.0.0)
|
|
150
|
+
--openapi <spec> Path to OpenAPI spec for Explorer
|
|
151
|
+
--db <path> Path to SQLite database file (default: apitool.db)
|
|
152
|
+
--watch Enable dev mode with hot reload (auto-refresh browser on file changes)
|
|
153
|
+
|
|
154
|
+
Options for 'ci init':
|
|
155
|
+
--github Generate GitHub Actions workflow
|
|
156
|
+
--gitlab Generate GitLab CI config
|
|
157
|
+
--dir <path> Project root directory (default: current directory)
|
|
158
|
+
--force Overwrite existing CI config
|
|
159
|
+
|
|
160
|
+
General:
|
|
161
|
+
--help, -h Show this help
|
|
162
|
+
--version, -v Show version`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const VALID_REPORTERS = new Set<string>(["console", "json", "junit"]);
|
|
166
|
+
|
|
167
|
+
async function main(): Promise<number> {
|
|
168
|
+
const { command, positional, flags } = parseArgs(process.argv);
|
|
169
|
+
|
|
170
|
+
// Help
|
|
171
|
+
if (command === "help" || command === "--help" || flags["help"] === true || flags["h"] === true) {
|
|
172
|
+
printUsage();
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Version
|
|
177
|
+
if (command === "--version" || flags["version"] === true || flags["v"] === true) {
|
|
178
|
+
console.log(`apitool ${VERSION} (${getRuntimeInfo()})`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!command) {
|
|
183
|
+
printUsage();
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
switch (command) {
|
|
188
|
+
case "add-api": {
|
|
189
|
+
const name = positional[0];
|
|
190
|
+
if (!name) {
|
|
191
|
+
printError("Missing name argument. Usage: apitool add-api <name> [--spec <path>] [--dir <dir>]");
|
|
192
|
+
return 2;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Collect all --env flags (parseArgs only stores last one, so re-parse)
|
|
196
|
+
const envValues: string[] = [];
|
|
197
|
+
const rawArgs = process.argv.slice(2);
|
|
198
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
199
|
+
if (rawArgs[i] === "--env" && rawArgs[i + 1] && rawArgs[i + 1]!.includes("=")) {
|
|
200
|
+
envValues.push(rawArgs[i + 1]!);
|
|
201
|
+
i++;
|
|
202
|
+
} else if (rawArgs[i]?.startsWith("--env=") && rawArgs[i]!.slice(6).includes("=")) {
|
|
203
|
+
envValues.push(rawArgs[i]!.slice(6));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return addApiCommand({
|
|
208
|
+
name,
|
|
209
|
+
spec: typeof flags["spec"] === "string" ? flags["spec"] : undefined,
|
|
210
|
+
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
211
|
+
envPairs: envValues.length > 0 ? envValues : undefined,
|
|
212
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case "run": {
|
|
217
|
+
let path = positional[0];
|
|
218
|
+
const apiFlag = typeof flags["api"] === "string" ? flags["api"] : undefined;
|
|
219
|
+
if (!path && apiFlag) {
|
|
220
|
+
try {
|
|
221
|
+
getDb(typeof flags["db"] === "string" ? flags["db"] : undefined);
|
|
222
|
+
const col = findCollectionByNameOrId(apiFlag);
|
|
223
|
+
if (!col) { printError(`API '${apiFlag}' not found`); return 1; }
|
|
224
|
+
path = col.test_path;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
printError(`Failed to resolve --api: ${(err as Error).message}`);
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!path) {
|
|
231
|
+
printError("Missing path argument. Usage: apitool run <path> or apitool run --api <name>");
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const report = (flags["report"] as string) ?? "console";
|
|
236
|
+
if (!VALID_REPORTERS.has(report)) {
|
|
237
|
+
printError(`Unknown reporter: ${report}. Available: console, json, junit`);
|
|
238
|
+
return 2;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const timeoutRaw = flags["timeout"];
|
|
242
|
+
let timeout: number | undefined;
|
|
243
|
+
if (typeof timeoutRaw === "string") {
|
|
244
|
+
timeout = parseInt(timeoutRaw, 10);
|
|
245
|
+
if (isNaN(timeout) || timeout <= 0) {
|
|
246
|
+
printError(`Invalid timeout value: ${timeoutRaw}`);
|
|
247
|
+
return 2;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Collect all --tag flags (parseArgs only stores last one, so re-parse)
|
|
252
|
+
const tagValues: string[] = [];
|
|
253
|
+
const rawRunArgs = process.argv.slice(2);
|
|
254
|
+
for (let i = 0; i < rawRunArgs.length; i++) {
|
|
255
|
+
if (rawRunArgs[i] === "--tag" && rawRunArgs[i + 1]) {
|
|
256
|
+
tagValues.push(rawRunArgs[i + 1]!);
|
|
257
|
+
i++;
|
|
258
|
+
} else if (rawRunArgs[i]?.startsWith("--tag=")) {
|
|
259
|
+
tagValues.push(rawRunArgs[i]!.slice("--tag=".length));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Support comma-separated: --tag smoke,crud → ["smoke", "crud"]
|
|
263
|
+
const tags = tagValues.flatMap(v => v.split(",")).filter(Boolean);
|
|
264
|
+
|
|
265
|
+
return runCommand({
|
|
266
|
+
path,
|
|
267
|
+
env: typeof flags["env"] === "string" ? flags["env"] : undefined,
|
|
268
|
+
report: report as ReporterName,
|
|
269
|
+
timeout,
|
|
270
|
+
bail: flags["bail"] === true,
|
|
271
|
+
noDb: flags["no-db"] === true,
|
|
272
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
273
|
+
authToken: typeof flags["auth-token"] === "string" ? flags["auth-token"] : undefined,
|
|
274
|
+
safe: flags["safe"] === true,
|
|
275
|
+
tag: tags.length > 0 ? tags : undefined,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case "validate": {
|
|
280
|
+
const path = positional[0];
|
|
281
|
+
if (!path) {
|
|
282
|
+
printError("Missing path argument. Usage: apitool validate <path>");
|
|
283
|
+
return 2;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return validateCommand({ path });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case "ai-generate": {
|
|
290
|
+
let from = flags["from"] as string | undefined;
|
|
291
|
+
let output = typeof flags["output"] === "string" ? flags["output"] : undefined;
|
|
292
|
+
const aiGenApiFlag = typeof flags["api"] === "string" ? flags["api"] : undefined;
|
|
293
|
+
|
|
294
|
+
// Resolve --api to spec and output dir from collection
|
|
295
|
+
if (aiGenApiFlag) {
|
|
296
|
+
try {
|
|
297
|
+
getDb(typeof flags["db"] === "string" ? flags["db"] : undefined);
|
|
298
|
+
const col = findCollectionByNameOrId(aiGenApiFlag);
|
|
299
|
+
if (!col) { printError(`API '${aiGenApiFlag}' not found`); return 1; }
|
|
300
|
+
if (!from && col.openapi_spec) from = col.openapi_spec;
|
|
301
|
+
if (!output && col.test_path) output = col.test_path;
|
|
302
|
+
} catch (err) {
|
|
303
|
+
printError(`Failed to resolve --api: ${(err as Error).message}`);
|
|
304
|
+
return 2;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (typeof from !== "string") {
|
|
309
|
+
printError("Missing --from <spec>. Usage: apitool ai-generate --from <spec> --prompt \"...\"");
|
|
310
|
+
return 2;
|
|
311
|
+
}
|
|
312
|
+
const prompt = flags["prompt"];
|
|
313
|
+
if (typeof prompt !== "string") {
|
|
314
|
+
printError("Missing --prompt <text>. Usage: apitool ai-generate --from <spec> --prompt \"...\"");
|
|
315
|
+
return 2;
|
|
316
|
+
}
|
|
317
|
+
return aiGenerateCommand({
|
|
318
|
+
from,
|
|
319
|
+
prompt,
|
|
320
|
+
provider: typeof flags["provider"] === "string" ? flags["provider"] : "ollama",
|
|
321
|
+
model: typeof flags["model"] === "string" ? flags["model"] : undefined,
|
|
322
|
+
apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : undefined,
|
|
323
|
+
baseUrl: typeof flags["base-url"] === "string" ? flags["base-url"] : undefined,
|
|
324
|
+
output,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case "collections": {
|
|
329
|
+
return collectionsCommand(
|
|
330
|
+
typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
case "serve": {
|
|
335
|
+
const portRaw = flags["port"];
|
|
336
|
+
let port: number | undefined;
|
|
337
|
+
if (typeof portRaw === "string") {
|
|
338
|
+
port = parseInt(portRaw, 10);
|
|
339
|
+
if (isNaN(port) || port <= 0) {
|
|
340
|
+
printError(`Invalid port value: ${portRaw}`);
|
|
341
|
+
return 2;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return serveCommand({
|
|
345
|
+
port,
|
|
346
|
+
host: typeof flags["host"] === "string" ? flags["host"] : undefined,
|
|
347
|
+
openapiSpec: typeof flags["openapi"] === "string" ? flags["openapi"] : undefined,
|
|
348
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
349
|
+
watch: flags["watch"] === true,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case "init": {
|
|
354
|
+
return initCommand({
|
|
355
|
+
force: flags["force"] === true,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case "mcp": {
|
|
360
|
+
return mcpCommand({
|
|
361
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
362
|
+
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case "chat": {
|
|
367
|
+
return chatCommand({
|
|
368
|
+
provider: typeof flags["provider"] === "string" ? flags["provider"] : undefined,
|
|
369
|
+
model: typeof flags["model"] === "string" ? flags["model"] : undefined,
|
|
370
|
+
apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : undefined,
|
|
371
|
+
baseUrl: typeof flags["base-url"] === "string" ? flags["base-url"] : undefined,
|
|
372
|
+
safe: flags["safe"] === true,
|
|
373
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
case "update": {
|
|
378
|
+
return updateCommand({ force: flags["force"] === true });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
case "envs": {
|
|
382
|
+
const sub = positional[0] as string | undefined;
|
|
383
|
+
const validActions = ["get", "set", "delete", "import", "export"] as const;
|
|
384
|
+
const action = validActions.includes(sub as any) ? (sub as typeof validActions[number]) : "list";
|
|
385
|
+
const name = action === "list" ? undefined : positional[1];
|
|
386
|
+
const pairs = action === "set" ? positional.slice(2) : undefined;
|
|
387
|
+
const file = action === "import" ? positional[2] : undefined;
|
|
388
|
+
|
|
389
|
+
return envsCommand({
|
|
390
|
+
action,
|
|
391
|
+
name,
|
|
392
|
+
pairs,
|
|
393
|
+
file,
|
|
394
|
+
api: typeof flags["api"] === "string" ? flags["api"] : undefined,
|
|
395
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case "runs": {
|
|
400
|
+
const idRaw = positional[0];
|
|
401
|
+
let runId: number | undefined;
|
|
402
|
+
if (idRaw) {
|
|
403
|
+
runId = parseInt(idRaw, 10);
|
|
404
|
+
if (isNaN(runId)) {
|
|
405
|
+
printError(`Invalid run ID: ${idRaw}`);
|
|
406
|
+
return 2;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const limitRaw = flags["limit"];
|
|
411
|
+
let limit: number | undefined;
|
|
412
|
+
if (typeof limitRaw === "string") {
|
|
413
|
+
limit = parseInt(limitRaw, 10);
|
|
414
|
+
if (isNaN(limit) || limit <= 0) {
|
|
415
|
+
printError(`Invalid limit value: ${limitRaw}`);
|
|
416
|
+
return 2;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return runsCommand({
|
|
421
|
+
runId,
|
|
422
|
+
limit,
|
|
423
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
case "ci": {
|
|
428
|
+
const ciSub = positional[0];
|
|
429
|
+
if (ciSub !== "init") {
|
|
430
|
+
printError("Usage: apitool ci init [--github|--gitlab] [--force]");
|
|
431
|
+
return 2;
|
|
432
|
+
}
|
|
433
|
+
let platform: "github" | "gitlab" | undefined;
|
|
434
|
+
if (flags["github"] === true) platform = "github";
|
|
435
|
+
else if (flags["gitlab"] === true) platform = "gitlab";
|
|
436
|
+
return ciInitCommand({
|
|
437
|
+
platform,
|
|
438
|
+
force: flags["force"] === true,
|
|
439
|
+
dir: typeof flags["dir"] === "string" ? flags["dir"] : undefined,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
case "doctor": {
|
|
444
|
+
return doctorCommand({
|
|
445
|
+
dbPath: typeof flags["db"] === "string" ? flags["db"] : undefined,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
case "coverage": {
|
|
450
|
+
let spec = flags["spec"] as string | undefined;
|
|
451
|
+
let tests = flags["tests"] as string | undefined;
|
|
452
|
+
const coverageApiFlag = typeof flags["api"] === "string" ? flags["api"] : undefined;
|
|
453
|
+
|
|
454
|
+
if (coverageApiFlag) {
|
|
455
|
+
try {
|
|
456
|
+
getDb(typeof flags["db"] === "string" ? flags["db"] : undefined);
|
|
457
|
+
const col = findCollectionByNameOrId(coverageApiFlag);
|
|
458
|
+
if (!col) { printError(`API '${coverageApiFlag}' not found`); return 1; }
|
|
459
|
+
if (!spec && col.openapi_spec) spec = col.openapi_spec;
|
|
460
|
+
if (!tests && col.test_path) tests = col.test_path;
|
|
461
|
+
} catch (err) {
|
|
462
|
+
printError(`Failed to resolve --api: ${(err as Error).message}`);
|
|
463
|
+
return 2;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (typeof spec !== "string") {
|
|
468
|
+
printError("Missing --spec <path>. Usage: apitool coverage --spec <path> --tests <dir>");
|
|
469
|
+
return 2;
|
|
470
|
+
}
|
|
471
|
+
if (typeof tests !== "string") {
|
|
472
|
+
printError("Missing --tests <dir>. Usage: apitool coverage --spec <path> --tests <dir>");
|
|
473
|
+
return 2;
|
|
474
|
+
}
|
|
475
|
+
return coverageCommand({ spec, tests });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
default: {
|
|
479
|
+
printError(`Unknown command: ${command}`);
|
|
480
|
+
printUsage();
|
|
481
|
+
return 2;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Only run when executed directly, not when imported
|
|
487
|
+
const scriptPath = process.argv[1]?.replaceAll("\\", "/") ?? "";
|
|
488
|
+
const metaFile = import.meta.filename?.replaceAll("\\", "/") ?? "";
|
|
489
|
+
const isMain = scriptPath === metaFile
|
|
490
|
+
|| scriptPath.endsWith("cli/index.ts")
|
|
491
|
+
|| import.meta.main === true;
|
|
492
|
+
if (isMain) {
|
|
493
|
+
try {
|
|
494
|
+
const code = await main();
|
|
495
|
+
process.exitCode = code;
|
|
496
|
+
} catch (err) {
|
|
497
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
498
|
+
process.exitCode = 2;
|
|
499
|
+
}
|
|
500
|
+
}
|