#!/usr/bin/env bun
/**
 * build-plugin.ts — materialize installable plugin trees from the human-edited
 * plugin/ source directory directly into the nested c4a-plugins/ repo
 * (remote: github.com/context4ai/context). c4a-plugins/ is .gitignore'd by
 * c4a; it is its own git repo, the single source of marketplace distribution.
 *
 * Source of truth:
 *   - plugin/commands/
 *   - plugin/skills/
 *   - plugin/.claude-plugin/plugin.json.template
 *   - plugin/.codex-plugin/plugin.json.template
 *   - plugin/.cursor-plugin/plugin.json.template
 *
 * Generated outputs under c4a-plugins/:
 *   - claude/   — Claude plugin root
 *   - codex/    — Codex plugin root
 *   - cursor/   — Cursor plugin root
 *   - skills/   — Vercel-style standalone skills (no plugin manifest)
 *
 * Plus three marketplace.json files at c4a-plugins/ root pointing at the
 * respective subdirs, so a single repo serves four install paths.
 */

import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgRoot = resolve(__dirname, "..");
const repoRoot = resolve(pkgRoot, "../..");
// Integration tests stage build into a temp dir; allow override so the staged
// run does not require a real c4a-plugins/ clone next to the staged scripts.
const PLUGINS_ROOT = process.env.C4A_PLUGINS_ROOT
  ? resolve(process.env.C4A_PLUGINS_ROOT)
  : resolve(repoRoot, "c4a-plugins");
const MARKETPLACE_NAME = "c4a";
const PLUGIN_NAME = "context";

interface CommandSource {
  slug: string;
  title: string;
  description: string;
  body: string;
}

const INTERNAL_PROCEDURES_DIR = "references/internal-procedures";

/**
 * Rewrite `packaged context:skill-*` references in a command body so agents
 * without a packaged-skill registry (Codex / Cursor / Vercel-style) get
 * direct file references they can read instead of an unactionable verb.
 *
 * Claude has a packaged-skill registry and consumes the original phrasing
 * verbatim, so this transformer is only applied to non-Claude builds.
 */
function rewritePackagedSkillReferences(body: string, pathFor: (skill: string) => string): string {
  let out = body;

  // Imperative: "Use/Invoke packaged `context:skill-X` [and follow its procedure]" → read the file
  // The optional trailer is consumed because "Read ... and follow it" already covers it.
  out = out.replace(
    /\b(?:Invoke|Use|invoke|use) packaged `context:(skill-[a-z-]+)`(?:\s+and follow its procedure)?/g,
    (_match, skill: string) => `Read \`${pathFor(skill)}\` in full and follow it`,
  );

  // Imperative: "Feed/Pass <stuff> to packaged `context:skill-X`" → pass to procedure file
  // Do not inject "do not reconstruct from memory" here; the surrounding sentence
  // already carries that warning in its own tail and double-stating it reads redundant.
  out = out.replace(
    /\b(Feed|Pass|feed|pass) ([^.`]*?) to packaged `context:(skill-[a-z-]+)`/g,
    (_match, verb: string, mid: string, skill: string) =>
      `${verb} ${mid} to the procedure in \`${pathFor(skill)}\` (read it in full first if you have not already)`,
  );

  // Imperative: "Apply packaged `context:skill-X` <rules>" → apply the procedure for those rules.
  out = out.replace(
    /\b(Apply|apply) packaged `context:(skill-[a-z-]+)`/g,
    (_match, verb: string, skill: string) => `${verb} the procedure in \`${pathFor(skill)}\` for`,
  );

  // "run `context:skill-X`" (no "packaged") → consult the procedure file
  out = out.replace(
    /\brun `context:(skill-[a-z-]+)`/g,
    (_match, skill: string) => `consult the procedure in \`${pathFor(skill)}\``,
  );

  // Anything left is a descriptive bare reference (e.g. "stop before invoking
  // `context:skill-X`"); substitute the file path so the agent can resolve it.
  // The wildcard form `context:skill-*` (with literal asterisk) stays untouched
  // because the [a-z-]+ class does not match `*`.
  out = out.replace(
    /`context:(skill-[a-z-]+)`/g,
    (_match, skill: string) => `\`${pathFor(skill)}\``,
  );

  return out;
}

function publicEntrySkillPath(skill: string): string {
  return `${INTERNAL_PROCEDURES_DIR}/${skill}.md`;
}

function cursorCommandSkillPath(skill: string): string {
  // Cursor command files live under `commands/`; bundled skills live under
  // sibling `skills/`. Use a plain relative path that any markdown reader can
  // resolve from the command file's location.
  return `../skills/${skill}/SKILL.md`;
}

function cursorCommandFileName(slug: string): string {
  return slug === "context" ? "context.md" : `context-${slug}.md`;
}

function rewriteClaudeSlashCommandsForCursor(body: string): string {
  return body
    .replace(/\/context:\*/g, "/context-*")
    .replace(/\/context:([a-z-]+)/g, (_match, slug: string) =>
      slug === "context" ? "/context" : `/context-${slug}`
    )
    .replace(/^## Your [Tt]ask$/gm, "## Workflow")
    .replace(/\bPackaged skill invoked by\b/g, "Internal procedure invoked by")
    .replace(/\bnot a user slash command\b/g, "not a user command");
}

/**
 * Scan a generated build root for residual packaged-skill references that
 * should have been rewritten in agent-facing entry files (Codex / Vercel
 * public skills, Cursor commands).
 *
 * Skips internal skill bodies — directories named `internal-procedures` (the
 * codex/vercel embed point) and any directory whose name starts with `skill-`
 * (the cursor-build internal skills tree, mirrored from `plugin/skills/`).
 * Cross-skill mentions inside those bodies are descriptive ("the next stage
 * handles X") and stay untouched.
 *
 * Throws on any leak so future commands or rewrite-pattern gaps fail the
 * build instead of silently shipping unactionable verbs.
 */
async function verifyNoPackagedSkillLeak(root: string, label: string): Promise<void> {
  const offenders: string[] = [];
  const stack: string[] = [root];
  while (stack.length > 0) {
    const current = stack.pop()!;
    const entries = await readdir(current, { withFileTypes: true });
    for (const entry of entries) {
      const path = join(current, entry.name);
      if (entry.isDirectory()) {
        if (entry.name === "internal-procedures" || entry.name.startsWith("skill-")) continue;
        stack.push(path);
        continue;
      }
      if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
      const content = await readFile(path, "utf8");
      const matches = content.match(/`context:skill-[a-z-]+`/g);
      if (matches && matches.length > 0) {
        offenders.push(`${path}: ${[...new Set(matches)].join(", ")}`);
      }
    }
  }
  if (offenders.length > 0) {
    throw new Error(
      `${label} build still contains unrewritten packaged skill references in agent-facing entry files — extend rewritePackagedSkillReferences() to cover the new phrasing:\n${offenders.join("\n")}`,
    );
  }
}

/**
 * Rewrite `${CLAUDE_PLUGIN_ROOT}/skills/<name>/...` cross-skill references
 * inside bundled internal procedure copies to plain relative paths that any
 * agent can resolve. The number of `..` segments depends on how deep the
 * file is below its build's "skills siblings root" (the directory under
 * which all `skill-*` siblings live).
 */
function rewriteClaudePluginRoot(content: string, levelsAboveSiblingsRoot: number): string {
  const upPrefix = "../".repeat(levelsAboveSiblingsRoot);
  return content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\/skills\//g, upPrefix);
}

function rewriteFlattenedProcedureReferences(content: string, skill: string): string {
  return content.replace(
    /(?<![\w./-])references\/([A-Za-z0-9._/-]+\.md)/g,
    `${skill}/references/$1`,
  );
}

function depthOfFileBelowSiblingsRoot(filePath: string, siblingsRoot: string): number {
  const rel = relative(siblingsRoot, dirname(filePath));
  if (rel === "" || rel === ".") return 0;
  return rel.split(sep).filter((seg: string) => seg.length > 0).length;
}

/**
 * Recursively copy a markdown tree, applying `rewriteClaudePluginRoot` to
 * every `.md` file based on its location below `siblingsRoot`. Non-markdown
 * files are copied verbatim. Empty subtrees (no files, no recursive content)
 * are skipped — the destination directory is created only when at least one
 * descendant is actually written, so accidental empty source dirs don't
 * propagate into build outputs.
 */
async function copyMarkdownTreeRewrittenForPublicEntry(
  src: string,
  dest: string,
  siblingsRoot: string,
): Promise<boolean> {
  const entries = await readdir(src, { withFileTypes: true });
  let wroteSomething = false;
  for (const entry of entries) {
    const srcPath = join(src, entry.name);
    const destPath = join(dest, entry.name);
    if (entry.isDirectory()) {
      const subtreeWrote = await copyMarkdownTreeRewrittenForPublicEntry(srcPath, destPath, siblingsRoot);
      if (subtreeWrote) wroteSomething = true;
      continue;
    }
    if (!entry.isFile()) continue;
    if (!wroteSomething) {
      await mkdir(dest, { recursive: true });
    }
    if (entry.name.endsWith(".md")) {
      const content = await readFile(srcPath, "utf8");
      const depth = depthOfFileBelowSiblingsRoot(destPath, siblingsRoot);
      await writeFile(destPath, rewriteClaudePluginRoot(content, depth), "utf8");
    } else {
      await cp(srcPath, destPath);
    }
    wroteSomething = true;
  }
  return wroteSomething;
}

/**
 * Scan a generated build root for residual `${CLAUDE_PLUGIN_ROOT}` tokens.
 * That token is Claude-specific and must not survive into Codex / Cursor /
 * Vercel-style builds — agents on those platforms cannot resolve it.
 */
async function verifyNoClaudePluginRootLeak(root: string, label: string): Promise<void> {
  const offenders: string[] = [];
  const stack: string[] = [root];
  while (stack.length > 0) {
    const current = stack.pop()!;
    const entries = await readdir(current, { withFileTypes: true });
    for (const entry of entries) {
      const path = join(current, entry.name);
      if (entry.isDirectory()) {
        stack.push(path);
        continue;
      }
      if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
      const content = await readFile(path, "utf8");
      if (content.includes("${CLAUDE_PLUGIN_ROOT}")) {
        offenders.push(path);
      }
    }
  }
  if (offenders.length > 0) {
    throw new Error(
      `${label} build contains \${CLAUDE_PLUGIN_ROOT} tokens — extend rewriteClaudePluginRoot() so non-Claude agents can resolve cross-skill paths:\n${offenders.join("\n")}`,
    );
  }
}

async function rewriteMarkdownFiles(root: string, rewrite: (content: string) => string): Promise<void> {
  const stack = [root];
  while (stack.length > 0) {
    const current = stack.pop()!;
    const entries = await readdir(current, { withFileTypes: true });
    for (const entry of entries) {
      const path = join(current, entry.name);
      if (entry.isDirectory()) {
        stack.push(path);
        continue;
      }
      if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
      const original = await readFile(path, "utf8");
      const next = rewrite(original);
      if (next !== original) await writeFile(path, next, "utf8");
    }
  }
}


function generatedNotice(kind: string): string {
  return [
    `# Generated ${kind}`,
    "",
    "This directory is generated by `bun run build:plugin`.",
    "Do not edit files here directly.",
    "Edit `packages/context-cli/plugin/` and rerun the build instead.",
    "",
  ].join("\n");
}

function parseFrontmatter(markdown: string, fileName: string): { frontmatter: string; body: string } {
  const match = markdown.match(/^---\n([\s\S]*?)\n---\n?/);
  if (!match) throw new Error(`${fileName} must start with YAML frontmatter`);
  return {
    frontmatter: match[1] ?? "",
    body: markdown.slice(match[0].length).trimStart(),
  };
}

function quotedFrontmatterValue(frontmatter: string, key: string, fileName: string): string {
  const match = frontmatter.match(new RegExp(`^${key}:\\s*["']([\\s\\S]*?)["']\\s*$`, "m"));
  if (!match?.[1]) throw new Error(`${fileName} must declare quoted ${key}`);
  return match[1];
}

function titleFromSlug(slug: string): string {
  if (slug === "context") return "Router";
  return slug
    .split("-")
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(" ");
}

function skillNameForCommand(slug: string): string {
  return slug === "context" ? "context-router" : `context-${slug}`;
}

function codexSkillNameForCommand(slug: string): string {
  return slug === "context" ? "router" : slug;
}

function claudeCommandForSlug(slug: string): string {
  return slug === "context" ? "/context:context" : `/context:${slug}`;
}

const CURSOR_COMMAND_SUMMARIES: Record<string, string> = {
  align: "Review raw material and confirm the Node tree before compiling knowledge.",
  capture: "Capture documents, source code, notes, inbox files, or refreshed sources into the workspace.",
  compile: "Turn confirmed aligned sources into source-linked knowledge through draft and semantic review.",
  context: "Route a natural-language C4A request to the right Context command.",
  drop: "Plan and apply source retraction with semantic decisions and archive safety.",
  extract: "Preview extraction output for a file without writing workspace state.",
  init: "Choose workspace layout, language, and focus, then create the C4A workspace.",
  ops: "Run operational package tasks such as local build/export, with future publish kept under one entry.",
  purge: "Permanently delete archived dropped-source artifacts after explicit confirmation.",
  query: "Answer from the local knowledge workspace with citations and gap handling.",
  status: "Show workspace state, pending work, cache health, and the next useful command.",
};

function cursorCommandSummary(command: CommandSource): string {
  return CURSOR_COMMAND_SUMMARIES[command.slug] ?? command.description;
}

function stripHtmlComments(markdown: string): string {
  return markdown.replace(/<!--[\s\S]*?-->\n*/g, "");
}

function publicSkillBody(command: CommandSource, publicName: string): string {
  const claudeCommand = claudeCommandForSlug(command.slug);
  const rewrittenBody = rewritePackagedSkillReferences(command.body, publicEntrySkillPath)
    .replace(/^## Your [Tt]ask$/gm, "## Workflow");
  return `---\nname: ${publicName}\ndescription: >\n  ${command.description} Equivalent to Claude ${claudeCommand}; use the local \`context\` CLI for workspace writes.\ntools:\n  - Bash\n---\n\n# ${command.title}\n\nPublic C4A Context entry for agents that expose skills instead of Claude slash commands.\n\n- Public entry: \`${publicName}\`\n- Claude equivalent: \`${claudeCommand}\`\n- CLI primitive prefix: \`context ...\`\n- Internal procedures live under \`${INTERNAL_PROCEDURES_DIR}/\`; read each referenced file in full before following that step.\n\n---\n\n${rewrittenBody.trimEnd()}\n`;
}

async function readCommands(): Promise<CommandSource[]> {
  const commandsRoot = join(pkgRoot, "plugin/commands");
  const files = (await readdir(commandsRoot)).filter((file) => file.endsWith(".md")).sort();
  const commands: CommandSource[] = [];
  for (const file of files) {
    const raw = await readFile(join(commandsRoot, file), "utf8");
    const { frontmatter, body } = parseFrontmatter(raw, file);
    const slug = basename(file, ".md");
    commands.push({
      slug,
      title: titleFromSlug(slug),
      description: quotedFrontmatterValue(frontmatter, "description", file),
      body,
    });
  }
  return commands;
}

async function renderManifest(input: {
  templatePath: string;
  outputPath: string;
  version: string;
  label: string;
}): Promise<Record<string, unknown>> {
  const template = await readFile(input.templatePath, "utf8");
  const rendered = template.replace(/__VERSION__/g, input.version);
  await mkdir(dirname(input.outputPath), { recursive: true });
  await writeFile(input.outputPath, rendered, "utf8");

  const parsed = JSON.parse(rendered) as Record<string, unknown>;
  if (parsed.name !== "context") {
    throw new Error(`${input.label} plugin.json name must be "context", got "${String(parsed.name)}"`);
  }
  if (parsed.version !== input.version) {
    throw new Error(`${input.label} plugin.json version mismatch: ${String(parsed.version)} vs ${input.version}`);
  }
  return parsed;
}

async function resetDir(dir: string): Promise<void> {
  await rm(dir, { recursive: true, force: true });
  await mkdir(dir, { recursive: true });
}

async function copyDir(from: string, to: string): Promise<void> {
  await cp(from, to, { recursive: true });
}

async function copyAssetsIfPresent(outputRoot: string): Promise<void> {
  const assetsRoot = join(pkgRoot, "plugin/assets");
  if (await pathExists(assetsRoot)) {
    await copyDir(assetsRoot, join(outputRoot, "assets"));
  }
}

async function pathExists(path: string): Promise<boolean> {
  try {
    await stat(path);
    return true;
  } catch {
    return false;
  }
}

async function writeGeneratedGuards(root: string, agentFile: "CLAUDE.md" | "AGENTS.md", label: string): Promise<void> {
  await writeFile(join(root, agentFile), generatedNotice(label), "utf8");
  await writeFile(join(root, "README.md"), generatedNotice(label), "utf8");
  await writeFile(join(root, ".generated"), "generated by packages/context-cli/scripts/build-plugin.ts\n", "utf8");
}

async function buildClaude(version: string): Promise<void> {
  const out = join(PLUGINS_ROOT, "claude");
  await resetDir(out);
  await renderManifest({
    label: "claude",
    version,
    templatePath: join(pkgRoot, "plugin/.claude-plugin/plugin.json.template"),
    outputPath: join(out, ".claude-plugin/plugin.json"),
  });
  await copyDir(join(pkgRoot, "plugin/commands"), join(out, "commands"));
  await copyDir(join(pkgRoot, "plugin/skills"), join(out, "skills"));
  await writeGeneratedGuards(out, "CLAUDE.md", "Claude plugin build");
}

async function writePublicSkills(
  outputRoot: string,
  commands: readonly CommandSource[],
  skillName: (slug: string) => string,
): Promise<void> {
  const skillsRoot = join(outputRoot, "skills");
  await mkdir(skillsRoot, { recursive: true });
  const internalSkillNames = (await readdir(join(pkgRoot, "plugin/skills")))
    .filter((name) => name.startsWith("skill-"))
    .sort();
  for (const command of commands) {
    const publicName = skillName(command.slug);
    const skillRoot = join(skillsRoot, publicName);
    await mkdir(skillRoot, { recursive: true });
    await writeFile(join(skillRoot, "SKILL.md"), publicSkillBody(command, publicName), "utf8");
    if (command.body.includes("context:skill-")) {
      const referencesRoot = join(skillRoot, INTERNAL_PROCEDURES_DIR);
      await mkdir(referencesRoot, { recursive: true });
      for (const skill of internalSkillNames) {
        const sourceRoot = join(pkgRoot, "plugin/skills", skill);
        // skill-X.md sits directly in the siblings root; depth = 0.
        const skillBody = await readFile(join(sourceRoot, "SKILL.md"), "utf8");
        await writeFile(
          join(referencesRoot, `${skill}.md`),
          rewriteFlattenedProcedureReferences(rewriteClaudePluginRoot(skillBody, 0), skill),
          "utf8",
        );
        const refs = join(sourceRoot, "references");
        if (await pathExists(refs)) {
          await copyMarkdownTreeRewrittenForPublicEntry(
            refs,
            join(referencesRoot, skill, "references"),
            referencesRoot,
          );
        }
      }
    }
  }
}

async function buildCodex(version: string, commands: readonly CommandSource[]): Promise<void> {
  const out = join(PLUGINS_ROOT, "codex");
  await resetDir(out);
  const manifest = await renderManifest({
    label: "codex",
    version,
    templatePath: join(pkgRoot, "plugin/.codex-plugin/plugin.json.template"),
    outputPath: join(out, ".codex-plugin/plugin.json"),
  });
  if (manifest.skills !== "./skills/") {
    throw new Error(`codex plugin.json must point skills to "./skills/", got "${String(manifest.skills)}"`);
  }
  await copyAssetsIfPresent(out);
  await writePublicSkills(out, commands, codexSkillNameForCommand);
  await writeGeneratedGuards(out, "AGENTS.md", "Codex plugin build");
  await verifyNoPackagedSkillLeak(out, "codex");
  await verifyNoClaudePluginRootLeak(out, "codex");
}

async function buildVercel(commands: readonly CommandSource[]): Promise<void> {
  // Vercel-style standalone skills live directly under c4a-plugins/skills/.
  // No plugin manifest, no per-build README; the top-level c4a-plugins/README.md
  // owns onboarding for this install path.
  const skillsRoot = join(PLUGINS_ROOT, "skills");
  await resetDir(skillsRoot);
  await writePublicSkills(PLUGINS_ROOT, commands, skillNameForCommand);
  await verifyNoPackagedSkillLeak(skillsRoot, "vercel");
  await verifyNoClaudePluginRootLeak(skillsRoot, "vercel");
}

async function writeCursorCommands(outRoot: string, commands: readonly CommandSource[]): Promise<void> {
  const dest = join(outRoot, "commands");
  await mkdir(dest, { recursive: true });
  const sourceRoot = join(pkgRoot, "plugin/commands");
  for (const command of commands) {
    const sourcePath = join(sourceRoot, `${command.slug}.md`);
    const raw = await readFile(sourcePath, "utf8");
    const { frontmatter } = parseFrontmatter(raw, `${command.slug}.md`);
    const rewrittenBody = rewriteClaudeSlashCommandsForCursor(
      rewritePackagedSkillReferences(command.body, cursorCommandSkillPath),
    );
    const body = stripHtmlComments(rewrittenBody).trimStart();
    const file = `---\n${frontmatter}\n---\n\n${cursorCommandSummary(command)}\n\n---\n\n${body.trimEnd()}\n`;
    await writeFile(join(dest, cursorCommandFileName(command.slug)), file, "utf8");
  }
}

async function buildCursor(version: string, commands: readonly CommandSource[]): Promise<void> {
  const out = join(PLUGINS_ROOT, "cursor");
  await resetDir(out);
  const manifest = await renderManifest({
    label: "cursor",
    version,
    templatePath: join(pkgRoot, "plugin/.cursor-plugin/plugin.json.template"),
    outputPath: join(out, ".cursor-plugin/plugin.json"),
  });
  if (manifest.skills !== "./skills/") {
    throw new Error(`cursor plugin.json must point skills to "./skills/", got "${String(manifest.skills)}"`);
  }
  if (manifest.commands !== "./commands/") {
    throw new Error(`cursor plugin.json must point commands to "./commands/", got "${String(manifest.commands)}"`);
  }
  await copyAssetsIfPresent(out);
  await writeCursorCommands(out, commands);
  // Cursor's internal skills tree is the siblings root; depth-aware rewrite
  // turns `${CLAUDE_PLUGIN_ROOT}/skills/<name>/...` into the right number of
  // `..` segments per file location.
  await copyMarkdownTreeRewrittenForPublicEntry(
    join(pkgRoot, "plugin/skills"),
    join(out, "skills"),
    join(out, "skills"),
  );
  await writeGeneratedGuards(out, "AGENTS.md", "Cursor plugin build");
  await writeFile(
    join(out, "README.md"),
    [
      generatedNotice("Cursor plugin build").trimEnd(),
      "",
      "Install shape:",
      "",
      "- Marketplace/GitHub plugin shape: this directory is a plugin root with `.cursor-plugin/plugin.json`, `commands/`, and `skills/`.",
      "- User entries are under `commands/`; Cursor command files are prefixed as `context-*` to avoid global slash-command collisions.",
      "- Local plugin fallback: symlink or copy this directory to `~/.cursor/plugins/local/context/` only when Marketplace import is unavailable.",
      "",
      "Do not copy these files into `.cursor/skills/` as the main install path. Cursor Marketplace installs this plugin root directly.",
      "",
    ].join("\n"),
    "utf8",
  );
  await rewriteMarkdownFiles(out, rewriteClaudeSlashCommandsForCursor);
  await verifyNoPackagedSkillLeak(out, "cursor");
  await verifyNoClaudePluginRootLeak(out, "cursor");
}

async function ensurePluginsRoot(): Promise<void> {
  try {
    const info = await stat(PLUGINS_ROOT);
    if (!info.isDirectory()) {
      throw new Error(`c4a-plugins exists but is not a directory: ${PLUGINS_ROOT}`);
    }
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code;
    if (code !== "ENOENT") throw err;
    throw new Error(
      `c4a-plugins/ not found at ${PLUGINS_ROOT}. ` +
      `Run: git clone https://github.com/context4ai/context.git c4a-plugins ` +
      `(from the c4a repo root) before building plugins.`
    );
  }
  try {
    await stat(join(PLUGINS_ROOT, ".git"));
  } catch {
    throw new Error(
      `c4a-plugins/ exists but is not a git repo. Expected remote: ` +
      `https://github.com/context4ai/context.git`
    );
  }
}

async function writeMarketplaceManifests(version: string): Promise<void> {
  const claudeMarketplace = {
    name: MARKETPLACE_NAME,
    owner: { name: "c4a" },
    plugins: [{
      name: PLUGIN_NAME,
      source: "./claude",
      description: "Context For AI — local knowledge workspace. Capture docs, extract code structure, compile into an interlinked wiki with source-traced facts.",
    }],
  };
  const cursorMarketplace = {
    name: MARKETPLACE_NAME,
    owner: { name: "Context4AI", email: "support@context4ai.dev" },
    metadata: { description: "C4A Context plugin marketplace" },
    plugins: [{
      name: PLUGIN_NAME,
      source: "./cursor",
      description: "Turn project sources into a local, source-linked knowledge workspace your agent can maintain and query.",
    }],
  };
  const codexMarketplace = {
    name: MARKETPLACE_NAME,
    interface: { displayName: "C4A Marketplace" },
    plugins: [{
      name: PLUGIN_NAME,
      source: { source: "local", path: "./codex" },
      policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
      category: "Productivity",
    }],
  };
  await mkdir(join(PLUGINS_ROOT, ".claude-plugin"), { recursive: true });
  await mkdir(join(PLUGINS_ROOT, ".cursor-plugin"), { recursive: true });
  await mkdir(join(PLUGINS_ROOT, ".agents", "plugins"), { recursive: true });
  await writeFile(
    join(PLUGINS_ROOT, ".claude-plugin", "marketplace.json"),
    `${JSON.stringify(claudeMarketplace, null, 2)}\n`,
    "utf8",
  );
  await writeFile(
    join(PLUGINS_ROOT, ".cursor-plugin", "marketplace.json"),
    `${JSON.stringify(cursorMarketplace, null, 2)}\n`,
    "utf8",
  );
  await writeFile(
    join(PLUGINS_ROOT, ".agents", "plugins", "marketplace.json"),
    `${JSON.stringify(codexMarketplace, null, 2)}\n`,
    "utf8",
  );
  await writeFile(
    join(PLUGINS_ROOT, "VERSION"),
    `${version}\n`,
    "utf8",
  );
}

async function writePluginsTopLevelDocs(): Promise<void> {
  for (const name of ["README.md", "README_CN.md"]) {
    const body = await readFile(join(pkgRoot, "plugin", name), "utf8");
    await writeFile(join(PLUGINS_ROOT, name), body, "utf8");
  }

  const assetsSrc = join(pkgRoot, "plugin/assets");
  if (await pathExists(assetsSrc)) {
    const assetsDst = join(PLUGINS_ROOT, "assets");
    await rm(assetsDst, { recursive: true, force: true });
    await copyDir(assetsSrc, assetsDst);
  }

  const gitignorePath = join(PLUGINS_ROOT, ".gitignore");
  if (!(await pathExists(gitignorePath))) {
    await writeFile(
      gitignorePath,
      [
        ".DS_Store",
        "node_modules/",
        ".idea/",
        ".vscode/",
        "",
      ].join("\n"),
      "utf8",
    );
  }
}

async function main(): Promise<void> {
  const pkgRaw = await readFile(resolve(pkgRoot, "package.json"), "utf-8");
  const pkg = JSON.parse(pkgRaw) as { version?: string };
  const version = pkg.version;
  if (!version) throw new Error("package.json is missing `version`");

  await ensurePluginsRoot();

  const commands = await readCommands();
  await buildClaude(version);
  await buildCodex(version, commands);
  await buildCursor(version, commands);
  await buildVercel(commands);
  await writeMarketplaceManifests(version);
  await writePluginsTopLevelDocs();

  process.stdout.write(`c4a-plugins/claude generated (version=${version})\n`);
  process.stdout.write(`c4a-plugins/codex generated (version=${version}, publicSkills=${commands.length})\n`);
  process.stdout.write(`c4a-plugins/cursor generated (version=${version}, publicSkills=${commands.length})\n`);
  process.stdout.write(`c4a-plugins/skills generated (publicSkills=${commands.length})\n`);
  process.stdout.write(`c4a-plugins/{marketplace.json,README.md,README_CN.md,VERSION} written\n`);
}

main().catch((err) => {
  process.stderr.write(`build-plugin failed: ${err instanceof Error ? err.message : String(err)}\n`);
  process.exit(1);
});
