heroku-commander 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 +5 -0
- data/.rspec +3 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +10 -0
- data/LICENSE.md +22 -0
- data/README.md +73 -0
- data/Rakefile +22 -0
- data/assets/heroku-commander.png +0 -0
- data/examples/heroku-config.rb +12 -0
- data/examples/heroku-run-detached.rb +17 -0
- data/examples/heroku-run.rb +17 -0
- data/heroku-commander.gemspec +17 -0
- data/lib/config/locales/en.yml +26 -0
- data/lib/heroku-commander.rb +15 -0
- data/lib/heroku/commander.rb +25 -0
- data/lib/heroku/commander/errors.rb +6 -0
- data/lib/heroku/commander/errors/already_running_error.rb +13 -0
- data/lib/heroku/commander/errors/base.rb +81 -0
- data/lib/heroku/commander/errors/client_eio_error.rb +11 -0
- data/lib/heroku/commander/errors/command_error.rb +34 -0
- data/lib/heroku/commander/errors/missing_command_error.rb +13 -0
- data/lib/heroku/commander/errors/unexpected_output_error.rb +14 -0
- data/lib/heroku/commander/version.rb +5 -0
- data/lib/heroku/config.rb +33 -0
- data/lib/heroku/executor.rb +82 -0
- data/lib/heroku/pty.rb +7 -0
- data/lib/heroku/runner.rb +114 -0
- data/lib/heroku_commander.rb +2 -0
- data/spec/heroku/commander_spec.rb +81 -0
- data/spec/heroku/config_spec.rb +65 -0
- data/spec/heroku/executor_spec.rb +42 -0
- data/spec/heroku/runner_spec.rb +92 -0
- data/spec/heroku/version_spec.rb +8 -0
- data/spec/spec_helper.rb +6 -0
- metadata +100 -0
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2012 Daniel Doubrovkine, Artsy Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+

|
2
|
+
Heroku::Commander [](https://travis-ci.org/dblock/heroku-commander)
|
3
|
+
=================
|
4
|
+
|
5
|
+
Master the Heroku CLI from Ruby.
|
6
|
+
|
7
|
+
Usage
|
8
|
+
-----
|
9
|
+
|
10
|
+
Add `heroku` and `heroku-commander` to Gemfile.
|
11
|
+
|
12
|
+
``` ruby
|
13
|
+
gem "heroku"
|
14
|
+
gem "heroku-commander"
|
15
|
+
```
|
16
|
+
|
17
|
+
Heroku Configuration
|
18
|
+
--------------------
|
19
|
+
|
20
|
+
Returns a hash of an application's configuration (output from `heroku config`).
|
21
|
+
|
22
|
+
|
23
|
+
``` ruby
|
24
|
+
commander = Heroku::Commander.new({ :app => "heroku-commander" })
|
25
|
+
commander.config # => a hash of all settings for the heroku-commander app
|
26
|
+
```
|
27
|
+
|
28
|
+
Heroku Run
|
29
|
+
----------
|
30
|
+
|
31
|
+
Executes a command via `heroku run`, pipes and returns output lines. Unlike the heroku client, this also checks the process return code and raises a `Heroku::Commander::Errors::CommandError` if the latter is not zero, which makes this suitable for Rake tasks.
|
32
|
+
|
33
|
+
``` ruby
|
34
|
+
commander = Heroku::Commander.new({ :app => "heroku-commander" })
|
35
|
+
commander.run "uname -a" # => [ "Linux 2.6.32-348-ec2 #54-Ubuntu SMP x86_64 GNU" ]
|
36
|
+
```
|
37
|
+
|
38
|
+
Heroku Detached Run
|
39
|
+
-------------------
|
40
|
+
|
41
|
+
Executes a command via `heroku run:detached`, spawns a `heroku logs --tail -p pid` for the process started on Heroku, pipes and returns output lines. This also checks the process return code and raises a `Heroku::Commander::Errors::CommandError` if the latter is not zero.
|
42
|
+
|
43
|
+
``` ruby
|
44
|
+
commander = Heroku::Commander.new({ :app => "heroku-commander" })
|
45
|
+
commander.run("uname -a", { :detached => true }) # => [ "Linux 2.6.32-348-ec2 #54-Ubuntu SMP x86_64 GNU" ]
|
46
|
+
```
|
47
|
+
|
48
|
+
You can examine the output from `heroku logs --tail -p pid` line-by-line.
|
49
|
+
|
50
|
+
``` ruby
|
51
|
+
commander.run("ls -R", { :detached => true }) do |line|
|
52
|
+
# each line from the output of the command
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
For more information about Heroku one-off dynos see [this documentation](https://devcenter.heroku.com/articles/one-off-dynos).
|
57
|
+
|
58
|
+
More Examples
|
59
|
+
-------------
|
60
|
+
|
61
|
+
See [examples](examples) for more.
|
62
|
+
|
63
|
+
Contributing
|
64
|
+
------------
|
65
|
+
|
66
|
+
Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topic branches.
|
67
|
+
|
68
|
+
Copyright and License
|
69
|
+
---------------------
|
70
|
+
|
71
|
+
MIT License, see [LICENSE](LICENSE.md) for details.
|
72
|
+
|
73
|
+
(c) 2013 [Daniel Doubrovkine](http://github.com/dblock), [Frank Macreery](http://github.com/macreery), [Artsy Inc.](http://artsy.net)
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
begin
|
5
|
+
Bundler.setup(:default, :development)
|
6
|
+
rescue Bundler::BundlerError => e
|
7
|
+
$stderr.puts e.message
|
8
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
9
|
+
exit e.status_code
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'rspec/core'
|
15
|
+
require 'rspec/core/rake_task'
|
16
|
+
|
17
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
18
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
19
|
+
end
|
20
|
+
|
21
|
+
task :default => :spec
|
22
|
+
|
Binary file
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :development)
|
3
|
+
|
4
|
+
require 'heroku-commander'
|
5
|
+
|
6
|
+
logger = Logger.new($stdout)
|
7
|
+
logger.level = Logger::DEBUG
|
8
|
+
commander = Heroku::Commander.new({ :logger => logger })
|
9
|
+
config = commander.config
|
10
|
+
config.each_pair do |name, value|
|
11
|
+
logger.info "#{name}: #{value}"
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :development)
|
3
|
+
|
4
|
+
require 'heroku-commander'
|
5
|
+
|
6
|
+
logger = Logger.new($stdout)
|
7
|
+
logger.level = Logger::DEBUG
|
8
|
+
commander = Heroku::Commander.new({ :logger => logger })
|
9
|
+
|
10
|
+
uname = commander.run "uname -a", { :detached => true }
|
11
|
+
logger.info "Heroku dyno is a #{uname.join('\n')}."
|
12
|
+
|
13
|
+
files = []
|
14
|
+
commander.run "ls -1", { :detached => true } do |line|
|
15
|
+
files << line
|
16
|
+
end
|
17
|
+
logger.info "The Heroku file system has #{files.count} file(s): #{files.join(', ')}"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :development)
|
3
|
+
|
4
|
+
require 'heroku-commander'
|
5
|
+
|
6
|
+
logger = Logger.new($stdout)
|
7
|
+
logger.level = Logger::DEBUG
|
8
|
+
commander = Heroku::Commander.new({ :logger => logger })
|
9
|
+
|
10
|
+
uname = commander.run "uname -a"
|
11
|
+
logger.info "Heroku dyno is a #{uname.join('\n')}."
|
12
|
+
|
13
|
+
files = []
|
14
|
+
commander.run "ls -1" do |line|
|
15
|
+
files << line
|
16
|
+
end
|
17
|
+
logger.info "The Heroku file system has #{files.count} file(s): #{files.join(', ')}"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "heroku/commander/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "heroku-commander"
|
6
|
+
s.version = Heroku::Commander::VERSION
|
7
|
+
s.authors = [ "Daniel Doubrovkine", "Frank Macreery" ]
|
8
|
+
s.email = "dblock@dblock.org"
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.required_rubygems_version = '>= 1.3.6'
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.require_paths = [ "lib" ]
|
13
|
+
s.homepage = "http://github.com/dblock/heroku-commander"
|
14
|
+
s.licenses = [ "MIT" ]
|
15
|
+
s.summary = "Control Heroku from Ruby via its `heroku` shell command."
|
16
|
+
s.add_dependency "i18n"
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
en:
|
2
|
+
heroku:
|
3
|
+
commander:
|
4
|
+
errors:
|
5
|
+
messages:
|
6
|
+
client_eio:
|
7
|
+
message: "Heroku client stopped sending output."
|
8
|
+
summary: "The heroku client stopped sending output before exiting."
|
9
|
+
resolution: "This is unexpected and represents a Heroku server error. Try again."
|
10
|
+
command_error:
|
11
|
+
message: "The command `%{cmd}` failed with exit status %{status}."
|
12
|
+
summary: "%{message}%{lines}"
|
13
|
+
resolution: "Examine the command line and refer to the command documentation."
|
14
|
+
unexpected_output:
|
15
|
+
message: "The command `%{cmd}` returned unexpected output."
|
16
|
+
summary: "The output unexpectedly contained %{line}."
|
17
|
+
resolution: "This is unexpected and could represent a Heroku server error. Try again."
|
18
|
+
missing_command_error:
|
19
|
+
message: "Missing command."
|
20
|
+
summary: "The instance of Heroku::Runner is missing a command argument."
|
21
|
+
resolution: "Specify a command, for example:\n
|
22
|
+
\_\_Heroku::Runner.new({ :command => 'ls -1' })"
|
23
|
+
already_running_error:
|
24
|
+
message: "The process is already running with pid %{pid}."
|
25
|
+
summary: "You can only run! or detach! an instance of Heroku::Runner once."
|
26
|
+
resolution: "This is a bug in your code."
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
I18n.load_path << File.join(File.dirname(__FILE__), "config", "locales", "en.yml")
|
4
|
+
|
5
|
+
require 'pty'
|
6
|
+
require 'heroku/pty.rb'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
require 'heroku/commander/version'
|
10
|
+
require 'heroku/commander/errors'
|
11
|
+
require 'heroku/config'
|
12
|
+
require 'heroku/commander'
|
13
|
+
require 'heroku/executor'
|
14
|
+
require 'heroku/runner'
|
15
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Commander
|
3
|
+
|
4
|
+
attr_accessor :app, :logger, :config
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@app = options[:app]
|
8
|
+
@logger = options[:logger]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns a loaded Heroku::Config instance.
|
12
|
+
def config
|
13
|
+
@config ||= Heroku::Config.new({ :app => app, :logger => logger }).tap do |config|
|
14
|
+
config.reload!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run a process synchronously
|
19
|
+
def run(command, options = {}, &block)
|
20
|
+
runner = Heroku::Runner.new({ :app => app, :logger => logger, :command => command })
|
21
|
+
runner.run!(options, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require 'heroku/commander/errors/base'
|
2
|
+
require 'heroku/commander/errors/client_eio_error'
|
3
|
+
require 'heroku/commander/errors/command_error'
|
4
|
+
require 'heroku/commander/errors/missing_command_error'
|
5
|
+
require 'heroku/commander/errors/unexpected_output_error'
|
6
|
+
require 'heroku/commander/errors/already_running_error'
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Commander
|
3
|
+
module Errors
|
4
|
+
class Base < StandardError
|
5
|
+
|
6
|
+
# Problem occurred.
|
7
|
+
attr_reader :problem
|
8
|
+
|
9
|
+
# Summary of the problem.
|
10
|
+
attr_reader :summary
|
11
|
+
|
12
|
+
# Suggested problem resolution.
|
13
|
+
attr_reader :resolution
|
14
|
+
|
15
|
+
# Compose the message.
|
16
|
+
# === Parameters
|
17
|
+
# [key] Lookup key in the translation table.
|
18
|
+
# [attributes] The objects to pass to create the message.
|
19
|
+
def compose_message(key, attributes = {})
|
20
|
+
@problem = create_problem(key, attributes)
|
21
|
+
@summary = create_summary(key, attributes)
|
22
|
+
@resolution = create_resolution(key, attributes)
|
23
|
+
|
24
|
+
"\nProblem:\n #{@problem}"+
|
25
|
+
"\nSummary:\n #{@summary}"+
|
26
|
+
"\nResolution:\n #{@resolution}"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
BASE_KEY = "heroku.commander.errors.messages" #:nodoc:
|
32
|
+
|
33
|
+
# Given the key of the specific error and the options hash, translate the
|
34
|
+
# message.
|
35
|
+
#
|
36
|
+
# === Parameters
|
37
|
+
# [key] The key of the error in the locales.
|
38
|
+
# [options] The objects to pass to create the message.
|
39
|
+
#
|
40
|
+
# Returns a localized error message string.
|
41
|
+
def translate(key, options)
|
42
|
+
::I18n.translate("#{BASE_KEY}.#{key}", { :locale => :en }.merge(options)).strip
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create the problem.
|
46
|
+
#
|
47
|
+
# === Parameters
|
48
|
+
# [key] The error key.
|
49
|
+
# [attributes] The attributes to interpolate.
|
50
|
+
#
|
51
|
+
# Returns the problem.
|
52
|
+
def create_problem(key, attributes)
|
53
|
+
translate("#{key}.message", attributes)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create the summary.
|
57
|
+
#
|
58
|
+
# === Parameters
|
59
|
+
# [key] The error key.
|
60
|
+
# [attributes] The attributes to interpolate.
|
61
|
+
#
|
62
|
+
# Returns the summary.
|
63
|
+
def create_summary(key, attributes)
|
64
|
+
translate("#{key}.summary", attributes)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create the resolution.
|
68
|
+
#
|
69
|
+
# === Parameters
|
70
|
+
# [key] The error key.
|
71
|
+
# [attributes] The attributes to interpolate.
|
72
|
+
#
|
73
|
+
# Returns the resolution.
|
74
|
+
def create_resolution(key, attributes)
|
75
|
+
translate("#{key}.resolution", attributes)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Commander
|
3
|
+
module Errors
|
4
|
+
class CommandError < Heroku::Commander::Errors::Base
|
5
|
+
|
6
|
+
attr_accessor :inner_exception
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
@inner_exception = opts[:inner_exception]
|
10
|
+
super(compose_message("command_error", prepare_lines(opts)))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def prepare_lines(opts)
|
16
|
+
if opts[:lines] && opts[:lines].size > 4
|
17
|
+
lines = opts[:lines][0..2]
|
18
|
+
lines.push "... skipping #{opts[:lines].size - 4} line(s) ..."
|
19
|
+
lines.concat opts[:lines][-2..-1]
|
20
|
+
result = opts.dup
|
21
|
+
result[:lines] = "\n\t" + lines.join("\n\t")
|
22
|
+
result
|
23
|
+
elsif opts[:lines]
|
24
|
+
result = opts.dup
|
25
|
+
result[:lines] = "\n\t" + result[:lines].join("\n\t")
|
26
|
+
result
|
27
|
+
else
|
28
|
+
opts
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Commander
|
3
|
+
module Errors
|
4
|
+
class UnexpectedOutputError < Heroku::Commander::Errors::Base
|
5
|
+
|
6
|
+
attr_accessor :inner_exception
|
7
|
+
|
8
|
+
def initialize(opts)
|
9
|
+
super(compose_message("unexpected_output", opts))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Config < Hash
|
3
|
+
|
4
|
+
attr_accessor :app, :logger
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@app = options[:app]
|
8
|
+
@logger = options[:logger]
|
9
|
+
end
|
10
|
+
|
11
|
+
def reload!
|
12
|
+
clear
|
13
|
+
cmd = cmdline
|
14
|
+
Heroku::Executor.run cmd, { :logger => logger } do |line|
|
15
|
+
logger.debug "< #{line}" if logger
|
16
|
+
parts = line.split "=", 2
|
17
|
+
raise Heroku::Commander::Errors::UnexpectedOutputError.new({
|
18
|
+
:cmd => cmd,
|
19
|
+
:line => line
|
20
|
+
}) if parts.size != 2
|
21
|
+
self[parts[0].strip] = parts[1].strip
|
22
|
+
end
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def cmdline
|
29
|
+
[ "heroku", "config", "-s", @app ? "--app #{@app}" : nil ].compact.join(" ")
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Executor
|
3
|
+
|
4
|
+
class Terminate < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
# Executes a command and yields output line-by-line.
|
10
|
+
def run(cmd, options = {}, &block)
|
11
|
+
lines = []
|
12
|
+
logger = options[:logger]
|
13
|
+
logger.debug "Running: #{cmd}" if logger
|
14
|
+
PTY.spawn(cmd) do |r, w, pid|
|
15
|
+
logger.debug "Started: #{pid}" if logger
|
16
|
+
terminated = false
|
17
|
+
begin
|
18
|
+
r.sync = true
|
19
|
+
r.each do |line|
|
20
|
+
line.strip! if line
|
21
|
+
logger.debug "#{pid}: #{line}" if logger
|
22
|
+
if block_given?
|
23
|
+
yield line
|
24
|
+
end
|
25
|
+
lines << line
|
26
|
+
end
|
27
|
+
rescue Heroku::Executor::Terminate
|
28
|
+
logger.debug "Terminating #{pid}." if logger
|
29
|
+
Process.kill("TERM", pid)
|
30
|
+
terminated = true
|
31
|
+
rescue Errno::EIO, IOError => e
|
32
|
+
logger.debug "#{e.class}: #{e.message}" if logger
|
33
|
+
rescue PTY::ChildExited => e
|
34
|
+
logger.debug "Terminated: #{pid}" if logger
|
35
|
+
terminted = true
|
36
|
+
raise e
|
37
|
+
ensure
|
38
|
+
unless terminated
|
39
|
+
logger.debug "Waiting: #{pid}" if logger
|
40
|
+
Process.wait(pid) rescue Errno::ECHILD
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
check_exit_status! cmd, $?.exitstatus, lines
|
45
|
+
lines
|
46
|
+
rescue Errno::ECHILD => e
|
47
|
+
logger.debug "#{e.class}: #{e.message}" if logger
|
48
|
+
check_exit_status! cmd, $?.exitstatus, lines
|
49
|
+
lines
|
50
|
+
rescue PTY::ChildExited => e
|
51
|
+
logger.debug "#{e.class}: #{e.message}" if logger
|
52
|
+
check_exit_status! cmd, $!.status.exitstatus, lines
|
53
|
+
lines
|
54
|
+
rescue Heroku::Commander::Errors::Base => e
|
55
|
+
logger.debug "Error: #{e.problem}" if logger
|
56
|
+
raise
|
57
|
+
rescue Exception => e
|
58
|
+
logger.debug "#{e.class}: #{e.respond_to?(:problem) ? e.problem : e.message}" if logger
|
59
|
+
raise Heroku::Commander::Errors::CommandError.new({
|
60
|
+
:cmd => cmd,
|
61
|
+
:status => $?.exitstatus,
|
62
|
+
:message => e.message,
|
63
|
+
:inner_exception => e,
|
64
|
+
:lines => lines
|
65
|
+
})
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def check_exit_status!(cmd, status, lines = nil)
|
71
|
+
return if ! status || status == 0
|
72
|
+
raise Heroku::Commander::Errors::CommandError.new({
|
73
|
+
:cmd => cmd,
|
74
|
+
:status => status,
|
75
|
+
:message => "The command #{cmd} failed with exit status #{status}.",
|
76
|
+
:lines => lines
|
77
|
+
})
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/heroku/pty.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Runner
|
3
|
+
|
4
|
+
attr_accessor :app, :logger, :command
|
5
|
+
attr_reader :pid, :running, :tail
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@app = options[:app]
|
9
|
+
@logger = options[:logger]
|
10
|
+
@command = options[:command]
|
11
|
+
raise Heroku::Commander::Errors::MissingCommandError unless @command
|
12
|
+
end
|
13
|
+
|
14
|
+
def running?
|
15
|
+
!! @running
|
16
|
+
end
|
17
|
+
|
18
|
+
def run!(options = {}, &block)
|
19
|
+
if options && options[:detached]
|
20
|
+
run_detached! &block
|
21
|
+
else
|
22
|
+
run_attached! &block
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def run_attached!(&block)
|
29
|
+
@pid = nil
|
30
|
+
previous_line = nil # delay by 1 to avoid rc=status line
|
31
|
+
lines = Heroku::Executor.run cmdline, { :logger => logger } do |line|
|
32
|
+
if ! @pid
|
33
|
+
check_pid(line)
|
34
|
+
elsif block_given?
|
35
|
+
yield previous_line if previous_line
|
36
|
+
previous_line = line
|
37
|
+
end
|
38
|
+
end
|
39
|
+
lines.shift # remove Running `...` attached to terminal... up, run.xyz
|
40
|
+
check_exit_status! lines
|
41
|
+
lines
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_detached!(&block)
|
45
|
+
raise Heroku::Commander::Errors::AlreadyRunningError.new({ :pid => @pid }) if running?
|
46
|
+
@running = true
|
47
|
+
@pid = nil
|
48
|
+
@tail = nil
|
49
|
+
lines = Heroku::Executor.run cmdline({ :detached => true }), { :logger => logger } do |line|
|
50
|
+
check_pid(line) unless @pid
|
51
|
+
@tail ||= tail!(&block) if @pid
|
52
|
+
end
|
53
|
+
check_exit_status! @tail || lines
|
54
|
+
@running = false
|
55
|
+
@tail || lines
|
56
|
+
end
|
57
|
+
|
58
|
+
def cmdline(options = {})
|
59
|
+
[ "heroku", options[:detached] ? "run:detached" : "run", "\"(#{command} 2>&1 ; echo rc=\\$?)\"", @app ? "--app #{@app}" : nil ].compact.join(" ")
|
60
|
+
end
|
61
|
+
|
62
|
+
def check_exit_status!(lines)
|
63
|
+
status = (lines.size > 0) && (match = lines[-1].match(/^rc=(\d+)$/)) ? match[1] : nil
|
64
|
+
lines.pop if status
|
65
|
+
raise Heroku::Commander::Errors::CommandError.new({
|
66
|
+
:cmd => @command,
|
67
|
+
:status => status,
|
68
|
+
:message => "The command #{@command} failed with exit status #{status}.",
|
69
|
+
:lines => lines
|
70
|
+
}) unless status && status == "0"
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_pid(line)
|
74
|
+
if (match = line.match /attached to terminal... up, (run.\d+)$/)
|
75
|
+
@pid = match[1]
|
76
|
+
logger.debug "Heroku pid #{@pid} up." if logger
|
77
|
+
elsif (match = line.match /detached... up, (run.\d+)$/)
|
78
|
+
@pid = match[1]
|
79
|
+
logger.debug "Heroku detached pid #{@pid} up." if logger
|
80
|
+
else
|
81
|
+
@pid = ''
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def tail!(&block)
|
86
|
+
lines = []
|
87
|
+
tail_cmdline = [ "heroku", "logs", "-p #{@pid}", "--tail", @app ? "--app #{@app}" : nil ].compact.join(" ")
|
88
|
+
previous_line = nil # delay by 1 to avoid rc=status line
|
89
|
+
Heroku::Executor.run tail_cmdline, { :logger => logger } do |line|
|
90
|
+
# remove any ANSI output
|
91
|
+
line = line.gsub /\e\[(\d+)m/, ''
|
92
|
+
# lines are returned as [date/time] app/heroku[pid]: output
|
93
|
+
line = line.split("[#{@pid}]:")[-1].strip
|
94
|
+
if line.match(/Starting process with command/) || line.match(/State changed from \w+ to up/)
|
95
|
+
# ignore
|
96
|
+
elsif line.match(/State changed from \w+ to complete/) || line.match(/Process exited with status \d+/)
|
97
|
+
terminate_executor!
|
98
|
+
else
|
99
|
+
if block_given?
|
100
|
+
yield previous_line if previous_line
|
101
|
+
previous_line = line
|
102
|
+
end
|
103
|
+
lines << line
|
104
|
+
end
|
105
|
+
end
|
106
|
+
lines
|
107
|
+
end
|
108
|
+
|
109
|
+
def terminate_executor!
|
110
|
+
raise Heroku::Executor::Terminate
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heroku::Commander do
|
4
|
+
context "without arguments" do
|
5
|
+
subject do
|
6
|
+
Heroku::Commander.new
|
7
|
+
end
|
8
|
+
its(:app) { should be_nil }
|
9
|
+
end
|
10
|
+
context "with app" do
|
11
|
+
before :each do
|
12
|
+
Heroku::Executor.stub(:run)
|
13
|
+
end
|
14
|
+
subject do
|
15
|
+
Heroku::Commander.new({ :app => "heroku-commander" })
|
16
|
+
end
|
17
|
+
its(:app) { should eq "heroku-commander" }
|
18
|
+
its(:config) { should be_a Heroku::Config }
|
19
|
+
end
|
20
|
+
context "with a heroku configuration" do
|
21
|
+
before :each do
|
22
|
+
Heroku::Executor.stub(:run).with("heroku config -s --app heroku-commander", { :logger => nil }).
|
23
|
+
and_yield("APP_NAME=heroku-commander").
|
24
|
+
and_yield("RACK_ENV=staging")
|
25
|
+
end
|
26
|
+
subject { Heroku::Commander.new({ :app => "heroku-commander" }).config }
|
27
|
+
context "config" do
|
28
|
+
its(:size) { should == 2 }
|
29
|
+
it { subject["APP_NAME"].should eq "heroku-commander" }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
context "with logger" do
|
33
|
+
subject do
|
34
|
+
logger = Logger.new($stdout)
|
35
|
+
Heroku::Commander.new({ :logger => logger })
|
36
|
+
end
|
37
|
+
context "reload!" do
|
38
|
+
it "passes the logger" do
|
39
|
+
PTY.stub(:spawn)
|
40
|
+
subject.logger.should_receive(:debug).with("Running: heroku config -s")
|
41
|
+
subject.config
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
context "run" do
|
46
|
+
it "runs the command" do
|
47
|
+
Heroku::Executor.stub(:run).
|
48
|
+
and_yield("Running `...` attached to terminal... up, run.1234").
|
49
|
+
and_yield("app").
|
50
|
+
and_yield("bin").
|
51
|
+
and_yield("rc=0").
|
52
|
+
and_return([ "Running `...` attached to terminal... up, run.1234", "app", "bin", "rc=0" ])
|
53
|
+
subject.run("ls -1").should == [ "app", "bin" ]
|
54
|
+
end
|
55
|
+
it "runs the command detached" do
|
56
|
+
Heroku::Executor.stub(:run).with("heroku run:detached \"(ls -1 2>&1 ; echo rc=\\$?)\"", { :logger => nil }).
|
57
|
+
and_yield("Running `ls -1` detached... up, run.8748").
|
58
|
+
and_yield("Use `heroku logs -p run.8748` to view the output.").
|
59
|
+
and_yield("rc=0").
|
60
|
+
and_return([ "Running `ls -1` detached... up, run.8748", "Use `heroku logs -p run.8748` to view the output.", "rc=0" ])
|
61
|
+
Heroku::Executor.stub(:run).with("heroku logs -p run.8748 --tail", { :logger => nil }).
|
62
|
+
and_yield("2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`").
|
63
|
+
and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: bin").
|
64
|
+
and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: app").
|
65
|
+
and_yield("2013-01-31T00:56:13+00:00 app[run.8748]: rc=0").
|
66
|
+
and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0").
|
67
|
+
and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete").
|
68
|
+
and_return([
|
69
|
+
"2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`",
|
70
|
+
"2013-01-31T01:39:31+00:00 app[run.8748]: bin",
|
71
|
+
"2013-01-31T01:39:31+00:00 app[run.8748]: app",
|
72
|
+
"2013-01-31T00:56:13+00:00 app[run.8748]: rc=0",
|
73
|
+
"2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0",
|
74
|
+
"2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete"
|
75
|
+
])
|
76
|
+
Heroku::Runner.any_instance.should_receive(:terminate_executor!).twice
|
77
|
+
subject.run("ls -1", { :detached => true }).should == [ "bin", "app" ]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heroku::Config do
|
4
|
+
context "without arguments" do
|
5
|
+
subject do
|
6
|
+
Heroku::Config.new
|
7
|
+
end
|
8
|
+
its(:app) { should be_nil }
|
9
|
+
its(:cmdline) { should eq "heroku config -s" }
|
10
|
+
context "reload!" do
|
11
|
+
it "reloads the configuration" do
|
12
|
+
Heroku::Executor.stub(:run).with("heroku config -s", { :logger => nil }).
|
13
|
+
and_yield("APP_NAME=heroku-commander").
|
14
|
+
and_yield("RACK_ENV=staging")
|
15
|
+
subject.reload!
|
16
|
+
subject.size.should == 2
|
17
|
+
subject["APP_NAME"].should eq "heroku-commander"
|
18
|
+
subject["RACK_ENV"].should eq "staging"
|
19
|
+
end
|
20
|
+
it "reloads the configuration a second time" do
|
21
|
+
subject["APP_NAME"] = "old"
|
22
|
+
subject["OLD_VARIABLE"] = "old"
|
23
|
+
Heroku::Executor.stub(:run).with("heroku config -s", { :logger => nil }).
|
24
|
+
and_yield("APP_NAME=heroku-commander").
|
25
|
+
and_yield("RACK_ENV=staging")
|
26
|
+
subject.reload!
|
27
|
+
subject.size.should == 2
|
28
|
+
subject["APP_NAME"].should eq "heroku-commander"
|
29
|
+
subject["RACK_ENV"].should eq "staging"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
context "with app" do
|
34
|
+
subject do
|
35
|
+
Heroku::Config.new({ :app => "heroku-commander" })
|
36
|
+
end
|
37
|
+
its(:app) { should eq "heroku-commander" }
|
38
|
+
its(:cmdline) { should eq "heroku config -s --app heroku-commander" }
|
39
|
+
context "reload!" do
|
40
|
+
it "reloads the configuration" do
|
41
|
+
Heroku::Executor.stub(:run).with("heroku config -s --app heroku-commander", { :logger => nil }).
|
42
|
+
and_yield("APP_NAME=heroku-commander").
|
43
|
+
and_yield("RACK_ENV=staging")
|
44
|
+
subject.reload!
|
45
|
+
subject.size.should == 2
|
46
|
+
subject["APP_NAME"].should eq "heroku-commander"
|
47
|
+
subject["RACK_ENV"].should eq "staging"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
context "with logger" do
|
52
|
+
subject do
|
53
|
+
logger = Logger.new($stdout)
|
54
|
+
Heroku::Config.new({ :logger => logger })
|
55
|
+
end
|
56
|
+
context "reload!" do
|
57
|
+
it "passes the logger" do
|
58
|
+
PTY.stub(:spawn)
|
59
|
+
subject.logger.should_receive(:debug).with("Running: heroku config -s")
|
60
|
+
subject.reload!
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heroku::Executor do
|
4
|
+
context "missing argument" do
|
5
|
+
subject { lambda { Heroku::Executor.run } }
|
6
|
+
it { should raise_error ArgumentError }
|
7
|
+
end
|
8
|
+
context "command does not exist" do
|
9
|
+
subject { lambda { Heroku::Executor.run "executor_spec.rb" } }
|
10
|
+
it { should raise_error Heroku::Commander::Errors::CommandError, /The command `executor_spec.rb` failed with exit status \d+./ }
|
11
|
+
end
|
12
|
+
context "command exists" do
|
13
|
+
subject { lambda { Heroku::Executor.run "ls -1" } }
|
14
|
+
it { should_not raise_error }
|
15
|
+
its(:call) { should include "Gemfile" }
|
16
|
+
end
|
17
|
+
context "line-by-line" do
|
18
|
+
it "yields" do
|
19
|
+
lines = []
|
20
|
+
Heroku::Executor.run "ls -1" do |line|
|
21
|
+
lines << line
|
22
|
+
end
|
23
|
+
lines.should include "Gemfile"
|
24
|
+
end
|
25
|
+
it "doesn't yield nil lines" do
|
26
|
+
r = double(IO)
|
27
|
+
r.stub(:sync=)
|
28
|
+
r.stub(:each).and_yield("line1").and_yield(nil).and_yield("rc=0")
|
29
|
+
Process.stub(:wait)
|
30
|
+
PTY.stub(:spawn).and_yield(r, nil, 42)
|
31
|
+
Heroku::Executor.run("foobar").should == [ "line1", nil, "rc=0" ]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
context "logger" do
|
35
|
+
it "logs command" do
|
36
|
+
logger = Logger.new($stdout)
|
37
|
+
logger.should_receive(:debug).at_least(2).times
|
38
|
+
Heroku::Executor.run "ls -1", { :logger => logger }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heroku::Runner do
|
4
|
+
context "without a command" do
|
5
|
+
it "raises a missing command error" do
|
6
|
+
expect {
|
7
|
+
Heroku::Runner.new
|
8
|
+
}.to raise_error Heroku::Commander::Errors::MissingCommandError, /Missing command./
|
9
|
+
end
|
10
|
+
end
|
11
|
+
context "with a command" do
|
12
|
+
subject do
|
13
|
+
Heroku::Runner.new({ :command => "ls -1" })
|
14
|
+
end
|
15
|
+
its(:app) { should be_nil }
|
16
|
+
its(:logger) { should be_nil }
|
17
|
+
its(:command) { should eq "ls -1" }
|
18
|
+
its(:cmdline) { should eq "heroku run \"(ls -1 2>&1 ; echo rc=\\$?)\"" }
|
19
|
+
its(:running?) { should be_false }
|
20
|
+
context "run!" do
|
21
|
+
before :each do
|
22
|
+
Heroku::Executor.stub(:run).with(subject.send(:cmdline), { :logger => nil }).
|
23
|
+
and_yield("Running `...` attached to terminal... up, run.9783").
|
24
|
+
and_yield("app").
|
25
|
+
and_yield("bin").
|
26
|
+
and_yield("rc= 0").
|
27
|
+
and_return([ "Running `...` attached to terminal... up, run.9783", "app", "bin", "rc=0" ])
|
28
|
+
end
|
29
|
+
it "runs the command w/o a block" do
|
30
|
+
subject.run!.should == [ "app", "bin" ]
|
31
|
+
subject.pid.should == "run.9783"
|
32
|
+
subject.should_not be_running
|
33
|
+
end
|
34
|
+
it "runs the command with a block" do
|
35
|
+
lines = []
|
36
|
+
subject.run!.each do |line|
|
37
|
+
lines << line
|
38
|
+
end
|
39
|
+
lines.should == [ "app", "bin" ]
|
40
|
+
subject.pid.should == "run.9783"
|
41
|
+
subject.should_not be_running
|
42
|
+
end
|
43
|
+
it "raises an exception if the command fails" do
|
44
|
+
Heroku::Executor.stub(:run).with(subject.send(:cmdline), { :logger => nil }).
|
45
|
+
and_return([ "Running `...` attached to terminal... up, run.9783", "app", "bin", "rc=1" ])
|
46
|
+
expect {
|
47
|
+
subject.run!
|
48
|
+
}.to raise_error Heroku::Commander::Errors::CommandError, /The command `ls -1` failed with exit status 1./
|
49
|
+
end
|
50
|
+
end
|
51
|
+
context "run! detached" do
|
52
|
+
before :each do
|
53
|
+
Heroku::Executor.stub(:run).with(subject.send(:cmdline, { :detached => true }), { :logger => nil }).
|
54
|
+
and_yield("Running `ls -1` detached... up, run.8748").
|
55
|
+
and_yield("Use `heroku logs -p run.8748` to view the output.").
|
56
|
+
and_yield("rc=0").
|
57
|
+
and_return([ "Running `ls -1` detached... up, run.8748", "Use `heroku logs -p run.8748` to view the output.", "rc=0" ])
|
58
|
+
Heroku::Executor.stub(:run).with("heroku logs -p run.8748 --tail", { :logger => nil }).
|
59
|
+
and_yield("2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`").
|
60
|
+
and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: bin").
|
61
|
+
and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: app").
|
62
|
+
and_yield("2013-01-31T00:56:13+00:00 app[run.8748]: rc=0").
|
63
|
+
and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0").
|
64
|
+
and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete").
|
65
|
+
and_return([
|
66
|
+
"2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`",
|
67
|
+
"2013-01-31T01:39:31+00:00 app[run.8748]: bin",
|
68
|
+
"2013-01-31T01:39:31+00:00 app[run.8748]: app",
|
69
|
+
"2013-01-31T00:56:13+00:00 app[run.8748]: rc=0",
|
70
|
+
"2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0",
|
71
|
+
"2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete"
|
72
|
+
])
|
73
|
+
Heroku::Runner.any_instance.should_receive(:terminate_executor!).twice
|
74
|
+
end
|
75
|
+
it "runs the command w/o a block" do
|
76
|
+
subject.run!({ :detached => true }).should == [ "bin", "app" ]
|
77
|
+
subject.pid.should == "run.8748"
|
78
|
+
subject.should_not be_running
|
79
|
+
end
|
80
|
+
it "runs the command with a block" do
|
81
|
+
lines = []
|
82
|
+
subject.run!({ :detached => true }).each do |line|
|
83
|
+
lines << line
|
84
|
+
end
|
85
|
+
lines.should == [ "bin", "app" ]
|
86
|
+
subject.pid.should == "run.8748"
|
87
|
+
subject.should_not be_running
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: heroku-commander
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Daniel Doubrovkine
|
9
|
+
- Frank Macreery
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-02-02 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: i18n
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
description:
|
32
|
+
email: dblock@dblock.org
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- .rspec
|
39
|
+
- .travis.yml
|
40
|
+
- CHANGELOG.md
|
41
|
+
- Gemfile
|
42
|
+
- LICENSE.md
|
43
|
+
- README.md
|
44
|
+
- Rakefile
|
45
|
+
- assets/heroku-commander.png
|
46
|
+
- examples/heroku-config.rb
|
47
|
+
- examples/heroku-run-detached.rb
|
48
|
+
- examples/heroku-run.rb
|
49
|
+
- heroku-commander.gemspec
|
50
|
+
- lib/config/locales/en.yml
|
51
|
+
- lib/heroku-commander.rb
|
52
|
+
- lib/heroku/commander.rb
|
53
|
+
- lib/heroku/commander/errors.rb
|
54
|
+
- lib/heroku/commander/errors/already_running_error.rb
|
55
|
+
- lib/heroku/commander/errors/base.rb
|
56
|
+
- lib/heroku/commander/errors/client_eio_error.rb
|
57
|
+
- lib/heroku/commander/errors/command_error.rb
|
58
|
+
- lib/heroku/commander/errors/missing_command_error.rb
|
59
|
+
- lib/heroku/commander/errors/unexpected_output_error.rb
|
60
|
+
- lib/heroku/commander/version.rb
|
61
|
+
- lib/heroku/config.rb
|
62
|
+
- lib/heroku/executor.rb
|
63
|
+
- lib/heroku/pty.rb
|
64
|
+
- lib/heroku/runner.rb
|
65
|
+
- lib/heroku_commander.rb
|
66
|
+
- spec/heroku/commander_spec.rb
|
67
|
+
- spec/heroku/config_spec.rb
|
68
|
+
- spec/heroku/executor_spec.rb
|
69
|
+
- spec/heroku/runner_spec.rb
|
70
|
+
- spec/heroku/version_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
homepage: http://github.com/dblock/heroku-commander
|
73
|
+
licenses:
|
74
|
+
- MIT
|
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
|
+
segments:
|
86
|
+
- 0
|
87
|
+
hash: 155166833
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.3.6
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.24
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Control Heroku from Ruby via its `heroku` shell command.
|
100
|
+
test_files: []
|