#!/usr/bin/env bash # scripts/lib/connect_state.sh — S7: local capability state management # Sourced by connect scripts. Manages .local/capability_state.json. # Do not execute directly. # # All state is written to .local/ (gitignored) — never into repo-tracked files. # Fixtures under data/fixtures/setup/ remain untouched. # # SECURITY: All Python invocations pass data via stdin, env vars, or sys.argv. # Shell variables are NEVER interpolated into Python source strings. set -euo pipefail # ── Local state directory ────────────────────────────────────────────────── LOCAL_DIR="${REPO_ROOT}/.local" STATE_FILE="${LOCAL_DIR}/capability_state.json" # Valid status values (must match setup-education ConnectionStatus enum) VALID_STATUSES="connected skipped later error offline" # ── Ensure .local directory exists ───────────────────────────────────────── ensure_local_dir() { if [[ ! -d "$LOCAL_DIR" ]]; then mkdir -p "$LOCAL_DIR" log_info "Created local state directory: $LOCAL_DIR" fi } # ── Read current state (returns empty JSON object if no state file) ──────── read_state() { if [[ -f "$STATE_FILE" ]]; then cat "$STATE_FILE" else echo '{}' fi } # ── Get status for a target ──────────────────────────────────────────────── # Usage: get_status "square" → "connected" | "skipped" | "later" | "error" | "" get_status() { local target="$1" local state state="$(read_state)" # Pass state via stdin, target via env var — no string interpolation printf '%s' "$state" | TARGET="$target" python3 -c " import json, sys, os state = json.loads(sys.stdin.read()) target = os.environ['TARGET'] print(state.get(target, {}).get('status', '')) " 2>/dev/null || echo "" } # ── Set status for a target ──────────────────────────────────────────────── # Usage: set_status "square" "connected" "Square MCP registered" set_status() { local target="$1" local status="$2" local details="${3:-}" # Validate status local valid=0 for s in $VALID_STATUSES; do if [[ "$s" == "$status" ]]; then valid=1 break fi done if [[ $valid -eq 0 ]]; then log_error "Invalid status '$status'. Must be one of: $VALID_STATUSES" return 1 fi ensure_local_dir local state state="$(read_state)" # Pass state via stdin, target/status/details via env vars — no interpolation printf '%s' "$state" | \ CONNECT_TARGET="$target" \ CONNECT_STATUS="$status" \ CONNECT_DETAILS="$details" \ python3 -c " import json, sys, os, datetime state = json.loads(sys.stdin.read()) target = os.environ['CONNECT_TARGET'] status = os.environ['CONNECT_STATUS'] details = os.environ['CONNECT_DETAILS'] state[target] = { 'status': status, 'details': details, 'updated_at': datetime.datetime.now(datetime.timezone.utc).isoformat() } # Ensure top-level metadata if 'generated_at' not in state: state['generated_at'] = datetime.datetime.now(datetime.timezone.utc).isoformat() if 'is_fixture' not in state: state['is_fixture'] = False json.dump(state, sys.stdout, indent=2) " > "${STATE_FILE}.tmp" 2>/dev/null mv "${STATE_FILE}.tmp" "$STATE_FILE" chmod 600 "$STATE_FILE" log_info "State updated: ${target} → ${status}" } # ── Print status summary (human-readable) ────────────────────────────────── print_status() { local state state="$(read_state)" if [[ "$state" == "{}" ]]; then log_info "No connection state recorded yet." log_info "Run connect commands to establish integrations." return 0 fi log_section "Connection Status" # Pass state via stdin — no interpolation printf '%s' "$state" | python3 -c " import json, sys state = json.loads(sys.stdin.read()) # Remove metadata keys meta_keys = {'generated_at', 'is_fixture'} targets = {k: v for k, v in state.items() if k not in meta_keys} if not targets: print(' No targets configured yet.') else: status_icons = { 'connected': '✅', 'skipped': '⏭️', 'later': '⏳', 'error': '❌', 'offline': '📋', } for target, info in sorted(targets.items()): status = info.get('status', 'unknown') icon = status_icons.get(status, '❓') details = info.get('details', '') updated = info.get('updated_at', '') display = target.replace('_', ' ').title() print(f' {icon} {display:25s} {status:10s} {details}') if updated: print(f' Updated: {updated}') " 2>/dev/null echo "" } # ── Export state as capability report JSON (compatible with setup-education) ─ # Produces the same schema as data/fixtures/setup/capability_matrix.json export_capability_report() { local output_file="${1:-}" local state state="$(read_state)" if [[ "$state" == "{}" ]]; then log_warn "No connection state to export." return 1 fi local sandbox_name sandbox_name="$(get_sandbox_name)" # Pass state via stdin, sandbox_name via env var — no interpolation local report report="$(printf '%s' "$state" | \ CONNECT_SANDBOX="$sandbox_name" \ python3 -c " import json, sys, os, datetime state = json.loads(sys.stdin.read()) sandbox_name = os.environ['CONNECT_SANDBOX'] meta_keys = {'generated_at', 'is_fixture'} targets = {k: v for k, v in state.items() if k not in meta_keys} capabilities = [] area_map = { 'name': 'identity', 'profile': 'profile', 'whatsapp': 'channels', 'email': 'channels', 'telegram': 'channels', 'square': 'scheduling', 'quickbooks': 'books', 'vagaro': 'scheduling', } for target, info in sorted(targets.items()): area = area_map.get(target, 'other') capabilities.append({ 'area': area, 'provider': target, 'status': info.get('status', 'offline'), 'details': info.get('details', ''), }) report = { 'salon_name': sandbox_name, 'is_fixture': False, 'generated_at': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'capabilities': capabilities, } json.dump(report, sys.stdout, indent=2) " 2>/dev/null)" if [[ -n "$output_file" ]]; then echo "$report" > "$output_file" log_info "Capability report exported to $output_file" else echo "$report" fi }