@hasna/terminal 2.0.5 → 2.2.0
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 +23 -9
- package/package.json +1 -1
- package/src/ai.ts +46 -113
- package/src/cli.tsx +22 -9
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +202 -0
- package/src/output-processor.ts +7 -18
- package/src/providers/base.ts +3 -1
- package/src/providers/groq.ts +108 -0
- package/src/providers/index.ts +26 -2
- package/src/providers/providers.test.ts +4 -2
- package/src/providers/xai.ts +108 -0
- package/dist/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -368
- package/dist/cache.js +0 -41
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -77
- package/dist/compression.js +0 -107
- package/dist/diff-cache.js +0 -107
- package/dist/economy.js +0 -79
- package/dist/expand-store.js +0 -38
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -98
- package/dist/mcp/server.js +0 -569
- package/dist/noise-filter.js +0 -86
- package/dist/output-processor.js +0 -136
- package/dist/output-router.js +0 -41
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -39
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -95
- package/dist/providers/index.js +0 -49
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -136
- package/dist/search/content-search.js +0 -68
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -320
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -120
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
package/dist/smart-display.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
// Smart output display — compress repetitive output into grouped patterns
|
|
2
|
-
import { dirname, basename } from "path";
|
|
3
|
-
/** Detect if lines look like file paths */
|
|
4
|
-
function looksLikePaths(lines) {
|
|
5
|
-
if (lines.length < 3)
|
|
6
|
-
return false;
|
|
7
|
-
const pathLike = lines.filter(l => l.trim().match(/^\.?\//) || l.trim().includes("/"));
|
|
8
|
-
return pathLike.length > lines.length * 0.6;
|
|
9
|
-
}
|
|
10
|
-
/** Find the varying part between similar strings and create a glob pattern */
|
|
11
|
-
function findPattern(items) {
|
|
12
|
-
if (items.length < 2)
|
|
13
|
-
return null;
|
|
14
|
-
const first = items[0];
|
|
15
|
-
const last = items[items.length - 1];
|
|
16
|
-
// Find common prefix
|
|
17
|
-
let prefixLen = 0;
|
|
18
|
-
while (prefixLen < first.length && prefixLen < last.length && first[prefixLen] === last[prefixLen]) {
|
|
19
|
-
prefixLen++;
|
|
20
|
-
}
|
|
21
|
-
// Find common suffix
|
|
22
|
-
let suffixLen = 0;
|
|
23
|
-
while (suffixLen < first.length - prefixLen &&
|
|
24
|
-
suffixLen < last.length - prefixLen &&
|
|
25
|
-
first[first.length - 1 - suffixLen] === last[last.length - 1 - suffixLen]) {
|
|
26
|
-
suffixLen++;
|
|
27
|
-
}
|
|
28
|
-
const prefix = first.slice(0, prefixLen);
|
|
29
|
-
const suffix = suffixLen > 0 ? first.slice(-suffixLen) : "";
|
|
30
|
-
if (prefix.length + suffix.length < first.length * 0.3)
|
|
31
|
-
return null; // too different
|
|
32
|
-
return `${prefix}*${suffix}`;
|
|
33
|
-
}
|
|
34
|
-
/** Group file paths by directory */
|
|
35
|
-
function groupByDir(paths) {
|
|
36
|
-
const groups = new Map();
|
|
37
|
-
for (const p of paths) {
|
|
38
|
-
const dir = dirname(p.trim());
|
|
39
|
-
const file = basename(p.trim());
|
|
40
|
-
if (!groups.has(dir))
|
|
41
|
-
groups.set(dir, []);
|
|
42
|
-
groups.get(dir).push(file);
|
|
43
|
-
}
|
|
44
|
-
return groups;
|
|
45
|
-
}
|
|
46
|
-
/** Detect duplicate filenames across directories */
|
|
47
|
-
function findDuplicates(paths) {
|
|
48
|
-
const byName = new Map();
|
|
49
|
-
for (const p of paths) {
|
|
50
|
-
const file = basename(p.trim());
|
|
51
|
-
if (!byName.has(file))
|
|
52
|
-
byName.set(file, []);
|
|
53
|
-
byName.get(file).push(dirname(p.trim()));
|
|
54
|
-
}
|
|
55
|
-
// Only return files that appear in 2+ dirs
|
|
56
|
-
const dupes = new Map();
|
|
57
|
-
for (const [file, dirs] of byName) {
|
|
58
|
-
if (dirs.length >= 2)
|
|
59
|
-
dupes.set(file, dirs);
|
|
60
|
-
}
|
|
61
|
-
return dupes;
|
|
62
|
-
}
|
|
63
|
-
/** Collapse node_modules paths */
|
|
64
|
-
function collapseNodeModules(paths) {
|
|
65
|
-
const nodeModulesPaths = [];
|
|
66
|
-
const otherPaths = [];
|
|
67
|
-
for (const p of paths) {
|
|
68
|
-
if (p.includes("node_modules")) {
|
|
69
|
-
nodeModulesPaths.push(p);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
otherPaths.push(p);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { nodeModulesPaths, otherPaths };
|
|
76
|
-
}
|
|
77
|
-
/** Smart display: compress file path output into grouped patterns */
|
|
78
|
-
export function smartDisplay(lines) {
|
|
79
|
-
if (lines.length <= 5)
|
|
80
|
-
return lines;
|
|
81
|
-
// Try ls -la table compression first
|
|
82
|
-
const lsCompressed = compressLsTable(lines);
|
|
83
|
-
if (lsCompressed)
|
|
84
|
-
return lsCompressed;
|
|
85
|
-
if (!looksLikePaths(lines))
|
|
86
|
-
return compressGeneric(lines);
|
|
87
|
-
const paths = lines.map(l => l.trim()).filter(l => l);
|
|
88
|
-
const result = [];
|
|
89
|
-
// Step 1: Separate node_modules
|
|
90
|
-
const { nodeModulesPaths, otherPaths } = collapseNodeModules(paths);
|
|
91
|
-
// Step 2: Find duplicates in non-node_modules paths
|
|
92
|
-
const dupes = findDuplicates(otherPaths);
|
|
93
|
-
const handledPaths = new Set();
|
|
94
|
-
// Show duplicates first
|
|
95
|
-
for (const [file, dirs] of dupes) {
|
|
96
|
-
if (dirs.length >= 3) {
|
|
97
|
-
result.push(` **/${file} ×${dirs.length}`);
|
|
98
|
-
result.push(` ${dirs.slice(0, 5).join(", ")}${dirs.length > 5 ? ` +${dirs.length - 5} more` : ""}`);
|
|
99
|
-
for (const d of dirs) {
|
|
100
|
-
handledPaths.add(`${d}/${file}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// Step 3: Group remaining by directory
|
|
105
|
-
const remaining = otherPaths.filter(p => !handledPaths.has(p.trim()));
|
|
106
|
-
const dirGroups = groupByDir(remaining);
|
|
107
|
-
for (const [dir, files] of dirGroups) {
|
|
108
|
-
if (files.length === 1) {
|
|
109
|
-
result.push(` ${dir}/${files[0]}`);
|
|
110
|
-
}
|
|
111
|
-
else if (files.length <= 3) {
|
|
112
|
-
result.push(` ${dir}/`);
|
|
113
|
-
for (const f of files)
|
|
114
|
-
result.push(` ${f}`);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
// Try to find a pattern
|
|
118
|
-
const sorted = files.sort();
|
|
119
|
-
const pattern = findPattern(sorted);
|
|
120
|
-
if (pattern) {
|
|
121
|
-
const dateRange = collapseDateRange(sorted);
|
|
122
|
-
const rangeStr = dateRange ? ` (${dateRange})` : "";
|
|
123
|
-
result.push(` ${dir}/${pattern} ×${files.length}${rangeStr}`);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
result.push(` ${dir}/ (${files.length} files)`);
|
|
127
|
-
// Show first 2 + count
|
|
128
|
-
result.push(` ${sorted[0]}, ${sorted[1]}${files.length > 2 ? `, +${files.length - 2} more` : ""}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Step 4: Collapsed node_modules summary
|
|
133
|
-
if (nodeModulesPaths.length > 0) {
|
|
134
|
-
if (nodeModulesPaths.length <= 2) {
|
|
135
|
-
for (const p of nodeModulesPaths)
|
|
136
|
-
result.push(` ${p}`);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
// Group node_modules by package name
|
|
140
|
-
const nmGroups = new Map();
|
|
141
|
-
for (const p of nodeModulesPaths) {
|
|
142
|
-
// Extract package name from path: ./X/node_modules/PKG/...
|
|
143
|
-
const match = p.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
|
|
144
|
-
const pkg = match ? match[1] : "other";
|
|
145
|
-
nmGroups.set(pkg, (nmGroups.get(pkg) ?? 0) + 1);
|
|
146
|
-
}
|
|
147
|
-
result.push(` node_modules/ (${nodeModulesPaths.length} matches)`);
|
|
148
|
-
const topPkgs = [...nmGroups.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
149
|
-
for (const [pkg, count] of topPkgs) {
|
|
150
|
-
result.push(` ${pkg} ×${count}`);
|
|
151
|
-
}
|
|
152
|
-
if (nmGroups.size > 3) {
|
|
153
|
-
result.push(` +${nmGroups.size - 3} more packages`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
/** Detect date range in timestamps and collapse */
|
|
160
|
-
function collapseDateRange(files) {
|
|
161
|
-
const timestamps = [];
|
|
162
|
-
for (const f of files) {
|
|
163
|
-
const match = f.match(/(\d{4})-(\d{2})-(\d{2})T?(\d{2})?/);
|
|
164
|
-
if (match) {
|
|
165
|
-
const [, y, m, d, h] = match;
|
|
166
|
-
timestamps.push(new Date(`${y}-${m}-${d}T${h ?? "00"}:00:00`));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (timestamps.length < 2)
|
|
170
|
-
return null;
|
|
171
|
-
timestamps.sort((a, b) => a.getTime() - b.getTime());
|
|
172
|
-
const first = timestamps[0];
|
|
173
|
-
const last = timestamps[timestamps.length - 1];
|
|
174
|
-
const fmt = (d) => `${d.getMonth() + 1}/${d.getDate()}`;
|
|
175
|
-
if (first.toDateString() === last.toDateString()) {
|
|
176
|
-
return `${fmt(first)}`;
|
|
177
|
-
}
|
|
178
|
-
return `${fmt(first)}–${fmt(last)}`;
|
|
179
|
-
}
|
|
180
|
-
/** Detect and compress ls -la style table output */
|
|
181
|
-
function compressLsTable(lines) {
|
|
182
|
-
// Detect ls -la format: permissions size date name
|
|
183
|
-
const lsPattern = /^[dlcbps-][rwxsStT-]{9}\s+\d+\s+\S+\s+\S+\s+\S+\s+\w+\s+\d+\s+[\d:]+\s+.+$/;
|
|
184
|
-
const isLsOutput = lines.filter(l => lsPattern.test(l.trim())).length > lines.length * 0.5;
|
|
185
|
-
if (!isLsOutput)
|
|
186
|
-
return null;
|
|
187
|
-
const result = [];
|
|
188
|
-
const dirs = [];
|
|
189
|
-
const files = [];
|
|
190
|
-
let totalSize = 0;
|
|
191
|
-
for (const line of lines) {
|
|
192
|
-
const match = line.trim().match(/^([dlcbps-])[rwxsStT-]{9}\s+\d+\s+\S+\s+\S+\s+(\S+)\s+\w+\s+\d+\s+[\d:]+\s+(.+)$/);
|
|
193
|
-
if (!match) {
|
|
194
|
-
if (line.trim().startsWith("total "))
|
|
195
|
-
continue;
|
|
196
|
-
result.push(line);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
const [, type, sizeStr, name] = match;
|
|
200
|
-
const size = parseInt(sizeStr) || 0;
|
|
201
|
-
totalSize += size;
|
|
202
|
-
if (type === "d") {
|
|
203
|
-
dirs.push(name);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
files.push({ name, size: formatSize(size) });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Compact display
|
|
210
|
-
if (dirs.length > 0) {
|
|
211
|
-
result.push(` 📁 ${dirs.join(" ")}${dirs.length > 5 ? ` (+${dirs.length - 5} more)` : ""}`);
|
|
212
|
-
}
|
|
213
|
-
if (files.length <= 8) {
|
|
214
|
-
for (const f of files) {
|
|
215
|
-
result.push(` ${f.size.padStart(6)} ${f.name}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
// Show top 5 by size + count
|
|
220
|
-
const sorted = files.sort((a, b) => parseSize(b.size) - parseSize(a.size));
|
|
221
|
-
for (const f of sorted.slice(0, 5)) {
|
|
222
|
-
result.push(` ${f.size.padStart(6)} ${f.name}`);
|
|
223
|
-
}
|
|
224
|
-
result.push(` ... +${files.length - 5} more files (${formatSize(totalSize)} total)`);
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
function formatSize(bytes) {
|
|
229
|
-
if (bytes >= 1_000_000)
|
|
230
|
-
return `${(bytes / 1_000_000).toFixed(1)}M`;
|
|
231
|
-
if (bytes >= 1_000)
|
|
232
|
-
return `${(bytes / 1_000).toFixed(1)}K`;
|
|
233
|
-
return `${bytes}B`;
|
|
234
|
-
}
|
|
235
|
-
function parseSize(s) {
|
|
236
|
-
const match = s.match(/([\d.]+)([BKMG])?/);
|
|
237
|
-
if (!match)
|
|
238
|
-
return 0;
|
|
239
|
-
const n = parseFloat(match[1]);
|
|
240
|
-
const unit = match[2];
|
|
241
|
-
if (unit === "K")
|
|
242
|
-
return n * 1000;
|
|
243
|
-
if (unit === "M")
|
|
244
|
-
return n * 1000000;
|
|
245
|
-
if (unit === "G")
|
|
246
|
-
return n * 1000000000;
|
|
247
|
-
return n;
|
|
248
|
-
}
|
|
249
|
-
/** Compress non-path generic output by deduplicating similar lines */
|
|
250
|
-
function compressGeneric(lines) {
|
|
251
|
-
if (lines.length <= 10)
|
|
252
|
-
return lines;
|
|
253
|
-
const result = [];
|
|
254
|
-
let repeatCount = 0;
|
|
255
|
-
let lastPattern = "";
|
|
256
|
-
for (let i = 0; i < lines.length; i++) {
|
|
257
|
-
const line = lines[i];
|
|
258
|
-
// Normalize: remove numbers, timestamps, hashes for pattern matching
|
|
259
|
-
const pattern = line
|
|
260
|
-
.replace(/\d{4}-\d{2}-\d{2}T[\d:.-]+Z?/g, "TIMESTAMP")
|
|
261
|
-
.replace(/\b[0-9a-f]{7,40}\b/g, "HASH")
|
|
262
|
-
.replace(/\b\d+\b/g, "N")
|
|
263
|
-
.trim();
|
|
264
|
-
if (pattern === lastPattern && i > 0) {
|
|
265
|
-
repeatCount++;
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
if (repeatCount > 1) {
|
|
269
|
-
result.push(` ... ×${repeatCount} similar`);
|
|
270
|
-
}
|
|
271
|
-
else if (repeatCount === 1) {
|
|
272
|
-
result.push(lines[i - 1]);
|
|
273
|
-
}
|
|
274
|
-
result.push(line);
|
|
275
|
-
lastPattern = pattern;
|
|
276
|
-
repeatCount = 0;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (repeatCount > 1) {
|
|
280
|
-
result.push(` ... ×${repeatCount} similar`);
|
|
281
|
-
}
|
|
282
|
-
else if (repeatCount === 1) {
|
|
283
|
-
result.push(lines[lines.length - 1]);
|
|
284
|
-
}
|
|
285
|
-
return result;
|
|
286
|
-
}
|
package/dist/snapshots.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// Session snapshots — capture terminal state for agent context handoff
|
|
2
|
-
import { loadHistory } from "./history.js";
|
|
3
|
-
import { bgStatus } from "./supervisor.js";
|
|
4
|
-
import { getEconomyStats, formatTokens } from "./economy.js";
|
|
5
|
-
import { listRecipes } from "./recipes/storage.js";
|
|
6
|
-
/** Capture a compact snapshot of the current terminal state */
|
|
7
|
-
export function captureSnapshot() {
|
|
8
|
-
// Filtered env — only relevant vars, no secrets
|
|
9
|
-
const safeEnvKeys = [
|
|
10
|
-
"PATH", "HOME", "USER", "SHELL", "NODE_ENV", "PWD", "LANG",
|
|
11
|
-
"TERM", "EDITOR", "VISUAL",
|
|
12
|
-
];
|
|
13
|
-
const env = {};
|
|
14
|
-
for (const key of safeEnvKeys) {
|
|
15
|
-
if (process.env[key])
|
|
16
|
-
env[key] = process.env[key];
|
|
17
|
-
}
|
|
18
|
-
// Running processes
|
|
19
|
-
const processes = bgStatus().map(p => ({
|
|
20
|
-
pid: p.pid,
|
|
21
|
-
command: p.command,
|
|
22
|
-
port: p.port,
|
|
23
|
-
uptime: Date.now() - p.startedAt,
|
|
24
|
-
}));
|
|
25
|
-
// Recent commands (last 10, compressed)
|
|
26
|
-
const history = loadHistory().slice(-10);
|
|
27
|
-
const recentCommands = history.map(h => ({
|
|
28
|
-
cmd: h.cmd,
|
|
29
|
-
exitCode: h.error,
|
|
30
|
-
summary: h.nl !== h.cmd ? h.nl : undefined,
|
|
31
|
-
}));
|
|
32
|
-
// Project recipes
|
|
33
|
-
const recipes = listRecipes(process.cwd()).slice(0, 10).map(r => ({
|
|
34
|
-
name: r.name,
|
|
35
|
-
command: r.command,
|
|
36
|
-
}));
|
|
37
|
-
// Economy
|
|
38
|
-
const econ = getEconomyStats();
|
|
39
|
-
return {
|
|
40
|
-
cwd: process.cwd(),
|
|
41
|
-
env,
|
|
42
|
-
runningProcesses: processes,
|
|
43
|
-
recentCommands,
|
|
44
|
-
recipes,
|
|
45
|
-
economy: {
|
|
46
|
-
tokensSaved: formatTokens(econ.totalTokensSaved),
|
|
47
|
-
tokensUsed: formatTokens(econ.totalTokensUsed),
|
|
48
|
-
},
|
|
49
|
-
timestamp: Date.now(),
|
|
50
|
-
};
|
|
51
|
-
}
|
package/dist/supervisor.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// Process supervisor — manages background processes for agents and humans
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
import { createConnection } from "net";
|
|
4
|
-
const processes = new Map();
|
|
5
|
-
/** Auto-detect port from common commands */
|
|
6
|
-
function detectPort(command) {
|
|
7
|
-
// "next dev -p 3001", "vite --port 4000", etc.
|
|
8
|
-
const portMatch = command.match(/-p\s+(\d+)|--port\s+(\d+)|PORT=(\d+)/);
|
|
9
|
-
if (portMatch)
|
|
10
|
-
return parseInt(portMatch[1] ?? portMatch[2] ?? portMatch[3]);
|
|
11
|
-
// Common defaults
|
|
12
|
-
if (/\bnext\s+dev\b/.test(command))
|
|
13
|
-
return 3000;
|
|
14
|
-
if (/\bvite\b/.test(command))
|
|
15
|
-
return 5173;
|
|
16
|
-
if (/\bnuxt\s+dev\b/.test(command))
|
|
17
|
-
return 3000;
|
|
18
|
-
if (/\bremix\s+dev\b/.test(command))
|
|
19
|
-
return 5173;
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
/** Start a background process */
|
|
23
|
-
export function bgStart(command, cwd) {
|
|
24
|
-
const workDir = cwd ?? process.cwd();
|
|
25
|
-
const proc = spawn("/bin/zsh", ["-c", command], {
|
|
26
|
-
cwd: workDir,
|
|
27
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
-
detached: false,
|
|
29
|
-
});
|
|
30
|
-
const meta = {
|
|
31
|
-
pid: proc.pid,
|
|
32
|
-
command,
|
|
33
|
-
cwd: workDir,
|
|
34
|
-
port: detectPort(command),
|
|
35
|
-
startedAt: Date.now(),
|
|
36
|
-
lastOutput: [],
|
|
37
|
-
};
|
|
38
|
-
const pushOutput = (d) => {
|
|
39
|
-
const lines = d.toString().split("\n").filter(l => l.trim());
|
|
40
|
-
meta.lastOutput.push(...lines);
|
|
41
|
-
// Keep last 50 lines
|
|
42
|
-
if (meta.lastOutput.length > 50) {
|
|
43
|
-
meta.lastOutput = meta.lastOutput.slice(-50);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
proc.stdout?.on("data", pushOutput);
|
|
47
|
-
proc.stderr?.on("data", pushOutput);
|
|
48
|
-
proc.on("close", (code) => {
|
|
49
|
-
meta.exitCode = code ?? 0;
|
|
50
|
-
});
|
|
51
|
-
processes.set(proc.pid, { proc, meta });
|
|
52
|
-
return meta;
|
|
53
|
-
}
|
|
54
|
-
/** List all managed processes */
|
|
55
|
-
export function bgStatus() {
|
|
56
|
-
const result = [];
|
|
57
|
-
for (const [pid, { proc, meta }] of processes) {
|
|
58
|
-
// Check if still alive
|
|
59
|
-
try {
|
|
60
|
-
process.kill(pid, 0);
|
|
61
|
-
result.push({ ...meta, lastOutput: meta.lastOutput.slice(-5) });
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// Process is dead
|
|
65
|
-
result.push({ ...meta, exitCode: meta.exitCode ?? -1, lastOutput: meta.lastOutput.slice(-5) });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
/** Stop a background process */
|
|
71
|
-
export function bgStop(pid) {
|
|
72
|
-
const entry = processes.get(pid);
|
|
73
|
-
if (!entry)
|
|
74
|
-
return false;
|
|
75
|
-
try {
|
|
76
|
-
entry.proc.kill("SIGTERM");
|
|
77
|
-
processes.delete(pid);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/** Get logs for a background process */
|
|
85
|
-
export function bgLogs(pid, tail = 20) {
|
|
86
|
-
const entry = processes.get(pid);
|
|
87
|
-
if (!entry)
|
|
88
|
-
return [];
|
|
89
|
-
return entry.meta.lastOutput.slice(-tail);
|
|
90
|
-
}
|
|
91
|
-
/** Wait for a port to be ready */
|
|
92
|
-
export function bgWaitPort(port, timeoutMs = 30000) {
|
|
93
|
-
return new Promise((resolve) => {
|
|
94
|
-
const start = Date.now();
|
|
95
|
-
const check = () => {
|
|
96
|
-
if (Date.now() - start > timeoutMs) {
|
|
97
|
-
resolve(false);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
101
|
-
sock.on("connect", () => {
|
|
102
|
-
sock.destroy();
|
|
103
|
-
resolve(true);
|
|
104
|
-
});
|
|
105
|
-
sock.on("error", () => {
|
|
106
|
-
sock.destroy();
|
|
107
|
-
setTimeout(check, 500);
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
check();
|
|
111
|
-
});
|
|
112
|
-
}
|
package/dist/test-watchlist.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
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, command) {
|
|
41
|
-
// If the command is explicitly a test command, trust it
|
|
42
|
-
if (command && /\b(bun\s+test|npm\s+test|jest|vitest|pytest|cargo\s+test|go\s+test)\b/.test(command))
|
|
43
|
-
return true;
|
|
44
|
-
// Otherwise require BOTH a summary line AND a test runner marker in the output
|
|
45
|
-
const summaryLine = /(?:\d+\s+pass|\d+\s+fail|Tests?:\s+\d+|Ran\s+\d+\s+tests?)\s*$/im;
|
|
46
|
-
const testMarkers = /(?:✓|✗|✔|✕|PASS\s+\S+\.test|FAIL\s+\S+\.test|bun test v|jest|vitest|pytest)/;
|
|
47
|
-
return summaryLine.test(output) && testMarkers.test(output);
|
|
48
|
-
}
|
|
49
|
-
/** Track test results and return only changes */
|
|
50
|
-
export function trackTests(cwd, output) {
|
|
51
|
-
const current = extractTests(output);
|
|
52
|
-
const prev = watchlists.get(cwd);
|
|
53
|
-
// Count totals from raw output (more reliable than extracted tests)
|
|
54
|
-
let totalPassed = 0, totalFailed = 0;
|
|
55
|
-
const summaryMatch = output.match(/(\d+)\s+pass/i);
|
|
56
|
-
const failMatch = output.match(/(\d+)\s+fail/i);
|
|
57
|
-
if (summaryMatch)
|
|
58
|
-
totalPassed = parseInt(summaryMatch[1]);
|
|
59
|
-
if (failMatch)
|
|
60
|
-
totalFailed = parseInt(failMatch[1]);
|
|
61
|
-
// Fallback to extracted counts
|
|
62
|
-
if (totalPassed === 0)
|
|
63
|
-
totalPassed = current.filter(t => t.status === "pass").length;
|
|
64
|
-
if (totalFailed === 0)
|
|
65
|
-
totalFailed = current.filter(t => t.status === "fail").length;
|
|
66
|
-
// Store current for next comparison
|
|
67
|
-
const currentMap = new Map();
|
|
68
|
-
for (const t of current)
|
|
69
|
-
currentMap.set(t.name, t);
|
|
70
|
-
watchlists.set(cwd, currentMap);
|
|
71
|
-
// First run — no comparison possible
|
|
72
|
-
if (!prev) {
|
|
73
|
-
return {
|
|
74
|
-
changed: [],
|
|
75
|
-
newTests: current.filter(t => t.status === "fail"), // only show failures on first run
|
|
76
|
-
totalPassed,
|
|
77
|
-
totalFailed,
|
|
78
|
-
unchangedCount: 0,
|
|
79
|
-
firstRun: true,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
// Compare with previous
|
|
83
|
-
const changed = [];
|
|
84
|
-
const newTests = [];
|
|
85
|
-
let unchangedCount = 0;
|
|
86
|
-
for (const [name, test] of currentMap) {
|
|
87
|
-
const prevTest = prev.get(name);
|
|
88
|
-
if (!prevTest) {
|
|
89
|
-
newTests.push(test);
|
|
90
|
-
}
|
|
91
|
-
else if (prevTest.status !== test.status) {
|
|
92
|
-
changed.push({ name, from: prevTest.status, to: test.status, error: test.error });
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
unchangedCount++;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return { changed, newTests, totalPassed, totalFailed, unchangedCount, firstRun: false };
|
|
99
|
-
}
|
|
100
|
-
/** Format watchlist result for display */
|
|
101
|
-
export function formatWatchResult(result) {
|
|
102
|
-
const lines = [];
|
|
103
|
-
if (result.firstRun) {
|
|
104
|
-
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed`);
|
|
105
|
-
if (result.newTests.length > 0) {
|
|
106
|
-
for (const t of result.newTests) {
|
|
107
|
-
lines.push(` ✗ ${t.name}${t.error ? `: ${t.error}` : ""}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return lines.join("\n");
|
|
111
|
-
}
|
|
112
|
-
// Status changes
|
|
113
|
-
for (const c of result.changed) {
|
|
114
|
-
if (c.to === "pass")
|
|
115
|
-
lines.push(` ✓ FIXED: ${c.name}`);
|
|
116
|
-
else
|
|
117
|
-
lines.push(` ✗ BROKE: ${c.name}${c.error ? ` — ${c.error}` : ""}`);
|
|
118
|
-
}
|
|
119
|
-
// New failures
|
|
120
|
-
for (const t of result.newTests.filter(t => t.status === "fail")) {
|
|
121
|
-
lines.push(` ✗ NEW FAIL: ${t.name}${t.error ? ` — ${t.error}` : ""}`);
|
|
122
|
-
}
|
|
123
|
-
// Summary
|
|
124
|
-
if (result.changed.length === 0 && result.newTests.filter(t => t.status === "fail").length === 0) {
|
|
125
|
-
lines.push(`✓ ${result.totalPassed} passed, ${result.totalFailed} failed (no changes)`);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed, ${result.unchangedCount} unchanged`);
|
|
129
|
-
}
|
|
130
|
-
return lines.join("\n");
|
|
131
|
-
}
|
package/dist/tree.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// Tree compression — convert flat file paths to compact tree representation
|
|
2
|
-
import { readdirSync, statSync } from "fs";
|
|
3
|
-
import { join, basename } from "path";
|
|
4
|
-
import { DEFAULT_EXCLUDE_DIRS } from "./search/filters.js";
|
|
5
|
-
/** Build a tree from a directory */
|
|
6
|
-
export function buildTree(dirPath, options = {}) {
|
|
7
|
-
const { maxDepth = 2, includeHidden = false, depth = 0 } = options;
|
|
8
|
-
const name = basename(dirPath) || dirPath;
|
|
9
|
-
const node = { name, type: "dir", children: [], fileCount: 0 };
|
|
10
|
-
if (depth >= maxDepth) {
|
|
11
|
-
// Count files without listing them
|
|
12
|
-
try {
|
|
13
|
-
const entries = readdirSync(dirPath);
|
|
14
|
-
node.fileCount = entries.length;
|
|
15
|
-
node.children = undefined; // don't expand
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
node.fileCount = 0;
|
|
19
|
-
}
|
|
20
|
-
return node;
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
const entries = readdirSync(dirPath);
|
|
24
|
-
for (const entry of entries) {
|
|
25
|
-
if (!includeHidden && entry.startsWith("."))
|
|
26
|
-
continue;
|
|
27
|
-
if (DEFAULT_EXCLUDE_DIRS.includes(entry)) {
|
|
28
|
-
// Show as collapsed with count
|
|
29
|
-
try {
|
|
30
|
-
const subPath = join(dirPath, entry);
|
|
31
|
-
const subStat = statSync(subPath);
|
|
32
|
-
if (subStat.isDirectory()) {
|
|
33
|
-
node.children.push({ name: entry, type: "dir", fileCount: -1 }); // -1 = hidden
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const fullPath = join(dirPath, entry);
|
|
42
|
-
try {
|
|
43
|
-
const stat = statSync(fullPath);
|
|
44
|
-
if (stat.isDirectory()) {
|
|
45
|
-
node.children.push(buildTree(fullPath, { maxDepth, includeHidden, depth: depth + 1 }));
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
node.children.push({ name: entry, type: "file", size: stat.size });
|
|
49
|
-
node.fileCount++;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
return node;
|
|
59
|
-
}
|
|
60
|
-
/** Render tree as compact string (for agents — minimum tokens) */
|
|
61
|
-
export function compactTree(node, indent = 0) {
|
|
62
|
-
const pad = " ".repeat(indent);
|
|
63
|
-
if (node.type === "file")
|
|
64
|
-
return `${pad}${node.name}`;
|
|
65
|
-
if (node.fileCount === -1)
|
|
66
|
-
return `${pad}${node.name}/ (hidden)`;
|
|
67
|
-
if (!node.children || node.children.length === 0)
|
|
68
|
-
return `${pad}${node.name}/ (empty)`;
|
|
69
|
-
if (!node.children.some(c => c.children)) {
|
|
70
|
-
// Leaf directory — compact single line
|
|
71
|
-
const files = node.children.filter(c => c.type === "file").map(c => c.name);
|
|
72
|
-
const dirs = node.children.filter(c => c.type === "dir");
|
|
73
|
-
const parts = [];
|
|
74
|
-
if (files.length <= 5) {
|
|
75
|
-
parts.push(...files);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
parts.push(`${files.length} files`);
|
|
79
|
-
}
|
|
80
|
-
for (const d of dirs) {
|
|
81
|
-
parts.push(`${d.name}/${d.fileCount != null ? ` (${d.fileCount === -1 ? "hidden" : d.fileCount + " files"})` : ""}`);
|
|
82
|
-
}
|
|
83
|
-
return `${pad}${node.name}/ [${parts.join(", ")}]`;
|
|
84
|
-
}
|
|
85
|
-
const lines = [`${pad}${node.name}/`];
|
|
86
|
-
for (const child of node.children) {
|
|
87
|
-
lines.push(compactTree(child, indent + 1));
|
|
88
|
-
}
|
|
89
|
-
return lines.join("\n");
|
|
90
|
-
}
|
|
91
|
-
/** Render tree as JSON (for MCP) */
|
|
92
|
-
export function treeToJson(node) {
|
|
93
|
-
return node;
|
|
94
|
-
}
|