@hasna/terminal 0.7.2 → 0.7.4
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/cli.js +18 -0
- package/dist/lazy-executor.js +4 -0
- package/dist/test-watchlist.js +127 -0
- package/package.json +1 -1
- package/src/cli.tsx +16 -0
- package/src/lazy-executor.ts +4 -0
- package/src/test-watchlist.ts +156 -0
package/dist/cli.js
CHANGED
|
@@ -82,6 +82,7 @@ if (args[0] === "exec") {
|
|
|
82
82
|
const { shouldBeLazy, toLazy, getSlice } = await import("./lazy-executor.js");
|
|
83
83
|
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
84
84
|
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
85
|
+
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
85
86
|
// Rewrite command if possible
|
|
86
87
|
const rw = rewriteCommand(command);
|
|
87
88
|
const actualCmd = rw.changed ? rw.rewritten : command;
|
|
@@ -123,6 +124,23 @@ if (args[0] === "exec") {
|
|
|
123
124
|
console.error(`[open-terminal] showing ${slice.lines.length}/${slice.total}, ${slice.total - (offset ?? 0) - slice.lines.length} remaining`);
|
|
124
125
|
process.exit(0);
|
|
125
126
|
}
|
|
127
|
+
// Test output detection — use watchlist for structured test tracking
|
|
128
|
+
if (isTestOutput(clean)) {
|
|
129
|
+
const result = trackTests(process.cwd(), clean);
|
|
130
|
+
const formatted = formatWatchResult(result);
|
|
131
|
+
const savedTokens = rawTokens - estimateTokens(formatted);
|
|
132
|
+
if (savedTokens > 20)
|
|
133
|
+
recordSaving("structured", savedTokens);
|
|
134
|
+
if (jsonMode) {
|
|
135
|
+
console.log(JSON.stringify({ exitCode: 0, type: "test-results", ...result, duration: Date.now() - start }));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(formatted);
|
|
139
|
+
}
|
|
140
|
+
if (savedTokens > 10)
|
|
141
|
+
console.error(`[open-terminal] test watchlist: saved ${savedTokens} tokens`);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
126
144
|
// Lazy mode for huge output (threshold 200, skip cat/summary commands)
|
|
127
145
|
if (shouldBeLazy(clean, actualCmd)) {
|
|
128
146
|
const lazy = toLazy(clean, actualCmd);
|
package/dist/lazy-executor.js
CHANGED
|
@@ -4,7 +4,11 @@ import { dirname } from "path";
|
|
|
4
4
|
const LAZY_THRESHOLD = 200; // lines before switching to lazy mode (was 100, too aggressive)
|
|
5
5
|
// Commands where the user explicitly wants full output — never lazify
|
|
6
6
|
const PASSTHROUGH_COMMANDS = [
|
|
7
|
+
// File reading — user explicitly wants content
|
|
7
8
|
/\bcat\b/, /\bhead\b/, /\btail\b/, /\bbat\b/, /\bless\b/, /\bmore\b/,
|
|
9
|
+
// Git review commands — truncating diffs/patches loses semantic meaning
|
|
10
|
+
/\bgit\s+diff\b/, /\bgit\s+show\b/, /\bgit\s+log\s+-p\b/, /\bgit\s+log\s+--patch\b/,
|
|
11
|
+
// Summary/report commands — summarizing a summary is pointless
|
|
8
12
|
/\bsummary\b/i, /\bstatus\b/i, /\breport\b/i, /\bstats\b/i,
|
|
9
13
|
/\bweek\b/i, /\btoday\b/i, /\bdashboard\b/i,
|
|
10
14
|
];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Test focus tracker — tracks test status across runs, only reports changes
|
|
2
|
+
// Instead of showing "248 passed, 2 failed" every time, shows:
|
|
3
|
+
// "auth.login: FIXED, auth.logout: STILL FAILING, 246 unchanged"
|
|
4
|
+
// Per-cwd watchlist
|
|
5
|
+
const watchlists = new Map();
|
|
6
|
+
/** Extract test names and status from test runner output (any runner) */
|
|
7
|
+
function extractTests(output) {
|
|
8
|
+
const tests = [];
|
|
9
|
+
const lines = output.split("\n");
|
|
10
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11
|
+
const line = lines[i];
|
|
12
|
+
// PASS/FAIL with test name: "PASS src/auth.test.ts" or "✓ login works" or "✗ logout fails"
|
|
13
|
+
const passMatch = line.match(/(?:PASS|✓|✔|✅)\s+(.+)/);
|
|
14
|
+
if (passMatch) {
|
|
15
|
+
tests.push({ name: passMatch[1].trim(), status: "pass" });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const failMatch = line.match(/(?:FAIL|✗|✕|❌|×)\s+(.+)/);
|
|
19
|
+
if (failMatch) {
|
|
20
|
+
// Capture error from next few lines
|
|
21
|
+
const errorLines = [];
|
|
22
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
23
|
+
if (lines[j].match(/(?:PASS|FAIL|✓|✗|✔|✕|Tests:|^\s*$)/))
|
|
24
|
+
break;
|
|
25
|
+
errorLines.push(lines[j].trim());
|
|
26
|
+
}
|
|
27
|
+
tests.push({ name: failMatch[1].trim(), status: "fail", error: errorLines.join(" ").slice(0, 200) });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Jest/vitest style: " ● test name" for failures
|
|
31
|
+
const jestFail = line.match(/^\s*●\s+(.+)/);
|
|
32
|
+
if (jestFail) {
|
|
33
|
+
tests.push({ name: jestFail[1].trim(), status: "fail" });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return tests;
|
|
38
|
+
}
|
|
39
|
+
/** Detect if output looks like test runner output */
|
|
40
|
+
export function isTestOutput(output) {
|
|
41
|
+
const markers = /(?:Tests?:|PASS|FAIL|✓|✗|passed|failed|\d+\s+pass|\d+\s+fail)/i;
|
|
42
|
+
const lines = output.split("\n");
|
|
43
|
+
return lines.filter(l => markers.test(l)).length >= 2;
|
|
44
|
+
}
|
|
45
|
+
/** Track test results and return only changes */
|
|
46
|
+
export function trackTests(cwd, output) {
|
|
47
|
+
const current = extractTests(output);
|
|
48
|
+
const prev = watchlists.get(cwd);
|
|
49
|
+
// Count totals from raw output (more reliable than extracted tests)
|
|
50
|
+
let totalPassed = 0, totalFailed = 0;
|
|
51
|
+
const summaryMatch = output.match(/(\d+)\s+pass/i);
|
|
52
|
+
const failMatch = output.match(/(\d+)\s+fail/i);
|
|
53
|
+
if (summaryMatch)
|
|
54
|
+
totalPassed = parseInt(summaryMatch[1]);
|
|
55
|
+
if (failMatch)
|
|
56
|
+
totalFailed = parseInt(failMatch[1]);
|
|
57
|
+
// Fallback to extracted counts
|
|
58
|
+
if (totalPassed === 0)
|
|
59
|
+
totalPassed = current.filter(t => t.status === "pass").length;
|
|
60
|
+
if (totalFailed === 0)
|
|
61
|
+
totalFailed = current.filter(t => t.status === "fail").length;
|
|
62
|
+
// Store current for next comparison
|
|
63
|
+
const currentMap = new Map();
|
|
64
|
+
for (const t of current)
|
|
65
|
+
currentMap.set(t.name, t);
|
|
66
|
+
watchlists.set(cwd, currentMap);
|
|
67
|
+
// First run — no comparison possible
|
|
68
|
+
if (!prev) {
|
|
69
|
+
return {
|
|
70
|
+
changed: [],
|
|
71
|
+
newTests: current.filter(t => t.status === "fail"), // only show failures on first run
|
|
72
|
+
totalPassed,
|
|
73
|
+
totalFailed,
|
|
74
|
+
unchangedCount: 0,
|
|
75
|
+
firstRun: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Compare with previous
|
|
79
|
+
const changed = [];
|
|
80
|
+
const newTests = [];
|
|
81
|
+
let unchangedCount = 0;
|
|
82
|
+
for (const [name, test] of currentMap) {
|
|
83
|
+
const prevTest = prev.get(name);
|
|
84
|
+
if (!prevTest) {
|
|
85
|
+
newTests.push(test);
|
|
86
|
+
}
|
|
87
|
+
else if (prevTest.status !== test.status) {
|
|
88
|
+
changed.push({ name, from: prevTest.status, to: test.status, error: test.error });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
unchangedCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { changed, newTests, totalPassed, totalFailed, unchangedCount, firstRun: false };
|
|
95
|
+
}
|
|
96
|
+
/** Format watchlist result for display */
|
|
97
|
+
export function formatWatchResult(result) {
|
|
98
|
+
const lines = [];
|
|
99
|
+
if (result.firstRun) {
|
|
100
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed`);
|
|
101
|
+
if (result.newTests.length > 0) {
|
|
102
|
+
for (const t of result.newTests) {
|
|
103
|
+
lines.push(` ✗ ${t.name}${t.error ? `: ${t.error}` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
// Status changes
|
|
109
|
+
for (const c of result.changed) {
|
|
110
|
+
if (c.to === "pass")
|
|
111
|
+
lines.push(` ✓ FIXED: ${c.name}`);
|
|
112
|
+
else
|
|
113
|
+
lines.push(` ✗ BROKE: ${c.name}${c.error ? ` — ${c.error}` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
// New failures
|
|
116
|
+
for (const t of result.newTests.filter(t => t.status === "fail")) {
|
|
117
|
+
lines.push(` ✗ NEW FAIL: ${t.name}${t.error ? ` — ${t.error}` : ""}`);
|
|
118
|
+
}
|
|
119
|
+
// Summary
|
|
120
|
+
if (result.changed.length === 0 && result.newTests.filter(t => t.status === "fail").length === 0) {
|
|
121
|
+
lines.push(`✓ ${result.totalPassed} passed, ${result.totalFailed} failed (no changes)`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed, ${result.unchangedCount} unchanged`);
|
|
125
|
+
}
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
package/package.json
CHANGED
package/src/cli.tsx
CHANGED
|
@@ -86,6 +86,7 @@ if (args[0] === "exec") {
|
|
|
86
86
|
const { shouldBeLazy, toLazy, getSlice } = await import("./lazy-executor.js");
|
|
87
87
|
const { parseOutput, estimateTokens } = await import("./parsers/index.js");
|
|
88
88
|
const { recordSaving, recordUsage } = await import("./economy.js");
|
|
89
|
+
const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
|
|
89
90
|
|
|
90
91
|
// Rewrite command if possible
|
|
91
92
|
const rw = rewriteCommand(command);
|
|
@@ -127,6 +128,21 @@ if (args[0] === "exec") {
|
|
|
127
128
|
process.exit(0);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
// Test output detection — use watchlist for structured test tracking
|
|
132
|
+
if (isTestOutput(clean)) {
|
|
133
|
+
const result = trackTests(process.cwd(), clean);
|
|
134
|
+
const formatted = formatWatchResult(result);
|
|
135
|
+
const savedTokens = rawTokens - estimateTokens(formatted);
|
|
136
|
+
if (savedTokens > 20) recordSaving("structured", savedTokens);
|
|
137
|
+
if (jsonMode) {
|
|
138
|
+
console.log(JSON.stringify({ exitCode: 0, type: "test-results", ...result, duration: Date.now() - start }));
|
|
139
|
+
} else {
|
|
140
|
+
console.log(formatted);
|
|
141
|
+
}
|
|
142
|
+
if (savedTokens > 10) console.error(`[open-terminal] test watchlist: saved ${savedTokens} tokens`);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
130
146
|
// Lazy mode for huge output (threshold 200, skip cat/summary commands)
|
|
131
147
|
if (shouldBeLazy(clean, actualCmd)) {
|
|
132
148
|
const lazy = toLazy(clean, actualCmd);
|
package/src/lazy-executor.ts
CHANGED
|
@@ -15,7 +15,11 @@ export interface LazyResult {
|
|
|
15
15
|
|
|
16
16
|
// Commands where the user explicitly wants full output — never lazify
|
|
17
17
|
const PASSTHROUGH_COMMANDS = [
|
|
18
|
+
// File reading — user explicitly wants content
|
|
18
19
|
/\bcat\b/, /\bhead\b/, /\btail\b/, /\bbat\b/, /\bless\b/, /\bmore\b/,
|
|
20
|
+
// Git review commands — truncating diffs/patches loses semantic meaning
|
|
21
|
+
/\bgit\s+diff\b/, /\bgit\s+show\b/, /\bgit\s+log\s+-p\b/, /\bgit\s+log\s+--patch\b/,
|
|
22
|
+
// Summary/report commands — summarizing a summary is pointless
|
|
19
23
|
/\bsummary\b/i, /\bstatus\b/i, /\breport\b/i, /\bstats\b/i,
|
|
20
24
|
/\bweek\b/i, /\btoday\b/i, /\bdashboard\b/i,
|
|
21
25
|
];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Test focus tracker — tracks test status across runs, only reports changes
|
|
2
|
+
// Instead of showing "248 passed, 2 failed" every time, shows:
|
|
3
|
+
// "auth.login: FIXED, auth.logout: STILL FAILING, 246 unchanged"
|
|
4
|
+
|
|
5
|
+
export interface TestStatus {
|
|
6
|
+
name: string;
|
|
7
|
+
status: "pass" | "fail";
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TestWatchResult {
|
|
12
|
+
/** Tests that changed status since last run */
|
|
13
|
+
changed: { name: string; from: "pass" | "fail"; to: "pass" | "fail"; error?: string }[];
|
|
14
|
+
/** New tests not seen before */
|
|
15
|
+
newTests: TestStatus[];
|
|
16
|
+
/** Summary counts */
|
|
17
|
+
totalPassed: number;
|
|
18
|
+
totalFailed: number;
|
|
19
|
+
unchangedCount: number;
|
|
20
|
+
/** Whether this is the first run (no previous data) */
|
|
21
|
+
firstRun: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Per-cwd watchlist
|
|
25
|
+
const watchlists = new Map<string, Map<string, TestStatus>>();
|
|
26
|
+
|
|
27
|
+
/** Extract test names and status from test runner output (any runner) */
|
|
28
|
+
function extractTests(output: string): TestStatus[] {
|
|
29
|
+
const tests: TestStatus[] = [];
|
|
30
|
+
const lines = output.split("\n");
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
|
|
35
|
+
// PASS/FAIL with test name: "PASS src/auth.test.ts" or "✓ login works" or "✗ logout fails"
|
|
36
|
+
const passMatch = line.match(/(?:PASS|✓|✔|✅)\s+(.+)/);
|
|
37
|
+
if (passMatch) {
|
|
38
|
+
tests.push({ name: passMatch[1].trim(), status: "pass" });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const failMatch = line.match(/(?:FAIL|✗|✕|❌|×)\s+(.+)/);
|
|
43
|
+
if (failMatch) {
|
|
44
|
+
// Capture error from next few lines
|
|
45
|
+
const errorLines: string[] = [];
|
|
46
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
47
|
+
if (lines[j].match(/(?:PASS|FAIL|✓|✗|✔|✕|Tests:|^\s*$)/)) break;
|
|
48
|
+
errorLines.push(lines[j].trim());
|
|
49
|
+
}
|
|
50
|
+
tests.push({ name: failMatch[1].trim(), status: "fail", error: errorLines.join(" ").slice(0, 200) });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Jest/vitest style: " ● test name" for failures
|
|
55
|
+
const jestFail = line.match(/^\s*●\s+(.+)/);
|
|
56
|
+
if (jestFail) {
|
|
57
|
+
tests.push({ name: jestFail[1].trim(), status: "fail" });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return tests;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Detect if output looks like test runner output */
|
|
66
|
+
export function isTestOutput(output: string): boolean {
|
|
67
|
+
const markers = /(?:Tests?:|PASS|FAIL|✓|✗|passed|failed|\d+\s+pass|\d+\s+fail)/i;
|
|
68
|
+
const lines = output.split("\n");
|
|
69
|
+
return lines.filter(l => markers.test(l)).length >= 2;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Track test results and return only changes */
|
|
73
|
+
export function trackTests(cwd: string, output: string): TestWatchResult {
|
|
74
|
+
const current = extractTests(output);
|
|
75
|
+
const prev = watchlists.get(cwd);
|
|
76
|
+
|
|
77
|
+
// Count totals from raw output (more reliable than extracted tests)
|
|
78
|
+
let totalPassed = 0, totalFailed = 0;
|
|
79
|
+
const summaryMatch = output.match(/(\d+)\s+pass/i);
|
|
80
|
+
const failMatch = output.match(/(\d+)\s+fail/i);
|
|
81
|
+
if (summaryMatch) totalPassed = parseInt(summaryMatch[1]);
|
|
82
|
+
if (failMatch) totalFailed = parseInt(failMatch[1]);
|
|
83
|
+
// Fallback to extracted counts
|
|
84
|
+
if (totalPassed === 0) totalPassed = current.filter(t => t.status === "pass").length;
|
|
85
|
+
if (totalFailed === 0) totalFailed = current.filter(t => t.status === "fail").length;
|
|
86
|
+
|
|
87
|
+
// Store current for next comparison
|
|
88
|
+
const currentMap = new Map<string, TestStatus>();
|
|
89
|
+
for (const t of current) currentMap.set(t.name, t);
|
|
90
|
+
watchlists.set(cwd, currentMap);
|
|
91
|
+
|
|
92
|
+
// First run — no comparison possible
|
|
93
|
+
if (!prev) {
|
|
94
|
+
return {
|
|
95
|
+
changed: [],
|
|
96
|
+
newTests: current.filter(t => t.status === "fail"), // only show failures on first run
|
|
97
|
+
totalPassed,
|
|
98
|
+
totalFailed,
|
|
99
|
+
unchangedCount: 0,
|
|
100
|
+
firstRun: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Compare with previous
|
|
105
|
+
const changed: TestWatchResult["changed"] = [];
|
|
106
|
+
const newTests: TestStatus[] = [];
|
|
107
|
+
let unchangedCount = 0;
|
|
108
|
+
|
|
109
|
+
for (const [name, test] of currentMap) {
|
|
110
|
+
const prevTest = prev.get(name);
|
|
111
|
+
if (!prevTest) {
|
|
112
|
+
newTests.push(test);
|
|
113
|
+
} else if (prevTest.status !== test.status) {
|
|
114
|
+
changed.push({ name, from: prevTest.status, to: test.status, error: test.error });
|
|
115
|
+
} else {
|
|
116
|
+
unchangedCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { changed, newTests, totalPassed, totalFailed, unchangedCount, firstRun: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Format watchlist result for display */
|
|
124
|
+
export function formatWatchResult(result: TestWatchResult): string {
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
|
|
127
|
+
if (result.firstRun) {
|
|
128
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed`);
|
|
129
|
+
if (result.newTests.length > 0) {
|
|
130
|
+
for (const t of result.newTests) {
|
|
131
|
+
lines.push(` ✗ ${t.name}${t.error ? `: ${t.error}` : ""}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Status changes
|
|
138
|
+
for (const c of result.changed) {
|
|
139
|
+
if (c.to === "pass") lines.push(` ✓ FIXED: ${c.name}`);
|
|
140
|
+
else lines.push(` ✗ BROKE: ${c.name}${c.error ? ` — ${c.error}` : ""}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// New failures
|
|
144
|
+
for (const t of result.newTests.filter(t => t.status === "fail")) {
|
|
145
|
+
lines.push(` ✗ NEW FAIL: ${t.name}${t.error ? ` — ${t.error}` : ""}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Summary
|
|
149
|
+
if (result.changed.length === 0 && result.newTests.filter(t => t.status === "fail").length === 0) {
|
|
150
|
+
lines.push(`✓ ${result.totalPassed} passed, ${result.totalFailed} failed (no changes)`);
|
|
151
|
+
} else {
|
|
152
|
+
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed, ${result.unchangedCount} unchanged`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|