fix(easy-docker): harden site bootstrap and details

This commit is contained in:
RocketQuack 2026-03-28 13:13:17 +01:00
parent bfa70da36e
commit 7c4d1d47ca
5 changed files with 237 additions and 42 deletions

View file

@ -1,5 +1,8 @@
#!/usr/bin/env bash
EASY_DOCKER_SITE_ERROR_DETAIL=""
EASY_DOCKER_SITE_ERROR_LOG_PATH=""
load_easy_docker_site_modules() {
local site_dir=""
site_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/site"

View file

@ -190,11 +190,98 @@ EOF
wrapped_backend_command="$(printf "cd /home/frappe/frappe-bench && %s" "${backend_command}")"
if [ -n "${fallback_erpnext_version}" ]; then
ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}"
ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}" </dev/null
return $?
fi
docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}"
docker compose --project-name "${compose_project_name}" --env-file "${env_path}" "${compose_args[@]}" exec -T backend bash -lc "${wrapped_backend_command}" </dev/null
}
reset_easy_docker_site_error_state() {
EASY_DOCKER_SITE_ERROR_DETAIL=""
EASY_DOCKER_SITE_ERROR_LOG_PATH=""
}
build_stack_site_error_log_relative_path() {
local result_var="${1}"
local action_name="${2:-site-error}"
local raw_timestamp=""
local safe_timestamp=""
local relative_path=""
raw_timestamp="$(get_current_utc_timestamp)"
safe_timestamp="$(printf '%s' "${raw_timestamp}" | tr ':' '-')"
relative_path="$(printf 'logs/%s-%s.log' "${action_name}" "${safe_timestamp}")"
printf -v "${result_var}" "%s" "${relative_path}"
}
write_stack_site_error_log() {
local result_var="${1}"
local stack_dir="${2}"
local action_name="${3:-site-error}"
local error_output="${4:-}"
local relative_path=""
local log_dir=""
local absolute_path=""
if [ -z "${error_output}" ]; then
printf -v "${result_var}" "%s" ""
return 0
fi
build_stack_site_error_log_relative_path relative_path "${action_name}"
log_dir="${stack_dir}/logs"
absolute_path="${stack_dir}/${relative_path}"
if ! mkdir -p "${log_dir}"; then
return 1
fi
if ! printf '%s\n' "${error_output}" >"${absolute_path}"; then
return 1
fi
printf -v "${result_var}" "%s" "${relative_path}"
return 0
}
run_stack_backend_bash_command_capture() {
local result_var="${1}"
local stack_dir="${2}"
local backend_command="${3}"
local command_output=""
local command_status=0
reset_easy_docker_site_error_state
command_output="$(run_stack_backend_bash_command "${stack_dir}" "${backend_command}" 2>&1)"
command_status=$?
if [ -n "${command_output}" ]; then
printf '%s\n' "${command_output}"
fi
printf -v "${result_var}" "%s" "${command_output}"
return "${command_status}"
}
capture_stack_site_error_log() {
local stack_dir="${1}"
local action_name="${2:-site-error}"
local error_output="${3:-}"
local log_path=""
EASY_DOCKER_SITE_ERROR_LOG_PATH=""
if [ -z "${error_output}" ]; then
return 0
fi
if ! write_stack_site_error_log log_path "${stack_dir}" "${action_name}" "${error_output}"; then
EASY_DOCKER_SITE_ERROR_DETAIL="${EASY_DOCKER_SITE_ERROR_DETAIL:-Failed to write site error log.}"
return 1
fi
EASY_DOCKER_SITE_ERROR_LOG_PATH="${log_path}"
return 0
}
stack_backend_service_is_running() {
@ -359,6 +446,76 @@ EOF
run_stack_backend_bash_command "${stack_dir}" "${read_command}"
}
get_stack_site_runtime_app_names_lines() {
local stack_dir="${1}"
local site_name="${2}"
local list_apps_command=""
if ! is_safe_stack_site_cleanup_name "${site_name}"; then
return 61
fi
list_apps_command="$(
printf "bench --site %s list-apps | awk 'NF { print \$1 }'" \
"$(shell_quote_site_command_arg "${site_name}")"
)"
run_stack_backend_bash_command "${stack_dir}" "${list_apps_command}"
}
get_stack_runtime_available_app_lines() {
local stack_dir="${1}"
run_stack_backend_bash_command "${stack_dir}" "ls -1 apps"
}
get_stack_site_runtime_selected_apps_lines() {
local result_var="${1}"
local stack_dir="${2}"
local site_name="${3}"
local selected_app_lines=""
local runtime_app_lines=""
local selected_app_name=""
local installed_app_lines=""
if ! get_stack_selected_installable_apps selected_app_lines "${stack_dir}"; then
printf -v "${result_var}" "%s" ""
return 0
fi
if [ -z "${selected_app_lines}" ]; then
printf -v "${result_var}" "%s" ""
return 0
fi
runtime_app_lines="$(get_stack_site_runtime_app_names_lines "${stack_dir}" "${site_name}" || true)"
if [ -z "${runtime_app_lines}" ]; then
printf -v "${result_var}" "%s" ""
return 1
fi
while IFS= read -r selected_app_name; do
if [ -z "${selected_app_name}" ]; then
continue
fi
if ! printf '%s\n' "${runtime_app_lines}" | grep -F -x -- "${selected_app_name}" >/dev/null 2>&1; then
continue
fi
if [ -z "${installed_app_lines}" ]; then
installed_app_lines="${selected_app_name}"
else
installed_app_lines="${installed_app_lines}"$'\n'"${selected_app_name}"
fi
done <<EOF
${selected_app_lines}
EOF
printf -v "${result_var}" "%s" "${installed_app_lines}"
return 0
}
repair_stack_site_runtime_state() {
local stack_dir="${1}"
local database_id=""
@ -589,6 +746,7 @@ create_first_stack_site() {
local site_name="${2}"
local admin_password="${3}"
local create_site_command=""
local create_site_output=""
create_site_command="$(
printf "bench new-site %s --mariadb-user-host-login-scope='%%' --admin-password %s --db-root-username root --db-root-password %s" \
@ -597,7 +755,9 @@ create_first_stack_site() {
"$(shell_quote_site_command_arg "$(get_stack_database_root_password "${stack_dir}")")"
)"
if ! run_stack_backend_bash_command "${stack_dir}" "${create_site_command}"; then
if ! run_stack_backend_bash_command_capture create_site_output "${stack_dir}" "${create_site_command}"; then
EASY_DOCKER_SITE_ERROR_DETAIL="bench new-site failed."
capture_stack_site_error_log "${stack_dir}" "site-create-error" "${create_site_output}" >/dev/null 2>&1 || true
return 55
fi
@ -612,24 +772,52 @@ install_stack_apps_on_site() {
local installed_app_lines=""
local app_name=""
local install_app_command=""
local install_app_output=""
local available_app_lines=""
local -a selected_apps=()
if ! get_stack_selected_installable_apps selected_app_lines "${stack_dir}"; then
printf -v "${result_var}" "%s" ""
return 0
fi
while IFS= read -r app_name; do
if [ -z "${selected_app_lines}" ]; then
printf -v "${result_var}" "%s" ""
return 0
fi
available_app_lines="$(get_stack_runtime_available_app_lines "${stack_dir}" || true)"
if [ -z "${available_app_lines}" ]; then
EASY_DOCKER_SITE_ERROR_DETAIL="Could not inspect available apps in the backend image."
capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "easy-docker could not list /home/frappe/frappe-bench/apps before install-app." >/dev/null 2>&1 || true
return 63
fi
mapfile -t selected_apps <<<"${selected_app_lines}"
for app_name in "${selected_apps[@]}"; do
if [ -z "${app_name}" ]; then
continue
fi
if ! printf '%s\n' "${available_app_lines}" | grep -F -x -- "${app_name}" >/dev/null 2>&1; then
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "Selected app '%s' is not available in the backend image." "${app_name}")"
capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "$(printf "Selected app '%s' was requested in stack metadata but is missing from /home/frappe/frappe-bench/apps.\nAvailable apps:\n%s" "${app_name}" "${available_app_lines}")" >/dev/null 2>&1 || true
if [ -n "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" ]; then
printf 'Details written to %s\n' "${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}" >&2
fi
printf -v "${result_var}" "%s" "${installed_app_lines}"
return 63
fi
install_app_command="$(
printf "bench --site %s install-app %s" \
"$(shell_quote_site_command_arg "${site_name}")" \
"$(shell_quote_site_command_arg "${app_name}")"
)"
if ! run_stack_backend_bash_command "${stack_dir}" "${install_app_command}"; then
if ! run_stack_backend_bash_command_capture install_app_output "${stack_dir}" "${install_app_command}"; then
EASY_DOCKER_SITE_ERROR_DETAIL="$(printf "bench install-app failed for '%s'." "${app_name}")"
capture_stack_site_error_log "${stack_dir}" "site-install-apps-error" "${install_app_output}" >/dev/null 2>&1 || true
printf -v "${result_var}" "%s" "${installed_app_lines}"
return 56
fi
@ -648,13 +836,12 @@ install_stack_apps_on_site() {
"${installed_app_lines}" \
"install-apps" \
"" \
"" \
"$(get_stack_site_created_at "${stack_dir}" || true)" \
"$(get_current_utc_timestamp)"; then
return 58
fi
done <<EOF
${selected_app_lines}
EOF
done
printf -v "${result_var}" "%s" "${installed_app_lines}"
return 0
@ -701,7 +888,7 @@ bootstrap_first_stack_site() {
created_at="$(get_current_utc_timestamp)"
updated_at="${created_at}"
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "requested" "" "create-site" "" "${created_at}" "${updated_at}"; then
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "requested" "" "create-site" "" "" "${created_at}" "${updated_at}"; then
return 58
fi
@ -714,18 +901,18 @@ bootstrap_first_stack_site() {
return "${cleanup_status}"
;;
60)
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Partial site artifacts could not be removed automatically. Manual cleanup is required." "" >/dev/null 2>&1 || true
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Partial site artifacts could not be removed automatically. Manual cleanup is required." "" "" >/dev/null 2>&1 || true
return 60
;;
*)
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Unexpected cleanup failure before create-site." "${created_at}" >/dev/null 2>&1 || true
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "cleanup-partial-site" "Unexpected cleanup failure before create-site." "" "${created_at}" >/dev/null 2>&1 || true
return 60
;;
esac
fi
updated_at="${created_at}"
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "creating" "" "create-site" "" "${created_at}" "${updated_at}"; then
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "creating" "" "create-site" "" "" "${created_at}" "${updated_at}"; then
return 58
fi
@ -734,12 +921,12 @@ bootstrap_first_stack_site() {
else
site_create_status=$?
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed. Partial site data was cleaned up automatically." "${created_at}" >/dev/null 2>&1 || true
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed. Partial site data was cleaned up automatically." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
return "${site_create_status}"
fi
cleanup_status=$?
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed and partial site data could not be cleaned up automatically. Manual cleanup is required." "${created_at}" >/dev/null 2>&1 || true
mark_stack_site_failed "${stack_dir}" "${site_name}" "" "create-site" "bench new-site failed and partial site data could not be cleaned up automatically. Manual cleanup is required." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
case "${cleanup_status}" in
54 | 52)
return "${cleanup_status}"
@ -751,7 +938,7 @@ bootstrap_first_stack_site() {
fi
updated_at="$(get_current_utc_timestamp)"
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "created" "" "create-site" "" "${created_at}" "${updated_at}"; then
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "created" "" "create-site" "" "" "${created_at}" "${updated_at}"; then
return 58
fi
@ -760,12 +947,12 @@ bootstrap_first_stack_site() {
else
app_install_status=$?
case "${app_install_status}" in
56)
56 | 63)
if cleanup_partial_stack_site "${stack_dir}" "${site_name}"; then
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "App installation failed. Partial site data was cleaned up automatically." "${created_at}" >/dev/null 2>&1 || true
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "${EASY_DOCKER_SITE_ERROR_DETAIL:-App installation failed. Partial site data was cleaned up automatically.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
else
cleanup_status=$?
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "App installation failed and partial site data could not be cleaned up automatically. Manual cleanup is required." "${created_at}" >/dev/null 2>&1 || true
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "${EASY_DOCKER_SITE_ERROR_DETAIL:-App installation failed and partial site data could not be cleaned up automatically. Manual cleanup is required.}" "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
case "${cleanup_status}" in
54 | 52)
return "${cleanup_status}"
@ -780,14 +967,14 @@ bootstrap_first_stack_site() {
return 58
;;
*)
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "Unknown app installation failure." "${created_at}" >/dev/null 2>&1 || true
mark_stack_site_failed "${stack_dir}" "${site_name}" "${installed_app_lines}" "install-apps" "Unknown app installation failure." "${EASY_DOCKER_SITE_ERROR_LOG_PATH}" "${created_at}" >/dev/null 2>&1 || true
;;
esac
return "${app_install_status}"
fi
updated_at="$(get_current_utc_timestamp)"
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "ready" "${installed_app_lines}" "install-apps" "" "${created_at}" "${updated_at}"; then
if ! persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "ready" "${installed_app_lines}" "install-apps" "" "" "${created_at}" "${updated_at}"; then
return 58
fi

View file

@ -220,19 +220,21 @@ build_stack_site_metadata_json_object() {
local apps_installed_lines="${5:-}"
local last_action="${6:-}"
local last_error="${7:-}"
local created_at="${8:-}"
local updated_at="${9:-}"
local error_log_path="${8:-}"
local created_at="${9:-}"
local updated_at="${10:-}"
local apps_installed_json_array=""
build_stack_site_apps_installed_json_array apps_installed_json_array "${apps_installed_lines}"
printf -v "${result_var}" '{\n "mode": "%s",\n "name": "%s",\n "state": "%s",\n "apps_installed": %s,\n "last_action": "%s",\n "last_error": "%s",\n "created_at": "%s",\n "updated_at": "%s"\n }' \
printf -v "${result_var}" '{\n "mode": "%s",\n "name": "%s",\n "state": "%s",\n "apps_installed": %s,\n "last_action": "%s",\n "last_error": "%s",\n "error_log_path": "%s",\n "created_at": "%s",\n "updated_at": "%s"\n }' \
"$(json_escape_string "${site_mode}")" \
"$(json_escape_string "${site_name}")" \
"$(json_escape_string "${site_state}")" \
"${apps_installed_json_array}" \
"$(json_escape_string "${last_action}")" \
"$(json_escape_string "${last_error}")" \
"$(json_escape_string "${error_log_path}")" \
"$(json_escape_string "${created_at}")" \
"$(json_escape_string "${updated_at}")"
}
@ -245,8 +247,9 @@ persist_stack_site_metadata() {
local apps_installed_lines="${5:-}"
local last_action="${6:-}"
local last_error="${7:-}"
local created_at="${8:-}"
local updated_at="${9:-}"
local error_log_path="${8:-}"
local created_at="${9:-}"
local updated_at="${10:-}"
local metadata_path=""
local metadata_tmp_path=""
local site_json_object=""
@ -257,7 +260,7 @@ persist_stack_site_metadata() {
return 1
fi
build_stack_site_metadata_json_object site_json_object "${site_mode}" "${site_name}" "${site_state}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${created_at}" "${updated_at}"
build_stack_site_metadata_json_object site_json_object "${site_mode}" "${site_name}" "${site_state}" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}"
if ! awk -v site_object="${site_json_object}" '
BEGIN {
@ -342,9 +345,10 @@ mark_stack_site_failed() {
local apps_installed_lines="${3:-}"
local last_action="${4:-bootstrap-site}"
local last_error="${5:-Unknown site bootstrap failure}"
local created_at="${6:-}"
local error_log_path="${6:-}"
local created_at="${7:-}"
local updated_at=""
updated_at="$(get_current_utc_timestamp)"
persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "failed" "${apps_installed_lines}" "${last_action}" "${last_error}" "${created_at}" "${updated_at}"
persist_stack_site_metadata "${stack_dir}" "single-site" "${site_name}" "failed" "${apps_installed_lines}" "${last_action}" "${last_error}" "${error_log_path}" "${created_at}" "${updated_at}"
}

View file

@ -131,7 +131,6 @@ prompt_manage_stack_site_admin_password_with_cancel() {
handle_manage_stack_site_flow() {
local stack_name="${1}"
local stack_dir="${2}"
local site_status_label=""
local site_action=""
local site_name=""
local admin_password=""
@ -144,11 +143,10 @@ handle_manage_stack_site_flow() {
local existing_site_details_action=""
while true; do
get_stack_site_status_label site_status_label "${stack_dir}"
existing_site_entry=""
get_stack_site_menu_entry existing_site_entry "${stack_dir}" || true
site_action="$(show_manage_stack_site_menu "${stack_name}" "${stack_dir}" "${site_status_label}" "${existing_site_entry}" || true)"
site_action="$(show_manage_stack_site_menu "${stack_name}" "${stack_dir}" "${existing_site_entry}" || true)"
case "${site_action}" in
"Create new site")
if ! prompt_manage_stack_site_name_with_cancel site_name "${stack_name}" "${stack_dir}"; then
@ -181,10 +179,10 @@ handle_manage_stack_site_flow() {
show_warning_and_wait "Cannot manage site because stack metadata, env, or compose inputs are incomplete." 4
;;
55)
show_warning_and_wait "Could not create the site. Check the output above for bench new-site details." 4
show_warning_and_wait "Could not create the site. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above for bench new-site details.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
;;
56)
show_warning_and_wait "The site was created, but app installation failed. Check the output above." 4
show_warning_and_wait "The site was created, but app installation failed. ${EASY_DOCKER_SITE_ERROR_DETAIL:-Check the output above.} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 6
;;
57)
show_warning_and_wait "Site bootstrap currently supports only MariaDB-backed single-host stacks." 4
@ -204,6 +202,9 @@ handle_manage_stack_site_flow() {
62)
show_warning_and_wait "Cannot prepare the stack for site creation because the bench runtime files could not be repaired." 4
;;
63)
show_warning_and_wait "Cannot install the selected stack apps because at least one app is missing from the backend image. ${EASY_DOCKER_SITE_ERROR_DETAIL} ${EASY_DOCKER_SITE_ERROR_LOG_PATH:+See ${stack_dir}/${EASY_DOCKER_SITE_ERROR_LOG_PATH}}" 7
;;
*)
show_warning_and_wait "Site bootstrap failed (${site_flow_status})." 4
;;
@ -220,6 +221,9 @@ handle_manage_stack_site_flow() {
existing_site_name="$(get_stack_site_name "${stack_dir}" || true)"
existing_site_created_at="$(get_stack_site_created_at "${stack_dir}" || true)"
existing_site_apps_lines="$(get_stack_site_apps_installed_lines "${stack_dir}" || true)"
if stack_backend_service_is_running "${stack_dir}" >/dev/null 2>&1; then
get_stack_site_runtime_selected_apps_lines existing_site_apps_lines "${stack_dir}" "${existing_site_name}" || true
fi
if [ -n "${existing_site_apps_lines}" ]; then
existing_site_apps_csv="$(printf '%s' "${existing_site_apps_lines}" | tr '\n' ',' | sed 's/,$//')"
else
@ -231,7 +235,6 @@ handle_manage_stack_site_flow() {
"${stack_name}" \
"${stack_dir}" \
"${existing_site_name}" \
"${site_status_label}" \
"${existing_site_created_at}" \
"${existing_site_apps_csv}" || true
)"

View file

@ -125,13 +125,12 @@ show_manage_stack_docker_menu() {
show_manage_stack_site_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local site_status="${3:-Not configured}"
local existing_site_entry="${4:-}"
local existing_site_entry="${3:-}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage stack site\n\nStack: %s\nDirectory: %s\nSite status: %s\n\nCreate a new site or select an existing site for this stack." "${stack_name}" "${stack_dir}" "${site_status}")"
status_text="$(printf "Manage stack site\n\nStack: %s\nDirectory: %s\n\nCreate a new site or select an existing site for this stack." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
if [ -n "${existing_site_entry}" ]; then
@ -192,18 +191,17 @@ show_manage_stack_site_details() {
local stack_name="${1}"
local stack_dir="${2}"
local site_name="${3}"
local site_status="${4:-Unknown}"
local created_at="${5:-}"
local installed_apps="${6:-None}"
local created_at="${4:-}"
local installed_apps="${5:-None}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage existing site\n\nStack: %s\nDirectory: %s\nSite: %s\nStatus: %s\nCreated at: %s\nInstalled apps: %s" "${stack_name}" "${stack_dir}" "${site_name}" "${site_status}" "${created_at:-n/a}" "${installed_apps}")"
status_text="$(printf "Manage existing site\n\nStack: %s\nDirectory: %s\nSite: %s\nCreated at: %s\nInstalled apps: %s" "${stack_name}" "${stack_dir}" "${site_name}" "${created_at:-n/a}" "${installed_apps}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 7 \
--height 8 \
--header "Site details" \
--cursor.foreground 63 \
--selected.foreground 45 \