g1t/apps/web/public/install/setup.mjs
| 1 | #!/usr/bin/env node |
| 2 | // Sets up g1t for Claude Code on this machine: signs in through the |
| 3 | // browser (nothing to paste), keeps the token in ~/.g1t, and adds g1t's |
| 4 | // hook to ~/.claude/settings.json so sessions are recorded onto the pull |
| 5 | // requests they belong to. Run by https://g1t.sh/install/claude.sh. |
| 6 | |
| 7 | import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; |
| 8 | import { homedir } from "node:os"; |
| 9 | import { join } from "node:path"; |
| 10 | |
| 11 | const API = process.env.G1T_API ?? "https://api.g1t.sh"; |
| 12 | const HOME = join(homedir(), ".g1t"); |
| 13 | const CONFIG = join(HOME, "config.json"); |
| 14 | const SETTINGS = join(homedir(), ".claude", "settings.json"); |
| 15 | const HOOK = `node "${join(HOME, "hook.mjs")}"`; |
| 16 | |
| 17 | const sleep = (ms) => new Promise((done) => setTimeout(done, ms)); |
| 18 | |
| 19 | async function signIn() { |
| 20 | if (existsSync(CONFIG)) { |
| 21 | try { |
| 22 | const config = JSON.parse(readFileSync(CONFIG, "utf8")); |
| 23 | const me = await fetch(`${API}/user`, { headers: { authorization: `Bearer ${config.token}` } }); |
| 24 | if (me.ok) { |
| 25 | console.log(`Already signed in as ${(await me.json()).username}.`); |
| 26 | return; |
| 27 | } |
| 28 | } catch {} |
| 29 | } |
| 30 | const started = await ( |
| 31 | await fetch(`${API}/device/code`, { |
| 32 | method: "POST", |
| 33 | headers: { "content-type": "application/json" }, |
| 34 | body: JSON.stringify({ client_name: "Claude Code on this machine" }), |
| 35 | }) |
| 36 | ).json(); |
| 37 | console.log(`\nOpen ${started.verification_uri_complete}`); |
| 38 | console.log(`and check that the code there is ${started.user_code}.\n`); |
| 39 | const deadline = Date.now() + started.expires_in * 1000; |
| 40 | while (Date.now() < deadline) { |
| 41 | await sleep(Math.max(started.interval, 2) * 1000); |
| 42 | const claim = await ( |
| 43 | await fetch(`${API}/device/token`, { |
| 44 | method: "POST", |
| 45 | headers: { "content-type": "application/json" }, |
| 46 | body: JSON.stringify({ device_code: started.device_code }), |
| 47 | }) |
| 48 | ).json(); |
| 49 | if (claim.status === "approved") { |
| 50 | mkdirSync(HOME, { recursive: true }); |
| 51 | writeFileSync(CONFIG, JSON.stringify({ api: API, token: claim.token }, null, 2)); |
| 52 | try { |
| 53 | chmodSync(CONFIG, 0o600); |
| 54 | } catch {} |
| 55 | console.log(`Signed in as ${claim.username}.`); |
| 56 | return; |
| 57 | } |
| 58 | if (claim.status === "denied" || claim.status === "expired") { |
| 59 | throw new Error(`Sign-in was ${claim.status}.`); |
| 60 | } |
| 61 | } |
| 62 | throw new Error("Sign-in timed out."); |
| 63 | } |
| 64 | |
| 65 | /** Adds g1t's hook to Claude Code's settings, keeping everything else. */ |
| 66 | function addHooks() { |
| 67 | let settings = {}; |
| 68 | try { |
| 69 | settings = JSON.parse(readFileSync(SETTINGS, "utf8")); |
| 70 | } catch {} |
| 71 | settings.hooks ??= {}; |
| 72 | for (const [event, matcher] of [ |
| 73 | ["UserPromptSubmit", undefined], |
| 74 | ["PostToolUse", "*"], |
| 75 | ["Stop", undefined], |
| 76 | ]) { |
| 77 | const groups = (settings.hooks[event] ??= []); |
| 78 | const present = groups.some((group) => group.hooks?.some((hook) => hook.command === HOOK)); |
| 79 | if (!present) { |
| 80 | groups.push({ ...(matcher ? { matcher } : {}), hooks: [{ type: "command", command: HOOK, timeout: 10 }] }); |
| 81 | } |
| 82 | } |
| 83 | mkdirSync(join(homedir(), ".claude"), { recursive: true }); |
| 84 | writeFileSync(SETTINGS, JSON.stringify(settings, null, 2)); |
| 85 | console.log(`Added g1t's hook to ${SETTINGS}.`); |
| 86 | } |
| 87 | |
| 88 | try { |
| 89 | await signIn(); |
| 90 | addHooks(); |
| 91 | console.log("\nDone. Sessions in a g1t pull request's working copy are now recorded onto it."); |
| 92 | console.log("To give Claude Code g1t's tools as well, run:"); |
| 93 | console.log(" claude mcp add --transport http g1t https://mcp.g1t.sh\n"); |
| 94 | } catch (error) { |
| 95 | console.error(`g1t: ${error.message}`); |
| 96 | process.exit(1); |
| 97 | } |