import { randomBytes } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; const force = process.argv.slice(2).includes("--force"); if (process.argv.slice(2).some((value) => value !== "--force")) { throw new Error("Usage: configure-inspector-course-chat [--force]"); } const credentialsDirectory = process.env.THOUGHTSTREAM_CREDENTIALS_DIR ?? path.join(os.homedir(), ".config", "thoughtstream", "credentials"); const destination = path.join(credentialsDirectory, "inspector-course-chat.env"); await refuseSymlink(credentialsDirectory); await fs.mkdir(credentialsDirectory, { recursive: true, mode: 0o700 }); await fs.chmod(credentialsDirectory, 0o700); if (!force) { const exists = await fs.stat(destination).then(() => true, (error: NodeJS.ErrnoException) => error.code === "ENOENT" ? false : Promise.reject(error)); if (exists) throw new Error(`Course chat capability already exists at ${destination}; use --force only for deliberate rotation`); } const temporary = `${destination}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`; await fs.writeFile( temporary, `THOUGHTSTREAM_COURSE_CHAT_CAPABILITY_B64=${randomBytes(32).toString("base64")}\n`, { mode: 0o600, flag: "wx" }, ); await fs.rename(temporary, destination); await fs.chmod(destination, 0o600); process.stdout.write(`Wrote one owner-only course chat capability to ${destination}.\n`); process.stdout.write("Both inspector services must load the same file. Generation does not restart or activate either service.\n"); async function refuseSymlink(target: string): Promise { const stat = await fs.lstat(target).catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return undefined; throw error; }); if (stat?.isSymbolicLink()) throw new Error(`Refusing symlinked directory: ${target}`); }