Ctrl K
ComunidadGuías…

bin/scripts/copy-seeded-connectors.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Prepack step for the published `cotal-ai` binary: materialize each built-in connector's PUBLISHED
// file set into `bin/seeded-connectors/<name>/`, so the shipped binary carries a stable, durable
// payload the first-run reconcile can `ext add --install-links` from (see implementations/cli/src/
// seed/paths.ts `shippedSourceDir` — the published branch resolves `<cotal-ai>/seeded-connectors/
// <name>`). In a source checkout the reconcile resolves the live `extensions/` dirs instead, so this
// only matters for a real publish.
//
// `npm pack` is used per connector because it honors each package's own `files`/`.npmignore` exactly
// (dotfiles like claude's `.claude-plugin`, hermes' `plugin/**` globs), producing the same bytes a
// standalone publish of that connector would. The connectors must be BUILT first (their `dist/`); we
// fail loud rather than silently ship an empty payload.
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { SEEDED_EXTENSIONS, SEEDED_SHARED_DIR, SEEDED_SHARED_FILES } from "@cotal-ai/workspace";

const binRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = join(binRoot, "..");
const outRoot = join(binRoot, "seeded-connectors");

// The generation the reconcile stamps is this binary's version; every bundled extension must carry
// the SAME version (the `fixed` changeset group versions them in lockstep). Assert it at pack time so
// a skewed payload can never be published inside the umbrella and then installed as the current
// generation (see verifyInstalled in seed/reconcile.ts for the install-time half of this guard).
const umbrellaVersion = JSON.parse(readFileSync(join(binRoot, "package.json"), "utf8")).version;

rmSync(outRoot, { recursive: true, force: true });
mkdirSync(outRoot, { recursive: true });

// Windows has no `npm` executable to spawn without a shell, only `npm.cmd`; run the npm that ships
// next to node.exe through this node instead, as the seat native build does for its node-gyp.
const npm = process.platform === "win32"
  ? (args) => [process.execPath, [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), ...args]]
  : (args) => ["npm", args];

for (const [name, { pkg, srcDir: srcSubdir }] of Object.entries(SEEDED_EXTENSIONS)) {
  const srcDir = join(repoRoot, srcSubdir);
  const meta = JSON.parse(readFileSync(join(srcDir, "package.json"), "utf8"));
  if (meta.name !== pkg) {
    throw new Error(`seeded extension "${name}": package.json name "${meta.name}" != expected "${pkg}"`);
  }
  if (meta.version !== umbrellaVersion) {
    throw new Error(`seeded extension ${pkg} is version "${meta.version}" but cotal-ai is "${umbrellaVersion}" - the fixed changeset group must version every bundled extension in lockstep with the umbrella`);
  }
  if (!existsSync(join(srcDir, "dist"))) {
    throw new Error(`first-party extension ${pkg} is not built (no dist at ${srcDir}) - run \`pnpm build\` before packing cotal-ai`);
  }
  // The dashboard's page is its own build step (vite) beside the server's tsc, so a dist holding the
  // server alone is possible, and an install of it would answer 503 on its own front page.
  if (pkg === "@cotal-ai/web" && !existsSync(join(srcDir, "dist", "web", "index.html"))) {
    throw new Error(`first-party extension ${pkg} has no client build (no dist/web/index.html at ${srcDir}) - run \`pnpm --filter @cotal-ai/web build\` before packing cotal-ai`);
  }
  const tmp = mkdtempSync(join(tmpdir(), "cotal-seed-"));
  try {
    // --ignore-scripts: dist is already built above; don't re-run the connector's own prepack.
    const stdout = execFileSync(...npm(["pack", "--ignore-scripts", "--silent", "--pack-destination", tmp, srcDir]), { encoding: "utf8" });
    const tgz = stdout.trim().split("\n").filter(Boolean).pop();
    if (!tgz) throw new Error(`npm pack produced no tarball for ${pkg}`);
    const dest = join(outRoot, name);
    mkdirSync(dest, { recursive: true });
    // The tarball's top-level dir is always `package/`; strip it so files land directly under <name>.
    execFileSync("tar", ["xzf", join(tmp, tgz), "-C", dest, "--strip-components=1"]);
    console.log(`seeded-connectors/${name}  <-  ${pkg}`);
  } finally {
    rmSync(tmp, { recursive: true, force: true });
  }
}

// Every connector bundle reads the same docs corpus and index from beside itself, so the payloads above
// hold identical copies of each. Ship one copy of each file and the paths it was removed from; the seed
// store puts a copy back at each path (implementations/cli/src/seed/shared.ts).
if (Object.hasOwn(SEEDED_EXTENSIONS, SEEDED_SHARED_DIR)) throw new Error(`"${SEEDED_SHARED_DIR}" is a seeded extension name and the shared-files directory`);
const sharedRoot = join(outRoot, SEEDED_SHARED_DIR);
mkdirSync(sharedRoot);
const files = {};
const placements = {};
for (const name of Object.keys(SEEDED_EXTENSIONS)) {
  const payload = join(outRoot, name);
  for (const rel of readdirSync(payload, { recursive: true }).sort()) {
    const path = join(payload, rel);
    const file = basename(rel);
    if (!SEEDED_SHARED_FILES.includes(file) || !statSync(path).isFile()) continue;
    const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
    if (files[file] === undefined) {
      files[file] = digest;
      copyFileSync(path, join(sharedRoot, file));
    } else if (files[file] !== digest) {
      throw new Error(`seeded-connectors/${name}/${rel} differs from the other copies of ${file} - every connector must ship the docs data built with this release`);
    }
    rmSync(path);
    (placements[name] ??= []).push(rel.split(sep).join("/"));
  }
}
writeFileSync(join(sharedRoot, "placements.json"), `${JSON.stringify({ v: 1, files, placements }, null, 2)}\n`);
const placed = Object.values(placements).flat().length;
console.log(`seeded-connectors/${SEEDED_SHARED_DIR}  <-  ${Object.keys(files).length} shared files for ${placed} placements`);