@frixaco/hbench 0.1.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/README.md +51 -0
- package/bin/hbench.js +59 -0
- package/bun-env.d.ts +17 -0
- package/lib/.gitkeep +0 -0
- package/package.json +74 -0
- package/server/build.ts +172 -0
- package/server/index.ts +539 -0
- package/server/review.ts +162 -0
- package/server/tsconfig.json +9 -0
- package/tsconfig.base.json +25 -0
- package/tsconfig.json +4 -0
- package/ui/app.tsx +15 -0
- package/ui/components/button.tsx +57 -0
- package/ui/components/cmd-bar.tsx +131 -0
- package/ui/components/diff-view.tsx +51 -0
- package/ui/components/input.tsx +18 -0
- package/ui/components/review-sheet.tsx +261 -0
- package/ui/components/review-view.tsx +40 -0
- package/ui/components/select.tsx +199 -0
- package/ui/components/sheet.tsx +131 -0
- package/ui/components/sonner.tsx +41 -0
- package/ui/components/tui.tsx +313 -0
- package/ui/ghostty-web.tsx +138 -0
- package/ui/index.html +13 -0
- package/ui/index.tsx +20 -0
- package/ui/lib/agent-patterns.ts +127 -0
- package/ui/lib/diff-client.ts +38 -0
- package/ui/lib/models.json +8 -0
- package/ui/lib/reviewer.ts +82 -0
- package/ui/lib/store.ts +90 -0
- package/ui/lib/utils.ts +7 -0
- package/ui/lib/websocket.tsx +144 -0
- package/ui/styles.css +89 -0
- package/ui/tsconfig.json +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Harness Bench
|
|
2
|
+
|
|
3
|
+
CLI agent benchmarker dashboard. Run multiple coding agents on the same task, watch their terminals live, and compare each output using reviewer.
|
|
4
|
+
|
|
5
|
+
https://github.com/user-attachments/assets/eb489f56-fbc7-4e8e-adb2-c232411303d2
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- Run `amp`, `opencode`, `claude`, `codex`, `pi`, `droid` in parallel
|
|
10
|
+
- WebSocket-driven PTY streaming for live terminal output
|
|
11
|
+
- Explicit global stop path via `POST /stop` with shutdown ladder (`Ctrl-C`, `Ctrl-C`, `SIGTERM`, `SIGKILL`)
|
|
12
|
+
- Dark, monospace-first UI with `ghostty-web` terminals (optionally `xtermjs`, `restty` alternatives)
|
|
13
|
+
- Per agent Git worktree set up with cleanup control
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Requirements:
|
|
18
|
+
|
|
19
|
+
- Git
|
|
20
|
+
- `OPENROUTER_API_KEY` (add to PATH or provide in UI)
|
|
21
|
+
- Bun: `curl -fsSL https://bun.sh/install | bash`
|
|
22
|
+
- Amp: `curl -fsSL https://ampcode.com/install.sh | bash`
|
|
23
|
+
- Droid: `curl -fsSL https://app.factory.ai/cli | sh`
|
|
24
|
+
- OpenCode: `curl -fsSL https://opencode.ai/install | bash`
|
|
25
|
+
- Codex: `bun i -g @openai/codex`
|
|
26
|
+
- Pi: `bun i -g @mariozechner/pi-coding-agent`
|
|
27
|
+
- Claude Code: `curl -fsSL https://claude.ai/install.sh | bash`
|
|
28
|
+
|
|
29
|
+
Run locally without installing:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bunx @frixaco/hbench
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Development:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun install
|
|
39
|
+
bun run dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun run dev # UI + PTY + REST server
|
|
46
|
+
bun run build # production build
|
|
47
|
+
bun run start # for hosting environment
|
|
48
|
+
bun run lint # eslint
|
|
49
|
+
bun run format # prettier
|
|
50
|
+
bun run check # format + lint
|
|
51
|
+
```
|
package/bin/hbench.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
6
|
+
console.log(`
|
|
7
|
+
hbench
|
|
8
|
+
|
|
9
|
+
Run the local hbench dashboard server.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
hbench [--port <number>]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
-p, --port <number> Override server port (default: Bun.serve defaults)
|
|
16
|
+
-h, --help Show this help message
|
|
17
|
+
`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const readPortArg = () => {
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const arg = args[i];
|
|
24
|
+
if (arg === "-p" || arg === "--port") {
|
|
25
|
+
return args[i + 1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (arg?.startsWith("--port=")) {
|
|
29
|
+
return arg.slice("--port=".length);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const normalizePort = (value) => {
|
|
37
|
+
if (!value) return null;
|
|
38
|
+
if (!/^\d+$/.test(value)) {
|
|
39
|
+
throw new Error(`Invalid port: ${value}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parsed = Number.parseInt(value, 10);
|
|
43
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
44
|
+
throw new Error(`Port out of range: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return String(parsed);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const port = normalizePort(readPortArg());
|
|
51
|
+
if (port) {
|
|
52
|
+
process.env.PORT = port;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!process.env.NODE_ENV) {
|
|
56
|
+
process.env.NODE_ENV = "production";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await import("../server/index.ts");
|
package/bun-env.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Generated by `bun init`
|
|
2
|
+
|
|
3
|
+
declare module "*.svg" {
|
|
4
|
+
/**
|
|
5
|
+
* A path to the SVG file
|
|
6
|
+
*/
|
|
7
|
+
const path: `${string}.svg`;
|
|
8
|
+
export = path;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "*.module.css" {
|
|
12
|
+
/**
|
|
13
|
+
* A record of class names to their corresponding CSS module classes
|
|
14
|
+
*/
|
|
15
|
+
const classes: { readonly [key: string]: string };
|
|
16
|
+
export = classes;
|
|
17
|
+
}
|
package/lib/.gitkeep
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frixaco/hbench",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"hbench": "./bin/hbench.js"
|
|
7
|
+
},
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"server",
|
|
14
|
+
"ui",
|
|
15
|
+
"lib",
|
|
16
|
+
"bun-env.d.ts",
|
|
17
|
+
"tsconfig.json",
|
|
18
|
+
"tsconfig.base.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE*"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "bun --hot server/index.ts",
|
|
24
|
+
"start": "NODE_ENV=production bun server/index.ts",
|
|
25
|
+
"build": "bun run server/build.ts",
|
|
26
|
+
"format": "prettier --write .",
|
|
27
|
+
"lint": "eslint",
|
|
28
|
+
"lint-fix": "eslint --fix",
|
|
29
|
+
"ts:ui": "bunx tsc -p ui/tsconfig.json --noEmit",
|
|
30
|
+
"ts:api": "bunx tsc -p server/tsconfig.json --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ai-sdk/react": "^3.0.88",
|
|
34
|
+
"@base-ui/react": "^1.2.0",
|
|
35
|
+
"@openrouter/ai-sdk-provider": "^2.2.3",
|
|
36
|
+
"@pierre/diffs": "^1.0.11",
|
|
37
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
38
|
+
"@xterm/addon-webgl": "^0.19.0",
|
|
39
|
+
"@xterm/xterm": "^6.0.0",
|
|
40
|
+
"ai": "^6.0.86",
|
|
41
|
+
"bun-plugin-tailwind": "^0.1.2",
|
|
42
|
+
"class-variance-authority": "^0.7.1",
|
|
43
|
+
"clsx": "^2.1.1",
|
|
44
|
+
"effect": "^3.19.17",
|
|
45
|
+
"ghostty-web": "^0.4.0",
|
|
46
|
+
"lucide-react": "^0.564.0",
|
|
47
|
+
"react": "^19",
|
|
48
|
+
"react-dom": "^19",
|
|
49
|
+
"restty": "^0.1.28",
|
|
50
|
+
"sonner": "^2.0.7",
|
|
51
|
+
"streamdown": "^2.2.0",
|
|
52
|
+
"tailwind-merge": "^3.4.0",
|
|
53
|
+
"tailwindcss": "^4.1.11",
|
|
54
|
+
"tw-animate-css": "^1.4.0",
|
|
55
|
+
"zustand": "^5.0.11"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@eslint/css": "^0.14.1",
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
60
|
+
"@eslint/json": "^1.0.1",
|
|
61
|
+
"@eslint/markdown": "^7.5.1",
|
|
62
|
+
"@types/bun": "^1.3.9",
|
|
63
|
+
"@types/react": "^19",
|
|
64
|
+
"@types/react-dom": "^19",
|
|
65
|
+
"eslint": "^10.0.0",
|
|
66
|
+
"eslint-config-flat-gitignore": "^2.1.0",
|
|
67
|
+
"eslint-plugin-react": "^7.37.5",
|
|
68
|
+
"globals": "^17.3.0",
|
|
69
|
+
"jiti": "^2.6.1",
|
|
70
|
+
"prettier": "^3.8.1",
|
|
71
|
+
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
72
|
+
"typescript-eslint": "^8.55.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/server/build.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
4
|
+
console.log(`
|
|
5
|
+
šļø Bun Build Script
|
|
6
|
+
|
|
7
|
+
Usage: bun run server/build.ts [options]
|
|
8
|
+
|
|
9
|
+
Common Options:
|
|
10
|
+
--outdir <path> Output directory (default: "dist")
|
|
11
|
+
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
|
|
12
|
+
--sourcemap <type> Sourcemap type: none|linked|inline|external
|
|
13
|
+
--target <target> Build target: browser|bun|node
|
|
14
|
+
--format <format> Output format: esm|cjs|iife
|
|
15
|
+
--splitting Enable code splitting
|
|
16
|
+
--packages <type> Package handling: bundle|external
|
|
17
|
+
--public-path <path> Public path for assets
|
|
18
|
+
--env <mode> Environment handling: inline|disable|prefix*
|
|
19
|
+
--conditions <list> Package.json export conditions (comma separated)
|
|
20
|
+
--external <list> External packages (comma separated)
|
|
21
|
+
--banner <text> Add banner text to output
|
|
22
|
+
--footer <text> Add footer text to output
|
|
23
|
+
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
|
|
24
|
+
--help, -h Show this help message
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
bun run server/build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
|
|
28
|
+
`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const toCamelCase = (str: string): string =>
|
|
33
|
+
str.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
34
|
+
|
|
35
|
+
const parseValue = (value: string): string | boolean | number | string[] => {
|
|
36
|
+
if (value === "true") return true;
|
|
37
|
+
if (value === "false") return false;
|
|
38
|
+
|
|
39
|
+
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
|
40
|
+
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
|
|
41
|
+
|
|
42
|
+
if (value.includes(",")) return value.split(",").map((v) => v.trim());
|
|
43
|
+
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function parseArgs(): Partial<Bun.BuildConfig> {
|
|
48
|
+
const config: Record<string, unknown> = {};
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg === undefined) continue;
|
|
54
|
+
if (!arg.startsWith("--")) continue;
|
|
55
|
+
|
|
56
|
+
if (arg.startsWith("--no-")) {
|
|
57
|
+
const key = toCamelCase(arg.slice(5));
|
|
58
|
+
config[key] = false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
!arg.includes("=") &&
|
|
64
|
+
(i === args.length - 1 || args[i + 1]?.startsWith("--"))
|
|
65
|
+
) {
|
|
66
|
+
const key = toCamelCase(arg.slice(2));
|
|
67
|
+
config[key] = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let key: string;
|
|
72
|
+
let value: string;
|
|
73
|
+
|
|
74
|
+
if (arg.includes("=")) {
|
|
75
|
+
[key, value] = arg.slice(2).split("=", 2) as [string, string];
|
|
76
|
+
} else {
|
|
77
|
+
key = arg.slice(2);
|
|
78
|
+
value = args[++i] ?? "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
key = toCamelCase(key);
|
|
82
|
+
|
|
83
|
+
if (key.includes(".")) {
|
|
84
|
+
const parts = key.split(".");
|
|
85
|
+
if (parts.length > 2) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`Warning: Deeply nested option "${key}" is not supported. Only single-level nesting (e.g., --minify.whitespace) is allowed.`,
|
|
88
|
+
);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const parentKey = parts[0]!;
|
|
92
|
+
const childKey = parts[1]!;
|
|
93
|
+
const existing = config[parentKey];
|
|
94
|
+
if (
|
|
95
|
+
typeof existing !== "object" ||
|
|
96
|
+
existing === null ||
|
|
97
|
+
Array.isArray(existing)
|
|
98
|
+
) {
|
|
99
|
+
config[parentKey] = {};
|
|
100
|
+
}
|
|
101
|
+
(config[parentKey] as Record<string, unknown>)[childKey] =
|
|
102
|
+
parseValue(value);
|
|
103
|
+
} else {
|
|
104
|
+
config[key] = parseValue(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return config as Partial<Bun.BuildConfig>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const formatFileSize = (bytes: number): string => {
|
|
112
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
113
|
+
let size = bytes;
|
|
114
|
+
let unitIndex = 0;
|
|
115
|
+
|
|
116
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
117
|
+
size /= 1024;
|
|
118
|
+
unitIndex++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
console.log("\nš Starting build process...\n");
|
|
125
|
+
|
|
126
|
+
const cliConfig = parseArgs();
|
|
127
|
+
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
|
|
128
|
+
|
|
129
|
+
if (existsSync(outdir)) {
|
|
130
|
+
console.log(`šļø Cleaning previous build at ${outdir}`);
|
|
131
|
+
await rm(outdir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const start = performance.now();
|
|
135
|
+
|
|
136
|
+
const entrypoints = [...new Bun.Glob("**.html").scanSync("ui")]
|
|
137
|
+
.map((a) => path.resolve("ui", a))
|
|
138
|
+
.filter((dir) => !dir.includes("node_modules"));
|
|
139
|
+
console.log(
|
|
140
|
+
`š Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const result = await Bun.build({
|
|
144
|
+
entrypoints,
|
|
145
|
+
outdir,
|
|
146
|
+
plugins: [plugin],
|
|
147
|
+
minify: true,
|
|
148
|
+
target: "browser",
|
|
149
|
+
sourcemap: "linked",
|
|
150
|
+
define: {
|
|
151
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
152
|
+
},
|
|
153
|
+
...cliConfig,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const end = performance.now();
|
|
157
|
+
|
|
158
|
+
const outputTable = result.outputs.map((output) => ({
|
|
159
|
+
File: path.relative(process.cwd(), output.path),
|
|
160
|
+
Type: output.kind,
|
|
161
|
+
Size: formatFileSize(output.size),
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
console.table(outputTable);
|
|
165
|
+
const buildTime = (end - start).toFixed(2);
|
|
166
|
+
|
|
167
|
+
console.log(`\nā
Build completed in ${buildTime}ms\n`);
|
|
168
|
+
|
|
169
|
+
import plugin from "bun-plugin-tailwind";
|
|
170
|
+
import { existsSync } from "fs";
|
|
171
|
+
import { rm } from "fs/promises";
|
|
172
|
+
import path from "path";
|