Newer
Older
bugtrail / packages / extension / scripts / publish.mjs
/**
 * Publishes the overlay module (JS + CSS) to the server so installed
 * extensions hot-update their UI without reinstalling.
 *
 * Usage:
 *   LTT_EMAIL=... LTT_PASSWORD=... node scripts/publish.mjs
 *   LTT_TOKEN=... LTT_SERVER=https://bugtrail.example node scripts/publish.mjs
 *
 * LTT_SERVER defaults to http://localhost:8001. Requires a prior
 * `node scripts/build.mjs chrome` (and firefox for the sha check).
 */
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const require = createRequire(import.meta.url);
const root = dirname(fileURLToPath(import.meta.url));
const extDir = join(root, "..");
const version = require(join(extDir, "..", "..", "package.json")).version;

const server = (process.env.LTT_SERVER ?? "http://localhost:8001").replace(/\/+$/, "");

function readAsset(name) {
  const path = join(extDir, "dist", "chrome", "assets", name);
  if (!existsSync(path)) {
    console.error(`publish: missing ${path} — run \`node scripts/build.mjs chrome\` first`);
    process.exit(1);
  }
  const content = readFileSync(path);
  // chrome and firefox builds share the overlay module; a mismatch means the
  // targets were built from different sources — warn but keep going
  const firefoxPath = join(extDir, "dist", "firefox", "assets", name);
  if (existsSync(firefoxPath)) {
    const firefoxSha = createHash("sha256").update(readFileSync(firefoxPath)).digest("hex");
    const chromeSha = createHash("sha256").update(content).digest("hex");
    if (firefoxSha !== chromeSha) {
      console.warn(`publish: warning — ${name} differs between chrome and firefox builds`);
    }
  }
  return content;
}

const overlayJs = readAsset("overlay.js");
const overlayCss = readAsset("overlay.css");

async function main() {
  let token = process.env.LTT_TOKEN;
  if (!token) {
    const email = process.env.LTT_EMAIL;
    const password = process.env.LTT_PASSWORD;
    if (!email || !password) {
      console.error("publish: set LTT_TOKEN or LTT_EMAIL+LTT_PASSWORD (and optionally LTT_SERVER)");
      process.exit(1);
    }
    const loginResponse = await fetch(`${server}/api/auth/login`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "X-Client": "extension" },
      body: JSON.stringify({ email, password }),
    });
    if (!loginResponse.ok) {
      console.error(`publish: login failed (${loginResponse.status})`);
      process.exit(1);
    }
    token = (await loginResponse.json()).token;
  }

  const response = await fetch(`${server}/api/ext/publish`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      version,
      files: {
        "overlay.js": overlayJs.toString("base64"),
        "overlay.css": overlayCss.toString("base64"),
      },
    }),
  });
  if (!response.ok) {
    const detail = await response.json().catch(() => null);
    console.error(`publish failed (${response.status}):`, JSON.stringify(detail));
    process.exit(1);
  }
  const manifest = await response.json();
  console.log(`published overlay ${manifest.version} -> ${server}/api/ext/manifest`);
  console.log(JSON.stringify(manifest.assets, null, 2));
}

main().catch((error) => {
  console.error("publish failed:", error.message);
  process.exit(1);
});