platform-tooling/scripts/lib/services.sh
Quinn Ftw 85621b287e chore: snapshot before monorepo consolidation
Capture current working state before converting platform-tooling
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:39 -08:00

1027 lines
29 KiB
Bash
Executable file

#!/usr/bin/env bash
# =============================================================================
# services.sh - Service configuration utilities for dev scripts
# =============================================================================
#
# Source this file in dev scripts to access service configuration from
# feature services.yaml files combined with ports.yaml
#
# Usage:
# source "$(dirname "$0")/lib/services.sh"
#
# # Get all dependencies for a feature
# get_feature_deps "analytics"
#
# # Get service URL
# get_service_url "analytics" "api"
#
# # Start all deps for a feature
# start_feature_deps "analytics"
#
# # Check health of all deps
# check_feature_health "analytics"
#
# =============================================================================
# Ensure ports.sh is loaded
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! declare -f get_port &>/dev/null; then
source "$_SCRIPT_DIR/ports.sh"
fi
# =============================================================================
# Configuration
# =============================================================================
SERVICES_PROJECT_ROOT="${SERVICES_PROJECT_ROOT:-$PORTS_PROJECT_ROOT}"
SERVICES_DIR="${SERVICES_PROJECT_ROOT}/codebase/features"
SERVICES_INFRA_DIR="${SERVICES_PROJECT_ROOT}/infrastructure/services/features"
# =============================================================================
# Service YAML Access
# =============================================================================
# Get the services.yaml path for a feature
# Usage: get_feature_services_yaml "analytics"
get_feature_services_yaml() {
local feature="$1"
local yaml_path="${SERVICES_DIR}/${feature}/services.yaml"
if [[ -f "$yaml_path" ]]; then
echo "$yaml_path"
return 0
fi
# Fallback to infrastructure symlink
yaml_path="${SERVICES_INFRA_DIR}/${feature}.yaml"
if [[ -f "$yaml_path" ]]; then
echo "$yaml_path"
return 0
fi
echo "ERROR: services.yaml not found for feature: $feature" >&2
return 1
}
# Check if a feature has a services.yaml
# Usage: has_services_yaml "analytics"
has_services_yaml() {
local feature="$1"
get_feature_services_yaml "$feature" &>/dev/null
}
# =============================================================================
# Service Access Functions
# =============================================================================
# Get list of services defined by a feature
# Usage: get_feature_services "analytics"
get_feature_services() {
_check_yq || return 1
local feature="$1"
local yaml_path
yaml_path=$(get_feature_services_yaml "$feature") || return 1
# Services are stored as arrays, extract the id field
yq '.services[].id' "$yaml_path" 2>/dev/null
}
# Get service type (api, postgresql, redis, frontend, etc.)
# Usage: get_service_type "analytics" "api"
get_service_type() {
_check_yq || return 1
local feature="$1"
local service="$2"
local yaml_path
yaml_path=$(get_feature_services_yaml "$feature") || return 1
# Services are arrays, find by id
yq ".services[] | select(.id == \"$service\") | .type // \"unknown\"" "$yaml_path" 2>/dev/null
}
# Get service port (resolved from ports.yaml)
# Usage: get_service_port "analytics" "api"
get_service_port() {
_check_yq || return 1
local feature="$1"
local service="$2"
# Try direct port lookup first
local port
port=$(get_port "features.${feature}.${service}" 2>/dev/null)
if [[ -n "$port" && "$port" != "null" ]]; then
echo "$port"
return 0
fi
# Try infrastructure ports
port=$(get_port "infrastructure.${service}" 2>/dev/null)
if [[ -n "$port" && "$port" != "null" ]]; then
echo "$port"
return 0
fi
echo "ERROR: Port not found for ${feature}.${service}" >&2
return 1
}
# Get service URL
# Usage: get_service_url "analytics" "api"
get_service_url() {
local feature="$1"
local service="${2:-api}"
local host="${3:-localhost}"
local port
port=$(get_service_port "$feature" "$service") || return 1
local service_type
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
case "$service_type" in
frontend)
echo "http://${host}:${port}"
;;
api|backend)
echo "http://${host}:${port}"
;;
postgresql|postgres)
echo "postgresql://${host}:${port}"
;;
redis)
echo "redis://${host}:${port}"
;;
*)
echo "http://${host}:${port}"
;;
esac
}
# Get API base URL for a feature (convenience function)
# Usage: get_api_url "analytics"
get_api_url() {
local feature="$1"
get_service_url "$feature" "api"
}
# Get frontend URL for a feature (convenience function)
# Usage: get_frontend_url "analytics"
get_frontend_url() {
local feature="$1"
get_service_url "$feature" "frontend"
}
# =============================================================================
# Dependency Functions
# =============================================================================
# Get dependencies for a feature service
# Usage: get_service_deps "analytics" "api"
get_service_deps() {
_check_yq || return 1
local feature="$1"
local service="$2"
local yaml_path
yaml_path=$(get_feature_services_yaml "$feature") || return 1
# Services are arrays, find by id
yq ".services[] | select(.id == \"$service\") | .dependencies[]?" "$yaml_path" 2>/dev/null
}
# Get all dependencies for a feature (across all services)
# Usage: get_feature_deps "analytics"
get_feature_deps() {
_check_yq || return 1
local feature="$1"
local yaml_path
yaml_path=$(get_feature_services_yaml "$feature") || return 1
yq '.services[].dependencies[]? | select(. != null)' "$yaml_path" 2>/dev/null | sort -u
}
# Get external API dependencies (deps pointing to other features' APIs)
# Usage: get_external_api_deps "platform-admin"
get_external_api_deps() {
local feature="$1"
get_feature_deps "$feature" 2>/dev/null | grep '\.api$' | sort -u
}
# Get infrastructure dependencies (postgresql, redis, etc.)
# Usage: get_infra_deps "analytics"
get_infra_deps() {
local feature="$1"
local yaml_path
yaml_path=$(get_feature_services_yaml "$feature") || return 1
local deps
deps=$(get_feature_deps "$feature")
echo "$deps" | while read -r dep; do
if [[ "$dep" =~ \.(postgresql|redis|elasticsearch|rabbitmq)$ ]]; then
echo "$dep"
fi
done
}
# =============================================================================
# Health Check Functions
# =============================================================================
# Get health check path for a service
# Usage: get_health_path "analytics" "api"
get_health_path() {
_check_yq || return 1
local feature="$1"
local service="$2"
local yaml_path
yaml_path=$(get_feature_services_yaml "$feature") || return 1
# Services are arrays, check both .health.path and .healthCheck.path patterns
local path
path=$(yq ".services[] | select(.id == \"$service\") | .healthCheck.path // .health.path // \"/health\"" "$yaml_path" 2>/dev/null)
echo "${path:-/health}"
}
# Check if a service is healthy
# Usage: check_service_health "analytics" "api"
check_service_health() {
local feature="$1"
local service="${2:-api}"
local timeout="${3:-5}"
local url
url=$(get_service_url "$feature" "$service") || return 1
local health_path
health_path=$(get_health_path "$feature" "$service")
local full_url="${url}${health_path}"
if curl -sf --connect-timeout "$timeout" "$full_url" &>/dev/null; then
return 0
fi
return 1
}
# Check health of all services for a feature
# Usage: check_feature_health "analytics"
check_feature_health() {
local feature="$1"
echo "Health check for feature '$feature':"
get_feature_services "$feature" 2>/dev/null | while read -r service; do
local service_type
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
local port
port=$(get_service_port "$feature" "$service" 2>/dev/null)
case "$service_type" in
api|frontend)
if check_service_health "$feature" "$service"; then
echo "$service ($service_type:$port) - healthy"
else
echo "$service ($service_type:$port) - unhealthy"
fi
;;
postgresql|postgres)
if is_port_in_use "$port"; then
echo "$service ($service_type:$port) - listening"
else
echo "$service ($service_type:$port) - not running"
fi
;;
redis)
if is_port_in_use "$port"; then
echo "$service ($service_type:$port) - listening"
else
echo "$service ($service_type:$port) - not running"
fi
;;
*)
if [[ -n "$port" ]] && is_port_in_use "$port"; then
echo " ? $service ($service_type:$port) - port in use"
else
echo " ? $service ($service_type:$port) - status unknown"
fi
;;
esac
done
}
# =============================================================================
# Environment Variable Generation
# =============================================================================
# Generate environment variables for a feature's service configuration
# Usage: generate_service_env "analytics"
generate_service_env() {
_check_yq || return 1
local feature="$1"
local prefix="${2:-}"
# Feature's own services
get_feature_services "$feature" 2>/dev/null | while read -r service; do
local port
port=$(get_service_port "$feature" "$service" 2>/dev/null)
if [[ -n "$port" ]]; then
local var_name
var_name=$(echo "${service}" | tr '[:lower:]-' '[:upper:]_')
if [[ -n "$prefix" ]]; then
echo "export ${prefix}_${var_name}_PORT=$port"
else
echo "export ${var_name}_PORT=$port"
fi
fi
done
# External API dependencies
get_external_api_deps "$feature" 2>/dev/null | while read -r dep; do
local dep_feature dep_service
dep_feature="${dep%.*}"
dep_service="${dep##*.}"
local url
url=$(get_service_url "$dep_feature" "$dep_service" 2>/dev/null)
if [[ -n "$url" ]]; then
local var_name
var_name=$(echo "${dep_feature}" | tr '[:lower:]-' '[:upper:]_')
echo "export ${var_name}_API_URL=$url"
fi
done
}
# Apply environment variables for a feature
# Usage: eval "$(apply_service_env "analytics")"
apply_service_env() {
generate_service_env "$@"
}
# =============================================================================
# Docker Integration
# =============================================================================
# Check if feature has a docker-compose.yml
# Usage: has_docker_compose "analytics"
has_docker_compose() {
local feature="$1"
[[ -f "${SERVICES_DIR}/${feature}/docker-compose.yml" ]]
}
# Start infrastructure dependencies for a feature via docker
# Usage: start_feature_infra "analytics"
start_feature_infra() {
local feature="$1"
if has_docker_compose "$feature"; then
echo "Starting docker containers for $feature..."
docker-compose -f "${SERVICES_DIR}/${feature}/docker-compose.yml" up -d
return $?
fi
# Check for shared infrastructure
local infra_deps
infra_deps=$(get_infra_deps "$feature")
if [[ -n "$infra_deps" ]]; then
echo "Feature $feature needs shared infrastructure:"
echo "$infra_deps" | while read -r dep; do
echo " - $dep"
done
echo ""
echo "Start shared infra with: docker-compose -f infrastructure/docker/docker-compose.dev.yml up -d"
fi
}
# =============================================================================
# Feature Discovery
# =============================================================================
# List all features with services.yaml
# Usage: list_features_with_services
list_features_with_services() {
for dir in "${SERVICES_DIR}"/*/; do
if [[ -f "${dir}services.yaml" ]]; then
basename "$dir"
fi
done
}
# List all services across all features
# Usage: list_all_services
list_all_services() {
_check_yq || return 1
list_features_with_services | while read -r feature; do
get_feature_services "$feature" 2>/dev/null | while read -r service; do
local port type
port=$(get_service_port "$feature" "$service" 2>/dev/null)
type=$(get_service_type "$feature" "$service" 2>/dev/null)
echo "${feature}.${service}:${port}:${type}"
done
done
}
# =============================================================================
# Transitive Dependency Resolution
# =============================================================================
# PID tracking directory
LILITH_PID_DIR="${LILITH_PID_DIR:-/tmp/lilith-services/${USER:-unknown}}"
LILITH_LOCK_DIR="${LILITH_LOCK_DIR:-/tmp/lilith-services/.locks}"
# Get transitive dependencies for a single service (recursive)
# Usage: get_transitive_deps "analytics.api"
get_transitive_deps() {
local service_id="$1"
local visited="${2:-}"
# Parse feature.service
local feature="${service_id%.*}"
local service="${service_id##*.}"
# Check for circular dependency
if [[ " $visited " == *" $service_id "* ]]; then
return 0 # Already visited
fi
local deps
deps=$(get_service_deps "$feature" "$service" 2>/dev/null)
for dep in $deps; do
echo "$dep"
# Recurse for transitive deps
get_transitive_deps "$dep" "$visited $service_id"
done
}
# Get all transitive deps for a feature (all services)
# Usage: get_feature_transitive_deps "analytics"
get_feature_transitive_deps() {
local feature="$1"
(
get_feature_services "$feature" 2>/dev/null | while read -r service; do
get_transitive_deps "${feature}.${service}"
done
) | sort -u
}
# Topologically sort dependencies (startup order)
# Usage: topo_sort "dep1 dep2 dep3"
topo_sort() {
local deps="$*"
local edges=""
for dep in $deps; do
local feature="${dep%.*}"
local service="${dep##*.}"
local subdeps
subdeps=$(get_service_deps "$feature" "$service" 2>/dev/null || true)
for subdep in $subdeps; do
# Only include edges for deps in our set
if [[ " $deps " == *" $subdep "* ]]; then
edges+="$subdep $dep"$'\n'
fi
done
# Add self-reference for nodes with no deps (ensures they appear in output)
if [[ -z "$subdeps" ]]; then
edges+="$dep $dep"$'\n'
fi
done
echo -e "$edges" | tsort 2>/dev/null | tac
}
# =============================================================================
# Service Status Detection (Deduplication)
# =============================================================================
# Check if a service is running via health check or port
# Usage: is_service_running "analytics" "api"
is_service_running() {
local feature="$1"
local service="$2"
# Try health check first
if check_service_health "$feature" "$service" 2>/dev/null; then
return 0
fi
# Try port check
local port
port=$(get_service_port "$feature" "$service" 2>/dev/null)
if [[ -n "$port" ]] && is_port_in_use "$port"; then
return 0
fi
return 1
}
# Get service status with source detection
# Usage: get_service_status "analytics" "api"
# Returns: "running:health_check" | "running:port" | "running:docker" | "not_running"
get_service_status() {
local feature="$1"
local service="$2"
local service_id="${feature}.${service}"
local service_type
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
# Check if we started it (PID file)
local pid_file="${LILITH_PID_DIR}/${service_id//./-}.pid"
if [[ -f "$pid_file" ]]; then
local pid
pid=$(cat "$pid_file" 2>/dev/null)
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
echo "running:pid_file:$pid"
return 0
fi
fi
# Check docker containers for db/redis
if [[ "$service_type" == "postgresql" || "$service_type" == "redis" ]]; then
local container_name="${feature}.*${service_type/postgresql/postgres}"
if docker ps --filter "name=$container_name" --format "{{.Status}}" 2>/dev/null | grep -q "Up"; then
echo "running:docker"
return 0
fi
fi
# Try health check
if check_service_health "$feature" "$service" 2>/dev/null; then
echo "running:health_check"
return 0
fi
# Try port check
local port
port=$(get_service_port "$feature" "$service" 2>/dev/null)
if [[ -n "$port" ]] && is_port_in_use "$port"; then
echo "running:port"
return 0
fi
echo "not_running"
return 1
}
# =============================================================================
# Service Startup Functions
# =============================================================================
# Ensure PID directory exists
_ensure_pid_dir() {
mkdir -p "$LILITH_PID_DIR"
mkdir -p "$LILITH_LOCK_DIR"
}
# Write PID file for a service we started
_write_pid_file() {
local service_id="$1"
local pid="$2"
_ensure_pid_dir
echo "$pid" > "${LILITH_PID_DIR}/${service_id//./-}.pid"
}
# Start a single dependency
# Usage: start_dependency "infrastructure.postgresql" [timeout]
start_dependency() {
local dep="$1"
local timeout="${2:-60}"
local feature="${dep%.*}"
local service="${dep##*.}"
local service_type
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
# Check if already running
local status
status=$(get_service_status "$feature" "$service")
if [[ "$status" =~ ^running ]]; then
return 0 # Already running
fi
case "$feature" in
infrastructure)
_start_infra_service "$service"
;;
*)
case "$service_type" in
postgresql|redis)
_start_feature_db "$feature" "$service_type"
;;
api)
_start_api_service "$feature"
;;
ml)
_start_ml_service "$feature"
;;
frontend)
_start_frontend_service "$feature"
;;
*)
echo "Unknown service type: $service_type for $dep" >&2
return 1
;;
esac
;;
esac
# Wait for health
_wait_for_dep_health "$dep" "$timeout"
}
# Start shared infrastructure service
_start_infra_service() {
local service="$1"
local compose_file="${SERVICES_PROJECT_ROOT}/infrastructure/docker/docker-compose.dev.yml"
if [[ ! -f "$compose_file" ]]; then
echo "Infrastructure compose file not found: $compose_file" >&2
return 1
fi
case "$service" in
postgresql|postgres)
docker-compose -f "$compose_file" up -d postgres
;;
redis)
docker-compose -f "$compose_file" up -d redis
;;
meilisearch)
docker-compose -f "$compose_file" up -d meilisearch
;;
minio)
docker-compose -f "$compose_file" up -d minio
;;
*)
echo "Unknown infrastructure service: $service" >&2
return 1
;;
esac
}
# Start feature-specific database/redis
_start_feature_db() {
local feature="$1"
local db_type="$2"
local compose_file="${SERVICES_DIR}/${feature}/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
echo "No docker-compose.yml for feature: $feature" >&2
return 1
fi
local service_name="${feature}-${db_type/postgresql/postgres}"
docker-compose -f "$compose_file" up -d "$service_name"
}
# Start feature API via pnpm
_start_api_service() {
local feature="$1"
local api_dir="${SERVICES_DIR}/${feature}/backend-api"
local service_id="${feature}.api"
if [[ ! -d "$api_dir" ]]; then
echo "No backend-api directory for feature: $feature" >&2
return 1
fi
_ensure_pid_dir
# Start in background
(
cd "$api_dir"
pnpm start:dev &
local pid=$!
_write_pid_file "$service_id" "$pid"
wait "$pid"
) &
# Give it a moment to write PID
sleep 1
}
# Start ML service via Python
_start_ml_service() {
local feature="$1"
local ml_dir="${SERVICES_DIR}/${feature}/ml-service"
local service_id="${feature}.ml-service"
if [[ ! -d "$ml_dir" ]]; then
echo "No ml-service directory for feature: $feature" >&2
return 1
fi
local port
port=$(get_service_port "$feature" "ml-service" 2>/dev/null)
_ensure_pid_dir
# Start in background
(
cd "$ml_dir"
python -m uvicorn main:app --host 0.0.0.0 --port "${port:-8000}" &
local pid=$!
_write_pid_file "$service_id" "$pid"
wait "$pid"
) &
sleep 1
}
# Start frontend service via pnpm
_start_frontend_service() {
local feature="$1"
local frontend_dir
local service_id="${feature}.frontend"
# Find frontend directory
for subdir in frontend frontend-admin frontend-public frontend-app; do
if [[ -d "${SERVICES_DIR}/${feature}/${subdir}" ]]; then
frontend_dir="${SERVICES_DIR}/${feature}/${subdir}"
break
fi
done
if [[ -z "$frontend_dir" ]]; then
echo "No frontend directory for feature: $feature" >&2
return 1
fi
local port
port=$(get_service_port "$feature" "frontend-dev" 2>/dev/null)
_ensure_pid_dir
# Start in background
(
cd "$frontend_dir"
pnpm dev --port "${port:-5173}" &
local pid=$!
_write_pid_file "$service_id" "$pid"
wait "$pid"
) &
sleep 1
}
# Wait for dependency to be healthy
_wait_for_dep_health() {
local dep="$1"
local timeout="$2"
local elapsed=0
local interval=2
local feature="${dep%.*}"
local service="${dep##*.}"
while [[ $elapsed -lt $timeout ]]; do
if is_service_running "$feature" "$service"; then
return 0
fi
sleep $interval
elapsed=$((elapsed + interval))
done
echo "Timeout waiting for $dep" >&2
return 1
}
# =============================================================================
# Main Orchestration Function
# =============================================================================
# Start all dependencies for a feature
# Usage: start_feature_deps "analytics" [--dry-run] [--timeout=60]
start_feature_deps() {
local feature="$1"
shift
local dry_run=false
local timeout=60
# Parse options
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) dry_run=true ;;
--timeout=*) timeout="${1#*=}" ;;
esac
shift
done
echo "=== Starting dependencies for: $feature ==="
echo ""
# Get all transitive dependencies
local all_deps
all_deps=$(get_feature_transitive_deps "$feature")
if [[ -z "$all_deps" ]]; then
echo "No dependencies found"
return 0
fi
# Topologically sort
local sorted_deps
sorted_deps=$(topo_sort $all_deps)
echo "Startup order:"
local i=1
for dep in $sorted_deps; do
echo " $i. $dep"
i=$((i + 1))
done
echo ""
if $dry_run; then
echo "(dry run - no services started)"
return 0
fi
local started=0
local skipped=0
local failed=0
# Start each dependency in order
for dep in $sorted_deps; do
local dep_feature="${dep%.*}"
local dep_service="${dep##*.}"
# Get current status
local status
status=$(get_service_status "$dep_feature" "$dep_service")
if [[ "$status" =~ ^running ]]; then
local source="${status#running:}"
echo "$dep already running ($source)"
skipped=$((skipped + 1))
continue
fi
echo -n "Starting $dep..."
if start_dependency "$dep" "$timeout"; then
echo " ✓"
started=$((started + 1))
else
echo " ✗ FAILED"
failed=$((failed + 1))
fi
done
echo ""
echo "Summary: $started started, $skipped already running, $failed failed"
[[ $failed -eq 0 ]]
}
# Stop services we started (from PID files)
# Usage: stop_our_services
stop_our_services() {
echo "Stopping services we started..."
local stopped=0
local failed=0
for pid_file in "${LILITH_PID_DIR}"/*.pid 2>/dev/null; do
[[ -f "$pid_file" ]] || continue
local service_id
service_id=$(basename "$pid_file" .pid | tr '-' '.')
local pid
pid=$(cat "$pid_file" 2>/dev/null)
if [[ -n "$pid" ]]; then
if kill -0 "$pid" 2>/dev/null; then
if kill "$pid" 2>/dev/null; then
echo " Stopped $service_id (PID $pid)"
stopped=$((stopped + 1))
else
echo " Failed to stop $service_id (PID $pid)"
failed=$((failed + 1))
fi
fi
fi
rm -f "$pid_file"
done
echo "Stopped $stopped services ($failed failed)"
echo "Note: Docker containers not stopped (may be shared)"
}
# =============================================================================
# CLI Interface
# =============================================================================
_services_cli() {
local cmd="${1:-help}"
shift || true
case "$cmd" in
url)
get_service_url "$@"
;;
port)
get_service_port "$@"
;;
deps)
get_feature_deps "$@"
;;
health)
check_feature_health "$@"
;;
env)
generate_service_env "$@"
;;
services)
if [[ -n "$1" ]]; then
get_feature_services "$@"
else
list_all_services
fi
;;
features)
list_features_with_services
;;
infra)
get_infra_deps "$@"
;;
start-infra)
start_feature_infra "$@"
;;
start-deps)
start_feature_deps "$@"
;;
stop)
stop_our_services
;;
status)
local feature="$1"
local service="$2"
if [[ -n "$feature" && -n "$service" ]]; then
get_service_status "$feature" "$service"
else
echo "Usage: services.sh status <feature> <service>"
fi
;;
transitive-deps)
get_feature_transitive_deps "$@"
;;
help|--help|-h)
cat << 'EOF'
services.sh - Service configuration utilities
Usage:
source services.sh # Load as library
./services.sh <command> [args] # Run as CLI
Commands:
url <feature> [service] Get service URL (default service: api)
port <feature> <service> Get service port
deps <feature> List all dependencies for a feature
transitive-deps <feature> List all transitive dependencies
health <feature> Check health of all feature services
status <feature> <service> Get detailed service status
env <feature> [prefix] Generate export statements for feature
services [feature] List all services (or services for a feature)
features List all features with services.yaml
infra <feature> List infrastructure dependencies
start-infra <feature> Start infrastructure for a feature
start-deps <feature> Start all dependencies for a feature
stop Stop services we started (PID-tracked)
Examples:
./services.sh url analytics api
./services.sh port analytics postgresql
./services.sh deps platform-admin
./services.sh transitive-deps seo
./services.sh health analytics
./services.sh status analytics api
./services.sh start-deps analytics --dry-run
./services.sh start-deps seo --timeout=120
./services.sh stop
In scripts:
source "$(dirname "$0")/lib/services.sh"
API_URL=$(get_service_url "analytics" "api")
start_feature_deps "analytics"
EOF
;;
*)
echo "Unknown command: $cmd" >&2
echo "Run with --help for usage" >&2
return 1
;;
esac
}
# Run CLI if script is executed directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
_services_cli "$@"
fi