wslc-wip 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37c2e70c286c4dd7b163201674eff9b4111fbe3f05ef032500a5b7ac516f97b5
4
- data.tar.gz: fbad7b94e10bb0cbd9efc8c70f1a2546830ac0c20f276a925b48e0d668679902
3
+ metadata.gz: 585bcc92d8ab78eb38c134e779738c6eb527dd57b8aad87bbfae18a724a10974
4
+ data.tar.gz: 908afba4af5470c777896379a143a6759ab08ef453f6d8baf727e22ff4a8adf2
5
5
  SHA512:
6
- metadata.gz: 70319980be04f8abdbb6d5e167e8941ca4fa3a8ab65b0cd71ec884daff639205a70935b4c5a756ec3403678982c2859423051930379aa3d79f302506b0f88326
7
- data.tar.gz: f100b5622d566ab77da4437cb028138a7fa33f47d4d19ff70874c683e2995e9c3213bc95b0109c03c82556827a26ee22b47a034bf7fc96895effc2c8fcb3950f
6
+ metadata.gz: c01879ea531294531b0283b53a7fdfe01586a8c9c17dbd83814ad92c11d915cecd5f141a0ffd6f94f96dd633e0e63657f15a10ec4f1e372cc53ea0207a8b22be
7
+ data.tar.gz: e3cb38ab627b56487b8965f9107c03f7081fd24defb2a731415af8e9533cd51e5af1eea5da3f0075d854371cbb77ed6e1b75c8843737df4528e806965bfc5137
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <img src="docs/logo.png" alt="wip logo" width="120" align="left">
2
+
1
3
  # wip
2
4
 
3
5
  [![Tests](https://github.com/slidict/wip/actions/workflows/test.yml/badge.svg)](https://github.com/slidict/wip/actions/workflows/test.yml)
@@ -5,7 +7,7 @@
5
7
  [![License: MIT](https://img.shields.io/github/license/slidict/wip.svg)](LICENSE)
6
8
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](wslc-wip.gemspec)
7
9
 
8
- Homepage: https://wslc-wip.slidict.com/
10
+ Homepage: https://wslc-wip.slidict.com/ · **[Full documentation: wip Wiki](https://github.com/slidict/wip/wiki)**
9
11
 
10
12
  `wip` is a Ruby-built OSS CLI wrapper that brings a [`dip`](https://github.com/bibendi/dip)-like
11
13
  workflow to Microsoft WSLC. It collects a project's container, image, environment variables, and
@@ -16,25 +18,19 @@ commands into a single `wip.yml`, and forwards them to `wslc.exe` / `wslc` as sa
16
18
 
17
19
  > **Status:** early release. Expect to track WSLC's own interface as it evolves.
18
20
 
21
+ This README covers the fastest path to a running `wip.yml`. For everything else — every config
22
+ key, every command's flags, guides, and troubleshooting — see the **[wip Wiki](https://github.com/slidict/wip/wiki)**.
23
+
19
24
  ## Contents
20
25
 
21
26
  - [Which mode should you use?](#which-mode-should-you-use)
22
27
  - [Requirements & installation](#requirements--installation)
23
28
  - [Quick start](#quick-start)
24
29
  - [Configuration](#configuration)
25
- - [Container mode](#container-mode)
26
- - [Compose mode](#compose-mode)
27
- - [Compose mode (native)](#compose-mode-native)
28
- - [.env](#env)
29
- - [.dockerignore](#dockerignore)
30
- - [Source sync](#source-sync)
31
30
  - [Commands](#commands)
32
- - [doctor](#doctor)
33
31
  - [Common errors](#common-errors)
34
- - [FAQ](#faq)
35
32
  - [Development](#development)
36
33
  - [Contributing](#contributing)
37
- - [Roadmap](#roadmap)
38
34
  - [License](#license)
39
35
 
40
36
  ## Which mode should you use?
@@ -43,12 +39,14 @@ commands into a single `wip.yml`, and forwards them to `wslc.exe` / `wslc` as sa
43
39
 
44
40
  | Situation | Use |
45
41
  |---|---|
46
- | No `compose.yml` — wip manages containers directly | [`mode: container`](#container-mode) (default) |
47
- | Have `compose.yml`, don't want to install a third-party tool | [`mode: compose-native`](#compose-mode-native) |
48
- | Have `compose.yml` and already use/prefer a third-party compose-for-`wslc` tool | [`mode: compose`](#compose-mode) |
42
+ | No `compose.yml` — wip manages containers directly | `mode: container` (default) |
43
+ | Have `compose.yml`, don't want to install a third-party tool | `mode: compose-native` |
44
+ | Have `compose.yml` and already use/prefer a third-party compose-for-`wslc` tool | `mode: compose` |
49
45
 
50
- `wip init` picks `compose-native` automatically when it finds a `compose.yml`/`docker-compose.yml`
51
- next to it, and `container` otherwise — see [Commands](#commands).
46
+ `wip init` picks `compose-native` automatically when it finds a `compose.yml`, `compose.yaml`,
47
+ `docker-compose.yml`, or `docker-compose.yaml` next to it, and `container` otherwise — see
48
+ [Commands](#commands). For the full breakdown and trade-offs, see
49
+ [Choosing a Mode](https://github.com/slidict/wip/wiki/Choosing-a-Mode) on the wiki.
52
50
 
53
51
  ## Requirements & installation
54
52
 
@@ -63,7 +61,8 @@ From source: `bundle install && bundle exec exe/wip version`.
63
61
  ## Quick start
64
62
 
65
63
  This walks through `mode: container` (the default). Already have a `compose.yml`? See
66
- [Which mode should you use?](#which-mode-should-you-use) first.
64
+ [Which mode should you use?](#which-mode-should-you-use) first, or read the wiki's
65
+ [Getting Started](https://github.com/slidict/wip/wiki/Getting-Started) guide.
67
66
 
68
67
  ```bash
69
68
  gem install wslc-wip
@@ -80,8 +79,6 @@ wip rails console
80
79
  Put a `wip.yml` in your project root. Running from a subdirectory walks up to find it, or pass
81
80
  `--config PATH` to point at one explicitly.
82
81
 
83
- ### Container mode
84
-
85
82
  ```yaml
86
83
  version: 1
87
84
  mode: container # default
@@ -148,450 +145,67 @@ sync: # optional; mirror the source into a named volume instead of bind-mounting
148
145
 
149
146
  `env` values are stringified. `wip config` masks any key matching token, password, secret,
150
147
  credential, or auth. Keep real secrets out of the config file and in your runtime environment
151
- instead.
148
+ instead — see [Secret Masking](https://github.com/slidict/wip/wiki/Secret-Masking).
152
149
 
153
150
  `interaction:` can also be spelled `commands:` — the same block under a different name, e.g. for
154
- projects that already use `commands:`. `wip config` prints the effective config using `commands:`.
155
- The two are aliases for the same feature, not separate ones: pick whichever name you like, but declaring both
156
- `commands:` and `interaction:` in the same `wip.yml` is a `ConfigError`.
157
-
158
- #### Dependency containers
159
-
160
- `dependencies:` holds every container uniformly — the primary one `container:` points at and any
161
- sidecar services (a database, Redis, ...) alongside it. Each entry accepts `image` (required),
162
- `command`, `env`, `ports`, `volumes`, and `workdir`; there's no separate, differently-shaped block
163
- for "the one you exec into." `container:` has no default; once `dependencies:` has any entries,
164
- wip needs to be told explicitly which one is primary rather than guessing a name.
165
-
166
- What sets the primary entry apart is operational, not structural: `wip up` brings up every other
167
- entry by name first (creating `network:` beforehand if it doesn't exist and set), then boots or
168
- starts the primary one so `bin/rails c` (or anything else run inside it) can reach
169
- `development.mysql`/`redis`/etc. by their dependency name, the same way Compose's service names
170
- resolve. `wip down` tears the primary container and all sidecars down (the network itself is left
171
- in place). Only the primary container is a target for `exec`/`run`/`build`/`interaction:` — sidecars
172
- are only ever started and stopped, matching Compose's own service-vs-you-exec-into-one-of-them
173
- split.
174
-
175
- ### Compose mode
176
-
177
- If your project already has a real `compose.yml`, don't duplicate it in `dependencies:` — point
178
- `wip` at it instead:
179
-
180
- ```yaml
181
- version: 1
182
- mode: compose # required to enable compose mode; a compose: block with no mode: compose is an error
183
- compose:
184
- service: app # required: which compose service wip run/exec/NAME target
185
- command: wslc-compose # required: the compose-for-wslc binary/path you have installed
186
- file: compose.yml # optional; auto-detected next to wip.yml otherwise
187
- project: myapp # optional; omitted lets the compose tool pick its own default
188
- ```
189
-
190
- `compose:` is mutually exclusive with `dependencies:`/`network` — pick one orchestration path per
191
- project. In compose mode, `wip` becomes a thin bridge to an external compose-for-`wslc` CLI rather
192
- than reimplementing Compose itself.
193
-
194
- `wslc` itself has no native Compose support yet (tracked upstream in
195
- [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and until it does,
196
- independent third-party tools fill the gap — for example
197
- [bacarndiaye/wslc-compose](https://github.com/bacarndiaye/wslc-compose) (Python) and
198
- [inuyume/wslc-compose](https://github.com/inuyume/wslc-compose) (Go), among others. `wslc` is new
199
- and still evolving, so expect more of these to show up (and existing ones to change) over time.
200
- `wip` deliberately doesn't pick a winner or default to any of them (unlike `wslc.command`, which
201
- defaults to `auto` and searches for `wslc.exe`/`wslc`): `compose.command` is required and treats
202
- every implementation equally — set it to whichever binary name or absolute path you've installed.
203
- Whichever one you use needs to understand `-f FILE [-p PROJECT] up|down|exec|logs`, the subset of
204
- the Compose CLI vocabulary `wip` drives. `wip doctor` reports whether the configured command is
205
- found, its version, and which compose file `wip` resolved.
206
-
207
- - `wip up`/`wip down` delegate straight to `<compose command> up -d`/`down`.
208
- - `wip exec`/`wip NAME` (custom `interaction:`) run inside `compose.service`.
209
- - `wip shell` also goes through the bridge: unless `interaction.shell` is defined in `wip.yml`, it
210
- `exec`s `bash` against `compose.service`, falling back to `sh`.
211
- - `wip logs [-f] [SERVICE...]` is only available in compose mode.
212
- - `wip run` has no ephemeral-container equivalent in this exec-only vocabulary, so it falls back
213
- to `exec` against the already-running `compose.service` (wip warns when this happens).
214
- - `interaction:` entries with `type: run`/`type: build` aren't supported in compose mode — use your
215
- compose tool's own `build`/`up --build` directly; compose owns builds for its own services.
216
-
217
- ### Compose mode (native)
218
-
219
- Don't want to install a third-party compose-for-`wslc` tool at all? `mode: compose-native` parses
220
- `compose.yml` itself and drives `wslc` directly, the same way `mode: container` +`dependencies:`
221
- already does — no external binary, and `wip run` gets a real ephemeral `wslc run --rm` instead of
222
- the `exec` fallback above:
223
-
224
- ```yaml
225
- version: 1
226
- mode: compose-native
227
-
228
- compose:
229
- service: app # required: which compose service wip run/exec/NAME target
230
- file: compose.yml # optional; auto-detected next to wip.yml otherwise
231
- project: myapp # optional; also names wip's own project network (defaults to the wip.yml
232
- # directory's name) so services can reach each other by name
233
- ```
234
-
235
- There's no `compose.command` here (no external binary to name), and no top-level `container:` —
236
- `compose.service` already names it.
237
-
238
- This is explicitly a stopgap for as long as `wslc` itself has no native Compose support (tracked
239
- upstream in [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)) and third-party
240
- compose-for-`wslc` tools stay incomplete. It only understands a minimal subset of the Compose spec.
241
- Within `services.<name>:`, anything outside that subset is a load-time `ConfigError` naming the
242
- offending key, rather than silently ignored — but everything *outside* `services:` at the document's
243
- top level (`networks:`, `volumes:`, `configs:`, `secrets:`, ...) is the one exception: it's read by
244
- real Compose tools, not by `wip`, so `wip` silently ignores it rather than rejecting an otherwise
245
- valid compose.yml over sections it doesn't need to look at:
246
-
247
- - Per service: `image`, `build` (string or `{context:, dockerfile:, args:, shadow_context:}`;
248
- `context` is resolved relative to `compose.yml`, not wherever `wip` is invoked from; `dockerfile`
249
- stays relative to `context` itself — see [.dockerignore](#dockerignore) for what `shadow_context`
250
- does), `command` (shell or exec form), `environment`
251
- (mapping or `KEY=VALUE` array — a mapping value must not be null; host environment pass-through
252
- isn't supported), `ports`/`volumes` (short syntax only — `"host:container"` strings, not
253
- long-syntax mappings), `working_dir`, `user`, `depends_on` (ordering only — a `condition:` other
254
- than `service_started` is rejected, since there's no health-check support). `tty`, `stdin_open`,
255
- and `networks` are accepted but silently ignored: TTY/stdin allocation is already decided per
256
- invocation (see "TTY allocation" below), not fixed per service, and every service already shares
257
- the one project network `compose.project` sets up.
258
- - `wip logs` takes at most one `SERVICE` (defaulting to `compose.service`) — `wslc logs`, like
259
- `docker logs`, follows a single container, unlike a real compose tool's multi-service view.
260
- - `sync:` behaves exactly like `mode: container`'s (falls back to the primary service's own image,
261
- defaults to `sync.mode: exec`) — none of the external bridge's `sync.image`/`sync.build`
262
- requirement applies, since wip itself boots every container here.
263
-
264
- ### .env
265
-
266
- Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
267
- per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
268
- passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
269
- custom commands. `.env` only fills in keys that aren't already set by the primary container's
270
- `env` or a command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH`
271
- to load a different file instead.
272
-
273
- ### .dockerignore
274
-
275
- `wip build` reads `.dockerignore` from the build context and excludes anything it matches before
276
- handing the context to `wslc build`, since `wslc` sends the context as-is otherwise. Set
277
- `commands.build.shadow_context` in `wip.yml` (or `build.shadow_context` on a compose-native
278
- `build:` service in `compose.yml`) to a directory on the Windows filesystem to enable a persistent
279
- shadow context for projects outside `/mnt/<drive>`. The first build copies every included file;
280
- later builds only copy added or changed files and remove deleted or newly ignored files. Without
281
- this setting the optimization is disabled, and it only applies under WSL2 — on WSL1 (or anywhere
282
- else) the context is handed to `wslc build` directly. Projects already on `/mnt/c` (or another
283
- mounted Windows drive) also continue to build directly even when the setting is present. The path
284
- must live outside the build context itself, or `wip build`/`wip up` refuses it.
285
-
286
- ### Source sync
287
-
288
- Bind-mounting the app directory (`.:/app`) is what usually makes a container boot crawl under
289
- wslc — see [Slow boot when the app directory is
290
- bind-mounted](#slow-boot-when-the-app-directory-is-bind-mounted) for why. A `sync:` block hands
291
- that problem to `wip`: the source is mounted read-only, the app runs off a named volume, and wip
292
- mirrors one into the other with `rsync`.
293
-
294
- `wip up`'s pre-boot mirror always uses a throwaway container (the primary one isn't running yet),
295
- so `sync.image`/`sync.build` apply there regardless of `sync.mode` — falling back to the primary
296
- container's own image if neither is set under `mode: container`/`compose-native` (both have a
297
- `dependencies:` entry to borrow it from); `mode: compose` has no such entry, so one of
298
- `sync.image`/`sync.build` is required there. Where `sync.mode` actually matters is every mirror
299
- *after* that: under `sync.mode: exec` (the default for `mode: container`/`compose-native`), `wip
300
- sync`/`wip sync --watch` run `rsync` inside the already-running primary container instead, so
301
- *that* image needs `rsync` installed — `sync.image`/`sync.build` are ignored for these. Under
302
- `sync.mode: run`, every mirror (pre-boot included) uses a throwaway container, so `sync.image`/
303
- `sync.build` (or the primary image fallback, where available) need `rsync` throughout.
304
-
305
- ```yaml
306
- sync:
307
- source: . # host path, relative to wip.yml (default: the wip.yml directory)
308
- target: /app # container path served by the volume (default: the primary container's workdir, else /app)
309
- volume: app-src # named volume holding the mirror (default: "<container>-src")
310
- mount: /host-src # where the source is bind-mounted read-only (default: /host-src)
311
- exclude: # rsync --exclude patterns
312
- - .git
313
- - tmp/
314
- - node_modules/
315
- delete: true # rsync --delete (default: true)
316
- command: rsync # binary that does the mirroring (default: rsync)
317
- options: [] # extra flags appended to the rsync invocation
318
- interval: 2 # seconds between syncs for `wip sync --watch` (default: 2)
319
- mode: exec # exec (mirror inside the running container) or run (a throwaway one);
320
- # default: exec for `mode: container`, run for `mode: compose`
321
- image: null # image for the mirror container; unused under mode: container unless set
322
- # (falls back to the primary container's own image). Under mode: compose,
323
- # one of image or build is required (there's no dependencies: entry to fall
324
- # back to)
325
- build: # optional; has wip build the mirror image itself instead of requiring one to
326
- # already exist. build.tag wins over image if both are set.
327
- dockerfile: |
328
- FROM alpine:latest
329
- RUN apk add --no-cache rsync
330
- tag: null # optional (default: "wip-sync-<container>:latest")
331
- ```
332
-
333
- `wip init --template NAME` writes `exclude`'s default list live, picked from that stack's own
334
- `github/gitignore` template:
335
-
336
- | `--template` | Stack | Default `exclude` |
337
- |---|---|---|
338
- | `rails` | Rails | `.git`, `log/`, `tmp/`, `storage/`, `public/assets/`, `public/packs/`, `.bundle/`, `vendor/bundle/`, `coverage/`, `node_modules/` |
339
- | `node` | Node.js | `.git`, `node_modules/`, `dist/`, `build/`, `.next/`, `.cache/`, `coverage/` |
340
- | `rust` | Rust | `.git`, `target/` |
341
- | `csharp` | C# | `.git`, `bin/`, `obj/`, `.vs/`, `packages/` |
342
- | (omitted) | — | `.git`, `tmp/`, `node_modules/` |
343
-
344
- Everything below `sync:` is optional — `sync: {}` alone already works. With it in place:
345
-
346
- - Any `volumes` entry on the primary container mounting `target` (the usual `.:/app`) is replaced
347
- by `<source>:/host-src:ro` plus `app-src:/app`, so the running app only ever touches the volume.
348
- Other volumes (`bundle:/usr/local/bundle`, ...) are passed through untouched, and sidecar
349
- `dependencies:` entries keep mounting whatever they declare.
350
- - `wip up` mirrors the source into the volume before the container boots; `wip up --no-sync`
351
- skips that step.
352
- - `wip sync` mirrors on demand: `sync.mode: exec` (the default under `mode: container`) execs
353
- rsync inside the already-running container; `sync.mode: run` always uses a throwaway container
354
- with the same mounts instead. Which one runs is fixed by config, not guessed at from whether a
355
- container happens to be up.
356
- - `wip sync --watch [--interval N]` keeps re-syncing until Ctrl-C, so host edits reach the
357
- container with a short delay. Run it in a second terminal alongside `wip up -d`.
358
- - `wip doctor` reports the resolved source, volume, and target, and fails if the source is missing.
359
- - With `sync.build` configured, `wip build`s that image once per `wip up`/`wip sync` invocation
360
- (including once before a `--watch` loop starts, not on every tick) before mirroring with it.
361
-
362
- Like every built-in command, `wip sync` takes precedence over an `interaction:` entry of the same
363
- name; wip says so and points at `wip dispatch sync`, which still runs yours.
364
-
365
- Two things to keep in mind. The mirror runs `rsync` *inside* the container, so the image needs it
366
- (`RUN apt-get update && apt-get install -y rsync`) — or point `sync.command` at a copy tool the
367
- image already has. And the mirror is one-way (host → volume): anything the app writes under
368
- `target` is removed by the next `--delete` pass unless you `exclude` it, give it its own volume,
369
- or set `delete: false`.
370
-
371
- `sync:` works alongside `mode: compose` too, but two things change:
372
-
373
- - Compose still owns the volume layout, so wip doesn't rewrite any mounts for you: the compose
374
- service that runs your app must itself declare a named volume with the exact same name as
375
- `sync.volume` (`<container>-src` by default) mounted at the path your app expects, e.g.:
376
- ```yaml
377
- # compose.yml
378
- services:
379
- app:
380
- volumes:
381
- - app-src:/app
382
- volumes:
383
- app-src:
384
- ```
385
- wip's mirror writes into that volume from a separate, disposable container; it never touches
386
- the compose service directly.
387
- - `sync.mode` defaults to `run` and can't be set to `exec` (only a container wip itself booted is
388
- guaranteed to have the read-only source mount attached, which compose services never do), and
389
- `sync.image` or `sync.build` becomes required, since that disposable container needs an image
390
- from somewhere — under `mode: container` it borrows the primary `dependencies:` entry's image,
391
- but compose mode has no such entry to borrow from.
392
-
393
- `wip up`'s pre-boot mirror (and the `--no-sync` flag that skips it) works the same way under
394
- `mode: compose` as it does otherwise: the source is mirrored into the volume before
395
- `compose up` starts the service that mounts it.
396
-
397
- Since `sync.mode: run` boots a fresh container on every mirror, reusing your app's full image here
398
- just adds startup overhead for something that only ever runs `rsync`. A dedicated, minimal image is
399
- worth it — `wip` doesn't publish or default to one itself (same reasoning as `compose.command`:
400
- picking a specific third-party image for you isn't its call to make), but `sync.build` covers it
401
- without needing to manage a separate image yourself:
402
-
403
- ```yaml
404
- sync:
405
- build:
406
- dockerfile: |
407
- FROM alpine:latest
408
- RUN apk add --no-cache rsync
409
- ```
410
-
411
- wip builds this once per `wip up`/`wip sync` invocation (not on every `--watch` tick — see above)
412
- and uses the result, tagged `wip-sync-<container>:latest` by default (`build.tag` overrides it).
413
- Prefer managing the image yourself instead? Build and tag it however you like, then set `sync.image`
414
- to that tag directly — `sync.build`'s tag wins if both are set, so don't configure both at once.
151
+ projects that already use `commands:`. The two are aliases for the same feature; declaring both in
152
+ the same `wip.yml` is a `ConfigError`. See [Interactions](https://github.com/slidict/wip/wiki/Interactions).
153
+
154
+ Every key above is covered across the wiki's feature pages, with the full behavior, edge cases,
155
+ and examples — start at the
156
+ **[Configuration Reference](https://github.com/slidict/wip/wiki/Configuration-Reference)**. Notably:
157
+
158
+ - [Dependencies](https://github.com/slidict/wip/wiki/Dependencies) the primary container vs. sidecars
159
+ - [Restart Policies](https://github.com/slidict/wip/wiki/Restart-Policies) /
160
+ [Auto Restarting Containers](https://github.com/slidict/wip/wiki/Auto-Restarting-Containers) `restart:` and `wip up --watch`
161
+ - [Compose Mode](https://github.com/slidict/wip/wiki/Compose-Mode) bridging to a third-party compose-for-`wslc` tool
162
+ - [Compose Native Mode](https://github.com/slidict/wip/wiki/Compose-Native-Mode) — wip parsing `compose.yml` itself
163
+ - [Env Files](https://github.com/slidict/wip/wiki/Env-Files) `.env` loading and precedence
164
+ - [Dockerignore](https://github.com/slidict/wip/wiki/Dockerignore) / [Shadow Build Context](https://github.com/slidict/wip/wiki/Shadow-Build-Context)
165
+ - [Source Sync](https://github.com/slidict/wip/wiki/Source-Sync) / [Sync Modes](https://github.com/slidict/wip/wiki/Sync-Modes) / [Continuous Sync](https://github.com/slidict/wip/wiki/Continuous-Sync)
415
166
 
416
167
  ## Commands
417
168
 
418
169
  | Command | Description |
419
170
  |---|---|
420
- | `wip init [--force] [--template NAME]` | Write a starter `wip.yml`: `mode: compose-native` if a `compose.yml`/`docker-compose.yml` is found next to it, `mode: container` otherwise. `--template` picks `sync.exclude`'s default patterns for a stack (`rails`, `node`, `rust`, `csharp`); omitted, it falls back to `.git`/`tmp/`/`node_modules/`. Refuses to overwrite an existing `wip.yml` unless `--force` |
171
+ | `wip init [--force] [--template NAME]` | Write a starter `wip.yml`: `mode: compose-native` if a `compose.yml`, `compose.yaml`, `docker-compose.yml`, or `docker-compose.yaml` is found next to it, `mode: container` otherwise |
421
172
  | `wip version` | wip's version, plus WSLC's if it can be detected |
422
173
  | `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
423
174
  | `wip config` | Print the effective configuration (secrets masked) |
424
- | `wip build [--no-cache] [-- OPTIONS]` | Build the image from the `build` definition. `wslc build` reuses matching local layers by default; `--no-cache` disables that. |
425
- | `wip up [-d] [--no-sync] [--no-cache]` | Start the primary `dependencies:` entry (`container:` names which one) and its sidecars (creating any that are missing, on `network:` if set). `-d` runs the main container in the background; with `sync:` configured, the source is mirrored into the volume first unless `--no-sync` |
426
- | `wip stop` | Stop the primary container and its sidecar `dependencies:` without removing them |
427
- | `wip down` | Stop and remove the primary container and its sidecar `dependencies:` |
175
+ | `wip build [--no-cache] [-- OPTIONS]` | Build the image from the `build` definition |
176
+ | `wip up [-d] [--no-sync] [--no-cache] [--watch] [--interval N]` | Start the configured stack, creating it if necessary |
177
+ | `wip stop` | Stop the configured stack without removing it |
178
+ | `wip down` | Stop and remove the configured stack |
428
179
  | `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
429
- | `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (mode: compose `exec`s into `compose.service` instead see "Compose mode" above) |
180
+ | `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (`mode: compose` has no ephemeral run — falls back to `exec` in the running service, with a warning) |
430
181
  | `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
431
- | `wip logs [-f] [SERVICE...]` | Follow compose service logs (compose modes only; mode: compose-native takes at most one `SERVICE`) |
432
- | `wip sync [-w] [--interval N]` | Mirror the source into the sync volume once, or keep re-syncing with `--watch` (needs `sync:`) |
182
+ | `wip logs [-f] [SERVICE...]` | Follow compose service logs (compose modes only; under `mode: compose-native`, at most one `SERVICE`) |
183
+ | `wip sync [-w\|--watch] [--interval N]` | Mirror the source into the sync volume once, or keep re-syncing with `--watch` (needs `sync:`) |
433
184
  | `wip NAME ARGS...` | Run `interaction.NAME`, appending any extra arguments |
434
185
 
435
- TTY allocation is decided by combining the command's config, the CLI option, and whether both
436
- stdin and stdout are real TTYs.
437
-
438
- Pass `--debug` (or set `WIP_DEBUG=1`) to see where time is going: wip prints each step it takes —
439
- checking for an existing network/container/dependency, and running the resolved `wslc`/`docker`
440
- command — along with how long that step took, e.g.:
441
-
442
- ```console
443
- $ wip rails c --debug
444
- wip: [debug] running: wslc.exe exec -it -w /app app bin/rails c
445
- + wslc.exe exec -it -w /app app bin/rails c
446
- ...
447
- wip: [debug] done in 4.32s: running: wslc.exe exec -it -w /app app bin/rails c
448
- ```
449
-
450
- For long-running interactive commands (like `rails c`), the "done" line only prints after you
451
- exit, but the timestamp of the `+ ...` line tells you when wip finished its own setup and handed
452
- off to `wslc`/`docker` — useful for telling wip-side overhead apart from time spent booting inside
453
- the container.
454
-
455
- While a step is still running, wip also prints a host resource snapshot (load average, memory,
456
- disk I/O, and the top CPU-consuming processes) every 5 seconds, so a hang is visible even before
457
- the command has produced any output of its own:
186
+ Every command has flags, per-mode behavior, and examples on its own wiki page see the
187
+ **[CLI Command Reference](https://github.com/slidict/wip/wiki/CLI-Command-Reference)**.
458
188
 
459
- ```console
460
- wip: [debug] still running (load 3.42 2.10 1.05 | mem 6.1G/15.6G | io read 12000KB/s write 400KB/s | top: wslc.exe(8842) cpu 61.0%/mem 3.2%, ...): running: wslc.exe exec -it -w /app app bin/rails c
461
- ```
462
-
463
- The disk I/O figure is worth watching first if the host's CPU and memory look idle — a slow
464
- `bundle`/`rails` boot is often WSL2's bind-mounted (`.:/app`-style) volumes doing a lot of small
465
- reads, not the container starving for CPU.
466
-
467
- For commands that hand the real terminal to the child (`-it`, e.g. `rails c`), these periodic
468
- snapshots go to a log file instead of your terminal — wip prints the path once at the start —
469
- since writing into a terminal the child controls in raw mode would garble both outputs. Commands
470
- that don't need a TTY still get the snapshots printed live.
471
-
472
- Override that choice with `--debug-log`:
189
+ Pass `--debug` (or set `WIP_DEBUG=1`) to see where time is going: wip prints each step it takes,
190
+ along with a periodic host resource snapshot (load, memory, disk I/O, top processes), so a hang is
191
+ visible even before a command produces output. See
192
+ [Debug Output](https://github.com/slidict/wip/wiki/Debug-Output) for the full behavior and the
193
+ `--debug-log` option.
473
194
 
474
- - `--debug-log=-` forces snapshots inline even for `-it` commands (only useful if you know your
475
- terminal/pager can tolerate the interleaving).
476
- - `--debug-log=PATH` always writes snapshots to `PATH`, including for non-TTY commands, e.g. to
477
- keep every run's snapshots in one place: `wip rails c --debug --debug-log=/tmp/wip-debug.log`.
478
-
479
- ## doctor
480
-
481
- Each check prints as `[OK]`, `[WARN]`, or `[FAIL]`. Warnings alone exit 0; a WSL2, interop, WSLC,
482
- or config problem that blocks execution exits 1. Git being unreachable from the real build
483
- environment is only a warning.
195
+ `wip doctor` prints `[OK]`/`[WARN]`/`[FAIL]` per check; warnings alone exit 0, a blocking problem
196
+ exits 1. See [`wip doctor`](https://github.com/slidict/wip/wiki/wip-doctor).
484
197
 
485
198
  ## Common errors
486
199
 
487
- ### WSLC not found
488
-
489
- Install or update the WSL container tooling, then run `wip doctor`. `auto` looks for `wslc.exe`,
490
- `wslc`, then `/mnt/c/Windows/System32/wslc.exe`, in that order.
200
+ - [WSLC not found](https://github.com/slidict/wip/wiki/WSLC-Not-Found)
201
+ - [Docker Hub / registry authentication](https://github.com/slidict/wip/wiki/Registry-Authentication)
202
+ - [Slow boot when the app directory is bind-mounted](https://github.com/slidict/wip/wiki/Fixing-a-Slow-Boot)
203
+ - [CPU architecture mismatch](https://github.com/slidict/wip/wiki/Architecture-Mismatch)
491
204
 
492
- ### Docker Hub authentication
493
-
494
- When `pull access denied` (or similar) is detected, wip suggests how to log in:
495
-
496
- ```bash
497
- wslc registry login -u <username> docker.io
498
- ```
499
-
500
- ### Slow boot when the app directory is bind-mounted
501
-
502
- wslc containers run in their own VM, so a bind-mounted host directory (`.:/app`) is always shared
503
- in over virtiofs, even when the host path is already on WSL's native filesystem. Frameworks that
504
- scan large directory trees at startup (Ruby's Zeitwerk autoloader, for example) issue many small
505
- per-file stat/open calls, and each one is a round trip through that layer — CPU on the Windows
506
- side can look busy while almost no data is actually transferred, and the process can appear hung
507
- for minutes with barely any resource usage to show for it.
508
-
509
- If a debug log shows a boot-time command "stuck" with low CPU/mem/IO in `resource_monitor`'s
510
- output, this is worth checking before assuming the app itself is broken. The fix is to stop
511
- bind-mounting the source live and mirror it into a named volume instead, so the app only ever
512
- touches fast native storage once it's running. `wip` does that for you — add a `sync:` block and
513
- it rewrites the mounts, mirrors before boot, and re-syncs on demand. See [Source
514
- sync](#source-sync).
515
-
516
- ### CPU architecture mismatch
517
-
518
- Check the image and publish a multi-arch (amd64/arm64) image:
519
-
520
- ```bash
521
- docker buildx imagetools inspect <image>
522
- docker buildx build \
523
- --platform linux/amd64,linux/arm64 \
524
- -t <image> \
525
- --push .
526
- ```
527
-
528
- ## FAQ
529
-
530
- **Which mode should I start with?**
531
- Pick whichever `mode:` fits your project — see [Which mode should you use?](#which-mode-should-you-use)
532
- for the breakdown.
533
-
534
- **Can I use `dependencies:` and `compose:` together?**
535
- No — `compose:` is mutually exclusive with `dependencies:`/`network`. Pick one orchestration path
536
- per project; see [Configuration](#configuration).
537
-
538
- **What's the difference between `mode: compose` and `mode: compose-native`?**
539
- `compose` delegates to a third-party compose-for-`wslc` binary you install yourself
540
- (`compose.command`); `compose-native` parses `compose.yml` itself and drives `wslc` directly, no
541
- external tool required. `compose-native`'s Compose coverage isn't frozen at whatever it handles
542
- today — it keeps growing until `wslc` ships native Compose support of its own. See
543
- [Compose mode](#compose-mode) and [Compose mode (native)](#compose-mode-native).
544
-
545
- **`dependencies:` already gives me sidecar containers — why would I need `compose-native` too?**
546
- `dependencies:` and `compose-native` aren't really alternatives to each other — they're for two
547
- different starting points. No `compose.yml`? Declare containers directly in `wip.yml`'s own shape
548
- with `dependencies:`. Already have a `compose.yml`? Reusing it is where `compose-native` and
549
- `mode: compose` both come in, so the real comparison is between those two, not against
550
- `dependencies:`: `mode: compose` reuses it too, but only by delegating to a third-party
551
- compose-for-`wslc` binary you install yourself; `compose-native` reuses the same `compose.yml`
552
- without installing anything external, parsing it and driving `wslc` directly — and gets a real
553
- `wslc run --rm` for `wip run` instead of the `exec` fallback `mode: compose` falls back to.
554
- `mode: compose`'s coverage is whatever the external tool you point it at supports; `compose-native`'s
555
- is maintained in this repo and actively extended, not treated as a permanent ceiling. See
556
- [Which mode should you use?](#which-mode-should-you-use) for the full picture.
557
-
558
- **What happens to `compose-native` once `wslc` gets official Compose support?**
559
- `compose-native` exists to close the gap for as long as `wslc` has no native Compose support of its
560
- own (tracked upstream in [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and
561
- we intend to keep extending its Compose coverage until that lands — see
562
- [Compose mode (native)](#compose-mode-native) and [Roadmap](#roadmap). `wip.yml`'s shape (`mode:`,
563
- `compose:`) isn't planned to change for existing `container`/`compose`/`compose-native` setups, so
564
- whatever we do once `wslc` catches up won't require rewriting your config.
565
-
566
- **Is `sync:` required?**
567
- No, it's entirely optional. Add it if boot times feel slow with a bind-mounted app directory — see
568
- [Slow boot when the app directory is bind-mounted](#slow-boot-when-the-app-directory-is-bind-mounted).
569
-
570
- **How does `wip` actually fix the slow bind-mount boot problem?**
571
- The slowness comes from `.:/app`-style bind mounts going through virtiofs, where frameworks that
572
- stat/open many small files at startup (e.g. Ruby's Zeitwerk) pay a round trip per file. A `sync:`
573
- block moves the app off that path entirely: the host source is mounted read-only, the app itself
574
- runs off a named volume (fast native storage inside the VM), and `wip` mirrors the read-only
575
- source into that volume with `rsync` — once before boot, and on demand afterward via `wip sync`
576
- (or continuously with `wip sync --watch`). Since the app never touches the bind mount directly, its
577
- own file access is no longer paying the virtiofs cost; the trade-off is a one-way, slightly-delayed
578
- mirror instead of an always-live view of host edits. See [Source sync](#source-sync) for the full
579
- config and [Slow boot when the app directory is bind-mounted](#slow-boot-when-the-app-directory-is-bind-mounted)
580
- for the root cause.
581
-
582
- **Is it safe to put passwords/secrets in `wip.yml`?**
583
- `wip config` masks any key matching token/password/secret/credential/auth when printing, but the
584
- raw file itself is not encrypted. Keep real secrets in your runtime environment or `.env` instead
585
- of committing them in `wip.yml` — but `.env` is only safe if it's actually untracked: add `.env` to
586
- `.gitignore` (and confirm with `git check-ignore .env`) before putting anything sensitive in it.
587
-
588
- **`wslc.exe`/`wslc` isn't found — what do I do?**
589
- See [WSLC not found](#wslc-not-found) under Common errors.
590
-
591
- **I'm migrating from `dip` — do I have to rename `interaction:`?**
592
- No, `wip.yml` accepts `interaction:` as-is — it's the primary spelling, same as in `dip`. `commands:`
593
- also works as an alias if you prefer it, but not both in the same file (that's a `ConfigError`). See
594
- [Container mode](#container-mode).
205
+ More errors, causes, and fixes are indexed on the wiki's
206
+ **[Troubleshooting & FAQ](https://github.com/slidict/wip/wiki/Troubleshooting-and-FAQ)** page,
207
+ including [Configuration Errors](https://github.com/slidict/wip/wiki/Configuration-Errors) (every
208
+ `ConfigError` and what triggers it).
595
209
 
596
210
  ## Development
597
211
 
@@ -606,7 +220,9 @@ bundle exec rake
606
220
 
607
221
  The test suite doesn't need WSLC — the resolution, build, and execution layers are all
608
222
  swappable. This project uses RuboCop for Ruby style and static analysis; `bundle exec rake` runs
609
- both RSpec and RuboCop. GitHub Actions checks them on Ruby 3.2, 3.3, 3.4, and 4.0.
223
+ both RSpec and RuboCop. GitHub Actions checks them on Ruby 3.2, 3.3, 3.4, and 4.0. See
224
+ [Development](https://github.com/slidict/wip/wiki/Development) and
225
+ [Architecture](https://github.com/slidict/wip/wiki/Architecture) on the wiki for more.
610
226
 
611
227
  ## Contributing
612
228
 
@@ -614,49 +230,6 @@ Bug reports and pull requests are welcome on [GitHub](https://github.com/slidict
614
230
  [CONTRIBUTING.md](CONTRIBUTING.md) for commit conventions, versioning policy, and the PR
615
231
  checklist.
616
232
 
617
- ## Roadmap
618
-
619
- `wip` already covers most of what [`dip`](https://github.com/bibendi/dip) adds on top of Compose —
620
- named commands (`interaction:`), `run`/`exec` hidden behind a single verb, `.env` passthrough, and
621
- sidecar services via `dependencies:` + `network:`. Rather than waiting on `wslc`'s own Compose
622
- support ([microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)) or an external
623
- compose-for-`wslc` tool staying complete, `mode: compose-native` (see
624
- [Compose mode (native)](#compose-mode-native)) parses `compose.yml` itself and drives `wslc`
625
- directly — no external binary in the loop, `wip run` gets a real `--rm` container, and iterating
626
- on the parser is faster than chasing a third-party tool's own bugs. It's still a deliberately
627
- minimal subset (`depends_on` ordering but no health checks, single-container `logs`, no named
628
- volumes/scaling), and `dependencies:` + `network:` remains the escape hatch for sidecars it
629
- doesn't model. Fuller Compose semantics beyond that subset stay behind delegating to a separate
630
- compose-for-`wslc` tool in [compose mode](#compose-mode) — which of those actually work is
631
- entirely up to the external tool you point `compose.command` at; `wip` only forwards
632
- `-f FILE [-p PROJECT] up|down|exec|logs`, so treat that list as what Compose offers, not as
633
- something `wip` guarantees. See that section for what compose mode covers
634
- and its current limitations (`run`, and `interaction:` of type `run`/`build`).
635
-
636
- Beyond Compose parity, a resident/daemon process, a GUI, PowerShell-specific tuning, direct
637
- registry API/manifest parsing, self-update, and plugins are all unimplemented and not currently
638
- planned. What's still planned for `wip`, roughly in priority order:
639
-
640
- 1. **`wip provision`** — a dip-style one-shot bootstrap hook (build → up deps → install deps →
641
- create/migrate/seed DB) so a new contributor can go from `git clone` to a working environment
642
- in two commands (`wip provision && wip up`).
643
- 2. **Config file merging** — `--config` currently accepts one file; support layering
644
- (`wip.yml` + `wip.override.yml`, or a `WIP_CONFIG` list) for dev/CI/debug variants without
645
- duplicating the whole file, plus a `wip config --resolved` view of the merged result.
646
- 3. **Bind-mount boot time (`rails c`, `bundle`, ...)** — commands like `wip rails c` still start
647
- noticeably slower than the equivalent under `docker compose`, mostly from WSL2 bind-mounted
648
- (`.:/app`-style) volumes doing many small reads for gems/`node_modules` (use `--debug` to
649
- confirm it's disk I/O and not `wip`'s own overhead). [Source sync](#source-sync) works around
650
- this today by running the app off a named volume and mirroring the host tree into it, at the
651
- cost of a one-way sync with a short delay. A tighter loop (host-side file watching instead of
652
- interval polling, two-way sync) is the natural next step. We're also hoping for improvements on
653
- the `wslc` side itself (faster bind-mount/cache behavior); `wip` will pick those up for free as
654
- soon as they land.
655
-
656
- Each of these should stay additive to the existing `wip.yml` shape — no breaking changes to
657
- `container`, `network`, `commands`, `dependencies`, or `compose` are planned. A resident daemon
658
- and a GUI remain out of scope.
659
-
660
233
  ## License
661
234
 
662
235
  [MIT License](LICENSE)
data/lib/wip/cli.rb CHANGED
@@ -16,6 +16,42 @@ module Wip
16
16
  class_option :debug_log, type: :string, desc: 'Where --debug snapshots go: a file path, or "-" for inline'
17
17
  default_task :dispatch
18
18
 
19
+ # Thor only recognizes class_options (--config, --debug, ...) once they
20
+ # appear after the command name (`wip up --config foo`). Pull a leading
21
+ # run of them off the front of ARGV and reinsert it right after the
22
+ # command name so `wip --config foo up` works too.
23
+ GLOBAL_SWITCHES = {
24
+ '--config' => :value,
25
+ '--env-file' => :value,
26
+ '--debug' => :flag,
27
+ '--debug-log' => :value
28
+ }.freeze
29
+
30
+ def self.start(given_args = ARGV, config = {})
31
+ super(reorder_global_options(given_args), config)
32
+ end
33
+
34
+ def self.reorder_global_options(args)
35
+ remaining = args.dup
36
+ extracted = []
37
+
38
+ until remaining.empty?
39
+ name, _, inline_value = remaining.first.to_s.partition('=')
40
+ kind = GLOBAL_SWITCHES[name]
41
+ break unless kind
42
+
43
+ switch = remaining.shift
44
+ extracted << switch
45
+ extracted << remaining.shift if kind == :value && inline_value.empty? && !remaining.empty?
46
+ end
47
+
48
+ command_index = remaining.index { |arg| !arg.start_with?('-') }
49
+ return args if command_index.nil?
50
+
51
+ remaining.insert(command_index + 1, *extracted)
52
+ remaining
53
+ end
54
+
19
55
  def self.exit_on_failure? = true
20
56
 
21
57
  # Thor only falls back to the default task when no command name is given at
@@ -82,21 +118,41 @@ module Wip
82
118
  progress&.finish
83
119
  end
84
120
 
121
+ # Real Compose values that trigger a restart when a container's exited; `no` (the default)
122
+ # and anything unrecognized both mean "leave it alone." Exact match only — a typo'd value
123
+ # like "always-invalid" must stay inert, not accidentally match via a loose prefix check.
124
+ AUTO_RESTART_POLICIES = %w[always unless-stopped].freeze
125
+ ON_FAILURE_POLICY = /\Aon-failure(?::\d+)?\z/ # optional `:MAX_RETRIES` suffix, e.g. "on-failure:3"
126
+
127
+ # WSLC's `WslcContainerState` (confirmed via microsoft/WSL's own docs and
128
+ # ContainerModel.h — ContainerInformation#State has no custom JSON enum serializer, so
129
+ # nlohmann::json emits its raw ordinal): 0 invalid, 1 created, 2 running, 3 exited, 4
130
+ # deleted. Unlike Docker, there's no separate "dead" state — only `exited` is a live,
131
+ # restartable exit; `deleted` means the container itself is gone (needs `wip up` to
132
+ # recreate it, not `start`).
133
+ WSLC_CONTAINER_STATE_EXITED = 3
134
+
85
135
  desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
86
136
  option :detach, type: :boolean, default: false, aliases: '-d'
87
137
  option :sync, type: :boolean, default: true, desc: 'Mirror the source into the sync volume first (--no-sync skips)'
88
138
  option :no_cache, type: :boolean, default: false, desc: 'Build compose-native images without cached layers'
139
+ option :watch, type: :boolean, default: false, aliases: '-w',
140
+ desc: 'Poll dependencies and restart any exited one whose restart: allows it (implies -d)'
141
+ option :interval, type: :numeric, default: 5, desc: 'Seconds between --watch polls (default: 5)'
89
142
  def up
90
- if load_config.compose?
91
- sync_before_boot if options[:sync]
92
- return execute(compose_bridge.up(detach: options[:detach]), interactive: tty?(!options[:detach]))
93
- end
143
+ return up_via_compose_bridge if load_config.compose?
144
+
145
+ # Validated up front, before any startup side effect (image build, network/dependency/
146
+ # container creation) — otherwise a bad --interval would only surface as a ConfigError
147
+ # after already bringing everything up.
148
+ interval = restart_interval if options[:watch]
94
149
 
95
150
  ensure_compose_images
96
151
  ensure_network
97
152
  sidecar_names.each { |name| ensure_dependency(name) }
98
153
  sync_before_boot if options[:sync]
99
154
  ensure_container
155
+ watch_restarts(interval) if options[:watch]
100
156
  end
101
157
 
102
158
  desc 'sync', 'Mirror the source tree into the sync volume'
@@ -194,7 +250,7 @@ module Wip
194
250
 
195
251
  desc 'dispatch COMMAND [ARGS...]', 'Run a command defined in wip.yml'
196
252
  def dispatch(name = nil, *arguments)
197
- raise ConfigError, 'A command is required' unless name
253
+ return help if name.nil?
198
254
 
199
255
  values = load_config.command(name) || raise(ConfigError, "Unknown command: #{name}")
200
256
  return dispatch_compose(name, values, arguments) if load_config.compose?
@@ -407,16 +463,79 @@ module Wip
407
463
  warn "wip: run `wip sync --watch` in another terminal to keep #{settings.target} up to date"
408
464
  end
409
465
 
466
+ def up_via_compose_bridge
467
+ if options[:watch]
468
+ raise ConfigError, '`wip up --watch` is not supported under mode: compose (wip never parses a ' \
469
+ 'compose.yml service list in that mode, so there is nothing to poll)'
470
+ end
471
+
472
+ sync_before_boot if options[:sync]
473
+ execute(compose_bridge.up(detach: options[:detach]), interactive: tty?(!options[:detach]))
474
+ end
475
+
410
476
  def ensure_container
411
477
  container = load_config.container
412
- interactive = tty?(!options[:detach])
478
+ interactive = tty?(!detach?)
413
479
  if resource_exists?(builder.find)
414
480
  warn "wip: starting existing container '#{container}'"
415
- execute(builder.start(detach: options[:detach]), interactive: interactive)
481
+ execute(builder.start(detach: detach?), interactive: interactive)
416
482
  else
417
483
  warn "wip: container '#{container}' not found, creating it"
418
- execute(builder.up(detach: options[:detach]), interactive: interactive)
484
+ execute(builder.up(detach: detach?), interactive: interactive)
485
+ end
486
+ end
487
+
488
+ # --watch polls in a loop after boot, which can't share this one thread with an attached
489
+ # (`-it`) primary container — force the same effective behavior `-d` gives ensure_container.
490
+ def detach? = options[:detach] || options[:watch]
491
+
492
+ # Approximates Docker Compose's `restart:` policy via a foreground poll loop — not a
493
+ # background daemon/service (see README "Roadmap"); the same opt-in, keep-a-terminal-open
494
+ # shape as `wip sync --watch`.
495
+ def watch_restarts(interval)
496
+ names = load_config.dependencies.keys
497
+ warn "wip: watching #{names.join(', ')} for exited restart: containers every #{interval}s " \
498
+ '(running detached; Ctrl-C to stop)'
499
+ loop do
500
+ names.each { |name| restart_if_exited(name) }
501
+ sleep interval
419
502
  end
503
+ rescue Interrupt
504
+ warn "\nwip: watch stopped"
505
+ end
506
+
507
+ def restart_interval
508
+ raise ConfigError, '--interval must be a positive number' unless options[:interval].positive?
509
+
510
+ options[:interval]
511
+ end
512
+
513
+ # Status-based, not transition-based: each tick checks current state, not whether it *just*
514
+ # exited. Can't distinguish "crashed on its own" from "you ran `wip stop`/`wip down` in
515
+ # another terminal" — Ctrl-C this loop first if you're about to do either (see README).
516
+ def restart_if_exited(name)
517
+ policy = load_config.dependency(name)['restart']
518
+ return unless auto_restart?(policy)
519
+ return unless container_status(name) == WSLC_CONTAINER_STATE_EXITED
520
+
521
+ warn "wip: '#{name}' has exited, restarting it (restart: #{policy})"
522
+ execute(builder.dependency_start(name), exit_on_failure: false)
523
+ end
524
+
525
+ def auto_restart?(policy) = AUTO_RESTART_POLICIES.include?(policy) || ON_FAILURE_POLICY.match?(policy.to_s)
526
+
527
+ # Isolated to this one method: if a future wslc release changes this shape, fixing it here
528
+ # is a one-line change. Logs the raw entry under --debug so that's immediately visible
529
+ # instead of silently no-op'ing forever.
530
+ def container_status(name)
531
+ code, output = probe(builder.dependency_find(name))
532
+ return nil unless code.zero?
533
+
534
+ entry = JSON.parse(output).first
535
+ warn "wip: [debug] '#{name}': #{entry.inspect}" if debug?
536
+ entry&.fetch('State', nil)
537
+ rescue JSON::ParserError
538
+ nil
420
539
  end
421
540
  end
422
541
  end
@@ -15,15 +15,17 @@ module Wip
15
15
  # "Compose mode (native)") — once wslc ships that support, or a
16
16
  # compose-for-wslc tool reliably supports `run`.
17
17
  class ComposeFile
18
- Service = Struct.new(:image, :build, :command, :env, :ports, :volumes, :workdir, :user, :depends_on, :profiles,
19
- keyword_init: true)
18
+ Service = Struct.new(:image, :build, :command, :env, :ports, :volumes, :workdir, :user, :restart, :depends_on,
19
+ :profiles, keyword_init: true)
20
20
 
21
- SERVICE_KEYS = %w[image build command environment ports volumes working_dir user depends_on profiles].freeze
21
+ SERVICE_KEYS = %w[image build command environment ports volumes working_dir user restart depends_on
22
+ profiles].freeze
22
23
  # Real Compose keys that read as meaningful here but have nothing to map onto: TTY/stdin
23
24
  # allocation is decided per invocation (CommandBuilder#tty?), not per service; every
24
25
  # service already shares one project network (config.rb); and `wslc run`/`exec` has no
25
- # restart-policy or capability flag to forward `restart:`/`cap_add:` to.
26
- IGNORED_SERVICE_KEYS = %w[tty stdin_open networks restart cap_add].freeze
26
+ # capability flag to forward `cap_add:` to. (`restart:` used to be here too, silently
27
+ # ignored it's parsed below now and approximated by `wip up --watch`, see cli.rb.)
28
+ IGNORED_SERVICE_KEYS = %w[tty stdin_open networks cap_add].freeze
27
29
  BUILD_KEYS = %w[context dockerfile args shadow_context].freeze
28
30
  SUPPORTED_CONDITIONS = %w[service_started].freeze
29
31
  LIST_HINT = 'only supports short syntax ("host:container"), not long-syntax mappings'
@@ -65,14 +67,14 @@ module Wip
65
67
  end.to_h
66
68
  end
67
69
 
68
- # Shaped like Config::DEPENDENCY_DEFAULTS expects: image/command/env/ports/volumes/workdir,
70
+ # Shaped like Config::DEPENDENCY_DEFAULTS expects: image/command/env/ports/volumes/workdir/restart,
69
71
  # in dependency order so callers iterating sidecars start them before their dependents.
70
72
  def to_dependencies_hash
71
73
  startable_order.to_h do |name|
72
74
  service = @services.fetch(name)
73
75
  [name, { 'image' => service.build ? image_tag(name, service) : service.image, 'command' => service.command,
74
76
  'env' => service.env, 'ports' => service.ports, 'volumes' => service.volumes,
75
- 'workdir' => service.workdir, 'user' => service.user }]
77
+ 'workdir' => service.workdir, 'user' => service.user, 'restart' => service.restart }]
76
78
  end
77
79
  end
78
80
 
@@ -97,6 +99,7 @@ module Wip
97
99
  Service.new(image: image, build: build, command: normalize_command(entry['command']),
98
100
  env: normalize_env(name, entry['environment']), **normalize_service_lists(name, entry),
99
101
  workdir: presence(entry['working_dir']), user: presence(entry['user']),
102
+ restart: normalize_restart(entry['restart']),
100
103
  depends_on: normalize_depends_on(name, entry['depends_on']))
101
104
  end
102
105
 
@@ -250,6 +253,20 @@ module Wip
250
253
  order << name
251
254
  end
252
255
 
256
+ # Compose's own default (`no`) applies whether restart: is absent or explicitly falsy —
257
+ # including the very common unquoted `restart: no`, which YAML resolves to the boolean
258
+ # `false`, not the string "no" (confirmed against this repo's own Psych: `YAML.safe_load
259
+ # ("restart: no")` => {"restart"=>false}). Every other value is accepted as-is, even ones
260
+ # outside always/unless-stopped/on-failure[:N]: this parser's job is to read what's in
261
+ # compose.yml, not police it — `wip up --watch` (cli.rb) decides which values it acts on.
262
+ # Rejecting a real, valid Compose value here would break projects that already work today
263
+ # (compose.yml predates wip, unlike wip.yml itself).
264
+ def normalize_restart(value)
265
+ return 'no' if value == false
266
+
267
+ presence(value) || 'no'
268
+ end
269
+
253
270
  def presence(value) = value.to_s.empty? ? nil : value.to_s
254
271
  end
255
272
  end
data/lib/wip/config.rb CHANGED
@@ -8,7 +8,7 @@ module Wip
8
8
  # Applied to every dependencies: entry, primary container included — there
9
9
  # is no separate, differently-shaped bucket for "the one you exec into."
10
10
  DEPENDENCY_DEFAULTS = { 'workdir' => nil, 'user' => nil, 'interactive' => false, 'remove' => true,
11
- 'env' => {}, 'ports' => [], 'volumes' => [] }.freeze
11
+ 'env' => {}, 'ports' => [], 'volumes' => [], 'restart' => 'no' }.freeze
12
12
  SECRET_PATTERN = /token|password|secret|credential|auth/i
13
13
  # Which orchestration path `up`/`down`/`sync`/etc. take. Explicit rather than
14
14
  # inferred from a `compose:` block's presence, so a config reader doesn't have
@@ -209,6 +209,16 @@ module Wip
209
209
  raise ConfigError, "dependencies.#{name} must set image" if entry['image'].to_s.empty?
210
210
 
211
211
  entry['env']&.transform_values!(&:to_s)
212
+ normalize_dependency_restart!(entry)
213
+ end
214
+
215
+ # Mirrors ComposeFile#normalize_restart: an unquoted `restart: no` parses as the boolean
216
+ # false rather than the string "no", and an explicit nil/"" should default the same way an
217
+ # absent key does (DEPENDENCY_DEFAULTS only fills in a genuinely *missing* key).
218
+ def normalize_dependency_restart!(entry)
219
+ return unless entry.key?('restart')
220
+
221
+ entry['restart'] = 'no' if entry['restart'] == false || entry['restart'].to_s.empty?
212
222
  end
213
223
 
214
224
  def stringify(object)
@@ -11,6 +11,7 @@ module Wip
11
11
  # --template values wip init accepts, and the label used in the exclude: comment.
12
12
  TEMPLATE_LABELS = {
13
13
  'rails' => 'Rails',
14
+ 'ruby' => 'Ruby',
14
15
  'node' => 'Node.js',
15
16
  'rust' => 'Rust',
16
17
  'csharp' => 'C#'
@@ -22,6 +23,7 @@ module Wip
22
23
  TEMPLATE_EXCLUDES = {
23
24
  'rails' => %w[.git log/ tmp/ storage/ public/assets/ public/packs/ .bundle/ vendor/bundle/
24
25
  coverage/ node_modules/],
26
+ 'ruby' => %w[.git log/ tmp/ .bundle/ vendor/bundle/ coverage/],
25
27
  'node' => %w[.git node_modules/ dist/ build/ .next/ .cache/ coverage/],
26
28
  'rust' => %w[.git target/],
27
29
  'csharp' => %w[.git bin/ obj/ .vs/ packages/]
@@ -159,6 +161,10 @@ module Wip
159
161
  # optional; remove the container after each run
160
162
  remove: true
161
163
 
164
+ # optional; restart policy `wip up --watch` polls for — always/unless-stopped/on-failure
165
+ # restart an exited container; "no" (the default) never does — see README
166
+ restart: "no"
167
+
162
168
  # optional; environment variables passed to the container, e.g. {FOO: bar}
163
169
  env: {}
164
170
 
data/lib/wip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- VERSION = '1.1.2'
4
+ VERSION = '1.1.4'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wslc-wip
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors
@@ -56,15 +56,14 @@ files:
56
56
  - lib/wip/sync_settings.rb
57
57
  - lib/wip/variable_interpolation.rb
58
58
  - lib/wip/version.rb
59
- homepage: https://wslc-wip.slidict.com/
59
+ homepage: https://github.com/slidict/wip
60
60
  licenses:
61
61
  - MIT
62
62
  metadata:
63
63
  rubygems_mfa_required: 'true'
64
- homepage_uri: https://wslc-wip.slidict.com/
65
- source_code_uri: https://github.com/slidict/wip
64
+ homepage_uri: https://github.com/slidict/wip
66
65
  changelog_uri: https://github.com/slidict/wip/releases
67
- bug_tracker_uri: https://github.com/slidict/wip/issues
66
+ source_code_uri: https://github.com/slidict/wip/tree/v1.1.4
68
67
  rdoc_options: []
69
68
  require_paths:
70
69
  - lib