#!/usr/bin/env node

// src/cli/install-skills.ts
import * as p from "@clack/prompts";
import pc from "picocolors";
import {
  readFileSync,
  readdirSync,
  writeFileSync,
  existsSync as existsSync2,
  mkdirSync,
  cpSync
} from "fs";
import { join as join2, dirname, relative } from "path";
import { fileURLToPath } from "url";

// src/core/agent-detect.ts
import { existsSync } from "fs";
import { join } from "path";
function detectInstalledAgents(projectRoot) {
  return {
    claudeCode: existsSync(join(projectRoot, ".claude")) || existsSync(join(projectRoot, "CLAUDE.md")),
    codex: existsSync(join(projectRoot, ".codex")) || existsSync(join(projectRoot, "AGENTS.md")) || existsSync(join(projectRoot, "AGENTS.override.md")),
    opencode: existsSync(join(projectRoot, ".opencode")) || existsSync(join(projectRoot, "opencode.json")) || existsSync(join(projectRoot, "opencode.jsonc")),
    cursor: existsSync(join(projectRoot, ".cursor")) || existsSync(join(projectRoot, ".cursorrules"))
  };
}

// src/cli/install-skills.ts
function findPackageRoot() {
  let cur = dirname(fileURLToPath(import.meta.url));
  for (let i = 0; i < 8; i++) {
    if (existsSync2(join2(cur, "skills"))) return cur;
    const parent = dirname(cur);
    if (parent === cur) break;
    cur = parent;
  }
  return null;
}
function listShippedSkills(packageRoot) {
  const skillsDir = join2(packageRoot, "skills");
  if (!existsSync2(skillsDir)) return [];
  return readdirSync(skillsDir).filter((name) => existsSync2(join2(skillsDir, name, "SKILL.md"))).sort();
}
function listCanonicalAgents(packageRoot) {
  const dir = join2(packageRoot, "agents");
  if (!existsSync2(dir)) return [];
  return readdirSync(dir).filter((name) => name.startsWith("omd-") && name.endsWith(".md")).sort();
}
function parseCanonicalAgent(packageRoot, filename) {
  const src = readFileSync(join2(packageRoot, "agents", filename), "utf8");
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(src);
  if (!match) {
    throw new Error(`agents/${filename}: missing YAML frontmatter`);
  }
  const fm = match[1];
  const body = match[2];
  const grab = (key) => {
    const re = new RegExp(`^${key}:\\s*(.+)$`, "m");
    const m = re.exec(fm);
    return m ? m[1].trim().replace(/^["']|["']$/g, "") : "";
  };
  return {
    name: grab("name") || filename.replace(/\.md$/, ""),
    description: grab("description"),
    tools: grab("tools").split(",").map((t) => t.trim()).filter(Boolean),
    model: grab("model") || "sonnet",
    body
  };
}
function claudeToolsToCodex(tools) {
  const m = {
    Read: "read_file",
    Write: "write_file",
    Edit: "edit_file",
    Bash: "shell",
    Glob: "search",
    Grep: "search",
    WebFetch: "web_fetch",
    WebSearch: "search",
    Agent: "spawn_agent",
    TaskCreate: "task",
    TaskUpdate: "task",
    TaskList: "task"
  };
  const out = /* @__PURE__ */ new Set();
  for (const t of tools) out.add(m[t] ?? t.toLowerCase());
  return [...out].sort();
}
function claudeModelToCodex(model) {
  const m = {
    haiku: "gpt-4.1-mini",
    sonnet: "gpt-4.1",
    opus: "gpt-4.1"
  };
  return m[model.toLowerCase()] ?? "gpt-4.1";
}
function renderClaudeAgent(a) {
  const fm = [
    "---",
    `name: ${a.name}`,
    `description: ${a.description}`,
    `tools: ${a.tools.join(", ")}`,
    `model: ${a.model}`,
    `omd_managed: true`,
    "---",
    ""
  ].join("\n");
  return fm + a.body;
}
function renderCodexAgent(a) {
  const tools = claudeToolsToCodex(a.tools);
  const model = claudeModelToCodex(a.model);
  const desc = a.description.replace(/"/g, '\\"');
  return [
    `[agent]`,
    `name = "${a.name}"`,
    `description = "${desc}"`,
    `model = "${model}"`,
    `max_threads = 1`,
    `allowed_tools = [${tools.map((t) => `"${t}"`).join(", ")}]`,
    "",
    `instructions = """`,
    `Source of truth: agents/${a.name}.md (canonical). The full role spec is`,
    `mirrored to .claude/agents/${a.name}.md when installed for Claude Code.`,
    `Follow that spec verbatim regardless of channel.`,
    "",
    `Codex notes:`,
    `- Spawn sub-agents via spawn_agent with names matching .codex/agents/<name>.toml`,
    `- Use shell to invoke CLI helpers (omd init prepare, omd remember, git apply, npx axe-core, npx lighthouse)`,
    `- All artifacts go inside .omd/runs/run-<latest>/ (or skills/omd-lab-02-design-harness/runs/<lab-version>-...)`,
    `"""`,
    ""
  ].join("\n");
}
function planForTarget(projectRoot, target) {
  switch (target) {
    case "claude-code":
      return {
        target,
        destDir: join2(projectRoot, ".claude", "skills"),
        layout: "folder"
      };
    case "codex":
      return {
        target,
        destDir: join2(projectRoot, ".codex", "skills"),
        layout: "folder"
      };
    case "opencode":
      return {
        target,
        destDir: join2(projectRoot, ".opencode", "agents"),
        layout: "flat"
      };
  }
}
var MANAGED_HEADER = "<!-- omd:installed-skill \u2014 managed by `omd install-skills`. Do not edit; rerun the command to refresh. -->";
var IGNORED_SKILL_ENTRIES = /* @__PURE__ */ new Set([".runtime", "__pycache__", ".DS_Store"]);
function parseSkillChannels(skillMd) {
  const fm = /^---\n([\s\S]*?)\n---/.exec(skillMd);
  if (!fm) return null;
  const m = /^x-omd-channels:\s*(.+)$/m.exec(fm[1]);
  if (!m) return null;
  const valid = ["claude-code", "codex", "opencode"];
  const list = m[1].split(/[,\s]+/).map((s) => s.trim()).filter((s) => valid.includes(s));
  return list.length > 0 ? list : null;
}
function installOne(packageRoot, plan, skill, force) {
  const skillDir = join2(packageRoot, "skills", skill);
  const src = readFileSync(join2(skillDir, "SKILL.md"), "utf8");
  const managed = MANAGED_HEADER + "\n\n" + src;
  const channels = parseSkillChannels(src);
  if (channels && !channels.includes(plan.target)) {
    return {
      target: plan.target,
      skill,
      destPath: join2(plan.destDir, skill + ".md"),
      status: "skipped-incompat"
    };
  }
  const extras = readdirSync(skillDir).filter(
    (n) => n !== "SKILL.md" && !IGNORED_SKILL_ENTRIES.has(n)
  );
  const isMultiFile = extras.length > 0;
  if (plan.layout !== "folder" && isMultiFile) {
    return {
      target: plan.target,
      skill,
      destPath: join2(plan.destDir, skill + ".md"),
      status: "skipped-incompat"
    };
  }
  const destPath = plan.layout === "folder" ? join2(plan.destDir, skill, "SKILL.md") : join2(plan.destDir, skill + ".md");
  const exists = existsSync2(destPath);
  const existing = exists ? readFileSync(destPath, "utf8") : "";
  if (exists && existing === managed && !isMultiFile) {
    return { target: plan.target, skill, destPath, status: "unchanged" };
  }
  if (exists && !existing.startsWith(MANAGED_HEADER) && !force) {
    return { target: plan.target, skill, destPath, status: "skipped-drift" };
  }
  mkdirSync(dirname(destPath), { recursive: true });
  writeFileSync(destPath, managed, "utf8");
  if (plan.layout === "folder" && isMultiFile) {
    const destSkillDir = join2(plan.destDir, skill);
    for (const entry of extras) {
      cpSync(join2(skillDir, entry), join2(destSkillDir, entry), {
        recursive: true,
        filter: (s) => !/(\/__pycache__|\/\.runtime|\.pyc$|\.DS_Store$)/.test(s)
      });
    }
  }
  return {
    target: plan.target,
    skill,
    destPath,
    status: exists ? "updated" : "created"
  };
}
function installHookFile(packageRoot, projectRoot, filename, force) {
  const target = "claude-code";
  const skillLabel = `hook:${filename}`;
  const srcPath = join2(packageRoot, ".claude", "hooks", filename);
  const destPath = join2(projectRoot, ".claude", "hooks", filename);
  if (!existsSync2(srcPath)) {
    return { target, skill: skillLabel, destPath, status: "skipped-drift" };
  }
  const src = readFileSync(srcPath, "utf8");
  const exists = existsSync2(destPath);
  const existing = exists ? readFileSync(destPath, "utf8") : "";
  if (exists && existing === src) {
    return { target, skill: skillLabel, destPath, status: "unchanged" };
  }
  if (exists && !force) {
    return { target, skill: skillLabel, destPath, status: "skipped-drift" };
  }
  mkdirSync(dirname(destPath), { recursive: true });
  writeFileSync(destPath, src);
  return { target, skill: skillLabel, destPath, status: exists ? "updated" : "created" };
}
function installSettingsJson(packageRoot, projectRoot, force) {
  const target = "claude-code";
  const skillLabel = "settings:.claude/settings.json";
  const srcPath = join2(packageRoot, ".claude", "settings.json");
  const destPath = join2(projectRoot, ".claude", "settings.json");
  if (!existsSync2(srcPath)) {
    return { target, skill: skillLabel, destPath, status: "skipped-drift" };
  }
  const src = readFileSync(srcPath, "utf8");
  const exists = existsSync2(destPath);
  const existing = exists ? readFileSync(destPath, "utf8") : "";
  if (exists && existing === src) {
    return { target, skill: skillLabel, destPath, status: "unchanged" };
  }
  if (exists && !force) {
    try {
      const parsed = JSON.parse(existing);
      if (typeof parsed._doc === "string" && parsed._doc.includes("OmD")) {
      } else {
        return { target, skill: skillLabel, destPath, status: "skipped-drift" };
      }
    } catch {
      return { target, skill: skillLabel, destPath, status: "skipped-drift" };
    }
  }
  mkdirSync(dirname(destPath), { recursive: true });
  writeFileSync(destPath, src);
  return { target, skill: skillLabel, destPath, status: exists ? "updated" : "created" };
}
function installDataFile(packageRoot, projectRoot, channelDir, dataFilename, force) {
  const target = channelDir === ".claude" ? "claude-code" : "codex";
  const skillLabel = `data:${dataFilename}`;
  const srcPath = join2(packageRoot, "data", dataFilename);
  const destPath = join2(projectRoot, channelDir, "data", dataFilename);
  if (!existsSync2(srcPath)) {
    return { target, skill: skillLabel, destPath, status: "skipped-drift" };
  }
  const src = readFileSync(srcPath, "utf8");
  const exists = existsSync2(destPath);
  const existing = exists ? readFileSync(destPath, "utf8") : "";
  if (exists && existing === src) {
    return { target, skill: skillLabel, destPath, status: "unchanged" };
  }
  if (exists && !force) {
    return { target, skill: skillLabel, destPath, status: "skipped-drift" };
  }
  mkdirSync(dirname(destPath), { recursive: true });
  writeFileSync(destPath, src, "utf8");
  return {
    target,
    skill: skillLabel,
    destPath,
    status: exists ? "updated" : "created"
  };
}
function installAgentFile(packageRoot, projectRoot, channel, filename, force) {
  const target = channel === "claude" ? "claude-code" : "codex";
  const skillLabel = `agent:${filename}`;
  const parsed = parseCanonicalAgent(packageRoot, filename);
  const rendered = channel === "claude" ? renderClaudeAgent(parsed) : renderCodexAgent(parsed);
  const destFilename = channel === "claude" ? filename : filename.replace(/\.md$/, ".toml");
  const destPath = join2(
    projectRoot,
    channel === "claude" ? ".claude" : ".codex",
    "agents",
    destFilename
  );
  const managed = channel === "claude" ? rendered : "# omd:installed-agent \u2014 generated from agents/" + filename + " by `omd install-skills`. Do not edit; rerun the command to refresh.\n\n" + rendered;
  const exists = existsSync2(destPath);
  const existing = exists ? readFileSync(destPath, "utf8") : "";
  if (exists && existing === managed) {
    return { target, skill: skillLabel, destPath, status: "unchanged" };
  }
  const isManaged = channel === "claude" ? /\nomd_managed:\s*true\b/.test(existing) : existing.startsWith("# omd:installed-agent");
  if (exists && !isManaged && !force) {
    return { target, skill: skillLabel, destPath, status: "skipped-drift" };
  }
  mkdirSync(dirname(destPath), { recursive: true });
  writeFileSync(destPath, managed, "utf8");
  return {
    target,
    skill: skillLabel,
    destPath,
    status: exists ? "updated" : "created"
  };
}
var STATUS_LABEL = {
  created: pc.green("created"),
  updated: pc.cyan("updated"),
  unchanged: pc.dim("unchanged"),
  "skipped-drift": pc.yellow("skipped"),
  "skipped-incompat": pc.yellow("skipped (claude-code only)")
};
function autoDetectTargets(projectRoot) {
  const presence = detectInstalledAgents(projectRoot);
  const targets = [];
  if (presence.claudeCode) targets.push("claude-code");
  if (presence.codex) targets.push("codex");
  if (presence.opencode) targets.push("opencode");
  if (targets.length === 0) {
    return ["claude-code", "codex", "opencode"];
  }
  return targets;
}
async function runInstallSkills(opts = {}) {
  const projectRoot = opts.dir ?? process.cwd();
  const packageRoot = findPackageRoot();
  if (!packageRoot) {
    console.error(pc.red("omd install-skills: package data not found"));
    return 1;
  }
  const allSkills = listShippedSkills(packageRoot);
  if (allSkills.length === 0) {
    console.error(pc.red("omd install-skills: no skills found in package"));
    return 1;
  }
  const allAgents = listCanonicalAgents(packageRoot);
  const force = opts.force ?? false;
  const minimal = opts.skillsOnly === true;
  p.intro(
    pc.bold("omd install-skills") + pc.dim(`  (${relative(process.cwd(), projectRoot) || "."})`)
  );
  const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
  const nonInteractive = opts.all || !isTTY || opts.skillsFilter || opts.agentsFilter || minimal;
  const detected = autoDetectTargets(projectRoot);
  const presence = detectInstalledAgents(projectRoot);
  const actuallyDetected = [
    presence.claudeCode ? "claude-code" : null,
    presence.codex ? "codex" : null,
    presence.opencode ? "opencode" : null
  ].filter((x) => x !== null);
  let skills;
  let canonicalAgents;
  let targets;
  if (nonInteractive) {
    skills = opts.skillsFilter ? allSkills.filter((s) => opts.skillsFilter.includes(s)) : allSkills;
    canonicalAgents = opts.agentsFilter ? allAgents.filter((a) => opts.agentsFilter.includes(a.replace(/\.md$/, ""))) : allAgents;
    targets = opts.agents ? opts.agents : opts.all ? ["claude-code", "codex", "opencode"] : detected;
  } else {
    const skillResult = await p.multiselect({
      message: "Skills \xB7 space = \uD1A0\uAE00 \xB7 a = \uC804\uCCB4 \xB7 enter = \uD655\uC778 (default ALL)",
      options: allSkills.map((s) => ({ value: s, label: s, hint: "omd skill" })),
      initialValues: allSkills,
      required: true
    });
    if (p.isCancel(skillResult)) {
      p.cancel("Install cancelled.");
      return 130;
    }
    skills = skillResult;
    if (allAgents.length > 0) {
      const agentResult = await p.multiselect({
        message: "Sub-agents \xB7 space = \uD1A0\uAE00 \xB7 a = \uC804\uCCB4 \xB7 enter = \uD655\uC778 (default ALL)",
        options: allAgents.map((a) => ({
          value: a,
          label: a.replace(/\.md$/, ""),
          hint: "subagent"
        })),
        initialValues: allAgents,
        required: false
      });
      if (p.isCancel(agentResult)) {
        p.cancel("Install cancelled.");
        return 130;
      }
      canonicalAgents = agentResult;
    } else {
      canonicalAgents = [];
    }
    if (opts.agents) {
      targets = opts.agents;
    } else {
      const targetResult = await p.multiselect({
        message: "Agent channels \xB7 space = \uD1A0\uAE00 \xB7 enter = \uD655\uC778 \xB7 \uCD5C\uC18C 1\uAC1C \uC120\uD0DD",
        options: [
          {
            value: "claude-code",
            label: "Claude Code",
            hint: actuallyDetected.includes("claude-code") ? ".claude/ detected" : ""
          },
          {
            value: "codex",
            label: "Codex",
            hint: actuallyDetected.includes("codex") ? ".codex/ detected" : ""
          },
          {
            value: "opencode",
            label: "OpenCode",
            hint: actuallyDetected.includes("opencode") ? ".opencode/ detected" : ""
          }
        ],
        initialValues: [],
        required: true
      });
      if (p.isCancel(targetResult)) {
        p.cancel("Install cancelled.");
        return 130;
      }
      targets = targetResult;
    }
  }
  const plans = targets.map((t) => planForTarget(projectRoot, t));
  p.log.message(
    pc.bold(`Skills (${skills.length}): `) + skills.map((s) => pc.cyan(s)).join(", ")
  );
  if (canonicalAgents.length > 0) {
    p.log.message(
      pc.bold(`Agents (${canonicalAgents.length}): `) + canonicalAgents.map((a) => pc.cyan(a.replace(/\.md$/, ""))).join(", ")
    );
  }
  p.log.message(
    pc.bold("Targets: ") + targets.map((t) => pc.cyan(t)).join(", ")
  );
  if (minimal) canonicalAgents = [];
  const results = [];
  for (const plan of plans) {
    for (const skill of skills) {
      results.push(installOne(packageRoot, plan, skill, force));
    }
  }
  for (const target of targets) {
    if (target === "claude-code") {
      for (const filename of canonicalAgents) {
        results.push(installAgentFile(packageRoot, projectRoot, "claude", filename, force));
      }
    } else if (target === "codex") {
      for (const filename of canonicalAgents) {
        results.push(installAgentFile(packageRoot, projectRoot, "codex", filename, force));
      }
    }
  }
  if (!minimal) {
    const dataFiles = [
      "reference-fingerprints.json",
      "reference-tags.md",
      "vocabulary.json",
      "synonyms.json",
      "opt-out-corpus.json"
    ];
    for (const target of targets) {
      if (target === "claude-code") {
        for (const dataFile of dataFiles) {
          results.push(installDataFile(packageRoot, projectRoot, ".claude", dataFile, force));
        }
      } else if (target === "codex") {
        for (const dataFile of dataFiles) {
          results.push(installDataFile(packageRoot, projectRoot, ".codex", dataFile, force));
        }
      }
    }
    if (targets.includes("claude-code")) {
      for (const hookFile of [
        "skill-activation.cjs",
        "session-state-loader.cjs",
        "post-edit-watch.cjs",
        "session-end-foldin.cjs"
      ]) {
        results.push(installHookFile(packageRoot, projectRoot, hookFile, force));
      }
      results.push(installSettingsJson(packageRoot, projectRoot, force));
    }
  }
  p.log.message(pc.bold("\nResults:"));
  for (const r of results) {
    const rel = relative(projectRoot, r.destPath);
    p.log.message(
      `  ${STATUS_LABEL[r.status]}  ${pc.dim(r.target.padEnd(12))} ${rel}`
    );
  }
  const driftCount = results.filter((r) => r.status === "skipped-drift").length;
  const writtenCount = results.filter(
    (r) => r.status === "created" || r.status === "updated"
  ).length;
  if (driftCount > 0) {
    p.outro(
      pc.yellow(
        `${writtenCount} written, ${driftCount} skipped (existing files lack the omd marker \u2014 rerun with --force to overwrite).`
      )
    );
    return 0;
  }
  if (minimal) {
    for (const r of results.filter((x) => x.status === "skipped-incompat")) {
      p.log.warn(
        `${pc.bold(r.skill)} ${pc.dim("skipped for ")}${pc.cyan(r.target)}${pc.dim(" \u2014 declares x-omd-channels (channel not supported).")}`
      );
    }
    const installed = results.filter(
      (r) => r.status === "created" || r.status === "updated"
    );
    if (installed.length === 0) {
      p.outro(pc.yellow("Nothing installed \u2014 no compatible skill/channel match."));
      return 0;
    }
    p.outro(
      pc.green(
        `Done. Installed ${skills.map((s) => pc.bold(s)).join(", ")} for ${targets.join(", ")}.`
      ) + pc.dim("  \u2192  restart your agent, then use the skill (e.g. ") + pc.cyan("/claude-design") + pc.dim(").")
    );
    return 0;
  }
  const nextSteps = [
    `${pc.bold("Restart your agent, then type your first prompt:")}`,
    "",
    `  ${pc.cyan("EN")}  ${pc.dim("Set up our design system \u2014 Toss-style, for a family meal-tracking app.")}`,
    `  ${pc.cyan("KR")}  ${pc.dim("\uD1A0\uC2A4 \uC2A4\uD0C0\uC77C\uB85C \uAC00\uC871 \uC2DD\uB2E8 \uACF5\uC720 \uC571 \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C \uC7A1\uC544\uC918")}`,
    "",
    `${pc.dim("Your agent runs omd:init and writes DESIGN.md. Then build against it:")}`,
    `  ${pc.cyan("EN")}  ${pc.dim("Design the home screen.")}   ${pc.cyan("KR")}  ${pc.dim("\uD648 \uD654\uBA74 \uB514\uC790\uC778\uD574\uC918")}`,
    "",
    `${pc.dim('Full walkthrough \u2192 "Your first 60 seconds" in the README. Routing is automatic \u2014 no slash command needed.')}`,
    `${pc.dim("Power user: ")}${pc.cyan("/omd-harness <task>")}${pc.dim(" \u2014 jump straight into the pipeline.")}`,
    "",
    `${pc.yellow("\u26A0 Already-running session?")} ${pc.dim("Run `/agents` to reload \u2014 or quit (Cmd+Q on macOS) and relaunch. Without reload, hooks/agents do not load.")}`
  ].join("\n");
  p.note(nextSteps, "Next");
  const hookCount = targets.includes("claude-code") ? 4 : 0;
  p.outro(
    pc.green(
      `Done. ${skills.length} skills \xB7 ${canonicalAgents.length} sub-agents \xB7 ${hookCount} hooks installed (${writtenCount} files).`
    )
  );
  return 0;
}
export {
  runInstallSkills
};
//# sourceMappingURL=install-skills-6QFSN5BN.js.map