const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const readline = require("node:readline");
const readlinePromises = require("node:readline/promises");

const { resolveAgentTargets } = require("./agents");
const { applyCleanPlan, planClean, scanInstalledSkills } = require("./cleaner");
const { loadLocalManifest, loadProfiles, loadRuntimeManifest, matchDomain, matchCommonStage, selectSkills, suggestSkillName } = require("./manifest");
const { applyInstallPlan, planInstall } = require("./planner");
const { loadNeutralPlugin, marketplacePluginRef, parsePluginManifest } = require("./plugin-builder");
const { inspectPlugin, renderPluginDoctor, renderPluginStatus } = require("./plugin-doctor");
const {
  applyLegacyCleanup,
  planLegacyCleanup,
  renderLegacyCleanup,
  renderLegacyDoctor,
  scanLegacy,
} = require("./plugin-doctor-legacy");
const { applyPluginInstallPlan, loadAndPlanPluginInstall, readPluginRecord } = require("./plugin-installer");
const { ensurePluginSourceRoot, ensurePluginWorkspaceRoot, listWorkspacePlugins } = require("./plugin-source");
const { applyPluginUninstallPlan, planPluginUninstall } = require("./plugin-uninstaller");
const { installPluginV2, precheckPluginV2Install, uninstallPluginV2, updatePluginV2 } = require("./plugin-v2-installer");
const { collectGarbage, repairScope, setHostEnabled } = require("./plugin-gc");
const { migratePlugin } = require("./plugin-migrate");
const { scopeRootFor } = require("./plugin-registry");
const {
  checkVersion,
  generateManifests,
  renderManifestGeneration,
  renderManifestVersionCheck,
} = require("./plugin-manifest-strategy");
const { ensureSkillSourceRoot, ensureSkillSourceRootAsync } = require("./source");
const { applyUpdatePlan, planUpdate, planUpdateAll, renderUpdateResults } = require("./updater");
const { buildTelemetryEvent, enqueueTelemetryEvents, flushTelemetryQueue, telemetryStatus } = require("./telemetry");

const packageJson = require("../package.json");
const COMMON_STAGES = ["cicd", "design", "development", "requirements", "testing", "troubleshooting"];
const ANSI = {
  reset: "\x1b[0m",
  bold: "\x1b[1m",
  dim: "\x1b[2m",
  cyan: "\x1b[36m",
  green: "\x1b[32m",
  red: "\x1b[31m",
  reverse: "\x1b[7m",
};
const AGENT_CHOICES = [
  { agent: "codex", label: "Codex", hint: "~/.codex/skills" },
  { agent: "claude", label: "Claude", hint: "~/.claude/skills" },
  { agent: "opencode", label: "OpenCode", hint: "~/.config/opencode/skills" },
  { agent: "all", label: "All", hint: "Codex + Claude + OpenCode" },
];
const INDIVIDUAL_AGENT_INDEXES = [0, 1, 2];
const ALL_AGENT_INDEX = 3;
const ACTION_CHOICES = [
  { action: "install", label: "Install skills", hint: "Choose OpenHarmony skill groups to install" },
  { action: "clean", label: "Clean installed skills", hint: "Scan an agent target and remove selected skills" },
];

async function main(argv = []) {
  if (argv.length === 0) {
    await runHome();
    return;
  }
  if (argv[0] === "--help" || argv[0] === "-h") {
    console.log(helpText());
    return;
  }
  const command = argv[0] || "help";
  const rest = argv.slice(1);
  const options = parseArgs(rest);

  if (command === "help" || options.help) {
    console.log(helpText());
    return;
  }
  if (command === "plugin") {
    console.log(renderPluginCommand(rest));
    return;
  }
  if (command === "list") {
    console.log(await renderList(options));
    return;
  }
  if (command === "install") {
    console.log(await renderInstall(options));
    return;
  }
  if (command === "uninstall") {
    const pluginName = options.names[0] || "<plugin>";
    throw new Error(workflowPluginNamespaceError("uninstall", pluginName));
  }
  if (command === "doctor") {
    console.log(renderDoctor(options));
    return;
  }
  if (command === "clean") {
    await runClean(options);
    return;
  }
  if (command === "update") {
    await runUpdate(options);
    return;
  }
  if (command === "update-all") {
    await runUpdateAll(options);
    return;
  }
  if (command === "telemetry") {
    console.log(renderTelemetry(rest));
    return;
  }
  throw new Error(`Unknown command "${command}". Run oh-skillhub help.`);
}

function parseArgs(args) {
  const options = { names: [] };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--help" || arg === "-h") options.help = true;
    else if (arg === "--domain") options.domain = args[++index];
    else if (arg === "--stage") options.stage = args[++index];
    else if (arg === "--common") options.common = args[++index];
    else if (arg === "--preset" || arg === "--profile") options.preset = args[++index];
    else if (arg === "--agent") options.agent = args[++index];
    else if (arg === "--scope") options.scope = args[++index];
    else if (arg === "--target") options.target = args[++index];
    else if (arg === "--all-common") options.allCommon = true;
    else if (arg === "--name") {
      const nameVal = args[++index];
      if (nameVal !== undefined) options.names.push(nameVal);
    }
    else if (arg === "--dry-run") options.dryRun = true;
    else if (arg === "--scan-legacy") options.scanLegacy = true;
    else if (arg === "--fix") options.fix = true;
    else if (arg === "--offline") options.offline = true;
    else if (arg === "--out") options.out = args[++index];
    else if (arg === "--check") options.check = true;
    else if (arg === "--no-telemetry") options.noTelemetry = true;
    else if (arg === "--force") options.force = true;
    else if (arg === "--atomic") options.atomic = true;
    else if (arg === "--no-gc") options.autoGc = false;
    else if (arg === "--strict") options.strict = true;
    else if (arg === "--replace-same-version") options.replaceSameVersion = true;
    else if (arg === "--allow-downgrade") options.allowDowngrade = true;
    else if (arg === "--all-scopes") options.allScopes = true;
    else if (arg === "--allow-materialized-copy") options.allowMaterializedCopy = true;
    else if (arg === "--purge") options.purge = true;
    else if (arg === "--verbose") options.verbose = true;
    else if (!arg.startsWith("-")) options.names.push(arg);
    else throw new Error(`Unknown option "${arg}".`);
  }
  return options;
}

function isPluginInstallRequest(options) {
  return false;
}

function hasPluginNames(options) {
  return false;
}

function isOnlyPluginNames(options) {
  return false;
}

async function runHome(input = process.stdin, output = process.stdout) {
  if (input.isTTY && output.isTTY) {
    const action = await runRawActionSelection(input, output);
    if (action === "clean") {
      await runClean({}, input, output);
      return;
    }
    await runInteractiveInstaller(input, output);
    return;
  }

  const answers = splitPromptAnswers(await readAll(input));
  const hasActionAnswer = answers.length >= 3 && isSingleSelection(answers[0], ACTION_CHOICES.length);
  if (!hasActionAnswer) {
    await runInteractiveInstallerFromAnswers(answers, output);
    return;
  }

  output.write(renderActionMenu());
  output.write("Select action [1]: \n");
  const action = ACTION_CHOICES[parseSingleSelection(answers[0], ACTION_CHOICES.length, 1)].action;
  if (action === "clean") {
    await runCleanWithAnswers({}, input, output, answers.slice(1));
    return;
  }
  await runInteractiveInstallerFromAnswers(answers.slice(1), output);
}

async function runClean(options, input = process.stdin, output = process.stdout) {
  const scriptedAnswers = input.isTTY ? null : splitPromptAnswers(await readAll(input));
  await runCleanWithAnswers(options, input, output, scriptedAnswers);
}

async function runCleanWithAnswers(options, input, output, scriptedAnswers = null) {
  const cleanOptions = { names: [], ...options };
  const hasScriptedAnswers = Array.isArray(scriptedAnswers);
  let answerIndex = 0;
  const takeAnswer = () => {
    if (!hasScriptedAnswers) return null;
    const answer = scriptedAnswers[answerIndex] || "";
    answerIndex += 1;
    return answer;
  };
  let agent = cleanOptions.agent;
  if (!agent && !cleanOptions.target) {
    if (!hasScriptedAnswers && input.isTTY && output.isTTY) {
      agent = await runRawAgentSelection(input, output);
    } else {
      output.write(renderAgentMenu());
      output.write("Select target [1]: \n");
      agent = parseAgentSelection(takeAnswer() || "");
    }
  }
  const targets = resolveAgentTargets({
    agent: agent || "codex",
    scope: cleanOptions.scope || "user",
    target: cleanOptions.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });
  output.write(renderMigrationNotice(targets));
  const discovered = scanInstalledSkills(targets);
  if (!discovered.length) {
    output.write(renderNoSkillsFound(targets));
    return;
  }
  let selected;
  let prefilledConfirmation = null;
  if (!cleanOptions.names.length && !cleanOptions.dryRun) {
    if (!hasScriptedAnswers && input.isTTY && output.isTTY) {
      selected = await promptCleanSelection(input, output, discovered);
    } else {
      output.write(renderCleanSelectionMenu(discovered));
      output.write("Select skills [1]: \n");
      selected = parseSelection(takeAnswer() || "", discovered.length, 1).map((index) => discovered[index]);
      prefilledConfirmation = takeAnswer() || "";
    }
  } else {
    selected = selectCleanSkills(discovered, cleanOptions.names);
    if (hasScriptedAnswers || !input.isTTY) {
      prefilledConfirmation = takeAnswer();
    }
  }
  if (!selected.length) {
    throw new Error(`No installed skills matched: ${cleanOptions.names.join(", ")}`);
  }
  const plan = planClean(selected, { mode: cleanOptions.purge ? "purge" : "trash" });
  if (cleanOptions.dryRun) {
    output.write(`${renderCleanPlan("Clean dry run", plan)}\n`);
    return;
  }
  await confirmClean(input, output, plan, prefilledConfirmation);
  const applied = applyCleanPlan(plan);
  output.write(`${renderCleanPlan("Clean summary", { operations: applied })}\n`);
  output.write(`${renderCleanCompletion(applied)}\n`);
}

async function promptCleanSelection(input, output, skills) {
  if (input.isTTY && output.isTTY) {
    const selectedIndexes = await runRawCleanSelection(input, output, skills);
    return selectedIndexes.map((index) => skills[index]);
  }
  output.write(renderCleanSelectionMenu(skills));
  const rl = readlinePromises.createInterface({ input, output });
  try {
    const answer = await rl.question("Select skills [1]: ");
    return parseSelection(answer, skills.length, 1).map((index) => skills[index]);
  } finally {
    rl.close();
  }
}

function selectCleanSkills(skills, names = []) {
  if (!names.length) {
    return skills;
  }
  const wanted = new Set(names);
  return skills.filter((skill) => wanted.has(skill.name) || wanted.has(skill.directoryName));
}

async function confirmClean(input, output, plan, prefilledAnswer = null) {
  const first = plan.operations[0];
  const action = first.action === "purge" ? "Permanently delete" : "Remove";
  const target = plan.operations.length
    ? `${first.agent}:${first.scope}`
    : "selected target";
  output.write(`${action} ${plan.operations.length} selected skill(s) from ${target}?\n`);
  if (first.action === "purge") {
    output.write("This cannot be undone.\n");
  } else {
    output.write("Selected directories will be moved to .oh-skillhub/trash.\n");
  }
  if (prefilledAnswer !== null) {
    output.write("Press Enter to confirm, or type anything else to cancel: \n");
    if (prefilledAnswer.trim()) {
      throw new Error("Cancelled. No skills were removed.");
    }
    return;
  }
  const rl = readlinePromises.createInterface({ input, output });
  try {
    const answer = await rl.question("Press Enter to confirm, or type anything else to cancel: ");
    if (answer.trim()) {
      throw new Error("Cancelled. No skills were removed.");
    }
  } finally {
    rl.close();
  }
}

function renderCleanSelectionMenu(skills) {
  const lines = ["Detected skills"];
  skills.forEach((skill, index) => {
    const group = formatSkillGroup([skill.domain, skill.stage]);
    lines.push(
      `${String(index + 1).padStart(2, " ")}. [ ] ${skill.name.padEnd(36, " ")} ${skill.status.padEnd(9, " ")} ${group}`,
    );
  });
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function renderCleanPlan(title, plan) {
  const lines = [title];
  for (const operation of plan.operations) {
    const destination = operation.destination || operation.source;
    lines.push(
      `${operation.action.toUpperCase()} ${operation.skill.name} -> ${operation.agent}:${operation.scope} ${destination}`,
    );
  }
  return lines.join("\n");
}

function renderCleanCompletion(applied) {
  const first = applied[0];
  const target = first ? `${first.agent}:${first.scope}` : "selected target";
  return ["", `Done. Removed ${applied.length} skill(s) from ${target}.`].join("\n");
}

function renderNoSkillsFound(targets) {
  const lines = ["No skills found."];
  for (const target of targets) {
    lines.push(`${target.agent}:${target.scope} ${target.dir}`);
  }
  return `${lines.join("\n")}\n`;
}

function renderMigrationNotice(targets) {
  const lines = [];
  for (const target of targets) {
    if (!target.migration) {
      continue;
    }
    const report = target.migration;
    if (report.removedEmpty) {
      lines.push(`Migrated ${target.agent}:${target.scope}: removed empty legacy ${report.oldDir}.`);
      continue;
    }
    const moved = report.moved.filter((item) => item.action === "moved").length;
    const merged = report.moved.filter((item) => item.action === "merged").length;
    const conflicts = (report.conflicts || []).length;
    let line = `Migrated ${target.agent}:${target.scope} from ${report.oldDir} to ${report.newDir} (${moved} moved`;
    if (merged) {
      line += `, ${merged} record merged`;
    }
    if (conflicts) {
      line += `, ${conflicts} conflict(s) kept new`;
    }
    line += `).`;
    lines.push(line);
  }
  return lines.length ? `${lines.join("\n")}\n` : "";
}

function formatSkillGroup(segments) {
  const known = segments.filter((value) => value && value !== "unknown");
  return known.length ? known.join("/") : "unknown";
}

function formatSkillBracket(skill) {
  if (skill.path) {
    const parts = skill.path.split("/").filter(Boolean);
    if (parts.length > 2 && parts[0] === "skills") {
      return parts.slice(1, -1).join("/");
    }
  }
  return formatSkillGroup([skill.scope, skill.domain, skill.stage]);
}

async function renderList(options) {
  const manifest = await loadRuntimeManifest({ env: process.env, offline: options.offline });
  const profiles = loadProfiles();
  let skills;
  if (options.preset) {
    skills = selectSkills(manifest, profiles, { preset: options.preset }).skills;
  } else {
    skills = manifest.skills
      .filter((skill) => !options.domain || matchDomain(skill, options.domain))
      .filter((skill) => !options.common || matchCommonStage(skill, options.common))
      .filter((skill) => !options.stage || skill.stage === options.stage);
  }
  if (!skills.length && !options.preset && options.domain && profiles[options.domain]) {
    skills = selectSkills(manifest, profiles, { preset: options.domain }).skills;
  }
  const lines = [`OpenHarmony skills (${skills.length})`];
  for (const skill of skills) {
    lines.push(`${skill.name} [${formatSkillBracket(skill)}]`);
    lines.push(`  ${skill.description}`);
  }
  return lines.join("\n");
}

async function renderInstall(options) {
  const manifest = await withSpinner(process.stdout, "Refreshing skill catalog", () =>
    loadRuntimeManifest({ env: process.env, offline: options.offline }),
  );
  const profiles = loadProfiles();
    const selectionOptions = {
    names: options.names,
    domain: options.domain,
    common: options.common,
    stage: options.stage,
    preset: options.preset || (options.domain && profiles[options.domain] ? options.domain : undefined),
    allCommon: options.allCommon,
  };
  const selection = selectSkills(manifest, profiles, selectionOptions);

  if (!selection.skills.length) {
    if (selection.unknownNames.length > 0) {
      throw new Error(buildUnknownSuggestions(selection.unknownNames, manifest));
    }
    throw new Error(buildNoMatchError(options, manifest, profiles));
  }

  const targets = resolveAgentTargets({
    agent: options.agent || "codex",
    scope: options.scope || "user",
    target: options.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });

  return executeInstall(manifest, selection, options, targets);
}

async function renderPluginInstall(options) {
  const pluginName = options.names[0];
  const targets = resolveAgentTargets({
    agent: options.agent || "codex",
    scope: options.scope || "user",
    target: options.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });
  const sourceRoot = ensurePluginSourceRoot(pluginName, { env: process.env });
  const plan = loadAndPlanPluginInstall(sourceRoot, pluginName, targets, {
    source: process.env.OH_SKILLHUB_SOURCE,
    ref: process.env.OH_SKILLHUB_REF,
    force: options.force,
  });

  if (options.dryRun) {
    return renderPluginInstallPlan("Plugin dry run install plan", plan);
  }
  const conflicts = plan.operations.filter((operation) => operation.action === "conflict");
  if (conflicts.length) {
    throw new Error(`${renderPluginInstallPlan("Plugin install conflicts", { operations: conflicts })}\nUse --force to replace unmanaged plugin payload directories.`);
  }

  const applied = applyPluginInstallPlan(plan, {
    source: process.env.OH_SKILLHUB_SOURCE,
    ref: process.env.OH_SKILLHUB_REF,
    packageVersion: packageJson.version,
    skipHostRegistration: process.env.OH_SKILLHUB_SKIP_HOST_REGISTRATION === "1",
  });
  return renderPluginInstallPlan("Plugin install summary", { operations: applied });
}

function renderPluginUninstall(options) {
  const pluginNames = options.names || [];
  if (!pluginNames.length) {
    throw new Error("Usage: uninstall <plugin> [--agent codex|claude|opencode|all] [--scope user|project]");
  }
  const targets = resolveAgentTargets({
    agent: options.agent || "all",
    scope: options.scope || "user",
    target: options.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });
  const plan = planPluginUninstall(pluginNames, targets, { force: options.force });
  if (options.dryRun) {
    return renderPluginUninstallPlan("Plugin dry run uninstall plan", plan);
  }
  const applied = applyPluginUninstallPlan(plan, {
    force: options.force,
    skipHostRegistration: process.env.OH_SKILLHUB_SKIP_HOST_REGISTRATION === "1",
  });
  return renderPluginUninstallPlan("Plugin uninstall summary", { operations: applied });
}

function renderPluginUpdate(options) {
  const pluginNames = options.names || [];
  if (!pluginNames.length) {
    throw new Error("Usage: update <plugin> [--agent codex|claude|opencode|all] [--scope user|project]");
  }
  const targets = resolveAgentTargets({
    agent: options.agent || "all",
    scope: options.scope || "user",
    target: options.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });
  const plan = { operations: [] };
  for (const pluginName of pluginNames) {
    const sourceRoot = ensurePluginSourceRoot(pluginName, { env: process.env });
    const installedTargets = targets.filter((target) => readPluginRecord(target.dir, pluginName, target.agent));
    const missingTargets = targets.filter((target) => !readPluginRecord(target.dir, pluginName, target.agent));
    const installPlan = loadAndPlanPluginInstall(sourceRoot, pluginName, installedTargets, {
      source: process.env.OH_SKILLHUB_SOURCE,
      ref: process.env.OH_SKILLHUB_REF,
      skipUnchanged: true,
    });
    plan.operations.push(...installPlan.operations);
    if (missingTargets.length) {
      const missingPlan = loadAndPlanPluginInstall(sourceRoot, pluginName, missingTargets, {
        source: process.env.OH_SKILLHUB_SOURCE,
        ref: process.env.OH_SKILLHUB_REF,
        skipUnchanged: true,
      });
      plan.operations.push(...missingPlan.operations.map((operation) => ({ ...operation, action: "missing" })));
    }
  }

  if (options.dryRun) {
    return renderPluginInstallPlan("Plugin dry run update plan", plan);
  }
  const applied = applyPluginInstallPlan(plan, {
    source: process.env.OH_SKILLHUB_SOURCE,
    ref: process.env.OH_SKILLHUB_REF,
    packageVersion: packageJson.version,
    skipHostRegistration: process.env.OH_SKILLHUB_SKIP_HOST_REGISTRATION === "1",
  });
  return renderPluginInstallPlan("Plugin update summary", { operations: mergeAppliedPluginOperations(plan.operations, applied) });
}

function renderPluginCommand(args) {
  const subcommand = args[0];
  if (!subcommand || subcommand === "--help" || subcommand === "-h") {
    return pluginHelpText();
  }
  const options = parseArgs(args.slice(1));
  if (subcommand === "list") {
    return renderPluginList(options);
  }
  if (subcommand === "status" || subcommand === "doctor") {
    const pluginName = options.names[0];
    if (!pluginName) {
      throw new Error(`Usage: plugin ${subcommand} <plugin> [--agent codex|claude|opencode|all] [--scope user|project]`);
    }
    if (options.fix && !options.scanLegacy) {
      throw new Error("--fix requires --scan-legacy");
    }
    if (options.force && !options.fix) {
      throw new Error("--force requires --fix (use with --scan-legacy --fix)");
    }
    if (options.scanLegacy && subcommand !== "doctor") {
      throw new Error("--scan-legacy requires plugin doctor");
    }
    const targets = resolveAgentTargets({
      agent: options.agent || "all",
      scope: options.scope || "user",
      target: options.target,
      cwd: process.cwd(),
      homeDir: os.homedir(),
      env: process.env,
      migrate: options.scanLegacy ? false : undefined,
    });
    if (options.scanLegacy) {
      return renderPluginLegacyDoctor(pluginName, targets, options);
    }
    const report = inspectPlugin(pluginName, targets, {
      scope: options.scope || "user",
      cwd: process.cwd(),
      homeDir: os.homedir(),
      allScopes: options.allScopes,
      allowMaterializedCopy: options.allowMaterializedCopy,
    });
    return subcommand === "doctor"
      ? renderPluginDoctor(report)
      : renderPluginStatus(report, { verbose: options.verbose });
  }
  if (subcommand === "install" || subcommand === "update") {
    const pluginName = options.names[0];
    if (!pluginName) {
      throw new Error(`Usage: plugin ${subcommand} <plugin> [--agent codex|claude|opencode|all] [--scope user|project]`);
    }
    const targets = resolveAgentTargets({
      agent: options.agent || "all",
      scope: options.scope || "user",
      target: options.target,
      cwd: process.cwd(),
      homeDir: os.homedir(),
      env: process.env,
    });
    const sourceRoot = ensurePluginSourceRoot(pluginName, { env: process.env });
    const plugin = loadNeutralPlugin(sourceRoot, pluginName);
    if (options.dryRun) {
      return renderPluginV2DryRun(subcommand, plugin, targets, {
        cwd: process.cwd(),
        homeDir: os.homedir(),
        env: process.env,
      });
    }
    const apply = subcommand === "update" ? updatePluginV2 : installPluginV2;
    const result = apply(plugin, targets, {
      source: process.env.OH_SKILLHUB_SOURCE,
      ref: process.env.OH_SKILLHUB_REF,
      packageVersion: packageJson.version,
      cwd: process.cwd(),
      homeDir: os.homedir(),
      force: options.force,
      atomic: options.atomic,
      autoGc: options.autoGc,
      strict: options.strict,
      replaceSameVersion: options.replaceSameVersion,
      allowDowngrade: options.allowDowngrade,
      skipHostRegistration: process.env.OH_SKILLHUB_SKIP_HOST_REGISTRATION === "1",
    });
    return renderPluginV2InstallResult(result);
  }
  if (subcommand === "uninstall") {
    const pluginName = options.names[0];
    if (!pluginName) {
      throw new Error("Usage: plugin uninstall <plugin> [--agent codex|claude|opencode|all] [--scope user|project]");
    }
    const targets = resolveAgentTargets({
      agent: options.agent || "all",
      scope: options.scope || "user",
      target: options.target,
      cwd: process.cwd(),
      homeDir: os.homedir(),
      env: process.env,
    });
    if (options.dryRun) {
      return renderPluginV2UninstallDryRun(pluginName, targets);
    }
    const result = uninstallPluginV2(pluginName, targets, {
      cwd: process.cwd(),
      homeDir: os.homedir(),
      atomic: options.atomic,
      skipHostRegistration: process.env.OH_SKILLHUB_SKIP_HOST_REGISTRATION === "1",
    });
    return renderPluginV2UninstallResult(result);
  }
  if (subcommand === "manifest") {
    const pluginName = options.names[0];
    if (!pluginName) {
      throw new Error("Usage: plugin manifest <plugin> [--agent codex|claude|opencode|all] [--out <dir>] [--check] [--scope user|project]");
    }
    const targets = resolveAgentTargets({
      agent: options.agent || "all",
      scope: options.scope || "user",
      target: options.target,
      cwd: process.cwd(),
      homeDir: os.homedir(),
      env: process.env,
    });
    const sourceRoot = ensurePluginSourceRoot(pluginName, { env: process.env });
    const agents = targets.map((target) => target.agent);
    if (options.check) {
      const report = checkVersion({
        sourceRoot,
        pluginName,
        agents,
        scope: options.scope || "user",
        cwd: process.cwd(),
        homeDir: os.homedir(),
      });
      return renderManifestVersionCheck(report);
    }
    const outDir = options.out
      ? path.resolve(process.cwd(), options.out)
      : path.join(process.cwd(), "dist");
    const report = generateManifests({ sourceRoot, pluginName, agents, outDir });
    return renderManifestGeneration(report);
  }
  if (subcommand === "gc") {
    const scopeRoot = scopeRootFor({
      scope: options.scope || "user",
      cwd: process.cwd(),
      homeDir: os.homedir(),
    });
    const report = collectGarbage(scopeRoot, { dryRun: options.dryRun });
    return renderPluginGc(report, { dryRun: options.dryRun, scopeRoot });
  }
  if (subcommand === "repair") {
    const scopeRoot = scopeRootFor({
      scope: options.scope || "user",
      cwd: process.cwd(),
      homeDir: os.homedir(),
    });
    const report = repairScope(scopeRoot);
    return renderPluginRepair(report, { scopeRoot });
  }
  if (subcommand === "enable" || subcommand === "disable") {
    const pluginName = options.names[0];
    if (!pluginName || !options.agent || options.agent === "all") {
      throw new Error(`Usage: plugin ${subcommand} <plugin> --agent codex|claude|opencode [--scope user|project]`);
    }
    const scopeRoot = scopeRootFor({
      scope: options.scope || "user",
      cwd: process.cwd(),
      homeDir: os.homedir(),
    });
    const result = setHostEnabled(scopeRoot, pluginName, options.agent, subcommand === "enable");
    return renderPluginHostToggle(result);
  }
  if (subcommand === "migrate") {
    const pluginName = options.names[0];
    if (!pluginName) {
      throw new Error("Usage: plugin migrate <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--atomic]");
    }
    const targets = resolveAgentTargets({
      agent: options.agent || "all",
      scope: options.scope || "user",
      target: options.target,
      cwd: process.cwd(),
      homeDir: os.homedir(),
      env: process.env,
    });
    const sourceRoot = ensurePluginSourceRoot(pluginName, { env: process.env });
    const plugin = loadNeutralPlugin(sourceRoot, pluginName);
    if (options.dryRun) {
      return renderPluginV2DryRun("migrate", plugin, targets, {
        cwd: process.cwd(),
        homeDir: os.homedir(),
        env: process.env,
      });
    }
    const result = migratePlugin(plugin, targets, {
      source: process.env.OH_SKILLHUB_SOURCE,
      ref: process.env.OH_SKILLHUB_REF,
      packageVersion: packageJson.version,
      cwd: process.cwd(),
      homeDir: os.homedir(),
      atomic: options.atomic,
      autoGc: options.autoGc,
      skipHostRegistration: process.env.OH_SKILLHUB_SKIP_HOST_REGISTRATION === "1",
    });
    return renderPluginMigrateResult(result);
  }
  throw new Error(`Unknown plugin command "${subcommand}". Run oh-skillhub plugin --help.`);
}

function renderPluginList(options = {}) {
  const sourceRoot = ensurePluginWorkspaceRoot({
    env: process.env,
    cwd: process.cwd(),
  });
  const plugins = listWorkspacePlugins(sourceRoot);
  const lines = [
    "Available plugins",
    `source: ${sourceRoot}`,
  ];
  if (!plugins.length) {
    lines.push("  (none)");
    return lines.join("\n");
  }
  for (const plugin of plugins) {
    const summary = readWorkspacePluginSummary(sourceRoot, plugin);
    const detail = [
      summary.version ? `v${summary.version}` : "",
      summary.description || plugin.description || "",
    ].filter(Boolean).join(" - ");
    lines.push(detail ? `  ${plugin.id}  ${detail}` : `  ${plugin.id}`);
    if (options.verbose) {
      lines.push(`    manifest: ${plugin.path}`);
    }
  }
  return lines.join("\n");
}

function readWorkspacePluginSummary(sourceRoot, plugin) {
  const root = path.resolve(sourceRoot);
  const manifestPath = path.resolve(root, plugin.path);
  const relative = path.relative(root, manifestPath);
  if (relative.startsWith("..") || path.isAbsolute(relative) || !fs.existsSync(manifestPath)) {
    return {};
  }
  try {
    const manifest = parsePluginManifest(fs.readFileSync(manifestPath, "utf8"));
    return {
      version: manifest.version || "",
      description: manifest.description || "",
    };
  } catch {
    return {};
  }
}

function renderPluginV2DryRun(action, plugin, targets, options = {}) {
  const label = action.toUpperCase();
  const lines = [`Plugin v2 dry run ${action} plan`, `${label} ${plugin.name} -> scope ${targets[0] ? targets[0].scope : "user"}`];
  const precheck = precheckPluginV2Install(plugin, targets, options);
  if (precheck.conflicts.length) {
    lines.push("", "Conflicts (install will be blocked without --force):");
    for (const conflict of precheck.conflicts) {
      lines.push(`  ${conflict.message}`);
    }
  }
  if (precheck.warnings.length) {
    lines.push("", "Warnings:");
    for (const warning of precheck.warnings) {
      lines.push(`  ${warning.message}`);
    }
  }
  for (const target of targets) {
    lines.push(`  projection: ${target.agent}:${target.scope}`);
  }
  lines.push("  store: canonical digest will be computed from neutral source files");
  lines.push("  registry: scope registry will be updated");
  lines.push("  channel: .ohos-channel marker will be written (openharmony-skills)");
  return lines.join("\n");
}

function renderPluginV2InstallResult(result) {
  const label = result.action.toUpperCase();
  const lines = [
    `Plugin v2 ${result.action} summary`,
    `${label} ${result.plugin.name} -> scope ${result.scope}`,
    `  store: ${result.store.storeRoot}`,
    `  digest: ${result.store.digest}`,
    `  registry: ${result.registryPath}`,
  ];
  if (result.warnings && result.warnings.length) {
    lines.push("  warnings:");
    for (const warning of result.warnings) {
      lines.push(`    ${warning.message}`);
    }
  }
  for (const item of result.targets) {
    lines.push(`  projection: ${item.target.agent}:${item.target.scope} ${item.payload.payloadRoot}`);
    lines.push(`    mode: ${item.projection.mode}`);
    if (item.channelMarker) {
      lines.push(`    channel marker: ${item.channelMarker}`);
    }
    if (item.registration && item.registration.skipped) {
      lines.push("    registration: skipped");
    } else if (item.registration) {
      lines.push(`    registration: ${item.registration.success ? "ok" : "failed"} (${item.registration.command})`);
      if (item.registration.success && item.registration.nextCommand) {
        lines.push(`    next: ${item.registration.nextCommand}`);
      }
      if (!item.registration.success && item.registration.message) {
        lines.push(`    registration detail: ${item.registration.message.split(/\r?\n/)[0]}`);
      }
    }
  }
  return lines.join("\n");
}

function renderPluginV2UninstallDryRun(pluginName, targets) {
  const lines = ["Plugin v2 dry run uninstall plan", `UNINSTALL ${pluginName} -> scope ${targets[0] ? targets[0].scope : "user"}`];
  for (const target of targets) {
    lines.push(`  projection: ${target.agent}:${target.scope}`);
  }
  lines.push("  registry: selected host references will be removed");
  lines.push("  store: retained until plugin gc");
  return lines.join("\n");
}

function renderPluginV2UninstallResult(result) {
  const lines = [
    "Plugin v2 uninstall summary",
    `UNINSTALL ${result.pluginName} -> scope ${result.scope}`,
    `  registry: ${result.registryPath}`,
    `  store: ${result.pendingGc ? "pending-gc" : "retained"}`,
  ];
  for (const item of result.targets) {
    lines.push(`  projection: ${item.target.agent}:${item.target.scope} ${item.payloadRoot}`);
    if (item.registration && item.registration.skipped) {
      lines.push("    registration: skipped");
    } else if (item.registration) {
      lines.push(`    registration: ${item.registration.success ? "ok" : "failed"} (${item.registration.command})`);
    }
  }
  return lines.join("\n");
}

function renderPluginGc(report, options = {}) {
  const action = options.dryRun ? "GC dry run" : "GC summary";
  const lines = [
    `Plugin ${action}`,
    `scope root: ${options.scopeRoot || ""}`,
    `  store dirs: ${report.storeDirs.length}`,
    `  projections: ${report.projections.length}`,
    `  trash dirs: ${report.trashDirs.length}`,
    `  staging roots: ${report.stagingRoots.length}`,
  ];
  if (report.registryCleaned && report.registryCleaned.length) {
    lines.push(`  registry cleaned: ${report.registryCleaned.join(", ")}`);
  }
  const all = [
    ...report.storeDirs.map((item) => `store: ${item}`),
    ...report.projections.map((item) => `projection: ${item}`),
    ...report.trashDirs.map((item) => `trash: ${item}`),
    ...report.stagingRoots.map((item) => `staging: ${item}`),
  ];
  if (all.length) {
    lines.push(options.dryRun ? "would remove:" : "removed:");
    lines.push(...all);
  }
  return lines.join("\n");
}

function renderPluginRepair(report, options = {}) {
  const lines = [
    "Plugin repair",
    `scope root: ${options.scopeRoot || ""}`,
  ];
  const recovery = report.recovery || [];
  if (recovery.length) {
    lines.push("journal recovery:");
    for (const item of recovery) {
      lines.push(`  ${item.txid}: ${item.result}`);
    }
  } else {
    lines.push("journal recovery: none");
  }
  const gc = report.gc || {};
  lines.push(
    `gc: store ${gc.storeDirs ? gc.storeDirs.length : 0}, projections ${gc.projections ? gc.projections.length : 0}, trash ${gc.trashDirs ? gc.trashDirs.length : 0}, staging ${gc.stagingRoots ? gc.stagingRoots.length : 0}`,
  );
  return lines.join("\n");
}

function renderPluginHostToggle(result) {
  return `Plugin host ${result.enabled ? "enabled" : "disabled"}: ${result.pluginName} (${result.agent})`;
}

function renderPluginMigrateResult(result) {
  const lines = [
    "Plugin migrate summary",
    `MIGRATE ${result.plugin.name} -> scope ${result.scope}`,
    `  store: ${result.store.storeRoot}`,
    `  digest: ${result.store.digest}`,
    `  registry: ${result.registryPath}`,
  ];
  if (result.legacyCleaned && result.legacyCleaned.length) {
    lines.push("  legacy payloads cleaned:");
    for (const item of result.legacyCleaned) {
      lines.push(`    ${item.agent}:${item.scope} (${item.filesRemoved} file(s) removed)`);
    }
  } else {
    lines.push("  legacy payloads: none");
  }
  for (const item of result.legacyUntouched || []) {
    lines.push(`  ${item.agent}:${item.scope}: no legacy payload to clean`);
  }
  return lines.join("\n");
}

function renderPluginLegacyDoctor(pluginName, targets, options) {
  const legacyOptions = {
    scope: options.scope || "user",
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  };
  const report = scanLegacy(pluginName, targets, legacyOptions);
  if (!options.fix) {
    return renderLegacyDoctor(report);
  }
  const plan = planLegacyCleanup(report, { force: options.force });
  const applied = applyLegacyCleanup(plan, {
    homeDir: os.homedir(),
    force: options.force,
    skipHostCli: process.env.OH_SKILLHUB_SKIP_HOST_REGISTRATION === "1",
    skipBackup: false,
  });
  const lines = [
    renderLegacyDoctor(report),
    "",
    renderLegacyCleanup("Legacy cleanup summary", applied, plan.mode),
  ];
  const v2Report = inspectPlugin(pluginName, targets, {
    scope: options.scope || "user",
    cwd: process.cwd(),
    homeDir: os.homedir(),
  });
  lines.push("", renderPluginDoctor(v2Report));
  return lines.join("\n");
}

function workflowPluginNamespaceError(action, pluginName) {
  return [
    `Workflow plugin ${action} has moved to the plugin namespace.`,
    `Use: npx oh-skillhub plugin ${action} ${pluginName} --agent all --scope user`,
  ].join("\n");
}

function mergeAppliedPluginOperations(operations, applied) {
  const byKey = new Map(applied.map((operation) => [pluginOperationKey(operation), operation]));
  return operations.map((operation) => byKey.get(pluginOperationKey(operation)) || operation);
}

function pluginOperationKey(operation) {
  return `${operation.plugin.name}:${operation.target.agent}:${operation.target.scope}:${operation.target.dir}`;
}

function buildUnknownSuggestions(unknownNames, manifest) {
  const lines = [];
  for (const name of unknownNames) {
    const suggestions = suggestSkillName(name, manifest);
    const suggested = suggestions
      .filter((item) => item.score > 0.4)
      .map((item) => item.name)
      .join(", ");
    if (suggested) {
      lines.push(`Unknown skill: ${name}`);
      lines.push(`  ${name} -> did you mean: ${suggested}?`);
    } else {
      lines.push(`Unknown skill: ${name}`);
    }
  }
  return lines.join("\n");
}

function buildNoMatchError(options, manifest, profiles) {
  if (options.domain) {
    return buildUnknownDomainError(options.domain, manifest, profiles);
  }
  if (options.common) {
    return buildUnknownCommonStageError(options.common, manifest);
  }
  if (options.stage) {
    const stages = uniqueSkillValues(manifest.skills, (skill) => skill.stage);
    return `Unknown stage: ${options.stage}\n  Available stages: ${stages.join(", ")}`;
  }
  return "No skills matched the install request.";
}

function buildUnknownDomainError(domain, manifest, profiles) {
  const domains = collectAvailableDomains(manifest);
  const lines = [`Unknown domain: ${domain}`];
  const stageHint = COMMON_STAGES.find(
    (stage) => stage.startsWith(domain) || domain.startsWith(stage),
  );
  if (stageHint) {
    lines.push(`  "${domain}" looks like a stage. Try: --stage ${stageHint}`);
  }
  const profileHint = Object.keys(profiles).find(
    (name) => name.startsWith(domain) || domain.startsWith(name),
  );
  if (profileHint) {
    lines.push(`  "${domain}" looks like a preset. Try: --preset ${profileHint}`);
  }
  lines.push(`  Available domains: ${domains.join(", ")}`);
  return lines.join("\n");
}

function collectDomainFolders(manifest) {
  const set = new Set();
  for (const skill of manifest.skills) {
    if (!skill.path) continue;
    const parts = skill.path.split("/");
    if (parts[0] === "skills" && parts[1] === "domain" && parts[2]) {
      set.add(parts[2]);
    }
  }
  return Array.from(set).sort();
}

function collectCommonStages(manifest) {
  const set = new Set();
  for (const skill of manifest.skills) {
    if (skill.scope === "common" && skill.stage) set.add(skill.stage);
    if (skill.path) {
      const parts = skill.path.split("/");
      if (parts[0] === "skills" && parts[1] === "common" && parts[2]) {
        set.add(parts[2]);
      }
    }
  }
  return Array.from(set).sort();
}

function buildUnknownCommonStageError(stage, manifest) {
  const stages = collectCommonStages(manifest);
  return `Unknown common stage: ${stage}\n  Available common stages: ${stages.join(", ")}`;
}

function collectAvailableDomains(manifest) {
  const set = new Set(collectDomainFolders(manifest));
  for (const skill of manifest.skills) {
    if (skill.domain) set.add(skill.domain);
  }
  return Array.from(set).sort();
}

function uniqueSkillValues(skills, selector) {
  return Array.from(new Set(skills.map(selector).filter(Boolean))).sort();
}

async function executeInstall(sourceManifest, selection, options, targets) {
  const plan = planInstall(selection.skills, targets);

  if (options.dryRun) {
    return renderInstallPlan("Dry run install plan", plan);
  }

  const sourceRoot = ensureSkillSourceRoot(sourceManifest, { env: process.env, skills: selection.skills });
  const applied = applyInstallPlan(plan, {
    sourceRoot,
    source: sourceManifest.source,
    ref: sourceManifest.ref,
    sourceRevision: sourceManifest.sourceRevision,
  });
  await recordTelemetryEvents(applied, {
    enabled: !options.noTelemetry && !options.offline,
    selection: {
      profile: options.preset || options.domain || null,
    },
  });

  return renderInstallPlan("Install summary", plan);
}

async function runInteractiveInstaller(input = process.stdin, output = process.stdout) {
  if (!(input.isTTY && output.isTTY)) {
    await runInteractiveInstallerFromAnswers(splitPromptAnswers(await readAll(input)), output);
    return;
  }

  const manifest = await loadInteractiveManifest(input, output);
  const choices = buildRepositoryChoices(manifest);
  const agent = await runRawAgentSelection(input, output);
  const selectedIndexes = await runRawTuiSelection(input, output, choices, agent);
  await installInteractiveSelection(manifest, choices, agent, selectedIndexes, output);
}

async function runInteractiveInstallerFromAnswers(answers, output = process.stdout) {
  const manifest = await loadRuntimeManifest({ env: process.env });
  const choices = buildRepositoryChoices(manifest);
  const [agentAnswer, groupAnswer] = answers.length > 1 ? [answers[0], answers.slice(1).join(" ")] : ["", answers[0] || ""];
  const defaultGroup = defaultGroupNumber(choices);
  output.write(renderAgentMenu());
  output.write("Select target [1]: \n");
  const agent = parseAgentSelection(agentAnswer);
  output.write(renderTuiMenu(choices, agent));
  output.write(`Select groups [${defaultGroup}]: \n`);
  const selectedIndexes = parseSelection(groupAnswer, choices.length, defaultGroup);
  await installInteractiveSelection(manifest, choices, agent, selectedIndexes, output);
}

async function loadInteractiveManifest(_input, output) {
  if (output.isTTY) {
    return withSpinner(output, "Refreshing skill catalog", () => loadRuntimeManifest({ env: process.env }));
  }
  return loadRuntimeManifest({ env: process.env });
}

async function installInteractiveSelection(manifest, choices, agent, selectedIndexes, output) {
  const selectedChoices = selectedIndexes.map((index) => choices[index]);
  const skills = selectSkillsForChoices(manifest, selectedChoices);
  if (!skills.length) {
    throw new Error("No skills matched the selected groups.");
  }
  output.write(`\nInstalling ${selectedChoices.map((choice) => choice.path).join(", ")} for ${formatAgentSelection(agent)}:user...\n`);
  const sourceRoot = await withSpinner(output, `Preparing skill source from ${manifest.source}#${manifest.ref}`, () =>
    ensureSkillSourceRootAsync(manifest, { env: process.env, skills }),
  );
  output.write(`${await renderInstallForSkills(skills, { agent, scope: "user" }, sourceRoot, manifest)}\n`);
  output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" }, output)}\n`);
}

async function withSpinner(output, message, task, options = {}) {
  if (!output.isTTY) {
    output.write(`${message}...\n`);
    return task();
  }

  const frames = options.frames || ["|", "/", "-", "\\"];
  const intervalMs = options.intervalMs || 120;
  let index = 0;
  const render = () => {
    output.write(`\r${frames[index % frames.length]} ${message}...`);
    index += 1;
  };

  render();
  const timer = setInterval(render, intervalMs);
  try {
    const result = await task();
    clearInterval(timer);
    output.write(`\r${" ".repeat(message.length + 8)}\r✓ ${message}\n`);
    return result;
  } catch (error) {
    clearInterval(timer);
    output.write(`\r${" ".repeat(message.length + 8)}\r! ${message}\n`);
    throw error;
  }
}

function readAll(input) {
  return new Promise((resolve, reject) => {
    let text = "";
    input.setEncoding("utf8");
    input.on("data", (chunk) => {
      text += chunk;
    });
    input.on("end", () => resolve(text));
    input.on("error", reject);
  });
}

function splitPromptAnswers(text) {
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").filter((line, index, lines) => {
    return line.length > 0 || index < lines.length - 1;
  });
}

function buildRepositoryChoices(manifest) {
  const choices = [];
  const commonSkills = manifest.skills.filter((skill) => skill.scope === "common");
  choices.push({
    scope: "common",
    stage: "all",
    path: "common / all",
    count: commonSkills.length,
    skills: commonSkills,
    allCommon: true,
  });
  for (const stage of COMMON_STAGES) {
    const skills = manifest.skills.filter((skill) => skill.scope === "common" && skill.stage === stage);
    choices.push({ scope: "common", stage, path: `common / ${stage}`, count: skills.length, skills });
  }
  for (const domain of collectDomainFolders(manifest)) {
    const skills = manifest.skills.filter((skill) => skill.scope === "domain" && matchDomain(skill, domain));
    choices.push({ scope: "domain", domain, path: `domain / ${domain}`, count: skills.length, skills });
  }
  return choices;
}

function renderTuiMenu(choices, agent = "codex") {
  const width = 72;
  const line = `+${"-".repeat(width - 2)}+`;
  const lines = [
    line,
    `| ${padRight("OH SkillHub", width - 4)} |`,
    `| ${padRight("OpenHarmony Skills Installer", width - 4)} |`,
    line,
    "",
    "Target",
    `  Agent: ${formatAgentSelection(agent)}`,
    "  Scope: user",
    "",
    "Choose skill groups",
    "  Interactive TTY: Up/Down or j/k to move, Space to toggle, Enter to install",
    "  Piped input: enter numbers separated by spaces, e.g. 1 for all common",
    "",
  ];
  choices.forEach((choice, index) => {
    const marker = choice.count > 0 ? "[ ]" : "[ ]";
    lines.push(`  ${String(index + 1).padStart(2, " ")}. ${marker} ${choice.path.padEnd(28, " ")} ${choice.count} skill(s)`);
    for (const leaf of renderSkillLeaves(choice)) {
      lines.push(`       - ${leaf}`);
    }
  });
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function renderAgentMenu() {
  const width = 72;
  const line = `+${"-".repeat(width - 2)}+`;
  const lines = [
    line,
    `| ${padRight("OH SkillHub", width - 4)} |`,
    `| ${padRight("OpenHarmony Skills Installer", width - 4)} |`,
    line,
    "",
    "Choose install target",
    "  Pick where the selected skills should be installed.",
    "",
  ];
  AGENT_CHOICES.forEach((choice, index) => {
    lines.push(`  ${index + 1}. [ ] ${choice.label.padEnd(10, " ")} ${choice.hint}`);
  });
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function renderActionMenu() {
  const width = 72;
  const line = `+${"-".repeat(width - 2)}+`;
  const lines = [
    line,
    `| ${padRight("OH SkillHub", width - 4)} |`,
    `| ${padRight("OpenHarmony Skills Manager", width - 4)} |`,
    line,
    "",
    "Choose action",
    "  Install new skills or clean existing skills from an agent target.",
    "",
  ];
  ACTION_CHOICES.forEach((choice, index) => {
    lines.push(`  ${index + 1}. [ ] ${choice.label.padEnd(22, " ")} ${choice.hint}`);
  });
  lines.push("");
  return `${lines.join("\n")}\n`;
}

async function runPromptAgentSelection(input, output, existingInterface) {
  output.write(renderAgentMenu());
  const rl = existingInterface || readlinePromises.createInterface({ input, output });
  try {
    const answer = await rl.question("Select target [1]: ");
    return AGENT_CHOICES[parseSingleSelection(answer, AGENT_CHOICES.length, 1)].agent;
  } finally {
    if (!existingInterface) rl.close();
  }
}

async function runPromptSelection(input, output, choices, agent = "codex", existingInterface) {
  output.write(renderTuiMenu(choices, agent));
  const rl = existingInterface || readlinePromises.createInterface({ input, output });
  try {
    const answer = await rl.question("Select groups [9]: ");
    return parseSelection(answer, choices.length, 9);
  } finally {
    if (!existingInterface) rl.close();
  }
}

function runRawTuiSelection(input, output, choices, agent = "codex") {
  return new Promise((resolve, reject) => {
    let cursor = 8;
    const selected = new Set();
    let notice = "";
    const wasRaw = input.isRaw;

    prepareRawInput(input);

    function render() {
      output.write("\x1b[2J\x1b[H");
      output.write(renderRawTuiMenu(choices, cursor, selected, agent, notice));
      notice = "";
    }

    function cleanup() {
      input.removeListener("keypress", onKeypress);
      releaseRawInput(input, wasRaw);
      output.write("\x1b[?25h");
    }

    function onKeypress(_str, key) {
      if (key && key.ctrl && key.name === "c") {
        cleanup();
        reject(new Error("Cancelled."));
        return;
      }
      if (key && (key.name === "down" || key.name === "j")) {
        cursor = (cursor + 1) % choices.length;
        render();
        return;
      }
      if (key && (key.name === "up" || key.name === "k")) {
        cursor = (cursor - 1 + choices.length) % choices.length;
        render();
        return;
      }
      if (key && key.name === "space") {
        if (selected.has(cursor)) selected.delete(cursor);
        else selected.add(cursor);
        render();
        return;
      }
      if (key && key.name === "return") {
        if (!selected.size) {
          notice = "Select at least one group with Space.";
          render();
          return;
        }
        cleanup();
        resolve(Array.from(selected).sort((a, b) => a - b));
      }
    }

    output.write("\x1b[?25l");
    render();
    input.on("keypress", onKeypress);
  });
}

function runRawAgentSelection(input, output) {
  return new Promise((resolve, reject) => {
    let cursor = 0;
    const selected = new Set();
    let notice = "";
    const wasRaw = input.isRaw;

    prepareRawInput(input);

    function render() {
      output.write("\x1b[2J\x1b[H");
      output.write(renderRawAgentMenu(cursor, selected, notice));
      notice = "";
    }

    function cleanup() {
      input.removeListener("keypress", onKeypress);
      releaseRawInput(input, wasRaw);
      output.write("\x1b[?25h");
    }

    function onKeypress(_str, key) {
      if (key && key.ctrl && key.name === "c") {
        cleanup();
        reject(new Error("Cancelled."));
        return;
      }
      if (key && (key.name === "down" || key.name === "j")) {
        cursor = (cursor + 1) % AGENT_CHOICES.length;
        render();
        return;
      }
      if (key && (key.name === "up" || key.name === "k")) {
        cursor = (cursor - 1 + AGENT_CHOICES.length) % AGENT_CHOICES.length;
        render();
        return;
      }
      if (key && key.name === "space") {
        toggleAgentSelection(selected, cursor);
        render();
        return;
      }
      if (key && key.name === "return") {
        if (!normalizeAgentIndexes(selected).size) {
          notice = "Select at least one target with Space.";
          render();
          return;
        }
        cleanup();
        resolve(agentSelectionFromIndexes(selected, cursor));
      }
    }

    output.write("\x1b[?25l");
    render();
    input.on("keypress", onKeypress);
  });
}

function runRawActionSelection(input, output) {
  return new Promise((resolve, reject) => {
    let cursor = 0;
    let selected = null;
    let notice = "";
    const wasRaw = input.isRaw;

    prepareRawInput(input);

    function render() {
      output.write("\x1b[2J\x1b[H");
      output.write(renderRawActionMenu(cursor, selected, notice));
      notice = "";
    }

    function cleanup() {
      input.removeListener("keypress", onKeypress);
      releaseRawInput(input, wasRaw);
      output.write("\x1b[?25h");
    }

    function onKeypress(_str, key) {
      if (key && key.ctrl && key.name === "c") {
        cleanup();
        reject(new Error("Cancelled."));
        return;
      }
      if (key && (key.name === "down" || key.name === "j")) {
        cursor = (cursor + 1) % ACTION_CHOICES.length;
        render();
        return;
      }
      if (key && (key.name === "up" || key.name === "k")) {
        cursor = (cursor - 1 + ACTION_CHOICES.length) % ACTION_CHOICES.length;
        render();
        return;
      }
      if (key && key.name === "space") {
        selected = cursor;
        render();
        return;
      }
      if (key && key.name === "return") {
        if (selected === null) {
          notice = "Select an action with Space.";
          render();
          return;
        }
        cleanup();
        resolve(ACTION_CHOICES[selected].action);
      }
    }

    output.write("\x1b[?25l");
    render();
    input.on("keypress", onKeypress);
  });
}

function renderRawActionMenu(cursor, selected = null, notice = "") {
  const width = 76;
  const line = `+${"-".repeat(width - 2)}+`;
  const lines = [
    line,
    rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
    rawHeaderLine("OpenHarmony Skills Manager", width, ANSI.dim),
    line,
    "",
    colorize("Choose action", ANSI.bold),
    colorize("  Up/Down or j/k: move    Space: select    Enter: confirm    Ctrl+C: cancel", ANSI.dim),
    "",
  ];
  ACTION_CHOICES.forEach((choice, index) => {
    const pointer = index === cursor ? ">" : " ";
    const highlighted = index === cursor;
    const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(22, " ")} ${choice.hint}`;
    lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
  });
  if (notice) {
    lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
  }
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function prepareRawInput(input) {
  if (typeof input.on === "function" && typeof input.listenerCount === "function") {
    readline.emitKeypressEvents(input);
  }
  input.setRawMode(true);
  if (typeof input.resume === "function") {
    input.resume();
  }
}

function releaseRawInput(input, wasRaw) {
  input.setRawMode(Boolean(wasRaw));
  if (!wasRaw && typeof input.pause === "function") {
    input.pause();
  }
}

function renderRawAgentMenu(cursor, selected = new Set(), notice = "") {
  const width = 76;
  const line = `+${"-".repeat(width - 2)}+`;
  const selectedIndexes = normalizeAgentIndexes(selected);
  const lines = [
    line,
    rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
    rawHeaderLine("OpenHarmony Skills Installer", width, ANSI.dim),
    line,
    "",
    colorize("Choose install target", ANSI.bold),
    colorize("  Up/Down or j/k: move    Space: select    Enter: confirm    Ctrl+C: cancel", ANSI.dim),
    "",
  ];
  AGENT_CHOICES.forEach((choice, index) => {
    const pointer = index === cursor ? ">" : " ";
    const highlighted = index === cursor;
    const row = `${pointer} ${rawCheckbox(isAgentIndexSelected(selectedIndexes, index), highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
    lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
  });
  if (notice) {
    lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
  }
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function renderRawTuiMenu(choices, cursor, selected, agent = "codex", notice = "") {
  const width = 76;
  const line = `+${"-".repeat(width - 2)}+`;
  const lines = [
    line,
    rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
    rawHeaderLine("OpenHarmony Skills Installer", width, ANSI.dim),
    line,
    "",
    colorize("Target", ANSI.bold),
    `  Agent: ${agent}`,
    "  Scope: user",
    "",
    colorize("Choose skill groups", ANSI.bold),
    colorize("  Up/Down or j/k: move    Space: select    Enter: install    Ctrl+C: cancel", ANSI.dim),
    "",
  ];
  choices.forEach((choice, index) => {
    const pointer = index === cursor ? ">" : " ";
    const highlighted = index === cursor;
    const checkbox = rawCheckbox(selected.has(index), highlighted);
    const row = `${pointer} ${checkbox} ${choice.path.padEnd(28, " ")} ${choice.count} skill(s)`;
    lines.push(highlighted ? colorize(row, ANSI.reverse, ANSI.bold) : row);
    for (const leaf of renderSkillLeaves(choice)) {
      lines.push(colorize(`      - ${leaf}`, ANSI.dim));
    }
  });
  if (notice) {
    lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
  }
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function runRawCleanSelection(input, output, skills) {
  return new Promise((resolve, reject) => {
    let cursor = 0;
    const selected = new Set();
    let notice = "";
    const wasRaw = input.isRaw;

    prepareRawInput(input);

    function render() {
      output.write("\x1b[2J\x1b[H");
      output.write(renderRawCleanSelectionMenu(skills, cursor, selected, notice));
      notice = "";
    }

    function cleanup() {
      input.removeListener("keypress", onKeypress);
      releaseRawInput(input, wasRaw);
      output.write("\x1b[?25h");
    }

    function onKeypress(_str, key) {
      if (key && key.ctrl && key.name === "c") {
        cleanup();
        reject(new Error("Cancelled."));
        return;
      }
      if (key && (key.name === "down" || key.name === "j")) {
        cursor = (cursor + 1) % skills.length;
        render();
        return;
      }
      if (key && (key.name === "up" || key.name === "k")) {
        cursor = (cursor - 1 + skills.length) % skills.length;
        render();
        return;
      }
      if (key && key.name === "space") {
        if (selected.has(cursor)) selected.delete(cursor);
        else selected.add(cursor);
        render();
        return;
      }
      if (key && key.name === "return") {
        if (!selected.size) {
          notice = "Select at least one skill with Space.";
          render();
          return;
        }
        cleanup();
        resolve(Array.from(selected).sort((a, b) => a - b));
      }
    }

    output.write("\x1b[?25l");
    render();
    input.on("keypress", onKeypress);
  });
}

function renderRawCleanSelectionMenu(skills, cursor, selected, notice = "") {
  const width = 92;
  const line = `+${"-".repeat(width - 2)}+`;
  const lines = [
    line,
    rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
    rawHeaderLine("OpenHarmony Skills Cleaner", width, ANSI.dim),
    line,
    "",
    colorize("Detected skills", ANSI.bold),
    colorize("  Up/Down or j/k: move    Space: select    Enter: clean    Ctrl+C: cancel", ANSI.dim),
    "",
  ];
  skills.forEach((skill, index) => {
    const pointer = index === cursor ? ">" : " ";
    const highlighted = index === cursor;
    const checkbox = rawCheckbox(selected.has(index), highlighted);
    const group = formatSkillGroup([skill.domain, skill.stage]);
    const row = `${pointer} ${checkbox} ${skill.name.padEnd(36, " ")} ${skill.status.padEnd(9, " ")} ${group}`;
    lines.push(highlighted ? colorize(row, ANSI.reverse, ANSI.bold) : row);
  });
  if (notice) {
    lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
  }
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function colorize(value, ...codes) {
  return `${codes.join("")}${value}${ANSI.reset}`;
}

function rawHeaderLine(value, width, ...codes) {
  const styled = colorize(value, ...codes);
  return `| ${styled}${" ".repeat(width - 4 - value.length)} |`;
}

function rawCheckbox(isSelected, highlighted) {
  if (!isSelected) {
    return "[ ]";
  }
  const restore = highlighted ? `${ANSI.reverse}${ANSI.bold}` : "";
  return `${colorize("[✓]", ANSI.green, ANSI.bold)}${restore}`;
}

function renderSkillLeaves(choice) {
  return (choice.skills || []).map((skill) => {
    if (choice.scope === "common") {
      return skill.name;
    }
    const prefix = `skills/domain/${choice.domain}/`;
    if (skill.path.startsWith(prefix)) {
      return skill.path.slice(prefix.length).split("/").join(" / ");
    }
    return skill.name;
  });
}

function padRight(value, width) {
  return value.length >= width ? value.slice(0, width) : `${value}${" ".repeat(width - value.length)}`;
}

function parseSelection(answer, max, defaultNumber) {
  const text = answer.trim();
  const tokens = text ? text.split(/\s+/) : [String(defaultNumber)];
  const indexes = [];
  for (const token of tokens) {
    const number = Number(token);
    if (!Number.isInteger(number) || number < 1 || number > max) {
      throw new Error(`Invalid selection "${token}". Choose numbers from 1 to ${max}.`);
    }
    const index = number - 1;
    if (!indexes.includes(index)) {
      indexes.push(index);
    }
  }
  return indexes;
}

function parseSingleSelection(answer, max, defaultNumber) {
  const indexes = parseSelection(answer, max, defaultNumber);
  if (indexes.length !== 1) {
    throw new Error("Choose one target.");
  }
  return indexes[0];
}

function parseAgentSelection(answer) {
  const indexes = parseSelection(answer, AGENT_CHOICES.length, 1);
  if (indexes.includes(ALL_AGENT_INDEX)) {
    return "all";
  }
  const agents = indexes.map((index) => AGENT_CHOICES[index].agent);
  return agents.length === 1 ? agents[0] : agents;
}

function isSingleSelection(answer, max) {
  try {
    parseSingleSelection(answer, max, 1);
    return true;
  } catch {
    return false;
  }
}

function defaultGroupNumber(choices) {
  const arkuiIndex = choices.findIndex((choice) => choice.scope === "domain" && choice.domain === "arkui");
  return arkuiIndex >= 0 ? arkuiIndex + 1 : 1;
}

function normalizeAgentIndexes(selected) {
  const indexes = selected instanceof Set ? Array.from(selected) : [selected];
  if (indexes.includes(ALL_AGENT_INDEX)) {
    return new Set(INDIVIDUAL_AGENT_INDEXES);
  }
  return new Set(indexes.filter((index) => INDIVIDUAL_AGENT_INDEXES.includes(index)));
}

function isAgentIndexSelected(selected, index) {
  if (index === ALL_AGENT_INDEX) {
    return INDIVIDUAL_AGENT_INDEXES.every((item) => selected.has(item));
  }
  return selected.has(index);
}

function toggleAgentSelection(selected, cursor) {
  if (cursor === ALL_AGENT_INDEX) {
    if (INDIVIDUAL_AGENT_INDEXES.every((index) => selected.has(index))) {
      selected.clear();
      selected.add(0);
      return;
    }
    selected.clear();
    for (const index of INDIVIDUAL_AGENT_INDEXES) {
      selected.add(index);
    }
    return;
  }
  if (selected.has(cursor) && selected.size > 1) {
    selected.delete(cursor);
    return;
  }
  selected.add(cursor);
}

function agentSelectionFromIndexes(selected, cursor = 0) {
  const normalized = normalizeAgentIndexes(selected);
  if (!normalized.size) {
    normalized.add(INDIVIDUAL_AGENT_INDEXES.includes(cursor) ? cursor : 0);
  }
  if (INDIVIDUAL_AGENT_INDEXES.every((index) => normalized.has(index))) {
    return "all";
  }
  const agents = Array.from(normalized)
    .sort((left, right) => left - right)
    .map((index) => AGENT_CHOICES[index].agent);
  return agents.length === 1 ? agents[0] : agents;
}

function formatAgentSelection(agent) {
  if (Array.isArray(agent)) {
    return agent.join(", ");
  }
  return agent;
}

function selectSkillsForChoices(manifest, choices) {
  const selected = new Map();
  for (const choice of choices) {
    for (const skill of manifest.skills) {
      if (choice.allCommon && skill.scope === "common") {
        selected.set(skill.name, skill);
      }
      if (choice.scope === "common" && skill.scope === "common" && skill.stage === choice.stage) {
        selected.set(skill.name, skill);
      }
      if (choice.scope === "domain" && skill.scope === "domain" && matchDomain(skill, choice.domain)) {
        selected.set(skill.name, skill);
      }
    }
  }
  return Array.from(selected.values()).sort((left, right) => left.name.localeCompare(right.name));
}

async function renderInstallForSkills(skills, targetOptions, sourceRootOverride = null, manifestOverride = null) {
  const targets = resolveAgentTargets({
    agent: targetOptions.agent,
    scope: targetOptions.scope,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });
  const plan = planInstall(skills, targets);
  const manifest = manifestOverride || loadLocalManifest();
  const sourceRoot = sourceRootOverride || ensureSkillSourceRoot(manifest, { env: process.env, skills });
  const applied = applyInstallPlan(plan, {
    sourceRoot,
    source: manifest.source,
    ref: manifest.ref,
    sourceRevision: manifest.sourceRevision,
  });
  await recordTelemetryEvents(applied, { enabled: true, selection: { profile: null } });
  const notice = renderMigrationNotice(targets);
  return `${notice}${renderInstallPlan("Install summary", plan)}`;
}

async function recordTelemetryEvents(operations, options = {}) {
  const telemetryEnabled = options.enabled !== false && process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
  if (!telemetryEnabled || !operations.length) {
    return { queued: 0, sent: 0, remaining: 0, skipped: true };
  }
  const installSessionId = randomSessionId();
  const events = operations.map((operation) =>
    buildTelemetryEvent({
      event: "skill_installed",
      operation: operation.action,
      installSessionId,
      skill: operation.skill,
      target: operation.target,
      selection: {
        profile: options.selection && options.selection.profile ? options.selection.profile : null,
        domain: options.selection && options.selection.domain ? options.selection.domain : operation.skill.domain,
        stage: options.selection && options.selection.stage ? options.selection.stage : operation.skill.stage,
      },
      packageVersion: packageJson.version,
    }),
  );
  const queued = enqueueTelemetryEvents(events, { enabled: telemetryEnabled });
  const flush = await flushTelemetryQueue();
  return { queued, ...flush };
}

function randomSessionId() {
  return `${Date.now().toString(36)}-${Math.random().toString(16).slice(2)}`;
}

function renderInteractiveCompletion(skills, targetOptions, output = process.stdout) {
  return [
    "",
    renderStatusLine(
      "success",
      "INSTALL SUCCESS",
      `Installed ${skills.length} skill(s) for ${formatAgentSelection(targetOptions.agent)}:${targetOptions.scope}.`,
      output,
    ),
  ].join("\n");
}

function renderStatusLine(kind, title, detail, output = process.stdout) {
  const color = kind === "failure" ? ANSI.red : ANSI.green;
  const heading = output.isTTY ? colorize(title, color, ANSI.bold) : title;
  return detail ? `${heading}\n${detail}` : heading;
}

function renderInstallPlan(title, plan) {
  const lines = [title];
  for (const operation of plan.operations) {
    lines.push(
      `${operation.action.toUpperCase()} ${operation.skill.name} -> ${operation.target.agent}:${operation.target.scope} ${operation.destination}`,
    );
  }
  return lines.join("\n");
}

function renderPluginInstallPlan(title, plan) {
  const lines = [title];
  for (const operation of plan.operations) {
    const skillCount = operation.payload.operations.filter((item) => item.type === "skill").length;
    const fileCount = operation.files ? operation.files.length : operation.payload.operations.length;
    lines.push(
      `${operation.action.toUpperCase()} ${operation.plugin.name} -> ${operation.target.agent}:${operation.target.scope} ${operation.payload.payloadRoot}`,
    );
    lines.push(`  skills: ${skillCount}`);
    lines.push(`  payload operations: ${operation.payload.operations.length}`);
    lines.push(`  managed files: ${fileCount}`);
    if (operation.conflicts && operation.conflicts.length) {
      for (const conflict of operation.conflicts) {
        lines.push(`  conflict: ${conflict}`);
      }
    }
    if (operation.action === "skip") {
      lines.push("  status: already up to date");
    }
    if (operation.action === "missing") {
      lines.push("  status: not installed");
    }
    if (operation.registration && !operation.registration.skipped) {
      lines.push(`  registration: ${operation.registration.success ? "ok" : "failed"} (${operation.registration.command})`);
      if (!operation.registration.success && operation.registration.message) {
        lines.push(`  registration detail: ${operation.registration.message.split(/\r?\n/)[0]}`);
      }
      if (operation.registration.success && operation.registration.nextCommand) {
        lines.push(`  next: ${operation.registration.nextCommand}`);
      }
    }
    if (operation.registration && operation.registration.skipped && operation.registration.reason) {
      lines.push(`  registration: skipped (${operation.registration.reason})`);
    }
  }
  lines.push(...renderClaudeUpdateHints(title, plan.operations));
  return lines.join("\n");
}

function renderClaudeUpdateHints(title, operations) {
  if (!/\bupdate\b/i.test(title)) {
    return [];
  }
  const refs = Array.from(
    new Map(
      operations
        .filter((operation) => operation.target.agent === "claude" && operation.action !== "missing")
        .map((operation) => [operation.plugin.name, marketplacePluginRef(operation.plugin)]),
    ).values(),
  );
  if (!refs.length) {
    return [];
  }
  return [
    "",
    "If the plugin is already installed in Claude Code, refresh Claude's installed plugin with:",
    ...refs.map((ref) => `  claude plugin update ${ref}`),
  ];
}

function renderPluginUninstallPlan(title, plan) {
  const lines = [title];
  for (const operation of plan.operations) {
    const fileCount = operation.filesRemoved
      ? operation.filesRemoved.length
      : operation.managedFiles.length;
    lines.push(
      `${operation.action.toUpperCase()} ${operation.plugin.name} -> ${operation.target.agent}:${operation.target.scope} ${operation.payloadRoot}`,
    );
    lines.push(`  managed files: ${fileCount}`);
    if (operation.action === "missing") {
      lines.push("  status: not installed");
    }
    if (operation.action === "blocked" && operation.validation && operation.validation.mismatched.length) {
      lines.push(`  blocked: ${operation.validation.mismatched.length} checksum mismatch(es); use --force to remove`);
    }
    if (operation.registration && !operation.registration.skipped) {
      lines.push(`  registration: ${operation.registration.success ? "ok" : "failed"} (${operation.registration.command})`);
      if (!operation.registration.success && operation.registration.message) {
        lines.push(`  registration detail: ${operation.registration.message.split(/\r?\n/)[0]}`);
      }
      if (operation.registration.success && operation.registration.nextCommand) {
        lines.push(`  next: ${operation.registration.nextCommand}`);
      }
    }
    if (operation.registration && operation.registration.skipped && operation.registration.reason) {
      lines.push(`  registration: skipped (${operation.registration.reason})`);
    }
  }
  return lines.join("\n");
}

function renderDoctor(options) {
  const targets = resolveAgentTargets({
    agent: options.agent || "codex",
    scope: options.scope || "user",
    target: options.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });
  const lines = ["Agent target diagnostics"];
  for (const target of targets) {
    const exists = fs.existsSync(target.dir);
    lines.push(`${target.agent} (${target.scope})`);
    lines.push(`  path: ${target.dir}`);
    lines.push(`  exists: ${exists ? "yes" : "no"}`);
    lines.push(`  writable: ${exists ? isWritable(target.dir) : "not checked"}`);
  }
  return lines.join("\n");
}

function renderTelemetry(args) {
  const action = args[0] || "status";
  if (action !== "status") {
    throw new Error("Only telemetry status is implemented in this version.");
  }
  const status = telemetryStatus();
  return [
    "Telemetry",
    `  enabled: ${status.enabled ? "yes" : "no"}`,
    `  collector: ${status.collectorUrl || "(not configured)"}`,
    `  cache: ${status.cacheDir}`,
    `  queue: ${status.queueSize} event(s)`,
  ].join("\n");
}

function isWritable(dir) {
  try {
    fs.accessSync(dir, fs.constants.W_OK);
    return "yes";
  } catch {
    return "no";
  }
}


async function runUpdate(options) {
  const targets = resolveAgentTargets({
    agent: options.agent || "all",
    scope: options.scope || "user",
    target: options.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });

  const skillNames = options.names || [];
  if (!skillNames.length) {
    throw new Error("Usage: update <skill-id> [--agent <agent>] [--scope <scope>]");
  }

  const offline = options.offline;
  for (const skillName of skillNames) {
    console.log("Updating " + skillName + "...");
    try {
      const updateResult = await planUpdate(skillName, targets, { env: process.env, offline });
      const results = updateResult.results;
      const plan = updateResult.plan;

      const skipped = results.filter((r) => r.skipped);
      for (const result of skipped) {
        console.log("  " + (result.message || result.skillName) + ": already up to date");
      }

      if (plan && plan.operations.length) {
        const toUpdate = results.filter((r) => r.needsUpdate);
        console.log("  Pulling latest for " + toUpdate.length + " skill(s)...");
        const applied = await applyUpdatePlan(plan, { env: process.env, offline });
        for (const op of applied) {
          console.log("  Updated: " + op.skill.name + " -> " + op.target.agent + ":" + op.target.scope);
        }
      }
    } catch (error) {
      console.error("  Failed to update " + skillName + ": " + error.message);
    }
  }
  console.log("");
  console.log("Update complete. Restart opencode to load updated skills.");
}

async function runUpdateAll(options) {
  const targets = resolveAgentTargets({
    agent: options.agent || "all",
    scope: options.scope || "user",
    target: options.target,
    cwd: process.cwd(),
    homeDir: os.homedir(),
    env: process.env,
  });

  const offline = options.offline;
  console.log("Checking for skill updates...");
  const updateResult = await planUpdateAll(targets, { env: process.env, offline });
  const results = updateResult.results;
  const plan = updateResult.plan;
  const message = updateResult.message;

  if (message) {
    console.log(message);
    return;
  }

  const skipped = results.filter((r) => r.skipped);
  for (const result of skipped) {
    console.log("  " + (result.message || result.skillName) + ": already up to date");
  }

  if (plan && plan.operations.length) {
    const toUpdate = results.filter((r) => r.needsUpdate);
    console.log("Updating " + toUpdate.length + " skill(s)...");
    const applied = await applyUpdatePlan(plan, { env: process.env, offline });
    for (const op of applied) {
      console.log("  Updated: " + op.skill.name + " -> " + op.target.agent + ":" + op.target.scope);
    }
  } else {
    console.log("All skills are up to date.");
  }
  console.log("");
  console.log("Update complete. Restart opencode to load updated skills.");
}

function helpText() {
  return [
    "oh-skillhub",
    "",
    "Run without arguments to choose install or clean interactively.",
    "",
    "Commands:",
    "  list [--domain <name>] [--common <stage>] [--stage <name>] [--preset <name>] [--offline]",
    "  install [skill...|plugin] [--name <skill>] [--domain <name>] [--common <stage>] [--preset <name>] [--all-common] [--agent codex|claude|opencode|all]",
    "  uninstall <plugin...> [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run] [--force]",
    "  update <skill...|plugin...> [--agent codex|claude|opencode|all] [--scope user|project] [--offline]",
    "  plugin list [--verbose]",
    "  plugin install <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run] [--force]",
    "  plugin update <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run] [--force]",
    "  plugin uninstall <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run]",
    "  plugin status <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--verbose]",
    "  plugin doctor <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--scan-legacy] [--fix] [--force]",
    "  plugin manifest <plugin> [--agent codex|claude|opencode|all] [--out <dir>] [--check] [--scope user|project]",
    "  plugin gc [--scope user|project] [--dry-run]",
    "  plugin repair [--scope user|project]",
    "  plugin enable <plugin> --agent codex|claude|opencode [--scope user|project]",
    "  plugin disable <plugin> --agent codex|claude|opencode [--scope user|project]",
    "  plugin migrate <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--atomic]",
    "  update-all [--agent codex|claude|opencode|all] [--scope user|project] [--offline]",
    "  clean [skill...] [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run] [--purge]",
    "  doctor [--agent codex|claude|opencode|all]",
    "  telemetry status",
    "",
    "Examples:",
    "  npx oh-skillhub install --all-common --agent codex --scope user",
    "  npx oh-skillhub install --common design --agent codex --scope user",
    "  npx oh-skillhub install --domain arkui --agent all --scope user",
    "  npx oh-skillhub install ohos-dev-cpp-coding-style --agent codex",
    "  npx oh-skillhub install --name ohos-dev-napi-module --agent all --scope user",
    "  npx oh-skillhub plugin install ohos-sdd --agent all --scope user",
    "  npx oh-skillhub plugin update ohos-sdd --agent all --scope user",
    "  npx oh-skillhub plugin uninstall ohos-sdd --agent codex --scope user",
    "  npx oh-skillhub plugin doctor ohos-sdd --agent all --scope user",
    "  npx oh-skillhub plugin doctor ohos-sdd --agent all --scan-legacy",
    "  npx oh-skillhub plugin doctor ohos-sdd --agent all --scan-legacy --fix",
    "  npx oh-skillhub update ohos-dev-cpp-coding-style --agent opencode",
    "  npx oh-skillhub update-all --agent opencode --scope user",
  ].join("\n");
}

function pluginHelpText() {
  return [
    "oh-skillhub plugin",
    "",
    "Read-only workflow plugin diagnostics for the lifecycle v2 migration.",
    "",
    "Commands:",
    "  plugin list [--verbose]",
    "  plugin install <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run] [--force]",
    "  plugin update <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run] [--force]",
    "  plugin uninstall <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--dry-run]",
    "  plugin status <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--verbose]",
    "  plugin doctor <plugin> [--agent codex|claude|opencode|all] [--scope user|project] [--scan-legacy] [--fix] [--force]",
    "  plugin manifest <plugin> [--agent codex|claude|opencode|all] [--out <dir>] [--check] [--scope user|project]",
    "",
    "Channel conflict options:",
    "  --force          Overwrite marketplace-channel installations (use when you need to switch channels)",
    "",
    "Scan-legacy options:",
    "  --scan-legacy    Detect cross-source residuals (dev repo, marketplace, oh-skillhub legacy) across all agents",
    "  --fix            Remove detected residuals (requires --scan-legacy); moves to trash by default",
    "  --force          Permanently delete instead of trashing (requires --fix)",
    "",
    "Examples:",
    "  npx oh-skillhub plugin list",
    "  npx oh-skillhub plugin install ohos-delivery-kit --agent all --scope user",
    "  npx oh-skillhub plugin update ohos-delivery-kit --agent all --scope user",
    "  npx oh-skillhub plugin uninstall ohos-delivery-kit --agent claude --scope user",
    "  npx oh-skillhub plugin status ohos-delivery-kit --agent all --scope user --verbose",
    "  npx oh-skillhub plugin doctor ohos-sdd --agent claude --scope user",
    "  npx oh-skillhub plugin manifest ohos-delivery-kit --agent all --out ./dist",
    "  npx oh-skillhub plugin manifest ohos-delivery-kit --agent codex --check",
    "  npx oh-skillhub plugin doctor ohos-sdd --agent all --scan-legacy",
    "  npx oh-skillhub plugin doctor ohos-sdd --agent all --scan-legacy --fix",
  ].join("\n");
}

module.exports = {
  buildRepositoryChoices,
  buildUnknownSuggestions,
  executeInstall,
  helpText,
  isPluginInstallRequest,
  main,
  parseArgs,
  prepareRawInput,
  releaseRawInput,
  renderActionMenu,
  renderAgentMenu,
  renderCleanPlan,
  renderDoctor,
  renderInstall,
  renderPluginInstallPlan,
  renderPluginCommand,
  renderPluginUpdate,
  renderPluginUninstall,
  renderPluginUninstallPlan,
  renderList,
  renderRawActionMenu,
  renderRawAgentMenu,
  renderRawCleanSelectionMenu,
  renderRawTuiMenu,
  renderStatusLine,
  renderTelemetry,
  renderTuiMenu,
  selectSkillsForChoices,
  withSpinner,
  parseSelection,
};