feat(scaffold): install binaries from latest release in install script

This commit is contained in:
thibaud-lclr 2026-04-16 17:52:25 +02:00
parent a9378885f2
commit 973770ed78
2 changed files with 688 additions and 25 deletions

View file

@ -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
}

View file

@ -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.`,