tomo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +221 -0
  4. data/exe/tomo +4 -0
  5. data/lib/tomo/cli/command.rb +36 -0
  6. data/lib/tomo/cli/common_options.rb +48 -0
  7. data/lib/tomo/cli/completions.rb +70 -0
  8. data/lib/tomo/cli/deploy_options.rb +59 -0
  9. data/lib/tomo/cli/error.rb +16 -0
  10. data/lib/tomo/cli/interrupted_error.rb +9 -0
  11. data/lib/tomo/cli/options.rb +38 -0
  12. data/lib/tomo/cli/parser.rb +92 -0
  13. data/lib/tomo/cli/project_options.rb +47 -0
  14. data/lib/tomo/cli/rules/argument.rb +42 -0
  15. data/lib/tomo/cli/rules/switch.rb +43 -0
  16. data/lib/tomo/cli/rules/value_switch.rb +58 -0
  17. data/lib/tomo/cli/rules.rb +98 -0
  18. data/lib/tomo/cli/rules_evaluator.rb +71 -0
  19. data/lib/tomo/cli/state.rb +29 -0
  20. data/lib/tomo/cli/usage.rb +42 -0
  21. data/lib/tomo/cli.rb +75 -0
  22. data/lib/tomo/colors.rb +46 -0
  23. data/lib/tomo/commands/completion_script.rb +46 -0
  24. data/lib/tomo/commands/default.rb +72 -0
  25. data/lib/tomo/commands/deploy.rb +67 -0
  26. data/lib/tomo/commands/help.rb +9 -0
  27. data/lib/tomo/commands/init.rb +92 -0
  28. data/lib/tomo/commands/run.rb +76 -0
  29. data/lib/tomo/commands/setup.rb +54 -0
  30. data/lib/tomo/commands/tasks.rb +32 -0
  31. data/lib/tomo/commands/version.rb +23 -0
  32. data/lib/tomo/commands.rb +13 -0
  33. data/lib/tomo/configuration/dsl/batch_block.rb +17 -0
  34. data/lib/tomo/configuration/dsl/config_file.rb +39 -0
  35. data/lib/tomo/configuration/dsl/environment_block.rb +13 -0
  36. data/lib/tomo/configuration/dsl/error_formatter.rb +75 -0
  37. data/lib/tomo/configuration/dsl/hosts_and_settings.rb +24 -0
  38. data/lib/tomo/configuration/dsl/tasks_block.rb +24 -0
  39. data/lib/tomo/configuration/dsl.rb +12 -0
  40. data/lib/tomo/configuration/environment.rb +12 -0
  41. data/lib/tomo/configuration/glob.rb +26 -0
  42. data/lib/tomo/configuration/plugin_file_not_found_error.rb +14 -0
  43. data/lib/tomo/configuration/plugin_resolver.rb +63 -0
  44. data/lib/tomo/configuration/plugins_registry/file_resolver.rb +43 -0
  45. data/lib/tomo/configuration/plugins_registry/gem_resolver.rb +63 -0
  46. data/lib/tomo/configuration/plugins_registry.rb +67 -0
  47. data/lib/tomo/configuration/project_not_found_error.rb +28 -0
  48. data/lib/tomo/configuration/role_based_task_filter.rb +42 -0
  49. data/lib/tomo/configuration/unknown_environment_error.rb +46 -0
  50. data/lib/tomo/configuration/unknown_plugin_error.rb +28 -0
  51. data/lib/tomo/configuration/unspecified_environment_error.rb +28 -0
  52. data/lib/tomo/configuration.rb +124 -0
  53. data/lib/tomo/console/key_reader.rb +51 -0
  54. data/lib/tomo/console/menu.rb +109 -0
  55. data/lib/tomo/console.rb +33 -0
  56. data/lib/tomo/error/suggestions.rb +44 -0
  57. data/lib/tomo/error.rb +22 -0
  58. data/lib/tomo/host.rb +57 -0
  59. data/lib/tomo/logger/tagged_io.rb +38 -0
  60. data/lib/tomo/logger.rb +70 -0
  61. data/lib/tomo/path.rb +19 -0
  62. data/lib/tomo/paths.rb +36 -0
  63. data/lib/tomo/plugin/bundler/helpers.rb +14 -0
  64. data/lib/tomo/plugin/bundler/tasks.rb +57 -0
  65. data/lib/tomo/plugin/bundler.rb +17 -0
  66. data/lib/tomo/plugin/core/helpers.rb +65 -0
  67. data/lib/tomo/plugin/core/tasks.rb +138 -0
  68. data/lib/tomo/plugin/core.rb +31 -0
  69. data/lib/tomo/plugin/env/tasks.rb +113 -0
  70. data/lib/tomo/plugin/env.rb +13 -0
  71. data/lib/tomo/plugin/git/helpers.rb +11 -0
  72. data/lib/tomo/plugin/git/tasks.rb +78 -0
  73. data/lib/tomo/plugin/git.rb +19 -0
  74. data/lib/tomo/plugin/nvm/tasks.rb +61 -0
  75. data/lib/tomo/plugin/nvm.rb +14 -0
  76. data/lib/tomo/plugin/puma/tasks.rb +38 -0
  77. data/lib/tomo/plugin/puma.rb +12 -0
  78. data/lib/tomo/plugin/rails/helpers.rb +20 -0
  79. data/lib/tomo/plugin/rails/tasks.rb +79 -0
  80. data/lib/tomo/plugin/rails.rb +11 -0
  81. data/lib/tomo/plugin/rbenv/tasks.rb +55 -0
  82. data/lib/tomo/plugin/rbenv.rb +12 -0
  83. data/lib/tomo/plugin/testing.rb +16 -0
  84. data/lib/tomo/plugin.rb +4 -0
  85. data/lib/tomo/plugin_dsl.rb +23 -0
  86. data/lib/tomo/remote.rb +55 -0
  87. data/lib/tomo/result.rb +28 -0
  88. data/lib/tomo/runtime/concurrent_ruby_load_error.rb +26 -0
  89. data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +50 -0
  90. data/lib/tomo/runtime/context.rb +21 -0
  91. data/lib/tomo/runtime/current.rb +41 -0
  92. data/lib/tomo/runtime/execution_plan.rb +107 -0
  93. data/lib/tomo/runtime/host_execution_step.rb +49 -0
  94. data/lib/tomo/runtime/inline_thread_pool.rb +27 -0
  95. data/lib/tomo/runtime/privileged_task.rb +6 -0
  96. data/lib/tomo/runtime/settings_interpolation.rb +55 -0
  97. data/lib/tomo/runtime/settings_required_error.rb +33 -0
  98. data/lib/tomo/runtime/task_aborted_error.rb +15 -0
  99. data/lib/tomo/runtime/task_runner.rb +56 -0
  100. data/lib/tomo/runtime/unknown_task_error.rb +23 -0
  101. data/lib/tomo/runtime.rb +82 -0
  102. data/lib/tomo/script.rb +44 -0
  103. data/lib/tomo/shell_builder.rb +108 -0
  104. data/lib/tomo/ssh/child_process.rb +64 -0
  105. data/lib/tomo/ssh/connection.rb +82 -0
  106. data/lib/tomo/ssh/connection_error.rb +16 -0
  107. data/lib/tomo/ssh/connection_validator.rb +87 -0
  108. data/lib/tomo/ssh/error.rb +11 -0
  109. data/lib/tomo/ssh/executable_error.rb +21 -0
  110. data/lib/tomo/ssh/options.rb +67 -0
  111. data/lib/tomo/ssh/permission_error.rb +18 -0
  112. data/lib/tomo/ssh/script_error.rb +23 -0
  113. data/lib/tomo/ssh/unknown_error.rb +13 -0
  114. data/lib/tomo/ssh/unsupported_version_error.rb +15 -0
  115. data/lib/tomo/ssh.rb +36 -0
  116. data/lib/tomo/task_library.rb +51 -0
  117. data/lib/tomo/templates/config.rb.erb +66 -0
  118. data/lib/tomo/testing/Dockerfile +10 -0
  119. data/lib/tomo/testing/connection.rb +34 -0
  120. data/lib/tomo/testing/docker_image.rb +115 -0
  121. data/lib/tomo/testing/docker_plugin_tester.rb +39 -0
  122. data/lib/tomo/testing/host_extensions.rb +27 -0
  123. data/lib/tomo/testing/local.rb +75 -0
  124. data/lib/tomo/testing/mock_plugin_tester.rb +26 -0
  125. data/lib/tomo/testing/mocked_exec_error.rb +6 -0
  126. data/lib/tomo/testing/plugin_tester.rb +49 -0
  127. data/lib/tomo/testing/remote_extensions.rb +10 -0
  128. data/lib/tomo/testing/ssh_extensions.rb +13 -0
  129. data/lib/tomo/testing/tomo_test_ed25519 +7 -0
  130. data/lib/tomo/testing/tomo_test_ed25519.pub +1 -0
  131. data/lib/tomo/testing/ubuntu_setup.sh +33 -0
  132. data/lib/tomo/testing.rb +39 -0
  133. data/lib/tomo/version.rb +3 -0
  134. data/lib/tomo.rb +45 -0
  135. metadata +308 -0
@@ -0,0 +1,42 @@
1
+ class Tomo::CLI::Rules
2
+ class Argument
3
+ def initialize(label, multiple: false, required: false, values_proc:)
4
+ @label = label
5
+ @multiple = multiple
6
+ @required = required
7
+ @values_proc = values_proc
8
+ end
9
+
10
+ def match(arg, literal: false)
11
+ 1 if literal || !arg.start_with?("-")
12
+ end
13
+
14
+ def process(arg, state:)
15
+ state.parsed_arg(arg)
16
+ end
17
+
18
+ def candidates(literal: false, state:)
19
+ values(state).reject { |val| literal && val.start_with?("-") }
20
+ end
21
+
22
+ def required?
23
+ @required
24
+ end
25
+
26
+ def multiple?
27
+ @multiple
28
+ end
29
+
30
+ def to_s
31
+ @label
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :values_proc
37
+
38
+ def values(state)
39
+ values_proc.call(*state.args, state.options)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ class Tomo::CLI::Rules
2
+ class Switch
3
+ def initialize(key,
4
+ *switches,
5
+ required: false,
6
+ callback_proc:,
7
+ &convert_proc)
8
+ @key = key
9
+ @switches = switches
10
+ @callback_proc = callback_proc
11
+ @convert_proc = convert_proc || proc { true }
12
+ @required = required
13
+ end
14
+
15
+ def match(arg, literal: false)
16
+ return nil if literal
17
+
18
+ 1 if switches.include?(arg)
19
+ end
20
+
21
+ def process(arg, state:)
22
+ value = convert_proc.call(arg)
23
+ callback_proc&.call(value)
24
+ state.parsed_option(key, value)
25
+ end
26
+
27
+ def candidates(literal: false, **_kwargs)
28
+ literal ? [] : switches
29
+ end
30
+
31
+ def required?
32
+ @required
33
+ end
34
+
35
+ def multiple?
36
+ true
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :key, :switches, :convert_proc, :callback_proc
42
+ end
43
+ end
@@ -0,0 +1,58 @@
1
+ class Tomo::CLI::Rules
2
+ class ValueSwitch < Switch
3
+ include Tomo::Colors
4
+
5
+ def initialize(key, *switches, values_proc:, callback_proc:)
6
+ super(key, *switches, callback_proc: callback_proc)
7
+
8
+ @values_proc = values_proc
9
+ end
10
+
11
+ def match(arg, literal: false)
12
+ return nil if literal
13
+ return 2 if switches.include?(arg)
14
+
15
+ 1 if arg.start_with?("--") && switches.include?(arg.split("=").first)
16
+ end
17
+
18
+ def process(switch, arg=nil, state:)
19
+ value = if switch.include?("=")
20
+ switch.split("=", 2).last
21
+ elsif !arg.to_s.start_with?("-")
22
+ arg
23
+ end
24
+
25
+ raise_missing_value(switch) if value.nil?
26
+
27
+ callback_proc&.call(value)
28
+ state.parsed_option(key, value)
29
+ end
30
+
31
+ def candidates(switch=nil, literal: false, state:)
32
+ return [] if literal
33
+
34
+ vals = values(state)
35
+ return vals.reject { |val| val.start_with?("-") } if switch
36
+
37
+ switches.each_with_object([]) do |each_switch, result|
38
+ result << each_switch
39
+ vals.each do |value|
40
+ result << "#{each_switch}=#{value}" if each_switch.start_with?("--")
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :values_proc
48
+
49
+ def values(state)
50
+ values_proc.call(*state.args, state.options)
51
+ end
52
+
53
+ def raise_missing_value(switch)
54
+ raise Tomo::CLI::Error,
55
+ "Please specify a value for the #{yellow(switch)} option."
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,98 @@
1
+ module Tomo
2
+ class CLI
3
+ class Rules
4
+ autoload :Argument, "tomo/cli/rules/argument"
5
+ autoload :Switch, "tomo/cli/rules/switch"
6
+ autoload :ValueSwitch, "tomo/cli/rules/value_switch"
7
+
8
+ ARG_PATTERNS = {
9
+ /\A\[[A-Z_]+\]\z/ => :optional_arg_rule,
10
+ /\A[A-Z_]+\z/ => :required_arg_rule,
11
+ /\A\[[A-Z_]+\.\.\.\]\z/ => :mutiple_optional_args_rule
12
+ }.freeze
13
+
14
+ OPTION_PATTERNS = {
15
+ /\A--\[no-\]([\-a-z]+)\z/ => :on_off_switch_rule,
16
+ /\A(-[a-z]), (--[\-a-z]+)\z/ => :basic_switch_rule,
17
+ /\A(-[a-z]), (--[\-a-z]+) [A-Z=_\-]+\z/ => :value_switch_rule
18
+ }.freeze
19
+
20
+ private_constant :ARG_PATTERNS, :OPTION_PATTERNS
21
+
22
+ def initialize
23
+ @rules = []
24
+ end
25
+
26
+ def add_arg(spec, values_proc)
27
+ rule = ARG_PATTERNS.find do |regexp, method|
28
+ break send(method, spec, values_proc) if regexp.match?(spec)
29
+ end
30
+ raise ArgumentError, "Unrecognized arg style: #{spec}" if rule.nil?
31
+
32
+ rules << rule
33
+ end
34
+
35
+ def add_option(key, spec, values_proc, &block)
36
+ rule = OPTION_PATTERNS.find do |regexp, method|
37
+ match = regexp.match(spec)
38
+ break send(method, key, *match.captures, values_proc, block) if match
39
+ end
40
+ raise ArgumentError, "Unrecognized option style: #{spec}" if rule.nil?
41
+
42
+ rules << rule
43
+ end
44
+
45
+ def to_a
46
+ rules
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :rules
52
+
53
+ def optional_arg_rule(spec, values_proc)
54
+ Rules::Argument.new(
55
+ spec,
56
+ values_proc: values_proc,
57
+ required: false,
58
+ multiple: false
59
+ )
60
+ end
61
+
62
+ def required_arg_rule(spec, values_proc)
63
+ Rules::Argument.new(
64
+ spec,
65
+ values_proc: values_proc,
66
+ required: true,
67
+ multiple: false
68
+ )
69
+ end
70
+
71
+ def mutiple_optional_args_rule(spec, values_proc)
72
+ Rules::Argument.new(spec, multiple: true, values_proc: values_proc)
73
+ end
74
+
75
+ def on_off_switch_rule(key, name, _values_proc, callback_proc)
76
+ Rules::Switch.new(key,
77
+ "--#{name}",
78
+ "--no-#{name}",
79
+ callback_proc: callback_proc) do |arg|
80
+ arg == "--#{name}"
81
+ end
82
+ end
83
+
84
+ def basic_switch_rule(key, *switches, _values_proc, callback_proc)
85
+ Rules::Switch.new(key, *switches, callback_proc: callback_proc)
86
+ end
87
+
88
+ def value_switch_rule(key, *switches, values_proc, callback_proc)
89
+ Rules::ValueSwitch.new(
90
+ key,
91
+ *switches,
92
+ values_proc: values_proc,
93
+ callback_proc: callback_proc
94
+ )
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,71 @@
1
+ module Tomo
2
+ class CLI
3
+ class RulesEvaluator
4
+ def self.evaluate(**kwargs)
5
+ new(**kwargs).call
6
+ end
7
+
8
+ def initialize(rules:, argv:, state:, literal:, completions: nil)
9
+ @rules = rules
10
+ @argv = argv.dup
11
+ @state = state
12
+ @literal = literal
13
+ @completions = completions || Completions.new(literal: literal)
14
+ end
15
+
16
+ def call
17
+ until argv.empty?
18
+ complete_if_needed(remaining_rules, *argv) if argv.length == 1
19
+ rule, matched_args = match_next_rule
20
+ complete_if_needed([rule], *matched_args) if argv.empty?
21
+ rule.process(*matched_args, state: state)
22
+ state.processed_rule(rule)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :rules, :argv, :state, :literal, :completions
29
+
30
+ def match_next_rule
31
+ matched_rule, length = remaining_rules.find do |rule|
32
+ matching_length = rule.match(argv.first, literal: literal)
33
+ break [rule, matching_length] if matching_length
34
+ end
35
+ raise_unrecognized_args if matched_rule.nil?
36
+
37
+ matched_args = argv.shift(length)
38
+ [matched_rule, matched_args]
39
+ end
40
+
41
+ def complete_if_needed(matched_rules, *matched_args)
42
+ return unless Completions.active?
43
+
44
+ completions.print_completions_and_exit(
45
+ matched_rules,
46
+ *matched_args,
47
+ state: state
48
+ )
49
+ end
50
+
51
+ def remaining_rules
52
+ rules.reject do |rule|
53
+ state.processed?(rule) && !rule.multiple?
54
+ end
55
+ end
56
+
57
+ def raise_unrecognized_args
58
+ problem_arg = argv.first
59
+ type = if literal || !problem_arg.start_with?("-")
60
+ "arg"
61
+ else
62
+ "option"
63
+ end
64
+
65
+ raise CLI::Error,
66
+ "#{Colors.yellow(problem_arg)} is not a recognized #{type} "\
67
+ "for this tomo command."
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ module Tomo
2
+ class CLI
3
+ class State
4
+ attr_reader :args, :options, :processed_rules
5
+
6
+ def initialize
7
+ @args = []
8
+ @options = Options.new
9
+ @processed_rules = []
10
+ end
11
+
12
+ def parsed_arg(arg)
13
+ args << arg
14
+ end
15
+
16
+ def parsed_option(key, value)
17
+ options.all(key) << value
18
+ end
19
+
20
+ def processed_rule(rule)
21
+ @processed_rules |= [rule]
22
+ end
23
+
24
+ def processed?(rule)
25
+ @processed_rules.include?(rule)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ module Tomo
2
+ class CLI
3
+ class Usage
4
+ def initialize
5
+ @options = []
6
+ @banner_proc = proc { "" }
7
+ end
8
+
9
+ def add_option(spec, desc)
10
+ options << [
11
+ spec.start_with?("--") ? " #{spec}" : spec,
12
+ desc
13
+ ]
14
+ end
15
+
16
+ def banner=(banner)
17
+ @banner_proc = banner.respond_to?(:call) ? banner : proc { banner }
18
+ end
19
+
20
+ def to_s
21
+ indent([
22
+ "", banner_proc.call, "Options:", "", indent(options_help), "\n"
23
+ ].join("\n"))
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :banner_proc, :options
29
+
30
+ def options_help
31
+ width = options.map(&:first).map(&:length).max
32
+ options.each_with_object([]) do |(spec, desc), help|
33
+ help << "#{Colors.yellow(spec.ljust(width))} #{desc}"
34
+ end.join("\n")
35
+ end
36
+
37
+ def indent(str)
38
+ str.gsub(/^/, " ")
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/tomo/cli.rb ADDED
@@ -0,0 +1,75 @@
1
+ require "abbrev"
2
+
3
+ module Tomo
4
+ class CLI
5
+ autoload :Command, "tomo/cli/command"
6
+ autoload :CommonOptions, "tomo/cli/common_options"
7
+ autoload :Completions, "tomo/cli/completions"
8
+ autoload :DeployOptions, "tomo/cli/deploy_options"
9
+ autoload :Error, "tomo/cli/error"
10
+ autoload :InterruptedError, "tomo/cli/interrupted_error"
11
+ autoload :Options, "tomo/cli/options"
12
+ autoload :Parser, "tomo/cli/parser"
13
+ autoload :ProjectOptions, "tomo/cli/project_options"
14
+ autoload :Rules, "tomo/cli/rules"
15
+ autoload :RulesEvaluator, "tomo/cli/rules_evaluator"
16
+ autoload :State, "tomo/cli/state"
17
+ autoload :UnknownOptionError, "tomo/cli/unknown_option_error"
18
+ autoload :Usage, "tomo/cli/usage"
19
+
20
+ class << self
21
+ attr_accessor :show_backtrace
22
+ end
23
+
24
+ COMMANDS = {
25
+ "deploy" => Tomo::Commands::Deploy,
26
+ "help" => Tomo::Commands::Help,
27
+ "init" => Tomo::Commands::Init,
28
+ "run" => Tomo::Commands::Run,
29
+ "setup" => Tomo::Commands::Setup,
30
+ "tasks" => Tomo::Commands::Tasks,
31
+ "version" => Tomo::Commands::Version,
32
+ "completion-script" => Tomo::Commands::CompletionScript
33
+ }.freeze
34
+
35
+ def call(argv)
36
+ prepare_completions(argv)
37
+ command, command_name = lookup_command(argv)
38
+ command.parse(argv)
39
+ rescue Interrupt
40
+ handle_error(InterruptedError.new, command_name)
41
+ rescue StandardError => e
42
+ handle_error(e, command_name)
43
+ end
44
+
45
+ private
46
+
47
+ def prepare_completions(argv)
48
+ return unless %w[--complete --complete-word].include?(argv[0])
49
+
50
+ Completions.activate
51
+ argv << "" if argv.shift == "--complete"
52
+ end
53
+
54
+ def lookup_command(argv)
55
+ command_name = argv.first unless Completions.active? && argv.length == 1
56
+ command_name = Abbrev.abbrev(COMMANDS.keys)[command_name]
57
+ argv.shift if command_name
58
+
59
+ command = COMMANDS[command_name] || Tomo::Commands::Default
60
+ [command, command_name]
61
+ end
62
+
63
+ def handle_error(error, command_name)
64
+ return if Completions.active?
65
+ raise error unless error.respond_to?(:to_console)
66
+
67
+ error.command_name = command_name if error.respond_to?(:command_name=)
68
+ Tomo.logger.error(error.to_console)
69
+ status = error.respond_to?(:exit_status) ? error.exit_status : 1
70
+ exit(status) unless Tomo::CLI.show_backtrace
71
+
72
+ raise error
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,46 @@
1
+ module Tomo
2
+ module Colors
3
+ ANSI_CODES = {
4
+ red: 31,
5
+ green: 32,
6
+ yellow: 33,
7
+ blue: 34,
8
+ gray: 90
9
+ }.freeze
10
+ private_constant :ANSI_CODES
11
+
12
+ class << self
13
+ attr_writer :enabled
14
+
15
+ def enabled?
16
+ return @enabled if defined?(@enabled)
17
+
18
+ @enabled = determine_color_support
19
+ end
20
+
21
+ private
22
+
23
+ def determine_color_support
24
+ if ENV["CLICOLOR_FORCE"] == "1"
25
+ true
26
+ elsif ENV["TERM"] == "dumb"
27
+ false
28
+ else
29
+ tty?($stdout) && tty?($stderr)
30
+ end
31
+ end
32
+
33
+ def tty?(io)
34
+ io.respond_to?(:tty?) && io.tty?
35
+ end
36
+ end
37
+
38
+ module_function
39
+
40
+ ANSI_CODES.each do |name, code|
41
+ define_method(name) do |str|
42
+ ::Tomo::Colors.enabled? ? "\e[0;#{code};49m#{str}\e[0m" : str
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ module Tomo
2
+ module Commands
3
+ class CompletionScript
4
+ def self.parse(_argv)
5
+ puts <<~'SCRIPT'
6
+ # TOMO COMPLETIONS FOR BASH
7
+ #
8
+ # Assuming tomo is in your PATH, you can install tomo bash completions by
9
+ # adding this line to your .bashrc:
10
+ #
11
+ # eval "$(tomo completion-script)"
12
+ #
13
+ # The eval technique is a bit slow but ensures bash is always using the
14
+ # latest version of the tomo completion script.
15
+ #
16
+ # Alternatively, you can copy and paste the current version of the script
17
+ # into your .bashrc. The full script is listed below.
18
+
19
+ _tomo_complete() {
20
+ local cur="${COMP_WORDS[COMP_CWORD]}"
21
+ local prev="${COMP_WORDS[COMP_CWORD-1]}"
22
+
23
+ if [[ $prev == "-c" || $prev == "--config" ]]; then
24
+ COMPREPLY=($(compgen -f -- ${cur}))
25
+ return 0
26
+ fi
27
+
28
+ if [[ "${COMP_LINE: -1}" == " " ]]; then
29
+ command=${COMP_LINE/tomo/tomo --complete}
30
+ else
31
+ command=${COMP_LINE/tomo/tomo --complete-word}
32
+ fi
33
+
34
+ suggestions=$($command)
35
+ local IFS=$'\n'
36
+ COMPREPLY=($suggestions)
37
+ return 0
38
+ }
39
+
40
+ complete -o nospace -F _tomo_complete tomo
41
+
42
+ SCRIPT
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,72 @@
1
+ module Tomo
2
+ module Commands
3
+ class Default < CLI::Command
4
+ arg "COMMAND", values: CLI::COMMANDS.keys
5
+
6
+ option :version, "-v, --version", "Display tomo’s version and exit" do
7
+ Version.parse([])
8
+ exit
9
+ end
10
+
11
+ include CLI::CommonOptions
12
+
13
+ def banner
14
+ <<~BANNER
15
+ Usage: #{green('tomo')} #{yellow('COMMAND [options]')}
16
+
17
+ Tomo is an extensible tool for deploying projects to remote hosts via SSH.
18
+ Please specify a #{yellow('COMMAND')}, which can be:
19
+
20
+ #{commands.map { |name, help| " #{yellow(name.ljust(10))} #{help}" }.join("\n")}
21
+
22
+ Tomo accepts abbreviations for its commands. For example, #{blue('tomo deploy')}
23
+ can be shortened to #{blue('tomo d')}. You can use bash completions as well!
24
+ Run #{blue('tomo completion-script')} for installation instructions.
25
+
26
+ For help with any command, add #{blue('-h')} to the command, like this:
27
+
28
+ #{blue('tomo run -h')}
29
+
30
+ Or read the full documentation for all commands at:
31
+
32
+ #{blue('https://tomo-deploy.com/')}
33
+ BANNER
34
+ end
35
+
36
+ def call(*args, options)
37
+ # The bare `tomo` command (i.e. without `--help` or `--version`) doesn't
38
+ # do anything, so if we got this far, something has gone wrong.
39
+
40
+ if options.any?
41
+ raise CLI::Error,
42
+ "Options must be specified after the command: " +
43
+ yellow("tomo #{args.first} [options]")
44
+ end
45
+
46
+ raise_unrecognized_command(args.first)
47
+ end
48
+
49
+ private
50
+
51
+ def raise_unrecognized_command(command)
52
+ error = "#{yellow(command)} is not a recognized tomo command."
53
+ if command.match?(/\A\S+:\S+\z/)
54
+ suggestion = "tomo run #{command}"
55
+ error << "\nMaybe you meant #{blue(suggestion)}?"
56
+ end
57
+
58
+ raise CLI::Error, error
59
+ end
60
+
61
+ def commands
62
+ CLI::COMMANDS.each_with_object({}) do |(name, klass), result|
63
+ command = klass.new
64
+ help = command.summary if command.respond_to?(:summary)
65
+ next if help.nil?
66
+
67
+ result[name] = help
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,67 @@
1
+ module Tomo
2
+ module Commands
3
+ class Deploy < CLI::Command
4
+ include CLI::DeployOptions
5
+ include CLI::ProjectOptions
6
+ include CLI::CommonOptions
7
+
8
+ def summary
9
+ "Deploy the current project to remote host(s)"
10
+ end
11
+
12
+ def banner
13
+ <<~BANNER
14
+ Usage: #{green('tomo deploy')} #{yellow('[--dry-run] [options]')}
15
+
16
+ Sequentially run the "deploy" list of tasks specified in #{DEFAULT_CONFIG_PATH} to
17
+ deploy the project to a remote host. Use the #{blue('--dry-run')} option to quickly
18
+ simulate the entire deploy without actually connecting to the host.
19
+
20
+ For a #{DEFAULT_CONFIG_PATH} that specifies distinct environments (e.g. staging,
21
+ production), you must specify the target environment using the #{blue('-e')} option. If
22
+ you omit this option, tomo will automatically prompt for it.
23
+
24
+ Tomo will use the settings specified in #{DEFAULT_CONFIG_PATH} to configure the
25
+ deploy. You may override these on the command line using #{blue('-s')}. E.g.:
26
+
27
+ #{blue('tomo deploy -e staging -s git_branch=develop')}
28
+
29
+ Or use environment variables with the special #{blue('TOMO_')} prefix:
30
+
31
+ #{blue('TOMO_GIT_BRANCH=develop tomo deploy -e staging')}
32
+
33
+ Bash completions are provided for tomo’s options. For example, you could type
34
+ #{blue('tomo deploy -s <TAB>')} to see a list of all settings, or #{blue('tomo deploy -e pr<TAB>')}
35
+ to expand #{blue('pr')} to #{blue('production')}. For bash completion installation instructions,
36
+ run #{blue('tomo completion-script')}.
37
+
38
+ More documentation and examples can be found here:
39
+
40
+ #{blue('https://tomo-deploy.com/commands/deploy')}
41
+ BANNER
42
+ end
43
+
44
+ def call(options)
45
+ logger.info "tomo deploy v#{Tomo::VERSION}"
46
+
47
+ runtime = configure_runtime(options)
48
+ plan = runtime.deploy!
49
+
50
+ log_completion(plan)
51
+ end
52
+
53
+ private
54
+
55
+ def log_completion(plan)
56
+ app = plan.settings[:application]
57
+ target = "#{app} to #{plan.applicable_hosts_sentence}"
58
+
59
+ if dry_run?
60
+ logger.info(green("* Simulated deploy of #{target} (dry run)"))
61
+ else
62
+ logger.info(green("✔ Deployed #{target}"))
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end