text_tunnel 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/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ text_tunnel (0.1.0)
5
+ rest-client (~> 1.6.7)
6
+ sinatra (~> 1.3.2)
7
+ thin (~> 1.3.1)
8
+ trollop (~> 1.16.2)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ ansi (1.4.3)
14
+ daemons (1.1.8)
15
+ eventmachine (0.12.10)
16
+ ffi (1.0.11)
17
+ guard (1.2.3)
18
+ listen (>= 0.4.2)
19
+ thor (>= 0.14.6)
20
+ guard-minitest (0.5.0)
21
+ guard (>= 0.4)
22
+ listen (0.4.7)
23
+ rb-fchange (~> 0.0.5)
24
+ rb-fsevent (~> 0.9.1)
25
+ rb-inotify (~> 0.8.8)
26
+ mime-types (1.19)
27
+ minitest (3.2.0)
28
+ rack (1.4.1)
29
+ rack-protection (1.2.0)
30
+ rack
31
+ rack-test (0.6.1)
32
+ rack (>= 1.0)
33
+ rb-fchange (0.0.5)
34
+ ffi
35
+ rb-fsevent (0.9.1)
36
+ rb-inotify (0.8.8)
37
+ ffi (>= 0.5.0)
38
+ rest-client (1.6.7)
39
+ mime-types (>= 1.16)
40
+ sinatra (1.3.2)
41
+ rack (~> 1.3, >= 1.3.6)
42
+ rack-protection (~> 1.2)
43
+ tilt (~> 1.3, >= 1.3.3)
44
+ thin (1.3.1)
45
+ daemons (>= 1.0.9)
46
+ eventmachine (>= 0.12.6)
47
+ rack (>= 1.0.0)
48
+ thor (0.15.4)
49
+ tilt (1.3.3)
50
+ trollop (1.16.2)
51
+ turn (0.9.6)
52
+ ansi
53
+
54
+ PLATFORMS
55
+ ruby
56
+
57
+ DEPENDENCIES
58
+ guard (~> 1.2.3)
59
+ guard-minitest (~> 0.5.0)
60
+ minitest (~> 3.2.0)
61
+ rack-test (~> 0.6.1)
62
+ text_tunnel!
63
+ turn (~> 0.9.6)
data/Guardfile ADDED
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'minitest' do
5
+ # with Minitest::Unit
6
+ watch(%r|^test/(.*)\/?(.*)_test\.rb|)
7
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
8
+ watch(%r|^test/test_helper\.rb|) { "test" }
9
+
10
+ # with Minitest::Spec
11
+ # watch(%r|^spec/(.*)_spec\.rb|)
12
+ # watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
13
+ # watch(%r|^spec/spec_helper\.rb|) { "spec" }
14
+
15
+ # Rails 3.2
16
+ # watch(%r|^app/controllers/(.*)\.rb|) { |m| "test/controllers/#{m[1]}_test.rb" }
17
+ # watch(%r|^app/helpers/(.*)\.rb|) { |m| "test/helpers/#{m[1]}_test.rb" }
18
+ # watch(%r|^app/models/(.*)\.rb|) { |m| "test/unit/#{m[1]}_test.rb" }
19
+
20
+ # Rails
21
+ # watch(%r|^app/controllers/(.*)\.rb|) { |m| "test/functional/#{m[1]}_test.rb" }
22
+ # watch(%r|^app/helpers/(.*)\.rb|) { |m| "test/helpers/#{m[1]}_test.rb" }
23
+ # watch(%r|^app/models/(.*)\.rb|) { |m| "test/unit/#{m[1]}_test.rb" }
24
+ end
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/text_tunnel ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ gem 'trollop', '~> 1.16.2'
4
+ require "trollop"
5
+
6
+ require "text_tunnel/version"
7
+
8
+ config = Trollop.options do
9
+ version "text_tunnel #{TextTunnel::VERSION}"
10
+ banner <<-EOS
11
+ Usage:
12
+ text_tunnel [options] <filename>
13
+ where [options] are:
14
+ EOS
15
+
16
+ opt :port, "Port", :type => :int, :default => 1777
17
+ end
18
+
19
+ Trollop.die "text_tunnel requires exactly one filename argument" unless ARGV.size == 1
20
+ file_path = ARGV[0]
21
+
22
+ require "text_tunnel/client"
23
+ begin
24
+ client = Client.new(config[:port], file_path)
25
+ puts "Editing #{file_path} via text_tunnel, CRTL+C to stop"
26
+
27
+ loop do
28
+ if client.poll
29
+ puts "Wrote #{file_path} at #{Time.now}"
30
+ end
31
+
32
+ sleep 1
33
+ end
34
+ rescue Errno::ECONNREFUSED => e
35
+ puts "ERROR: Unable to connect to text_tunneld web server."
36
+ rescue Errno::EISDIR
37
+ puts "ERROR: #{file_path} is a directory."
38
+ rescue Errno::EACCES
39
+ puts "ERROR: Access denied - #{file_path}"
40
+ rescue RestClient::Exception, UnexpectedResponseError => e
41
+ puts "ERROR: Connected to a web server, but received an unexpected response."
42
+ puts "HTTP Status Code: #{e.http_code}"
43
+ puts "HTTP Body:"
44
+ puts e.http_body
45
+ rescue Interrupt
46
+ puts "Exiting..."
47
+ ensure
48
+ client.cleanup if client
49
+ end
data/bin/text_tunneld ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ gem 'trollop', '~> 1.16.2'
4
+ require "trollop"
5
+
6
+ require "text_tunnel/version"
7
+
8
+ config = Trollop.options do
9
+ version "text_tunneld #{TextTunnel::VERSION}"
10
+
11
+ opt :editor, "Editor command e.g. /usr/local/bin/subl (otherwise will use ENV[\"EDITOR\"])", :type => :string
12
+ opt :daemon, "Run as daemon in background"
13
+ opt :port, "Port", :type => :int, :default => 1777
14
+ opt :log, "Log file location", :type => :string
15
+ opt :pid, "PID file location", :type => :string
16
+ end
17
+
18
+ editor = config[:editor] || ENV["EDITOR"]
19
+ Trollop.die "No --editor argument or EDITOR environment variable" unless editor
20
+
21
+ require "text_tunnel/watched_file"
22
+ require "text_tunnel/watched_file_repository"
23
+ require "text_tunnel/server"
24
+
25
+ File.umask(0077)
26
+
27
+ Server.configure do |s|
28
+ s.set :environment, "production"
29
+ s.set :bind, "127.0.0.1"
30
+ s.set :port, config[:port]
31
+
32
+ # Nesting callback because Sinatra evaluates the first automatically, so to
33
+ # get a callback usable by Server it has to be wrapped.
34
+ s.set :editor_spawner do
35
+ Proc.new do |local_path|
36
+ # Ordinarily, string concatenation should not be used for the command
37
+ # string to spawn. However, if editor and local path are passed as
38
+ # separate variables spawn will fail if the editor command has options
39
+ # as it will treat the options as part of the file path.
40
+ pid = spawn "#{editor} #{local_path}"
41
+ Process.detach(pid)
42
+ end
43
+ end
44
+
45
+ s.set :watched_files, WatchedFileRepository.new
46
+
47
+ s.enable :logging
48
+ end
49
+
50
+ if config[:daemon]
51
+ Process.daemon(true)
52
+ end
53
+
54
+ if config[:log]
55
+ STDERR.reopen(open(config[:log], "w+"))
56
+ end
57
+
58
+ if config[:pid]
59
+ File.write(config[:pid], Process.pid.to_s)
60
+ at_exit { File.delete(config[:pid]) }
61
+ end
62
+
63
+ Server.run!
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ gem 'rest-client', '~> 1.6.7'
4
+ require "rest_client"
5
+
6
+ # Interface is compatible with RestClient exception
7
+ class UnexpectedResponseError < StandardError
8
+ attr_reader :response
9
+
10
+ def initialize(response)
11
+ @response = response
12
+ end
13
+
14
+ def http_code
15
+ response.code.to_i
16
+ end
17
+
18
+ def http_body
19
+ response.body
20
+ end
21
+ end
22
+
23
+ class Client
24
+ # Establishes initial connection to text_tunneld server
25
+ def initialize(port, file_path)
26
+ @file_path = file_path
27
+
28
+ file_name = File.basename(@file_path)
29
+ file_data = File.exist?(@file_path) ? File.read(@file_path) : ""
30
+
31
+ response = RestClient.post "http://localhost:#{port}/files",
32
+ :name => file_name,
33
+ :data => file_data
34
+ raise UnexpectedResponseError.new(response) unless response.code == 201
35
+ @location = response.headers[:location]
36
+ @etag = response.headers[:etag]
37
+ end
38
+
39
+ # Returns a truthy value if a change was made
40
+ def poll
41
+ response = RestClient.get(@location, :if_none_match => @etag)
42
+ raise UnexpectedResponseError.new(response) unless response.code == 200
43
+
44
+ @etag = response.headers[:etag]
45
+
46
+ File.write(@file_path, response.body)
47
+ rescue RestClient::NotModified
48
+ false
49
+ end
50
+
51
+ def cleanup
52
+ # This call can fail, especially if text_tunnel is terminating because of
53
+ # a previous error. So swallow any errors.
54
+ RestClient.delete(@location) rescue nil
55
+ end
56
+ end
@@ -0,0 +1,44 @@
1
+ gem "sinatra", "~> 1.3.2"
2
+ require "sinatra/base"
3
+
4
+ class Server < Sinatra::Base
5
+ post '/files' do
6
+ watched_file = watched_files.create(params[:name], params[:data])
7
+ spawn_editor(watched_file.local_path)
8
+
9
+ logger.info "#{watched_file.id} - new - #{params[:name]} (#{watched_file.data.size} bytes)"
10
+
11
+ status 201
12
+ etag watched_file.hash
13
+ headers "Location" => url("/files/#{watched_file.id}")
14
+ nil
15
+ end
16
+
17
+ get "/files/:id" do
18
+ logger.debug "#{params[:id]} - poll"
19
+
20
+ watched_file = watched_files.find(params[:id])
21
+ watched_file.poll
22
+ etag watched_file.hash
23
+ body watched_file.data
24
+
25
+ logger.info "#{params[:id]} - sent - #{watched_file.data.size} bytes"
26
+ end
27
+
28
+ delete "/files/:id" do
29
+ watched_file = watched_files.find(params[:id])
30
+ watched_files.remove(watched_file)
31
+
32
+ logger.info "#{params[:id]} - deleted"
33
+
34
+ nil
35
+ end
36
+
37
+ def watched_files
38
+ settings.watched_files
39
+ end
40
+
41
+ def spawn_editor(local_path)
42
+ settings.editor_spawner.call(local_path)
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module TextTunnel
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,47 @@
1
+ require "tmpdir"
2
+ require "fileutils"
3
+ require "securerandom"
4
+ require "digest"
5
+
6
+ class WatchedFile
7
+ attr_reader :id
8
+ attr_reader :local_path
9
+ attr_reader :data
10
+ attr_reader :hash
11
+
12
+ def initialize(name, data)
13
+ @name = sanitize_name(name)
14
+ @data = data
15
+ @id = SecureRandom.hex
16
+ @local_dir = "#{Dir.tmpdir}/text-tunnel/#{id}"
17
+ @local_path = "#{@local_dir}/#{@name}"
18
+
19
+ write_temp_file
20
+ end
21
+
22
+ def poll
23
+ old_mtime = @mtime
24
+ @mtime = File.mtime(local_path)
25
+ if @mtime != old_mtime
26
+ @mtime = old_mtime
27
+ @data = File.read(local_path)
28
+ hash_data
29
+ end
30
+ end
31
+
32
+ private
33
+ def write_temp_file
34
+ FileUtils.mkdir_p @local_dir
35
+ File.write(@local_path, @data)
36
+ @mtime = File.mtime(local_path)
37
+ hash_data
38
+ end
39
+
40
+ def hash_data
41
+ @hash = Digest::SHA1.hexdigest(@data)
42
+ end
43
+
44
+ def sanitize_name(name)
45
+ name.gsub(/[^a-zA-Z0-9\-_. ]/, "").strip
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ require "text_tunnel/watched_file"
2
+
3
+ class WatchedFileRepository
4
+ def initialize
5
+ @watched_files = {}
6
+ end
7
+
8
+ def create(name, data)
9
+ watched_file = WatchedFile.new(name, data)
10
+ @watched_files[watched_file.id] = watched_file
11
+ end
12
+
13
+ def remove(watched_file)
14
+ @watched_files.delete(watched_file.id)
15
+ end
16
+
17
+ def find(id)
18
+ @watched_files.fetch(id)
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ require "foo/version"
2
+
3
+ module TextTunnel
4
+ # Your code goes here...
5
+ end
data/readme.md ADDED
@@ -0,0 +1,49 @@
1
+ # Text Tunnel
2
+
3
+ Text Tunnel is a tool to edit remote files with a local editor.
4
+
5
+ ## How It Works
6
+
7
+ The text_tunneld server runs on the local host. Using SSH, a reverse port
8
+ forward is established on the remote host connecting back to the server. The
9
+ text_tunnel client runs on the remote machine and is used as the editor
10
+ binary. When the client is told to edit a file it sends the file to the
11
+ server, and the server loads it in the local editor. The client polls the
12
+ server for changes and downloads them whenever they occur.
13
+
14
+ ## Installation
15
+
16
+ The text_tunnel gem needs to be installed on both the local and remote hosts.
17
+
18
+ gem install text_tunnel
19
+
20
+ ## Usage
21
+
22
+ Start the server on the local machine. If you don't have an EDITOR environment
23
+ variable set you will need to include an --editor option.
24
+
25
+ text_tunneld -e /usr/local/bin/subl
26
+
27
+ Connect to the remote host with SSH and specify a reverse port forward (the
28
+ default port for Text Tunnel is 1777).
29
+
30
+ ssh -R 1777:localhost:1777 remote-host
31
+
32
+ On the remote machine use text_tunnel as your editor.
33
+
34
+ text_tunnel /path/to/file
35
+
36
+ The file should open on your local machine. Do your edits and save your file.
37
+ It will automatically be transferred to the remote host. When you are done hit
38
+ Crtl+C on the remote host to terminate Text Tunnel.
39
+
40
+ ## Shortcuts
41
+
42
+ The reverse port forward can be configured in ~/.ssh/config to avoid having to
43
+ retype the reverse port forward on the command line every time.
44
+
45
+ Consider setting text_tunnel as your EDITOR environment variable on your
46
+ remote hosts. This will let you use a local text editor for git commit
47
+ messages, crontabs, etc.
48
+
49
+ text_tunneld supports a background daemon mode. Run text_tunneld -h for full options.
@@ -0,0 +1,5 @@
1
+ gem 'minitest'
2
+ require 'minitest/autorun'
3
+ require 'turn/autorun'
4
+
5
+ ENV["RACK_ENV"] = "test"
@@ -0,0 +1,49 @@
1
+ require_relative "../test_helper"
2
+ require "rack/test"
3
+ require "text_tunnel/watched_file"
4
+ require "text_tunnel/watched_file_repository"
5
+ require "text_tunnel/server"
6
+
7
+ Server.configure do |s|
8
+ s.set :editor_spawner do
9
+ Proc.new {}
10
+ end
11
+ s.set :watched_files, WatchedFileRepository.new
12
+ end
13
+
14
+ class ServerTest < MiniTest::Unit::TestCase
15
+ include Rack::Test::Methods
16
+
17
+ def app
18
+ Server
19
+ end
20
+
21
+ def setup
22
+ @watched_files = WatchedFileRepository.new
23
+ Server.configure do |s|
24
+ s.set :watched_files, @watched_files
25
+ end
26
+ end
27
+
28
+ def test_complete_editing_session
29
+ post "/files", "name" => "foo", :data => "bar"
30
+ location = last_response["Location"]
31
+ etag = last_response["Etag"]
32
+
33
+ header("If-None-Match", etag)
34
+ get location
35
+ assert_equal 304, last_response.status
36
+
37
+ watched_file_id = location[/\w+\Z/]
38
+ local_file = @watched_files.find(watched_file_id).local_path
39
+ File.write(local_file, "new content")
40
+
41
+ get location
42
+ assert_equal 200, last_response.status
43
+ assert_equal "new content", last_response.body
44
+
45
+ delete location
46
+ assert_equal 200, last_response.status
47
+ assert_raises(KeyError) { @watched_files.find(watched_file_id) }
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ require_relative "../test_helper"
2
+ require "text_tunnel/watched_file_repository"
3
+
4
+ class WatchedFileRepositoryTest < MiniTest::Unit::TestCase
5
+ def setup
6
+ @repo = WatchedFileRepository.new
7
+ end
8
+
9
+ def test_create_returns_a_WatchedFile
10
+ watched_file = @repo.create "foo", "bar"
11
+ assert_kind_of WatchedFile, watched_file
12
+ end
13
+
14
+ def test_create_stores_created_WatchedFile
15
+ watched_file = @repo.create "foo", "bar"
16
+ assert_equal watched_file, @repo.find(watched_file.id)
17
+ end
18
+
19
+ def test_remove_makes_watched_file_no_longer_findable
20
+ watched_file = @repo.create "foo", "bar"
21
+ @repo.remove(watched_file)
22
+ assert_raises(KeyError) { @repo.find(watched_file.id) }
23
+ end
24
+
25
+ def test_find_existing_WatchedFile
26
+ watched_file = @repo.create "foo", "bar"
27
+ assert_equal watched_file, @repo.find(watched_file.id)
28
+ end
29
+
30
+ def test_find_raises_KeyError_on_missing_id
31
+ assert_raises(KeyError) { @repo.find("missing") }
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ require_relative "../test_helper"
2
+ require "text_tunnel/watched_file"
3
+
4
+ class WatchedFileTest < MiniTest::Unit::TestCase
5
+ def test_id_is_constant
6
+ wf = WatchedFile.new("foo", "bar")
7
+ assert_equal wf.id, wf.id
8
+ end
9
+
10
+ def test_id_is_different_for_each_object
11
+ refute_equal WatchedFile.new("foo", "bar").id, WatchedFile.new("foo", "bar").id
12
+ end
13
+
14
+ def test_local_path_points_to_file_with_data
15
+ wf = WatchedFile.new("foo", "bar")
16
+ assert_equal "bar", File.read(wf.local_path)
17
+ end
18
+
19
+ def test_poll_updates_data_and_hash_when_file_changes
20
+ wf = WatchedFile.new("foo", "bar")
21
+ old_hash, old_data = wf.hash, wf.data
22
+
23
+ # Without the sleep the original write and the following are so close
24
+ # together that the mtime is the same. I suppose this could be fixed by
25
+ # using guard-listen or EventMachine::FileWatcher, but in real usage it
26
+ # shouldn't be an issue. I prefer to keep the brain-dead simple polling
27
+ # instead of complicating this by requiring eventing or threading.
28
+ sleep 0.1
29
+
30
+ File.write(wf.local_path, "quz")
31
+ wf.poll
32
+
33
+ refute_equal old_hash, wf.hash
34
+ refute_equal old_data, wf.data
35
+ end
36
+
37
+ def test_sanitize_name_replaces_bad_characters
38
+ dirty_name = "/\\\"'asdf#!@$%^&*(){}}<>?'"
39
+ sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
40
+ assert_equal "asdf", sanitized_name
41
+ end
42
+
43
+ def test_sanitize_name_does_not_replace_periods
44
+ dirty_name = "foo.rb"
45
+ sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
46
+ assert_equal "foo.rb", sanitized_name
47
+ end
48
+
49
+ def test_sanitize_name_leaves_spaced_in_middle
50
+ dirty_name = "foo bar.rb"
51
+ sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
52
+ assert_equal "foo bar.rb", sanitized_name
53
+ end
54
+
55
+ def test_sanitize_name_trims_spaces_on_edges
56
+ dirty_name = " foo bar.rb "
57
+ sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
58
+ assert_equal "foo bar.rb", sanitized_name
59
+ end
60
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/text_tunnel/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jack Christensen"]
6
+ gem.email = ["jack@jackchristensen.com"]
7
+ gem.description = %q{Use your local text editor to edit files on remote servers.}
8
+ gem.summary = %q{Contains client and server that enables editing remote files with a local text editor.}
9
+ gem.homepage = "https://github.com/JackC/text_tunnel"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "text_tunnel"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = TextTunnel::VERSION
17
+
18
+
19
+ gem.add_dependency 'rest-client', '~> 1.6.7'
20
+ gem.add_dependency 'sinatra', "~> 1.3.2"
21
+ gem.add_dependency 'trollop', '~> 1.16.2'
22
+ gem.add_dependency 'thin', '~> 1.3.1'
23
+
24
+ gem.add_development_dependency 'minitest', "~> 3.2.0"
25
+ gem.add_development_dependency 'turn', "~> 0.9.6"
26
+ gem.add_development_dependency 'guard', "~> 1.2.3"
27
+ gem.add_development_dependency 'guard-minitest', "~> 0.5.0"
28
+ gem.add_development_dependency 'rack-test', '~> 0.6.1'
29
+ end
metadata ADDED
@@ -0,0 +1,215 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: text_tunnel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jack Christensen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.6.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.6.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: sinatra
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.3.2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.3.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: trollop
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.16.2
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.16.2
62
+ - !ruby/object:Gem::Dependency
63
+ name: thin
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.3.1
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.1
78
+ - !ruby/object:Gem::Dependency
79
+ name: minitest
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 3.2.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 3.2.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: turn
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 0.9.6
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 0.9.6
110
+ - !ruby/object:Gem::Dependency
111
+ name: guard
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 1.2.3
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 1.2.3
126
+ - !ruby/object:Gem::Dependency
127
+ name: guard-minitest
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: 0.5.0
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: 0.5.0
142
+ - !ruby/object:Gem::Dependency
143
+ name: rack-test
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: 0.6.1
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: 0.6.1
158
+ description: Use your local text editor to edit files on remote servers.
159
+ email:
160
+ - jack@jackchristensen.com
161
+ executables:
162
+ - text_tunnel
163
+ - text_tunneld
164
+ extensions: []
165
+ extra_rdoc_files: []
166
+ files:
167
+ - .gitignore
168
+ - Gemfile
169
+ - Gemfile.lock
170
+ - Guardfile
171
+ - Rakefile
172
+ - bin/text_tunnel
173
+ - bin/text_tunneld
174
+ - lib/text_tunnel.rb
175
+ - lib/text_tunnel/client.rb
176
+ - lib/text_tunnel/server.rb
177
+ - lib/text_tunnel/version.rb
178
+ - lib/text_tunnel/watched_file.rb
179
+ - lib/text_tunnel/watched_file_repository.rb
180
+ - readme.md
181
+ - test/test_helper.rb
182
+ - test/text_tunnel/server_test.rb
183
+ - test/text_tunnel/watched_file_repository_test.rb
184
+ - test/text_tunnel/watched_file_test.rb
185
+ - text_tunnel.gemspec
186
+ homepage: https://github.com/JackC/text_tunnel
187
+ licenses: []
188
+ post_install_message:
189
+ rdoc_options: []
190
+ require_paths:
191
+ - lib
192
+ required_ruby_version: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ none: false
200
+ requirements:
201
+ - - ! '>='
202
+ - !ruby/object:Gem::Version
203
+ version: '0'
204
+ requirements: []
205
+ rubyforge_project:
206
+ rubygems_version: 1.8.23
207
+ signing_key:
208
+ specification_version: 3
209
+ summary: Contains client and server that enables editing remote files with a local
210
+ text editor.
211
+ test_files:
212
+ - test/test_helper.rb
213
+ - test/text_tunnel/server_test.rb
214
+ - test/text_tunnel/watched_file_repository_test.rb
215
+ - test/text_tunnel/watched_file_test.rb