@kirrosh/zond 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/package.json +53 -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 +163 -0
- package/src/cli/commands/collections.ts +41 -0
- package/src/cli/commands/compare.ts +129 -0
- package/src/cli/commands/coverage.ts +156 -0
- package/src/cli/commands/doctor.ts +127 -0
- package/src/cli/commands/init.ts +84 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/run.ts +156 -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 +529 -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 +28 -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 +46 -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/diagnostics/failure-hints.ts +63 -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/chunker.ts +47 -0
- package/src/core/generator/coverage-scanner.ts +87 -0
- package/src/core/generator/data-factory.ts +115 -0
- package/src/core/generator/endpoint-warnings.ts +43 -0
- package/src/core/generator/index.ts +12 -0
- package/src/core/generator/openapi-reader.ts +143 -0
- package/src/core/generator/schema-utils.ts +52 -0
- package/src/core/generator/serializer.ts +189 -0
- package/src/core/generator/types.ts +48 -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 +52 -0
- package/src/core/parser/variables.ts +154 -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 +173 -0
- package/src/core/runner/execute-run.ts +97 -0
- package/src/core/runner/executor.ts +183 -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 +113 -0
- package/src/core/utils.ts +9 -0
- package/src/db/queries.ts +774 -0
- package/src/db/schema.ts +159 -0
- package/src/mcp/descriptions.ts +88 -0
- package/src/mcp/server.ts +52 -0
- package/src/mcp/tools/ci-init.ts +54 -0
- package/src/mcp/tools/coverage-analysis.ts +141 -0
- package/src/mcp/tools/describe-endpoint.ts +241 -0
- package/src/mcp/tools/explore-api.ts +84 -0
- package/src/mcp/tools/generate-and-save.ts +129 -0
- package/src/mcp/tools/generate-missing-tests.ts +91 -0
- package/src/mcp/tools/generate-tests-guide.ts +391 -0
- package/src/mcp/tools/manage-server.ts +86 -0
- package/src/mcp/tools/query-db.ts +255 -0
- package/src/mcp/tools/run-tests.ts +71 -0
- package/src/mcp/tools/save-test-suite.ts +218 -0
- package/src/mcp/tools/send-request.ts +63 -0
- package/src/mcp/tools/set-work-dir.ts +35 -0
- package/src/mcp/tools/setup-api.ts +84 -0
- package/src/mcp/tools/validate-tests.ts +43 -0
- package/src/tui/chat-ui.ts +150 -0
- package/src/web/data/collection-state.ts +360 -0
- package/src/web/routes/api.ts +234 -0
- package/src/web/routes/dashboard.ts +313 -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 +827 -0
- package/src/web/views/endpoints-tab.ts +170 -0
- package/src/web/views/health-strip.ts +92 -0
- package/src/web/views/layout.ts +48 -0
- package/src/web/views/results.ts +209 -0
- package/src/web/views/runs-tab.ts +126 -0
- package/src/web/views/suites-tab.ts +153 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { startMcpServer } from "../../mcp/server.ts";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface McpCommandOptions {
|
|
5
|
+
dbPath?: string;
|
|
6
|
+
dir?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function mcpCommand(options: McpCommandOptions): Promise<number> {
|
|
10
|
+
if (options.dir) {
|
|
11
|
+
process.chdir(resolve(options.dir));
|
|
12
|
+
}
|
|
13
|
+
await startMcpServer({ dbPath: options.dbPath });
|
|
14
|
+
// Server runs until stdin closes — this promise never resolves during normal operation
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { dirname } from "path";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { parse } from "../../core/parser/yaml-parser.ts";
|
|
4
|
+
import { loadEnvironment } from "../../core/parser/variables.ts";
|
|
5
|
+
import { filterSuitesByTags } from "../../core/parser/filter.ts";
|
|
6
|
+
import { runSuite } from "../../core/runner/executor.ts";
|
|
7
|
+
import { getReporter } from "../../core/reporter/index.ts";
|
|
8
|
+
import type { ReporterName } from "../../core/reporter/types.ts";
|
|
9
|
+
import type { TestSuite } from "../../core/parser/types.ts";
|
|
10
|
+
import type { TestRunResult } from "../../core/runner/types.ts";
|
|
11
|
+
import { printError, printWarning } from "../output.ts";
|
|
12
|
+
import { getDb } from "../../db/schema.ts";
|
|
13
|
+
import { createRun, finalizeRun, saveResults, findCollectionByTestPath } from "../../db/queries.ts";
|
|
14
|
+
|
|
15
|
+
export interface RunOptions {
|
|
16
|
+
path: string;
|
|
17
|
+
env?: string;
|
|
18
|
+
report: ReporterName;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
bail: boolean;
|
|
21
|
+
noDb?: boolean;
|
|
22
|
+
dbPath?: string;
|
|
23
|
+
authToken?: string;
|
|
24
|
+
safe?: boolean;
|
|
25
|
+
tag?: string[];
|
|
26
|
+
envVars?: string[];
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runCommand(options: RunOptions): Promise<number> {
|
|
31
|
+
// 1. Parse test files
|
|
32
|
+
let suites: TestSuite[];
|
|
33
|
+
try {
|
|
34
|
+
suites = await parse(options.path);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (suites.length === 0) {
|
|
41
|
+
printWarning(`No test files found in ${options.path}`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 1b. Tag filter
|
|
46
|
+
if (options.tag && options.tag.length > 0) {
|
|
47
|
+
suites = filterSuitesByTags(suites, options.tag);
|
|
48
|
+
if (suites.length === 0) {
|
|
49
|
+
printWarning("No suites match the specified tags");
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 1c. Safe mode: filter to GET-only tests
|
|
55
|
+
if (options.safe) {
|
|
56
|
+
for (const suite of suites) {
|
|
57
|
+
suite.tests = suite.tests.filter(t => t.method === "GET");
|
|
58
|
+
}
|
|
59
|
+
suites = suites.filter(s => s.tests.length > 0);
|
|
60
|
+
if (suites.length === 0) {
|
|
61
|
+
printWarning("No GET tests found. Nothing to run in safe mode.");
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Load environment (resolve collection for scoped envs)
|
|
67
|
+
// Use path itself as searchDir if it's a directory; dirname() on a dir path gives the parent
|
|
68
|
+
const pathStat = await stat(options.path).catch(() => null);
|
|
69
|
+
const searchDir = pathStat?.isDirectory() ? options.path : dirname(options.path);
|
|
70
|
+
let collectionForEnv: { id: number } | null = null;
|
|
71
|
+
if (!options.noDb) {
|
|
72
|
+
try {
|
|
73
|
+
getDb(options.dbPath);
|
|
74
|
+
collectionForEnv = findCollectionByTestPath(options.path);
|
|
75
|
+
} catch { /* DB not available — OK */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let env: Record<string, string> = {};
|
|
79
|
+
try {
|
|
80
|
+
env = await loadEnvironment(options.env, searchDir);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
printError(`Failed to load environment: ${(err as Error).message}`);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Inject CLI auth token — overrides env file value
|
|
87
|
+
if (options.authToken) {
|
|
88
|
+
env.auth_token = options.authToken;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Inject --env-var KEY=VALUE overrides (highest priority)
|
|
92
|
+
if (options.envVars && options.envVars.length > 0) {
|
|
93
|
+
for (const pair of options.envVars) {
|
|
94
|
+
const eqIdx = pair.indexOf("=");
|
|
95
|
+
if (eqIdx > 0) {
|
|
96
|
+
env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Warn if --env was explicitly set but file was not found (empty env)
|
|
102
|
+
if (options.env && Object.keys(env).length === 0) {
|
|
103
|
+
printWarning(`Environment file .env.${options.env}.yaml not found in ${searchDir}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Apply timeout override
|
|
107
|
+
if (options.timeout !== undefined) {
|
|
108
|
+
for (const suite of suites) {
|
|
109
|
+
suite.config.timeout = options.timeout;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. Run suites
|
|
114
|
+
const results: TestRunResult[] = [];
|
|
115
|
+
const dryRun = options.dryRun === true;
|
|
116
|
+
if (options.bail) {
|
|
117
|
+
// Sequential with bail at suite level
|
|
118
|
+
for (const suite of suites) {
|
|
119
|
+
const result = await runSuite(suite, env, dryRun);
|
|
120
|
+
results.push(result);
|
|
121
|
+
if (!dryRun && (result.failed > 0 || result.steps.some((s) => s.status === "error"))) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Parallel
|
|
127
|
+
const all = await Promise.all(suites.map((suite) => runSuite(suite, env, dryRun)));
|
|
128
|
+
results.push(...all);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 5. Report
|
|
132
|
+
const reporter = getReporter(options.report);
|
|
133
|
+
reporter.report(results);
|
|
134
|
+
|
|
135
|
+
// 6. Save to DB
|
|
136
|
+
if (!options.noDb) {
|
|
137
|
+
try {
|
|
138
|
+
getDb(options.dbPath);
|
|
139
|
+
const collection = findCollectionByTestPath(options.path);
|
|
140
|
+
const runId = createRun({
|
|
141
|
+
started_at: results[0]?.started_at ?? new Date().toISOString(),
|
|
142
|
+
environment: options.env,
|
|
143
|
+
collection_id: collection?.id,
|
|
144
|
+
});
|
|
145
|
+
finalizeRun(runId, results);
|
|
146
|
+
saveResults(runId, results);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
printWarning(`Failed to save results to DB: ${(err as Error).message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 7. Exit code (always 0 in dry-run mode)
|
|
153
|
+
if (dryRun) return 0;
|
|
154
|
+
const hasFailures = results.some((r) => r.failed > 0 || r.steps.some((s) => s.status === "error"));
|
|
155
|
+
return hasFailures ? 1 : 0;
|
|
156
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getDb } from "../../db/schema.ts";
|
|
2
|
+
import { listRuns, getRunById, getResultsByRunId } from "../../db/queries.ts";
|
|
3
|
+
import { printError } from "../output.ts";
|
|
4
|
+
|
|
5
|
+
export interface RunsOptions {
|
|
6
|
+
runId?: number;
|
|
7
|
+
limit?: number;
|
|
8
|
+
dbPath?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RESET = "\x1b[0m";
|
|
12
|
+
const GREEN = "\x1b[32m";
|
|
13
|
+
const RED = "\x1b[31m";
|
|
14
|
+
const YELLOW = "\x1b[33m";
|
|
15
|
+
|
|
16
|
+
function useColor(): boolean {
|
|
17
|
+
return process.stdout.isTTY ?? false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function statusIcon(passed: number, failed: number): string {
|
|
21
|
+
const color = useColor();
|
|
22
|
+
if (failed === 0) return color ? `${GREEN}PASS${RESET}` : "PASS";
|
|
23
|
+
return color ? `${RED}FAIL${RESET}` : "FAIL";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runsCommand(options: RunsOptions): number {
|
|
27
|
+
const { runId, limit = 20, dbPath } = options;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
getDb(dbPath);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
printError(`Failed to open database: ${(err as Error).message}`);
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (runId !== undefined) {
|
|
37
|
+
return showRunDetail(runId);
|
|
38
|
+
}
|
|
39
|
+
return showRunList(limit);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function showRunList(limit: number): number {
|
|
43
|
+
const runs = listRuns(limit);
|
|
44
|
+
|
|
45
|
+
if (runs.length === 0) {
|
|
46
|
+
console.log("No runs found.");
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Print table
|
|
51
|
+
const header = "ID STATUS TOTAL PASS FAIL ENV DURATION STARTED";
|
|
52
|
+
console.log(header);
|
|
53
|
+
console.log("-".repeat(header.length));
|
|
54
|
+
|
|
55
|
+
for (const run of runs) {
|
|
56
|
+
const status = statusIcon(run.passed, run.failed);
|
|
57
|
+
const env = (run.environment ?? "-").slice(0, 10).padEnd(10);
|
|
58
|
+
const duration = run.duration_ms != null ? `${run.duration_ms}ms` : "-";
|
|
59
|
+
const started = run.started_at.slice(0, 19).replace("T", " ");
|
|
60
|
+
console.log(
|
|
61
|
+
`${String(run.id).padEnd(6)} ${status.padEnd(useColor() ? 14 : 6)} ${String(run.total).padEnd(5)} ${String(run.passed).padEnd(4)} ${String(run.failed).padEnd(4)} ${env} ${duration.padEnd(8)} ${started}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function showRunDetail(runId: number): number {
|
|
69
|
+
const run = getRunById(runId);
|
|
70
|
+
if (!run) {
|
|
71
|
+
printError(`Run #${runId} not found`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const color = useColor();
|
|
76
|
+
|
|
77
|
+
console.log(`Run #${run.id}`);
|
|
78
|
+
console.log(` Started: ${run.started_at}`);
|
|
79
|
+
if (run.finished_at) console.log(` Finished: ${run.finished_at}`);
|
|
80
|
+
if (run.environment) console.log(` Environment: ${run.environment}`);
|
|
81
|
+
if (run.duration_ms != null) console.log(` Duration: ${run.duration_ms}ms`);
|
|
82
|
+
console.log(` Total: ${run.total} Passed: ${run.passed} Failed: ${run.failed} Skipped: ${run.skipped}`);
|
|
83
|
+
|
|
84
|
+
const results = getResultsByRunId(runId);
|
|
85
|
+
if (results.length === 0) {
|
|
86
|
+
console.log("\nNo step results recorded.");
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log("\nSteps:");
|
|
91
|
+
for (const r of results) {
|
|
92
|
+
let statusStr: string;
|
|
93
|
+
if (r.status === "pass") {
|
|
94
|
+
statusStr = color ? `${GREEN}PASS${RESET}` : "PASS";
|
|
95
|
+
} else if (r.status === "fail" || r.status === "error") {
|
|
96
|
+
statusStr = color ? `${RED}${r.status.toUpperCase()}${RESET}` : r.status.toUpperCase();
|
|
97
|
+
} else {
|
|
98
|
+
statusStr = color ? `${YELLOW}SKIP${RESET}` : "SKIP";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(` ${statusStr} ${r.test_name} (${r.duration_ms}ms)`);
|
|
102
|
+
if (r.error_message) {
|
|
103
|
+
console.log(` ${color ? RED : ""}${r.error_message}${color ? RESET : ""}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
@@ -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/zond/releases/latest", {
|
|
48
|
+
headers: { "User-Agent": "zond-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 = `zond-${target}.${archive}`;
|
|
69
|
+
const downloadUrl = `https://github.com/kirrosh/zond/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, `zond-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" ? "zond.exe" : "zond";
|
|
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
|
+
}
|