repopilot 0.11.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const test = require("node:test");

const launcher = path.join(__dirname, "..", "bin", "repopilot.js");
const launcherModule = require("../bin/repopilot.js");
const npmPackages = require("../scripts/build-npm-platform-packages.js");

function checksumFile(file) {
  const checksum = crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
  fs.writeFileSync(`${file}.sha256`, `${checksum}  ${path.basename(file)}\n`);
  return checksum;
}

function run(command, args, options = {}) {
  const result = spawnSync(command, args, { encoding: "utf8", ...options });
  assert.equal(result.status, 0, `${command} ${args.join(" ")}\n${result.stderr}`);
  return result;
}

function writeReleaseArchive(dist, version, target) {
  const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), "repopilot-archive-"));
  const binary = path.join(sourceDir, target.binary);
  fs.writeFileSync(binary, `binary for ${target.target}`);
  fs.chmodSync(binary, 0o755);

  const archive = path.join(dist, npmPackages.archiveName(version, target));
  if (target.archiveExt === "zip") {
    run("zip", ["-j", archive, binary], { stdio: "pipe" });
  } else {
    run("tar", ["-czf", archive, "-C", sourceDir, target.binary], { stdio: "pipe" });
  }

  fs.rmSync(sourceDir, { recursive: true, force: true });
  return checksumFile(archive);
}

test("maps supported platforms to optional native packages", () => {
  assert.equal(launcherModule.platformPackage("darwin", "arm64").packageName, "@repopilot/darwin-arm64");
  assert.equal(launcherModule.platformPackage("darwin", "x64").packageName, "@repopilot/darwin-x64");
  assert.equal(launcherModule.platformPackage("linux", "x64", "glibc").packageName, "@repopilot/linux-x64-gnu");
  assert.equal(launcherModule.platformPackage("linux", "arm64", "glibc").packageName, "@repopilot/linux-arm64-gnu");
  assert.equal(launcherModule.platformPackage("win32", "x64").packageName, "@repopilot/win32-x64-msvc");
  assert.throws(() => launcherModule.platformPackage("linux", "x64", "musl"), /Unsupported npm platform/);
});

test("builds release asset names", () => {
  const macosArm = npmPackages.TARGETS.find((target) => target.target === "aarch64-apple-darwin");
  const windowsX64 = npmPackages.TARGETS.find((target) => target.target === "x86_64-pc-windows-msvc");

  assert.equal(
    npmPackages.archiveName("0.6.0", macosArm),
    "repopilot-v0.6.0-aarch64-apple-darwin.tar.gz"
  );
  assert.equal(
    npmPackages.archiveName("0.6.0", windowsX64),
    "repopilot-v0.6.0-x86_64-pc-windows-msvc.zip"
  );
});

test("verifies sha256 checksum text", () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "repopilot-test-"));
  const file = path.join(tmp, "archive.tar.gz");
  fs.writeFileSync(file, "archive");
  const hash = crypto.createHash("sha256").update("archive").digest("hex");

  assert.doesNotThrow(() => npmPackages.verifyChecksum(file, `${hash}  archive.tar.gz\n`));
  assert.throws(() => npmPackages.verifyChecksum(file, `${"0".repeat(64)}  archive.tar.gz\n`), /Checksum mismatch/);

  fs.rmSync(tmp, { recursive: true, force: true });
});

test("resolves binary path from optional package", () => {
  const resolved = launcherModule.resolveBinaryPath({
    env: {},
    platform: "darwin",
    arch: "arm64",
    resolver: (request) => {
      assert.equal(request, "@repopilot/darwin-arm64/package.json");
      return "/tmp/project/node_modules/@repopilot/darwin-arm64/package.json";
    },
  });

  assert.equal(resolved, "/tmp/project/node_modules/@repopilot/darwin-arm64/bin/repopilot");
});

test("launcher reports a missing binary clearly", () => {
  const result = spawnSync(process.execPath, [launcher], {
    env: {
      ...process.env,
      REPOPILOT_BINARY_PATH: path.join(os.tmpdir(), "missing-repopilot-binary"),
    },
    encoding: "utf8",
  });

  assert.equal(result.status, 1);
  assert.match(result.stderr, /RepoPilot binary was not found/);
  assert.match(result.stderr, /REPOPILOT_BINARY_PATH/);
});

test("launcher reports missing optional package clearly", () => {
  const missingModule = new Error("Cannot find module");
  missingModule.code = "MODULE_NOT_FOUND";

  assert.throws(
    () =>
      launcherModule.resolveBinaryPath({
        env: {},
        platform: "darwin",
        arch: "arm64",
        resolver: () => {
          throw missingModule;
        },
      }),
    launcherModule.MissingOptionalPackageError
  );

  const message = launcherModule.formatResolveError(
    new launcherModule.MissingOptionalPackageError("@repopilot/darwin-arm64", missingModule)
  );
  assert.match(message, /optional native package was not installed/);
  assert.match(message, /@repopilot\/darwin-arm64/);
});

test("launcher uses REPOPILOT_BINARY_PATH override", () => {
  const result = spawnSync(process.execPath, [launcher, "--version"], {
    env: {
      ...process.env,
      REPOPILOT_BINARY_PATH: process.execPath,
    },
    encoding: "utf8",
  });

  assert.equal(result.status, 0);
  assert.match(result.stdout, /^v?\d+\./);
});

test("builds platform npm packages from release archives", () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "repopilot-npm-packages-"));
  const dist = path.join(tmp, "dist");
  const out = path.join(tmp, "out");
  const version = "0.0.0-test.0";
  fs.mkdirSync(dist, { recursive: true });

  const checksums = new Map();
  for (const target of npmPackages.TARGETS) {
    checksums.set(target.target, writeReleaseArchive(dist, version, target));
  }

  const packages = npmPackages.buildPlatformPackages({ dist, out, version });
  assert.equal(packages.length, npmPackages.TARGETS.length);

  const linuxX64 = packages.find((pkg) => pkg.packageName === "@repopilot/linux-x64-gnu");
  const manifest = JSON.parse(fs.readFileSync(path.join(linuxX64.directory, "package.json"), "utf8"));
  const metadata = JSON.parse(fs.readFileSync(path.join(linuxX64.directory, "repopilot-native.json"), "utf8"));

  assert.equal(manifest.name, "@repopilot/linux-x64-gnu");
  assert.deepEqual(manifest.os, ["linux"]);
  assert.deepEqual(manifest.cpu, ["x64"]);
  assert.deepEqual(manifest.libc, ["glibc"]);
  assert.equal(manifest.scripts, undefined);
  assert.equal(metadata.sourceArchiveSha256, checksums.get("x86_64-unknown-linux-gnu"));
  assert.equal(fs.existsSync(path.join(linuxX64.directory, "bin", "repopilot")), true);

  const pack = run("npm", ["pack", "--dry-run", "--json"], {
    cwd: linuxX64.directory,
    stdio: "pipe",
  });
  const files = JSON.parse(pack.stdout)[0].files.map((file) => file.path).sort();
  assert.deepEqual(files, ["README.md", "bin/repopilot", "package.json", "repopilot-native.json"]);

  fs.rmSync(tmp, { recursive: true, force: true });
});

test("root npm package excludes installer scripts", () => {
  const result = run("npm", ["pack", "--dry-run", "--json"], {
    cwd: path.join(__dirname, ".."),
    stdio: "pipe",
  });
  const files = JSON.parse(result.stdout)[0].files.map((file) => file.path);

  assert.equal(files.some((file) => file.startsWith("scripts/")), false);
  assert.equal(files.includes("scripts/install.js"), false);
});