Skip to content

feat(dew-win): run as grove's services engine (dew.toml, --services-only, services/exec --json)#40

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

feat(dew-win): run as grove's services engine (dew.toml, --services-only, services/exec --json)#40
linyiru merged 7 commits into
mainfrom
feat/dew-win-services-engine

Conversation

@linyiru

@linyiru linyiru commented Jul 1, 2026

Copy link
Copy Markdown
Member

Makes dew-win satisfy the dew CLI contract grove drives, so grove can use WSL2 as its container engine on Windows the same way it uses Apple Virtualization dew on macOS. grove is macOS-only today; the documented hard blocker is "dew shipping a Windows build" — this is that build's engine surface.

What grove drives (and what this adds)

grove renders a union dew.toml (one [[service]] per installed app), spawns dew up --services-only detached with cwd = its engine dir, polls a readiness marker, then reads dew services --json for health/list/stats and dew exec --json for guest commands.

  • dew up --services-only — loads the union dew.toml via the platform-neutral internal/dewfile, starts each service via podman on the distro's host network (reusing the --with path), drops the readiness marker ~/.local/state/dew/default.sock, and stays foreground holding the distro alive until dew vm stop or Ctrl+C. The marker is written before pulling images (matching macOS dew's socket-on-boot timing) so a slow first pull can't blow grove's readiness window. vm stop clears the marker.
  • dew services --json — reads the union dew.toml and emits {"ok":true,"schema_version":"1.0","data":{"services":[{name,running,host_port,guest_port}]}}. Running state is probed passively via podman ps (never starts the distro); host_port == guest_port == the primary port (host networking).
  • dew exec --json — honours a leading --json (grove places it right after exec) and emits {...,"data":{guest_exit_code,stdout,stderr}}, exiting 0 even when the guest exits non-zero (grove turns guest_exit_code into a GuestExitError).

Verified end-to-end on real Windows 11 / WSL2

grove's rendered pocketbase dew.toml → engine → podman ps: dew-svc-pocketbase-pocketbase Updew services --json reports running:true (host/guest 8090) → PocketBase answers HTTP 200 on Windows localhost:8090 via mirrored networking. dew exec --json sh -c 'echo hi; exit 7'{guest_exit_code:7,stdout:"hi\n"}.

Notes / deferred

  • grove no longer uses vm forward add (host-net + mirrored networking makes it unnecessary), and dew vm stop exiting 0 already satisfies grove's VMStop (it bypasses envelope parsing) — so neither is needed here.
  • Data-volume persistence (services are ephemeral) and a distro-hold that survives host sleep are follow-ups, not needed for the core install→run→open flow.

Unit tests cover the --services-only flag, the dewfile→services mapping, and the dew services --json envelope shape. Windows CI (go test ./cmd/dew-win) is green.

linyiru added 4 commits July 1, 2026 13:52
grove drives dew as a services engine: it renders a union dew.toml (one
[[service]] per installed app), sets cwd to that dir, spawns
`dew up --services-only` detached, then polls ~/.local/state/dew/default.sock
to decide the engine is up.

dew-win now honours that contract. `up --services-only` loads the dew.toml via
the platform-neutral internal/dewfile, starts each service via podman on the
distro's host network (reusing the --with path), drops the readiness marker
grove stats, and stays foreground holding the distro (and its containers) alive
until `dew vm stop` terminates it or Ctrl+C. `vm stop` clears the marker.

grove never dials the marker — VMRunning() only os.Stats it — so a plain file
satisfies the contract without a real daemon socket. Data (volume persistence)
and Ports (extra forwards) are not yet mapped; --network=host already puts every
container port on Windows localhost.

Unit tests cover the new --services-only flag and the dewfile->services mapping.
The services engine wrote its readiness marker only after every service was
pulled and health-gated, so grove's engine-readiness poll had to wait out a
slow first image pull — which can far exceed its window — even though the VM
itself was up in a second.

Write the marker as soon as the distro boots, matching macOS dew (its daemon
socket appears when the VM comes up, before images are pulled). grove
health-checks each app on its own generous timeout, so surfacing engine-ready
early is correct and independent of pull time. Moving the marker ahead of the
service loop also drops the data race between the appended started-slice and the
Ctrl+C goroutine (which now just terminates the distro to kill the containers).
grove's dewbridge.Services() drives `dew services --json` to learn each
service's running state and forwarded host port: it polls this for install
health (waitServiceRunning waits for running==true) and reads it for
`grove list` / `stats`.

dew-win now serves that contract. `dew services` reads the union dew.toml from
the working dir (grove sets cwd to its engine dir) and reports each service as
{name, running, host_port, guest_port} inside the standard ok-envelope
({"ok":true,"schema_version":"1.0","data":{...}}). Services run on the
distro's host network, so host_port == guest_port == the primary port. Running
state is probed passively via `podman ps` — if the distro isn't up the query
reports every service stopped rather than starting it (grove polls while the
engine is still booting). A bare `dew services` prints a human table.

Adds the emitEnvelope helper (reused by later --json commands) and a unit test
asserting the envelope shape and stopped-state mapping.
grove's dewbridge runs `dew exec --json sh -c CMD` (uninstall's data-dir
cleanup, and its load-bearing guest-failure check) with --json placed right
after exec, since a trailing --json would be handed to the guest sh.

dew-win now honours a leading --json on exec: it captures the guest command's
stdout/stderr and exit code and emits {"ok":true,...,"data":{
"guest_exit_code":N,"stdout":...,"stderr":...}}. dew is 'ok' whenever it
dispatched the command, so this exits 0 even when the guest exits non-zero — the
real code rides in guest_exit_code, which grove turns into a GuestExitError.
Only a genuine dispatch failure (wsl missing) surfaces as an error. The
passthrough (non-JSON) exec path is unchanged.

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 the missing “services engine” surface to dew-win so it can satisfy grove’s dew CLI contract on Windows/WSL2 (readiness marker, up --services-only, services --json, and exec --json).

Changes:

  • Adds dew up --services-only to run a union dew.toml as a foreground “services engine” and manage a grove readiness marker.
  • Adds dew services [--json] to report service list + running state derived from dew.toml and passive podman ps.
  • Adds dew exec --json to emit a grove-compatible JSON envelope containing guest exit code and captured stdout/stderr.

Reviewed changes

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

File Description
cmd/dew-win/main.go Implements services command, services-engine mode for up, readiness marker lifecycle, and exec --json envelope output.
cmd/dew-win/main_test.go Updates parseUpArgs tests and adds unit coverage for dewfile→service mapping and dew services --json envelope shape.

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

Comment thread cmd/dew-win/main.go Outdated
return filepath.Join(home, ".local", "state", "dew", "default.sock"), nil
}

// writeEngineMarker creates the readiness marker after services are up.

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 12b478c — corrected the writeEngineMarker comment to say the marker is written as soon as the distro is up (before pulling images), matching the marker-early ordering the engine relies on.

Comment thread cmd/dew-win/main.go
Comment on lines +752 to +763
jsonOut := false
dir := "."
for _, a := range args {
switch {
case a == "--json":
jsonOut = true
case strings.HasPrefix(a, "-"):
return fmt.Errorf("unknown flag %q for dew services", a)
default:
dir = a
}
}

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 12b478c — dew services now rejects a second positional dir with a clear error ("takes at most one dir"), matching dew up. Covered by TestCmdServicesRejectsMultipleDirs.

Comment thread cmd/dew-win/main.go Outdated
Comment on lines +448 to +452
// grove drives `dew exec --json sh -c CMD`, placing --json right after exec
// (a trailing --json would be handed to the guest sh). So only a LEADING
// --json is dew's own flag; strip it and switch to the captured-envelope
// path. Any later --json stays part of the guest argv.
jsonOut := false

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 12b478c — extracted splitExecJSONFlag and added TestSplitExecJSONFlag covering (1) a leading --json triggers the envelope and (2) a non-leading --json stays part of the guest argv. The ok:true + guest_exit_code path on a non-zero guest exit is exercised by the real-machine check (dew exec --json sh -c 'echo hi; exit 7' -> guest_exit_code:7).

Three review fixes in cmd/dew-win (all in main.go):
- writeEngineMarker's comment said 'after services are up'; correct it to
  before (the marker is written as soon as the distro is up, ahead of image
  pulls — the point of the marker-early ordering).
- dew services silently took the last positional dir when given several
  (`dew services a b`); reject a second positional with a clear error,
  matching dew up.
- Extract splitExecJSONFlag as a pure helper so the contract-critical
  leading-vs-trailing --json parsing is unit-testable.

Refs #40

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 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread cmd/dew-win/main.go Outdated
Comment on lines +734 to +736
if running, err := distroRunningNow(); err != nil || !running {
return set
}

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 8039826 — runningServiceContainers now returns (map, error): a stopped distro stays an empty set with no error, but a distroRunningNow failure propagates through dew services so callers distinguish 'stopped' from 'couldn't determine'. Covered by TestCmdServicesPropagatesProbeError.

Comment thread cmd/dew-win/main.go
Comment on lines +737 to +741
out, err := exec.Command("wsl", "-d", distroName, "--exec",
"podman", "ps", "--format", "{{.Names}}").Output()
if err != nil {
return set
}

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 8039826 — the podman ps probe now uses CombinedOutput and returns an error (with stderr in the message) when the distro is running but the probe fails, so 'unable to determine' no longer masquerades as running:false.

Comment thread cmd/dew-win/main.go
Comment on lines +889 to +894
// Hold the distro open — an active wsl session keeps WSL2 (and the
// host-network containers) alive. Returns when `dew vm stop` runs
// `wsl --terminate`, at which point we clear the marker and exit.
exec.Command("wsl", "-d", distroName, "--exec", "sh", "-c", "while true; do sleep 86400; done").Run()
removeEngineMarker()
return 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 8039826 — a non-ExitError from the hold-distro Run() (wsl couldn't start the session) is now returned. A normal terminate via wsl --terminate is an ExitError and stays the expected stop path.

Comment thread cmd/dew-win/main.go
Comment on lines +448 to 451
jsonOut, args := splitExecJSONFlag(args)
if len(args) == 0 {
return fmt.Errorf("usage: dew exec <cmd> [args...]")
return fmt.Errorf("usage: dew exec [--json] <cmd> [args...]")
}

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 8039826 — printUsage now lists dew services and dew up --services-only, and shows dew exec [--json].

linyiru added 2 commits July 1, 2026 18:11
Address Copilot review on #40:
- runningServiceContainers now returns an error. A stopped distro stays a
  normal empty-set result (grove polls while the engine boots), but a genuine
  probe failure — distroRunningNow erroring, or podman ps failing (now via
  CombinedOutput so stderr is in the message) — propagates through
  dew services so callers can tell 'stopped' from 'couldn't determine' instead
  of misreporting every service as stopped.
- cmdUpServicesEngine's hold-distro wsl session: a non-ExitError from Run()
  (wsl couldn't start the session at all) is now returned, so the engine
  doesn't report success after 'services engine ready' when it never actually
  held the distro. A normal terminate (ExitError from wsl --terminate) stays
  the expected stop path.
- printUsage now lists dew services and dew up --services-only, and shows
  dew exec [--json].

Refs #40
TestCmdServicesPropagatesProbeError asserts cmdServices returns an error (not a
silent all-stopped report) when the running-state probe fails. Refs #40

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 2 out of 2 changed files in this pull request and generated no new comments.

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 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread cmd/dew-win/main.go
Comment on lines +831 to +834
// services.Service. Data (volume persistence) and Ports (extra forwards) are
// not yet wired on Windows: with --network=host every container port already
// lands on the distro's netns (hence Windows localhost), and volume
// persistence is a follow-up.
@linyiru linyiru merged commit 63fb7d6 into main Jul 1, 2026
6 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