#!/usr/bin/env bash
set -euo pipefail

DISTRO_ID="${DISTRO_ID:?DISTRO_ID is required, e.g. debian or ubuntu}"
DISTRO_VERSION="${DISTRO_VERSION:?DISTRO_VERSION is required, e.g. 13 or 24.04}"
ARCH="${ARCH:-amd64}"
PACKAGE_VERSION="${PACKAGE_VERSION:-$(date -u +%Y%m%d%H%M%S)}"

PLAYBOOKS_REPO="${PLAYBOOKS_REPO:-https://github.com/ai-workspace-infra/playbooks.git}"
PLAYBOOKS_REF="${PLAYBOOKS_REF:-main}"
CONSOLE_REPO="${CONSOLE_REPO:-https://github.com/ai-workspace-lab/xworkspace-console.git}"
CONSOLE_REF="${CONSOLE_REF:-main}"
CORE_SKILLS_REPO="${CORE_SKILLS_REPO:-https://github.com/ai-workspace-lab/xworkspace-core-skills.git}"
CORE_SKILLS_REF="${CORE_SKILLS_REF:-main}"
CONSOLE_RUNTIME_RELEASE_REPO="${CONSOLE_RUNTIME_RELEASE_REPO:-ai-workspace-lab/xworkspace-console}"
CONSOLE_RUNTIME_RELEASE_TAG="${CONSOLE_RUNTIME_RELEASE_TAG:-latest-runtime}"
BRIDGE_RUNTIME_RELEASE_REPO="${BRIDGE_RUNTIME_RELEASE_REPO:-ai-workspace-lab/xworkmate-bridge}"
BRIDGE_RUNTIME_RELEASE_TAG="${BRIDGE_RUNTIME_RELEASE_TAG:-latest-runtime}"
QMD_RUNTIME_RELEASE_REPO="${QMD_RUNTIME_RELEASE_REPO:-ai-workspace-services/qmd}"
QMD_RUNTIME_RELEASE_TAG="${QMD_RUNTIME_RELEASE_TAG:-latest-runtime}"
LITELLM_RUNTIME_RELEASE_REPO="${LITELLM_RUNTIME_RELEASE_REPO:-ai-workspace-services/litellm}"
LITELLM_RUNTIME_RELEASE_TAG="${LITELLM_RUNTIME_RELEASE_TAG:-latest-runtime}"

NODEJS_MAJOR_VERSIONS="${NODEJS_MAJOR_VERSIONS:-22 24}"
NODEJS_22_VERSION="${NODEJS_22_VERSION:-22.22.3}"
NODEJS_24_VERSION="${NODEJS_24_VERSION:-24.16.0}"
VAULT_VERSION="${VAULT_VERSION:-1.21.4}"
TTYD_VERSION="${TTYD_VERSION:-latest}"
POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:17.7}"
OPENCLAW_VERSION="${OPENCLAW_VERSION:-2026.6.6}"
PLAYWRIGHT_VERSION="${PLAYWRIGHT_VERSION:-1.60.0}"
GOOGLE_CHROME_VERSION="${GOOGLE_CHROME_VERSION:-149.0.7827.114-1}"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"

case "${ARCH}" in
  x86_64|amd64) ARCH=amd64 ;;
  aarch64|arm64) ARCH=arm64 ;;
  *) echo "Unsupported ARCH: ${ARCH}" >&2; exit 1 ;;
esac

case "${DISTRO_ID}:${DISTRO_VERSION}" in
  debian:11|debian:12|debian:13|ubuntu:22.04|ubuntu:24.04|ubuntu:26.04) ;;
  *)
    echo "Unsupported target: ${DISTRO_ID}:${DISTRO_VERSION}" >&2
    echo "Supported: debian 11/12/13 and ubuntu 22.04/24.04/26.04" >&2
    exit 1
    ;;
esac

WORKDIR="${WORKDIR:-${REPO_ROOT}/ai-workspace-all-in-one-offline-${DISTRO_ID}-${DISTRO_VERSION}-${ARCH}}"
OUT="${OUT:-${REPO_ROOT}/ai-workspace-all-in-one-offline-${DISTRO_ID}-${DISTRO_VERSION}-${ARCH}.tar.gz}"

image_for_target() {
  case "${DISTRO_ID}" in
    debian) echo "debian:${DISTRO_VERSION}" ;;
    ubuntu) echo "ubuntu:${DISTRO_VERSION}" ;;
  esac
}

safe_name() {
  tr '/:@+' '----' <<<"$1"
}

clone_repo() {
  local url=$1
  local ref=$2
  local dest=$3

  git clone --depth 1 --branch "${ref}" "${url}" "${dest}" 2>/dev/null || {
    git clone --depth 1 "${url}" "${dest}"
    git -C "${dest}" fetch --depth 1 origin "${ref}"
    git -C "${dest}" checkout FETCH_HEAD
  }
  git -C "${dest}" rev-parse HEAD
}

github_api() {
  local path=$1
  local headers=(-H "Accept: application/vnd.github+json")
  if [ -n "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]; then
    headers+=(-H "Authorization: Bearer ${GH_TOKEN:-${GITHUB_TOKEN}}")
  fi
  curl -fsSL --retry 5 --retry-all-errors "${headers[@]}" "https://api.github.com${path}"
}

resolve_runtime_tag() {
  local repo=$1
  local requested=$2
  local attempt tag=""
  if [ "${requested}" != "latest-runtime" ]; then
    printf '%s\n' "${requested}"
    return
  fi
  for attempt in $(seq 1 40); do
    tag="$(
      github_api "/repos/${repo}/releases?per_page=100" |
        jq -r '[.[] | select(.draft == false and (.tag_name | startswith("runtime-")))] | first | .tag_name // empty'
    )"
    if [ -n "${tag}" ]; then
      printf '%s\n' "${tag}"
      return
    fi
    echo "Waiting for the first runtime release from ${repo} (${attempt}/40)..." >&2
    sleep 30
  done
}

download_release_asset() {
  local repo=$1 requested_tag=$2 asset=$3 destination=$4 metadata_name=$5
  local tag release_json asset_url checksums_url expected actual
  tag="$(resolve_runtime_tag "${repo}" "${requested_tag}")"
  [ -n "${tag}" ] || { echo "No runtime release found for ${repo}" >&2; exit 1; }
  release_json="$(github_api "/repos/${repo}/releases/tags/${tag}")"
  asset_url="$(jq -r --arg name "${asset}" '.assets[] | select(.name == $name) | .browser_download_url' <<<"${release_json}")"
  checksums_url="$(jq -r '.assets[] | select(.name == "SHA256SUMS") | .browser_download_url' <<<"${release_json}")"
  [ -n "${asset_url}" ] && [ "${asset_url}" != "null" ] ||
    { echo "Release asset ${asset} is missing from ${repo}@${tag}" >&2; exit 1; }
  [ -n "${checksums_url}" ] && [ "${checksums_url}" != "null" ] ||
    { echo "SHA256SUMS is missing from ${repo}@${tag}" >&2; exit 1; }

  mkdir -p "$(dirname "${destination}")" "${WORKDIR}/metadata/components"
  curl -fsSL --retry 5 --retry-all-errors "${asset_url}" -o "${destination}"
  curl -fsSL --retry 5 --retry-all-errors "${checksums_url}" \
    -o "${WORKDIR}/metadata/components/${metadata_name}.SHA256SUMS"
  expected="$(awk -v file="${asset}" '$2 == file || $2 == "*" file { print $1; exit }' \
    "${WORKDIR}/metadata/components/${metadata_name}.SHA256SUMS")"
  actual="$(sha256sum "${destination}" | awk '{print $1}')"
  [ -n "${expected}" ] && [ "${expected}" = "${actual}" ] ||
    { echo "Checksum mismatch or missing checksum for ${repo}@${tag}/${asset}" >&2; exit 1; }
  printf '%s\n' "${tag}" > "${WORKDIR}/metadata/components/${metadata_name}.tag"
  printf '%s\n' "${actual}" > "${WORKDIR}/metadata/components/${metadata_name}.sha256"
}

download_component_releases() {
  local component_dir="${WORKDIR}/packages/components"
  local bridge_tmp="${WORKDIR}/.bridge-runtime"
  local litellm_tmp="${WORKDIR}/.litellm-runtime"
  local console_asset="xworkspace-console-runtime-linux-${ARCH}.tar.gz"
  local bridge_asset="xworkmate-bridge-linux-${ARCH}.tar.gz"
  local qmd_asset="qmd-runtime-linux-${ARCH}.tar.gz"
  local litellm_asset="litellm-runtime-${DISTRO_ID}-${DISTRO_VERSION}-${ARCH}.tar.gz"

  mkdir -p "${component_dir}" "${bridge_tmp}" "${litellm_tmp}" "${WORKDIR}/packages/bin"
  download_release_asset "${CONSOLE_RUNTIME_RELEASE_REPO}" "${CONSOLE_RUNTIME_RELEASE_TAG}" \
    "${console_asset}" "${component_dir}/${console_asset}" xworkspace-console
  download_release_asset "${BRIDGE_RUNTIME_RELEASE_REPO}" "${BRIDGE_RUNTIME_RELEASE_TAG}" \
    "${bridge_asset}" "${component_dir}/${bridge_asset}" xworkmate-bridge
  download_release_asset "${QMD_RUNTIME_RELEASE_REPO}" "${QMD_RUNTIME_RELEASE_TAG}" \
    "${qmd_asset}" "${component_dir}/${qmd_asset}" qmd
  download_release_asset "${LITELLM_RUNTIME_RELEASE_REPO}" "${LITELLM_RUNTIME_RELEASE_TAG}" \
    "${litellm_asset}" "${component_dir}/${litellm_asset}" litellm

  tar -xzf "${component_dir}/${bridge_asset}" -C "${bridge_tmp}"
  install -m 0755 "${bridge_tmp}/xworkmate-bridge/bin/xworkmate-go-core" \
    "${WORKDIR}/packages/bin/xworkmate-go-core.${ARCH}"
  tar -xzf "${component_dir}/${litellm_asset}" -C "${litellm_tmp}"
  cp -a "${litellm_tmp}/litellm-runtime/packages/pip" "${WORKDIR}/packages/"
  if [ -d "${litellm_tmp}/litellm-runtime/packages/python" ]; then
    cp -a "${litellm_tmp}/litellm-runtime/packages/python" "${WORKDIR}/packages/"
  fi
  cp "${litellm_tmp}/litellm-runtime/metadata/runtime.env" \
    "${WORKDIR}/metadata/litellm-runtime.env"
}

download_apt_packages() {
  local image=$1
  local platform="linux/${ARCH}"
  local apt_dir="${WORKDIR}/packages/apt"
  local apt_lists="${WORKDIR}/metadata/apt"
  mkdir -p "${apt_dir}" "${apt_lists}"

  docker run --rm --platform "${platform}" \
    -e DISTRO_ID="${DISTRO_ID}" \
    -e DISTRO_VERSION="${DISTRO_VERSION}" \
    -e NODEJS_MAJOR_VERSIONS="${NODEJS_MAJOR_VERSIONS}" \
    -e NODEJS_22_VERSION="${NODEJS_22_VERSION}" \
    -e NODEJS_24_VERSION="${NODEJS_24_VERSION}" \
    -e GOOGLE_CHROME_VERSION="${GOOGLE_CHROME_VERSION}" \
    -v "${apt_dir}:/offline-apt" \
    -v "${apt_lists}:/offline-meta" \
    "${image}" \
    bash -lc "$(cat <<'CONTAINER'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

retry_command() {
  local attempt
  for attempt in 1 2 3 4 5; do
    if "$@"; then
      return 0
    fi
    if [ "${attempt}" -lt 5 ]; then
      echo "Command failed (attempt ${attempt}/5), retrying: $*" >&2
      sleep $((attempt * 3))
    fi
  done
  return 1
}

cat > /etc/apt/apt.conf.d/80-ai-workspace-retries <<'EOF'
Acquire::Retries "5";
Acquire::http::Timeout "30";
Acquire::https::Timeout "30";
EOF
retry_command apt-get update -y
retry_command apt-get install -y ca-certificates curl gnupg dpkg-dev apt-transport-https

install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /etc/apt/keyrings/nodesource.asc
for major in ${NODEJS_MAJOR_VERSIONS}; do
  echo "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.com/node_${major}.x nodistro main" \
    > "/etc/apt/sources.list.d/nodesource-node${major}.list"
done
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key -o /etc/apt/keyrings/caddy-stable.asc
echo "deb [signed-by=/etc/apt/keyrings/caddy-stable.asc] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main" \
  > /etc/apt/sources.list.d/caddy-stable.list
if [ "$(dpkg --print-architecture)" = "amd64" ]; then
  curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
    | gpg --dearmor -o /etc/apt/keyrings/google-linux-signing-key.gpg
  echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-linux-signing-key.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \
    > /etc/apt/sources.list.d/google-chrome.list
fi
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg -o /etc/apt/keyrings/yarn.asc
echo "deb [signed-by=/etc/apt/keyrings/yarn.asc] https://dl.yarnpkg.com/debian/ stable main" \
  > /etc/apt/sources.list.d/yarn.list
if curl -fsSL https://download.docker.com/linux/${DISTRO_ID}/gpg -o /etc/apt/keyrings/docker.asc; then
  . /etc/os-release
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${DISTRO_ID} ${VERSION_CODENAME} stable" \
    > /etc/apt/sources.list.d/docker.list
fi

retry_command apt-get update -y

required_packages=(
  ansible git curl ca-certificates gnupg jq rsync unzip wget xdg-utils
  caddy xfce4 python3 python3-pip python3-venv python3-dev python3-setuptools
  build-essential pkg-config libpq-dev python-is-python3 pandoc nodejs yarn golang-go
  texlive-xetex texlive-latex-extra texlive-fonts-recommended
  texlive-lang-chinese latexmk fonts-noto-cjk
  fonts-noto-cjk-extra fonts-wqy-zenhei fonts-wqy-microhei
)

resolve_required_package() {
  local candidate
  for candidate in "$@"; do
    if apt-cache show "${candidate}" 2>/dev/null | awk '
      BEGIN { found = 0 }
      /^Package: / { package = $2 }
      /^Version: / { found = 1 }
      END { exit found ? 0 : 1 }
    '; then
      printf '%s\n' "${candidate}"
      return
    fi
  done
  echo "Required APT package is unavailable for this target: $*" >&2
  exit 1
}

case "$(dpkg --print-architecture)" in
  amd64)
    if apt-cache madison google-chrome-stable | awk '{print $3}' | grep -Fxq "${GOOGLE_CHROME_VERSION}"; then
      chrome_package="google-chrome-stable=${GOOGLE_CHROME_VERSION}"
    elif apt-cache show google-chrome-stable >/dev/null 2>&1; then
      echo "Required Google Chrome version is unavailable: ${GOOGLE_CHROME_VERSION}; using the repository candidate instead." >&2
      chrome_package="google-chrome-stable"
    else
      echo "Required Google Chrome package is unavailable for this target." >&2
      exit 1
    fi
    ;;
  arm64)
    browser_dependency_groups=(
      "libasound2 libasound2t64"
      "libatk-bridge2.0-0 libatk-bridge2.0-0t64"
      "libatk1.0-0 libatk1.0-0t64"
      "libatspi2.0-0 libatspi2.0-0t64"
      "libcairo2"
      "libcups2 libcups2t64"
      "libdbus-1-3"
      "libdrm2"
      "libgbm1"
      "libglib2.0-0 libglib2.0-0t64"
      "libnspr4"
      "libnss3"
      "libpango-1.0-0"
      "libx11-6"
      "libxcb1"
      "libxcomposite1"
      "libxdamage1"
      "libxext6"
      "libxfixes3"
      "libxkbcommon0"
      "libxrandr2"
    )
    : > /offline-meta/browser-deb-packages.txt
    for dependency_group in "${browser_dependency_groups[@]}"; do
      read -r -a dependency_candidates <<<"${dependency_group}"
      resolved_dependency="$(resolve_required_package "${dependency_candidates[@]}")"
      required_packages+=("${resolved_dependency}")
      printf '%s\n' "${resolved_dependency}" >> /offline-meta/browser-deb-packages.txt
    done
    ;;
esac
packages=()

for package in "${required_packages[@]}"; do
  if ! apt-cache show "${package}" >/dev/null 2>&1; then
    echo "Required APT package is unavailable for this target: ${package}" >&2
    exit 1
  fi
  packages+=("${package}")
done

if [ "$(dpkg --print-architecture)" = "amd64" ]; then
  packages+=("${chrome_package}")
fi

if apt-cache show docker-ce >/dev/null 2>&1; then
  packages+=(docker-ce docker-ce-cli containerd.io)
  for package in docker-buildx-plugin docker-compose-plugin; do
    if apt-cache show "${package}" >/dev/null 2>&1; then
      packages+=("${package}")
    fi
  done
elif apt-cache show docker.io >/dev/null 2>&1; then
  packages+=(docker.io)
  if apt-cache show docker-compose-plugin >/dev/null 2>&1; then
    packages+=(docker-compose-plugin)
  fi
fi

retry_command apt-get install --download-only -y --no-install-recommends "${packages[@]}"
retry_command apt-get download "nodejs=${NODEJS_22_VERSION}-1nodesource1"
retry_command apt-get download "nodejs=${NODEJS_24_VERSION}-1nodesource1"
cp -n ./*.deb /offline-apt/ 2>/dev/null || true
find /var/cache/apt/archives -name '*.deb' -exec cp -n {} /offline-apt/ \;
cd /offline-apt
dpkg-scanpackages --multiversion . /dev/null | gzip -9c > Packages.gz
find . -maxdepth 1 -name '*.deb' -printf '%f\n' | sort > /offline-meta/deb-files.txt

CONTAINER
)"
}

warm_npm_dependency_cache() {
  local npm_cache_dir="${WORKDIR}/packages/npm-cache"
  mkdir -p "${npm_cache_dir}"
  docker run --rm --platform "linux/${ARCH}" \
    -e OPENCLAW_VERSION="${OPENCLAW_VERSION}" \
    -e PLAYWRIGHT_VERSION="${PLAYWRIGHT_VERSION}" \
    -v "${npm_cache_dir}:/offline-cache" \
    node:24-bookworm \
    bash -lc "$(cat <<'CONTAINER'
set -euo pipefail
export npm_config_cache=/offline-cache
export npm_config_audit=false
export npm_config_fund=false

npm install --ignore-scripts --no-audit --no-fund --prefix /tmp/global \
  opencode-ai \
  @google/gemini-cli \
  @openai/codex \
  @anthropic-ai/claude-code \
  "openclaw@${OPENCLAW_VERSION}" \
  "@openclaw/codex@${OPENCLAW_VERSION}" \
  "playwright@${PLAYWRIGHT_VERSION}"
CONTAINER
)"
}

download_playwright_browser() {
  if [ "${ARCH}" != "arm64" ]; then
    return
  fi

  local browser_dir="${WORKDIR}/packages/playwright-browsers"
  mkdir -p "${browser_dir}"
  docker run --rm --platform "linux/${ARCH}" \
    -e PLAYWRIGHT_VERSION="${PLAYWRIGHT_VERSION}" \
    -e PLAYWRIGHT_BROWSERS_PATH=/offline-browsers \
    -v "${browser_dir}:/offline-browsers" \
    node:24-bookworm \
    bash -lc 'npx --yes "playwright@${PLAYWRIGHT_VERSION}" install chromium'

  if ! find "${browser_dir}" -type f \
      \( -path '*/chrome-linux/chrome' -o -path '*/chrome-linux64/chrome' \) \
      -print -quit | grep -q .; then
    echo "Bundled Playwright Chromium executable was not found for ${ARCH}" >&2
    exit 1
  fi
}

download_binaries() {
  local bin_dir="${WORKDIR}/packages/bin"
  local vault_arch="${ARCH}"
  local ttyd_arch
  mkdir -p "${bin_dir}"

  case "${ARCH}" in
    amd64) ttyd_arch=x86_64 ;;
    arm64) ttyd_arch=aarch64 ;;
  esac

  curl -fsSL \
    "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_${vault_arch}.zip" \
    -o "${bin_dir}/vault_${VAULT_VERSION}_linux_${vault_arch}.zip"

  if [ "${TTYD_VERSION}" = "latest" ]; then
    curl -fsSL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${ttyd_arch}" \
      -o "${bin_dir}/ttyd.${ttyd_arch}"
  else
    curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}" \
      -o "${bin_dir}/ttyd.${ttyd_arch}"
  fi
  chmod +x "${bin_dir}/ttyd.${ttyd_arch}"
}

export_container_images() {
  local images_dir="${WORKDIR}/packages/images"
  mkdir -p "${images_dir}"
  docker pull --platform "linux/${ARCH}" "${POSTGRES_IMAGE}"
  docker save "${POSTGRES_IMAGE}" -o "${images_dir}/$(safe_name "${POSTGRES_IMAGE}").tar"
}

write_manifest() {
  local manifest="${WORKDIR}/metadata/manifest.json"
  mkdir -p "$(dirname "${manifest}")"
  cat > "${WORKDIR}/metadata/target.env" <<EOF
DISTRO_ID=${DISTRO_ID}
DISTRO_VERSION=${DISTRO_VERSION}
ARCH=${ARCH}
EOF
  cat > "${manifest}" <<JSON
{
  "name": "ai-workspace-all-in-one-offline",
  "packageVersion": "${PACKAGE_VERSION}",
  "target": {
    "distro": "${DISTRO_ID}",
    "version": "${DISTRO_VERSION}",
    "arch": "${ARCH}"
  },
  "sources": {
    "playbooks": {"repo": "${PLAYBOOKS_REPO}", "ref": "${PLAYBOOKS_REF}"},
    "xworkspaceConsole": {"repo": "${CONSOLE_REPO}", "ref": "${CONSOLE_REF}"},
    "xworkspaceCoreSkills": {"repo": "${CORE_SKILLS_REPO}", "ref": "${CORE_SKILLS_REF}"}
  },
  "componentReleases": {
    "xworkspaceConsole": {"repo": "${CONSOLE_RUNTIME_RELEASE_REPO}", "tag": "$(cat "${WORKDIR}/metadata/components/xworkspace-console.tag")"},
    "xworkmateBridge": {"repo": "${BRIDGE_RUNTIME_RELEASE_REPO}", "tag": "$(cat "${WORKDIR}/metadata/components/xworkmate-bridge.tag")"},
    "qmd": {"repo": "${QMD_RUNTIME_RELEASE_REPO}", "tag": "$(cat "${WORKDIR}/metadata/components/qmd.tag")"},
    "litellm": {"repo": "${LITELLM_RUNTIME_RELEASE_REPO}", "tag": "$(cat "${WORKDIR}/metadata/components/litellm.tag")"}
  },
  "versions": {
    "vault": "${VAULT_VERSION}",
    "openclaw": "${OPENCLAW_VERSION}",
    "playwright": "${PLAYWRIGHT_VERSION}",
    "googleChrome": "${GOOGLE_CHROME_VERSION}",
    "postgresImage": "${POSTGRES_IMAGE}"
  }
}
JSON
}

main() {
  rm -rf "${WORKDIR}" "${OUT}"
  mkdir -p "${WORKDIR}/repos" "${WORKDIR}/scripts" "${WORKDIR}/metadata"

  local image
  image="$(image_for_target)"

  echo "Building AI Workspace offline package for ${DISTRO_ID} ${DISTRO_VERSION} ${ARCH}"
  echo "Using package workspace: ${WORKDIR}"

  clone_repo "${PLAYBOOKS_REPO}" "${PLAYBOOKS_REF}" "${WORKDIR}/repos/playbooks" > "${WORKDIR}/metadata/playbooks.commit"
  clone_repo "${CONSOLE_REPO}" "${CONSOLE_REF}" "${WORKDIR}/repos/xworkspace-console" > "${WORKDIR}/metadata/xworkspace-console.commit"
  clone_repo "${CORE_SKILLS_REPO}" "${CORE_SKILLS_REF}" "${WORKDIR}/repos/xworkspace-core-skills" > "${WORKDIR}/metadata/xworkspace-core-skills.commit"
  download_apt_packages "${image}"
  download_component_releases
  warm_npm_dependency_cache
  download_playwright_browser
  download_binaries
  export_container_images

  cp "${SCRIPT_DIR}/ai-workspace-offline-install.sh" "${WORKDIR}/scripts/"
  chmod +x "${WORKDIR}/scripts/ai-workspace-offline-install.sh"
  write_manifest

  cat > "${WORKDIR}/README.md" <<'README'
# AI Workspace All-in-One Offline Package

The standard online bootstrap can use matching packages published from
`https://github.com/ai-workspace-lab/xworkspace-console/releases` automatically:

```bash
curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | bash -
```

Extract this archive on the target host, then run:

```bash
sudo ./scripts/ai-workspace-offline-install.sh
```

The installer configures a local APT repository from `packages/apt`, loads bundled
container images when Docker is available, and runs the packaged
`xworkspace-console/scripts/setup-ai-workspace-all-in-one.sh` with local source
directories for playbooks, console, core skills, and bridge code.

Set the same environment variables supported by the online installer before
running the script, for example:

```bash
sudo env \
  XWORKMATE_BRIDGE_DOMAIN=acp-bridge.onwalk.net \
  AI_WORKSPACE_SECURITY_LEVEL=strict \
  ./scripts/ai-workspace-offline-install.sh
```
README

  tar -czf "${OUT}" -C "$(dirname "${WORKDIR}")" "$(basename "${WORKDIR}")"
  ls -lh "${OUT}"
}

main "$@"
