@hasna/terminal 0.7.2 → 0.7.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/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);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
6
  "bin": {
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);
@@ -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
+ }