wslc-wip 0.14.2 → 0.16.0

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: 6f9b53bdcacc5491c788acf9af933f377cc0afe046ef133391ae866700145c83
4
- data.tar.gz: b4ad717fba296d8082bc272238a8dd83afa206f7aaca67628e47186a0551c531
3
+ metadata.gz: c93fe67017509908d6ea81f4a80555ef193991504fb9f19cba7e598bd231635b
4
+ data.tar.gz: a19a5d66f81bfa19be592a778cba3369301f2f1c7f3c1a24a6a111513a2cceca
5
5
  SHA512:
6
- metadata.gz: d65e654b92b30b19a987c2b239014bb0071b92bd99d0fdb09728e23ce3948488a330ebbf61878d7c81c7b4f588888d87720e41542f91e947c7358a02e495fbe2
7
- data.tar.gz: 3008f5c32bfaa6771ef60a75d8445a427b3ddcbc9ef19f226602275a3b4eb997a6f9b7d99fc649dcc481dcfacbeea8b32e0c03a6902934f82d6c7534a08901af
6
+ metadata.gz: a3a6bd4bae10abee5389f7314dbbf7693882e7559412c9207569a7bed16206aceef5ed781b01ab96b94791dfe2a20bb16944b4fad7d2bfcd3e5fa74667576343
7
+ data.tar.gz: 439b070a2f3f00f9396b595dd939c251d1854cf8ba65ee3d6b93c55c512d90d85090e2be2b77cd2d68a8fc0aae149125b235fbba3fbd4d57a943dad7dc512410
data/README.md CHANGED
@@ -181,6 +181,17 @@ sync:
181
181
  tag: null # optional (default: "wip-sync-<container>:latest")
182
182
  ```
183
183
 
184
+ `wip init --template NAME` writes `exclude`'s default list live, picked from that stack's own
185
+ `github/gitignore` template:
186
+
187
+ | `--template` | Stack | Default `exclude` |
188
+ |---|---|---|
189
+ | `rails` | Rails | `.git`, `log/`, `tmp/`, `storage/`, `public/assets/`, `public/packs/`, `.bundle/`, `vendor/bundle/`, `coverage/`, `node_modules/` |
190
+ | `node` | Node.js | `.git`, `node_modules/`, `dist/`, `build/`, `.next/`, `.cache/`, `coverage/` |
191
+ | `rust` | Rust | `.git`, `target/` |
192
+ | `csharp` | C# | `.git`, `bin/`, `obj/`, `.vs/`, `packages/` |
193
+ | (omitted) | — | `.git`, `tmp/`, `node_modules/` |
194
+
184
195
  Everything below `sync:` is optional — `sync: {}` alone already works. With it in place:
185
196
 
186
197
  - Any `volumes` entry on the primary container mounting `target` (the usual `.:/app`) is replaced
@@ -329,8 +340,11 @@ valid compose.yml over sections it doesn't need to look at:
329
340
  `compose.yml`, not wherever `wip` is invoked from), `command` (shell or exec form), `environment`
330
341
  (mapping or `KEY=VALUE` array — a mapping value must not be null; host environment pass-through
331
342
  isn't supported), `ports`/`volumes` (short syntax only — `"host:container"` strings, not
332
- long-syntax mappings), `working_dir`, `depends_on` (ordering only — a `condition:` other than
333
- `service_started` is rejected, since there's no health-check support).
343
+ long-syntax mappings), `working_dir`, `user`, `depends_on` (ordering only — a `condition:` other
344
+ than `service_started` is rejected, since there's no health-check support). `tty`, `stdin_open`,
345
+ and `networks` are accepted but silently ignored: TTY/stdin allocation is already decided per
346
+ invocation (see "TTY allocation" below), not fixed per service, and every service already shares
347
+ the one project network `compose.project` sets up.
334
348
  - `wip logs` takes at most one `SERVICE` (defaulting to `compose.service`) — `wslc logs`, like
335
349
  `docker logs`, follows a single container, unlike a real compose tool's multi-service view.
336
350
  - `sync:` behaves exactly like `mode: container`'s (falls back to the primary service's own image,
@@ -341,7 +355,7 @@ valid compose.yml over sections it doesn't need to look at:
341
355
 
342
356
  | Command | Description |
343
357
  |---|---|
344
- | `wip init [--force]` | Write a starter `wip.yml`: `mode: compose-native` if a `compose.yml`/`docker-compose.yml` is found next to it, `mode: container` otherwise. Refuses to overwrite an existing `wip.yml` unless `--force` |
358
+ | `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` |
345
359
  | `wip version` | wip's version, plus WSLC's if it can be detected |
346
360
  | `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
347
361
  | `wip config` | Print the effective configuration (secrets masked) |
@@ -470,21 +484,28 @@ checklist.
470
484
 
471
485
  ## Not in the initial release
472
486
 
473
- Full Compose compatibility isn't reimplemented in `wip` itself — `mode: compose-native` only
474
- covers a minimal subset (see [Compose mode (native)](#compose-mode-native)), and full parity is
475
- otherwise available by delegating to a third-party compose-for-`wslc` tool (see
476
- [Compose mode](#compose-mode)). A resident/daemon process, a GUI, PowerShell-specific tuning,
477
- direct registry API/manifest parsing, self-update, and plugins are all unimplemented.
487
+ Full Compose compatibility isn't reimplemented in `wip` itself — `mode: compose-native` parses
488
+ `compose.yml` and drives `wslc` directly, but only covers a minimal subset (see
489
+ [Compose mode (native)](#compose-mode-native)); full parity is otherwise available by delegating
490
+ to a third-party compose-for-`wslc` tool (see [Compose mode](#compose-mode)). A resident/daemon
491
+ process, a GUI, PowerShell-specific tuning, direct registry API/manifest parsing, self-update, and
492
+ plugins are all unimplemented.
478
493
 
479
494
  ## Roadmap
480
495
 
481
496
  `wip` already covers most of what [`dip`](https://github.com/bibendi/dip) adds on top of Compose —
482
497
  named commands (`commands:`), `run`/`exec` hidden behind a single verb, `.env` passthrough, and
483
- sidecar services via `dependencies:` + `network:`. Fuller Compose semantics
484
- (`depends_on` ordering/health checks, log aggregation, named volumes, profiles, scaling) are now
485
- handled by delegating to a separate compose-for-`wslc` tool in [compose mode](#compose-mode)
486
- rather than being reimplemented in `wip` itself. Which of those actually work is entirely up to
487
- the external tool you point `compose.command` at `wip` only forwards
498
+ sidecar services via `dependencies:` + `network:`. Rather than waiting on `wslc`'s own Compose
499
+ support ([microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)) or an external
500
+ compose-for-`wslc` tool staying complete, `mode: compose-native` (see
501
+ [Compose mode (native)](#compose-mode-native)) parses `compose.yml` itself and drives `wslc`
502
+ directly — no external binary in the loop, `wip run` gets a real `--rm` container, and iterating
503
+ on the parser is faster than chasing a third-party tool's own bugs. It's still a deliberately
504
+ minimal subset (`depends_on` ordering but no health checks, single-container `logs`, no named
505
+ volumes/scaling), and `dependencies:` + `network:` remains the escape hatch for sidecars it
506
+ doesn't model. Fuller Compose semantics beyond that subset stay behind delegating to a separate
507
+ compose-for-`wslc` tool in [compose mode](#compose-mode) — which of those actually work is
508
+ entirely up to the external tool you point `compose.command` at; `wip` only forwards
488
509
  `-f FILE [-p PROJECT] up|down|exec|logs`, so treat that list as what Compose offers, not as
489
510
  something `wip` guarantees. See that section for what compose mode covers
490
511
  and its current limitations (`run`, and `commands:` of type `run`/`build`). What's still planned
data/lib/wip/cli.rb CHANGED
@@ -42,11 +42,13 @@ module Wip
42
42
 
43
43
  desc 'init', 'Create a starter wip.yml (detects an existing compose.yml/docker-compose.yml)'
44
44
  option :force, type: :boolean, default: false, desc: 'Overwrite an existing wip.yml'
45
+ option :template, type: :string,
46
+ desc: "sync.exclude preset: #{Initializer::TEMPLATE_LABELS.keys.join(', ')} (default: none)"
45
47
  def init
46
48
  path = Pathname(options[:config] || ConfigLoader::FILENAME).expand_path
47
49
  raise Error, "#{path} already exists (use --force to overwrite)" if path.file? && !options[:force]
48
50
 
49
- initializer = Initializer.new(dir: path.dirname)
51
+ initializer = Initializer.new(dir: path.dirname, template: options[:template])
50
52
  path.write(initializer.call)
51
53
  warn "wip: wrote #{path} (mode: #{initializer.compose? ? 'compose-native' : 'container'})"
52
54
  end
@@ -292,6 +294,7 @@ module Wip
292
294
  def ensure_compose_images
293
295
  load_config.compose_build_specs.each_value do |spec|
294
296
  extra = spec['dockerfile'] ? ['-f', spec['dockerfile']] : []
297
+ spec['args']&.each { |key, value| extra.push('--build-arg', "#{key}=#{value}") }
295
298
  execute(builder.build(settings: { 'context' => spec['context'], 'tag' => spec['tag'] }, extra: extra))
296
299
  end
297
300
  end
@@ -159,17 +159,26 @@ module Wip
159
159
  private
160
160
 
161
161
  def options(values, include_container: false, include_publish: true, sync: true)
162
- result = []
163
- result.push('-w', values['workdir']) unless values['workdir'].to_s.empty?
162
+ result = scalar_options(values)
164
163
  merged_env(values).each { |key, value| result.push('-e', "#{key}=#{value}") }
165
- if include_publish
166
- Array(values['ports']).each { |port| result.push('-p', port.to_s) }
167
- volume_specs(values, sync: sync).each { |volume| result.push('-v', volume) }
168
- end
164
+ result.concat(publish_options(values, sync: sync)) if include_publish
169
165
  result << required(values, 'container') if include_container
170
166
  result
171
167
  end
172
168
 
169
+ def scalar_options(values)
170
+ result = []
171
+ result.push('-w', values['workdir']) unless values['workdir'].to_s.empty?
172
+ result.push('-u', values['user']) unless values['user'].to_s.empty?
173
+ result
174
+ end
175
+
176
+ def publish_options(values, sync:)
177
+ result = Array(values['ports']).flat_map { |port| ['-p', port.to_s] }
178
+ volume_specs(values, sync: sync).each { |volume| result.push('-v', volume) }
179
+ result
180
+ end
181
+
173
182
  # With sync configured, a live bind mount of the target (`.:/app`) is
174
183
  # swapped for the read-only source mount plus the named volume, so the
175
184
  # running app only ever touches the volume.
@@ -15,10 +15,19 @@ 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, :depends_on, keyword_init: true)
19
-
20
- SERVICE_KEYS = %w[image build command environment ports volumes working_dir depends_on].freeze
21
- BUILD_KEYS = %w[context dockerfile].freeze
18
+ Service = Struct.new(:image, :build, :command, :env, :ports, :volumes, :workdir, :user, :depends_on,
19
+ keyword_init: true)
20
+
21
+ SERVICE_KEYS = %w[image build command environment ports volumes working_dir user depends_on].freeze
22
+ # Real Compose keys that read as meaningful here but have nothing to map onto: TTY/stdin
23
+ # allocation is already decided per invocation (see CommandBuilder#tty?), not fixed per
24
+ # service; there's no per-service network to join beyond the one project network `network`
25
+ # (config.rb) already puts every service on; `wslc run`/`exec` has no restart-policy or
26
+ # capability flag to forward `restart:`/`cap_add:` to; and `profiles:` gates which services a
27
+ # real `docker compose up` starts by default, which doesn't apply here since wip never starts
28
+ # "every service" — only `compose.service` and whatever it reaches via `depends_on`.
29
+ IGNORED_SERVICE_KEYS = %w[tty stdin_open networks restart cap_add profiles].freeze
30
+ BUILD_KEYS = %w[context dockerfile args].freeze
22
31
  SUPPORTED_CONDITIONS = %w[service_started].freeze
23
32
 
24
33
  def self.load(path)
@@ -44,10 +53,10 @@ module Wip
44
53
  # name => {context:, dockerfile:, tag:} for every service with a build:.
45
54
  def build_specs
46
55
  @order.filter_map do |name|
47
- build = @services.fetch(name).build
48
- next unless build
56
+ service = @services.fetch(name)
57
+ next unless service.build
49
58
 
50
- [name, build.merge('tag' => image_tag(name))]
59
+ [name, service.build.merge('tag' => image_tag(name, service))]
51
60
  end.to_h
52
61
  end
53
62
 
@@ -56,35 +65,41 @@ module Wip
56
65
  def to_dependencies_hash
57
66
  @order.to_h do |name|
58
67
  service = @services.fetch(name)
59
- [name, { 'image' => service.build ? image_tag(name) : service.image, 'command' => service.command,
68
+ [name, { 'image' => service.build ? image_tag(name, service) : service.image, 'command' => service.command,
60
69
  'env' => service.env, 'ports' => service.ports, 'volumes' => service.volumes,
61
- 'workdir' => service.workdir }]
70
+ 'workdir' => service.workdir, 'user' => service.user }]
62
71
  end
63
72
  end
64
73
 
65
74
  private
66
75
 
67
- def image_tag(name) = "wip-compose-#{name}:latest"
76
+ # A service naming both `build:` and `image:` builds via the former and tags the
77
+ # result with the latter (real Compose's own rule for that combination), instead
78
+ # of the auto-generated tag a build:-only service gets.
79
+ def image_tag(name, service) = service.image || "wip-compose-#{name}:latest"
68
80
 
69
81
  def build_service(name, entry)
70
82
  raise ConfigError, "#{@path}: services.#{name} must be a mapping" unless entry.is_a?(Hash)
71
83
 
72
- unknown = entry.keys.map(&:to_s) - SERVICE_KEYS
84
+ unknown = entry.keys.map(&:to_s) - SERVICE_KEYS - IGNORED_SERVICE_KEYS
73
85
  raise ConfigError, "#{@path}: services.#{name} has unsupported key(s): #{unknown.join(', ')}" if unknown.any?
74
86
 
75
87
  image, build = image_or_build(name, entry)
76
88
  Service.new(image: image, build: build, command: normalize_command(entry['command']),
77
- env: normalize_env(name, entry['environment']),
78
- ports: normalize_list(name, entry['ports'], 'ports'),
79
- volumes: normalize_list(name, entry['volumes'], 'volumes'),
80
- workdir: presence(entry['working_dir']), depends_on: normalize_depends_on(name, entry['depends_on']))
89
+ env: normalize_env(name, entry['environment']), **normalize_service_lists(name, entry),
90
+ workdir: presence(entry['working_dir']), user: presence(entry['user']),
91
+ depends_on: normalize_depends_on(name, entry['depends_on']))
92
+ end
93
+
94
+ def normalize_service_lists(name, entry)
95
+ { ports: normalize_list(name, entry['ports'], 'ports'),
96
+ volumes: normalize_list(name, entry['volumes'], 'volumes') }
81
97
  end
82
98
 
83
99
  def image_or_build(name, entry)
84
100
  image = presence(entry['image'])
85
101
  build = normalize_build(name, entry['build'])
86
102
  raise ConfigError, "#{@path}: services.#{name} must set image or build" unless image || build
87
- raise ConfigError, "#{@path}: services.#{name} must not set both image and build" if image && build
88
103
 
89
104
  [image, build]
90
105
  end
@@ -118,27 +133,33 @@ module Wip
118
133
 
119
134
  context = resolve_context(presence(value['context']) || '.')
120
135
  dockerfile = presence(value['dockerfile'])
121
- { 'context' => context, 'dockerfile' => dockerfile && Pathname(context).join(dockerfile).to_s }.compact
136
+ { 'context' => context, 'dockerfile' => dockerfile && Pathname(context).join(dockerfile).to_s,
137
+ 'args' => normalize_kv(name, value['args'], 'build.args') }.compact
122
138
  end
123
139
 
124
140
  # build.context is relative to compose.yml's own directory (Compose's own rule),
125
141
  # not wherever `wip` happens to be invoked from.
126
142
  def resolve_context(raw) = Pathname(@path).expand_path.dirname.join(raw).to_s
127
143
 
128
- def normalize_env(name, value)
144
+ def normalize_env(name, value) = normalize_kv(name, value, 'environment')
145
+
146
+ # Shared by `environment:` and `build.args:` — both accept a mapping or a
147
+ # KEY=VALUE array, and neither supports host environment pass-through (a
148
+ # null mapping value, or a bare KEY with no `=`).
149
+ def normalize_kv(name, value, label)
129
150
  return {} unless value
130
151
 
131
152
  case value
132
- when Hash then normalize_env_mapping(name, value)
133
- when Array then normalize_env_array(name, value)
134
- else raise ConfigError, "#{@path}: services.#{name}.environment must be a mapping or an array of KEY=VALUE"
153
+ when Hash then normalize_kv_mapping(name, value, label)
154
+ when Array then normalize_kv_array(name, value, label)
155
+ else raise ConfigError, "#{@path}: services.#{name}.#{label} must be a mapping or an array of KEY=VALUE"
135
156
  end
136
157
  end
137
158
 
138
- def normalize_env_mapping(name, value)
159
+ def normalize_kv_mapping(name, value, label)
139
160
  value.to_h do |key, val|
140
161
  if val.nil?
141
- raise ConfigError, "#{@path}: services.#{name}.environment.#{key} must have a value " \
162
+ raise ConfigError, "#{@path}: services.#{name}.#{label}.#{key} must have a value " \
142
163
  '(host environment pass-through is not supported)'
143
164
  end
144
165
 
@@ -146,10 +167,10 @@ module Wip
146
167
  end
147
168
  end
148
169
 
149
- def normalize_env_array(name, value)
170
+ def normalize_kv_array(name, value, label)
150
171
  value.to_h do |item|
151
172
  key, val = item.to_s.split('=', 2)
152
- raise ConfigError, "#{@path}: services.#{name}.environment entries must be KEY=VALUE" unless val
173
+ raise ConfigError, "#{@path}: services.#{name}.#{label} entries must be KEY=VALUE" unless val
153
174
 
154
175
  [key, val]
155
176
  end
data/lib/wip/config.rb CHANGED
@@ -7,7 +7,7 @@ module Wip
7
7
  class Config
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
- DEPENDENCY_DEFAULTS = { 'workdir' => nil, 'interactive' => false, 'remove' => true,
10
+ DEPENDENCY_DEFAULTS = { 'workdir' => nil, 'user' => nil, 'interactive' => false, 'remove' => true,
11
11
  'env' => {}, 'ports' => [], 'volumes' => [] }.freeze
12
12
  SECRET_PATTERN = /token|password|secret|credential|auth/i
13
13
  # Which orchestration path `up`/`down`/`sync`/etc. take. Explicit rather than
@@ -8,72 +8,54 @@ module Wip
8
8
  # mode: compose-native and mode: container, since that's the one decision
9
9
  # `wip init` can't leave to a placeholder.
10
10
  class Initializer
11
- # Appended to sync: in both templates. Every key here is either a plain constant
12
- # default (safe to state literally) or, where commented, a value SyncSettings
13
- # derives from another key (target from workdir, volume from the container/service
14
- # name) — those stay unset so they keep tracking that key instead of going stale.
15
- SYNC_EXTRAS = <<~YAML.chomp.gsub(/^/, ' ')
16
- source: . # optional; host path to mirror (default: wip.yml directory)
17
- # target: /app # optional; in-container mount point for the mirror (default: the container's workdir, else /app)
18
- mount: /host-src # optional; read-only host mount inside the container
19
- # volume: app-src # optional; named volume holding the mirrored tree (default: "<container>-src")
20
- delete: true # optional; rsync --delete so removals on the host are mirrored too
21
- exclude: [] # optional; rsync --exclude patterns, e.g. ["node_modules", "*.log"]
22
- command: rsync # optional; mirror binary to shell out to
23
- options: [] # optional; extra flags appended to the mirror command
24
- interval: 2 # optional; seconds between mirrors
25
- mode: exec # optional; exec (default, mirrors into the running container) | run (always a fresh container)
26
- # image: your/image:tag # only needed if sync.mode: run (or under mode: compose); image the mirror container runs
27
- # build: # alternative to image — let wip build a small dedicated mirror image itself
28
- # dockerfile: |
29
- # FROM alpine:latest
30
- # RUN apk add --no-cache rsync
31
- # tag: wip-sync-app:latest # optional (default: wip-sync-<container>:latest)
32
- YAML
11
+ # --template values wip init accepts, and the label used in the exclude: comment.
12
+ TEMPLATE_LABELS = {
13
+ 'rails' => 'Rails',
14
+ 'node' => 'Node.js',
15
+ 'rust' => 'Rust',
16
+ 'csharp' => 'C#'
17
+ }.freeze
18
+
19
+ # sync.exclude patterns written live for each --template. They mirror that
20
+ # stack's own github/gitignore template directories that are either
21
+ # regenerated inside the container or too large/irrelevant to mirror over rsync.
22
+ TEMPLATE_EXCLUDES = {
23
+ 'rails' => %w[.git log/ tmp/ storage/ public/assets/ public/packs/ .bundle/ vendor/bundle/
24
+ coverage/ node_modules/],
25
+ 'node' => %w[.git node_modules/ dist/ build/ .next/ .cache/ coverage/],
26
+ 'rust' => %w[.git target/],
27
+ 'csharp' => %w[.git bin/ obj/ .vs/ packages/]
28
+ }.freeze
29
+
30
+ # Used when --template is omitted — the same starting point the README's own
31
+ # example uses.
32
+ FALLBACK_EXCLUDES = %w[.git tmp/ node_modules/].freeze
33
33
 
34
34
  # Appended to both templates after dependencies/compose. commands: itself is left
35
35
  # commented since an empty commands: {} is indistinguishable from omitting the key.
36
36
  COMMANDS_EXAMPLE = <<~YAML.chomp
37
- # commands: # optional; custom subcommands, e.g. `wip test` — see README
37
+ # optional; custom subcommands, e.g. `wip test` — see README
38
+ # commands:
38
39
  # test:
39
- # type: exec # optional; exec (default, runs in the running container) | run (fresh container) | build
40
- # command: bundle exec rspec # TODO
41
- YAML
42
-
43
- CONTAINER_TEMPLATE = <<~YAML.freeze
44
- version: 1
45
- mode: container # container | compose | compose-native (see README)
46
- container: app # TODO: rename freely, as long as it matches a key under dependencies: below
47
-
48
- wslc:
49
- command: auto # optional; wslc binary/path wip shells out to ("auto" resolves it for you)
40
+ # optional; exec (default, runs in the running container) | run (fresh container) | build
41
+ # type: exec
50
42
 
51
- # network: my-network # optional; container network name (mode: container only)
52
-
53
- dependencies:
54
- app: # this is the container wip creates, execs into, and runs commands in
55
- image: your/image:tag # TODO: image to run
56
- workdir: /app # TODO: adjust to match your image, or delete this line
57
- interactive: false # optional; keep stdin open / allocate a tty
58
- remove: true # optional; remove the container after each run
59
- env: {} # optional; environment variables passed to the container, e.g. {FOO: bar}
60
- ports: [] # optional; published ports, e.g. ["3000:3000"]
61
- volumes: [] # optional; extra -v specs beyond the sync mounts below
62
-
63
- #{COMMANDS_EXAMPLE}
64
-
65
- sync: # optional; mirrors the source into a named volume instead of bind-mounting it live
66
- #{SYNC_EXTRAS}
43
+ # TODO
44
+ # command: bundle exec rspec
67
45
  YAML
68
46
 
69
- def initialize(dir: Dir.pwd)
47
+ def initialize(dir: Dir.pwd, template: nil)
48
+ raise Error, "unknown --template #{template.inspect} (valid: #{TEMPLATE_LABELS.keys.join(', ')})" \
49
+ if template && !TEMPLATE_LABELS.key?(template)
50
+
70
51
  @dir = dir
52
+ @template = template
71
53
  end
72
54
 
73
55
  def compose? = !!compose_file
74
56
 
75
57
  def call
76
- compose? ? compose_template : CONTAINER_TEMPLATE
58
+ compose? ? compose_template : container_template
77
59
  end
78
60
 
79
61
  private
@@ -87,29 +69,156 @@ module Wip
87
69
  # without duplicating it as a live value that would go stale if the directory moves.
88
70
  def compose_project_default = Pathname(@dir).basename.to_s
89
71
 
72
+ def exclude_comment
73
+ if @template
74
+ "# optional; rsync --exclude patterns picked for --template #{@template} (#{TEMPLATE_LABELS[@template]})"
75
+ else
76
+ '# optional; rsync --exclude patterns, e.g. ["node_modules", "*.log"]'
77
+ end
78
+ end
79
+
80
+ def exclude_list
81
+ patterns = @template ? TEMPLATE_EXCLUDES.fetch(@template) : FALLBACK_EXCLUDES
82
+ patterns.map { |pattern| " - #{pattern}" }.join("\n")
83
+ end
84
+
85
+ # Appended to sync: in both templates. Every key here is either a plain constant
86
+ # default (safe to state literally) or, where commented, a value SyncSettings
87
+ # derives from another key (target from workdir, volume from the container/service
88
+ # name) — those stay unset so they keep tracking that key instead of going stale.
89
+ # A blank line separates every key from its neighbor; the gsub only indents
90
+ # non-blank lines so those separators stay actual blank lines, not trailing
91
+ # whitespace.
92
+ def sync_extras
93
+ <<~YAML.chomp.gsub(/^(?=.)/, ' ')
94
+ # optional; host path to mirror (default: wip.yml directory)
95
+ source: .
96
+
97
+ # optional; in-container mount point for the mirror (default: the container's workdir, else /app)
98
+ # target: /app
99
+
100
+ # optional; read-only host mount inside the container
101
+ mount: /host-src
102
+
103
+ # optional; named volume holding the mirrored tree (default: "<container>-src")
104
+ # volume: app-src
105
+
106
+ # optional; rsync --delete so removals on the host are mirrored too
107
+ delete: true
108
+
109
+ #{exclude_comment}
110
+ exclude:
111
+ #{exclude_list}
112
+
113
+ # optional; mirror binary to shell out to
114
+ command: rsync
115
+
116
+ # optional; extra flags appended to the mirror command
117
+ options: []
118
+
119
+ # optional; seconds between mirrors
120
+ interval: 2
121
+
122
+ # optional; exec (default, mirrors into the running container) | run (always a fresh container)
123
+ mode: exec
124
+
125
+ # only needed if sync.mode: run (or under mode: compose); image the mirror container runs
126
+ # image: your/image:tag
127
+
128
+ # alternative to image — let wip build a small dedicated mirror image itself
129
+ # build:
130
+ # dockerfile: |
131
+ # FROM alpine:latest
132
+ # RUN apk add --no-cache rsync
133
+
134
+ # optional (default: wip-sync-<container>:latest)
135
+ # tag: wip-sync-app:latest
136
+ YAML
137
+ end
138
+
139
+ def container_template
140
+ <<~YAML
141
+ version: 1
142
+
143
+ # container | compose | compose-native (see README)
144
+ mode: container
145
+
146
+ # TODO: rename freely, as long as it matches a key under dependencies: below
147
+ container: app
148
+
149
+ wslc:
150
+ # optional; wslc binary/path wip shells out to ("auto" resolves it for you)
151
+ command: auto
152
+
153
+ # optional; container network name (mode: container only)
154
+ # network: my-network
155
+
156
+ dependencies:
157
+ # this is the container wip creates, execs into, and runs commands in
158
+ app:
159
+ # TODO: image to run
160
+ image: your/image:tag
161
+
162
+ # TODO: adjust to match your image, or delete this line
163
+ workdir: /app
164
+
165
+ # optional; keep stdin open / allocate a tty
166
+ interactive: false
167
+
168
+ # optional; remove the container after each run
169
+ remove: true
170
+
171
+ # optional; environment variables passed to the container, e.g. {FOO: bar}
172
+ env: {}
173
+
174
+ # optional; published ports, e.g. ["3000:3000"]
175
+ ports: []
176
+
177
+ # optional; extra -v specs beyond the sync mounts below
178
+ volumes: []
179
+
180
+ #{COMMANDS_EXAMPLE}
181
+
182
+ # optional; mirrors the source into a named volume instead of bind-mounting it live
183
+ sync:
184
+ #{sync_extras}
185
+ YAML
186
+ end
187
+
90
188
  def compose_template
91
189
  <<~YAML
92
190
  version: 1
93
- mode: compose-native # wip parses #{compose_file} itself and drives wslc directly — no external
94
- # compose-for-wslc binary needed. Prefer a real compose tool instead? Use
95
- # mode: compose (see README "Compose mode") and set compose.command.
191
+
192
+ # wip parses #{compose_file} itself and drives wslc directly no external
193
+ # compose-for-wslc binary needed. Prefer a real compose tool instead? Use
194
+ # mode: compose (see README "Compose mode") and set compose.command.
195
+ mode: compose-native
96
196
 
97
197
  wslc:
98
- command: auto # optional; wslc binary/path wip shells out to ("auto" resolves it for you)
198
+ # optional; wslc binary/path wip shells out to ("auto" resolves it for you)
199
+ command: auto
99
200
 
100
201
  compose:
101
- service: app # TODO: which service in #{compose_file} wip run/exec/NAME target
102
- # file: #{compose_file} # optional; override which compose file wip parses (default: auto-detected)
103
- # project: #{compose_project_default} # optional; project/network name (default: this directory's name)
104
- # command: # only used under mode: compose (external compose-for-wslc binary); unused here under compose-native
202
+ # TODO: which service in #{compose_file} wip run/exec/NAME target
203
+ service: app
204
+
205
+ # optional; override which compose file wip parses (default: auto-detected)
206
+ # file: #{compose_file}
207
+
208
+ # optional; project/network name (default: this directory's name)
209
+ # project: #{compose_project_default}
210
+
211
+ # only used under mode: compose (external compose-for-wslc binary); unused here under compose-native
212
+ # command:
105
213
 
106
214
  # network: is derived from compose.project above (or this directory's name) — setting it
107
215
  # directly conflicts with compose: (ConfigError), so it's intentionally left out here.
108
216
 
109
217
  #{COMMANDS_EXAMPLE}
110
218
 
111
- sync: # optional; mirrors the source into a named volume instead of bind-mounting it live
112
- #{SYNC_EXTRAS}
219
+ # optional; mirrors the source into a named volume instead of bind-mounting it live
220
+ sync:
221
+ #{sync_extras}
113
222
  YAML
114
223
  end
115
224
  end
data/lib/wip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- VERSION = '0.14.2'
4
+ VERSION = '0.16.0'
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: 0.14.2
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors