#!/bin/sh

umask 022

BASE_URL=${DOCSTRAL_BASE_URL:-https://docstral-mcp.solutions.mistralsol.com}
BASE_URL=${BASE_URL%/}
MCP_URL=${DOCSTRAL_MCP_URL:-$BASE_URL/mcp}
START_MARKER='<!-- docstral-managed:start -->'
END_MARKER='<!-- docstral-managed:end -->'
FAILURES=0
HAS_CODEX=0
HAS_CLAUDE=0
HAS_OPENCODE=0
HAS_VIBE=0
HAS_CURSOR=0

BOLD=
DIM=
GREEN=
ORANGE=
RED=
RESET=
if [ -t 1 ] && [ "${TERM:-dumb}" != dumb ] && [ -z "${NO_COLOR:-}" ]; then
    BOLD=$(printf '\033[1m')
    DIM=$(printf '\033[2m')
    GREEN=$(printf '\033[32m')
    ORANGE=$(printf '\033[38;5;208m')
    RED=$(printf '\033[31m')
    RESET=$(printf '\033[0m')
fi

require_command() {
    if ! command -v "$1" >/dev/null 2>&1; then
        printf 'docstral: required command not found: %s\n' "$1" >&2
        exit 1
    fi
}

for command_name in awk cat chmod cmp cp curl dd dirname grep mkdir mktemp mv readlink rm sed stat; do
    require_command "$command_name"
done

if [ -z "${HOME:-}" ]; then
    printf 'docstral: HOME is not set\n' >&2
    exit 1
fi

WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/docstral-install.XXXXXX") || exit 1
trap 'rm -rf "$WORK_DIR"' 0
trap 'exit 1' HUP INT TERM

SKILL_SOURCE="$WORK_DIR/SKILL.md"
MCP_UPDATER="$WORK_DIR/update-mcp.js"

if ! curl -fsSL "$BASE_URL/skills/docstral/SKILL.md" -o "$SKILL_SOURCE" || [ ! -s "$SKILL_SOURCE" ]; then
    printf 'docstral: failed to download the skill\n' >&2
    exit 1
fi

file_mode() {
    mode=$(stat -f '%Lp' "$1" 2>/dev/null)
    case "$mode" in
        ''|*[!0-7]*) stat -c '%a' "$1" 2>/dev/null ;;
        *) printf '%s\n' "$mode" ;;
    esac
}

resolve_destination() {
    destination=$1
    remaining=20
    while [ -L "$destination" ]; do
        [ "$remaining" -gt 0 ] || return 1
        link=$(readlink "$destination") || return 1
        case "$link" in
            /*) destination=$link ;;
            *) destination=$(dirname "$destination")/$link ;;
        esac
        remaining=$((remaining - 1))
    done
    printf '%s\n' "$destination"
}

preflight_destination() (
    label=$1
    destination=$(resolve_destination "$2") || {
        printf 'docstral: cannot resolve %s destination %s\n' "$label" "$2" >&2
        return 1
    }
    if [ -e "$destination" ] && [ ! -f "$destination" ]; then
        printf 'docstral: %s destination is not a file: %s\n' "$label" "$destination" >&2
        return 1
    fi
    if [ -f "$destination" ]; then
        if [ ! -r "$destination" ] || ! file_mode "$destination" >/dev/null; then
            printf 'docstral: cannot read %s destination %s\n' "$label" "$destination" >&2
            return 1
        fi
    fi
    directory=$(dirname "$destination")
    ancestor=$directory
    while [ ! -e "$ancestor" ]; do
        parent=$(dirname "$ancestor")
        [ "$parent" != "$ancestor" ] || {
            printf 'docstral: cannot find a writable parent for %s\n' "$destination" >&2
            return 1
        }
        ancestor=$parent
    done
    if [ ! -d "$ancestor" ] || [ ! -w "$ancestor" ] || [ ! -x "$ancestor" ]; then
        printf 'docstral: parent directory is not writable for %s\n' "$destination" >&2
        return 1
    fi
)

stage_config() (
    destination=$1
    staged=$2
    if [ -f "$destination" ]; then
        cp -p "$destination" "$staged.original" && cp -p "$staged.original" "$staged" && chmod u+w "$staged"
    else
        [ ! -e "$destination" ] && [ ! -L "$destination" ] && rm -f "$staged"
    fi
)

check_snapshot() (
    original=$1
    destination=$2
    if [ ! -L "$destination" ]; then
        if [ -f "$original" ]; then
            if cmp -s "$original" "$destination" && \
                [ "$(file_mode "$original")" = "$(file_mode "$destination")" ]; then
                return 0
            fi
        elif [ ! -e "$destination" ]; then
            return 0
        fi
    fi
    printf 'docstral: configuration changed during installation; left untouched: %s. Rerun the installer.\n' "$destination" >&2
    return 1
)

commit_config() (
    source=$1
    destination=$2
    backup=$3
    preflight_destination config "$destination" || return 1
    check_snapshot "$source.original" "$destination" || return 1
    directory=$(dirname "$destination")
    mkdir -p "$directory" || return 1
    temporary=$(mktemp "$directory/.docstral-config.XXXXXX") || return 1
    mode=${4:-600}
    if [ -f "$destination" ]; then
        mode=$(file_mode "$destination") || {
            rm -f "$temporary"
            return 1
        }
        if [ -n "$backup" ] && [ ! -e "$backup" ] && ! cp -p "$source.original" "$backup"; then
            rm -f "$temporary"
            return 1
        fi
        if ! cp -p "$destination" "$temporary"; then
            rm -f "$temporary"
            return 1
        fi
    fi
    if ! chmod u+w "$temporary" || ! : > "$temporary" || ! dd if="$source" 2>/dev/null >> "$temporary" || \
        ! chmod "$mode" "$temporary" || ! check_snapshot "$source.original" "$destination" || \
        ! mv -f "$temporary" "$destination"; then
        rm -f "$temporary"
        return 1
    fi
)

configure_mcp() (
    label=$1
    config=$(resolve_destination "$2") || {
        printf 'docstral: cannot resolve %s MCP configuration\n' "$label" >&2
        printf 'failed\n'
        exit
    }
    if ! preflight_destination config "$config"; then
        printf 'failed\n'
        exit
    fi
    staged_home="$WORK_DIR/$label-home"
    staged_config="$staged_home/$3"
    if ! mkdir -p "$staged_home" || ! stage_config "$config" "$staged_config"; then
        printf 'failed\n'
        exit
    fi
    status=$("$4" "$staged_home" "$staged_config" "$config") || status=failed
    case "$status" in
        installed|updated)
            if ! commit_config "$staged_config" "$config" "$config.docstral.bak"; then
                printf 'docstral: cannot publish %s MCP configuration\n' "$label" >&2
                status=failed
            fi
            ;;
        unchanged|disabled|failed) ;;
        *) status=failed ;;
    esac
    printf '%s\n' "$status"
)

install_skill() (
    destination=$(resolve_destination "$1") || {
        printf 'docstral: cannot resolve skill destination %s\n' "$1" >&2
        printf 'failed\n'
        exit
    }
    staged=$(mktemp "$WORK_DIR/skill.XXXXXX") || {
        printf 'failed\n'
        exit
    }
    if ! preflight_destination skill "$destination" || ! stage_config "$destination" "$staged"; then
        printf 'failed\n'
        exit
    fi
    if [ -f "$staged.original" ] && cmp -s "$SKILL_SOURCE" "$staged.original"; then
        printf 'unchanged\n'
        exit
    fi
    status=installed
    if [ -f "$staged.original" ]; then
        status=updated
    fi
    if ! cp "$SKILL_SOURCE" "$staged" || ! commit_config "$staged" "$destination" '' 644; then
        printf 'docstral: cannot install skill at %s\n' "$destination" >&2
        printf 'failed\n'
        exit
    fi
    printf '%s\n' "$status"
)

marker_info() {
    LC_ALL=C awk -v start="$START_MARKER" -v end="$END_MARKER" '
        BEGIN { offset = 0 }
        {
            line = $0
            sub(/\r$/, "", line)
        }
        line == start {
            starts += 1
            if (start_line == 0) {
                start_line = NR
                start_offset = offset
            }
        }
        line == end {
            ends += 1
            if (end_line == 0) {
                end_line = NR
                end_offset = offset + length($0) + 1
            }
        }
        { offset += length($0) + 1 }
        END {
            if (starts == 0 && ends == 0) print "absent"
            else if (starts == 1 && ends == 1 && start_line < end_line) {
                print "valid", start_offset, end_offset
            }
            else print "invalid"
        }
    ' "$1"
}

remove_legacy_instructions() (
    destination=$(resolve_destination "$1") || {
        printf 'docstral: cannot resolve instruction destination %s\n' "$1" >&2
        printf 'failed\n'
        exit
    }
    if [ ! -e "$destination" ]; then
        printf 'unchanged\n'
        exit
    fi
    if [ ! -f "$destination" ] || [ ! -r "$destination" ]; then
        printf 'docstral: cannot read instruction destination %s\n' "$destination" >&2
        printf 'failed\n'
        exit
    fi
    info=$(marker_info "$destination") || {
        printf 'docstral: cannot inspect %s\n' "$destination" >&2
        printf 'failed\n'
        exit
    }
    set -- $info
    state=$1
    if [ "$state" = invalid ]; then
        printf 'docstral: malformed managed block in %s\n' "$destination" >&2
        printf 'failed\n'
        exit
    fi
    if [ "$state" = absent ]; then
        printf 'unchanged\n'
        exit
    fi
    temporary=$(mktemp "$WORK_DIR/instructions.XXXXXX") || {
        printf 'failed\n'
        exit
    }
    if ! preflight_destination instruction "$destination" || ! stage_config "$destination" "$temporary"; then
        printf 'failed\n'
        exit
    fi
    original="$temporary.original"
    info=$(marker_info "$original") || {
        printf 'docstral: cannot inspect %s\n' "$destination" >&2
        printf 'failed\n'
        exit
    }
    set -- $info
    if [ "$1" = absent ]; then
        printf 'unchanged\n'
        exit
    fi
    if [ "$1" = invalid ]; then
        printf 'docstral: malformed managed block in %s\n' "$destination" >&2
        printf 'failed\n'
        exit
    fi
    start_offset=$2
    end_offset=$3
    if ! {
        dd if="$original" bs=1 count="$start_offset" 2>/dev/null > "$temporary" &&
            dd if="$original" bs=1 skip="$end_offset" 2>/dev/null >> "$temporary"
    }; then
        rm -f "$temporary"
        printf 'failed\n'
        exit
    fi
    if ! commit_config "$temporary" "$destination" "$destination.docstral.bak"; then
        rm -f "$temporary"
        printf 'docstral: cannot update %s\n' "$destination" >&2
        printf 'failed\n'
        exit
    fi
    printf 'removed\n'
)

cleanup_legacy_instructions() {
    combined=unchanged
    for destination in "$@"; do
        status=$(remove_legacy_instructions "$destination")
        if [ "$status" = failed ]; then
            combined=failed
        elif [ "$status" = removed ] && [ "$combined" = unchanged ]; then
            combined=removed
        fi
    done
    printf '%s\n' "$combined"
}
codex_mcp() {
    configure_mcp Codex "${CODEX_HOME:-$HOME/.codex}/config.toml" config.toml configure_codex_mcp
}

configure_codex_mcp() (
    staged_home=$1
    config=$3
    output=$(CODEX_HOME="$staged_home" codex mcp get docstral --json 2>/dev/null)
    exists=$?
    if printf '%s\n' "$output" | grep -Eq '"enabled":[[:space:]]*false'; then
        printf 'docstral: Codex MCP is disabled; set mcp_servers.docstral.enabled to true in %s, then rerun.\n' "$config" >&2
        printf 'disabled\n'
        exit
    fi
    current=$(printf '%s\n' "$output" | sed -n 's/.*"url":[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p')
    if [ "$current" = "$MCP_URL" ]; then
        printf 'unchanged\n'
        exit
    fi
    if ! HTTPS_PROXY=http://127.0.0.1:9 \
        HTTP_PROXY=http://127.0.0.1:9 \
        ALL_PROXY=http://127.0.0.1:9 \
        NO_PROXY= \
        https_proxy=http://127.0.0.1:9 \
        http_proxy=http://127.0.0.1:9 \
        all_proxy=http://127.0.0.1:9 \
        no_proxy= \
        CODEX_HOME="$staged_home" codex mcp add docstral --url "$MCP_URL" >/dev/null 2>&1; then
        printf 'docstral: failed to configure Codex MCP\n' >&2
        printf 'failed\n'
        exit
    fi
    verified=$(CODEX_HOME="$staged_home" codex mcp get docstral --json 2>/dev/null)
    verified_url=$(printf '%s\n' "$verified" | sed -n 's/.*"url":[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p')
    if [ "$verified_url" != "$MCP_URL" ]; then
        printf 'docstral: Codex produced an invalid MCP configuration\n' >&2
        printf 'failed\n'
        exit
    fi
    if [ "$exists" -eq 0 ]; then
        printf 'updated\n'
    else
        printf 'installed\n'
    fi
)

claude_mcp() (
    config="$HOME/.claude.json"
    if [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
        config="$CLAUDE_CONFIG_DIR/.claude.json"
    fi
    configure_mcp Claude "$config" .claude.json configure_claude_mcp
)

configure_claude_mcp() (
    staged_home=$1
    staged_config=$2
    current=$(read_mcp_url "$staged_config") || {
        printf 'docstral: failed to inspect Claude MCP configuration; install Node.js, Python 3, or jq.\n' >&2
        printf 'failed\n'
        exit
    }
    if [ "$current" = "$MCP_URL" ]; then
        printf 'unchanged\n'
        exit
    fi
    if [ "$current" != __missing__ ] && ! (cd "$staged_home" && HOME="$staged_home" CLAUDE_CONFIG_DIR="$staged_home" claude mcp remove --scope user docstral >/dev/null 2>&1); then
        printf 'docstral: failed to remove the existing Claude MCP entry\n' >&2
        printf 'failed\n'
        exit
    fi
    if ! (cd "$staged_home" && HOME="$staged_home" CLAUDE_CONFIG_DIR="$staged_home" claude mcp add --scope user --transport http docstral "$MCP_URL" >/dev/null 2>&1); then
        printf 'docstral: failed to configure Claude MCP\n' >&2
        printf 'failed\n'
        exit
    fi
    verified=$(read_mcp_url "$staged_config") || {
        printf 'docstral: failed to verify Claude MCP configuration\n' >&2
        printf 'failed\n'
        exit
    }
    if [ "$verified" != "$MCP_URL" ]; then
        printf 'docstral: Claude produced an invalid MCP configuration\n' >&2
        printf 'failed\n'
        exit
    fi
    if [ "$current" != __missing__ ]; then
        printf 'updated\n'
    else
        printf 'installed\n'
    fi
)

opencode_mcp() (
    if [ -n "${OPENCODE_CONFIG_CONTENT:-}" ]; then
        printf 'docstral: OpenCode uses OPENCODE_CONFIG_CONTENT; move that configuration to a file before installing.\n' >&2
        printf 'failed\n'
        exit
    fi
    runtime=$(json_node) || {
        printf 'docstral: OpenCode installation requires Node.js to safely edit JSONC configuration. Install Node.js and rerun.\n' >&2
        printf 'failed\n'
        exit
    }
    status=$("$runtime" "$MCP_UPDATER" opencode '' "$MCP_URL") || {
        printf 'docstral: failed to configure OpenCode MCP; Node.js and valid JSONC configuration are required.\n' >&2
        printf 'failed\n'
        exit
    }
    printf '%s\n' "$status"
)

vibe_python() (
    executable=$(resolve_destination "$(command -v vibe)") || return 1
    for candidate in "$(dirname "$executable")/python" \
        "$(command -v python3 2>/dev/null)" "$(command -v python 2>/dev/null)"; do
        if [ -x "$candidate" ] && "$candidate" -c 'import tomllib' >/dev/null 2>&1; then
            printf '%s\n' "$candidate"
            return 0
        fi
    done
    printf 'docstral: Vibe configuration requires its Python runtime or Python 3.11+ on PATH.\n' >&2
    return 1
)

vibe_current_url() (
    if [ ! -f "$1" ]; then
        printf '__missing__\n'
        return
    fi
    runtime=$(vibe_python) || return 1
    "$runtime" -c '
import sys, tomllib
with open(sys.argv[1], "rb") as source:
    config = tomllib.load(source)
server = next((entry for entry in config.get("mcp_servers", []) if entry.get("name") == "docstral"), None)
print("__missing__" if server is None else server.get("url") or "__configured__")
' "$1"
)

vibe_mcp() {
    configure_mcp Vibe "${VIBE_HOME:-$HOME/.vibe}/config.toml" config.toml configure_vibe_mcp
}

configure_vibe_mcp() (
    staged_home=$1
    staged_config=$2
    current=$(vibe_current_url "$staged_config") || {
        printf 'docstral: failed to inspect Vibe MCP configuration\n' >&2
        printf 'failed\n'
        exit
    }
    if [ "$current" = "$MCP_URL" ]; then
        printf 'unchanged\n'
        exit
    fi
    if [ "$current" != __missing__ ] && ! VIBE_HOME="$staged_home" vibe mcp remove docstral >/dev/null 2>&1; then
        printf 'docstral: failed to remove the existing Vibe MCP entry\n' >&2
        printf 'failed\n'
        exit
    fi
    if ! VIBE_HOME="$staged_home" \
        vibe mcp add docstral --transport streamable-http --url "$MCP_URL" --no-login >/dev/null 2>&1; then
        printf 'docstral: failed to configure Vibe MCP\n' >&2
        printf 'failed\n'
        exit
    fi
    verified=$(vibe_current_url "$staged_config") || {
        printf 'docstral: failed to verify Vibe MCP configuration\n' >&2
        printf 'failed\n'
        exit
    }
    if [ "$verified" != "$MCP_URL" ]; then
        printf 'docstral: Vibe produced an invalid MCP configuration\n' >&2
        printf 'failed\n'
        exit
    fi
    if [ "$current" != __missing__ ]; then
        printf 'updated\n'
    else
        printf 'installed\n'
    fi
)

json_node() {
    if [ -n "${DOCSTRAL_JSON_NODE:-}" ] && [ -x "$DOCSTRAL_JSON_NODE" ]; then
        printf '%s\n' "$DOCSTRAL_JSON_NODE"
        return
    fi
    if [ -n "${DOCSTRAL_CURSOR_NODE:-}" ] && [ -x "$DOCSTRAL_CURSOR_NODE" ]; then
        printf '%s\n' "$DOCSTRAL_CURSOR_NODE"
        return
    fi
    if command -v node >/dev/null 2>&1; then
        command -v node
        return
    fi
    for command_name in cursor-agent agent; do
        command_path=$(command -v "$command_name" 2>/dev/null || true)
        [ -n "$command_path" ] || continue
        link=$(readlink "$command_path" 2>/dev/null || true)
        if [ -n "$link" ]; then
            case "$link" in
                /*) command_path=$link ;;
                *) command_path=$(dirname "$command_path")/$link ;;
            esac
        fi
        candidate=$(dirname "$command_path")/node
        if [ -x "$candidate" ]; then
            printf '%s\n' "$candidate"
            return
        fi
    done
    for candidate in \
        "/Applications/Cursor.app/Contents/Resources/app/resources/helpers/node" \
        "/usr/share/cursor/resources/app/resources/helpers/node" \
        "/opt/Cursor/resources/app/resources/helpers/node" \
        "/opt/cursor/resources/app/resources/helpers/node" \
        "$HOME"/.local/share/cursor-agent/versions/*/node; do
        if [ -x "$candidate" ]; then
            printf '%s\n' "$candidate"
            return
        fi
    done
    return 1
}

read_mcp_url() {
    config=$1
    if [ ! -f "$config" ]; then
        printf '__missing__\n'
        return
    fi
    if runtime=$(json_node); then
        "$runtime" "$MCP_UPDATER" read "$config" '' "${2:-mcpServers}"
        return
    fi
    if command -v python3 >/dev/null 2>&1; then
        python3 -c '
import json, sys
with open(sys.argv[1]) as source:
    servers = json.load(source).get(sys.argv[2], {})
server = servers.get("docstral")
value = "__missing__" if "docstral" not in servers else "__configured__"
if isinstance(server, dict):
    value = server.get("url") if isinstance(server.get("url"), str) else value
    if server.get("enabled") is False or server.get("disabled") is True:
        value = "__disabled__"
print(value)
' "$config" "${2:-mcpServers}"
        return
    fi
    if command -v jq >/dev/null 2>&1; then
        jq -r --arg key "${2:-mcpServers}" '
            .[$key] // {} |
            if has("docstral") | not then "__missing__"
            elif .docstral.enabled == false or .docstral.disabled == true then "__disabled__"
            else .docstral.url // "__configured__" end
        ' "$config"
        return
    fi
    printf 'docstral: reading MCP configuration requires Node.js, Python 3, or jq.\n' >&2
    return 1
}

cursor_mcp() {
    configure_mcp Cursor "$HOME/.cursor/mcp.json" mcp.json configure_cursor_mcp
}

configure_cursor_mcp() (
    staged_config=$2
    runtime=$(json_node) || {
        printf 'docstral: Cursor needs its bundled Node.js runtime to update ~/.cursor/mcp.json\n' >&2
        printf 'failed\n'
        exit
    }
    status=$("$runtime" "$MCP_UPDATER" update "$staged_config" "$MCP_URL" 2> "$WORK_DIR/cursor-mcp-error") || {
        printf 'docstral: failed to configure Cursor MCP: ' >&2
        sed -n '1p' "$WORK_DIR/cursor-mcp-error" >&2
        printf 'failed\n'
        exit
    }
    printf '%s\n' "$status"
)
cat > "$MCP_UPDATER" <<'DOCSTRAL_MCP_JS'
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");

function skipTrivia(input, start) {
    let index = start;
    while (index < input.length) {
        if (/\s/.test(input[index])) {
            index += 1;
        } else if (input[index] === "/" && input[index + 1] === "/") {
            index += 2;
            while (index < input.length && input[index] !== "\n") index += 1;
        } else if (input[index] === "/" && input[index + 1] === "*") {
            const end = input.indexOf("*/", index + 2);
            if (end === -1) throw new Error("unterminated block comment");
            index = end + 2;
        } else {
            break;
        }
    }
    return index;
}

function parseString(input, start) {
    let escaped = false;
    for (let index = start + 1; index < input.length; index += 1) {
        const character = input[index];
        if (escaped) escaped = false;
        else if (character === "\\") escaped = true;
        else if (character === '"') {
            const end = index + 1;
            return { start, end, value: JSON.parse(input.slice(start, end)) };
        }
    }
    throw new Error("unterminated string");
}

function parsePrimitive(input, start) {
    const source = input.slice(start);
    const literal = /^(true|false|null)/.exec(source);
    if (literal) {
        const value = literal[1] === "true" ? true : literal[1] === "false" ? false : null;
        return { start, end: start + literal[1].length, value };
    }
    const number = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(source);
    if (!number) throw new Error("invalid JSON value");
    return { start, end: start + number[0].length, value: Number(number[0]) };
}

function parseObject(input, start) {
    if (input[start] !== "{") throw new Error("expected JSON object");
    const properties = [];
    const value = Object.create(null);
    let index = skipTrivia(input, start + 1);
    while (input[index] !== "}") {
        if (input[index] !== '"') throw new Error("expected quoted JSON key");
        const keyNode = parseString(input, index);
        index = skipTrivia(input, keyNode.end);
        if (input[index] !== ":") throw new Error("expected JSON colon");
        const valueNode = parseValue(input, index + 1);
        index = skipTrivia(input, valueNode.end);
        let comma = null;
        if (input[index] === ",") {
            comma = index;
            index = skipTrivia(input, index + 1);
        }
        properties.push({
            key: keyNode.value,
            keyStart: keyNode.start,
            valueStart: valueNode.start,
            valueEnd: valueNode.end,
            comma,
        });
        value[keyNode.value] = valueNode.value;
        if (input[index] !== "}" && comma === null) throw new Error("expected JSON comma");
    }
    return { start, end: index + 1, close: index, properties, value };
}

function parseArray(input, start) {
    const value = [];
    let index = skipTrivia(input, start + 1);
    while (input[index] !== "]") {
        const node = parseValue(input, index);
        value.push(node.value);
        index = skipTrivia(input, node.end);
        if (input[index] === ",") {
            index = skipTrivia(input, index + 1);
        } else if (input[index] !== "]") {
            throw new Error("expected JSON comma");
        }
    }
    return { start, end: index + 1, value };
}

function parseValue(input, start) {
    const index = skipTrivia(input, start);
    if (input[index] === '"') return parseString(input, index);
    if (input[index] === "{") return parseObject(input, index);
    if (input[index] === "[") return parseArray(input, index);
    return parsePrimitive(input, index);
}

function parseDocument(input) {
    const source = input.trim() ? input : "{}\n";
    const root = parseValue(source, 0);
    if (!root.properties) throw new Error("MCP configuration must be a JSON object");
    if (skipTrivia(source, root.end) !== source.length) throw new Error("unexpected content after JSON object");
    return { source, root };
}

function indentation(input, position) {
    const start = input.lastIndexOf("\n", position - 1) + 1;
    const prefix = input.slice(start, position);
    return /^\s*$/.test(prefix) ? prefix : "";
}

function insertionEdits(input, object, key, value) {
    const encodedKey = JSON.stringify(key);
    if (object.properties.length === 0) {
        const closeIndent = indentation(input, object.close);
        const propertyIndent = `${closeIndent}  `;
        const compact = input.slice(object.start + 1, object.close).length === 0;
        return [{ start: object.close, end: object.close, text: compact ? `${encodedKey}: ${value}` : `${propertyIndent}${encodedKey}: ${value}\n` }];
    }
    const last = object.properties[object.properties.length - 1];
    const edits = [];
    const closeLineStart = input.lastIndexOf("\n", object.close - 1) + 1;
    const closeIndent = indentation(input, object.close);
    const firstIndent = indentation(input, object.properties[0].keyStart);
    const propertyIndent = firstIndent || `${closeIndent}  `;
    if (closeLineStart > object.start && /^\s*$/.test(input.slice(closeLineStart, object.close))) {
        if (last.comma === null) edits.push({ start: last.valueEnd, end: last.valueEnd, text: "," });
        edits.push({ start: closeLineStart, end: closeLineStart, text: `${propertyIndent}${encodedKey}: ${value}\n` });
    } else {
        const separator = last.comma === null ? "," : "";
        edits.push({ start: object.close, end: object.close, text: `${separator} ${encodedKey}: ${value}` });
    }
    return edits;
}

function applyEdits(input, edits) {
    return edits
        .sort((left, right) => right.start - left.start)
        .reduce((value, edit) => value.slice(0, edit.start) + edit.text + value.slice(edit.end), input);
}

function resolveDestination(filename) {
    let resolved = path.resolve(filename);
    for (let remaining = 20; remaining > 0; remaining -= 1) {
        let stat;
        try { stat = fs.lstatSync(resolved); }
        catch (error) {
            if (error.code === "ENOENT") return resolved;
            throw error;
        }
        if (!stat.isSymbolicLink()) return resolved;
        resolved = path.resolve(path.dirname(resolved), fs.readlinkSync(resolved));
    }
    throw new Error(`cannot resolve configuration symlink: ${filename}`);
}

function readSnapshot(filename) {
    const resolved = resolveDestination(filename);
    const bytes = fs.existsSync(resolved) ? fs.readFileSync(resolved) : null;
    const mode = bytes === null ? 0o600 : fs.statSync(resolved).mode & 0o7777;
    return { filename, resolved, bytes, mode, document: parseDocument(bytes === null ? "" : bytes.toString("utf8")) };
}

function checkSnapshots(snapshots) {
    for (const snapshot of snapshots) {
        const current = readSnapshot(snapshot.filename);
        if (current.resolved !== snapshot.resolved || current.mode !== snapshot.mode ||
            (snapshot.bytes === null ? current.bytes !== null : current.bytes === null || !snapshot.bytes.equals(current.bytes))) {
            throw new Error(`configuration changed during installation; rerun: ${snapshot.filename}`);
        }
    }
}

function serverObject(document, serverKey) {
    const properties = document.root.properties.filter((property) => property.key === serverKey);
    if (properties.length > 1) throw new Error(`duplicate ${serverKey} properties`);
    if (properties.length === 0) return null;
    const object = parseObject(document.source, properties[0].valueStart);
    if (object.properties.filter((property) => property.key === "docstral").length > 1) {
        throw new Error("duplicate docstral entries");
    }
    return object;
}

function effectiveServer(snapshots, serverKey) {
    let current;
    for (const snapshot of snapshots) {
        const object = serverObject(snapshot.document, serverKey);
        if (!object || !Object.prototype.hasOwnProperty.call(object.value, "docstral")) continue;
        const entry = object.value.docstral;
        if (!entry || Array.isArray(entry) || typeof entry !== "object") throw new Error("docstral must be an object");
        current = Object.assign(Object.create(null), current, entry);
    }
    return current;
}

function opencodeFiles() {
    const directory = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME, ".config"), "opencode");
    const filenames = ["config.json", "opencode.json", "opencode.jsonc"].map((name) => path.join(directory, name));
    let target = filenames.filter((name) => fs.existsSync(name)).pop() || filenames[2];
    if (process.env.OPENCODE_CONFIG) {
        target = path.resolve(process.env.OPENCODE_CONFIG);
        filenames.push(target);
    }
    if (process.env.OPENCODE_CONFIG_DIR) {
        const custom = ["opencode.json", "opencode.jsonc"].map((name) => path.join(process.env.OPENCODE_CONFIG_DIR, name));
        filenames.push(...custom);
        target = custom.filter((name) => fs.existsSync(name)).pop() || custom[1];
    }
    return { filenames, target };
}

function main() {
    const [operation, configPath, url, requestedKey = "mcpServers"] = process.argv.slice(2);
    if (!["read", "update", "opencode"].includes(operation)) throw new Error("unknown operation");
    const serverKey = operation === "opencode" ? "mcp" : requestedKey;
    const files = operation === "opencode" ? opencodeFiles() : { filenames: [configPath], target: configPath };
    const snapshots = files.filenames.map(readSnapshot);
    const resolved = resolveDestination(files.target);
    const target = snapshots.find((snapshot) => snapshot.resolved === resolved);
    if (!target) throw new Error("configuration target is not in its layers");
    const current = effectiveServer(snapshots, serverKey);
    if (operation === "read") {
        const value = !current ? "__missing__"
            : current.enabled === false || current.disabled === true ? "__disabled__"
            : typeof current.url === "string" ? current.url : "__configured__";
        process.stdout.write(`${value}\n`);
        return;
    }
    if (operation === "opencode" && current && current.enabled === false) {
        process.stderr.write("docstral: OpenCode MCP is disabled in its configuration layers; enable it explicitly, then rerun.\n");
        process.stdout.write("disabled\n");
        return;
    }
    if (current && current.url === url && (operation !== "opencode" || current.type === "remote")) {
        process.stdout.write("unchanged\n");
        return;
    }
    if (operation === "opencode") {
        for (const snapshot of snapshots) {
            if (snapshot.resolved === resolved) continue;
            const object = serverObject(snapshot.document, serverKey);
            const inherited = object && object.value.docstral;
            if (inherited && Object.keys(inherited).some((key) => !["type", "url", "enabled", "timeout"].includes(key))) {
                throw new Error(`refusing Docstral migration: settings would be inherited from ${snapshot.filename}. Remove its old mcp.docstral entry and rerun; no OpenCode MCP configuration was changed.`);
            }
        }
    }
    const document = target.document;
    const raw = document.source;
    const object = serverObject(document, serverKey);
    const entry = operation === "opencode" ? { type: "remote", url } : { url };
    if (operation === "opencode" && object && object.value.docstral && object.value.docstral.enabled !== undefined) {
        entry.enabled = object.value.docstral.enabled;
    }
    let edits;
    if (!object) {
        edits = insertionEdits(raw, document.root, serverKey, JSON.stringify({ docstral: entry }, null, 2));
    } else {
        const property = object.properties.find((property) => property.key === "docstral");
        edits = property
            ? [{ start: property.valueStart, end: property.valueEnd, text: JSON.stringify(entry) }]
            : insertionEdits(raw, object, "docstral", JSON.stringify(entry));
    }
    const updated = applyEdits(raw, edits);
    const updatedDocument = parseDocument(updated);
    const projected = snapshots.map((snapshot) => snapshot.resolved === resolved ? { ...snapshot, document: updatedDocument } : snapshot);
    const verified = effectiveServer(projected, serverKey);
    if (!verified || verified.url !== url || (operation === "opencode" && (verified.type !== "remote" || verified.enabled === false))) {
        throw new Error("configuration layers override the requested Docstral server");
    }
    checkSnapshots(snapshots);
    fs.mkdirSync(path.dirname(resolved), { recursive: true });
    const temporaryDirectory = fs.mkdtempSync(path.join(path.dirname(resolved), ".docstral-mcp."));
    const temporary = path.join(temporaryDirectory, "config");
    try {
        if (target.bytes !== null) {
            execFileSync("cp", ["-p", resolved, temporary]);
            fs.chmodSync(temporary, target.mode | 0o200);
        }
        fs.writeFileSync(temporary, updated, { mode: 0o600, flag: target.bytes === null ? "wx" : "w" });
        fs.chmodSync(temporary, target.mode);
        checkSnapshots(snapshots);
        if (target.bytes !== null) {
            try {
                fs.writeFileSync(`${resolved}.docstral.bak`, target.bytes, { mode: 0o600, flag: "wx" });
            }
            catch (error) { if (error.code !== "EEXIST") throw error; }
        }
        checkSnapshots(snapshots);
        fs.renameSync(temporary, resolved);
    } finally {
        if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
        fs.rmdirSync(temporaryDirectory);
    }
    process.stdout.write(current ? "updated\n" : "installed\n");
}

try { main(); }
catch (error) {
    process.stderr.write(`docstral: ${error.message}\n`);
    process.exit(1);
}
DOCSTRAL_MCP_JS

combine_statuses() {
    combined=unchanged
    for status in "$@"; do
        if [ "$status" = failed ]; then
            printf 'failed\n'
            return
        fi
        if [ "$status" = updated ]; then
            combined=updated
        elif [ "$status" = installed ] && [ "$combined" = unchanged ]; then
            combined=installed
        fi
    done
    printf '%s\n' "$combined"
}

print_status() {
    label=$1
    status=$2
    case "$status" in
        installed) color=$GREEN ;;
        updated) color=$ORANGE ;;
        removed) color=$ORANGE ;;
        unchanged) color=$DIM ;;
        skipped) color=$DIM ;;
        failed) color=$RED ;;
        disabled) color=$ORANGE ;;
        *) color= ;;
    esac
    printf '%-10s %s%s%s\n' "$label:" "$color" "$status" "$RESET"
}

print_next_steps() {
    printf '\n%sNEXT STEP: run these commands%s\n\n' "$BOLD" "$RESET"
    [ "$HAS_CODEX" -eq 1 ] && printf '  %-10s %s\n' 'Codex' 'codex mcp login docstral'
    [ "$HAS_CLAUDE" -eq 1 ] && printf '  %-10s %s\n' 'Claude' 'claude mcp login docstral'
    [ "$HAS_OPENCODE" -eq 1 ] && printf '  %-10s %s\n' 'OpenCode' 'opencode mcp auth docstral'
    [ "$HAS_VIBE" -eq 1 ] && printf '  %-10s %s\n' 'Vibe' 'in Vibe: /mcp login docstral'
    [ "$HAS_CURSOR" -eq 1 ] && printf '  %-10s %s\n' 'Cursor' 'agent mcp login docstral'
    printf '\nRestart open agent sessions after authenticating.\n'
}

print_completion() {
    if [ "$FAILURES" -ne 0 ]; then
        printf '\nReview the failed entries above, fix them, then rerun:\n'
        printf '  curl -fsSL %s/install.sh | sh\n' "$BASE_URL"
    fi
    if [ "$HAS_CODEX" -eq 1 ] || [ "$HAS_CLAUDE" -eq 1 ] || [ "$HAS_OPENCODE" -eq 1 ] || \
        [ "$HAS_VIBE" -eq 1 ] || [ "$HAS_CURSOR" -eq 1 ]; then
        print_next_steps
    fi
}

run_client() {
    label=$1
    skill_path=$2
    mcp_function=$3
    if ! preflight_destination skill "$skill_path"; then
        print_status "$label" failed
        FAILURES=1
        return 1
    fi
    mcp_status=$("$mcp_function")
    if [ "$mcp_status" = failed ] || [ "$mcp_status" = disabled ]; then
        print_status "$label" "$mcp_status"
        FAILURES=1
        return 1
    fi
    skill_status=$(install_skill "$skill_path")
    status=$(combine_statuses "$skill_status" "$mcp_status")
    print_status "$label" "$status"
    if [ "$status" = failed ]; then
        FAILURES=1
        return 1
    fi
    return 0
}

CODEX_HOME_PATH=${CODEX_HOME:-$HOME/.codex}
CLAUDE_HOME_PATH=${CLAUDE_CONFIG_DIR:-$HOME/.claude}
VIBE_HOME_PATH=${VIBE_HOME:-$HOME/.vibe}

legacy_status=$(cleanup_legacy_instructions \
    "$HOME/.agents/AGENTS.md" \
    "$CODEX_HOME_PATH/AGENTS.md" \
    "$CODEX_HOME_PATH/AGENTS.override.md" \
    "$CLAUDE_HOME_PATH/CLAUDE.md" \
    "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/AGENTS.md" \
    "$VIBE_HOME_PATH/AGENTS.md" \
    "$HOME/.cursor/AGENTS.md")
if [ "$legacy_status" != unchanged ]; then
    print_status 'Legacy instructions' "$legacy_status"
fi
if [ "$legacy_status" = failed ]; then
    FAILURES=1
fi

printf '%sInstalling the shared skill%s\n\n' "$BOLD" "$RESET"
shared_skill_status=$(install_skill "$HOME/.agents/skills/docstral/SKILL.md")
print_status Shared "$shared_skill_status"
if [ "$shared_skill_status" = failed ]; then
    FAILURES=1
fi

printf '\n%sInstalling with the CLIs%s\n\n' "$BOLD" "$RESET"

if command -v codex >/dev/null 2>&1; then
    if run_client Codex "$HOME/.agents/skills/docstral/SKILL.md" codex_mcp; then
        HAS_CODEX=1
    fi
else
    print_status Codex skipped
fi

if command -v claude >/dev/null 2>&1; then
    if run_client Claude "$CLAUDE_HOME_PATH/skills/docstral/SKILL.md" claude_mcp; then
        HAS_CLAUDE=1
    fi
else
    print_status Claude skipped
fi

if command -v opencode >/dev/null 2>&1; then
    if run_client OpenCode "$HOME/.agents/skills/docstral/SKILL.md" opencode_mcp; then
        HAS_OPENCODE=1
    fi
else
    print_status OpenCode skipped
fi

if command -v vibe >/dev/null 2>&1; then
    if run_client Vibe "$VIBE_HOME_PATH/skills/docstral/SKILL.md" vibe_mcp; then
        HAS_VIBE=1
    fi
else
    print_status Vibe skipped
fi

if command -v cursor-agent >/dev/null 2>&1 || command -v cursor >/dev/null 2>&1 || command -v agent >/dev/null 2>&1; then
    if run_client Cursor "$HOME/.cursor/skills/docstral/SKILL.md" cursor_mcp; then
        HAS_CURSOR=1
    fi
else
    print_status Cursor skipped
fi

print_completion

exit "$FAILURES"
