laracrew 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/LICENSE +21 -0
- package/README.md +598 -0
- package/dist/index.js +3524 -0
- package/dist/index.js.map +1 -0
- package/examples/README.md +79 -0
- package/examples/django-celery/stack.yaml +94 -0
- package/examples/frontend-monorepo/stack.yaml +76 -0
- package/examples/laravel-dual/stack.yaml +107 -0
- package/examples/node-api-and-web/stack.yaml +99 -0
- package/examples/polyglot-microservices/stack.yaml +120 -0
- package/package.json +55 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Five working stacks. Only one of them is Laravel — laracrew supervises processes, and it does
|
|
4
|
+
not care what language wrote them.
|
|
5
|
+
|
|
6
|
+
Each example is self-contained: projects are declared inline, so there is nothing to wire up in
|
|
7
|
+
`projects.yaml` first. Copy one, change the paths at the top, and run it.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cp -r examples/node-api-and-web ~/.laracrew/stacks/
|
|
11
|
+
$EDITOR ~/.laracrew/stacks/node-api-and-web/stack.yaml # fix the paths
|
|
12
|
+
laracrew doctor node-api-and-web
|
|
13
|
+
laracrew up node-api-and-web
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Each stack declares a `command:`, so `laracrew link <name>` turns it into a single global word
|
|
17
|
+
you can type from anywhere.
|
|
18
|
+
|
|
19
|
+
| Example | Stack | What it is |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| [laravel-dual](laravel-dual/stack.yaml) | `dual` | Two interconnected Laravel apps: queues, Redis stream listeners, schedulers, Vite |
|
|
22
|
+
| [node-api-and-web](node-api-and-web/stack.yaml) | `web` | TypeScript API, Vite frontend, BullMQ worker, Postgres and Redis |
|
|
23
|
+
| [django-celery](django-celery/stack.yaml) | `django` | Django, a Celery worker, beat, and the optional extras |
|
|
24
|
+
| [polyglot-microservices](polyglot-microservices/stack.yaml) | `micro` | Go, Rust, Node and Python behind a gateway, with Docker Compose for infrastructure |
|
|
25
|
+
| [frontend-monorepo](frontend-monorepo/stack.yaml) | `fe` | The smallest useful stack: tsc, Tailwind, Storybook and docs watchers in one repo |
|
|
26
|
+
|
|
27
|
+
## What each one is there to teach
|
|
28
|
+
|
|
29
|
+
**laravel-dual** — the case laracrew was built for. Graceful worker shutdown through
|
|
30
|
+
`php artisan queue:restart`, so a job is never killed mid-flight; the portal waiting on a real
|
|
31
|
+
HTTP 200 from the API rather than a sleep; profiles to skip Vite when you are only touching the
|
|
32
|
+
backend.
|
|
33
|
+
|
|
34
|
+
**node-api-and-web** — no PHP anywhere. A one-shot `types:build` that everything else depends on,
|
|
35
|
+
readiness by log output for tools that announce themselves (`Local: http://…`), per-service
|
|
36
|
+
environment variables, and a secret read from your own shell with `${env:VAR:fallback}` instead
|
|
37
|
+
of being committed.
|
|
38
|
+
|
|
39
|
+
**django-celery** — migrations that must finish before the web server starts, `PYTHONUNBUFFERED`
|
|
40
|
+
so the log pane is not empty, a `SIGTERM` graceful stop for Celery instead of an artisan command,
|
|
41
|
+
and a second worker plus Flower left dormant behind `autostart: false`.
|
|
42
|
+
|
|
43
|
+
**polyglot-microservices** — the ordering problem at full size. `docker compose up` supervised as
|
|
44
|
+
an ordinary process so it stops with everything else; a gateway that starts only once all three
|
|
45
|
+
services answer `/health`; a generous gate on the Rust service because a cold `cargo build` is
|
|
46
|
+
slow, rather than a boot that fails at 30 seconds.
|
|
47
|
+
|
|
48
|
+
**frontend-monorepo** — proof that a "stack" needs neither multiple apps nor a server. Watchers
|
|
49
|
+
in one repository, ordered so types build before Storybook consumes them, with `vitest --watch`
|
|
50
|
+
and `lint:fix` sitting idle until you press `s`.
|
|
51
|
+
|
|
52
|
+
## Patterns worth stealing
|
|
53
|
+
|
|
54
|
+
**Gate on reality, not on time.** `ready: { http: … }` or `{ tcp: … }` beats a sleep. When a
|
|
55
|
+
build is genuinely slow, raise `timeoutMs` rather than removing the gate.
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
ready: { http: "http://127.0.0.1:8002/health", timeoutMs: 300000, intervalMs: 2000 }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Keep the occasional things in the stack, just not running.** A backfill command, a database
|
|
62
|
+
UI, a test watcher — define them with `autostart: false` so they are documented and one keypress
|
|
63
|
+
away, instead of living in your shell history.
|
|
64
|
+
|
|
65
|
+
**Let laracrew own Docker Compose too.** It is a long-running process like any other. Supervised
|
|
66
|
+
here, it stops when the rest of the stack stops.
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
- name: infra
|
|
70
|
+
cmd: ["docker", "compose", "up"]
|
|
71
|
+
ready: { tcp: "127.0.0.1:5432", timeoutMs: 120000, intervalMs: 1000 }
|
|
72
|
+
stop: { graceMs: 30000 }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Use the argv form when arguments get interesting.** `cmd: ["npm", "run", "dev"]` cannot
|
|
76
|
+
mis-split on a path containing spaces the way a shell string can.
|
|
77
|
+
|
|
78
|
+
**A gate with nothing to start is fine.** An `external: true` service is just a health check, so
|
|
79
|
+
it can stand in as a named wait step — see `kafka-wait` in the microservices example.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Django with Celery: web server, worker, beat scheduler, and the optional extras.
|
|
2
|
+
#
|
|
3
|
+
# Demonstrates: reading values out of the project's own .env with ${project.env:...},
|
|
4
|
+
# a virtualenv interpreter, autostart:false for tools you open occasionally,
|
|
5
|
+
# and a worker whose graceful stop is a signal rather than an artisan command.
|
|
6
|
+
#
|
|
7
|
+
# Setup: edit the path and the interpreter, then
|
|
8
|
+
# cp -r django-celery ~/.laracrew/stacks/
|
|
9
|
+
# laracrew up django-celery
|
|
10
|
+
name: django-celery
|
|
11
|
+
description: Django + Celery worker + beat
|
|
12
|
+
command: django
|
|
13
|
+
|
|
14
|
+
projects:
|
|
15
|
+
app:
|
|
16
|
+
path: ~/code/bookstore
|
|
17
|
+
envFile: .env # DJANGO_SETTINGS_MODULE, REDIS_URL, DB_*
|
|
18
|
+
color: green
|
|
19
|
+
|
|
20
|
+
defaults:
|
|
21
|
+
restart: on-failure
|
|
22
|
+
backoff: { initialMs: 1000, maxMs: 20000, factor: 2, maxRestarts: 10 }
|
|
23
|
+
# Celery handles SIGTERM properly: it stops accepting work and drains what it holds.
|
|
24
|
+
stop: { signal: SIGTERM, graceMs: 20000 }
|
|
25
|
+
|
|
26
|
+
services:
|
|
27
|
+
- name: redis
|
|
28
|
+
external: true
|
|
29
|
+
ready: { tcp: "127.0.0.1:6379", timeoutMs: 10000 }
|
|
30
|
+
|
|
31
|
+
- name: postgres
|
|
32
|
+
external: true
|
|
33
|
+
ready: { tcp: "127.0.0.1:5432", timeoutMs: 10000 }
|
|
34
|
+
|
|
35
|
+
# Migrations run to completion before anything that touches the database starts.
|
|
36
|
+
- name: migrate
|
|
37
|
+
project: app
|
|
38
|
+
cmd: [".venv/bin/python", "manage.py", "migrate", "--noinput"]
|
|
39
|
+
groups: [setup]
|
|
40
|
+
needs: [postgres]
|
|
41
|
+
restart: never
|
|
42
|
+
ready: { logMatch: "No migrations to apply|Applying|OK", timeoutMs: 120000 }
|
|
43
|
+
|
|
44
|
+
- name: web
|
|
45
|
+
project: app
|
|
46
|
+
cmd: [".venv/bin/python", "manage.py", "runserver", "0.0.0.0:${port:8000}"]
|
|
47
|
+
groups: [http]
|
|
48
|
+
needs: [postgres, redis, migrate]
|
|
49
|
+
env:
|
|
50
|
+
PYTHONUNBUFFERED: "1" # without this, output is buffered and the log pane stays empty
|
|
51
|
+
DJANGO_SETTINGS_MODULE: bookstore.settings.local
|
|
52
|
+
ready: { http: "http://127.0.0.1:8000/healthz", timeoutMs: 30000 }
|
|
53
|
+
url: http://127.0.0.1:8000
|
|
54
|
+
stop: { graceMs: 5000 }
|
|
55
|
+
|
|
56
|
+
- name: celery:worker
|
|
57
|
+
project: app
|
|
58
|
+
cmd: [".venv/bin/celery", "-A", "bookstore", "worker", "--loglevel=info", "--concurrency=4"]
|
|
59
|
+
groups: [workers]
|
|
60
|
+
needs: [redis, migrate]
|
|
61
|
+
env:
|
|
62
|
+
PYTHONUNBUFFERED: "1"
|
|
63
|
+
ready: { logMatch: "ready\\.$|celery@.* ready", timeoutMs: 30000 }
|
|
64
|
+
|
|
65
|
+
- name: celery:beat
|
|
66
|
+
project: app
|
|
67
|
+
cmd: [".venv/bin/celery", "-A", "bookstore", "beat", "--loglevel=info"]
|
|
68
|
+
groups: [scheduler]
|
|
69
|
+
needs: [redis, celery:worker]
|
|
70
|
+
env:
|
|
71
|
+
PYTHONUNBUFFERED: "1"
|
|
72
|
+
|
|
73
|
+
# A second worker for a queue that only matters during imports.
|
|
74
|
+
- name: celery:imports
|
|
75
|
+
project: app
|
|
76
|
+
cmd: [".venv/bin/celery", "-A", "bookstore", "worker", "--queues=imports", "--concurrency=1"]
|
|
77
|
+
groups: [workers]
|
|
78
|
+
needs: [redis]
|
|
79
|
+
autostart: false
|
|
80
|
+
env:
|
|
81
|
+
PYTHONUNBUFFERED: "1"
|
|
82
|
+
|
|
83
|
+
# Monitoring UI — useful, but not something you need running all day.
|
|
84
|
+
- name: flower
|
|
85
|
+
project: app
|
|
86
|
+
cmd: [".venv/bin/celery", "-A", "bookstore", "flower", "--port=5555"]
|
|
87
|
+
groups: [tooling]
|
|
88
|
+
needs: [redis]
|
|
89
|
+
autostart: false
|
|
90
|
+
url: http://127.0.0.1:5555
|
|
91
|
+
|
|
92
|
+
profiles:
|
|
93
|
+
no-scheduler: { except: [scheduler] }
|
|
94
|
+
workers-only: { only: [workers, redis, migrate, postgres] }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# The smallest useful stack: a handful of watchers in one repository. No servers,
|
|
2
|
+
# no databases, no projects — just the processes you would otherwise keep in six tabs.
|
|
3
|
+
#
|
|
4
|
+
# Demonstrates: that a "stack" does not need multiple apps or any Laravel at all, and that
|
|
5
|
+
# `cwd` alone is enough when everything lives under one root.
|
|
6
|
+
#
|
|
7
|
+
# Setup: point `root` at your repository, then
|
|
8
|
+
# cp -r frontend-monorepo ~/.laracrew/stacks/
|
|
9
|
+
# laracrew up frontend-monorepo
|
|
10
|
+
name: frontend-monorepo
|
|
11
|
+
description: Type checking, bundling and docs for a pnpm workspace
|
|
12
|
+
command: fe
|
|
13
|
+
|
|
14
|
+
projects:
|
|
15
|
+
root:
|
|
16
|
+
path: ~/code/design-system
|
|
17
|
+
color: cyan
|
|
18
|
+
|
|
19
|
+
defaults:
|
|
20
|
+
restart: on-failure
|
|
21
|
+
backoff: { initialMs: 1000, maxMs: 10000, factor: 2, maxRestarts: 5 }
|
|
22
|
+
stop: { graceMs: 3000 }
|
|
23
|
+
|
|
24
|
+
services:
|
|
25
|
+
# Types first: everything else consumes the built declarations.
|
|
26
|
+
- name: types
|
|
27
|
+
project: root
|
|
28
|
+
cwd: packages/core # relative to the project path
|
|
29
|
+
cmd: ["pnpm", "exec", "tsc", "--watch", "--preserveWatchOutput"]
|
|
30
|
+
groups: [build]
|
|
31
|
+
ready: { logMatch: "Found 0 errors|Watching for file changes", timeoutMs: 120000 }
|
|
32
|
+
|
|
33
|
+
- name: tailwind
|
|
34
|
+
project: root
|
|
35
|
+
cwd: packages/ui
|
|
36
|
+
cmd: ["pnpm", "exec", "tailwindcss", "-i", "./src/index.css", "-o", "./dist/index.css", "--watch"]
|
|
37
|
+
groups: [build]
|
|
38
|
+
ready: { logMatch: "Done in|Rebuilding", timeoutMs: 60000 }
|
|
39
|
+
|
|
40
|
+
- name: storybook
|
|
41
|
+
project: root
|
|
42
|
+
cwd: packages/ui
|
|
43
|
+
cmd: ["pnpm", "run", "storybook"]
|
|
44
|
+
groups: [docs]
|
|
45
|
+
needs: [types, tailwind]
|
|
46
|
+
ready: { logMatch: "Storybook.*started|Local:", timeoutMs: 180000 }
|
|
47
|
+
url: http://127.0.0.1:6006
|
|
48
|
+
|
|
49
|
+
- name: docs
|
|
50
|
+
project: root
|
|
51
|
+
cwd: apps/docs
|
|
52
|
+
cmd: ["pnpm", "run", "dev"] # Astro, Docusaurus, Next — whatever you use
|
|
53
|
+
groups: [docs]
|
|
54
|
+
needs: [types]
|
|
55
|
+
ready: { logMatch: "Local:|ready in", timeoutMs: 60000 }
|
|
56
|
+
url: http://127.0.0.1:4321
|
|
57
|
+
|
|
58
|
+
# Noisy on every keystroke — start it when you are actually fixing tests.
|
|
59
|
+
- name: tests
|
|
60
|
+
project: root
|
|
61
|
+
cmd: ["pnpm", "exec", "vitest", "--watch"]
|
|
62
|
+
groups: [tooling]
|
|
63
|
+
autostart: false
|
|
64
|
+
restart: never
|
|
65
|
+
|
|
66
|
+
# One-shot: select it, press `s`, watch it finish, carry on.
|
|
67
|
+
- name: lint
|
|
68
|
+
project: root
|
|
69
|
+
cmd: ["pnpm", "run", "lint:fix"]
|
|
70
|
+
groups: [tooling]
|
|
71
|
+
autostart: false
|
|
72
|
+
restart: never
|
|
73
|
+
|
|
74
|
+
profiles:
|
|
75
|
+
build-only: { only: [build] }
|
|
76
|
+
no-storybook: { except: [storybook] } # it is the slowest thing here
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Two interconnected Laravel apps: queues, Redis stream listeners, schedulers, Vite.
|
|
2
|
+
#
|
|
3
|
+
# Demonstrates: artisan-aware graceful stop, readiness gates that order the boot,
|
|
4
|
+
# autostart:false for commands you run occasionally, groups and profiles.
|
|
5
|
+
#
|
|
6
|
+
# Setup: edit the two paths below, then
|
|
7
|
+
# cp -r laravel-dual ~/.laracrew/stacks/
|
|
8
|
+
# laracrew doctor laravel-dual && laracrew up laravel-dual
|
|
9
|
+
name: laravel-dual
|
|
10
|
+
description: API + Portal with queues, streams and schedulers
|
|
11
|
+
command: dual
|
|
12
|
+
|
|
13
|
+
# Projects are declared inline here so the example is self-contained. Move them to
|
|
14
|
+
# ~/.laracrew/projects.yaml and use `use: [api, portal]` once you have several stacks.
|
|
15
|
+
projects:
|
|
16
|
+
api:
|
|
17
|
+
path: D:/work/api
|
|
18
|
+
php: php # or an absolute path to a specific build
|
|
19
|
+
envFile: .env # read for QUEUE_CONNECTION, REDIS_*, APP_URL
|
|
20
|
+
color: cyan
|
|
21
|
+
portal:
|
|
22
|
+
path: D:/work/portal
|
|
23
|
+
php: php
|
|
24
|
+
color: magenta
|
|
25
|
+
|
|
26
|
+
defaults:
|
|
27
|
+
restart: on-failure
|
|
28
|
+
backoff: { initialMs: 2000, maxMs: 30000, factor: 2, maxRestarts: 10 }
|
|
29
|
+
# Workers finish the job they are holding before laracrew terminates them.
|
|
30
|
+
stop: { artisan: "queue:restart", graceMs: 15000 }
|
|
31
|
+
|
|
32
|
+
services:
|
|
33
|
+
- name: redis
|
|
34
|
+
external: true # health-checked, never started or stopped by laracrew
|
|
35
|
+
ready: { tcp: "127.0.0.1:6379", timeoutMs: 10000 }
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------- api
|
|
38
|
+
- name: api:serve
|
|
39
|
+
project: api
|
|
40
|
+
cmd: php artisan serve --port=${port:8000}
|
|
41
|
+
groups: [http]
|
|
42
|
+
needs: [redis]
|
|
43
|
+
ready: { http: "http://127.0.0.1:8000/up", timeoutMs: 20000 }
|
|
44
|
+
url: http://127.0.0.1:8000
|
|
45
|
+
stop: { graceMs: 5000 } # a web server has no job to finish
|
|
46
|
+
|
|
47
|
+
- name: api:queue
|
|
48
|
+
project: api
|
|
49
|
+
cmd:
|
|
50
|
+
["php", "artisan", "queue:work", "redis", "--queue=high,default", "--tries=3", "--timeout=90"]
|
|
51
|
+
groups: [workers]
|
|
52
|
+
needs: [redis]
|
|
53
|
+
metrics: { queues: [high, default] }
|
|
54
|
+
|
|
55
|
+
- name: api:streams
|
|
56
|
+
project: api
|
|
57
|
+
cmd: ["php", "artisan", "redis-stream:run", "orders_sync"]
|
|
58
|
+
groups: [workers, streams]
|
|
59
|
+
needs: [redis]
|
|
60
|
+
metrics:
|
|
61
|
+
streams:
|
|
62
|
+
- { key: orders_sync, group: api-consumers }
|
|
63
|
+
|
|
64
|
+
- name: api:schedule
|
|
65
|
+
project: api
|
|
66
|
+
cmd: ["php", "artisan", "schedule:work"] # schedule:run is one tick; :work is the daemon
|
|
67
|
+
groups: [scheduler]
|
|
68
|
+
needs: [redis]
|
|
69
|
+
|
|
70
|
+
- name: api:vite
|
|
71
|
+
project: api
|
|
72
|
+
cmd: npm run dev
|
|
73
|
+
groups: [assets]
|
|
74
|
+
ready: { logMatch: "ready in|Local:", timeoutMs: 30000 }
|
|
75
|
+
stop: { graceMs: 3000 }
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------- portal
|
|
78
|
+
# Nothing here starts until the API is actually answering HTTP.
|
|
79
|
+
- name: portal:serve
|
|
80
|
+
project: portal
|
|
81
|
+
cmd: php artisan serve --port=${port:8001}
|
|
82
|
+
groups: [http]
|
|
83
|
+
needs: [redis, api:serve]
|
|
84
|
+
ready: { http: "http://127.0.0.1:8001/up", timeoutMs: 20000 }
|
|
85
|
+
url: http://127.0.0.1:8001
|
|
86
|
+
stop: { graceMs: 5000 }
|
|
87
|
+
|
|
88
|
+
- name: portal:queue
|
|
89
|
+
project: portal
|
|
90
|
+
cmd: ["php", "artisan", "queue:work", "redis", "--tries=3"]
|
|
91
|
+
groups: [workers]
|
|
92
|
+
needs: [redis, api:serve]
|
|
93
|
+
metrics: { queues: [default] }
|
|
94
|
+
|
|
95
|
+
# Defined but not launched — select it and press `s` when you need a backfill.
|
|
96
|
+
- name: portal:backfill
|
|
97
|
+
project: portal
|
|
98
|
+
cmd: ["php", "artisan", "portal:backfill-orders"]
|
|
99
|
+
groups: [maintenance]
|
|
100
|
+
needs: [redis]
|
|
101
|
+
autostart: false
|
|
102
|
+
restart: never
|
|
103
|
+
|
|
104
|
+
profiles:
|
|
105
|
+
light: { except: [assets] } # skip Vite when you are only touching the backend
|
|
106
|
+
workers-only: { only: [workers, redis] }
|
|
107
|
+
api-only: { except: [portal:serve, portal:queue, portal:backfill] }
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# A TypeScript API, a Vite frontend, a BullMQ worker and a database — no PHP anywhere.
|
|
2
|
+
#
|
|
3
|
+
# Demonstrates: logMatch readiness for tools that announce themselves, per-service env,
|
|
4
|
+
# a build step that must finish before anything else starts, argv-form
|
|
5
|
+
# commands so paths with spaces cannot mis-split.
|
|
6
|
+
#
|
|
7
|
+
# Setup: edit the paths, then
|
|
8
|
+
# cp -r node-api-and-web ~/.laracrew/stacks/
|
|
9
|
+
# laracrew up node-api-and-web
|
|
10
|
+
name: node-api-and-web
|
|
11
|
+
description: Node API + Vite web + BullMQ worker
|
|
12
|
+
command: web
|
|
13
|
+
|
|
14
|
+
projects:
|
|
15
|
+
api:
|
|
16
|
+
path: ~/code/shop/api
|
|
17
|
+
color: cyan
|
|
18
|
+
web:
|
|
19
|
+
path: ~/code/shop/web
|
|
20
|
+
color: magenta
|
|
21
|
+
shared:
|
|
22
|
+
path: ~/code/shop/packages/types
|
|
23
|
+
color: green
|
|
24
|
+
|
|
25
|
+
defaults:
|
|
26
|
+
restart: on-failure
|
|
27
|
+
backoff: { initialMs: 1000, maxMs: 15000, factor: 2, maxRestarts: 10 }
|
|
28
|
+
stop: { graceMs: 5000 }
|
|
29
|
+
|
|
30
|
+
services:
|
|
31
|
+
# Infrastructure this stack expects to already be running.
|
|
32
|
+
- name: postgres
|
|
33
|
+
external: true
|
|
34
|
+
ready: { tcp: "127.0.0.1:5432", timeoutMs: 10000 }
|
|
35
|
+
|
|
36
|
+
- name: redis
|
|
37
|
+
external: true
|
|
38
|
+
ready: { tcp: "127.0.0.1:6379", timeoutMs: 10000 }
|
|
39
|
+
|
|
40
|
+
# A one-shot build. `restart: never` means it is finished, not crashed, when it exits 0.
|
|
41
|
+
# Everything that imports the shared types waits for it.
|
|
42
|
+
- name: types:build
|
|
43
|
+
project: shared
|
|
44
|
+
cmd: ["npm", "run", "build"]
|
|
45
|
+
groups: [build]
|
|
46
|
+
restart: never
|
|
47
|
+
ready: { logMatch: "build (complete|succeeded)|Done in", timeoutMs: 120000 }
|
|
48
|
+
|
|
49
|
+
- name: api
|
|
50
|
+
project: api
|
|
51
|
+
cmd: ["npm", "run", "dev"] # e.g. tsx watch src/server.ts
|
|
52
|
+
groups: [backend]
|
|
53
|
+
needs: [postgres, redis, types:build]
|
|
54
|
+
env:
|
|
55
|
+
NODE_ENV: development
|
|
56
|
+
PORT: "3000"
|
|
57
|
+
# Pull a secret from your own shell rather than committing it.
|
|
58
|
+
STRIPE_KEY: ${env:STRIPE_TEST_KEY:sk_test_placeholder}
|
|
59
|
+
ready: { http: "http://127.0.0.1:3000/health", timeoutMs: 30000 }
|
|
60
|
+
url: http://127.0.0.1:3000
|
|
61
|
+
|
|
62
|
+
- name: api:worker
|
|
63
|
+
project: api
|
|
64
|
+
cmd: ["npm", "run", "worker"] # BullMQ / Bee-Queue consumer
|
|
65
|
+
groups: [backend, workers]
|
|
66
|
+
needs: [redis, types:build]
|
|
67
|
+
# Give an in-flight job a moment to settle before the process is taken down.
|
|
68
|
+
stop: { graceMs: 10000 }
|
|
69
|
+
|
|
70
|
+
- name: web
|
|
71
|
+
project: web
|
|
72
|
+
cmd: ["npm", "run", "dev"]
|
|
73
|
+
groups: [frontend]
|
|
74
|
+
needs: [api] # don't boot the UI against an API that isn't answering
|
|
75
|
+
env:
|
|
76
|
+
VITE_API_URL: http://127.0.0.1:3000
|
|
77
|
+
ready: { logMatch: "Local:\\s+http", timeoutMs: 30000 }
|
|
78
|
+
url: http://127.0.0.1:5173
|
|
79
|
+
stop: { graceMs: 3000 }
|
|
80
|
+
|
|
81
|
+
# Handy, but noisy — start it when you actually want it.
|
|
82
|
+
- name: web:test
|
|
83
|
+
project: web
|
|
84
|
+
cmd: ["npm", "run", "test:watch"]
|
|
85
|
+
groups: [tooling]
|
|
86
|
+
autostart: false
|
|
87
|
+
restart: never
|
|
88
|
+
|
|
89
|
+
- name: api:studio
|
|
90
|
+
project: api
|
|
91
|
+
cmd: ["npm", "run", "db:studio"] # Prisma Studio, Drizzle Studio, etc.
|
|
92
|
+
groups: [tooling]
|
|
93
|
+
needs: [postgres]
|
|
94
|
+
autostart: false
|
|
95
|
+
url: http://127.0.0.1:5555
|
|
96
|
+
|
|
97
|
+
profiles:
|
|
98
|
+
backend-only: { only: [backend, postgres, redis, types:build] }
|
|
99
|
+
frontend-only: { only: [frontend, types:build] }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Four services in four languages, plus their infrastructure in Docker.
|
|
2
|
+
#
|
|
3
|
+
# Demonstrates: laracrew managing `docker compose` itself as a supervised process,
|
|
4
|
+
# a strict boot order enforced by HTTP and TCP gates rather than sleeps,
|
|
5
|
+
# and a gateway that refuses to start before its dependencies answer.
|
|
6
|
+
#
|
|
7
|
+
# This is the case laracrew is really for: the ordering is the hard part, not the running.
|
|
8
|
+
#
|
|
9
|
+
# Setup: edit the paths, then
|
|
10
|
+
# cp -r polyglot-microservices ~/.laracrew/stacks/
|
|
11
|
+
# laracrew up polyglot-microservices
|
|
12
|
+
name: polyglot-microservices
|
|
13
|
+
description: Go gateway, Rust search, Node orders, Python pricing
|
|
14
|
+
command: micro
|
|
15
|
+
|
|
16
|
+
projects:
|
|
17
|
+
infra:
|
|
18
|
+
path: ~/code/platform/infra # holds docker-compose.yml
|
|
19
|
+
color: yellow
|
|
20
|
+
gateway:
|
|
21
|
+
path: ~/code/platform/gateway # Go
|
|
22
|
+
color: cyan
|
|
23
|
+
search:
|
|
24
|
+
path: ~/code/platform/search # Rust
|
|
25
|
+
color: red
|
|
26
|
+
orders:
|
|
27
|
+
path: ~/code/platform/orders # Node
|
|
28
|
+
color: magenta
|
|
29
|
+
pricing:
|
|
30
|
+
path: ~/code/platform/pricing # Python
|
|
31
|
+
color: green
|
|
32
|
+
|
|
33
|
+
defaults:
|
|
34
|
+
restart: on-failure
|
|
35
|
+
backoff: { initialMs: 2000, maxMs: 30000, factor: 2, maxRestarts: 8 }
|
|
36
|
+
stop: { graceMs: 10000 }
|
|
37
|
+
|
|
38
|
+
services:
|
|
39
|
+
# Docker Compose is just another long-running process. laracrew starts it, streams its
|
|
40
|
+
# output, and stops it with the rest of the stack — no separate terminal, no forgetting.
|
|
41
|
+
- name: infra
|
|
42
|
+
project: infra
|
|
43
|
+
cmd: ["docker", "compose", "up"]
|
|
44
|
+
groups: [infra]
|
|
45
|
+
# Ready when every port we depend on is actually accepting connections.
|
|
46
|
+
ready: { tcp: "127.0.0.1:5432", timeoutMs: 120000, intervalMs: 1000 }
|
|
47
|
+
# Compose needs time to stop its own containers cleanly.
|
|
48
|
+
stop: { graceMs: 30000 }
|
|
49
|
+
|
|
50
|
+
- name: kafka-wait
|
|
51
|
+
external: true # nothing to start; this row exists purely as a gate
|
|
52
|
+
needs: [infra]
|
|
53
|
+
ready: { tcp: "127.0.0.1:9092", timeoutMs: 120000, intervalMs: 1000 }
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------- services
|
|
56
|
+
- name: pricing
|
|
57
|
+
project: pricing
|
|
58
|
+
cmd: ["uvicorn", "app.main:app", "--reload", "--port", "${port:8001}"]
|
|
59
|
+
groups: [services]
|
|
60
|
+
needs: [infra]
|
|
61
|
+
env:
|
|
62
|
+
PYTHONUNBUFFERED: "1"
|
|
63
|
+
ready: { http: "http://127.0.0.1:8001/health", timeoutMs: 60000 }
|
|
64
|
+
url: http://127.0.0.1:8001
|
|
65
|
+
|
|
66
|
+
- name: search
|
|
67
|
+
project: search
|
|
68
|
+
cmd: ["cargo", "watch", "-x", "run"]
|
|
69
|
+
groups: [services]
|
|
70
|
+
needs: [infra]
|
|
71
|
+
env:
|
|
72
|
+
RUST_LOG: info
|
|
73
|
+
# A cold cargo build is slow; give the gate room rather than failing the boot.
|
|
74
|
+
ready: { http: "http://127.0.0.1:8002/health", timeoutMs: 300000, intervalMs: 2000 }
|
|
75
|
+
url: http://127.0.0.1:8002
|
|
76
|
+
|
|
77
|
+
- name: orders
|
|
78
|
+
project: orders
|
|
79
|
+
cmd: ["npm", "run", "dev"]
|
|
80
|
+
groups: [services]
|
|
81
|
+
needs: [infra, kafka-wait]
|
|
82
|
+
env:
|
|
83
|
+
NODE_ENV: development
|
|
84
|
+
KAFKA_BROKERS: 127.0.0.1:9092
|
|
85
|
+
ready: { http: "http://127.0.0.1:8003/health", timeoutMs: 60000 }
|
|
86
|
+
url: http://127.0.0.1:8003
|
|
87
|
+
|
|
88
|
+
# The gateway proxies all three, so it starts last — by declaration, not by sleeping.
|
|
89
|
+
- name: gateway
|
|
90
|
+
project: gateway
|
|
91
|
+
cmd: ["go", "run", "./cmd/gateway"]
|
|
92
|
+
groups: [edge]
|
|
93
|
+
needs: [pricing, search, orders]
|
|
94
|
+
env:
|
|
95
|
+
PRICING_URL: http://127.0.0.1:8001
|
|
96
|
+
SEARCH_URL: http://127.0.0.1:8002
|
|
97
|
+
ORDERS_URL: http://127.0.0.1:8003
|
|
98
|
+
ready: { http: "http://127.0.0.1:8080/health", timeoutMs: 60000 }
|
|
99
|
+
url: http://127.0.0.1:8080
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------- on demand
|
|
102
|
+
- name: orders:consumer
|
|
103
|
+
project: orders
|
|
104
|
+
cmd: ["npm", "run", "consumer"]
|
|
105
|
+
groups: [workers]
|
|
106
|
+
needs: [kafka-wait]
|
|
107
|
+
autostart: false
|
|
108
|
+
|
|
109
|
+
- name: seed
|
|
110
|
+
project: infra
|
|
111
|
+
cmd: ["./scripts/seed.sh"]
|
|
112
|
+
groups: [maintenance]
|
|
113
|
+
needs: [infra]
|
|
114
|
+
autostart: false
|
|
115
|
+
restart: never
|
|
116
|
+
|
|
117
|
+
profiles:
|
|
118
|
+
# Work on the gateway against services you are not editing.
|
|
119
|
+
edge-only: { only: [edge, infra, kafka-wait] }
|
|
120
|
+
no-search: { except: [search] } # skip the slow Rust rebuild
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "laracrew",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Boot and supervise all the long-running processes of your Laravel projects with one command.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"laravel",
|
|
7
|
+
"cli",
|
|
8
|
+
"tui",
|
|
9
|
+
"queue",
|
|
10
|
+
"horizon",
|
|
11
|
+
"redis",
|
|
12
|
+
"process-manager",
|
|
13
|
+
"dev-tools"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"laracrew": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.js",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"examples",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsx src/index.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
40
|
+
"prepack": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^12.1.0",
|
|
44
|
+
"yaml": "^2.5.1",
|
|
45
|
+
"zod": "^3.23.8"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.7.4",
|
|
49
|
+
"tsup": "^8.3.0",
|
|
50
|
+
"tsx": "^4.19.1",
|
|
51
|
+
"typescript": "^5.6.2",
|
|
52
|
+
"vitest": "^2.1.1"
|
|
53
|
+
},
|
|
54
|
+
"author": "viduranga <https://github.com/vidux>"
|
|
55
|
+
}
|