wslc-wip 0.3.0 → 0.4.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: aacee07e7929d736a13e1a7a4812a52155431b311913a4e5fa25409f6c772f60
4
- data.tar.gz: 31de690c5b1040a9d0cb0c12890f20cf8d07f1d3d57e74d44140d8af30d7e0f0
3
+ metadata.gz: c1e6e4afaf5e33e69fa5dd6dc35f4fc32695e9916b0ceeac998ae58334fab3d4
4
+ data.tar.gz: 56e438283cad62937ac37132bb49048a1159897de4d3058c122f14272bca9e68
5
5
  SHA512:
6
- metadata.gz: 13e7a7c473c92063c6c9bc505ea72b03a8fab8269a6deb0ae2f27fb6309a2848477257b3d759b917eb2e6ddf86dc3b3144fbb98031e6548ece4ae2eaf14f166e
7
- data.tar.gz: 494a24787a32f87479e8c984617e1928c54b43864b9a247da042281b138cd48795970e05ebb69b9805b31a8dd239a0c190f5554f9f427b29f81c1b9a42b669d9
6
+ metadata.gz: 29bfbc9430132bf62ae58813e333f64df00d9228ad98386f3374e3a0de897e55605b977d7208bf7ce9622957e0a3acd1d598e25f25a594eed839fc7007deb37d
7
+ data.tar.gz: 8588e1b97851a59936bb42daf6a8d09002e1c3fdba12dafba0a35bfe8a1c9e430742bee1b8f818a677b83cc793f6b96151be1d729f7465632c3c368d9b518cd8
data/README.md CHANGED
@@ -1,43 +1,56 @@
1
1
  # wip
2
2
 
3
- `wip` は、Docker 開発環境 CLI の dip に近い操作感を Microsoft WSLC に提供する Ruby 製 OSS CLI ラッパーです。プロジェクト固有のコンテナ、イメージ、環境変数やコマンドを `wip.yml` にまとめ、安全な引数配列として `wslc.exe` / `wslc` に渡します。
3
+ [![Tests](https://github.com/slidict/wip/actions/workflows/test.yml/badge.svg)](https://github.com/slidict/wip/actions/workflows/test.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/wslc-wip.svg)](https://rubygems.org/gems/wslc-wip)
5
+ [![License: MIT](https://img.shields.io/github/license/slidict/wip.svg)](LICENSE)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](wslc-wip.gemspec)
4
7
 
5
- > **Status:** 初期リリース 0.1.0。WSLC 自体の仕様変更に追随が必要になる可能性があります。
8
+ `wip` is a Ruby-built OSS CLI wrapper that brings a [`dip`](https://github.com/bibendi/dip)-like
9
+ workflow to Microsoft WSLC. It collects a project's container, image, environment variables, and
10
+ commands into a single `wip.yml`, and forwards them to `wslc.exe` / `wslc` as safe argument arrays
11
+ (no shell interpolation).
6
12
 
7
- ## 対応環境とインストール
13
+ ![wip demo](https://raw.githubusercontent.com/slidict/wip/main/docs/demo.gif)
8
14
 
9
- Ruby 3.2 以上、WSL2、および Microsoft WSLC が必要です。RubyGems 公開後は次で導入できます。
15
+ > **Status:** early release. Expect to track WSLC's own interface as it evolves.
16
+
17
+ ## Requirements & installation
18
+
19
+ Ruby 3.2+, WSL2, and Microsoft WSLC.
10
20
 
11
21
  ```bash
12
- gem install wip
22
+ gem install wslc-wip
13
23
  ```
14
24
 
15
- ソースからは `bundle install && bundle exec exe/wip version` を使用します。
25
+ From source: `bundle install && bundle exec exe/wip version`.
16
26
 
17
- ## クイックスタート
27
+ ## Quick start
18
28
 
19
29
  ```bash
20
- gem install wip
30
+ gem install wslc-wip
21
31
  cd my-project
22
32
  wip doctor
23
33
  wip build
34
+ wip up -d
24
35
  wip rails console
25
36
  ```
26
37
 
27
- ## 完全な設定例
38
+ ## Full configuration example
28
39
 
29
- プロジェクトルートに `wip.yml` を置きます。子ディレクトリから実行しても親方向へ探索され、`--config PATH` で明示指定もできます。
40
+ Put a `wip.yml` in your project root. Running from a subdirectory walks up to find it, or pass
41
+ `--config PATH` to point at one explicitly.
30
42
 
31
43
  ```yaml
32
44
  version: 1
33
45
  wslc:
34
- command: auto # wslc.exewslcSystem32 の順。絶対パスも指定可能
46
+ command: auto # tries wslc.exe, wslc, then System32; an absolute path also works
35
47
  defaults:
36
48
  container: app
37
49
  image: slidict/slidict:development
38
50
  workdir: /app
39
51
  interactive: false
40
52
  remove: true
53
+ network: app-tier # optional; shared with `dependencies` so containers can resolve each other by name
41
54
  env:
42
55
  RAILS_ENV: development
43
56
  PORT: "3000"
@@ -46,7 +59,17 @@ defaults:
46
59
  volumes:
47
60
  - ".:/app"
48
61
  up:
49
- command: server # `wip up` がコンテナ作成時にイメージへ渡す常駐コマンド(未指定ならイメージの既定 CMD)
62
+ command: server # command `wip up` passes to the image when it creates the container
63
+ # (omit to use the image's default CMD)
64
+ dependencies:
65
+ redis:
66
+ image: redis:latest
67
+ development.mysql:
68
+ image: mysql:8.0
69
+ command: --default-authentication-plugin=mysql_native_password
70
+ env:
71
+ MYSQL_ROOT_PASSWORD: password
72
+ MYSQL_DATABASE: development
50
73
  commands:
51
74
  rails:
52
75
  type: exec
@@ -74,46 +97,63 @@ commands:
74
97
  tag: slidict/slidict:development
75
98
  ```
76
99
 
77
- `env` の値は文字列化されます。`config` の表示では tokenpasswordsecret、credential、auth を含むキーを伏せます。秘密値は可能なら設定ファイルではなく実行環境側で管理してください。
100
+ `env` values are stringified. `wip config` masks any key matching token, password, secret,
101
+ credential, or auth. Keep real secrets out of the config file and in your runtime environment
102
+ instead.
103
+
104
+ ### Dependency containers
78
105
 
79
- ## コマンド
106
+ If your app needs sidecar services (a database, Redis, ...), declare them under `dependencies`
107
+ and set `defaults.network`. `wip up` creates the network first (if it doesn't exist), then brings
108
+ up each dependency by name before the main container — so `bin/rails c` (or anything else run
109
+ inside the main container) can reach `development.mysql`/`redis`/etc. by their dependency name,
110
+ the same way Compose's service names resolve. `wip down` tears the main container and all
111
+ dependencies down (the network itself is left in place). Each dependency entry accepts `image`
112
+ (required), `command`, `env`, `ports`, `volumes`, and `workdir` — the same shape as `defaults`.
80
113
 
81
- | コマンド | 説明 |
114
+ ## Commands
115
+
116
+ | Command | Description |
82
117
  |---|---|
83
- | `wip version` | wip と、検出できた WSLC のバージョン |
84
- | `wip doctor` | WSL2、相互運用、WSLC、設定、アーキテクチャ、Git を診断 |
85
- | `wip config` | デフォルト適用済み設定(秘密値をマスク) |
86
- | `wip build -- --no-cache` | build 定義でイメージをビルド |
87
- | `wip up [-d]` | `defaults.container` を起動(無ければ `up.command` 付きで作成)。`-d` でバックグラウンド実行 |
88
- | `wip down` | `defaults.container` を停止・削除 |
89
- | `wip exec [--no-interactive] COMMAND...` | 既存コンテナ内で実行 |
90
- | `wip run [--no-interactive] COMMAND...` | `--rm` の新規コンテナで実行 |
91
- | `wip shell` | 定義済み shell、または `bash`、`sh` の順に起動 |
92
- | `wip NAME ARGS...` | `commands.NAME` を実行し、引数を末尾に追加 |
93
-
94
- TTY はコマンド設定、CLI オプション、標準入出力が双方 TTY かを合わせて判定します。`WIP_DEBUG=1` では `Shellwords.join` した実行コマンドを表示します。
118
+ | `wip version` | wip's version, plus WSLC's if it can be detected |
119
+ | `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
120
+ | `wip config` | Print the effective configuration (secrets masked) |
121
+ | `wip build -- --no-cache` | Build the image from the `build` definition |
122
+ | `wip up [-d]` | Start `defaults.container` and its `dependencies` (creating any that are missing, on `defaults.network` if set). `-d` runs the main container in the background |
123
+ | `wip down` | Stop and remove `defaults.container` and its `dependencies` |
124
+ | `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
125
+ | `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container |
126
+ | `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
127
+ | `wip NAME ARGS...` | Run `commands.NAME`, appending any extra arguments |
128
+
129
+ TTY allocation is decided by combining the command's config, the CLI option, and whether both
130
+ stdin and stdout are real TTYs. Set `WIP_DEBUG=1` to print the `Shellwords`-joined command before
131
+ running it.
95
132
 
96
133
  ## doctor
97
134
 
98
- `[OK]`、`[WARN]`、`[FAIL]` で各診断を表示します。警告だけなら終了コード 0、WSL2/相互運用/WSLC/設定など実行を妨げる問題があれば 1 です。実際のビルド環境から Git が見えない場合は警告になります。
135
+ Each check prints as `[OK]`, `[WARN]`, or `[FAIL]`. Warnings alone exit 0; a WSL2, interop, WSLC,
136
+ or config problem that blocks execution exits 1. Git being unreachable from the real build
137
+ environment is only a warning.
99
138
 
100
- ## よくあるエラー
139
+ ## Common errors
101
140
 
102
- ### WSLC がない
141
+ ### WSLC not found
103
142
 
104
- WSLC/WSL コンテナツールをインストールまたは更新し、`wip doctor` を実行してください。`auto` `wslc.exe`、`wslc`、`/mnt/c/Windows/System32/wslc.exe` の順で探します。
143
+ Install or update the WSL container tooling, then run `wip doctor`. `auto` looks for `wslc.exe`,
144
+ `wslc`, then `/mnt/c/Windows/System32/wslc.exe`, in that order.
105
145
 
106
- ### Docker Hub の認証
146
+ ### Docker Hub authentication
107
147
 
108
- `pull access denied` 等を検出するとログイン方法を補足します。
148
+ When `pull access denied` (or similar) is detected, wip suggests how to log in:
109
149
 
110
150
  ```bash
111
151
  wslc registry login -u <username> docker.io
112
152
  ```
113
153
 
114
- ### CPU アーキテクチャ不一致
154
+ ### CPU architecture mismatch
115
155
 
116
- イメージを確認し、amd64/arm64 のマルチアーキテクチャイメージを公開します。
156
+ Check the image and publish a multi-arch (amd64/arm64) image:
117
157
 
118
158
  ```bash
119
159
  docker buildx imagetools inspect <image>
@@ -123,10 +163,10 @@ docker buildx build \
123
163
  --push .
124
164
  ```
125
165
 
126
- ## 開発
166
+ ## Development
127
167
 
128
168
  ```bash
129
- git clone <repository-url>
169
+ git clone https://github.com/slidict/wip.git
130
170
  cd wip
131
171
  bundle install
132
172
  bundle exec rspec
@@ -134,11 +174,21 @@ bundle exec rubocop
134
174
  bundle exec rake
135
175
  ```
136
176
 
137
- テストは WSLC を必要とせず、解決・構築・実行層を差し替え可能にしています。GitHub Actions Ruby 3.2、3.3、3.4 RSpec RuboCop を実行します。
177
+ The test suite doesn't need WSLC the resolution, build, and execution layers are all
178
+ swappable. GitHub Actions runs RSpec and RuboCop on Ruby 3.2, 3.3, 3.4, and 4.0.
179
+
180
+ ## Contributing
181
+
182
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/slidict/wip). See
183
+ [CONTRIBUTING.md](CONTRIBUTING.md) for commit conventions, versioning policy, and the PR
184
+ checklist.
138
185
 
139
- ## 初期リリースに含まれないもの
186
+ ## Not in the initial release
140
187
 
141
- Compose 互換、常駐プロセス、GUI、PowerShell 最適化、レジストリ API/manifest の直接解析、自動更新、プラグインは未実装です。将来は設定スキーマ、ライフサイクルフック、複数コンテナ、プラットフォーム選択、より詳細な診断の追加が有用です。
188
+ Full Compose compatibility (multiple networks, `depends_on` ordering/health checks, profiles), a
189
+ resident/daemon process, a GUI, PowerShell-specific tuning, direct registry API/manifest parsing,
190
+ self-update, and plugins are all unimplemented. Likely future additions: a richer config schema,
191
+ lifecycle hooks, platform selection, and more detailed diagnostics.
142
192
 
143
193
  ## License
144
194
 
data/lib/wip/cli.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'thor'
4
4
  require 'yaml'
5
+ require 'json'
6
+ require 'stringio'
5
7
 
6
8
  module Wip
7
9
  # Thor-based command-line interface for wip.
@@ -51,40 +53,53 @@ module Wip
51
53
  execute(builder.build(settings: load_config.command('build') || {}, extra: extra))
52
54
  end
53
55
 
54
- desc 'up', 'Start the configured container, creating it if necessary'
56
+ desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
55
57
  option :detach, type: :boolean, default: false, aliases: '-d'
56
58
  def up
57
- code = execute(builder.start(detach: options[:detach]), exit_on_failure: false)
58
- execute(builder.up(detach: options[:detach])) if code != 0
59
+ ensure_network
60
+ load_config.dependencies.each_key { |name| ensure_dependency(name) }
61
+ ensure_container
59
62
  end
60
63
 
61
- desc 'down', 'Stop and remove the configured container'
64
+ desc 'down', 'Stop and remove the configured container and its dependencies'
62
65
  def down
63
66
  execute(builder.down, exit_on_failure: false)
64
67
  execute(builder.remove, exit_on_failure: false)
68
+ load_config.dependencies.each_key do |name|
69
+ execute(builder.dependency_down(name), exit_on_failure: false)
70
+ execute(builder.dependency_remove(name), exit_on_failure: false)
71
+ end
65
72
  end
66
73
 
67
74
  desc 'exec COMMAND...', 'Execute a command in the running container'
68
75
  option :interactive, type: :boolean, default: true
69
76
  def exec(*command)
70
- execute(builder.exec(command, interactive: options[:interactive]))
77
+ tty = builder.tty?(options[:interactive])
78
+ execute(builder.exec(command, interactive: options[:interactive]), interactive: tty)
71
79
  end
72
80
 
73
81
  desc 'run COMMAND...', 'Run a command in a new container'
74
82
  option :interactive, type: :boolean, default: true
75
83
  def run_command(*command)
76
- execute(builder.run(command, interactive: options[:interactive]))
84
+ tty = builder.tty?(options[:interactive])
85
+ execute(builder.run(command, interactive: options[:interactive]), interactive: tty)
77
86
  end
78
87
  map 'run' => :run_command
79
88
 
80
89
  desc 'shell', 'Open a shell in the configured container'
81
90
  def shell_command
82
91
  configured = load_config.command('shell')
83
- return execute(builder.custom('shell', [])) if configured
92
+ if configured
93
+ tty = builder.tty?(configured.fetch('interactive', false))
94
+ return execute(builder.custom('shell', []), interactive: tty)
95
+ end
84
96
 
97
+ tty = builder.tty?(true)
85
98
  code = execute(builder.exec(['bash'], settings: { 'interactive' => true }, interactive: true),
86
- exit_on_failure: false)
87
- execute(builder.exec(['sh'], settings: { 'interactive' => true }, interactive: true)) if code != 0
99
+ interactive: tty, exit_on_failure: false)
100
+ return if code.zero?
101
+
102
+ execute(builder.exec(['sh'], settings: { 'interactive' => true }, interactive: true), interactive: tty)
88
103
  end
89
104
  map 'shell' => :shell_command
90
105
 
@@ -92,7 +107,8 @@ module Wip
92
107
  def dispatch(name = nil, *arguments)
93
108
  raise ConfigError, 'A command is required' unless name
94
109
 
95
- execute(builder.custom(name, arguments))
110
+ values = load_config.command(name) || raise(ConfigError, "Unknown command: #{name}")
111
+ execute(builder.custom(name, arguments), interactive: builder.tty?(values.fetch('interactive', false)))
96
112
  end
97
113
 
98
114
  private
@@ -102,10 +118,59 @@ module Wip
102
118
  def resolver = CommandResolver.new
103
119
  def builder = CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config)
104
120
 
105
- def execute(command, exit_on_failure: true)
106
- code = CommandRunner.new.run(command)
121
+ def execute(command, interactive: false, exit_on_failure: true)
122
+ code = CommandRunner.new.run(command, interactive: interactive)
107
123
  exit(code) if exit_on_failure && !code.zero?
108
124
  code
109
125
  end
126
+
127
+ def resource_exists?(find_command)
128
+ out = StringIO.new
129
+ code = CommandRunner.new(stdout: out, stderr: StringIO.new).run(find_command)
130
+ code.zero? && !JSON.parse(out.string).empty?
131
+ rescue JSON::ParserError
132
+ false
133
+ end
134
+
135
+ def ensure_network
136
+ network = load_config.network
137
+ return unless network
138
+ return if network_exists?(network)
139
+
140
+ warn "wip: creating network '#{network}'"
141
+ execute(builder.network_create, exit_on_failure: false)
142
+ end
143
+
144
+ def network_exists?(network)
145
+ out = StringIO.new
146
+ code = CommandRunner.new(stdout: out, stderr: StringIO.new).run(builder.network_list)
147
+ return false unless code.zero?
148
+
149
+ JSON.parse(out.string).any? { |entry| entry['Name'] == network }
150
+ rescue JSON::ParserError
151
+ false
152
+ end
153
+
154
+ def ensure_dependency(name)
155
+ if resource_exists?(builder.dependency_find(name))
156
+ warn "wip: starting existing dependency '#{name}'"
157
+ execute(builder.dependency_start(name))
158
+ else
159
+ warn "wip: dependency '#{name}' not found, creating it"
160
+ execute(builder.dependency_up(name))
161
+ end
162
+ end
163
+
164
+ def ensure_container
165
+ container = load_config.defaults['container']
166
+ interactive = builder.tty?(!options[:detach])
167
+ if resource_exists?(builder.find)
168
+ warn "wip: starting existing container '#{container}'"
169
+ execute(builder.start(detach: options[:detach]), interactive: interactive)
170
+ else
171
+ warn "wip: container '#{container}' not found, creating it"
172
+ execute(builder.up(detach: options[:detach]), interactive: interactive)
173
+ end
174
+ end
110
175
  end
111
176
  end
@@ -29,6 +29,7 @@ module Wip
29
29
  def up(detach: false)
30
30
  values = @config.defaults
31
31
  command = [@wslc, 'run', '--name', required(values, 'container')]
32
+ command.push('--network', @config.network) if @config.network
32
33
  command << '-d' if detach
33
34
  command << '-it' if !detach && tty?(true)
34
35
  command.concat(options(values)).push(required(values, 'image'))
@@ -42,6 +43,11 @@ module Wip
42
43
  command
43
44
  end
44
45
 
46
+ def find
47
+ container = required(@config.defaults, 'container')
48
+ [@wslc, 'list', '--all', '--filter', "name=#{container}", '--format', 'json']
49
+ end
50
+
45
51
  def down
46
52
  [@wslc, 'stop', required(@config.defaults, 'container')]
47
53
  end
@@ -50,6 +56,40 @@ module Wip
50
56
  [@wslc, 'remove', '-f', required(@config.defaults, 'container')]
51
57
  end
52
58
 
59
+ def network_create
60
+ [@wslc, 'network', 'create', required_network]
61
+ end
62
+
63
+ def network_list
64
+ [@wslc, 'network', 'list', '--format', 'json']
65
+ end
66
+
67
+ def dependency_up(name, detach: true)
68
+ values = dependency_values(name)
69
+ command = [@wslc, 'run', '--name', name.to_s]
70
+ command.push('--network', @config.network) if @config.network
71
+ command << '-d' if detach
72
+ command.concat(options(values)).push(required(values, 'image'))
73
+ command.concat(Shellwords.split(values['command'].to_s)) unless values['command'].to_s.empty?
74
+ command
75
+ end
76
+
77
+ def dependency_start(name)
78
+ [@wslc, 'start', name.to_s]
79
+ end
80
+
81
+ def dependency_find(name)
82
+ [@wslc, 'list', '--all', '--filter', "name=#{name}", '--format', 'json']
83
+ end
84
+
85
+ def dependency_down(name)
86
+ [@wslc, 'stop', name.to_s]
87
+ end
88
+
89
+ def dependency_remove(name)
90
+ [@wslc, 'remove', '-f', name.to_s]
91
+ end
92
+
53
93
  def build(settings:, extra: [])
54
94
  values = @config.defaults.merge(settings)
55
95
  context = values['context'] || '.'
@@ -68,6 +108,8 @@ module Wip
68
108
  public_send(type, base + arguments, settings: values, interactive: values.fetch('interactive', false))
69
109
  end
70
110
 
111
+ def tty?(requested) = requested && @environment.interactive?
112
+
71
113
  private
72
114
 
73
115
  def options(values, include_container: false, include_publish: true)
@@ -89,6 +131,15 @@ module Wip
89
131
  value
90
132
  end
91
133
 
92
- def tty?(requested) = requested && @environment.interactive?
134
+ def required_network
135
+ network = @config.network
136
+ raise ConfigError, 'Configured network must not be empty' if network.to_s.empty?
137
+
138
+ network
139
+ end
140
+
141
+ def dependency_values(name)
142
+ @config.dependency(name) || raise(ConfigError, "Unknown dependency: #{name}")
143
+ end
93
144
  end
94
145
  end
@@ -13,8 +13,10 @@ module Wip
13
13
  @interpreter = interpreter
14
14
  end
15
15
 
16
- def run(command, env: {})
16
+ def run(command, env: {}, interactive: false)
17
17
  @stderr.puts "+ #{Shellwords.join(command)}" if ENV['WIP_DEBUG']
18
+ return run_attached(command, env) if interactive
19
+
18
20
  captured = +''
19
21
  status = nil
20
22
  Open3.popen3(env, *command) do |input, output, error, wait|
@@ -33,6 +35,19 @@ module Wip
33
35
 
34
36
  private
35
37
 
38
+ # Piping stdin/stdout/stderr (as `run` does above) closes the child's
39
+ # stdin immediately, which breaks anything that reads from the terminal
40
+ # (a shell, `rails console`, ...). Inherit the real file descriptors
41
+ # instead so the child gets a genuine TTY.
42
+ def run_attached(command, env)
43
+ pid = Process.spawn(env, *command)
44
+ _, status = Process.wait2(pid)
45
+ status.exitstatus
46
+ rescue Errno::ENOENT => e
47
+ @stderr.puts e.message
48
+ 127
49
+ end
50
+
36
51
  def pump(source, destination, captured)
37
52
  Thread.new do
38
53
  source.each(4096) do |chunk|
data/lib/wip/config.rb CHANGED
@@ -18,6 +18,8 @@ module Wip
18
18
  def commands = @raw['commands'] || {}
19
19
  def defaults = DEFAULTS.merge(@raw['defaults'] || {})
20
20
  def up_command = @raw.dig('up', 'command')
21
+ def dependencies = @raw['dependencies'] || {}
22
+ def network = defaults['network']
21
23
 
22
24
  def command(name)
23
25
  entry = commands[name.to_s]
@@ -26,9 +28,16 @@ module Wip
26
28
  defaults.merge('type' => 'exec').merge(entry)
27
29
  end
28
30
 
31
+ def dependency(name)
32
+ entry = dependencies[name.to_s]
33
+ return unless entry
34
+
35
+ { 'workdir' => nil, 'env' => {}, 'ports' => [], 'volumes' => [] }.merge(entry)
36
+ end
37
+
29
38
  def to_h(redact: true)
30
39
  value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'defaults' => defaults,
31
- 'up' => { 'command' => up_command },
40
+ 'up' => { 'command' => up_command }, 'dependencies' => dependencies,
32
41
  'commands' => commands.transform_values { |entry| defaults.merge('type' => 'exec').merge(entry) } }
33
42
  redact ? redact_secrets(value) : value
34
43
  end
@@ -37,12 +46,24 @@ module Wip
37
46
 
38
47
  def validate!
39
48
  raise ConfigError, "Unsupported configuration version: #{@raw['version']}" unless (@raw['version'] || 1) == 1
40
- raise ConfigError, 'commands must be a mapping' unless commands.is_a?(Hash)
41
49
  raise ConfigError, 'up must be a mapping' if @raw.key?('up') && !@raw['up'].is_a?(Hash)
42
50
 
51
+ validate_commands!
52
+ validate_dependencies!
53
+ end
54
+
55
+ def validate_commands!
56
+ raise ConfigError, 'commands must be a mapping' unless commands.is_a?(Hash)
57
+
43
58
  commands.each { |name, entry| validate_command!(name, entry) }
44
59
  end
45
60
 
61
+ def validate_dependencies!
62
+ raise ConfigError, 'dependencies must be a mapping' unless dependencies.is_a?(Hash)
63
+
64
+ dependencies.each { |name, entry| validate_dependency!(name, entry) }
65
+ end
66
+
46
67
  def validate_command!(name, entry)
47
68
  raise ConfigError, "commands.#{name} must be a mapping" unless entry.is_a?(Hash)
48
69
 
@@ -52,6 +73,13 @@ module Wip
52
73
  entry['env']&.transform_values!(&:to_s)
53
74
  end
54
75
 
76
+ def validate_dependency!(name, entry)
77
+ raise ConfigError, "dependencies.#{name} must be a mapping" unless entry.is_a?(Hash)
78
+ raise ConfigError, "dependencies.#{name} must set image" if entry['image'].to_s.empty?
79
+
80
+ entry['env']&.transform_values!(&:to_s)
81
+ end
82
+
55
83
  def stringify(object)
56
84
  case object
57
85
  when Hash then object.to_h { |key, value| [key.to_s, stringify(value)] }
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.3.0'
4
+ VERSION = '0.4.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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors
@@ -44,11 +44,14 @@ files:
44
44
  - lib/wip/error_interpreter.rb
45
45
  - lib/wip/errors.rb
46
46
  - lib/wip/version.rb
47
- homepage: https://github.com/example/wip
47
+ homepage: https://github.com/slidict/wip
48
48
  licenses:
49
49
  - MIT
50
50
  metadata:
51
51
  rubygems_mfa_required: 'true'
52
+ source_code_uri: https://github.com/slidict/wip
53
+ changelog_uri: https://github.com/slidict/wip/releases
54
+ bug_tracker_uri: https://github.com/slidict/wip/issues
52
55
  rdoc_options: []
53
56
  require_paths:
54
57
  - lib