diff --git a/scaffold/scaffold.go b/scaffold/scaffold.go index 1300206..135f4af 100644 --- a/scaffold/scaffold.go +++ b/scaffold/scaffold.go @@ -357,6 +357,30 @@ BINARY_NAME="{{.BinaryName}}" MODULE_PATH="{{.ModulePath}}" DEFAULT_PROFILE="{{.DefaultProfile}}" PROFILE_ENV="{{.ProfileEnv}}" +DEFAULT_RELEASE_DRIVER="{{.ReleaseDriver}}" +DEFAULT_RELEASE_BASE_URL="{{.ReleaseBaseURL}}" +DEFAULT_RELEASE_REPOSITORY="{{.ReleaseRepository}}" +DEFAULT_ASSET_NAME_TEMPLATE="{binary}-{os}-{arch}{ext}" +DEFAULT_CHECKSUM_ASSET_NAME="{asset}.sha256" +DEFAULT_CHECKSUM_REQUIRED="true" +DEFAULT_TOKEN_HEADER="Authorization" +DEFAULT_TOKEN_PREFIX="token" +DEFAULT_TOKEN_ENV_NAME="{{.ReleaseTokenEnv}}" +RELEASE_DRIVER="$DEFAULT_RELEASE_DRIVER" +RELEASE_BASE_URL="$DEFAULT_RELEASE_BASE_URL" +RELEASE_REPOSITORY="$DEFAULT_RELEASE_REPOSITORY" +LATEST_RELEASE_URL="" +ASSET_NAME_TEMPLATE="$DEFAULT_ASSET_NAME_TEMPLATE" +CHECKSUM_ASSET_NAME="$DEFAULT_CHECKSUM_ASSET_NAME" +CHECKSUM_REQUIRED="$DEFAULT_CHECKSUM_REQUIRED" +TOKEN_HEADER="$DEFAULT_TOKEN_HEADER" +TOKEN_PREFIX="$DEFAULT_TOKEN_PREFIX" +AUTH_HEADER_NAME="" +AUTH_HEADER_VALUE="" +TOKEN_ENV_NAMES=() +if [ -n "$DEFAULT_TOKEN_ENV_NAME" ]; then + TOKEN_ENV_NAMES=("$DEFAULT_TOKEN_ENV_NAME") +fi PREFILL_SERVER_NAME="" PREFILL_PROFILE_VALUE="" PREFILL_COMMAND_PATH="" @@ -543,15 +567,546 @@ toml_escape() { printf "%s" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' } -go_bin_dir() { - local gobin - gobin="$(go env GOBIN 2>/dev/null || true)" - if [ -n "$gobin" ]; then - printf "%s\n" "$gobin" +trim_whitespace() { + printf "%s" "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' +} + +unquote_toml() { + local value + value="$(trim_whitespace "$1")" + if [ "${#value}" -ge 2 ]; then + case "$value" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + fi + printf "%s" "$value" +} + +normalize_bool() { + local value + value="$(printf "%s" "$1" | tr '[:upper:]' '[:lower:]')" + value="$(trim_whitespace "$value")" + case "$value" in + true|1|yes|y) + printf "true" + ;; + false|0|no|n) + printf "false" + ;; + *) + printf "%s" "$2" + ;; + esac +} + +toml_read_update_value() { + local key="$1" + awk -v key="$key" ' + BEGIN { + in_update = 0 + } + { + line = $0 + sub(/[[:space:]]*#.*/, "", line) + if (line ~ /^[[:space:]]*\[/) { + if (line ~ /^[[:space:]]*\[update\][[:space:]]*$/) { + in_update = 1 + } else { + in_update = 0 + } + } + if (in_update == 1 && line ~ "^[[:space:]]*" key "[[:space:]]*=") { + sub("^[[:space:]]*" key "[[:space:]]*=[[:space:]]*", "", line) + print line + exit + } + } + ' mcp.toml +} + +toml_read_update_array() { + local key="$1" + local raw + raw="$(toml_read_update_value "$key")" + raw="$(trim_whitespace "$raw")" + if [ -z "$raw" ]; then return fi - go env GOPATH 2>/dev/null | awk '{print $1 "/bin"}' + raw="${raw#\[}" + raw="${raw%\]}" + local items=() + local item="" + IFS=',' read -r -a items <<< "$raw" + for item in "${items[@]}"; do + item="$(unquote_toml "$item")" + item="$(trim_whitespace "$item")" + if [ -n "$item" ]; then + printf "%s\n" "$item" + fi + done +} + +normalize_release_config() { + RELEASE_DRIVER="$(printf "%s" "$RELEASE_DRIVER" | tr '[:upper:]' '[:lower:]')" + RELEASE_DRIVER="$(trim_whitespace "$RELEASE_DRIVER")" + + RELEASE_REPOSITORY="$(trim_whitespace "$RELEASE_REPOSITORY")" + RELEASE_REPOSITORY="${RELEASE_REPOSITORY#/}" + RELEASE_REPOSITORY="${RELEASE_REPOSITORY%/}" + + RELEASE_BASE_URL="$(trim_whitespace "$RELEASE_BASE_URL")" + RELEASE_BASE_URL="${RELEASE_BASE_URL%/}" + + LATEST_RELEASE_URL="$(trim_whitespace "$LATEST_RELEASE_URL")" + ASSET_NAME_TEMPLATE="$(trim_whitespace "$ASSET_NAME_TEMPLATE")" + CHECKSUM_ASSET_NAME="$(trim_whitespace "$CHECKSUM_ASSET_NAME")" + TOKEN_HEADER="$(trim_whitespace "$TOKEN_HEADER")" + TOKEN_PREFIX="$(trim_whitespace "$TOKEN_PREFIX")" + + if [ -z "$ASSET_NAME_TEMPLATE" ]; then + ASSET_NAME_TEMPLATE="$DEFAULT_ASSET_NAME_TEMPLATE" + fi + if [ -z "$CHECKSUM_ASSET_NAME" ]; then + CHECKSUM_ASSET_NAME="$DEFAULT_CHECKSUM_ASSET_NAME" + fi + + local filtered_env_names=() + local env_name="" + for env_name in "${TOKEN_ENV_NAMES[@]}"; do + env_name="$(trim_whitespace "$env_name")" + if [ -n "$env_name" ]; then + filtered_env_names+=("$env_name") + fi + done + TOKEN_ENV_NAMES=("${filtered_env_names[@]}") + + case "$RELEASE_DRIVER" in + gitea) + if [ -z "$TOKEN_HEADER" ]; then + TOKEN_HEADER="Authorization" + fi + if [ -z "$TOKEN_PREFIX" ]; then + TOKEN_PREFIX="token" + fi + if [ "${#TOKEN_ENV_NAMES[@]}" -eq 0 ]; then + TOKEN_ENV_NAMES=("GITEA_TOKEN") + fi + ;; + gitlab) + if [ -z "$RELEASE_BASE_URL" ]; then + RELEASE_BASE_URL="https://gitlab.com" + fi + if [ -z "$TOKEN_HEADER" ]; then + TOKEN_HEADER="PRIVATE-TOKEN" + fi + if [ "${#TOKEN_ENV_NAMES[@]}" -eq 0 ]; then + TOKEN_ENV_NAMES=("GITLAB_TOKEN" "GITLAB_PRIVATE_TOKEN") + fi + ;; + github) + if [ -z "$RELEASE_BASE_URL" ]; then + RELEASE_BASE_URL="https://api.github.com" + fi + if [ -z "$TOKEN_HEADER" ]; then + TOKEN_HEADER="Authorization" + fi + if [ -z "$TOKEN_PREFIX" ]; then + TOKEN_PREFIX="Bearer" + fi + if [ "${#TOKEN_ENV_NAMES[@]}" -eq 0 ]; then + TOKEN_ENV_NAMES=("GITHUB_TOKEN") + fi + ;; + esac +} + +load_release_config_from_manifest() { + if [ ! -f "mcp.toml" ]; then + ui_warn "mcp.toml introuvable: utilisation de la configuration release integree au script." + normalize_release_config + return + fi + + local value + value="$(toml_read_update_value "driver")" + if [ -n "$value" ]; then + RELEASE_DRIVER="$(printf "%s" "$(unquote_toml "$value")" | tr '[:upper:]' '[:lower:]')" + fi + + value="$(toml_read_update_value "repository")" + if [ -n "$value" ]; then + RELEASE_REPOSITORY="$(unquote_toml "$value")" + fi + + value="$(toml_read_update_value "base_url")" + if [ -n "$value" ]; then + RELEASE_BASE_URL="$(unquote_toml "$value")" + fi + + value="$(toml_read_update_value "latest_release_url")" + if [ -n "$value" ]; then + LATEST_RELEASE_URL="$(unquote_toml "$value")" + fi + + value="$(toml_read_update_value "asset_name_template")" + if [ -n "$value" ]; then + ASSET_NAME_TEMPLATE="$(unquote_toml "$value")" + fi + + value="$(toml_read_update_value "checksum_asset_name")" + if [ -n "$value" ]; then + CHECKSUM_ASSET_NAME="$(unquote_toml "$value")" + fi + + value="$(toml_read_update_value "checksum_required")" + if [ -n "$value" ]; then + CHECKSUM_REQUIRED="$(normalize_bool "$(unquote_toml "$value")" "$CHECKSUM_REQUIRED")" + fi + + value="$(toml_read_update_value "token_header")" + if [ -n "$value" ]; then + TOKEN_HEADER="$(unquote_toml "$value")" + fi + + value="$(toml_read_update_value "token_prefix")" + if [ -n "$value" ]; then + TOKEN_PREFIX="$(unquote_toml "$value")" + fi + + local parsed_token_env_names=() + local parsed_env_name="" + while IFS= read -r parsed_env_name; do + if [ -n "$parsed_env_name" ]; then + parsed_token_env_names+=("$parsed_env_name") + fi + done < <(toml_read_update_array "token_env_names") + if [ "${#parsed_token_env_names[@]}" -gt 0 ]; then + TOKEN_ENV_NAMES=("${parsed_token_env_names[@]}") + fi + + normalize_release_config +} + +resolve_goos() { + case "$(uname -s)" in + Linux) + printf "linux" + ;; + Darwin) + printf "darwin" + ;; + FreeBSD) + printf "freebsd" + ;; + OpenBSD) + printf "openbsd" + ;; + NetBSD) + printf "netbsd" + ;; + CYGWIN*|MINGW*|MSYS*) + printf "windows" + ;; + *) + ui_error "OS non supporte: $(uname -s)" + exit 1 + ;; + esac +} + +resolve_goarch() { + case "$(uname -m)" in + x86_64|amd64) + printf "amd64" + ;; + aarch64|arm64) + printf "arm64" + ;; + armv7*|armv6*|armhf) + printf "arm" + ;; + i386|i686) + printf "386" + ;; + ppc64le) + printf "ppc64le" + ;; + s390x) + printf "s390x" + ;; + riscv64) + printf "riscv64" + ;; + *) + ui_error "Architecture non supportee: $(uname -m)" + exit 1 + ;; + esac +} + +resolve_asset_name() { + local goos="$1" + local goarch="$2" + local ext="" + if [ "$goos" = "windows" ]; then + ext=".exe" + fi + + local template="$ASSET_NAME_TEMPLATE" + if [ -z "$template" ]; then + template="$DEFAULT_ASSET_NAME_TEMPLATE" + fi + + local asset_name="$template" + asset_name="${asset_name//\{binary\}/$BINARY_NAME}" + asset_name="${asset_name//\{os\}/$goos}" + asset_name="${asset_name//\{arch\}/$goarch}" + asset_name="${asset_name//\{ext\}/$ext}" + asset_name="$(trim_whitespace "$asset_name")" + + if [ -z "$asset_name" ]; then + ui_error "Le template d'asset resolve vers une valeur vide." + exit 1 + fi + + case "$asset_name" in + */*|*\\*) + ui_error "Nom d'asset invalide (separateur de chemin): $asset_name" + exit 1 + ;; + esac + + printf "%s" "$asset_name" +} + +resolve_latest_release_url() { + if [ -n "$LATEST_RELEASE_URL" ]; then + printf "%s" "$LATEST_RELEASE_URL" + return + fi + + if [ -z "$RELEASE_DRIVER" ]; then + ui_error "Configuration release incomplete: definir [update].driver ou [update].latest_release_url." + exit 1 + fi + + if [ -z "$RELEASE_REPOSITORY" ]; then + ui_error "Configuration release incomplete: definir [update].repository." + exit 1 + fi + + case "$RELEASE_DRIVER" in + gitea) + if [ -z "$RELEASE_BASE_URL" ]; then + ui_error "Configuration release incomplete: [update].base_url requis pour driver gitea." + exit 1 + fi + printf "%s/api/v1/repos/%s/releases/latest" "$RELEASE_BASE_URL" "$RELEASE_REPOSITORY" + ;; + gitlab) + local encoded_repository + encoded_repository="$(jq -nr --arg value "$RELEASE_REPOSITORY" '$value|@uri')" + printf "%s/api/v4/projects/%s/releases/permalink/latest" "$RELEASE_BASE_URL" "$encoded_repository" + ;; + github) + printf "%s/repos/%s/releases/latest" "$RELEASE_BASE_URL" "$RELEASE_REPOSITORY" + ;; + *) + ui_error "Driver release non supporte: $RELEASE_DRIVER (attendu: gitea, gitlab ou github)." + exit 1 + ;; + esac +} + +resolve_auth_header() { + AUTH_HEADER_NAME="" + AUTH_HEADER_VALUE="" + + local token="" + local env_name + for env_name in "${TOKEN_ENV_NAMES[@]}"; do + if [ -z "$env_name" ]; then + continue + fi + if [ -n "${!env_name:-}" ]; then + token="$(trim_whitespace "${!env_name}")" + if [ -n "$token" ]; then + break + fi + fi + done + + if [ -z "$token" ]; then + return + fi + + local header_name + header_name="$(trim_whitespace "$TOKEN_HEADER")" + if [ -z "$header_name" ]; then + return + fi + + local prefix + prefix="$(trim_whitespace "$TOKEN_PREFIX")" + if [ -n "$prefix" ]; then + local lower_token + local lower_prefix + lower_token="$(printf "%s" "$token" | tr '[:upper:]' '[:lower:]')" + lower_prefix="$(printf "%s" "$prefix" | tr '[:upper:]' '[:lower:]')" + if [[ "$lower_token" != "$lower_prefix"* ]]; then + token="$prefix $token" + fi + fi + + AUTH_HEADER_NAME="$header_name" + AUTH_HEADER_VALUE="$token" +} + +curl_download() { + local url="$1" + local output_path="$2" + local mode="${3:-}" + local curl_args=( + -fsSL + -H "User-Agent: mcp install wizard" + ) + + if [ "$mode" = "json" ]; then + curl_args+=(-H "Accept: application/json") + fi + if [ -n "$AUTH_HEADER_NAME" ] && [ -n "$AUTH_HEADER_VALUE" ]; then + curl_args+=(-H "${AUTH_HEADER_NAME}: ${AUTH_HEADER_VALUE}") + fi + + curl "${curl_args[@]}" "$url" -o "$output_path" +} + +resolve_url_reference() { + local value="$1" + local base="$2" + + if printf "%s" "$value" | grep -Eq '^https?://'; then + printf "%s" "$value" + return + fi + + local origin + origin="$(printf "%s" "$base" | sed -E 's#^(https?://[^/]+).*$#\1#')" + + if [ -z "$origin" ]; then + printf "%s" "$value" + return + fi + + if [ "${value#/}" != "$value" ]; then + printf "%s%s" "$origin" "$value" + return + fi + + local base_no_query + base_no_query="$(printf "%s" "$base" | sed 's/[?#].*$//')" + local base_dir="${base_no_query%/*}" + printf "%s/%s" "$base_dir" "$value" +} + +resolve_asset_url_from_release() { + local release_json_path="$1" + local asset_name="$2" + jq -r --arg asset "$asset_name" ' + ( + .assets.links[]? | select(.name == $asset) | (.direct_asset_url // .browser_download_url // .url // empty) + ), + ( + .assets[]? | select(.name == $asset) | (.direct_asset_url // .browser_download_url // .url // empty) + ) + | select(. != null and . != "") + ' "$release_json_path" | head -n 1 +} + +release_assets_preview() { + local release_json_path="$1" + jq -r ' + [ + (.assets.links[]?.name), + (.assets[]?.name) + ] + | map(select(. != null and . != "")) + | unique + | .[:8] + | join(", ") + ' "$release_json_path" +} + +parse_checksum_for_asset() { + local checksum_file="$1" + local asset_name="$2" + awk -v asset="$asset_name" ' + function is_hex(value) { + return value ~ /^[0-9A-Fa-f]{64}$/ + } + { + line = $0 + sub(/#.*/, "", line) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) + if (line == "") { + next + } + + n = split(line, fields, /[[:space:]]+/) + if (n == 1 && is_hex(fields[1]) && fallback == "") { + fallback = fields[1] + next + } + + if (n >= 2 && is_hex(fields[1])) { + candidate = fields[2] + sub(/^\*/, "", candidate) + sub(/^\.\/+/, "", candidate) + if (candidate == asset) { + print fields[1] + exit + } + } + + if (n >= 2 && is_hex(fields[n])) { + candidate = fields[1] + sub(/^\*/, "", candidate) + sub(/^\.\/+/, "", candidate) + if (candidate == asset) { + print fields[n] + exit + } + } + } + END { + if (fallback != "") { + print fallback + } + } + ' "$checksum_file" +} + +sha256_file() { + local file_path="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file_path" | awk '{print $1}' + return + fi + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file_path" | awk '{print $1}' + return + fi + + ui_error "Aucun outil SHA-256 detecte (sha256sum ou shasum requis)." + exit 1 } resolve_binary_path() { @@ -560,13 +1115,9 @@ resolve_binary_path() { return fi - if command -v go >/dev/null 2>&1; then - local bin_dir - bin_dir="$(go_bin_dir)" - if [ -n "$bin_dir" ] && [ -x "$bin_dir/$BINARY_NAME" ]; then - printf "%s\n" "$bin_dir/$BINARY_NAME" - return - fi + if [ -x "$HOME/.local/bin/$BINARY_NAME" ]; then + printf "%s\n" "$HOME/.local/bin/$BINARY_NAME" + return fi printf "%s\n" "$HOME/.local/bin/$BINARY_NAME" @@ -583,10 +1134,18 @@ ensure_cli() { } install_binary() { + ensure_cli "curl" + ensure_cli "jq" + + load_release_config_from_manifest + resolve_auth_header + + local target_path="$HOME/.local/bin/$BINARY_NAME" if command -v "$BINARY_NAME" >/dev/null 2>&1; then - ui_success "Binaire detecte: $(command -v "$BINARY_NAME")" + target_path="$(command -v "$BINARY_NAME")" + ui_success "Binaire detecte: $target_path" local reinstall - reinstall="$(prompt "Reinstaller via go install ? (y/N)" "N")" + reinstall="$(prompt "Reinstaller depuis la derniere release ? (y/N)" "N")" case "$reinstall" in y|Y|yes|YES) ;; @@ -596,19 +1155,118 @@ install_binary() { esac fi - if ! command -v go >/dev/null 2>&1; then - ui_error "Go n'est pas installe. Installe Go ou choisis une configuration manuelle." + if [ ! -w "$(dirname "$target_path")" ]; then + ui_warn "Pas de droit d'ecriture dans $(dirname "$target_path"), installation dans $HOME/.local/bin." + target_path="$HOME/.local/bin/$BINARY_NAME" + fi + + local goos + goos="$(resolve_goos)" + local goarch + goarch="$(resolve_goarch)" + local asset_name + asset_name="$(resolve_asset_name "$goos" "$goarch")" + local release_url + release_url="$(resolve_latest_release_url)" + + ui_info "Recherche de la derniere release pour $RELEASE_REPOSITORY ($goos/$goarch)..." + local release_json + release_json="$(mktemp)" + if ! curl_download "$release_url" "$release_json" "json"; then + rm -f "$release_json" + ui_error "Impossible de recuperer la release latest depuis $release_url." + if [ "${#TOKEN_ENV_NAMES[@]}" -gt 0 ]; then + ui_info "Si le repo est prive, configure un token via: ${TOKEN_ENV_NAMES[*]}" + fi exit 1 fi - ui_info "Installation du binaire via go install..." - go install "${MODULE_PATH}/cmd/${BINARY_NAME}@latest" + local release_tag + release_tag="$(jq -r '.tag_name // empty' "$release_json")" + if [ -z "$release_tag" ]; then + rm -f "$release_json" + ui_error "Reponse release invalide: tag_name manquant." + exit 1 + fi - local bin_dir - bin_dir="$(go_bin_dir)" - if [ -n "$bin_dir" ]; then - ui_success "Binaire installe dans $bin_dir" - ui_info "Ajoute ce dossier au PATH si necessaire." + local asset_url + asset_url="$(resolve_asset_url_from_release "$release_json" "$asset_name")" + if [ -z "$asset_url" ]; then + local preview + preview="$(release_assets_preview "$release_json")" + rm -f "$release_json" + ui_error "Aucun asset \"$asset_name\" dans la release $release_tag." + if [ -n "$preview" ]; then + ui_info "Assets disponibles: $preview" + fi + exit 1 + fi + asset_url="$(resolve_url_reference "$asset_url" "$release_url")" + + ui_info "Telechargement de l'asset $asset_name ($release_tag)..." + local download_path + download_path="$(mktemp)" + if ! curl_download "$asset_url" "$download_path"; then + rm -f "$release_json" "$download_path" + ui_error "Echec du telechargement de l'asset: $asset_url" + exit 1 + fi + + local checksum_asset_name + checksum_asset_name="${CHECKSUM_ASSET_NAME//\{asset\}/$asset_name}" + if [ -z "$checksum_asset_name" ]; then + checksum_asset_name="${asset_name}.sha256" + fi + local checksum_url + checksum_url="$(resolve_asset_url_from_release "$release_json" "$checksum_asset_name")" + if [ -n "$checksum_url" ]; then + checksum_url="$(resolve_url_reference "$checksum_url" "$release_url")" + local checksum_path + checksum_path="$(mktemp)" + if ! curl_download "$checksum_url" "$checksum_path"; then + rm -f "$release_json" "$download_path" "$checksum_path" + ui_error "Echec du telechargement du checksum: $checksum_url" + exit 1 + fi + + local expected_checksum + expected_checksum="$(parse_checksum_for_asset "$checksum_path" "$asset_name")" + if [ -z "$expected_checksum" ]; then + rm -f "$release_json" "$download_path" "$checksum_path" + ui_error "Checksum introuvable dans $checksum_asset_name pour l'asset $asset_name." + exit 1 + fi + + local actual_checksum + actual_checksum="$(sha256_file "$download_path")" + local actual_checksum_lc + actual_checksum_lc="$(printf "%s" "$actual_checksum" | tr '[:upper:]' '[:lower:]')" + local expected_checksum_lc + expected_checksum_lc="$(printf "%s" "$expected_checksum" | tr '[:upper:]' '[:lower:]')" + if [ "$actual_checksum_lc" != "$expected_checksum_lc" ]; then + rm -f "$release_json" "$download_path" "$checksum_path" + ui_error "Checksum invalide pour $asset_name." + exit 1 + fi + rm -f "$checksum_path" + ui_success "Checksum verifie pour $asset_name." + else + if [ "$CHECKSUM_REQUIRED" = "true" ]; then + rm -f "$release_json" "$download_path" + ui_error "Checksum requis mais asset \"$checksum_asset_name\" introuvable." + exit 1 + fi + ui_warn "Checksum non disponible pour $asset_name (verification ignoree)." + fi + + chmod +x "$download_path" + mkdir -p "$(dirname "$target_path")" + mv "$download_path" "$target_path" + rm -f "$release_json" + ui_success "Binaire installe: $target_path" + + if ! printf ":%s:" "$PATH" | grep -Fq ":$(dirname "$target_path"):"; then + ui_info "Ajoute $(dirname "$target_path") au PATH si necessaire." fi } diff --git a/scaffold/scaffold_test.go b/scaffold/scaffold_test.go index 2f59ace..67e3aef 100644 --- a/scaffold/scaffold_test.go +++ b/scaffold/scaffold_test.go @@ -127,7 +127,12 @@ func TestGenerateCreatesRecommendedSkeleton(t *testing.T) { for _, snippet := range []string{ "#!/usr/bin/env bash", `MODULE_PATH="example.com/acme/my-mcp"`, - `go install "${MODULE_PATH}/cmd/${BINARY_NAME}@latest"`, + `DEFAULT_RELEASE_REPOSITORY="org/my-mcp"`, + `load_release_config_from_manifest`, + `resolve_latest_release_url()`, + `curl_download "$release_url" "$release_json" "json"`, + `asset_name="$(resolve_asset_name "$goos" "$goarch")"`, + `Reinstaller depuis la derniere release ? (y/N)`, "MCP Install Wizard", `menu_select() {`, `Utilise ↑/↓ puis Entrée.`,