108 lines
3.1 KiB
JavaScript
108 lines
3.1 KiB
JavaScript
/**
|
|
* Shared helpers for the grok-glance hook scripts and CLI.
|
|
*
|
|
* Deliberately dependency-free and stdlib-only: these run on the critical path of
|
|
* every Grok Build tool call, so they must start fast and never wedge a session.
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import os from "node:os";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export const PLUGIN_ROOT = path.resolve(fileURLToPath(import.meta.url), "../..");
|
|
|
|
export const DEFAULT_PORT = 8791;
|
|
|
|
/**
|
|
* State lives in one fixed place so that hooks (which get GROK_PLUGIN_DATA) and the
|
|
* CLI (which does not) always agree on where config, credentials and events are.
|
|
*/
|
|
export function glanceHome() {
|
|
if (process.env.GLANCE_HOME) return path.resolve(process.env.GLANCE_HOME);
|
|
return path.join(os.homedir(), ".grok", "glance");
|
|
}
|
|
|
|
export function readConfig() {
|
|
const file = path.join(glanceHome(), "config.json");
|
|
let raw = {};
|
|
try {
|
|
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
} catch {
|
|
// No config yet, or unreadable: defaults are always usable.
|
|
}
|
|
const port = Number(process.env.GLANCE_PORT ?? raw.port ?? DEFAULT_PORT);
|
|
return {
|
|
...raw,
|
|
port: Number.isFinite(port) ? port : DEFAULT_PORT,
|
|
host: raw.host ?? "127.0.0.1",
|
|
};
|
|
}
|
|
|
|
export function baseUrl(cfg = readConfig()) {
|
|
return `http://127.0.0.1:${cfg.port}`;
|
|
}
|
|
|
|
/** Read the hook payload that Grok Build writes to stdin. Returns {} if there is none. */
|
|
export async function readStdinJson() {
|
|
if (process.stdin.isTTY) return {};
|
|
const chunks = [];
|
|
try {
|
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
} catch {
|
|
return {};
|
|
}
|
|
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
if (!text) return {};
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Grok Build also passes the event in the environment. We merge it in so a payload that
|
|
* is missing fields (or absent entirely) still produces a usable event.
|
|
*/
|
|
export function envEnvelope(payload) {
|
|
return {
|
|
hookEventName: payload.hookEventName ?? process.env.GROK_HOOK_EVENT ?? "Unknown",
|
|
sessionId: payload.sessionId ?? process.env.GROK_SESSION_ID ?? "unknown",
|
|
workspaceRoot:
|
|
payload.workspaceRoot ?? process.env.GROK_WORKSPACE_ROOT ?? payload.cwd ?? process.cwd(),
|
|
cwd: payload.cwd ?? process.cwd(),
|
|
hookName: process.env.GROK_HOOK_NAME ?? undefined,
|
|
...payload,
|
|
};
|
|
}
|
|
|
|
export async function postJson(url, body, timeoutMs) {
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
signal: AbortSignal.timeout(timeoutMs),
|
|
});
|
|
const text = await res.text();
|
|
if (!text) return { status: res.status, data: null };
|
|
try {
|
|
return { status: res.status, data: JSON.parse(text) };
|
|
} catch {
|
|
return { status: res.status, data: null };
|
|
}
|
|
}
|
|
|
|
export async function isDaemonUp(cfg = readConfig(), timeoutMs = 400) {
|
|
try {
|
|
const res = await fetch(`${baseUrl(cfg)}/healthz`, {
|
|
signal: AbortSignal.timeout(timeoutMs),
|
|
});
|
|
return res.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|