benchworks 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Benchworks' runner. `benchworks connect --code XXXX-XXXX` pairs this Mac with your Benchworks account, and keeps it
4
+ * connected while this Terminal window is open. It checks what the Assistant needs (Claude Code, its login, git), finds
5
+ * the git repositories in the usual folders, and reports both to benchworks.dev every few seconds. It sends folder names
6
+ * and versions, never the contents of a file. Run it again without a code and it reconnects as the same Mac.
7
+ */
8
+ import { execFile } from "node:child_process";
9
+ import { chmod, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
10
+ import { homedir, hostname, platform } from "node:os";
11
+ import { join } from "node:path";
12
+ import { promisify } from "node:util";
13
+
14
+ const VERSION = "0.1.1";
15
+ const API = (process.env.BENCHWORKS_URL ?? "https://benchworks.dev").replace(/\/+$/, "");
16
+ const HOME = homedir();
17
+ const STATE = join(HOME, ".benchworks", "runner.json");
18
+ const EVERY_MS = 5_000;
19
+ const REPOS_EVERY_MS = 60_000;
20
+
21
+ const run = promisify(execFile);
22
+ const say = (line = "") => process.stdout.write(`${line}\n`);
23
+ const dim = (s) => (process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s);
24
+ const ok = (s) => (process.stdout.isTTY ? `\x1b[32m${s}\x1b[0m` : s);
25
+ const warn = (s) => (process.stdout.isTTY ? `\x1b[33m${s}\x1b[0m` : s);
26
+
27
+ async function out(cmd, args, cwd) {
28
+ try {
29
+ const { stdout } = await run(cmd, args, { cwd, timeout: 8_000, env: process.env });
30
+ return stdout.trim();
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ const tilde = (p) => (p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p);
37
+
38
+ /* ---------------------------------------------------------------- what the Assistant needs */
39
+
40
+ async function checks() {
41
+ const claudeOut = await out("claude", ["--version"]);
42
+ const claude = claudeOut ? { ok: true, detail: claudeOut.split(/\s+/)[0] ?? claudeOut } : { ok: false, detail: "" };
43
+ let login = { ok: false, detail: "" };
44
+ if (claude.ok) {
45
+ // who Claude Code is signed in as, from its own settings; only the address is read
46
+ try {
47
+ const settings = JSON.parse(await readFile(join(HOME, ".claude.json"), "utf8"));
48
+ const email = settings?.oauthAccount?.emailAddress;
49
+ if (typeof email === "string" && email) login = { ok: true, detail: email };
50
+ } catch {
51
+ /* no settings file: not signed in */
52
+ }
53
+ if (!login.ok && process.env.ANTHROPIC_API_KEY) login = { ok: true, detail: "with an API key" };
54
+ }
55
+ const gitOut = await out("git", ["--version"]);
56
+ const git = gitOut ? { ok: true, detail: gitOut.replace(/^git version\s*/, "").split(/\s+/)[0] } : { ok: false, detail: "" };
57
+ return { claude, login, git };
58
+ }
59
+
60
+ /* ---------------------------------------------------------------- the repositories on this Mac */
61
+
62
+ const SKIP = new Set(["Library", "Applications", "Movies", "Music", "Pictures", "Public", "Downloads", "node_modules"]);
63
+
64
+ async function dirs(path) {
65
+ try {
66
+ const entries = await readdir(path, { withFileTypes: true });
67
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP.has(e.name)).slice(0, 200).map((e) => join(path, e.name));
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+
73
+ const isRepo = (path) => stat(join(path, ".git")).then(() => true, () => false);
74
+
75
+ /** Git repositories in the home folder and one level down, and in ~/Documents/GitHub; newest work first. */
76
+ async function repos() {
77
+ const found = new Set();
78
+ for (const top of await dirs(HOME)) {
79
+ if (await isRepo(top)) found.add(top);
80
+ else for (const inner of await dirs(top)) if (await isRepo(inner)) found.add(inner);
81
+ }
82
+ for (const inner of await dirs(join(HOME, "Documents", "GitHub"))) if (await isRepo(inner)) found.add(inner);
83
+ const list = await Promise.all(
84
+ [...found].map(async (path) => ({
85
+ name: path.split("/").pop(),
86
+ path: tilde(path),
87
+ branch: (await out("git", ["rev-parse", "--abbrev-ref", "HEAD"], path)) ?? "",
88
+ at: (await out("git", ["log", "-1", "--format=%cI"], path)) || null,
89
+ })),
90
+ );
91
+ return list.sort((a, b) => (b.at ?? "").localeCompare(a.at ?? "")).slice(0, 20);
92
+ }
93
+
94
+ /* ---------------------------------------------------------------- talking to Benchworks */
95
+
96
+ async function machine() {
97
+ if (platform() === "darwin") {
98
+ const name = await out("scutil", ["--get", "ComputerName"]);
99
+ if (name) return name;
100
+ }
101
+ return hostname();
102
+ }
103
+
104
+ async function api(path, body, key) {
105
+ const res = await fetch(`${API}${path}`, {
106
+ method: "POST",
107
+ headers: { "content-type": "application/json", "user-agent": `benchworks-runner/${VERSION}`, ...(key ? { authorization: `Bearer ${key}` } : {}) },
108
+ body: JSON.stringify(body),
109
+ signal: AbortSignal.timeout(15_000),
110
+ });
111
+ const answer = await res.json().catch(() => ({}));
112
+ return { status: res.status, body: answer };
113
+ }
114
+
115
+ const WHY = {
116
+ no_code: "That code does not work. Make a new one on benchworks.dev and run the command again.",
117
+ used: "That code was used already. Make a new one on benchworks.dev and run the command again.",
118
+ expired: "That code ran out. Make a new one on benchworks.dev and run the command again.",
119
+ too_many_tries: "Too many codes that did not work. Wait ten minutes, then try a new one.",
120
+ };
121
+
122
+ async function readState() {
123
+ try {
124
+ return JSON.parse(await readFile(STATE, "utf8"));
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ async function saveState(state) {
131
+ await mkdir(join(HOME, ".benchworks"), { recursive: true, mode: 0o700 });
132
+ await writeFile(STATE, JSON.stringify(state, null, 2), { mode: 0o600 });
133
+ await chmod(STATE, 0o600);
134
+ }
135
+
136
+ /** The folder picker the page asked for: the Mac's own, through AppleScript. Null when it was cancelled. */
137
+ async function pickFolder() {
138
+ const path = await out("osascript", ["-e", 'POSIX path of (choose folder with prompt "Pick the folder Benchworks should work in")']);
139
+ return path ? tilde(path.replace(/\/$/, "")) : null;
140
+ }
141
+
142
+ function printChecks(c, before) {
143
+ const lines = [
144
+ ["claude", c.claude.ok ? `Claude Code ${c.claude.detail}` : "Claude Code is not on this Mac. Install it: npm install -g @anthropic-ai/claude-code"],
145
+ ["login", c.login.ok ? `Signed in to Claude Code as ${c.login.detail}` : c.claude.ok ? "Claude Code is not signed in. Run claude and sign in with /login" : null],
146
+ ["git", c.git.ok ? `git ${c.git.detail}` : "git is not on this Mac. Install Apple's command line tools: xcode-select --install"],
147
+ ];
148
+ for (const [id, line] of lines) {
149
+ if (!line) continue;
150
+ if (before && before[id]?.ok === c[id].ok && before[id]?.detail === c[id].detail) continue;
151
+ say(` ${c[id].ok ? ok("✓") : warn("✗")} ${line}`);
152
+ }
153
+ }
154
+
155
+ async function connect(code) {
156
+ say(`Benchworks runner ${VERSION}`);
157
+ if (platform() !== "darwin") say(warn("Benchworks runs on macOS for now; this may not work here."));
158
+ let found = await repos();
159
+ let foundAt = Date.now();
160
+ let c = await checks();
161
+ const report = () => ({ version: VERSION, platform: platform(), checks: c, repos: found });
162
+
163
+ let state = await readState();
164
+ if (code) {
165
+ const name = await machine();
166
+ const paired = await api("/api/runner/pair", { code, machine: name, report: report() }).catch(() => null);
167
+ if (!paired) return fail(`Cannot reach ${API}. Check the connection and run the command again.`);
168
+ if (paired.status !== 201) return fail(WHY[paired.body.error] ?? `Benchworks said no (${paired.body.error ?? paired.status}).`);
169
+ state = { url: API, runnerId: paired.body.runnerId, key: paired.body.token, email: paired.body.email, machine: name, again: paired.body.again };
170
+ await saveState(state);
171
+ say(`Connected ${name} to Benchworks as ${paired.body.email}.`);
172
+ } else if (!state?.key) {
173
+ return fail("This Mac is not connected yet. Copy the command with its code from benchworks.dev and run it here.");
174
+ } else {
175
+ say(`Reconnecting ${state.machine} as ${state.email}.`);
176
+ }
177
+ printChecks(c, null);
178
+ again = state.again ?? again;
179
+ say(dim(`Keep this window open while you work. Ctrl-C disconnects; to come back, run: ${again}`));
180
+
181
+ let away = false;
182
+ let picked;
183
+ for (;;) {
184
+ const before = c;
185
+ c = await checks();
186
+ printChecks(c, before);
187
+ if (Date.now() - foundAt > REPOS_EVERY_MS) {
188
+ found = await repos();
189
+ foundAt = Date.now();
190
+ }
191
+ const body = { report: report(), ...(picked !== undefined ? { picked } : {}) };
192
+ picked = undefined;
193
+ const answer = await api("/api/runner/report", body, state.key).catch(() => null);
194
+ if (!answer || answer.status >= 500) {
195
+ if (!away) say(warn(`Cannot reach ${API}. Trying again.`));
196
+ away = true;
197
+ } else if (answer.status === 401) {
198
+ return fail("Benchworks no longer knows this Mac. Make a new code on benchworks.dev and run the command with it.");
199
+ } else {
200
+ if (away) say("Back in touch with Benchworks.");
201
+ away = false;
202
+ if (answer.body.ask === "folder") {
203
+ say("Benchworks asked for a folder. The picker is open.");
204
+ const path = await pickFolder();
205
+ picked = path ?? "";
206
+ say(path ? ` Working in ${path}` : " No folder picked.");
207
+ continue;
208
+ }
209
+ }
210
+ await new Promise((r) => setTimeout(r, EVERY_MS));
211
+ }
212
+ }
213
+
214
+ function fail(line) {
215
+ say(warn(line));
216
+ process.exitCode = 1;
217
+ }
218
+
219
+ function usage() {
220
+ say("Usage: npx benchworks connect --code XXXX-XXXX");
221
+ say(" npx benchworks connect (again, on a Mac that is connected already)");
222
+ }
223
+
224
+ /** How this Mac comes back without a code; Benchworks says which command when it pairs. */
225
+ let again = "npx benchworks connect";
226
+
227
+ process.on("SIGINT", () => {
228
+ say(`\nDisconnected. To come back, run: ${again}`);
229
+ process.exit(0);
230
+ });
231
+
232
+ const [command, ...rest] = process.argv.slice(2);
233
+ const at = rest.indexOf("--code");
234
+ const code = at >= 0 ? rest[at + 1] : undefined;
235
+ if (command === "connect") {
236
+ if (rest.includes("--always-on")) say(dim("--always-on comes with the next version; for now, keep this window open."));
237
+ await connect(code?.toUpperCase());
238
+ } else if (command === "--version" || command === "-v") {
239
+ say(VERSION);
240
+ } else {
241
+ usage();
242
+ if (command && command !== "--help" && command !== "-h") process.exitCode = 1;
243
+ }
244
+
package/package.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "name": "benchworks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Connects your Mac to Benchworks, so the Assistant at benchworks.dev runs your own Claude Code with your logins and your files.",
5
+ "type": "module",
6
+ "bin": {
7
+ "benchworks": "bin/benchworks.mjs"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "os": [
16
+ "darwin"
17
+ ],
18
+ "homepage": "https://benchworks.dev",
19
+ "license": "UNLICENSED"
20
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.