npm package: @jsleekr/depstart
Description: <div align="center">
Latest version: 0.1.0

<div align="center">

# 🚀 depstart

### Start services in the right order

[![GitHub Stars](https://img.shields.io/github/stars/JSLEEKR/depstart?style=for-the-badge&logo=github&color=yellow)](https://github.com/JSLEEKR/depstart/stargazers)
[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.4%2B-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](#)
[![Tests](https://img.shields.io/badge/tests-129%20passing-brightgreen?style=for-the-badge)](#)

<br/>

**Local dev service orchestrator with dependency ordering and health checks.**

Dependency DAG + Parallel Startup + Health Checks

[Quick Start](#quick-start) | [Features](#features)

</div>

---

## Why This Exists

Starting a local development stack by hand means memorizing which services depend on which, waiting for databases to be ready before launching APIs, and manually restarting anything that crashes. depstart reads a simple YAML config and handles all of it -- services launch in the correct topological order, health checks gate each layer, and crashes trigger automatic restarts with exponential backoff. One command replaces a morning ritual of terminal tabs.

### Features

| Feature | Description |
|---------|-------------|
| Dependency DAG | Topological sort ensures correct start order |
| Parallel Startup | Independent services launch simultaneously |
| Health Checks | TCP, HTTP, and command-based readiness probes |
| Colored Logs | Multiplexed output with per-service prefixes |
| Auto-Restart | Exponential backoff with configurable policies |
| Graceful Shutdown | SIGTERM -> SIGKILL cascade (Windows: taskkill) |
| Config Validation | Fail-fast with actionable error messages |
| Programmatic API | Import and use from TypeScript/JavaScript |
| Cross-Platform | Works on macOS, Linux, and Windows |

```
depstart.yaml          depstart up
+------------+        +---------+---------+
| postgres   |   =>   | Layer 0 | pg, redis (parallel)
| redis      |        +---------+---------+
| api -> [pg,redis]   | Layer 1 | api
| web -> [api]|        +---------+---------+
+------------+        | Layer 2 | web
                      +-------------------+
```

## Requirements

- **Node.js >= 18** (uses ES2022 features)
- npm >= 9

## Quick Start

```bash
# Install globally
npm install -g depstart

# Create a config in your project
depstart init          # generates depstart.yaml with example services

# Validate your config (optional)
depstart validate      # checks YAML syntax, dependencies, health checks

# Start all services
depstart up            # starts in dependency order, shows colored logs

# Stop everything
# Press Ctrl+C in the terminal running depstart up
```

## Usage

```bash
depstart init              # Generate sample depstart.yaml
depstart up                # Start services in dependency order
depstart up -c custom.yaml # Use a custom config file
depstart status            # Show service configuration
depstart validate          # Validate config without starting
depstart down              # Stop all services (use Ctrl+C on running instance)
depstart logs [service]    # View service logs (shown in real-time during `up`)
```

## Configuration

Create a `depstart.yaml` in your project root:

```yaml
services:
  postgres:
    cmd: "docker run -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:16"
    health:
      tcp: "localhost:5432"
      timeout: 10s

  redis:
    cmd: "redis-server"
    health:
      tcp: "localhost:6379"

  api:
    cmd: "npm run dev"
    depends_on: [postgres, redis]
    health:
      http: "http://localhost:8080/health"
    env:
      DATABASE_URL: "postgres://localhost/mydb"
      REDIS_URL: "redis://localhost:6379"

  frontend:
    cmd: "npm run dev -- --port 3000"
    depends_on: [api]
```

### Service Options

| Option       | Type     | Default      | Description                              |
|-------------|----------|--------------|------------------------------------------|
| `cmd`       | string   | **required** | Command to start the service             |
| `depends_on`| string[] | `[]`         | Services that must be healthy first      |
| `health`    | object   | none         | Health check configuration               |
| `env`       | object   | `{}`         | Environment variables                    |
| `cwd`       | string   | `process.cwd()` | Working directory                     |
| `restart`   | string   | `on-failure` | Restart policy: `always`, `on-failure`, `never` |

### Health Check Options

| Option     | Type   | Default | Description                         |
|-----------|--------|---------|-------------------------------------|
| `tcp`     | string | -       | TCP host:port to check              |
| `http`    | string | -       | HTTP URL (expects 2xx)              |
| `cmd`     | string | -       | Command (expects exit code 0)       |
| `interval`| string | `1s`    | Time between checks                 |
| `timeout` | string | `30s`   | Overall timeout                     |
| `retries` | number | `3`     | Number of retries before giving up  |

Only one of `tcp`, `http`, or `cmd` can be specified per health check.

## Example Configs

### Node.js + PostgreSQL

```yaml
services:
  postgres:
    cmd: "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16"
    health:
      tcp: "localhost:5432"
      timeout: 15s
      retries: 5

  app:
    cmd: "npm run dev"
    depends_on: [postgres]
    health:
      http: "http://localhost:3000/health"
    env:
      DATABASE_URL: "postgres://postgres:dev@localhost:5432/mydb"
```

### Python + Redis

```yaml
services:
  redis:
    cmd: "redis-server --port 6380"
    health:
      tcp: "localhost:6380"

  celery:
    cmd: "celery -A tasks worker --loglevel=info"
    depends_on: [redis]
    env:
      CELERY_BROKER_URL: "redis://localhost:6380/0"
    restart: on-failure

  api:
    cmd: "uvicorn main:app --reload --port 8000"
    depends_on: [redis]
    health:
      http: "http://localhost:8000/health"
    env:
      REDIS_URL: "redis://localhost:6380/0"
```

### Full-stack (React + Express + PostgreSQL + Redis)

```yaml
services:
  postgres:
    cmd: "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16"
    health:
      tcp: "localhost:5432"
      timeout: 15s

  redis:
    cmd: "docker run --rm -p 6379:6379 redis:7"
    health:
      tcp: "localhost:6379"

  api:
    cmd: "npm run dev"
    cwd: "./packages/api"
    depends_on: [postgres, redis]
    health:
      http: "http://localhost:4000/health"
    env:
      DATABASE_URL: "postgres://postgres:dev@localhost:5432/app"
      REDIS_URL: "redis://localhost:6379"

  web:
    cmd: "npm run dev"
    cwd: "./packages/web"
    depends_on: [api]
    env:
      VITE_API_URL: "http://localhost:4000"
```

## Troubleshooting

### Config file not found

```
Config error: Config file not found: /path/to/depstart.yaml
```

Run `depstart init` to generate a sample config, or specify a path with `depstart up -c path/to/config.yaml`.

### Service fails to start

- Check that the command works standalone: run `cmd` directly in your terminal
- Verify that any ports are not already in use
- Check the `cwd` path is correct (relative to where you run depstart)

### Health check always fails

- **TCP**: Ensure the service is binding to the correct host/port. Use `127.0.0.1` instead of `localhost` if DNS resolution is slow.
- **HTTP**: Verify the endpoint returns a 2xx status code. The URL must be fully qualified (e.g., `http://localhost:3000/health`).
- **Command**: Run the health check command manually to ensure it exits with code 0.
- Increase `timeout` and `retries` for services that take longer to boot.

### Cyclic dependency error

```
Dependency error: Cyclic dependency detected: a -> b -> a
```

Review `depends_on` fields. Two services cannot depend on each other. Restructure your services so dependencies form a DAG (directed acyclic graph).

### Process keeps restarting

The default restart policy is `on-failure`. Set `restart: never` to disable auto-restart, or `restart: always` if you want the service to always restart (even on clean exit).

## FAQ

**Q: Can I use depstart with Docker Compose?**
A: Yes. You can use depstart to orchestrate services that run Docker containers via their `cmd` field. See the Integration Patterns docs for details.

**Q: What happens when I press Ctrl+C?**
A: depstart sends SIGTERM to all managed processes, waits up to 5 seconds for graceful shutdown, then sends SIGKILL. On Windows, `taskkill /F` is used.

**Q: Can services run in parallel?**
A: Yes. Services without dependencies (or whose dependencies are all healthy) start in parallel. depstart uses topological sort to determine layers.

**Q: Does depstart support .env files?**
A: Not yet. For now, use the `env` field in `depstart.yaml` or set environment variables in your shell before running depstart.

**Q: What restart policies are available?**
A: `on-failure` (default) restarts only on non-zero exit, `always` restarts on any exit, `never` never restarts. Restarts use exponential backoff (1s, 2s, 4s, ... up to 30s).

## Integration Patterns

### Alongside Docker Compose

depstart and docker-compose serve different purposes and work well together. Use docker-compose for infrastructure (databases, message queues) and depstart for your application services:

```yaml
# docker-compose.yaml - infrastructure only
services:
  postgres:
    image: postgres:16
    ports: ["5432:5432"]
    environment:
      POSTGRES_PASSWORD: dev

  redis:
    image: redis:7
    ports: ["6379:6379"]
```

```yaml
# depstart.yaml - application services that depend on docker-compose infra
services:
  infra:
    cmd: "docker compose up"
    health:
      tcp: "localhost:5432"
      timeout: 30s
      retries: 10

  api:
    cmd: "npm run dev"
    depends_on: [infra]
    health:
      http: "http://localhost:3000/health"
    env:
      DATABASE_URL: "postgres://postgres:dev@localhost:5432/app"

  worker:
    cmd: "npm run worker"
    depends_on: [infra]
    env:
      REDIS_URL: "redis://localhost:6379"
```

### With Makefile

```makefile
.PHONY: dev stop

dev:
	depstart up

stop:
	@echo "Press Ctrl+C on the depstart process to stop services"

validate:
	depstart validate

ci:
	npm run ci
```

### Programmatic Usage

depstart exports its core modules for use in scripts and custom tools:

```typescript
import { parseConfig, resolveOrder, ProcessManager, OutputMultiplexer } from 'depstart';

const services = parseConfig(yamlString);
const layers = resolveOrder(services);

const output = new OutputMultiplexer();
const manager = new ProcessManager(output);

for (const layer of layers) {
  await Promise.all(layer.map(svc => manager.startService(svc)));
}

// Later...
await manager.stopAll();
```

### With npm scripts

```json
{
  "scripts": {
    "dev": "depstart up",
    "dev:validate": "depstart validate",
    "dev:status": "depstart status"
  }
}
```

## How It Works

depstart orchestrates your local development services in four phases:

### 1. Config Parsing & Validation

When you run `depstart up`, it reads `depstart.yaml` and validates every field: service commands, dependency references, health check formats, restart policies, and environment variables. Invalid configs fail fast with clear error messages.

### 2. DAG Resolution (Topological Sort)

Services and their `depends_on` relationships form a directed acyclic graph (DAG). depstart uses **Kahn's algorithm** to sort services into layers:

```
Layer 0: [postgres, redis]    -- no dependencies, start first
Layer 1: [api]                -- depends on postgres + redis
Layer 2: [web]                -- depends on api
```

If a cycle is detected (e.g., A depends on B, B depends on A), depstart reports the exact cycle path and exits.

### 3. Parallel Start with Health Polling

Each layer starts all its services **in parallel**. For each service:

1. The command is spawned as a child process
2. stdout/stderr are captured and multiplexed with colored prefixes
3. If a health check is configured, depstart polls at the configured `interval`
4. The service is marked **healthy** when the check passes, or **unhealthy** after `retries` failures or `timeout` expires

depstart waits for all services in a layer to become healthy before starting the next layer.

### 4. Steady State & Shutdown

Once all layers are started, depstart keeps running and streaming logs. On **Ctrl+C** (SIGINT/SIGTERM):

1. All child processes receive SIGTERM (or `taskkill /F` on Windows)
2. depstart waits up to 5 seconds for graceful shutdown
3. Remaining processes are killed with SIGKILL
4. depstart exits

Services with `restart: on-failure` automatically restart with exponential backoff (1s, 2s, 4s, ... up to 30s) if they crash.

### Architecture

```
depstart.yaml
    |
    v
[Config Parser] --> validates YAML, fields, references
    |
    v
[DAG Resolver]  --> topological sort into parallel layers
    |
    v
[Process Manager] --> spawns processes, manages lifecycle
    |         |
    v         v
[Health Checker]  [Output Multiplexer]
  polls tcp/http/cmd    colored log prefixes
```

## License

MIT