tomo 0.1.0

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.
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