refactor(easy-docker): split compose and production modules

This commit is contained in:
RocketQuack 2026-03-25 13:10:17 +01:00
parent d2ee473c68
commit 2a9aabbdb3
8 changed files with 906 additions and 834 deletions

View file

@ -4,420 +4,16 @@ EASY_DOCKER_BUILD_ERROR_DETAIL=""
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails.
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
render_stack_compose_from_metadata() {
local stack_dir="${1}"
local metadata_path=""
local env_path=""
local generated_compose_path=""
local generated_compose_tmp_path=""
local compose_files_lines=""
local compose_file=""
local source_compose_path=""
local env_erpnext_version=""
local fallback_erpnext_version=""
local repo_root=""
local -a compose_args=()
load_easy_docker_compose_modules() {
local compose_dir=""
compose_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/compose"
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
generated_compose_tmp_path="${generated_compose_path}.tmp"
if [ ! -f "${metadata_path}" ]; then
return 1
fi
if [ ! -f "${env_path}" ]; then
return 1
fi
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
if [ -z "${env_erpnext_version}" ]; then
fallback_erpnext_version="$(get_default_erpnext_version || true)"
fi
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
if [ -z "${compose_files_lines}" ]; then
return 1
fi
repo_root="$(get_easy_docker_repo_root)"
while IFS= read -r compose_file; do
if [ -z "${compose_file}" ]; then
continue
fi
source_compose_path="${repo_root}/${compose_file}"
if [ ! -f "${source_compose_path}" ]; then
return 1
fi
compose_args+=(-f "${source_compose_path}")
done <<EOF
${compose_files_lines}
EOF
if [ "${#compose_args[@]}" -eq 0 ]; then
return 1
fi
if [ -n "${fallback_erpnext_version}" ]; then
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
if ! mv -- "${generated_compose_tmp_path}" "${generated_compose_path}"; then
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
return 0
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/render.sh
source "${compose_dir}/render.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/start.sh
source "${compose_dir}/start.sh"
# shellcheck source=scripts/easy-docker/lib/app/wizard/common/compose/build.sh
source "${compose_dir}/build.sh"
}
start_stack_with_compose_from_metadata() {
local stack_dir="${1}"
local metadata_path=""
local env_path=""
local compose_files_lines=""
local compose_file=""
local source_compose_path=""
local env_erpnext_version=""
local fallback_erpnext_version=""
local configured_pull_policy=""
local runtime_pull_policy=""
local custom_image=""
local custom_tag=""
local image_ref=""
local stack_topology=""
local repo_root=""
local -a compose_args=()
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails.
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
if [ ! -f "${metadata_path}" ]; then
return 31
fi
if [ ! -f "${env_path}" ]; then
return 32
fi
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
if [ -z "${stack_topology}" ]; then
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 33.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology"
return 33
fi
case "${stack_topology}" in
"single-host") ;;
*)
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 34.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
return 34
;;
esac
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
if [ -z "${env_erpnext_version}" ]; then
fallback_erpnext_version="$(get_default_erpnext_version || true)"
fi
configured_pull_policy="$(get_env_file_key_value "${env_path}" "PULL_POLICY" || true)"
if [ -z "${configured_pull_policy}" ]; then
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then
image_ref="${custom_image}:${custom_tag}"
if docker image inspect "${image_ref}" >/dev/null 2>&1; then
runtime_pull_policy="if_not_present"
fi
fi
fi
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
if [ -z "${compose_files_lines}" ]; then
return 35
fi
repo_root="$(get_easy_docker_repo_root)"
while IFS= read -r compose_file; do
if [ -z "${compose_file}" ]; then
continue
fi
source_compose_path="${repo_root}/${compose_file}"
if [ ! -f "${source_compose_path}" ]; then
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 36.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}"
return 36
fi
compose_args+=(-f "${source_compose_path}")
done <<EOF
${compose_files_lines}
EOF
if [ "${#compose_args[@]}" -eq 0 ]; then
return 35
fi
if [ -n "${fallback_erpnext_version}" ] && [ -n "${runtime_pull_policy}" ]; then
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" PULL_POLICY="${runtime_pull_policy}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
elif [ -n "${fallback_erpnext_version}" ]; then
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
elif [ -n "${runtime_pull_policy}" ]; then
if ! PULL_POLICY="${runtime_pull_policy}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
return 0
}
get_stack_compose_runtime_status_label() {
local result_var="${1}"
local stack_dir="${2}"
local metadata_path=""
local env_path=""
local stack_topology=""
local compose_files_lines=""
local compose_file=""
local source_compose_path=""
local env_erpnext_version=""
local fallback_erpnext_version=""
local running_services_lines=""
local compose_status=0
local running_services_count=0
local repo_root=""
local status_label=""
local -a compose_args=()
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
if [ ! -f "${metadata_path}" ]; then
printf -v "${result_var}" "%s" "Unknown (metadata missing)"
return 0
fi
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
if [ -z "${stack_topology}" ]; then
printf -v "${result_var}" "%s" "Unknown (topology missing)"
return 0
fi
case "${stack_topology}" in
"single-host") ;;
*)
printf -v "${result_var}" "%s" "N/A (${stack_topology})"
return 0
;;
esac
if [ ! -f "${env_path}" ]; then
printf -v "${result_var}" "%s" "Unknown (env missing)"
return 0
fi
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
if [ -z "${env_erpnext_version}" ]; then
fallback_erpnext_version="$(get_default_erpnext_version || true)"
fi
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
if [ -z "${compose_files_lines}" ]; then
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
return 0
fi
repo_root="$(get_easy_docker_repo_root)"
while IFS= read -r compose_file; do
if [ -z "${compose_file}" ]; then
continue
fi
source_compose_path="${repo_root}/${compose_file}"
if [ ! -f "${source_compose_path}" ]; then
printf -v "${result_var}" "%s" "Unknown (missing file: ${compose_file})"
return 0
fi
compose_args+=(-f "${source_compose_path}")
done <<EOF
${compose_files_lines}
EOF
if [ "${#compose_args[@]}" -eq 0 ]; then
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
return 0
fi
if [ -n "${fallback_erpnext_version}" ]; then
running_services_lines="$(
ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" ps --status running --services 2>/dev/null
)"
compose_status=$?
else
running_services_lines="$(
docker compose --env-file "${env_path}" "${compose_args[@]}" ps --status running --services 2>/dev/null
)"
compose_status=$?
fi
if [ "${compose_status}" -ne 0 ]; then
printf -v "${result_var}" "%s" "Unknown (docker compose status failed)"
return 0
fi
while IFS= read -r compose_file; do
if [ -n "${compose_file}" ]; then
running_services_count=$((running_services_count + 1))
fi
done <<EOF
${running_services_lines}
EOF
if [ "${running_services_count}" -gt 0 ]; then
status_label="Running (${running_services_count} services)"
else
status_label="Not running"
fi
printf -v "${result_var}" "%s" "${status_label}"
return 0
}
build_stack_custom_image() {
local stack_dir="${1}"
local metadata_path=""
local env_path=""
local apps_json_path=""
local custom_image=""
local custom_tag=""
local frappe_branch=""
local frappe_path="https://github.com/frappe/frappe"
local repo_root=""
local containerfile_path=""
local apps_json_base64=""
local apps_refs_lines=""
local app_ref_line=""
local app_url=""
local app_branch=""
local git_error=""
local image_ref=""
EASY_DOCKER_BUILD_ERROR_DETAIL=""
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
apps_json_path="${stack_dir}/apps.json"
if [ ! -f "${metadata_path}" ]; then
return 11
fi
if [ ! -f "${env_path}" ]; then
return 12
fi
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
frappe_branch="$(get_stack_frappe_branch "${stack_dir}" || true)"
if [ -z "${custom_image}" ]; then
return 13
fi
if [ -z "${custom_tag}" ]; then
return 14
fi
if [ -z "${frappe_branch}" ]; then
return 15
fi
# Keep apps.json aligned with current metadata app selection before build.
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
return 16
fi
if [ ! -f "${apps_json_path}" ]; then
return 17
fi
if ! command_exists git; then
return 22
fi
apps_refs_lines="$(
awk '
match($0, /"url"[[:space:]]*:[[:space:]]*"([^"]+)"/, url_parts) &&
match($0, /"branch"[[:space:]]*:[[:space:]]*"([^"]+)"/, branch_parts) {
print url_parts[1] "|" branch_parts[1]
}
' "${apps_json_path}"
)"
if [ -z "${apps_refs_lines}" ]; then
return 23
fi
while IFS= read -r app_ref_line; do
if [ -z "${app_ref_line}" ]; then
continue
fi
app_url="${app_ref_line%%|*}"
app_branch="${app_ref_line#*|}"
if [ -z "${app_url}" ] || [ -z "${app_branch}" ]; then
continue
fi
if git_error="$(git ls-remote --exit-code --heads "${app_url}" "${app_branch}" 2>&1)"; then
:
else
# shellcheck disable=SC2034 # Read by manage flow after build_stack_custom_image returns 24.
EASY_DOCKER_BUILD_ERROR_DETAIL="$(printf '%s@%s :: %s' "${app_url}" "${app_branch}" "${git_error}")"
return 24
fi
done <<EOF
${apps_refs_lines}
EOF
if ! command_exists base64; then
return 18
fi
apps_json_base64="$(base64 "${apps_json_path}" | tr -d '\r\n')"
if [ -z "${apps_json_base64}" ]; then
return 19
fi
repo_root="$(get_easy_docker_repo_root)"
containerfile_path="${repo_root}/images/layered/Containerfile"
if [ ! -f "${containerfile_path}" ]; then
return 20
fi
image_ref="${custom_image}:${custom_tag}"
docker build \
-f "${containerfile_path}" \
--build-arg "FRAPPE_BRANCH=${frappe_branch}" \
--build-arg "FRAPPE_PATH=${frappe_path}" \
--build-arg "APPS_JSON_BASE64=${apps_json_base64}" \
-t "${image_ref}" \
"${repo_root}" || return 21
return 0
}
load_easy_docker_compose_modules

View file

@ -0,0 +1,120 @@
#!/usr/bin/env bash
build_stack_custom_image() {
local stack_dir="${1}"
local metadata_path=""
local env_path=""
local apps_json_path=""
local custom_image=""
local custom_tag=""
local frappe_branch=""
local frappe_path="https://github.com/frappe/frappe"
local repo_root=""
local containerfile_path=""
local apps_json_base64=""
local apps_refs_lines=""
local app_ref_line=""
local app_url=""
local app_branch=""
local git_error=""
local image_ref=""
EASY_DOCKER_BUILD_ERROR_DETAIL=""
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
apps_json_path="${stack_dir}/apps.json"
if [ ! -f "${metadata_path}" ]; then
return 11
fi
if [ ! -f "${env_path}" ]; then
return 12
fi
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
frappe_branch="$(get_stack_frappe_branch "${stack_dir}" || true)"
if [ -z "${custom_image}" ]; then
return 13
fi
if [ -z "${custom_tag}" ]; then
return 14
fi
if [ -z "${frappe_branch}" ]; then
return 15
fi
# Keep apps.json aligned with current metadata app selection before build.
if ! persist_stack_apps_json_from_metadata_apps "${stack_dir}"; then
return 16
fi
if [ ! -f "${apps_json_path}" ]; then
return 17
fi
if ! command_exists git; then
return 22
fi
apps_refs_lines="$(
awk '
match($0, /"url"[[:space:]]*:[[:space:]]*"([^"]+)"/, url_parts) &&
match($0, /"branch"[[:space:]]*:[[:space:]]*"([^"]+)"/, branch_parts) {
print url_parts[1] "|" branch_parts[1]
}
' "${apps_json_path}"
)"
if [ -z "${apps_refs_lines}" ]; then
return 23
fi
while IFS= read -r app_ref_line; do
if [ -z "${app_ref_line}" ]; then
continue
fi
app_url="${app_ref_line%%|*}"
app_branch="${app_ref_line#*|}"
if [ -z "${app_url}" ] || [ -z "${app_branch}" ]; then
continue
fi
if git_error="$(git ls-remote --exit-code --heads "${app_url}" "${app_branch}" 2>&1)"; then
:
else
# shellcheck disable=SC2034 # Read by manage flow after build_stack_custom_image returns 24.
EASY_DOCKER_BUILD_ERROR_DETAIL="$(printf '%s@%s :: %s' "${app_url}" "${app_branch}" "${git_error}")"
return 24
fi
done <<EOF
${apps_refs_lines}
EOF
if ! command_exists base64; then
return 18
fi
apps_json_base64="$(base64 "${apps_json_path}" | tr -d '\r\n')"
if [ -z "${apps_json_base64}" ]; then
return 19
fi
repo_root="$(get_easy_docker_repo_root)"
containerfile_path="${repo_root}/images/layered/Containerfile"
if [ ! -f "${containerfile_path}" ]; then
return 20
fi
image_ref="${custom_image}:${custom_tag}"
docker build \
-f "${containerfile_path}" \
--build-arg "FRAPPE_BRANCH=${frappe_branch}" \
--build-arg "FRAPPE_PATH=${frappe_path}" \
--build-arg "APPS_JSON_BASE64=${apps_json_base64}" \
-t "${image_ref}" \
"${repo_root}" || return 21
return 0
}

View file

@ -0,0 +1,76 @@
#!/usr/bin/env bash
render_stack_compose_from_metadata() {
local stack_dir="${1}"
local metadata_path=""
local env_path=""
local generated_compose_path=""
local generated_compose_tmp_path=""
local compose_files_lines=""
local compose_file=""
local source_compose_path=""
local env_erpnext_version=""
local fallback_erpnext_version=""
local repo_root=""
local -a compose_args=()
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
generated_compose_path="$(get_stack_generated_compose_path "${stack_dir}")"
generated_compose_tmp_path="${generated_compose_path}.tmp"
if [ ! -f "${metadata_path}" ]; then
return 1
fi
if [ ! -f "${env_path}" ]; then
return 1
fi
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
if [ -z "${env_erpnext_version}" ]; then
fallback_erpnext_version="$(get_default_erpnext_version || true)"
fi
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
if [ -z "${compose_files_lines}" ]; then
return 1
fi
repo_root="$(get_easy_docker_repo_root)"
while IFS= read -r compose_file; do
if [ -z "${compose_file}" ]; then
continue
fi
source_compose_path="${repo_root}/${compose_file}"
if [ ! -f "${source_compose_path}" ]; then
return 1
fi
compose_args+=(-f "${source_compose_path}")
done <<EOF
${compose_files_lines}
EOF
if [ "${#compose_args[@]}" -eq 0 ]; then
return 1
fi
if [ -n "${fallback_erpnext_version}" ]; then
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" config >"${generated_compose_tmp_path}"; then
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
if ! mv -- "${generated_compose_tmp_path}" "${generated_compose_path}"; then
rm -f -- "${generated_compose_tmp_path}" >/dev/null 2>&1 || true
return 1
fi
return 0
}

View file

@ -0,0 +1,242 @@
#!/usr/bin/env bash
start_stack_with_compose_from_metadata() {
local stack_dir="${1}"
local metadata_path=""
local env_path=""
local compose_files_lines=""
local compose_file=""
local source_compose_path=""
local env_erpnext_version=""
local fallback_erpnext_version=""
local configured_pull_policy=""
local runtime_pull_policy=""
local custom_image=""
local custom_tag=""
local image_ref=""
local image_inspect_error=""
local stack_topology=""
local repo_root=""
local -a compose_args=()
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata fails.
EASY_DOCKER_COMPOSE_ERROR_DETAIL=""
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
if [ ! -f "${metadata_path}" ]; then
return 31
fi
if [ ! -f "${env_path}" ]; then
return 32
fi
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
if [ -z "${stack_topology}" ]; then
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 33.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="metadata.json missing topology"
return 33
fi
case "${stack_topology}" in
"single-host") ;;
*)
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 34.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${stack_topology}"
return 34
;;
esac
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
if [ -z "${env_erpnext_version}" ]; then
fallback_erpnext_version="$(get_default_erpnext_version || true)"
fi
configured_pull_policy="$(get_env_file_key_value "${env_path}" "PULL_POLICY" || true)"
if [ -z "${configured_pull_policy}" ]; then
custom_image="$(get_env_file_key_value "${env_path}" "CUSTOM_IMAGE" || true)"
custom_tag="$(get_env_file_key_value "${env_path}" "CUSTOM_TAG" || true)"
if [ -n "${custom_image}" ] && [ -n "${custom_tag}" ]; then
image_ref="${custom_image}:${custom_tag}"
if image_inspect_error="$(docker image inspect "${image_ref}" 2>&1 >/dev/null)"; then
runtime_pull_policy="if_not_present"
else
case "${image_inspect_error}" in
*"No such image"* | *"No such object"*)
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 38.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_ref}"
return 38
;;
*)
if [ -z "${image_inspect_error}" ]; then
image_inspect_error="docker image inspect failed for ${image_ref}"
fi
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 39.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${image_inspect_error}"
return 39
;;
esac
fi
fi
fi
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
if [ -z "${compose_files_lines}" ]; then
return 35
fi
repo_root="$(get_easy_docker_repo_root)"
while IFS= read -r compose_file; do
if [ -z "${compose_file}" ]; then
continue
fi
source_compose_path="${repo_root}/${compose_file}"
if [ ! -f "${source_compose_path}" ]; then
# shellcheck disable=SC2034 # Read by manage flow after start_stack_with_compose_from_metadata returns 36.
EASY_DOCKER_COMPOSE_ERROR_DETAIL="${source_compose_path}"
return 36
fi
compose_args+=(-f "${source_compose_path}")
done <<EOF
${compose_files_lines}
EOF
if [ "${#compose_args[@]}" -eq 0 ]; then
return 35
fi
if [ -n "${fallback_erpnext_version}" ] && [ -n "${runtime_pull_policy}" ]; then
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" PULL_POLICY="${runtime_pull_policy}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
elif [ -n "${fallback_erpnext_version}" ]; then
if ! ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
elif [ -n "${runtime_pull_policy}" ]; then
if ! PULL_POLICY="${runtime_pull_policy}" docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
elif ! docker compose --env-file "${env_path}" "${compose_args[@]}" up -d; then
return 37
fi
return 0
}
get_stack_compose_runtime_status_label() {
local result_var="${1}"
local stack_dir="${2}"
local metadata_path=""
local env_path=""
local stack_topology=""
local compose_files_lines=""
local compose_file=""
local source_compose_path=""
local env_erpnext_version=""
local fallback_erpnext_version=""
local running_services_lines=""
local compose_status=0
local running_services_count=0
local repo_root=""
local status_label=""
local -a compose_args=()
metadata_path="${stack_dir}/metadata.json"
env_path="$(get_stack_env_path "${stack_dir}")"
if [ ! -f "${metadata_path}" ]; then
printf -v "${result_var}" "%s" "Unknown (metadata missing)"
return 0
fi
stack_topology="$(get_stack_topology "${stack_dir}" || true)"
if [ -z "${stack_topology}" ]; then
printf -v "${result_var}" "%s" "Unknown (topology missing)"
return 0
fi
case "${stack_topology}" in
"single-host") ;;
*)
printf -v "${result_var}" "%s" "N/A (${stack_topology})"
return 0
;;
esac
if [ ! -f "${env_path}" ]; then
printf -v "${result_var}" "%s" "Unknown (env missing)"
return 0
fi
env_erpnext_version="$(get_env_file_key_value "${env_path}" "ERPNEXT_VERSION" || true)"
if [ -z "${env_erpnext_version}" ]; then
fallback_erpnext_version="$(get_default_erpnext_version || true)"
fi
compose_files_lines="$(get_metadata_compose_files_lines "${metadata_path}" || true)"
if [ -z "${compose_files_lines}" ]; then
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
return 0
fi
repo_root="$(get_easy_docker_repo_root)"
while IFS= read -r compose_file; do
if [ -z "${compose_file}" ]; then
continue
fi
source_compose_path="${repo_root}/${compose_file}"
if [ ! -f "${source_compose_path}" ]; then
printf -v "${result_var}" "%s" "Unknown (missing file: ${compose_file})"
return 0
fi
compose_args+=(-f "${source_compose_path}")
done <<EOF
${compose_files_lines}
EOF
if [ "${#compose_args[@]}" -eq 0 ]; then
printf -v "${result_var}" "%s" "Unknown (compose files missing)"
return 0
fi
if [ -n "${fallback_erpnext_version}" ]; then
running_services_lines="$(
ERPNEXT_VERSION="${fallback_erpnext_version}" docker compose --env-file "${env_path}" "${compose_args[@]}" ps --status running --services 2>/dev/null
)"
compose_status=$?
else
running_services_lines="$(
docker compose --env-file "${env_path}" "${compose_args[@]}" ps --status running --services 2>/dev/null
)"
compose_status=$?
fi
if [ "${compose_status}" -ne 0 ]; then
printf -v "${result_var}" "%s" "Unknown (docker compose status failed)"
return 0
fi
while IFS= read -r compose_file; do
if [ -n "${compose_file}" ]; then
running_services_count=$((running_services_count + 1))
fi
done <<EOF
${running_services_lines}
EOF
if [ "${running_services_count}" -gt 0 ]; then
status_label="Running (${running_services_count} services)"
else
status_label="Not running"
fi
printf -v "${result_var}" "%s" "${status_label}"
return 0
}

View file

@ -1,425 +1,15 @@
#!/usr/bin/env bash
get_setup_display_label() {
local setup_type="${1}"
load_production_screen_modules() {
local screen_dir=""
screen_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
case "${setup_type}" in
development)
printf 'Development'
;;
production | *)
printf 'Production'
;;
esac
# shellcheck source=scripts/easy-docker/lib/ui/screens/production/setup.sh
source "${screen_dir}/production/setup.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens/production/topology.sh
source "${screen_dir}/production/topology.sh"
# shellcheck source=scripts/easy-docker/lib/ui/screens/production/manage.sh
source "${screen_dir}/production/manage.sh"
}
show_setup_menu() {
local setup_type="${1}"
local setup_label=""
local status_text=""
render_main_screen 1 >&2
setup_label="$(get_setup_display_label "${setup_type}")"
status_text="$(printf "%s stack\n\nChoose whether to create a new stack or manage an existing one." "${setup_label}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "${setup_label} stack actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Create new stack" \
"Manage existing stacks" \
"Back" \
"Exit and close easy-docker"
}
show_production_setup_menu() {
show_setup_menu "production"
}
show_development_setup_menu() {
show_setup_menu "development"
}
prompt_new_stack_name() {
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Create new stack\n\nEnter a stack name.\nType /cancel or press Ctrl+C to abort.")"
render_box_message "${status_text}" "0 2" >&2
gum input \
--header "Stack name (/cancel to abort)" \
--prompt "name> " \
--placeholder "my-production-stack"
}
show_frappe_version_profile_menu() {
local stack_name="${1}"
local options_lines="${2:-}"
local selected_label="${3:-}"
local status_text=""
local option_line=""
local -a menu_options=()
local -a gum_args=()
render_main_screen 1 >&2
status_text="$(printf "Create stack: %s\n\nSelect the Frappe branch profile from frappe.tsv.\nThis sets the stack default for branch suggestions." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
while IFS= read -r option_line; do
if [ -z "${option_line}" ]; then
continue
fi
menu_options+=("${option_line}")
done <<EOF
${options_lines}
EOF
if [ "${#menu_options[@]}" -eq 0 ]; then
return 1
fi
gum_args=(
--height 10
--header "Frappe branch profile"
--cursor.foreground 63
--selected.foreground 45
)
if [ -n "${selected_label}" ]; then
gum_args+=(--selected "${selected_label}")
fi
gum choose "${gum_args[@]}" "${menu_options[@]}" "Back"
}
show_stack_topology_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack created: %s\nDirectory: %s\n\nChoose the deployment topology.\n\n- Single-host: easiest setup on one server.\n- Split services: separate app and infra stacks for more control." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Topology" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Single-host (recommended)" \
"Split services" \
"Abort wizard to main menu"
}
show_single_host_proxy_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nSingle-host setup (step 1/3)\nChoose the proxy mode.\n\n- Traefik and nginx-proxy run inside compose.\n- Caddy is external and uses the no-proxy compose mode." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 11 \
--header "Single-host: proxy mode" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Traefik (HTTP, built-in proxy)" \
"Traefik (HTTPS + Let's Encrypt)" \
"nginx-proxy (HTTP)" \
"nginx-proxy + acme-companion (HTTPS)" \
"Caddy (external reverse proxy)" \
"No reverse proxy (direct :8080)" \
"Back to topology selection"
}
show_single_host_database_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nSingle-host setup (step 2/3)\nChoose the database service." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Single-host: database" \
--cursor.foreground 63 \
--selected.foreground 45 \
"MariaDB (recommended)" \
"PostgreSQL" \
"Back to topology selection"
}
show_single_host_redis_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nSingle-host setup (step 3/3)\nChoose whether Redis services should be included." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Single-host: redis" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Include Redis (recommended)" \
"Skip Redis (experienced users only)" \
"Back to topology selection"
}
show_custom_modular_apps_multi_select() {
local stack_dir="${1}"
local options_lines="${2:-}"
local selected_labels_csv="${3:-}"
local stack_name=""
local status_text=""
local option_line=""
local selected_label=""
local -a menu_options=()
local -a selected_labels=()
local -a gum_args=()
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nApps\nUse Space to toggle apps from apps.tsv. Press Enter to continue to branch selection per app.\nUse Ctrl+C to go back." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
while IFS= read -r option_line; do
if [ -z "${option_line}" ]; then
continue
fi
menu_options+=("${option_line}")
done <<EOF
${options_lines}
EOF
if [ "${#menu_options[@]}" -eq 0 ]; then
return 1
fi
gum_args=(
--no-limit
--height 14
--header "Apps"
--cursor.foreground 63
--selected.foreground 45
)
if [ -n "${selected_labels_csv}" ]; then
IFS=',' read -r -a selected_labels <<<"${selected_labels_csv}"
for selected_label in "${selected_labels[@]}"; do
trim_predefined_catalog_field selected_label "${selected_label}"
if [ -z "${selected_label}" ]; then
continue
fi
gum_args+=(--selected "${selected_label}")
done
fi
gum choose "${gum_args[@]}" "${menu_options[@]}"
}
prompt_single_host_env_value() {
local stack_dir="${1}"
local variable_name="${2}"
local guidance_text="${3}"
local placeholder="${4:-}"
local render_context="${5:-1}"
local input_feedback="${6:-}"
local stack_name=""
local status_text=""
if [ "${render_context}" = "1" ]; then
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
guidance_text="${guidance_text//\\n/$'\n'}"
status_text="$(printf "Stack: %s\n\nConfigure %s\n\n%s" "${stack_name}" "${variable_name}" "${guidance_text}")"
render_box_message "${status_text}" "0 2" >&2
fi
if [ -n "${input_feedback}" ]; then
gum style --foreground 214 "${input_feedback}" >&2
fi
gum input \
--header "${variable_name}" \
--prompt "value> " \
--placeholder "${placeholder}"
}
show_split_services_examples() {
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Split services examples\n\n- DB in a separate stack/project.\n- Proxy in a separate stack/project.\n- One or more app stacks referencing shared infra.")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 7 \
--header "Split services" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Use this topology" \
"Back to topology selection"
}
show_abort_wizard_prompt() {
local stack_dir="${1}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Abort wizard\n\nStack directory:\n%s\n\nRollback created files before returning to main menu?" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Abort options" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Rollback files and return to main menu" \
"Keep files and return to main menu" \
"Back to topology selection"
}
show_manage_stacks_menu() {
local setup_type="${1}"
shift
local stack_count="${#}"
local setup_label=""
local status_text=""
render_main_screen 1 >&2
setup_label="$(get_setup_display_label "${setup_type}")"
if [ "${stack_count}" -eq 1 ]; then
status_text="$(printf "Manage existing %s stacks\n\n1 stack found. Select a stack." "${setup_label}")"
else
status_text="$(printf "Manage existing %s stacks\n\n%s stacks found. Select a stack." "${setup_label}" "${stack_count}")"
fi
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 14 \
--header "Existing stacks" \
--cursor.foreground 63 \
--selected.foreground 45 \
"$@" \
"Back" \
"Exit and close easy-docker"
}
show_manage_stacks_placeholder() {
local setup_type="${1}"
local setup_label=""
local status_text=""
render_main_screen 1 >&2
setup_label="$(get_setup_display_label "${setup_type}")"
status_text="$(printf "Manage existing %s stacks\n\nNo stacks found in .easy-docker/stacks yet." "${setup_label}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 7 \
--header "Manage stacks actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Back" \
"Exit and close easy-docker"
}
show_manage_stack_actions_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local stack_runtime_status="${3:-Unknown}"
local menu_header=""
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage stack\n\nStack: %s\nDirectory: %s\nRuntime status: %s\n\nChoose an action for this stack." "${stack_name}" "${stack_dir}" "${stack_runtime_status}")"
render_box_message "${status_text}" "0 2" >&2
menu_header="$(printf "Stack actions | %s" "${stack_runtime_status}")"
gum choose \
--height 8 \
--header "${menu_header}" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Apps" \
"Docker" \
"Start stack in Docker Compose" \
"Back" \
"Exit and close easy-docker"
}
show_manage_stack_apps_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage stack apps\n\nStack: %s\nDirectory: %s\n\nChoose an app-related action for this stack." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Stack apps actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Regenerate apps.json from metadata" \
"Select apps and branches" \
"Back" \
"Exit and close easy-docker"
}
show_manage_stack_docker_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage stack docker\n\nStack: %s\nDirectory: %s\n\nChoose a docker-related action for this stack." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Stack docker actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Build custom image" \
"Generate docker compose from env" \
"Back" \
"Exit and close easy-docker"
}
load_production_screen_modules

View file

@ -0,0 +1,142 @@
#!/usr/bin/env bash
show_manage_stacks_menu() {
local setup_type="${1}"
shift
local stack_count="${#}"
local setup_label=""
local status_text=""
render_main_screen 1 >&2
setup_label="$(get_setup_display_label "${setup_type}")"
if [ "${stack_count}" -eq 1 ]; then
status_text="$(printf "Manage existing %s stacks\n\n1 stack found. Select a stack." "${setup_label}")"
else
status_text="$(printf "Manage existing %s stacks\n\n%s stacks found. Select a stack." "${setup_label}" "${stack_count}")"
fi
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 14 \
--header "Existing stacks" \
--cursor.foreground 63 \
--selected.foreground 45 \
"$@" \
"Back" \
"Exit and close easy-docker"
}
show_manage_stacks_placeholder() {
local setup_type="${1}"
local setup_label=""
local status_text=""
render_main_screen 1 >&2
setup_label="$(get_setup_display_label "${setup_type}")"
status_text="$(printf "Manage existing %s stacks\n\nNo stacks found in .easy-docker/stacks yet." "${setup_label}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 7 \
--header "Manage stacks actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Back" \
"Exit and close easy-docker"
}
show_manage_stack_actions_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local stack_runtime_status="${3:-Unknown}"
local menu_header=""
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage stack\n\nStack: %s\nDirectory: %s\nRuntime status: %s\n\nChoose an action for this stack." "${stack_name}" "${stack_dir}" "${stack_runtime_status}")"
render_box_message "${status_text}" "0 2" >&2
menu_header="$(printf "Stack actions | %s" "${stack_runtime_status}")"
gum choose \
--height 8 \
--header "${menu_header}" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Apps" \
"Docker" \
"Start stack in Docker Compose" \
"Back" \
"Exit and close easy-docker"
}
show_manage_stack_apps_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage stack apps\n\nStack: %s\nDirectory: %s\n\nChoose an app-related action for this stack." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Stack apps actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Regenerate apps.json from metadata" \
"Select apps and branches" \
"Back" \
"Exit and close easy-docker"
}
show_manage_stack_docker_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Manage stack docker\n\nStack: %s\nDirectory: %s\n\nChoose a docker-related action for this stack." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Stack docker actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Build custom image" \
"Generate docker compose from env" \
"Back" \
"Exit and close easy-docker"
}
show_missing_custom_image_start_menu() {
local stack_name="${1}"
local stack_dir="${2}"
local image_ref="${3}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Custom image missing\n\nStack: %s\nDirectory: %s\nImage: %s\n\nBuild the custom image now before starting the stack?" "${stack_name}" "${stack_dir}" "${image_ref}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Missing custom image" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Build custom image now" \
"Back" \
"Exit and close easy-docker"
}

View file

@ -0,0 +1,100 @@
#!/usr/bin/env bash
get_setup_display_label() {
local setup_type="${1}"
case "${setup_type}" in
development)
printf 'Development'
;;
production | *)
printf 'Production'
;;
esac
}
show_setup_menu() {
local setup_type="${1}"
local setup_label=""
local status_text=""
render_main_screen 1 >&2
setup_label="$(get_setup_display_label "${setup_type}")"
status_text="$(printf "%s stack\n\nChoose whether to create a new stack or manage an existing one." "${setup_label}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "${setup_label} stack actions" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Create new stack" \
"Manage existing stacks" \
"Back" \
"Exit and close easy-docker"
}
show_production_setup_menu() {
show_setup_menu "production"
}
show_development_setup_menu() {
show_setup_menu "development"
}
prompt_new_stack_name() {
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Create new stack\n\nEnter a stack name.\nType /cancel or press Ctrl+C to abort.")"
render_box_message "${status_text}" "0 2" >&2
gum input \
--header "Stack name (/cancel to abort)" \
--prompt "name> " \
--placeholder "my-production-stack"
}
show_frappe_version_profile_menu() {
local stack_name="${1}"
local options_lines="${2:-}"
local selected_label="${3:-}"
local status_text=""
local option_line=""
local -a menu_options=()
local -a gum_args=()
render_main_screen 1 >&2
status_text="$(printf "Create stack: %s\n\nSelect the Frappe branch profile from frappe.tsv.\nThis sets the stack default for branch suggestions." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
while IFS= read -r option_line; do
if [ -z "${option_line}" ]; then
continue
fi
menu_options+=("${option_line}")
done <<EOF
${options_lines}
EOF
if [ "${#menu_options[@]}" -eq 0 ]; then
return 1
fi
gum_args=(
--height 10
--header "Frappe branch profile"
--cursor.foreground 63
--selected.foreground 45
)
if [ -n "${selected_label}" ]; then
gum_args+=(--selected "${selected_label}")
fi
gum choose "${gum_args[@]}" "${menu_options[@]}" "Back"
}

View file

@ -0,0 +1,206 @@
#!/usr/bin/env bash
show_stack_topology_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack created: %s\nDirectory: %s\n\nChoose the deployment topology.\n\n- Single-host: easiest setup on one server.\n- Split services: separate app and infra stacks for more control." "${stack_name}" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Topology" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Single-host (recommended)" \
"Split services" \
"Abort wizard to main menu"
}
show_single_host_proxy_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nSingle-host setup (step 1/3)\nChoose the proxy mode.\n\n- Traefik and nginx-proxy run inside compose.\n- Caddy is external and uses the no-proxy compose mode." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 11 \
--header "Single-host: proxy mode" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Traefik (HTTP, built-in proxy)" \
"Traefik (HTTPS + Let's Encrypt)" \
"nginx-proxy (HTTP)" \
"nginx-proxy + acme-companion (HTTPS)" \
"Caddy (external reverse proxy)" \
"No reverse proxy (direct :8080)" \
"Back to topology selection"
}
show_single_host_database_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nSingle-host setup (step 2/3)\nChoose the database service." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Single-host: database" \
--cursor.foreground 63 \
--selected.foreground 45 \
"MariaDB (recommended)" \
"PostgreSQL" \
"Back to topology selection"
}
show_single_host_redis_menu() {
local stack_dir="${1}"
local stack_name=""
local status_text=""
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nSingle-host setup (step 3/3)\nChoose whether Redis services should be included." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Single-host: redis" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Include Redis (recommended)" \
"Skip Redis (experienced users only)" \
"Back to topology selection"
}
show_custom_modular_apps_multi_select() {
local stack_dir="${1}"
local options_lines="${2:-}"
local selected_labels_csv="${3:-}"
local stack_name=""
local status_text=""
local option_line=""
local selected_label=""
local -a menu_options=()
local -a selected_labels=()
local -a gum_args=()
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
status_text="$(printf "Stack: %s\n\nApps\nUse Space to toggle apps from apps.tsv. Press Enter to continue to branch selection per app.\nUse Ctrl+C to go back." "${stack_name}")"
render_box_message "${status_text}" "0 2" >&2
while IFS= read -r option_line; do
if [ -z "${option_line}" ]; then
continue
fi
menu_options+=("${option_line}")
done <<EOF
${options_lines}
EOF
if [ "${#menu_options[@]}" -eq 0 ]; then
return 1
fi
gum_args=(
--no-limit
--height 14
--header "Apps"
--cursor.foreground 63
--selected.foreground 45
)
if [ -n "${selected_labels_csv}" ]; then
IFS=',' read -r -a selected_labels <<<"${selected_labels_csv}"
for selected_label in "${selected_labels[@]}"; do
trim_predefined_catalog_field selected_label "${selected_label}"
if [ -z "${selected_label}" ]; then
continue
fi
gum_args+=(--selected "${selected_label}")
done
fi
gum choose "${gum_args[@]}" "${menu_options[@]}"
}
prompt_single_host_env_value() {
local stack_dir="${1}"
local variable_name="${2}"
local guidance_text="${3}"
local placeholder="${4:-}"
local render_context="${5:-1}"
local input_feedback="${6:-}"
local stack_name=""
local status_text=""
if [ "${render_context}" = "1" ]; then
render_main_screen 1 >&2
stack_name="${stack_dir##*/}"
guidance_text="${guidance_text//\\n/$'\n'}"
status_text="$(printf "Stack: %s\n\nConfigure %s\n\n%s" "${stack_name}" "${variable_name}" "${guidance_text}")"
render_box_message "${status_text}" "0 2" >&2
fi
if [ -n "${input_feedback}" ]; then
gum style --foreground 214 "${input_feedback}" >&2
fi
gum input \
--header "${variable_name}" \
--prompt "value> " \
--placeholder "${placeholder}"
}
show_split_services_examples() {
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Split services examples\n\n- DB in a separate stack/project.\n- Proxy in a separate stack/project.\n- One or more app stacks referencing shared infra.")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 7 \
--header "Split services" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Use this topology" \
"Back to topology selection"
}
show_abort_wizard_prompt() {
local stack_dir="${1}"
local status_text=""
render_main_screen 1 >&2
status_text="$(printf "Abort wizard\n\nStack directory:\n%s\n\nRollback created files before returning to main menu?" "${stack_dir}")"
render_box_message "${status_text}" "0 2" >&2
gum choose \
--height 8 \
--header "Abort options" \
--cursor.foreground 63 \
--selected.foreground 45 \
"Rollback files and return to main menu" \
"Keep files and return to main menu" \
"Back to topology selection"
}