wslc-wip 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8e5f43bc1610d2c7af26c4ed9052ca4a055bcbbc97957f756452b5992b020b3
4
+ data.tar.gz: 24299f3953026ccde81f3bc485234769aa417bac0bc6a22ab37c82ce76d1c8ac
5
+ SHA512:
6
+ metadata.gz: 643895970056693e00703eddbecb9303fafd798bfe1f2ea005435f5b6daf36678e687784a75105fb88dbc01ca524060d4a9b8862ab3b38fce8121a577bcd868f
7
+ data.tar.gz: 0542772f24b25763b610aba4d940f589bae73327f444103d77e9c899b6376f73ffcb78ce6a56c91fd180a5c1ac053e0ea8e5269008919d19541036d7d0550aa0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 slidict
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # wip
2
+
3
+ `wip` は、Docker 開発環境 CLI の dip に近い操作感を Microsoft WSLC に提供する Ruby 製 OSS CLI ラッパーです。プロジェクト固有のコンテナ、イメージ、環境変数やコマンドを `wip.yml` にまとめ、安全な引数配列として `wslc.exe` / `wslc` に渡します。
4
+
5
+ > **Status:** 初期リリース 0.1.0。WSLC 自体の仕様変更に追随が必要になる可能性があります。
6
+
7
+ ## 対応環境とインストール
8
+
9
+ Ruby 3.2 以上、WSL2、および Microsoft WSLC が必要です。RubyGems 公開後は次で導入できます。
10
+
11
+ ```bash
12
+ gem install wip
13
+ ```
14
+
15
+ ソースからは `bundle install && bundle exec exe/wip version` を使用します。
16
+
17
+ ## クイックスタート
18
+
19
+ ```bash
20
+ gem install wip
21
+ cd my-project
22
+ wip doctor
23
+ wip build
24
+ wip rails console
25
+ ```
26
+
27
+ ## 完全な設定例
28
+
29
+ プロジェクトルートに `wip.yml` を置きます。子ディレクトリから実行しても親方向へ探索され、`--config PATH` で明示指定もできます。
30
+
31
+ ```yaml
32
+ version: 1
33
+ wslc:
34
+ command: auto # wslc.exe、wslc、System32 の順。絶対パスも指定可能
35
+ defaults:
36
+ container: app
37
+ image: slidict/slidict:development
38
+ workdir: /app
39
+ interactive: false
40
+ remove: true
41
+ env:
42
+ RAILS_ENV: development
43
+ PORT: "3000"
44
+ ports:
45
+ - "3000:3000"
46
+ volumes:
47
+ - ".:/app"
48
+ commands:
49
+ rails:
50
+ type: exec
51
+ command: bin/rails
52
+ container: app
53
+ interactive: true
54
+ workdir: /app
55
+ env:
56
+ RAILS_ENV: development
57
+ bundle:
58
+ command: bundle
59
+ rspec:
60
+ command: bundle exec rspec
61
+ shell:
62
+ command: bash
63
+ interactive: true
64
+ migrate:
65
+ type: run
66
+ command: bundle exec rails db:migrate
67
+ image: slidict/slidict:development
68
+ remove: true
69
+ build:
70
+ type: build
71
+ context: .
72
+ tag: slidict/slidict:development
73
+ ```
74
+
75
+ `env` の値は文字列化されます。`config` の表示では token、password、secret、credential、auth を含むキーを伏せます。秘密値は可能なら設定ファイルではなく実行環境側で管理してください。
76
+
77
+ ## コマンド
78
+
79
+ | コマンド | 説明 |
80
+ |---|---|
81
+ | `wip version` | wip と、検出できた WSLC のバージョン |
82
+ | `wip doctor` | WSL2、相互運用、WSLC、設定、アーキテクチャ、Git を診断 |
83
+ | `wip config` | デフォルト適用済み設定(秘密値をマスク) |
84
+ | `wip build -- --no-cache` | build 定義でイメージをビルド |
85
+ | `wip exec [--no-interactive] COMMAND...` | 既存コンテナ内で実行 |
86
+ | `wip run [--no-interactive] COMMAND...` | `--rm` の新規コンテナで実行 |
87
+ | `wip shell` | 定義済み shell、または `bash`、`sh` の順に起動 |
88
+ | `wip NAME ARGS...` | `commands.NAME` を実行し、引数を末尾に追加 |
89
+
90
+ TTY はコマンド設定、CLI オプション、標準入出力が双方 TTY かを合わせて判定します。`WIP_DEBUG=1` では `Shellwords.join` した実行コマンドを表示します。
91
+
92
+ ## doctor
93
+
94
+ `[OK]`、`[WARN]`、`[FAIL]` で各診断を表示します。警告だけなら終了コード 0、WSL2/相互運用/WSLC/設定など実行を妨げる問題があれば 1 です。実際のビルド環境から Git が見えない場合は警告になります。
95
+
96
+ ## よくあるエラー
97
+
98
+ ### WSLC がない
99
+
100
+ WSLC/WSL コンテナツールをインストールまたは更新し、`wip doctor` を実行してください。`auto` は `wslc.exe`、`wslc`、`/mnt/c/Windows/System32/wslc.exe` の順で探します。
101
+
102
+ ### Docker Hub の認証
103
+
104
+ `pull access denied` 等を検出するとログイン方法を補足します。
105
+
106
+ ```bash
107
+ wslc registry login -u <username> docker.io
108
+ ```
109
+
110
+ ### CPU アーキテクチャ不一致
111
+
112
+ イメージを確認し、amd64/arm64 のマルチアーキテクチャイメージを公開します。
113
+
114
+ ```bash
115
+ docker buildx imagetools inspect <image>
116
+ docker buildx build \
117
+ --platform linux/amd64,linux/arm64 \
118
+ -t <image> \
119
+ --push .
120
+ ```
121
+
122
+ ## 開発
123
+
124
+ ```bash
125
+ git clone <repository-url>
126
+ cd wip
127
+ bundle install
128
+ bundle exec rspec
129
+ bundle exec rubocop
130
+ bundle exec rake
131
+ ```
132
+
133
+ テストは WSLC を必要とせず、解決・構築・実行層を差し替え可能にしています。GitHub Actions は Ruby 3.2、3.3、3.4 で RSpec と RuboCop を実行します。
134
+
135
+ ## 初期リリースに含まれないもの
136
+
137
+ Compose 互換、常駐プロセス、GUI、PowerShell 最適化、レジストリ API/manifest の直接解析、自動更新、プラグインは未実装です。将来は設定スキーマ、ライフサイクルフック、複数コンテナ、プラットフォーム選択、より詳細な診断の追加が有用です。
138
+
139
+ ## License
140
+
141
+ [MIT License](LICENSE)
data/exe/wip ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'wip'
5
+
6
+ begin
7
+ Wip::CLI.start(ARGV)
8
+ rescue Wip::Error => e
9
+ warn "wip: #{e.message}"
10
+ exit 1
11
+ end
data/lib/wip/cli.rb ADDED
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'yaml'
5
+
6
+ module Wip
7
+ # Thor-based command-line interface for wip.
8
+ class CLI < Thor
9
+ class_option :config, type: :string, desc: 'Path to wip.yml'
10
+ default_task :dispatch
11
+
12
+ desc 'version', 'Show wip and WSLC versions'
13
+ def version
14
+ puts "wip #{VERSION}"
15
+ config = load_config
16
+ command = resolver.resolve(config.wslc_command)
17
+ system(command, 'version')
18
+ rescue Error
19
+ nil
20
+ end
21
+
22
+ desc 'doctor', 'Diagnose the development environment'
23
+ def doctor
24
+ results = Doctor.new(loader: loader).call
25
+ results.each { |item| puts "[#{item.level.to_s.upcase}] #{item.message}" }
26
+ exit(1) if results.any? { |item| item.level == :fail }
27
+ end
28
+
29
+ desc 'config', 'Print the effective configuration'
30
+ def config
31
+ puts YAML.dump(load_config.to_h)
32
+ end
33
+
34
+ desc 'build [OPTIONS]', 'Build the configured image'
35
+ def build(*extra)
36
+ extra.shift if extra.first == '--'
37
+ execute(builder.build(settings: load_config.command('build') || {}, extra: extra))
38
+ end
39
+
40
+ desc 'exec COMMAND...', 'Execute a command in the running container'
41
+ option :interactive, type: :boolean, default: true
42
+ def exec(*command)
43
+ execute(builder.exec(command, interactive: options[:interactive]))
44
+ end
45
+
46
+ desc 'run COMMAND...', 'Run a command in a new container'
47
+ option :interactive, type: :boolean, default: true
48
+ def run_command(*command)
49
+ execute(builder.run(command, interactive: options[:interactive]))
50
+ end
51
+ map 'run' => :run_command
52
+
53
+ desc 'shell', 'Open a shell in the configured container'
54
+ def shell_command
55
+ configured = load_config.command('shell')
56
+ return execute(builder.custom('shell', [])) if configured
57
+
58
+ code = execute(builder.exec(['bash'], settings: { 'interactive' => true }, interactive: true))
59
+ execute(builder.exec(['sh'], settings: { 'interactive' => true }, interactive: true)) if code != 0
60
+ end
61
+ map 'shell' => :shell_command
62
+
63
+ desc 'dispatch COMMAND [ARGS...]', 'Run a command defined in wip.yml'
64
+ def dispatch(name = nil, *arguments)
65
+ raise ConfigError, 'A command is required' unless name
66
+
67
+ execute(builder.custom(name, arguments))
68
+ end
69
+
70
+ private
71
+
72
+ def loader = ConfigLoader.new(path: options[:config])
73
+ def load_config = (@load_config ||= loader.load)
74
+ def resolver = CommandResolver.new
75
+ def builder = CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config)
76
+
77
+ def execute(command)
78
+ code = CommandRunner.new.run(command)
79
+ exit(code) unless code.zero?
80
+ code
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Wip
6
+ # Builds the argument arrays for wslc build/exec/run/custom invocations.
7
+ class CommandBuilder
8
+ def initialize(wslc:, config:, environment: Environment.new)
9
+ @wslc = wslc
10
+ @config = config
11
+ @environment = environment
12
+ end
13
+
14
+ def exec(arguments, settings: {}, interactive: true)
15
+ values = @config.defaults.merge(settings)
16
+ command = [@wslc, 'exec']
17
+ command << '-it' if tty?(interactive)
18
+ command.concat(options(values, include_container: true)).concat(arguments)
19
+ end
20
+
21
+ def run(arguments, settings: {}, interactive: true)
22
+ values = @config.defaults.merge(settings)
23
+ command = [@wslc, 'run']
24
+ command << '--rm' if values['remove']
25
+ command << '-it' if tty?(interactive)
26
+ command.concat(options(values)).push(required(values, 'image')).concat(arguments)
27
+ end
28
+
29
+ def build(settings:, extra: [])
30
+ values = @config.defaults.merge(settings)
31
+ context = values['context'] || '.'
32
+ tag = values['tag'] || values['image']
33
+ raise ConfigError, 'Build image/tag must not be empty' if tag.to_s.empty?
34
+
35
+ [@wslc, 'build', '-t', tag, *extra, context]
36
+ end
37
+
38
+ def custom(name, arguments)
39
+ values = @config.command(name) || raise(ConfigError, "Unknown command: #{name}")
40
+ type = values['type'] || (name.to_s == 'build' ? 'build' : 'exec')
41
+ base = Shellwords.split(values['command'].to_s)
42
+ return build(settings: values, extra: arguments) if type == 'build'
43
+
44
+ public_send(type, base + arguments, settings: values, interactive: values.fetch('interactive', false))
45
+ end
46
+
47
+ private
48
+
49
+ def options(values, include_container: false)
50
+ result = []
51
+ result.push('-w', values['workdir']) unless values['workdir'].to_s.empty?
52
+ values.fetch('env', {}).each { |key, value| result.push('-e', "#{key}=#{value}") }
53
+ Array(values['ports']).each { |port| result.push('-p', port.to_s) }
54
+ Array(values['volumes']).each { |volume| result.push('-v', volume.to_s) }
55
+ result << required(values, 'container') if include_container
56
+ result
57
+ end
58
+
59
+ def required(values, key)
60
+ value = values[key]
61
+ raise ConfigError, "Configured #{key} must not be empty" if value.to_s.empty?
62
+
63
+ value
64
+ end
65
+
66
+ def tty?(requested) = requested && @environment.interactive?
67
+ end
68
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Locates the wslc/wslc.exe executable on the current system.
5
+ class CommandResolver
6
+ CANDIDATES = ['wslc.exe', 'wslc', '/mnt/c/Windows/System32/wslc.exe'].freeze
7
+
8
+ def initialize(path: ENV.fetch('PATH', ''), executable: nil)
9
+ @path = path
10
+ @executable = executable || method(:executable?)
11
+ end
12
+
13
+ def resolve(configured = 'auto')
14
+ return configured if configured != 'auto' && @executable.call(configured)
15
+ return raise_not_found if configured != 'auto'
16
+
17
+ CANDIDATES.find { |candidate| @executable.call(candidate) } || raise_not_found
18
+ end
19
+
20
+ private
21
+
22
+ def executable?(command)
23
+ return File.executable?(command) if command.include?(File::SEPARATOR)
24
+
25
+ @path.split(File::PATH_SEPARATOR).any? { |directory| File.executable?(File.join(directory, command)) }
26
+ end
27
+
28
+ def raise_not_found
29
+ raise CommandNotFoundError, <<~MESSAGE
30
+ WSLC was not found.
31
+
32
+ Checked:
33
+ #{CANDIDATES.join("\n ")}
34
+
35
+ Install or update the WSL container tooling, then run:
36
+
37
+ wip doctor
38
+ MESSAGE
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'shellwords'
5
+
6
+ module Wip
7
+ # Executes a built command, pumping its I/O and returning the exit status.
8
+ class CommandRunner
9
+ def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr, interpreter: ErrorInterpreter.new)
10
+ @stdin = stdin
11
+ @stdout = stdout
12
+ @stderr = stderr
13
+ @interpreter = interpreter
14
+ end
15
+
16
+ def run(command, env: {})
17
+ @stderr.puts "+ #{Shellwords.join(command)}" if ENV['WIP_DEBUG']
18
+ captured = +''
19
+ status = nil
20
+ Open3.popen3(env, *command) do |input, output, error, wait|
21
+ input.close
22
+ threads = [pump(output, @stdout, captured), pump(error, @stderr, captured)]
23
+ threads.each(&:join)
24
+ status = wait.value
25
+ end
26
+ hint = @interpreter.interpret(captured)
27
+ @stderr.puts("\n#{hint}") if !status.success? && hint
28
+ status.exitstatus
29
+ rescue Errno::ENOENT => e
30
+ @stderr.puts e.message
31
+ 127
32
+ end
33
+
34
+ private
35
+
36
+ def pump(source, destination, captured)
37
+ Thread.new do
38
+ source.each(4096) do |chunk|
39
+ destination.write(chunk)
40
+ captured << chunk
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/wip/config.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Validated, defaulted access to a parsed wip.yml document.
5
+ class Config
6
+ DEFAULTS = { 'container' => 'app', 'workdir' => '/app', 'interactive' => false,
7
+ 'remove' => true, 'env' => {}, 'ports' => [], 'volumes' => [] }.freeze
8
+ SECRET_PATTERN = /token|password|secret|credential|auth/i
9
+ attr_reader :path
10
+
11
+ def initialize(raw, path = nil)
12
+ @raw = stringify(raw)
13
+ @path = path
14
+ validate!
15
+ end
16
+
17
+ def wslc_command = @raw.dig('wslc', 'command') || 'auto'
18
+ def commands = @raw['commands'] || {}
19
+ def defaults = DEFAULTS.merge(@raw['defaults'] || {})
20
+
21
+ def command(name)
22
+ entry = commands[name.to_s]
23
+ return unless entry
24
+
25
+ defaults.merge('type' => 'exec').merge(entry)
26
+ end
27
+
28
+ def to_h(redact: true)
29
+ value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'defaults' => defaults,
30
+ 'commands' => commands.transform_values { |entry| defaults.merge('type' => 'exec').merge(entry) } }
31
+ redact ? redact_secrets(value) : value
32
+ end
33
+
34
+ private
35
+
36
+ def validate!
37
+ raise ConfigError, "Unsupported configuration version: #{@raw['version']}" unless (@raw['version'] || 1) == 1
38
+ raise ConfigError, 'commands must be a mapping' unless commands.is_a?(Hash)
39
+
40
+ commands.each { |name, entry| validate_command!(name, entry) }
41
+ end
42
+
43
+ def validate_command!(name, entry)
44
+ raise ConfigError, "commands.#{name} must be a mapping" unless entry.is_a?(Hash)
45
+
46
+ type = entry['type'] || (name == 'build' ? 'build' : 'exec')
47
+ raise ConfigError, "Invalid command type for #{name}: #{type}" unless %w[exec run build].include?(type)
48
+
49
+ entry['env']&.transform_values!(&:to_s)
50
+ end
51
+
52
+ def stringify(object)
53
+ case object
54
+ when Hash then object.to_h { |key, value| [key.to_s, stringify(value)] }
55
+ when Array then object.map { |value| stringify(value) }
56
+ else object
57
+ end
58
+ end
59
+
60
+ def redact_secrets(object)
61
+ return object.map { |item| redact_secrets(item) } if object.is_a?(Array)
62
+ return object unless object.is_a?(Hash)
63
+
64
+ object.to_h { |key, value| [key, key.match?(SECRET_PATTERN) ? '[REDACTED]' : redact_secrets(value)] }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'yaml'
5
+
6
+ module Wip
7
+ # Finds and parses wip.yml, searching parent directories when unset.
8
+ class ConfigLoader
9
+ FILENAME = 'wip.yml'
10
+
11
+ def initialize(start_dir: Dir.pwd, path: nil)
12
+ @start_dir = Pathname(start_dir).expand_path
13
+ @path = path
14
+ end
15
+
16
+ def find
17
+ return Pathname(@path).expand_path if @path
18
+
19
+ @start_dir.ascend do |directory|
20
+ candidate = directory.join(FILENAME)
21
+ return candidate if candidate.file?
22
+ end
23
+ nil
24
+ end
25
+
26
+ def load
27
+ path = find
28
+ raise ConfigError, "wip.yml was not found (searched from #{@start_dir} to the filesystem root)" unless path&.file?
29
+
30
+ data = YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {}
31
+ Config.new(data, path)
32
+ rescue Psych::Exception => e
33
+ raise ConfigError, "Could not parse #{path}: #{e.message}"
34
+ end
35
+ end
36
+ end
data/lib/wip/doctor.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Wip
6
+ # Runs environment diagnostics and reports pass/warn/fail results.
7
+ class Doctor
8
+ Result = Data.define(:level, :message)
9
+
10
+ def initialize(loader:, resolver: CommandResolver.new, environment: Environment.new)
11
+ @loader = loader
12
+ @resolver = resolver
13
+ @environment = environment
14
+ end
15
+
16
+ def call
17
+ results = []
18
+ results << result(@environment.wsl2? ? :ok : :fail, 'Running on WSL2', 'Not running on WSL2')
19
+ results << result(@environment.windows_interop? ? :ok : :fail, 'Windows executable interoperability is enabled',
20
+ 'Windows executable interoperability is disabled')
21
+ results << Result.new(:ok, "Architecture: #{@environment.architecture}")
22
+ config = load_config(results)
23
+ command = resolve(config, results) if config
24
+ check_version(command, results) if command
25
+ results << result(command_available?('git') ? :ok : :warn, 'Git is available',
26
+ 'Git is not available to the WSLC build environment')
27
+ results
28
+ end
29
+
30
+ private
31
+
32
+ def load_config(results)
33
+ config = @loader.load
34
+ invalid = %w[container image].select { |key| config.defaults[key].to_s.empty? }
35
+ results << Result.new(invalid.empty? ? :ok : :fail,
36
+ invalid.empty? ? 'Loaded wip.yml' : "Empty defaults: #{invalid.join(', ')}")
37
+ config
38
+ rescue ConfigError => e
39
+ results << Result.new(:fail, e.message)
40
+ nil
41
+ end
42
+
43
+ def resolve(config, results)
44
+ command = @resolver.resolve(config.wslc_command)
45
+ results << Result.new(:ok, "Found #{command}")
46
+ command
47
+ rescue CommandNotFoundError => e
48
+ results << Result.new(:fail, e.message)
49
+ nil
50
+ end
51
+
52
+ def check_version(command, results)
53
+ _output, status = Open3.capture2e(command, 'version')
54
+ results << Result.new(status.success? ? :ok : :fail,
55
+ status.success? ? 'WSLC is available' : 'WSLC version failed')
56
+ rescue Errno::ENOENT
57
+ results << Result.new(:fail, 'WSLC version failed')
58
+ end
59
+
60
+ def result(condition, ok_message, fail_message)
61
+ Result.new(condition,
62
+ condition == :ok ? ok_message : fail_message)
63
+ end
64
+
65
+ def command_available?(name) = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, name)) }
66
+ end
67
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Detects WSL2, Windows interop, and architecture facts about the host.
5
+ class Environment
6
+ def initialize(stdin: $stdin, stdout: $stdout)
7
+ @stdin = stdin
8
+ @stdout = stdout
9
+ end
10
+
11
+ def wsl2?
12
+ File.read('/proc/version').match?(/microsoft.*WSL2/i)
13
+ rescue Errno::ENOENT
14
+ false
15
+ end
16
+
17
+ def windows_interop? = File.executable?('/proc/sys/fs/binfmt_misc/WSLInterop') || ENV.key?('WSL_INTEROP')
18
+ def interactive? = @stdin.tty? && @stdout.tty?
19
+
20
+ def architecture
21
+ machine = `uname -m`.strip
22
+ { 'x86_64' => 'linux/amd64', 'aarch64' => 'linux/arm64', 'arm64' => 'linux/arm64' }.fetch(machine,
23
+ "linux/#{machine}")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Translates raw WSLC error output into friendlier hints.
5
+ class ErrorInterpreter
6
+ def initialize(architecture: Environment.new.architecture)
7
+ @architecture = architecture
8
+ end
9
+
10
+ def interpret(output)
11
+ case output
12
+ when /pull access denied|insufficient_scope|authorization failed/i then registry_message
13
+ when %r{no matching manifest for linux/(?:amd64|arm64)}i then architecture_message
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def registry_message
20
+ <<~TEXT
21
+ The container registry rejected the request.
22
+
23
+ Try logging in with:
24
+
25
+ wslc registry login -u <username> docker.io
26
+ TEXT
27
+ end
28
+
29
+ def architecture_message
30
+ <<~TEXT
31
+ The image does not contain a manifest for the current CPU architecture.
32
+
33
+ Current architecture:
34
+ #{@architecture}
35
+
36
+ Inspect the image with:
37
+
38
+ docker buildx imagetools inspect <image>
39
+
40
+ Rebuild and push a multi-platform image with:
41
+
42
+ docker buildx build \\
43
+ --platform linux/amd64,linux/arm64 \\
44
+ -t <image> \\
45
+ --push .
46
+ TEXT
47
+ end
48
+ end
49
+ end
data/lib/wip/errors.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ class Error < StandardError; end
5
+ class ConfigError < Error; end
6
+ class CommandNotFoundError < Error; end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ VERSION = '0.1.1'
5
+ end
data/lib/wip.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'wip/version'
4
+ require_relative 'wip/errors'
5
+ require_relative 'wip/config'
6
+ require_relative 'wip/config_loader'
7
+ require_relative 'wip/environment'
8
+ require_relative 'wip/command_resolver'
9
+ require_relative 'wip/error_interpreter'
10
+ require_relative 'wip/command_builder'
11
+ require_relative 'wip/command_runner'
12
+ require_relative 'wip/doctor'
13
+ require_relative 'wip/cli'
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wslc-wip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Wip contributors
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: thor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ description: Wip provides project commands and diagnostics for WSLC development environments.
27
+ executables:
28
+ - wip
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - LICENSE
33
+ - README.md
34
+ - exe/wip
35
+ - lib/wip.rb
36
+ - lib/wip/cli.rb
37
+ - lib/wip/command_builder.rb
38
+ - lib/wip/command_resolver.rb
39
+ - lib/wip/command_runner.rb
40
+ - lib/wip/config.rb
41
+ - lib/wip/config_loader.rb
42
+ - lib/wip/doctor.rb
43
+ - lib/wip/environment.rb
44
+ - lib/wip/error_interpreter.rb
45
+ - lib/wip/errors.rb
46
+ - lib/wip/version.rb
47
+ homepage: https://github.com/example/wip
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ rubygems_mfa_required: 'true'
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '3.2'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.6.7
67
+ specification_version: 4
68
+ summary: A developer-friendly CLI wrapper for Microsoft WSLC
69
+ test_files: []