.mise/tasks/mise-linux-shell
.mise/tasks/mise-linux-shellBrowse 1970 files
6,192 tokens
20,662 bytes
Token encoding: o200k_base
Snapshot 21a254f
← Back to SKILL.md
1#!/usr/bin/env bash2# Keep this host-side orchestration in Bash so ShellCheck can analyze it;3# ShellCheck does not support zsh scripts.4#MISE description="Linux shell (Apple container machine) using host Docker"5#MISE interactive=true6#USAGE flag "--restart" help="Delete and recreate the machine from scratch"7#USAGE flag "--stop" help="Delete the machine and stop the host Docker TCP proxy (socat)"8 9# mise tool installation and project builds can behave differently on Linux10# than on macOS. This task provides a persistent Linux environment for11# reproducing and debugging those differences while sharing the Mac project12# tree and host Docker daemon.13 14set -euo pipefail15 16NAME=railpack-linux17# Alpine is used because Apple container machines require /sbin/init in the18# image. Stock ubuntu:24.04 does not have it (needs a custom systemd image /19# Dockerfile). Alpine ships busybox init and needs no image build.20IMAGE=alpine:latest21PORT=237522# Keep all mise state on the machine disk. The Mac home is mounted at23# /Users/...; if mise used ~/.local/share or ~/.config there it would mix24# host (darwin) installs with guest (linux) ones.25MISE_DATA_DIR_GUEST=/var/lib/mise26MISE_CACHE_DIR_GUEST=/var/cache/mise27MISE_CONFIG_DIR_GUEST=/var/lib/mise/config28# Host path of this repo (same path inside the machine via the /Users mount).29PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"30RAILPACK_MISE_VERSION="$(<"${PROJECT_ROOT}/core/mise/version.txt")"31RAILPACK_MISE_DIR=/tmp/railpack/mise32RUNTIME_ROOT="${TMPDIR:-/tmp}"33PROXY_STATE_DIR="${RUNTIME_ROOT%/}/railpack-linux-shell-${UID}"34PROXY_STATE_FILE="${PROXY_STATE_DIR}/socat.state"35PROXY_LOCK_FILE="${PROXY_STATE_DIR}/socat.lock"36PROXY_LOG_FILE="${PROXY_STATE_DIR}/socat.log"37 38GATEWAY=""39DOCKER_HOST_TCP=""40DOCKER_SOCKET=""41PROXY_LOCK_HELD=false42PROXY_START_PID=""43MANAGED_PROXY_PID=""44MANAGED_PROXY_GATEWAY=""45MANAGED_PROXY_SOCKET=""46WAIT_PID=""47 48log() { echo "$*" >&2; }49 50sleep_ms() {51 local milliseconds="$1"52 local duration53 printf -v duration '%d.%03d' "$((milliseconds / 1000))" "$((milliseconds % 1000))"54 sleep "$duration"55}56 57# Retry a predicate or explicitly retryable command with capped exponential backoff.58retry_command() {59 local max_attempts="$1"60 local delay_ms="$2"61 local max_delay_ms="$3"62 local label="$4"63 local report_retries="$5"64 shift 565 66 local attempt=167 local ec=068 while ((attempt <= max_attempts)); do69 if "$@"; then70 return 071 else72 ec=$?73 fi74 75 if ((attempt == max_attempts)); then76 return "$ec"77 fi78 if [[ "$report_retries" == "true" ]]; then79 log "==> ${label} failed (exit ${ec}); retry $((attempt + 1))/${max_attempts}"80 fi81 sleep_ms "$delay_ms"82 delay_ms=$((delay_ms * 2))83 if ((delay_ms > max_delay_ms)); then84 delay_ms="$max_delay_ms"85 fi86 attempt=$((attempt + 1))87 done88}89 90# Host gateway on Apple's default container network (not OrbStack, not loopback).91discover_gateway() {92 local quiet="${1:-false}"93 local network_json gateway94 95 if ! command -v container >/dev/null 2>&1; then96 [[ "$quiet" == "true" ]] || log "error: Apple container CLI is not installed"97 return 198 fi99 if ! command -v jq >/dev/null 2>&1; then100 [[ "$quiet" == "true" ]] || log "error: jq is required to inspect the container network"101 return 1102 fi103 if ! network_json="$(container network inspect default 2>&1)"; then104 if [[ "$quiet" != "true" ]]; then105 log "error: could not inspect the default container network (is 'container system' running?)"106 log "$network_json"107 fi108 return 1109 fi110 if ! gateway="$(printf '%s\n' "$network_json" | jq -er '.[0].status.ipv4Gateway | select(type == "string" and length > 0)' 2>/dev/null)"; then111 [[ "$quiet" == "true" ]] || log "error: default container network has no IPv4 gateway"112 return 1113 fi114 115 GATEWAY="$gateway"116 DOCKER_HOST_TCP="tcp://${GATEWAY}:${PORT}"117}118 119resolve_docker_socket() {120 local quiet="${1:-false}"121 local socket122 if ! socket="$(realpath /var/run/docker.sock 2>/dev/null)" || [[ ! -S "$socket" ]]; then123 [[ "$quiet" == "true" ]] || log "error: Docker socket not found at /var/run/docker.sock (is Docker/OrbStack running?)"124 return 1125 fi126 DOCKER_SOCKET="$socket"127}128 129docker_api_ok() {130 curl -fsS --connect-timeout 1 "http://${GATEWAY}:${PORT}/_ping" 2>/dev/null | grep -qx OK131}132 133# Guest commands run once so a real guest failure is not mistaken for a134# transient machine transport failure. Readiness checks are retried separately.135machine_run() {136 container machine run -n "$NAME" "$@"137}138 139machine_is_ready() {140 container machine run -n "$NAME" -- true >/dev/null 2>&1141}142 143wait_machine_ready() {144 if retry_command 10 250 2000 "container machine readiness" false machine_is_ready; then145 return 0146 fi147 log "error: container machine $NAME did not become ready after repeated attempts"148 return 1149}150 151wait_docker_proxy_ready() {152 retry_command 6 100 1000 "Docker proxy readiness" false docker_api_ok153}154 155lsof_path() {156 if [[ -x /usr/sbin/lsof ]]; then157 echo /usr/sbin/lsof158 else159 command -v lsof 2>/dev/null160 fi161}162 163load_proxy_state() {164 MANAGED_PROXY_PID=""165 MANAGED_PROXY_GATEWAY=""166 MANAGED_PROXY_SOCKET=""167 [[ -f "$PROXY_STATE_FILE" ]] || return 1168 {169 IFS= read -r MANAGED_PROXY_PID170 IFS= read -r MANAGED_PROXY_GATEWAY171 IFS= read -r MANAGED_PROXY_SOCKET172 } <"$PROXY_STATE_FILE"173 [[ "$MANAGED_PROXY_PID" =~ ^[0-9]+$ ]] &&174 [[ -n "$MANAGED_PROXY_GATEWAY" ]] &&175 [[ -n "$MANAGED_PROXY_SOCKET" ]]176}177 178write_proxy_state() {179 local pid="$1" gateway="$2" socket="$3"180 local state_tmp181 mkdir -p "$PROXY_STATE_DIR"182 chmod 0700 "$PROXY_STATE_DIR"183 state_tmp="$(mktemp "${PROXY_STATE_FILE}.XXXXXX")"184 printf '%s\n%s\n%s\n' "$pid" "$gateway" "$socket" >"$state_tmp"185 mv -f "$state_tmp" "$PROXY_STATE_FILE"186}187 188clear_proxy_state() {189 rm -f -- "$PROXY_STATE_FILE"190}191 192try_acquire_proxy_lock() {193 local owner=""194 mkdir -p "$PROXY_STATE_DIR"195 chmod 0700 "$PROXY_STATE_DIR"196 if ln -s "$$" "$PROXY_LOCK_FILE" 2>/dev/null; then197 PROXY_LOCK_HELD=true198 return 0199 fi200 owner="$(readlink "$PROXY_LOCK_FILE" 2>/dev/null || true)"201 if [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; then202 # Recheck before removing a stale lock in case another process replaced it.203 if [[ "$(readlink "$PROXY_LOCK_FILE" 2>/dev/null || true)" == "$owner" ]]; then204 rm -f -- "$PROXY_LOCK_FILE"205 fi206 fi207 return 1208}209 210acquire_proxy_lock() {211 if retry_command 40 50 250 "Docker proxy lock" false try_acquire_proxy_lock; then212 return 0213 fi214 log "error: timed out waiting for the Docker proxy lock: $PROXY_LOCK_FILE"215 return 1216}217 218release_proxy_lock() {219 if [[ "$PROXY_LOCK_HELD" == "true" ]] &&220 [[ "$(readlink "$PROXY_LOCK_FILE" 2>/dev/null || true)" == "$$" ]]; then221 rm -f -- "$PROXY_LOCK_FILE"222 fi223 PROXY_LOCK_HELD=false224}225 226socat_process_matches() {227 local pid="$1" gateway="$2" socket="$3"228 local lsof_bin comm args listen_arg connect_arg229 [[ "$pid" =~ ^[0-9]+$ ]] || return 1230 [[ -n "$gateway" && -n "$socket" ]] || return 1231 if ! lsof_bin="$(lsof_path)" || [[ -z "$lsof_bin" ]]; then232 return 1233 fi234 comm="$(ps -p "$pid" -o comm= 2>/dev/null || true)"235 comm="${comm#"${comm%%[![:space:]]*}"}"236 comm="${comm%"${comm##*[![:space:]]}"}"237 [[ "${comm##*/}" == "socat" ]] || return 1238 args="$(ps -p "$pid" -o command= 2>/dev/null || true)"239 listen_arg="TCP-LISTEN:${PORT},bind=${gateway},reuseaddr,fork"240 connect_arg="UNIX-CONNECT:${socket}"241 [[ "$args" == *"$listen_arg"* && "$args" == *"$connect_arg"* ]] || return 1242 "$lsof_bin" -a -p "$pid" -t -nP "-iTCP@${gateway}:${PORT}" -sTCP:LISTEN 2>/dev/null |243 grep -qx "$pid"244}245 246listener_pids() {247 local gateway="$1" lsof_bin248 if ! lsof_bin="$(lsof_path)" || [[ -z "$lsof_bin" ]]; then249 return 1250 fi251 "$lsof_bin" -t -nP "-iTCP@${gateway}:${PORT}" -sTCP:LISTEN 2>/dev/null252}253 254find_matching_socat() {255 local gateway="$1" socket="$2" pid256 while IFS= read -r pid; do257 if socat_process_matches "$pid" "$gateway" "$socket"; then258 echo "$pid"259 return 0260 fi261 done < <(listener_pids "$gateway" || true)262 return 1263}264 265process_is_gone() {266 ! kill -0 "$WAIT_PID" 2>/dev/null267}268 269cleanup_proxy_setup() {270 if [[ -n "$PROXY_START_PID" ]] && kill -0 "$PROXY_START_PID" 2>/dev/null; then271 kill "$PROXY_START_PID" 2>/dev/null || true272 fi273 PROXY_START_PID=""274 release_proxy_lock275}276 277# Stop only a listener proven to be socat with the expected bridge and socket arguments.278stop_docker_proxy() {279 local pid="" gateway="" socket=""280 acquire_proxy_lock281 trap cleanup_proxy_setup EXIT282 283 if load_proxy_state; then284 gateway="$MANAGED_PROXY_GATEWAY"285 socket="$MANAGED_PROXY_SOCKET"286 if socat_process_matches "$MANAGED_PROXY_PID" "$gateway" "$socket"; then287 pid="$MANAGED_PROXY_PID"288 else289 pid="$(find_matching_socat "$gateway" "$socket" || true)"290 fi291 else292 # Best-effort adoption supports proxies started before state tracking existed.293 if discover_gateway true && resolve_docker_socket true; then294 gateway="$GATEWAY"295 socket="$DOCKER_SOCKET"296 pid="$(find_matching_socat "$gateway" "$socket" || true)"297 fi298 fi299 300 if [[ -z "$pid" ]]; then301 log "==> No matching socat Docker proxy found"302 clear_proxy_state303 release_proxy_lock304 trap - EXIT305 return 0306 fi307 308 if ! socat_process_matches "$pid" "$gateway" "$socket"; then309 log "error: refusing to stop PID ${pid}; it no longer matches the expected socat proxy"310 clear_proxy_state311 release_proxy_lock312 trap - EXIT313 return 1314 fi315 log "==> Stopping socat Docker proxy on ${gateway}:${PORT} (PID ${pid})"316 kill "$pid"317 WAIT_PID="$pid"318 if ! retry_command 10 100 500 "socat shutdown" false process_is_gone; then319 log "error: socat process ${pid} did not stop"320 release_proxy_lock321 trap - EXIT322 return 1323 fi324 clear_proxy_state325 log "==> Stopped socat Docker proxy (PID ${pid})"326 release_proxy_lock327 trap - EXIT328}329 330# The container machine cannot use the host's Unix Docker socket directly: the331# socket lives on macOS, while Docker commands run inside the Linux guest. socat332# bridges a TCP endpoint on Apple's container-machine gateway to that host Unix333# socket, allowing the guest Docker CLI to use it through DOCKER_HOST.334#335# Reuse an existing Docker API listener (OrbStack/etc.) when possible. Otherwise336# start our own proxy bound only to the machine bridge address, never 0.0.0.0,337# because access to this unauthenticated endpoint grants control of host Docker.338ensure_docker_proxy() {339 local pid="" existing_pids="" socat_bin=""340 acquire_proxy_lock341 trap cleanup_proxy_setup EXIT342 343 if docker_api_ok; then344 if load_proxy_state &&345 socat_process_matches "$MANAGED_PROXY_PID" "$MANAGED_PROXY_GATEWAY" "$MANAGED_PROXY_SOCKET"; then346 log "==> Using managed socat Docker proxy at ${DOCKER_HOST_TCP} (PID ${MANAGED_PROXY_PID})"347 elif resolve_docker_socket true &&348 pid="$(find_matching_socat "$GATEWAY" "$DOCKER_SOCKET" || true)" && [[ -n "$pid" ]]; then349 write_proxy_state "$pid" "$GATEWAY" "$DOCKER_SOCKET"350 log "==> Using recognized socat Docker proxy at ${DOCKER_HOST_TCP} (PID ${pid})"351 log "==> mise-linux-shell --stop will stop this exact proxy"352 else353 clear_proxy_state354 log "==> Using existing Docker API listener at ${DOCKER_HOST_TCP}"355 log "==> Listener is not a matching socat proxy and will not be stopped by --stop"356 fi357 release_proxy_lock358 trap - EXIT359 return 0360 fi361 362 if ! socat_bin="$(command -v socat 2>/dev/null)" || [[ -z "$socat_bin" ]]; then363 log "error: host Docker API not reachable at ${DOCKER_HOST_TCP} and socat is not installed"364 log ""365 log " brew install socat"366 log " # then re-run: mise run mise-linux-shell"367 release_proxy_lock368 trap - EXIT369 return 1370 fi371 if ! resolve_docker_socket; then372 release_proxy_lock373 trap - EXIT374 return 1375 fi376 377 pid="$(find_matching_socat "$GATEWAY" "$DOCKER_SOCKET" || true)"378 if [[ -n "$pid" ]]; then379 write_proxy_state "$pid" "$GATEWAY" "$DOCKER_SOCKET"380 log "error: matching socat proxy is listening at ${DOCKER_HOST_TCP} (PID ${pid}), but the Docker API did not respond"381 log " check that Docker is running and the socket is healthy: ${DOCKER_SOCKET}"382 release_proxy_lock383 trap - EXIT384 return 1385 fi386 existing_pids="$(listener_pids "$GATEWAY" || true)"387 if [[ -n "$existing_pids" ]]; then388 log "error: ${GATEWAY}:${PORT} is already occupied by a non-matching listener (pids: ${existing_pids//$'\n'/ })"389 release_proxy_lock390 trap - EXIT391 return 1392 fi393 394 # Bind only to the bridge gateway — not 0.0.0.0 — so the API stays off the LAN.395 : >"$PROXY_LOG_FILE"396 nohup "$socat_bin" \397 "TCP-LISTEN:${PORT},bind=${GATEWAY},reuseaddr,fork" \398 "UNIX-CONNECT:${DOCKER_SOCKET}" >"$PROXY_LOG_FILE" 2>&1 &399 PROXY_START_PID=$!400 disown 2>/dev/null || true401 log "==> Starting socat Docker proxy on ${GATEWAY}:${PORT} -> ${DOCKER_SOCKET} (PID ${PROXY_START_PID})"402 403 if ! wait_docker_proxy_ready; then404 log "error: started socat but Docker API still not reachable at ${DOCKER_HOST_TCP}"405 if [[ -s "$PROXY_LOG_FILE" ]]; then406 log "==> socat output ($PROXY_LOG_FILE):"407 sed -n '1,20p' "$PROXY_LOG_FILE" >&2408 fi409 cleanup_proxy_setup410 trap - EXIT411 return 1412 fi413 414 PROXY_START_PID="$(find_matching_socat "$GATEWAY" "$DOCKER_SOCKET" || true)"415 if [[ -z "$PROXY_START_PID" ]]; then416 log "error: Docker API became reachable, but the expected socat listener could not be identified"417 cleanup_proxy_setup418 trap - EXIT419 return 1420 fi421 write_proxy_state "$PROXY_START_PID" "$GATEWAY" "$DOCKER_SOCKET"422 log "==> Started managed socat Docker proxy at ${DOCKER_HOST_TCP} (PID ${PROXY_START_PID})"423 log "==> Run 'mise run mise-linux-shell --stop' to stop it"424 PROXY_START_PID=""425 release_proxy_lock426 trap - EXIT427 return 0428}429 430destroy_machine() {431 log "==> Stopping machine: $NAME"432 container machine stop "$NAME" 2>/dev/null || true433 container machine delete "$NAME" 2>/dev/null || true434}435 436if [[ "${usage_stop:-false}" == "true" ]]; then437 if [[ "${usage_restart:-false}" == "true" ]]; then438 log "error: --stop and --restart are mutually exclusive"439 exit 1440 fi441 destroy_machine442 stop_docker_proxy443 log "==> Stopped"444 exit 0445fi446 447guest_mise_env=(448 -e "MISE_DATA_DIR=${MISE_DATA_DIR_GUEST}"449 -e "MISE_CACHE_DIR=${MISE_CACHE_DIR_GUEST}"450 -e "MISE_CONFIG_DIR=${MISE_CONFIG_DIR_GUEST}"451 -e "MISE_GLOBAL_CONFIG_FILE=${MISE_CONFIG_DIR_GUEST}/config.toml"452)453 454ensure_mise_dirs() {455 machine_run --root -- \456 mkdir -p "$MISE_DATA_DIR_GUEST" "$MISE_CACHE_DIR_GUEST" "$MISE_CONFIG_DIR_GUEST"457 # The machine user owns mise state; other guest users only need traversal.458 machine_run --root -- \459 chown "$USER" "$MISE_DATA_DIR_GUEST" "$MISE_CACHE_DIR_GUEST" "$MISE_CONFIG_DIR_GUEST"460 machine_run --root -- \461 chmod 0755 "$MISE_DATA_DIR_GUEST" "$MISE_CACHE_DIR_GUEST" "$MISE_CONFIG_DIR_GUEST"462 machine_run -- \463 touch "${MISE_CONFIG_DIR_GUEST}/config.toml"464 # Empty global config used for project `mise install` so guest-only tools465 # like usage (completions) are not required to be in the project lockfile.466 machine_run -- \467 rm -f "${MISE_CONFIG_DIR_GUEST}/empty.toml"468 machine_run -- \469 touch "${MISE_CONFIG_DIR_GUEST}/empty.toml"470}471 472# Packages + Linux mise + zsh. container machine run mangles `sh -c '...'`.473bootstrap_machine() {474 wait_machine_ready475 476 log "==> Bootstrapping machine packages"477 # bash/git: mise's Python backend uses python-build to resolve versions478 # docker-cli: talk to host Docker via DOCKER_HOST479 # zsh: interactive shell480 # libstdc++/libgcc/gcompat: many mise tools ship glibc/musl hybrids481 # curl: mise installer482 machine_run --root -- \483 apk add bash git docker-cli zsh curl libstdc++ libgcc gcompat484 485 log "==> Installing mise"486 machine_run --root -- \487 curl -fsSL -o /tmp/mise-install.sh https://mise.run488 # Alpine is musl — force the musl build (see mise.run / server.sh tip).489 machine_run --root \490 -e MISE_INSTALL_MUSL=1 \491 -e MISE_INSTALL_PATH=/usr/local/bin/mise \492 -e "MISE_VERSION=${RAILPACK_MISE_VERSION}" \493 -- sh /tmp/mise-install.sh494 495 ensure_mise_dirs496 setup_zsh497}498 499# Railpack caches a separate mise binary for host-side version resolution. Its500# generic Linux download is glibc-linked, so reuse the pinned musl binary that501# this Alpine machine already installed instead.502configure_railpack_mise() {503 log "==> Configuring Railpack to use musl mise ${RAILPACK_MISE_VERSION}"504 machine_run -- mkdir -p "$RAILPACK_MISE_DIR"505 machine_run -- \506 ln -sfn /usr/local/bin/mise "${RAILPACK_MISE_DIR}/mise-${RAILPACK_MISE_VERSION}"507}508 509# mise stops at the ceiling and does not load configs *at* that path. Use the510# project's parent so this repo's mise.toml loads, but never parent dirs or ~.511guest_ceiling() {512 echo "${MISE_CEILING_PATHS:-$(dirname "$PROJECT_ROOT")}"513}514 515# Host project tree is mounted into the machine; guest mise state is fresh after516# create/restart, so trust the project before any command that loads its config.517# Trust PROJECT_ROOT only — not --all (that walks parents too).518trust_guest_mise() {519 local ceiling520 ceiling="$(guest_ceiling)"521 machine_run \522 -e "MISE_CEILING_PATHS=${ceiling}" \523 "${guest_mise_env[@]}" \524 -- mise trust "$PROJECT_ROOT"525}526 527# Install usage CLI for mise task completions without adding it to any mise528# config. Project settings.locked=true would reject a global `usage` tool that529# is not in mise.lock.530install_usage_cli() {531 log "==> Installing usage (completions helper, not a project tool)"532 # Isolate from project + guest global config so locked mode cannot apply.533 machine_run \534 -w /var/tmp \535 -e MISE_CEILING_PATHS=/var/tmp \536 -e "MISE_DATA_DIR=${MISE_DATA_DIR_GUEST}" \537 -e "MISE_CACHE_DIR=${MISE_CACHE_DIR_GUEST}" \538 -e "MISE_CONFIG_DIR=${MISE_CONFIG_DIR_GUEST}" \539 -e "MISE_GLOBAL_CONFIG_FILE=${MISE_CONFIG_DIR_GUEST}/empty.toml" \540 -e MISE_LOCKED=0 \541 -e CI=1 \542 -- mise install usage@latest543 # Keep global config free of usage so project activate stays clean.544 machine_run -- \545 cp "${MISE_CONFIG_DIR_GUEST}/empty.toml" "${MISE_CONFIG_DIR_GUEST}/config.toml"546 machine_run --root -- \547 ln -sfn "${MISE_DATA_DIR_GUEST}/installs/usage/latest/usage" /usr/local/bin/usage548}549 550# zsh + mise (modeled on https://github.com/wintermi/zsh-mise/blob/main/zsh-mise.plugin.zsh).551# usage is a prerequisite for mise completions (same as that plugin's README).552setup_zsh() {553 log "==> Configuring zsh + mise"554 local zshrc_host555 trust_guest_mise556 install_usage_cli557 558 machine_run --root -- mkdir -p /etc/zsh559 560 # Write zshrc via the shared project mount (no stdin pipe into machine run).561 mkdir -p "${PROJECT_ROOT}/tmp"562 zshrc_host="$(mktemp "${PROJECT_ROOT}/tmp/railpack-linux-zshrc.XXXXXX")"563 trap 'rm -f -- "$zshrc_host"' EXIT564 printf '%s\n' \565 '# railpack-linux-shell — https://github.com/wintermi/zsh-mise' \566 'if (( $+commands[mise] )); then' \567 ' source <(mise activate zsh)' \568 ' source <(mise hook-env -s zsh)' \569 ' _mise_comp_dir="${MISE_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/mise}/zsh-completions"' \570 ' mkdir -p "$_mise_comp_dir"' \571 ' fpath=("$_mise_comp_dir" $fpath)' \572 ' if [[ ! -f "$_mise_comp_dir/_mise" ]]; then' \573 ' typeset -g -A _comps' \574 ' autoload -Uz _mise' \575 ' _comps[mise]=_mise' \576 ' fi' \577 ' mise completion zsh >| "$_mise_comp_dir/_mise" &|' \578 'fi' \579 'autoload -Uz compinit && compinit' \580 >"$zshrc_host"581 machine_run --root -- cp "$zshrc_host" /etc/zsh/zshrc582 rm -f -- "$zshrc_host"583 trap - EXIT584 585 # Alpine has neither chsh nor usermod; interactive entry always runs zsh -l.586}587 588run_mise_install() {589 local ceiling590 ceiling="$(guest_ceiling)"591 log "==> mise install (project tools)"592 trust_guest_mise593 # Use empty global config: guest global config may list `usage` for shell594 # completions, which is not in the project lockfile (settings.locked=true).595 machine_run \596 -e "MISE_CEILING_PATHS=${ceiling}" \597 -e "MISE_DATA_DIR=${MISE_DATA_DIR_GUEST}" \598 -e "MISE_CACHE_DIR=${MISE_CACHE_DIR_GUEST}" \599 -e "MISE_CONFIG_DIR=${MISE_CONFIG_DIR_GUEST}" \600 -e "MISE_GLOBAL_CONFIG_FILE=${MISE_CONFIG_DIR_GUEST}/empty.toml" \601 -e CI=1 \602 -- mise install603}604 605create_machine() {606 log "==> Creating $NAME ($IMAGE)"607 container machine create "$IMAGE" --name "$NAME" --cpus 4 --memory 8G608 bootstrap_machine609 run_mise_install610}611 612discover_gateway613ensure_docker_proxy614 615if [[ "${usage_restart:-false}" == "true" ]]; then616 log "==> --restart: deleting $NAME"617 destroy_machine618fi619 620if ! container machine inspect "$NAME" &>/dev/null; then621 create_machine622else623 wait_machine_ready624 if ! machine_run -- command -v mise >/dev/null 2>&1; then625 bootstrap_machine626 run_mise_install627 else628 ensure_mise_dirs629 fi630fi631 632wait_machine_ready633configure_railpack_mise634 635CEILING="$(guest_ceiling)"636 637log "==> DOCKER_HOST=$DOCKER_HOST_TCP"638log "==> MISE_DATA_DIR=$MISE_DATA_DIR_GUEST (machine-local, not Mac home)"639log "==> MISE_CEILING_PATHS=$CEILING"640exec container machine run -it \641 -n "$NAME" \642 -e "DOCKER_HOST=${DOCKER_HOST_TCP}" \643 -e "MISE_CEILING_PATHS=${CEILING}" \644 "${guest_mise_env[@]}" \645 -- zsh -l646