agenthud 0.6.1 → 0.6.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/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import React3 from "react";
5
5
  import { render } from "ink";
6
- import { existsSync } from "fs";
6
+ import { existsSync as existsSync8 } from "fs";
7
7
 
8
8
  // src/ui/App.tsx
9
9
  import React2, { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo3, useRef as useRef3 } from "react";
@@ -1346,18 +1346,17 @@ function useHotkeys({
1346
1346
  }
1347
1347
 
1348
1348
  // src/data/git.ts
1349
- import { execSync as nodeExecSync, exec as nodeExec } from "child_process";
1349
+ import { execSync, exec } from "child_process";
1350
1350
  import { promisify } from "util";
1351
- var execAsync = promisify(nodeExec);
1351
+ var execAsync = promisify(exec);
1352
1352
  function cleanOutput(str) {
1353
1353
  let result = str.replace(/^\uFEFF/, "");
1354
1354
  result = result.replace(/^"|"$/g, "");
1355
1355
  return result.trim();
1356
1356
  }
1357
- var execFn = (command, options2) => nodeExecSync(command, options2);
1358
1357
  function getUncommittedCount() {
1359
1358
  try {
1360
- const result = execFn("git status --porcelain", {
1359
+ const result = execSync("git status --porcelain", {
1361
1360
  encoding: "utf-8"
1362
1361
  });
1363
1362
  const lines = result.trim().split("\n").filter(Boolean);
@@ -1408,21 +1407,21 @@ function getGitData(config) {
1408
1407
  };
1409
1408
  let branch = null;
1410
1409
  try {
1411
- const result = execFn(commands.branch, { encoding: "utf-8" });
1410
+ const result = execSync(commands.branch, { encoding: "utf-8" });
1412
1411
  branch = result.trim();
1413
1412
  } catch {
1414
1413
  branch = null;
1415
1414
  }
1416
1415
  let commits = [];
1417
1416
  try {
1418
- const result = execFn(commands.commits, { encoding: "utf-8" });
1417
+ const result = execSync(commands.commits, { encoding: "utf-8" });
1419
1418
  commits = parseCommitsOutput(result);
1420
1419
  } catch {
1421
1420
  commits = [];
1422
1421
  }
1423
1422
  let stats = { added: 0, deleted: 0, files: 0 };
1424
1423
  try {
1425
- const result = execFn(commands.stats, { encoding: "utf-8" });
1424
+ const result = execSync(commands.stats, { encoding: "utf-8" });
1426
1425
  stats = parseStatsOutput(result);
1427
1426
  } catch {
1428
1427
  stats = { added: 0, deleted: 0, files: 0 };
@@ -1462,29 +1461,249 @@ async function getGitDataAsync(config) {
1462
1461
  }
1463
1462
 
1464
1463
  // src/data/tests.ts
1465
- import { readFileSync as nodeReadFileSync } from "fs";
1466
- import { execSync as nodeExecSync2 } from "child_process";
1464
+ import { readFileSync as readFileSync3 } from "fs";
1465
+ import { execSync as execSync3 } from "child_process";
1467
1466
  import { join } from "path";
1468
- var AGENT_DIR = ".agenthud";
1469
- var TEST_RESULTS_FILE = "test-results.json";
1470
- var readFileFn = (path) => nodeReadFileSync(path, "utf-8");
1471
- var getHeadHashFn = () => {
1472
- return nodeExecSync2("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
1467
+
1468
+ // src/runner/command.ts
1469
+ import { execSync as execSync2 } from "child_process";
1470
+ import {
1471
+ existsSync as existsSync2,
1472
+ unlinkSync,
1473
+ readFileSync as readFileSync2
1474
+ } from "fs";
1475
+
1476
+ // src/data/detectTestFramework.ts
1477
+ import {
1478
+ existsSync,
1479
+ readFileSync
1480
+ } from "fs";
1481
+ var TEST_RESULTS_FILE = ".agenthud/test-results.xml";
1482
+ var FRAMEWORK_COMMANDS = {
1483
+ vitest: `npx vitest run --reporter=junit --outputFile=${TEST_RESULTS_FILE}`,
1484
+ jest: `JEST_JUNIT_OUTPUT_FILE=${TEST_RESULTS_FILE} npx jest --reporters=jest-junit`,
1485
+ mocha: `npx mocha --reporter mocha-junit-reporter --reporter-options mochaFile=${TEST_RESULTS_FILE}`,
1486
+ pytest: `uv run pytest --junitxml=${TEST_RESULTS_FILE}`
1473
1487
  };
1474
- var getCommitCountFn = (fromHash) => {
1475
- const result = nodeExecSync2(`git rev-list ${fromHash}..HEAD --count`, {
1488
+ var JS_FRAMEWORKS = ["vitest", "jest", "mocha"];
1489
+ function detectTestFramework() {
1490
+ const jsFramework = detectJsFramework();
1491
+ if (jsFramework) {
1492
+ return jsFramework;
1493
+ }
1494
+ const pythonFramework = detectPythonFramework();
1495
+ if (pythonFramework) {
1496
+ return pythonFramework;
1497
+ }
1498
+ return null;
1499
+ }
1500
+ function detectJsFramework() {
1501
+ if (!existsSync("package.json")) {
1502
+ return null;
1503
+ }
1504
+ let packageJson;
1505
+ try {
1506
+ const content = readFileSync("package.json", "utf-8");
1507
+ packageJson = JSON.parse(content);
1508
+ } catch {
1509
+ return null;
1510
+ }
1511
+ const allDeps = {
1512
+ ...packageJson.dependencies,
1513
+ ...packageJson.devDependencies
1514
+ };
1515
+ for (const framework of JS_FRAMEWORKS) {
1516
+ if (allDeps[framework]) {
1517
+ return {
1518
+ framework,
1519
+ command: FRAMEWORK_COMMANDS[framework]
1520
+ };
1521
+ }
1522
+ }
1523
+ return null;
1524
+ }
1525
+ function detectPythonFramework() {
1526
+ const pytestIndicators = ["pytest.ini", "conftest.py"];
1527
+ for (const file of pytestIndicators) {
1528
+ if (existsSync(file)) {
1529
+ return {
1530
+ framework: "pytest",
1531
+ command: FRAMEWORK_COMMANDS.pytest
1532
+ };
1533
+ }
1534
+ }
1535
+ if (existsSync("pyproject.toml")) {
1536
+ try {
1537
+ const content = readFileSync("pyproject.toml", "utf-8");
1538
+ if (content.includes("[tool.pytest") || content.includes("[tool.pytest.ini_options]")) {
1539
+ return {
1540
+ framework: "pytest",
1541
+ command: FRAMEWORK_COMMANDS.pytest
1542
+ };
1543
+ }
1544
+ } catch {
1545
+ }
1546
+ }
1547
+ const requirementsFiles = ["requirements.txt", "requirements-dev.txt"];
1548
+ for (const file of requirementsFiles) {
1549
+ if (existsSync(file)) {
1550
+ try {
1551
+ const content = readFileSync(file, "utf-8");
1552
+ if (content.includes("pytest")) {
1553
+ return {
1554
+ framework: "pytest",
1555
+ command: FRAMEWORK_COMMANDS.pytest
1556
+ };
1557
+ }
1558
+ } catch {
1559
+ }
1560
+ }
1561
+ }
1562
+ return null;
1563
+ }
1564
+
1565
+ // src/runner/command.ts
1566
+ function parseJUnitXml(xml) {
1567
+ try {
1568
+ if (!xml.includes("<testsuite") && !xml.includes("<testsuites")) {
1569
+ return null;
1570
+ }
1571
+ let totalTests = 0;
1572
+ let totalErrors = 0;
1573
+ let totalFailures = 0;
1574
+ let totalSkipped = 0;
1575
+ const failures = [];
1576
+ const testsuiteMatches = xml.match(/<testsuite\b[^>]*(?:\/>|>[\s\S]*?<\/testsuite>)/g) || [];
1577
+ const testsuites = testsuiteMatches.length > 0 ? testsuiteMatches : [xml];
1578
+ for (const suite of testsuites) {
1579
+ const suiteTag = suite.match(/<testsuite[^>]*>/)?.[0] || "";
1580
+ const testsMatch = suiteTag.match(/tests="(\d+)"/);
1581
+ const errorsMatch = suiteTag.match(/errors="(\d+)"/);
1582
+ const failuresMatch = suiteTag.match(/failures="(\d+)"/);
1583
+ const skippedMatch = suiteTag.match(/skipped="(\d+)"/);
1584
+ totalTests += testsMatch ? parseInt(testsMatch[1], 10) : 0;
1585
+ totalErrors += errorsMatch ? parseInt(errorsMatch[1], 10) : 0;
1586
+ totalFailures += failuresMatch ? parseInt(failuresMatch[1], 10) : 0;
1587
+ totalSkipped += skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
1588
+ const testcaseRegex = /<testcase[^>]*classname="([^"]*)"[^>]*name="([^"]*)"[^/>]*(?:\/>|>[\s\S]*?<\/testcase>)/g;
1589
+ let testcaseMatch;
1590
+ while ((testcaseMatch = testcaseRegex.exec(suite)) !== null) {
1591
+ const testcaseContent = testcaseMatch[0];
1592
+ const classname = testcaseMatch[1];
1593
+ const name = testcaseMatch[2];
1594
+ if (testcaseContent.includes("<failure") || testcaseContent.includes("<error")) {
1595
+ failures.push({
1596
+ file: classname,
1597
+ name
1598
+ });
1599
+ }
1600
+ }
1601
+ }
1602
+ if (totalTests === 0 && testsuiteMatches.length === 0) {
1603
+ return null;
1604
+ }
1605
+ const failed = totalFailures + totalErrors;
1606
+ const passed = totalTests - failed - totalSkipped;
1607
+ return {
1608
+ passed,
1609
+ failed,
1610
+ skipped: totalSkipped,
1611
+ failures
1612
+ };
1613
+ } catch {
1614
+ return null;
1615
+ }
1616
+ }
1617
+ function getHeadHash() {
1618
+ try {
1619
+ return execSync2("git rev-parse --short HEAD", {
1620
+ encoding: "utf-8",
1621
+ stdio: ["pipe", "pipe", "pipe"]
1622
+ }).trim();
1623
+ } catch {
1624
+ return "unknown";
1625
+ }
1626
+ }
1627
+ function runTestCommand(command, source = TEST_RESULTS_FILE) {
1628
+ try {
1629
+ if (existsSync2(source)) {
1630
+ unlinkSync(source);
1631
+ }
1632
+ } catch {
1633
+ }
1634
+ try {
1635
+ execSync2(command, {
1636
+ encoding: "utf-8",
1637
+ stdio: ["pipe", "pipe", "pipe"]
1638
+ });
1639
+ } catch {
1640
+ }
1641
+ if (!existsSync2(source)) {
1642
+ return {
1643
+ results: null,
1644
+ isOutdated: false,
1645
+ commitsBehind: 0,
1646
+ error: "Test command failed to produce output file"
1647
+ };
1648
+ }
1649
+ let content;
1650
+ try {
1651
+ content = readFileSync2(source, "utf-8");
1652
+ } catch (error) {
1653
+ const message = error instanceof Error ? error.message : String(error);
1654
+ return {
1655
+ results: null,
1656
+ isOutdated: false,
1657
+ commitsBehind: 0,
1658
+ error: `Failed to read result file: ${message}`
1659
+ };
1660
+ }
1661
+ const parsed = parseJUnitXml(content);
1662
+ if (!parsed) {
1663
+ return {
1664
+ results: null,
1665
+ isOutdated: false,
1666
+ commitsBehind: 0,
1667
+ error: "Failed to parse test results XML"
1668
+ };
1669
+ }
1670
+ const hash = getHeadHash();
1671
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1672
+ const results = {
1673
+ hash,
1674
+ timestamp,
1675
+ passed: parsed.passed,
1676
+ failed: parsed.failed,
1677
+ skipped: parsed.skipped,
1678
+ failures: parsed.failures
1679
+ };
1680
+ return {
1681
+ results,
1682
+ isOutdated: false,
1683
+ commitsBehind: 0
1684
+ };
1685
+ }
1686
+
1687
+ // src/data/tests.ts
1688
+ var AGENT_DIR = ".agenthud";
1689
+ var TEST_RESULTS_FILE2 = "test-results.json";
1690
+ function getHeadHash2() {
1691
+ return execSync3("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
1692
+ }
1693
+ function getCommitCount(fromHash) {
1694
+ const result = execSync3(`git rev-list ${fromHash}..HEAD --count`, {
1476
1695
  encoding: "utf-8"
1477
1696
  }).trim();
1478
1697
  return parseInt(result, 10) || 0;
1479
- };
1698
+ }
1480
1699
  function getTestData(dir = process.cwd()) {
1481
- const testResultsPath = join(dir, AGENT_DIR, TEST_RESULTS_FILE);
1700
+ const testResultsPath = join(dir, AGENT_DIR, TEST_RESULTS_FILE2);
1482
1701
  let results = null;
1483
1702
  let isOutdated = false;
1484
1703
  let commitsBehind = 0;
1485
1704
  let error;
1486
1705
  try {
1487
- const content = readFileFn(testResultsPath);
1706
+ const content = readFileSync3(testResultsPath, "utf-8");
1488
1707
  results = JSON.parse(content);
1489
1708
  } catch (e) {
1490
1709
  if (e instanceof SyntaxError) {
@@ -1495,10 +1714,10 @@ function getTestData(dir = process.cwd()) {
1495
1714
  return { results: null, isOutdated: false, commitsBehind: 0, error };
1496
1715
  }
1497
1716
  try {
1498
- const currentHash = getHeadHashFn();
1717
+ const currentHash = getHeadHash2();
1499
1718
  if (results.hash !== currentHash) {
1500
1719
  isOutdated = true;
1501
- commitsBehind = getCommitCountFn(results.hash);
1720
+ commitsBehind = getCommitCount(results.hash);
1502
1721
  }
1503
1722
  } catch {
1504
1723
  isOutdated = false;
@@ -1508,16 +1727,12 @@ function getTestData(dir = process.cwd()) {
1508
1727
  }
1509
1728
 
1510
1729
  // src/data/project.ts
1511
- import { execSync as nodeExecSync3 } from "child_process";
1730
+ import { execSync as execSync4 } from "child_process";
1512
1731
  import {
1513
- existsSync as nodeExistsSync,
1514
- readFileSync as nodeReadFileSync2,
1515
- readdirSync as nodeReaddirSync
1732
+ existsSync as existsSync3,
1733
+ readFileSync as readFileSync4
1516
1734
  } from "fs";
1517
1735
  import { basename } from "path";
1518
- var fileExistsFn = nodeExistsSync;
1519
- var readFileFn2 = (path) => nodeReadFileSync2(path, "utf-8");
1520
- var execFn2 = (cmd, opts) => nodeExecSync3(cmd, opts);
1521
1736
  var LANGUAGE_INDICATORS = [
1522
1737
  { file: "tsconfig.json", language: "TypeScript" },
1523
1738
  { file: "package.json", language: "JavaScript" },
@@ -1587,7 +1802,7 @@ var SOURCE_DIRS = ["src", "lib", "app"];
1587
1802
  var EXCLUDE_DIRS = ["node_modules", "dist", "build", ".git", "__pycache__", "venv", ".venv", "target"];
1588
1803
  function detectLanguage() {
1589
1804
  for (const { file, language } of LANGUAGE_INDICATORS) {
1590
- if (fileExistsFn(file)) {
1805
+ if (existsSync3(file)) {
1591
1806
  return language;
1592
1807
  }
1593
1808
  }
@@ -1716,29 +1931,29 @@ function parseSetupPy(content) {
1716
1931
  }
1717
1932
  function getFolderName() {
1718
1933
  try {
1719
- return execFn2('basename "$PWD"', { encoding: "utf-8" }).trim();
1934
+ return execSync4('basename "$PWD"', { encoding: "utf-8" }).trim();
1720
1935
  } catch {
1721
1936
  return basename(process.cwd());
1722
1937
  }
1723
1938
  }
1724
1939
  function getProjectInfo() {
1725
- if (fileExistsFn("package.json")) {
1940
+ if (existsSync3("package.json")) {
1726
1941
  try {
1727
- const content = readFileFn2("package.json");
1942
+ const content = readFileSync4("package.json", "utf-8");
1728
1943
  return parsePackageJson(content);
1729
1944
  } catch {
1730
1945
  }
1731
1946
  }
1732
- if (fileExistsFn("pyproject.toml")) {
1947
+ if (existsSync3("pyproject.toml")) {
1733
1948
  try {
1734
- const content = readFileFn2("pyproject.toml");
1949
+ const content = readFileSync4("pyproject.toml", "utf-8");
1735
1950
  return parsePyprojectToml(content);
1736
1951
  } catch {
1737
1952
  }
1738
1953
  }
1739
- if (fileExistsFn("setup.py")) {
1954
+ if (existsSync3("setup.py")) {
1740
1955
  try {
1741
- const content = readFileFn2("setup.py");
1956
+ const content = readFileSync4("setup.py", "utf-8");
1742
1957
  return parseSetupPy(content);
1743
1958
  } catch {
1744
1959
  }
@@ -1769,7 +1984,7 @@ function detectStack(deps) {
1769
1984
  }
1770
1985
  function findSourceDir() {
1771
1986
  for (const dir of SOURCE_DIRS) {
1772
- if (fileExistsFn(dir)) {
1987
+ if (existsSync3(dir)) {
1773
1988
  return dir;
1774
1989
  }
1775
1990
  }
@@ -1788,7 +2003,7 @@ function countFiles(language) {
1788
2003
  const excludes = EXCLUDE_DIRS.map((d) => `-path "*/${d}/*"`).join(" -o ");
1789
2004
  const namePatterns = config.patterns.map((p) => `-name "${p}"`).join(" -o ");
1790
2005
  const cmd = `find ${sourceDir} \\( ${excludes} \\) -prune -o -type f \\( ${namePatterns} \\) -print | wc -l`;
1791
- const result = execFn2(cmd, { encoding: "utf-8" });
2006
+ const result = execSync4(cmd, { encoding: "utf-8" });
1792
2007
  const count = parseInt(result.trim(), 10) || 0;
1793
2008
  return { count, extension: config.ext };
1794
2009
  } catch {
@@ -1808,7 +2023,7 @@ function countLines(language) {
1808
2023
  const excludes = EXCLUDE_DIRS.map((d) => `-path "*/${d}/*"`).join(" -o ");
1809
2024
  const namePatterns = config.patterns.map((p) => `-name "${p}"`).join(" -o ");
1810
2025
  const cmd = `find ${sourceDir} \\( ${excludes} \\) -prune -o -type f \\( ${namePatterns} \\) -print0 | xargs -0 wc -l 2>/dev/null | tail -1 | awk '{print $1}'`;
1811
- const result = execFn2(cmd, { encoding: "utf-8" });
2026
+ const result = execSync4(cmd, { encoding: "utf-8" });
1812
2027
  return parseInt(result.trim(), 10) || 0;
1813
2028
  } catch {
1814
2029
  return 0;
@@ -1851,10 +2066,10 @@ function getProjectData() {
1851
2066
 
1852
2067
  // src/data/claude.ts
1853
2068
  import {
1854
- existsSync as nodeExistsSync2,
1855
- readFileSync as nodeReadFileSync3,
1856
- readdirSync as nodeReaddirSync2,
1857
- statSync as nodeStatSync
2069
+ existsSync as existsSync4,
2070
+ readFileSync as readFileSync5,
2071
+ readdirSync as readdirSync2,
2072
+ statSync
1858
2073
  } from "fs";
1859
2074
  import { homedir } from "os";
1860
2075
  import { join as join2, basename as basename2 } from "path";
@@ -1881,12 +2096,6 @@ var ICONS = {
1881
2096
  };
1882
2097
 
1883
2098
  // src/data/claude.ts
1884
- var fs = {
1885
- existsSync: nodeExistsSync2,
1886
- readFileSync: (path) => nodeReadFileSync3(path, "utf-8"),
1887
- readdirSync: (path) => nodeReaddirSync2(path),
1888
- statSync: nodeStatSync
1889
- };
1890
2099
  var MAX_LINES_TO_SCAN = 200;
1891
2100
  var DEFAULT_MAX_ACTIVITIES = 10;
1892
2101
  function getClaudeSessionPath(projectPath) {
@@ -1894,10 +2103,10 @@ function getClaudeSessionPath(projectPath) {
1894
2103
  return join2(homedir(), ".claude", "projects", encoded);
1895
2104
  }
1896
2105
  function findActiveSession(sessionDir, sessionTimeout) {
1897
- if (!fs.existsSync(sessionDir)) {
2106
+ if (!existsSync4(sessionDir)) {
1898
2107
  return null;
1899
2108
  }
1900
- const files = fs.readdirSync(sessionDir);
2109
+ const files = readdirSync2(sessionDir);
1901
2110
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1902
2111
  if (jsonlFiles.length === 0) {
1903
2112
  return null;
@@ -1907,7 +2116,7 @@ function findActiveSession(sessionDir, sessionTimeout) {
1907
2116
  let latestSize = 0;
1908
2117
  for (const file of jsonlFiles) {
1909
2118
  const filePath = join2(sessionDir, file);
1910
- const stat = fs.statSync(filePath);
2119
+ const stat = statSync(filePath);
1911
2120
  if (stat.mtimeMs > latestMtime || stat.mtimeMs === latestMtime && stat.size > latestSize) {
1912
2121
  latestMtime = stat.mtimeMs;
1913
2122
  latestSize = stat.size;
@@ -1947,12 +2156,12 @@ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES)
1947
2156
  sessionStartTime: null,
1948
2157
  todos: null
1949
2158
  };
1950
- if (!fs.existsSync(sessionFile)) {
2159
+ if (!existsSync4(sessionFile)) {
1951
2160
  return defaultState;
1952
2161
  }
1953
2162
  let content;
1954
2163
  try {
1955
- content = fs.readFileSync(sessionFile);
2164
+ content = readFileSync5(sessionFile, "utf-8");
1956
2165
  } catch {
1957
2166
  return defaultState;
1958
2167
  }
@@ -2090,13 +2299,13 @@ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES)
2090
2299
  }
2091
2300
  }
2092
2301
  const subagentsDir = join2(sessionFile.replace(/\.jsonl$/, ""), "subagents");
2093
- if (fs.existsSync(subagentsDir)) {
2302
+ if (existsSync4(subagentsDir)) {
2094
2303
  try {
2095
- const subagentFiles = fs.readdirSync(subagentsDir).filter((f) => f.endsWith(".jsonl"));
2304
+ const subagentFiles = readdirSync2(subagentsDir).filter((f) => f.endsWith(".jsonl"));
2096
2305
  for (const file of subagentFiles) {
2097
2306
  const filePath = join2(subagentsDir, file);
2098
2307
  try {
2099
- const subContent = fs.readFileSync(filePath);
2308
+ const subContent = readFileSync5(filePath, "utf-8");
2100
2309
  const subLines = subContent.trim().split("\n").filter(Boolean);
2101
2310
  for (const line of subLines) {
2102
2311
  try {
@@ -2133,7 +2342,7 @@ function getClaudeData(projectPath, maxActivities, sessionTimeout = DEFAULT_SESS
2133
2342
  };
2134
2343
  try {
2135
2344
  const sessionDir = getClaudeSessionPath(projectPath);
2136
- const hasSession = fs.existsSync(sessionDir);
2345
+ const hasSession = existsSync4(sessionDir);
2137
2346
  const sessionFile = findActiveSession(sessionDir, sessionTimeout);
2138
2347
  if (!sessionFile) {
2139
2348
  return {
@@ -2161,26 +2370,20 @@ function getClaudeData(projectPath, maxActivities, sessionTimeout = DEFAULT_SESS
2161
2370
 
2162
2371
  // src/data/otherSessions.ts
2163
2372
  import {
2164
- existsSync as nodeExistsSync3,
2165
- readFileSync as nodeReadFileSync4,
2166
- readdirSync as nodeReaddirSync3,
2167
- statSync as nodeStatSync2
2373
+ existsSync as existsSync5,
2374
+ readFileSync as readFileSync6,
2375
+ readdirSync as readdirSync3,
2376
+ statSync as statSync2
2168
2377
  } from "fs";
2169
2378
  import { homedir as homedir2 } from "os";
2170
2379
  import { join as join3, basename as basename3, sep } from "path";
2171
- var fs2 = {
2172
- existsSync: nodeExistsSync3,
2173
- readFileSync: (path) => nodeReadFileSync4(path, "utf-8"),
2174
- readdirSync: (path) => nodeReaddirSync3(path),
2175
- statSync: nodeStatSync2
2176
- };
2177
2380
  var MAX_LINES_TO_SCAN2 = 100;
2178
2381
  function getProjectsDir() {
2179
2382
  return join3(homedir2(), ".claude", "projects");
2180
2383
  }
2181
2384
  function decodeProjectPath(encoded) {
2182
2385
  const naiveDecoded = encoded.replace(/-/g, sep);
2183
- if (fs2.existsSync(naiveDecoded)) {
2386
+ if (existsSync5(naiveDecoded)) {
2184
2387
  return naiveDecoded;
2185
2388
  }
2186
2389
  const segments = encoded.split("-").filter(Boolean);
@@ -2195,9 +2398,9 @@ function decodeProjectPath(encoded) {
2195
2398
  const segment = segments.slice(i, j).join("-");
2196
2399
  const testPath = currentPath ? join3(currentPath, segment) : sep + segment;
2197
2400
  try {
2198
- if (fs2.existsSync(testPath)) {
2199
- const stat = fs2.statSync(testPath);
2200
- if (stat.isDirectory?.()) {
2401
+ if (existsSync5(testPath)) {
2402
+ const stat = statSync2(testPath);
2403
+ if (stat.isDirectory()) {
2201
2404
  currentPath = testPath;
2202
2405
  i = j;
2203
2406
  found = true;
@@ -2216,16 +2419,16 @@ function decodeProjectPath(encoded) {
2216
2419
  }
2217
2420
  function getAllProjects() {
2218
2421
  const projectsDir = getProjectsDir();
2219
- if (!fs2.existsSync(projectsDir)) {
2422
+ if (!existsSync5(projectsDir)) {
2220
2423
  return [];
2221
2424
  }
2222
- const entries = fs2.readdirSync(projectsDir);
2425
+ const entries = readdirSync3(projectsDir);
2223
2426
  const projects = [];
2224
2427
  for (const entry of entries) {
2225
2428
  const fullPath = join3(projectsDir, entry);
2226
2429
  try {
2227
- const stat = fs2.statSync(fullPath);
2228
- if (stat.isDirectory?.()) {
2430
+ const stat = statSync2(fullPath);
2431
+ if (stat.isDirectory()) {
2229
2432
  projects.push({
2230
2433
  encodedPath: entry,
2231
2434
  decodedPath: decodeProjectPath(entry)
@@ -2237,12 +2440,12 @@ function getAllProjects() {
2237
2440
  return projects;
2238
2441
  }
2239
2442
  function parseLastAssistantMessage(sessionFile) {
2240
- if (!fs2.existsSync(sessionFile)) {
2443
+ if (!existsSync5(sessionFile)) {
2241
2444
  return null;
2242
2445
  }
2243
2446
  let content;
2244
2447
  try {
2245
- content = fs2.readFileSync(sessionFile);
2448
+ content = readFileSync6(sessionFile, "utf-8");
2246
2449
  } catch {
2247
2450
  return null;
2248
2451
  }
@@ -2290,12 +2493,12 @@ function formatRelativeTime2(date) {
2290
2493
  return `${days}d ago`;
2291
2494
  }
2292
2495
  function findMostRecentSession(projectDir) {
2293
- if (!fs2.existsSync(projectDir)) {
2496
+ if (!existsSync5(projectDir)) {
2294
2497
  return null;
2295
2498
  }
2296
2499
  let files;
2297
2500
  try {
2298
- files = fs2.readdirSync(projectDir);
2501
+ files = readdirSync3(projectDir);
2299
2502
  } catch {
2300
2503
  return null;
2301
2504
  }
@@ -2308,7 +2511,7 @@ function findMostRecentSession(projectDir) {
2308
2511
  for (const file of jsonlFiles) {
2309
2512
  const filePath = join3(projectDir, file);
2310
2513
  try {
2311
- const stat = fs2.statSync(filePath);
2514
+ const stat = statSync2(filePath);
2312
2515
  if (stat.mtimeMs && stat.mtimeMs > latestMtime) {
2313
2516
  latestMtime = stat.mtimeMs;
2314
2517
  latestFile = filePath;
@@ -2331,7 +2534,7 @@ function getOtherSessionsData(currentProjectPath, options2 = {}) {
2331
2534
  recentSession: null,
2332
2535
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2333
2536
  };
2334
- if (!fs2.existsSync(projectsDir)) {
2537
+ if (!existsSync5(projectsDir)) {
2335
2538
  return defaultResult;
2336
2539
  }
2337
2540
  const allProjects = getAllProjects();
@@ -2388,12 +2591,10 @@ function getOtherSessionsData(currentProjectPath, options2 = {}) {
2388
2591
  }
2389
2592
 
2390
2593
  // src/data/custom.ts
2391
- import { execSync as nodeExecSync4, exec as nodeExec2 } from "child_process";
2392
- import { readFileSync as nodeReadFileSync5, promises as fsPromises } from "fs";
2594
+ import { execSync as execSync5, exec as exec2 } from "child_process";
2595
+ import { readFileSync as readFileSync7, promises as fsPromises } from "fs";
2393
2596
  import { promisify as promisify2 } from "util";
2394
- var execAsync2 = promisify2(nodeExec2);
2395
- var execFn3 = (cmd, options2) => nodeExecSync4(cmd, options2);
2396
- var readFileFn3 = (path) => nodeReadFileSync5(path, "utf-8");
2597
+ var execAsync2 = promisify2(exec2);
2397
2598
  function capitalizeFirst(str) {
2398
2599
  return str.charAt(0).toUpperCase() + str.slice(1);
2399
2600
  }
@@ -2404,7 +2605,7 @@ function getCustomPanelData(name, panelConfig) {
2404
2605
  };
2405
2606
  if (panelConfig.command) {
2406
2607
  try {
2407
- const output = execFn3(panelConfig.command, { encoding: "utf-8" }).trim();
2608
+ const output = execSync5(panelConfig.command, { encoding: "utf-8" }).trim();
2408
2609
  try {
2409
2610
  const parsed = JSON.parse(output);
2410
2611
  return {
@@ -2438,7 +2639,7 @@ function getCustomPanelData(name, panelConfig) {
2438
2639
  }
2439
2640
  if (panelConfig.source) {
2440
2641
  try {
2441
- const content = readFileFn3(panelConfig.source);
2642
+ const content = readFileSync7(panelConfig.source, "utf-8");
2442
2643
  const parsed = JSON.parse(content);
2443
2644
  return {
2444
2645
  data: {
@@ -2549,98 +2750,12 @@ async function getCustomPanelDataAsync(name, panelConfig) {
2549
2750
  };
2550
2751
  }
2551
2752
 
2552
- // src/runner/command.ts
2553
- import { execSync as nodeExecSync5 } from "child_process";
2554
- var execFn4 = (command, options2) => nodeExecSync5(command, options2);
2555
- function parseVitestOutput(output) {
2556
- try {
2557
- const data = JSON.parse(output);
2558
- if (typeof data.numPassedTests !== "number" || typeof data.numFailedTests !== "number") {
2559
- return null;
2560
- }
2561
- const failures = [];
2562
- for (const testResult of data.testResults || []) {
2563
- for (const assertion of testResult.assertionResults || []) {
2564
- if (assertion.status === "failed") {
2565
- failures.push({
2566
- file: testResult.name,
2567
- name: assertion.title
2568
- });
2569
- }
2570
- }
2571
- }
2572
- return {
2573
- passed: data.numPassedTests,
2574
- failed: data.numFailedTests,
2575
- skipped: data.numPendingTests || 0,
2576
- failures
2577
- };
2578
- } catch {
2579
- return null;
2580
- }
2581
- }
2582
- function getHeadHash() {
2583
- try {
2584
- return execFn4("git rev-parse --short HEAD", {
2585
- encoding: "utf-8",
2586
- stdio: ["pipe", "pipe", "pipe"]
2587
- }).trim();
2588
- } catch {
2589
- return "unknown";
2590
- }
2591
- }
2592
- function runTestCommand(command) {
2593
- let output;
2594
- try {
2595
- output = execFn4(command, {
2596
- encoding: "utf-8",
2597
- stdio: ["pipe", "pipe", "pipe"]
2598
- });
2599
- } catch (error) {
2600
- const message = error instanceof Error ? error.message : String(error);
2601
- return {
2602
- results: null,
2603
- isOutdated: false,
2604
- commitsBehind: 0,
2605
- error: message
2606
- };
2607
- }
2608
- const parsed = parseVitestOutput(output);
2609
- if (!parsed) {
2610
- return {
2611
- results: null,
2612
- isOutdated: false,
2613
- commitsBehind: 0,
2614
- error: "Failed to parse test output"
2615
- };
2616
- }
2617
- const hash = getHeadHash();
2618
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2619
- const results = {
2620
- hash,
2621
- timestamp,
2622
- passed: parsed.passed,
2623
- failed: parsed.failed,
2624
- skipped: parsed.skipped,
2625
- failures: parsed.failures
2626
- };
2627
- return {
2628
- results,
2629
- isOutdated: false,
2630
- commitsBehind: 0
2631
- };
2632
- }
2633
-
2634
2753
  // src/config/parser.ts
2635
2754
  import {
2636
- existsSync as nodeExistsSync4,
2637
- readFileSync as nodeReadFileSync6
2755
+ existsSync as existsSync6,
2756
+ readFileSync as readFileSync8
2638
2757
  } from "fs";
2639
2758
  import { parse as parseYaml } from "yaml";
2640
- var fs3 = {
2641
- existsSync: nodeExistsSync4,
2642
- readFileSync: (path) => nodeReadFileSync6(path, "utf-8")
2643
- };
2644
2759
  var DEFAULT_WIDTH = DEFAULT_PANEL_WIDTH;
2645
2760
  var MIN_WIDTH = MIN_TERMINAL_WIDTH;
2646
2761
  var MAX_WIDTH = MAX_TERMINAL_WIDTH;
@@ -2718,12 +2833,12 @@ function parseBasePanelConfig(panelConfig, targetConfig, panelName, warnings) {
2718
2833
  function parseConfig() {
2719
2834
  const warnings = [];
2720
2835
  const defaultConfig = getDefaultConfig();
2721
- if (!fs3.existsSync(CONFIG_PATH)) {
2836
+ if (!existsSync6(CONFIG_PATH)) {
2722
2837
  return { config: defaultConfig, warnings };
2723
2838
  }
2724
2839
  let rawConfig;
2725
2840
  try {
2726
- const content = fs3.readFileSync(CONFIG_PATH);
2841
+ const content = readFileSync8(CONFIG_PATH, "utf-8");
2727
2842
  rawConfig = parseYaml(content);
2728
2843
  } catch (error) {
2729
2844
  const message = error instanceof Error ? error.message : String(error);
@@ -2838,7 +2953,7 @@ function parseConfig() {
2838
2953
  }
2839
2954
 
2840
2955
  // src/cli.ts
2841
- import { readFileSync } from "fs";
2956
+ import { readFileSync as readFileSync9 } from "fs";
2842
2957
  import { dirname, join as join4 } from "path";
2843
2958
  import { fileURLToPath } from "url";
2844
2959
  function getHelp() {
@@ -2857,13 +2972,12 @@ Options:
2857
2972
  function getVersion() {
2858
2973
  const __dirname3 = dirname(fileURLToPath(import.meta.url));
2859
2974
  const packageJson = JSON.parse(
2860
- readFileSync(join4(__dirname3, "..", "package.json"), "utf-8")
2975
+ readFileSync9(join4(__dirname3, "..", "package.json"), "utf-8")
2861
2976
  );
2862
2977
  return packageJson.version;
2863
2978
  }
2864
- var clearFn = () => console.clear();
2865
2979
  function clearScreen() {
2866
- clearFn();
2980
+ console.clear();
2867
2981
  }
2868
2982
  function parseArgs(args) {
2869
2983
  const hasOnce = args.includes("--once");
@@ -2985,7 +3099,7 @@ function DashboardApp({ mode }) {
2985
3099
  }, [config.panels.tests.command]);
2986
3100
  const initialTestData = useMemo3(() => getTestDataFromConfig(), [getTestDataFromConfig]);
2987
3101
  const [testsDisabled, setTestsDisabled] = useState4(
2988
- !!(initialTestData.error && config.panels.tests.command)
3102
+ !!(initialTestData.error && !config.panels.tests.command)
2989
3103
  );
2990
3104
  const [testData, setTestData] = useState4(initialTestData);
2991
3105
  const [customPanelData, setCustomPanelData] = useState4(() => {
@@ -3272,124 +3386,23 @@ function App({ mode, agentDirExists: agentDirExists2 = true }) {
3272
3386
 
3273
3387
  // src/commands/init.ts
3274
3388
  import {
3275
- existsSync as nodeExistsSync6,
3276
- mkdirSync as nodeMkdirSync,
3277
- writeFileSync as nodeWriteFileSync,
3278
- readFileSync as nodeReadFileSync8,
3279
- appendFileSync as nodeAppendFileSync
3389
+ existsSync as existsSync7,
3390
+ mkdirSync,
3391
+ writeFileSync,
3392
+ readFileSync as readFileSync10,
3393
+ appendFileSync
3280
3394
  } from "fs";
3281
3395
  import { fileURLToPath as fileURLToPath2 } from "url";
3282
3396
  import { dirname as dirname2, join as join5 } from "path";
3283
3397
  import { homedir as homedir3 } from "os";
3284
-
3285
- // src/data/detectTestFramework.ts
3286
- import {
3287
- existsSync as nodeExistsSync5,
3288
- readFileSync as nodeReadFileSync7
3289
- } from "fs";
3290
- var fs4 = {
3291
- existsSync: nodeExistsSync5,
3292
- readFileSync: (path) => nodeReadFileSync7(path, "utf-8")
3293
- };
3294
- var FRAMEWORK_COMMANDS = {
3295
- vitest: "npx vitest run --reporter=json",
3296
- jest: "npx jest --json",
3297
- mocha: "npx mocha --reporter=json",
3298
- pytest: "pytest --json-report --json-report-file=.agenthud/test-results.json"
3299
- };
3300
- var JS_FRAMEWORKS = ["vitest", "jest", "mocha"];
3301
- function detectTestFramework() {
3302
- const jsFramework = detectJsFramework();
3303
- if (jsFramework) {
3304
- return jsFramework;
3305
- }
3306
- const pythonFramework = detectPythonFramework();
3307
- if (pythonFramework) {
3308
- return pythonFramework;
3309
- }
3310
- return null;
3311
- }
3312
- function detectJsFramework() {
3313
- if (!fs4.existsSync("package.json")) {
3314
- return null;
3315
- }
3316
- let packageJson;
3317
- try {
3318
- const content = fs4.readFileSync("package.json");
3319
- packageJson = JSON.parse(content);
3320
- } catch {
3321
- return null;
3322
- }
3323
- const allDeps = {
3324
- ...packageJson.dependencies,
3325
- ...packageJson.devDependencies
3326
- };
3327
- for (const framework of JS_FRAMEWORKS) {
3328
- if (allDeps[framework]) {
3329
- return {
3330
- framework,
3331
- command: FRAMEWORK_COMMANDS[framework]
3332
- };
3333
- }
3334
- }
3335
- return null;
3336
- }
3337
- function detectPythonFramework() {
3338
- const pytestIndicators = ["pytest.ini", "conftest.py"];
3339
- for (const file of pytestIndicators) {
3340
- if (fs4.existsSync(file)) {
3341
- return {
3342
- framework: "pytest",
3343
- command: FRAMEWORK_COMMANDS.pytest
3344
- };
3345
- }
3346
- }
3347
- if (fs4.existsSync("pyproject.toml")) {
3348
- try {
3349
- const content = fs4.readFileSync("pyproject.toml");
3350
- if (content.includes("[tool.pytest") || content.includes("[tool.pytest.ini_options]")) {
3351
- return {
3352
- framework: "pytest",
3353
- command: FRAMEWORK_COMMANDS.pytest
3354
- };
3355
- }
3356
- } catch {
3357
- }
3358
- }
3359
- const requirementsFiles = ["requirements.txt", "requirements-dev.txt"];
3360
- for (const file of requirementsFiles) {
3361
- if (fs4.existsSync(file)) {
3362
- try {
3363
- const content = fs4.readFileSync(file);
3364
- if (content.includes("pytest")) {
3365
- return {
3366
- framework: "pytest",
3367
- command: FRAMEWORK_COMMANDS.pytest
3368
- };
3369
- }
3370
- } catch {
3371
- }
3372
- }
3373
- }
3374
- return null;
3375
- }
3376
-
3377
- // src/commands/init.ts
3378
- var fs5 = {
3379
- existsSync: nodeExistsSync6,
3380
- mkdirSync: nodeMkdirSync,
3381
- writeFileSync: nodeWriteFileSync,
3382
- readFileSync: (path) => nodeReadFileSync8(path, "utf-8"),
3383
- appendFileSync: nodeAppendFileSync
3384
- };
3385
3398
  var __filename2 = fileURLToPath2(import.meta.url);
3386
3399
  var __dirname2 = dirname2(__filename2);
3387
3400
  function getDefaultConfig2() {
3388
3401
  let templatePath = join5(__dirname2, "templates", "config.yaml");
3389
- if (!nodeExistsSync6(templatePath)) {
3402
+ if (!existsSync7(templatePath)) {
3390
3403
  templatePath = join5(__dirname2, "..", "templates", "config.yaml");
3391
3404
  }
3392
- return nodeReadFileSync8(templatePath, "utf-8");
3405
+ return readFileSync10(templatePath, "utf-8");
3393
3406
  }
3394
3407
  function getClaudeSessionPath2(projectPath) {
3395
3408
  const encoded = projectPath.replace(/[/\\]/g, "-");
@@ -3401,14 +3414,14 @@ function runInit(cwd = process.cwd()) {
3401
3414
  skipped: [],
3402
3415
  warnings: []
3403
3416
  };
3404
- if (!fs5.existsSync(".agenthud")) {
3405
- fs5.mkdirSync(".agenthud", { recursive: true });
3417
+ if (!existsSync7(".agenthud")) {
3418
+ mkdirSync(".agenthud", { recursive: true });
3406
3419
  result.created.push(".agenthud/");
3407
3420
  } else {
3408
3421
  result.skipped.push(".agenthud/");
3409
3422
  }
3410
- if (!fs5.existsSync(".agenthud/tests")) {
3411
- fs5.mkdirSync(".agenthud/tests", { recursive: true });
3423
+ if (!existsSync7(".agenthud/tests")) {
3424
+ mkdirSync(".agenthud/tests", { recursive: true });
3412
3425
  result.created.push(".agenthud/tests/");
3413
3426
  } else {
3414
3427
  result.skipped.push(".agenthud/tests/");
@@ -3417,7 +3430,7 @@ function runInit(cwd = process.cwd()) {
3417
3430
  if (testFramework) {
3418
3431
  result.detectedTestFramework = testFramework.framework;
3419
3432
  }
3420
- if (!fs5.existsSync(".agenthud/config.yaml")) {
3433
+ if (!existsSync7(".agenthud/config.yaml")) {
3421
3434
  let configContent = getDefaultConfig2();
3422
3435
  if (testFramework) {
3423
3436
  configContent = configContent.replace(
@@ -3430,28 +3443,28 @@ function runInit(cwd = process.cwd()) {
3430
3443
  "# command: (auto-detect failed - configure manually)"
3431
3444
  );
3432
3445
  }
3433
- fs5.writeFileSync(".agenthud/config.yaml", configContent);
3446
+ writeFileSync(".agenthud/config.yaml", configContent);
3434
3447
  result.created.push(".agenthud/config.yaml");
3435
3448
  } else {
3436
3449
  result.skipped.push(".agenthud/config.yaml");
3437
3450
  }
3438
- if (!fs5.existsSync(".gitignore")) {
3439
- fs5.writeFileSync(".gitignore", ".agenthud/\n");
3451
+ if (!existsSync7(".gitignore")) {
3452
+ writeFileSync(".gitignore", ".agenthud/\n");
3440
3453
  result.created.push(".gitignore");
3441
3454
  } else {
3442
- const content = fs5.readFileSync(".gitignore");
3455
+ const content = readFileSync10(".gitignore", "utf-8");
3443
3456
  if (!content.includes(".agenthud/")) {
3444
- fs5.appendFileSync(".gitignore", "\n.agenthud/\n");
3457
+ appendFileSync(".gitignore", "\n.agenthud/\n");
3445
3458
  result.created.push(".gitignore");
3446
3459
  } else {
3447
3460
  result.skipped.push(".gitignore");
3448
3461
  }
3449
3462
  }
3450
- if (!fs5.existsSync(".git")) {
3463
+ if (!existsSync7(".git")) {
3451
3464
  result.warnings.push("Not a git repository - Git panel will show limited info");
3452
3465
  }
3453
3466
  const claudeSessionPath = getClaudeSessionPath2(cwd);
3454
- if (!fs5.existsSync(claudeSessionPath)) {
3467
+ if (!existsSync7(claudeSessionPath)) {
3455
3468
  result.warnings.push("No Claude session found - start Claude to see activity");
3456
3469
  }
3457
3470
  return result;
@@ -3460,14 +3473,10 @@ function runInit(cwd = process.cwd()) {
3460
3473
  // src/utils/performance.ts
3461
3474
  import { performance } from "perf_hooks";
3462
3475
  var DEFAULT_CLEANUP_INTERVAL = 6e4;
3463
- var perfFunctions = {
3464
- clearMarks: () => performance.clearMarks(),
3465
- clearMeasures: () => performance.clearMeasures()
3466
- };
3467
3476
  var cleanupInterval = null;
3468
3477
  function clearPerformanceEntries() {
3469
- perfFunctions.clearMarks();
3470
- perfFunctions.clearMeasures();
3478
+ performance.clearMarks();
3479
+ performance.clearMeasures();
3471
3480
  }
3472
3481
  function startPerformanceCleanup(intervalMs = DEFAULT_CLEANUP_INTERVAL) {
3473
3482
  if (cleanupInterval !== null) {
@@ -3513,7 +3522,7 @@ if (options.command === "init") {
3513
3522
  console.log(" Run: npx agenthud\n");
3514
3523
  process.exit(0);
3515
3524
  }
3516
- var agentDirExists = existsSync(".agenthud");
3525
+ var agentDirExists = existsSync8(".agenthud");
3517
3526
  if (options.mode === "watch") {
3518
3527
  clearScreen();
3519
3528
  }
@@ -21,7 +21,8 @@ panels:
21
21
  enabled: true
22
22
  interval: manual
23
23
  # command is auto-detected from: vitest, jest, mocha, pytest
24
- command: npx vitest run --reporter=json
24
+ # all frameworks output JUnit XML to .agenthud/test-results.xml
25
+ command: npx vitest run --reporter=junit --outputFile=.agenthud/test-results.xml
25
26
 
26
27
  project:
27
28
  enabled: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenthud",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "CLI tool to monitor agent status in real-time. Works with Claude Code, multi-agent workflows, and any AI agent system.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",