Skip to content

feat(dew-win): dew up --with <service> (podman-backed services)#39

Merged
linyiru merged 12 commits into
mainfrom
feat/dew-win-with-services
Jul 1, 2026
Merged

feat(dew-win): dew up --with <service> (podman-backed services)#39
linyiru merged 12 commits into
mainfrom
feat/dew-win-with-services

Conversation

@linyiru

@linyiru linyiru commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Add dew up --with <service> to the Windows/WSL2 wrapper — the headline
dev-services feature, matching macOS dew up --with.

dew up --with postgres,redis ./my-app
# dew: starting postgres (docker.io/library/postgres:16-alpine)...
# dew:   postgres ready -> postgresql://postgres:dew@127.0.0.1:5432/dew
# dew:   redis ready -> redis://127.0.0.1:6379
# ...dev server runs; services reachable on Windows localhost; Ctrl+C stops all

How it works

  • Reuses the platform-neutral internal/services Registry, so service
    definitions (image, port, env, connstring) are identical to macOS.
  • Each service runs as a rootful podman container with --network=host.
    Host networking is required: podman's netavark bridge conflicts with WSL2
    mirrored networking (the container dies). With host net + mirrored mode the
    service port lands directly on the Windows host's localhost — no port
    mapping needed.
  • Health-gated via services.ListenProbeCmd before printing the connstring.
  • podman is apk added into the distro on first --with use (the minimal
    Alpine rootfs has no runtime baked in yet).
  • Lifecycle: services live alongside the dev server (which holds the distro
    open). They're removed when the dev server exits, and a Ctrl+C handler stops
    them too (the dev server's os.Exit / a services-only blocking wait would
    otherwise skip cleanup). --with also works project-less (services only).

Testing (unit + integration + e2e)

  • Unit (main_test.go, no wsl/podman): parseUpArgs, splitCSV,
    serviceContainer, and podmanRunArgs (exact argv incl. --network=host,
    env-before-image, args-after-image ordering).
  • Integration / e2e (smoke-test.ps1, real WSL2): starts redis via
    dew up --with redis, asserts it reports ready, is reachable on Windows
    localhost:6379, the container is running, and — after the dev server
    self-exits — the container is removed.

Verified on real Windows 11 + WSL2

  • dew up --with redis <proj> → redis +PONG from Windows localhost, dev
    server serving, podman ps shows dew-svc-redis, cleanup on exit confirmed.
  • Full smoke test green (23 checks), unit tests green.

Notes / follow-ups

Add `dew up --with postgres,redis,...` to the Windows wrapper, reusing
the internal/services Registry so service definitions match macOS. Each
service runs as a rootful podman container on the distro's host network
(--network=host — required, since podman's netavark bridge conflicts
with WSL2 mirrored networking), is health-gated via ListenProbeCmd, and
its ConnString is printed. Services live alongside the dev server (which
holds the distro open) and are removed when it exits or on Ctrl+C.
podman is apk-installed into the distro on first --with use.

Tests: unit (parseUpArgs, splitCSV, serviceContainer, podmanRunArgs) and
a real-machine smoke check that starts redis, verifies it's reachable on
Windows localhost:6379, and confirms the container is cleaned up after
the dev server exits. Verified end-to-end on Windows 11 + WSL2.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Windows/WSL2 parity with macOS “dev services” by introducing dew up --with <service> in dew-win, starting selected services as podman containers on host networking and cleaning them up when the dev server exits (or on Ctrl+C).

Changes:

  • Implement dew up --with <csv> parsing and service lifecycle management (podman install, run, readiness probe, cleanup) in cmd/dew-win.
  • Add unit tests for new parsing / argv construction helpers.
  • Extend the Windows smoke test to validate redis service readiness, localhost reachability, and cleanup behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
cmd/dew-win/main.go Adds --with parsing, podman-backed service start/stop with readiness gating, and dev-server-exit cleanup.
cmd/dew-win/main_test.go Adds unit tests for argument parsing and podman argv construction.
cmd/dew-win/smoke-test.ps1 Adds an e2e WSL2 smoke test for dew up --with redis lifecycle and cleanup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/dew-win/main.go Outdated
Comment on lines +740 to +744
@@ -573,7 +741,7 @@
// when interpolating WSL paths into an inline `sh -c` string. The
// WSL path is alphanumeric + / + . in practice but a user's project
// dir could theoretically contain a single-quote in a parent dir
// name (rare), so escape per POSIX: '...'\''...' .
// name (rare), so escape per POSIX: '...'\...' .

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in bce3c74 — restored the literal POSIX escape '...'''...' (the curly quote had regressed); verified the committed blob is ASCII.

Comment thread cmd/dew-win/main.go
Comment on lines +455 to +475
func parseUpArgs(args []string) (dir string, with []string, err error) {
dir = "."
for i := 0; i < len(args); i++ {
a := args[i]
switch {
case a == "--with":
if i+1 >= len(args) {
return "", nil, fmt.Errorf("--with needs a comma-separated service list")
}
with = append(with, splitCSV(args[i+1])...)
i++
case strings.HasPrefix(a, "--with="):
with = append(with, splitCSV(strings.TrimPrefix(a, "--with="))...)
case strings.HasPrefix(a, "--"):
return "", nil, fmt.Errorf("unknown flag %q for dew up", a)
default:
dir = a
}
}
return dir, with, nil
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 9090b58 — parseUpArgs now rejects a second positional dir (e.g. 'dew up ./a ./b') instead of silently taking the last one.

Comment thread cmd/dew-win/main_test.go
Comment on lines +467 to +476
}{
{"empty", nil, ".", nil, false},
{"dir only", []string{"./app"}, "./app", nil, false},
{"with space form", []string{"--with", "postgres,redis"}, ".", []string{"postgres", "redis"}, false},
{"with equals form", []string{"--with=postgres"}, ".", []string{"postgres"}, false},
{"dir and with", []string{"./app", "--with", "redis"}, "./app", []string{"redis"}, false},
{"csv trims blanks", []string{"--with", "a, b ,,c"}, ".", []string{"a", "b", "c"}, false},
{"with needs arg", []string{"--with"}, "", nil, true},
{"unknown flag", []string{"--bogus"}, "", nil, true},
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 9090b58 — added the flag-then-dir ordering case ('--with redis ./app') and a multiple-dirs rejection case to TestParseUpArgs.

Comment thread cmd/dew-win/smoke-test.ps1 Outdated
Comment on lines +164 to +165
if (-not $svc.HasExited) { $svc.WaitForExit(25000) | Out-Null }
Start-Sleep -Seconds 2

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 93256dc — the --with smoke check now Kill()s the dev server if it overruns the wait, so a running dew up can't leak into later checks.

linyiru added 3 commits July 1, 2026 08:06
parseUpArgs overwrote dir on every positional arg, so `dew up ./a ./b`
silently took ./b. Reject a second positional as a user error. Add test
cases for the flag-then-dir ordering and the multiple-dir rejection.
The escape example regressed to a curly quote; restore the literal
'...'\''...' that shellQuote emits.
If the dev server doesn't self-exit within the wait, Kill() it so a
running dew up doesn't leak into later checks.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread cmd/dew-win/main.go
Comment on lines +637 to +640
stop, err := startWithServices(withServices)
if err != nil {
return err
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in b761fa3 — added a defer stop() right after startWithServices so early error returns (winPathToWSL, npm install, missing script) no longer leak the containers; the os.Exit branch keeps its explicit stop() since defers don't run through os.Exit.

Comment thread cmd/dew-win/main.go
Comment on lines +577 to +582
go func() {
<-sig
fmt.Println("\ndew: stopping services...")
stop()
os.Exit(130)
}()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in b761fa3 — the Ctrl+C handler now terminates the distro after stopping the containers, so the dev server (a child wsl.exe that exiting dew won't reliably kill on Windows) comes down too.

Two leaks in the --with path: (1) cmdUp only called stop() after
dev.Run(), so early error returns (winPathToWSL, npm install, missing
script) left the podman containers running — add a defer stop() right
after startWithServices and keep the explicit stop() only on the
os.Exit branch that would skip the defer; (2) the Ctrl+C handler
stopped the containers but not the dev server, which runs as a child
wsl.exe that exiting dew doesn't reliably kill on Windows — terminate
the distro in the handler so the dev server comes down too.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread cmd/dew-win/main.go
Comment on lines +618 to 622
for _, name := range withServices {
if services.Lookup(name) == nil {
return fmt.Errorf("unknown service %q (available: %s)", name, strings.Join(services.Names(), ", "))
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in ed3da7d — sort services.Names() before joining so the available-services list in the error is deterministic.

Comment thread cmd/dew-win/main.go Outdated
Comment on lines +461 to +468
case a == "--with":
if i+1 >= len(args) {
return "", nil, fmt.Errorf("--with needs a comma-separated service list")
}
with = append(with, splitCSV(args[i+1])...)
i++
case strings.HasPrefix(a, "--with="):
with = append(with, splitCSV(strings.TrimPrefix(a, "--with="))...)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in ed3da7d — an explicitly-empty --with (--with= or a blank value) now errors with the same 'needs a comma-separated service list' message instead of silently starting nothing.

Reject an explicitly-empty --with (`--with=`, `--with '  '`) instead of
silently starting no services, and sort services.Names() before joining
it into the unknown-service error so the 'available:' list is stable
(it iterates a map). Add test cases for both.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread cmd/dew-win/main.go
Comment on lines +642 to 646
_, statErr := os.Stat(pkgPath)
hasProject := statErr == nil
if !hasProject && len(withServices) == 0 {
return fmt.Errorf("no package.json in %s — dew up on Windows supports Node-style projects; or use --with <service>", absDir)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 1a1fc25 — dew up now returns the stat error unless it's os.IsNotExist, so a permission/IO problem on package.json no longer silently falls into the services-only path.

A permission/IO stat error on package.json was treated as 'no project',
which with --with silently fell into the services-only path and masked
the real failure. Return the stat error unless it's a plain
os.IsNotExist.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread cmd/dew-win/main.go Outdated
Usage:
dew setup Install/update WSL2 distro
dew up [dir] Detect a Node project + run its dev server in WSL2
dew up --with <svc> Also start services (postgres,redis,mysql,mongo,minio)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in f9697af — the usage line now shows --with <svc,...> to reflect that it accepts a comma-separated list.

The usage placeholder <svc> implied a single service; --with accepts a
comma-separated list, so show <svc,...>.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread cmd/dew-win/main.go
Comment on lines +626 to 633
for _, name := range withServices {
if services.Lookup(name) == nil {
// services.Names() iterates a map; sort for a stable message.
avail := services.Names()
sort.Strings(avail)
return fmt.Errorf("unknown service %q (available: %s)", name, strings.Join(avail, ", "))
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in 609079f — parseUpArgs now dedups the service list preserving first-seen order, so '--with redis,redis' starts redis once. Added a test case.

Comment on lines +166 to +171
if (-not $svc.HasExited) { $svc.WaitForExit(25000) | Out-Null }
if (-not $svc.HasExited) { $svc.Kill() }
Start-Sleep -Seconds 2
Check "service container removed after dev server exits" {
-not ((wsl -d dew -e podman ps -a --format '{{.Names}}' | Out-String) -match 'dew-svc-redis')
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in ef69c97 — the smoke check now polls for removal instead of a fixed sleep, and force-kills fall back to an explicit podman rm -f so nothing leaks into later checks.

linyiru added 2 commits July 1, 2026 08:40
`--with redis,redis` started the same container name twice — the second
startService rm -f'd and restarted the first. Dedup the service list in
parseUpArgs, preserving first-seen order (as the macOS CLI does). Add a
test case.
Poll for container removal instead of a fixed 2s sleep (flaky on slow
machines), and if the dev server had to be force-killed (which skips
stop()), podman rm -f the container so it doesn't leak into the rest of
the smoke run.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread cmd/dew-win/main.go
Comment on lines 649 to 653
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("resolve %q: %w", dir, err)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in a32f6e7 — dew up now stats the project dir and requires it to be a directory, so a typoed path errors instead of silently falling into services-only mode; a real dir without package.json still runs services-only.

A typoed path with --with (e.g. `dew up --with redis ./typo`) was
treated as project-less and silently fell into services-only mode.
Stat absDir and require it to be a directory, so a bad path errors while
a real dir without package.json still runs services-only.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread cmd/dew-win/main.go Outdated
Comment on lines +611 to +612
s := *services.Lookup(name)
fmt.Printf("dew: starting %s (%s)...\n", name, s.Image)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted in d799f83 — startWithServices now nil-checks services.Lookup and returns an error for an unknown name instead of panicking (cmdUp still validates up front, but the function no longer depends on that).

startWithServices dereferenced services.Lookup(name) without a nil
check. cmdUp validates names first so it can't be nil today, but a
future caller could; return a normal error (the function already
returns one) rather than panicking.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@linyiru linyiru merged commit 575c0b8 into main Jul 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants