@openqa/cli 2.1.1 → 2.1.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/README.md +26 -0
- package/dist/agent/index-v2.js +244 -37
- package/dist/agent/index-v2.js.map +1 -1
- package/dist/agent/index.js +24 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/daemon.js +960 -565
- package/dist/cli/env.html.js +717 -529
- package/dist/cli/index.js +709 -521
- package/dist/cli/server.js +709 -521
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
<a href="https://github.com/Orka-Community/OpenQA/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@openqa/cli.svg" alt="license"></a>
|
|
15
15
|
<a href="https://openqa.orkajs.com"><img src="https://img.shields.io/badge/docs-orkajs.com-blue.svg" alt="documentation"></a>
|
|
16
16
|
<a href="https://discord.com/invite/DScfpuPysP"><img src="https://img.shields.io/badge/discord-join%20chat-7289da.svg" alt="discord"></a>
|
|
17
|
+
<a href="https://hub.docker.com/r/orklab/openqa"><img src="https://img.shields.io/docker/v/orklab/openqa?label=docker&logo=docker&color=0db7ed" alt="Docker Hub"></a>
|
|
18
|
+
<a href="https://hub.docker.com/r/orklab/openqa"><img src="https://img.shields.io/docker/pulls/orklab/openqa.svg" alt="Docker Pulls"></a>
|
|
17
19
|
</p>
|
|
18
20
|
|
|
19
21
|
---
|
|
@@ -48,6 +50,30 @@ OpenQA is a **truly autonomous** QA testing agent that thinks, codes, and execut
|
|
|
48
50
|
- **Regression Tests** - Verify bug fixes
|
|
49
51
|
- **Performance Tests** - Load times, resource usage
|
|
50
52
|
|
|
53
|
+
## 🐳 Docker Hub
|
|
54
|
+
|
|
55
|
+
OpenQA is available on Docker Hub at **[orklab/openqa](https://hub.docker.com/r/orklab/openqa)**.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Pull the latest image
|
|
59
|
+
docker pull orklab/openqa:latest
|
|
60
|
+
|
|
61
|
+
# Run with your API key
|
|
62
|
+
docker run -d \
|
|
63
|
+
-p 4242:3000 \
|
|
64
|
+
-e LLM_PROVIDER=openai \
|
|
65
|
+
-e OPENAI_API_KEY=sk-xxx \
|
|
66
|
+
-e OPENQA_JWT_SECRET=$(openssl rand -hex 32) \
|
|
67
|
+
-e SAAS_URL=https://your-app.com \
|
|
68
|
+
-v openqa-data:/app/data \
|
|
69
|
+
--name openqa \
|
|
70
|
+
orklab/openqa:latest
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then open **http://localhost:4242** — first run will prompt you to create an admin account.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
51
77
|
## 🚀 Quick Start
|
|
52
78
|
|
|
53
79
|
### Development (Local)
|
package/dist/agent/index-v2.js
CHANGED
|
@@ -771,9 +771,9 @@ function createQuickConfig(name, description, url, options) {
|
|
|
771
771
|
// agent/brain/index.ts
|
|
772
772
|
init_esm_shims();
|
|
773
773
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
774
|
-
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as
|
|
775
|
-
import { join as
|
|
776
|
-
import { execSync } from "child_process";
|
|
774
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
775
|
+
import { join as join4 } from "path";
|
|
776
|
+
import { execSync as execSync2 } from "child_process";
|
|
777
777
|
|
|
778
778
|
// agent/brain/llm-resilience.ts
|
|
779
779
|
init_esm_shims();
|
|
@@ -1011,10 +1011,150 @@ var ResilientLLM = class extends EventEmitter {
|
|
|
1011
1011
|
}
|
|
1012
1012
|
};
|
|
1013
1013
|
|
|
1014
|
+
// agent/brain/diff-analyzer.ts
|
|
1015
|
+
init_esm_shims();
|
|
1016
|
+
import { execSync } from "child_process";
|
|
1017
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1018
|
+
import { join as join3, basename, dirname as dirname2 } from "path";
|
|
1019
|
+
var DiffAnalyzer = class {
|
|
1020
|
+
/**
|
|
1021
|
+
* Get files changed between current branch and base branch
|
|
1022
|
+
*/
|
|
1023
|
+
getChangedFiles(repoPath, baseBranch = "main") {
|
|
1024
|
+
try {
|
|
1025
|
+
const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
|
|
1026
|
+
cwd: repoPath,
|
|
1027
|
+
stdio: "pipe"
|
|
1028
|
+
}).toString().trim();
|
|
1029
|
+
if (!output) return [];
|
|
1030
|
+
return output.split("\n").filter(Boolean);
|
|
1031
|
+
} catch {
|
|
1032
|
+
try {
|
|
1033
|
+
const output = execSync("git diff --name-only HEAD~1", {
|
|
1034
|
+
cwd: repoPath,
|
|
1035
|
+
stdio: "pipe"
|
|
1036
|
+
}).toString().trim();
|
|
1037
|
+
if (!output) return [];
|
|
1038
|
+
return output.split("\n").filter(Boolean);
|
|
1039
|
+
} catch {
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Map changed source files to their likely test files
|
|
1046
|
+
*/
|
|
1047
|
+
mapFilesToTests(changedFiles, repoPath) {
|
|
1048
|
+
const testFiles = /* @__PURE__ */ new Set();
|
|
1049
|
+
for (const file of changedFiles) {
|
|
1050
|
+
if (!this.isSourceFile(file)) continue;
|
|
1051
|
+
const candidates = this.getTestCandidates(file);
|
|
1052
|
+
for (const candidate of candidates) {
|
|
1053
|
+
if (existsSync2(join3(repoPath, candidate))) {
|
|
1054
|
+
testFiles.add(candidate);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
for (const file of changedFiles) {
|
|
1059
|
+
if (this.isTestFile(file)) {
|
|
1060
|
+
testFiles.add(file);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return Array.from(testFiles);
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Analyze a diff and return risk assessment + affected tests
|
|
1067
|
+
*/
|
|
1068
|
+
analyze(repoPath, baseBranch = "main") {
|
|
1069
|
+
const changedFiles = this.getChangedFiles(repoPath, baseBranch);
|
|
1070
|
+
const affectedTests = this.mapFilesToTests(changedFiles, repoPath);
|
|
1071
|
+
const riskLevel = this.assessRisk(changedFiles);
|
|
1072
|
+
const summary = this.buildSummary(changedFiles, affectedTests, riskLevel);
|
|
1073
|
+
return { changedFiles, affectedTests, riskLevel, summary };
|
|
1074
|
+
}
|
|
1075
|
+
getTestCandidates(filePath) {
|
|
1076
|
+
const dir = dirname2(filePath);
|
|
1077
|
+
const base = basename(filePath);
|
|
1078
|
+
const candidates = [];
|
|
1079
|
+
const nameMatch = base.match(/^(.+)\.(tsx?|jsx?|vue|svelte|py|go|rs)$/);
|
|
1080
|
+
if (!nameMatch) return candidates;
|
|
1081
|
+
const name = nameMatch[1];
|
|
1082
|
+
const ext = nameMatch[2];
|
|
1083
|
+
const testExts = ext.startsWith("ts") ? ["test.ts", "test.tsx", "spec.ts", "spec.tsx"] : ext.startsWith("js") ? ["test.js", "test.jsx", "spec.js", "spec.jsx"] : ext === "py" ? ["test.py"] : ext === "go" ? ["_test.go"] : [];
|
|
1084
|
+
for (const testExt of testExts) {
|
|
1085
|
+
candidates.push(join3(dir, `${name}.${testExt}`));
|
|
1086
|
+
candidates.push(join3(dir, "__tests__", `${name}.${testExt}`));
|
|
1087
|
+
candidates.push(join3(dir, "test", `${name}.${testExt}`));
|
|
1088
|
+
candidates.push(join3(dir, "tests", `${name}.${testExt}`));
|
|
1089
|
+
candidates.push(join3("__tests__", dir, `${name}.${testExt}`));
|
|
1090
|
+
}
|
|
1091
|
+
if (ext === "go") {
|
|
1092
|
+
candidates.push(join3(dir, `${name}_test.go`));
|
|
1093
|
+
}
|
|
1094
|
+
return candidates;
|
|
1095
|
+
}
|
|
1096
|
+
isSourceFile(file) {
|
|
1097
|
+
return /\.(tsx?|jsx?|vue|svelte|py|go|rs)$/.test(file) && !this.isTestFile(file);
|
|
1098
|
+
}
|
|
1099
|
+
isTestFile(file) {
|
|
1100
|
+
return /\.(test|spec)\.(tsx?|jsx?|py)$/.test(file) || /_test\.go$/.test(file) || file.includes("__tests__/");
|
|
1101
|
+
}
|
|
1102
|
+
assessRisk(changedFiles) {
|
|
1103
|
+
const highRiskPatterns = [
|
|
1104
|
+
/auth/i,
|
|
1105
|
+
/security/i,
|
|
1106
|
+
/middleware/i,
|
|
1107
|
+
/database/i,
|
|
1108
|
+
/migration/i,
|
|
1109
|
+
/config/i,
|
|
1110
|
+
/\.env/,
|
|
1111
|
+
/package\.json$/,
|
|
1112
|
+
/docker/i,
|
|
1113
|
+
/ci\//i,
|
|
1114
|
+
/payment/i,
|
|
1115
|
+
/billing/i,
|
|
1116
|
+
/permission/i
|
|
1117
|
+
];
|
|
1118
|
+
const mediumRiskPatterns = [
|
|
1119
|
+
/api/i,
|
|
1120
|
+
/route/i,
|
|
1121
|
+
/controller/i,
|
|
1122
|
+
/service/i,
|
|
1123
|
+
/model/i,
|
|
1124
|
+
/hook/i,
|
|
1125
|
+
/context/i,
|
|
1126
|
+
/store/i,
|
|
1127
|
+
/util/i
|
|
1128
|
+
];
|
|
1129
|
+
let highCount = 0;
|
|
1130
|
+
let mediumCount = 0;
|
|
1131
|
+
for (const file of changedFiles) {
|
|
1132
|
+
if (highRiskPatterns.some((p) => p.test(file))) highCount++;
|
|
1133
|
+
else if (mediumRiskPatterns.some((p) => p.test(file))) mediumCount++;
|
|
1134
|
+
}
|
|
1135
|
+
if (highCount >= 2 || highCount >= 1 && changedFiles.length > 5) return "high";
|
|
1136
|
+
if (highCount >= 1 || mediumCount >= 3) return "medium";
|
|
1137
|
+
return "low";
|
|
1138
|
+
}
|
|
1139
|
+
buildSummary(changedFiles, affectedTests, riskLevel) {
|
|
1140
|
+
const lines = [];
|
|
1141
|
+
lines.push(`${changedFiles.length} file(s) changed, ${affectedTests.length} test(s) affected.`);
|
|
1142
|
+
lines.push(`Risk level: ${riskLevel}.`);
|
|
1143
|
+
if (affectedTests.length > 0) {
|
|
1144
|
+
lines.push(`Run: ${affectedTests.slice(0, 5).join(", ")}${affectedTests.length > 5 ? ` (+${affectedTests.length - 5} more)` : ""}`);
|
|
1145
|
+
} else if (changedFiles.length > 0) {
|
|
1146
|
+
lines.push("No matching test files found \u2014 consider running full suite.");
|
|
1147
|
+
}
|
|
1148
|
+
return lines.join(" ");
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1014
1152
|
// agent/brain/index.ts
|
|
1015
1153
|
var OpenQABrain = class extends EventEmitter2 {
|
|
1016
1154
|
db;
|
|
1017
1155
|
llm;
|
|
1156
|
+
cache = new LLMCache();
|
|
1157
|
+
diffAnalyzer = new DiffAnalyzer();
|
|
1018
1158
|
saasConfig;
|
|
1019
1159
|
generatedTests = /* @__PURE__ */ new Map();
|
|
1020
1160
|
dynamicAgents = /* @__PURE__ */ new Map();
|
|
@@ -1075,7 +1215,9 @@ Respond in JSON format:
|
|
|
1075
1215
|
"suggestedAgents": ["Agent for X", "Agent for Y"],
|
|
1076
1216
|
"risks": ["Risk 1", "Risk 2"]
|
|
1077
1217
|
}`;
|
|
1078
|
-
const
|
|
1218
|
+
const cached = this.cache.get(prompt);
|
|
1219
|
+
const response = cached ?? await this.llm.generate(prompt);
|
|
1220
|
+
if (!cached) this.cache.set(prompt, response);
|
|
1079
1221
|
try {
|
|
1080
1222
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1081
1223
|
if (jsonMatch) {
|
|
@@ -1094,11 +1236,11 @@ Respond in JSON format:
|
|
|
1094
1236
|
async analyzeCodebase() {
|
|
1095
1237
|
let repoPath = this.saasConfig.localPath;
|
|
1096
1238
|
if (this.saasConfig.repoUrl && !this.saasConfig.localPath) {
|
|
1097
|
-
repoPath =
|
|
1098
|
-
if (!
|
|
1239
|
+
repoPath = join4(this.workDir, "repo");
|
|
1240
|
+
if (!existsSync3(repoPath)) {
|
|
1099
1241
|
logger.info("Cloning repository", { url: this.saasConfig.repoUrl });
|
|
1100
1242
|
try {
|
|
1101
|
-
|
|
1243
|
+
execSync2(`git clone --depth 1 ${this.saasConfig.repoUrl} ${repoPath}`, {
|
|
1102
1244
|
stdio: "pipe"
|
|
1103
1245
|
});
|
|
1104
1246
|
} catch (e) {
|
|
@@ -1107,13 +1249,13 @@ Respond in JSON format:
|
|
|
1107
1249
|
}
|
|
1108
1250
|
}
|
|
1109
1251
|
}
|
|
1110
|
-
if (!repoPath || !
|
|
1252
|
+
if (!repoPath || !existsSync3(repoPath)) {
|
|
1111
1253
|
return "";
|
|
1112
1254
|
}
|
|
1113
1255
|
const analysis = [];
|
|
1114
1256
|
try {
|
|
1115
|
-
const packageJsonPath =
|
|
1116
|
-
if (
|
|
1257
|
+
const packageJsonPath = join4(repoPath, "package.json");
|
|
1258
|
+
if (existsSync3(packageJsonPath)) {
|
|
1117
1259
|
const pkg = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
1118
1260
|
analysis.push(`### Package Info`);
|
|
1119
1261
|
analysis.push(`- Name: ${pkg.name}`);
|
|
@@ -1138,8 +1280,8 @@ Respond in JSON format:
|
|
|
1138
1280
|
}
|
|
1139
1281
|
const routePatterns = ["routes", "pages", "views", "controllers", "api"];
|
|
1140
1282
|
for (const pattern of routePatterns) {
|
|
1141
|
-
const routeDir =
|
|
1142
|
-
if (
|
|
1283
|
+
const routeDir = join4(repoPath, "src", pattern);
|
|
1284
|
+
if (existsSync3(routeDir)) {
|
|
1143
1285
|
const routes = this.findFiles(routeDir, [".ts", ".tsx", ".js", ".jsx"], 20);
|
|
1144
1286
|
if (routes.length > 0) {
|
|
1145
1287
|
analysis.push(`
|
|
@@ -1166,7 +1308,7 @@ Respond in JSON format:
|
|
|
1166
1308
|
for (const item of items) {
|
|
1167
1309
|
if (results.length >= limit) return;
|
|
1168
1310
|
if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") continue;
|
|
1169
|
-
const fullPath =
|
|
1311
|
+
const fullPath = join4(d, item);
|
|
1170
1312
|
const stat = statSync2(fullPath);
|
|
1171
1313
|
if (stat.isDirectory()) {
|
|
1172
1314
|
find(fullPath);
|
|
@@ -1212,7 +1354,9 @@ Respond with JSON:
|
|
|
1212
1354
|
"code": "// complete test code here",
|
|
1213
1355
|
"priority": 1-5
|
|
1214
1356
|
}`;
|
|
1215
|
-
const
|
|
1357
|
+
const cached2 = this.cache.get(prompt);
|
|
1358
|
+
const response = cached2 ?? await this.llm.generate(prompt);
|
|
1359
|
+
if (!cached2) this.cache.set(prompt, response);
|
|
1216
1360
|
let testData = { name: target, description: "", code: "", priority: 3 };
|
|
1217
1361
|
try {
|
|
1218
1362
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
@@ -1239,7 +1383,7 @@ Respond with JSON:
|
|
|
1239
1383
|
}
|
|
1240
1384
|
saveTest(test) {
|
|
1241
1385
|
const filename = `${test.type}_${test.id}.ts`;
|
|
1242
|
-
const filepath =
|
|
1386
|
+
const filepath = join4(this.testsDir, filename);
|
|
1243
1387
|
const content = `/**
|
|
1244
1388
|
* Generated by OpenQA
|
|
1245
1389
|
* Type: ${test.type}
|
|
@@ -1277,7 +1421,9 @@ Respond with JSON:
|
|
|
1277
1421
|
"prompt": "Complete system prompt for this agent (be specific and detailed)",
|
|
1278
1422
|
"tools": ["tool1", "tool2"] // from: navigate, click, fill, screenshot, check_console, create_issue, create_ticket
|
|
1279
1423
|
}`;
|
|
1280
|
-
const
|
|
1424
|
+
const cached3 = this.cache.get(prompt);
|
|
1425
|
+
const response = cached3 ?? await this.llm.generate(prompt);
|
|
1426
|
+
if (!cached3) this.cache.set(prompt, response);
|
|
1281
1427
|
let agentData = { name: purpose, purpose, prompt: "", tools: [] };
|
|
1282
1428
|
try {
|
|
1283
1429
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
@@ -1308,8 +1454,8 @@ Respond with JSON:
|
|
|
1308
1454
|
test.executedAt = /* @__PURE__ */ new Date();
|
|
1309
1455
|
this.emit("test-started", test);
|
|
1310
1456
|
try {
|
|
1311
|
-
if (test.targetFile &&
|
|
1312
|
-
const result =
|
|
1457
|
+
if (test.targetFile && existsSync3(test.targetFile)) {
|
|
1458
|
+
const result = execSync2(`npx playwright test ${test.targetFile} --reporter=json`, {
|
|
1313
1459
|
cwd: this.testsDir,
|
|
1314
1460
|
stdio: "pipe",
|
|
1315
1461
|
timeout: 12e4
|
|
@@ -1363,7 +1509,9 @@ Respond with JSON:
|
|
|
1363
1509
|
{ "type": "analyze", "target": "what to analyze", "reason": "why" }
|
|
1364
1510
|
]
|
|
1365
1511
|
}`;
|
|
1366
|
-
const
|
|
1512
|
+
const cached4 = this.cache.get(prompt);
|
|
1513
|
+
const response = cached4 ?? await this.llm.generate(prompt);
|
|
1514
|
+
if (!cached4) this.cache.set(prompt, response);
|
|
1367
1515
|
try {
|
|
1368
1516
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1369
1517
|
if (jsonMatch) {
|
|
@@ -1455,13 +1603,50 @@ Respond with JSON:
|
|
|
1455
1603
|
}
|
|
1456
1604
|
};
|
|
1457
1605
|
}
|
|
1606
|
+
/**
|
|
1607
|
+
* B8 — Incremental mode: only generate tests for files changed vs baseBranch.
|
|
1608
|
+
* Emits 'diff-analyzed' with the DiffResult, then runs autonomously with a reduced
|
|
1609
|
+
* scope derived from the changed files.
|
|
1610
|
+
*/
|
|
1611
|
+
async runIncrementally(baseBranch = "main") {
|
|
1612
|
+
const repoPath = this.saasConfig.localPath;
|
|
1613
|
+
if (!repoPath) {
|
|
1614
|
+
logger.warn("runIncrementally: no localPath configured, falling back to full run");
|
|
1615
|
+
return this.runAutonomously();
|
|
1616
|
+
}
|
|
1617
|
+
const diff = this.diffAnalyzer.analyze(repoPath, baseBranch);
|
|
1618
|
+
logger.info("Incremental diff", { changedFiles: diff.changedFiles.length, affectedTests: diff.affectedTests.length, riskLevel: diff.riskLevel });
|
|
1619
|
+
this.emit("diff-analyzed", diff);
|
|
1620
|
+
if (diff.changedFiles.length === 0) {
|
|
1621
|
+
logger.info("No changed files \u2014 skipping incremental run");
|
|
1622
|
+
this.emit("session-complete", { testsGenerated: 0, agentsCreated: 0, incremental: true });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
for (const file of diff.changedFiles.slice(0, 10)) {
|
|
1626
|
+
const testType = this.inferTestType(file);
|
|
1627
|
+
const context = `Changed file: ${file}
|
|
1628
|
+
Risk level: ${diff.riskLevel}
|
|
1629
|
+
${diff.summary}`;
|
|
1630
|
+
try {
|
|
1631
|
+
await this.generateTest(testType, `Test coverage for ${file}`, context);
|
|
1632
|
+
} catch (e) {
|
|
1633
|
+
logger.error("Failed to generate test for file", { file, error: e instanceof Error ? e.message : String(e) });
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
this.emit("session-complete", {
|
|
1637
|
+
testsGenerated: this.generatedTests.size,
|
|
1638
|
+
agentsCreated: this.dynamicAgents.size,
|
|
1639
|
+
incremental: true,
|
|
1640
|
+
diff
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1458
1643
|
};
|
|
1459
1644
|
|
|
1460
1645
|
// agent/tools/browser.ts
|
|
1461
1646
|
init_esm_shims();
|
|
1462
1647
|
import { chromium } from "playwright";
|
|
1463
1648
|
import { mkdirSync as mkdirSync3 } from "fs";
|
|
1464
|
-
import { join as
|
|
1649
|
+
import { join as join5 } from "path";
|
|
1465
1650
|
var BrowserTools = class {
|
|
1466
1651
|
browser = null;
|
|
1467
1652
|
page = null;
|
|
@@ -1562,7 +1747,7 @@ var BrowserTools = class {
|
|
|
1562
1747
|
if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
|
|
1563
1748
|
try {
|
|
1564
1749
|
const filename = `${Date.now()}_${name}.png`;
|
|
1565
|
-
const path2 =
|
|
1750
|
+
const path2 = join5(this.screenshotDir, filename);
|
|
1566
1751
|
await this.page.screenshot({ path: path2, fullPage: true });
|
|
1567
1752
|
this.db.createAction({
|
|
1568
1753
|
session_id: this.sessionId,
|
|
@@ -1718,6 +1903,16 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1718
1903
|
for (const commit of commits) {
|
|
1719
1904
|
if (this.lastCommitSha && commit.sha === this.lastCommitSha) break;
|
|
1720
1905
|
const isMerge = commit.parents && commit.parents.length > 1;
|
|
1906
|
+
let changedFiles;
|
|
1907
|
+
try {
|
|
1908
|
+
const { data: detail } = await this.octokit.repos.getCommit({
|
|
1909
|
+
owner: this.config.owner,
|
|
1910
|
+
repo: this.config.repo,
|
|
1911
|
+
ref: commit.sha
|
|
1912
|
+
});
|
|
1913
|
+
changedFiles = (detail.files ?? []).map((f) => f.filename);
|
|
1914
|
+
} catch {
|
|
1915
|
+
}
|
|
1721
1916
|
const event = {
|
|
1722
1917
|
type: isMerge ? "merge" : "push",
|
|
1723
1918
|
provider: "github",
|
|
@@ -1725,7 +1920,8 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1725
1920
|
commit: commit.sha,
|
|
1726
1921
|
author: commit.commit.author?.name || "unknown",
|
|
1727
1922
|
message: commit.commit.message,
|
|
1728
|
-
timestamp: new Date(commit.commit.author?.date || Date.now())
|
|
1923
|
+
timestamp: new Date(commit.commit.author?.date || Date.now()),
|
|
1924
|
+
changedFiles
|
|
1729
1925
|
};
|
|
1730
1926
|
this.emit("git-event", event);
|
|
1731
1927
|
if (isMerge) {
|
|
@@ -1811,6 +2007,16 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1811
2007
|
for (const commit of commits) {
|
|
1812
2008
|
if (this.lastCommitSha && commit.id === this.lastCommitSha) break;
|
|
1813
2009
|
const isMerge = commit.parent_ids && commit.parent_ids.length > 1;
|
|
2010
|
+
let changedFiles;
|
|
2011
|
+
try {
|
|
2012
|
+
const diffRes = await fetch(
|
|
2013
|
+
`${baseUrl}/api/v4/projects/${projectPath}/repository/commits/${commit.id}/diff`,
|
|
2014
|
+
{ headers }
|
|
2015
|
+
);
|
|
2016
|
+
const diffs = await diffRes.json();
|
|
2017
|
+
changedFiles = diffs.map((d) => d.new_path);
|
|
2018
|
+
} catch {
|
|
2019
|
+
}
|
|
1814
2020
|
const event = {
|
|
1815
2021
|
type: isMerge ? "merge" : "push",
|
|
1816
2022
|
provider: "gitlab",
|
|
@@ -1818,7 +2024,8 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1818
2024
|
commit: commit.id,
|
|
1819
2025
|
author: commit.author_name,
|
|
1820
2026
|
message: commit.message,
|
|
1821
|
-
timestamp: new Date(commit.created_at)
|
|
2027
|
+
timestamp: new Date(commit.created_at),
|
|
2028
|
+
changedFiles
|
|
1822
2029
|
};
|
|
1823
2030
|
this.emit("git-event", event);
|
|
1824
2031
|
if (isMerge) {
|
|
@@ -1913,8 +2120,8 @@ var GitListener = class extends EventEmitter3 {
|
|
|
1913
2120
|
init_esm_shims();
|
|
1914
2121
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
1915
2122
|
import { spawn } from "child_process";
|
|
1916
|
-
import { existsSync as
|
|
1917
|
-
import { join as
|
|
2123
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "fs";
|
|
2124
|
+
import { join as join6, resolve } from "path";
|
|
1918
2125
|
function sanitizeRepoPath(inputPath) {
|
|
1919
2126
|
if (typeof inputPath !== "string" || !inputPath.trim()) {
|
|
1920
2127
|
throw new ProjectRunnerError("repoPath must be a non-empty string");
|
|
@@ -1942,14 +2149,14 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
1942
2149
|
*/
|
|
1943
2150
|
detectProjectType(repoPath) {
|
|
1944
2151
|
repoPath = sanitizeRepoPath(repoPath);
|
|
1945
|
-
const pkgPath =
|
|
1946
|
-
if (
|
|
2152
|
+
const pkgPath = join6(repoPath, "package.json");
|
|
2153
|
+
if (existsSync4(pkgPath)) {
|
|
1947
2154
|
return this.detectNodeProject(repoPath, pkgPath);
|
|
1948
2155
|
}
|
|
1949
|
-
if (
|
|
2156
|
+
if (existsSync4(join6(repoPath, "requirements.txt")) || existsSync4(join6(repoPath, "pyproject.toml"))) {
|
|
1950
2157
|
return this.detectPythonProject(repoPath);
|
|
1951
2158
|
}
|
|
1952
|
-
if (
|
|
2159
|
+
if (existsSync4(join6(repoPath, "go.mod"))) {
|
|
1953
2160
|
return {
|
|
1954
2161
|
language: "go",
|
|
1955
2162
|
packageManager: "go",
|
|
@@ -1958,7 +2165,7 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
1958
2165
|
devCommand: "go run ."
|
|
1959
2166
|
};
|
|
1960
2167
|
}
|
|
1961
|
-
if (
|
|
2168
|
+
if (existsSync4(join6(repoPath, "Cargo.toml"))) {
|
|
1962
2169
|
return {
|
|
1963
2170
|
language: "rust",
|
|
1964
2171
|
packageManager: "cargo",
|
|
@@ -1974,9 +2181,9 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
1974
2181
|
const scripts = pkg.scripts || {};
|
|
1975
2182
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1976
2183
|
let packageManager = "npm";
|
|
1977
|
-
if (
|
|
1978
|
-
else if (
|
|
1979
|
-
else if (
|
|
2184
|
+
if (existsSync4(join6(repoPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
|
|
2185
|
+
else if (existsSync4(join6(repoPath, "yarn.lock"))) packageManager = "yarn";
|
|
2186
|
+
else if (existsSync4(join6(repoPath, "bun.lockb"))) packageManager = "bun";
|
|
1980
2187
|
let framework;
|
|
1981
2188
|
if (deps["next"]) framework = "next";
|
|
1982
2189
|
else if (deps["nuxt"]) framework = "nuxt";
|
|
@@ -2028,13 +2235,13 @@ var ProjectRunner = class extends EventEmitter4 {
|
|
|
2028
2235
|
}
|
|
2029
2236
|
detectPythonProject(repoPath) {
|
|
2030
2237
|
let testRunner;
|
|
2031
|
-
const reqPath =
|
|
2032
|
-
if (
|
|
2238
|
+
const reqPath = join6(repoPath, "requirements.txt");
|
|
2239
|
+
if (existsSync4(reqPath)) {
|
|
2033
2240
|
const content = readFileSync4(reqPath, "utf-8");
|
|
2034
2241
|
if (content.includes("pytest")) testRunner = "pytest";
|
|
2035
2242
|
}
|
|
2036
|
-
const pyprojectPath =
|
|
2037
|
-
if (
|
|
2243
|
+
const pyprojectPath = join6(repoPath, "pyproject.toml");
|
|
2244
|
+
if (existsSync4(pyprojectPath)) {
|
|
2038
2245
|
const content = readFileSync4(pyprojectPath, "utf-8");
|
|
2039
2246
|
if (content.includes("pytest")) testRunner = "pytest";
|
|
2040
2247
|
}
|