@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 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)
@@ -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 existsSync2, mkdirSync as mkdirSync2 } from "fs";
775
- import { join as join3 } from "path";
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 response = await this.llm.generate(prompt);
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 = join3(this.workDir, "repo");
1098
- if (!existsSync2(repoPath)) {
1239
+ repoPath = join4(this.workDir, "repo");
1240
+ if (!existsSync3(repoPath)) {
1099
1241
  logger.info("Cloning repository", { url: this.saasConfig.repoUrl });
1100
1242
  try {
1101
- execSync(`git clone --depth 1 ${this.saasConfig.repoUrl} ${repoPath}`, {
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 || !existsSync2(repoPath)) {
1252
+ if (!repoPath || !existsSync3(repoPath)) {
1111
1253
  return "";
1112
1254
  }
1113
1255
  const analysis = [];
1114
1256
  try {
1115
- const packageJsonPath = join3(repoPath, "package.json");
1116
- if (existsSync2(packageJsonPath)) {
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 = join3(repoPath, "src", pattern);
1142
- if (existsSync2(routeDir)) {
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 = join3(d, item);
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 response = await this.llm.generate(prompt);
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 = join3(this.testsDir, filename);
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 response = await this.llm.generate(prompt);
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 && existsSync2(test.targetFile)) {
1312
- const result = execSync(`npx playwright test ${test.targetFile} --reporter=json`, {
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 response = await this.llm.generate(prompt);
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 join4 } from "path";
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 = join4(this.screenshotDir, filename);
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 existsSync3, readFileSync as readFileSync4, statSync } from "fs";
1917
- import { join as join5, resolve } from "path";
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 = join5(repoPath, "package.json");
1946
- if (existsSync3(pkgPath)) {
2152
+ const pkgPath = join6(repoPath, "package.json");
2153
+ if (existsSync4(pkgPath)) {
1947
2154
  return this.detectNodeProject(repoPath, pkgPath);
1948
2155
  }
1949
- if (existsSync3(join5(repoPath, "requirements.txt")) || existsSync3(join5(repoPath, "pyproject.toml"))) {
2156
+ if (existsSync4(join6(repoPath, "requirements.txt")) || existsSync4(join6(repoPath, "pyproject.toml"))) {
1950
2157
  return this.detectPythonProject(repoPath);
1951
2158
  }
1952
- if (existsSync3(join5(repoPath, "go.mod"))) {
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 (existsSync3(join5(repoPath, "Cargo.toml"))) {
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 (existsSync3(join5(repoPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
1978
- else if (existsSync3(join5(repoPath, "yarn.lock"))) packageManager = "yarn";
1979
- else if (existsSync3(join5(repoPath, "bun.lockb"))) packageManager = "bun";
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 = join5(repoPath, "requirements.txt");
2032
- if (existsSync3(reqPath)) {
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 = join5(repoPath, "pyproject.toml");
2037
- if (existsSync3(pyprojectPath)) {
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
  }