sesh 0.0.8 → 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.
data/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # Sesh: remote background sessions powered by tmux and tmuxinator.
2
+ Runs a headless tmuxinator session for remote slave machines to connect to.
3
+
4
+ ## Usage
5
+
6
+ sesh command [project]
7
+
8
+ Leave project blank to use the name of your current directory.
9
+
10
+ ## Commands
11
+
12
+ sesh new Create a new tmuxinator configuration.
13
+ sesh start [project] Start a Sesh session for a project.
14
+ sesh stop [project] Stop a Sesh session for a project.
15
+ sesh list List running Sesh sessions on this machine.
16
+ sesh enslave [project] [user@host] Connect a slave to a local Sesh session.
17
+ sesh connect [project] user@host Connect as a slave to a Sesh session.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sesh"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/exe/sesh ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'sesh'
3
+ Sesh::Cli.start
data/lib/sesh/cli.rb ADDED
@@ -0,0 +1,244 @@
1
+ require 'sesh'
2
+ require 'tmuxinator'
3
+ require 'optparse'
4
+ require 'yaml'
5
+ require 'colorize'
6
+ require 'open3'
7
+ require 'deep_merge'
8
+
9
+ module Sesh
10
+ class Cli
11
+ def self.start
12
+ puts "Sesh v#{Sesh::VERSION}".green
13
+ if ARGV.empty? or ARGV.include? '-h' or ARGV.include? '--help'
14
+ puts HELP_BANNER.blue; exit end
15
+
16
+ parse_options!
17
+ @tmux_control = TmuxControl.new @options[:project], @options[:tmux]
18
+ @ssh_control = SshControl.new @tmux_control, @options[:ssh]
19
+
20
+ if @command
21
+ case @command
22
+ when 'start'
23
+ if @tmux_control.already_running?
24
+ Logger.fatal "Sesh project '#{@options[:project]}' is already running!"
25
+ else
26
+ Logger.debug "Starting Sesh project '#{@options[:project]}'..."
27
+ end
28
+ @tmux_control.kill_running_processes
29
+ if @tmux_control.issue_start_command! &&
30
+ Logger.show_progress_until(-> { @tmux_control.already_running? })
31
+ sleep 1
32
+ if @tmux_control.already_running?
33
+ @tmux_control.store_pids_from_session!
34
+ Logger.debug 'Sesh started successfully.'
35
+ puts
36
+ else Logger.fatal 'Sesh failed to start!' end
37
+ else Logger.fatal 'Sesh failed to start after ten seconds!' end
38
+ when 'stop'
39
+ if @tmux_control.already_running?
40
+ Logger.debug "Stopping Sesh project '#{@options[:project]}'..."
41
+ else
42
+ Logger.fatal "Sesh project '#{@options[:project]}' is not running!"
43
+ end
44
+ @tmux_control.kill_running_processes
45
+ @tmux_control.issue_stop_command!
46
+ if $? && Logger.show_progress_until(-> { !@tmux_control.already_running? })
47
+ Logger.debug 'Sesh stopped successfully.'
48
+ puts
49
+ else
50
+ Logger.fatal 'Sesh failed to stop after ten seconds!'
51
+ end
52
+ when 'restart'
53
+ Logger.fatal("Sesh project '#{@options[:project]}' is not running!") unless already_running?
54
+ puts `sesh stop #{@options[:project]} #{@options if @options.any?}`.strip
55
+ sleep 0.5
56
+ puts `sesh start #{@options[:project]} #{@options if @options.any?}`.strip
57
+ when 'new'
58
+ config = Tmuxinator::Config.project(@options[:project])
59
+ unless Tmuxinator::Config.exists?(@options[:project])
60
+ template = File.join(File.dirname(File.expand_path(__FILE__)),
61
+ "../lib/sesh/assets/sample.yml")
62
+ erb = Erubis::Eruby.new(File.read(template)).result(binding)
63
+ File.open(config, "w") { |f| f.write(erb) }
64
+ end
65
+
66
+ Kernel.system("#{Inferences.infer_default_editor} #{config}") ||
67
+ Tmuxinator::Cli.new.doctor
68
+ puts
69
+ when 'edit'
70
+ config = Tmuxinator::Config.project(@options[:project])
71
+ if Tmuxinator::Config.exists? @options[:project]
72
+ Kernel.system("#{Inferences.infer_default_editor} #{config}") ||
73
+ Tmuxinator::Cli.new.doctor
74
+ puts
75
+ else
76
+ Logger.fatal "Sesh project '#{@options[:project]}' does not exist yet!"
77
+ end
78
+ when 'list'
79
+ output = Sesh.format_and_run_command <<-BASH
80
+ ps aux | grep tmux | grep "env TMUX='' mux start" \
81
+ | grep -v "[g]rep" | sed -e "s/.*mux start \\(.*\\)/\\1/"
82
+ BASH
83
+ running_projects = output.split("\n")
84
+ pcount = running_projects.count
85
+ if pcount > 0
86
+ Logger.info "#{pcount} project#{pcount>1 ? 's':''} currently running:"
87
+ puts running_projects
88
+ puts
89
+ else
90
+ Logger.fatal "There are no Sesh projects currently running."
91
+ end
92
+ when 'connect'
93
+ if @options[:ssh][:local_addr] == @options[:ssh][:remote_addr]
94
+ unless @tmux_control.already_running?
95
+ Logger.fatal "Sesh project '#{@options[:project]}' is not running!"
96
+ end
97
+ end
98
+ system @ssh_control.enter_slave_mode_command(@options[:ssh][:local_addr])
99
+ when 'enslave'
100
+ Logger.fatal("Sesh project '#{@options[:project]}' is not running!") unless @tmux_control.already_running?
101
+ Logger.fatal("You must specify a machine to enslave! Eg: user@ip") if @options[:ssh][:remote_addr].nil?
102
+ Logger.debug "Attempting to connect #{@options[:ssh][:remote_addr]} to Sesh project '#{@options[:project]}'..."
103
+ if @ssh_control.enslave_peer!
104
+ Logger.debug "Sesh client connected successfully."
105
+ puts
106
+ else
107
+ Logger.fatal 'Sesh client failed to start.'
108
+ end
109
+ when 'run'
110
+ unless ARGV.any?
111
+ Logger.fatal 'A second command is required!'
112
+ end
113
+ @shell_command = ARGV.join(' ')
114
+ puts "Subcommand: #{@shell_command}"
115
+ when 'rspec'
116
+ unless ARGV.any?
117
+ Logger.fatal 'A path to a spec is required!'
118
+ end
119
+ @shell_command = ARGV.join(' ')
120
+ puts "Spec: #{@shell_command}"
121
+ else
122
+ Logger.fatal "Command not recognized!"
123
+ end
124
+ exit 0
125
+ end
126
+
127
+ Logger.fatal 'You must specify a command.'
128
+ end
129
+
130
+ def self.parse_options!
131
+ @command = ARGV.shift
132
+
133
+ # Load config from a YAML file in the project root if available.
134
+ @config_filepath = nil
135
+ @config_friendly_filepath = nil
136
+ POSSIBLE_CONFIG_LOCATIONS.each do |path|
137
+ fullpath = File.join Dir.pwd, path
138
+ next unless File.exists? fullpath
139
+ @config_filepath = fullpath
140
+ @config_friendly_filepath = path
141
+ break
142
+ end
143
+ @defaults = DEFAULT_OPTIONS.dup
144
+ if @config_filepath.nil?
145
+ @config_friendly_filepath = POSSIBLE_CONFIG_LOCATIONS[0]
146
+ else
147
+ loaded_config = deep_symbolize YAML::load_file(@config_filepath)
148
+ @defaults.deep_merge! loaded_config
149
+ end
150
+
151
+ # Parse options given to the command.
152
+ parsed_options = @defaults.dup
153
+ OptionParser.new do |opts|
154
+ opts.banner = HELP_BANNER
155
+
156
+ opts.on("-p", "--project=project", 'Project') {|v|
157
+ parsed_options[:project] = v }
158
+
159
+ opts.on("-L", "--local-ssh-addr=addr", 'Local SSH Address') {|v|
160
+ parsed_options[:ssh][:local_addr] = v }
161
+ opts.on("-R", "--remote-ssh-addr=addr", 'Remote SSH Address') {|v|
162
+ parsed_options[:ssh][:remote_addr] = v }
163
+
164
+ opts.on('-S', '--tmux-socket-file=path', 'Path to Tmux Socket File') {|v|
165
+ # fatal("Socket file #{v} does not exist.") unless File.exist?(v)
166
+ parsed_options[:tmux][:socket_file] = v }
167
+ opts.on('-P', '--tmux-pids-file=path', 'Path to Tmux Pids File') {|v|
168
+ parsed_options[:tmux][:pids_file] = v }
169
+
170
+ # # target_opts = DEPLOYMENT_TARGETS.join '|'
171
+ # opts.on("-T", "--target=target", 'Titanium Deployment Target') do |v|
172
+ # if DEPLOYMENT_TARGETS.include? v
173
+ # platform = parsed_options[:titanium_command][:platform]
174
+ # if v == 'xcode'
175
+ # parsed_options[:titanium_command][:run_with_xcode] = true
176
+ # v = 'simulator'
177
+ # end
178
+ # if v == 'emulator' && platform == 'ios' then v = 'simulator'
179
+ # elsif v == 'simulator' && platform == 'android' then v = 'emulator' end
180
+ # parsed_options[:titanium_command][:target] = v
181
+ # else fatal "Target \"#{v}\" not recognized." end
182
+ # end
183
+ #
184
+ # opts.on('-O', '--output-dir=dir', 'Titanium Output Directory') {|v|
185
+ # parsed_options[:titanium_command][:output_dir] = v }
186
+ #
187
+ # opts.on('-L', '--log-level=level', 'Titanium Log Level') {|v|
188
+ # parsed_options[:titanium_command][:log_level] = v }
189
+ #
190
+ # opts.on('-V', '--developer-certificate=certificate',
191
+ # 'iOS Developer Certificate') {|v|
192
+ # parsed_options[:ios_credentials][:development][:developer_certificate] = v
193
+ # parsed_options[:ios_credentials][:production][:developer_certificate] = v }
194
+ # opts.on('-P', '--provisioning-profile=profile',
195
+ # 'iOS Provisioning Profile') {|v|
196
+ # parsed_options[:ios_credentials][:development][:provisioning_profile] = v
197
+ # parsed_options[:ios_credentials][:production][:provisioning_profile] = v }
198
+ #
199
+ # opts.on('-U', '--simulator-udid=udid', 'iOS Simulator UDID') {|v|
200
+ # parsed_options[:ios_simulator][:udid] = v }
201
+ # opts.on('-D', '--devicetypeid=dtid', 'iOS Simulator DeviceTypeID') {|v|
202
+ # parsed_options[:ios_simulator][:devicetypeid] = v }
203
+ #
204
+ # opts.on('--no-autofocus', 'Disable iOS Simulator Autofocus') {
205
+ # parsed_options[:ios_simulator][:autofocus] = false }
206
+
207
+ opts.on('--help', 'Prints this help') { puts opts.banner; exit }
208
+ end.parse!
209
+ @options = parsed_options
210
+ if @options[:project].nil?
211
+ project_required = !%w(new list).include?(@command)
212
+ ARGV.each {|a|
213
+ if Tmuxinator::Config.exists? a
214
+ @options[:project] = ARGV.delete a
215
+ break end } if project_required && ARGV.any?
216
+ @options[:project] ||= Inferences.infer_project_from_current_directory
217
+ if project_required && @options[:project].nil?
218
+ Logger.warn 'A matching Sesh project could not be found.'
219
+ Logger.fatal 'Hint: run sesh new or specify an existing project with your commmand.'
220
+ end
221
+ end
222
+ @options[:tmux][:socket_file] ||= "/tmp/#{@options[:project]}.sock"
223
+ @options[:tmux][:pids_file] ||= "/tmp/#{@options[:project]}.pids.txt"
224
+ @options[:ssh][:local_addr] ||= Sesh::Inferences.infer_local_ssh_addr
225
+ if @options[:ssh][:remote_addr].nil?
226
+ if ARGV.any?
227
+ ARGV.each {|a|
228
+ if a =~ /\.local$/ || a =~ /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ || a =~ /^(.+)@(.+)$/
229
+ @options[:ssh][:remote_addr] = a
230
+ break end }
231
+ @options[:ssh][:remote_addr] ||= ARGV.shift
232
+ end
233
+ if @options[:ssh][:remote_addr].nil?
234
+ if %w(enslave run rspec).include? @command
235
+ Logger.warn 'A remote address is required.'
236
+ Logger.fatal 'Hint: specify a remote ssh address using the -R flag.'
237
+ elsif %w(connect).include? @command
238
+ @options[:ssh][:remote_addr] = Inferences.infer_local_ssh_addr
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,36 @@
1
+ require 'sesh'
2
+ require 'tmuxinator'
3
+
4
+ module Sesh
5
+ module Inferences
6
+ def self.infer_project_from_current_directory
7
+ output = `printf '%q\n' "${PWD##*/}"`.strip
8
+ return output if Tmuxinator::Config.exists?(output)
9
+ end
10
+ def self.infer_local_ssh_addr
11
+ inferred_user = `echo $USER`.strip
12
+ inferred_hostname = `scutil --get LocalHostName`.strip.downcase
13
+ inferred_hostname += '.local' unless inferred_hostname =~ /\.local$/
14
+ "#{inferred_user}@#{inferred_hostname}"
15
+ end
16
+ def self.infer_terminal_app
17
+ if OS.mac?
18
+ output = `osascript -e 'try' -e 'get exists application "iTerm"' -e 'end try'`.strip
19
+ output.length > 0 ? 'iTerm' : fatal("iTerm 2 is not installed.") # 'Terminal'
20
+ # TODO: support more platforms
21
+ end
22
+ end
23
+ def self.infer_default_editor
24
+ if OS.windows? then 'notepad.exe'
25
+ else o = `echo $EDITOR`.strip; o = 'vim' unless o.length > 0; o end
26
+ end
27
+
28
+ module OS
29
+ def OS.windows?
30
+ (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil end
31
+ def OS.mac?; (/darwin/ =~ RUBY_PLATFORM) != nil end
32
+ def OS.unix?; !OS.windows? end
33
+ def OS.linux?; OS.unix? and not OS.mac? end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ require 'sesh'
2
+ require 'colorize'
3
+
4
+ module Sesh
5
+ class Logger
6
+ def self.debug(msg) $stderr.puts "> #{msg}" end
7
+ def self.fatal(msg)
8
+ $stderr.puts msg.red; $stderr.puts; exit 1 end
9
+ def self.warn(msg, nest_level=0)
10
+ $stderr.puts "#{' ' * nest_level * 2}> #{msg}".yellow end
11
+ def self.info(msg, nest_level=0)
12
+ $stdout.puts "#{' ' * nest_level * 2}> #{msg}".blue end
13
+ def self.success(msg, nest_level=0)
14
+ $stdout.puts "#{' ' * nest_level * 2}> #{msg}".green end
15
+
16
+ def self.show_progress_until(condition_lambda, timeout=10)
17
+ started_progress_at = Time.now
18
+ return true if condition_lambda[]
19
+ print '> '
20
+ until condition_lambda[] or Time.now - started_progress_at > timeout
21
+ print '.'
22
+ $stdout.flush
23
+ sleep 0.5
24
+ end
25
+ puts
26
+ return condition_lambda[]
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ require 'sesh'
2
+
3
+ module Sesh
4
+ class SshControl
5
+ def initialize(tmux_control, options={})
6
+ @tmux_control = tmux_control
7
+ @project = @tmux_control.project
8
+ @options = DEFAULT_OPTIONS[:ssh].merge(options)
9
+ end
10
+
11
+ def connection_command(addr=nil)
12
+ addr ||= @options[:local_addr]
13
+ tmux_cmd = @tmux_control.connection_command
14
+ return tmux_cmd if addr == @options[:local_addr]
15
+ "ssh #{addr} -t '#{tmux_cmd}'"
16
+ end
17
+
18
+ def enter_slave_mode_command(addr=nil)
19
+ @term_app ||= Inferences.infer_terminal_app
20
+ case @term_app
21
+ when 'iTerm'
22
+ tell_term_app = 'tell application "' + @term_app + '"'
23
+ tell_term_process = 'tell application "System Events" to tell process "' +
24
+ @term_app + '"'
25
+ Sesh.format_command <<-BASH
26
+ osascript \
27
+ -e '#{tell_term_app} to activate' \
28
+ -e '#{tell_term_process} to keystroke \"n\" using command down' \
29
+ -e 'delay 1' \
30
+ -e "#{tell_term_app.gsub('"', '\\"')} to tell session -1 of current \
31
+ terminal to write text \\"#{connection_command(addr)}\\"" \
32
+ -e '#{tell_term_process} to keystroke return using command down'
33
+ BASH
34
+ when 'Terminal' then Sesh.format_command connection_command(addr)
35
+ end
36
+ end
37
+
38
+ def enslave_peer!
39
+ output = Sesh.format_and_run_command <<-BASH
40
+ ssh #{@options[:remote_addr]} "sesh connect #{@project} #{@options[:local_addr]}"
41
+ BASH
42
+ puts output
43
+ $?
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,51 @@
1
+ require 'sesh'
2
+
3
+ module Sesh
4
+ class TmuxControl
5
+ def initialize(project_name, options={})
6
+ @project = project || Inferences::infer_project_from_current_directory
7
+ @options = DEFAULT_OPTIONS[:tmux].merge(options)
8
+ end
9
+
10
+ def already_running?
11
+ `ps aux | grep "#{project_name_matcher}"`.strip.length > 0 end
12
+
13
+ def project_name_matcher
14
+ pn = @project.gsub '-', '\-'
15
+ "[t]mux.*[#{pn[0]}]#{pn[1..-1]}" end
16
+
17
+ def issue_start_command!
18
+ cmd = Sesh.format_command <<-BASH
19
+ tmux -S "#{@options[:socket_file]}" new-session -d "eval \$SHELL -l; env TMUX='' mux start #{@project}" 2>&1
20
+ BASH
21
+ output = `#{cmd}`.strip
22
+ return true if output.length == 0
23
+ Logger.warn "Tmux failed to start with the following error: #{output}"; false
24
+ end
25
+
26
+ def issue_stop_command!; `pkill -f "#{project_name_matcher}"` end
27
+
28
+ def connection_command; "tmux -S #{@options[:socket_file]} a" end
29
+
30
+ def obtain_pids_from_session
31
+ # TODO: grep this to just those pids from the current project
32
+ `tmux list-panes -s -F "\#{pane_pid} \#{pane_current_command}" | grep -v tmux | awk '{print $1}'`.strip.lines end
33
+ def store_pids_from_session!
34
+ File.open(@options[:pids_file], 'w') {|f|
35
+ obtain_pids_from_session.each{|pid| f.puts pid } }
36
+ end
37
+
38
+ def kill_running_processes
39
+ if File.exists? @options[:pids_file]
40
+ File.readlines(@options[:pids_file]).each{|pid| kill_process! pid }
41
+ File.delete @options[:pids_file]
42
+ end
43
+ end
44
+ def kill_process!(pid); `kill -9 #{pid}` end
45
+
46
+ # Getter methods for passthru to SshControl class
47
+ def project; @project end
48
+ def options; @options end
49
+
50
+ end
51
+ end
data/lib/sesh/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sesh
2
- VERSION = '0.0.8'
2
+ VERSION = '0.1.0'
3
3
  end
data/lib/sesh.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'sesh/version'
2
+ require 'sesh/logger'
3
+ require 'sesh/inferences'
4
+ require 'sesh/ssh_control'
5
+ require 'sesh/tmux_control'
6
+ require 'sesh/cli'
7
+
8
+ module Sesh
9
+ HELP_BANNER = <<-HELP
10
+ Sesh: remote background sessions powered by tmux and tmuxinator.
11
+ Runs a headless tmuxinator session for remote slave machines to connect to.
12
+
13
+ Usage: #{File.basename $0} command [project]
14
+
15
+ Commands:
16
+
17
+ sesh new Create a new tmuxinator configuration.
18
+ sesh edit [project] Edit an existing tmuxinator configuration.
19
+ sesh start [project] Start a Sesh session for a project.
20
+ sesh stop [project] Stop a Sesh session for a project.
21
+ sesh list List running Sesh sessions on this machine.
22
+ sesh enslave [project] user@host Connect a slave to a local Sesh session.
23
+ sesh connect [project] [user@host] Connect as a slave to a Sesh session.
24
+ sesh run [location] [command] Run a shell command in the specified location.
25
+ sesh rspec [location] [spec] Run a spec in the specified location.
26
+
27
+ Leave project blank to use the name of your current directory.
28
+
29
+ HELP
30
+
31
+ DEFAULT_OPTIONS = {
32
+ project: nil,
33
+ ssh: {
34
+ local_addr: nil,
35
+ remote_addr: nil
36
+ },
37
+ tmux: {
38
+ socket_file: nil,
39
+ pids_file: nil
40
+ }
41
+ }
42
+ POSSIBLE_CONFIG_LOCATIONS = %w( sesh_config.yml config/sesh.yml )
43
+
44
+ def self.format_command(command) command.gsub(/\ [ ]+/, ' ').strip end
45
+ def self.format_and_run_command(command) `#{format_command(command)}`.strip end
46
+ end
data/sesh.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "sesh/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'sesh'
8
+ s.version = Sesh::VERSION
9
+ s.date = '2015-03-17'
10
+ s.summary = "Sesh"
11
+ s.description = "Remote background sessions powered by tmux and tmuxinator."
12
+ s.authors = ["MacKinley Smith"]
13
+ s.email = 'smithmackinley@gmail.com'
14
+ # s.files = Dir["lib/**/*", "spec/**/*", "bin/*"]
15
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ s.bindir = "exe"
17
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ # s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
20
+ s.require_paths = ["lib"]
21
+ s.homepage = 'http://rubygems.org/gems/sesh'
22
+ s.license = 'GNU GPLv3'
23
+
24
+ s.add_development_dependency "bundler", "~> 1.10"
25
+ s.add_development_dependency "rake", "~> 10.0"
26
+ s.add_development_dependency "rspec"
27
+
28
+ s.add_dependency 'tmuxinator', '~> 0.6.9'
29
+ s.add_dependency 'deep_merge'
30
+ s.add_dependency 'awesome_print'
31
+ s.add_dependency 'colorize'
32
+ end