#!/bin/sh
# tazlab - SliTaz development lab for foreign hosts (Debian, Ubuntu...)
#
# Copyright (C) 2026 SliTaz GNU/Linux - BSD License
# Author: Christophe Lincoln <pankso@slitaz.org>
#

VERSION="0.4"

# --- UI & Colors ---
esc="$(printf '\033')"
C_RED="${esc}[1;31m"
C_GREEN="${esc}[1;32m"
C_YELLOW="${esc}[1;33m"
C_BLUE="${esc}[1;34m"
C_CYAN="${esc}[1;36m"
C_NONE="${esc}[0m"

msg()     { echo "${C_BLUE}::${C_NONE} $*"; }
success() { echo " ${C_GREEN}✓${C_NONE} $*"; }
warn()    { echo " ${C_YELLOW}⚠${C_NONE} $*" >&2; }
die()     { echo " ${C_RED}☠${C_NONE} $*" >&2; exit 1; }

# Resolve the calling user when running as root, so state lives in their home
# (~/.slitaz) rather than /root/.slitaz. need_root() captures the invoking user
# in TAZLAB_USER/TAZLAB_HOME before elevating — essential on SliTaz, where sudo
# is a `su -c` wrapper that drops SUDO_USER and resets HOME to /root. Fall back
# to SUDO_USER (real sudo) then logname, and to /etc/passwd if getent is absent.
if [ "$(id -u)" -eq 0 ]; then
	[ -n "$TAZLAB_USER" ] && : ${SUDO_USER:=$TAZLAB_USER}
	: ${SUDO_USER:=$(logname 2>/dev/null)}
	if [ -n "$TAZLAB_HOME" ]; then
		HOME="$TAZLAB_HOME"
	elif [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != root ]; then
		REAL_HOME=$(getent passwd "$SUDO_USER" 2>/dev/null | cut -d: -f6)
		[ -n "$REAL_HOME" ] || REAL_HOME=$(awk -F: -v u="$SUDO_USER" \
			'$1==u {print $6; exit}' /etc/passwd)
		[ -n "$REAL_HOME" ] && HOME="$REAL_HOME"
	fi
fi

# Load config: /etc, then user, then cwd (last wins).
[ -r /etc/slitaz/tazlab.conf ]    && . /etc/slitaz/tazlab.conf
[ -r "$HOME/.slitaz/tazlab.conf" ] && . "$HOME/.slitaz/tazlab.conf"
[ -r ./tazlab.conf ]              && . ./tazlab.conf

: ${SLITAZ_HOME:=$HOME/.slitaz}
: ${ARCH:=x86_64}
: ${ISO_URL_i486:=http://mirror.slitaz.org/iso/rolling/slitaz-rolling.iso}
: ${ISO_URL_x86_64:=https://lab.slitaz.org/x86_64/iso/slitaz-base-x86_64-cooking.iso}

validate_arch() {
	case "$1" in
		i486|x86_64) return 0 ;;
		*) return 1 ;;
	esac
}

# Recognizes built-in archs AND custom environments that have an
# existing chroot (so 'tazlab enter mylab' works after setup).
is_env() {
	[ -n "${1:-}" ] || return 0
	validate_arch "$1" && return 0
	[ -d "$SLITAZ_HOME/$1/chroot/bin" ] && return 0
	return 1
}

# Validates a new environment name for 'setup'. No path separators,
# no '.', no '..'. Empty is OK (means "use default arch").
check_name() {
	[ -n "${1:-}" ] || return 0
	case "${1:-}" in
		*/*|.|..) die "invalid environment name: $1" ;;
	esac
}

# Detect architecture from command-line args (tazlab <cmd> <arch> [rest...]).
# Do not shift yet: need_root() re-executes with full $@.
if [ -n "${2:-}" ] && is_env "$2"; then
	ARCH="$2"
fi

# Resolve per-arch ISO_URL (hyphens in env names become underscores in var names).
_arch_var=$(printf '%s' "$ARCH" | tr '-' '_')
ISO_URL=$(eval echo "\$ISO_URL_${_arch_var}")
: ${ISO_URL:=$ISO_URL_x86_64}

# Default unprivileged user for `enter-user` / `setup-user`. Falls back to
# $SUDO_USER (the host user that invoked sudo), then $USER. Override in
# tazlab.conf with BUILDER="someone".
: ${BUILDER:=${SUDO_USER:-$USER}}

# HG repos to clone into ~/.slitaz/repos/ (space-separated). Wok is always
# cloned by default.
: ${REPOS_LIST:=cookutils slitaz-base-files slitaz-boot-scripts}
: ${HG_BASE:=https://hg.slitaz.org}

# Where cooked .tazpkg live on the host, per source arch. %ARCH% is replaced
# with the source arch (the --pkgs= value, else the env arch). Default keeps
# the per-env layout ~/.slitaz/<arch>/packages. On a cooker/build host (e.g.
# marmite) set: PACKAGES_DIR="/home/slitaz/cooker/%ARCH%/cooking/packages".
: ${PACKAGES_DIR:=$SLITAZ_HOME/%ARCH%/packages}

# Network mirror set as the chroot's tazpkg mirror on `setup --build`, so
# `tazpkg recharge && get-install` work out of the box without copying packages
# locally. %ARCH% is replaced with the packages arch. `tazlab sync` overrides
# it with the local dir; devs can add a local extra mirror (tazpkg add-undigest).
: ${COOKING_MIRROR:=http://mirror.slitaz.org/%ARCH%/cooking/packages/}

# Qemu defaults (overridable in tazlab.conf).
: ${QEMU_MEM:=512M}
: ${QEMU_SMP:=2}
: ${QEMU_ARGS:=-net nic -net user}

# Use a UTS namespace so the in-chroot `hostname slitaz` doesn't rename the
# host. SliTaz's busybox may lack the unshare applet — then we skip it.
command -v unshare >/dev/null 2>&1 && UNSHARE="unshare -u" || UNSHARE=""

CHROOT="$SLITAZ_HOME/$ARCH/chroot"
PACKAGES="$SLITAZ_HOME/$ARCH/packages"
CACHE="$SLITAZ_HOME/$ARCH/cache"
LOG="$SLITAZ_HOME/$ARCH/log"
WOK="$SLITAZ_HOME/$ARCH/wok"
DISTRO="$SLITAZ_HOME/$ARCH/distro"
# Shared between architectures
SRC="$SLITAZ_HOME/src"
REPOS="$SLITAZ_HOME/repos"
ISO="$SLITAZ_HOME/iso"

# Re-derive per-arch paths after ARCH changes (new custom envs bypass is_env).
recalc_paths() {
	CHROOT="$SLITAZ_HOME/$ARCH/chroot"
	PACKAGES="$SLITAZ_HOME/$ARCH/packages"
	CACHE="$SLITAZ_HOME/$ARCH/cache"
	LOG="$SLITAZ_HOME/$ARCH/log"
	WOK="$SLITAZ_HOME/$ARCH/wok"
	DISTRO="$SLITAZ_HOME/$ARCH/distro"
}

# --- Build from packages (setup --build) ----------------------------

FLAVOR_base="slitaz-base-files busybox glibc-base gcc-lib-base gcc-lib-math \
    ncurses ncurses-common libtinfo readline zlib linux-api-headers \
    lzlib lzma liblzma xz gettext-base spk tazpkg cookutils cacerts"
FLAVOR_slitaz_ai="$FLAVOR_base \
    llama.cpp llama.cpp-tools llama3pure"

# Resolve the host packages dir for a source arch, expanding %ARCH% in
# PACKAGES_DIR. Lets a cooker host point at e.g. cooker/<arch>/cooking/packages
# instead of the default per-env ~/.slitaz/<arch>/packages.
_pkg_src_dir() {
	printf '%s\n' "${PACKAGES_DIR%/}" | sed "s|%ARCH%|$1|g"
}

_find_tazpkg() {
	local name="$1" src="$2" f pkg_name
	for f in "$src/${name}"-*.tazpkg; do
		[ -f "$f" ] || continue
		pkg_name=$(cpio --quiet -i --to-stdout receipt < "$f" 2>/dev/null \
			| grep '^PACKAGE=' | head -1 | sed 's/^PACKAGE=//;s/"//g')
		[ "$pkg_name" = "$name" ] && { echo "$f"; return 0; }
	done
}

# Read one KEY="value" field from a tazpkg receipt without executing it.
# Joins backslash-continued lines and strips surrounding quotes. Safe against
# receipt bashisms that would break `. receipt` under /bin/sh.
_receipt_var() {
	awk -v k="$2" '
		index($0, k "=") == 1 {
			v = substr($0, length(k) + 2)
			while (v ~ /\\$/) { sub(/\\$/, "", v); if (getline n <= 0) break; v = v " " n }
			gsub(/^"|"$/, "", v)
			print v; exit
		}' "$1"
}

_install_tazpkg() {
	local pkg_file="$1" dest="$2" name tmp
	name=$(basename "$pkg_file" .tazpkg)
	tmp=$(mktemp -d) || die "mktemp failed"
	( cd "$tmp" && cpio --quiet -idm < "$pkg_file" 2>/dev/null )
	if [ -f "$tmp/fs.tar.xz" ]; then
		tar -xJf "$tmp/fs.tar.xz" -C "$dest" 2>/dev/null \
			|| { rm -rf "$tmp"; die "tar extract failed: $name"; }
	elif [ -f "$tmp/fs.cpio.lzma" ]; then
		unlzma -c "$tmp/fs.cpio.lzma" | ( cd "$tmp" && cpio --quiet -idm 2>/dev/null )
		[ -d "$tmp/fs" ] || { rm -rf "$tmp"; die "cpio: no fs/ dir in: $name"; }
		# --remove-destination: a later package may ship a regular file where an
		# earlier one left a symlink (e.g. cookutils /bin/uname over busybox's
		# /bin/uname -> busybox). Without this, cp follows the symlink and
		# overwrites the *target* (/bin/busybox), corrupting the chroot.
		cp -a --remove-destination "$tmp/fs/." "$dest/"
	else
		rm -rf "$tmp"; die "unknown fs format in: $name"
	fi
	# Register the package in the chroot's tazpkg DB so spk/tazpkg inside the
	# chroot sees it as installed (deps resolve, `tazpkg list` works, recook ok).
	if [ -f "$tmp/receipt" ]; then
		local pkg ver cat short web tags sizes deps psum db idir
		# Read receipt fields without *sourcing* it: receipts use bashisms
		# (e.g. ${TARBALL:0:1}) that abort under /bin/sh=dash, yielding empty
		# values. awk joins backslash-continued lines and strips the quotes.
		pkg=$(_receipt_var "$tmp/receipt" PACKAGE)
		ver=$(_receipt_var "$tmp/receipt" VERSION)$(_receipt_var "$tmp/receipt" EXTRAVERSION)
		cat=$(_receipt_var "$tmp/receipt" CATEGORY)
		short=$(_receipt_var "$tmp/receipt" SHORT_DESC)
		web=$(_receipt_var "$tmp/receipt" WEB_SITE)
		tags=$(_receipt_var "$tmp/receipt" TAGS)
		sizes=$(echo "$(_receipt_var "$tmp/receipt" PACKED_SIZE) $(_receipt_var "$tmp/receipt" UNPACKED_SIZE)" | sed 's|\.0||g')
		deps=$(_receipt_var "$tmp/receipt" DEPENDS | tr -s ' ')
		psum=$(md5sum "$pkg_file" | cut -d' ' -f1)
		db="$dest/var/lib/tazpkg"
		idir="$db/installed/$pkg"
		mkdir -p "$idir"
		for fl in receipt files.list md5sum description.txt; do
			[ -f "$tmp/$fl" ] && cp -a "$tmp/$fl" "$idir/$fl"
		done
		# Tab-separated index line; replace any stale entry for this package.
		[ -f "$db/installed.info" ] && sed -i "/^$pkg	/d" "$db/installed.info"
		printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
			"$pkg" "$ver" "$cat" "$short" "$web" "$tags" "$sizes" "$deps" "$psum" \
			>> "$db/installed.info"
		sed -i "/ $(basename "$pkg_file")\$/d" "$db/installed.md5" 2>/dev/null
		printf '%s  %s\n' "$psum" "$(basename "$pkg_file")" >> "$db/installed.md5"
	fi
	rm -rf "$tmp"
}

_cmd_setup_build() {
	local pkg_arch="$1" flavor="$2"
	local pkg_src=$(_pkg_src_dir "$pkg_arch")
	local pkgs missing="" name f count=0
	case "$flavor" in
		base)      pkgs="$FLAVOR_base" ;;
		slitaz-ai) pkgs="$FLAVOR_slitaz_ai" ;;
		*) die "unknown flavor: $flavor (base, slitaz-ai)" ;;
	esac
	[ -d "$pkg_src" ] || die "packages dir not found: $pkg_src"
	chroot_in_use && die "a process is running in $CHROOT — exit it first"
	if mount | grep -q " $CHROOT[/ ]"; then
		warn "mounts found under $CHROOT — unmounting"
		for m in home/slitaz/iso home/slitaz/distro home/slitaz/repos home/slitaz/src \
		          home/slitaz/log home/slitaz/cache home/slitaz/packages \
		          home/slitaz/wok dev/shm dev/pts dev sys proc; do
			mountpoint -q "$CHROOT/$m" 2>/dev/null && umount "$CHROOT/$m"
		done
		mount | grep -q " $CHROOT[/ ]" && die "could not unmount $CHROOT"
	fi
	for name in $pkgs; do
		f=$(_find_tazpkg "$name" "$pkg_src")
		[ -n "$f" ] || missing="$missing $name"
	done
	[ -n "$missing" ] && { warn "missing:$missing"; die "cook missing packages first"; }
	[ -d "$CHROOT" ] && { msg "Wiping: $CHROOT"; rm -rf "$CHROOT"; }
	mkdir -p "$CHROOT" "$SLITAZ_HOME/$ARCH/wok" "$SLITAZ_HOME/$ARCH/packages" \
		"$SLITAZ_HOME/$ARCH/cache" "$SLITAZ_HOME/$ARCH/log" \
		"$SLITAZ_HOME/$ARCH/distro" "$SRC" "$REPOS"
	msg "Building chroot '$ARCH'  flavor: $flavor  pkgs from: $pkg_arch"
	echo ""
	for name in $pkgs; do
		f=$(_find_tazpkg "$name" "$pkg_src")
		printf " ${C_CYAN}→${C_NONE} %-30s" "$name "
		_install_tazpkg "$f" "$CHROOT"
		echo "${C_GREEN}ok${C_NONE}"
		count=$((count + 1))
	done
	echo ""
	[ -e "$CHROOT/lib64" ] || ln -sf lib "$CHROOT/lib64"
	chroot "$CHROOT" /bin/busybox --install -s 2>/dev/null \
		|| warn "busybox --install failed"
	# slitaz-base-files ships slitaz.conf with SLITAZ_ARCH="i486". In an
	# x86_64 chroot that leaves tazpkg looking on the i486 mirror and finding
	# nothing — pin it to the arch of the packages we just installed.
	if [ -f "$CHROOT/etc/slitaz/slitaz.conf" ]; then
		sed -i "s|^SLITAZ_ARCH=.*|SLITAZ_ARCH=\"$pkg_arch\"|" \
			"$CHROOT/etc/slitaz/slitaz.conf"
	fi
	# Point tazpkg at the network cooking mirror for this arch, so recharge/
	# get-install work without a local copy. `tazlab sync` overrides this with
	# the bind-mounted local dir when an offline mirror is wanted instead.
	mkdir -p "$CHROOT/var/lib/tazpkg"
	printf '%s\n' "${COOKING_MIRROR%/}/" | sed "s|%ARCH%|$pkg_arch|g" \
		> "$CHROOT/var/lib/tazpkg/mirror"
	rm -f "$CHROOT/var/lib/tazpkg/IDs" "$CHROOT/var/lib/tazpkg/ID"
	# cook builds each package inside an aufs/overlay jail whose lowerdir is the
	# chroot's '/'. Our wok and shared build dirs are bind mounts, not part of
	# that '/' filesystem — neither overlayfs nor the jail's non-recursive
	# `mount -o bind /home` carries them in, so the jailed cook sees an EMPTY wok
	# and dies with 'Unable to find package "<pkg>" in the wok' even though the
	# receipt is right there. List our binds in AUFS_MOUNTS so the jail re-binds
	# each one. Thanks to Claudinei Pereira (@claudineiaga) for hitting and
	# tracking this down.
	if [ -f "$CHROOT/etc/slitaz/cook.conf" ] &&
	   ! grep -q '^AUFS_MOUNTS=.*home/slitaz/wok' "$CHROOT/etc/slitaz/cook.conf"; then
		cat >> "$CHROOT/etc/slitaz/cook.conf" <<'COOKCONF'

# tazlab: the wok and shared build dirs are bind mounts, not part of the '/'
# overlay lowerdir, so list them here or cook's aufs jail sees an empty wok.
AUFS_MOUNTS="$AUFS_MOUNTS /home/slitaz/wok /home/slitaz/cache /home/slitaz/log /home/slitaz/repos /home/slitaz/distro"
COOKCONF
	fi
	echo "slitaz" > "$CHROOT/etc/hostname"
	_populate_dev "$CHROOT"
	mkdir -p "$CHROOT/tmp" && chmod 1777 "$CHROOT/tmp"
	# root lands in /root on `tazlab enter`; give it the same welcome .profile.
	mkdir -p "$CHROOT/root"
	cat > "$CHROOT/root/.profile" <<PROFILE
echo ""
echo "Welcome to SliTaz Chroot - Env: $ARCH"
echo ""
export SHELL=/bin/ash
PROFILE
	if [ -n "$SUDO_USER" ]; then
		mkdir -p "$CHROOT/home/$SUDO_USER"
		cat > "$CHROOT/home/$SUDO_USER/.profile" <<PROFILE
echo ""
echo "Welcome to SliTaz Chroot - Env: $ARCH"
echo ""
export SHELL=/bin/ash
PROFILE
		chown "$SUDO_USER": "$SLITAZ_HOME/$ARCH" \
			"$SLITAZ_HOME/$ARCH/packages" "$SLITAZ_HOME/$ARCH/cache" \
			"$SLITAZ_HOME/$ARCH/log" "$CHROOT/home/$SUDO_USER" \
			"$CHROOT/home/$SUDO_USER/.profile" 2>/dev/null
	fi
	# Remember which packages dir this chroot was built from, so `tazlab sync`
	# can default its source without re-passing --pkgs=.
	echo "$pkg_arch" > "$SLITAZ_HOME/$ARCH/pkgsrc"
	[ -n "$SUDO_USER" ] && chown "$SUDO_USER": "$SLITAZ_HOME/$ARCH/pkgsrc" 2>/dev/null
	success "$count packages installed into $CHROOT"

	# Cooker chroot (carries cookiso)? Re-apply the build-host tazlito/flavors
	# wiring so it survives this rebuild — otherwise every regen loses the
	# tazlito.conf paths and tazlito itself. cmd_mount brings the shared repos/
	# bind-mount (and resolv.conf) up first, so `cookiso setup` finds the shared
	# flavors instead of cloning a stray copy; then restore the unmounted state.
	if [ -x "$CHROOT/usr/bin/cookiso" ]; then
		msg "Wiring build-host config (cookiso setup)..."
		cmd_mount
		in_chroot 'cookiso setup' \
			|| warn "cookiso setup failed — run it manually via: tazlab enter $ARCH"
		cmd_umount
	fi

	msg "Next: sudo tazlab setup-user $ARCH"
	msg "Optional: sudo tazlab sync $ARCH  (mirror all '$pkg_arch' packages into the chroot)"
}

cmd_flavors() {
	echo "Flavors (tazlab setup [env] --build [flavor]):"
	echo ""
	printf "  ${C_CYAN}base${C_NONE}      minimal: init libc shell ncurses zlib\n"
	for p in $FLAVOR_base; do printf "      %s\n" "$p"; done
	echo ""
	printf "  ${C_CYAN}slitaz-ai${C_NONE} base + llama.cpp + llama3pure\n"
	for p in $FLAVOR_slitaz_ai; do
		echo "$FLAVOR_base" | tr ' ' '\n' | grep -qx "$p" && continue
		printf "      %s\n" "$p"
	done
}

usage() {
	cat <<EOF
Usage: tazlab <command> [env] [args]

  Environment: i486, x86_64, or a custom name (e.g. 'mydev').
  Default from config: $ARCH. Examples:
    tazlab setup i486        tazlab enter x86_64
    tazlab setup mylab       tazlab enter mylab

Commands set up a per-env chroot, wok, packages, cache and logs under
$SLITAZ_HOME/<env>/. Repos, src and ISOs are shared across envs.

Commands:
    setup [env]     Download SliTaz ISO and extract rootfs to chroot
    setup-user [e][u]Create an unprivileged user in the chroot (default: $BUILDER)
    enter [env]     Mount and enter the chroot as root
    enter-user [e][u]Mount and enter the chroot as an unprivileged user
    umount [env]    Unmount the chroot
    cook [env] <p>  Cook a package inside the chroot (root)
    run [env] <cmd> Run an arbitrary command inside the chroot
    update-chroot [e]Update all packages inside the chroot (tazpkg upgrade)
    sync [env][src] rsync <src>/packages into <env>/packages + set tazpkg mirror
                    (src defaults to the arch used by setup --build)
    nuke [env]      Wipe chroot+packages+cache+log for an environment (wok kept)
    status [env]    Show chroot, wok, packages, repos status

  Repos:
    clone [env]     Clone wok + extra repos (REPOS_LIST)
    pull [env]      hg pull -u on wok(s) and shared repos
                    (no env: all per-env woks; with env: that wok only)
    repos           Show status of each cloned repo
    add-repo <url>  Add an extra HG repo to REPOS_LIST in tazlab.conf

  Qemu:
    qemu [env] [iso]Run SliTaz ISO in Qemu (-iso <url> to download first)

  Inspect:
    log <pkg>        Show build log (or tail -f if building)
    list [filter]    List package recipes in the wok
    search <pat>     Search for a pattern in all receipts
    info <pkg>       Show package receipt (version, deps, desc...)
    edit <pkg>       Open a package receipt in \$EDITOR
    deps <pkg>       Show build and runtime dependencies of a package

  Maintenance:
    check            Verify all host dependencies are installed
    config           Show effective configuration (all variables resolved)
    init             Interactive first-time setup wizard
    clean            Clean cache and build logs

Commands that touch the chroot or mounts auto-elevate via sudo.
Builder user (current default): $BUILDER

Config: /etc/slitaz/tazlab.conf, ~/.slitaz/tazlab.conf, ./tazlab.conf
Active arch: $ARCH  Chroot: $CHROOT

tazlab $VERSION - SliTaz GNU/Linux
EOF
}

need_root() {
	[ "$(id -u)" -eq 0 ] && return 0
	command -v sudo >/dev/null 2>&1 \
		|| die "this command needs root and sudo is not installed"
	# Capture the real user/home before elevating so the root process can put
	# state under their home. SliTaz's sudo is a `su -c` wrapper that drops
	# SUDO_USER and resets HOME, but busybox su preserves other env vars.
	TAZLAB_USER="${TAZLAB_USER:-$(id -un)}"
	TAZLAB_HOME="${TAZLAB_HOME:-$HOME}"
	export TAZLAB_USER TAZLAB_HOME
	exec sudo "$0" "$@"
}

is_mounted() {
	mountpoint -q "$CHROOT/$1" 2>/dev/null
}

# True when at least one process has $CHROOT as its root (another session).
chroot_in_use() {
	for p in /proc/[0-9]*/root; do
		[ "$(readlink "$p" 2>/dev/null)" = "$CHROOT" ] && return 0
	done
	return 1
}

# Decompress to stdout, auto-detecting format from magic bytes.
# SliTaz keeps the .gz extension for legacy reasons but actually uses lzma.
# stderr is dropped: initramfs streams often have trailing padding that
# triggers cosmetic "corrupted data" warnings; cpio + the post-extract
# sanity check are the real correctness gates.
decomp() {
	magic=$(od -An -N6 -tx1 "$1" 2>/dev/null | tr -d ' \n')
	case "$magic" in
		1f8b*)        zcat       "$1" 2>/dev/null ;;
		fd377a585a00) xzcat      "$1" 2>/dev/null ;;
		5d00*)        unlzma -c  "$1" 2>/dev/null ;;
		*)            cat        "$1" ;;
	esac
}

# Read a little-endian unsigned integer from a file: _le <file> <offset> <bytes>.
# Pure shell (dd | od + $(()) hex) so it works under busybox on a SliTaz host.
_le() {
	_hx=$(dd if="$1" bs=1 skip="$2" count="$3" 2>/dev/null | od -An -tx1 | tr -d ' \n')
	_v=0
	while [ ${#_hx} -ge 2 ]; do
		_byte=${_hx#${_hx%??}}; _hx=${_hx%??}
		_v=$(( _v*256 + 0x$_byte ))
	done
	echo "$_v"
}

# Echo "<offset-bytes> <size-bytes>" of the EFI System Partition in GPT disk
# image $1, or return 1. Newer UEFI-only SliTaz ISOs hide boot/rootfs.gz inside
# this FAT partition instead of the iso9660 tree.
_efi_part() {
	[ "$(dd if="$1" bs=1 skip=512 count=8 2>/dev/null)" = "EFI PART" ] || return 1
	_pe=$(_le "$1" 584 8); _esz=$(_le "$1" 596 4); _num=$(_le "$1" 592 4)
	[ "$_num" -gt 256 ] && _num=256
	_i=0
	while [ "$_i" -lt "$_num" ]; do
		_b=$(( _pe*512 + _i*_esz ))
		_guid=$(dd if="$1" bs=1 skip="$_b" count=16 2>/dev/null | od -An -tx1 | tr -d ' \n')
		if [ "$_guid" = "28732ac11ff8d211ba4b00a0c93ec93b" ]; then
			_s=$(_le "$1" $((_b+32)) 8); _e=$(_le "$1" $((_b+40)) 8)
			echo "$((_s*512)) $(((_e-_s+1)*512))"; return 0
		fi
		_i=$((_i+1))
	done
	return 1
}

# Echo the rootfs layer(s) under a mounted ISO/EFI dir $1, sorted. SliTaz uses a
# single boot/rootfs.gz, or layered boot/rootfs[1-4].gz.
_find_layers() {
	if [ -f "$1/boot/rootfs.gz" ]; then
		echo "$1/boot/rootfs.gz"
	else
		ls "$1"/boot/rootfs?.gz 2>/dev/null | sort
	fi
}

# Tear down the temp ISO + EFI-partition mounts/files created during setup.
_setup_iso_cleanup() {
	[ -n "$efimnt" ] && mountpoint -q "$efimnt" 2>/dev/null && umount "$efimnt"
	[ -n "$efimnt" ] && [ -d "$efimnt" ] && rmdir "$efimnt"
	[ -n "$efiimg" ] && rm -f "$efiimg"
	[ -n "$mnt" ] && mountpoint -q "$mnt" 2>/dev/null && umount "$mnt"
	[ -n "$mnt" ] && [ -d "$mnt" ] && rmdir "$mnt"
}

# --- Commands ---------------------------------------------------------

cmd_setup() {
	local _build="" _sync="" _pkg_arch="$ARCH" _flavor="base" _a
	for _a in "$@"; do
		case "$_a" in
			--build)  _build=1 ;;
			--sync)   _sync=1 ;;
			--pkgs=*) _pkg_arch="${_a#--pkgs=}" ;;
			--*)      die "unknown setup option: $_a" ;;
			*)        _flavor="$_a" ;;
		esac
	done
	[ -n "$_sync" ] && [ -z "$_build" ] && die "--sync requires --build (or use: tazlab sync $ARCH)"
	[ -n "$_build" ] && {
		_cmd_setup_build "$_pkg_arch" "$_flavor"
		[ -n "$_sync" ] && cmd_sync "$_pkg_arch"
		return
	}

	[ -d "$CHROOT/bin" ] && die "chroot already set up at: $CHROOT"
	_arch_var=$(printf '%s' "$ARCH" | tr '-' '_')
	ISO_URL=$(eval echo "\$ISO_URL_${_arch_var}")
	case "$ARCH" in
		i486|x86_64) : ${ISO_URL:=$ISO_URL_x86_64} ;;
		*) [ -n "$ISO_URL" ] || die "no ISO_URL_${_arch_var} configured. Add to ~/.slitaz/tazlab.conf: ISO_URL_${_arch_var}=\"...\"" ;;
	esac
	mkdir -p "$SLITAZ_HOME/$ARCH" "$CHROOT" "$WOK" "$PACKAGES" \
		"$CACHE" "$LOG" "$SRC" "$REPOS" "$DISTRO" "$SLITAZ_HOME/iso"
	msg "Environment: $ARCH"
	iso="$SLITAZ_HOME/iso/$(basename "$ISO_URL")"
	if [ ! -s "$iso" ]; then
		[ -f "$iso" ] && msg "Cached ISO is empty, re-downloading."
		msg "Downloading ISO: $ISO_URL"
		wget -O "$iso" "$ISO_URL" || die "ISO download failed"
	else
		msg "Using cached ISO: $iso"
	fi
	mnt=$(mktemp -d) || die "mktemp failed"
	efiimg=""; efimnt=""
	mount -o loop,ro "$iso" "$mnt" || { rmdir "$mnt"; die "ISO mount failed"; }
	# Classic SliTaz ISO: boot/rootfs.gz (or layered rootfs[1-4].gz) lives in the
	# iso9660 tree. Newer UEFI-only ISOs hide it inside the FAT EFI System
	# Partition, invisible to this mount — carve that partition out and mount it.
	layers=$(_find_layers "$mnt")
	if [ -z "$layers" ]; then
		geom=$(_efi_part "$iso") \
			|| { _setup_iso_cleanup; die "no rootfs found in ISO"; }
		msg "rootfs not in iso9660 tree; reading EFI System Partition..."
		efiimg=$(mktemp) || { _setup_iso_cleanup; die "mktemp failed"; }
		dd if="$iso" of="$efiimg" bs=512 skip=$(( ${geom% *} / 512 )) \
			count=$(( ${geom#* } / 512 )) 2>/dev/null \
			|| { _setup_iso_cleanup; die "EFI partition read failed"; }
		efimnt=$(mktemp -d) || { _setup_iso_cleanup; die "mktemp failed"; }
		mount -o loop,ro -t vfat "$efiimg" "$efimnt" \
			|| { _setup_iso_cleanup; die "EFI partition mount failed"; }
		layers=$(_find_layers "$efimnt")
	fi
	[ -n "$layers" ] || { _setup_iso_cleanup; die "no rootfs found in ISO"; }
	msg "Extracting rootfs to: $CHROOT"
	for l in $layers; do echo "  layer: $l ($(stat -c%s "$l") bytes)"; done
	# On extraction failure, wipe the partial chroot so the next 'setup' run
	# isn't blocked by the "already set up" guard.
	for l in $layers; do
		if ! decomp "$l" | ( cd "$CHROOT" && cpio -idmu --quiet ); then
			_setup_iso_cleanup
			rm -rf "$CHROOT"
			die "rootfs extraction failed (chroot wiped, retry: tazlab setup $ARCH)"
		fi
	done
	_setup_iso_cleanup
	if [ ! -x "$CHROOT/bin/sh" ]; then
		rm -rf "$CHROOT"
		die "extracted chroot looks broken: no /bin/sh (chroot wiped)"
	fi
	echo slitaz > "$CHROOT/etc/hostname"
	# Give the chroot a private /dev (static nodes + ptmx->pts/ptmx symlink) so
	# it's complete before the first enter; cmd_mount adds the fresh devpts.
	_populate_dev "$CHROOT"
	# root lands in /root on `tazlab enter`; give it the same welcome .profile.
	mkdir -p "$CHROOT/root"
	cat > "$CHROOT/root/.profile" <<EOF
echo ""
echo "Welcome to SliTaz Chroot - Env: $ARCH"
echo ""
export SHELL=/bin/ash
EOF
	# Create sudo user's home inside the chroot so 'cd' works out of the
	# box, and provide a .profile with a welcome message.
	if [ -n "$SUDO_USER" ]; then
		mkdir -p "$CHROOT/home/$SUDO_USER"
		cat > "$CHROOT/home/$SUDO_USER/.profile" <<EOF
echo ""
echo "Welcome to SliTaz Chroot - Env: $ARCH"
echo ""
export SHELL=/bin/ash
EOF
		chown "$SUDO_USER": "$CHROOT/home/$SUDO_USER" \
			"$CHROOT/home/$SUDO_USER/.profile" 2>/dev/null
	fi
	# Make host-shared dirs owned by the calling user so recipes can be
	# edited without sudo. Chroot itself stays root-owned.
	if [ -n "$SUDO_USER" ]; then
		chown "$SUDO_USER": "$SLITAZ_HOME" "$SLITAZ_HOME/$ARCH" \
			"$PACKAGES" "$CACHE" "$LOG" \
			"$WOK" "$DISTRO" "$SRC" "$REPOS" "$SLITAZ_HOME/iso" 2>/dev/null
	fi
	success "Done. Run: sudo tazlab enter $ARCH"
}

# Populate a private /dev in the chroot: static nodes + symlinks + the pts/shm
# mountpoints. /dev/ptmx is a symlink to pts/ptmx so opening it routes to this
# chroot's own devpts instance (mounted with newinstance in cmd_mount), giving
# each chroot a separate /dev/pts. Idempotent.
_populate_dev() {
	local d="$1/dev"
	mkdir -p "$d/pts" "$d/shm"
	[ -c "$d/null" ]    || mknod -m 666 "$d/null"    c 1 3
	[ -c "$d/zero" ]    || mknod -m 666 "$d/zero"    c 1 5
	[ -c "$d/full" ]    || mknod -m 666 "$d/full"    c 1 7
	[ -c "$d/random" ]  || mknod -m 444 "$d/random"  c 1 8
	[ -c "$d/urandom" ] || mknod -m 444 "$d/urandom" c 1 9
	[ -c "$d/tty" ]     || mknod -m 666 "$d/tty"     c 5 0
	[ -c "$d/console" ] || mknod -m 600 "$d/console" c 5 1
	# ptmx MUST be a symlink to pts/ptmx (not the c5:2 node), else opens go to
	# the inherited/shared devpts and the per-chroot isolation is lost.
	[ -L "$d/ptmx" ] || { rm -f "$d/ptmx"; ln -s pts/ptmx "$d/ptmx"; }
	# -n: if the link already exists as a symlink to a dir (e.g. fd from the
	# extracted rootfs), replace it instead of creating the link *inside* it.
	ln -sfn /proc/self/fd   "$d/fd"
	ln -sfn /proc/self/fd/0 "$d/stdin"
	ln -sfn /proc/self/fd/1 "$d/stdout"
	ln -sfn /proc/self/fd/2 "$d/stderr"
}

cmd_mount() {
	[ -d "$CHROOT/bin" ] || die "chroot not set up. Run: tazlab setup $ARCH"
	is_mounted proc    || mount -t proc  proc "$CHROOT/proc"
	is_mounted sys     || mount -t sysfs sys  "$CHROOT/sys"
	_populate_dev "$CHROOT"
	is_mounted dev/pts || mount -t devpts -o newinstance,ptmxmode=666,gid=5,mode=620 \
		devpts "$CHROOT/dev/pts"
	is_mounted dev/shm || mount -t tmpfs tmpfs "$CHROOT/dev/shm"
	# Bind shared dirs so builds persist on the host.
	mkdir -p "$REPOS" "$DISTRO" "$ISO"
	if [ -n "$SUDO_USER" ]; then
		chown "$SUDO_USER": "$REPOS" "$DISTRO" 2>/dev/null
	fi
	mkdir -p "$CHROOT/home/slitaz/wok" \
		"$CHROOT/home/slitaz/packages" \
		"$CHROOT/home/slitaz/cache" \
		"$CHROOT/home/slitaz/log" \
		"$CHROOT/home/slitaz/src" \
		"$CHROOT/home/slitaz/repos" \
		"$CHROOT/home/slitaz/distro" \
		"$CHROOT/home/slitaz/iso"
	is_mounted home/slitaz/wok      || mount --bind "$WOK"      "$CHROOT/home/slitaz/wok"
	is_mounted home/slitaz/packages || mount --bind "$PACKAGES" "$CHROOT/home/slitaz/packages"
	is_mounted home/slitaz/cache    || mount --bind "$CACHE"    "$CHROOT/home/slitaz/cache"
	is_mounted home/slitaz/log      || mount --bind "$LOG"      "$CHROOT/home/slitaz/log"
	is_mounted home/slitaz/src      || mount --bind "$SRC"      "$CHROOT/home/slitaz/src"
	is_mounted home/slitaz/repos  || mount --bind "$REPOS"  "$CHROOT/home/slitaz/repos"
	is_mounted home/slitaz/distro || mount --bind "$DISTRO" "$CHROOT/home/slitaz/distro"
	is_mounted home/slitaz/iso    || mount --bind "$ISO"    "$CHROOT/home/slitaz/iso"
	cp -L /etc/resolv.conf "$CHROOT/etc/resolv.conf"
}

cmd_umount() {
	if chroot_in_use; then
		msg "Chroot still in use, keeping mounts."
		return 0
	fi
	for m in 	home/slitaz/iso home/slitaz/distro home/slitaz/repos home/slitaz/src home/slitaz/log \
		home/slitaz/cache home/slitaz/packages home/slitaz/wok \
		dev/shm dev/pts dev sys proc; do
		is_mounted "$m" && umount "$CHROOT/$m"
	done
	success "Chroot unmounted."
}

# Run a shell snippet inside the chroot. With a UTS namespace we also set the
# hostname to 'slitaz'; without unshare we drop the rename so the host keeps
# its own hostname.
in_chroot() {
	if [ -n "$UNSHARE" ]; then
		$UNSHARE chroot "$CHROOT" /bin/sh -c "hostname slitaz; $1"
	else
		chroot "$CHROOT" /bin/sh -c "$1"
	fi
}

cmd_enter() {
	cmd_mount
	in_chroot 'HOME=/root; export HOME; cd "$HOME"; exec /bin/sh -l'
	cmd_umount
}

# Resolve a user name to a numeric UID from the *host* /etc/passwd. The chroot
# will be configured to use the same UID so bind-mounted files keep consistent
# ownership across host and chroot.
_host_uid() {
	getent passwd "$1" 2>/dev/null | cut -d: -f3
}

cmd_setup_user() {
	hint="${1:-$BUILDER}"
	[ -n "$hint" ] || die "no user specified and BUILDER is empty"
	[ -d "$CHROOT/bin" ] || die "chroot not set up. Run: tazlab setup $ARCH"
	uid=$(_host_uid "$hint")
	[ -n "$uid" ] || die "user not found on host: $hint"

	# If something already owns this UID inside the chroot, reuse it instead
	# of failing. Typical case: `cook setup` created a `slitaz` user at the
	# same UID. Bind-mounts map by UID, so files written by that user will
	# still appear as `$hint:$hint` on the host.
	existing=$(awk -F: -v u="$uid" '$3==u {print $1; exit}' "$CHROOT/etc/passwd" 2>/dev/null)

	if [ -n "$existing" ]; then
		user="$existing"
		msg "User '$user' already owns uid=$uid in chroot; reusing it."
	else
		user="$hint"
		# If GID $uid is already taken by a group (typical: `cook setup`
		# creates `slitaz` at GID 1000 without any user), reuse it as the
		# primary group instead of letting adduser try to recreate it.
		gname=$(awk -F: -v g="$uid" '$3==g {print $1; exit}' \
			"$CHROOT/etc/group" 2>/dev/null)
		if [ -n "$gname" ]; then
			msg "Creating '$user' in chroot with uid=$uid (primary group: $gname, gid=$uid)..."
			chroot "$CHROOT" /bin/sh -c \
				"adduser -u $uid -G $gname -D -H -h /home/$user -s /bin/sh $user" \
				|| die "adduser failed in chroot"
			mkdir -p "$CHROOT/home/$user"
			chroot "$CHROOT" chown "$user:$gname" "/home/$user"
		else
			msg "Creating '$user' in chroot with uid=$uid..."
			# BusyBox adduser auto-creates a primary group with the same
			# name and matching GID.
			chroot "$CHROOT" /bin/sh -c \
				"adduser -u $uid -D -h /home/$user -s /bin/sh $user" \
				|| die "adduser failed in chroot"
		fi
	fi

	# Wheel membership + passwordless sudo (chroot is a dev sandbox).
	grep -q '^wheel:' "$CHROOT/etc/group" 2>/dev/null \
		|| echo "wheel:x:10:" >> "$CHROOT/etc/group"
	if ! awk -F: -v u="$user" '$1=="wheel" {print $4}' "$CHROOT/etc/group" \
		| tr ',' '\n' | grep -qx "$user"; then
		chroot "$CHROOT" /bin/sh -c "addgroup $user wheel" \
			|| die "addgroup $user wheel failed"
	fi

	if [ -f "$CHROOT/etc/sudoers" ]; then
		if ! grep -qE '^%wheel[[:space:]]+ALL=' "$CHROOT/etc/sudoers"; then
			echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> "$CHROOT/etc/sudoers"
		fi
	else
		mkdir -p "$CHROOT/etc"
		printf 'root ALL=(ALL) ALL\n%%wheel ALL=(ALL) NOPASSWD: ALL\n' \
			> "$CHROOT/etc/sudoers"
		chmod 0440 "$CHROOT/etc/sudoers"
	fi

	# Resolve the user's actual primary group (may be `slitaz` if we reused it).
	pgid=$(awk -F: -v u="$user" '$1==u {print $4; exit}' "$CHROOT/etc/passwd")
	pgroup=$(awk -F: -v g="$pgid" '$3==g {print $1; exit}' "$CHROOT/etc/group")
	: "${pgroup:=$user}"

	mkdir -p "$CHROOT/home/$user"
	chroot "$CHROOT" chown -R "$user:$pgroup" "/home/$user"
	success "Done. Enter as this user with: tazlab enter-user $user"
}

cmd_enter_user() {
	user="$1"
	if [ -z "$user" ]; then
		# Auto-resolve: find the chroot user whose UID matches $BUILDER on
		# the host (so `cook setup`'s `slitaz` is picked up automatically).
		[ -n "$BUILDER" ] || die "no user specified and BUILDER is empty"
		uid=$(_host_uid "$BUILDER")
		[ -n "$uid" ] || die "user not found on host: $BUILDER"
		user=$(awk -F: -v u="$uid" '$3==u {print $1; exit}' \
			"$CHROOT/etc/passwd" 2>/dev/null)
		[ -n "$user" ] \
			|| die "no chroot user at uid=$uid. Run: tazlab setup-user $ARCH"
	fi
	grep -q "^$user:" "$CHROOT/etc/passwd" 2>/dev/null \
		|| die "user '$user' not in chroot. Run: tazlab setup-user $ARCH $user"
	cmd_mount
	in_chroot "exec su - $user"
	cmd_umount
}

cmd_cook() {
	[ -n "$1" ] || die "usage: tazlab cook <pkg>"
	cmd_mount
	in_chroot "cook $1"
	cmd_umount
}

cmd_run() {
	[ -n "$1" ] || die "usage: tazlab run <command>"
	cmd_mount
	in_chroot "$*"
	cmd_umount
}

cmd_update_chroot() {
	cmd_mount
	msg "Updating chroot packages..."
	in_chroot "tazpkg recharge && tazpkg upgrade"
	cmd_umount
	success "Chroot updated."
}

# Host-side mirror: rsync one env's packages dir into another's, so the
# target env's chroot (which bind-mounts $PACKAGES at /home/slitaz/packages)
# has the whole set available. Pure host operation — no unmount needed, since
# we write the host dir, not the chroot rootfs. Then point tazpkg at it so
# `tazpkg recharge && tazpkg get-install <pkg>` works offline.
# Not run by setup unless --sync is passed; setup suggests it instead.
cmd_sync() {
	local src_arch="${1:-}" src dest n f
	# Default source arch: the one recorded at build time, else current ARCH.
	if [ -z "$src_arch" ]; then
		[ -r "$SLITAZ_HOME/$ARCH/pkgsrc" ] \
			&& src_arch=$(cat "$SLITAZ_HOME/$ARCH/pkgsrc")
		: ${src_arch:=$ARCH}
	fi
	src=$(_pkg_src_dir "$src_arch")
	dest="$PACKAGES"   # = $SLITAZ_HOME/$ARCH/packages
	command -v rsync >/dev/null 2>&1 || die "rsync not installed on host"
	[ -d "$src" ] || die "no packages dir: $src"
	n=$(ls "$src"/*.tazpkg 2>/dev/null | wc -l)
	[ "$n" -gt 0 ] || die "no .tazpkg in $src (cook some packages first)"
	for f in packages.list packages.info packages.md5 packages.equiv; do
		[ -f "$src/$f" ] \
			|| warn "missing index: $f (regenerate with: cook pkgdb)"
	done
	if [ "$src" = "$dest" ]; then
		# Same env: packages already in $PACKAGES, nothing to copy — just
		# (re)point tazpkg at them below.
		msg "$n packages already in $ARCH; configuring mirror only"
	else
		mkdir -p "$dest"
		msg "Syncing $n packages (host): $src_arch -> $ARCH"
		rsync -a "$src"/ "$dest"/ || die "rsync failed"
		[ -n "$SUDO_USER" ] && chown "$SUDO_USER": "$dest" 2>/dev/null
	fi
	# Point the env's chroot at its (now-mirrored) local packages dir. Plain
	# path: recharge/get-install use cp for non-http mirrors. Invalidate IDs
	# so the next recharge re-reads the list instead of seeing "up to date".
	if [ -d "$CHROOT/bin" ]; then
		mkdir -p "$CHROOT/var/lib/tazpkg"
		echo "/home/slitaz/packages/" > "$CHROOT/var/lib/tazpkg/mirror"
		rm -f "$CHROOT/var/lib/tazpkg/IDs" "$CHROOT/var/lib/tazpkg/ID"
		msg "tazpkg mirror set; inside chroot: tazpkg recharge && tazpkg get-install <pkg>"
	fi
	success "$n packages synced: $src_arch -> $ARCH packages"
}

cmd_clean() {
	rm -rf "$CACHE"/* "$LOG"/*
	touch "$CACHE/broken" "$CACHE/cooktime"
	success "Cache and logs cleaned."
}

cmd_nuke() {
	# Refuse if anything still references the chroot — rm -rf can otherwise
	# follow bind-mounts and wipe shared host dirs (wok, repos, src...).
	if chroot_in_use; then
		die "chroot in use by another session: $CHROOT (exit it first)"
	fi
	cmd_umount
	if mount | grep -q " $CHROOT[/ ]"; then
		die "mounts still present under $CHROOT, refusing to nuke"
	fi
	rm -rf "$CHROOT" "$PACKAGES" "$CACHE" "$LOG" "$DISTRO"
	msg "Env '$ARCH' wiped: $SLITAZ_HOME/$ARCH"
	msg "Wok kept (clean build files with: tazlab enter $ARCH)."
	msg "Shared dirs (repos, src, iso) kept."
	msg "Run: tazlab setup $ARCH"
}

cmd_status() {
	_env_status() {
		echo "  Environment: ${C_CYAN}$1${C_NONE}"
		if [ -d "$SLITAZ_HOME/$1/chroot/bin" ]; then
			echo "    Chroot:    $SLITAZ_HOME/$1/chroot"
			echo "    Size:      $(du -sh "$SLITAZ_HOME/$1/chroot" 2>/dev/null | cut -f1)"
			echo "    Mounted:   $(mount | grep -c " $SLITAZ_HOME/$1/chroot")"
			echo "    Wok:       $(ls "$SLITAZ_HOME/$1/wok" 2>/dev/null | wc -l) recipes"
			echo "    Packages:  $(ls "$SLITAZ_HOME/$1/packages"/*.tazpkg 2>/dev/null | wc -l)"
			echo "    Cache:     $(du -sh "$SLITAZ_HOME/$1/cache" 2>/dev/null | cut -f1)"
			echo "    Log:       $(du -sh "$SLITAZ_HOME/$1/log" 2>/dev/null | cut -f1)"
			echo "    Distro:    $(du -sh "$SLITAZ_HOME/$1/distro" 2>/dev/null | cut -f1)"
		else
			echo "    Status:    not set up (run: tazlab setup $1)"
		fi
	}
	# Built-in archs always shown.
	_env_status i486; echo
	_env_status x86_64; echo
	# Discover custom envs (any dir with a chroot that isn't i486/x86_64).
	for d in "$SLITAZ_HOME"/*/chroot/bin; do
		[ -x "$d" ] || continue
		env=$(echo "$d" | sed "s|$SLITAZ_HOME/||;s|/chroot/bin||")
		case "$env" in i486|x86_64) continue ;; esac
		echo
		_env_status "$env"
	done
	echo
	echo "  Shared:"
	echo "    ISO:       $(ls "$SLITAZ_HOME/iso"/*.iso 2>/dev/null | wc -l) files"
	echo "    Src:       $(du -sh "$SRC" 2>/dev/null | cut -f1)"
	if [ -d "$REPOS" ] && [ -n "$(ls "$REPOS" 2>/dev/null)" ]; then
		echo "    Repos:"
		for r in "$REPOS"/*; do
			[ -d "$r/.hg" ] || continue
			name=$(basename "$r")
			branch=$(hg -R "$r" branch 2>/dev/null)
			rev=$(hg -R "$r" log -l1 --template '{rev}:{node|short}' 2>/dev/null)
			echo "      $name  $branch  $rev"
		done
	else
		echo "    Repos:     none (run: tazlab clone)"
	fi
}

# --- Maintenance ------------------------------------------------------

cmd_check() {
	err=0
	missing=""
	_check() {
		command -v "$1" >/dev/null 2>&1 && return 0
		# chroot/mount live in sbin, often off a normal user's PATH; tazlab
		# runs them as root (sudo) where sbin is present, so accept them there.
		for d in /usr/sbin /sbin /usr/bin /bin; do
			[ -x "$d/$1" ] && return 0
		done
		missing="$missing $1"
		err=1
	}

	_check wget
	_check cpio
	_check mount
	_check hg
	_check unlzma
	_check zcat
	_check xzcat
	_check mountpoint
	_check stat
	_check od
	_check awk
	_check chroot
	_check sudo
	_check mktemp
	_check rsync

	# unshare/getent are optional: tazlab falls back to a plain chroot and to
	# parsing /etc/passwd when they're missing (e.g. busybox on SliTaz).
	command -v unshare >/dev/null 2>&1 \
		|| warn "unshare not found (optional; host hostname not isolated in chroot)"
	command -v getent >/dev/null 2>&1 \
		|| warn "getent not found (optional; using /etc/passwd fallback)"

	# Qemu is optional.
	command -v qemu-system-x86_64 >/dev/null 2>&1 \
		|| command -v qemu-system-i386 >/dev/null 2>&1 \
		|| warn "qemu not found (optional, needed for 'tazlab qemu')"

	if [ -n "$missing" ]; then
		warn "Missing host dependencies:$missing"
		# Distro hint.
		if [ -f /etc/os-release ]; then
			. /etc/os-release
			case "$ID" in
				debian|ubuntu)
					echo "  Install with: sudo apt install$missing" ;;
				arch|manjaro)
					echo "  Install with: sudo pacman -S$missing" ;;
				fedora)
					echo "  Install with: sudo dnf install$missing" ;;
			esac
		fi
		return 1
	fi
	success "All host dependencies found."
}

cmd_config() {
	msg "TazLab v$VERSION configuration"
	echo "ARCH        = $ARCH"
	echo "SLITAZ_HOME = $SLITAZ_HOME"
	echo "CHROOT      = $CHROOT"
	echo "ISO_URL     = $ISO_URL"
	echo "BUILDER     = $BUILDER"
	echo "WOK         = $WOK"
	echo "PACKAGES    = $PACKAGES"
	echo "PACKAGES_DIR= $PACKAGES_DIR"
	echo "COOKING_MIRROR = $COOKING_MIRROR"
	echo "CACHE       = $CACHE"
	echo "LOG         = $LOG"
	echo "SRC         = $SRC"
	echo "REPOS       = $REPOS"
	echo "REPOS_LIST  = $REPOS_LIST"
	echo "HG_BASE     = $HG_BASE"
	echo "QEMU_MEM    = $QEMU_MEM"
	echo "QEMU_SMP    = $QEMU_SMP"
	echo "QEMU_ARGS   = $QEMU_ARGS"
	echo "---"
	echo "ISO_URL_i486   = $ISO_URL_i486"
	echo "ISO_URL_x86_64 = $ISO_URL_x86_64"
	echo "---"
	msg "Config files loaded (last wins):"
	[ -r /etc/slitaz/tazlab.conf ]    && echo "  /etc/slitaz/tazlab.conf"
	[ -r "$HOME/.slitaz/tazlab.conf" ] && echo "  $HOME/.slitaz/tazlab.conf"
	[ -r ./tazlab.conf ]              && echo "  ./tazlab.conf"
}

cmd_init() {
	conf="$HOME/.slitaz/tazlab.conf"
	if [ -f "$conf" ]; then
		warn "Config already exists: $conf"
		msg "Delete it first if you want to recreate."
		exit 1
	fi

	msg "╭─ TazLab first-time setup ───────────╮"

	printf "SLITAZ_HOME [%s/.slitaz]: " "$HOME"
	read -r answer
	: "${answer:=$HOME/.slitaz}"
	SLITAZ_HOME="$answer"

	printf "Default environment [x86_64]: "
	read -r answer
	: "${answer:=x86_64}"
	ARCH="$answer"

	printf "Builder user [%s]: " "${SUDO_USER:-$USER}"
	read -r answer
	: "${answer:=${SUDO_USER:-$USER}}"
	BUILDER="$answer"

	printf "Extra repos [cookutils slitaz-base-files slitaz-boot-scripts]: "
	read -r answer
	: "${answer:=cookutils slitaz-base-files slitaz-boot-scripts}"
	REPOS_LIST="$answer"

	printf "Qemu memory [512M]: "
	read -r answer
	: "${answer:=512M}"
	QEMU_MEM="$answer"

	printf "Qemu CPU cores [2]: "
	read -r answer
	: "${answer:=2}"
	QEMU_SMP="$answer"

	mkdir -p "$HOME/.slitaz"
	cat > "$conf" <<EOF
# tazlab configuration
SLITAZ_HOME="$SLITAZ_HOME"
ARCH="$ARCH"
BUILDER="$BUILDER"
REPOS_LIST="$REPOS_LIST"
QEMU_MEM="$QEMU_MEM"
QEMU_SMP="$QEMU_SMP"
EOF
	success "Configuration saved to: $conf"
	msg "Next steps:"
	msg "  tazlab clone"
	msg "  sudo tazlab setup $ARCH"
}

# --- Repos ------------------------------------------------------------

cmd_clone() {
	wok_url="$HG_BASE/wok"
	if [ -d "$WOK/.hg" ]; then
		msg "Already cloned: wok (use 'tazlab pull $ARCH')"
	else
		mkdir -p "$(dirname "$WOK")"
		if [ -n "$SUDO_USER" ]; then
			chown "$SUDO_USER": "$(dirname "$WOK")" 2>/dev/null
		fi
		msg "Cloning: wok ($wok_url) -> $WOK"
		hg clone "$wok_url" "$WOK" || die "clone failed: wok"
	fi

	mkdir -p "$REPOS"
	if [ -n "$SUDO_USER" ]; then
		chown "$SUDO_USER": "$REPOS" 2>/dev/null
	fi

	_clone_one() {
		name=$(basename "$1" .hg)
		[ -d "$REPOS/$name/.hg" ] && {
			msg "Already cloned: $name (use 'tazlab pull')"
			return 0
		}
		msg "Cloning: $name ($1)"
		hg clone "$1" "$REPOS/$name" || die "clone failed: $name"
	}

	# Extra repos from config.
	for r in $REPOS_LIST; do
		_clone_one "$HG_BASE/$r"
	done
	success "Done. Wok in: $WOK, repos in: $REPOS"
}

cmd_pull() {
	target_env="${1:-}"
	count=0

	_pull_one() {
		[ -d "$1/.hg" ] || return 0
		# Label per-env woks as "<env>/wok" to disambiguate in output.
		case "$1" in
			"$SLITAZ_HOME"/*/wok)
				name=$(echo "$1" | sed "s|$SLITAZ_HOME/||") ;;
			*)  name=$(basename "$1") ;;
		esac
		msg "Pulling: $name"
		hg -R "$1" pull -u || warn "pull failed for $name"
		count=$((count + 1))
	}

	if [ -n "$target_env" ]; then
		_pull_one "$SLITAZ_HOME/$target_env/wok"
	else
		# All per-env woks that are hg clones (skips non-wok dirs like iso/, src/).
		for w in "$SLITAZ_HOME"/*/wok; do
			_pull_one "$w"
		done
	fi

	for r in "$REPOS"/*; do
		_pull_one "$r"
	done
	[ "$count" -gt 0 ] || die "no HG repos found"
	success "Done. $count repos pulled."
}

cmd_repos() {
	[ -d "$REPOS" ] || die "no repos directory. Run: tazlab clone"
	for r in "$REPOS"/*; do
		[ -d "$r/.hg" ] || continue
		name=$(basename "$r")
		branch=$(hg -R "$r" branch 2>/dev/null)
		rev=$(hg -R "$r" log -l1 --template '{rev}:{node|short}' 2>/dev/null)
		age=$(hg -R "$r" log -l1 --template '{date|age}' 2>/dev/null)
		echo "$name | $branch | $rev | $age"
	done
}

cmd_add_repo() {
	[ -n "$1" ] || die "usage: tazlab add-repo <url>"
	conf="$HOME/.slitaz/tazlab.conf"
	touch "$conf"
	if grep -q "^REPOS_LIST=" "$conf" 2>/dev/null; then
		if grep -q "$1" "$conf"; then
			msg "Repo already in REPOS_LIST: $1"
			return 0
		fi
		sed -i "s|^REPOS_LIST=\"|&$1 |" "$conf"
	else
		echo "REPOS_LIST=\"$1\"" >> "$conf"
	fi
	success "Added to REPOS_LIST: $1"
	msg "Run: tazlab clone"
}

# --- Qemu --------------------------------------------------------------

cmd_qemu() {
	iso="$1"
	if [ -n "$iso" ] && [ "${iso#--iso=}" != "$iso" ]; then
		url="${iso#--iso=}"
		iso="$SLITAZ_HOME/iso/$(basename "$url")"
		mkdir -p "$SLITAZ_HOME/iso"
		if [ ! -f "$iso" ]; then
			msg "Downloading ISO: $url"
			wget -O "$iso" "$url" || die "download failed"
		fi
	elif [ -z "$iso" ]; then
		_arch_var=$(printf '%s' "$ARCH" | tr '-' '_')
		ISO_URL=$(eval echo "\$ISO_URL_${_arch_var}")
		: ${ISO_URL:=$ISO_URL_x86_64}
		iso="$SLITAZ_HOME/iso/$(basename "$ISO_URL")"
		mkdir -p "$SLITAZ_HOME/iso"
		if [ ! -f "$iso" ]; then
			msg "Downloading ISO ($ARCH): $ISO_URL"
			wget -O "$iso" "$ISO_URL" || die "download failed"
		fi
	fi
	msg "Environment: $ARCH"
	msg "ISO: $iso"

	case "$ARCH" in
		x86_64)
			qemu_bin="qemu-system-x86_64"
			command -v "$qemu_bin" >/dev/null 2>&1 \
				|| qemu_bin="qemu-system-i386"
			;;
		i486)
			qemu_bin="qemu-system-i386"
			;;
		*)
			qemu_bin="qemu-system-x86_64"
			command -v "$qemu_bin" >/dev/null 2>&1 \
				|| qemu_bin="qemu-system-i386"
			;;
	esac
	command -v "$qemu_bin" >/dev/null 2>&1 \
		|| die "qemu not found (install $qemu_bin)"

	set -- -m "$QEMU_MEM" -smp "$QEMU_SMP" $QEMU_ARGS -cdrom "$iso"
	if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
		set -- -enable-kvm "$@"
	fi
	msg "Running: $qemu_bin $*"
	exec "$qemu_bin" "$@"
}

# --- Inspect -----------------------------------------------------------

# Shell wrapper: source a receipt and print its variables.
_show_receipt() {
	receipt="$1"
	shift
	[ -f "$receipt" ] || die "no receipt: $receipt"
	( . "$receipt"
	  for var in "$@"; do
		  eval "echo \"$var: \$$var\""
	  done
	)
}

cmd_log() {
	[ -n "$1" ] || die "usage: tazlab log <pkg>"
	logf="$LOG/$1.log"
	if [ -f "$logf" ]; then
		if [ -r "$CACHE/$1.lock" ] 2>/dev/null; then
			tail -f "$logf"
		else
			cat "$logf"
		fi
	else
		msg "No build log for: $1"
		msg "Run: tazlab cook $1"
	fi
}

cmd_list() {
	[ -d "$WOK" ] || die "no wok for $ARCH. Run: tazlab clone $ARCH"
	pat="${1:-}*"
	count=0
	for d in "$WOK"/$pat; do
		[ -f "$d/receipt" ] || continue
		basename "$d"
		count=$((count + 1))
	done
	if [ -n "${1:-}" ]; then
		msg "$count recipe(s) matching '$pat' in $ARCH wok"
	else
		msg "$count recipe(s) in $ARCH wok"
	fi
}

cmd_search() {
	[ -n "$1" ] || die "usage: tazlab search <pattern>"
	[ -d "$WOK" ] || die "no wok for $ARCH. Run: tazlab clone $ARCH"
	grep -rl "$1" "$WOK"/*/receipt 2>/dev/null | while read -r r; do
		pkg=$(basename "$(dirname "$r")")
		desc=$(awk -F\" '/^SHORT_DESC=/{print $2; exit}' "$r")
		echo "$pkg - $desc"
	done
}

cmd_info() {
	[ -n "$1" ] || die "usage: tazlab info <pkg>"
	receipt="$WOK/$1/receipt"
	_show_receipt "$receipt" \
		PACKAGE VERSION CATEGORY SHORT_DESC WEB_SITE MAINTAINER \
		DEPENDS BUILD_DEPENDS WANTED
}

cmd_edit() {
	[ -n "$1" ] || die "usage: tazlab edit <pkg>"
	receipt="$WOK/$1/receipt"
	[ -f "$receipt" ] || die "no receipt: $receipt"
	${EDITOR:-vi} "$receipt"
}

cmd_deps() {
	[ -n "$1" ] || die "usage: tazlab deps <pkg>"
	receipt="$WOK/$1/receipt"
	[ -f "$receipt" ] || die "no receipt: $receipt"
	(
		. "$receipt"
		msg "${PACKAGE:-$1} ${VERSION:-unknown}"
		echo "  Build deps:   ${BUILD_DEPENDS:--}"
		echo "  Runtime deps: ${DEPENDS:--}"
	)
}

# --- Dispatch ---------------------------------------------------------

# Auto-elevate for commands that touch mounts, chroot, or hardware.
case "$1" in
	setup|setup-user|enter|enter-user|umount|cook|run|update-chroot|nuke|qemu|sync)
		need_root "$@" ;;
esac

case "$1" in
	setup)
		case "${2:-}" in
			""| --*) shift ;;
			*) check_name "${2:-}" && ARCH="${2:-$ARCH}"; shift 2 ;;
		esac
		recalc_paths; cmd_setup "$@" ;;
	setup-user)    { [ -n "${2:-}" ] && is_env "$2" && ARCH="$2" && shift; } || true; shift; cmd_setup_user "$@" ;;
	enter)         is_env "${2:-}" && ARCH="${2:-$ARCH}" && shift || die "unknown env: ${2:-} (use i486, x86_64, or setup a custom env)"; cmd_enter ;;
	enter-user)    { [ -n "${2:-}" ] && is_env "$2" && ARCH="$2" && shift; } || true; shift; cmd_enter_user "$@" ;;
	umount)        is_env "${2:-}" && ARCH="${2:-$ARCH}" && shift || die "unknown env: ${2:-}"; cmd_umount ;;
	cook)          { [ -n "${2:-}" ] && is_env "$2" && ARCH="$2" && shift; } || true; shift; cmd_cook "$@" ;;
	run)           { [ -n "${2:-}" ] && is_env "$2" && ARCH="$2" && shift; } || true; shift; cmd_run "$@" ;;
	update-chroot) is_env "${2:-}" && ARCH="${2:-$ARCH}" && shift || die "unknown env: ${2:-}"; cmd_update_chroot ;;
	sync)          { [ -n "${2:-}" ] && is_env "$2" && ARCH="$2" && shift; } || true; shift; cmd_sync "$@" ;;
	nuke)          is_env "${2:-}" && ARCH="${2:-$ARCH}" && shift || die "unknown env: ${2:-}"; cmd_nuke ;;
	clone)         check_name "${2:-}" && ARCH="${2:-$ARCH}" && shift; recalc_paths; cmd_clone ;;
	pull)          shift; if [ -n "${1:-}" ]; then is_env "$1" || die "unknown env: $1"; cmd_pull "$1"; else cmd_pull; fi ;;
	repos)         cmd_repos ;;
	add-repo)      shift; cmd_add_repo "$@" ;;
	qemu)          { [ -n "${2:-}" ] && is_env "$2" && ARCH="$2" && shift; } || true; shift; cmd_qemu "$@" ;;
	log)           { [ -n "${2:-}" ] && is_env "$2" && shift; } || true; shift; cmd_log "$@" ;;
	list)          { [ -n "${2:-}" ] && is_env "$2" && shift; } || true; shift; cmd_list "$@" ;;
	search)        { [ -n "${2:-}" ] && is_env "$2" && shift; } || true; shift; cmd_search "$@" ;;
	info)          { [ -n "${2:-}" ] && is_env "$2" && shift; } || true; shift; cmd_info "$@" ;;
	edit)          { [ -n "${2:-}" ] && is_env "$2" && shift; } || true; shift; cmd_edit "$@" ;;
	deps)          { [ -n "${2:-}" ] && is_env "$2" && shift; } || true; shift; cmd_deps "$@" ;;
	flavors)       cmd_flavors ;;
	check)         cmd_check ;;
	config)        cmd_config ;;
	init)          cmd_init ;;
	clean)         if [ -n "${2:-}" ]; then is_env "$2" || die "unknown env: $2"; shift; fi; cmd_clean ;;
	status)        cmd_status ;;
	""|help|-h|--help|-v|--version) usage ;;
	*) die "unknown command: $1 (try: tazlab help)" ;;
esac
