flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/web/public/install/setup.mjs

97 lines3,578 bytesCodeBlame
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
7import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8import { homedir } from "node:os";
9import { join } from "node:path";
10
11const API = process.env.G1T_API ?? "https://api.g1t.sh";
12const HOME = join(homedir(), ".g1t");
13const CONFIG = join(HOME, "config.json");
14const SETTINGS = join(homedir(), ".claude", "settings.json");
15const HOOK = `node "${join(HOME, "hook.mjs")}"`;
16
17const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
18
19async 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. */
66function 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
88try {
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}