Skip to content

Commit b573f13

Browse files
committed
feat(cli): improve status and doctor UI
- Tree view grouped by project with ANSI colors - Green dot indicator per service - Header with version, project/service count - Column titles (SERVICE, ENDPOINT, WINDOWS) - --verbose flag to show Windows IP column - Doctor uses colored checkmarks - Better empty state message - Version injected via ldflags in release build
1 parent a1cb3db commit b573f13

2 files changed

Lines changed: 84 additions & 18 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ jobs:
1818
go-version-file: go.mod
1919

2020
- name: Build
21-
run: CGO_ENABLED=0 go build -ldflags="-s -w" -o devproxy ./cmd/devproxy
21+
env:
22+
TAG: ${{ github.ref_name }}
23+
run: CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$TAG" -o devproxy ./cmd/devproxy
2224

2325
- name: Create release
2426
env:

cmd/devproxy/main.go

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ import (
2222
"github.com/miekg/dns"
2323
)
2424

25+
var version = "v1.0.0"
26+
27+
const (
28+
colorReset = "\033[0m"
29+
colorBold = "\033[1m"
30+
colorDim = "\033[2m"
31+
colorGreen = "\033[32m"
32+
colorRed = "\033[31m"
33+
colorCyan = "\033[36m"
34+
)
35+
2536
func main() {
2637
if len(os.Args) < 2 {
2738
fmt.Fprintln(os.Stderr, "usage: devproxy <command>")
@@ -104,6 +115,17 @@ func runDown() {
104115
}
105116

106117
func runStatus() {
118+
jsonFlag := false
119+
verboseFlag := false
120+
for _, arg := range os.Args[2:] {
121+
switch arg {
122+
case "--json":
123+
jsonFlag = true
124+
case "--verbose", "-v":
125+
verboseFlag = true
126+
}
127+
}
128+
107129
client := unixClient("/run/devproxy/devproxy.sock")
108130
resp, err := client.Get("http://devproxy/status")
109131
if err != nil {
@@ -112,7 +134,6 @@ func runStatus() {
112134
}
113135
defer resp.Body.Close()
114136

115-
jsonFlag := len(os.Args) > 2 && os.Args[2] == "--json"
116137
if jsonFlag {
117138
var raw json.RawMessage
118139
json.NewDecoder(resp.Body).Decode(&raw)
@@ -136,21 +157,63 @@ func runStatus() {
136157
json.NewDecoder(resp.Body).Decode(&result)
137158

138159
if len(result.Projects) == 0 {
139-
fmt.Println("No active projects")
160+
fmt.Println()
161+
fmt.Printf(" %sNo projects running.%s\n", colorDim, colorReset)
162+
fmt.Printf(" Start a docker-compose project to get started.\n")
163+
fmt.Println()
140164
return
141165
}
142166

143-
fmt.Printf("%-20s %-15s %-30s %-15s\n", "PROJECT", "SERVICE", "WSL2", "WINDOWS IP")
167+
totalServices := 0
144168
for _, p := range result.Projects {
145-
for _, port := range p.Ports {
146-
fmt.Printf("%-20s %-15s %-30s %-15s\n",
147-
p.Name,
148-
port.Service,
149-
fmt.Sprintf("%s.localhost:%d", p.Name, port.Container),
150-
fmt.Sprintf("%s:%d", p.WindowsIP, port.Container),
151-
)
169+
totalServices += len(p.Ports)
170+
}
171+
172+
fmt.Println()
173+
fmt.Printf(" %sdevproxy%s %s%s%s · %d projects · %d services\n",
174+
colorBold, colorReset, colorDim, version, colorReset,
175+
len(result.Projects), totalServices)
176+
fmt.Println()
177+
178+
if verboseFlag {
179+
fmt.Printf(" %s SERVICE ENDPOINT WINDOWS%s\n", colorDim, colorReset)
180+
} else {
181+
fmt.Printf(" %s SERVICE ENDPOINT%s\n", colorDim, colorReset)
182+
}
183+
184+
for i, p := range result.Projects {
185+
fmt.Printf(" %s%s%s %s· %s%s\n",
186+
colorBold, p.Name, colorReset,
187+
colorDim, p.IP, colorReset)
188+
189+
for j, port := range p.Ports {
190+
connector := "├─"
191+
if j == len(p.Ports)-1 {
192+
connector = "└─"
193+
}
194+
195+
endpoint := fmt.Sprintf("%s.localhost:%d", p.Name, port.Container)
196+
197+
if verboseFlag {
198+
winEndpoint := fmt.Sprintf("%s:%d", p.WindowsIP, port.Container)
199+
fmt.Printf(" %s %s●%s %-20s %s%-33s%s %s%s%s\n",
200+
connector, colorGreen, colorReset,
201+
port.Service,
202+
colorCyan, endpoint, colorReset,
203+
colorDim, winEndpoint, colorReset)
204+
} else {
205+
fmt.Printf(" %s %s●%s %-20s %s%s%s\n",
206+
connector, colorGreen, colorReset,
207+
port.Service,
208+
colorCyan, endpoint, colorReset)
209+
}
210+
}
211+
212+
if i < len(result.Projects)-1 {
213+
fmt.Println()
152214
}
153215
}
216+
fmt.Println()
154217
}
155218

156219
func runCleanup() {
@@ -255,13 +318,14 @@ func runDoctor() {
255318
allPassed := true
256319
dnsDirectOK := false
257320

321+
fmt.Println()
258322
for _, c := range checks {
259323
err := c.fn()
260324
if err != nil {
261-
fmt.Printf(" [FAIL] %s: %v\n", c.name, err)
325+
fmt.Printf(" %s✗%s %s: %v\n", colorRed, colorReset, c.name, err)
262326
allPassed = false
263327
} else {
264-
fmt.Printf(" [ OK ] %s\n", c.name)
328+
fmt.Printf(" %s✓%s %s\n", colorGreen, colorReset, c.name)
265329
if c.name == "DNS server (127.0.53.53)" {
266330
dnsDirectOK = true
267331
}
@@ -273,19 +337,19 @@ func runDoctor() {
273337
// Check if resolved was the one that failed
274338
if err := checkResolved(); err != nil {
275339
fmt.Println()
276-
fmt.Println(" Hint: DNS server is running but systemd-resolved cannot resolve")
277-
fmt.Println(" devproxy domains. Check that /etc/systemd/resolved.conf.d/")
278-
fmt.Println(" has the correct devproxy DNS delegation configuration.")
340+
fmt.Printf(" %sHint: DNS server is running but systemd-resolved cannot resolve%s\n", colorDim, colorReset)
341+
fmt.Printf(" %sdevproxy domains. Check /etc/systemd/resolved.conf.d/ config.%s\n", colorDim, colorReset)
279342
}
280343
}
281344

282345
fmt.Println()
283346
if allPassed {
284-
fmt.Println("All checks passed.")
347+
fmt.Printf(" %s%sAll checks passed.%s\n", colorBold, colorGreen, colorReset)
285348
} else {
286-
fmt.Println("Some checks failed. See above for details.")
349+
fmt.Printf(" %sSome checks failed. See above for details.%s\n", colorRed, colorReset)
287350
os.Exit(1)
288351
}
352+
fmt.Println()
289353
}
290354

291355
// getWSLIP returns the WSL2 eth0 IPv4 address.

0 commit comments

Comments
 (0)