PSA/cli/dev-env.nu
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

1239 lines
58 KiB
Plaintext

use "config.nu" *
# Helper function to sanitize branch names for Kubernetes resources
def sanitize-branch-name [branch: string] {
# Sanitize branch name for Kubernetes namespace (lowercase, alphanumeric and hyphens only)
# First replace slashes with hyphens, then clean up any other special characters
let sanitized_base = ($branch | str replace -a "/" "-" | str downcase | str replace -a "[^a-z0-9-]" "-" | str replace -r "^-+|-+$" "" | str replace -r "-+" "-")
# Ensure Helm release name stays under 53 character limit
# Release name format: "alga-dev-{sanitized_branch}" = 9 + sanitized_branch length
let max_branch_length = 43 # 53 - 9 = 44, but we need 43 to be safe
if ($sanitized_base | str length) > $max_branch_length {
# For long names, take first part and add hash of full name for uniqueness
let hash_suffix = ($sanitized_base | hash sha256 | str substring 0..7)
let prefix_length = $max_branch_length - 9 # 9 chars for "-" + 8-char hash
let prefix = ($sanitized_base | str substring 0..$prefix_length)
$"($prefix)-($hash_suffix)"
} else {
$sanitized_base
}
}
# Start development environment with Docker Compose
export def dev-up [
--detached (-d) # Run in detached mode (background)
--edition (-e): string = "ce" # Edition to start: ce (community) or ee (enterprise)
] {
let project_root = find-project-root
# Validate edition parameter
if not ($edition in ["ce", "ee"]) {
error make { msg: $"($env.ALGA_COLOR_RED)Invalid edition '($edition)'. Must be 'ce' (community) or 'ee' (enterprise).($env.ALGA_COLOR_RESET)" }
}
let edition_file = if $edition == "ce" { "docker-compose.prebuilt.ce.yaml" } else { "docker-compose.ee.yaml" }
let base_file = if $edition == "ce" { "docker-compose.prebuilt.base.yaml" } else { "docker-compose.base.yaml" }
let edition_name = if $edition == "ce" { "Community Edition" } else { "Enterprise Edition" }
print $"($env.ALGA_COLOR_CYAN)Starting development environment (($edition_name))...($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Project root: ($project_root)($env.ALGA_COLOR_RESET)"
if $detached {
let command = $"docker compose -f ($base_file) -f ($edition_file) --env-file server/.env up --build -d"
print $"($env.ALGA_COLOR_YELLOW)Running: ($command)($env.ALGA_COLOR_RESET)"
let result = do {
cd $project_root
docker compose -f $base_file -f $edition_file --env-file server/.env up -d | complete
}
if $result.exit_code == 0 {
print $"($env.ALGA_COLOR_GREEN)Development environment (($edition_name)) started in background.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Access the application at: http://localhost:3000($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)View logs with: docker compose logs -f($env.ALGA_COLOR_RESET)"
} else {
print $"($env.ALGA_COLOR_RED)($result.stderr)($env.ALGA_COLOR_RESET)"
error make { msg: $"($env.ALGA_COLOR_RED)Failed to start development environment($env.ALGA_COLOR_RESET)", code: $result.exit_code }
}
} else {
let command = $"docker compose -f ($base_file) -f ($edition_file) --env-file server/.env up"
print $"($env.ALGA_COLOR_YELLOW)Running: ($command)($env.ALGA_COLOR_RESET)"
# Stream output directly without capturing
cd $project_root
docker compose -f $base_file -f $edition_file --env-file server/.env up --build
}
}
# Stop development environment
export def dev-down [] {
let project_root = find-project-root
print $"($env.ALGA_COLOR_CYAN)Stopping development environment...($env.ALGA_COLOR_RESET)"
let result = do {
cd $project_root
docker compose down | complete
}
if $result.exit_code == 0 {
print $result.stdout
print $"($env.ALGA_COLOR_GREEN)Development environment stopped.($env.ALGA_COLOR_RESET)"
} else {
print $"($env.ALGA_COLOR_RED)($result.stderr)($env.ALGA_COLOR_RESET)"
error make { msg: $"($env.ALGA_COLOR_RED)Failed to stop development environment($env.ALGA_COLOR_RESET)", code: $result.exit_code }
}
}
# Create development environment for branch
#
# Environment variables for AI automation (read from .env file or shell environment):
# CUSTOM_OPENAI_API_KEY: Required - API key for LLM provider (e.g., OpenRouter key)
# CUSTOM_OPENAI_BASE_URL: Optional - API endpoint (default: https://openrouter.ai/api/v1)
# CUSTOM_OPENAI_MODEL: Optional - Model name (default: google/gemini-2.5-flash-preview-05-20)
#
export def dev-env-create [
branch: string # Git branch name
--edition: string = "ee" # Edition: ce or ee
--use-latest = false # Use 'latest' tag instead of unique tag
--checkout = true # Checkout the branch locally
--from-tag: string = "latest" # Deploy from existing image tag instead of building
--author-name: string = "" # Git author name (e.g., "John Doe")
--author-email: string = "" # Git author email (e.g., "john@example.com")
] {
let project_root = find-project-root
# Validate edition parameter
if not ($edition in ["ce", "ee"]) {
error make { msg: $"($env.ALGA_COLOR_RED)Invalid edition '($edition)'. Must be 'ce' (community) or 'ee' (enterprise).($env.ALGA_COLOR_RESET)" }
}
# Check for mutually exclusive options
if ($from_tag | str length) > 0 and $use_latest {
error make { msg: $"($env.ALGA_COLOR_RED)Cannot use both --from-tag and --use-latest. Choose one or the other.($env.ALGA_COLOR_RESET)" }
}
# Sanitize branch name for Kubernetes namespace and Helm release name length limits
let sanitized_branch = (sanitize-branch-name $branch)
let namespace = $"alga-dev-($sanitized_branch)"
# Checkout the branch if requested
if $checkout {
print $"($env.ALGA_COLOR_CYAN)Checking out branch: ($branch)($env.ALGA_COLOR_RESET)"
let checkout_result = do {
cd $project_root
git checkout $branch | complete
}
if $checkout_result.exit_code != 0 {
# Try to fetch and checkout if branch doesn't exist locally
print $"($env.ALGA_COLOR_YELLOW)Branch not found locally, fetching from remote...($env.ALGA_COLOR_RESET)"
let fetch_result = do {
cd $project_root
git fetch origin $branch | complete
}
if $fetch_result.exit_code == 0 {
let checkout_retry = do {
cd $project_root
git checkout -b $branch origin/$branch | complete
}
if $checkout_retry.exit_code != 0 {
print $"($env.ALGA_COLOR_YELLOW)Warning: Could not checkout branch ($branch). Continuing with current branch.($env.ALGA_COLOR_RESET)"
}
} else {
print $"($env.ALGA_COLOR_YELLOW)Warning: Branch ($branch) not found in remote. Continuing with current branch.($env.ALGA_COLOR_RESET)"
}
}
}
# Find available ports for external access
print $"($env.ALGA_COLOR_CYAN)Finding available ports for external access...($env.ALGA_COLOR_RESET)"
# Function to find a free port
def find-free-port [start_port: int] {
mut port = $start_port
mut found = false
while not $found and $port < 65535 {
# Copy mutable variable to avoid capture issue
let current_port = $port
# Check if port is in use
let check_result = do {
bash -c $"nc -z localhost ($current_port) 2>/dev/null" | complete
}
if $check_result.exit_code != 0 {
# Port is free
$found = true
} else {
$port = $port + 1
}
}
if $found { $port } else { 0 }
}
# Find ports for each service
let app_port = find-free-port 30000
let code_server_port = find-free-port ($app_port + 1)
let code_app_port = find-free-port ($code_server_port + 1)
let ai_web_port = find-free-port ($code_app_port + 1)
if $app_port == 0 or $code_server_port == 0 or $code_app_port == 0 or $ai_web_port == 0 {
error make { msg: $"($env.ALGA_COLOR_RED)Could not find available ports for services($env.ALGA_COLOR_RESET)" }
}
print $"($env.ALGA_COLOR_GREEN)Assigned external ports:($env.ALGA_COLOR_RESET)"
print $" Main App: ($app_port)"
print $" Code Server: ($code_server_port)"
print $" Code App: ($code_app_port)"
print $" AI Web: ($ai_web_port)"
# Determine image tag and whether to build
let tag_info = if ($from_tag | str length) > 0 {
# User explicitly provided a tag, so don't build
{ tag: $from_tag, build: false, reason: $"using provided tag '($from_tag)'" }
} else if $use_latest {
# User wants to build and tag as 'latest'
{ tag: "latest", build: true, reason: "building and using 'latest' tag" }
} else {
# Default behavior: use existing 'latest' tag without building
{ tag: "latest", build: false, reason: "using default 'latest' tag" }
}
let image_tag = $tag_info.tag
let should_build = $tag_info.build
let reason = $tag_info.reason
# Build image only if determined necessary
if $should_build {
print $"($env.ALGA_COLOR_CYAN)Building image before deployment... ($reason)($env.ALGA_COLOR_RESET)"
# The --use-latest flag in build-image handles tagging with both SHA and 'latest'
if $image_tag == "latest" {
build-image $edition --use-latest --push
} else {
build-image $edition --tag $image_tag --push
}
} else {
print $"($env.ALGA_COLOR_CYAN)Skipping build - ($reason)($env.ALGA_COLOR_RESET)"
}
print $"($env.ALGA_COLOR_CYAN)Creating development environment for branch: ($branch)($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Sanitized name: ($sanitized_branch)($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Edition: ($edition)($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)AI Automation: enabled($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Namespace: ($namespace)($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Image Tag: ($image_tag)($env.ALGA_COLOR_RESET)"
# Check if environment already exists
let existing_check = do {
kubectl get namespace $namespace | complete
}
if $existing_check.exit_code == 0 {
# Check if namespace is in Terminating state
let namespace_status = do {
kubectl get namespace $namespace -o jsonpath='{.status.phase}' | complete
}
if $namespace_status.exit_code == 0 and ($namespace_status.stdout | str trim) == "Terminating" {
print $"($env.ALGA_COLOR_YELLOW)Warning: Namespace ($namespace) is stuck in Terminating state.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Attempting to force cleanup...($env.ALGA_COLOR_RESET)"
# Force cleanup the stuck namespace
let force_cleanup = do {
kubectl delete namespace $namespace --grace-period=0 --force | complete
}
if $force_cleanup.exit_code == 0 {
print $"($env.ALGA_COLOR_GREEN)Stuck namespace cleaned up. Proceeding with creation...($env.ALGA_COLOR_RESET)"
sleep 5sec # Give it a moment to fully clear
} else {
print $"($env.ALGA_COLOR_RED)Failed to cleanup stuck namespace. Manual intervention required.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)Try running: kubectl delete namespace ($namespace) --grace-period=0 --force($env.ALGA_COLOR_RESET)"
return
}
} else {
print $"($env.ALGA_COLOR_YELLOW)Warning: Environment for branch ($branch) already exists in namespace ($namespace)($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)Use 'dev-env-destroy ($branch)' to remove it first if you want to recreate it.($env.ALGA_COLOR_RESET)"
return
}
}
# Create temporary values file (replace slashes with dashes in filename)
let safe_filename = ($branch | str replace -a "/" "-")
let temp_values_file = $"($project_root)/temp-values-($safe_filename).yaml"
let edition_comment = if $edition == "ee" { "# Enterprise Edition settings" } else { "# Community Edition settings" }
let image_name = $"harbor.nineminds.com/nineminds/alga-psa-($edition)"
# Load environment variables from user's home .env file if it exists
let home_env_file = ($nu.home-path | path join ".env")
if ($home_env_file | path exists) {
print $"($env.ALGA_COLOR_CYAN)Loading environment variables from ($home_env_file)...($env.ALGA_COLOR_RESET)"
let env_vars = (open $home_env_file
| lines
| each { |line| $line | str trim }
| where { |line| not ($line | str starts-with '#') and ($line | str length) > 0 and ($line | str contains '=') }
| split column "=" -n 2
| rename key value
| update key {|it| $it.key | str trim }
| update value {|it| if ($it.value | is-empty) { "" } else { $it.value | str trim | str trim -c '"' | str trim -c "'" } }
| reduce -f {} { |item, acc| $acc | upsert $item.key $item.value })
# Load all variables from .env, overwriting existing ones from the environment
load-env $env_vars
}
# Get LLM configuration from environment variables
let custom_openai_api_key = ($env.CUSTOM_OPENAI_API_KEY? | default "")
let custom_openai_base_url = ($env.CUSTOM_OPENAI_BASE_URL? | default "https://openrouter.ai/api/v1")
let custom_openai_model = ($env.CUSTOM_OPENAI_MODEL? | default "google/gemini-2.5-flash-preview-05-20")
# Show warning if API key is not set
if ($custom_openai_api_key | str length) == 0 {
print $"($env.ALGA_COLOR_YELLOW)Warning: CUSTOM_OPENAI_API_KEY environment variable not set. AI automation may not work.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)Set the environment variable in your .env file or export CUSTOM_OPENAI_API_KEY=your-key-here($env.ALGA_COLOR_RESET)"
}
# Load configuration defaults
let config = load-config
# Use provided values, fall back to config, then to defaults
let git_author_name = if ($author_name | str length) > 0 {
$author_name
} else if ($config.dev_env.author.name | str length) > 0 {
$config.dev_env.author.name
} else {
"Dev Environment"
}
let git_author_email = if ($author_email | str length) > 0 {
$author_email
} else if ($config.dev_env.author.email | str length) > 0 {
$config.dev_env.author.email
} else {
"dev@alga.local"
}
# Show what author info is being used
if ($config.dev_env.author.name | str length) > 0 and ($author_name | str length) == 0 {
print $"($env.ALGA_COLOR_CYAN)Using git author from config: ($git_author_name) <($git_author_email)>($env.ALGA_COLOR_RESET)"
}
let values_content = $"
# Generated values for branch ($branch) development environment
devEnv:
enabled: true
branch: \"($branch)\"
sanitizedBranch: \"($sanitized_branch)\"
namespace: \"($namespace)\"
repository:
url: \"https://github.com/Nine-Minds/alga-psa.git\"
branch: \"($branch)\"
git:
authorName: \"($git_author_name)\"
authorEmail: \"($git_author_email)\"
codeServer:
enabled: true
aiAutomation:
enabled: true
# External port configuration
externalPorts:
app: ($app_port)
codeServer: ($code_server_port)
codeApp: ($code_app_port)
aiWeb: ($ai_web_port)
server:
image:
name: \"($image_name)\"
tag: \"($image_tag)\"
pullPolicy: Always # Force pull to avoid cache issues
# LLM Configuration
config:
llm:
customOpenaiApiKey: \"($custom_openai_api_key)\"
customOpenaiBaseUrl: \"($custom_openai_base_url)\"
customOpenaiModel: \"($custom_openai_model)\"
($edition_comment)"
# Write temporary values file
$values_content | save -f $temp_values_file
try {
# Deploy using Helm (namespace will be created by template)
print $"($env.ALGA_COLOR_CYAN)Deploying Helm chart...($env.ALGA_COLOR_RESET)"
let helm_result = do {
cd $project_root
helm upgrade --install $"alga-dev-($sanitized_branch)" ./helm -f helm/values-dev-env.yaml -f $temp_values_file -n $namespace --create-namespace | complete
}
# Check for actual deployment failures (ignore benign warnings/messages)
let has_real_error = if $helm_result.exit_code != 0 {
# Check if stderr contains actual errors vs just warnings
let stderr_content = ($helm_result.stderr | str downcase)
let is_namespace_exists_only = ($stderr_content | str contains 'already exists')
let has_warnings = ($stderr_content | str contains 'warning:')
let has_errors = ($stderr_content | str contains 'error:')
let is_warnings_only = ($has_warnings and not $has_errors)
# Only treat as real error if it's not just namespace exists or warnings
not ($is_namespace_exists_only or $is_warnings_only)
} else { false }
if $has_real_error {
print $"($env.ALGA_COLOR_RED)Helm deployment failed:($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_RED)($helm_result.stderr)($env.ALGA_COLOR_RESET)"
let error_msg = $"Failed to deploy development environment: ($helm_result.stderr)"
error make { msg: $"($env.ALGA_COLOR_RED)($error_msg)($env.ALGA_COLOR_RESET)", code: $helm_result.exit_code }
} else if $helm_result.exit_code != 0 {
# Helm deployment had issues but resources are deployed - try upgrade to trigger hooks
print $"($env.ALGA_COLOR_YELLOW)Initial deployment had issues, attempting upgrade to ensure hooks run...($env.ALGA_COLOR_RESET)"
let upgrade_result = do {
cd $project_root
helm upgrade $"alga-dev-($sanitized_branch)" ./helm -f helm/values-dev-env.yaml -f $temp_values_file -n $namespace | complete
}
if $upgrade_result.exit_code == 0 {
print $"($env.ALGA_COLOR_GREEN)Upgrade successful - hooks should have run for database initialization.($env.ALGA_COLOR_RESET)"
} else {
print $"($env.ALGA_COLOR_YELLOW)Warning: Upgrade also had issues. Database may not be initialized.($env.ALGA_COLOR_RESET)"
}
}
# Show warnings but don't treat as errors
if $helm_result.exit_code != 0 and not $has_real_error {
print $"($env.ALGA_COLOR_YELLOW)Helm completed with warnings - ignoring:($env.ALGA_COLOR_RESET)"
# Filter out the file permission warnings which are just noise
let filtered_stderr = ($helm_result.stderr | lines | where { |line|
(not ($line | str contains 'Kubernetes configuration file is'))
} | where { |line|
(not ($line | str contains 'deprecated since'))
} | where { |line|
(($line | str trim | str length) > 0)
})
if ($filtered_stderr | length) > 0 {
$filtered_stderr | each { |line| print $" ($line)" }
}
}
print $helm_result.stdout
print $"($env.ALGA_COLOR_GREEN)Helm deployment completed successfully.($env.ALGA_COLOR_RESET)"
# Wait for deployments to be ready
print $"($env.ALGA_COLOR_CYAN)Waiting for deployments to be ready...($env.ALGA_COLOR_RESET)"
let wait_result = do {
kubectl wait --for=condition=available --timeout=300s deployment -l app.kubernetes.io/instance=$"alga-dev-($sanitized_branch)" -n $namespace | complete
}
if $wait_result.exit_code == 0 {
print $"($env.ALGA_COLOR_GREEN)All deployments are ready!($env.ALGA_COLOR_RESET)"
# Show environment status
dev-env-status $branch
} else {
print $"($env.ALGA_COLOR_YELLOW)Warning: Some deployments may still be starting. Use 'dev-env-status ($branch)' to check progress.($env.ALGA_COLOR_RESET)"
}
} catch { |err|
print $"($env.ALGA_COLOR_RED)Error during deployment: ($err)($env.ALGA_COLOR_RESET)"
}
# Clean up temporary files
if ($temp_values_file | path exists) {
rm $temp_values_file
}
}
# List active development environments
export def dev-env-list [] {
print $"($env.ALGA_COLOR_CYAN)Active development environments:($env.ALGA_COLOR_RESET)"
let namespaces_result = do {
kubectl get namespaces -l type=dev-environment -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.branch}{"\n"}{end}' | complete
}
if $namespaces_result.exit_code != 0 {
print $"($env.ALGA_COLOR_RED)Failed to list environments: ($namespaces_result.stderr)($env.ALGA_COLOR_RESET)"
return
}
let environments = ($namespaces_result.stdout | lines | where ($it | str trim | str length) > 0)
if ($environments | length) == 0 {
print $"($env.ALGA_COLOR_YELLOW)No active development environments found.($env.ALGA_COLOR_RESET)"
return
}
print "┌──────────────────────────────────────────────────────────────────────────┐"
print "│ Namespace │ Branch │ Status │"
print "├──────────────────────────────────────────────────────────────────────────┤"
for env_line in $environments {
let parts = ($env_line | split column "\t")
let namespace = ($parts | get column1 | get 0)
let branch = ($parts | get column2? | get 0? | default "Unknown")
let status_result = do {
kubectl get deployments -n $namespace -o jsonpath='{range .items[*]}{.status.readyReplicas}{" "}{.status.replicas}{"\n"}{end}' | complete
}
let status = if $status_result.exit_code == 0 {
let ready_total = ($status_result.stdout | lines | each { |line|
if ($line | str trim | str length) > 0 {
let parts = ($line | split row " ")
let ready = ($parts | get 0? | default "0" | if ($in == "") { "0" } else { $in } | into int)
let total = ($parts | get 1? | default "0" | if ($in == "") { "0" } else { $in } | into int)
{ ready: $ready, total: $total }
}
} | compact)
let total_ready = if ($ready_total | is-empty) { 0 } else { ($ready_total | get ready | math sum) }
let total_deployments = if ($ready_total | is-empty) { 0 } else { ($ready_total | get total | math sum) }
if $total_ready == $total_deployments {
"Ready"
} else {
$"($total_ready)/($total_deployments) Ready"
}
} else {
"Error"
}
print $"│ ($namespace | fill -w 24) │ ($branch | fill -w 20) │ ($status | fill -w 22) │"
}
print "└──────────────────────────────────────────────────────────────────────────┘"
}
# Connect to development environment
export def dev-env-connect [
branch: string # Branch name to connect to
] {
# Sanitize branch name for namespace lookup
let sanitized_branch = (sanitize-branch-name $branch)
let namespace = $"alga-dev-($sanitized_branch)"
# Check if environment exists
let env_check = do {
kubectl get namespace $namespace | complete
}
if $env_check.exit_code != 0 {
print $"($env.ALGA_COLOR_RED)Environment for branch ($branch) not found.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)Use 'dev-env-list' to see available environments.($env.ALGA_COLOR_RESET)"
return
}
print $"($env.ALGA_COLOR_CYAN)Connecting to development environment for branch: ($branch)($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Setting up port forwarding...($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)This will run in foreground. Press Ctrl+C to stop.($env.ALGA_COLOR_RESET)"
# Find available ports dynamically at connect time
print $"($env.ALGA_COLOR_CYAN)Finding available ports for port forwarding...($env.ALGA_COLOR_RESET)"
# Function to find a free port (same as in dev-env-create)
def find-free-port [start_port: int] {
mut port = $start_port
mut found = false
while not $found and $port < 65535 {
# Copy mutable variable to avoid capture issue
let current_port = $port
# Check if port is in use
let check_result = do {
bash -c $"nc -z localhost ($current_port) 2>/dev/null" | complete
}
if $check_result.exit_code != 0 {
# Port is free
$found = true
} else {
$port = $port + 1
}
}
if $found { $port } else { 0 }
}
# Find ports for each service
let app_port = find-free-port 30000
let code_server_port = find-free-port ($app_port + 1)
let code_app_port = find-free-port ($code_server_port + 1)
let ai_web_port = find-free-port ($code_app_port + 1)
if $app_port == 0 or $code_server_port == 0 or $code_app_port == 0 or $ai_web_port == 0 {
print $"($env.ALGA_COLOR_RED)Could not find available ports for services($env.ALGA_COLOR_RESET)"
return
}
print $"($env.ALGA_COLOR_GREEN)Found available ports:($env.ALGA_COLOR_RESET)"
print $" Main App: ($app_port)"
print $" Code Server: ($code_server_port)"
print $" Code App: ($code_app_port)"
print $" AI Web: ($ai_web_port)"
# Start port forwarding processes with found ports
print $"($env.ALGA_COLOR_CYAN)Starting port forwarding processes...($env.ALGA_COLOR_RESET)"
# Start processes using bash for proper backgrounding with specific ports
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch)-code-server --address=127.0.0.1 ($code_server_port):8080 > /tmp/pf-code-server-($sanitized_branch).log 2>&1 &"
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch) --address=127.0.0.1 ($app_port):3000 > /tmp/pf-main-app-($sanitized_branch).log 2>&1 &"
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch)-code-server --address=127.0.0.1 ($code_app_port):3000 > /tmp/pf-code-app-($sanitized_branch).log 2>&1 &"
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch)-ai-nginx --address=127.0.0.1 ($ai_web_port):8080 > /tmp/pf-ai-web-($sanitized_branch).log 2>&1 &"
# Give processes time to start
sleep 2sec
# Check if port forwarding started successfully
let pf_check = do {
bash -c $"ps aux | grep -E 'kubectl port-forward.*alga-dev-($sanitized_branch)' | grep -v grep | wc -l" | complete
}
if ($pf_check.stdout | str trim | into int) < 4 {
print $"($env.ALGA_COLOR_YELLOW)Warning: Some port forwarding processes may not have started properly($env.ALGA_COLOR_RESET)"
print "Checking logs..."
# Show any errors from log files
for log_file in [
$"/tmp/pf-code-server-($sanitized_branch).log"
$"/tmp/pf-main-app-($sanitized_branch).log"
$"/tmp/pf-code-app-($sanitized_branch).log"
$"/tmp/pf-ai-web-($sanitized_branch).log"
] {
if ($log_file | path exists) {
let content = (open $log_file)
if ($content | str contains "error") {
print $"($env.ALGA_COLOR_RED)Errors in ($log_file):($env.ALGA_COLOR_RESET)"
print $content
}
}
}
}
# Display the URLs
print $"($env.ALGA_COLOR_CYAN)Port forwarding setup:($env.ALGA_COLOR_RESET)"
print $" Code Server: http://localhost:($code_server_port)"
print $" Password: alga-dev"
print $" PSA App \(main\): http://localhost:($app_port)"
print $" PSA App \(in code\): http://localhost:($code_app_port)"
print $" AI Web: http://localhost:($ai_web_port)"
# Update NEXTAUTH_URL in .env files for main app pods only (code server uses hardcoded internal URL)
print $"($env.ALGA_COLOR_CYAN)Configuring NEXTAUTH_URL in main app .env files...($env.ALGA_COLOR_RESET)"
# Helper function to update .env file
def update-env-file [pod_name: string, nextauth_url: string, description: string] {
print $" Updating ($description): ($pod_name)"
# Check if NEXTAUTH_URL already exists in .env file
let env_check = do {
kubectl exec -n $namespace $pod_name -- grep -E "^NEXTAUTH_URL=" .env 2>/dev/null | complete
}
if $env_check.exit_code == 0 and ($env_check.stdout | str trim | str length) > 0 {
# NEXTAUTH_URL exists, check if it's different
let current_url = ($env_check.stdout | str trim | split column "=" | get column2.0)
if $current_url != $nextauth_url {
print $" ($env.ALGA_COLOR_YELLOW)Warning: .env already contains NEXTAUTH_URL=($current_url)($env.ALGA_COLOR_RESET)"
print $" ($env.ALGA_COLOR_YELLOW)This may indicate another developer is using this environment.($env.ALGA_COLOR_RESET)"
print $" ($env.ALGA_COLOR_YELLOW)Updating to your port configuration: ($nextauth_url)($env.ALGA_COLOR_RESET)"
# Update existing entry
kubectl exec -n $namespace $pod_name -- sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=($nextauth_url)|" .env | complete
} else {
print $" ($env.ALGA_COLOR_GREEN)NEXTAUTH_URL already correctly set to ($nextauth_url)($env.ALGA_COLOR_RESET)"
}
} else {
# NEXTAUTH_URL doesn't exist, add it
print $" ($env.ALGA_COLOR_CYAN)Adding NEXTAUTH_URL=($nextauth_url) to .env file($env.ALGA_COLOR_RESET)"
kubectl exec -n $namespace $pod_name -- sh -c $"echo 'NEXTAUTH_URL=($nextauth_url)' >> .env" | complete
}
}
# Get main app pod names and update their .env files (skip code server - it uses hardcoded internal URL)
let main_app_pods = do {
kubectl get pods -n $namespace -l "app.kubernetes.io/component!=code-server,app.kubernetes.io/component!=ai-automation-api,app.kubernetes.io/component!=ai-automation-web" -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
}
# Update main app pods only
if $main_app_pods.exit_code == 0 and not ($main_app_pods.stdout | is-empty) {
let main_pods = ($main_app_pods.stdout | str trim | split row ' ')
for pod in $main_pods {
if ($pod | str trim | str length) > 0 {
update-env-file $pod $"http://localhost:($app_port)" "main app"
}
}
}
print $"($env.ALGA_COLOR_GREEN)NEXTAUTH_URL configuration completed.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_CYAN)Note: Code server uses hardcoded internal URL \(http://code-server:3000\) for browser testing within the environment.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_GREEN)Port forwarding active!($env.ALGA_COLOR_RESET)"
# Wait for user to stop
input "Press Enter to stop port forwarding..."
# Kill all kubectl port-forward processes
bash -c $"pkill -f 'kubectl port-forward.*alga-dev-($sanitized_branch)'"
# Clean up log files
rm -f $"/tmp/pf-code-server-($sanitized_branch).log"
rm -f $"/tmp/pf-main-app-($sanitized_branch).log"
rm -f $"/tmp/pf-code-app-($sanitized_branch).log"
rm -f $"/tmp/pf-ai-web-($sanitized_branch).log"
print $"($env.ALGA_COLOR_CYAN)Port forwarding stopped.($env.ALGA_COLOR_RESET)"
}
# Destroy development environment
export def dev-env-destroy [
branch: string # Branch name to destroy
--force # Force deletion without confirmation
] {
# Sanitize branch name for namespace lookup
let sanitized_branch = (sanitize-branch-name $branch)
let namespace = $"alga-dev-($sanitized_branch)"
# Helper to get all ai-api pods in the namespace
def get-ai-pods [] {
let pods_by_label = do {
kubectl get pods -n $namespace -l 'app.kubernetes.io/component=ai-automation-api' -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
}
let pods_by_name = do {
kubectl get pods -n $namespace -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
}
let pods1 = if $pods_by_label.exit_code == 0 and not ($pods_by_label.stdout | is-empty) {
$pods_by_label.stdout | str trim | split row ' '
} else {
[]
}
let pods2 = if $pods_by_name.exit_code == 0 and not ($pods_by_name.stdout | is-empty) {
$pods_by_name.stdout | str trim | split row ' ' | where {|it| $it | str contains "ai-api"}
} else {
[]
}
$pods1 | append $pods2 | uniq
}
# Check if environment exists
let env_check = do {
kubectl get namespace $namespace | complete
}
if $env_check.exit_code != 0 {
print $"($env.ALGA_COLOR_YELLOW)Environment for branch ($branch) not found or already destroyed.($env.ALGA_COLOR_RESET)"
return
}
if not $force {
print $"($env.ALGA_COLOR_YELLOW)This will permanently destroy the development environment for branch ($branch).($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)All data in the environment will be lost.($env.ALGA_COLOR_RESET)"
let confirmation = (input $"Type 'yes' to confirm destruction: ")
if $confirmation != "yes" {
print $"($env.ALGA_COLOR_CYAN)Destruction cancelled.($env.ALGA_COLOR_RESET)"
return
}
}
print $"($env.ALGA_COLOR_CYAN)Destroying development environment for branch: ($branch)...($env.ALGA_COLOR_RESET)"
# Step 1: Kill any stuck hook jobs first
print $"($env.ALGA_COLOR_CYAN)1. Cleaning up stuck hook jobs...($env.ALGA_COLOR_RESET)"
let stuck_jobs = do {
kubectl get jobs -n $namespace -o jsonpath='{.items[*].metadata.name}' | complete
}
if $stuck_jobs.exit_code == 0 and ($stuck_jobs.stdout | str trim | str length) > 0 {
let job_names = ($stuck_jobs.stdout | str trim | split row ' ')
for job in $job_names {
if ($job | str trim | str length) > 0 {
print $" Deleting job: ($job)"
kubectl delete job $job -n $namespace --timeout=10s --force --grace-period=0 | complete
}
}
} else {
print $" No stuck jobs found"
}
# Step 2: Force stop ai-api pods specifically (known to cause stuck namespaces)
# Step 2: Scale down and remove ai-api resources to prevent them from getting stuck
print $"($env.ALGA_COLOR_CYAN)2. Scaling down ai-api deployment...($env.ALGA_COLOR_RESET)"
let ai_api_deployments = do {
kubectl get deployment -n $namespace -l 'app.kubernetes.io/component=ai-automation-api' -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
}
if $ai_api_deployments.exit_code == 0 and not ($ai_api_deployments.stdout | is-empty) {
let deployment_names = ($ai_api_deployments.stdout | str trim | split row ' ')
for deployment in $deployment_names {
if ($deployment | str trim | str length) > 0 {
print $" Scaling down deployment: ($deployment)"
kubectl scale deployment $deployment --replicas=0 -n $namespace --timeout=30s | complete
}
}
} else {
print $" No ai-api deployment found to scale down."
}
# Now, wait for the pods to terminate
print $"($env.ALGA_COLOR_CYAN)Waiting for ai-api pods to terminate...($env.ALGA_COLOR_RESET)"
mut wait_retries = 0
while $wait_retries < 30 { # Wait for up to 60 seconds
let remaining_pods = (get-ai-pods)
if ($remaining_pods | is-empty) {
print $"\n($env.ALGA_COLOR_GREEN)All targeted ai-api pods have been terminated.($env.ALGA_COLOR_RESET)"
break
} else {
let remaining_str = ($remaining_pods | str join ", ")
print -n $"\r Waiting... remaining: ($remaining_str)"
sleep 2sec
$wait_retries = $wait_retries + 1
}
}
if $wait_retries >= 30 {
print $"\n($env.ALGA_COLOR_YELLOW)Warning: Pods did not terminate gracefully. Forcing deletion...($env.ALGA_COLOR_RESET)"
let remaining_pods = (get-ai-pods)
if not ($remaining_pods | is-empty) {
for pod in $remaining_pods {
if ($pod | str trim | str length) > 0 {
print $" Force deleting pod: ($pod)"
kubectl delete pod $pod -n $namespace --force --grace-period=0 | complete
}
}
sleep 5sec # Give it a moment after force deletion
}
}
# Step 3: Check for other stuck resources and handle them
print $"($env.ALGA_COLOR_CYAN)3. Checking for other stuck resources...($env.ALGA_COLOR_RESET)"
# Check for stuck pods
let stuck_pods = do {
kubectl get pods -n $namespace --field-selector=status.phase!=Running,status.phase!=Succeeded | complete
}
let has_terminating = ($stuck_pods.stdout | str contains 'Terminating')
let has_pending = ($stuck_pods.stdout | str contains 'Pending')
if $stuck_pods.exit_code == 0 and ($has_terminating or $has_pending) {
print $" Found stuck pods, force deleting..."
kubectl delete pods --all -n $namespace --force --grace-period=0 | complete
}
# Step 4: Remove PV finalizers if stuck
print $"($env.ALGA_COLOR_CYAN)4. Checking for stuck persistent volumes...($env.ALGA_COLOR_RESET)"
let stuck_pvs = do {
kubectl get pv | grep $namespace | awk '{print $1}' | complete
}
if $stuck_pvs.exit_code == 0 and ($stuck_pvs.stdout | str trim | str length) > 0 {
let pv_names = ($stuck_pvs.stdout | lines | where { |line| ($line | str trim | str length) > 0 })
for pv_name in $pv_names {
print $" Checking PV finalizers: ($pv_name)"
# Try to patch finalizers to empty array to unstick the PV
kubectl patch pv ($pv_name | str trim) -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
}
} else {
print $" No stuck persistent volumes found"
}
# Step 5: Find and remove Helm release - check where it actually exists
print $"($env.ALGA_COLOR_CYAN)5. Locating and removing Helm release...($env.ALGA_COLOR_RESET)"
let release_name = $"alga-dev-($sanitized_branch)"
# Check if release exists in the environment namespace
let helm_check_ns = do {
helm status $release_name -n $namespace | complete
}
# Check if release exists in default namespace
let helm_check_default = do {
helm status $release_name -n default | complete
}
if $helm_check_ns.exit_code == 0 {
print $" Found release in ($namespace), removing..."
let helm_result = do {
helm uninstall $release_name -n $namespace --timeout=60s --no-hooks --cascade=background | complete
}
if $helm_result.exit_code == 0 {
print $" ($env.ALGA_COLOR_GREEN)Helm release removed successfully from ($namespace).($env.ALGA_COLOR_RESET)"
} else {
print $" ($env.ALGA_COLOR_YELLOW)Warning: Helm uninstall had issues, trying force cleanup...($env.ALGA_COLOR_RESET)"
# Force delete the release by removing finalizers
helm uninstall $release_name -n $namespace --timeout=30s --no-hooks | complete
}
} else if $helm_check_default.exit_code == 0 {
print $" Found release in default namespace, removing..."
let helm_result = do {
helm uninstall $release_name -n default --timeout=60s --no-hooks --cascade=background | complete
}
if $helm_result.exit_code == 0 {
print $" ($env.ALGA_COLOR_GREEN)Helm release removed successfully from default namespace.($env.ALGA_COLOR_RESET)"
} else {
print $" ($env.ALGA_COLOR_YELLOW)Warning: Helm uninstall had issues, trying force cleanup...($env.ALGA_COLOR_RESET)"
# Force delete the release by removing finalizers
helm uninstall $release_name -n default --timeout=30s --no-hooks | complete
}
} else {
print $" No Helm release found for ($release_name) in either namespace."
}
# Step 6: Clean up remaining resources systematically
print $"($env.ALGA_COLOR_CYAN)6. Cleaning up remaining resources...($env.ALGA_COLOR_RESET)"
# Delete all resources in the namespace first
print $" Deleting all workload resources..."
kubectl delete all --all -n $namespace --timeout=30s | complete
print $" Deleting persistent volume claims..."
kubectl delete pvc --all -n $namespace --timeout=30s | complete
print $" Deleting config and secrets..."
kubectl delete configmaps,secrets --all -n $namespace --timeout=30s | complete
print $" Deleting ingress resources..."
kubectl delete ingress --all -n $namespace --timeout=30s | complete
# Step 7: Force cleanup any remaining persistent volumes
print $"($env.ALGA_COLOR_CYAN)7. Force cleaning up persistent volumes...($env.ALGA_COLOR_RESET)"
let remaining_pv_list = do {
kubectl get pv | grep $namespace | awk '{print $1}' | complete
}
if $remaining_pv_list.exit_code == 0 and ($remaining_pv_list.stdout | str trim | str length) > 0 {
let pv_names = ($remaining_pv_list.stdout | lines | where { |line| ($line | str trim | str length) > 0 })
for pv_name in $pv_names {
print $" Force deleting PV: ($pv_name)"
# Remove finalizers first, then delete
kubectl patch pv ($pv_name | str trim) -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
kubectl delete pv ($pv_name | str trim) --timeout=10s --force --grace-period=0 | complete
}
} else {
print $" No remaining persistent volumes found"
}
# Step 8: Delete the namespace
print $"($env.ALGA_COLOR_CYAN)8. Deleting namespace...($env.ALGA_COLOR_RESET)"
# First try to patch out any finalizers on the namespace itself
print $" Removing namespace finalizers..."
kubectl patch namespace $namespace -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
# Short timeout for initial deletion attempt
let namespace_result = do {
kubectl delete namespace $namespace --timeout=30s | complete
}
if $namespace_result.exit_code == 0 {
print $"($env.ALGA_COLOR_GREEN)Development environment for branch ($branch) destroyed successfully.($env.ALGA_COLOR_RESET)"
} else {
print $" ($env.ALGA_COLOR_YELLOW)Warning: Standard namespace deletion had issues. Attempting force cleanup...($env.ALGA_COLOR_RESET)"
# Wait a moment for any pending deletions to complete
sleep 2sec
# Check if namespace still exists
let ns_check = do {
kubectl get namespace $namespace | complete
}
if $ns_check.exit_code != 0 {
print $" ($env.ALGA_COLOR_GREEN)Namespace was deleted during wait period.($env.ALGA_COLOR_RESET)"
} else {
print $" Namespace still exists, forcing deletion..."
# Final attempt to delete namespace with grace period 0 and shorter timeout
let force_namespace_result = do {
kubectl delete namespace $namespace --grace-period=0 --force --timeout=20s | complete
}
if $force_namespace_result.exit_code == 0 {
print $" ($env.ALGA_COLOR_GREEN)Namespace force deleted successfully.($env.ALGA_COLOR_RESET)"
} else {
print $" ($env.ALGA_COLOR_YELLOW)Force delete timed out or failed. Checking if deletion is in progress...($env.ALGA_COLOR_RESET)"
# Check final status
let final_ns_check = do {
kubectl get namespace $namespace | complete
}
if $final_ns_check.exit_code != 0 {
print $" ($env.ALGA_COLOR_GREEN)Namespace deletion completed.($env.ALGA_COLOR_RESET)"
} else {
print $" ($env.ALGA_COLOR_RED)Namespace still exists. May require manual cleanup.($env.ALGA_COLOR_RESET)"
print $" ($env.ALGA_COLOR_YELLOW)The namespace may be stuck due to remaining finalizers.($env.ALGA_COLOR_RESET)"
print $" ($env.ALGA_COLOR_YELLOW)Try: kubectl patch namespace ($namespace) -p '{\\\"metadata\\\":{\\\"finalizers\\\":null}}' --type=merge($env.ALGA_COLOR_RESET)"
print $" ($env.ALGA_COLOR_YELLOW)Then: kubectl delete namespace ($namespace) --grace-period=0 --force($env.ALGA_COLOR_RESET)"
}
}
}
}
# Step 9: Final verification and cleanup
print $"($env.ALGA_COLOR_CYAN)9. Final verification...($env.ALGA_COLOR_RESET)"
let final_check = do {
kubectl get namespace $namespace | complete
}
if $final_check.exit_code == 0 {
print $" ($env.ALGA_COLOR_YELLOW)Namespace still exists, attempting final cleanup...($env.ALGA_COLOR_RESET)"
# Last resort - try to remove any stuck finalizers on the namespace itself
kubectl patch namespace $namespace -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
kubectl delete namespace $namespace --grace-period=0 --force | complete
} else {
print $" ($env.ALGA_COLOR_GREEN)Namespace successfully removed.($env.ALGA_COLOR_RESET)"
}
}
# Force cleanup stuck development environment resources
export def dev-env-force-cleanup [
branch: string # Branch name to force cleanup
] {
# Sanitize branch name for namespace lookup
let sanitized_branch = (sanitize-branch-name $branch)
let namespace = $"alga-dev-($sanitized_branch)"
# Helper to get all ai-api pods in the namespace
def get-ai-pods [] {
let pods_by_label = do {
kubectl get pods -n $namespace -l 'app.kubernetes.io/component=ai-automation-api' -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
}
let pods_by_name = do {
kubectl get pods -n $namespace -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
}
let pods1 = if $pods_by_label.exit_code == 0 and not ($pods_by_label.stdout | is-empty) {
$pods_by_label.stdout | str trim | split row ' '
} else {
[]
}
let pods2 = if $pods_by_name.exit_code == 0 and not ($pods_by_name.stdout | is-empty) {
$pods_by_name.stdout | str trim | split row ' ' | where {|it| $it | str contains "ai-api"}
} else {
[]
}
$pods1 | append $pods2 | uniq
}
print $"($env.ALGA_COLOR_CYAN)Force cleaning up development environment for branch: ($branch)...($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)This will aggressively remove all resources and may take some time.($env.ALGA_COLOR_RESET)"
# Remove Helm release from both potential namespaces
print $"($env.ALGA_COLOR_CYAN)Removing Helm releases...($env.ALGA_COLOR_RESET)"
let release_name = $"alga-dev-($sanitized_branch)"
# Check and remove from environment namespace
let helm_check_ns = do { helm status $release_name -n $namespace | complete }
if $helm_check_ns.exit_code == 0 {
print $"($env.ALGA_COLOR_CYAN)Removing release from ($namespace)...($env.ALGA_COLOR_RESET)"
helm uninstall $release_name -n $namespace | complete
}
# Check and remove from default namespace
let helm_check_default = do { helm status $release_name -n default | complete }
if $helm_check_default.exit_code == 0 {
print $"($env.ALGA_COLOR_CYAN)Removing release from default namespace...($env.ALGA_COLOR_RESET)"
helm uninstall $release_name -n default | complete
}
# Force stop ai-api pods first (known to cause stuck namespaces)
print $"($env.ALGA_COLOR_CYAN)Force stopping ai-api pods...($env.ALGA_COLOR_RESET)"
let pod_names_to_delete = (get-ai-pods)
if ($pod_names_to_delete | is-empty) {
print $" No ai-api pods found."
} else {
print $" Found pods to delete: ($pod_names_to_delete | str join ', ')"
for pod in $pod_names_to_delete {
if ($pod | str trim | str length) > 0 {
print $" Force deleting ai-api pod: ($pod)"
kubectl delete pod $pod -n $namespace --force --grace-period=0 | complete
}
}
# Wait for ai-api pods to be fully terminated
print $"($env.ALGA_COLOR_CYAN)Waiting for ai-api pods to terminate...($env.ALGA_COLOR_RESET)"
mut wait_retries = 0
while $wait_retries < 15 { # Wait for up to 30 seconds
let remaining_pods = (get-ai-pods)
if ($remaining_pods | is-empty) {
print $"\n($env.ALGA_COLOR_GREEN)All targeted ai-api pods have been terminated.($env.ALGA_COLOR_RESET)"
break
} else {
let remaining_text = ($remaining_pods | str join ', ')
print -n $"\r Waiting... remaining: ($remaining_text)"
sleep 2sec
$wait_retries = $wait_retries + 1
}
}
if $wait_retries >= 15 {
print $"\n($env.ALGA_COLOR_YELLOW)Warning: Some ai-api pods may not have terminated correctly. Continuing...($env.ALGA_COLOR_RESET)"
}
}
# Delete all resources in the namespace
print $"($env.ALGA_COLOR_CYAN)Deleting all namespace resources...($env.ALGA_COLOR_RESET)"
kubectl delete all --all -n $namespace --timeout=30s | complete
kubectl delete pvc --all -n $namespace --timeout=30s | complete
kubectl delete configmaps,secrets --all -n $namespace --timeout=30s | complete
kubectl delete ingress --all -n $namespace --timeout=30s | complete
# Remove finalizers from persistent volumes if they exist
print $"($env.ALGA_COLOR_CYAN)Checking for stuck persistent volumes...($env.ALGA_COLOR_RESET)"
let pvs_result = do {
kubectl get pv -o json | complete
}
if $pvs_result.exit_code == 0 {
# This would require jq to parse JSON properly, so we'll skip PV cleanup for now
print $"($env.ALGA_COLOR_YELLOW)Note: If PVs are stuck, you may need to manually remove finalizers($env.ALGA_COLOR_RESET)"
}
# Force delete the namespace
print $"($env.ALGA_COLOR_CYAN)Force deleting namespace...($env.ALGA_COLOR_RESET)"
# Remove namespace finalizers first
print $" Removing namespace finalizers..."
kubectl patch namespace $namespace -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
let namespace_result = do {
kubectl delete namespace $namespace --grace-period=0 --force --timeout=30s | complete
}
if $namespace_result.exit_code == 0 {
print $"($env.ALGA_COLOR_GREEN)Force cleanup completed successfully.($env.ALGA_COLOR_RESET)"
} else {
# Wait and check if deletion completed
print $" Waiting for namespace deletion to complete..."
sleep 3sec
let final_check = do {
kubectl get namespace $namespace | complete
}
if $final_check.exit_code != 0 {
print $"($env.ALGA_COLOR_GREEN)Namespace deletion completed after wait.($env.ALGA_COLOR_RESET)"
} else {
print $"($env.ALGA_COLOR_YELLOW)Some resources may still need manual cleanup.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)Check with: kubectl get all -A | grep ($sanitized_branch)($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)Or try: kubectl patch namespace ($namespace) -p '{\\\"metadata\\\":{\\\"finalizers\\\":null}}' --type=merge($env.ALGA_COLOR_RESET)"
}
}
}
# Get environment status and URLs
export def dev-env-status [
branch?: string # Optional branch name, shows all if omitted
] {
if ($branch | is-empty) {
dev-env-list
return
}
# Sanitize branch name for namespace lookup
let sanitized_branch = (sanitize-branch-name $branch)
let namespace = $"alga-dev-($sanitized_branch)"
# Check if environment exists
let env_check = do {
kubectl get namespace $namespace | complete
}
if $env_check.exit_code != 0 {
print $"($env.ALGA_COLOR_RED)Environment for branch ($branch) not found.($env.ALGA_COLOR_RESET)"
print $"($env.ALGA_COLOR_YELLOW)Use 'dev-env-list' to see available environments.($env.ALGA_COLOR_RESET)"
return
}
print $"($env.ALGA_COLOR_CYAN)Development Environment Status - Branch: ($branch)($env.ALGA_COLOR_RESET)"
print "═══════════════════════════════════════════════════════"
# Get deployment status
print $"($env.ALGA_COLOR_CYAN)Deployments:($env.ALGA_COLOR_RESET)"
let deployments_result = do {
kubectl get deployments -n $namespace -o custom-columns="NAME:.metadata.name,READY:.status.readyReplicas,TOTAL:.status.replicas,AGE:.metadata.creationTimestamp" --no-headers | complete
}
if $deployments_result.exit_code == 0 {
($deployments_result.stdout | lines | each { |line|
if ($line | str trim | str length) > 0 {
print $" ($line)"
}
})
} else {
print $" ($env.ALGA_COLOR_RED)Error getting deployment status($env.ALGA_COLOR_RESET)"
}
print ""
# Get service URLs
print $"($env.ALGA_COLOR_CYAN)Service URLs:($env.ALGA_COLOR_RESET)"
let ingress_result = do {
kubectl get ingress -n $namespace -o jsonpath='{range .items[*]}{range .spec.rules[*]}{.host}{"\n"}{end}{end}' | complete
}
if $ingress_result.exit_code == 0 {
let hosts = ($ingress_result.stdout | lines | each { |line| $line | str trim } | where { |x| ($x | str length) > 0 })
for host in $hosts {
let url = $"https://($host)"
if ($host | str contains "code") {
print $" Code Server: ($url)"
} else if ($host | str contains "ai-api") {
print $" AI API: ($url)"
} else if ($host | str contains "ai") {
print $" AI Web: ($url)"
} else {
print $" PSA App: ($url)"
}
}
} else {
print $" ($env.ALGA_COLOR_YELLOW)No ingress URLs found($env.ALGA_COLOR_RESET)"
}
print ""
# Get external ports if available
let ports_result = do {
kubectl get configmap -n $namespace $"alga-dev-($sanitized_branch)-external-ports" -o json | complete
}
if $ports_result.exit_code == 0 {
# Parse the ConfigMap data
let configmap_data = ($ports_result.stdout | from json)
let ports_data = $configmap_data.data
print $"($env.ALGA_COLOR_CYAN)Assigned External Ports:($env.ALGA_COLOR_RESET)"
print $" Main App: localhost:($ports_data.app)"
print $" Code Server: localhost:($ports_data.codeServer)"
print $" Code App: localhost:($ports_data.codeApp)"
print $" AI Web: localhost:($ports_data.aiWeb)"
print ""
}
# Port forward instructions
print $"($env.ALGA_COLOR_CYAN)Local Access - Port Forward:($env.ALGA_COLOR_RESET)"
print $" Run: dev-env-connect ($branch)"
print $" This will use the pre-assigned ports shown above"
print ""
print $"($env.ALGA_COLOR_CYAN)Management Commands:($env.ALGA_COLOR_RESET)"
print $" Connect: dev-env-connect ($branch)"
print $" Destroy: dev-env-destroy ($branch) [--force]"
}