@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 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
+ }
@@ -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";