#!/bin/bash
set -e

# Drafted — Shared surface for AI-human collaboration
# This script installs the Drafted MCP server and CLI globally via npm,
# then registers it with Claude Desktop, Claude Code, Codex, and Cursor.
#
# Run with (download first, then run the local file):
#   curl -fsSL https://drafted.live/install.sh -o ~/Downloads/install-drafted.sh
#   bash ~/Downloads/install-drafted.sh

SERVER="https://drafted.live"
INSTALLER_VERSION="1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_MODE="production"
INSTALL_NAME="${DRAFTED_MCP_NAME:-drafted}"
INSTALL_SERVER="${DRAFTED_SERVER:-$SERVER}"
INSTALL_AUTH_FILE="${DRAFTED_AUTH_FILE:-}"

while [ $# -gt 0 ]; do
  case "$1" in
    --local)
      INSTALL_MODE="local"
      INSTALL_NAME="${DRAFTED_MCP_NAME:-drafted-local}"
      INSTALL_SERVER="${DRAFTED_SERVER:-http://localhost:3477}"
      INSTALL_AUTH_FILE="${DRAFTED_AUTH_FILE:-$HOME/.drafted/auth.local.json}"
      if [ -z "${DRAFTED_TELEMETRY+x}" ]; then export DRAFTED_TELEMETRY=0; fi
      shift
      ;;
    --server)
      INSTALL_SERVER="$2"
      shift 2
      ;;
    --name)
      INSTALL_NAME="$2"
      shift 2
      ;;
    --auth-file)
      INSTALL_AUTH_FILE="$2"
      shift 2
      ;;
    --help|-h)
      echo "Usage: install-mcp.sh [--local] [--server URL] [--name MCP_NAME] [--auth-file PATH]"
      echo "  default: installs production MCP named drafted -> https://drafted.live"
      echo "  --local: installs duplicate MCP named drafted-local -> http://localhost:3477"
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
  esac
done


BOLD="\033[1m"
DIM="\033[2m"
GREEN="\033[32m"
YELLOW="\033[33m"
RED="\033[31m"
CYAN="\033[36m"
RESET="\033[0m"

step=0
step() {
  step=$((step + 1))
  echo ""
  echo -e "${CYAN}[$step]${RESET} ${BOLD}$1${RESET}"
}

ok() {
  echo -e "    ${GREEN}✓${RESET} $1"
}

fail() {
  echo -e "    ${RED}✗${RESET} $1"
}

init_telemetry() {
  INSTALL_ID=""
  TELEMETRY_ENABLED=true
  if [ "${DRAFTED_TELEMETRY:-}" = "0" ]; then
    TELEMETRY_ENABLED=false
    return 0
  fi
  mkdir -p "$HOME/.drafted"
  INSTALL_ID="$(node - "$HOME/.drafted/install.json" <<'NODE'
const fs = require('fs');
const crypto = require('crypto');
const p = process.argv[2];
let data = {};
try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
if (data.telemetry === false) process.exit(2);
if (!/^[0-9a-f-]{36}$/i.test(String(data.installId || ''))) data.installId = crypto.randomUUID();
data.telemetry = data.telemetry !== false;
data.updatedAt = new Date().toISOString();
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
process.stdout.write(data.installId);
NODE
)" || TELEMETRY_ENABLED=false
  if [ "$TELEMETRY_ENABLED" = true ]; then
    echo -e "    ${DIM}Drafted sends anonymous install telemetry. Set DRAFTED_TELEMETRY=0 to opt out.${RESET}"
  fi
}

report_telemetry() {
  [ "${TELEMETRY_ENABLED:-false}" = true ] || return 0
  [ -n "${INSTALL_ID:-}" ] || return 0
  local event="$1"
  local helper_status="${2:-installed}"
  node - "$SERVER" "$INSTALL_ID" "$event" "$INSTALLER_VERSION" "$helper_status" \
    "${CLIENT_CLAUDE_DESKTOP:-false}" "${CLIENT_CLAUDE_CODE:-false}" "${CLIENT_CODEX:-false}" "${CLIENT_CURSOR:-false}" <<'NODE' >/dev/null 2>&1 || true
const [server, installId, event, installerVersion, updateHelperStatus, claudeDesktop, claudeCode, codex, cursor] = process.argv.slice(2);
const os = require('os');
const cp = require('child_process');
function run(cmd) { try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().slice(0, 80); } catch { return undefined; } }
const platform = os.platform();
const osFamily = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : platform === 'linux' ? 'linux' : 'unknown';
fetch(`${server}/api/installations/report`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'User-Agent': 'Drafted Installer' },
  body: JSON.stringify({
    installId,
    event,
    schemaVersion: 1,
    installerVersion,
    cliVersion: run('drafted --version'),
    osFamily,
    osVersion: os.release(),
    arch: os.arch(),
    nodeVersion: process.version,
    npmVersion: run('npm --version'),
    clientsConfigured: {
      claudeDesktop: claudeDesktop === 'true',
      claudeCode: claudeCode === 'true',
      codex: codex === 'true',
      cursor: cursor === 'true'
    },
    updateHelperStatus,
    mcpMode: 'stdio',
    source: 'installer'
  })
}).catch(() => {});
NODE
}

install_portable_node() {
  local os arch platform filename url tmp node_dir
  os="$(uname -s)"
  arch="$(uname -m)"
  case "$os" in
    Darwin) platform="darwin" ;;
    Linux) platform="linux" ;;
    *) fail "Unsupported OS for automatic Node.js install: $os"; exit 1 ;;
  esac
  case "$arch" in
    x86_64|amd64) arch="x64" ;;
    arm64|aarch64) arch="arm64" ;;
    *) fail "Unsupported CPU for automatic Node.js install: $arch"; exit 1 ;;
  esac

  mkdir -p "$HOME/.drafted"
  node_dir="$HOME/.drafted/node"
  tmp="$(mktemp -d)"
  filename="$(curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt | awk "/node-v.*-${platform}-${arch}\\.tar\\.xz/ {print \$2; exit}")"
  if [ -z "$filename" ]; then
    fail "Could not find Node.js 22 download for $platform-$arch"
    exit 1
  fi
  url="https://nodejs.org/dist/latest-v22.x/$filename"
  echo -e "    ${YELLOW}Downloading Node.js 22 for $platform-$arch...${RESET}"
  curl -fsSL "$url" -o "$tmp/node.tar.xz"
  rm -rf "$node_dir"
  mkdir -p "$node_dir"
  tar -xJf "$tmp/node.tar.xz" -C "$node_dir" --strip-components=1
  rm -rf "$tmp"
  export PATH="$node_dir/bin:$PATH"
  ok "Installed portable Node.js $(node -v)"
}

# ── Welcome ───────────────────────────────────────────────────────

echo ""
echo -e "${BOLD}Welcome to Drafted${RESET}"
echo -e "This will set up Drafted so your agents can create work on a shared surface."
echo -e "It only takes a minute."
if [ "$INSTALL_MODE" = "local" ]; then
  echo -e "${DIM}Local mode: installing MCP ${BOLD}$INSTALL_NAME${RESET}${DIM} -> $INSTALL_SERVER without touching production drafted.${RESET}"
fi

# ── Prerequisites ─────────────────────────────────────────────────

step "Checking your system"

# Node.js
if command -v node &>/dev/null; then
  NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
  if [ "$NODE_VERSION" -lt 22 ]; then
    echo -e "    ${YELLOW}Node.js $(node -v) is too old; installing Node.js 22 for Drafted.${RESET}"
    install_portable_node
  else
    ok "Node.js $(node -v)"
  fi
else
  echo -e "    ${YELLOW}Node.js is not installed; installing Node.js 22 for Drafted.${RESET}"
  install_portable_node
fi

init_telemetry

NPM_GLOBAL_PREFIX="$HOME/.drafted/npm-global"
mkdir -p "$NPM_GLOBAL_PREFIX"

# A `drafted` resolved elsewhere on PATH (a leftover install from before this
# fixed-prefix scheme, or a manual `npm i -g` under the default prefix) would
# permanently shadow the fixed-prefix copy: the updater keeps this prefix
# current, but the shell keeps running the stale one. Remove it at its source.
STALE_DRAFTED_BIN="$(command -v drafted 2>/dev/null || true)"
if [ -n "$STALE_DRAFTED_BIN" ]; then
  # Derived from where the resolved binary actually sits (bin's grandparent),
  # not `npm config get prefix` — that reflects our own target prefix once
  # it's already been set on a prior run, which would make this a no-op on
  # every machine that's already installed once (i.e. every real machine).
  CANDIDATE_PREFIX="$(dirname "$(dirname "$STALE_DRAFTED_BIN")")"
  if [ "$CANDIDATE_PREFIX" != "$NPM_GLOBAL_PREFIX" ] && [ -d "$CANDIDATE_PREFIX/lib/node_modules/drafted" ]; then
    echo -e "    ${YELLOW}Found a stale Drafted install at $CANDIDATE_PREFIX — removing it so it can't shadow the current one.${RESET}"
    npm uninstall -g drafted --prefix "$CANDIDATE_PREFIX" >/dev/null 2>&1 || true
  fi
fi

# Do NOT `npm config set prefix` — that repoints the user's GLOBAL npm prefix, so every
# `npm install -g <pkg>` they ever run lands in our dir AND our uninstall (`rm -rf ~/.drafted`)
# would wipe all their other globals. Install drafted with an explicit per-command --prefix
# instead (below), and HEAL any global prefix pin an older version of this installer wrote so
# the user's default is restored. Preserve every other ~/.npmrc line (auth tokens, etc.).
NPMRC="${npm_config_userconfig:-$HOME/.npmrc}"
if [ -f "$NPMRC" ] && grep -Eq '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC"; then
  tmp_npmrc="$(mktemp)"
  grep -Ev '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC" > "$tmp_npmrc" && cat "$tmp_npmrc" > "$NPMRC"
  rm -f "$tmp_npmrc"
fi
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"

# Persist the prefix's bin dir at the FRONT of PATH for future shells — without
# this, npm's prefix config makes `npm install -g` land in the right place, but
# a bare `drafted` in a new terminal keeps resolving through whatever was
# already on PATH (see the installedVersion() comment on the login-shell PATH
# gap below). Idempotent guarded block, same pattern as the agent-instructions
# markers further down.
persist_npm_global_path() {
  local marker_begin="# BEGIN drafted-path"
  local marker_end="# END drafted-path"
  local line="export PATH=\"$NPM_GLOBAL_PREFIX/bin:\$PATH\""
  local rc
  case "$(basename "${SHELL:-}")" in
    zsh) rc="$HOME/.zshrc" ;;
    bash) rc="$HOME/.bash_profile" ;;
    *) rc="$HOME/.profile" ;;
  esac
  [ -f "$rc" ] || : > "$rc"
  grep -qF "$marker_begin" "$rc" 2>/dev/null && return 0
  { echo ""; echo "$marker_begin"; echo "$line"; echo "$marker_end"; } >> "$rc"
}
persist_npm_global_path

# ── Install ───────────────────────────────────────────────────────

step "Installing Drafted"

# Retry with back-off: a fresh `npm publish` can briefly 404/ETARGET on the CDN
# edge before the tarball propagates, so an updater that fires the instant a new
# version lands would otherwise report a spurious "update failed". Ride it out.
install_drafted_pkg() {
  attempts=5; delay=4; n=1
  while :; do
    if npm install -g drafted@latest --force --prefix "$NPM_GLOBAL_PREFIX"; then return 0; fi
    if [ "$n" -ge "$attempts" ]; then return 1; fi
    echo -e "    ${YELLOW}npm install failed (attempt $n/$attempts) — retrying in ${delay}s (a new release may still be propagating to the npm CDN)...${RESET}"
    sleep "$delay"
    n=$((n + 1)); delay=$((delay * 2))
  done
}
if ! install_drafted_pkg; then
  echo -e "    ${RED}npm install -g drafted@latest failed after multiple attempts.${RESET}"
  exit 1
fi
hash -r 2>/dev/null || true
NPM_ROOT="$(npm root -g --prefix "$NPM_GLOBAL_PREFIX" 2>/dev/null || true)"
MCP_SERVER_MODULE="$NPM_ROOT/drafted/mcp/server.mjs"
if [ -n "$NPM_ROOT" ] && [ -f "$MCP_SERVER_MODULE" ]; then
  node -e "import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(process.argv[1]).href)).then(() => process.exit(0), (err) => { console.error(err); process.exit(1); })" "$MCP_SERVER_MODULE"
elif [ "${DRAFTED_HEADLESS:-}" = "1" ]; then
  echo -e "    ${DIM}Skipping package import check in headless installer test.${RESET}"
else
  echo -e "    ${RED}Could not find installed MCP server module at $MCP_SERVER_MODULE${RESET}"
  exit 1
fi
ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"

# ── Configure ─────────────────────────────────────────────────────

step "Connecting to your tools"

# Write server URL config
mkdir -p "$HOME/.drafted"
if [ "$INSTALL_MODE" = "production" ]; then
  echo "{\"server\":\"$INSTALL_SERVER\"}" > "$HOME/.drafted/config.json"
  ok "Server: $INSTALL_SERVER"
else
  ok "Local server: $INSTALL_SERVER"
fi

# Local stdio MCP command. Production uses the installed package; --local uses this checkout when available.
if [ "$INSTALL_MODE" = "local" ] && [ -f "$SCRIPT_DIR/mcp/server.mjs" ]; then
  DRAFTED_MCP_COMMAND="$(command -v node)"
  DRAFTED_MCP_ARGS_JSON="$(node -e 'console.log(JSON.stringify([process.argv[1]]))' "$SCRIPT_DIR/mcp/server.mjs")"
else
  DRAFTED_MCP_COMMAND="$(command -v drafted-mcp)"
  DRAFTED_MCP_ARGS_JSON="[]"
fi

configure_mcp() {
  local config_path="$1"
  local label="$2"
  local config_dir
  config_dir="$(dirname "$config_path")"
  mkdir -p "$config_dir"

  node -e "
    const fs = require('fs');
    const p = process.argv[1];
    const name = process.argv[2];
    const command = process.argv[3];
    const args = JSON.parse(process.argv[4]);
    const server = process.argv[5];
    const authFile = process.argv[6];
    const mode = process.argv[7];
    let c = {};
    try { c = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
    if (!c.mcpServers) c.mcpServers = {};
    const env = { DRAFTED_SERVER: server };
    if (authFile) env.DRAFTED_AUTH_FILE = authFile;
    if (mode === 'local') env.DRAFTED_TELEMETRY = '0';
    c.mcpServers[name] = { command, args, env };
    fs.writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
  " "$config_path" "$INSTALL_NAME" "$DRAFTED_MCP_COMMAND" "$DRAFTED_MCP_ARGS_JSON" "$INSTALL_SERVER" "$INSTALL_AUTH_FILE" "$INSTALL_MODE"
  ok "$label"
}


verify_no_legacy_http_config() {
  local found=false
  for file in "$CLAUDE_DESKTOP_CONFIG" "$CLAUDE_CODE_CONFIG" "$CURSOR_CONFIG" "$CODEX_CONFIG"; do
    [ -f "$file" ] || continue
    if grep -q "https://drafted.live/mcp" "$file" 2>/dev/null; then
      found=true
      echo -e "    ${RED}✗${RESET} Legacy HTTP Drafted MCP still present in $file"
    fi
  done
  if [ "$found" = true ]; then
    fail "Installer migration incomplete. Remove legacy https://drafted.live/mcp entries and rerun."
    exit 1
  fi
}

configure_codex() {
  local config_path="$1"
  local label="$2"
  local config_dir
  config_dir="$(dirname "$config_path")"
  mkdir -p "$config_dir"

  node - "$config_path" "$INSTALL_NAME" "$DRAFTED_MCP_COMMAND" "$DRAFTED_MCP_ARGS_JSON" "$INSTALL_SERVER" "$INSTALL_AUTH_FILE" "$INSTALL_MODE" <<'NODE'
const fs = require('fs');
const p = process.argv[2];
const mcpName = process.argv[3];
const command = process.argv[4];
const args = JSON.parse(process.argv[5]);
const server = process.argv[6];
const authFile = process.argv[7];
const mode = process.argv[8];

let text = '';
try { text = fs.readFileSync(p, 'utf8'); } catch {}

const lines = text.split(/\r?\n/);
const out = [];
let skip = false;

for (const line of lines) {
  const section = line.match(/^\[(.+)\]$/);
  if (section) {
    const name = section[1].trim();
    if (name === `mcp_servers.${mcpName}` || name === `mcp_servers.${mcpName}.env`) {
      skip = true;
      continue;
    }
    if (skip) skip = false;
  }
  if (!skip) out.push(line);
}

let next = out.join('\n').replace(/\s+$/, '');
if (next) next += '\n\n';
next += `[mcp_servers.${mcpName}]\n`;
next += 'command = ' + JSON.stringify(command) + '\n';
next += 'args = [' + args.map(a => JSON.stringify(a)).join(', ') + ']\n';
next += `\n[mcp_servers.${mcpName}.env]\n`;
next += 'DRAFTED_SERVER = ' + JSON.stringify(server) + '\n';
if (authFile) next += 'DRAFTED_AUTH_FILE = ' + JSON.stringify(authFile) + '\n';
if (mode === 'local') next += 'DRAFTED_TELEMETRY = "0"\n';
next += '\n';

fs.writeFileSync(p, next);
NODE

  ok "$label"
}

CLAUDE_DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
CLAUDE_CODE_CONFIG="$HOME/.claude.json"
CODEX_CONFIG="$HOME/.codex/config.toml"
CURSOR_CONFIG="$HOME/.cursor/mcp.json"

CONFIGURED=false
CLIENT_CLAUDE_DESKTOP=false
CLIENT_CLAUDE_CODE=false
CLIENT_CODEX=false
CLIENT_CURSOR=false

# Claude Desktop
if [ -d "/Applications/Claude.app" ] || [ -d "$HOME/Applications/Claude.app" ] || [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
  configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop"
  CLIENT_CLAUDE_DESKTOP=true
  CONFIGURED=true
fi

# Claude Code
if command -v claude &>/dev/null || [ -f "$CLAUDE_CODE_CONFIG" ] || [ -d "$HOME/.claude" ]; then
  configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code"
  CLIENT_CLAUDE_CODE=true
  CONFIGURED=true
fi

# Codex
if command -v codex &>/dev/null || [ -d "$HOME/.codex" ]; then
  configure_codex "$CODEX_CONFIG" "Codex"
  CLIENT_CODEX=true
  CONFIGURED=true
fi

# Cursor
if [ -d "/Applications/Cursor.app" ] || [ -d "$HOME/Applications/Cursor.app" ] || command -v cursor &>/dev/null || [ -f "$CURSOR_CONFIG" ]; then
  configure_mcp "$CURSOR_CONFIG" "Cursor"
  CLIENT_CURSOR=true
  CONFIGURED=true
fi

# If nothing detected, pre-configure all
if [ "$CONFIGURED" = false ]; then
  echo -e "    ${YELLOW}No supported editors detected — pre-configuring all.${RESET}"
  configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop (pre-configured)"
  configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code (pre-configured)"
  configure_codex "$CODEX_CONFIG" "Codex (pre-configured)"
  configure_mcp "$CURSOR_CONFIG" "Cursor (pre-configured)"
  CLIENT_CLAUDE_DESKTOP=true
  CLIENT_CLAUDE_CODE=true
  CLIENT_CODEX=true
  CLIENT_CURSOR=true
fi

# ── Agent instructions ────────────────────────────────────────────

step "Installing global agent instructions"

load_global_agent_instructions() {
  local local_path="$SCRIPT_DIR/agent-instructions/global.md"
  if [ -f "$local_path" ]; then
    cat "$local_path"
    return 0
  fi
  cat <<'DRAFTED_GLOBAL_INSTRUCTIONS'
<drafted>
You have Drafted MCP tools — a shared surface for durable, reviewable work: produce substantive output as frames on the surface (not only in chat), put knowledge in the org wiki, and encode repeatable methods as skills. The full operating manual is the `drafted` skill installed with the plugin — follow it when working with Drafted. Address orgs by path: `fs(ls, path="/")` lists them, and `/o/<org>/<root>/...` (root ∈ wiki, skills, projects) is the canonical path form — the org is part of the path, never a separate switch. Before writing, verify the org/project echoed in the response is the one you intend.
</drafted>
DRAFTED_GLOBAL_INSTRUCTIONS
}

install_agent_instructions() {
  local instructions_path="$1"
  local label="$2"
  local instructions_dir
  local body
  instructions_dir="$(dirname "$instructions_path")"
  mkdir -p "$instructions_dir"

  if ! body="$(load_global_agent_instructions)"; then
    fail "Could not load Drafted global instructions"
    return 1
  fi

  DRAFTED_GLOBAL_INSTRUCTIONS_BODY="$body" node - "$instructions_path" <<'NODE'
const fs = require('fs');
const p = process.argv[2];
const begin = '<!-- BEGIN drafted-global-instructions -->';
const end = '<!-- END drafted-global-instructions -->';
const body = process.env.DRAFTED_GLOBAL_INSTRUCTIONS_BODY || '';
if (!body.trim()) throw new Error('Drafted global instructions are empty');
const block = `${begin}\n${body.replace(/\s+$/, '')}\n${end}`;
let text = '';
try { text = fs.readFileSync(p, 'utf8'); } catch {}
const escapeRe = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`${escapeRe(begin)}[\\s\\S]*?${escapeRe(end)}`);
let next;
if (re.test(text)) {
  next = text.replace(re, block);
} else {
  next = text.replace(/\s+$/, '');
  if (next) next += '\n\n';
  next += block + '\n';
}
fs.writeFileSync(p, next.endsWith('\n') ? next : next + '\n');
NODE

  ok "$label"
}

install_agent_instructions "$HOME/.claude/CLAUDE.md" "Claude global instructions"
install_agent_instructions "$HOME/.codex/CODEX.md" "Codex global instructions"

# ── Skills & commands ─────────────────────────────────────────────

step "Installing skills and commands"

# Resolve the plugin source: a local checkout when present (e.g. --local from this
# repo), otherwise the installed npm package's bundled plugin/ directory.
# require.resolve() misses packages under the custom global prefix
# ($HOME/.drafted/npm-global) when run from an arbitrary cwd (curl | bash), so
# prefer the already-computed global node_modules root from the install step.
if [ -n "${NPM_ROOT:-}" ] && [ -d "$NPM_ROOT/drafted" ]; then
  DRAFTED_PKG_DIR="$NPM_ROOT/drafted"
else
  DRAFTED_PKG_DIR="$(node -e "try { console.log(require.resolve('drafted/package.json').replace('/package.json','')) } catch { process.exit(1) }" 2>/dev/null)" || true
fi
if [ -d "$SCRIPT_DIR/plugin/skills" ] || [ -d "$SCRIPT_DIR/plugin/commands" ]; then
  PLUGIN_SRC="$SCRIPT_DIR/plugin"
elif [ -n "$DRAFTED_PKG_DIR" ] && [ -d "$DRAFTED_PKG_DIR/plugin" ]; then
  PLUGIN_SRC="$DRAFTED_PKG_DIR/plugin"
else
  PLUGIN_SRC=""
fi

# Skills and commands are LINKED, never copied: a copy is a point-in-time snapshot that only
# refreshes when someone re-runs this installer, so `npm i -g drafted@latest` would leave the
# installed skill silently behind the package it came from. A link tracks the upgrade.
#
# link_plugin_entry is idempotent and non-destructive: a correct link is left alone, a wrong or
# dangling one is repointed, and a real file/directory (which the user may have written by hand)
# is moved aside rather than deleted. Backups go OUTSIDE the skills/commands dirs — a
# `drafted.backup/` sitting next to `drafted/` would register as a second, stale skill.
DRAFTED_BACKUP_DIR="$HOME/.claude/.drafted-backups"
link_plugin_entry() {
  local source="$1" target="$2" source_real backup
  source_real="$(cd "$(dirname "$source")" && pwd)/$(basename "$source")"
  if [ -L "$target" ]; then
    if [ "$(readlink "$target")" = "$source_real" ]; then return 0; fi
    rm -f "$target"
  elif [ -e "$target" ]; then
    # Byte-identical to the source: this is a prior copy-install of ours, not user content, and
    # the link reproduces it losslessly. Anything else is backed up.
    if diff -rq "$source_real" "$target" >/dev/null 2>&1; then
      rm -rf "$target"
    else
      mkdir -p "$DRAFTED_BACKUP_DIR"
      backup="$DRAFTED_BACKUP_DIR/$(basename "$target").$(date +%Y%m%d%H%M%S)"
      mv "$target" "$backup"
      echo -e "    ${YELLOW}Existing $(basename "$target") moved aside → $backup${RESET}"
    fi
  fi
  ln -s "$source_real" "$target"
}

# Remove links WE made whose source is gone — an entry dropped from the package, or the package
# uninstalled entirely. A dangling skill dir is worse than a stale one: it looks installed and
# loads nothing. Matched on the link's own target path so other installers' links are untouched.
sweep_plugin_links() {
  local dir="$1" marker="$2" link
  [ -d "$dir" ] || return 0
  while IFS= read -r link; do
    case "$(readlink "$link")" in
      *"$marker"*)
        if [ ! -e "$link" ]; then
          rm -f "$link"
          echo -e "    ${YELLOW}Removed stale link: $(basename "$link")${RESET}"
        fi
        ;;
    esac
  done < <(find "$dir" -maxdepth 1 -type l 2>/dev/null)
}

# Skills — Claude Code auto-loads these from ~/.claude/skills/<name>/SKILL.md
SKILLS_DIR="$HOME/.claude/skills"
sweep_plugin_links "$SKILLS_DIR" "/plugin/skills/"
if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/skills" ]; then
  mkdir -p "$SKILLS_DIR"
  SKILL_COUNT=0
  for d in "$PLUGIN_SRC/skills/"*/; do
    [ -d "$d" ] || continue
    DIRNAME="$(basename "$d")"
    link_plugin_entry "${d%/}" "$SKILLS_DIR/$DIRNAME"
    SKILL_COUNT=$((SKILL_COUNT + 1))
  done
  ok "Linked $SKILL_COUNT skill(s) into $SKILLS_DIR (source: $PLUGIN_SRC/skills)"
else
  echo -e "    ${YELLOW}Skill source not found in package — skipping skills.${RESET}"
fi

# Commands — namespaced under drafted/ so they register as /drafted:<name>,
# matching the marketplace install and the commands' own cross-references. Linked per FILE into a
# real directory (not a linked directory) so discovery is unchanged; the containing dir is no
# longer rm -rf'd, so anything else a user put there survives.
CLAUDE_CMD_DIR="$HOME/.claude/commands/drafted"
CODEX_CMD_DIR="$HOME/.codex/prompts/drafted"
sweep_plugin_links "$CLAUDE_CMD_DIR" "/plugin/commands/"
sweep_plugin_links "$CODEX_CMD_DIR" "/plugin/commands/"
if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/commands" ]; then
  link_commands_into() {
    local dest="$1" count=0 f
    mkdir -p "$dest"
    for f in "$PLUGIN_SRC/commands/"*.md; do
      [ -f "$f" ] || continue
      link_plugin_entry "$f" "$dest/$(basename "$f")"
      count=$((count + 1))
    done
    ok "Linked $count command(s) into $dest"
  }

  if [ "$CLIENT_CLAUDE_CODE" = true ]; then link_commands_into "$CLAUDE_CMD_DIR"; fi
  if [ "$CLIENT_CODEX" = true ]; then link_commands_into "$CODEX_CMD_DIR"; fi
else
  echo -e "    ${YELLOW}Command source not found in package — skipping commands.${RESET}"
fi


# ── Update menu bar helper ───────────────────────────────────────

# Idempotently remove the legacy Swift menu-bar updater so it can neither run alongside the
# new desktop app nor be resurrected at next login. Safe to call when nothing legacy exists.
retire_legacy_updater() {
  local uid legacy_plist
  uid="$(id -u)"
  legacy_plist="$HOME/Library/LaunchAgents/live.drafted.updater.plist"
  launchctl bootout "gui/$uid/live.drafted.updater" >/dev/null 2>&1 || true
  if [ -f "$legacy_plist" ]; then
    launchctl bootout "gui/$uid" "$legacy_plist" >/dev/null 2>&1 || true
  fi
  rm -f "$legacy_plist" >/dev/null 2>&1 || true
  pkill -x DraftedUpdater >/dev/null 2>&1 || true
  rm -rf "/Applications/Drafted Updater.app" "$HOME/Applications/Drafted Updater.app" >/dev/null 2>&1 || true
}

# Install (or detect) the Tauri desktop app that replaces the legacy menu-bar updater.
# Returns 0 ONLY when a valid Drafted.app is present afterward, so the caller can safely
# retire the legacy updater. Returns 1 when no new bundle is available yet — the caller then
# falls back to building the legacy Swift updater, so users are never left without a tray.
install_desktop_app() {
  local install_dir app_bundle app_exe uid plist tmp tarball url
  install_dir="/Applications"
  [ -w "$install_dir" ] || install_dir="$HOME/Applications"
  mkdir -p "$install_dir"
  app_bundle="$install_dir/Drafted.app"

  if [ -n "${DRAFTED_DESKTOP_APP:-}" ] && [ -d "${DRAFTED_DESKTOP_APP}" ]; then
    # Local prebuilt bundle — lets us test the cutover before the CDN endpoint exists.
    rm -rf "$app_bundle" >/dev/null 2>&1 || true
    ditto "${DRAFTED_DESKTOP_APP}" "$app_bundle" >/dev/null 2>&1 || true
  else
    # Signed bundle tarball (expects Drafted.app at the archive root). Until this endpoint is
    # published the request 404s and we fall through to legacy — that is the cutover gate.
    url="${DRAFTED_DESKTOP_URL:-$INSTALL_SERVER/desktop/Drafted-macos.tar.gz}"
    tmp="$(mktemp -d)"
    tarball="$tmp/Drafted-macos.tar.gz"
    if curl -fsSL --max-time 120 "$url" -o "$tarball" >/dev/null 2>&1; then
      rm -rf "$app_bundle" >/dev/null 2>&1 || true
      tar -xzf "$tarball" -C "$install_dir" >/dev/null 2>&1 || true
    fi
    rm -rf "$tmp" >/dev/null 2>&1 || true
  fi

  # Validate: a real bundle whose declared executable exists. The bundle dir is Drafted.app
  # (productName) but the binary is the Cargo bin name (drafted-desktop), so read the actual
  # name from Info.plist rather than assuming. If absent/invalid, signal "not available yet".
  local exe_name
  exe_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app_bundle/Contents/Info.plist" 2>/dev/null || true)"
  app_exe="$app_bundle/Contents/MacOS/$exe_name"
  if [ -z "$exe_name" ] || [ ! -x "$app_exe" ]; then
    return 1
  fi

  # Clear quarantine so Gatekeeper doesn't block a freshly downloaded bundle.
  xattr -dr com.apple.quarantine "$app_bundle" >/dev/null 2>&1 || true

  # Register run-at-login for the new app (mirrors the legacy launch agent). Write the
  # plist BEFORE touching launchd so the bootstrap below loads the new binary path.
  uid="$(id -u)"
  plist="$HOME/Library/LaunchAgents/live.drafted.desktop.plist"
  mkdir -p "$HOME/Library/LaunchAgents"
  cat > "$plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>live.drafted.desktop</string>
  <key>ProgramArguments</key>
  <array>
    <string>$app_exe</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
</dict>
</plist>
PLIST
  # Restart under launchd DETERMINISTICALLY. bootout is async: bootstrapping before the old
  # service is fully torn down races, and when it loses the label is left UNLOADED — the app
  # dies with no KeepAlive to save it (that bricked the in-app updater, which is itself a child
  # of the very app being replaced). And a bare pkill while the service is still loaded just
  # trips KeepAlive into respawning the old binary mid-swap. So: bootout, POLL until the label
  # is actually gone, kill any stray non-launchd instance, bootstrap the new plist, and
  # kickstart -k to guarantee it comes up (RunAtLoad alone is not proof it started).
  local svc="gui/$uid/live.drafted.desktop"
  launchctl bootout "$svc" >/dev/null 2>&1 || true
  local waited=0
  while [ "$waited" -lt 40 ] && launchctl print "$svc" >/dev/null 2>&1; do
    sleep 0.25
    waited=$((waited + 1))
  done
  pkill -x "$exe_name" >/dev/null 2>&1 || true
  launchctl bootstrap "gui/$uid" "$plist" >/dev/null 2>&1 || true
  launchctl kickstart -k "$svc" >/dev/null 2>&1 || true
  return 0
}

install_update_menu_icon() {
  if [ "${DRAFTED_HEADLESS:-}" = "1" ]; then
    echo -e "    ${YELLOW}Headless mode: skipping update menu icon.${RESET}"
    return 0
  fi
  if [ "$(uname -s)" != "Darwin" ]; then
    echo -e "    ${YELLOW}Update menu icon is only installed on macOS by this script.${RESET}"
    return 0
  fi

  # Prefer the Tauri desktop app. If it installs (or is already present and valid), retire the
  # legacy Swift updater so the two can never run side by side or be resurrected at next login.
  if install_desktop_app; then
    retire_legacy_updater
    ok "Drafted desktop app"
    return 0
  fi
  echo -e "    ${YELLOW}Desktop app bundle not available yet; using the menu bar updater.${RESET}"

  if ! command -v swiftc >/dev/null 2>&1; then
    echo -e "    ${YELLOW}Swift compiler not found; skipping macOS menu bar updater.${RESET}"
    return 0
  fi

  local work_dir install_dir app_bundle app_macos app_resources swift_file exe_path plist_dir plist uid logo_path legacy_bundle
  work_dir="$HOME/.drafted/updater"
  install_dir="/Applications"
  if [ ! -w "$install_dir" ]; then
    install_dir="$HOME/Applications"
  fi
  app_bundle="$install_dir/Drafted Updater.app"
  app_macos="$app_bundle/Contents/MacOS"
  app_resources="$app_bundle/Contents/Resources"
  swift_file="$work_dir/DraftedUpdater.swift"
  exe_path="$app_macos/DraftedUpdater"
  plist_dir="$HOME/Library/LaunchAgents"
  plist="$plist_dir/live.drafted.updater.plist"
  uid="$(id -u)"
  legacy_bundle="$work_dir/DraftedUpdater.app"
  mkdir -p "$work_dir" "$install_dir" "$app_macos" "$app_resources" "$plist_dir"
  rm -rf "$legacy_bundle"

  logo_path="$app_resources/logo.svg"
  if [ -f "$SCRIPT_DIR/server/vendor/logo.svg" ]; then
    cp "$SCRIPT_DIR/server/vendor/logo.svg" "$logo_path"
  else
    curl -fsSL "$INSTALL_SERVER/vendor/logo.svg" -o "$logo_path" >/dev/null 2>&1 || true
  fi
  if [ -f "$logo_path" ]; then
    perl -0pi -e 's/fill="currentColor"/fill="#FFFFFF"/g; s/fill="var\(--logo-letter, #[^)]+\)"/fill="#0A2540"/g' "$logo_path" >/dev/null 2>&1 || true
  fi

  cat > "$swift_file" <<'SWIFT'
import SwiftUI
import AppKit

@main
struct DraftedUpdaterApp: App {
  @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

  init() {
    Telemetry.report(event: "drafted_update_helper_started", updateHelperStatus: "running")
    Telemetry.report(event: "drafted_heartbeat", updateHelperStatus: "running")
  }

  var body: some Scene {
    MenuBarExtra {
      Button("Check for Updates…") { Actions.checkForUpdate(userInitiated: true) }
      Button("Update Drafted") { Actions.updateDrafted() }
      Button("View Logs") { Actions.viewLogs() }
      Button("Open Drafted") { Actions.openDrafted() }
      Divider()
      Button("Quit") { NSApp.terminate(nil) }
    } label: {
      if let image = Actions.logoImage() {
        Image(nsImage: image)
          .resizable()
          .frame(width: 16, height: 18)
          .accessibilityLabel("Drafted")
      } else {
        Text("Drafted")
      }
    }
    .menuBarExtraStyle(.menu)
  }
}

// Drives the genuine auto-update: a background check shortly after login and
// every 6 hours while the menu-bar helper is resident. When a newer published
// version exists (and the user hasn't skipped it), it prompts Update / Skip.
final class AppDelegate: NSObject, NSApplicationDelegate {
  private var timer: Timer?

  func applicationDidFinishLaunching(_ notification: Notification) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 25) {
      Actions.checkForUpdate(userInitiated: false)
    }
    let t = Timer(timeInterval: 6 * 3600, repeats: true) { _ in
      Actions.checkForUpdate(userInitiated: false)
    }
    RunLoop.main.add(t, forMode: .common)
    timer = t
  }
}


final class UpdateStatusWindow: NSWindowController {
  private let stack = NSStackView()
  private let spinner = NSProgressIndicator()
  private let titleField = NSTextField(labelWithString: "Updating Drafted…")
  private let bodyField = NSTextField(labelWithString: "Downloading and running the installer. This can take a minute.")
  private let closeButton = NSButton(title: "OK", target: nil, action: nil)

  init() {
    let panel = NSPanel(
      contentRect: NSRect(x: 0, y: 0, width: 380, height: 168),
      styleMask: [.titled, .closable],
      backing: .buffered,
      defer: false
    )
    panel.title = "Drafted Update"
    panel.isReleasedWhenClosed = false
    panel.level = .floating
    super.init(window: panel)

    stack.orientation = .vertical
    stack.alignment = .centerX
    stack.spacing = 12
    stack.translatesAutoresizingMaskIntoConstraints = false

    spinner.style = .spinning
    spinner.controlSize = .regular

    titleField.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
    titleField.alignment = .center

    bodyField.font = NSFont.systemFont(ofSize: 13)
    bodyField.textColor = .secondaryLabelColor
    bodyField.alignment = .center
    bodyField.maximumNumberOfLines = 3
    bodyField.lineBreakMode = .byWordWrapping

    closeButton.target = self
    closeButton.action = #selector(closeClicked)
    closeButton.isHidden = true

    stack.addArrangedSubview(spinner)
    stack.addArrangedSubview(titleField)
    stack.addArrangedSubview(bodyField)
    stack.addArrangedSubview(closeButton)

    panel.contentView = NSView()
    panel.contentView?.addSubview(stack)
    NSLayoutConstraint.activate([
      stack.leadingAnchor.constraint(equalTo: panel.contentView!.leadingAnchor, constant: 28),
      stack.trailingAnchor.constraint(equalTo: panel.contentView!.trailingAnchor, constant: -28),
      stack.centerYAnchor.constraint(equalTo: panel.contentView!.centerYAnchor)
    ])
  }

  required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

  func showUpdating() {
    titleField.stringValue = "Updating Drafted…"
    bodyField.stringValue = "Downloading and running the installer. This can take a minute."
    closeButton.isHidden = true
    spinner.isHidden = false
    spinner.startAnimation(nil)
    NSApp.activate(ignoringOtherApps: true)
    showWindow(nil)
    window?.center()
  }

  func showCompleted(success: Bool, message: String) {
    spinner.stopAnimation(nil)
    spinner.isHidden = true
    titleField.stringValue = success ? "Update completed" : "Update failed"
    bodyField.stringValue = message
    closeButton.isHidden = false
    NSApp.activate(ignoringOtherApps: true)
    showWindow(nil)
    window?.center()
  }

  @objc private func closeClicked() { window?.close() }
}

struct Actions {
  static let home = FileManager.default.homeDirectoryForCurrentUser

  static func logoImage() -> NSImage? {
    guard
      let url = Bundle.main.url(forResource: "logo", withExtension: "svg"),
      let image = NSImage(contentsOf: url)
    else { return nil }
    image.size = NSSize(width: 16, height: 18)
    image.isTemplate = false
    return image
  }

  private static var isUpdating = false
  private static var updateWindow: UpdateStatusWindow?

  // MARK: - Auto-update check

  // Compare the installed CLI version to npm's published `latest`. Prompt only
  // when newer and not skipped (background), or always report (user-initiated).
  static func checkForUpdate(userInitiated: Bool) {
    let installed = installedVersion()
    latestVersion { latest in
      DispatchQueue.main.async {
        guard let latest = latest else {
          if userInitiated {
            showInfo(title: "Couldn’t check for updates", body: "Please check your connection and try again.")
          }
          return
        }
        guard let installed = installed, isNewer(latest, than: installed) else {
          if userInitiated {
            showInfo(title: "Drafted is up to date", body: "You’re on \(installed ?? "the latest") version.")
          }
          return
        }
        if !userInitiated && skippedVersion() == latest { return }
        presentUpdateAlert(latest: latest, installed: installed)
      }
    }
  }

  static func presentUpdateAlert(latest: String, installed: String) {
    Telemetry.report(event: "drafted_update_available", updateHelperStatus: "running")
    let alert = NSAlert()
    alert.messageText = "Drafted \(latest) is available"
    alert.informativeText = "You have \(installed). Update now? Your editor needs to be restarted afterward to load the new MCP tools."
    alert.addButton(withTitle: "Update")
    alert.addButton(withTitle: "Skip This Version")
    alert.addButton(withTitle: "Later")
    if let logo = logoImage() { alert.icon = logo }
    NSApp.activate(ignoringOtherApps: true)
    switch alert.runModal() {
    case .alertFirstButtonReturn:
      updateDrafted()
    case .alertSecondButtonReturn:
      setSkippedVersion(latest)
      Telemetry.report(event: "drafted_update_skipped", updateHelperStatus: "running")
    default:
      break
    }
  }

  static func showInfo(title: String, body: String) {
    let alert = NSAlert()
    alert.messageText = title
    alert.informativeText = body
    alert.addButton(withTitle: "OK")
    if let logo = logoImage() { alert.icon = logo }
    NSApp.activate(ignoringOtherApps: true)
    alert.runModal()
  }

  // Installed version, resolved WITHOUT depending on the login-shell PATH —
  // the installer's npm prefix (~/.drafted/npm-global) is never written to any
  // login profile, so `bash -lc "drafted"` finds nothing. Read the installed
  // package.json at the fixed prefix first (the source npm itself writes), then
  // fall back to the absolute binary, then a login shell (custom prefixes on PATH).
  static func installedVersion() -> String? {
    let pkg = home.appendingPathComponent(".drafted/npm-global/lib/node_modules/drafted/package.json")
    if
      let data = try? Data(contentsOf: pkg),
      let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
      let version = json["version"] as? String,
      isSemver(version)
    { return version }

    let bin = home.appendingPathComponent(".drafted/npm-global/bin/drafted").path
    for command in ["\"\(bin)\" --version 2>/dev/null", "drafted --version 2>/dev/null"] {
      let process = Process()
      process.executableURL = URL(fileURLWithPath: "/bin/bash")
      process.arguments = ["-lc", command]
      let pipe = Pipe()
      process.standardOutput = pipe
      process.standardError = FileHandle.nullDevice
      do { try process.run(); process.waitUntilExit() } catch { continue }
      let data = pipe.fileHandleForReading.readDataToEndOfFile()
      if let out = String(data: data, encoding: .utf8), let version = firstSemver(in: out) {
        return version
      }
    }
    return nil
  }

  static func latestVersion(_ completion: @escaping (String?) -> Void) {
    guard let url = URL(string: "https://registry.npmjs.org/drafted/latest") else { completion(nil); return }
    var request = URLRequest(url: url)
    request.timeoutInterval = 15
    URLSession.shared.dataTask(with: request) { data, _, _ in
      guard
        let data = data,
        let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
        let version = json["version"] as? String
      else { completion(nil); return }
      completion(version)
    }.resume()
  }

  static func skippedVersion() -> String? {
    let path = home.appendingPathComponent(".drafted/install.json")
    guard
      let data = try? Data(contentsOf: path),
      let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
    else { return nil }
    return json["skippedVersion"] as? String
  }

  static func setSkippedVersion(_ version: String) {
    let path = home.appendingPathComponent(".drafted/install.json")
    var json = ((try? Data(contentsOf: path)).flatMap { try? JSONSerialization.jsonObject(with: $0) } as? [String: Any]) ?? [:]
    json["skippedVersion"] = version
    if let out = try? JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys]) {
      try? out.write(to: path)
    }
  }

  // First dotted-numeric token in the text, e.g. "1.11.7" from "drafted 1.11.7".
  static func firstSemver(in text: String) -> String? {
    var current = ""
    for ch in text {
      if ch.isNumber || ch == "." {
        current.append(ch)
      } else {
        if isSemver(current) { return current }
        current = ""
      }
    }
    return isSemver(current) ? current : nil
  }

  static func isSemver(_ s: String) -> Bool {
    let parts = s.split(separator: ".")
    return parts.count >= 2 && parts.allSatisfy { !$0.isEmpty && $0.allSatisfy { $0.isNumber } }
  }

  // Numeric, dot-separated comparison (our versions have no prerelease tags).
  static func isNewer(_ a: String, than b: String) -> Bool {
    let pa = a.split(separator: ".").map { Int($0) ?? 0 }
    let pb = b.split(separator: ".").map { Int($0) ?? 0 }
    for i in 0..<max(pa.count, pb.count) {
      let x = i < pa.count ? pa[i] : 0
      let y = i < pb.count ? pb[i] : 0
      if x != y { return x > y }
    }
    return false
  }

  static func updateDrafted() {
    if isUpdating {
      updateWindow?.showUpdating()
      return
    }

    isUpdating = true
    let statusWindow = UpdateStatusWindow()
    updateWindow = statusWindow
    statusWindow.showUpdating()

    let command = "tmp=$(mktemp); curl -fsSL https://drafted.live/install.sh -o \"$tmp\" && bash \"$tmp\""
    let process = Process()
    process.executableURL = URL(fileURLWithPath: "/bin/bash")
    process.arguments = ["-lc", command]
    process.terminationHandler = { proc in
      if proc.terminationStatus != 0 {
        Telemetry.report(event: "drafted_update_helper_failed", updateHelperStatus: "failed", errorCode: "installer_exit_\(proc.terminationStatus)")
      }
      DispatchQueue.main.async {
        isUpdating = false
        let success = proc.terminationStatus == 0
        statusWindow.showCompleted(
          success: success,
          message: success ? "Restart your editor to use the latest MCP tools." : "Run the Drafted installer again from drafted.live/install."
        )
      }
    }
    do { try process.run() } catch {
      isUpdating = false
      Telemetry.report(event: "drafted_update_helper_failed", updateHelperStatus: "failed", errorCode: "process_run_failed")
      statusWindow.showCompleted(success: false, message: error.localizedDescription)
      return
    }
  }

  static func viewLogs() {
    let fm = FileManager.default
    let home = fm.homeDirectoryForCurrentUser
    let fallback = home.appendingPathComponent(".drafted")
    try? fm.createDirectory(at: fallback, withIntermediateDirectories: true)
    let readme = fallback.appendingPathComponent("logs-readme.txt")
    if !fm.fileExists(atPath: readme.path) {
      let text = """
Drafted MCP logs are written by the host app that launched drafted-mcp.

Most useful locations:
- Drafted MCP client errors: ~/.drafted/mcp-client.log
- Claude Desktop: ~/Library/Logs/Claude/mcp-server-drafted.log
- Claude Code: ~/.claude/projects (session transcripts) or ~/.claude/logs when present
- Drafted installer/updater: ~/.drafted

If a tool returns `fetch failed`, open the Claude log or transcript from the same run and look for `mcp-server-drafted` or `Drafted MCP`.
"""
      try? text.write(to: readme, atomically: true, encoding: .utf8)
    }

    let candidates = [
      home.appendingPathComponent(".drafted/mcp-client.log"),
      home.appendingPathComponent("Library/Logs/Claude/mcp-server-drafted.log"),
      home.appendingPathComponent("Library/Logs/Claude"),
      home.appendingPathComponent(".claude/logs"),
      home.appendingPathComponent(".claude/projects"),
      fallback,
    ]
    for url in candidates where fm.fileExists(atPath: url.path) {
      var isDirectory: ObjCBool = false
      fm.fileExists(atPath: url.path, isDirectory: &isDirectory)
      if isDirectory.boolValue {
        NSWorkspace.shared.open(url)
      } else {
        NSWorkspace.shared.activateFileViewerSelecting([url])
      }
      return
    }
  }

  static func openDrafted() {
    NSWorkspace.shared.open(URL(string: "https://drafted.live")!)
  }
}

struct Telemetry {
  static func report(event: String, updateHelperStatus: String, errorCode: String? = nil) {
    guard ProcessInfo.processInfo.environment["DRAFTED_TELEMETRY"] != "0" else { return }
    let path = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".drafted/install.json")
    guard
      let data = try? Data(contentsOf: path),
      let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
      json["telemetry"] as? Bool != false,
      let installId = json["installId"] as? String
    else { return }
    var body: [String: Any] = [
      "installId": installId,
      "event": event,
      "schemaVersion": 1,
      "osFamily": "macos",
      "osVersion": ProcessInfo.processInfo.operatingSystemVersionString,
      "arch": SystemVersion.machine,
      "updateHelperStatus": updateHelperStatus,
      "source": "macos-helper"
    ]
    if let errorCode = errorCode { body["errorCode"] = errorCode }
    guard
      let url = URL(string: "https://drafted.live/api/installations/report"),
      let payload = try? JSONSerialization.data(withJSONObject: body)
    else { return }
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = payload
    URLSession.shared.dataTask(with: request).resume()
  }
}

enum SystemVersion {
  static var machine: String {
    #if arch(arm64)
    return "arm64"
    #elseif arch(x86_64)
    return "x64"
    #else
    return "unknown"
    #endif
  }
}
SWIFT

  cat > "$app_bundle/Contents/Info.plist" <<APPPLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleExecutable</key>
  <string>DraftedUpdater</string>
  <key>CFBundleIdentifier</key>
  <string>live.drafted.updater</string>
  <key>CFBundleName</key>
  <string>Drafted Updater</string>
  <key>CFBundleDisplayName</key>
  <string>Drafted Updater</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleShortVersionString</key>
  <string>1.0</string>
  <key>LSMinimumSystemVersion</key>
  <string>13.0</string>
  <key>LSUIElement</key>
  <true/>
  <key>NSHighResolutionCapable</key>
  <true/>
</dict>
</plist>
APPPLIST

  if ! swiftc -parse-as-library "$swift_file" -o "$exe_path" -framework SwiftUI -framework AppKit >/dev/null 2>&1; then
    echo -e "    ${YELLOW}Could not build macOS menu bar updater; skipping.${RESET}"
    return 0
  fi
  codesign -s - -f "$exe_path" >/dev/null 2>&1 || true

  cat > "$plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>live.drafted.updater</string>
  <key>ProgramArguments</key>
  <array>
    <string>open</string>
    <string>-a</string>
    <string>$app_bundle</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <false/>
</dict>
</plist>
PLIST

  launchctl bootout "gui/$uid" "$plist" >/dev/null 2>&1 || true
  pkill -x DraftedUpdater >/dev/null 2>&1 || true
  mdimport "$app_bundle" >/dev/null 2>&1 || true
  open "$app_bundle" >/dev/null 2>&1 || true
  ok "macOS menu bar updater"
}
if [ "$INSTALL_MODE" = "production" ]; then
  step "Installing update helper"
  install_update_menu_icon
fi
verify_no_legacy_http_config
report_telemetry "drafted_mcp_configured" "installed"
report_telemetry "drafted_install" "installed"

# ── Done ─────────────────────────────────────────────────────────

echo ""
echo ""
echo -e "${GREEN}${BOLD}You're all set!${RESET}"
echo ""
echo -e "  ${DIM}MCP name:${RESET}  ${BOLD}$INSTALL_NAME${RESET}"
echo -e "  ${DIM}Server:${RESET}    ${BOLD}$INSTALL_SERVER${RESET}"
echo -e "  ${DIM}To update production:${RESET}  rerun the installer from drafted.live/install (curl -fsSL https://drafted.live/install.sh -o /tmp/install-drafted.sh && bash /tmp/install-drafted.sh)"
echo -e "  ${DIM}To uninstall:${RESET}  npm uninstall -g drafted --prefix ~/.drafted/npm-global && rm -rf ~/.drafted"
echo ""
echo -e "${YELLOW}${BOLD}"
echo "  ┌─────────────────────────────────────────────────────────┐"
echo "  │                                                         │"
echo "  │   >>>  RESTART YOUR EDITOR TO ACTIVATE DRAFTED  <<<     │"
echo "  │                                                         │"
echo "  │   Close and reopen Claude Desktop, Claude Code,         │"
echo "  │   Codex, or Cursor so it picks up the new MCP server.   │"
echo "  │                                                         │"
echo "  └─────────────────────────────────────────────────────────┘"
echo -e "${RESET}"
