squared 0.0.4 → 0.0.6

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,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squared
4
+ module Workspace
5
+ class Application
6
+ include Common
7
+ include Format
8
+ include ::Rake::DSL
9
+
10
+ TASK_BASE = {
11
+ build: [],
12
+ refresh: [],
13
+ depend: [],
14
+ outdated: [],
15
+ doc: [],
16
+ test: [],
17
+ copy: [],
18
+ clean: []
19
+ }
20
+ TASK_EXTEND = {}
21
+ WORKSPACE_KEYS = TASK_BASE.keys.freeze
22
+ private_constant :TASK_BASE, :TASK_EXTEND
23
+
24
+ class << self
25
+ def implement(*objs)
26
+ objs.each do |obj|
27
+ next unless obj < Project::Base
28
+
29
+ project_kind.unshift(obj)
30
+ obj.tasks&.each do |val|
31
+ TASK_BASE[val] ||= []
32
+ (TASK_EXTEND[val] ||= []).push(obj)
33
+ end
34
+ end
35
+ end
36
+
37
+ def find(path: nil, ref: nil)
38
+ if ref.is_a?(::Symbol) || ref.is_a?(::String)
39
+ ret = project_kind.find { |proj| proj.to_sym == ref.to_sym }
40
+ return ret if ret
41
+ end
42
+ project_kind.find { |proj| proj.is_a?(path) } if path
43
+ end
44
+
45
+ def to_s
46
+ super.to_s.match(/[^:]+$/)[0]
47
+ end
48
+
49
+ attr_reader :project_kind
50
+ end
51
+
52
+ @project_kind = []
53
+
54
+ attr_reader :main, :root, :home, :series, :theme, :warning
55
+ attr_accessor :exception, :pipe, :verbose
56
+
57
+ def initialize(main, *, common: true, exception: false, pipe: false, verbose: nil, **)
58
+ @main = main.to_s
59
+ @home = Pathname.pwd
60
+ @root = @home.parent
61
+ @series = Series.new(TASK_BASE, self)
62
+ @project = {}
63
+ @extensions = []
64
+ @script = {
65
+ group: {},
66
+ ref: {},
67
+ build: nil,
68
+ dev: nil,
69
+ prod: nil
70
+ }
71
+ @theme = common && @verbose ? __get__(:theme)[:workspace] : {}
72
+ @exception = exception.is_a?(String) ? !env(exception, ignore: '0').nil? : exception
73
+ @pipe = pipe.is_a?(String) ? !env(pipe, ignore: '0').nil? : pipe
74
+ @verbose = verbose.nil? ? !@pipe : verbose
75
+ @warning = true
76
+ end
77
+
78
+ def build(**kwargs)
79
+ return unless enabled?
80
+
81
+ @project.each_value do |proj|
82
+ next unless proj.enabled?
83
+
84
+ series.populate(proj)
85
+ proj.populate(**kwargs)
86
+ end
87
+
88
+ Application.project_kind.each { |obj| obj.populate(self, **kwargs) }
89
+ @extensions.each { |ext| __send__(ext, **kwargs) }
90
+
91
+ series[:refresh].clear if series[:refresh].all? { |val| val.end_with?(':build') }
92
+
93
+ task default: kwargs[:default] if series.has?(kwargs[:default]) && !task_defined?('default')
94
+
95
+ if series.has?('build')
96
+ init = ['depend', dev? && series.has?('refresh') ? 'refresh' : 'build']
97
+
98
+ task default: init[1] unless task_defined?('default')
99
+
100
+ if series.has?(init[0]) && !task_defined?('init')
101
+ desc 'init'
102
+ task init: init
103
+ end
104
+ end
105
+ series.finalize!(kwargs.fetch(:parallel, []))
106
+
107
+ yield self if block_given?
108
+ end
109
+
110
+ def run(script, group: nil, ref: nil)
111
+ script_command :run, script, group, ref
112
+ end
113
+
114
+ def depend(script, group: nil, ref: nil)
115
+ script_command :depend, script, group, ref
116
+ end
117
+
118
+ def clean(script, group: nil, ref: nil)
119
+ script_command :clean, script, group, ref
120
+ end
121
+
122
+ def doc(script, group: nil, ref: nil)
123
+ script_command :doc, script, group, ref
124
+ end
125
+
126
+ def test(script, group: nil, ref: nil)
127
+ script_command :test, script, group, ref
128
+ end
129
+
130
+ def add(name, path = nil, ref: nil, **kwargs)
131
+ path = root_path((path || name).to_s)
132
+ project = if !ref.is_a?(::Class)
133
+ Application.find(path: path, ref: ref)
134
+ elsif ref < Project::Base
135
+ ref
136
+ end
137
+ instance = (project || Project::Base).new(name, path, self, **kwargs)
138
+ @project[n = name.to_sym] = instance
139
+ __get__(:project)[n] = instance unless kwargs[:private]
140
+ self
141
+ end
142
+
143
+ def group(name, path, **kwargs)
144
+ root_path(path).children.map do |ent|
145
+ next unless (dir = Pathname.new(ent)).directory?
146
+
147
+ index = 0
148
+ basename = dir.basename.to_s.to_sym
149
+ override = kwargs.delete(basename)
150
+ while @project[basename]
151
+ index += 1
152
+ basename = :"#{basename}-#{index}"
153
+ end
154
+ [basename, dir, override]
155
+ end
156
+ .each do |basename, dir, override|
157
+ opts = kwargs.dup
158
+ opts.merge!(override) if override
159
+ add(basename, dir, group: name, **opts)
160
+ end
161
+ self
162
+ end
163
+
164
+ def compose(name, &blk)
165
+ namespace(name, &blk)
166
+ self
167
+ end
168
+
169
+ def apply(&blk)
170
+ instance_eval(&blk)
171
+ self
172
+ end
173
+
174
+ def style(name, *args, target: nil, empty: false)
175
+ data = nil
176
+ if target
177
+ as_a(target).each_with_index do |val, i|
178
+ if i == 0
179
+ break unless (data = __get__(:theme)[val.to_sym])
180
+ else
181
+ data = data[val.to_sym] ||= {}
182
+ end
183
+ end
184
+ return unless data
185
+ end
186
+ apply_style(data || theme, name, *args, empty: empty)
187
+ self
188
+ end
189
+
190
+ def script(group: nil, ref: nil)
191
+ if group || ref
192
+ (group && @script[:group][group.to_sym]) || (ref && @script[:ref][ref.to_sym])
193
+ else
194
+ @script[:build]
195
+ end
196
+ end
197
+
198
+ def env(key, equals: nil, ignore: nil, default: nil)
199
+ ret = ENV.fetch("#{key}_#{main.gsub(/[^\w]/, '_').upcase}", ENV[key]).to_s
200
+ return ret == equals.to_s unless equals.nil?
201
+
202
+ ret.empty? || (ignore && as_a(ignore).any? { |val| ret == val.to_s }) ? default : ret
203
+ end
204
+
205
+ def root_path(*args)
206
+ root.join(*args)
207
+ end
208
+
209
+ def home_path(*args)
210
+ home.join(*args)
211
+ end
212
+
213
+ def find_base(obj)
214
+ Application.project_kind.find { |proj| obj.instance_of?(proj) }
215
+ end
216
+
217
+ def to_s
218
+ root.to_s
219
+ end
220
+
221
+ def inspect
222
+ "#<#{self.class}: #{main} => #{self}>"
223
+ end
224
+
225
+ def enabled?
226
+ !@extensions.empty? || @project.any? { |_, proj| proj.enabled? }
227
+ end
228
+
229
+ def task_base?(key, val)
230
+ return val if task_defined?(key)
231
+
232
+ val if key == :refresh && series[:build].include?(val = "#{val.split(':').first}:build")
233
+ end
234
+
235
+ def task_include?(obj, key)
236
+ TASK_EXTEND.fetch(key, []).any? { |item| obj.is_a?(item) }
237
+ end
238
+
239
+ def task_defined?(key)
240
+ ::Rake::Task.task_defined?(key)
241
+ end
242
+
243
+ def dev?(script: nil, pat: nil, global: nil)
244
+ with?(:dev, script: script, pat: pat, global: global || (pat.nil? && global.nil?))
245
+ end
246
+
247
+ def prod?(script: nil, pat: nil, global: false)
248
+ return false if global && (@script[:dev] == true || !@script[:prod])
249
+
250
+ with?(:prod, script: script, pat: pat, global: global)
251
+ end
252
+
253
+ protected
254
+
255
+ def confirm(msg, agree: 'Y', cancel: 'N', default: nil, attempts: 5, timeout: 15)
256
+ require 'readline'
257
+ require 'timeout'
258
+ agree = /^#{agree}$/i if agree.is_a?(String)
259
+ cancel = /^#{cancel}$/i if cancel.is_a?(String)
260
+ Timeout.timeout(timeout) do
261
+ begin
262
+ while (ch = Readline.readline(msg, true))
263
+ ch = ch.chomp
264
+ ch = default if ch.empty?
265
+ case ch
266
+ when agree
267
+ return true
268
+ when cancel
269
+ return false
270
+ end
271
+ attempts -= 1
272
+ exit 1 unless attempts >= 0
273
+ end
274
+ rescue Interrupt
275
+ puts
276
+ exit 0
277
+ else
278
+ false
279
+ end
280
+ end
281
+ end
282
+
283
+ private
284
+
285
+ def script_command(task, val, group, ref)
286
+ if group
287
+ label = :group
288
+ items = as_a(group)
289
+ else
290
+ label = :ref
291
+ items = as_a(ref)
292
+ end
293
+ items.each { |name| (@script[label][name.to_sym] ||= {})[task] = val }
294
+ self
295
+ end
296
+
297
+ def with?(state, script: nil, pat: nil, global: false)
298
+ if global
299
+ pat = @script[state] if pat.nil?
300
+ script ||= @script[:build]
301
+ end
302
+ pat.is_a?(::Regexp) ? pat.match?(script) : pat == true
303
+ end
304
+ end
305
+ end
306
+ end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
3
  require 'date'
4
+ require 'logger'
5
5
 
6
6
  module Squared
7
- module Repo
7
+ module Workspace
8
8
  module Project
9
9
  class Base
10
10
  include Common
@@ -12,7 +12,8 @@ module Squared
12
12
  include Shell
13
13
  include Task
14
14
  include ::Rake::DSL
15
- extend Forwardable
15
+
16
+ SEM_VER = /^(\d+)(\.)(\d+)(?:(\.)(\d+))?$/.freeze
16
17
 
17
18
  class << self
18
19
  include Common::Task
@@ -25,13 +26,16 @@ module Squared
25
26
  end
26
27
 
27
28
  def as_path(val)
28
- return val if val.is_a?(::Pathname)
29
-
30
- val.is_a?(::String) ? Pathname.new(val) : nil
29
+ case val
30
+ when ::Pathname
31
+ val
32
+ when ::String
33
+ Pathname.new(val)
34
+ end
31
35
  end
32
36
 
33
37
  def to_s
34
- /[^:]+$/.match(super.to_s)[0]
38
+ super.to_s.match(/[^:]+$/)[0]
35
39
  end
36
40
 
37
41
  def to_sym
@@ -42,15 +46,9 @@ module Squared
42
46
  @@print_order = 0
43
47
  @@tasks = {}
44
48
 
45
- alias __warn__ warn
46
-
47
- attr_reader :name, :project, :workspace, :group, :path, :logger, :theme
49
+ attr_reader :name, :project, :workspace, :group, :path, :log, :theme
48
50
  attr_accessor :warning
49
51
 
50
- protected :logger
51
-
52
- def_delegators :logger, :log, :<<, :debug, :info, :warn, :error, :fatal, :unknown
53
-
54
52
  def initialize(name, path, workspace, *, group: nil, log: nil, common: true, **kwargs)
55
53
  @name = name.to_s
56
54
  @path = workspace.root_path(path.to_s)
@@ -63,12 +61,14 @@ module Squared
63
61
  @output = [kwargs[:run], nil]
64
62
  @copy = kwargs[:copy]
65
63
  @clean = kwargs[:clean]
66
- @theme = if common
64
+ @warning = workspace.warning
65
+ @theme = if !workspace.verbose
66
+ {}
67
+ elsif common
67
68
  workspace.theme
68
69
  else
69
70
  __get__(:theme)[:project][to_sym] ||= {}
70
71
  end
71
- @warning = workspace.warning
72
72
  log = { file: log } unless log.is_a?(::Hash)
73
73
  if (logfile = env('LOG_FILE')).nil? && (auto = env('LOG_AUTO'))
74
74
  logfile = case auto
@@ -87,10 +87,10 @@ module Squared
87
87
  logfile.realdirpath
88
88
  rescue StandardError => e
89
89
  logfile = nil
90
- __warn__ e if @warning
90
+ warn e if @warning
91
91
  end
92
92
  end
93
- @logger = Logger.new(logfile, progname: @name, level: env('LOG_LEVEL') || log[:level] || Logger::INFO)
93
+ @log = Logger.new(logfile, progname: @name, level: env('LOG_LEVEL') || log[:level] || Logger::INFO)
94
94
  end
95
95
 
96
96
  def initialize_build(ref, **kwargs)
@@ -100,15 +100,13 @@ module Squared
100
100
  elsif @script && @output[0] != false
101
101
  @output[0] ||= @script[:run]
102
102
  end
103
+ @dev = kwargs.delete(:dev)
103
104
  if env('BUILD', suffix: 'DEV', equals: '0')
104
105
  @dev = false
105
- else
106
- @dev = kwargs.delete(:dev)
107
- if env('BUILD', suffix: 'DEV')
108
- @dev = true
109
- elsif @dev.nil?
110
- @dev = workspace.dev?
111
- end
106
+ elsif env('BUILD', suffix: 'DEV')
107
+ @dev = true
108
+ elsif @dev.nil?
109
+ @dev = workspace.dev?
112
110
  end
113
111
  end
114
112
 
@@ -125,12 +123,8 @@ module Squared
125
123
  def populate(*)
126
124
  namespace name do
127
125
  workspace.series.each_key do |key|
128
- case key
129
- when :build, :refresh, :depend, :outdated, :doc, :test, :copy, :clean
130
- next unless has?(key)
131
- else
132
- next unless workspace.task_defined?(self, key)
133
- end
126
+ next unless Application::WORKSPACE_KEYS.include?(key) ? has?(key) : workspace.task_include?(self, key)
127
+
134
128
  desc message(name, key)
135
129
  task key do
136
130
  __send__(key)
@@ -139,7 +133,7 @@ module Squared
139
133
  end
140
134
  end
141
135
 
142
- def build(*args)
136
+ def build(*args, sync: true)
143
137
  if args.empty?
144
138
  cmd, opts = @output
145
139
  opts &&= shell_escape(opts)
@@ -160,13 +154,13 @@ module Squared
160
154
  cmd = compose(opts)
161
155
  banner = env('REPO_BUILD') == 'verbose'
162
156
  end
163
- run(cmd, exception: workspace.exception, banner: banner)
157
+ run(cmd, exception: workspace.exception, banner: banner, sync: sync)
164
158
  end
165
159
 
166
160
  def refresh(*)
167
- build
161
+ build(sync: invoked_sync?('depend'))
168
162
  key = "#{name}:copy"
169
- if ::Rake::Task.task_defined?(key)
163
+ if workspace.task_defined?(key)
170
164
  invoke key
171
165
  else
172
166
  copy
@@ -174,34 +168,35 @@ module Squared
174
168
  end
175
169
 
176
170
  def depend(*)
177
- run(@depend, exception: workspace.exception) if @depend
171
+ run(@depend, exception: workspace.exception, sync: invoked_sync?('depend')) if @depend
178
172
  end
179
173
 
180
174
  def copy(*)
181
- run_s @copy
175
+ run_s(@copy, sync: invoked_sync?('copy'))
182
176
  end
183
177
 
184
178
  def doc
185
- build @doc if @doc
179
+ build(@doc, sync: invoked_sync?('doc')) if @doc
186
180
  end
187
181
 
188
182
  def test
189
- build @test if @test
183
+ build(@test, sync: invoked_sync?('test')) if @test
190
184
  end
191
185
 
192
186
  def clean
193
187
  return unless @clean
194
188
 
195
189
  if @clean.is_a?(::String)
196
- run_s @clean
190
+ run_s(@clean, sync: invoked_sync?('clean'))
197
191
  else
198
192
  @clean.each do |val|
199
- if (val = val.to_s).match?(%r{[\\/]$})
193
+ val = val.to_s
194
+ if val =~ %r{[\\/]$}
200
195
  dir = Pathname.new(val)
201
196
  dir = base_path(dir) unless dir.absolute?
202
197
  next unless dir.directory?
203
198
 
204
- warn "rm -rf #{dir}"
199
+ log.warn "rm -rf #{dir}"
205
200
  dir.rmtree(verbose: true)
206
201
  else
207
202
  files = val.include?('*') ? Dir[base_path(val)] : [base_path(val)]
@@ -209,7 +204,7 @@ module Squared
209
204
  begin
210
205
  File.delete(file) if File.file?(file)
211
206
  rescue StandardError => e
212
- error e
207
+ log.error e
213
208
  end
214
209
  end
215
210
  end
@@ -246,7 +241,7 @@ module Squared
246
241
  end
247
242
 
248
243
  def refresh?
249
- build? && (copy? || ::Rake::Task.task_defined?("#{name}:copy"))
244
+ build? && (copy? || workspace.task_defined?("#{name}:copy"))
250
245
  end
251
246
 
252
247
  def depend?
@@ -271,36 +266,36 @@ module Squared
271
266
 
272
267
  protected
273
268
 
274
- def run(cmd = @session, exception: false, banner: true, req: nil)
269
+ def run(cmd = @session, exception: false, banner: true, sync: true, req: nil)
275
270
  return if req && !base_path(req).exist?
276
271
 
277
272
  cmd = close_session(cmd)
278
- info cmd
279
- print_item format_banner(cmd, banner: banner)
273
+ log.info cmd
274
+ print_item format_banner(cmd, banner: banner) if sync
280
275
  begin
281
276
  shell(cmd, chdir: path, exception: exception)
282
277
  rescue StandardError => e
283
- error e
278
+ log.error e
284
279
  raise
285
280
  end
286
281
  end
287
282
 
288
- def run_s(cmd)
289
- run(cmd, exception: workspace.exception, banner: verbose?) if cmd.is_a?(::String)
283
+ def run_s(cmd, **kwargs)
284
+ run(cmd, exception: workspace.exception, banner: verbose?, **kwargs) if cmd.is_a?(::String)
290
285
  end
291
286
 
292
- def env(key, suffix: nil, equals: nil, strict: false)
293
- alt = "#{key}_#{name.upcase}"
294
- ret = if suffix
295
- ENV.fetch(([alt] + as_a(suffix)).join('_'), '')
296
- else
297
- ENV.fetch(alt, strict ? '' : ENV[key]).to_s
298
- end
299
- if !equals.nil?
300
- ret == equals.to_s
301
- elsif !ret.empty? && ret != '0'
302
- ret
287
+ def env(key, equals: nil, ignore: ['0'].freeze, default: nil, suffix: nil, strict: false)
288
+ a = "#{key}_#{name.upcase}"
289
+ b = ''
290
+ if suffix
291
+ a = [a, suffix].flatten.join('_')
292
+ elsif !strict
293
+ b = ENV.fetch(key, '')
303
294
  end
295
+ ret = ENV.fetch(a, b)
296
+ return ret == equals.to_s unless equals.nil?
297
+
298
+ ret.empty? || as_a(ignore).any? { |val| ret == val.to_s } ? default : ret
304
299
  end
305
300
 
306
301
  def session(*cmd, options: nil)
@@ -312,8 +307,8 @@ module Squared
312
307
 
313
308
  def close_session(cmd)
314
309
  return cmd unless cmd.respond_to?(:done)
315
- raise ArgumentError, message('none were provided', hint: name) if cmd.empty?
316
310
 
311
+ raise_error('none were provided', hint: name) if cmd.empty?
317
312
  @session = nil if cmd == @session
318
313
  cmd.done
319
314
  end
@@ -324,26 +319,40 @@ module Squared
324
319
  puts val unless val.empty? || (val.size == 1 && val.first.nil?)
325
320
  end
326
321
 
327
- def print_banner(*lines, styles: nil, pad: 0, first: false)
322
+ def print_banner(*lines, styles: theme[:banner], border: theme[:border])
323
+ pad = 0
324
+ if styles
325
+ if styles.any? { |s| s.to_s.end_with?('!') }
326
+ pad = 1
327
+ elsif styles.size <= 1
328
+ styles = [:bold] + styles
329
+ end
330
+ end
328
331
  n = max_width(lines)
329
332
  ch = ' ' * pad
330
333
  index = -1
331
334
  out = lines.map do |val|
332
335
  index += 1
333
336
  val = ch + val.ljust(n - (pad * 2)) + ch
334
- if styles && (!first || index == 0)
337
+ if styles && (pad == 1 || index == 0)
335
338
  sub_style(val, *styles)
336
339
  else
337
340
  val
338
341
  end
339
342
  end
340
- out << ('-' * n)
343
+ out << sub_style('-' * n, styles: border)
341
344
  out.join("\n")
342
345
  end
343
346
 
344
- def print_footer(*lines)
347
+ def print_footer(*lines, sub: nil, border: theme[:border])
345
348
  n = max_width(lines)
346
- ['-' * n, *lines.map { |val| val.ljust(n) }].join("\n")
349
+ sub = as_a(sub)
350
+ lines.map! do |val|
351
+ s = val.ljust(n)
352
+ sub.each { |h| s = sub_style(s, **h) }
353
+ s
354
+ end
355
+ [sub_style('-' * n, styles: border), *lines].join("\n")
347
356
  end
348
357
 
349
358
  def format_desc(action, flag, opts = nil, req: 'opts*')
@@ -355,20 +364,12 @@ module Squared
355
364
  message(name, action, opts ? "#{flag}[#{opts}]" : flag)
356
365
  end
357
366
 
358
- def format_banner(cmd, banner: true, multitask: false)
367
+ def format_banner(cmd, banner: true, multiple: false)
359
368
  return unless banner
360
369
 
361
370
  if verbose?
362
- pad = 0
363
- if (args = theme[:banner])
364
- if args.any? { |s| s.to_s.end_with?('!') }
365
- pad = 1
366
- elsif args.size <= 1
367
- args = [:bold] + args
368
- end
369
- end
370
- print_banner(cmd.sub(/^\S+/, &:upcase), path.to_s, styles: args, pad: pad, first: pad == 0)
371
- elsif multitask && workspace.multitask?
371
+ print_banner(cmd.sub(/^\S+/, &:upcase), path.to_s)
372
+ elsif multiple && workspace.series.multiple?
372
373
  "## #{path} ##"
373
374
  end
374
375
  end
@@ -386,17 +387,17 @@ module Squared
386
387
  return unless (val = args[key]).nil? || (pat && !val.match?(pat))
387
388
 
388
389
  @session = nil
389
- raise ArgumentError, message(action, "#{flag}[#{key}]", hint: val.nil? ? 'missing' : 'invalid')
390
+ raise_error(action, "#{flag}[#{key}]", hint: val.nil? ? 'missing' : 'invalid')
390
391
  elsif args.is_a?(::Array)
391
392
  return unless args.empty?
392
393
 
393
394
  @session = nil
394
- raise ArgumentError, message(action, "#{flag}[]", hint: 'empty')
395
+ raise_error(action, "#{flag}[]", hint: 'empty')
395
396
  end
396
397
  return unless action.to_s.empty?
397
398
 
398
399
  @session = nil
399
- raise ArgumentError, message('parameter', flag, hint: 'missing')
400
+ raise_error('parameter', flag, hint: 'missing')
400
401
  end
401
402
 
402
403
  def verbose?
@@ -408,13 +409,12 @@ module Squared
408
409
  end
409
410
 
410
411
  def invoked_sync?(action)
411
- return true if invoked?(sync = "#{action}:sync")
412
+ return true if workspace.series.sync?("#{action}:sync")
412
413
 
413
- missing = ->(val) { ::Rake::Task.tasks.none? { |item| item.name == val } }
414
414
  check = lambda do |val|
415
415
  if invoked?(val)
416
- missing.("#{val}:sync")
417
- elsif invoked?("#{val}:sync")
416
+ !workspace.task_defined?("#{val}:sync")
417
+ elsif workspace.series.sync?("#{val}:sync")
418
418
  true
419
419
  end
420
420
  end
@@ -426,7 +426,7 @@ module Squared
426
426
  ret = check.("#{action}:#{base.to_sym}")
427
427
  return ret unless ret.nil?
428
428
  end
429
- invoked?("#{name}:#{action}") && (!invoked?(action) || missing.(sync))
429
+ invoked?("#{name}:#{action}") && (!invoked?(action) || !workspace.task_defined?("#{action}:sync"))
430
430
  end
431
431
 
432
432
  private