squared 0.3.6 → 0.4.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.
@@ -0,0 +1,622 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squared
4
+ module Workspace
5
+ module Project
6
+ class Docker < Base
7
+ include Prompt
8
+
9
+ COMPOSEFILE = %w[compose.yaml compose.yml docker-compose.yaml compose.yml docker-compose.yml].freeze
10
+ BAKEFILE = %w[docker-bake.json docker-bake.hcl docker-bake.override.json docker-bake.override.hcl].freeze
11
+ DIR_DOCKER = (COMPOSEFILE + BAKEFILE).freeze
12
+ OPT_DOCKER = {
13
+ common: %w[tls tlsverify config=p c|context=b D|debug H|host=q l|log-level=b tlscacert=p tlscert=p
14
+ tlskey=p].freeze,
15
+ buildx: {
16
+ common: %w[builder=b D|debug],
17
+ build: %w[load pull push add-host=q annotation=q attest=q build-arg=qq ent=q iidfile=p label=q a-file=p
18
+ network=b no-cache-filter=b o|output=q platform=b q|quiet secret=qq shm-size=b ssh=qq t|tag=b
19
+ target=b ulimit=q].freeze,
20
+ bake: %w[load print pull push list=q metadata-file=p set=q].freeze,
21
+ shared: %w[check no-cache allow=q call=b? f|file=p progress=b provenance=q sbom=q].freeze
22
+ }.freeze,
23
+ compose: {
24
+ common: %w[all-resources compatibility dry-run ansi|b env-file=p f|file=p parallel=b profile=b progress=b
25
+ project-directory=p p|project-name=e].freeze,
26
+ build: %w[no-cache pull push with-dependencies q|quiet build-arg=qq builder=b m|memory=b ssh=qq].freeze,
27
+ exec: %w[dry-run privileged d|detach e|env=qq index=i T|no-TTY=b? user=e w|workdir=q].freeze,
28
+ run: %w[build dry-run no-deps quiet-pull remove-orphans rm P|service-ports use-aliases cap-add=b cap-drop=b
29
+ d|detach entrypoint=q e|env=qq i|interactive=b? l|label=q name=b T|no-TTY=b? p|publish=e pull=b
30
+ u|user=e v|volume=q w|workdir=q].freeze,
31
+ up: %w[y abort-on-container-exit abort-on-container-failure always-recreate-deps attach-dependencies build
32
+ d|detach dry-run force-recreate menu no-build no-color no-deps no-log-prefix no-recreate no-start
33
+ quiet-pull remove-orphans V|renew-anon-volumes timestamps wait w|watch attach=b exit-code-from=b
34
+ no-attach=b pull=b scale=i t|timeout=i wait-timeout=i].freeze
35
+ }.freeze,
36
+ container: {
37
+ run: %w[d|detach init i|interactive no-healthcheck oom-kill-disable privileged P|publish-all q|quiet
38
+ read-only rm runtime t|tty add-host=q annotation=q a|attach=b blkio-weight-device=i cap-add=b
39
+ cap-drop=b cgroup-parent=b cgroupns=b cidfile=p detach-keys=q device=q device-cgroup-rule=q
40
+ device-read-bps=q device-read-iops=q device-write-bps=q device-write-iops=q
41
+ disable-content-trust=b? dns=e dns-option=e dns-search=e domainname=b entrypoint=q e|env=qq
42
+ env-file=p expose=e gpus=q group-add=b health-cmd=q health-interval=b health-retries=i
43
+ health-start-interval=b health-start-period=b health-timeout=b h|hostname=e io-maxbandwidth=b
44
+ io-maxiops=b ip=b ip6=e ipc=b isolation=b kernel-memory=b l|label=q label-file=p link=b
45
+ link-local-ip=b log-driver=b log-opt=q mac-address=e memory-swappiness=b mount=qq name=b network=b
46
+ network-alias=b oom-score-adj=b pid=b platform=b p|publish=e pull=b restart=b runtime=b
47
+ security-opt=q shm-size=b sig-proxy=b? stop-signal=b stop-timeout=i storage-opt=q sysctl=q tmpfs=q
48
+ ulimit=q user=e userns=b uts=b v|volume=q volume-driver=b volumes-from=b w|workdir=q].freeze,
49
+ exec: %w[d|detach i|interactive privileged t|tty detach-keys=q e|env=qq env-file=p user=e
50
+ w|workdir=q].freeze,
51
+ update: %w[blkio-weight=i cpu-period=i cpu-quota=i cpu-rt-period=i cpu-rt-runtime=i c|cpu-shares=i cpus=f
52
+ cpuset-cpus=b cpuset-mems=b m|memory=b memory-reservation=b memory-swap=b pids-limit=b
53
+ restart=q].freeze,
54
+ commit: %w[a|author=q c|change=q m|message=q pause=b?].freeze,
55
+ inspect: %w[s|size f|format=q].freeze,
56
+ start: %w[a|attach i|interactive detach-keys=q].freeze,
57
+ stop: %w[s|signal=b t|time=i].freeze,
58
+ restart: %w[s|signal=b t|time=i].freeze,
59
+ kill: %w[s|signal=b].freeze,
60
+ stats: %w[no-trunc format|q].freeze
61
+ }.freeze,
62
+ image: {
63
+ list: %w[a|all digests no-trunc f|filter=q format=q].freeze,
64
+ push: %w[a|all-tags disable-content-trust=b? platform=b q|quiet].freeze,
65
+ rm: %w[f|force no-prune].freeze
66
+ }.freeze
67
+ }.freeze
68
+ VAL_DOCKER = {
69
+ run: {
70
+ bind: %w[type source src destination dst target readonly ro bind-propagation].freeze,
71
+ tmpfs: %w[type destination dst target tmpfs-size tmpfs-mode].freeze
72
+ }.freeze
73
+ }.freeze
74
+ private_constant :COMPOSEFILE, :BAKEFILE, :OPT_DOCKER, :VAL_DOCKER
75
+
76
+ class << self
77
+ def tasks
78
+ [].freeze
79
+ end
80
+
81
+ def config?(val)
82
+ return false unless (val = as_path(val))
83
+
84
+ val.join('Dockerfile').exist? || DIR_DOCKER.any? { |file| val.join(file).exist? }
85
+ end
86
+ end
87
+
88
+ @@tasks[ref] = {
89
+ 'build' => %i[tag context bake].freeze,
90
+ 'compose' => %i[build run exec up].freeze,
91
+ 'image' => %i[list rm push].freeze,
92
+ 'container' => %i[run exec update commit inspect diff start stop restart pause unpause top stats kill
93
+ rm].freeze
94
+ }.freeze
95
+
96
+ attr_reader :context
97
+ attr_accessor :tag
98
+
99
+ def initialize(*, file: nil, context: nil, tag: nil, secrets: nil, mounts: [], registry: nil, **kwargs)
100
+ super
101
+ return unless dockerfile(file).exist?
102
+
103
+ @context = context
104
+ @tag = tag || "#{@project}:latest"
105
+ @mounts = mounts
106
+ @secrets = secrets
107
+ @registry = [registry, kwargs[:username]].compact.join('/')
108
+ initialize_ref Docker.ref
109
+ initialize_logger(**kwargs)
110
+ initialize_env(**kwargs)
111
+ @output[4] = merge_opts(kwargs[:args], @output[4]) if kwargs[:args]
112
+ end
113
+
114
+ def ref
115
+ Docker.ref
116
+ end
117
+
118
+ def populate(*, **)
119
+ super
120
+ return unless ref?(Docker.ref)
121
+
122
+ namespace name do
123
+ @@tasks[Docker.ref].each do |action, flags|
124
+ next if @pass.include?(action)
125
+
126
+ namespace action do
127
+ flags.each do |flag|
128
+ case action
129
+ when 'build'
130
+ case flag
131
+ when :tag, :context
132
+ format_desc(action, flag, 'opts*', before: flag == :tag ? 'name' : 'dir')
133
+ task flag, [flag] do |_, args|
134
+ param = param_guard(action, flag, args: args, key: flag)
135
+ buildx(:build, args.to_a.drop(1), "#{flag}": param)
136
+ end
137
+ when :bake
138
+ format_desc action, flag, 'opts*,target*,context?'
139
+ task flag do |_, args|
140
+ args = param_guard(action, flag, args: args.to_a)
141
+ buildx flag, args
142
+ end
143
+ end
144
+ when 'compose'
145
+ case flag
146
+ when :build, :up
147
+ format_desc action, flag, 'opts*,service*'
148
+ task flag do |_, args|
149
+ composex flag, args.to_a
150
+ end
151
+ when :exec, :run
152
+ format_desc action, flag, "service,command#{flag == :exec ? '' : '?'},args*,opts*"
153
+ task flag, [:service] do |_, args|
154
+ service = param_guard(action, flag, args: args, key: :service)
155
+ composex(flag, args.to_a.drop(1), service: service)
156
+ end
157
+ end
158
+ when 'container'
159
+ case flag
160
+ when :exec, :commit
161
+ format_desc(action, flag, 'id/name,opts*', after: flag == :exec ? 'args+' : 'tag?')
162
+ task flag, [:id] do |_, args|
163
+ id = param_guard(action, flag, args: args, key: :id)
164
+ container(flag, args.to_a.drop(1), id: id)
165
+ end
166
+ when :run
167
+ format_desc action, flag, 'image,opts*,args*'
168
+ task flag, [:image] do |_, args|
169
+ image = param_guard(action, flag, args: args, key: :image)
170
+ container(flag, args.to_a.drop(1), id: image)
171
+ end
172
+ else
173
+ format_desc(action, flag, 'opts*', after: "id/name#{flag == :update ? '+' : '*'}")
174
+ task flag do |_, args|
175
+ container flag, args.to_a
176
+ end
177
+ end
178
+ when 'image'
179
+ case flag
180
+ when :push
181
+ format_desc action, flag, 'tag,registry/username?,opts*'
182
+ task flag, [:tag] do |_, args|
183
+ tag = param_guard(action, flag, args: args, key: :tag)
184
+ image(flag, args.to_a.drop(1), id: tag)
185
+ end
186
+ else
187
+ format_desc(action, flag, flag == :rm ? 'id*,opts*' : 'opts*,args*')
188
+ task flag do |_, args|
189
+ image flag, args.to_a
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ def clean(*, sync: invoked_sync?('clean'), **)
200
+ return super unless @clean.nil?
201
+
202
+ image(:rm, sync: sync)
203
+ end
204
+
205
+ def compose(opts, flags = nil, script: false, args: nil, from: :build, **)
206
+ return opts if script == false
207
+
208
+ ret = docker_session
209
+ if from == :build
210
+ case (n = filetype)
211
+ when 1, 2
212
+ ret << 'buildx' << 'bake'
213
+ append_file n
214
+ from = :bake
215
+ when 3, 4
216
+ ret << 'compose' << 'build'
217
+ append_file n
218
+ from = :compose
219
+ else
220
+ ret << 'build'
221
+ end
222
+ else
223
+ ret << from
224
+ end
225
+ case opts
226
+ when String
227
+ ret << opts
228
+ when Hash
229
+ ret.merge(append_hash(opts, build: true))
230
+ when Enumerable
231
+ ret.merge(opts.to_a)
232
+ end
233
+ [args, flags].each_with_index do |target, index|
234
+ next unless target
235
+
236
+ target = append_any(target, target: []) unless target.is_a?(Array)
237
+ ret.merge(target.map { |arg| index == 0 ? fill_option(arg) : quote_option('build-arg', arg) })
238
+ end
239
+ case from
240
+ when :build
241
+ case @secrets
242
+ when String
243
+ quote_option('secret', @secrets, double: true)
244
+ when Hash
245
+ append = lambda do |type|
246
+ as_a(@secrets[type]).each { |arg| ret << quote_option('secret', "type=#{type},#{arg}", double: true) }
247
+ end
248
+ append.(:file)
249
+ append.(:env)
250
+ else
251
+ as_a(@secrets).each { |arg| ret << quote_option('secret', arg) }
252
+ end
253
+ if (val = option('tag', ignore: false))
254
+ ret << quote_option('tag', val)
255
+ elsif !session_arg?('t', 'tag')
256
+ ret << quote_option('tag', tag)
257
+ end
258
+ append_context
259
+ when :bake, :compose
260
+ if (val = option(from == :bake ? 'target' : 'service', ignore: false))
261
+ ret.merge(split_escape(val).map { |s| shell_escape(s) })
262
+ end
263
+ end
264
+ ret
265
+ end
266
+
267
+ def buildx(flag, opts = [], tag: nil, context: nil)
268
+ cmd, opts = docker_session('buildx', opts: opts)
269
+ opts = option_sanitize(opts, OPT_DOCKER[:buildx][:common]).first
270
+ cmd << flag
271
+ out = option_sanitize(opts, OPT_DOCKER[:buildx][flag] + OPT_DOCKER[:buildx][:shared]).first
272
+ case flag
273
+ when :build
274
+ cmd.merge(as_a(tag).map { |val| quote_option('tag', val) })
275
+ append_context context
276
+ when :bake
277
+ unless out.empty?
278
+ args = out.dup
279
+ out.clear
280
+ if Dir.exist?(args.last)
281
+ if projectpath?(val = args.pop)
282
+ context = val
283
+ else
284
+ out << val
285
+ end
286
+ end
287
+ append_value(args, escape: true)
288
+ contextdir(context) if context
289
+ end
290
+ end
291
+ option_clear out
292
+ run(from: :"buildx:#{flag}")
293
+ end
294
+
295
+ def composex(flag, opts = [], service: nil)
296
+ cmd, opts = docker_session('compose', opts: opts)
297
+ opts = option_sanitize(opts, OPT_DOCKER[:compose][:common]).first
298
+ append_file filetype unless session_arg?('f', 'file')
299
+ cmd << flag
300
+ out = option_sanitize(opts, OPT_DOCKER[:compose][flag] + OPT_DOCKER[:common]).first
301
+ from = :"compose:#{flag}"
302
+ case flag
303
+ when :build, :up
304
+ append_value(out, escape: true)
305
+ when :exec, :run
306
+ append_command(flag, service, out, from: from)
307
+ end
308
+ run(from: from)
309
+ end
310
+
311
+ def container(flag, opts = [], id: nil)
312
+ cmd, opts = docker_session('container', flag, opts: opts)
313
+ list = OPT_DOCKER[:container].fetch(flag, [])
314
+ list += OPT_DOCKER[:container][:update] if flag == :run
315
+ out = option_sanitize(opts, list, first: flag == :exec).first
316
+ from = :"container:#{flag}"
317
+ case flag
318
+ when :run, :exec
319
+ if flag == :run && !session_arg?('mount')
320
+ run = VAL_DOCKER[:run]
321
+ both = run[:bind] + run[:tmpfs]
322
+ diff = run[:bind].reject { |val| run[:tmpfs].include?(val) }
323
+ delim = Regexp.new(",\\s*(?=#{both.join('|')})")
324
+ as_a(@mounts).each do |val|
325
+ args = []
326
+ tmpfs = true
327
+ val.split(delim).each do |opt|
328
+ k, v, q = split_option(opt)
329
+ next unless both.include?(k)
330
+
331
+ if k == 'type'
332
+ tmpfs = false if v == 'bind'
333
+ next
334
+ elsif diff.include?(k)
335
+ tmpfs = false
336
+ end
337
+ case k
338
+ when 'readonly', 'ro'
339
+ args << k
340
+ next
341
+ when 'source', 'src', 'destination', 'dst', 'target'
342
+ v = basepath(v)
343
+ v = shell_quote(v, option: false, force: false) if q == ''
344
+ tmpfs = false if k[0] == 's'
345
+ end
346
+ args << "#{k}=#{q + v + q}"
347
+ end
348
+ cmd << "--mount type=#{tmpfs ? 'tmpfs' : 'bind'},#{args.join(',')}"
349
+ end
350
+ end
351
+ append_command(flag, id.to_s.empty? ? tag : id, out, target: cmd, from: from)
352
+ when :update
353
+ raise_error('missing container', hint: from) if out.empty?
354
+ append_value(out, escape: true)
355
+ when :commit
356
+ latest = out.shift || tag
357
+ cmd << id << latest
358
+ raise_error("unknown args: #{out.join(', ')}", hint: from) unless out.empty?
359
+ return unless confirm_command(cmd.to_s, title: from, target: id, as: latest)
360
+
361
+ registry = option('registry') || @registry
362
+ run(from: from, exception: registry.nil? ? exception : true)
363
+ return unless registry
364
+
365
+ opts = []
366
+ append_option('platform', target: opts, equals: true)
367
+ case option('disable-content-trust', ignore: false)
368
+ when 'true', '1'
369
+ opts << 'disable-content-trust'
370
+ when 'false', '0'
371
+ opts << 'disable-content-trust=false'
372
+ end
373
+ opts << '--quiet' unless verbose
374
+ return image(:push, opts, id: latest, registry: registry)
375
+ else
376
+ if out.empty?
377
+ ps = docker_output 'ps', '-a'
378
+ status = []
379
+ no = true
380
+ case flag
381
+ when :inspect, :diff
382
+ no = false
383
+ when :start
384
+ status = %w[created exited]
385
+ no = false
386
+ when :stop, :pause
387
+ status = %w[running restarting]
388
+ when :restart
389
+ status = %w[running paused exited]
390
+ when :unpause
391
+ status << 'paused'
392
+ no = false
393
+ when :top, :stats
394
+ status << 'running'
395
+ cmd << '--no-stream' if flag == :stats
396
+ no = false
397
+ when :kill
398
+ status = %w[running restarting paused]
399
+ when :rm
400
+ status = %w[created exited dead]
401
+ end
402
+ ps.merge(status.map { |s| "--filter=\"status=#{s}\"" })
403
+ list_image(flag, ps, no: no, hint: "status: #{status.join(', ')}", from: from) do |id|
404
+ run(cmd.temp(id), from: from)
405
+ end
406
+ return
407
+ else
408
+ append_value(out, escape: true)
409
+ end
410
+ end
411
+ run(from: from)
412
+ end
413
+
414
+ def image(flag, opts = [], sync: true, id: nil, registry: nil)
415
+ cmd, opts = docker_session('image', flag == :exec ? :list : flag, opts: opts)
416
+ out = option_sanitize(opts, OPT_DOCKER[:image][flag]).first
417
+ from = :"image:#{flag}"
418
+ case flag
419
+ when :list
420
+ if opts.size == out.size
421
+ index = 0
422
+ name = nil
423
+ opts.each do |opt|
424
+ if (name = opt[/^name=["']?(.+)["']?$/, 1])
425
+ opts.delete(opt)
426
+ break
427
+ end
428
+ end
429
+ flag = :run if flag == :list
430
+ list_image(flag, cmd << '-a', from: from) do |val|
431
+ container(flag, if name
432
+ opts.dup << "name=#{index == 0 ? name : "#{name}-#{index}"}"
433
+ else
434
+ opts
435
+ end, id: val)
436
+ index += 1
437
+ end
438
+ return
439
+ else
440
+ option_clear out
441
+ end
442
+ when :rm
443
+ if id
444
+ cmd << id
445
+ elsif !out.empty?
446
+ out.each { |val| run(cmd.temp(val), sync: sync, from: from) }
447
+ return
448
+ else
449
+ list_image(flag, docker_output('image', 'ls -a'), from: from) do |val|
450
+ image(:rm, opts, sync: sync, id: val)
451
+ end
452
+ return
453
+ end
454
+ when :push
455
+ id ||= tag
456
+ registry ||= out.shift || @registry
457
+ raise_error(id ? "unknown args: #{out.join(', ')}" : 'no id/tag given', hint: from) unless id && out.empty?
458
+ raise_error('username/registry not provided', hint: from) unless registry
459
+ registry.chomp!('/')
460
+ uri = shell_quote("#{registry}/#{id}")
461
+ cmd << uri
462
+ img = docker_output 'image', 'tag', id, uri
463
+ return unless confirm_command(img.to_s, cmd.to_s, target: id, as: registry, title: from)
464
+
465
+ run(img, exception: true, sync: false, banner: false)
466
+ end
467
+ run(sync: sync, from: from)
468
+ end
469
+
470
+ def build?
471
+ @output[0] != false && dockerfile.exist?
472
+ end
473
+
474
+ def clean?
475
+ super || dockerfile.exist?
476
+ end
477
+
478
+ def dockerfile(val = nil)
479
+ if val == 'Dockerfile'
480
+ @file = false
481
+ elsif val
482
+ @file = if val.is_a?(Array)
483
+ val = val.select { |file| basepath(file).exist? }
484
+ val.size > 1 ? val : val.first
485
+ else
486
+ val || DIR_DOCKER.find { |file| basepath(file).exist? }
487
+ end
488
+ @file ||= false
489
+ end
490
+ basepath((@file.is_a?(Array) ? @file.first : @file) || 'Dockerfile')
491
+ end
492
+
493
+ private
494
+
495
+ def docker_session(*cmd, opts: nil)
496
+ return session('docker', *cmd) unless opts
497
+
498
+ ret = session 'docker'
499
+ opts = option_sanitize(opts, OPT_DOCKER[:common]).first
500
+ [ret.merge(cmd), opts]
501
+ end
502
+
503
+ def docker_output(*cmd, **kwargs)
504
+ session('docker', *cmd, main: false, options: false, **kwargs)
505
+ end
506
+
507
+ def append_command(flag, val, list, target: @session, from: nil)
508
+ raise_error('no command args', hint: from) if flag == :exec && list.empty?
509
+ target << val << list.shift
510
+ target << shell_quote(list.join(' && '), double: true, option: false) unless list.empty?
511
+ end
512
+
513
+ def append_file(type, target: @session)
514
+ return unless type == 2 || type == 4 || @file.is_a?(Array)
515
+
516
+ target.merge(as_a(@file).map { |val| quote_option('file', basepath(val)) })
517
+ end
518
+
519
+ def append_context(ctx = nil, target: @session)
520
+ if @file.is_a?(String) && !session_arg?('f', 'file') && !bake?(dockerfile) && !compose?(dockerfile)
521
+ target << quote_option('file', dockerfile)
522
+ end
523
+ target << contextdir(ctx || context)
524
+ end
525
+
526
+ def list_image(flag, cmd, hint: nil, from: nil, no: true, &blk)
527
+ pwd_set do
528
+ found = false
529
+ IO.popen(session_done(cmd << '--format=json')).each_with_index do |line, index|
530
+ data = JSON.parse(line)
531
+ rt = [data['Repository'], data['Tag']].reject { |val| val == '<none>' }.join(':')
532
+ rt = nil if tag.empty?
533
+ aa = if data['Names']
534
+ as_a(data['Names']).join(', ')
535
+ elsif tag
536
+ dd = true
537
+ data['Repository']
538
+ else
539
+ data['ID']
540
+ end
541
+ bb = index.succ.to_s
542
+ cc = bb.size + 1
543
+ a = sub_style(data['Image'] || rt || aa, styles: theme[:inline])
544
+ b = "Execute #{sub_style(flag, styles: theme[:active])} on #{a} (#{data['ID']})"
545
+ c, d = no ? ['y/N', 'N'] : ['Y/n', 'Y']
546
+ e = time_format(time_offset(data['CreatedAt']), pass: ['ms'])
547
+ f = sub_style(ARG[:BORDER][0], styles: theme[:inline])
548
+ g = ' ' * (cc + 1)
549
+ h = "#{sub_style(bb.rjust(cc), styles: theme[:current])} #{f} "
550
+ puts unless index == 0
551
+ puts "#{h + sub_style(aa, styles: theme[:subject])} (created #{e} ago)"
552
+ %w[Tag Status Ports Size].each do |key|
553
+ next if (key == 'Tag' && !dd) || (key == 'Size' && data[key] == '0B')
554
+
555
+ puts "#{g + f} #{key}: #{as_a(data[key]).join(', ')}" unless data[key].to_s.empty?
556
+ end
557
+ w = 9 + flag.to_s.size + 4 + aa.size
558
+ puts g + sub_style(ARG[:BORDER][6] + (ARG[:BORDER][1] * w), styles: theme[:inline])
559
+ found = true
560
+ next unless confirm("#{h + b}? [#{c}] ", d, timeout: 60)
561
+
562
+ puts if @@print_order == 0
563
+ blk.call data['ID']
564
+ end
565
+ puts log_message(Logger::INFO, 'none detected', subject: "#{name}:#{from}", hint: hint) unless found
566
+ end
567
+ rescue StandardError => e
568
+ log.error e
569
+ ret = on(:error, from, e)
570
+ raise if exception && ret != true
571
+
572
+ warn log_message(Logger::WARN, e, pass: true) if warning?
573
+ end
574
+
575
+ def confirm_command(*args, title: nil, target: nil, as: nil)
576
+ return false unless title && target
577
+
578
+ puts unless @@print_order == 0
579
+ t = title.to_s.split(':')
580
+ emphasize(args, title: message(t.first.upcase, *t.drop(1)), border: borderstyle, sub: [
581
+ { pat: /\A(\w+(?: => \w+)+)(.*)\z/, styles: theme[:header] },
582
+ { pat: /\A(.+)\z/, styles: theme[:caution] }
583
+ ])
584
+ @@print_order += 1
585
+ a = t.last.capitalize
586
+ b = sub_style(target, styles: theme[:subject])
587
+ c = as && sub_style(as, styles: theme[:inline])
588
+ confirm("#{a} #{b}#{c ? " as #{c}" : ''}? [y/N] ", 'N', timeout: 60)
589
+ end
590
+
591
+ def filetype(val = dockerfile)
592
+ case File.extname(val)
593
+ when '.hcl', '.json'
594
+ bake?(val) ? 1 : 2
595
+ when '.yml', '.yaml'
596
+ if compose?(val)
597
+ path.children.any? { |file| bake?(file) } ? 1 : 3
598
+ else
599
+ 4
600
+ end
601
+ else
602
+ 0
603
+ end
604
+ end
605
+
606
+ def contextdir(val = nil)
607
+ val && projectpath?(val) ? shell_quote(basepath(val)) : '.'
608
+ end
609
+
610
+ def compose?(file)
611
+ COMPOSEFILE.include?(File.basename(file))
612
+ end
613
+
614
+ def bake?(file)
615
+ BAKEFILE.include?(File.basename(file))
616
+ end
617
+ end
618
+
619
+ Application.implement Docker
620
+ end
621
+ end
622
+ end