#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/../.." && pwd)" env_file="${repo_root}/.env.devprod" compose_file="" output_mode="json" usage() { cat < Repository root (default: auto) --env-file Env file to inspect (default: .env.devprod) --compose-file Compose file override --json JSON output (default) --text Human-readable output -h, --help Show this help USAGE } json_escape() { local input="${1:-}" input="${input//\\/\\\\}" input="${input//\"/\\\"}" input="${input//$'\n'/\\n}" input="${input//$'\r'/}" input="${input//$'\t'/\\t}" printf '%s' "${input}" } bool_json() { if [[ "${1:-0}" == "1" ]]; then printf 'true' else printf 'false' fi } run_timeout_cmd() { if command -v timeout >/dev/null 2>&1; then timeout 20 "$@" else "$@" fi } host_occ_runner() { if [[ -z "${host_occ_path:-}" || ! -f "${host_occ_path:-}" ]]; then return 1 fi if [[ -z "$(command -v php || true)" ]]; then return 1 fi if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then if command -v sudo >/dev/null 2>&1; then run_timeout_cmd sudo -u www-data php "${host_occ_path}" "$@" return $? fi if command -v runuser >/dev/null 2>&1; then run_timeout_cmd runuser -u www-data -- php "${host_occ_path}" "$@" return $? fi fi run_timeout_cmd php "${host_occ_path}" "$@" } read_kv() { local key="$1" local file="$2" local line line="$(grep -m1 -E "^${key}=" "${file}" 2>/dev/null || true)" if [[ -z "${line}" ]]; then return 1 fi printf '%s' "${line#*=}" } while [[ $# -gt 0 ]]; do case "$1" in --repo-root) repo_root="${2:-}" shift 2 ;; --repo-root=*) repo_root="${1#*=}" shift ;; --env-file) env_file="${2:-}" shift 2 ;; --env-file=*) env_file="${1#*=}" shift ;; --compose-file) compose_file="${2:-}" shift 2 ;; --compose-file=*) compose_file="${1#*=}" shift ;; --json) output_mode="json" shift ;; --text) output_mode="text" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done if [[ "${repo_root}" != /* ]]; then repo_root="$(cd "${repo_root}" 2>/dev/null && pwd)" fi if [[ "${env_file}" != /* ]]; then env_file="${repo_root}/${env_file}" fi if [[ -z "${compose_file}" ]]; then if [[ -f "${repo_root}/docker-compose.devprod.nossl.yml" ]]; then compose_file="${repo_root}/docker-compose.devprod.nossl.yml" elif [[ -f "${repo_root}/docker-compose.devprod.yml" ]]; then compose_file="${repo_root}/docker-compose.devprod.yml" else compose_file="${repo_root}/docker-compose.devprod.nossl.yml" fi fi if [[ "${compose_file}" != /* ]]; then compose_file="${repo_root}/${compose_file}" fi has_docker_cli="0" has_compose_plugin="0" docker_daemon_reachable="0" env_exists="0" compose_exists="0" compose_config_ok="0" compose_has_app_service="0" compose_occ_ok="0" compose_services_count="0" host_occ_exists="0" host_occ_status_ok="0" host_occ_path="" if command -v docker >/dev/null 2>&1; then has_docker_cli="1" if docker compose version >/dev/null 2>&1; then has_compose_plugin="1" fi if docker info >/dev/null 2>&1; then docker_daemon_reachable="1" fi fi if [[ -f "${env_file}" ]]; then env_exists="1" fi if [[ -f "${compose_file}" ]]; then compose_exists="1" fi services_list="" if [[ "${has_compose_plugin}" == "1" && "${compose_exists}" == "1" && "${env_exists}" == "1" ]]; then if services_list="$(docker compose -f "${compose_file}" --env-file "${env_file}" config --services 2>/dev/null || true)"; then if [[ -n "${services_list}" ]]; then compose_config_ok="1" compose_services_count="$(printf '%s\n' "${services_list}" | sed '/^$/d' | wc -l | awk '{print $1}')" if printf '%s\n' "${services_list}" | grep -q '^app$'; then compose_has_app_service="1" fi fi fi fi if [[ "${compose_has_app_service}" == "1" && "${docker_daemon_reachable}" == "1" ]]; then if docker compose -f "${compose_file}" --env-file "${env_file}" exec -T app php occ status >/dev/null 2>&1; then compose_occ_ok="1" fi fi occ_candidates=( "/var/www/nextcloud/occ" "/var/www/html/nextcloud/occ" "/var/www/html/occ" "/srv/www/nextcloud/occ" "/var/lib/nextcloud/occ" ) for candidate in "${occ_candidates[@]}"; do if [[ -f "${candidate}" ]]; then host_occ_exists="1" host_occ_path="${candidate}" break fi done if [[ "${host_occ_exists}" == "1" && -n "${host_occ_path}" && -x "$(command -v php || true)" ]]; then if host_occ_runner status >/dev/null 2>&1; then host_occ_status_ok="1" fi fi topology_id="unknown" topology_confidence="low" topology_reason="No known Nextcloud runtime detected." recommended_profile="manual" recommended_command="" recommended_notes=() if [[ "${compose_occ_ok}" == "1" ]]; then topology_id="repo_docker_devprod" topology_confidence="high" topology_reason="Detected Docker Compose stack with working Nextcloud OCC in app container." recommended_profile="repo-devprod-bootstrap" recommended_command="./scripts/topology/apply-topology-profile.sh --profile repo-devprod-bootstrap --repo-root \"${repo_root}\" --env-file \"${env_file}\" --compose-file \"${compose_file}\"" recommended_notes+=("Best path for this topology: run recreate + finish setup using repository scripts.") elif [[ "${compose_config_ok}" == "1" && "${has_docker_cli}" == "1" ]]; then topology_id="docker_compose_generic" topology_confidence="medium" topology_reason="Detected Docker Compose config, but Nextcloud OCC in app container is not currently healthy." recommended_profile="repo-devprod-bootstrap" recommended_command="./scripts/topology/apply-topology-profile.sh --profile repo-devprod-bootstrap --repo-root \"${repo_root}\" --env-file \"${env_file}\" --compose-file \"${compose_file}\"" recommended_notes+=("Run profile apply to attempt full container recreate and repair.") elif [[ "${host_occ_exists}" == "1" ]]; then if [[ "${host_occ_status_ok}" == "1" && "${docker_daemon_reachable}" == "1" ]]; then topology_id="host_nextcloud_plus_docker" topology_confidence="high" topology_reason="Detected host-level Nextcloud OCC with working status check and a reachable Docker daemon." recommended_notes+=("This is the primary existing-instance adoption target: keep host Nextcloud in place and add NuQloud sidecars around it.") recommended_notes+=("The host-hybrid installer now handles bootstrap, app wiring, and lifecycle helpers for this topology.") recommended_notes+=("If Talk/Office/notify_push are already configured on the host, prefer reusing them instead of replacing them.") else topology_id="host_nextcloud" topology_confidence="medium" topology_reason="Detected host-level Nextcloud OCC path outside repository Docker stack." fi recommended_profile="host-nextcloud-hybrid" recommended_command="./scripts/install-host-nextcloud-hybrid.sh --repo-root \"${repo_root}\" --occ-path \"${host_occ_path}\"" recommended_notes+=("Host-level integration now has a dedicated installer plus restart/stop/update wrappers.") fi if [[ "${output_mode}" == "text" ]]; then echo "Topology ID: ${topology_id}" echo "Confidence: ${topology_confidence}" echo "Reason: ${topology_reason}" echo echo "Capabilities:" echo " docker_cli: ${has_docker_cli}" echo " docker_compose_plugin: ${has_compose_plugin}" echo " docker_daemon_reachable: ${docker_daemon_reachable}" echo " env_file_exists: ${env_exists}" echo " compose_file_exists: ${compose_exists}" echo " compose_config_ok: ${compose_config_ok}" echo " compose_has_app_service: ${compose_has_app_service}" echo " compose_occ_ok: ${compose_occ_ok}" echo " host_occ_exists: ${host_occ_exists}" echo " host_occ_status_ok: ${host_occ_status_ok}" echo echo "Recommendation:" echo " profile: ${recommended_profile}" if [[ -n "${recommended_command}" ]]; then echo " command: ${recommended_command}" fi if [[ "${#recommended_notes[@]}" -gt 0 ]]; then echo " notes:" for note in "${recommended_notes[@]}"; do echo " - ${note}" done fi exit 0 fi notes_json="" if [[ "${#recommended_notes[@]}" -gt 0 ]]; then note_first="1" for note in "${recommended_notes[@]}"; do if [[ "${note_first}" == "1" ]]; then notes_json="${notes_json}\"$(json_escape "${note}")\"" note_first="0" else notes_json="${notes_json},\"$(json_escape "${note}")\"" fi done fi generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" printf '{\n' printf ' "ok": true,\n' printf ' "generatedAt": "%s",\n' "$(json_escape "${generated_at}")" printf ' "topology": {\n' printf ' "id": "%s",\n' "$(json_escape "${topology_id}")" printf ' "confidence": "%s",\n' "$(json_escape "${topology_confidence}")" printf ' "reason": "%s"\n' "$(json_escape "${topology_reason}")" printf ' },\n' printf ' "paths": {\n' printf ' "repoRoot": "%s",\n' "$(json_escape "${repo_root}")" printf ' "envFile": "%s",\n' "$(json_escape "${env_file}")" printf ' "composeFile": "%s",\n' "$(json_escape "${compose_file}")" printf ' "hostOccPath": "%s"\n' "$(json_escape "${host_occ_path}")" printf ' },\n' printf ' "capabilities": {\n' printf ' "dockerCli": %s,\n' "$(bool_json "${has_docker_cli}")" printf ' "dockerComposePlugin": %s,\n' "$(bool_json "${has_compose_plugin}")" printf ' "dockerDaemonReachable": %s,\n' "$(bool_json "${docker_daemon_reachable}")" printf ' "envFileExists": %s,\n' "$(bool_json "${env_exists}")" printf ' "composeFileExists": %s,\n' "$(bool_json "${compose_exists}")" printf ' "composeConfigOk": %s,\n' "$(bool_json "${compose_config_ok}")" printf ' "composeHasAppService": %s,\n' "$(bool_json "${compose_has_app_service}")" printf ' "composeOccOk": %s,\n' "$(bool_json "${compose_occ_ok}")" printf ' "composeServicesCount": %s,\n' "$(json_escape "${compose_services_count}")" printf ' "hostOccExists": %s,\n' "$(bool_json "${host_occ_exists}")" printf ' "hostOccStatusOk": %s\n' "$(bool_json "${host_occ_status_ok}")" printf ' },\n' printf ' "recommendation": {\n' printf ' "profile": "%s",\n' "$(json_escape "${recommended_profile}")" printf ' "command": "%s",\n' "$(json_escape "${recommended_command}")" printf ' "notes": [%s]\n' "${notes_json}" printf ' }\n' printf '}\n'