pair 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # jeweler generated
15
+ pkg
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
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pair.gemspec
4
+ gemspec
@@ -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)
@@ -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
+
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+
5
+ require "pair/command_line"
6
+ Pair::CommandLine.run!(ARGV)
7
+
@@ -0,0 +1,10 @@
1
+ require 'httparty'
2
+ require 'ruby-debug' if ENV["DEBUGGER"] == "true"
3
+
4
+ require "pair/version"
5
+ require "pair/api"
6
+ require "pair/session"
7
+
8
+ module Pair
9
+ # Your code goes here...
10
+ end
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Pair
2
+ VERSION = "0.0.1"
3
+ end
@@ -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,13 @@
1
+ session:
2
+ id: 124
3
+ version: 0.1.0
4
+ name: Testing Channel
5
+ tunnel:
6
+ ssh_user: host
7
+ ssh_server: bastion.pairmill.com
8
+ ssh_port: 22
9
+ remote_binding: localhost
10
+ remote_user: chad
11
+ remote_port: 22
12
+ tunnel_port: 2222
13
+
@@ -0,0 +1,8 @@
1
+ session:
2
+ id: 124
3
+ version: 0.1.0
4
+ name: Testing Channel
5
+ tunnel:
6
+ ssh_user: host
7
+ ssh_server: bastion.pairmill.com
8
+ ssh_port: 22
@@ -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
@@ -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