---
name: dev-start
description: Launch SmartStack dev environment (backend + frontend + admin credentials)
argument-hint: "[--stop] [--reset] [--backend-only] [--frontend-only]"
model: haiku
allowed-tools: Read, Bash, Glob, Write, TaskOutput
---

## Arguments
$ARGUMENTS

## Current state (auto-injected)

- Dev cache: !`cat .claude/config/dev-run.json 2>/dev/null || echo "no cache"`
- Dev session: !`cat .claude/dev-session.json 2>/dev/null || echo "no session"`
- Ports running: !`netstat.exe -ano 2>/dev/null | grep -E "LISTENING" | grep -E ":(5[0-9]{3}|6173|3000) " | head -5 || echo "none"`

<objective>
Launch the SmartStack development environment in one command: detect configuration, validate config coherence, check SQL connectivity, start backend + frontend, and provide admin credentials.
Idempotent: safe to run multiple times. Detects already-running processes.
Uses a config cache to skip re-detection on subsequent runs when config files haven't changed.
</objective>

<quick_start>
```bash
/dev-start                  # Launch everything (idempotent)
/dev-start --stop           # Stop backend + frontend
/dev-start --reset          # Force admin password reset + re-detect config
/dev-start --backend-only   # Launch only the backend
/dev-start --frontend-only  # Launch only the frontend
```
</quick_start>

<subcommands>

| Command | Description |
|---------|-------------|
| `/dev-start` | Launch all (idempotent) + validate configs |
| `/dev-start --stop` | Stop backend + frontend via taskkill.exe |
| `/dev-start --reset` | Force reset admin password + re-detect config |
| `/dev-start --backend-only` | Launch only backend |
| `/dev-start --frontend-only` | Launch only frontend |

</subcommands>

<workflow>

## Handle --stop

If `--stop` argument detected, execute the stop workflow and exit:

1. Determine ports to stop:
   - If `.claude/config/dev-run.json` exists → read `api_port` and `web_port` from cache
   - Otherwise → scan default ports: 5142, 5290 (API) and 6173, 3000 (Web)

2. Find backend PID:
   ```bash
   netstat.exe -ano | grep ":${API_PORT} .*LISTENING" | awk '{print $5}' | head -1
   ```
3. If found: `taskkill.exe /PID $PID /F`
4. Find frontend PID:
   ```bash
   netstat.exe -ano | grep ":${WEB_PORT} .*LISTENING" | awk '{print $5}' | head -1
   ```
5. If found: `taskkill.exe /PID $PID /F`
6. Display stopped status and exit.

If no processes found, display "Nothing running" and exit.

## Step 0: Load cache

If `--stop` argument → skip this step entirely (--stop uses netstat only).

1. Read cache file:
   ```bash
   cat .claude/config/dev-run.json 2>/dev/null
   ```

2. **If no cache OR `--reset` argument** → CACHE_HIT=false, go to Step 1.

3. **If cache exists**, verify config files haven't changed:
   ```bash
   API_DIR=$(grep -o '"api_dir": *"[^"]*"' .claude/config/dev-run.json | cut -d'"' -f4)
   WEB_DIR=$(grep -o '"web_dir": *"[^"]*"' .claude/config/dev-run.json | cut -d'"' -f4)
   MODE=$(grep -o '"mode": *"[^"]*"' .claude/config/dev-run.json | cut -d'"' -f4)

   if [ "$MODE" = "Local" ]; then
     CURRENT_HASH=$(stat -c %Y "$API_DIR/appsettings.Local.json" "$WEB_DIR/.env.standalone" "$API_DIR/Properties/launchSettings.json" 2>/dev/null | md5sum | cut -d' ' -f1)
   else
     CURRENT_HASH=$(stat -c %Y "$API_DIR/appsettings.json" "$WEB_DIR/.env.development" "$API_DIR/Properties/launchSettings.json" 2>/dev/null | md5sum | cut -d' ' -f1)
   fi

   CACHED_HASH=$(grep -o '"config_hash": *"[^"]*"' .claude/config/dev-run.json | cut -d'"' -f4)
   ```

4. **If CURRENT_HASH = CACHED_HASH** → CACHE_HIT=true :
   - Load all values from cache (api_dir, web_dir, mode, ports, commands, sql_status)
   - Display: `[CACHE] Using cached config (from {cached_at})`
   - **Skip directly to Step 4** (check if already running)

5. **If hash mismatch** → CACHE_HIT=false :
   - Display: `[CACHE] Config changed, re-detecting...`
   - Continue to Step 1

## Step 1: Detect project

**Skip if CACHE_HIT=true (values loaded from cache in Step 0).**

Find the API and Web directories using bash (Glob doesn't match directories):
```bash
API_DIR=$(ls -d src/*.Api 2>/dev/null | head -1)
WEB_DIR=$(ls -d web/*-web 2>/dev/null | head -1)
```

If neither found, display error and exit.

## Step 2: Detect environment and ports

**Skip if CACHE_HIT=true (values loaded from cache in Step 0).**

Check if `appsettings.Local.json` exists in the API directory:

| Condition | Mode | Backend | Frontend | Backend command | Frontend command |
|-----------|------|---------|----------|-----------------|------------------|
| `appsettings.Local.json` exists | **Local** | Port from `Kestrel:Endpoints:Http:Url` | Port from `.env.standalone` or `package.json` local script | `dotnet run --launch-profile Local` | `npm run local` |
| No local config | **Development** | Port 5142 (launchSettings http profile) | Port 6173 (vite default) | `dotnet run --environment Development` | `npm run dev` |

**Reading ports:**
- **Local backend port**: Parse `appsettings.Local.json` -> `Kestrel.Endpoints.Http.Url` -> extract port from URL (e.g., `http://localhost:5290` -> `5290`)
- **Local frontend port**: Parse `.env.standalone` -> look for port in `VITE_API_URL`, or parse `package.json` -> `scripts.local` for `--port` flag
- **Dev backend port**: Default `5142`
- **Dev frontend port**: Default `6173`

**IMPORTANT: Read config files in parallel.** Use a single message with multiple Read tool calls for:
- `{API_DIR}/appsettings.json` (or `appsettings.Local.json`)
- `{WEB_DIR}/.env.development` (or `.env.standalone`)
- `{API_DIR}/Properties/launchSettings.json`

## Step 3: Validate config coherence

**Skip if CACHE_HIT=true (values loaded from cache in Step 0).**

**CRITICAL: Verify that backend and frontend configs are consistent.**

### Rules to check:

| Rule | Backend source | Frontend source | Validation |
|------|---------------|-----------------|------------|
| Frontend calls correct backend | Port API (Kestrel or launchSettings) | `VITE_API_URL` in `.env.standalone` (Local) or `.env.development` (Dev) | Port in VITE_API_URL must match API port |
| Backend allows frontend (CORS) | `SmartStack:CorsOrigins[]` in appsettings | Frontend port | CorsOrigins must contain `http://localhost:{WEB_PORT}` |
| OAuth redirects to correct frontend | `Authentication:FrontendUrl` in appsettings | Frontend port | FrontendUrl must point to `http://localhost:{WEB_PORT}` |
| Local files exist in pairs | `appsettings.Local.json` | `.env.standalone` | If one exists, the other must too |

### In Local mode:

1. Read `appsettings.Local.json` -> extract `Kestrel:Endpoints:Http:Url` (API port)
2. Read `appsettings.Local.json` -> extract `Authentication:FrontendUrl` (expected frontend URL)
3. Read `.env.standalone` -> extract `VITE_API_URL` (expected API URL by frontend)
4. Verify `VITE_API_URL` port matches Kestrel port
5. Verify `Authentication:FrontendUrl` port matches frontend port
6. If `appsettings.Local.json` exists but NOT `.env.standalone` -> **create `.env.standalone`** with correct values (ask user first)
7. If `.env.standalone` exists but NOT `appsettings.Local.json` -> **warn** (config incomplete)

### In Development mode:

1. Verify `.env.development` contains `VITE_API_URL=http://localhost:5142` (NOT 5290 which is Local profile)
2. Verify `appsettings.json` -> `SmartStack:CorsOrigins` contains `http://localhost:6173`
3. Verify `Authentication:FrontendUrl` = `http://localhost:6173`

### If incoherence detected:

1. Display a clear WARNING with detected values vs expected values
2. Propose to auto-correct (write the correct files)
3. If user refuses, continue with a warning

### Output format for coherence:

```
Config Coherence Check:
  [OK] VITE_API_URL matches backend port (5290)
  [OK] CORS allows frontend (http://localhost:3000)
  [OK] OAuth redirect matches frontend
  [OK] Local config files paired

  -- OR --

  [WARN] VITE_API_URL=http://localhost:5290 but backend runs on port 5142
         Expected: VITE_API_URL=http://localhost:5142
  [WARN] .env.standalone missing (appsettings.Local.json exists)
         -> Creating .env.standalone with correct values...
```

### Rule 5: SQL Server connectivity

1. Extract connection string from appsettings:
   - **Local mode**: `appsettings.Local.json` → `ConnectionStrings.DefaultConnection`
   - **Dev mode**: `appsettings.json` → `ConnectionStrings.DefaultConnection`

2. Parse server and database:
   ```bash
   SQL_SERVER=$(echo "$CONN_STR" | grep -oiP '(?:Server|Data Source)=\K[^;]+')
   SQL_DATABASE=$(echo "$CONN_STR" | grep -oiP '(?:Database|Initial Catalog)=\K[^;]+')
   USE_WIN_AUTH=$(echo "$CONN_STR" | grep -qi 'Integrated Security=true\|Trusted_Connection=true' && echo "true" || echo "false")
   ```

3. Test connectivity (5s timeout):
   ```bash
   # Normalize (local) → .
   SQL_SERVER_NORM=$(echo "$SQL_SERVER" | sed 's/(local)/./')

   if [ "$USE_WIN_AUTH" = "true" ]; then
     sqlcmd -S "$SQL_SERVER_NORM" -d master -Q "SELECT 1" -t 5 -E -C -h -1 -W 2>/dev/null
   else
     SQL_USER=$(echo "$CONN_STR" | grep -oiP '(?:User Id|Uid)=\K[^;]+')
     SQL_PASS=$(echo "$CONN_STR" | grep -oiP '(?:Password|Pwd)=\K[^;]+')
     sqlcmd -S "$SQL_SERVER_NORM" -d master -Q "SELECT 1" -t 5 -U "$SQL_USER" -P "$SQL_PASS" -C -h -1 -W 2>/dev/null
   fi
   ```

4. If reachable, check database exists:
   ```bash
   sqlcmd -S "$SQL_SERVER_NORM" -Q "SET NOCOUNT ON; SELECT name FROM sys.databases WHERE name = '$SQL_DATABASE'" -t 5 -E -C -h -1 -W 2>/dev/null
   ```

5. Output format:
   | Scenario | Output | Blocking? |
   |----------|--------|-----------|
   | Server OK + DB exists | `[OK] SQL Server (server) — Database 'db' exists` | No |
   | Server OK + DB missing | `[WARN] Database 'db' not found (AutoMigrate will create it)` | No |
   | Server unreachable | `[FAIL] SQL Server (server) unreachable — Is the service running?` | No |
   | sqlcmd not installed | `[SKIP] sqlcmd not found — SQL check skipped` | No |
   | No connection string | `[SKIP] No connection string — SQL check skipped` | No |

## Step 3b: Write cache

**Only execute if CACHE_HIT=false (Steps 1-3 were executed).**

1. Ensure directory exists: `mkdir -p .claude/config`

2. Compute config hash:
   ```bash
   if [ "$MODE" = "Local" ]; then
     CONFIG_HASH=$(stat -c %Y "$API_DIR/appsettings.Local.json" "$WEB_DIR/.env.standalone" "$API_DIR/Properties/launchSettings.json" 2>/dev/null | md5sum | cut -d' ' -f1)
   else
     CONFIG_HASH=$(stat -c %Y "$API_DIR/appsettings.json" "$WEB_DIR/.env.development" "$API_DIR/Properties/launchSettings.json" 2>/dev/null | md5sum | cut -d' ' -f1)
   fi
   ```

3. Write `.claude/config/dev-run.json` using the Write tool with all detected values:
   ```json
   {
     "version": 2,
     "api_dir": "{API_DIR}",
     "web_dir": "{WEB_DIR}",
     "mode": "{MODE}",
     "api_port": {API_PORT},
     "web_port": {WEB_PORT},
     "backend_cmd": "{BACKEND_CMD}",
     "frontend_cmd": "{FRONTEND_CMD}",
     "connection_string": "{CONN_STR}",
     "sql_server": "{SQL_SERVER}",
     "sql_database": "{SQL_DATABASE}",
     "sql_status": "{ok|unreachable|db_missing|skipped}",
     "config_hash": "{CONFIG_HASH}",
     "cached_at": "{ISO_TIMESTAMP}"
   }
   ```

## Step 4: Check if already running

```bash
netstat.exe -ano | grep ":${API_PORT} .*LISTENING"
netstat.exe -ano | grep ":${WEB_PORT} .*LISTENING"
```

If `--backend-only`: skip frontend checks.
If `--frontend-only`: skip backend checks.

## Step 5: Launch backend

Track whether the backend was just launched: `BACKEND_JUST_LAUNCHED=true/false`. This is used in Step 7 for cache invalidation.

If NOT already running and NOT `--frontend-only` (set `BACKEND_JUST_LAUNCHED=true`):

```bash
# cd into the API directory first
cd {API_DIR} && dotnet run {LAUNCH_ARGS}
```

Use `Bash(run_in_background=true)` so it runs in background. **Save the task ID** returned by the background command.

### Fail-fast crash detection

After launching, wait 5 seconds then check if the process crashed:
```bash
sleep 5
```
Then call `TaskOutput(task_id="{TASK_ID}", block=false)` to read the background output.

**If the output contains a stack trace, "Unhandled exception", or the process has already exited:**
1. Display the error output to the user
2. Display: `[FAIL] Backend crashed on startup. Fix the error above and retry /dev-start`
3. **STOP immediately. Do NOT attempt to diagnose, fix, rebuild, or retry.**

## Step 6: Launch frontend

If NOT already running and NOT `--backend-only`:

```bash
# cd into the Web directory first
cd {WEB_DIR} && npm run {SCRIPT}
```

Use `Bash(run_in_background=true)` so it runs in background.

## Step 7: Handle admin password

**Cache invalidation rule:** If the backend was just launched in Step 5 (i.e., it was NOT already running), the cache is STALE because `MigrateAsync()` + `HasData()` may have re-seeded the admin user with the default password hash, overwriting any previously set password. In this case, ALWAYS run a fresh admin reset regardless of `dev-session.json`.

1. If `.claude/dev-session.json` exists and contains a password AND `--reset` NOT specified AND the backend was already running (NOT launched in Step 5) -> reuse stored credentials
2. If `--reset` OR no stored password OR backend was just launched in Step 5:
   a. If backend was just launched, wait for it to be UP first.
      Run the full wait in a **single bash command** to avoid multiple tool calls:
      ```bash
      # Phase 1: Wait for port to be bound (up to 30s)
      echo "Waiting for backend port ${API_PORT}..."
      for i in $(seq 1 10); do
        netstat.exe -ano | grep ":${API_PORT} .*LISTENING" > /dev/null 2>&1 && echo "Port bound." && break
        sleep 3
      done
      # Phase 2: Wait for API to respond (up to 30s) — accept 200 or 302
      echo "Waiting for API to respond..."
      for i in $(seq 1 10); do
        HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${API_PORT}/scalar" 2>/dev/null)
        [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ] && echo "API ready (HTTP $HTTP_CODE)." && break
        sleep 3
      done
      ```
      If `/scalar` does not respond after 30s, warn the user and attempt the reset anyway.
   b. Run admin reset using the connection string extracted from appsettings (Step 3):
      ```bash
      smartstack admin reset --connection '{CONN_STR}' --force --json
      ```
      **IMPORTANT:** Always pass `--connection` with the full connection string (single-quoted to avoid shell expansion). Without it, the CLI shows an interactive environment picker that blocks execution.
   c. Parse JSON output to extract email and password
   d. Write `.claude/dev-session.json`:
      ```json
      {
        "admin_password": "...",
        "admin_email": "local.admin@smartstack.local",
        "last_reset": "2026-03-10T14:30:00Z",
        "api_port": 5142,
        "web_port": 6173,
        "environment": "Development"
      }
      ```
   e. Verify `.claude/dev-session.json` is in `.gitignore`. If not, warn the user (contains credentials).

## Step 8: Display connection info

```
================================================================
          SMARTSTACK DEV ENVIRONMENT
================================================================

  Backend:     http://localhost:{API_PORT}       [RUNNING|STARTING]
  Frontend:    http://localhost:{WEB_PORT}       [RUNNING|STARTING]
  Swagger:     http://localhost:{API_PORT}/scalar
  SQL Server:  {SQL_SERVER}/{SQL_DATABASE}       [OK|WARN|FAIL|SKIP]

  Email:       local.admin@smartstack.local
  Password:    xxxxxxxxxxxxxx

  Environment: {Development|Local}
  Config:      OK | WARNING (details)
  Cache:       HIT (from {cached_at}) | MISS (re-detected)
================================================================
```

</workflow>

<important_notes>

1. **NEVER debug or fix code** - This skill ONLY starts processes. If the backend or frontend fails to start, display the error and STOP. Do NOT attempt to: clean/restore/rebuild, upgrade packages, inspect DLLs, clear NuGet cache, read .csproj files, or any other diagnostic/repair action. The user will fix the issue themselves.
2. **Fail-fast on crash** - After launching backend in background, wait 5s then check TaskOutput. If the process already exited or shows errors, report and STOP immediately.
3. **Backend must be READY before admin reset** - First poll the port (netstat), then poll the `/scalar` endpoint (HTTP 200/302) to confirm the API is fully initialized. Kestrel binds the port before the app finishes starting, so port-only checks are insufficient.
4. **Admin reset requires --connection** - Always pass `--connection '{CONN_STR}'` (single-quoted to avoid shell expansion) to avoid the interactive environment picker.
5. **Read config files in parallel** - When reading appsettings, .env, and launchSettings, use multiple Read calls in a single message.
6. **Cross-worktree support** - Detect the correct API directory regardless of which worktree we're in.
7. **No MCP required** - This skill only uses bash commands and the `smartstack` CLI.
8. **Config coherence is critical** - Mismatched ports between backend/frontend is the #1 cause of "it doesn't work" issues.
9. **Idempotent** - Running twice should detect already-running processes and reuse stored credentials.
10. **dev-session.json contains secrets** - Must be in `.gitignore`.
11. **Cache** - Config detection is cached in `.claude/config/dev-run.json`. Cache is invalidated when config files change (mtime-based hash). Use `--reset` to force re-detection.
12. **SQL check is non-blocking** - A failed SQL check displays a warning but does not prevent launch. AutoMigrate may create the database on startup.

</important_notes>

<success_criteria>
- Backend and frontend detected and launched (or already running)
- Config coherence validated (or warnings displayed)
- Admin credentials available (reused or freshly reset)
- Connection info displayed in clear format
- No errors on repeated invocations (idempotent)
- Cache written on first run, reused on subsequent runs
- SQL Server connectivity reported
</success_criteria>
