Class: Wip::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/wip/cli.rb

Overview

Thor-based command-line interface for wip.

Constant Summary collapse

GLOBAL_SWITCHES =

Thor only recognizes class_options (--config, --debug, ...) once they appear after the command name (wip up --config foo). Pull a leading run of them off the front of ARGV and reinsert it right after the command name so wip --config foo up works too.

{
  '--config' => :value,
  '--env-file' => :value,
  '--debug' => :flag,
  '--debug-log' => :value
}.freeze
AUTO_RESTART_POLICIES =

Real Compose values that trigger a restart when a container's exited; no (the default) and anything unrecognized both mean "leave it alone." Exact match only — a typo'd value like "always-invalid" must stay inert, not accidentally match via a loose prefix check.

%w[always unless-stopped].freeze
ON_FAILURE_POLICY =

optional :MAX_RETRIES suffix, e.g. "on-failure:3"

/\Aon-failure(?::\d+)?\z/
WSLC_CONTAINER_STATE_EXITED =

WSLC's WslcContainerState (confirmed via microsoft/WSL's own docs and ContainerModel.h — ContainerInformation#State has no custom JSON enum serializer, so nlohmann::json emits its raw ordinal): 0 invalid, 1 created, 2 running, 3 exited, 4 deleted. Unlike Docker, there's no separate "dead" state — only exited is a live, restartable exit; deleted means the container itself is gone (needs wip up to recreate it, not start).

3

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.dispatch(meth, given_args, given_opts, config) ⇒ Object

Thor only falls back to the default task when no command name is given at all; an unrecognized first argument (e.g. a custom wip.yml command name) would otherwise raise "Could not find command". Route those to dispatch.



60
61
62
63
64
65
66
67
# File 'lib/wip/cli.rb', line 60

def self.dispatch(meth, given_args, given_opts, config)
  name = given_args.first
  if meth.nil? && name && !name.to_s.start_with?('-') && find_command_possibilities(name).empty?
    return super('dispatch', given_args, given_opts, config)
  end

  super
end

.exit_on_failure?Boolean

Returns:

  • (Boolean)


55
# File 'lib/wip/cli.rb', line 55

def self.exit_on_failure? = true

.reorder_global_options(args) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/wip/cli.rb', line 34

def self.reorder_global_options(args)
  remaining = args.dup
  extracted = []

  until remaining.empty?
    name, _, inline_value = remaining.first.to_s.partition('=')
    kind = GLOBAL_SWITCHES[name]
    break unless kind

    switch = remaining.shift
    extracted << switch
    extracted << remaining.shift if kind == :value && inline_value.empty? && !remaining.empty?
  end

  command_index = remaining.index { |arg| !arg.start_with?('-') }
  return args if command_index.nil?

  remaining.insert(command_index + 1, *extracted)
  remaining
end

.start(given_args = ARGV, config = {}) ⇒ Object



30
31
32
# File 'lib/wip/cli.rb', line 30

def self.start(given_args = ARGV, config = {})
  super(reorder_global_options(given_args), config)
end

Instance Method Details

#build(*extra) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/wip/cli.rb', line 106

def build(*extra)
  extra = build_extra_options(extra)
  settings = load_config.command('build') || {}
  context = Pathname(load_config.path).dirname.join(settings['context'] || '.').to_s
  warn "wip: staging build context (#{context})"
  progress = StagingProgress.new
  build_context = BuildContext.new(context, shadow_root: settings['shadow_context'])
  build_context.stage(on_progress: progress.method(:tick)) do |staged_context|
    progress.finish
    run_staged_build(build_context, staged_context, settings, extra)
  end
ensure
  progress&.finish
end

#configObject



100
101
102
# File 'lib/wip/cli.rb', line 100

def config
  puts YAML.dump(load_config.to_h)
end

#dispatch(name = nil, *arguments) ⇒ Object



252
253
254
255
256
257
258
259
# File 'lib/wip/cli.rb', line 252

def dispatch(name = nil, *arguments)
  return help if name.nil?

  values = load_config.command(name) || raise(ConfigError, "Unknown command: #{name}")
  return dispatch_compose(name, values, arguments) if load_config.compose?

  execute(builder.custom(name, arguments), interactive: tty?(values.fetch('interactive', false)))
end

#doctorObject



93
94
95
96
97
# File 'lib/wip/cli.rb', line 93

def doctor
  results = Doctor.new(loader: loader).call
  results.each { |item| puts "[#{item.level.to_s.upcase}] #{item.message}" }
  exit(1) if results.any? { |item| item.level == :fail }
end

#downObject



189
190
191
192
193
194
195
196
# File 'lib/wip/cli.rb', line 189

def down
  return execute(compose_bridge.down, exit_on_failure: false) if load_config.compose?

  execute(builder.remove, exit_on_failure: false)
  sidecar_names.each do |name|
    execute(builder.dependency_remove(name), exit_on_failure: false)
  end
end

#exec(*command) ⇒ Object



200
201
202
# File 'lib/wip/cli.rb', line 200

def exec(*command)
  execute(exec_target(command, interactive: options[:interactive]), interactive: tty?(options[:interactive]))
end

#initObject

Raises:



83
84
85
86
87
88
89
90
# File 'lib/wip/cli.rb', line 83

def init
  path = Pathname(options[:config] || ConfigLoader::FILENAME).expand_path
  raise Error, "#{path} already exists (use --force to overwrite)" if path.file? && !options[:force]

  initializer = Initializer.new(dir: path.dirname, template: options[:template])
  path.write(initializer.call)
  warn "wip: wrote #{path} (mode: #{initializer.compose? ? 'compose-native' : 'container'})"
end

#logs(*services) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/wip/cli.rb', line 232

def logs(*services)
  unless load_config.compose? || load_config.compose_native?
    raise ConfigError, '`wip logs` is only available in compose mode'
  end
  if load_config.compose?
    return execute(compose_bridge.logs(services: services, follow: options[:follow]),
                   interactive: true)
  end

  # wslc has no compose-style multi-service log aggregation (see CommandBuilder#logs) —
  # exactly one SERVICE is required, defaulting to compose.service when none is given.
  if services.size > 1
    raise ConfigError, '`wip logs` under mode: compose-native takes at most one SERVICE ' \
                       '(wslc logs, unlike a real compose tool, only follows one container at a time)'
  end

  execute(builder.logs(services.first || load_config.compose_service, follow: options[:follow]), interactive: true)
end

#run_command(*command) ⇒ Object



206
207
208
209
210
211
212
213
214
215
# File 'lib/wip/cli.rb', line 206

def run_command(*command)
  if load_config.compose?
    warn "wip: compose mode has no ephemeral 'run'; executing in the running " \
         "'#{load_config.compose_service}' service instead"
    return execute(exec_target(command, interactive: options[:interactive]),
                   interactive: tty?(options[:interactive]))
  end

  execute(builder.run(command, interactive: options[:interactive]), interactive: tty?(options[:interactive]))
end

#shell_commandObject



219
220
221
222
223
224
225
226
227
# File 'lib/wip/cli.rb', line 219

def shell_command
  configured = load_config.command('shell')
  return dispatch('shell') if configured

  code = execute(exec_target(['bash'], interactive: true), interactive: tty?(true), exit_on_failure: false)
  return if code.zero?

  execute(exec_target(['sh'], interactive: true), interactive: tty?(true))
end

#stopObject



179
180
181
182
183
184
185
186
# File 'lib/wip/cli.rb', line 179

def stop
  return execute(compose_bridge.stop, exit_on_failure: false) if load_config.compose?

  execute(builder.stop, exit_on_failure: false)
  sidecar_names.each do |name|
    execute(builder.dependency_stop(name), exit_on_failure: false)
  end
end

#syncObject



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/wip/cli.rb', line 161

def sync
  settings = sync_settings!
  warn_shadowed_command('sync')
  ensure_sync_image(settings)
  return run_sync unless options[:watch]

  interval = watch_interval(settings)
  warn "wip: syncing #{settings.source} -> #{settings.volume}:#{settings.target} " \
       "every #{interval}s (Ctrl-C to stop)"
  loop do
    run_sync(exit_on_failure: false)
    sleep interval
  end
rescue Interrupt
  warn "\nwip: sync stopped"
end

#upObject



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/wip/cli.rb', line 142

def up
  return up_via_compose_bridge if load_config.compose?

  # Validated up front, before any startup side effect (image build, network/dependency/
  # container creation) — otherwise a bad --interval would only surface as a ConfigError
  # after already bringing everything up.
  interval = restart_interval if options[:watch]

  ensure_compose_images
  ensure_network
  sidecar_names.each { |name| ensure_dependency(name) }
  sync_before_boot if options[:sync]
  ensure_container
  watch_restarts(interval) if options[:watch]
end

#versionObject



70
71
72
73
74
75
76
77
# File 'lib/wip/cli.rb', line 70

def version
  puts "wip #{VERSION}"
  config = load_config
  command = resolver.resolve(config.wslc_command)
  system(command, 'version')
rescue Error
  nil
end