Skip to content

Commit cd49b4a

Browse files
committed
Updated New Service Generator feature to build and
deploy service and database with a single command same as existing service. Added regression tests.
1 parent d023af9 commit cd49b4a

30 files changed

Lines changed: 307 additions & 51 deletions

‎.gitignore‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@ desktop.ini
2727
logs/
2828

2929
# ===== Claude =====
30-
.claude/
30+
.claude/
31+
32+
# ===== tmp =====
33+
tmp/

‎CLAUDE.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
`LocalDevelopmentStack` is a Kotlin CLI tool that scaffolds a local development environment. It has two modes:
88

9-
1. **New service scaffold** — given a service type and database, generates a complete runnable service + `docker-compose.yml` from scratch.
9+
1. **New service scaffold** — given a service type and database, generates a complete runnable service from a template, plus `Dockerfile.dev` and `docker-compose.yml`. The whole stack (DB + service) comes up with one `docker-compose up --build`; hot-reload is enabled.
1010
2. **Existing service scaffold** (`--existing-dir`) — auto-detects the language in an existing directory, then generates `Dockerfile.dev` + `docker-compose.yml` so the full local stack (service + database) runs with `docker-compose up --build`. Hot-reload is enabled; source changes are picked up automatically without a rebuild.
1111

1212
Both modes support optional database-migration scaffolding via `--migration <tool>`, which adds an opt-in one-shot `migrate:` service to the generated compose file plus an example migration in the appropriate format for the tool.
@@ -167,7 +167,7 @@ All database services are named `db:` in the compose file so connection URLs use
167167

168168
| `--service` | Implementation | Hot-reload tool |
169169
| ------------ | -------------------------------- | ---------------------------------- |
170-
| `springboot` | `SpringBootDockerfileGenerator` | `./gradlew bootRun` |
170+
| `springboot` | `SpringBootDockerfileGenerator` | `gradle bootRun` |
171171
| `go` | `GoDockerfileGenerator` | `air` (cosmtrek/air) |
172172
| `python` | `PythonDockerfileGenerator` | `uvicorn --reload` |
173173
| `node` | `NodeDockerfileGenerator` | `nodemon` |
@@ -238,13 +238,16 @@ Multiple distinct types → `DetectionException` with explicit `--service` overr
238238

239239
### Generated output structure
240240

241+
Both modes produce a stack that comes up with a single `docker-compose up --build` — DB + service container with hot-reload via volume-mounted source. The only difference is whether the source is generated from a template (new) or pre-existing (existing-dir). New-scaffold mode passes a `ServiceComposeConfig` with `buildContext = "./service"` so the compose `build:` block points at the subdirectory; existing-dir keeps the default `buildContext = "."`.
242+
241243
**New service scaffold:**
242244
```
243245
<output>/
244246
├── service/
245-
│ └── <language-specific source files>
246-
│ └── GET /health → {"status":"ok"}
247-
└── docker-compose.yml # database only; service runs directly on host
247+
│ ├── <language-specific source files>
248+
│ │ └── GET /health → {"status":"ok"}
249+
│ └── Dockerfile.dev # single-stage, hot-reload, no COPY . .
250+
└── docker-compose.yml # database + your service container (volume-mounted source)
248251
```
249252

250253
**Existing service scaffold:**

‎README.md‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
Instantly scaffold a containerised local development environment for any service and database — with a single command.
44

5-
Two modes:
5+
Two modes — both produce a stack that comes up with a single `docker-compose up --build`:
66

7-
- **New service** — generates a complete runnable service + `docker-compose.yml` from scratch.
7+
- **New service** — generates a complete runnable service + `Dockerfile.dev` + `docker-compose.yml` from scratch. Hot-reload enabled.
88
- **Existing service** — wraps your existing code with a `Dockerfile.dev` + `docker-compose.yml`. Hot-reload is enabled; source changes are picked up automatically inside the container.
99

1010
Optional **database migrations** — pass `--migration <tool>` (Flyway, Liquibase, migrate-mongo, golang-migrate) to scaffold an example migration plus a one-shot `migrate:` service that runs on demand via `docker-compose run --rm migrate`.
@@ -45,35 +45,43 @@ irm https://raw.githubusercontent.com/krishgok/localdevstack/main/scripts/instal
4545

4646
## Usage: Scaffold a new service
4747

48-
Generates source code + `docker-compose.yml` from scratch.
48+
Generates source code + `Dockerfile.dev` + `docker-compose.yml` from scratch. Hot-reload is enabled — edit source files and the watcher inside the container recompiles/restarts your service automatically.
4949

5050
**Step 1 — Generate the stack:**
5151

5252
```bash
5353
localdevstack --service go --database postgres --output ./my-project --name my-api
5454
```
5555

56-
**Step 2 — Start the database:**
56+
This produces:
5757

58-
```bash
59-
cd my-project
60-
docker-compose up -d
58+
```
59+
my-project/
60+
├── service/
61+
│ ├── <generated source files>
62+
│ └── Dockerfile.dev ← dev container for your service (hot-reload enabled)
63+
└── docker-compose.yml ← your service + the database, fully wired
6164
```
6265

63-
**Step 3 — Start the generated service:**
66+
**Step 2 — Start the full local stack:**
6467

6568
```bash
66-
cd service && go run ./...
69+
cd my-project
70+
docker-compose up --build
6771
```
6872

69-
**Step 4 — Verify:**
73+
This builds the dev image on first run and starts both the database and your service. Subsequent `docker-compose up` calls skip the build.
74+
75+
**Step 3 — Verify:**
7076

7177
```bash
7278
curl http://localhost:8080/health
7379
# → {"status":"ok"}
7480
```
7581

76-
> The service runs directly on your host in this mode. Replace `go run ./...` with the appropriate command for your language (see [Supported service types](#supported-service-types)).
82+
**Step 4 — Edit, save, and see changes reload automatically:**
83+
84+
Hot-reload works the same way as in [existing-service mode](#step-4--edit-save-and-see-changes-reload-automatically) — edit a source file under `service/`, the watcher inside the container picks it up. Only re-run `docker-compose up --build` if you change `Dockerfile.dev` itself or add/remove dependencies (e.g. `go.mod`, `package.json`, `requirements.txt`).
7785

7886
---
7987

@@ -298,9 +306,8 @@ Pass `--migration <tool>` to scaffold migration files plus a one-shot `migrate:`
298306
```bash
299307
localdevstack --service go --database postgres --migration flyway --output ./my-project --name my-api
300308
cd my-project
301-
docker-compose up -d # starts postgres only — migrate is skipped (compose profile)
302-
docker-compose run --rm migrate # applies all pending migrations
303-
cd service && go run ./... # start your service
309+
docker-compose up --build -d # service + db (migrate skipped — compose profile)
310+
docker-compose run --rm migrate # apply migrations on demand
304311
```
305312
306313
### Existing service with migrations

‎my-test-stack/docker-compose.yml‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# WARNING: these credentials are for LOCAL DEVELOPMENT ONLY.
2+
# Change POSTGRES_PASSWORD before committing or deploying to any shared environment.
3+
4+
services:
5+
db:
6+
image: postgres:16
7+
container_name: local-postgres
8+
environment:
9+
POSTGRES_DB: app_db
10+
POSTGRES_USER: postgres
11+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_dev_only}
12+
ports:
13+
- "5432:5432"
14+
volumes:
15+
- postgres_data:/var/lib/postgresql/data
16+
healthcheck:
17+
test: ["CMD-SHELL", "pg_isready -U postgres"]
18+
interval: 10s
19+
timeout: 5s
20+
retries: 5
21+
22+
my-test-api-service:
23+
build:
24+
context: ./service
25+
dockerfile: Dockerfile.dev
26+
container_name: local-my-test-api-service
27+
ports:
28+
- "8080:8080"
29+
environment:
30+
- DATABASE_URL=postgresql://postgres:postgres_dev_only@db:5432/app_db
31+
volumes:
32+
- ./service:/app
33+
- /app/build
34+
- /app/.gradle
35+
depends_on:
36+
db:
37+
condition: service_healthy
38+
volumes:
39+
postgres_data:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM gradle:8-jdk17-alpine
2+
WORKDIR /app
3+
COPY build.gradle.kts settings.gradle.kts ./
4+
RUN gradle dependencies --no-daemon -q 2>/dev/null || true
5+
EXPOSE 8080
6+
CMD ["gradle", "bootRun", "--no-daemon"]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
plugins {
2+
kotlin("jvm") version "1.9.22"
3+
kotlin("plugin.spring") version "1.9.22"
4+
id("org.springframework.boot") version "3.2.3"
5+
id("io.spring.dependency-management") version "1.1.4"
6+
}
7+
8+
group = "com.example"
9+
version = "0.0.1-SNAPSHOT"
10+
11+
repositories {
12+
mavenCentral()
13+
}
14+
15+
dependencies {
16+
implementation("org.springframework.boot:spring-boot-starter-web")
17+
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
18+
implementation("org.jetbrains.kotlin:kotlin-reflect")
19+
runtimeOnly("org.postgresql:postgresql")
20+
testImplementation("org.springframework.boot:spring-boot-starter-test")
21+
}
22+
23+
kotlin {
24+
jvmToolchain(17)
25+
}
26+
27+
tasks.withType<Test> {
28+
useJUnitPlatform()
29+
}

‎my-test-stack/service/go.mod‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module my-test-api-service
2+
3+
go 1.22
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package handler
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
)
7+
8+
type HealthHandler struct{}
9+
10+
func NewHealthHandler() *HealthHandler {
11+
return &HealthHandler{}
12+
}
13+
14+
func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
15+
w.Header().Set("Content-Type", "application/json")
16+
if err := json.NewEncoder(w).Encode(map[string]string{"status": "ok"}); err != nil {
17+
http.Error(w, "encoding response", http.StatusInternalServerError)
18+
}
19+
}

‎my-test-stack/service/main.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
7+
"my-test-api-service/handler"
8+
)
9+
10+
func main() {
11+
mux := http.NewServeMux()
12+
13+
healthHandler := handler.NewHealthHandler()
14+
mux.HandleFunc("/health", healthHandler.Health)
15+
16+
log.Println("Server starting on :8080")
17+
log.Fatal(http.ListenAndServe(":8080", mux))
18+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package service
2+
3+
type HealthService struct{}
4+
5+
func NewHealthService() *HealthService {
6+
return &HealthService{}
7+
}
8+
9+
func (s *HealthService) Status() string {
10+
return "ok"
11+
}

0 commit comments

Comments
 (0)