@hasna/terminal 0.5.2 → 0.6.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/.claude/scheduled_tasks.lock +1 -1
- package/dist/cli.js +83 -0
- package/dist/command-rewriter.js +64 -0
- package/dist/compression.js +2 -2
- package/dist/diff-cache.js +21 -1
- package/dist/expand-store.js +38 -0
- package/dist/lazy-executor.js +41 -0
- package/dist/mcp/server.js +70 -10
- package/dist/noise-filter.js +70 -0
- package/dist/output-processor.js +26 -0
- package/dist/search/semantic.js +39 -1
- package/package.json +1 -1
- package/src/cli.tsx +84 -0
- package/src/command-rewriter.ts +80 -0
- package/src/compression.test.ts +2 -3
- package/src/compression.ts +2 -2
- package/src/diff-cache.ts +25 -2
- package/src/expand-store.ts +51 -0
- package/src/hooks/claude-hook.sh +52 -0
- package/src/lazy-executor.ts +57 -0
- package/src/mcp/server.ts +86 -11
- package/src/noise-filter.ts +83 -0
- package/src/output-processor.ts +39 -1
- package/src/search/semantic.ts +35 -1
- package/dist/compression.test.js +0 -42
- package/dist/diff-cache.test.js +0 -27
- package/dist/economy.test.js +0 -13
- package/dist/parsers/parsers.test.js +0 -136
- package/dist/providers/providers.test.js +0 -14
- package/dist/recipes/recipes.test.js +0 -36
- package/dist/search/search.test.js +0 -22
package/src/output-processor.ts
CHANGED
|
@@ -12,10 +12,18 @@ export interface ProcessedOutput {
|
|
|
12
12
|
full: string;
|
|
13
13
|
/** Structured JSON if the AI could extract it */
|
|
14
14
|
structured?: Record<string, unknown>;
|
|
15
|
-
/** How many tokens were saved */
|
|
15
|
+
/** How many tokens were saved (net, after subtracting AI cost) */
|
|
16
16
|
tokensSaved: number;
|
|
17
|
+
/** Tokens used by the AI summarization call */
|
|
18
|
+
aiTokensUsed: number;
|
|
17
19
|
/** Whether AI processing was used (vs passthrough) */
|
|
18
20
|
aiProcessed: boolean;
|
|
21
|
+
/** Cost of the AI call in USD (Cerebras pricing) */
|
|
22
|
+
aiCostUsd: number;
|
|
23
|
+
/** Value of tokens saved in USD (at Claude Sonnet rates) */
|
|
24
|
+
savingsValueUsd: number;
|
|
25
|
+
/** Net ROI: savings minus AI cost */
|
|
26
|
+
netSavingsUsd: number;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
const MIN_LINES_TO_PROCESS = 15;
|
|
@@ -53,7 +61,11 @@ export async function processOutput(
|
|
|
53
61
|
summary: output,
|
|
54
62
|
full: output,
|
|
55
63
|
tokensSaved: 0,
|
|
64
|
+
aiTokensUsed: 0,
|
|
56
65
|
aiProcessed: false,
|
|
66
|
+
aiCostUsd: 0,
|
|
67
|
+
savingsValueUsd: 0,
|
|
68
|
+
netSavingsUsd: 0,
|
|
57
69
|
};
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -94,12 +106,34 @@ export async function processOutput(
|
|
|
94
106
|
}
|
|
95
107
|
} catch { /* not JSON, that's fine */ }
|
|
96
108
|
|
|
109
|
+
// Cost calculation
|
|
110
|
+
// AI input: system prompt (~200 tokens) + command + output sent to AI
|
|
111
|
+
const aiInputTokens = estimateTokens(SUMMARIZE_PROMPT) + estimateTokens(toSummarize) + 20;
|
|
112
|
+
const aiOutputTokens = summaryTokens;
|
|
113
|
+
const aiTokensUsed = aiInputTokens + aiOutputTokens;
|
|
114
|
+
|
|
115
|
+
// Cerebras qwen-3-235b pricing: $0.60/M input, $1.20/M output
|
|
116
|
+
const aiCostUsd = (aiInputTokens * 0.60 + aiOutputTokens * 1.20) / 1_000_000;
|
|
117
|
+
|
|
118
|
+
// Value of tokens saved (at Claude Sonnet $3/M input — what the agent would pay)
|
|
119
|
+
const savingsValueUsd = (saved * 3.0) / 1_000_000;
|
|
120
|
+
const netSavingsUsd = savingsValueUsd - aiCostUsd;
|
|
121
|
+
|
|
122
|
+
// Only record savings if net positive (AI cost < token savings value)
|
|
123
|
+
if (netSavingsUsd > 0 && saved > 0) {
|
|
124
|
+
recordSaving("compressed", saved);
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
return {
|
|
98
128
|
summary,
|
|
99
129
|
full: output,
|
|
100
130
|
structured,
|
|
101
131
|
tokensSaved: saved,
|
|
132
|
+
aiTokensUsed,
|
|
102
133
|
aiProcessed: true,
|
|
134
|
+
aiCostUsd,
|
|
135
|
+
savingsValueUsd,
|
|
136
|
+
netSavingsUsd,
|
|
103
137
|
};
|
|
104
138
|
} catch {
|
|
105
139
|
// AI unavailable — fall back to simple truncation
|
|
@@ -111,7 +145,11 @@ export async function processOutput(
|
|
|
111
145
|
summary: fallback,
|
|
112
146
|
full: output,
|
|
113
147
|
tokensSaved: Math.max(0, estimateTokens(output) - estimateTokens(fallback)),
|
|
148
|
+
aiTokensUsed: 0,
|
|
114
149
|
aiProcessed: false,
|
|
150
|
+
aiCostUsd: 0,
|
|
151
|
+
savingsValueUsd: 0,
|
|
152
|
+
netSavingsUsd: 0,
|
|
115
153
|
};
|
|
116
154
|
}
|
|
117
155
|
}
|
package/src/search/semantic.ts
CHANGED
|
@@ -37,6 +37,40 @@ export function extractSymbolsFromFile(filePath: string): CodeSymbol[] {
|
|
|
37
37
|
return extractSymbols(filePath);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Extract the complete code block for a symbol by name */
|
|
41
|
+
export function extractBlock(filePath: string, symbolName: string): { code: string; startLine: number; endLine: number } | null {
|
|
42
|
+
if (!existsSync(filePath)) return null;
|
|
43
|
+
const content = readFileSync(filePath, "utf8");
|
|
44
|
+
const lines = content.split("\n");
|
|
45
|
+
const symbols = extractSymbols(filePath);
|
|
46
|
+
|
|
47
|
+
const symbol = symbols.find(s => s.name === symbolName && s.kind !== "import");
|
|
48
|
+
if (!symbol) return null;
|
|
49
|
+
|
|
50
|
+
const startLine = symbol.line - 1; // 0-indexed
|
|
51
|
+
let braceDepth = 0;
|
|
52
|
+
let foundOpen = false;
|
|
53
|
+
let endLine = startLine;
|
|
54
|
+
|
|
55
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
56
|
+
const line = lines[i];
|
|
57
|
+
for (const ch of line) {
|
|
58
|
+
if (ch === "{") { braceDepth++; foundOpen = true; }
|
|
59
|
+
if (ch === "}") { braceDepth--; }
|
|
60
|
+
}
|
|
61
|
+
endLine = i;
|
|
62
|
+
if (foundOpen && braceDepth <= 0) break;
|
|
63
|
+
// For single-line arrow functions without braces
|
|
64
|
+
if (i === startLine && !line.includes("{") && line.includes("=>")) break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
code: lines.slice(startLine, endLine + 1).join("\n"),
|
|
69
|
+
startLine: startLine + 1, // 1-indexed
|
|
70
|
+
endLine: endLine + 1,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
function extractSymbols(filePath: string): CodeSymbol[] {
|
|
41
75
|
if (!existsSync(filePath)) return [];
|
|
42
76
|
const content = readFileSync(filePath, "utf8");
|
|
@@ -50,7 +84,7 @@ function extractSymbols(filePath: string): CodeSymbol[] {
|
|
|
50
84
|
const isExported = line.trimStart().startsWith("export");
|
|
51
85
|
|
|
52
86
|
// Functions: export function X(...) or export const X = (...) =>
|
|
53
|
-
const funcMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(
|
|
87
|
+
const funcMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/);
|
|
54
88
|
if (funcMatch) {
|
|
55
89
|
const prevLine = i > 0 ? lines[i - 1] : "";
|
|
56
90
|
const doc = prevLine.trim().startsWith("/**") || prevLine.trim().startsWith("//")
|
package/dist/compression.test.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { compress, stripAnsi } from "./compression.js";
|
|
3
|
-
describe("stripAnsi", () => {
|
|
4
|
-
it("removes ANSI escape codes", () => {
|
|
5
|
-
expect(stripAnsi("\x1b[31mred\x1b[0m")).toBe("red");
|
|
6
|
-
expect(stripAnsi("\x1b[1;32mbold green\x1b[0m")).toBe("bold green");
|
|
7
|
-
});
|
|
8
|
-
it("leaves clean text unchanged", () => {
|
|
9
|
-
expect(stripAnsi("hello world")).toBe("hello world");
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
describe("compress", () => {
|
|
13
|
-
it("strips ANSI by default", () => {
|
|
14
|
-
const result = compress("ls", "\x1b[32mfile.ts\x1b[0m");
|
|
15
|
-
expect(result.content).not.toContain("\x1b");
|
|
16
|
-
});
|
|
17
|
-
it("uses structured parser when format=json", () => {
|
|
18
|
-
const output = `total 16
|
|
19
|
-
-rw-r--r-- 1 user staff 450 Mar 10 09:00 package.json
|
|
20
|
-
drwxr-xr-x 5 user staff 160 Mar 10 09:00 src`;
|
|
21
|
-
const result = compress("ls -la", output, { format: "json" });
|
|
22
|
-
// Parser may or may not save tokens on small input, just check it parsed
|
|
23
|
-
expect(result.content).toBeTruthy();
|
|
24
|
-
const parsed = JSON.parse(result.content);
|
|
25
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
it("respects maxTokens budget", () => {
|
|
28
|
-
const longOutput = Array.from({ length: 100 }, (_, i) => `Line ${i}: some output text here`).join("\n");
|
|
29
|
-
const result = compress("some-command", longOutput, { maxTokens: 50 });
|
|
30
|
-
expect(result.compressedTokens).toBeLessThanOrEqual(60); // allow some slack
|
|
31
|
-
});
|
|
32
|
-
it("deduplicates similar lines", () => {
|
|
33
|
-
const output = Array.from({ length: 20 }, (_, i) => `Compiling module ${i}...`).join("\n");
|
|
34
|
-
const result = compress("build", output);
|
|
35
|
-
expect(result.compressedTokens).toBeLessThan(result.originalTokens);
|
|
36
|
-
});
|
|
37
|
-
it("tracks savings on large output", () => {
|
|
38
|
-
const output = Array.from({ length: 100 }, (_, i) => `Line ${i}: some long output text here that takes tokens`).join("\n");
|
|
39
|
-
const result = compress("cmd", output, { maxTokens: 50 });
|
|
40
|
-
expect(result.compressedTokens).toBeLessThan(result.originalTokens);
|
|
41
|
-
});
|
|
42
|
-
});
|
package/dist/diff-cache.test.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { diffOutput, clearDiffCache } from "./diff-cache.js";
|
|
3
|
-
describe("diffOutput", () => {
|
|
4
|
-
it("returns first run with no previous", () => {
|
|
5
|
-
clearDiffCache();
|
|
6
|
-
const result = diffOutput("npm test", "/tmp", "PASS\n5 passed");
|
|
7
|
-
expect(result.hasPrevious).toBe(false);
|
|
8
|
-
expect(result.diffSummary).toBe("first run");
|
|
9
|
-
expect(result.tokensSaved).toBe(0);
|
|
10
|
-
});
|
|
11
|
-
it("detects identical output", () => {
|
|
12
|
-
clearDiffCache();
|
|
13
|
-
diffOutput("npm test", "/tmp/id", "PASS\n5 passed");
|
|
14
|
-
const result = diffOutput("npm test", "/tmp/id", "PASS\n5 passed");
|
|
15
|
-
expect(result.unchanged).toBe(true);
|
|
16
|
-
expect(result.diffSummary).toBe("identical to previous run");
|
|
17
|
-
});
|
|
18
|
-
it("computes diff for changed output", () => {
|
|
19
|
-
clearDiffCache();
|
|
20
|
-
diffOutput("npm test", "/tmp/diff", "PASS test1\nPASS test2\nFAIL test3");
|
|
21
|
-
const result = diffOutput("npm test", "/tmp/diff", "PASS test1\nPASS test2\nPASS test3");
|
|
22
|
-
expect(result.hasPrevious).toBe(true);
|
|
23
|
-
expect(result.unchanged).toBe(false);
|
|
24
|
-
expect(result.added).toContain("PASS test3");
|
|
25
|
-
expect(result.removed).toContain("FAIL test3");
|
|
26
|
-
});
|
|
27
|
-
});
|
package/dist/economy.test.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { formatTokens } from "./economy.js";
|
|
3
|
-
describe("formatTokens", () => {
|
|
4
|
-
it("formats small numbers", () => {
|
|
5
|
-
expect(formatTokens(42)).toBe("42");
|
|
6
|
-
});
|
|
7
|
-
it("formats thousands", () => {
|
|
8
|
-
expect(formatTokens(1500)).toBe("1.5K");
|
|
9
|
-
});
|
|
10
|
-
it("formats millions", () => {
|
|
11
|
-
expect(formatTokens(2500000)).toBe("2.5M");
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { parseOutput, tokenSavings, estimateTokens } from "./index.js";
|
|
3
|
-
describe("parseOutput", () => {
|
|
4
|
-
it("parses ls -la output", () => {
|
|
5
|
-
const output = `total 32
|
|
6
|
-
drwxr-xr-x 5 user staff 160 Mar 10 09:00 src
|
|
7
|
-
-rw-r--r-- 1 user staff 450 Mar 10 09:00 package.json
|
|
8
|
-
lrwxr-xr-x 1 user staff 20 Mar 10 09:00 link -> target`;
|
|
9
|
-
const result = parseOutput("ls -la", output);
|
|
10
|
-
expect(result).not.toBeNull();
|
|
11
|
-
expect(result.parser).toBe("ls");
|
|
12
|
-
const data = result.data;
|
|
13
|
-
expect(data.length).toBe(3);
|
|
14
|
-
expect(data[0].name).toBe("src");
|
|
15
|
-
expect(data[0].type).toBe("dir");
|
|
16
|
-
expect(data[1].name).toBe("package.json");
|
|
17
|
-
expect(data[1].type).toBe("file");
|
|
18
|
-
expect(data[2].type).toBe("symlink");
|
|
19
|
-
});
|
|
20
|
-
it("parses find output and filters node_modules", () => {
|
|
21
|
-
const output = `./src/lib/webhooks.ts
|
|
22
|
-
./node_modules/@types/node/async_hooks.d.ts
|
|
23
|
-
./node_modules/@types/node/perf_hooks.d.ts
|
|
24
|
-
./dist/lib/webhooks.d.ts
|
|
25
|
-
./src/routes/api.ts`;
|
|
26
|
-
const result = parseOutput("find . -name '*hooks*' -type f", output);
|
|
27
|
-
expect(result).not.toBeNull();
|
|
28
|
-
expect(result.parser).toBe("find");
|
|
29
|
-
const data = result.data;
|
|
30
|
-
expect(data.source.length).toBe(2); // webhooks.ts and api.ts
|
|
31
|
-
expect(data.filtered.length).toBeGreaterThan(0);
|
|
32
|
-
expect(data.filtered.find((f) => f.reason === "node_modules")?.count).toBe(2);
|
|
33
|
-
});
|
|
34
|
-
it("parses test output (jest style)", () => {
|
|
35
|
-
const output = `PASS src/auth.test.ts
|
|
36
|
-
FAIL src/db.test.ts
|
|
37
|
-
✗ should connect to database
|
|
38
|
-
Error: Connection refused
|
|
39
|
-
Tests: 5 passed, 1 failed, 1 skipped, 7 total
|
|
40
|
-
Time: 3.2s`;
|
|
41
|
-
const result = parseOutput("npm test", output);
|
|
42
|
-
expect(result).not.toBeNull();
|
|
43
|
-
expect(result.parser).toBe("test");
|
|
44
|
-
const data = result.data;
|
|
45
|
-
expect(data.passed).toBe(5);
|
|
46
|
-
expect(data.failed).toBe(1);
|
|
47
|
-
expect(data.skipped).toBe(1);
|
|
48
|
-
expect(data.total).toBe(7);
|
|
49
|
-
});
|
|
50
|
-
it("parses git status", () => {
|
|
51
|
-
const output = `On branch main
|
|
52
|
-
Changes to be committed:
|
|
53
|
-
new file: src/mcp/server.ts
|
|
54
|
-
modified: src/ai.ts
|
|
55
|
-
|
|
56
|
-
Changes not staged for commit:
|
|
57
|
-
modified: package.json
|
|
58
|
-
|
|
59
|
-
Untracked files:
|
|
60
|
-
src/tree.ts`;
|
|
61
|
-
const result = parseOutput("git status", output);
|
|
62
|
-
expect(result).not.toBeNull();
|
|
63
|
-
expect(result.parser).toBe("git-status");
|
|
64
|
-
const data = result.data;
|
|
65
|
-
expect(data.branch).toBe("main");
|
|
66
|
-
expect(data.staged.length).toBe(2);
|
|
67
|
-
expect(data.unstaged.length).toBe(1);
|
|
68
|
-
expect(data.untracked.length).toBe(1);
|
|
69
|
-
});
|
|
70
|
-
it("parses git log", () => {
|
|
71
|
-
const output = `commit af19ce3456789
|
|
72
|
-
Author: Andrei Hasna <andrei@hasna.com>
|
|
73
|
-
Date: Sat Mar 15 10:00:00 2026
|
|
74
|
-
|
|
75
|
-
feat: add MCP server
|
|
76
|
-
|
|
77
|
-
commit 3963db5123456
|
|
78
|
-
Author: Andrei Hasna <andrei@hasna.com>
|
|
79
|
-
Date: Fri Mar 14 09:00:00 2026
|
|
80
|
-
|
|
81
|
-
feat: tabs and browse mode`;
|
|
82
|
-
const result = parseOutput("git log", output);
|
|
83
|
-
expect(result).not.toBeNull();
|
|
84
|
-
expect(result.parser).toBe("git-log");
|
|
85
|
-
const data = result.data;
|
|
86
|
-
expect(data.length).toBe(2);
|
|
87
|
-
expect(data[0].hash).toBe("af19ce34");
|
|
88
|
-
expect(data[0].message).toBe("feat: add MCP server");
|
|
89
|
-
});
|
|
90
|
-
it("parses npm install output", () => {
|
|
91
|
-
const output = `added 47 packages in 3.2s
|
|
92
|
-
2 vulnerabilities found`;
|
|
93
|
-
const result = parseOutput("npm install", output);
|
|
94
|
-
expect(result).not.toBeNull();
|
|
95
|
-
expect(result.parser).toBe("npm-install");
|
|
96
|
-
const data = result.data;
|
|
97
|
-
expect(data.installed).toBe(47);
|
|
98
|
-
expect(data.duration).toBe("3.2s");
|
|
99
|
-
expect(data.vulnerabilities).toBe(2);
|
|
100
|
-
});
|
|
101
|
-
it("parses build output", () => {
|
|
102
|
-
const output = `Compiling...
|
|
103
|
-
1 warning
|
|
104
|
-
Found 0 errors
|
|
105
|
-
Done in 2.5s`;
|
|
106
|
-
const result = parseOutput("npm run build", output);
|
|
107
|
-
expect(result).not.toBeNull();
|
|
108
|
-
expect(result.parser).toBe("build");
|
|
109
|
-
const data = result.data;
|
|
110
|
-
expect(data.status).toBe("success");
|
|
111
|
-
expect(data.warnings).toBe(1);
|
|
112
|
-
});
|
|
113
|
-
it("detects errors", () => {
|
|
114
|
-
const output = `Error: EADDRINUSE: address already in use :3000`;
|
|
115
|
-
const result = parseOutput("node server.js", output);
|
|
116
|
-
expect(result).not.toBeNull();
|
|
117
|
-
expect(result.parser).toBe("error");
|
|
118
|
-
const data = result.data;
|
|
119
|
-
expect(data.type).toBe("port_in_use");
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
describe("estimateTokens", () => {
|
|
123
|
-
it("estimates roughly 4 chars per token", () => {
|
|
124
|
-
expect(estimateTokens("hello world")).toBe(3); // 11 chars / 4 = 2.75 → 3
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
describe("tokenSavings", () => {
|
|
128
|
-
it("calculates savings correctly", () => {
|
|
129
|
-
const raw = "a".repeat(400); // 100 tokens
|
|
130
|
-
const parsed = { status: "ok" };
|
|
131
|
-
const result = tokenSavings(raw, parsed);
|
|
132
|
-
expect(result.rawTokens).toBe(100);
|
|
133
|
-
expect(result.saved).toBeGreaterThan(0);
|
|
134
|
-
expect(result.percent).toBeGreaterThan(0);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { availableProviders, resetProvider } from "./index.js";
|
|
3
|
-
describe("providers", () => {
|
|
4
|
-
it("lists available providers", () => {
|
|
5
|
-
const providers = availableProviders();
|
|
6
|
-
expect(providers.length).toBe(2);
|
|
7
|
-
expect(providers[0].name).toBe("cerebras");
|
|
8
|
-
expect(providers[1].name).toBe("anthropic");
|
|
9
|
-
});
|
|
10
|
-
it("resetProvider clears cache", () => {
|
|
11
|
-
// Should not throw
|
|
12
|
-
resetProvider();
|
|
13
|
-
});
|
|
14
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { genId, substituteVariables, extractVariables } from "./model.js";
|
|
3
|
-
describe("genId", () => {
|
|
4
|
-
it("generates unique ids", () => {
|
|
5
|
-
const a = genId();
|
|
6
|
-
const b = genId();
|
|
7
|
-
expect(a).not.toBe(b);
|
|
8
|
-
expect(a.length).toBe(8);
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
describe("substituteVariables", () => {
|
|
12
|
-
it("replaces {var} placeholders", () => {
|
|
13
|
-
expect(substituteVariables("kill -9 {pid}", { pid: "1234" })).toBe("kill -9 1234");
|
|
14
|
-
});
|
|
15
|
-
it("replaces multiple variables", () => {
|
|
16
|
-
expect(substituteVariables("curl {host}:{port}", { host: "localhost", port: "3000" }))
|
|
17
|
-
.toBe("curl localhost:3000");
|
|
18
|
-
});
|
|
19
|
-
it("replaces all occurrences of same variable", () => {
|
|
20
|
-
expect(substituteVariables("{x} and {x}", { x: "y" })).toBe("y and y");
|
|
21
|
-
});
|
|
22
|
-
it("leaves unmatched vars alone", () => {
|
|
23
|
-
expect(substituteVariables("{a} {b}", { a: "1" })).toBe("1 {b}");
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
describe("extractVariables", () => {
|
|
27
|
-
it("extracts variable names from command", () => {
|
|
28
|
-
expect(extractVariables("lsof -i :{port} -t | xargs kill")).toEqual(["port"]);
|
|
29
|
-
});
|
|
30
|
-
it("deduplicates", () => {
|
|
31
|
-
expect(extractVariables("{x} {x} {y}")).toEqual(["x", "y"]);
|
|
32
|
-
});
|
|
33
|
-
it("returns empty for no variables", () => {
|
|
34
|
-
expect(extractVariables("ls -la")).toEqual([]);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { isSourceFile, isExcludedDir, relevanceScore } from "./filters.js";
|
|
3
|
-
describe("filters", () => {
|
|
4
|
-
it("identifies source files", () => {
|
|
5
|
-
expect(isSourceFile("src/app.ts")).toBe(true);
|
|
6
|
-
expect(isSourceFile("index.tsx")).toBe(true);
|
|
7
|
-
expect(isSourceFile("main.py")).toBe(true);
|
|
8
|
-
expect(isSourceFile("image.png")).toBe(false);
|
|
9
|
-
expect(isSourceFile("binary")).toBe(false);
|
|
10
|
-
});
|
|
11
|
-
it("identifies excluded directories", () => {
|
|
12
|
-
expect(isExcludedDir("./node_modules/foo/bar.js")).toBe(true);
|
|
13
|
-
expect(isExcludedDir("./dist/index.js")).toBe(true);
|
|
14
|
-
expect(isExcludedDir("./.git/config")).toBe(true);
|
|
15
|
-
expect(isExcludedDir("./src/lib/utils.ts")).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
it("scores source files highest", () => {
|
|
18
|
-
expect(relevanceScore("src/app.ts")).toBe(10);
|
|
19
|
-
expect(relevanceScore("./node_modules/foo.js")).toBe(0);
|
|
20
|
-
expect(relevanceScore("binary")).toBe(3);
|
|
21
|
-
});
|
|
22
|
-
});
|