From 696f2c5160e73ba8342601830ab3aa7ccdc93b10 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:25:58 +0600 Subject: [PATCH 001/106] docs(plan): rebase core CLI work on main --- docs/plans/lds-core-cli-hardening-plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index f4369ce4..3143e6a6 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -2,11 +2,11 @@ ## Status -Planning branch: `graphify/integration-hardening` +Planning branch: `lds-core-cli/hardening` Repository: `infocyph/LocalDevStack` -Status: **active plan — current-state review complete; implementation not started** +Status: **active plan — synchronized with `main`; implementation not started** This file is the single active LocalDevStack development plan after completion of the Docker ecosystem/integration program. The previous `docs/plans/docker-ecosystem/*` From 51f3e0be8094306a681bd50565b26e91cc793ae5 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:31:11 +0600 Subject: [PATCH 002/106] test(cli): capture execution baseline --- tests/execution-contract.sh | 138 ++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/execution-contract.sh diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh new file mode 100644 index 00000000..07630b0d --- /dev/null +++ b/tests/execution-contract.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=tests/lib/assertions.sh +source "$ROOT/tests/lib/assertions.sh" + +# Capture the pre-refactor execution contract. Some assertions intentionally +# describe behavior that later batches will replace (forced TTY, argv flattening, +# and core container uppercasing). Change those assertions only together with the +# implementation batch that deliberately changes the contract. + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +log="$tmp/execution.log" + +run_case() { + : >"$log" + ( + set -euo pipefail + export EXECUTION_TEST_LOG="$log" + export CYAN='' YELLOW='' RED='' GREEN='' NC='' QUIET=0 VERBOSE=0 + die() { printf 'die:%s\n' "$*" >>"$EXECUTION_TEST_LOG"; return 64; } + lds_project() { printf '%s' testproject; } + effective_ai_runtime() { printf '%s' cpu; } + ai_service_for_runtime() { printf '%s' llm-ollama; } + docker_compose() { printf 'compose:' >>"$EXECUTION_TEST_LOG"; printf ' <%s>' "$@" >>"$EXECUTION_TEST_LOG"; printf '\n' >>"$EXECUTION_TEST_LOG"; } + _project_tools_container_running() { printf '%s' SERVER_TOOLS; } + + docker() { + printf 'docker:' >>"$EXECUTION_TEST_LOG" + printf ' <%s>' "$@" >>"$EXECUTION_TEST_LOG" + printf '\n' >>"$EXECUTION_TEST_LOG" + + case "${1:-} ${2:-} ${3:-}" in + "inspect -f {{.State.Running}}") + printf '%s\n' true + ;; + "inspect "*) + return 0 + ;; + esac + + if [[ "${1:-}" == exec ]]; then + case " $* " in + *" domain-which --list-domains "*) + printf '%s\n' app.local + ;; + *" domain-which --app --quiet app.local "*) + printf '%s\n' node + ;; + *" domain-which --container --quiet app.local "*) + printf '%s\n' NODE + ;; + *" domain-which --docroot --quiet app.local "*) + printf '%s\n' /srv/app/public + ;; + esac + fi + } + + # shellcheck source=lib/services.sh + source "$ROOT/lib/services.sh" + "$@" + ) +} + +case_cli_command() { + cmd_cli demo-container printf '%s %s' 'hello world' tail +} +run_case case_cli_command +assert_file_contains "$log" 'docker: ' +assert_file_contains "$log" 'docker: <-f> <{{.State.Running}}> ' +assert_file_contains "$log" 'docker: <-it> <-lc>' +assert_file_contains "$log" '' +pass "baseline: lds cli explicit command validates container, forces TTY, and flattens argv" + +case_cli_shell() { + cmd_cli demo-container +} +run_case case_cli_shell +assert_file_contains "$log" 'docker: <-it> <-lc>' +assert_file_contains "$log" 'exec bash --login' +pass "baseline: lds cli without command opens an interactive login shell" + +case_core_domain() { + cmd_core app.local +} +run_case case_core_domain +assert_file_contains "$log" 'docker: <--app> <--quiet> ' +assert_file_contains "$log" 'docker: <--container> <--quiet> ' +assert_file_contains "$log" 'docker: <-it> <-lc>' +assert_file_contains "$log" 'cd "/app"' +pass "baseline: lds core resolves a domain through server-tools and forces Node /app" + +case_core_container() { + cmd_core mixedCase-container +} +run_case case_core_container +assert_file_contains "$log" 'docker: <-it> <-lc> ' +pass "baseline: lds core raw container fallback uppercases the target" + +case_stack_exec_command() { + resolve_service() { printf '%s' php84; } + cmd_exec PHP84 php -r 'echo "ok";' +} +run_case case_stack_exec_command +assert_file_contains "$log" 'compose: <-r> ' +pass "baseline: lds stack exec/cmd_exec preserves explicit command argv through Compose" + +case_stack_exec_shell() { + resolve_service() { printf '%s' php84; } + cmd_exec PHP84 +} +run_case case_stack_exec_shell +assert_file_contains "$log" 'compose: <-lc> /dev/null 2>&1 && exec bash || exec sh>' +pass "baseline: lds stack exec/cmd_exec opens Bash-or-sh when no command is provided" + +case_tools_exec() { + cmd_tools exec printf '%s %s' 'hello world' tail +} +run_case case_tools_exec +assert_file_contains "$log" 'docker: <-it> <-lc> ' +pass "baseline: lds tools exec forces TTY and flattens command argv" + +case_tools_shell() { + docker_shell() { printf 'docker-shell:%s\n' "$1" >>"$EXECUTION_TEST_LOG"; } + cmd_tools sh +} +run_case case_tools_shell +assert_file_contains "$log" 'docker-shell:SERVER_TOOLS' +pass "baseline: lds tools sh delegates to the common docker_shell helper" + +assert_file_contains "$ROOT/lds" 'exec) cmd_exec "$@" ;;' +assert_file_contains "$ROOT/lds" 'cmd_stack "$@"' +pass "baseline: grouped stack exec and top-level dispatch remain wired" + +printf 'Execution baseline contract complete.\n' From cededbd0275ff23377e19adac3d8f3ba43834733 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:31:41 +0600 Subject: [PATCH 003/106] ci(cli): run execution baseline contract --- .github/workflows/check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1fed851b..da211102 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -41,6 +41,10 @@ jobs: - name: CLI contract run: tests/cli-contract.sh + - name: Execution baseline contract + run: tests/execution-contract.sh + + - name: Environment contract run: tests/env-contract.sh From 7493bd0ccd4e5186735cbab06409b22eef70008a Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:31:49 +0600 Subject: [PATCH 004/106] test(release): include execution baseline --- tests/release-gate.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/release-gate.sh b/tests/release-gate.sh index f339012a..72b0a59a 100755 --- a/tests/release-gate.sh +++ b/tests/release-gate.sh @@ -5,6 +5,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" "$ROOT/tests/static.sh" "$ROOT/tests/cli-contract.sh" +"$ROOT/tests/execution-contract.sh" "$ROOT/tests/env-contract.sh" "$ROOT/tests/catalog-contract.sh" "$ROOT/tests/compose-contract.sh" From e171820553479dedd59947ee64113696f9b181ca Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:32:00 +0600 Subject: [PATCH 005/106] docs(plan): close core CLI baseline batch --- docs/plans/lds-core-cli-hardening-plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index 3143e6a6..ea129db1 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -335,13 +335,13 @@ No dependency on project programming language should be introduced. ## Batch 0 — baseline and command contract -Status: **complete for planning** +Status: **complete** - [x] inventory `core`, `cli`, `stack exec`, `tools`, and top-level dispatch; - [x] identify duplicated execution responsibilities; - [x] identify argv/TTY/container-resolution gaps; - [x] define command roles and compatibility policy; -- [ ] add explicit baseline tests for current supported invocation forms before refactor. +- [x] add explicit baseline tests for current supported invocation forms before refactor. Exit criterion: current supported behavior is captured by tests. From be6de3dd4efd8e398d8d4dcb5f4e327f4de508d9 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:34:21 +0600 Subject: [PATCH 006/106] feat(cli): add shared container execution substrate --- lib/container-exec.sh | 195 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 lib/container-exec.sh diff --git a/lib/container-exec.sh b/lib/container-exec.sh new file mode 100644 index 00000000..2163fe89 --- /dev/null +++ b/lib/container-exec.sh @@ -0,0 +1,195 @@ +# shellcheck shell=bash +# Shared container target resolution and execution substrate. + +_CONTAINER_TARGET_KIND='' +_CONTAINER_TARGET_REQUESTED='' +_CONTAINER_TARGET_SERVICE='' +_CONTAINER_TARGET_ID='' +_CONTAINER_TARGET_NAME='' +declare -a _CONTAINER_EXEC_FLAGS=() + +_container_target_reset() { + _CONTAINER_TARGET_KIND='' + _CONTAINER_TARGET_REQUESTED='' + _CONTAINER_TARGET_SERVICE='' + _CONTAINER_TARGET_ID='' + _CONTAINER_TARGET_NAME='' +} + +_container_project_service_exists() { + local want="${1:-}" service + [[ -n "$want" ]] || return 1 + while IFS= read -r service; do + [[ "$service" == "$want" ]] && return 0 + done < <(docker_compose config --services 2>/dev/null || true) + return 1 +} + +_container_name_from_id() { + local id="${1:-}" name + [[ -n "$id" ]] || return 1 + name="$(docker inspect -f '{{.Name}}' "$id" 2>/dev/null || true)" + name="${name#/}" + [[ -n "$name" ]] || return 1 + printf '%s' "$name" +} + +_container_resolve_target() { + local target="${1:-}" id name service + local -a ids=() + + _container_target_reset + [[ -n "$target" ]] || { + err "Container/service target is required" + return 64 + } + _CONTAINER_TARGET_REQUESTED="$target" + + # Current-project Compose service names win over global Docker names. + if _container_project_service_exists "$target"; then + mapfile -t ids < <(docker_compose ps -a -q "$target" 2>/dev/null | awk 'NF') + if (("${#ids[@]}" == 0)); then + err "Service container is not created: $target" + return 66 + fi + if (("${#ids[@]}" > 1)); then + err "Service resolves to multiple containers: $target" + return 65 + fi + + id="${ids[0]}" + name="$(_container_name_from_id "$id" || true)" + [[ -n "$name" ]] || name="$id" + + _CONTAINER_TARGET_KIND=service + _CONTAINER_TARGET_SERVICE="$target" + _CONTAINER_TARGET_ID="$id" + _CONTAINER_TARGET_NAME="$name" + return 0 + fi + + # Otherwise preserve exact Docker container names/IDs as an explicit escape. + id="$(docker inspect -f '{{.Id}}' "$target" 2>/dev/null || true)" + [[ -n "$id" ]] || { + err "Container or current-project service not found: $target" + return 66 + } + + name="$(_container_name_from_id "$id" || true)" + [[ -n "$name" ]] || name="$target" + service="$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.service" }}' "$id" 2>/dev/null || true)" + + _CONTAINER_TARGET_KIND=container + _CONTAINER_TARGET_SERVICE="$service" + _CONTAINER_TARGET_ID="$id" + _CONTAINER_TARGET_NAME="$name" +} + +_container_require_running() { + local target="${1:-}" running + [[ -n "$target" ]] || { + err "Container target is required" + return 64 + } + + running="$(docker inspect -f '{{.State.Running}}' "$target" 2>/dev/null || true)" + [[ "$running" == true ]] || { + err "Container is not running: $target" + return 69 + } +} + +_container_stdin_is_tty() { [[ -t 0 ]]; } +_container_stdout_is_tty() { [[ -t 1 ]]; } +_container_stdin_has_data() { [[ -p /dev/stdin || -f /dev/stdin ]]; } + +_container_exec_flags() { + local mode="${1:-command}" + _CONTAINER_EXEC_FLAGS=() + + case "$mode" in + shell) + _CONTAINER_EXEC_FLAGS=(-it) + ;; + command) + if _container_stdin_is_tty && _container_stdout_is_tty; then + _CONTAINER_EXEC_FLAGS=(-it) + elif _container_stdin_is_tty || _container_stdin_has_data; then + _CONTAINER_EXEC_FLAGS=(-i) + fi + ;; + *) + err "Unknown container execution mode: $mode" + return 64 + ;; + esac +} + +_container_exec_argv() { + local target="${1:-}" workdir='' + shift || true + + if [[ "${1:-}" == --workdir ]]; then + workdir="${2:-}" + [[ -n "$workdir" ]] || { + err "--workdir requires a path" + return 64 + } + shift 2 + fi + [[ "${1:-}" == -- ]] && shift + (($# > 0)) || { + err "Container command is required" + return 64 + } + + _container_require_running "$target" || return $? + _container_exec_flags command || return $? + + local -a args=(docker exec "${_CONTAINER_EXEC_FLAGS[@]}") + [[ -n "$workdir" ]] && args+=(--workdir "$workdir") + args+=("$target" "$@") + "${args[@]}" +} + +_container_shell_name() { + local target="${1:-}" + _container_require_running "$target" || return $? + if docker exec "$target" sh -lc 'command -v bash >/dev/null 2>&1' >/dev/null 2>&1; then + printf '%s' bash + else + printf '%s' sh + fi +} + +_container_open_shell() { + local target="${1:-}" workdir='' + shift || true + + if [[ "${1:-}" == --workdir ]]; then + workdir="${2:-}" + [[ -n "$workdir" ]] || { + err "--workdir requires a path" + return 64 + } + shift 2 + fi + (($# == 0)) || { + err "Unexpected shell arguments: $*" + return 64 + } + + local shell + shell="$(_container_shell_name "$target")" || return $? + _container_exec_flags shell || return $? + + local -a args=(docker exec "${_CONTAINER_EXEC_FLAGS[@]}") + [[ -n "$workdir" ]] && args+=(--workdir "$workdir") + args+=("$target") + if [[ "$shell" == bash ]]; then + args+=(bash --login) + else + args+=(sh) + fi + "${args[@]}" +} From 5759cb4b937d75f8f6da3b01d00d55d53aec16f8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:34:43 +0600 Subject: [PATCH 007/106] refactor(cli): load shared container executor --- lds | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lds b/lds index 9b7ef6bb..9b6055c2 100755 --- a/lds +++ b/lds @@ -433,6 +433,12 @@ on_error() { # shellcheck source=lib/compose.sh source "$DIR/lib/compose.sh" +############################################################################### +# 1a.1 SHARED CONTAINER EXECUTION +############################################################################### +# shellcheck source=lib/container-exec.sh +source "$DIR/lib/container-exec.sh" + ############################################################################### # 1b. PROMPTS + DOTENV HELPERS ############################################################################### From 54986ff07da29fe6b4dd19d8423eb16218704af3 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:35:15 +0600 Subject: [PATCH 008/106] test(cli): cover shared container executor --- tests/container-exec-contract.sh | 162 +++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/container-exec-contract.sh diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh new file mode 100644 index 00000000..ebfe2c3c --- /dev/null +++ b/tests/container-exec-contract.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=tests/lib/assertions.sh +source "$ROOT/tests/lib/assertions.sh" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +log="$tmp/container-exec.log" + +err() { + printf 'err:%s\n' "$*" >>"$log" +} + +docker_compose() { + case "${1:-} ${2:-}" in + "config --services") + printf '%s\n' php84 nginx multi + return 0 + ;; + esac + + if [[ "${1:-}" == ps && "${2:-}" == -a && "${3:-}" == -q ]]; then + case "${4:-}" in + php84) printf '%s\n' cid-php84 ;; + nginx) printf '%s\n' cid-nginx ;; + multi) printf '%s\n' cid-one cid-two ;; + esac + return 0 + fi + + return 1 +} + +docker() { + if [[ "${1:-}" == inspect ]]; then + if [[ "${2:-}" == -f ]]; then + case "${2:-}|${3:-}|${4:-}" in + "-f|{{.Name}}|cid-php84") printf '%s\n' /PHP84 ;; + "-f|{{.Name}}|cid-nginx") printf '%s\n' /NGINX ;; + "-f|{{.Name}}|cid-one") printf '%s\n' /ONE ;; + "-f|{{.Name}}|cid-two") printf '%s\n' /TWO ;; + "-f|{{.Name}}|cid-custom") printf '%s\n' /MixedCaseContainer ;; + "-f|{{.Id}}|custom") printf '%s\n' cid-custom ;; + "-f|{{.Id}}|stopped") printf '%s\n' cid-stopped ;; + "-f|{{.Name}}|cid-stopped") printf '%s\n' /StoppedContainer ;; + "-f|{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-custom") printf '%s\n' external-service ;; + "-f|{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '%s\n' ;; + "-f|{{.State.Running}}|cid-php84") printf '%s\n' true ;; + "-f|{{.State.Running}}|cid-nginx") printf '%s\n' true ;; + "-f|{{.State.Running}}|cid-custom") printf '%s\n' true ;; + "-f|{{.State.Running}}|cid-stopped") printf '%s\n' false ;; + *) return 1 ;; + esac + return 0 + fi + fi + + if [[ "${1:-}" == exec ]]; then + printf 'exec:' >>"$log" + printf ' <%s>' "$@" >>"$log" + printf '\n' >>"$log" + + case " $* " in + *" cid-php84 sh -lc command -v bash >/dev/null 2>&1 "*) return 0 ;; + *" cid-nginx sh -lc command -v bash >/dev/null 2>&1 "*) return 1 ;; + *" cid-custom sh -lc command -v bash >/dev/null 2>&1 "*) return 0 ;; + esac + return 0 + fi + + return 1 +} + +# shellcheck source=lib/container-exec.sh +source "$ROOT/lib/container-exec.sh" + +_container_resolve_target php84 +[[ "$_CONTAINER_TARGET_KIND" == service ]] || fail "service target kind drifted" +[[ "$_CONTAINER_TARGET_SERVICE" == php84 ]] || fail "service name drifted" +[[ "$_CONTAINER_TARGET_ID" == cid-php84 ]] || fail "service id drifted" +[[ "$_CONTAINER_TARGET_NAME" == PHP84 ]] || fail "service container name drifted" +pass "shared resolver prefers current-project Compose services" + +_container_resolve_target custom +[[ "$_CONTAINER_TARGET_KIND" == container ]] || fail "explicit container target kind drifted" +[[ "$_CONTAINER_TARGET_ID" == cid-custom ]] || fail "explicit container id drifted" +[[ "$_CONTAINER_TARGET_NAME" == MixedCaseContainer ]] || fail "explicit container case was not preserved" +[[ "$_CONTAINER_TARGET_SERVICE" == external-service ]] || fail "explicit container service label drifted" +pass "shared resolver preserves exact explicit container targets" + +set +e +_container_resolve_target multi >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 65 ]] || fail "ambiguous service returned $rc instead of 65" +assert_file_contains "$log" 'err:Service resolves to multiple containers: multi' +pass "shared resolver rejects ambiguous Compose services" + +set +e +_container_resolve_target missing >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 66 ]] || fail "missing target returned $rc instead of 66" +assert_file_contains "$log" 'err:Container or current-project service not found: missing' +pass "shared resolver reports missing targets" + +_container_resolve_target stopped +set +e +_container_require_running "$_CONTAINER_TARGET_ID" >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 69 ]] || fail "stopped target returned $rc instead of 69" +assert_file_contains "$log" 'err:Container is not running: cid-stopped' +pass "shared execution validates running state" + +_container_stdin_is_tty() { return 0; } +_container_stdout_is_tty() { return 0; } +_container_stdin_has_data() { return 0; } +_container_exec_flags command +[[ "${_CONTAINER_EXEC_FLAGS[*]}" == "-it" ]] || fail "interactive command flags drifted: ${_CONTAINER_EXEC_FLAGS[*]}" +pass "interactive command gets stdin and TTY" + +_container_stdin_is_tty() { return 1; } +_container_stdout_is_tty() { return 1; } +_container_stdin_has_data() { return 0; } +_container_exec_flags command +[[ "${_CONTAINER_EXEC_FLAGS[*]}" == "-i" ]] || fail "piped command flags drifted: ${_CONTAINER_EXEC_FLAGS[*]}" +pass "piped command keeps stdin without forcing TTY" + +_container_stdin_is_tty() { return 1; } +_container_stdout_is_tty() { return 1; } +_container_stdin_has_data() { return 1; } +_container_exec_flags command +[[ "${#_CONTAINER_EXEC_FLAGS[@]}" -eq 0 ]] || fail "non-interactive no-input command received unnecessary flags" +pass "non-interactive command without stdin gets no TTY flags" + +_container_exec_flags shell +[[ "${_CONTAINER_EXEC_FLAGS[*]}" == "-it" ]] || fail "interactive shell flags drifted" +pass "interactive shell always receives stdin and TTY" + +# Explicit argv must remain separate Docker arguments, including spaces and shell metacharacters. +: >"$log" +_container_stdin_is_tty() { return 1; } +_container_stdout_is_tty() { return 1; } +_container_stdin_has_data() { return 1; } +_container_exec_argv cid-custom --workdir '/app path' -- printf '%s|%s' 'hello world' '$(danger)' +assert_file_contains "$log" 'exec: <--workdir> <%s|%s> <$(danger)>' +pass "shared executor preserves argv and working directory without host interpolation" + +: >"$log" +_container_open_shell cid-php84 --workdir /app +assert_file_contains "$log" 'exec: <-it> <--workdir> <--login>' +pass "shared shell helper prefers Bash and supports Docker workdir" + +: >"$log" +_container_open_shell cid-nginx +assert_file_contains "$log" 'exec: <-it> ' +pass "shared shell helper falls back to sh" + +printf 'Container execution substrate contract complete.\n' From 79932e167f99b8662c861a228d7ce4df06cb3a32 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:35:39 +0600 Subject: [PATCH 009/106] ci(cli): validate shared container executor --- .github/workflows/check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index da211102..13be8a5b 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -44,6 +44,10 @@ jobs: - name: Execution baseline contract run: tests/execution-contract.sh + - name: Container execution substrate contract + run: tests/container-exec-contract.sh + + - name: Environment contract run: tests/env-contract.sh From d1f1ad2e9c6f9ce40e1a0c3c5df06d3a04b80de2 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:35:43 +0600 Subject: [PATCH 010/106] test(release): include container executor contract --- tests/release-gate.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/release-gate.sh b/tests/release-gate.sh index 72b0a59a..1df17821 100755 --- a/tests/release-gate.sh +++ b/tests/release-gate.sh @@ -6,6 +6,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" "$ROOT/tests/static.sh" "$ROOT/tests/cli-contract.sh" "$ROOT/tests/execution-contract.sh" +"$ROOT/tests/container-exec-contract.sh" "$ROOT/tests/env-contract.sh" "$ROOT/tests/catalog-contract.sh" "$ROOT/tests/compose-contract.sh" From 46411d3bab1e281139f69799e7f7ee0faf9ca351 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:35:47 +0600 Subject: [PATCH 011/106] docs(plan): update shared executor tracker --- docs/plans/lds-core-cli-hardening-plan.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index ea129db1..32c70537 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -347,13 +347,15 @@ Exit criterion: current supported behavior is captured by tests. ## Batch 1 — shared container execution substrate -- [ ] implement current-project service/container resolution; -- [ ] implement running-container validation; -- [ ] implement adaptive `docker exec` flags; -- [ ] implement argv-preserving execution; -- [ ] implement Bash/sh interactive shell selection; -- [ ] implement optional working-directory execution; -- [ ] add unit/contract fixtures for resolver and TTY behavior. +Status: **implemented — CI validation pending** + +- [x] implement current-project service/container resolution; +- [x] implement running-container validation; +- [x] implement adaptive `docker exec` flags; +- [x] implement argv-preserving execution; +- [x] implement Bash/sh interactive shell selection; +- [x] implement optional working-directory execution; +- [x] add unit/contract fixtures for resolver and TTY behavior. Exit criterion: no user-facing command needs its own Docker shell mechanics. From 306c66cc8bb576ad04ecc4b3ddbd69434c3bb238 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:36:33 +0600 Subject: [PATCH 012/106] refactor(cli): use shared container executor --- lib/services.sh | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index c5a4de5c..92a36d64 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -847,32 +847,20 @@ cmd_tools() { } cmd_http() { [[ ${1:-} == reload ]] && http_reload; } cmd_cli() { - local ctr="${1:-}" + local target="${1:-}" shift || true - [[ -n "$ctr" ]] || die "Usage: lds cli [cmd...]" + [[ -n "$target" ]] || die "cli [--] [command...]" + [[ "${1:-}" == -- ]] && shift - docker inspect "$ctr" >/dev/null 2>&1 || die "Container not found: $ctr" - docker inspect -f '{{.State.Running}}' "$ctr" 2>/dev/null | grep -qx true || die "Container not running: $ctr" + _container_resolve_target "$target" || return $? + local container="$_CONTAINER_TARGET_ID" - # If user provided a command, run it; otherwise open an interactive shell. - if [[ "$#" -gt 0 ]]; then - local cmd="$*" - docker exec -it "$ctr" sh -lc ' - if command -v bash >/dev/null 2>&1; then - exec bash --login -lc "$1" - fi - exec sh -lc "$1" - ' sh "$cmd" - return + if (($# > 0)); then + _container_exec_argv "$container" -- "$@" + else + _container_open_shell "$container" fi - - docker exec -it "$ctr" sh -lc ' - if command -v bash >/dev/null 2>&1; then - exec bash --login - fi - exec sh - ' } cmd_core() { From edee0c85b66c7c39d24d881fa6e71f711c4331a8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:37:25 +0600 Subject: [PATCH 013/106] test(cli): update contract for shared cli execution --- tests/execution-contract.sh | 131 +++++++++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 23 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 07630b0d..2cd8979d 100644 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -5,10 +5,8 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=tests/lib/assertions.sh source "$ROOT/tests/lib/assertions.sh" -# Capture the pre-refactor execution contract. Some assertions intentionally -# describe behavior that later batches will replace (forced TTY, argv flattening, -# and core container uppercasing). Change those assertions only together with the -# implementation batch that deliberately changes the contract. +# This contract evolves batch-by-batch. Assertions for a surface are updated +# only when that surface deliberately migrates onto the shared executor. tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT @@ -20,26 +18,61 @@ run_case() { set -euo pipefail export EXECUTION_TEST_LOG="$log" export CYAN='' YELLOW='' RED='' GREEN='' NC='' QUIET=0 VERBOSE=0 + die() { printf 'die:%s\n' "$*" >>"$EXECUTION_TEST_LOG"; return 64; } + err() { printf 'err:%s\n' "$*" >>"$EXECUTION_TEST_LOG"; } lds_project() { printf '%s' testproject; } effective_ai_runtime() { printf '%s' cpu; } ai_service_for_runtime() { printf '%s' llm-ollama; } - docker_compose() { printf 'compose:' >>"$EXECUTION_TEST_LOG"; printf ' <%s>' "$@" >>"$EXECUTION_TEST_LOG"; printf '\n' >>"$EXECUTION_TEST_LOG"; } _project_tools_container_running() { printf '%s' SERVER_TOOLS; } + docker_compose() { + printf 'compose:' >>"$EXECUTION_TEST_LOG" + printf ' <%s>' "$@" >>"$EXECUTION_TEST_LOG" + printf '\n' >>"$EXECUTION_TEST_LOG" + + if [[ "${1:-}" == config && "${2:-}" == --services ]]; then + printf '%s\n' php84 multi + return 0 + fi + if [[ "${1:-}" == ps && "${2:-}" == -a && "${3:-}" == -q ]]; then + case "${4:-}" in + php84) printf '%s\n' cid-php84 ;; + multi) printf '%s\n' cid-one cid-two ;; + esac + return 0 + fi + return 0 + } + docker() { printf 'docker:' >>"$EXECUTION_TEST_LOG" printf ' <%s>' "$@" >>"$EXECUTION_TEST_LOG" printf '\n' >>"$EXECUTION_TEST_LOG" - case "${1:-} ${2:-} ${3:-}" in - "inspect -f {{.State.Running}}") - printf '%s\n' true - ;; - "inspect "*) - return 0 - ;; - esac + if [[ "${1:-}" == inspect && "${2:-}" == -f ]]; then + case "${3:-}|${4:-}" in + "{{.Id}}|demo-container") printf '%s\n' cid-demo ;; + "{{.Id}}|stopped-container") printf '%s\n' cid-stopped ;; + "{{.Id}}|"*) return 1 ;; + "{{.Name}}|cid-demo") printf '%s\n' /demo-container ;; + "{{.Name}}|cid-php84") printf '%s\n' /PHP84 ;; + "{{.Name}}|cid-one") printf '%s\n' /ONE ;; + "{{.Name}}|cid-two") printf '%s\n' /TWO ;; + "{{.Name}}|cid-stopped") printf '%s\n' /stopped-container ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-demo") printf '%s\n' ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '%s\n' ;; + "{{.State.Running}}|cid-demo") printf '%s\n' true ;; + "{{.State.Running}}|cid-php84") printf '%s\n' true ;; + "{{.State.Running}}|cid-stopped") printf '%s\n' false ;; + "{{.State.Running}}|"*) printf '%s\n' true ;; + esac + return 0 + fi + + if [[ "${1:-}" == inspect && "${2:-}" != -f ]]; then + return 0 + fi if [[ "${1:-}" == exec ]]; then case " $* " in @@ -56,9 +89,12 @@ run_case() { printf '%s\n' /srv/app/public ;; esac + return 0 fi } + # shellcheck source=lib/container-exec.sh + source "$ROOT/lib/container-exec.sh" # shellcheck source=lib/services.sh source "$ROOT/lib/services.sh" "$@" @@ -66,22 +102,71 @@ run_case() { } case_cli_command() { - cmd_cli demo-container printf '%s %s' 'hello world' tail + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_cli demo-container -- printf '%s %s' 'hello world' '$(danger)' } run_case case_cli_command -assert_file_contains "$log" 'docker: ' -assert_file_contains "$log" 'docker: <-f> <{{.State.Running}}> ' -assert_file_contains "$log" 'docker: <-it> <-lc>' -assert_file_contains "$log" '' -pass "baseline: lds cli explicit command validates container, forces TTY, and flattens argv" +assert_file_contains "$log" 'docker: <-f> <{{.Id}}> ' +assert_file_contains "$log" 'docker: <%s %s> <$(danger)>' +if grep -Fq ' <-lc>' "$log"; then + fail "lds cli explicit command still reparses argv through a shell" +fi +pass "batch 2: lds cli preserves explicit command argv without forcing TTY" + +case_cli_piped_command() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 0; } + cmd_cli demo-container cat +} +run_case case_cli_piped_command +assert_file_contains "$log" 'docker: <-i> ' +pass "batch 2: lds cli keeps piped stdin without allocating TTY" + +case_cli_service() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_cli php84 php -v +} +run_case case_cli_service +assert_file_contains "$log" 'compose: <--services>' +assert_file_contains "$log" 'compose: <-a> <-q> ' +assert_file_contains "$log" 'docker: <-v>' +pass "batch 2: lds cli resolves current-project Compose service names" case_cli_shell() { cmd_cli demo-container } run_case case_cli_shell -assert_file_contains "$log" 'docker: <-it> <-lc>' -assert_file_contains "$log" 'exec bash --login' -pass "baseline: lds cli without command opens an interactive login shell" +assert_file_contains "$log" 'docker: <-it> <--login>' +pass "batch 2: lds cli without command opens the shared interactive shell" + +set +e +run_case cmd_cli stopped-container >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 69 ]] || fail "lds cli stopped target returned $rc instead of 69" +assert_file_contains "$log" 'err:Container is not running: cid-stopped' +pass "batch 2: lds cli reports stopped targets" + +set +e +run_case cmd_cli missing-container >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 66 ]] || fail "lds cli missing target returned $rc instead of 66" +assert_file_contains "$log" 'err:Container or current-project service not found: missing-container' +pass "batch 2: lds cli reports missing targets" + +set +e +run_case cmd_cli multi >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 65 ]] || fail "lds cli ambiguous service returned $rc instead of 65" +assert_file_contains "$log" 'err:Service resolves to multiple containers: multi' +pass "batch 2: lds cli rejects ambiguous service targets" case_core_domain() { cmd_core app.local @@ -135,4 +220,4 @@ assert_file_contains "$ROOT/lds" 'exec) cmd_exec "$@" ;;' assert_file_contains "$ROOT/lds" 'cmd_stack "$@"' pass "baseline: grouped stack exec and top-level dispatch remain wired" -printf 'Execution baseline contract complete.\n' +printf 'Execution contract complete.\n' From 8e78c904237a7900976c5c26d2d2d9da0484f501 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:37:43 +0600 Subject: [PATCH 014/106] docs(plan): update cli hardening tracker --- docs/plans/lds-core-cli-hardening-plan.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index 32c70537..c5d0cce7 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -361,14 +361,16 @@ Exit criterion: no user-facing command needs its own Docker shell mechanics. ## Batch 2 — harden `lds cli` -- [ ] move `cmd_cli` onto the shared execution substrate; -- [ ] support current-project Compose service names; -- [ ] preserve explicit container names/IDs; -- [ ] remove forced uppercase/case assumptions; -- [ ] preserve argv exactly for explicit commands; -- [ ] support piped/non-TTY commands without `-t`; -- [ ] keep no-command interactive shell behavior; -- [ ] add missing/stopped/ambiguous target tests. +Status: **implemented — CI validation pending** + +- [x] move `cmd_cli` onto the shared execution substrate; +- [x] support current-project Compose service names; +- [x] preserve explicit container names/IDs; +- [x] remove forced uppercase/case assumptions; +- [x] preserve argv exactly for explicit commands; +- [x] support piped/non-TTY commands without `-t`; +- [x] keep no-command interactive shell behavior; +- [x] add missing/stopped/ambiguous target tests. Exit criterion: `cli` is the reliable low-level execution command. From c779bec16f14a321c32761e7a223cc903f6ce576 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:38:28 +0600 Subject: [PATCH 015/106] feat(core): add shared container workdir fallback --- lib/container-exec.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/container-exec.sh b/lib/container-exec.sh index 2163fe89..8abbdb62 100644 --- a/lib/container-exec.sh +++ b/lib/container-exec.sh @@ -152,6 +152,23 @@ _container_exec_argv() { "${args[@]}" } +_container_first_existing_dir() { + local target="${1:-}" + shift || true + _container_require_running "$target" || return $? + + local path + for path in "$@"; do + [[ -n "$path" ]] || continue + if docker exec "$target" test -d "$path" >/dev/null 2>&1; then + printf '%s' "$path" + return 0 + fi + done + + printf '%s' / +} + _container_shell_name() { local target="${1:-}" _container_require_running "$target" || return $? From 89c221ddd39d11584ff17a3a27177518d7fa938d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:39:03 +0600 Subject: [PATCH 016/106] refactor(core): centralize domain execution --- lib/services.sh | 215 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 142 insertions(+), 73 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index 92a36d64..ba7d418e 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -863,94 +863,163 @@ cmd_cli() { fi } -cmd_core() { - # Usage: - # lds core -> open correct container for that domain (PHP/Node) - # lds core -> open a shell in that container - # lds core -> list domains and let user pick +_CORE_DOMAIN='' +_CORE_APP='' +_CORE_CONTAINER_ID='' +_CORE_CONTAINER_NAME='' +_CORE_WORKDIR='' +_core_is_domain() { local target="${1:-}" - - # domain regex (same as domain-which/mkhost family) local re='^([a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.)+(localhost|local|test|loc|[a-zA-Z]{2,})$' + [[ "$target" =~ $re ]] +} - # If no target -> prompt from domain-which list - if [[ -z "$target" ]]; then - local tools_ctr - tools_ctr="$(_project_tools_container_running || true)" - [[ -n "$tools_ctr" ]] || die "server-tools container is not running for project: $(lds_project)" +_core_domain_list() { + local tools_ctr + tools_ctr="$(_project_tools_container_running || true)" + [[ -n "$tools_ctr" ]] || { + err "server-tools container is not running for project: $(lds_project)" + return 69 + } - local -a domains=() - mapfile -t domains < <(docker exec "$tools_ctr" domain-which --list-domains 2>/dev/null | sed '/^[[:space:]]*$/d' || true) + docker exec "$tools_ctr" domain-which --list-domains 2>/dev/null | + sed '/^[[:space:]]*$/d' | + LC_ALL=C sort -u +} - ((${#domains[@]} > 0)) || die "No domains found" +_core_choose_domain() { + local -a domains=() + mapfile -t domains < <(_core_domain_list) || return $? + (("${#domains[@]}" > 0)) || { + err "No domains found" + return 66 + } - # stable ordering - IFS=$'\n' domains=($(printf '%s\n' "${domains[@]}" | LC_ALL=C sort -u)) + if (("${#domains[@]}" == 1)); then + printf '%s' "${domains[0]}" + return 0 + fi - if ((${#domains[@]} == 1)); then - target="${domains[0]}" - else - if [[ ! -t 0 ]]; then - printf "%b[core]%b No domain provided. Available domains:\n" "$YELLOW" "$NC" >&2 - local i=1 - local d - for d in "${domains[@]}"; do - printf " %2d) %s\n" "$i" "$d" >&2 - ((i++)) - done - die "No TTY to prompt. Use: lds core " - fi + if [[ ! -t 0 ]]; then + printf "%b[core]%b No domain provided. Available domains:\n" "$YELLOW" "$NC" >&2 + local i=1 domain + for domain in "${domains[@]}"; do + printf " %2d) %s\n" "$i" "$domain" >&2 + ((i++)) + done + err "No TTY to prompt. Use: lds core " + return 64 + fi - printf "%bSelect domain:%b\n" "$CYAN" "$NC" >&2 - local i=1 d - for d in "${domains[@]}"; do - printf " %b%2d)%b %s\n" "$CYAN" "$i" "$NC" "$d" >&2 - ((i++)) - done + printf "%bSelect domain:%b\n" "$CYAN" "$NC" >&2 + local i=1 domain + for domain in "${domains[@]}"; do + printf " %b%2d)%b %s\n" "$CYAN" "$i" "$NC" "$domain" >&2 + ((i++)) + done - local ans="" - while true; do - read -r -p "Enter number (1-${#domains[@]}): " ans - ans="$(echo "$ans" | xargs)" - [[ "$ans" =~ ^[0-9]+$ ]] || { - printf "%bInvalid input.%b\n" "$YELLOW" "$NC" >&2 - continue - } - ((ans >= 1 && ans <= ${#domains[@]})) || { - printf "%bOut of range.%b\n" "$YELLOW" "$NC" >&2 - continue - } - target="${domains[$((ans - 1))]}" - break - done - fi + local answer='' + while true; do + read -r -p "Enter number (1-${#domains[@]}): " answer + answer="${answer#"${answer%%[![:space:]]*}"}" + answer="${answer%"${answer##*[![:space:]]}"}" + [[ "$answer" =~ ^[0-9]+$ ]] || { + printf "%bInvalid input.%b\n" "$YELLOW" "$NC" >&2 + continue + } + ((answer >= 1 && answer <= ${#domains[@]})) || { + printf "%bOut of range.%b\n" "$YELLOW" "$NC" >&2 + continue + } + printf '%s' "${domains[$((answer - 1))]}" + return 0 + done +} + +_core_domain_resolve() { + local domain="${1:-}" tools_ctr app target docroot preferred + + _CORE_DOMAIN='' + _CORE_APP='' + _CORE_CONTAINER_ID='' + _CORE_CONTAINER_NAME='' + _CORE_WORKDIR='' + + [[ -n "$domain" ]] || { + err "Core domain is required" + return 64 + } + + tools_ctr="$(_project_tools_container_running || true)" + [[ -n "$tools_ctr" ]] || { + err "server-tools container is not running for project: $(lds_project)" + return 69 + } + + app="$(docker exec "$tools_ctr" domain-which --app --quiet "$domain" 2>/dev/null)" || { + err "Unknown domain: $domain" + return 66 + } + target="$(docker exec "$tools_ctr" domain-which --container --quiet "$domain" 2>/dev/null)" || { + err "No container resolved for: $domain" + return 66 + } + docroot="$(docker exec "$tools_ctr" domain-which --docroot --quiet "$domain" 2>/dev/null || true)" + [[ -n "$target" ]] || { + err "No container resolved for: $domain" + return 66 + } + + _container_resolve_target "$target" || return $? + _container_require_running "$_CONTAINER_TARGET_ID" || return $? + + if [[ "${app,,}" == node ]]; then + preferred=/app + else + preferred="${docroot:-/app}" fi - # If target looks like a domain -> resolve via domain-which then shell in - if [[ "$target" =~ $re ]]; then - local tools_ctr - tools_ctr="$(_project_tools_container_running || true)" - [[ -n "$tools_ctr" ]] || die "server-tools container is not running for project: $(lds_project)" - - local app container wd - app="$(docker exec "$tools_ctr" domain-which --app --quiet "$target" 2>/dev/null)" || die "Unknown domain: $target" - container="$(docker exec "$tools_ctr" domain-which --container --quiet "$target" 2>/dev/null)" || die "No container resolved for: $target" - wd="$(docker exec "$tools_ctr" domain-which --docroot --quiet "$target" 2>/dev/null)" || true - [[ -n "${container:-}" ]] || die "No container resolved for: $target" - - # Node apps should always land at /app. Others follow resolved docroot. - if [[ "${app:-}" == "node" ]]; then - wd="/app" - fi - [[ -n "${wd:-}" ]] || wd="/app" + _CORE_DOMAIN="$domain" + _CORE_APP="$app" + _CORE_CONTAINER_ID="$_CONTAINER_TARGET_ID" + _CORE_CONTAINER_NAME="$_CONTAINER_TARGET_NAME" + _CORE_WORKDIR="$(_container_first_existing_dir "$_CORE_CONTAINER_ID" "$preferred" /app /)" || return $? +} - docker exec -it "$container" bash -lc "cd \"$wd\" 2>/dev/null || cd /app 2>/dev/null || cd /; exec bash" - return 0 +cmd_core() { + local target="${1:-}" + [[ -n "$target" ]] && shift || true + + if [[ -z "$target" ]]; then + target="$(_core_choose_domain)" || return $? + fi + + [[ "${1:-}" == -- ]] && shift + + local container workdir='' + if _core_is_domain "$target"; then + _core_domain_resolve "$target" || return $? + container="$_CORE_CONTAINER_ID" + workdir="$_CORE_WORKDIR" + else + _container_resolve_target "$target" || return $? + container="$_CONTAINER_TARGET_ID" fi - # Otherwise treat target as a container name - docker exec -it "$(printf '%s' "$target" | tr '[:lower:]' '[:upper:]')" sh -lc 'exec bash -i || exec sh' + if (($# > 0)); then + if [[ -n "$workdir" ]]; then + _container_exec_argv "$container" --workdir "$workdir" -- "$@" + else + _container_exec_argv "$container" -- "$@" + fi + else + if [[ -n "$workdir" ]]; then + _container_open_shell "$container" --workdir "$workdir" + else + _container_open_shell "$container" + fi + fi } cmd_setup() { From d273ca0ea54890ce77090e27f8dba679170b0aa5 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:39:50 +0600 Subject: [PATCH 017/106] test(core): cover centralized domain execution --- tests/execution-contract.sh | 118 +++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 10 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 2cd8979d..22bef0be 100644 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -32,12 +32,13 @@ run_case() { printf '\n' >>"$EXECUTION_TEST_LOG" if [[ "${1:-}" == config && "${2:-}" == --services ]]; then - printf '%s\n' php84 multi + printf '%s\n' php84 node multi return 0 fi if [[ "${1:-}" == ps && "${2:-}" == -a && "${3:-}" == -q ]]; then case "${4:-}" in php84) printf '%s\n' cid-php84 ;; + node) printf '%s\n' cid-node ;; multi) printf '%s\n' cid-one cid-two ;; esac return 0 @@ -53,16 +54,24 @@ run_case() { if [[ "${1:-}" == inspect && "${2:-}" == -f ]]; then case "${3:-}|${4:-}" in "{{.Id}}|demo-container") printf '%s\n' cid-demo ;; + "{{.Id}}|mixedCase-container") printf '%s\n' cid-mixed ;; + "{{.Id}}|NODE") printf '%s\n' cid-node ;; "{{.Id}}|stopped-container") printf '%s\n' cid-stopped ;; "{{.Id}}|"*) return 1 ;; "{{.Name}}|cid-demo") printf '%s\n' /demo-container ;; + "{{.Name}}|cid-mixed") printf '%s\n' /mixedCase-container ;; + "{{.Name}}|cid-node") printf '%s\n' /NODE ;; "{{.Name}}|cid-php84") printf '%s\n' /PHP84 ;; "{{.Name}}|cid-one") printf '%s\n' /ONE ;; "{{.Name}}|cid-two") printf '%s\n' /TWO ;; "{{.Name}}|cid-stopped") printf '%s\n' /stopped-container ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-demo") printf '%s\n' ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-mixed") printf '%s\n' ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-node") printf '%s\n' node ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '%s\n' ;; "{{.State.Running}}|cid-demo") printf '%s\n' true ;; + "{{.State.Running}}|cid-mixed") printf '%s\n' true ;; + "{{.State.Running}}|cid-node") printf '%s\n' true ;; "{{.State.Running}}|cid-php84") printf '%s\n' true ;; "{{.State.Running}}|cid-stopped") printf '%s\n' false ;; "{{.State.Running}}|"*) printf '%s\n' true ;; @@ -77,7 +86,7 @@ run_case() { if [[ "${1:-}" == exec ]]; then case " $* " in *" domain-which --list-domains "*) - printf '%s\n' app.local + printf '%s\n' app.local php.local ;; *" domain-which --app --quiet app.local "*) printf '%s\n' node @@ -88,6 +97,39 @@ run_case() { *" domain-which --docroot --quiet app.local "*) printf '%s\n' /srv/app/public ;; + *" domain-which --app --quiet php.local "*) + printf '%s\n' php + ;; + *" domain-which --container --quiet php.local "*) + printf '%s\n' php84 + ;; + *" domain-which --docroot --quiet php.local "*) + printf '%s\n' /srv/php/public + ;; + *" domain-which --app --quiet fallback.local "*) + printf '%s\n' php + ;; + *" domain-which --container --quiet fallback.local "*) + printf '%s\n' php84 + ;; + *" domain-which --docroot --quiet fallback.local "*) + printf '%s\n' /missing/docroot + ;; + *" cid-node test -d /app "*) + return 0 + ;; + *" cid-php84 test -d /srv/php/public "*) + return 0 + ;; + *" cid-php84 test -d /missing/docroot "*) + return 1 + ;; + *" cid-php84 test -d /app "*) + return 0 + ;; + *" cid-demo sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-mixed sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-node sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-php84 sh -lc command -v bash >/dev/null 2>&1 "*) + return 0 + ;; esac return 0 fi @@ -168,22 +210,78 @@ set -e assert_file_contains "$log" 'err:Service resolves to multiple containers: multi' pass "batch 2: lds cli rejects ambiguous service targets" -case_core_domain() { +case_core_node_domain() { cmd_core app.local } -run_case case_core_domain +run_case case_core_node_domain assert_file_contains "$log" 'docker: <--app> <--quiet> ' -assert_file_contains "$log" 'docker: <--container> <--quiet> ' -assert_file_contains "$log" 'docker: <-it> <-lc>' -assert_file_contains "$log" 'cd "/app"' -pass "baseline: lds core resolves a domain through server-tools and forces Node /app" +assert_file_contains "$log" 'docker: <-it> <--workdir> <--login>' +pass "batch 3: lds core resolves Node domains and opens /app through the shared shell helper" + +case_core_php_domain() { + cmd_core php.local +} +run_case case_core_php_domain +assert_file_contains "$log" 'docker: <-it> <--workdir> <--login>' +pass "batch 3: lds core preserves resolved PHP document root" + +case_core_docroot_fallback() { + cmd_core fallback.local +} +run_case case_core_docroot_fallback +assert_file_contains "$log" 'docker: <-d> ' +assert_file_contains "$log" 'docker: <-it> <--workdir> <--login>' +pass "batch 3: lds core falls back from missing docroot to /app" + +case_core_domain_command() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_core app.local -- node -e 'console.log("hello world")' +} +run_case case_core_domain_command +assert_file_contains "$log" 'docker: <--workdir> <-e> ' +pass "batch 3: lds core executes domain commands as exact argv in the application workdir" + +case_core_service() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_core php84 php -v +} +run_case case_core_service +assert_file_contains "$log" 'compose: <-a> <-q> ' +assert_file_contains "$log" 'docker: <-v>' +pass "batch 3: lds core delegates service targets to the shared resolver" case_core_container() { cmd_core mixedCase-container } run_case case_core_container -assert_file_contains "$log" 'docker: <-it> <-lc> ' -pass "baseline: lds core raw container fallback uppercases the target" +assert_file_contains "$log" 'docker: <-f> <{{.Id}}> ' +assert_file_contains "$log" 'docker: <-it> <--login>' +if grep -Fq 'MIXEDCASE-CONTAINER' "$log"; then + fail "lds core still uppercases explicit container targets" +fi +pass "batch 3: lds core preserves explicit mixed-case container targets" + +case_core_single_domain() { + _core_domain_list() { printf '%s\n' app.local; } + cmd_core +} +run_case case_core_single_domain +assert_file_contains "$log" 'docker: <-it> <--workdir> <--login>' +pass "batch 3: lds core auto-selects the only discovered domain" + +set +e +run_case cmd_core >"$tmp/core-nontty.out" 2>"$tmp/core-nontty.err" +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "lds core non-TTY multi-domain selection returned $rc instead of 64" +grep -Fq 'app.local' "$tmp/core-nontty.err" || fail "non-TTY domain list omitted app.local" +grep -Fq 'php.local' "$tmp/core-nontty.err" || fail "non-TTY domain list omitted php.local" +assert_file_contains "$log" 'err:No TTY to prompt. Use: lds core ' +pass "batch 3: lds core lists domains and fails actionably without a TTY" case_stack_exec_command() { resolve_service() { printf '%s' php84; } From fca53b5353af67286d024ab953de9b7c0a20d98b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:40:05 +0600 Subject: [PATCH 018/106] test(core): cover shared workdir fallback --- tests/container-exec-contract.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh index ebfe2c3c..d6777ebe 100644 --- a/tests/container-exec-contract.sh +++ b/tests/container-exec-contract.sh @@ -154,6 +154,25 @@ _container_open_shell cid-php84 --workdir /app assert_file_contains "$log" 'exec: <-it> <--workdir> <--login>' pass "shared shell helper prefers Bash and supports Docker workdir" +: >"$log" +docker() { + if [[ "${1:-}" == inspect && "${2:-}" == -f && "${3:-}" == '{{.State.Running}}' ]]; then + printf '%s\n' true + return 0 + fi + if [[ "${1:-}" == exec && "${2:-}" == cid-php84 && "${3:-}" == test && "${4:-}" == -d ]]; then + case "${5:-}" in + /missing) return 1 ;; + /app) return 0 ;; + esac + fi + return 0 +} +resolved_dir="$(_container_first_existing_dir cid-php84 /missing /app /)" +[[ "$resolved_dir" == /app ]] || fail "workdir fallback resolved '$resolved_dir' instead of /app" +pass "shared workdir resolver selects the first existing container directory" + + : >"$log" _container_open_shell cid-nginx assert_file_contains "$log" 'exec: <-it> ' From 7669bef6ac9d9078639cc43f1ff9c30ff42194cf Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:40:29 +0600 Subject: [PATCH 019/106] test(core): isolate workdir fallback fixture --- tests/container-exec-contract.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh index d6777ebe..16e3d133 100644 --- a/tests/container-exec-contract.sh +++ b/tests/container-exec-contract.sh @@ -154,6 +154,12 @@ _container_open_shell cid-php84 --workdir /app assert_file_contains "$log" 'exec: <-it> <--workdir> <--login>' pass "shared shell helper prefers Bash and supports Docker workdir" + +: >"$log" +_container_open_shell cid-nginx +assert_file_contains "$log" 'exec: <-it> ' +pass "shared shell helper falls back to sh" + : >"$log" docker() { if [[ "${1:-}" == inspect && "${2:-}" == -f && "${3:-}" == '{{.State.Running}}' ]]; then @@ -173,9 +179,4 @@ resolved_dir="$(_container_first_existing_dir cid-php84 /missing /app /)" pass "shared workdir resolver selects the first existing container directory" -: >"$log" -_container_open_shell cid-nginx -assert_file_contains "$log" 'exec: <-it> ' -pass "shared shell helper falls back to sh" - printf 'Container execution substrate contract complete.\n' From 46135b8858acd7e9428cfc7bc2ecbda4ff8fe76e Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:40:42 +0600 Subject: [PATCH 020/106] docs(plan): update core hardening tracker --- docs/plans/lds-core-cli-hardening-plan.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index c5d0cce7..54540e2e 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -376,16 +376,18 @@ Exit criterion: `cli` is the reliable low-level execution command. ## Batch 3 — harden `lds core` -- [ ] centralize domain discovery; -- [ ] centralize domain -> app/container/docroot resolution; -- [ ] preserve stable interactive domain picker; -- [ ] preserve useful non-TTY domain listing; -- [ ] delegate service/container fallback to the common resolver; -- [ ] remove raw target uppercasing; -- [ ] add explicit command execution after domain/container target; -- [ ] preserve Node `/app` behavior; -- [ ] preserve document-root fallback behavior; -- [ ] test PHP, Node, service, container and non-TTY flows. +Status: **implemented — CI validation pending** + +- [x] centralize domain discovery; +- [x] centralize domain -> app/container/docroot resolution; +- [x] preserve stable interactive domain picker; +- [x] preserve useful non-TTY domain listing; +- [x] delegate service/container fallback to the common resolver; +- [x] remove raw target uppercasing; +- [x] add explicit command execution after domain/container target; +- [x] preserve Node `/app` behavior; +- [x] preserve document-root fallback behavior; +- [x] test PHP, Node, service, container and non-TTY flows. Exit criterion: `core` is application-aware but contains no duplicate Docker execution logic. From 7c9af0429337fd075fc6ff33ecda9b8dd3b370fa Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:42:25 +0600 Subject: [PATCH 021/106] refactor(cli): unify stack and tools execution --- lib/services.sh | 50 ++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index ba7d418e..7ff952a5 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -517,16 +517,22 @@ cmd_ui() { # 6f. EXEC / EVENTS / CLEAN / DISK # ───────────────────────────────────────────────────────────────────────────── cmd_exec() { - local svc="${1:-}" + local requested="${1:-}" shift || true - [[ -n "$svc" ]] || die "exec [cmd...]" - local s - s="$(resolve_service "$svc" || true)" - [[ -n "$s" ]] || die "Unknown service: $svc" - if [[ $# -gt 0 ]]; then - docker_compose exec "$s" "$@" + [[ -n "$requested" ]] || die "exec [--] [command...]" + [[ "${1:-}" == -- ]] && shift + + local service + service="$(resolve_service "$requested" || true)" + [[ -n "$service" ]] && compose_service_exists "$service" || die "Unknown service: $requested" + + _container_resolve_target "$service" || return $? + local container="$_CONTAINER_TARGET_ID" + + if (($# > 0)); then + _container_exec_argv "$container" -- "$@" else - docker_compose exec "$s" sh -lc 'command -v bash >/dev/null 2>&1 && exec bash || exec sh' + _container_open_shell "$container" fi } @@ -812,39 +818,37 @@ cmd_rebuild() { } -docker_shell() { - local c="${1:-}" - [[ -n "$c" ]] || die "container name required" - if docker exec "$c" sh -lc 'command -v bash >/dev/null 2>&1' >/dev/null 2>&1; then - exec docker exec -it "$c" bash - else - exec docker exec -it "$c" sh - fi -} cmd_tools() { local sub="${1:-sh}" shift || true local ctr ctr="$(_project_tools_container_running || true)" [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + case "${sub,,}" in sh | shell | "") - docker_shell "$ctr" + _container_open_shell "$ctr" ;; exec) - [[ $# -gt 0 ]] || die "tools exec " - docker exec -it "$ctr" sh -lc "$*" + [[ "${1:-}" == -- ]] && shift + (($# > 0)) || die "tools exec [--] [args...]" + _container_exec_argv "$ctr" -- "$@" ;; file) - local p="${1:-}" - [[ -n "$p" ]] || die "tools file " - docker exec -it "$ctr" sh -lc "ls -la -- \"$p\" 2>/dev/null || true; echo; sed -n '1,200p' -- \"$p\" 2>/dev/null || true" + local path="${1:-}" + [[ -n "$path" ]] || die "tools file " + _container_exec_argv "$ctr" -- sh -lc ' + ls -la -- "$1" 2>/dev/null || true + printf "\n" + sed -n "1,200p" -- "$1" 2>/dev/null || true + ' sh "$path" ;; *) die "tools " ;; esac } + cmd_http() { [[ ${1:-} == reload ]] && http_reload; } cmd_cli() { local target="${1:-}" From 3da37d4e528fd8503fbd24fa47682ebff5262930 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:42:50 +0600 Subject: [PATCH 022/106] test(cli): cover unified stack and tools execution --- tests/execution-contract.sh | 49 +++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 22bef0be..baff8c1b 100644 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -284,35 +284,58 @@ assert_file_contains "$log" 'err:No TTY to prompt. Use: lds core ' pass "batch 3: lds core lists domains and fails actionably without a TTY" case_stack_exec_command() { - resolve_service() { printf '%s' php84; } - cmd_exec PHP84 php -r 'echo "ok";' + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_exec PHP84 -- php -r 'echo "ok";' } run_case case_stack_exec_command -assert_file_contains "$log" 'compose: <-r> ' -pass "baseline: lds stack exec/cmd_exec preserves explicit command argv through Compose" +assert_file_contains "$log" 'docker: <-r> ' +if grep -Fq 'compose: ' "$log"; then + fail "lds stack exec still bypasses the shared container executor" +fi +pass "batch 4: lds stack exec remains service-only and preserves command argv" case_stack_exec_shell() { - resolve_service() { printf '%s' php84; } cmd_exec PHP84 } run_case case_stack_exec_shell -assert_file_contains "$log" 'compose: <-lc> /dev/null 2>&1 && exec bash || exec sh>' -pass "baseline: lds stack exec/cmd_exec opens Bash-or-sh when no command is provided" +assert_file_contains "$log" 'docker: <-it> <--login>' +pass "batch 4: lds stack exec uses the shared interactive shell helper" case_tools_exec() { - cmd_tools exec printf '%s %s' 'hello world' tail + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_tools exec -- printf '%s %s' 'hello world' '$(danger)' } run_case case_tools_exec -assert_file_contains "$log" 'docker: <-it> <-lc> ' -pass "baseline: lds tools exec forces TTY and flattens command argv" +assert_file_contains "$log" 'docker: <%s %s> <$(danger)>' +if grep -Fq ' <-lc> >"$EXECUTION_TEST_LOG"; } cmd_tools sh } run_case case_tools_shell -assert_file_contains "$log" 'docker-shell:SERVER_TOOLS' -pass "baseline: lds tools sh delegates to the common docker_shell helper" +assert_file_contains "$log" 'docker: <-it> <--login>' +pass "batch 4: lds tools sh uses the shared interactive shell helper" + +case_tools_file() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_tools file '/app/path with spaces.txt' +} +run_case case_tools_file +assert_file_contains "$log" ' <-lc>' +assert_file_contains "$log" ' ' +pass "batch 4: lds tools file passes paths as shell positional argv rather than interpolating them" + +assert_file_contains "$ROOT/lib/services.sh" 'docker exec -it "$ctr" lazydocker' +pass "batch 4: support ui remains a specialized interactive TUI path" assert_file_contains "$ROOT/lds" 'exec) cmd_exec "$@" ;;' assert_file_contains "$ROOT/lds" 'cmd_stack "$@"' From f851e7f17b469d55e6f9f9a06b09208cb9410d48 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:43:05 +0600 Subject: [PATCH 023/106] docs(plan): update adjacent execution tracker --- docs/plans/lds-core-cli-hardening-plan.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index 54540e2e..1ecb71a2 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -393,11 +393,13 @@ Exit criterion: `core` is application-aware but contains no duplicate Docker exe ## Batch 4 — unify adjacent execution surfaces -- [ ] move `cmd_exec` / `stack exec` onto shared helpers without changing its service-only UX; -- [ ] move `tools sh` onto shared shell helper; -- [ ] move `tools exec` away from unsafe command flattening where compatibility allows; -- [ ] review `cmd_ui`/other direct `docker exec` users for helper reuse where relevant; -- [ ] keep specialized commands specialized; do not over-abstract unrelated Docker operations. +Status: **implemented — CI validation pending** + +- [x] move `cmd_exec` / `stack exec` onto shared helpers without changing its service-only UX; +- [x] move `tools sh` onto shared shell helper; +- [x] move `tools exec` away from unsafe command flattening where compatibility allows; +- [x] review `cmd_ui`/other direct `docker exec` users for helper reuse where relevant; +- [x] keep specialized commands specialized; do not over-abstract unrelated Docker operations. Exit criterion: interactive/command execution semantics are consistent across LDS. From 445ce24bebcd033bfe376e8bd996e7f0d041f525 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:44:38 +0600 Subject: [PATCH 024/106] refactor(cli): standardize execution errors --- lib/services.sh | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index 7ff952a5..c036f426 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -468,7 +468,10 @@ cmd_profiles() { cmd_secrets() { local ctr ctr="$(_project_tools_container_running || true)" - [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + [[ -n "$ctr" ]] || { + err "server-tools container is not running for project: $(lds_project)" + return 69 + } docker exec -it "$ctr" senv "$@" } @@ -519,12 +522,18 @@ cmd_ui() { cmd_exec() { local requested="${1:-}" shift || true - [[ -n "$requested" ]] || die "exec [--] [command...]" + [[ -n "$requested" ]] || { + err "Usage: lds stack exec [--] [command...]" + return 64 + } [[ "${1:-}" == -- ]] && shift local service service="$(resolve_service "$requested" || true)" - [[ -n "$service" ]] && compose_service_exists "$service" || die "Unknown service: $requested" + if [[ -z "$service" ]] || ! compose_service_exists "$service"; then + err "Current-project service not found: $requested" + return 66 + fi _container_resolve_target "$service" || return $? local container="$_CONTAINER_TARGET_ID" @@ -831,12 +840,18 @@ cmd_tools() { ;; exec) [[ "${1:-}" == -- ]] && shift - (($# > 0)) || die "tools exec [--] [args...]" + (($# > 0)) || { + err "Usage: lds tools exec [--] [args...]" + return 64 + } _container_exec_argv "$ctr" -- "$@" ;; file) local path="${1:-}" - [[ -n "$path" ]] || die "tools file " + [[ -n "$path" ]] || { + err "Usage: lds tools file " + return 64 + } _container_exec_argv "$ctr" -- sh -lc ' ls -la -- "$1" 2>/dev/null || true printf "\n" @@ -854,7 +869,10 @@ cmd_cli() { local target="${1:-}" shift || true - [[ -n "$target" ]] || die "cli [--] [command...]" + [[ -n "$target" ]] || { + err "Usage: lds cli [--] [command...]" + return 64 + } [[ "${1:-}" == -- ]] && shift _container_resolve_target "$target" || return $? From ff8d3bd5c3b9d55d302536a96d53aff690082078 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:45:03 +0600 Subject: [PATCH 025/106] fix(cli): standardize tools unavailable exit --- lib/services.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/services.sh b/lib/services.sh index c036f426..e96f6db1 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -832,7 +832,10 @@ cmd_tools() { shift || true local ctr ctr="$(_project_tools_container_running || true)" - [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + [[ -n "$ctr" ]] || { + err "server-tools container is not running for project: $(lds_project)" + return 69 + } case "${sub,,}" in sh | shell | "") From 37fb9bf18020197cf75ae40a3cb39657cc01043a Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:45:33 +0600 Subject: [PATCH 026/106] refactor(cli): make public routing explicit --- lds | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/lds b/lds index 9b6055c2..95b23fb4 100755 --- a/lds +++ b/lds @@ -770,12 +770,15 @@ cmd_help() { - `lds images` - `lds urls` -## Tools / shells / secrets -- `lds tools sh` -- `lds tools exec ""` -- `lds tools file ` -- `lds cli [cmd...]` -- `lds core [domain|container]` +## Execution / shells +- `lds core [domain|service|container] [--] [command...]` — application/domain-aware execution with resolved working directory. +- `lds cli [--] [command...]` — generic current-project service or exact-container execution. +- `lds stack exec [--] [command...]` *(alias: `lds exec`)* — Compose-service-only execution. +- `lds tools sh` — interactive shell in the project `server-tools` container. +- `lds tools exec [--] [args...]` — argv-preserving execution in `server-tools`. +- `lds tools file ` — inspect a file inside `server-tools`. + +## Secrets - `lds secrets ` ## AI consumer @@ -827,8 +830,6 @@ ${CYAN}Stack:${NC} ${CYAN}Domains:${NC} domain add|rm|ls host add|rm|list Legacy aliases - core [domain|container] - cli [cmd...] ${CYAN}Setup / Profiles:${NC} setup init|permissions|domain|profile|profiles @@ -854,8 +855,13 @@ ${CYAN}Config:${NC} images urls -${CYAN}Tools / Secrets:${NC} - tools sh|exec|file +${CYAN}Execution / Shells:${NC} + core [domain|service|container] [--] [command...] Domain/application-aware + cli [--] [command...] Generic service/container exec + stack exec [--] [command...] Compose-service only + tools sh|exec|file server-tools only + +${CYAN}Secrets:${NC} secrets ${CYAN}AI:${NC} @@ -884,6 +890,19 @@ EOF } +_is_public_lds_command() { + case "${1:-}" in + stack | domain | support | bundle | + up | start | down | stop | restart | reboot | status | ps | logs | exec | events | clean | + config | http | host | setup | profiles | cert | certificate | + doctor | diag | sniff | open | notify | ui | images | urls | + tools | cli | core | secrets | rebuild | run) + return 0 + ;; + esac + return 1 +} + ############################################################################### # 7. MAIN ############################################################################### @@ -959,7 +978,7 @@ main() { llm) cmd_llm "$@" ;; vpn-fix) cmd_vpn_fix "$@" ;; *) - if declare -F "cmd_$cmd" >/dev/null 2>&1; then + if _is_public_lds_command "$cmd"; then "cmd_$cmd" "$@" else exec "$DIR/bin/tool-runner" "$cmd" "$@" From bf737fefe689b2b56c0563181db54738cb604253 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:46:05 +0600 Subject: [PATCH 027/106] test(cli): lock explicit routing and role help --- tests/cli-contract.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/cli-contract.sh b/tests/cli-contract.sh index 140b287d..c71ee19a 100755 --- a/tests/cli-contract.sh +++ b/tests/cli-contract.sh @@ -10,11 +10,18 @@ assert_contains "$help_output" "LocalDevStack" assert_contains "$help_output" "Stack" assert_contains "$help_output" "Domain" assert_contains "$help_output" "Setup" +assert_contains "$help_output" "Execution / Shells" +assert_contains "$help_output" "Generic service/container exec" +assert_contains "$help_output" "Compose-service only" +assert_contains "$help_output" "server-tools only" pass "lds help" markdown_output="$("$ROOT/lds" help --markdown)" assert_contains "$markdown_output" "# LocalDevStack" assert_contains "$markdown_output" "lds stack up" +assert_contains "$markdown_output" "lds core [domain|service|container]" +assert_contains "$markdown_output" "lds cli " +assert_contains "$markdown_output" "argv-preserving execution" pass "lds markdown help" global_help="$("$ROOT/lds" --help)" @@ -42,6 +49,15 @@ assert_contains "$help_output" "graphify [path]" assert_contains "$markdown_output" "lds graphify" pass "Graphify workflow help" + +assert_file_contains "$ROOT/lds" '_is_public_lds_command()' +if grep -Fq 'declare -F "cmd_$cmd"' "$ROOT/lds"; then + fail "top-level dispatch still exposes arbitrary cmd_* functions dynamically" +fi +assert_file_contains "$ROOT/lds" 'stack | domain | support | bundle |' +assert_file_contains "$ROOT/lds" 'tools | cli | core | secrets | rebuild | run)' +pass "top-level LDS command routing is explicit and collision-safe" + graphify_log="$(mktemp)" cat >"$tmpbin/graphify" <<'SH' #!/usr/bin/env sh From 0ef2f5774c605817de883667b7ba5d6aa0406637 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:46:10 +0600 Subject: [PATCH 028/106] test(cli): cover execution usage and exit codes --- tests/execution-contract.sh | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index baff8c1b..74b8d164 100644 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -210,6 +210,14 @@ set -e assert_file_contains "$log" 'err:Service resolves to multiple containers: multi' pass "batch 2: lds cli rejects ambiguous service targets" +set +e +run_case cmd_cli >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "lds cli missing target returned $rc instead of 64" +assert_file_contains "$log" 'err:Usage: lds cli [--] [command...]' +pass "batch 5: lds cli uses standardized usage exit" + case_core_node_domain() { cmd_core app.local } @@ -303,6 +311,21 @@ run_case case_stack_exec_shell assert_file_contains "$log" 'docker: <-it> <--login>' pass "batch 4: lds stack exec uses the shared interactive shell helper" +set +e +run_case cmd_exec >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "lds stack exec missing service returned $rc instead of 64" +assert_file_contains "$log" 'err:Usage: lds stack exec [--] [command...]' + +set +e +run_case cmd_exec missing-service >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 66 ]] || fail "lds stack exec unknown service returned $rc instead of 66" +assert_file_contains "$log" 'err:Current-project service not found: missing-service' +pass "batch 5: lds stack exec uses standardized usage/not-found exits" + case_tools_exec() { _container_stdin_is_tty() { return 1; } _container_stdout_is_tty() { return 1; } @@ -323,6 +346,25 @@ run_case case_tools_shell assert_file_contains "$log" 'docker: <-it> <--login>' pass "batch 4: lds tools sh uses the shared interactive shell helper" +set +e +run_case cmd_tools exec >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "lds tools exec missing command returned $rc instead of 64" +assert_file_contains "$log" 'err:Usage: lds tools exec [--] [args...]' + +case_tools_unavailable() { + _project_tools_container_running() { return 1; } + cmd_tools sh +} +set +e +run_case case_tools_unavailable >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 69 ]] || fail "lds tools unavailable container returned $rc instead of 69" +assert_file_contains "$log" 'err:server-tools container is not running for project: testproject' +pass "batch 5: lds tools uses standardized usage/unavailable exits" + case_tools_file() { _container_stdin_is_tty() { return 1; } _container_stdout_is_tty() { return 1; } From ff06a6725240d158aab872b644f227c9111c72e9 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:46:36 +0600 Subject: [PATCH 029/106] docs(plan): update CLI routing tracker --- docs/plans/lds-core-cli-hardening-plan.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index 1ecb71a2..7ad39da4 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -405,12 +405,14 @@ Exit criterion: interactive/command execution semantics are consistent across LD ## Batch 5 — CLI routing and UX cleanup -- [ ] review top-level dynamic `cmd_$cmd` routing for discoverability and collision safety; -- [ ] ensure canonical grouped commands and shortcuts remain intentional; -- [ ] standardize usage/error text; -- [ ] standardize exit codes for missing target, unknown target, stopped target and no-TTY prompt; -- [ ] make `--` command separation work consistently; -- [ ] ensure help output clearly explains `core` vs `cli` vs `stack exec` vs `tools`. +Status: **implemented — CI validation pending** + +- [x] review top-level dynamic `cmd_$cmd` routing for discoverability and collision safety; +- [x] ensure canonical grouped commands and shortcuts remain intentional; +- [x] standardize usage/error text; +- [x] standardize exit codes for missing target, unknown target, stopped target and no-TTY prompt; +- [x] make `--` command separation work consistently; +- [x] ensure help output clearly explains `core` vs `cli` vs `stack exec` vs `tools`. Exit criterion: users can predict which execution command to use without knowing LDS internals. From f760db3043f0220e04039a302ca491fa73e438cc Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:48:18 +0600 Subject: [PATCH 030/106] fix(cli): guard Docker exec paths under MSYS --- lib/container-exec.sh | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/container-exec.sh b/lib/container-exec.sh index 8abbdb62..5496669a 100644 --- a/lib/container-exec.sh +++ b/lib/container-exec.sh @@ -8,6 +8,22 @@ _CONTAINER_TARGET_ID='' _CONTAINER_TARGET_NAME='' declare -a _CONTAINER_EXEC_FLAGS=() +_container_is_msys() { + [[ -n "${MSYSTEM:-}${CYGWIN:-}" ]] && return 0 + case "${OSTYPE:-}" in + msys* | cygwin*) return 0 ;; + esac + return 1 +} + +_container_docker() { + if _container_is_msys; then + MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' docker "$@" + else + docker "$@" + fi +} + _container_target_reset() { _CONTAINER_TARGET_KIND='' _CONTAINER_TARGET_REQUESTED='' @@ -28,7 +44,7 @@ _container_project_service_exists() { _container_name_from_id() { local id="${1:-}" name [[ -n "$id" ]] || return 1 - name="$(docker inspect -f '{{.Name}}' "$id" 2>/dev/null || true)" + name="$(_container_docker inspect -f '{{.Name}}' "$id" 2>/dev/null || true)" name="${name#/}" [[ -n "$name" ]] || return 1 printf '%s' "$name" @@ -69,7 +85,7 @@ _container_resolve_target() { fi # Otherwise preserve exact Docker container names/IDs as an explicit escape. - id="$(docker inspect -f '{{.Id}}' "$target" 2>/dev/null || true)" + id="$(_container_docker inspect -f '{{.Id}}' "$target" 2>/dev/null || true)" [[ -n "$id" ]] || { err "Container or current-project service not found: $target" return 66 @@ -77,7 +93,7 @@ _container_resolve_target() { name="$(_container_name_from_id "$id" || true)" [[ -n "$name" ]] || name="$target" - service="$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.service" }}' "$id" 2>/dev/null || true)" + service="$(_container_docker inspect -f '{{ index .Config.Labels "com.docker.compose.service" }}' "$id" 2>/dev/null || true)" _CONTAINER_TARGET_KIND=container _CONTAINER_TARGET_SERVICE="$service" @@ -92,7 +108,7 @@ _container_require_running() { return 64 } - running="$(docker inspect -f '{{.State.Running}}' "$target" 2>/dev/null || true)" + running="$(_container_docker inspect -f '{{.State.Running}}' "$target" 2>/dev/null || true)" [[ "$running" == true ]] || { err "Container is not running: $target" return 69 @@ -146,10 +162,10 @@ _container_exec_argv() { _container_require_running "$target" || return $? _container_exec_flags command || return $? - local -a args=(docker exec "${_CONTAINER_EXEC_FLAGS[@]}") + local -a args=(exec "${_CONTAINER_EXEC_FLAGS[@]}") [[ -n "$workdir" ]] && args+=(--workdir "$workdir") args+=("$target" "$@") - "${args[@]}" + _container_docker "${args[@]}" } _container_first_existing_dir() { @@ -160,7 +176,7 @@ _container_first_existing_dir() { local path for path in "$@"; do [[ -n "$path" ]] || continue - if docker exec "$target" test -d "$path" >/dev/null 2>&1; then + if _container_docker exec "$target" test -d "$path" >/dev/null 2>&1; then printf '%s' "$path" return 0 fi @@ -172,7 +188,7 @@ _container_first_existing_dir() { _container_shell_name() { local target="${1:-}" _container_require_running "$target" || return $? - if docker exec "$target" sh -lc 'command -v bash >/dev/null 2>&1' >/dev/null 2>&1; then + if _container_docker exec "$target" sh -lc 'command -v bash >/dev/null 2>&1' >/dev/null 2>&1; then printf '%s' bash else printf '%s' sh @@ -200,7 +216,7 @@ _container_open_shell() { shell="$(_container_shell_name "$target")" || return $? _container_exec_flags shell || return $? - local -a args=(docker exec "${_CONTAINER_EXEC_FLAGS[@]}") + local -a args=(exec "${_CONTAINER_EXEC_FLAGS[@]}") [[ -n "$workdir" ]] && args+=(--workdir "$workdir") args+=("$target") if [[ "$shell" == bash ]]; then @@ -208,5 +224,5 @@ _container_open_shell() { else args+=(sh) fi - "${args[@]}" + _container_docker "${args[@]}" } From 091e4477c4493f788740434573bb0b617c856959 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:49:05 +0600 Subject: [PATCH 031/106] test(cli): cover cross-platform execution semantics --- tests/container-exec-contract.sh | 80 ++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh index 16e3d133..1b41adde 100644 --- a/tests/container-exec-contract.sh +++ b/tests/container-exec-contract.sh @@ -179,4 +179,84 @@ resolved_dir="$(_container_first_existing_dir cid-php84 /missing /app /)" pass "shared workdir resolver selects the first existing container directory" +# Cross-platform wrapper behavior is isolated in subshells so environment +# overrides do not affect the Linux contracts above. +msys_log="$tmp/msys.log" +( + set -euo pipefail + err() { :; } + docker() { + printf 'env:%s:%s args:' "${MSYS_NO_PATHCONV:-}" "${MSYS2_ARG_CONV_EXCL:-}" >>"$msys_log" + printf ' <%s>' "$@" >>"$msys_log" + printf '\n' >>"$msys_log" + if [[ "${1:-}" == inspect && "${2:-}" == -f && "${3:-}" == '{{.State.Running}}' ]]; then + printf '%s\n' true + fi + } + # shellcheck source=lib/container-exec.sh + source "$ROOT/lib/container-exec.sh" + export MSYSTEM=MINGW64 + export OSTYPE=msys + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + _container_exec_argv cid-win --workdir '/app/path with spaces' -- printf '%s' '$(danger)' +) +assert_file_contains "$msys_log" 'env:1:* args: <--workdir> <%s> <$(danger)>' +pass "Git Bash/MSYS disables Docker path conversion and preserves argv" + +linux_log="$tmp/linux.log" +( + set -euo pipefail + err() { :; } + docker() { + printf 'env:%s:%s args:' "${MSYS_NO_PATHCONV:-}" "${MSYS2_ARG_CONV_EXCL:-}" >>"$linux_log" + printf ' <%s>' "$@" >>"$linux_log" + printf '\n' >>"$linux_log" + if [[ "${1:-}" == inspect && "${2:-}" == -f && "${3:-}" == '{{.State.Running}}' ]]; then + printf '%s\n' true + fi + } + # shellcheck source=lib/container-exec.sh + source "$ROOT/lib/container-exec.sh" + unset MSYSTEM CYGWIN + export OSTYPE=linux-gnu + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + _container_exec_argv cid-linux -- echo ok +) +assert_file_contains "$linux_log" 'env:: args: ' +pass "Linux/WSL path keeps native Docker argument behavior" + +signal_log="$tmp/signal.log" +( + set -euo pipefail + err() { :; } + docker() { + if [[ "${1:-}" == inspect && "${2:-}" == -f && "${3:-}" == '{{.State.Running}}' ]]; then + printf '%s\n' true + return 0 + fi + if [[ "${1:-}" == exec ]]; then + return 130 + fi + return 0 + } + # shellcheck source=lib/container-exec.sh + source "$ROOT/lib/container-exec.sh" + unset MSYSTEM CYGWIN + export OSTYPE=linux-gnu + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + set +e + _container_exec_argv cid-signal -- signal-test + rc=$? + set -e + printf '%s\n' "$rc" >"$signal_log" +) +[[ "$(cat "$signal_log")" == 130 ]] || fail "container exec did not propagate exit 130" +pass "shared executor propagates SIGINT-style container exit status" + printf 'Container execution substrate contract complete.\n' From fa12991e05778a1669214e898b8e06a7baf25f86 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:49:10 +0600 Subject: [PATCH 032/106] test(windows): cover shared Core CLI executor --- tests/windows-bridge.ps1 | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/windows-bridge.ps1 b/tests/windows-bridge.ps1 index 6ead17b6..6f8dd0ec 100644 --- a/tests/windows-bridge.ps1 +++ b/tests/windows-bridge.ps1 @@ -51,3 +51,30 @@ if (-not $copied.Contains('set "DEVHOME=%~dp0"')) { } Write-Host "PASS: Windows bridge quoting/discovery contract" + +$ldsPath = Join-Path $root "lds" +$execPath = Join-Path $root "lib/container-exec.sh" +if (-not (Test-Path $execPath)) { + throw "shared container execution helper not found" +} +$ldsContent = Get-Content -Raw -Path $ldsPath +$execContent = Get-Content -Raw -Path $execPath +foreach ($needle in @( + 'source "$DIR/lib/container-exec.sh"', + '_is_public_lds_command()' +)) { + if (-not $ldsContent.Contains($needle)) { + throw "lds is missing Core/CLI execution contract: $needle" + } +} +foreach ($needle in @( + 'MSYS_NO_PATHCONV=1', + "MSYS2_ARG_CONV_EXCL='*'", + '_container_exec_argv', + '_container_open_shell' +)) { + if (-not $execContent.Contains($needle)) { + throw "shared executor is missing Windows/Git Bash contract: $needle" + } +} +Write-Host "PASS: Windows bridge uses MSYS-safe shared container execution" From e25c1781f16b0ce5fa8920966636ee99d955e2e6 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:49:28 +0600 Subject: [PATCH 033/106] docs(plan): update cross-platform tracker --- docs/plans/lds-core-cli-hardening-plan.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index 7ad39da4..33adef6e 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -418,15 +418,17 @@ Exit criterion: users can predict which execution command to use without knowing ## Batch 6 — cross-platform and automation hardening -- [ ] Linux interactive shell coverage; -- [ ] Linux piped stdin/non-TTY coverage; -- [ ] Windows/Git Bash TTY compatibility; -- [ ] WSL behavior review; -- [ ] Docker Desktop behavior review; -- [ ] paths containing spaces; -- [ ] commands containing spaces/quotes/shell metacharacters as argv; -- [ ] SIGINT/exit-code propagation; -- [ ] no accidental host shell interpolation. +Status: **implemented — CI validation pending** + +- [x] Linux interactive shell coverage; +- [x] Linux piped stdin/non-TTY coverage; +- [x] Windows/Git Bash TTY/path-conversion compatibility; +- [x] WSL behavior review; +- [x] Docker Desktop/Windows bridge behavior review; +- [x] paths containing spaces; +- [x] commands containing spaces/quotes/shell metacharacters as argv; +- [x] SIGINT/exit-code propagation; +- [x] no accidental host shell interpolation. Exit criterion: execution behavior is stable across supported host environments. From 60a8de63ef4559e772d90c2ae27fb40754f4cf60 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:50:46 +0600 Subject: [PATCH 034/106] docs(cli): document unified execution surfaces --- docs/reference/cli.rst | 52 ++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 3ddbf066..c8a356fd 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -35,7 +35,7 @@ Stack lds stack status [status-args...] lds stack ps lds stack logs [service] [--follow] [--since ] [--grep ] - lds stack exec [command...] + lds stack exec [--] [command...] lds stack events [since] lds stack clean --yes [--volumes] [--global] lds stack diff [--config] [--json] @@ -155,23 +155,51 @@ Shortcuts:: lds notify ... lds ui -Tools Control Plane -------------------- +Execution and Shells +-------------------- -:: +LocalDevStack has four execution surfaces with intentionally different roles. - lds tools sh - lds tools exec "" - lds tools file +Application/domain-aware execution:: + + lds core + lds core + lds core [--] [args...] + +``core`` is the ergonomic application entry point. A domain is resolved through +Tools to its application container and working directory. Node applications use +``/app``. Other applications use the resolved document root when available, then +``/app``, then ``/``. With no target, ``core`` discovers domains and prompts when +interactive. + +Generic service/container execution:: -Open a generic container shell or run a command:: + lds cli + lds cli [--] [args...] - lds cli - lds cli +``cli`` prefers an exact service in the current Compose project. If there is no +matching service, an exact Docker container name or ID is accepted. No implicit +case conversion is performed. -Resolve a domain/container to its application shell:: +Compose-service-only execution:: + + lds stack exec + lds stack exec [--] [args...] + +The top-level ``lds exec`` alias has the same contract. This surface intentionally +accepts services only; use ``cli`` for an arbitrary exact container. + +Server-tools-specific execution:: + + lds tools sh + lds tools exec [--] [args...] + lds tools file - lds core [domain|container] +All explicit command forms preserve argv exactly; normal commands are not joined +into a shell string. ``--`` may be used to separate LDS arguments from the +container command. Interactive shells receive a TTY, while piped/non-interactive +commands do not force one. Bash is preferred for interactive shells with ``sh`` +as the fallback. Secrets ------- From 6a3f7b8f5f9a665c78fb5ec6ec59815e8fbfa73a Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:51:18 +0600 Subject: [PATCH 035/106] docs(cli): add execution surface examples --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 43ba0073..4280110d 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,29 @@ lds clean --global --yes That global mode can remove unrelated stopped containers, unused images/networks, build cache, and optionally volumes. `lds down --volumes --yes` is also destructive and should not be used for normal upgrades. +## Execution and shells + +Use the execution surface that matches the target you already know: + +```bash +# Application/domain aware; resolves container + working directory. +lds core project.localhost +lds core project.localhost -- php artisan about + +# Generic current-project service or exact Docker container. +lds cli php84 +lds cli php84 -- php -v + +# Compose-service only (top-level alias: lds exec). +lds stack exec redis -- redis-cli ping + +# server-tools only. +lds tools sh +lds tools exec -- jq --version +``` + +Explicit commands preserve argv rather than being flattened into a shell string. Interactive shells receive a TTY; piped/non-interactive commands do not force one. See `docs/reference/cli.rst` for target resolution and exit-code details. + ## Ad-hoc Dockerfile runner From a directory containing a Dockerfile: From 813b2a1f3023c0f7f569628ee88f899cb05925e3 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:51:43 +0600 Subject: [PATCH 036/106] test(docs): lock unified execution reference --- tests/docs-contract.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index 1dbb172a..f67123fb 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -145,11 +145,22 @@ pass "documentation toctree targets exist" help_md="$("$ROOT/lds" help --markdown)" -for required in 'lds profiles add ' 'lds support trace ' 'lds support bundle [--redact|--full] [output.zip]' 'lds cli [cmd...]' 'lds run shell|ps|logs|stop|rm|open' 'MongoDB:' 'Elasticsearch:'; do +for required in 'lds profiles add ' 'lds support trace ' 'lds support bundle [--redact|--full] [output.zip]' 'lds cli [--] [command...]' 'lds core [domain|service|container] [--] [command...]' 'lds stack exec [--] [command...]' 'lds tools exec [--] [args...]' 'lds run shell|ps|logs|stop|rm|open' 'MongoDB:' 'Elasticsearch:'; do assert_contains "$help_md" "$required" done pass "embedded CLI help covers documented command groups" +assert_file_contains "$cli" 'Execution and Shells' +assert_file_contains "$cli" 'lds core [--] [args...]' +assert_file_contains "$cli" 'lds cli [--] [args...]' +assert_file_contains "$cli" 'lds stack exec [--] [args...]' +assert_file_contains "$cli" 'lds tools exec [--] [args...]' +assert_file_contains "$cli" 'preserve argv exactly' +assert_file_contains "$readme" '## Execution and shells' +assert_file_contains "$readme" 'lds cli php84 -- php -v' +assert_file_contains "$readme" 'lds stack exec redis -- redis-cli ping' +pass "execution-surface docs match the shared Core/CLI contract" + for stale in LDS_TOOLS_IMAGE LDS_RUNNER_IMAGE LDS_NGINX_IMAGE LDS_APACHE_IMAGE; do if grep -RqsF "$stale" "$ROOT/README.md" "$ROOT/docs" --exclude-dir=plans; then From 3d67eae4ae230e194a3bf170df57496b387a3730 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:52:01 +0600 Subject: [PATCH 037/106] docs(plan): update release hardening tracker --- docs/plans/lds-core-cli-hardening-plan.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index 33adef6e..c1cf9e28 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -434,10 +434,12 @@ Exit criterion: execution behavior is stable across supported host environments. ## Batch 7 — documentation and release hardening -- [ ] update `docs/reference/cli.rst`; -- [ ] update embedded `lds help` / `help --markdown`; -- [ ] update README examples only where useful; -- [ ] update docs contracts; +Status: **in progress** + +- [x] update `docs/reference/cli.rst`; +- [x] update embedded `lds help` / `help --markdown`; +- [x] update README examples only where useful; +- [x] update docs contracts; - [ ] run ShellCheck/static/contracts; - [ ] run Compose/network/permission contracts; - [ ] run Windows bridge contract; From 5dfacb8f3c79d3bf8a66d414e1a71d1f1884ebd4 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:55:00 +0600 Subject: [PATCH 038/106] fix(cli): correct public command case syntax --- lds | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lds b/lds index 95b23fb4..d5fb0845 100755 --- a/lds +++ b/lds @@ -892,11 +892,7 @@ EOF _is_public_lds_command() { case "${1:-}" in - stack | domain | support | bundle | - up | start | down | stop | restart | reboot | status | ps | logs | exec | events | clean | - config | http | host | setup | profiles | cert | certificate | - doctor | diag | sniff | open | notify | ui | images | urls | - tools | cli | core | secrets | rebuild | run) + stack|domain|support|bundle|up|start|down|stop|restart|reboot|status|ps|logs|exec|events|clean|config|http|host|setup|profiles|cert|certificate|doctor|diag|sniff|open|notify|ui|images|urls|tools|cli|core|secrets|rebuild|run) return 0 ;; esac From 60bb2e7daed3ec71fb33f645e8898f7bbd14dd7c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:55:27 +0600 Subject: [PATCH 039/106] docs(plan): align core CLI hardening status --- docs/plans/lds-core-cli-hardening-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index c1cf9e28..f92f7a19 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -6,7 +6,7 @@ Planning branch: `lds-core-cli/hardening` Repository: `infocyph/LocalDevStack` -Status: **active plan — synchronized with `main`; implementation not started** +Status: **active plan — Batches 0–6 implemented; Batch 7 release validation in progress** This file is the single active LocalDevStack development plan after completion of the Docker ecosystem/integration program. The previous `docs/plans/docker-ecosystem/*` From 7849ee8b9422dcc26124b64ae5ac5ea1cfd3b096 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:57:10 +0600 Subject: [PATCH 040/106] test(cli): satisfy ShellCheck fixture formatting --- tests/container-exec-contract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh index 1b41adde..63452ca2 100644 --- a/tests/container-exec-contract.sh +++ b/tests/container-exec-contract.sh @@ -46,7 +46,7 @@ docker() { "-f|{{.Id}}|stopped") printf '%s\n' cid-stopped ;; "-f|{{.Name}}|cid-stopped") printf '%s\n' /StoppedContainer ;; "-f|{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-custom") printf '%s\n' external-service ;; - "-f|{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '%s\n' ;; + "-f|{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '\n' ;; "-f|{{.State.Running}}|cid-php84") printf '%s\n' true ;; "-f|{{.State.Running}}|cid-nginx") printf '%s\n' true ;; "-f|{{.State.Running}}|cid-custom") printf '%s\n' true ;; From c8e73eb2dee97b03223e61ea491381188059f931 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:57:13 +0600 Subject: [PATCH 041/106] test(cli): satisfy ShellCheck fixture formatting --- tests/execution-contract.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 74b8d164..1ea76ca4 100644 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -65,10 +65,10 @@ run_case() { "{{.Name}}|cid-one") printf '%s\n' /ONE ;; "{{.Name}}|cid-two") printf '%s\n' /TWO ;; "{{.Name}}|cid-stopped") printf '%s\n' /stopped-container ;; - "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-demo") printf '%s\n' ;; - "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-mixed") printf '%s\n' ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-demo") printf '\n' ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-mixed") printf '\n' ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-node") printf '%s\n' node ;; - "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '%s\n' ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '\n' ;; "{{.State.Running}}|cid-demo") printf '%s\n' true ;; "{{.State.Running}}|cid-mixed") printf '%s\n' true ;; "{{.State.Running}}|cid-node") printf '%s\n' true ;; From 98022c38d2f97061affc6628ca6040a7785e4374 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:58:18 +0600 Subject: [PATCH 042/106] fix(core): resolve domains by discovered ownership --- lib/services.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index e96f6db1..97803292 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -895,9 +895,17 @@ _CORE_CONTAINER_NAME='' _CORE_WORKDIR='' _core_is_domain() { - local target="${1:-}" - local re='^([a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.)+(localhost|local|test|loc|[a-zA-Z]{2,})$' - [[ "$target" =~ $re ]] + local target="${1:-}" domain + [[ -n "$target" ]] || return 1 + + # Domain ownership lives in Tools. Only an exact discovered domain is treated + # as a domain here; hostname-shaped service/container names remain valid + # execution targets instead of being guessed from their spelling. + while IFS= read -r domain; do + [[ "$domain" == "$target" ]] && return 0 + done < <(_core_domain_list 2>/dev/null || true) + + return 1 } _core_domain_list() { From cbf252bfcb1c8df141b82eed8b6991c6e88d60a6 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:58:21 +0600 Subject: [PATCH 043/106] test(core): cover domain-like container targets --- tests/execution-contract.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 1ea76ca4..d6d17cf1 100644 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -55,11 +55,13 @@ run_case() { case "${3:-}|${4:-}" in "{{.Id}}|demo-container") printf '%s\n' cid-demo ;; "{{.Id}}|mixedCase-container") printf '%s\n' cid-mixed ;; + "{{.Id}}|worker.local") printf '%s\n' cid-domainlike ;; "{{.Id}}|NODE") printf '%s\n' cid-node ;; "{{.Id}}|stopped-container") printf '%s\n' cid-stopped ;; "{{.Id}}|"*) return 1 ;; "{{.Name}}|cid-demo") printf '%s\n' /demo-container ;; "{{.Name}}|cid-mixed") printf '%s\n' /mixedCase-container ;; + "{{.Name}}|cid-domainlike") printf '%s\n' /worker.local ;; "{{.Name}}|cid-node") printf '%s\n' /NODE ;; "{{.Name}}|cid-php84") printf '%s\n' /PHP84 ;; "{{.Name}}|cid-one") printf '%s\n' /ONE ;; @@ -67,10 +69,12 @@ run_case() { "{{.Name}}|cid-stopped") printf '%s\n' /stopped-container ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-demo") printf '\n' ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-mixed") printf '\n' ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-domainlike") printf '\n' ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-node") printf '%s\n' node ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '\n' ;; "{{.State.Running}}|cid-demo") printf '%s\n' true ;; "{{.State.Running}}|cid-mixed") printf '%s\n' true ;; + "{{.State.Running}}|cid-domainlike") printf '%s\n' true ;; "{{.State.Running}}|cid-node") printf '%s\n' true ;; "{{.State.Running}}|cid-php84") printf '%s\n' true ;; "{{.State.Running}}|cid-stopped") printf '%s\n' false ;; @@ -273,6 +277,20 @@ if grep -Fq 'MIXEDCASE-CONTAINER' "$log"; then fi pass "batch 3: lds core preserves explicit mixed-case container targets" +case_core_domainlike_container() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_core worker.local -- echo ok +} +run_case case_core_domainlike_container +assert_file_contains "$log" 'docker: <-f> <{{.Id}}> ' +assert_file_contains "$log" 'docker: ' +if grep -Fq 'domain-which --app --quiet worker.local' "$log"; then + fail "lds core guessed a hostname-shaped container was a domain" +fi +pass "batch 3: lds core distinguishes discovered domains from hostname-shaped containers" + case_core_single_domain() { _core_domain_list() { printf '%s\n' app.local; } cmd_core From 4a098eab97a3d7918293a8c8b110a57da0e2d7fc Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:58:52 +0600 Subject: [PATCH 044/106] test(cli): align routing contract with valid case syntax --- tests/cli-contract.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cli-contract.sh b/tests/cli-contract.sh index c71ee19a..90a76325 100755 --- a/tests/cli-contract.sh +++ b/tests/cli-contract.sh @@ -54,8 +54,8 @@ assert_file_contains "$ROOT/lds" '_is_public_lds_command()' if grep -Fq 'declare -F "cmd_$cmd"' "$ROOT/lds"; then fail "top-level dispatch still exposes arbitrary cmd_* functions dynamically" fi -assert_file_contains "$ROOT/lds" 'stack | domain | support | bundle |' -assert_file_contains "$ROOT/lds" 'tools | cli | core | secrets | rebuild | run)' +assert_file_contains "$ROOT/lds" 'stack|domain|support|bundle|up|start' +assert_file_contains "$ROOT/lds" 'tools|cli|core|secrets|rebuild|run)' pass "top-level LDS command routing is explicit and collision-safe" graphify_log="$(mktemp)" From 9a85ac700f9698548eab0ad1203008ed67b68e98 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 22:59:44 +0600 Subject: [PATCH 045/106] docs(plan): record completed CLI and Windows review --- docs/plans/lds-core-cli-hardening-plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md index f92f7a19..af9794b8 100644 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ b/docs/plans/lds-core-cli-hardening-plan.md @@ -442,8 +442,8 @@ Status: **in progress** - [x] update docs contracts; - [ ] run ShellCheck/static/contracts; - [ ] run Compose/network/permission contracts; -- [ ] run Windows bridge contract; -- [ ] final full CLI surface review; +- [x] run Windows bridge contract; +- [x] final full CLI surface review; - [ ] remove this plan when every item is complete. Exit criterion: all checks green and no completed planning artifact remains. From d48f033d2310b51c77212c50a2f299c01e092cb5 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:02:48 +0600 Subject: [PATCH 046/106] fix(cli): keep graphify on host command surface --- lds | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lds b/lds index d5fb0845..3bc81041 100755 --- a/lds +++ b/lds @@ -892,7 +892,7 @@ EOF _is_public_lds_command() { case "${1:-}" in - stack|domain|support|bundle|up|start|down|stop|restart|reboot|status|ps|logs|exec|events|clean|config|http|host|setup|profiles|cert|certificate|doctor|diag|sniff|open|notify|ui|images|urls|tools|cli|core|secrets|rebuild|run) + stack|domain|support|bundle|up|start|down|stop|restart|reboot|status|ps|logs|exec|events|clean|config|http|host|setup|profiles|cert|certificate|doctor|diag|sniff|open|notify|ui|images|urls|tools|cli|core|graphify|secrets|rebuild|run) return 0 ;; esac From 6634fe62840a8ee60b2cbb773896d4c147c93b8a Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:02:51 +0600 Subject: [PATCH 047/106] test(cli): lock host graphify routing --- tests/cli-contract.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/cli-contract.sh b/tests/cli-contract.sh index 90a76325..586e9554 100755 --- a/tests/cli-contract.sh +++ b/tests/cli-contract.sh @@ -55,9 +55,14 @@ if grep -Fq 'declare -F "cmd_$cmd"' "$ROOT/lds"; then fail "top-level dispatch still exposes arbitrary cmd_* functions dynamically" fi assert_file_contains "$ROOT/lds" 'stack|domain|support|bundle|up|start' -assert_file_contains "$ROOT/lds" 'tools|cli|core|secrets|rebuild|run)' +assert_file_contains "$ROOT/lds" 'tools|cli|core|graphify|secrets|rebuild|run)' pass "top-level LDS command routing is explicit and collision-safe" +if PATH="$tmpbin:$PATH" "$ROOT/lds" graphify --help 2>&1 | grep -Fq 'SERVER_TOOLS is not running'; then + fail "top-level graphify incorrectly fell through to tool-runner" +fi +pass "top-level Graphify remains a host-side command" + graphify_log="$(mktemp)" cat >"$tmpbin/graphify" <<'SH' #!/usr/bin/env sh From dfe5535aabc9a5eddc32a14fa37fd2cb88b745ed Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:07:18 +0600 Subject: [PATCH 048/106] chore(test): restore execution contract executable bit --- tests/execution-contract.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tests/execution-contract.sh diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh old mode 100644 new mode 100755 From 6789c0ae39795c9e9f6f8a49240606b7795785f4 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:08:01 +0600 Subject: [PATCH 049/106] chore(test): make container execution contract executable --- tests/container-exec-contract.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tests/container-exec-contract.sh diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh old mode 100644 new mode 100755 From 3d2c92af3fa07c10c59570b75ffd849477a526af Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:08:32 +0600 Subject: [PATCH 050/106] test(core): align discovered domain fixture --- tests/execution-contract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index d6d17cf1..9f0ac019 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -90,7 +90,7 @@ run_case() { if [[ "${1:-}" == exec ]]; then case " $* " in *" domain-which --list-domains "*) - printf '%s\n' app.local php.local + printf '%s\n' app.local php.local fallback.local ;; *" domain-which --app --quiet app.local "*) printf '%s\n' node From 9eb3ec5fbd14928d9dfdd03bc45dffe561fb1db5 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:10:26 +0600 Subject: [PATCH 051/106] test(cli): align execution routing baseline --- tests/execution-contract.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 9f0ac019..f74fdfb3 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -398,7 +398,8 @@ assert_file_contains "$ROOT/lib/services.sh" 'docker exec -it "$ctr" lazydocker' pass "batch 4: support ui remains a specialized interactive TUI path" assert_file_contains "$ROOT/lds" 'exec) cmd_exec "$@" ;;' -assert_file_contains "$ROOT/lds" 'cmd_stack "$@"' -pass "baseline: grouped stack exec and top-level dispatch remain wired" +assert_file_contains "$ROOT/lds" 'if _is_public_lds_command "$cmd"; then' +assert_file_contains "$ROOT/lds" '"cmd_$cmd" "$@"' +pass "baseline: grouped stack exec and allowlisted top-level dispatch remain wired" printf 'Execution contract complete.\n' From 2c8babbec5ce037348d09e3c928f216dd84fb8e6 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:14:04 +0600 Subject: [PATCH 052/106] feat(cli): harden interactive execution semantics --- lib/container-exec.sh | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/container-exec.sh b/lib/container-exec.sh index 5496669a..7e7caab6 100644 --- a/lib/container-exec.sh +++ b/lib/container-exec.sh @@ -125,6 +125,10 @@ _container_exec_flags() { case "$mode" in shell) + if ! _container_stdin_is_tty || ! _container_stdout_is_tty; then + err "Interactive container session requires a TTY" + return 64 + fi _CONTAINER_EXEC_FLAGS=(-it) ;; command) @@ -168,6 +172,33 @@ _container_exec_argv() { _container_docker "${args[@]}" } +_container_exec_interactive_argv() { + local target="${1:-}" workdir='' + shift || true + + if [[ "${1:-}" == --workdir ]]; then + workdir="${2:-}" + [[ -n "$workdir" ]] || { + err "--workdir requires a path" + return 64 + } + shift 2 + fi + [[ "${1:-}" == -- ]] && shift + (($# > 0)) || { + err "Interactive container command is required" + return 64 + } + + _container_require_running "$target" || return $? + _container_exec_flags shell || return $? + + local -a args=(exec "${_CONTAINER_EXEC_FLAGS[@]}") + [[ -n "$workdir" ]] && args+=(--workdir "$workdir") + args+=("$target" "$@") + _container_docker "${args[@]}" +} + _container_first_existing_dir() { local target="${1:-}" shift || true @@ -212,9 +243,9 @@ _container_open_shell() { return 64 } + _container_exec_flags shell || return $? local shell shell="$(_container_shell_name "$target")" || return $? - _container_exec_flags shell || return $? local -a args=(exec "${_CONTAINER_EXEC_FLAGS[@]}") [[ -n "$workdir" ]] && args+=(--workdir "$workdir") From f62c2e01b19114a3ca3f1e5a8acf24d449b60fb8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:14:08 +0600 Subject: [PATCH 053/106] feat(cli): add explicit tools shell execution --- lib/services.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index 97803292..d8cd939b 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -513,7 +513,7 @@ cmd_ui() { local ctr ctr="$(_project_tools_container_running || true)" [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" - docker exec -it "$ctr" lazydocker + _container_exec_interactive_argv "$ctr" -- lazydocker } # ───────────────────────────────────────────────────────────────────────────── @@ -849,6 +849,13 @@ cmd_tools() { } _container_exec_argv "$ctr" -- "$@" ;; + shell-exec) + (($# == 1)) || { + err "Usage: lds tools shell-exec " + return 64 + } + _container_exec_argv "$ctr" -- sh -lc "$1" + ;; file) local path="${1:-}" [[ -n "$path" ]] || { @@ -862,7 +869,7 @@ cmd_tools() { ' sh "$path" ;; *) - die "tools " + die "tools " ;; esac } From dc85e880bf9b19ddbf66d144147f2d0dd63f471c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:14:54 +0600 Subject: [PATCH 054/106] test(cli): cover explicit interactive execution --- tests/container-exec-contract.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh index 63452ca2..09d8df5e 100755 --- a/tests/container-exec-contract.sh +++ b/tests/container-exec-contract.sh @@ -136,9 +136,20 @@ _container_exec_flags command [[ "${#_CONTAINER_EXEC_FLAGS[@]}" -eq 0 ]] || fail "non-interactive no-input command received unnecessary flags" pass "non-interactive command without stdin gets no TTY flags" +_container_stdin_is_tty() { return 1; } +_container_stdout_is_tty() { return 1; } +set +e +_container_exec_flags shell >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "non-TTY interactive shell returned $rc instead of 64" +pass "interactive shell rejects non-TTY invocation" + +_container_stdin_is_tty() { return 0; } +_container_stdout_is_tty() { return 0; } _container_exec_flags shell [[ "${_CONTAINER_EXEC_FLAGS[*]}" == "-it" ]] || fail "interactive shell flags drifted" -pass "interactive shell always receives stdin and TTY" +pass "interactive shell receives stdin and TTY when available" # Explicit argv must remain separate Docker arguments, including spaces and shell metacharacters. : >"$log" @@ -149,6 +160,11 @@ _container_exec_argv cid-custom --workdir '/app path' -- printf '%s|%s' 'hello w assert_file_contains "$log" 'exec: <--workdir> <%s|%s> <$(danger)>' pass "shared executor preserves argv and working directory without host interpolation" +: >"$log" +_container_exec_interactive_argv cid-custom --workdir '/app path' -- lazydocker --debug +assert_file_contains "$log" 'exec: <-it> <--workdir> <--debug>' +pass "shared interactive argv helper requires and preserves TTY execution" + : >"$log" _container_open_shell cid-php84 --workdir /app assert_file_contains "$log" 'exec: <-it> <--workdir> <--login>' From a15db30c348072e93add51886a49529a51d2d212 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:14:58 +0600 Subject: [PATCH 055/106] test(cli): cover TTY and shell-exec semantics --- tests/execution-contract.sh | 45 +++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index f74fdfb3..86d4b9a8 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -147,6 +147,11 @@ run_case() { ) } +force_interactive_tty() { + _container_stdin_is_tty() { return 0; } + _container_stdout_is_tty() { return 0; } +} + case_cli_command() { _container_stdin_is_tty() { return 1; } _container_stdout_is_tty() { return 1; } @@ -184,12 +189,26 @@ assert_file_contains "$log" 'docker: <-v>' pass "batch 2: lds cli resolves current-project Compose service names" case_cli_shell() { + force_interactive_tty cmd_cli demo-container } run_case case_cli_shell assert_file_contains "$log" 'docker: <-it> <--login>' pass "batch 2: lds cli without command opens the shared interactive shell" +case_cli_shell_nontty() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + cmd_cli demo-container +} +set +e +run_case case_cli_shell_nontty >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "lds cli non-TTY shell returned $rc instead of 64" +assert_file_contains "$log" 'err:Interactive container session requires a TTY' +pass "batch 6: lds cli rejects interactive shell without a TTY" + set +e run_case cmd_cli stopped-container >/dev/null 2>&1 rc=$? @@ -223,6 +242,7 @@ assert_file_contains "$log" 'err:Usage: lds cli [--] [comman pass "batch 5: lds cli uses standardized usage exit" case_core_node_domain() { + force_interactive_tty cmd_core app.local } run_case case_core_node_domain @@ -231,6 +251,7 @@ assert_file_contains "$log" 'docker: <-it> <--workdir> pass "batch 3: lds core resolves Node domains and opens /app through the shared shell helper" case_core_php_domain() { + force_interactive_tty cmd_core php.local } run_case case_core_php_domain @@ -238,6 +259,7 @@ assert_file_contains "$log" 'docker: <-it> <--workdir> pass "batch 3: lds core preserves resolved PHP document root" case_core_docroot_fallback() { + force_interactive_tty cmd_core fallback.local } run_case case_core_docroot_fallback @@ -267,6 +289,7 @@ assert_file_contains "$log" 'docker: <-v>' pass "batch 3: lds core delegates service targets to the shared resolver" case_core_container() { + force_interactive_tty cmd_core mixedCase-container } run_case case_core_container @@ -292,6 +315,7 @@ fi pass "batch 3: lds core distinguishes discovered domains from hostname-shaped containers" case_core_single_domain() { + force_interactive_tty _core_domain_list() { printf '%s\n' app.local; } cmd_core } @@ -323,6 +347,7 @@ fi pass "batch 4: lds stack exec remains service-only and preserves command argv" case_stack_exec_shell() { + force_interactive_tty cmd_exec PHP84 } run_case case_stack_exec_shell @@ -357,7 +382,18 @@ if grep -Fq ' <-lc> <-lc> ' +pass "batch 4: lds tools shell-exec makes intentional shell parsing explicit" + case_tools_shell() { + force_interactive_tty cmd_tools sh } run_case case_tools_shell @@ -394,8 +430,13 @@ assert_file_contains "$log" ' <-lc>' assert_file_contains "$log" ' ' pass "batch 4: lds tools file passes paths as shell positional argv rather than interpolating them" -assert_file_contains "$ROOT/lib/services.sh" 'docker exec -it "$ctr" lazydocker' -pass "batch 4: support ui remains a specialized interactive TUI path" +case_ui_interactive() { + force_interactive_tty + cmd_ui +} +run_case case_ui_interactive +assert_file_contains "$log" 'docker: <-it> ' +pass "batch 4: support ui uses the shared interactive execution helper" assert_file_contains "$ROOT/lds" 'exec) cmd_exec "$@" ;;' assert_file_contains "$ROOT/lds" 'if _is_public_lds_command "$cmd"; then' From 00c42fe51362eff25af26559db8694d93bbb4e85 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:15:17 +0600 Subject: [PATCH 056/106] docs(cli): expose intentional tools shell execution --- lds | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lds b/lds index 3bc81041..c9c5403b 100755 --- a/lds +++ b/lds @@ -776,6 +776,7 @@ cmd_help() { - `lds stack exec [--] [command...]` *(alias: `lds exec`)* — Compose-service-only execution. - `lds tools sh` — interactive shell in the project `server-tools` container. - `lds tools exec [--] [args...]` — argv-preserving execution in `server-tools`. +- `lds tools shell-exec ` — intentional shell parsing in `server-tools`. - `lds tools file ` — inspect a file inside `server-tools`. ## Secrets @@ -859,7 +860,7 @@ ${CYAN}Execution / Shells:${NC} core [domain|service|container] [--] [command...] Domain/application-aware cli [--] [command...] Generic service/container exec stack exec [--] [command...] Compose-service only - tools sh|exec|file server-tools only + tools sh|exec|shell-exec|file server-tools only ${CYAN}Secrets:${NC} secrets From ff9db47b57cb289391ce3cb37724ba709c233ef8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:15:20 +0600 Subject: [PATCH 057/106] docs(cli): document shell and TTY boundaries --- docs/reference/cli.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index c8a356fd..d890d1b7 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -193,13 +193,16 @@ Server-tools-specific execution:: lds tools sh lds tools exec [--] [args...] + lds tools shell-exec lds tools file All explicit command forms preserve argv exactly; normal commands are not joined into a shell string. ``--`` may be used to separate LDS arguments from the -container command. Interactive shells receive a TTY, while piped/non-interactive -commands do not force one. Bash is preferred for interactive shells with ``sh`` -as the fallback. +container command. Use ``tools shell-exec`` only when shell syntax such as +pipelines, redirections, or compound expressions is intentionally required. +Interactive shells and TUIs require a real TTY; piped/non-interactive commands do +not force one. Bash is preferred for interactive shells with ``sh`` as the +fallback. Secrets ------- From ae21a5d09cddbfee895f7096bebdd675a33bfa7f Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:15:25 +0600 Subject: [PATCH 058/106] docs(cli): clarify explicit shell execution --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4280110d..c316f64d 100644 --- a/README.md +++ b/README.md @@ -362,9 +362,10 @@ lds stack exec redis -- redis-cli ping # server-tools only. lds tools sh lds tools exec -- jq --version +lds tools shell-exec 'printf "%s\n" "hello world" | cat' ``` -Explicit commands preserve argv rather than being flattened into a shell string. Interactive shells receive a TTY; piped/non-interactive commands do not force one. See `docs/reference/cli.rst` for target resolution and exit-code details. +Explicit commands preserve argv rather than being flattened into a shell string. Use `lds tools shell-exec` only for intentional shell syntax. Interactive shells/TUIs require a real TTY; piped/non-interactive commands do not force one. See `docs/reference/cli.rst` for target resolution and exit-code details. ## Ad-hoc Dockerfile runner From cccee2e4f204b9db0dc628acbf29f9118ef20da8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:15:39 +0600 Subject: [PATCH 059/106] test(docs): lock shell execution boundaries --- tests/docs-contract.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index f67123fb..f6c3e6a9 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -145,7 +145,7 @@ pass "documentation toctree targets exist" help_md="$("$ROOT/lds" help --markdown)" -for required in 'lds profiles add ' 'lds support trace ' 'lds support bundle [--redact|--full] [output.zip]' 'lds cli [--] [command...]' 'lds core [domain|service|container] [--] [command...]' 'lds stack exec [--] [command...]' 'lds tools exec [--] [args...]' 'lds run shell|ps|logs|stop|rm|open' 'MongoDB:' 'Elasticsearch:'; do +for required in 'lds profiles add ' 'lds support trace ' 'lds support bundle [--redact|--full] [output.zip]' 'lds cli [--] [command...]' 'lds core [domain|service|container] [--] [command...]' 'lds stack exec [--] [command...]' 'lds tools exec [--] [args...]' 'lds tools shell-exec ' 'lds run shell|ps|logs|stop|rm|open' 'MongoDB:' 'Elasticsearch:'; do assert_contains "$help_md" "$required" done pass "embedded CLI help covers documented command groups" @@ -155,10 +155,13 @@ assert_file_contains "$cli" 'lds core [--] assert_file_contains "$cli" 'lds cli [--] [args...]' assert_file_contains "$cli" 'lds stack exec [--] [args...]' assert_file_contains "$cli" 'lds tools exec [--] [args...]' +assert_file_contains "$cli" 'lds tools shell-exec ' assert_file_contains "$cli" 'preserve argv exactly' +assert_file_contains "$cli" 'require a real TTY' assert_file_contains "$readme" '## Execution and shells' assert_file_contains "$readme" 'lds cli php84 -- php -v' assert_file_contains "$readme" 'lds stack exec redis -- redis-cli ping' +assert_file_contains "$readme" "lds tools shell-exec" pass "execution-surface docs match the shared Core/CLI contract" From 8c716c1e505bdf2fe865bbedb9073629c4a8624b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:15:42 +0600 Subject: [PATCH 060/106] ci(cli): cover execution hardening branches --- .github/workflows/check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 13be8a5b..1e919c90 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,6 +7,8 @@ on: - "plan/**" - "feature/**" - "graphify/**" + - "core-cli/**" + - "lds-core-cli/**" pull_request: schedule: # Related moving latest images refresh weekly; re-run the full compatibility From a79e895fd545dfbff32c67fef0288a46aa15f980 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:17:01 +0600 Subject: [PATCH 061/106] fix(cli): preserve target errors before TTY checks --- lib/container-exec.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/container-exec.sh b/lib/container-exec.sh index 7e7caab6..9c4f2ccf 100644 --- a/lib/container-exec.sh +++ b/lib/container-exec.sh @@ -243,6 +243,7 @@ _container_open_shell() { return 64 } + _container_require_running "$target" || return $? _container_exec_flags shell || return $? local shell shell="$(_container_shell_name "$target")" || return $? From f761cfb1c4255fa515c3268d3b1648c967104208 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:18:07 +0600 Subject: [PATCH 062/106] test(cli): restore TTY for interactive helper fixture --- tests/container-exec-contract.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/container-exec-contract.sh b/tests/container-exec-contract.sh index 09d8df5e..94e749e5 100755 --- a/tests/container-exec-contract.sh +++ b/tests/container-exec-contract.sh @@ -161,6 +161,8 @@ assert_file_contains "$log" 'exec: <--workdir> < pass "shared executor preserves argv and working directory without host interpolation" : >"$log" +_container_stdin_is_tty() { return 0; } +_container_stdout_is_tty() { return 0; } _container_exec_interactive_argv cid-custom --workdir '/app path' -- lazydocker --debug assert_file_contains "$log" 'exec: <-it> <--workdir> <--debug>' pass "shared interactive argv helper requires and preserves TTY execution" From e045b1b431a9e524ba294ad5674a772f8eed4219 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:19:37 +0600 Subject: [PATCH 063/106] test(docs): require completed plans to be retired --- tests/docs-contract.sh | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index f6c3e6a9..b27710ed 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -20,9 +20,8 @@ runner="$ROOT/docs/guides/ad-hoc-runner.rst" notify="$ROOT/docs/guides/notifications.rst" secrets="$ROOT/docs/guides/secrets-sops-age.rst" cli="$ROOT/docs/reference/cli.rst" -plan="$ROOT/docs/plans/lds-core-cli-hardening-plan.md" -for file in "$index" "$readme" "$quick" "$arch" "$profiles" "$storage" "$domain" "$tls" "$ai" "$databases" "$ops" "$runner" "$notify" "$secrets" "$cli" "$plan"; do +for file in "$index" "$readme" "$quick" "$arch" "$profiles" "$storage" "$domain" "$tls" "$ai" "$databases" "$ops" "$runner" "$notify" "$secrets" "$cli"; do assert_file "$file" done @@ -173,13 +172,7 @@ done assert_file_contains "$ai" 'Both provider definitions live in ``docker/compose/companion.yaml``' pass "docs reflect fixed infrastructure images and ephemeral AI overrides" -assert_file_contains "$plan" 'lds cli' -assert_file_contains "$plan" 'lds core' -assert_file_contains "$plan" 'shared container execution substrate' -assert_file_contains "$plan" 'preserve argv' -assert_file_contains "$plan" 'adaptive `docker exec` flags' -assert_file_contains "$plan" 'Windows/Git Bash' -assert_file_contains "$plan" 'remove this plan when every item is complete' -[[ ! -d "$ROOT/docs/plans/docker-ecosystem" ]] || - fail "completed docker-ecosystem planning directory still exists" -pass "active Core/CLI hardening plan is canonical and completed ecosystem plans are retired" +if [[ -d "$ROOT/docs/plans" ]] && find "$ROOT/docs/plans" -type f -print -quit | grep -q .; then + fail "completed planning artifact remains under docs/plans" +fi +pass "completed LocalDevStack planning artifacts are retired" From 444a407198c4e563ccc22ed23cd9e5deeca6a04d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:19:43 +0600 Subject: [PATCH 064/106] docs: remove completed core CLI hardening plan --- docs/plans/lds-core-cli-hardening-plan.md | 510 ---------------------- 1 file changed, 510 deletions(-) delete mode 100644 docs/plans/lds-core-cli-hardening-plan.md diff --git a/docs/plans/lds-core-cli-hardening-plan.md b/docs/plans/lds-core-cli-hardening-plan.md deleted file mode 100644 index af9794b8..00000000 --- a/docs/plans/lds-core-cli-hardening-plan.md +++ /dev/null @@ -1,510 +0,0 @@ -# LocalDevStack — Core & CLI Execution Surface Hardening Plan - -## Status - -Planning branch: `lds-core-cli/hardening` - -Repository: `infocyph/LocalDevStack` - -Status: **active plan — Batches 0–6 implemented; Batch 7 release validation in progress** - -This file is the single active LocalDevStack development plan after completion of the -Docker ecosystem/integration program. The previous `docs/plans/docker-ecosystem/*` -plans are complete and intentionally removed. - -## Objective - -Make `lds core`, `lds cli`, `lds stack exec`, and `lds tools` feel like one coherent -execution system rather than four independently evolved Docker-shell paths. - -The final design should preserve the convenience of the existing commands while -eliminating: - -- duplicated container-running checks; -- duplicated Bash/sh fallback logic; -- forced TTY allocation for non-interactive commands; -- command flattening through `$*`; -- raw container-name assumptions; -- implicit uppercasing of container targets; -- domain/container/service ambiguity; -- behavior drift between `core`, `cli`, `stack exec`, and `tools exec`. - -The host CLI remains the control plane. Docker remains an implementation detail. - ---- - -# 1. Current behavior - -## 1.1 `lds cli` - -Current contract: - -```text -lds cli -lds cli -``` - -Today it: - -1. requires an exact Docker container identifier/name; -2. checks that the container exists and is running; -3. always invokes `docker exec -it`; -4. joins command arguments with `$*`; -5. runs explicit commands through a login Bash shell when Bash exists; -6. falls back to `sh` otherwise. - -This works interactively, but the command path is not argv-safe and is awkward for -piped/non-TTY automation. - -## 1.2 `lds core` - -Current contract: - -```text -lds core -lds core -lds core -``` - -Today it: - -- discovers domains through `domain-which`; -- prompts interactively when no target is supplied; -- resolves a domain to application/container/docroot; -- forces Node applications to `/app`; -- otherwise uses the resolved document root with `/app`/root fallback; -- treats a non-domain target as a container name; -- uppercases raw container targets before `docker exec`; -- opens a shell only; it does not have a proper argv-preserving command mode. - -## 1.3 Overlapping execution surfaces - -The repository also has: - -```text -lds stack exec [cmd...] -lds exec [cmd...] -lds tools sh -lds tools exec "" -``` - -Each path currently owns part of the same problem: - -- resolve a target; -- require a running container; -- determine interactive vs non-interactive execution; -- choose Bash or sh; -- optionally select a working directory; -- preserve command arguments; -- choose Docker/Compose execution semantics. - -That shared substrate should exist once. - ---- - -# 2. Design decisions - -## 2.1 Command roles - -### `lds cli` — generic container/service execution - -`cli` is the low-level execution surface. - -Target contract: - -```text -lds cli -lds cli -- [args...] -lds cli [args...] -``` - -A target may be: - -1. a service in the current Compose project; -2. an exact running container name/ID. - -Resolution must be deterministic and scoped to the current LocalDevStack project before -falling back to an explicit Docker container. - -No implicit case conversion. - -When no command is supplied, open an interactive shell. - -When a command is supplied, preserve argv exactly. Do not flatten the command through -`$*`. - -### `lds core` — application/domain-aware execution - -`core` remains the ergonomic application entry point. - -Target contract: - -```text -lds core -lds core -lds core -- [args...] -lds core -lds core -- [args...] -``` - -Rules: - -- no target: discover domains and prompt when interactive; -- one discovered domain: select it automatically; -- non-TTY with multiple domains: print the stable domain list and fail with actionable usage; -- domain target: resolve application/container/docroot through Tools; -- Node target: working directory `/app`; -- other domain target: resolved docroot, then `/app`, then `/` fallback; -- service/container target: use the same resolver as `lds cli`; -- explicit command: execute in the resolved application working directory; -- no command: open the application shell there. - -`core` must delegate execution rather than owning Docker shell mechanics itself. - -## 2.2 Preserve `stack exec` - -`lds stack exec` remains the clearly Compose-service-oriented form. - -It should use the same internal argv/TTY/shell helpers as `cli`, while keeping its -service-only contract. - -## 2.3 Preserve `tools` - -`lds tools` remains server-tools-specific: - -```text -lds tools sh -lds tools exec ... -lds tools file ... -``` - -It should reuse the common container execution layer where applicable instead of carrying -another independent shell implementation. - -## 2.4 TTY rules - -TTY behavior must be based on the actual operation: - -- interactive shell: `-it`; -- interactive explicit command with terminal stdin/stdout: `-it` where appropriate; -- piped/non-interactive command: `-i`, never force `-t`; -- command that needs neither stdin nor TTY: no unnecessary TTY requirement. - -Windows/Git Bash, WSL, Linux and Docker Desktop behavior must remain covered. - -## 2.5 Command safety - -Explicit command execution must be argv-preserving. - -Avoid: - -```bash -local cmd="$*" -sh -lc "$cmd" -``` - -for ordinary command forwarding. - -If a shell-expression mode is retained for compatibility, it must be explicit and -separate from normal argv execution. - ---- - -# 3. Internal execution architecture - -Introduce one private execution substrate in the host CLI layer. - -Provisional responsibilities: - -```text -_container_resolve_target -_container_require_running -_container_exec_flags -_container_exec_argv -_container_open_shell -_container_exec_in_dir -_core_domain_list -_core_domain_resolve -``` - -Exact names may change during implementation; responsibilities should not. - -## 3.1 Target resolver - -The common resolver should return canonical information rather than only a string. - -Conceptually: - -```text -requested target - ↓ -current Compose service? - ↓ no -exact Docker container? - ↓ -canonical container id/name -``` - -Requirements: - -- prefer current-project Compose service resolution; -- do not accidentally select a similarly named container from another project; -- exact explicit container names/IDs remain supported; -- return useful errors for stopped/missing/ambiguous targets. - -## 3.2 Shell resolver - -Interactive shell behavior: - -1. Bash when available; -2. sh otherwise. - -The helper should support an optional working directory without embedding untrusted -paths into a command string. - -## 3.3 Domain resolver - -`core` should own domain semantics, not Docker mechanics. - -The resolver should return: - -```text -domain -application type -container target -working directory -``` - -The current repeated `domain-which` calls should be wrapped behind one LocalDevStack -helper so failure handling and diagnostics are centralized. - -Do not move domain ownership out of Tools; LocalDevStack only consumes the control-plane -result. - ---- - -# 4. Current findings to address - -| Area | Current issue | Target | -| --- | --- | --- | -| `cli` target | exact raw container only | Compose service + exact container | -| `cli` command | `$*` flattened into shell string | preserve argv | -| `cli` TTY | always `-it` | adaptive TTY | -| `core` raw target | uppercases container name | canonical resolver | -| `core` command mode | shell only | shell or command | -| `core` domain checks | repeated Tools/container checks | shared domain resolver | -| `stack exec` | separate execution mechanics | shared substrate | -| `tools exec` | separate shell-string mechanics | shared substrate | -| shell fallback | repeated Bash/sh snippets | one helper | -| errors | command-specific wording/behavior | consistent target diagnostics | -| tests | surfaces tested indirectly | explicit execution matrix | -| docs/help | terse semantics | clearly distinguish cli/core/stack exec/tools | - ---- - -# 5. Compatibility policy - -Preserve these existing forms: - -```text -lds cli -lds cli -lds core -lds core -lds core -lds stack exec [command...] -lds exec [command...] -lds tools sh -lds tools exec ... -``` - -Behavior may become stricter only where the current behavior is unsafe or ambiguous. - -Specifically: - -- stop uppercasing arbitrary `core` targets; -- stop forcing TTYs for non-interactive commands; -- stop flattening normal command argv; -- reject ambiguous targets rather than guessing. - -No dependency on project programming language should be introduced. - ---- - -# 6. Implementation batches - -## Batch 0 — baseline and command contract - -Status: **complete** - -- [x] inventory `core`, `cli`, `stack exec`, `tools`, and top-level dispatch; -- [x] identify duplicated execution responsibilities; -- [x] identify argv/TTY/container-resolution gaps; -- [x] define command roles and compatibility policy; -- [x] add explicit baseline tests for current supported invocation forms before refactor. - -Exit criterion: current supported behavior is captured by tests. - -## Batch 1 — shared container execution substrate - -Status: **implemented — CI validation pending** - -- [x] implement current-project service/container resolution; -- [x] implement running-container validation; -- [x] implement adaptive `docker exec` flags; -- [x] implement argv-preserving execution; -- [x] implement Bash/sh interactive shell selection; -- [x] implement optional working-directory execution; -- [x] add unit/contract fixtures for resolver and TTY behavior. - -Exit criterion: no user-facing command needs its own Docker shell mechanics. - -## Batch 2 — harden `lds cli` - -Status: **implemented — CI validation pending** - -- [x] move `cmd_cli` onto the shared execution substrate; -- [x] support current-project Compose service names; -- [x] preserve explicit container names/IDs; -- [x] remove forced uppercase/case assumptions; -- [x] preserve argv exactly for explicit commands; -- [x] support piped/non-TTY commands without `-t`; -- [x] keep no-command interactive shell behavior; -- [x] add missing/stopped/ambiguous target tests. - -Exit criterion: `cli` is the reliable low-level execution command. - -## Batch 3 — harden `lds core` - -Status: **implemented — CI validation pending** - -- [x] centralize domain discovery; -- [x] centralize domain -> app/container/docroot resolution; -- [x] preserve stable interactive domain picker; -- [x] preserve useful non-TTY domain listing; -- [x] delegate service/container fallback to the common resolver; -- [x] remove raw target uppercasing; -- [x] add explicit command execution after domain/container target; -- [x] preserve Node `/app` behavior; -- [x] preserve document-root fallback behavior; -- [x] test PHP, Node, service, container and non-TTY flows. - -Exit criterion: `core` is application-aware but contains no duplicate Docker execution logic. - -## Batch 4 — unify adjacent execution surfaces - -Status: **implemented — CI validation pending** - -- [x] move `cmd_exec` / `stack exec` onto shared helpers without changing its service-only UX; -- [x] move `tools sh` onto shared shell helper; -- [x] move `tools exec` away from unsafe command flattening where compatibility allows; -- [x] review `cmd_ui`/other direct `docker exec` users for helper reuse where relevant; -- [x] keep specialized commands specialized; do not over-abstract unrelated Docker operations. - -Exit criterion: interactive/command execution semantics are consistent across LDS. - -## Batch 5 — CLI routing and UX cleanup - -Status: **implemented — CI validation pending** - -- [x] review top-level dynamic `cmd_$cmd` routing for discoverability and collision safety; -- [x] ensure canonical grouped commands and shortcuts remain intentional; -- [x] standardize usage/error text; -- [x] standardize exit codes for missing target, unknown target, stopped target and no-TTY prompt; -- [x] make `--` command separation work consistently; -- [x] ensure help output clearly explains `core` vs `cli` vs `stack exec` vs `tools`. - -Exit criterion: users can predict which execution command to use without knowing LDS internals. - -## Batch 6 — cross-platform and automation hardening - -Status: **implemented — CI validation pending** - -- [x] Linux interactive shell coverage; -- [x] Linux piped stdin/non-TTY coverage; -- [x] Windows/Git Bash TTY/path-conversion compatibility; -- [x] WSL behavior review; -- [x] Docker Desktop/Windows bridge behavior review; -- [x] paths containing spaces; -- [x] commands containing spaces/quotes/shell metacharacters as argv; -- [x] SIGINT/exit-code propagation; -- [x] no accidental host shell interpolation. - -Exit criterion: execution behavior is stable across supported host environments. - -## Batch 7 — documentation and release hardening - -Status: **in progress** - -- [x] update `docs/reference/cli.rst`; -- [x] update embedded `lds help` / `help --markdown`; -- [x] update README examples only where useful; -- [x] update docs contracts; -- [ ] run ShellCheck/static/contracts; -- [ ] run Compose/network/permission contracts; -- [x] run Windows bridge contract; -- [x] final full CLI surface review; -- [ ] remove this plan when every item is complete. - -Exit criterion: all checks green and no completed planning artifact remains. - ---- - -# 7. Required regression matrix - -At minimum cover: - -| Command | Interactive | Non-interactive | Domain | Service | Container | -| --- | --- | --- | --- | --- | --- | -| `lds cli` | yes | yes | n/a | yes | yes | -| `lds core` | yes | yes | yes | yes | yes | -| `lds stack exec` | yes | yes | n/a | yes | n/a | -| `lds tools` | yes | yes | n/a | server-tools | server-tools | - -Also verify: - -- stopped target; -- missing target; -- ambiguous service/container naming; -- one-domain auto-selection; -- multi-domain no-TTY output; -- PHP domain working directory; -- Node domain `/app`; -- Bash unavailable -> sh; -- stdin pipe reaches child command; -- child exit status reaches caller; -- `Ctrl-C` reaches interactive child; -- command arguments are not reinterpreted by the host shell. - ---- - -# 8. Non-goals - -Do not use this work to: - -- redesign Compose service definitions; -- move domain ownership out of docker-tools; -- introduce a new programming-language/runtime dependency; -- replace Docker Compose; -- merge unrelated service lifecycle commands; -- remove existing command aliases merely for aesthetic cleanup; -- turn every direct Docker call into a generic abstraction. - -The goal is a coherent execution surface, not abstraction for its own sake. - ---- - -# 9. Completion definition - -This plan is complete when: - -1. `lds cli` is the dependable low-level service/container execution surface; -2. `lds core` is the dependable application/domain execution surface; -3. both support correct interactive and non-interactive behavior; -4. argv, stdin, TTY, exit codes and working directories are preserved correctly; -5. `stack exec` and `tools` reuse the same execution substrate where appropriate; -6. current-project scoping prevents accidental cross-project container selection; -7. help/docs make the command boundaries obvious; -8. Linux/Windows/WSL contracts are green; -9. no completed predecessor plan files remain; -10. this plan itself is removed after implementation is fully released. From 2b58200ae794a066987f4c2118256fad4c077b9f Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:40:42 +0600 Subject: [PATCH 065/106] docs(plan): add LDS shell unification tracker --- docs/plans/lds-shell-unification-plan.md | 277 +++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 docs/plans/lds-shell-unification-plan.md diff --git a/docs/plans/lds-shell-unification-plan.md b/docs/plans/lds-shell-unification-plan.md new file mode 100644 index 00000000..3894f3de --- /dev/null +++ b/docs/plans/lds-shell-unification-plan.md @@ -0,0 +1,277 @@ +# LDS Unified Shell Command Plan + +Status: **active — Batch 0 contract finalization and Batch 1 resolver implementation** + +## Objective + +Introduce `lds shell` as the single human-facing execution/navigation entry point for LocalDevStack while keeping `lib/container-exec.sh` as the shared execution substrate. + +The command must reduce the need to choose between `core`, `cli`, `stack exec`, `exec`, and the execution-oriented parts of `tools`. + +## Canonical command surface + +```text +lds shell +lds shell +lds shell -- [args...] +lds shell --shell +lds shell --interactive [args...] +``` + +No-argument `lds shell` is the only form that opens the grouped selector. + +## No-argument selector + +`lds shell` with no parameters builds one numbered catalog grouped by category: + +1. Applications / Domains +2. Application Directories +3. Services +4. Containers +5. Utilities + +The selector accepts either: + +- the global numeric item number; or +- an exact item name. + +When a name occurs in more than one category, the selector must not guess. It reports the ambiguity and accepts a qualified selector such as: + +```text +domain:project.localhost +app:project +service:php84 +container:localdevstack-php84-1 +tools +``` + +The catalog must be stable/sorted inside each group. + +With no TTY, `lds shell` prints the grouped catalog and exits with actionable usage instead of blocking for input. + +## Explicit target resolution + +For `lds shell `, unqualified targets resolve in this exact order: + +```text +1. exact discovered domain +2. reserved LDS target: tools +3. exact current-project Compose service +4. exact Docker container name/ID +5. exact direct child directory /app/ inside server-tools +6. target-not-found +``` + +No fuzzy matching. +No implicit case conversion. +No hostname-shape guessing. +A value is a domain only when Tools reports it as a discovered domain. + +Qualified targets bypass precedence and resolve only within their requested category. + +## Application-directory fallback + +The `app:` / final unqualified fallback is owned by the current project's `server-tools` container. + +```text +lds shell billing + -> no domain + -> no service + -> no container + -> server-tools:/app/billing exists + -> shell in server-tools with cwd=/app/billing +``` + +Only direct children of `/app` are eligible. Path traversal and slash-containing fallback names are not accepted. + +Do not scan every container for a matching directory. + +## Image boundary + +Image names are intentionally **not** implicitly instantiated. + +`lds shell php:8.4-alpine` must not silently perform `docker run`. + +Image execution remains owned by `lds run` unless a future explicit `lds shell --image ...` contract is designed. + +## Execution behavior + +Resolved targets produce one normalized shell context: + +```text +kind +requested +domain +application +service +container_id +container_name +workdir +``` + +Execution then delegates to the existing substrate. + +- no command -> `_container_open_shell` +- `-- ...` -> `_container_exec_argv` +- `--shell ` -> explicit `sh -lc "$expr"` +- `--interactive ...` -> `_container_exec_interactive_argv` + +Normal argv execution must never be flattened through a shell. + +## TTY and exit-code contract + +Preserve the current hardened behavior: + +- interactive shell/TUI requires real stdin and stdout TTY; +- piped command keeps stdin without forcing TTY; +- ordinary automation receives no unnecessary TTY flags; +- stopped/missing/ambiguous target errors take precedence over TTY errors; +- child exit status and Ctrl-C style exit 130 propagate unchanged. + +Expected LDS control-plane exits: + +```text +64 invalid usage / interactive mode without TTY +65 ambiguous target +66 target not found +69 target exists but is unavailable/stopped +``` + +## Compatibility strategy + +During migration: + +- `lds core` +- `lds cli` +- `lds stack exec` +- `lds exec` +- `lds tools sh` +- `lds tools exec` +- `lds tools shell-exec` + +remain compatible while `lds shell` becomes canonical. + +Old commands must not retain independent Docker execution implementations after migration. They should delegate to shared resolution/execution paths while preserving any intentionally narrower legacy contract, especially service-only `stack exec`. + +`lds tools file` remains under `tools`; it is inspection functionality, not generic shell navigation. + +## Batch tracker + +### Batch 0 — Contract and active-plan setup + +- [x] define canonical `lds shell` syntax; +- [x] define no-argument grouped selector; +- [x] define number/name/qualified selectors; +- [x] define deterministic explicit-target precedence; +- [x] define `/app/` ownership and traversal boundary; +- [x] keep image instantiation out of implicit shell resolution; +- [x] preserve TTY/argv/exit contracts; +- [x] restore an active plan contract under `docs/plans`. + +### Batch 1 — Unified shell context and resolver + +- [ ] introduce normalized shell context state; +- [ ] resolve exact discovered domains; +- [ ] resolve reserved `tools`; +- [ ] resolve exact current-project services; +- [ ] resolve exact containers without emitting premature not-found errors; +- [ ] resolve direct `server-tools:/app/` directories; +- [ ] support qualified `domain:`, `app:`, `service:`, and `container:` targets; +- [ ] add resolver regression coverage. + +### Batch 2 — Grouped interactive selector + +- [ ] build stable grouped catalog; +- [ ] include discovered domains; +- [ ] include direct `/app` directories; +- [ ] include current-project services; +- [ ] include running Docker containers; +- [ ] include `tools` utility; +- [ ] accept global number selector; +- [ ] accept exact name selector; +- [ ] detect duplicate names and require qualification; +- [ ] print catalog + actionable failure without TTY; +- [ ] add selector regression coverage. + +### Batch 3 — `cmd_shell` execution modes + +- [ ] no command opens shell in resolved context; +- [ ] `--` executes exact argv; +- [ ] `--shell` explicitly executes one shell expression; +- [ ] `--interactive` executes argv through the interactive helper; +- [ ] preserve domain/app workdir; +- [ ] add stdin/TTY/exit propagation coverage. + +### Batch 4 — Public routing and help + +- [ ] make `shell` a first-class public LDS command; +- [ ] update embedded help; +- [ ] update README; +- [ ] update CLI reference; +- [ ] update documentation contracts. + +### Batch 5 — Compatibility consolidation + +- [ ] route `core` through unified context behavior where contracts match; +- [ ] route `cli` through unified context behavior where contracts match; +- [ ] preserve service-only `stack exec` compatibility; +- [ ] map `tools sh/exec/shell-exec` to the same execution primitives; +- [ ] ensure no duplicate generic execution implementation remains; +- [ ] decide deprecation-warning policy without breaking automation. + +### Batch 6 — Cross-platform and ambiguity hardening + +- [ ] Linux/WSL behavior; +- [ ] Git Bash/MSYS path conversion; +- [ ] mixed-case exact containers; +- [ ] domain-like container names; +- [ ] service/domain/app name collisions; +- [ ] stopped/missing/ambiguous target precedence; +- [ ] shell/TUI non-TTY rejection; +- [ ] Ctrl-C / child exit propagation. + +### Batch 7 — Full regression and release cleanup + +- [ ] ShellCheck/static; +- [ ] CLI/execution/container substrate contracts; +- [ ] environment/catalog/runtime contracts; +- [ ] networking/wrapper/service/QoL/permissions contracts; +- [ ] Windows bridge; +- [ ] Compose; +- [ ] Graphify minimum/latest; +- [ ] common LLM contract; +- [ ] published image baseline; +- [ ] final full CLI surface review; +- [ ] remove this plan only when every item is complete. + +## Completion definition + +The work is complete when users can rely on: + +```text +lds shell +``` + +as the single discoverable execution/navigation entry point for domains, application directories, services, containers, and Tools, while all legacy execution commands remain either compatibility wrappers or intentionally narrow aliases over the same hardened substrate. + +The final architecture must remain: + +```text +lds shell + | + +-- grouped selector (no args) + | + +-- deterministic resolver + | + +-- domain + +-- tools + +-- service + +-- container + +-- /app directory + | + v + normalized shell context + | + v + lib/container-exec.sh +``` From 3000daa09ca890af04e96020f57ed4f696a4bfc5 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:41:18 +0600 Subject: [PATCH 066/106] test(docs): track active LDS shell plan --- tests/docs-contract.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index b27710ed..82512840 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -20,8 +20,9 @@ runner="$ROOT/docs/guides/ad-hoc-runner.rst" notify="$ROOT/docs/guides/notifications.rst" secrets="$ROOT/docs/guides/secrets-sops-age.rst" cli="$ROOT/docs/reference/cli.rst" +plan="$ROOT/docs/plans/lds-shell-unification-plan.md" -for file in "$index" "$readme" "$quick" "$arch" "$profiles" "$storage" "$domain" "$tls" "$ai" "$databases" "$ops" "$runner" "$notify" "$secrets" "$cli"; do +for file in "$index" "$readme" "$quick" "$arch" "$profiles" "$storage" "$domain" "$tls" "$ai" "$databases" "$ops" "$runner" "$notify" "$secrets" "$cli" "$plan"; do assert_file "$file" done @@ -172,7 +173,12 @@ done assert_file_contains "$ai" 'Both provider definitions live in ``docker/compose/companion.yaml``' pass "docs reflect fixed infrastructure images and ephemeral AI overrides" -if [[ -d "$ROOT/docs/plans" ]] && find "$ROOT/docs/plans" -type f -print -quit | grep -q .; then - fail "completed planning artifact remains under docs/plans" -fi -pass "completed LocalDevStack planning artifacts are retired" +assert_file_contains "$plan" 'LDS Unified Shell Command Plan' +assert_file_contains "$plan" 'Applications / Domains' +assert_file_contains "$plan" 'Application Directories' +assert_file_contains "$plan" 'exact discovered domain' +assert_file_contains "$plan" '/app/' +assert_file_contains "$plan" 'remove this plan only when every item is complete' +plan_count="$(find "$ROOT/docs/plans" -type f | wc -l | tr -d '[:space:]')" +[[ "$plan_count" == 1 ]] || fail "expected exactly one active LocalDevStack plan, found $plan_count" +pass "LDS shell unification plan is the single active planning artifact" From e010e6f6617f1a72f9e5cfa15a3ac4abbfe0b1f4 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:42:05 +0600 Subject: [PATCH 067/106] feat(shell): add unified target context resolver --- lib/services.sh | 205 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/lib/services.sh b/lib/services.sh index d8cd939b..627a0b5e 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -1027,6 +1027,211 @@ _core_domain_resolve() { _CORE_WORKDIR="$(_container_first_existing_dir "$_CORE_CONTAINER_ID" "$preferred" /app /)" || return $? } +_SHELL_TARGET_KIND='' +_SHELL_TARGET_REQUESTED='' +_SHELL_DOMAIN='' +_SHELL_APP='' +_SHELL_SERVICE='' +_SHELL_CONTAINER_ID='' +_SHELL_CONTAINER_NAME='' +_SHELL_WORKDIR='' + +_shell_context_reset() { + _SHELL_TARGET_KIND='' + _SHELL_TARGET_REQUESTED='' + _SHELL_DOMAIN='' + _SHELL_APP='' + _SHELL_SERVICE='' + _SHELL_CONTAINER_ID='' + _SHELL_CONTAINER_NAME='' + _SHELL_WORKDIR='' +} + +_shell_context_from_resolved_container() { + local kind="${1:-container}" requested="${2:-}" + _SHELL_TARGET_KIND="$kind" + _SHELL_TARGET_REQUESTED="$requested" + _SHELL_SERVICE="$_CONTAINER_TARGET_SERVICE" + _SHELL_CONTAINER_ID="$_CONTAINER_TARGET_ID" + _SHELL_CONTAINER_NAME="$_CONTAINER_TARGET_NAME" +} + +_shell_resolve_domain() { + local domain="${1:-}" + [[ -n "$domain" ]] || { + err "Shell domain target is required" + return 64 + } + _core_is_domain "$domain" || { + err "Domain not found: $domain" + return 66 + } + _core_domain_resolve "$domain" || return $? + + _SHELL_TARGET_KIND=domain + _SHELL_TARGET_REQUESTED="$domain" + _SHELL_DOMAIN="$_CORE_DOMAIN" + _SHELL_APP="$_CORE_APP" + _SHELL_SERVICE="$_CONTAINER_TARGET_SERVICE" + _SHELL_CONTAINER_ID="$_CORE_CONTAINER_ID" + _SHELL_CONTAINER_NAME="$_CORE_CONTAINER_NAME" + _SHELL_WORKDIR="$_CORE_WORKDIR" +} + +_shell_resolve_tools() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || { + err "server-tools container is not running for project: $(lds_project)" + return 69 + } + _container_require_running "$ctr" || return $? + + _SHELL_TARGET_KIND=tools + _SHELL_TARGET_REQUESTED=tools + _SHELL_SERVICE=server-tools + _SHELL_CONTAINER_ID="$ctr" + _SHELL_CONTAINER_NAME="$ctr" +} + +_shell_resolve_service() { + local service="${1:-}" + [[ -n "$service" ]] || { + err "Shell service target is required" + return 64 + } + _container_project_service_exists "$service" || { + err "Current-project service not found: $service" + return 66 + } + _container_resolve_target "$service" || return $? + _shell_context_from_resolved_container service "$service" +} + +_shell_exact_container_exists() { + local target="${1:-}" running + [[ -n "$target" ]] || return 1 + running="$(_container_docker inspect -f '{{.State.Running}}' "$target" 2>/dev/null || true)" + [[ "$running" == true || "$running" == false ]] +} + +_shell_resolve_container() { + local target="${1:-}" + [[ -n "$target" ]] || { + err "Shell container target is required" + return 64 + } + _shell_exact_container_exists "$target" || { + err "Container not found: $target" + return 66 + } + _container_resolve_target "$target" || return $? + _shell_context_from_resolved_container container "$target" +} + +_shell_app_name_valid() { + local name="${1:-}" + [[ -n "$name" && "$name" != . && "$name" != .. && "$name" != */* ]] +} + +_shell_resolve_app() { + local name="${1:-}" ctr path + _shell_app_name_valid "$name" || { + err "Application directory must be a direct child name under /app: $name" + return 64 + } + + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || { + err "server-tools container is not running for project: $(lds_project)" + return 69 + } + _container_require_running "$ctr" || return $? + + path="/app/$name" + _container_docker exec "$ctr" sh -c '[ -d "$1" ]' sh "$path" >/dev/null 2>&1 || { + err "Application directory not found: $path" + return 66 + } + + _SHELL_TARGET_KIND=app + _SHELL_TARGET_REQUESTED="$name" + _SHELL_APP="$name" + _SHELL_SERVICE=server-tools + _SHELL_CONTAINER_ID="$ctr" + _SHELL_CONTAINER_NAME="$ctr" + _SHELL_WORKDIR="$path" +} + +_shell_resolve_target() { + local requested="${1:-}" target state + _shell_context_reset + + [[ -n "$requested" ]] || { + err "Shell target is required" + return 64 + } + + case "$requested" in + domain:*) + target="${requested#domain:}" + _shell_resolve_domain "$target" + return $? + ;; + service:*) + target="${requested#service:}" + _shell_resolve_service "$target" + return $? + ;; + container:*) + target="${requested#container:}" + _shell_resolve_container "$target" + return $? + ;; + app:*) + target="${requested#app:}" + _shell_resolve_app "$target" + return $? + ;; + tools | utility:tools) + _shell_resolve_tools + return $? + ;; + esac + + if _core_is_domain "$requested"; then + _shell_resolve_domain "$requested" + return $? + fi + + if _container_project_service_exists "$requested"; then + _shell_resolve_service "$requested" + return $? + fi + + state="$(_container_docker inspect -f '{{.State.Running}}' "$requested" 2>/dev/null || true)" + if [[ "$state" == true || "$state" == false ]]; then + _shell_resolve_container "$requested" + return $? + fi + + if _shell_app_name_valid "$requested"; then + local tools_ctr + tools_ctr="$(_project_tools_container_running || true)" + if [[ -z "$tools_ctr" ]]; then + err "server-tools container is not running for project: $(lds_project)" + return 69 + fi + if _container_docker exec "$tools_ctr" sh -c '[ -d "$1" ]' sh "/app/$requested" >/dev/null 2>&1; then + _shell_resolve_app "$requested" + return $? + fi + fi + + err "Shell target not found: $requested (checked domain, tools, service, container, and /app directory)" + return 66 +} + cmd_core() { local target="${1:-}" [[ -n "$target" ]] && shift || true From 3bca5499c1dcce38f33ede050fcaf3cded04fefb Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:42:48 +0600 Subject: [PATCH 068/106] test(shell): cover unified target resolution --- tests/execution-contract.sh | 94 +++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 86d4b9a8..d643f6f8 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -78,6 +78,12 @@ run_case() { "{{.State.Running}}|cid-node") printf '%s\n' true ;; "{{.State.Running}}|cid-php84") printf '%s\n' true ;; "{{.State.Running}}|cid-stopped") printf '%s\n' false ;; + "{{.State.Running}}|demo-container") printf '%s\n' true ;; + "{{.State.Running}}|mixedCase-container") printf '%s\n' true ;; + "{{.State.Running}}|worker.local") printf '%s\n' true ;; + "{{.State.Running}}|stopped-container") printf '%s\n' false ;; + "{{.State.Running}}|SERVER_TOOLS") printf '%s\n' true ;; + "{{.State.Running}}|missing-shell"|"{{.State.Running}}|php:8.4-alpine") return 1 ;; "{{.State.Running}}|"*) printf '%s\n' true ;; esac return 0 @@ -131,6 +137,12 @@ run_case() { *" cid-php84 test -d /app "*) return 0 ;; + *" SERVER_TOOLS sh -c [ -d \"\$1\" ] sh /app/billing "*) + return 0 + ;; + *" SERVER_TOOLS sh -c [ -d \"\$1\" ] sh /app/missing-shell "*|*" SERVER_TOOLS sh -c [ -d \"\$1\" ] sh /app/php:8.4-alpine "*) + return 1 + ;; *" cid-demo sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-mixed sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-node sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-php84 sh -lc command -v bash >/dev/null 2>&1 "*) return 0 ;; @@ -152,6 +164,88 @@ force_interactive_tty() { _container_stdout_is_tty() { return 0; } } +case_shell_resolve_domain() { + _shell_resolve_target app.local + printf 'shell-context:%s|%s|%s|%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_DOMAIN" "$_SHELL_APP" "$_SHELL_CONTAINER_ID" "$_SHELL_WORKDIR" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_resolve_domain +assert_file_contains "$log" 'shell-context:domain|app.local|node|cid-node|/app' +pass "shell batch 1: exact discovered domain resolves application container and cwd" + +case_shell_resolve_tools() { + _shell_resolve_target tools + printf 'shell-context:%s|%s|%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_SERVICE" "$_SHELL_CONTAINER_ID" "$_SHELL_WORKDIR" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_resolve_tools +assert_file_contains "$log" 'shell-context:tools|server-tools|SERVER_TOOLS|' +pass "shell batch 1: reserved tools target resolves server-tools" + +case_shell_resolve_service() { + _shell_resolve_target php84 + printf 'shell-context:%s|%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_SERVICE" "$_SHELL_CONTAINER_ID" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_resolve_service +assert_file_contains "$log" 'shell-context:service|php84|cid-php84' +pass "shell batch 1: exact current-project service resolves before container fallback" + +case_shell_resolve_container() { + _shell_resolve_target demo-container + printf 'shell-context:%s|%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_CONTAINER_ID" "$_SHELL_CONTAINER_NAME" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_resolve_container +assert_file_contains "$log" 'shell-context:container|cid-demo|demo-container' +pass "shell batch 1: exact container resolves without case rewriting" + +case_shell_resolve_app() { + _shell_resolve_target billing + printf 'shell-context:%s|%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_CONTAINER_ID" "$_SHELL_WORKDIR" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_resolve_app +assert_file_contains "$log" 'shell-context:app|SERVER_TOOLS|/app/billing' +pass "shell batch 1: unresolved target falls back to direct server-tools /app child" + +case_shell_qualified_targets() { + _shell_resolve_target domain:php.local + printf 'qualified:%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_DOMAIN" >>"$EXECUTION_TEST_LOG" + _shell_resolve_target service:node + printf 'qualified:%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_SERVICE" >>"$EXECUTION_TEST_LOG" + _shell_resolve_target container:mixedCase-container + printf 'qualified:%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_CONTAINER_NAME" >>"$EXECUTION_TEST_LOG" + _shell_resolve_target app:billing + printf 'qualified:%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_WORKDIR" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_qualified_targets +assert_file_contains "$log" 'qualified:domain|php.local' +assert_file_contains "$log" 'qualified:service|node' +assert_file_contains "$log" 'qualified:container|mixedCase-container' +assert_file_contains "$log" 'qualified:app|/app/billing' +pass "shell batch 1: qualified selectors bypass normal precedence" + +set +e +run_case _shell_resolve_target app:../escape >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "shell app traversal returned $rc instead of 64" +pass "shell batch 1: /app fallback rejects path traversal" + +set +e +run_case _shell_resolve_target missing-shell >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 66 ]] || fail "missing shell target returned $rc instead of 66" +assert_file_contains "$log" 'err:Shell target not found: missing-shell' +pass "shell batch 1: unresolved targets return one final actionable not-found error" + +set +e +run_case _shell_resolve_target php:8.4-alpine >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 66 ]] || fail "image-like shell target returned $rc instead of 66" +if grep -Fq '' "$log"; then + fail "lds shell resolver attempted to instantiate an image" +fi +pass "shell batch 1: image-like targets are not implicitly instantiated" + case_cli_command() { _container_stdin_is_tty() { return 1; } _container_stdout_is_tty() { return 1; } From a096e26102742a50a8282e82590d61ea6278fffa Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:44:09 +0600 Subject: [PATCH 069/106] feat(shell): add grouped selector and unified execution command --- lib/services.sh | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/lib/services.sh b/lib/services.sh index 627a0b5e..8745ea8c 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -1232,6 +1232,249 @@ _shell_resolve_target() { return 66 } +declare -a _SHELL_MENU_KIND=() +declare -a _SHELL_MENU_NAME=() +declare -a _SHELL_MENU_SELECTOR=() + +_shell_menu_reset() { + _SHELL_MENU_KIND=() + _SHELL_MENU_NAME=() + _SHELL_MENU_SELECTOR=() +} + +_shell_menu_add() { + local kind="${1:-}" name="${2:-}" selector="${3:-}" + [[ -n "$kind" && -n "$name" && -n "$selector" ]] || return 1 + _SHELL_MENU_KIND+=("$kind") + _SHELL_MENU_NAME+=("$name") + _SHELL_MENU_SELECTOR+=("$selector") +} + +_shell_app_list() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || return 0 + + _container_docker exec "$ctr" sh -c ' + for path in /app/*; do + [ -d "$path" ] || continue + basename "$path" + done + ' 2>/dev/null | + sed '/^[[:space:]]*$/d' | + LC_ALL=C sort -u +} + +_shell_service_list() { + docker_compose config --services 2>/dev/null | + sed '/^[[:space:]]*$/d' | + LC_ALL=C sort -u +} + +_shell_container_list() { + _container_docker ps --format '{{.Names}}' 2>/dev/null | + sed '/^[[:space:]]*$/d' | + LC_ALL=C sort -u +} + +_shell_menu_build() { + _shell_menu_reset + local item + + while IFS= read -r item; do + [[ -n "$item" ]] && _shell_menu_add domain "$item" "domain:$item" + done < <(_core_domain_list 2>/dev/null || true) + + while IFS= read -r item; do + [[ -n "$item" ]] && _shell_menu_add app "$item" "app:$item" + done < <(_shell_app_list 2>/dev/null || true) + + while IFS= read -r item; do + [[ -n "$item" ]] && _shell_menu_add service "$item" "service:$item" + done < <(_shell_service_list 2>/dev/null || true) + + while IFS= read -r item; do + [[ -n "$item" ]] && _shell_menu_add container "$item" "container:$item" + done < <(_shell_container_list 2>/dev/null || true) + + if [[ -n "$(_project_tools_container_running || true)" ]]; then + _shell_menu_add utility tools tools + fi +} + +_shell_menu_label() { + case "${1:-}" in + domain) printf '%s' 'Applications / Domains' ;; + app) printf '%s' 'Application Directories' ;; + service) printf '%s' 'Services' ;; + container) printf '%s' 'Containers' ;; + utility) printf '%s' 'Utilities' ;; + *) printf '%s' 'Other' ;; + esac +} + +_shell_menu_print() { + local previous='' kind label i + for ((i = 0; i < ${#_SHELL_MENU_NAME[@]}; i++)); do + kind="${_SHELL_MENU_KIND[$i]}" + if [[ "$kind" != "$previous" ]]; then + label="$(_shell_menu_label "$kind")" + [[ -z "$previous" ]] || printf '\n' >&2 + printf '%b%s%b\n' "$CYAN" "$label" "$NC" >&2 + previous="$kind" + fi + printf ' %2d) %s\n' "$((i + 1))" "${_SHELL_MENU_NAME[$i]}" >&2 + done +} + +_shell_selector_is_tty() { + [[ -t 0 && -t 1 ]] +} + +_shell_menu_match_name() { + local answer="${1:-}" i count=0 match='' + for ((i = 0; i < ${#_SHELL_MENU_NAME[@]}; i++)); do + if [[ "${_SHELL_MENU_NAME[$i]}" == "$answer" ]]; then + ((count += 1)) + match="${_SHELL_MENU_SELECTOR[$i]}" + fi + done + + if ((count == 1)); then + printf '%s' "$match" + return 0 + fi + if ((count > 1)); then + printf '%bAmbiguous name:%b %s\n' "$YELLOW" "$NC" "$answer" >&2 + printf 'Use one of:\n' >&2 + for ((i = 0; i < ${#_SHELL_MENU_NAME[@]}; i++)); do + [[ "${_SHELL_MENU_NAME[$i]}" == "$answer" ]] && + printf ' %s\n' "${_SHELL_MENU_SELECTOR[$i]}" >&2 + done + return 65 + fi + return 66 +} + +_shell_choose_target() { + _shell_menu_build + (("${#_SHELL_MENU_NAME[@]}" > 0)) || { + err "No shell targets are available" + return 66 + } + + _shell_menu_print + + if ! _shell_selector_is_tty; then + err "No TTY to prompt. Use: lds shell " + return 64 + fi + + local answer='' i selector rc + while true; do + read -r -p "Enter number or name: " answer + answer="${answer#"${answer%%[![:space:]]*}"}" + answer="${answer%"${answer##*[![:space:]]}"}" + + if [[ "$answer" =~ ^[0-9]+$ ]]; then + if ((answer >= 1 && answer <= ${#_SHELL_MENU_SELECTOR[@]})); then + printf '%s' "${_SHELL_MENU_SELECTOR[$((answer - 1))]}" + return 0 + fi + printf '%bOut of range.%b\n' "$YELLOW" "$NC" >&2 + continue + fi + + for ((i = 0; i < ${#_SHELL_MENU_SELECTOR[@]}; i++)); do + if [[ "${_SHELL_MENU_SELECTOR[$i]}" == "$answer" ]]; then + printf '%s' "$answer" + return 0 + fi + done + + set +e + selector="$(_shell_menu_match_name "$answer")" + rc=$? + set -e + if ((rc == 0)); then + printf '%s' "$selector" + return 0 + fi + ((rc == 65)) && continue + + printf '%bUnknown target.%b %s\n' "$YELLOW" "$NC" "$answer" >&2 + done +} + +cmd_shell() { + local target="${1:-}" + if [[ -z "$target" ]]; then + target="$(_shell_choose_target)" || return $? + else + shift || true + fi + + _shell_resolve_target "$target" || return $? + + local container="$_SHELL_CONTAINER_ID" + local workdir="$_SHELL_WORKDIR" + + if (($# == 0)); then + if [[ -n "$workdir" ]]; then + _container_open_shell "$container" --workdir "$workdir" + else + _container_open_shell "$container" + fi + return $? + fi + + case "${1:-}" in + --) + shift + (($# > 0)) || { + err "Usage: lds shell -- [args...]" + return 64 + } + if [[ -n "$workdir" ]]; then + _container_exec_argv "$container" --workdir "$workdir" -- "$@" + else + _container_exec_argv "$container" -- "$@" + fi + ;; + --shell) + shift + (($# == 1)) || { + err "Usage: lds shell --shell " + return 64 + } + if [[ -n "$workdir" ]]; then + _container_exec_argv "$container" --workdir "$workdir" -- sh -lc "$1" + else + _container_exec_argv "$container" -- sh -lc "$1" + fi + ;; + --interactive | -i) + shift + (($# > 0)) || { + err "Usage: lds shell --interactive [args...]" + return 64 + } + if [[ -n "$workdir" ]]; then + _container_exec_interactive_argv "$container" --workdir "$workdir" -- "$@" + else + _container_exec_interactive_argv "$container" -- "$@" + fi + ;; + *) + if [[ -n "$workdir" ]]; then + _container_exec_argv "$container" --workdir "$workdir" -- "$@" + else + _container_exec_argv "$container" -- "$@" + fi + ;; + esac +} + cmd_core() { local target="${1:-}" [[ -n "$target" ]] && shift || true From a1e2dedcab7bd4efcdf87c9996a4646601aa6b29 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:44:48 +0600 Subject: [PATCH 070/106] fix(shell): preserve documented target precedence --- lib/services.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/services.sh b/lib/services.sh index 8745ea8c..89d2c915 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -1193,7 +1193,7 @@ _shell_resolve_target() { _shell_resolve_app "$target" return $? ;; - tools | utility:tools) + utility:tools) _shell_resolve_tools return $? ;; @@ -1204,6 +1204,11 @@ _shell_resolve_target() { return $? fi + if [[ "$requested" == tools ]]; then + _shell_resolve_tools + return $? + fi + if _container_project_service_exists "$requested"; then _shell_resolve_service "$requested" return $? From 3065c74140ea89e81d04842a81e0c61233f7f17d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:45:14 +0600 Subject: [PATCH 071/106] test(shell): cover grouped selector and execution modes --- tests/execution-contract.sh | 115 ++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index d643f6f8..67a4534a 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -51,6 +51,11 @@ run_case() { printf ' <%s>' "$@" >>"$EXECUTION_TEST_LOG" printf '\n' >>"$EXECUTION_TEST_LOG" + if [[ "${1:-}" == ps && "${2:-}" == --format ]]; then + printf '%s\n' demo-container external-worker worker.local + return 0 + fi + if [[ "${1:-}" == inspect && "${2:-}" == -f ]]; then case "${3:-}|${4:-}" in "{{.Id}}|demo-container") printf '%s\n' cid-demo ;; @@ -137,6 +142,10 @@ run_case() { *" cid-php84 test -d /app "*) return 0 ;; + *" SERVER_TOOLS sh -c "*"for path in /app/*"* ) + printf '%s\n' billing node + return 0 + ;; *" SERVER_TOOLS sh -c [ -d \"\$1\" ] sh /app/billing "*) return 0 ;; @@ -246,6 +255,112 @@ if grep -Fq '' "$log"; then fi pass "shell batch 1: image-like targets are not implicitly instantiated" +case_shell_menu_build() { + _shell_menu_build + local i + for ((i = 0; i < ${#_SHELL_MENU_NAME[@]}; i++)); do + printf 'menu:%s|%s|%s\n' "${_SHELL_MENU_KIND[$i]}" "${_SHELL_MENU_NAME[$i]}" "${_SHELL_MENU_SELECTOR[$i]}" >>"$EXECUTION_TEST_LOG" + done +} +run_case case_shell_menu_build +assert_file_contains "$log" 'menu:domain|app.local|domain:app.local' +assert_file_contains "$log" 'menu:app|billing|app:billing' +assert_file_contains "$log" 'menu:service|php84|service:php84' +assert_file_contains "$log" 'menu:container|demo-container|container:demo-container' +assert_file_contains "$log" 'menu:utility|tools|tools' +pass "shell batch 2: grouped catalog includes domains apps services containers and tools" + +case_shell_choose_number() { + _shell_selector_is_tty() { return 0; } + local choice + choice="$(printf '4\n' | _shell_choose_target)" + printf 'choice:%s\n' "$choice" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_choose_number +assert_file_contains "$log" 'choice:app:billing' +pass "shell batch 2: global numeric selector resolves across grouped categories" + +case_shell_choose_name() { + _shell_selector_is_tty() { return 0; } + local choice + choice="$(printf 'billing\n' | _shell_choose_target)" + printf 'choice:%s\n' "$choice" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_choose_name +assert_file_contains "$log" 'choice:app:billing' +pass "shell batch 2: exact unique name selector resolves category target" + +case_shell_choose_ambiguous_name() { + _shell_selector_is_tty() { return 0; } + local choice + choice="$(printf 'node\nservice:node\n' | _shell_choose_target)" + printf 'choice:%s\n' "$choice" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_choose_ambiguous_name +assert_file_contains "$log" 'choice:service:node' +pass "shell batch 2: ambiguous names require a qualified selector" + +case_shell_menu_nontty() { + _shell_selector_is_tty() { return 1; } + _shell_choose_target +} +set +e +run_case case_shell_menu_nontty >"$tmp/shell-menu.out" 2>"$tmp/shell-menu.err" +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "bare shell non-TTY selector returned $rc instead of 64" +grep -Fq 'Applications / Domains' "$tmp/shell-menu.err" || fail "bare shell catalog omitted domain heading" +grep -Fq 'Application Directories' "$tmp/shell-menu.err" || fail "bare shell catalog omitted app heading" +grep -Fq 'Services' "$tmp/shell-menu.err" || fail "bare shell catalog omitted services heading" +grep -Fq 'Containers' "$tmp/shell-menu.err" || fail "bare shell catalog omitted containers heading" +grep -Fq 'Utilities' "$tmp/shell-menu.err" || fail "bare shell catalog omitted utilities heading" +assert_file_contains "$log" 'err:No TTY to prompt. Use: lds shell ' +pass "shell batch 2: bare shell prints grouped catalog and fails actionably without TTY" + +case_shell_domain_shell() { + force_interactive_tty + cmd_shell app.local +} +run_case case_shell_domain_shell +assert_file_contains "$log" 'docker: <-it> <--workdir> <--login>' +pass "shell batch 3: domain target opens application-aware interactive shell" + +case_shell_app_shell() { + force_interactive_tty + cmd_shell billing +} +run_case case_shell_app_shell +assert_file_contains "$log" 'docker: <-it> <--workdir> <--login>' +pass "shell batch 3: app-directory fallback opens server-tools at matching /app child" + +case_shell_service_command() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_shell php84 -- php -v +} +run_case case_shell_service_command +assert_file_contains "$log" 'docker: <-v>' +pass "shell batch 3: explicit command preserves argv for service target" + +case_shell_expression() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_shell billing --shell 'printf "%s\n" "hello world" | cat' +} +run_case case_shell_expression +assert_file_contains "$log" 'docker: <--workdir> <-lc> ' +pass "shell batch 3: --shell makes intentional shell parsing explicit" + +case_shell_interactive_command() { + force_interactive_tty + cmd_shell tools --interactive lazydocker +} +run_case case_shell_interactive_command +assert_file_contains "$log" 'docker: <-it> ' +pass "shell batch 3: --interactive routes TUI argv through shared interactive helper" + case_cli_command() { _container_stdin_is_tty() { return 1; } _container_stdout_is_tty() { return 1; } From c50bda1fc9aa1c1a41ea5e5c29f8e6468f6fe78c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:46:09 +0600 Subject: [PATCH 072/106] feat(shell): expose unified shell command --- lds | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lds b/lds index c9c5403b..4ec1f7bf 100755 --- a/lds +++ b/lds @@ -771,7 +771,13 @@ cmd_help() { - `lds urls` ## Execution / shells -- `lds core [domain|service|container] [--] [command...]` — application/domain-aware execution with resolved working directory. +- `lds shell` — grouped interactive selector for domains, application directories, services, containers, and Tools. +- `lds shell ` — open an interactive shell in the resolved context. +- `lds shell [--] [args...]` — argv-preserving execution. +- `lds shell --shell ` — intentional shell parsing. +- `lds shell --interactive [args...]` — interactive/TUI argv execution. +- Qualified selectors: `domain:`, `app:`, `service:`, `container:`. +- `lds core [domain|service|container] [--] [command...]` — compatibility application/domain-aware execution with resolved working directory. - `lds cli [--] [command...]` — generic current-project service or exact-container execution. - `lds stack exec [--] [command...]` *(alias: `lds exec`)* — Compose-service-only execution. - `lds tools sh` — interactive shell in the project `server-tools` container. @@ -857,7 +863,10 @@ ${CYAN}Config:${NC} urls ${CYAN}Execution / Shells:${NC} - core [domain|service|container] [--] [command...] Domain/application-aware + shell [target] [--] [command...] Unified selector/execution + shell --shell Intentional shell syntax + shell --interactive [args...] Interactive/TUI command + core [domain|service|container] [--] [command...] Compatibility domain/app entry cli [--] [command...] Generic service/container exec stack exec [--] [command...] Compose-service only tools sh|exec|shell-exec|file server-tools only @@ -893,7 +902,7 @@ EOF _is_public_lds_command() { case "${1:-}" in - stack|domain|support|bundle|up|start|down|stop|restart|reboot|status|ps|logs|exec|events|clean|config|http|host|setup|profiles|cert|certificate|doctor|diag|sniff|open|notify|ui|images|urls|tools|cli|core|graphify|secrets|rebuild|run) + stack|domain|support|bundle|up|start|down|stop|restart|reboot|status|ps|logs|exec|events|clean|config|http|host|setup|profiles|cert|certificate|doctor|diag|sniff|open|notify|ui|images|urls|tools|cli|core|shell|graphify|secrets|rebuild|run) return 0 ;; esac From 525afe96ab627a19ff1184829129c95b53a15b5c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:46:22 +0600 Subject: [PATCH 073/106] docs(shell): introduce unified execution navigator --- README.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c316f64d..33e2c472 100644 --- a/README.md +++ b/README.md @@ -345,27 +345,33 @@ That global mode can remove unrelated stopped containers, unused images/networks ## Execution and shells -Use the execution surface that matches the target you already know: +`lds shell` is the canonical execution navigator. With no arguments it presents a numbered catalog grouped into domains, application directories, services, containers, and utilities; choose by number, exact name, or a qualified selector. ```bash -# Application/domain aware; resolves container + working directory. -lds core project.localhost -lds core project.localhost -- php artisan about +# Interactive grouped selector. +lds shell -# Generic current-project service or exact Docker container. -lds cli php84 -lds cli php84 -- php -v +# Domain/application-aware shell and command. +lds shell project.localhost +lds shell project.localhost -- php artisan about -# Compose-service only (top-level alias: lds exec). -lds stack exec redis -- redis-cli ping +# Current-project service or exact container. +lds shell php84 -- php -v -# server-tools only. -lds tools sh -lds tools exec -- jq --version -lds tools shell-exec 'printf "%s\n" "hello world" | cat' +# Direct server-tools /app child fallback. +lds shell billing + +# Reserved Tools target. +lds shell tools -- jq --version + +# Intentional shell syntax or interactive TUI. +lds shell tools --shell 'printf "%s\n" "hello world" | cat' +lds shell tools --interactive lazydocker ``` -Explicit commands preserve argv rather than being flattened into a shell string. Use `lds tools shell-exec` only for intentional shell syntax. Interactive shells/TUIs require a real TTY; piped/non-interactive commands do not force one. See `docs/reference/cli.rst` for target resolution and exit-code details. +Explicit unqualified targets resolve in this order: discovered domain, reserved `tools`, exact current-project service, exact container, then an exact direct child under `server-tools:/app`. Qualified `domain:`, `app:`, `service:`, and `container:` selectors bypass collisions. Image names are not implicitly instantiated. + +Normal commands preserve argv rather than being flattened into a shell string. Interactive shells/TUIs require a real TTY; piped/non-interactive commands do not force one. `core`, `cli`, `stack exec`, `exec`, and execution-oriented `tools` commands remain compatibility surfaces during migration. See `docs/reference/cli.rst` for target resolution and exit-code details. ## Ad-hoc Dockerfile runner From 4522128f92a946bb4f9ceca29d723be051f1e382 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:46:47 +0600 Subject: [PATCH 074/106] docs(shell): define selector and resolution contract --- docs/reference/cli.rst | 65 +++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index d890d1b7..2dfc9144 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -158,51 +158,58 @@ Shortcuts:: Execution and Shells -------------------- -LocalDevStack has four execution surfaces with intentionally different roles. +``lds shell`` is the canonical execution/navigation surface. -Application/domain-aware execution:: +With no arguments it builds a stable grouped catalog of discovered domains, +direct ``server-tools:/app`` child directories, current-project Compose +services, running Docker containers, and the ``tools`` utility target:: - lds core - lds core - lds core [--] [args...] + lds shell -``core`` is the ergonomic application entry point. A domain is resolved through -Tools to its application container and working directory. Node applications use -``/app``. Other applications use the resolved document root when available, then -``/app``, then ``/``. With no target, ``core`` discovers domains and prompts when -interactive. +The selector accepts the displayed global number or an exact name. If the same +name exists in multiple categories, use a qualified selector:: -Generic service/container execution:: + domain:project.localhost + app:project + service:php84 + container:localdevstack-php84-1 - lds cli - lds cli [--] [args...] +Explicit targets use deterministic precedence: exact discovered domain, reserved +``tools`` target, exact current-project service, exact Docker container, then an +exact direct child ``/app/`` inside the current project's server-tools +container. No fuzzy matching or implicit case conversion is performed. Image +names are not implicitly instantiated. -``cli`` prefers an exact service in the current Compose project. If there is no -matching service, an exact Docker container name or ID is accepted. No implicit -case conversion is performed. +Canonical forms:: -Compose-service-only execution:: + lds shell + lds shell [--] [args...] + lds shell --shell + lds shell --interactive [args...] - lds stack exec - lds stack exec [--] [args...] +Domain targets retain application-aware working-directory behavior: Node uses +``/app``; other applications use the resolved document root when available, +then ``/app`` and ``/``. Application-directory fallback opens +``server-tools`` at ``/app/``. -The top-level ``lds exec`` alias has the same contract. This surface intentionally -accepts services only; use ``cli`` for an arbitrary exact container. +Normal command forms preserve argv exactly. ``--shell`` is the explicit escape +hatch for pipelines, redirections, and compound shell syntax. +``--interactive`` routes argv through the shared real-TTY execution helper. +Interactive shells and TUIs require real stdin/stdout TTYs; piped commands keep +stdin without forcing a TTY. -Server-tools-specific execution:: +The older execution surfaces remain compatible during migration:: + lds core [domain|service|container] [--] [command...] + lds cli [--] [command...] + lds stack exec [--] [command...] lds tools sh lds tools exec [--] [args...] lds tools shell-exec lds tools file -All explicit command forms preserve argv exactly; normal commands are not joined -into a shell string. ``--`` may be used to separate LDS arguments from the -container command. Use ``tools shell-exec`` only when shell syntax such as -pipelines, redirections, or compound expressions is intentionally required. -Interactive shells and TUIs require a real TTY; piped/non-interactive commands do -not force one. Bash is preferred for interactive shells with ``sh`` as the -fallback. +``stack exec`` remains service-only. ``tools file`` remains inspection +functionality rather than generic shell navigation. Secrets ------- From 8b08afe689aef4ee19e0751e4873c600b0f538cc Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:47:03 +0600 Subject: [PATCH 075/106] test(shell): lock public routing and help --- tests/cli-contract.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/cli-contract.sh b/tests/cli-contract.sh index 586e9554..fc47583c 100755 --- a/tests/cli-contract.sh +++ b/tests/cli-contract.sh @@ -55,7 +55,7 @@ if grep -Fq 'declare -F "cmd_$cmd"' "$ROOT/lds"; then fail "top-level dispatch still exposes arbitrary cmd_* functions dynamically" fi assert_file_contains "$ROOT/lds" 'stack|domain|support|bundle|up|start' -assert_file_contains "$ROOT/lds" 'tools|cli|core|graphify|secrets|rebuild|run)' +assert_file_contains "$ROOT/lds" 'tools|cli|core|shell|graphify|secrets|rebuild|run)' pass "top-level LDS command routing is explicit and collision-safe" if PATH="$tmpbin:$PATH" "$ROOT/lds" graphify --help 2>&1 | grep -Fq 'SERVER_TOOLS is not running'; then @@ -63,6 +63,10 @@ if PATH="$tmpbin:$PATH" "$ROOT/lds" graphify --help 2>&1 | grep -Fq 'SERVER_TOOL fi pass "top-level Graphify remains a host-side command" +assert_contains "$help_output" "shell [target]" +assert_contains "$markdown_output" "lds shell " +pass "unified shell is exposed in embedded help" + graphify_log="$(mktemp)" cat >"$tmpbin/graphify" <<'SH' #!/usr/bin/env sh From 532cfc9ba70887cb5f99043b3535a4849f744564 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:47:06 +0600 Subject: [PATCH 076/106] test(docs): lock unified shell UX contract --- tests/docs-contract.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index 82512840..fd2141fe 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -145,13 +145,17 @@ pass "documentation toctree targets exist" help_md="$("$ROOT/lds" help --markdown)" -for required in 'lds profiles add ' 'lds support trace ' 'lds support bundle [--redact|--full] [output.zip]' 'lds cli [--] [command...]' 'lds core [domain|service|container] [--] [command...]' 'lds stack exec [--] [command...]' 'lds tools exec [--] [args...]' 'lds tools shell-exec ' 'lds run shell|ps|logs|stop|rm|open' 'MongoDB:' 'Elasticsearch:'; do +for required in 'lds profiles add ' 'lds support trace ' 'lds support bundle [--redact|--full] [output.zip]' 'lds shell [--] [args...]' 'lds shell --shell ' 'lds cli [--] [command...]' 'lds core [domain|service|container] [--] [command...]' 'lds stack exec [--] [command...]' 'lds tools exec [--] [args...]' 'lds tools shell-exec ' 'lds run shell|ps|logs|stop|rm|open' 'MongoDB:' 'Elasticsearch:'; do assert_contains "$help_md" "$required" done pass "embedded CLI help covers documented command groups" assert_file_contains "$cli" 'Execution and Shells' -assert_file_contains "$cli" 'lds core [--] [args...]' +assert_file_contains "$cli" 'lds shell [--] [args...]' +assert_file_contains "$cli" 'Applications' +assert_file_contains "$cli" 'domain:project.localhost' +assert_file_contains "$cli" 'Image' +assert_file_contains "$cli" 'lds core [domain|service|container] [--] [command...]' assert_file_contains "$cli" 'lds cli [--] [args...]' assert_file_contains "$cli" 'lds stack exec [--] [args...]' assert_file_contains "$cli" 'lds tools exec [--] [args...]' @@ -159,9 +163,10 @@ assert_file_contains "$cli" 'lds tools shell-exec ' assert_file_contains "$cli" 'preserve argv exactly' assert_file_contains "$cli" 'require a real TTY' assert_file_contains "$readme" '## Execution and shells' -assert_file_contains "$readme" 'lds cli php84 -- php -v' -assert_file_contains "$readme" 'lds stack exec redis -- redis-cli ping' -assert_file_contains "$readme" "lds tools shell-exec" +assert_file_contains "$readme" 'lds shell' +assert_file_contains "$readme" 'lds shell project.localhost -- php artisan about' +assert_file_contains "$readme" 'lds shell php84 -- php -v' +assert_file_contains "$readme" 'lds shell tools --interactive lazydocker' pass "execution-surface docs match the shared Core/CLI contract" From 4e3f0000834f59568d329bf74b43b9ef41d7538c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:48:01 +0600 Subject: [PATCH 077/106] fix(shell): keep selector interactive and collision-safe --- lib/services.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index 89d2c915..a31938c9 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -1303,7 +1303,7 @@ _shell_menu_build() { done < <(_shell_container_list 2>/dev/null || true) if [[ -n "$(_project_tools_container_running || true)" ]]; then - _shell_menu_add utility tools tools + _shell_menu_add utility tools utility:tools fi } @@ -1333,7 +1333,7 @@ _shell_menu_print() { } _shell_selector_is_tty() { - [[ -t 0 && -t 1 ]] + [[ -t 0 ]] } _shell_menu_match_name() { From a850b0e5202611802ef7af9d33c700a6ba535202 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:48:11 +0600 Subject: [PATCH 078/106] test(shell): align qualified tools selector --- tests/execution-contract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 67a4534a..a71e416a 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -267,7 +267,7 @@ assert_file_contains "$log" 'menu:domain|app.local|domain:app.local' assert_file_contains "$log" 'menu:app|billing|app:billing' assert_file_contains "$log" 'menu:service|php84|service:php84' assert_file_contains "$log" 'menu:container|demo-container|container:demo-container' -assert_file_contains "$log" 'menu:utility|tools|tools' +assert_file_contains "$log" 'menu:utility|tools|utility:tools' pass "shell batch 2: grouped catalog includes domains apps services containers and tools" case_shell_choose_number() { From d53e9e89ac47e80e4ceaed3b6b902d977e5236a8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:48:34 +0600 Subject: [PATCH 079/106] refactor(shell): preserve caller shell error mode --- lib/services.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index a31938c9..61b13b8f 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -1397,13 +1397,11 @@ _shell_choose_target() { fi done - set +e - selector="$(_shell_menu_match_name "$answer")" - rc=$? - set -e - if ((rc == 0)); then + if selector="$(_shell_menu_match_name "$answer")"; then printf '%s' "$selector" return 0 + else + rc=$? fi ((rc == 65)) && continue From 107d3c4c5aef6687c554ce4a0321b24019de9af4 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:48:59 +0600 Subject: [PATCH 080/106] docs(plan): record LDS shell batches 1 through 4 --- docs/plans/lds-shell-unification-plan.md | 62 ++++++++++++------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/plans/lds-shell-unification-plan.md b/docs/plans/lds-shell-unification-plan.md index 3894f3de..4e76344a 100644 --- a/docs/plans/lds-shell-unification-plan.md +++ b/docs/plans/lds-shell-unification-plan.md @@ -1,6 +1,6 @@ # LDS Unified Shell Command Plan -Status: **active — Batch 0 contract finalization and Batch 1 resolver implementation** +Status: **active — Batches 0–4 implemented; CI validation pending before compatibility consolidation** ## Objective @@ -170,45 +170,45 @@ Old commands must not retain independent Docker execution implementations after ### Batch 1 — Unified shell context and resolver -- [ ] introduce normalized shell context state; -- [ ] resolve exact discovered domains; -- [ ] resolve reserved `tools`; -- [ ] resolve exact current-project services; -- [ ] resolve exact containers without emitting premature not-found errors; -- [ ] resolve direct `server-tools:/app/` directories; -- [ ] support qualified `domain:`, `app:`, `service:`, and `container:` targets; -- [ ] add resolver regression coverage. +- [x] introduce normalized shell context state; +- [x] resolve exact discovered domains; +- [x] resolve reserved `tools`; +- [x] resolve exact current-project services; +- [x] resolve exact containers without emitting premature not-found errors; +- [x] resolve direct `server-tools:/app/` directories; +- [x] support qualified `domain:`, `app:`, `service:`, and `container:` targets; +- [x] add resolver regression coverage. ### Batch 2 — Grouped interactive selector -- [ ] build stable grouped catalog; -- [ ] include discovered domains; -- [ ] include direct `/app` directories; -- [ ] include current-project services; -- [ ] include running Docker containers; -- [ ] include `tools` utility; -- [ ] accept global number selector; -- [ ] accept exact name selector; -- [ ] detect duplicate names and require qualification; -- [ ] print catalog + actionable failure without TTY; -- [ ] add selector regression coverage. +- [x] build stable grouped catalog; +- [x] include discovered domains; +- [x] include direct `/app` directories; +- [x] include current-project services; +- [x] include running Docker containers; +- [x] include `tools` utility; +- [x] accept global number selector; +- [x] accept exact name selector; +- [x] detect duplicate names and require qualification; +- [x] print catalog + actionable failure without TTY; +- [x] add selector regression coverage. ### Batch 3 — `cmd_shell` execution modes -- [ ] no command opens shell in resolved context; -- [ ] `--` executes exact argv; -- [ ] `--shell` explicitly executes one shell expression; -- [ ] `--interactive` executes argv through the interactive helper; -- [ ] preserve domain/app workdir; -- [ ] add stdin/TTY/exit propagation coverage. +- [x] no command opens shell in resolved context; +- [x] `--` executes exact argv; +- [x] `--shell` explicitly executes one shell expression; +- [x] `--interactive` executes argv through the interactive helper; +- [x] preserve domain/app workdir; +- [x] add stdin/TTY/exit propagation coverage. ### Batch 4 — Public routing and help -- [ ] make `shell` a first-class public LDS command; -- [ ] update embedded help; -- [ ] update README; -- [ ] update CLI reference; -- [ ] update documentation contracts. +- [x] make `shell` a first-class public LDS command; +- [x] update embedded help; +- [x] update README; +- [x] update CLI reference; +- [x] update documentation contracts. ### Batch 5 — Compatibility consolidation From 6ad7b5d530df011c8bd6115ca76d42a40db631a8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:50:48 +0600 Subject: [PATCH 081/106] test(shell): model tools app directory probes directly --- tests/execution-contract.sh | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index a71e416a..b4b64b1d 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -99,6 +99,18 @@ run_case() { fi if [[ "${1:-}" == exec ]]; then + if [[ "${2:-}" == SERVER_TOOLS && "${3:-}" == sh && "${4:-}" == -c ]]; then + if [[ "${5:-}" == *'for path in /app/*'* ]]; then + printf '%s\n' billing node + return 0 + fi + if [[ "${5:-}" == '[ -d "$1" ]' && "${6:-}" == sh ]]; then + case "${7:-}" in + /app/billing) return 0 ;; + /app/missing-shell|/app/php:8.4-alpine) return 1 ;; + esac + fi + fi case " $* " in *" domain-which --list-domains "*) printf '%s\n' app.local php.local fallback.local @@ -142,16 +154,6 @@ run_case() { *" cid-php84 test -d /app "*) return 0 ;; - *" SERVER_TOOLS sh -c "*"for path in /app/*"* ) - printf '%s\n' billing node - return 0 - ;; - *" SERVER_TOOLS sh -c [ -d \"\$1\" ] sh /app/billing "*) - return 0 - ;; - *" SERVER_TOOLS sh -c [ -d \"\$1\" ] sh /app/missing-shell "*|*" SERVER_TOOLS sh -c [ -d \"\$1\" ] sh /app/php:8.4-alpine "*) - return 1 - ;; *" cid-demo sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-mixed sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-node sh -lc command -v bash >/dev/null 2>&1 "*|*" cid-php84 sh -lc command -v bash >/dev/null 2>&1 "*) return 0 ;; From 0772cc814561c18fdeef6cf3b7e76251f301ecee Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:52:04 +0600 Subject: [PATCH 082/106] test(shell): stabilize app directory fixture matching --- tests/execution-contract.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index b4b64b1d..0e2930e6 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -100,16 +100,14 @@ run_case() { if [[ "${1:-}" == exec ]]; then if [[ "${2:-}" == SERVER_TOOLS && "${3:-}" == sh && "${4:-}" == -c ]]; then - if [[ "${5:-}" == *'for path in /app/*'* ]]; then + case "${7:-}" in + /app/billing) return 0 ;; + /app/missing-shell|/app/php:8.4-alpine) return 1 ;; + esac + if [[ "${5:-}" == *'for path in /app/'* ]]; then printf '%s\n' billing node return 0 fi - if [[ "${5:-}" == '[ -d "$1" ]' && "${6:-}" == sh ]]; then - case "${7:-}" in - /app/billing) return 0 ;; - /app/missing-shell|/app/php:8.4-alpine) return 1 ;; - esac - fi fi case " $* " in *" domain-which --list-domains "*) From 3e00c8604921ca753ba208de6c45b596e50c143c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:53:31 +0600 Subject: [PATCH 083/106] test(shell): let app fixture reach directory fallback --- tests/execution-contract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index 0e2930e6..ab7dafee 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -88,7 +88,7 @@ run_case() { "{{.State.Running}}|worker.local") printf '%s\n' true ;; "{{.State.Running}}|stopped-container") printf '%s\n' false ;; "{{.State.Running}}|SERVER_TOOLS") printf '%s\n' true ;; - "{{.State.Running}}|missing-shell"|"{{.State.Running}}|php:8.4-alpine") return 1 ;; + "{{.State.Running}}|billing"|"{{.State.Running}}|missing-shell"|"{{.State.Running}}|php:8.4-alpine") return 1 ;; "{{.State.Running}}|"*) printf '%s\n' true ;; esac return 0 From a4726fe5c0c0319280823c8c05aa14c9fc88343b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:55:08 +0600 Subject: [PATCH 084/106] docs(shell): name grouped selector categories explicitly --- docs/reference/cli.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 2dfc9144..d9424ada 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -160,9 +160,17 @@ Execution and Shells ``lds shell`` is the canonical execution/navigation surface. -With no arguments it builds a stable grouped catalog of discovered domains, -direct ``server-tools:/app`` child directories, current-project Compose -services, running Docker containers, and the ``tools`` utility target:: +With no arguments it builds a stable grouped catalog:: + + Applications / Domains + Application Directories + Services + Containers + Utilities + +The groups contain discovered domains, direct ``server-tools:/app`` child +directories, current-project Compose services, running Docker containers, and +the ``tools`` utility target:: lds shell From 1289e418ef61193de66d32a8a6a8506b36084143 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:56:59 +0600 Subject: [PATCH 085/106] test(docs): align legacy shell compatibility syntax --- tests/docs-contract.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index fd2141fe..9410d22c 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -156,8 +156,8 @@ assert_file_contains "$cli" 'Applications' assert_file_contains "$cli" 'domain:project.localhost' assert_file_contains "$cli" 'Image' assert_file_contains "$cli" 'lds core [domain|service|container] [--] [command...]' -assert_file_contains "$cli" 'lds cli [--] [args...]' -assert_file_contains "$cli" 'lds stack exec [--] [args...]' +assert_file_contains "$cli" 'lds cli [--] [command...]' +assert_file_contains "$cli" 'lds stack exec [--] [command...]' assert_file_contains "$cli" 'lds tools exec [--] [args...]' assert_file_contains "$cli" 'lds tools shell-exec ' assert_file_contains "$cli" 'preserve argv exactly' From 84d8f31ddd10e8057c0e7a41038a79f9b622cc7d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:57:51 +0600 Subject: [PATCH 086/106] refactor(shell): centralize shell context execution --- lib/services.sh | 108 ++++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index 61b13b8f..c1eff024 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -1237,6 +1237,60 @@ _shell_resolve_target() { return 66 } +_shell_resolve_service_or_container() { + local target="${1:-}" + _shell_context_reset + _container_resolve_target "$target" || return $? + _shell_context_from_resolved_container "$_CONTAINER_TARGET_KIND" "$target" +} + +_shell_context_open() { + local container="$_SHELL_CONTAINER_ID" workdir="$_SHELL_WORKDIR" + [[ -n "$container" ]] || { + err "Shell context has no container" + return 64 + } + if [[ -n "$workdir" ]]; then + _container_open_shell "$container" --workdir "$workdir" + else + _container_open_shell "$container" + fi +} + +_shell_context_exec_argv() { + local container="$_SHELL_CONTAINER_ID" workdir="$_SHELL_WORKDIR" + (($# > 0)) || { + err "Shell context command is required" + return 64 + } + if [[ -n "$workdir" ]]; then + _container_exec_argv "$container" --workdir "$workdir" -- "$@" + else + _container_exec_argv "$container" -- "$@" + fi +} + +_shell_context_exec_expression() { + (($# == 1)) || { + err "Shell expression is required" + return 64 + } + _shell_context_exec_argv sh -lc "$1" +} + +_shell_context_exec_interactive() { + local container="$_SHELL_CONTAINER_ID" workdir="$_SHELL_WORKDIR" + (($# > 0)) || { + err "Interactive shell context command is required" + return 64 + } + if [[ -n "$workdir" ]]; then + _container_exec_interactive_argv "$container" --workdir "$workdir" -- "$@" + else + _container_exec_interactive_argv "$container" -- "$@" + fi +} + declare -a _SHELL_MENU_KIND=() declare -a _SHELL_MENU_NAME=() declare -a _SHELL_MENU_SELECTOR=() @@ -1419,15 +1473,8 @@ cmd_shell() { _shell_resolve_target "$target" || return $? - local container="$_SHELL_CONTAINER_ID" - local workdir="$_SHELL_WORKDIR" - if (($# == 0)); then - if [[ -n "$workdir" ]]; then - _container_open_shell "$container" --workdir "$workdir" - else - _container_open_shell "$container" - fi + _shell_context_open return $? fi @@ -1438,11 +1485,7 @@ cmd_shell() { err "Usage: lds shell -- [args...]" return 64 } - if [[ -n "$workdir" ]]; then - _container_exec_argv "$container" --workdir "$workdir" -- "$@" - else - _container_exec_argv "$container" -- "$@" - fi + _shell_context_exec_argv "$@" ;; --shell) shift @@ -1450,11 +1493,7 @@ cmd_shell() { err "Usage: lds shell --shell " return 64 } - if [[ -n "$workdir" ]]; then - _container_exec_argv "$container" --workdir "$workdir" -- sh -lc "$1" - else - _container_exec_argv "$container" -- sh -lc "$1" - fi + _shell_context_exec_expression "$1" ;; --interactive | -i) shift @@ -1462,18 +1501,10 @@ cmd_shell() { err "Usage: lds shell --interactive [args...]" return 64 } - if [[ -n "$workdir" ]]; then - _container_exec_interactive_argv "$container" --workdir "$workdir" -- "$@" - else - _container_exec_interactive_argv "$container" -- "$@" - fi + _shell_context_exec_interactive "$@" ;; *) - if [[ -n "$workdir" ]]; then - _container_exec_argv "$container" --workdir "$workdir" -- "$@" - else - _container_exec_argv "$container" -- "$@" - fi + _shell_context_exec_argv "$@" ;; esac } @@ -1488,28 +1519,17 @@ cmd_core() { [[ "${1:-}" == -- ]] && shift - local container workdir='' if _core_is_domain "$target"; then - _core_domain_resolve "$target" || return $? - container="$_CORE_CONTAINER_ID" - workdir="$_CORE_WORKDIR" + _shell_context_reset + _shell_resolve_domain "$target" || return $? else - _container_resolve_target "$target" || return $? - container="$_CONTAINER_TARGET_ID" + _shell_resolve_service_or_container "$target" || return $? fi if (($# > 0)); then - if [[ -n "$workdir" ]]; then - _container_exec_argv "$container" --workdir "$workdir" -- "$@" - else - _container_exec_argv "$container" -- "$@" - fi + _shell_context_exec_argv "$@" else - if [[ -n "$workdir" ]]; then - _container_open_shell "$container" --workdir "$workdir" - else - _container_open_shell "$container" - fi + _shell_context_open fi } From d4e5a56235884e86f121a82c640f71043c7a2ba2 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:58:16 +0600 Subject: [PATCH 087/106] refactor(shell): consolidate legacy execution surfaces --- lib/services.sh | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index c1eff024..877e7dfd 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -510,10 +510,9 @@ cmd_host() { } cmd_ui() { - local ctr - ctr="$(_project_tools_container_running || true)" - [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" - _container_exec_interactive_argv "$ctr" -- lazydocker + _shell_context_reset + _shell_resolve_tools || return $? + _shell_context_exec_interactive lazydocker } # ───────────────────────────────────────────────────────────────────────────── @@ -535,13 +534,13 @@ cmd_exec() { return 66 fi - _container_resolve_target "$service" || return $? - local container="$_CONTAINER_TARGET_ID" + _shell_context_reset + _shell_resolve_service "$service" || return $? if (($# > 0)); then - _container_exec_argv "$container" -- "$@" + _shell_context_exec_argv "$@" else - _container_open_shell "$container" + _shell_context_open fi } @@ -830,16 +829,13 @@ cmd_rebuild() { cmd_tools() { local sub="${1:-sh}" shift || true - local ctr - ctr="$(_project_tools_container_running || true)" - [[ -n "$ctr" ]] || { - err "server-tools container is not running for project: $(lds_project)" - return 69 - } + + _shell_context_reset + _shell_resolve_tools || return $? case "${sub,,}" in sh | shell | "") - _container_open_shell "$ctr" + _shell_context_open ;; exec) [[ "${1:-}" == -- ]] && shift @@ -847,14 +843,14 @@ cmd_tools() { err "Usage: lds tools exec [--] [args...]" return 64 } - _container_exec_argv "$ctr" -- "$@" + _shell_context_exec_argv "$@" ;; shell-exec) (($# == 1)) || { err "Usage: lds tools shell-exec " return 64 } - _container_exec_argv "$ctr" -- sh -lc "$1" + _shell_context_exec_expression "$1" ;; file) local path="${1:-}" @@ -862,7 +858,7 @@ cmd_tools() { err "Usage: lds tools file " return 64 } - _container_exec_argv "$ctr" -- sh -lc ' + _shell_context_exec_argv sh -lc ' ls -la -- "$1" 2>/dev/null || true printf "\n" sed -n "1,200p" -- "$1" 2>/dev/null || true @@ -885,13 +881,12 @@ cmd_cli() { } [[ "${1:-}" == -- ]] && shift - _container_resolve_target "$target" || return $? - local container="$_CONTAINER_TARGET_ID" + _shell_resolve_service_or_container "$target" || return $? if (($# > 0)); then - _container_exec_argv "$container" -- "$@" + _shell_context_exec_argv "$@" else - _container_open_shell "$container" + _shell_context_open fi } From cc47d866e885ab41d9a45d1fac0fc6a8ba524ee9 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:59:15 +0600 Subject: [PATCH 088/106] fix(shell): make qualified container resolution exact --- lib/services.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/services.sh b/lib/services.sh index 877e7dfd..66869d7d 100644 --- a/lib/services.sh +++ b/lib/services.sh @@ -1111,7 +1111,7 @@ _shell_exact_container_exists() { } _shell_resolve_container() { - local target="${1:-}" + local target="${1:-}" id name service [[ -n "$target" ]] || { err "Shell container target is required" return 64 @@ -1120,8 +1120,21 @@ _shell_resolve_container() { err "Container not found: $target" return 66 } - _container_resolve_target "$target" || return $? - _shell_context_from_resolved_container container "$target" + + id="$(_container_docker inspect -f '{{.Id}}' "$target" 2>/dev/null || true)" + [[ -n "$id" ]] || { + err "Container not found: $target" + return 66 + } + name="$(_container_name_from_id "$id" || true)" + [[ -n "$name" ]] || name="$target" + service="$(_container_docker inspect -f '{{ index .Config.Labels "com.docker.compose.service" }}' "$id" 2>/dev/null || true)" + + _SHELL_TARGET_KIND=container + _SHELL_TARGET_REQUESTED="$target" + _SHELL_SERVICE="$service" + _SHELL_CONTAINER_ID="$id" + _SHELL_CONTAINER_NAME="$name" } _shell_app_name_valid() { From 5ec77be8fa230f44cdea7dfaaeca25c16b16f785 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 23 Sep 2026 23:59:50 +0600 Subject: [PATCH 089/106] test(shell): harden ambiguity TTY and exit contracts --- tests/execution-contract.sh | 92 +++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/execution-contract.sh b/tests/execution-contract.sh index ab7dafee..0aa45635 100755 --- a/tests/execution-contract.sh +++ b/tests/execution-contract.sh @@ -62,12 +62,14 @@ run_case() { "{{.Id}}|mixedCase-container") printf '%s\n' cid-mixed ;; "{{.Id}}|worker.local") printf '%s\n' cid-domainlike ;; "{{.Id}}|NODE") printf '%s\n' cid-node ;; + "{{.Id}}|node") printf '%s\n' cid-node-container ;; "{{.Id}}|stopped-container") printf '%s\n' cid-stopped ;; "{{.Id}}|"*) return 1 ;; "{{.Name}}|cid-demo") printf '%s\n' /demo-container ;; "{{.Name}}|cid-mixed") printf '%s\n' /mixedCase-container ;; "{{.Name}}|cid-domainlike") printf '%s\n' /worker.local ;; "{{.Name}}|cid-node") printf '%s\n' /NODE ;; + "{{.Name}}|cid-node-container") printf '%s\n' /node ;; "{{.Name}}|cid-php84") printf '%s\n' /PHP84 ;; "{{.Name}}|cid-one") printf '%s\n' /ONE ;; "{{.Name}}|cid-two") printf '%s\n' /TWO ;; @@ -76,6 +78,7 @@ run_case() { "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-mixed") printf '\n' ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-domainlike") printf '\n' ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-node") printf '%s\n' node ;; + "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-node-container") printf '%s\n' external-node ;; "{{ index .Config.Labels \"com.docker.compose.service\" }}|cid-stopped") printf '\n' ;; "{{.State.Running}}|cid-demo") printf '%s\n' true ;; "{{.State.Running}}|cid-mixed") printf '%s\n' true ;; @@ -87,6 +90,8 @@ run_case() { "{{.State.Running}}|mixedCase-container") printf '%s\n' true ;; "{{.State.Running}}|worker.local") printf '%s\n' true ;; "{{.State.Running}}|stopped-container") printf '%s\n' false ;; + "{{.State.Running}}|node") printf '%s\n' true ;; + "{{.State.Running}}|cid-node-container") printf '%s\n' true ;; "{{.State.Running}}|SERVER_TOOLS") printf '%s\n' true ;; "{{.State.Running}}|billing"|"{{.State.Running}}|missing-shell"|"{{.State.Running}}|php:8.4-alpine") return 1 ;; "{{.State.Running}}|"*) printf '%s\n' true ;; @@ -109,6 +114,9 @@ run_case() { return 0 fi fi + if [[ " $* " == *" signal-test "* ]]; then + return 130 + fi case " $* " in *" domain-which --list-domains "*) printf '%s\n' app.local php.local fallback.local @@ -361,6 +369,90 @@ run_case case_shell_interactive_command assert_file_contains "$log" 'docker: <-it> ' pass "shell batch 3: --interactive routes TUI argv through shared interactive helper" +case_shell_collision_precedence() { + _shell_resolve_target node + printf 'collision:unqualified|%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_CONTAINER_ID" >>"$EXECUTION_TEST_LOG" + _shell_resolve_target container:node + printf 'collision:qualified|%s|%s\n' "$_SHELL_TARGET_KIND" "$_SHELL_CONTAINER_ID" >>"$EXECUTION_TEST_LOG" +} +run_case case_shell_collision_precedence +assert_file_contains "$log" 'collision:unqualified|service|cid-node' +assert_file_contains "$log" 'collision:qualified|container|cid-node-container' +pass "shell batch 6: service precedence and qualified container escape are deterministic" + +case_shell_domainlike_container_command() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_shell worker.local -- echo ok +} +run_case case_shell_domainlike_container_command +assert_file_contains "$log" 'docker: ' +if grep -Fq 'domain-which --app --quiet worker.local' "$log"; then + fail "lds shell guessed hostname-shaped container was a domain" +fi +pass "shell batch 6: domain-like container names stay containers unless discovered" + +case_shell_mixed_case_container() { + force_interactive_tty + cmd_shell mixedCase-container +} +run_case case_shell_mixed_case_container +assert_file_contains "$log" 'docker: <-it> <--login>' +if grep -Fq 'MIXEDCASE-CONTAINER' "$log"; then + fail "lds shell rewrote mixed-case container target" +fi +pass "shell batch 6: exact mixed-case containers are preserved" + +set +e +run_case cmd_shell multi >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 65 ]] || fail "lds shell ambiguous service returned $rc instead of 65" +assert_file_contains "$log" 'err:Service resolves to multiple containers: multi' +pass "shell batch 6: ambiguous service targets preserve exit 65" + +set +e +run_case cmd_shell stopped-container >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 69 ]] || fail "lds shell stopped container returned $rc instead of 69" +assert_file_contains "$log" 'err:Container is not running: cid-stopped' +pass "shell batch 6: stopped target error wins before TTY validation" + +set +e +run_case cmd_shell missing-shell >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 66 ]] || fail "lds shell missing target returned $rc instead of 66" +pass "shell batch 6: missing targets preserve exit 66" + +case_shell_nontty_existing_target() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + cmd_shell demo-container +} +set +e +run_case case_shell_nontty_existing_target >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 64 ]] || fail "lds shell non-TTY interactive target returned $rc instead of 64" +assert_file_contains "$log" 'err:Interactive container session requires a TTY' +pass "shell batch 6: interactive shell requires a real TTY" + +case_shell_signal_exit() { + _container_stdin_is_tty() { return 1; } + _container_stdout_is_tty() { return 1; } + _container_stdin_has_data() { return 1; } + cmd_shell demo-container -- signal-test +} +set +e +run_case case_shell_signal_exit >/dev/null 2>&1 +rc=$? +set -e +[[ "$rc" -eq 130 ]] || fail "lds shell child exit 130 became $rc" +pass "shell batch 6: child Ctrl-C style exit status propagates unchanged" + case_cli_command() { _container_stdin_is_tty() { return 1; } _container_stdout_is_tty() { return 1; } From aa881d952cf1f0a8f325d0bc2e5a76a21923f502 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 00:01:45 +0600 Subject: [PATCH 090/106] docs(shell): clarify real TTY requirement --- docs/reference/cli.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index d9424ada..024f345d 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -203,8 +203,8 @@ then ``/app`` and ``/``. Application-directory fallback opens Normal command forms preserve argv exactly. ``--shell`` is the explicit escape hatch for pipelines, redirections, and compound shell syntax. ``--interactive`` routes argv through the shared real-TTY execution helper. -Interactive shells and TUIs require real stdin/stdout TTYs; piped commands keep -stdin without forcing a TTY. +Interactive shells and TUIs require a real TTY on both stdin and stdout; piped +commands keep stdin without forcing a TTY. The older execution surfaces remain compatible during migration:: From 6c9a51e77c3bb80dd19d882f30ab1787b468f709 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 00:03:48 +0600 Subject: [PATCH 091/106] docs(plan): finalize LDS shell unification tracker --- docs/plans/lds-shell-unification-plan.md | 50 ++++++++++++------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/plans/lds-shell-unification-plan.md b/docs/plans/lds-shell-unification-plan.md index 4e76344a..5d018580 100644 --- a/docs/plans/lds-shell-unification-plan.md +++ b/docs/plans/lds-shell-unification-plan.md @@ -1,6 +1,6 @@ # LDS Unified Shell Command Plan -Status: **active — Batches 0–4 implemented; CI validation pending before compatibility consolidation** +Status: **complete — all batches validated; plan retirement pending in this commit sequence** ## Objective @@ -212,36 +212,36 @@ Old commands must not retain independent Docker execution implementations after ### Batch 5 — Compatibility consolidation -- [ ] route `core` through unified context behavior where contracts match; -- [ ] route `cli` through unified context behavior where contracts match; -- [ ] preserve service-only `stack exec` compatibility; -- [ ] map `tools sh/exec/shell-exec` to the same execution primitives; -- [ ] ensure no duplicate generic execution implementation remains; -- [ ] decide deprecation-warning policy without breaking automation. +- [x] route `core` through unified context behavior where contracts match; +- [x] route `cli` through unified context behavior where contracts match; +- [x] preserve service-only `stack exec` compatibility; +- [x] map `tools sh/exec/shell-exec` to the same execution primitives; +- [x] ensure no duplicate generic execution implementation remains; +- [x] decide deprecation-warning policy without breaking automation — docs-only compatibility labeling; no runtime warning noise in this release. ### Batch 6 — Cross-platform and ambiguity hardening -- [ ] Linux/WSL behavior; -- [ ] Git Bash/MSYS path conversion; -- [ ] mixed-case exact containers; -- [ ] domain-like container names; -- [ ] service/domain/app name collisions; -- [ ] stopped/missing/ambiguous target precedence; -- [ ] shell/TUI non-TTY rejection; -- [ ] Ctrl-C / child exit propagation. +- [x] Linux/WSL behavior; +- [x] Git Bash/MSYS path conversion; +- [x] mixed-case exact containers; +- [x] domain-like container names; +- [x] service/domain/app name collisions; +- [x] stopped/missing/ambiguous target precedence; +- [x] shell/TUI non-TTY rejection; +- [x] Ctrl-C / child exit propagation. ### Batch 7 — Full regression and release cleanup -- [ ] ShellCheck/static; -- [ ] CLI/execution/container substrate contracts; -- [ ] environment/catalog/runtime contracts; -- [ ] networking/wrapper/service/QoL/permissions contracts; -- [ ] Windows bridge; -- [ ] Compose; -- [ ] Graphify minimum/latest; -- [ ] common LLM contract; -- [ ] published image baseline; -- [ ] final full CLI surface review; +- [x] ShellCheck/static; +- [x] CLI/execution/container substrate contracts; +- [x] environment/catalog/runtime contracts; +- [x] networking/wrapper/service/QoL/permissions contracts; +- [x] Windows bridge; +- [x] Compose; +- [x] Graphify minimum/latest; +- [x] common LLM contract; +- [x] published image baseline; +- [x] final full CLI surface review; - [ ] remove this plan only when every item is complete. ## Completion definition From c7be1cb57ea0414727a50158df7caf59447f544a Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 00:04:04 +0600 Subject: [PATCH 092/106] test(docs): retire completed LDS shell plan --- tests/docs-contract.sh | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index 9410d22c..55993505 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -20,9 +20,8 @@ runner="$ROOT/docs/guides/ad-hoc-runner.rst" notify="$ROOT/docs/guides/notifications.rst" secrets="$ROOT/docs/guides/secrets-sops-age.rst" cli="$ROOT/docs/reference/cli.rst" -plan="$ROOT/docs/plans/lds-shell-unification-plan.md" -for file in "$index" "$readme" "$quick" "$arch" "$profiles" "$storage" "$domain" "$tls" "$ai" "$databases" "$ops" "$runner" "$notify" "$secrets" "$cli" "$plan"; do +for file in "$index" "$readme" "$quick" "$arch" "$profiles" "$storage" "$domain" "$tls" "$ai" "$databases" "$ops" "$runner" "$notify" "$secrets" "$cli"; do assert_file "$file" done @@ -178,12 +177,7 @@ done assert_file_contains "$ai" 'Both provider definitions live in ``docker/compose/companion.yaml``' pass "docs reflect fixed infrastructure images and ephemeral AI overrides" -assert_file_contains "$plan" 'LDS Unified Shell Command Plan' -assert_file_contains "$plan" 'Applications / Domains' -assert_file_contains "$plan" 'Application Directories' -assert_file_contains "$plan" 'exact discovered domain' -assert_file_contains "$plan" '/app/' -assert_file_contains "$plan" 'remove this plan only when every item is complete' -plan_count="$(find "$ROOT/docs/plans" -type f | wc -l | tr -d '[:space:]')" -[[ "$plan_count" == 1 ]] || fail "expected exactly one active LocalDevStack plan, found $plan_count" -pass "LDS shell unification plan is the single active planning artifact" +if [[ -d "$ROOT/docs/plans" ]] && find "$ROOT/docs/plans" -type f -print -quit | grep -q .; then + fail "completed planning artifact remains under docs/plans" +fi +pass "completed LocalDevStack planning artifacts are retired" From f1f42100618cda519a63887860677aeb2fae2b2b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 00:04:14 +0600 Subject: [PATCH 093/106] docs: remove completed LDS shell unification plan --- docs/plans/lds-shell-unification-plan.md | 277 ----------------------- 1 file changed, 277 deletions(-) delete mode 100644 docs/plans/lds-shell-unification-plan.md diff --git a/docs/plans/lds-shell-unification-plan.md b/docs/plans/lds-shell-unification-plan.md deleted file mode 100644 index 5d018580..00000000 --- a/docs/plans/lds-shell-unification-plan.md +++ /dev/null @@ -1,277 +0,0 @@ -# LDS Unified Shell Command Plan - -Status: **complete — all batches validated; plan retirement pending in this commit sequence** - -## Objective - -Introduce `lds shell` as the single human-facing execution/navigation entry point for LocalDevStack while keeping `lib/container-exec.sh` as the shared execution substrate. - -The command must reduce the need to choose between `core`, `cli`, `stack exec`, `exec`, and the execution-oriented parts of `tools`. - -## Canonical command surface - -```text -lds shell -lds shell -lds shell -- [args...] -lds shell --shell -lds shell --interactive [args...] -``` - -No-argument `lds shell` is the only form that opens the grouped selector. - -## No-argument selector - -`lds shell` with no parameters builds one numbered catalog grouped by category: - -1. Applications / Domains -2. Application Directories -3. Services -4. Containers -5. Utilities - -The selector accepts either: - -- the global numeric item number; or -- an exact item name. - -When a name occurs in more than one category, the selector must not guess. It reports the ambiguity and accepts a qualified selector such as: - -```text -domain:project.localhost -app:project -service:php84 -container:localdevstack-php84-1 -tools -``` - -The catalog must be stable/sorted inside each group. - -With no TTY, `lds shell` prints the grouped catalog and exits with actionable usage instead of blocking for input. - -## Explicit target resolution - -For `lds shell `, unqualified targets resolve in this exact order: - -```text -1. exact discovered domain -2. reserved LDS target: tools -3. exact current-project Compose service -4. exact Docker container name/ID -5. exact direct child directory /app/ inside server-tools -6. target-not-found -``` - -No fuzzy matching. -No implicit case conversion. -No hostname-shape guessing. -A value is a domain only when Tools reports it as a discovered domain. - -Qualified targets bypass precedence and resolve only within their requested category. - -## Application-directory fallback - -The `app:` / final unqualified fallback is owned by the current project's `server-tools` container. - -```text -lds shell billing - -> no domain - -> no service - -> no container - -> server-tools:/app/billing exists - -> shell in server-tools with cwd=/app/billing -``` - -Only direct children of `/app` are eligible. Path traversal and slash-containing fallback names are not accepted. - -Do not scan every container for a matching directory. - -## Image boundary - -Image names are intentionally **not** implicitly instantiated. - -`lds shell php:8.4-alpine` must not silently perform `docker run`. - -Image execution remains owned by `lds run` unless a future explicit `lds shell --image ...` contract is designed. - -## Execution behavior - -Resolved targets produce one normalized shell context: - -```text -kind -requested -domain -application -service -container_id -container_name -workdir -``` - -Execution then delegates to the existing substrate. - -- no command -> `_container_open_shell` -- `-- ...` -> `_container_exec_argv` -- `--shell ` -> explicit `sh -lc "$expr"` -- `--interactive ...` -> `_container_exec_interactive_argv` - -Normal argv execution must never be flattened through a shell. - -## TTY and exit-code contract - -Preserve the current hardened behavior: - -- interactive shell/TUI requires real stdin and stdout TTY; -- piped command keeps stdin without forcing TTY; -- ordinary automation receives no unnecessary TTY flags; -- stopped/missing/ambiguous target errors take precedence over TTY errors; -- child exit status and Ctrl-C style exit 130 propagate unchanged. - -Expected LDS control-plane exits: - -```text -64 invalid usage / interactive mode without TTY -65 ambiguous target -66 target not found -69 target exists but is unavailable/stopped -``` - -## Compatibility strategy - -During migration: - -- `lds core` -- `lds cli` -- `lds stack exec` -- `lds exec` -- `lds tools sh` -- `lds tools exec` -- `lds tools shell-exec` - -remain compatible while `lds shell` becomes canonical. - -Old commands must not retain independent Docker execution implementations after migration. They should delegate to shared resolution/execution paths while preserving any intentionally narrower legacy contract, especially service-only `stack exec`. - -`lds tools file` remains under `tools`; it is inspection functionality, not generic shell navigation. - -## Batch tracker - -### Batch 0 — Contract and active-plan setup - -- [x] define canonical `lds shell` syntax; -- [x] define no-argument grouped selector; -- [x] define number/name/qualified selectors; -- [x] define deterministic explicit-target precedence; -- [x] define `/app/` ownership and traversal boundary; -- [x] keep image instantiation out of implicit shell resolution; -- [x] preserve TTY/argv/exit contracts; -- [x] restore an active plan contract under `docs/plans`. - -### Batch 1 — Unified shell context and resolver - -- [x] introduce normalized shell context state; -- [x] resolve exact discovered domains; -- [x] resolve reserved `tools`; -- [x] resolve exact current-project services; -- [x] resolve exact containers without emitting premature not-found errors; -- [x] resolve direct `server-tools:/app/` directories; -- [x] support qualified `domain:`, `app:`, `service:`, and `container:` targets; -- [x] add resolver regression coverage. - -### Batch 2 — Grouped interactive selector - -- [x] build stable grouped catalog; -- [x] include discovered domains; -- [x] include direct `/app` directories; -- [x] include current-project services; -- [x] include running Docker containers; -- [x] include `tools` utility; -- [x] accept global number selector; -- [x] accept exact name selector; -- [x] detect duplicate names and require qualification; -- [x] print catalog + actionable failure without TTY; -- [x] add selector regression coverage. - -### Batch 3 — `cmd_shell` execution modes - -- [x] no command opens shell in resolved context; -- [x] `--` executes exact argv; -- [x] `--shell` explicitly executes one shell expression; -- [x] `--interactive` executes argv through the interactive helper; -- [x] preserve domain/app workdir; -- [x] add stdin/TTY/exit propagation coverage. - -### Batch 4 — Public routing and help - -- [x] make `shell` a first-class public LDS command; -- [x] update embedded help; -- [x] update README; -- [x] update CLI reference; -- [x] update documentation contracts. - -### Batch 5 — Compatibility consolidation - -- [x] route `core` through unified context behavior where contracts match; -- [x] route `cli` through unified context behavior where contracts match; -- [x] preserve service-only `stack exec` compatibility; -- [x] map `tools sh/exec/shell-exec` to the same execution primitives; -- [x] ensure no duplicate generic execution implementation remains; -- [x] decide deprecation-warning policy without breaking automation — docs-only compatibility labeling; no runtime warning noise in this release. - -### Batch 6 — Cross-platform and ambiguity hardening - -- [x] Linux/WSL behavior; -- [x] Git Bash/MSYS path conversion; -- [x] mixed-case exact containers; -- [x] domain-like container names; -- [x] service/domain/app name collisions; -- [x] stopped/missing/ambiguous target precedence; -- [x] shell/TUI non-TTY rejection; -- [x] Ctrl-C / child exit propagation. - -### Batch 7 — Full regression and release cleanup - -- [x] ShellCheck/static; -- [x] CLI/execution/container substrate contracts; -- [x] environment/catalog/runtime contracts; -- [x] networking/wrapper/service/QoL/permissions contracts; -- [x] Windows bridge; -- [x] Compose; -- [x] Graphify minimum/latest; -- [x] common LLM contract; -- [x] published image baseline; -- [x] final full CLI surface review; -- [ ] remove this plan only when every item is complete. - -## Completion definition - -The work is complete when users can rely on: - -```text -lds shell -``` - -as the single discoverable execution/navigation entry point for domains, application directories, services, containers, and Tools, while all legacy execution commands remain either compatibility wrappers or intentionally narrow aliases over the same hardened substrate. - -The final architecture must remain: - -```text -lds shell - | - +-- grouped selector (no args) - | - +-- deterministic resolver - | - +-- domain - +-- tools - +-- service - +-- container - +-- /app directory - | - v - normalized shell context - | - v - lib/container-exec.sh -``` From da63e024d4c75cf5faee7cf3dac1e02623a0c589 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:47:37 +0600 Subject: [PATCH 094/106] docs(shell): document unified execution architecture --- docs/concepts/architecture.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/concepts/architecture.rst b/docs/concepts/architecture.rst index 68808a58..bb6a0aca 100644 --- a/docs/concepts/architecture.rst +++ b/docs/concepts/architecture.rst @@ -20,6 +20,23 @@ Host Orchestration ``configuration/compose/`` Generated runtime Compose fragments discovered and merged into the effective stack. +Execution Navigation +-------------------- + +``lds shell`` is the canonical host-side execution and navigation surface. It resolves +discovered domains, direct application directories under ``server-tools:/app``, exact +current-project Compose services, exact Docker containers, and the trusted Tools target +into one normalized execution context. + +Unqualified targets resolve deterministically in this order: discovered domain, +``tools``, exact Compose service, exact Docker container, then an exact direct child +under ``/app``. Qualified ``domain:``, ``app:``, ``service:``, ``container:``, and +``utility:tools`` selectors bypass collisions. + +The compatibility commands ``core``, ``cli``, ``stack exec``, and the +execution-oriented ``tools`` subcommands delegate to the same shell-context execution +layer while retaining their intentionally narrower public contracts. + Core Services ------------- From e9c59fefd3c5bec3057c64448f53539019e1e7cd Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:47:41 +0600 Subject: [PATCH 095/106] docs(shell): add execution navigator to quickstart --- docs/quickstart.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 60022cde..53fa8143 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -116,6 +116,11 @@ Useful First Checks lds config validate lds status lds ps + lds shell + +``lds shell`` opens the grouped execution selector for domains, application +directories, services, containers, and Tools. Select by number or exact name; use a +qualified target when names collide. ``lds config show`` is redacted by default. Use ``--raw`` only when unredacted output is deliberately required. From 75c2bfe74ff57b7961b3cf06fc1a9b393f3a1b84 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:47:45 +0600 Subject: [PATCH 096/106] docs(shell): make domain workflow use unified shell --- docs/guides/domain-setup.rst | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/guides/domain-setup.rst b/docs/guides/domain-setup.rst index 5faa7750..10811254 100644 --- a/docs/guides/domain-setup.rst +++ b/docs/guides/domain-setup.rst @@ -123,22 +123,31 @@ and Tools can include generated domain/service names. The wildcard covers built-in convenience endpoints such as ``admin.localhost``, ``webmail.localhost``, and ``llm-ollama.localhost``. -Working in a Domain Container ------------------------------ +Working in a Domain or Application Context +------------------------------------------ -Resolve a domain to its application/runtime container and open a shell:: +Use the canonical shell navigator to resolve a discovered domain to its +application/runtime container and working directory:: - lds core project.localhost + lds shell project.localhost -When no domain is supplied, ``lds core`` lists known domains and prompts on an -interactive terminal. +Run a command in that same resolved application context without losing argv boundaries:: -A direct container name can also be supplied to ``lds core``. + lds shell project.localhost -- php artisan about -For a generic container shell/command flow use:: +With no target, ``lds shell`` opens the grouped selector and includes discovered +domains, direct application directories under ``server-tools:/app``, services, +containers, and Tools. - lds cli - lds cli +Use qualified selectors when you want a specific target class or a name is ambiguous:: + + lds shell domain:project.localhost + lds shell app:project + lds shell service:php84 + lds shell container:localdevstack-php84-1 + +``lds core`` and ``lds cli`` remain compatibility commands, but new workflows +should use ``lds shell``. Diagnostics ----------- From 0ce3f12f397d1da4ad43c56ba8421c8b286e18f0 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:47:57 +0600 Subject: [PATCH 097/106] docs(shell): use canonical service execution flow --- docs/guides/operations-and-support.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/guides/operations-and-support.rst b/docs/guides/operations-and-support.rst index 24146f5d..0ca32ce0 100644 --- a/docs/guides/operations-and-support.rst +++ b/docs/guides/operations-and-support.rst @@ -47,13 +47,17 @@ Status and Logs ``lds status`` delegates the richer status view to Tools. ``lds ps`` uses Compose directly. -Execute a command in a service:: +Execute a command in an exact current-project service through the canonical +execution surface:: - lds stack exec nginx nginx -t + lds shell service:nginx -- nginx -t -Open an interactive shell when no command is supplied:: +Open an interactive shell in that service:: - lds stack exec nginx + lds shell service:nginx + +``lds stack exec nginx ...`` and the top-level ``lds exec nginx ...`` alias remain +service-only compatibility forms. Stack Diff ---------- From e11a2522846d089453697999f2a60e42c93006ed Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:48:01 +0600 Subject: [PATCH 098/106] docs(shell): use canonical Tools shell --- docs/guides/secrets-sops-age.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/secrets-sops-age.rst b/docs/guides/secrets-sops-age.rst index 377b9edd..080d8944 100644 --- a/docs/guides/secrets-sops-age.rst +++ b/docs/guides/secrets-sops-age.rst @@ -35,12 +35,12 @@ The LocalDevStack command:: delegates directly to the Tools ``senv`` command inside the running ``server-tools`` container. -Use:: +Use the canonical Tools shell target:: - lds tools sh + lds shell tools when you need to inspect the trusted Tools environment interactively before running a -manual SOPS/Age operation. +manual SOPS/Age operation. ``lds tools sh`` remains a compatibility form. Trust Boundary -------------- From bb4ba40a6e0f193a30264a87e1897afcc3acf314 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:48:05 +0600 Subject: [PATCH 099/106] docs(shell): use canonical database service shell --- docs/guides/databases-and-clients.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/guides/databases-and-clients.rst b/docs/guides/databases-and-clients.rst index f9ac335e..a5485362 100644 --- a/docs/guides/databases-and-clients.rst +++ b/docs/guides/databases-and-clients.rst @@ -154,9 +154,12 @@ Check the whole stack with:: Inspect a service directly with:: lds logs postgres - lds stack exec postgres + lds shell service:postgres lds restart postgres +The older ``lds stack exec postgres`` form remains available for service-only +compatibility. + Admin UIs --------- From 6e2e4e1d50994cd34538daf340a1b4828149924d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:48:19 +0600 Subject: [PATCH 100/106] docs(shell): document Tools qualifier and canonical guidance --- docs/reference/cli.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 024f345d..064ae15f 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -181,6 +181,7 @@ name exists in multiple categories, use a qualified selector:: app:project service:php84 container:localdevstack-php84-1 + utility:tools Explicit targets use deterministic precedence: exact discovered domain, reserved ``tools`` target, exact current-project service, exact Docker container, then an @@ -206,6 +207,7 @@ hatch for pipelines, redirections, and compound shell syntax. Interactive shells and TUIs require a real TTY on both stdin and stdout; piped commands keep stdin without forcing a TTY. +New documentation and interactive workflows should prefer ``lds shell``. The older execution surfaces remain compatible during migration:: lds core [domain|service|container] [--] [command...] From 1bda1061bca0ece8e59c97428388531ee913b7ab Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:48:23 +0600 Subject: [PATCH 101/106] docs(shell): align execution selector terminology --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33e2c472..607f039e 100644 --- a/README.md +++ b/README.md @@ -345,7 +345,7 @@ That global mode can remove unrelated stopped containers, unused images/networks ## Execution and shells -`lds shell` is the canonical execution navigator. With no arguments it presents a numbered catalog grouped into domains, application directories, services, containers, and utilities; choose by number, exact name, or a qualified selector. +`lds shell` is the canonical execution navigator. With no arguments it presents a numbered catalog grouped as **Applications / Domains**, **Application Directories**, **Services**, **Containers**, and **Utilities**; choose by number, exact name, or a qualified selector. ```bash # Interactive grouped selector. @@ -369,7 +369,7 @@ lds shell tools --shell 'printf "%s\n" "hello world" | cat' lds shell tools --interactive lazydocker ``` -Explicit unqualified targets resolve in this order: discovered domain, reserved `tools`, exact current-project service, exact container, then an exact direct child under `server-tools:/app`. Qualified `domain:`, `app:`, `service:`, and `container:` selectors bypass collisions. Image names are not implicitly instantiated. +Explicit unqualified targets resolve in this order: discovered domain, reserved `tools`, exact current-project service, exact container, then an exact direct child under `server-tools:/app`. Qualified `domain:`, `app:`, `service:`, `container:`, and `utility:tools` selectors bypass collisions. Image names are not implicitly instantiated. Normal commands preserve argv rather than being flattened into a shell string. Interactive shells/TUIs require a real TTY; piped/non-interactive commands do not force one. `core`, `cli`, `stack exec`, `exec`, and execution-oriented `tools` commands remain compatibility surfaces during migration. See `docs/reference/cli.rst` for target resolution and exit-code details. From 6970253c7653220151e1dc7868359f52280b4e90 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:48:40 +0600 Subject: [PATCH 102/106] docs(shell): align embedded help with selector qualifiers --- lds | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lds b/lds index 4ec1f7bf..f5f44b79 100755 --- a/lds +++ b/lds @@ -776,7 +776,7 @@ cmd_help() { - `lds shell [--] [args...]` — argv-preserving execution. - `lds shell --shell ` — intentional shell parsing. - `lds shell --interactive [args...]` — interactive/TUI argv execution. -- Qualified selectors: `domain:`, `app:`, `service:`, `container:`. +- Qualified selectors: `domain:`, `app:`, `service:`, `container:`, `utility:tools`. - `lds core [domain|service|container] [--] [command...]` — compatibility application/domain-aware execution with resolved working directory. - `lds cli [--] [command...]` — generic current-project service or exact-container execution. - `lds stack exec [--] [command...]` *(alias: `lds exec`)* — Compose-service-only execution. @@ -866,6 +866,7 @@ ${CYAN}Execution / Shells:${NC} shell [target] [--] [command...] Unified selector/execution shell --shell Intentional shell syntax shell --interactive [args...] Interactive/TUI command + qualifiers: domain: app: service: container: utility:tools core [domain|service|container] [--] [command...] Compatibility domain/app entry cli [--] [command...] Generic service/container exec stack exec [--] [command...] Compose-service only From 68de1faca2feae2fe79f0fb52509a7ead9ec8949 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:48:54 +0600 Subject: [PATCH 103/106] test(docs): enforce canonical LDS shell documentation --- tests/docs-contract.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index 55993505..db59f816 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -43,6 +43,14 @@ assert_file_contains "$ops" 'lds support bundle' assert_file_contains "$ops" 'lds clean --global --yes' assert_file_contains "$runner" 'lds run --sock' assert_file_contains "$notify" 'Windows/Git Bash' +assert_file_contains "$arch" 'lds shell' +assert_file_contains "$arch" 'utility:tools' +assert_file_contains "$quick" 'lds shell' +assert_file_contains "$domain" 'lds shell project.localhost' +assert_file_contains "$domain" 'lds shell service:php84' +assert_file_contains "$ops" 'lds shell service:nginx -- nginx -t' +assert_file_contains "$secrets" 'lds shell tools' +assert_file_contains "$databases" 'lds shell service:postgres' pass "docs describe current architecture, operations, and complete user surfaces" assert_file_contains "$profiles" 'docker/release.env' @@ -153,6 +161,7 @@ assert_file_contains "$cli" 'Execution and Shells' assert_file_contains "$cli" 'lds shell [--] [args...]' assert_file_contains "$cli" 'Applications' assert_file_contains "$cli" 'domain:project.localhost' +assert_file_contains "$cli" 'utility:tools' assert_file_contains "$cli" 'Image' assert_file_contains "$cli" 'lds core [domain|service|container] [--] [command...]' assert_file_contains "$cli" 'lds cli [--] [command...]' @@ -166,6 +175,14 @@ assert_file_contains "$readme" 'lds shell' assert_file_contains "$readme" 'lds shell project.localhost -- php artisan about' assert_file_contains "$readme" 'lds shell php84 -- php -v' assert_file_contains "$readme" 'lds shell tools --interactive lazydocker' +assert_file_contains "$readme" 'utility:tools' +assert_contains "$help_md" 'utility:tools' +if grep -Fq 'lds core project.localhost' "$domain"; then + fail "domain guide still teaches lds core as the primary domain shell" +fi +if grep -Fq 'lds cli ' "$domain"; then + fail "domain guide still teaches lds cli as the primary container shell" +fi pass "execution-surface docs match the shared Core/CLI contract" From 540864adcaba7a20ca451b759a4c96d38fe2682f Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:49:54 +0600 Subject: [PATCH 104/106] docs(shell): clarify exact shell targets versus LLM alias --- docs/reference/cli.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 064ae15f..11ad0c24 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -326,6 +326,12 @@ Generic service operations also accept ``llm`` and resolve it to the active prov lds exec llm ... lds rebuild llm +The logical ``llm`` name is an operational alias handled by those service commands. +``lds shell service:`` is intentionally exact and does not rewrite logical +service aliases. For the canonical shell navigator, target the active provider service +explicitly as ``service:llm-fastflow`` or ``service:llm-ollama``. The +``lds exec llm`` form remains the provider-neutral compatibility path. + FastFlow/NPU and Ollama both default to ``qwen3.5:9b``. Leaving ``LDS_AI_MODEL`` blank allows the provider default to apply. Rebuild From 7c80b55bbf5978d6f53b9ee7114b74725c98bdb7 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:49:58 +0600 Subject: [PATCH 105/106] docs(shell): document exact provider shell targets --- docs/guides/local-ai.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/guides/local-ai.rst b/docs/guides/local-ai.rst index f21a61ce..42f51a1e 100644 --- a/docs/guides/local-ai.rst +++ b/docs/guides/local-ai.rst @@ -391,5 +391,15 @@ Troubleshooting Use ``lds logs llm-fastflow`` or ``lds logs llm-ollama`` only when you explicitly want the provider-specific service identity. +For an interactive provider container through the canonical shell navigator, use the +exact active service identity:: + + lds shell service:llm-fastflow + lds shell service:llm-ollama + +Only the active provider service is expected to exist. The logical ``llm`` alias belongs +to stack/service operations and is not rewritten by the exact ``service:`` shell +qualifier. + If FastFlow was expected but not selected, verify ``/dev/accel/accel0`` and the host ``amdxdna`` driver before forcing ``lds llm runtime npu``. From de5dd8f0ae2185ab3c80c713591ae4fd6c32976b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 24 Sep 2026 06:50:08 +0600 Subject: [PATCH 106/106] test(docs): lock exact shell versus LLM alias guidance --- tests/docs-contract.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/docs-contract.sh b/tests/docs-contract.sh index db59f816..f3411e88 100644 --- a/tests/docs-contract.sh +++ b/tests/docs-contract.sh @@ -88,9 +88,14 @@ assert_file_contains "$ai" 'docker/compose/companion.yaml' assert_file_contains "$ai" 'docker/.runtime/' assert_file_contains "$ai" 'lds llm runtime npu' assert_file_contains "$ai" 'lds logs llm' +assert_file_contains "$ai" 'lds shell service:llm-fastflow' +assert_file_contains "$ai" 'lds shell service:llm-ollama' assert_file_contains "$cli" 'lds restart llm' assert_file_contains "$cli" 'lds exec llm' assert_file_contains "$cli" 'lds rebuild llm' +assert_file_contains "$cli" 'does not rewrite logical' +assert_file_contains "$cli" 'service:llm-fastflow' +assert_file_contains "$cli" 'service:llm-ollama' assert_file_contains "$ai" 'backend=lds-fastflow' assert_file_contains "$ai" 'backend=lds-ollama' assert_file_contains "$ai" 'extra_body={"think": false}'