Skip to content

Commit 71d80f1

Browse files
intel352claude
andcommitted
feat: wfctl wizard — Bubbletea TUI for interactive project setup
Task 5: Bubbletea TUI wizard (wizard.go + wizard_models.go) with 9 screens: project info → services → infrastructure → environments → deployment → secrets → CI/CD → review → write. Registered as `wfctl wizard`. Generates a complete app.yaml. Dependencies: charm.land/bubbletea/v2 + charm.land/lipgloss/v2. Task 6: docs/WFCTL.md updated with dev up/down/logs/status/restart, --local/--k8s/--expose, and wizard command reference. CHANGELOG.md updated. cmd/wfctl/dev_integration_test.go: 5 integration tests for generateDevCompose covering postgres/redis/nats, multi-service port mappings, and empty config fallback — all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d46c464 commit 71d80f1

File tree

9 files changed

+1276
-1
lines changed

9 files changed

+1276
-1
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **`wfctl dev`** (`cmd/wfctl/dev.go`, `dev_compose.go`, `dev_process.go`, `dev_k8s.go`, `dev_expose.go`): local development cluster management. Subcommands: `up`, `down`, `logs`, `status`, `restart`. Three modes: docker-compose (default), process (`--local`, with hot-reload via fsnotify), and minikube (`--k8s`). Exposure integrations: Tailscale Funnel, Cloudflare Tunnel, ngrok (`--expose`). Auto-detects `environments.local.exposure.method` when `--expose` is omitted.
13+
- **`wfctl wizard`** (`cmd/wfctl/wizard.go`, `wizard_models.go`): interactive Bubbletea TUI wizard for project setup. Nine screens: project info → services → infrastructure → environments → deployment → secrets → CI/CD → review → write. Generates a complete `app.yaml` and optionally triggers `wfctl ci init`. Navigates with Enter/Esc/Tab/Space/arrows.
14+
15+
### Documentation
16+
17+
- `docs/WFCTL.md`: added `dev up/down/logs/status/restart` reference (flags, mode table, exposure methods), `wizard` reference (screen list, navigation keys).
18+
- `CHANGELOG.md`: entry for wfctl dev + wizard.
19+
20+
21+
1222
- **`services:` config section** (`config/services_config.go`): new top-level YAML key for multi-service applications. Each service declares a binary path, scaling policy (replicas/min/max/metric/target), exposed ports, per-service modules/pipelines, and plugins.
1323
- **`mesh:` config section** (`config/services_config.go`): inter-service communication config. Declares transport (nats/http/grpc), service discovery, NATS connection details, and explicit service-to-service routes with via/subject/endpoint.
1424
- **`networking:` config section** (`config/networking_config.go`): network exposure and policy config. Declares ingress entries (service, port, TLS termination), inter-service network policies, and DNS records.

cmd/wfctl/dev_integration_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/GoCodeAlone/workflow/config"
8+
)
9+
10+
// TestDevIntegration_GenerateDevCompose verifies that generateDevCompose
11+
// produces a valid docker-compose YAML from a fixture config.
12+
func TestDevIntegration_GenerateDevCompose(t *testing.T) {
13+
t.Run("single service with postgres", func(t *testing.T) {
14+
cfg := &config.WorkflowConfig{
15+
Modules: []config.ModuleConfig{
16+
{Name: "db", Type: "database.postgres"},
17+
{Name: "server", Type: "http.server", Config: map[string]any{"port": 8080}},
18+
},
19+
}
20+
21+
out, err := generateDevCompose(cfg)
22+
if err != nil {
23+
t.Fatalf("generateDevCompose returned error: %v", err)
24+
}
25+
26+
// Must contain the postgres service.
27+
if !strings.Contains(out, "postgres:16") {
28+
t.Errorf("expected postgres:16 image in output, got:\n%s", out)
29+
}
30+
31+
// Must contain the default app service.
32+
if !strings.Contains(out, "app:") || (!strings.Contains(out, "app:dev") && !strings.Contains(out, "build:")) {
33+
// either image: app:dev or build: context is acceptable
34+
if !strings.Contains(out, "app") {
35+
t.Errorf("expected app service in output, got:\n%s", out)
36+
}
37+
}
38+
39+
// Must contain volume for postgres persistence.
40+
if !strings.Contains(out, "pgdata") {
41+
t.Errorf("expected pgdata volume in output, got:\n%s", out)
42+
}
43+
44+
// Must declare services: section.
45+
if !strings.Contains(out, "services:") {
46+
t.Errorf("expected 'services:' key in output, got:\n%s", out)
47+
}
48+
})
49+
50+
t.Run("multi-service with redis and nats", func(t *testing.T) {
51+
cfg := &config.WorkflowConfig{
52+
Modules: []config.ModuleConfig{
53+
{Name: "cache", Type: "cache.redis"},
54+
{Name: "mq", Type: "messaging.nats"},
55+
},
56+
Services: map[string]*config.ServiceConfig{
57+
"api": {
58+
Binary: "./cmd/api",
59+
Expose: []config.ExposeConfig{{Port: 8080, Protocol: "http"}},
60+
},
61+
"worker": {
62+
Binary: "./cmd/worker",
63+
},
64+
},
65+
}
66+
67+
out, err := generateDevCompose(cfg)
68+
if err != nil {
69+
t.Fatalf("generateDevCompose returned error: %v", err)
70+
}
71+
72+
// Infrastructure images must be present.
73+
if !strings.Contains(out, "redis:7-alpine") {
74+
t.Errorf("expected redis:7-alpine in output, got:\n%s", out)
75+
}
76+
if !strings.Contains(out, "nats:latest") {
77+
t.Errorf("expected nats:latest in output, got:\n%s", out)
78+
}
79+
80+
// Both services must appear.
81+
if !strings.Contains(out, "api:") {
82+
t.Errorf("expected 'api' service in output, got:\n%s", out)
83+
}
84+
if !strings.Contains(out, "worker:") {
85+
t.Errorf("expected 'worker' service in output, got:\n%s", out)
86+
}
87+
})
88+
89+
t.Run("port mappings match config", func(t *testing.T) {
90+
cfg := &config.WorkflowConfig{
91+
Modules: []config.ModuleConfig{
92+
{Name: "db", Type: "database.postgres"},
93+
},
94+
Services: map[string]*config.ServiceConfig{
95+
"api": {
96+
Expose: []config.ExposeConfig{
97+
{Port: 9090, Protocol: "http"},
98+
{Port: 9091, Protocol: "grpc"},
99+
},
100+
},
101+
},
102+
}
103+
104+
out, err := generateDevCompose(cfg)
105+
if err != nil {
106+
t.Fatalf("generateDevCompose returned error: %v", err)
107+
}
108+
109+
// Both exposed ports must appear as port mappings.
110+
if !strings.Contains(out, "9090:9090") {
111+
t.Errorf("expected port mapping 9090:9090 in output, got:\n%s", out)
112+
}
113+
if !strings.Contains(out, "9091:9091") {
114+
t.Errorf("expected port mapping 9091:9091 in output, got:\n%s", out)
115+
}
116+
})
117+
118+
t.Run("empty config generates minimal app service", func(t *testing.T) {
119+
cfg := &config.WorkflowConfig{
120+
Modules: []config.ModuleConfig{},
121+
}
122+
123+
out, err := generateDevCompose(cfg)
124+
if err != nil {
125+
t.Fatalf("generateDevCompose returned error: %v", err)
126+
}
127+
128+
// At minimum there should be an 'app' service.
129+
if !strings.Contains(out, "app") {
130+
t.Errorf("expected 'app' service in minimal output, got:\n%s", out)
131+
}
132+
133+
// There should be no postgres, redis, or nats since no modules declared.
134+
if strings.Contains(out, "postgres:") {
135+
t.Errorf("unexpected postgres image in minimal output, got:\n%s", out)
136+
}
137+
})
138+
139+
t.Run("http server port detected for app service", func(t *testing.T) {
140+
cfg := &config.WorkflowConfig{
141+
Modules: []config.ModuleConfig{
142+
{Name: "server", Type: "http.server", Config: map[string]any{"port": 3000}},
143+
},
144+
}
145+
146+
out, err := generateDevCompose(cfg)
147+
if err != nil {
148+
t.Fatalf("generateDevCompose returned error: %v", err)
149+
}
150+
151+
// Port 3000 should be mapped for the app service.
152+
if !strings.Contains(out, "3000:3000") {
153+
t.Errorf("expected port mapping 3000:3000 in output, got:\n%s", out)
154+
}
155+
})
156+
}

cmd/wfctl/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ var commands = map[string]func([]string) error{
8888
"secrets": runSecrets,
8989
"ports": runPorts,
9090
"security": runSecurity,
91+
"wizard": runWizard,
92+
"dev": runDev,
9193
}
9294

9395
func main() {

cmd/wfctl/wfctl.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ workflows:
6969
description: "Inspect port usage declared in a workflow config (list)"
7070
- name: security
7171
description: "Security tooling (audit: scan config for issues, generate-network-policies: output k8s NetworkPolicy YAML)"
72+
- name: dev
73+
description: "Local development cluster (up, down, logs, status, restart; --local, --k8s, --expose)"
74+
- name: wizard
75+
description: "Interactive TUI wizard for project setup (generates app.yaml)"
7276

7377
# Each command is expressed as a workflow pipeline triggered by the CLI.
7478
# The pipeline delegates to the registered Go implementation via step.cli_invoke,
@@ -438,3 +442,25 @@ pipelines:
438442
type: step.cli_invoke
439443
config:
440444
command: security
445+
446+
cmd-dev:
447+
trigger:
448+
type: cli
449+
config:
450+
command: dev
451+
steps:
452+
- name: run
453+
type: step.cli_invoke
454+
config:
455+
command: dev
456+
457+
cmd-wizard:
458+
trigger:
459+
type: cli
460+
config:
461+
command: wizard
462+
steps:
463+
- name: run
464+
type: step.cli_invoke
465+
config:
466+
command: wizard

0 commit comments

Comments
 (0)