pair 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +15 -0
- data/.rspec +1 -0
- data/.rvmrc +13 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +30 -0
- data/README.rdoc +38 -0
- data/Rakefile +15 -0
- data/bin/pair +7 -0
- data/lib/pair.rb +10 -0
- data/lib/pair/api.rb +60 -0
- data/lib/pair/command_line.rb +60 -0
- data/lib/pair/command_line/host.rb +44 -0
- data/lib/pair/session.rb +15 -0
- data/lib/pair/session/authorized_keys_file.rb +100 -0
- data/lib/pair/session/hosted_session.rb +90 -0
- data/lib/pair/session/tmux.rb +74 -0
- data/lib/pair/session/tunnel.rb +62 -0
- data/lib/pair/version.rb +3 -0
- data/pair.gemspec +21 -0
- data/spec/fixture_helper.rb +28 -0
- data/spec/fixtures/api_hosted_session.yml +13 -0
- data/spec/fixtures/api_joined_session.yml +8 -0
- data/spec/fixtures/user_keys.yml +18 -0
- data/spec/pairmill/session/authorized_keys_file_spec.rb +79 -0
- data/spec/pairmill/session/joined_session_spec.rb +16 -0
- data/spec/pairmill/session/session_spec.rb +40 -0
- data/spec/spec_helper.rb +15 -0
- metadata +105 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color --format=documentation
|
data/.rvmrc
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
rvm 1.9.2-p180@pair-gem --create
|
2
|
+
|
3
|
+
bundler=`gem list bundler | grep bundler`
|
4
|
+
if [ $? -eq 1 ] ; then
|
5
|
+
echo "Installing bundler for project:\n\n";
|
6
|
+
gem install --pre bundler;
|
7
|
+
|
8
|
+
echo; read -s -n 1 -p "Run bundler install y/n" confirm; echo; echo;
|
9
|
+
if [ "$confirm" == "y" ]; then
|
10
|
+
bundle install;
|
11
|
+
fi
|
12
|
+
fi
|
13
|
+
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pair (0.0.1)
|
5
|
+
httparty
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.1.3)
|
11
|
+
httparty (0.8.1)
|
12
|
+
multi_json
|
13
|
+
multi_xml
|
14
|
+
multi_json (1.0.3)
|
15
|
+
multi_xml (0.4.1)
|
16
|
+
rspec (2.3.0)
|
17
|
+
rspec-core (~> 2.3.0)
|
18
|
+
rspec-expectations (~> 2.3.0)
|
19
|
+
rspec-mocks (~> 2.3.0)
|
20
|
+
rspec-core (2.3.1)
|
21
|
+
rspec-expectations (2.3.0)
|
22
|
+
diff-lcs (~> 1.1.2)
|
23
|
+
rspec-mocks (2.3.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
pair!
|
30
|
+
rspec (~> 2.3.0)
|
data/README.rdoc
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= Pair
|
2
|
+
|
3
|
+
Pair gem for collaborating between two or more remote pairs. The gem will facilite starting a hosted
|
4
|
+
session where multiple pairs access the computer of the hosted user.
|
5
|
+
|
6
|
+
== Requirements
|
7
|
+
|
8
|
+
Hosting Users:
|
9
|
+
|
10
|
+
* All users must have a github account with up-to-date public keys
|
11
|
+
* Must have a variant of unix installed
|
12
|
+
* Must have tmux installed
|
13
|
+
|
14
|
+
== Example
|
15
|
+
|
16
|
+
=== Interactive hosted session
|
17
|
+
|
18
|
+
bin/pairmill host -p<github-user>,<github-user>,<github-user>
|
19
|
+
|
20
|
+
=== Interactive hosted session
|
21
|
+
|
22
|
+
bin/pairmill host -v<github-user>,<github-user>,<github-user>
|
23
|
+
|
24
|
+
== Contributing to pair
|
25
|
+
|
26
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
27
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
28
|
+
* Fork the project
|
29
|
+
* Start a feature/bugfix branch
|
30
|
+
* Commit and push until you are happy with your contribution
|
31
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
32
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
33
|
+
|
34
|
+
== Copyright
|
35
|
+
|
36
|
+
Copyright (c) 2011 Pair. See LICENSE.txt for
|
37
|
+
further details.
|
38
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
7
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
11
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
12
|
+
spec.rcov = true
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :spec
|
data/bin/pair
ADDED
data/lib/pair.rb
ADDED
data/lib/pair/api.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Pair
|
4
|
+
module Api
|
5
|
+
def create_session(session)
|
6
|
+
post("/v1/sessions", {
|
7
|
+
:body => {
|
8
|
+
:session => {
|
9
|
+
:name => session.name,
|
10
|
+
:viewers => session.viewers,
|
11
|
+
:participants => session.participants,
|
12
|
+
:tunnel => {
|
13
|
+
:host_login => session.host_login
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
})
|
18
|
+
end
|
19
|
+
|
20
|
+
def join_session(session)
|
21
|
+
options = { :query => { :name => session.name, :host => session.host, :format => "json" } }
|
22
|
+
get("/v1/sessions/search.json", options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def api_token
|
26
|
+
config[:api_token]
|
27
|
+
end
|
28
|
+
|
29
|
+
def config
|
30
|
+
@config ||= if File.exists?(config_file)
|
31
|
+
YAML.load_file(config_file)[base_uri] || setup_config
|
32
|
+
else
|
33
|
+
setup_config
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def config_file
|
38
|
+
File.expand_path("~/.pair.yml")
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_config
|
42
|
+
config = YAML.load_file(config_file) rescue {}
|
43
|
+
|
44
|
+
print "Please input your API token for #{base_uri}: "
|
45
|
+
config[base_uri] = {:api_token => $stdin.gets.chomp}
|
46
|
+
|
47
|
+
File.open(config_file, 'w') do |f|
|
48
|
+
f.write(YAML.dump(config))
|
49
|
+
end
|
50
|
+
|
51
|
+
config[base_uri]
|
52
|
+
end
|
53
|
+
|
54
|
+
include HTTParty
|
55
|
+
extend self
|
56
|
+
|
57
|
+
base_uri ENV['BASE_URI'] || 'api.pairmill.com'
|
58
|
+
default_params :api_token => api_token
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "pair"
|
2
|
+
require "optparse"
|
3
|
+
|
4
|
+
module Pair
|
5
|
+
class CommandLine
|
6
|
+
attr_accessor :arguments, :options
|
7
|
+
private :arguments=, :options=
|
8
|
+
|
9
|
+
def self.run!(*arguments)
|
10
|
+
new(*arguments).run!
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(arguments)
|
14
|
+
self.arguments = arguments
|
15
|
+
self.options = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def run!
|
19
|
+
case command = arguments.shift
|
20
|
+
when 'host'
|
21
|
+
require "pair/command_line/host"
|
22
|
+
Host.run!(arguments)
|
23
|
+
else
|
24
|
+
unknown_command(command)
|
25
|
+
end
|
26
|
+
rescue SystemExit
|
27
|
+
raise
|
28
|
+
rescue
|
29
|
+
if $-d
|
30
|
+
STDOUT.puts "\n"
|
31
|
+
STDOUT.puts " Please contact support@pairmill.com, there"
|
32
|
+
STDOUT.puts " was an issue creating your session."
|
33
|
+
STDOUT.puts "\n"
|
34
|
+
else
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def unknown_command(command)
|
41
|
+
puts "Unknown command: #{command}" if command
|
42
|
+
|
43
|
+
#{$0} join [options]
|
44
|
+
abort %Q[
|
45
|
+
Usage: #{$0} host [options]
|
46
|
+
|
47
|
+
You can pass -h to a subcommand to learn more about it.
|
48
|
+
|
49
|
+
e.g. #{$0} join -h
|
50
|
+
].gsub(/^ {0,9}/,'')
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse
|
54
|
+
opts = OptionParser.new { |o| yield(o) }
|
55
|
+
opts.parse!(arguments)
|
56
|
+
opts
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "pair/command_line"
|
2
|
+
|
3
|
+
module Pair
|
4
|
+
class CommandLine
|
5
|
+
class Host < self
|
6
|
+
def run!
|
7
|
+
parse!
|
8
|
+
Pair::Session.host(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse!
|
12
|
+
opts = parse do |opts|
|
13
|
+
opts.banner = "Usage: #{$0} host [-s SESSION_NAME] [-v PAIR[,PAIR[,...]] [-p PAIR[,PAIR[,...]]" +
|
14
|
+
"\n\n" +
|
15
|
+
"At least one PAIR (of any type must be defined). A PAIR takes the form of a Github username." +
|
16
|
+
"\n\n"+
|
17
|
+
"Options:"
|
18
|
+
|
19
|
+
opts.on("-s", "--session-name=SESSION_NAME", "Automatically generated by server if not provided.") do |session_name|
|
20
|
+
options[:name] = session_name
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-v", "--viewers=PAIRS", Array) do |pairs|
|
24
|
+
options[:viewers] = pairs
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-p", "--participants=PAIRS", Array) do |pairs|
|
28
|
+
options[:participants] = pairs
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on_tail("-h", "--help", "Display this text") do
|
32
|
+
puts opts
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if options[:viewers].to_a.empty? && options[:participants].to_a.empty?
|
38
|
+
$stderr.puts "ERROR: At least one PAIR is required...\n\n"
|
39
|
+
abort(opts.inspect)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/pair/session.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "pair/session/tmux"
|
2
|
+
require "pair/session/tunnel"
|
3
|
+
require "pair/session/authorized_keys_file"
|
4
|
+
require "pair/session/hosted_session"
|
5
|
+
|
6
|
+
module Pair
|
7
|
+
class Session
|
8
|
+
attr_accessor :host, :name, :options, :viewers, :participants
|
9
|
+
private :host=, :name=, :options=, :viewers=, :participants=
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
self.options = options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Pair
|
2
|
+
class Session
|
3
|
+
class AuthorizedKeysFile
|
4
|
+
ACCESS_TYPE = "type"
|
5
|
+
KEYS = "keys"
|
6
|
+
|
7
|
+
attr_accessor :member_keys
|
8
|
+
attr_accessor :session
|
9
|
+
attr_accessor :key_file_path
|
10
|
+
|
11
|
+
def initialize(member_keys = {}, session)
|
12
|
+
self.member_keys = member_keys
|
13
|
+
self.session = session
|
14
|
+
self.key_file_path = File.expand_path("~/.ssh/authorized_keys")
|
15
|
+
end
|
16
|
+
|
17
|
+
def install
|
18
|
+
return nil if self.member_keys.values.empty? || self.member_keys.values.map { |k,v| v }.empty?
|
19
|
+
|
20
|
+
backup_authorized_keys if key_file_exists?
|
21
|
+
create_authorized_keys
|
22
|
+
end
|
23
|
+
|
24
|
+
def cleanup
|
25
|
+
cleanup_authorized_keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def cleanup_authorized_keys
|
29
|
+
remove_existing_file
|
30
|
+
move_backup_file if backup_key_file_exists?
|
31
|
+
end
|
32
|
+
|
33
|
+
def backup_authorized_keys
|
34
|
+
puts "Backing up authorized_keys: #{self.key_file_path}" if $-d
|
35
|
+
FileUtils.cp(self.key_file_path, backup_key_file_path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_existing_file
|
39
|
+
puts "Removing authorized_keys: #{self.key_file_path}" if $-d
|
40
|
+
FileUtils.rm(self.key_file_path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def move_backup_file
|
44
|
+
puts "Moving backup: #{self.backup_key_file_path}" if $-d
|
45
|
+
FileUtils.mv(backup_key_file_path, self.key_file_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def line_numbers_of(key)
|
49
|
+
`grep -ns ".*#{key}.*" #{self.key_file_path} | sed 's/\:.*//'`.split('\n').map(&:strip)
|
50
|
+
end
|
51
|
+
|
52
|
+
def key_file_exists?
|
53
|
+
File.exists? self.key_file_path
|
54
|
+
end
|
55
|
+
|
56
|
+
def backup_key_file_exists?
|
57
|
+
File.exists? backup_key_file_path
|
58
|
+
end
|
59
|
+
|
60
|
+
def backup_key_file_path
|
61
|
+
"#{self.key_file_path}.pair"
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def create_authorized_keys
|
67
|
+
File.open(self.key_file_path, 'w') do |file|
|
68
|
+
self.member_keys.each do |user, hash|
|
69
|
+
write_comment_for user, file
|
70
|
+
write_rows_for hash[KEYS], hash[ACCESS_TYPE], file
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
self.key_file_path
|
75
|
+
end
|
76
|
+
|
77
|
+
def write_comment_for user, in_file
|
78
|
+
in_file.puts "# #{user}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def write_rows_for keys, of_type, in_file
|
82
|
+
keys.each do |key|
|
83
|
+
in_file.puts "command=\"#{command(session, of_type)}\",#{key_options.join(',')} #{key["content"]} #id:#{key["id"]}"
|
84
|
+
end
|
85
|
+
in_file.puts ""
|
86
|
+
end
|
87
|
+
|
88
|
+
def command(session, type)
|
89
|
+
options = ["-S /tmp/pairmill/tmux-#{session.name} attach"]
|
90
|
+
options << "-r" if type == 'viewer'
|
91
|
+
|
92
|
+
"/usr/local/bin/tmux #{options.join(' ')}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def key_options
|
96
|
+
%w{no-port-forwarding no-X11-forwarding no-agent-forwarding}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Pair
|
2
|
+
class Session
|
3
|
+
class HostedSession < self
|
4
|
+
attr_accessor :tunnel, :authorized_keys_file, :tmux, :response
|
5
|
+
private :tunnel=, :authorized_keys_file=, :tmux=, :response=
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
puts "Hosting session #{"called #{options[:name].inspect} " if options[:name]}..."
|
9
|
+
|
10
|
+
self.name = options.delete(:name)
|
11
|
+
self.viewers = options.delete(:viewers) || []
|
12
|
+
self.participants = options.delete(:participants) || []
|
13
|
+
self.tmux = Tmux.new(self)
|
14
|
+
|
15
|
+
super(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def start!
|
19
|
+
if setup
|
20
|
+
display_startup_message
|
21
|
+
|
22
|
+
tunnel.open do
|
23
|
+
tmux.start
|
24
|
+
tmux.attach
|
25
|
+
end
|
26
|
+
else
|
27
|
+
puts "There was a problem starting the host session %s" % (name && name.inspect)
|
28
|
+
puts "response: #{response.inspect}" if response && $-d
|
29
|
+
puts ""
|
30
|
+
end
|
31
|
+
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
|
35
|
+
def display_startup_message
|
36
|
+
puts "Your pairs can connect to this session using the following command:"
|
37
|
+
puts ""
|
38
|
+
puts " #{connect_command}"
|
39
|
+
puts ""
|
40
|
+
print "Press any key to continue..."
|
41
|
+
|
42
|
+
gets
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: this should get host/user from response
|
46
|
+
def connect_command
|
47
|
+
"ssh-add; ssh -tqA -l#{bastion["join_user"]} #{bastion["host"]} #{name}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def bastion
|
51
|
+
response["tunnel"]["bastion"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def host_login
|
55
|
+
`whoami`.chomp
|
56
|
+
end
|
57
|
+
|
58
|
+
def cleanup_authorized_keys
|
59
|
+
authorized_keys_file.cleanup
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def setup
|
65
|
+
create_session_on_server && authorized_keys_file.install
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_session_on_server
|
69
|
+
self.response = Api.create_session(self)["session"]
|
70
|
+
|
71
|
+
if self.response
|
72
|
+
self.name = response["name"]
|
73
|
+
self.tunnel = Tunnel.new(self.response["tunnel"].merge(:host => true))
|
74
|
+
self.authorized_keys_file = AuthorizedKeysFile.new(self.response["member_keys"], self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stop!
|
79
|
+
tunnel.close
|
80
|
+
tmux.stop
|
81
|
+
cleanup_authorized_keys
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.host(options)
|
86
|
+
HostedSession.new(options).start!
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Pair
|
4
|
+
class Session
|
5
|
+
class Tmux
|
6
|
+
TMP_PATH = "/tmp"
|
7
|
+
|
8
|
+
attr_accessor :session
|
9
|
+
private :session=
|
10
|
+
|
11
|
+
def initialize(session)
|
12
|
+
self.session = session
|
13
|
+
create_socket_directory
|
14
|
+
end
|
15
|
+
|
16
|
+
def app_path
|
17
|
+
self.class.to_s.split('::').first.downcase
|
18
|
+
end
|
19
|
+
|
20
|
+
def unique
|
21
|
+
@unique ||= srand.to_s[0,5]
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
args = %W[-S #{socket_path} new-session -d]
|
26
|
+
system "tmux", *args
|
27
|
+
|
28
|
+
at_exit { stop }
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop
|
32
|
+
`lsof -t #{socket_path}/ | xargs kill -9`
|
33
|
+
FileUtils.rm_f(socket_path)
|
34
|
+
self.session.cleanup_authorized_keys if self.session.respond_to?(:cleanup_authorized_keys)
|
35
|
+
end
|
36
|
+
|
37
|
+
def window(command)
|
38
|
+
args = %W[
|
39
|
+
-S #{socket_path}
|
40
|
+
new-window
|
41
|
+
-t #{session.name}:0
|
42
|
+
-n 'Pairing'
|
43
|
+
'ssh pair@bastion.pairmill.com -A'
|
44
|
+
]
|
45
|
+
|
46
|
+
system "tmux", *args
|
47
|
+
end
|
48
|
+
|
49
|
+
def attach(read_only = false)
|
50
|
+
args = %W[-S #{socket_path} attach]
|
51
|
+
args += " -r" if read_only
|
52
|
+
|
53
|
+
system "tmux", *args
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def create_socket_directory
|
58
|
+
FileUtils.mkdir_p(socket_directory, :mode => 0700)
|
59
|
+
end
|
60
|
+
|
61
|
+
def socket_directory
|
62
|
+
File.join TMP_PATH, app_path
|
63
|
+
end
|
64
|
+
|
65
|
+
def socket_path
|
66
|
+
File.join socket_directory, socket_name
|
67
|
+
end
|
68
|
+
|
69
|
+
def socket_name
|
70
|
+
"tmux-#{session.name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Pair
|
2
|
+
class Session
|
3
|
+
class Tunnel
|
4
|
+
attr_accessor :options, :tunnel
|
5
|
+
private :options=, :tunnel=
|
6
|
+
|
7
|
+
# E.g. options:
|
8
|
+
# {
|
9
|
+
# "host_port" => 22,
|
10
|
+
# "host_login" => "bjeanes",
|
11
|
+
# "port" => 2222,
|
12
|
+
# "bastion" => {
|
13
|
+
# "host" => "bastion.pairmill.com",
|
14
|
+
# "ip_address" => "12.12.12.12",
|
15
|
+
# "ssh_port" => 22,
|
16
|
+
# "host_user" => "host",
|
17
|
+
# "join_user" => "join",
|
18
|
+
# }
|
19
|
+
# }
|
20
|
+
def initialize(options)
|
21
|
+
self.options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def open
|
25
|
+
self.tunnel = IO.popen(ssh_command)
|
26
|
+
puts "SSH tunnel started (PID = #{tunnel.pid})" if $-d
|
27
|
+
at_exit { close }
|
28
|
+
yield if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@tunnel_closed ||= begin
|
33
|
+
puts "Closing connection..."
|
34
|
+
Process.kill("INT", tunnel.pid)
|
35
|
+
Process.wait
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def ssh_command
|
43
|
+
options = []
|
44
|
+
if self.options[:host]
|
45
|
+
options << "-nq"
|
46
|
+
options << "-l #{bastion["host_user"]}"
|
47
|
+
options << "-p #{bastion["ssh_port"]}" unless bastion["ssh_port"] == 22
|
48
|
+
options << "-R #{port}:localhost:#{host_port}"
|
49
|
+
else
|
50
|
+
options << "-tAq"
|
51
|
+
options << "-l #{bastion["join_user"]}"
|
52
|
+
end
|
53
|
+
|
54
|
+
"ssh #{bastion["host"]} #{options.join(" ")}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def method_missing(method, *args, &block)
|
58
|
+
options[method.to_s] || super(method, *args, &block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/pair/version.rb
ADDED
data/pair.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/pair/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Bodaniel Jeanes", "Chad W. Pry"]
|
6
|
+
gem.email = ["me@bjeanes.com", "chad.pry@gmail.com"]
|
7
|
+
gem.description = %q{Effortless remote pairing}
|
8
|
+
gem.summary = %q{Pair with remote programmers with a single command.}
|
9
|
+
gem.homepage = "http://www.pairmill.com"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "pair"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Pair::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency("httparty", "~> 0.8.1")
|
19
|
+
|
20
|
+
gem.add_development_dependency("rspec", "~> 2.3.0")
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class FixtureHelper
|
4
|
+
FIXTURE_FILES = Dir[File.expand_path("#{Dir.pwd}/spec/fixtures/*.yml")]
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def method_missing(sym, *args, &block)
|
8
|
+
if fixture_exists? sym
|
9
|
+
load_fixture(sym)
|
10
|
+
else
|
11
|
+
super(sym, args, block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_fixture name
|
16
|
+
instance_variable_get("@#{name}") || instance_variable_set("@#{name}", YAML.load_file(FIXTURE_FILES[line_of_fixture(name)]))
|
17
|
+
end
|
18
|
+
|
19
|
+
def fixture_exists? name
|
20
|
+
!line_of_fixture(name).nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def line_of_fixture name
|
24
|
+
FIXTURE_FILES.map { |f| f.split('/').last }.index("#{name}.yml")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
:"chad.pry@gmail.com":
|
2
|
+
:type: interactive
|
3
|
+
:keys:
|
4
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAyIMHpjBSmn5/6iOjohj6u/WOm80OHc5S5srm3ARwrPx0oLSaMiMpt1cE+N2Tfz76xUkgqaYTjYoOIToJAJEufEdIlsvVfwrJlZ1/gY7mNV1K4W68gt5Z3xtOj6kaD8j6H385/qKUoMNzOAy6hA6PcpxbfCEP8N5NCIXiAldrxouf/Xh8Gd+AS4eaXzYarf/ShPAOoO/QqxWBgNNbyi/x8ulcxORxjIx0l4YSKHtWzHyypWVBjf0Ifd5bXUU13I20kG/D5SHFO5oYu6vC/jj9Ei4/jyzkvGH+2wcOTE5CjSr2kZ2Y55dFi/Kuu76wLgIVi5Ka6Z88208KXC97Mx4aQQ==
|
5
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0GKrgfzjAC2cXF3gZONipGwBNZaEaZk2Z23gpCZ1t7UH5JCZtoFT3ABtv4AAlwgXvW3Gnc4ycKT0ldkqwArAxjDDFzXdOvln7acWTNdoCchkJ6A9p3x14FhyhtyhvIRbJSqlKwd0AlgutehErvT31xX0b1sRF2pR3iaM/XTJFjr/49XD9iowH0zhLtD7FnNVwtZ4tGODRjU2p7bPPsvMYSwQ4oPbhC5ypzUaI6PQlSlgwljsdFK/NfGnMQK/7nWSlah+cbmJVwoXGZZFlmwsgvbg5qmG6bdtf602bRsU4utfGtOBAqkXc8C8EVDG68AzYbQrm95EsE+eQ8itV6Fr5
|
6
|
+
:"me@bjeanes.com":
|
7
|
+
:type: interactive
|
8
|
+
:keys:
|
9
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA7G6H0jo9njZdmPkHcSartymreAugZUOGTMvzrqfarvmejSM2K3k7IdCH5jO4PSQrmIKuV2MW01yc1s1DGSAjD77jji2fJrbv/zg5az0CyiCrX0w2drBIOecGkXFVuHbFEicXpCoOm9ieek0JR7LALw5kRyDcQnacmvV1d4/vnX4lG0fm4XWtLNi/0Vf1pcI+K+Ew/JGEVJNelYB3SaUihjDlID0nRR8jgc2WytT4srylPlhrjqvdV5dFkTA3l5RzmlF1KvoGacEqUEgzSW8RvDg6akg3XsTAhlfumq3UAF44BJ7zSOV/Z4RrBAGYbYXTBZkI26trzUX9SiLuGEzZ5Q==
|
10
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA62DymZvqPPXGFiq+d1ct/fhSOsUvFwxwfU8KkRTYMlApv8MDpg25X/PKyFwFDaHqKxzdkVRbeTuRdrkwRssRahxoXH4JTUhwviN+JrjSNw2saHdQEg/J4V/+p7PEmsAokXyYC0m/F9CO1EcyOMxjD4eU13f73KX7Ina9beOpDshjzzvW3jDrk1zS4q7C36uD8Ncn7dRlpTXCoJQSyBigxT4cJEvn9VX0z2Er5UZcz3JwJV/vDSUGjV+lNQECT5FsX1Glx/lSLuyIGrnRUiYjLNER0y3uO2gncQ198KN8GHH6SZzPOmq2KuR5iKNhUtPVXpcPic19BafPCTsoDyicmQ==
|
11
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvsdraiKK/2ngrGpYZjbU0leHFpeX9gWxB9epecYFDDNGyBkCQebQY1KAHQEWpRWijkUvhy7smNoGnhpTdI/P2g8v+h+p6UKdGd8dV54rjITYonGJQ0TGsCMVwaPQQckwjClHLY/UV/gP6JJurX1wNrjLugTV+ZXCBSrE6tC12qEjMpG/49ql8WtIb7uMwN3arhY/7DC5L6umWTw67gl096saq0fZ65LxsfjRTrWGnP+fikGtDgBYopE0cACDedKjw8v1+/pF+B4K3OL+hqRmgY+f/+dwPfPqZpYmd8lRnKY2cYVvv3a2CqrD7azj0+DTFO/n302puT6iUsvqQ/q01Q==
|
12
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzNrq260yIKNntEiIheoP6y8TWOf1zPjJJ3ZVy5rDvhtXaQzMFajQkKT54iRS6VZRQC4QB5H7DcT+NDanw7xkHMj44LA8qtiW9I5rFSu6XQIc1wxM0exuUD3R/qE9S1yker3QOTaaMiePmwOpN95ZtET6a1VFWCORxsxJ56mp3pgHryeMeEVJo17nSjXiodZ+DawM5YoAniRgWQSZXfu1eaNU/r8uHspwEl+kfFS7Bij0AL3jZj2fPJOSx2blHSYFEMQajE/DIQ/abeEkEFbYVrzJzVlEOONUCWQbixI+usySwrEJiNUDY2dij1OHwxONAUDF5maheVBQ1ceO9H5W4Q==
|
13
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApzAg5xAIS3kWxg1Sfs6iQ0kdb1221J75vs9tsFpfVgCefhYd4huBwXOZ3AQmcw2K4olGcwmbvYgZl17lXywJ0hlJ8HI9S9T34ooIj7NW9OBC03xTy98HxGON8++o3Youue7NaHIuQ2SG+m5R9bUKc2Zt7NxCP5f1Y4gDrSB6CDBUJ/53aEtfqynw2eAxvmG2ccRriFtLzyK8G/ylOSya2D34E1bET5Ca3s1rfO70iNg6z6Q9fYjRzXspDP+Fbk3JADa8YC2gtPEs4amOHtNvanvjwbGh/V5PqqoYrjomvLj4ksRkWEsPaXlgQR4A5+/0ng/mgpHB+TPCT3y914vPww==
|
14
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAunpMQw/49hXvh1Lf4IWYecJVOatvD3rJ/VgyPtpxVAwaCNAeWp1nKCKy68SO+z1NWhDcjsyzOQTtI8fdAn992SZRq2KMHlWVlmV3w2bal0AiES5bKqJfb3jfHBI1nyKBSML3Z74gV9ytULMn6COgRPMuraJiFDC7L1A2JsKT4AmTYZ6QAoD0eAlQeHw3OYmSCp5ExTjVhlEwN0di497N63KcjjVs2BYqwwUBeOI6iB7MVkZ0+l/Waq2zhh0ayqWREJfIdAUvjf/vgvN3H3G7d7cWJNu42IVQlZHE2CwE9u5krKRi1L28iPleK/px2Ny/fRlGYMKplsf5GEct1OrxKw==
|
15
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAxdP10RggBts2yKeOEqkPaPa2x12KWfzLWFTYcQ0IZhFEQK3VQFNDLg78jK6fNdzx1fuDlFR5Zp5ftOW9ltw/cXSgJE90jWKHFtGLljQoCsj0LreAXRGmqFzz7rVFCZYXYLwQjnyYvYXA/B/yfNpaDGVOUVievYxA12BS12s4FY5/+1bvkW1KlBdQvlPecFNyKHhEuxekwM/7TbHhBbberjqsxXKMANb3+IZ/JwNCmqlcPVz3iFE33NM8465852nNpOP1qmA32oE6UHxULqsaEPjGvF1GfsFw5235rgaYk+tLwQ2BMIsHUHLGFzR/HTud53E289itY0/JwTRRI9Ih2w==
|
16
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAykMNkh+QNla1nVEtup9ZCAPF841jscTj/Jltp8U3mkL0YmCybrH1a92xtAJr1jHqERXzjmtjhdLcXhdW7f+O2OgeJWK/KXQXDoptHL8NQxnG3DF8R4pF93XbldAHaEhEiysmkKOLC1axan+iK16eC7Q0cwPhXdvUqF3cjUlcdi1coJIS9fXJczxzpnNlhBMHKOs7NK9QyPyhOXPmObHbLQfWnIowp3hJg4WBQk/s0zAAaXSzwVxoj8Hfz1UwzEMyDFKV9HPyu6i+zxGO6z01w4GnvMD7+Nmq3/ynJhooxxGboc5MmzKo8foedyzPHMXwigNNNRFHo0JLMnrooLjZSQ==
|
17
|
+
- ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvCv77nf5C5eyzaFpojxIVxmHRDYzfpPfd/4c8rrvTJaeAJBCpnDgkoDNOO1r2VueEjK4rDLhwautumRfLk3k4E/t1Cngh+H1k1sfckPzjsrOfrVVMwG//h+F3Aon1HD1bMDTFQXVHkaavfMy/pCgc5pqR7/T14VxTpRf4LlP+ZYb5P3Q+zDsK3Y+MRkeECY4XSXjqX4w8RHYlgRTS/tHeXUlVQwaHwkXfPyEG9JBTq+Gc8ta4ZcXfFQ6M2WXDdnwpfiQpO7hHzqvhVgZyR0KbSGQQ+agwB+15qzDE4YnLwMsI5wx+AQGOALRhgvSRwES8aa2+hFx/l7pvWRrCrjcMw== bjeanes@g10506.cybergroupon.net
|
18
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
describe Pairmill::Session::AuthorizedKeysFile do
|
6
|
+
let(:member_keys) { FixtureHelper.user_keys }
|
7
|
+
let(:session) { "testing-session" }
|
8
|
+
let(:subject) { Pairmill::Session::AuthorizedKeysFile.new(member_keys, session) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
subject.stub!(:backup_authorized_keys)
|
12
|
+
subject.stub!(:create_authorized_keys)
|
13
|
+
subject.stub!(:remove_existing_file)
|
14
|
+
subject.stub!(:move_backup_file)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "when instantiating an object" do
|
18
|
+
it "sets the instance member_keys value" do
|
19
|
+
subject.member_keys.should == member_keys
|
20
|
+
end
|
21
|
+
|
22
|
+
it "sets the key file path to the current user" do
|
23
|
+
subject.key_file_path.should == File.expand_path("~/.ssh/authorized_keys")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "key_file_exists?" do
|
28
|
+
it "is true when the key file path exists" do
|
29
|
+
File.should_receive(:exists?).with(subject.key_file_path).and_return(true)
|
30
|
+
subject.key_file_exists?.should be_true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "backup_key_file_exists?" do
|
35
|
+
it "is true when the backup key file path exists" do
|
36
|
+
File.should_receive(:exists?).with(subject.backup_key_file_path).and_return(true)
|
37
|
+
subject.backup_key_file_exists?.should be_true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "installing an authorized keys file" do
|
42
|
+
describe "when an existing file is present" do
|
43
|
+
it "is backed up" do
|
44
|
+
subject.stub!(:key_file_exists?).and_return(true)
|
45
|
+
subject.should_receive(:backup_authorized_keys)
|
46
|
+
subject.install
|
47
|
+
end
|
48
|
+
|
49
|
+
it "adds a new file" do
|
50
|
+
subject.should_receive(:create_authorized_keys)
|
51
|
+
subject.install
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "when there is not an existing file" do
|
56
|
+
it "does not backup" do
|
57
|
+
subject.should_not_receive(:move_backup_file)
|
58
|
+
subject.install
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "cleaning up an authorized keys file" do
|
64
|
+
before do
|
65
|
+
subject.stub!(:key_file_exists?).and_return(true)
|
66
|
+
subject.stub!(:backup_key_file_exists?).and_return(true)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "removes the current authorized keys file" do
|
70
|
+
subject.should_receive(:remove_existing_file)
|
71
|
+
subject.cleanup
|
72
|
+
end
|
73
|
+
|
74
|
+
it "moves an existing backup key file" do
|
75
|
+
subject.should_receive(:move_backup_file)
|
76
|
+
subject.cleanup
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe Pairmill::Session::JoinedSession do
|
4
|
+
let(:host) { "chad.pry@gmail.com" }
|
5
|
+
let(:name) { "testing-conference" }
|
6
|
+
|
7
|
+
describe "when instantiating an object" do
|
8
|
+
it "sets the host" do
|
9
|
+
Pairmill::Session::JoinedSession.new(host, name).host.should == host
|
10
|
+
end
|
11
|
+
|
12
|
+
it "sets the name" do
|
13
|
+
Pairmill::Session::JoinedSession.new(host, name).name.should == name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe Pairmill::Session do
|
4
|
+
let(:host) { "chad.pry@gmail.com" }
|
5
|
+
let(:name) { "testing-conference" }
|
6
|
+
|
7
|
+
# describe "when joining a session" do
|
8
|
+
# describe "the returned session" do
|
9
|
+
# let(:joined_session) { Pairmill::Session::JoinedSession.new(host, name) }
|
10
|
+
#
|
11
|
+
# before do
|
12
|
+
# Pairmill::Session::JoinedSession.stub!(:new).and_return(joined_session)
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# it "starts a joined session" do
|
16
|
+
# joined_session.should_receive(:start!)
|
17
|
+
# Pairmill::Session.join(:host => host, :session_name => name)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
|
22
|
+
describe "starting a session" do
|
23
|
+
let(:session_json) { FixtureHelper.api_joined_session }
|
24
|
+
|
25
|
+
before do
|
26
|
+
Pairmill::Api.stub!(:join_session).and_return(session_json)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "fetches a session from the api" do
|
30
|
+
session = Pairmill::Session::JoinedSession.new(host, name)
|
31
|
+
session.should_receive(:fetch_session_details)
|
32
|
+
session.start!
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns a joined session instance" do
|
36
|
+
session = Pairmill::Session::JoinedSession.new(host, name)
|
37
|
+
session.should be_a(Pairmill::Session::JoinedSession)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'json'
|
5
|
+
require 'fixture_helper'
|
6
|
+
require 'pairmill'
|
7
|
+
require 'pairmill/session'
|
8
|
+
|
9
|
+
# Requires supporting files with custom matchers and macros, etc,
|
10
|
+
# in ./support/ and its subdirectories.
|
11
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pair
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bodaniel Jeanes
|
9
|
+
- Chad W. Pry
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-11-26 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: httparty
|
17
|
+
requirement: &70280668289560 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.8.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70280668289560
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rspec
|
28
|
+
requirement: &70280668276060 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70280668276060
|
37
|
+
description: Effortless remote pairing
|
38
|
+
email:
|
39
|
+
- me@bjeanes.com
|
40
|
+
- chad.pry@gmail.com
|
41
|
+
executables:
|
42
|
+
- pair
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- .gitignore
|
47
|
+
- .rspec
|
48
|
+
- .rvmrc
|
49
|
+
- Gemfile
|
50
|
+
- Gemfile.lock
|
51
|
+
- README.rdoc
|
52
|
+
- Rakefile
|
53
|
+
- bin/pair
|
54
|
+
- lib/pair.rb
|
55
|
+
- lib/pair/api.rb
|
56
|
+
- lib/pair/command_line.rb
|
57
|
+
- lib/pair/command_line/host.rb
|
58
|
+
- lib/pair/session.rb
|
59
|
+
- lib/pair/session/authorized_keys_file.rb
|
60
|
+
- lib/pair/session/hosted_session.rb
|
61
|
+
- lib/pair/session/tmux.rb
|
62
|
+
- lib/pair/session/tunnel.rb
|
63
|
+
- lib/pair/version.rb
|
64
|
+
- pair.gemspec
|
65
|
+
- spec/fixture_helper.rb
|
66
|
+
- spec/fixtures/api_hosted_session.yml
|
67
|
+
- spec/fixtures/api_joined_session.yml
|
68
|
+
- spec/fixtures/user_keys.yml
|
69
|
+
- spec/pairmill/session/authorized_keys_file_spec.rb
|
70
|
+
- spec/pairmill/session/joined_session_spec.rb
|
71
|
+
- spec/pairmill/session/session_spec.rb
|
72
|
+
- spec/spec_helper.rb
|
73
|
+
homepage: http://www.pairmill.com
|
74
|
+
licenses: []
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 1.8.10
|
94
|
+
signing_key:
|
95
|
+
specification_version: 3
|
96
|
+
summary: Pair with remote programmers with a single command.
|
97
|
+
test_files:
|
98
|
+
- spec/fixture_helper.rb
|
99
|
+
- spec/fixtures/api_hosted_session.yml
|
100
|
+
- spec/fixtures/api_joined_session.yml
|
101
|
+
- spec/fixtures/user_keys.yml
|
102
|
+
- spec/pairmill/session/authorized_keys_file_spec.rb
|
103
|
+
- spec/pairmill/session/joined_session_spec.rb
|
104
|
+
- spec/pairmill/session/session_spec.rb
|
105
|
+
- spec/spec_helper.rb
|