strobe 0.0.1
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/Gemfile +4 -0
- data/Gemfile.lock +69 -0
- data/Rakefile +36 -0
- data/bin/strobe +14 -0
- data/features/1_sign_up.feature +16 -0
- data/features/2_deploying.feature +35 -0
- data/features/3_deploying_sproutcore_apps.feature +19 -0
- data/features/step_definitions/cli_steps.rb +8 -0
- data/features/step_definitions/deploying_steps.rb +58 -0
- data/features/step_definitions/sign_up.rb +74 -0
- data/features/support/helpers.rb +104 -0
- data/features/support/strobe.rb +57 -0
- data/lib/strobe.rb +118 -0
- data/lib/strobe/account.rb +67 -0
- data/lib/strobe/cli.rb +78 -0
- data/lib/strobe/deploy.rb +131 -0
- data/lib/strobe/errors.rb +12 -0
- data/lib/strobe/http.rb +94 -0
- data/lib/strobe/server.rb +13 -0
- data/lib/strobe/settings.rb +56 -0
- data/lib/strobe/ui.rb +81 -0
- data/lib/strobe/user.rb +9 -0
- data/lib/strobe/version.rb +3 -0
- data/strobe.gemspec +35 -0
- metadata +250 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
require "excon"
|
2
|
+
require "timeout"
|
3
|
+
require "thor"
|
4
|
+
require "child_labor"
|
5
|
+
|
6
|
+
class Strobe
|
7
|
+
module Spec
|
8
|
+
module CLIExecution
|
9
|
+
def strobe(command, &block)
|
10
|
+
run("strobe #{command}", &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(command)
|
14
|
+
merged_env = env.merge("HOME" => system_home, "STROBE_URL" => "http://localhost:3000/")
|
15
|
+
env_string = merged_env.inject([]) { |env_str,env| env_str << %{#{env.first}="#{env.last}"} }
|
16
|
+
|
17
|
+
self.task = ChildLabor.subprocess("cd #{dir} && #{env_string.join(" ")} #{command}")
|
18
|
+
|
19
|
+
if block_given?
|
20
|
+
yield task
|
21
|
+
task.terminate unless task.terminated?
|
22
|
+
task.wait unless task.terminated?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def task
|
27
|
+
Thread.current[:task]
|
28
|
+
end
|
29
|
+
|
30
|
+
def task=(child)
|
31
|
+
Thread.current[:task] = child
|
32
|
+
end
|
33
|
+
|
34
|
+
def out
|
35
|
+
task.read
|
36
|
+
end
|
37
|
+
|
38
|
+
def err
|
39
|
+
task.read_stderr
|
40
|
+
end
|
41
|
+
|
42
|
+
def out_until_block
|
43
|
+
# read 1 first so we wait until the process is done processing the last write
|
44
|
+
chars = task.stdout.read(1)
|
45
|
+
|
46
|
+
loop do
|
47
|
+
chars << task.stdout.read_nonblock(1000)
|
48
|
+
sleep 0.05
|
49
|
+
end
|
50
|
+
rescue Errno::EAGAIN, EOFError
|
51
|
+
@last_out = chars
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
World(Strobe::Spec::CLIExecution)
|
data/lib/strobe.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
class Strobe
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# this isn't currently needed, but why start with globals when you could
|
8
|
+
# just as easily start with a singleton instance, which may be helpful
|
9
|
+
# later
|
10
|
+
def main
|
11
|
+
@main ||= new
|
12
|
+
end
|
13
|
+
|
14
|
+
def ui
|
15
|
+
main.ui
|
16
|
+
end
|
17
|
+
|
18
|
+
def ui=(ui)
|
19
|
+
main.ui = ui
|
20
|
+
end
|
21
|
+
|
22
|
+
def root
|
23
|
+
main.root
|
24
|
+
end
|
25
|
+
|
26
|
+
def app_root
|
27
|
+
main.app_root
|
28
|
+
end
|
29
|
+
|
30
|
+
def account
|
31
|
+
main.account
|
32
|
+
end
|
33
|
+
|
34
|
+
def settings
|
35
|
+
main.settings
|
36
|
+
end
|
37
|
+
|
38
|
+
def config_dir
|
39
|
+
main.config_dir
|
40
|
+
end
|
41
|
+
|
42
|
+
def local_config_dir
|
43
|
+
main.local_config_dir
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_writer :ui
|
48
|
+
|
49
|
+
def ui
|
50
|
+
@ui ||= UI::Basic.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def root
|
54
|
+
sproutcore = "#{app_root}/tmp/build"
|
55
|
+
File.directory?(sproutcore) ? sproutcore : app_root
|
56
|
+
end
|
57
|
+
|
58
|
+
def app_root
|
59
|
+
@app_root ||= Pathname.new(Dir.pwd)
|
60
|
+
end
|
61
|
+
|
62
|
+
def config_dir
|
63
|
+
@config_dir ||= begin
|
64
|
+
dir = Pathname.new(File.join(Strobe.user_home, ".strobe"))
|
65
|
+
FileUtils.mkdir_p(dir) unless dir.exist?
|
66
|
+
dir
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def local_config_dir
|
71
|
+
@local_config_dir ||= begin
|
72
|
+
dir = Pathname.new(File.join(app_root, ".strobe"))
|
73
|
+
FileUtils.mkdir_p(dir) unless dir.exist?
|
74
|
+
dir
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def settings
|
79
|
+
@settings ||= Settings.new
|
80
|
+
end
|
81
|
+
|
82
|
+
autoload :Account, "strobe/account"
|
83
|
+
autoload :CLI, "strobe/cli"
|
84
|
+
autoload :Deploy, "strobe/deploy"
|
85
|
+
autoload :Error, "strobe/errors"
|
86
|
+
autoload :HTTP, "strobe/http"
|
87
|
+
autoload :Server, "strobe/server"
|
88
|
+
autoload :Settings, "strobe/settings"
|
89
|
+
autoload :UI, "strobe/ui"
|
90
|
+
autoload :User, "strobe/user"
|
91
|
+
autoload :Version, "strobe/version"
|
92
|
+
|
93
|
+
# ERRORS
|
94
|
+
autoload :AccountError, "strobe/errors"
|
95
|
+
autoload :ApplicationError, "strobe/errors"
|
96
|
+
|
97
|
+
private
|
98
|
+
# sadly, copied from Rubygems
|
99
|
+
def self.user_home
|
100
|
+
unless RUBY_VERSION > '1.9' then
|
101
|
+
['HOME', 'USERPROFILE'].each do |homekey|
|
102
|
+
return File.expand_path(ENV[homekey]) if ENV[homekey]
|
103
|
+
end
|
104
|
+
|
105
|
+
if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] then
|
106
|
+
return File.expand_path("#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
File.expand_path "~"
|
111
|
+
rescue
|
112
|
+
if File::ALT_SEPARATOR then
|
113
|
+
"C:/"
|
114
|
+
else
|
115
|
+
"/"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "excon"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
class Strobe
|
5
|
+
class Account
|
6
|
+
def self.signup_or_login
|
7
|
+
Strobe.ui.info "You aren't logged in with a Strobe user on this machine"
|
8
|
+
Strobe.ui.info " 1. Create a new Strobe user"
|
9
|
+
Strobe.ui.info " 2. Log in with an existing Strobe user"
|
10
|
+
option = Strobe.ui.ask "Please select an option: "
|
11
|
+
|
12
|
+
case option
|
13
|
+
when "1"
|
14
|
+
signup
|
15
|
+
when "2"
|
16
|
+
login
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.signup
|
21
|
+
while true
|
22
|
+
email, pass = ask_for_login
|
23
|
+
passc = Strobe.ui.password "Repeat Password: "
|
24
|
+
break if pass == passc
|
25
|
+
|
26
|
+
Strobe.ui.info "The password and confirmation do not match. Please try again."
|
27
|
+
end
|
28
|
+
|
29
|
+
response = HTTP.new.signup(email, pass)
|
30
|
+
persist_details(*response)
|
31
|
+
|
32
|
+
Strobe.ui.info "Your account was created successfully."
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.login
|
36
|
+
email, pass = ask_for_login
|
37
|
+
|
38
|
+
response = HTTP.new.login(email, pass)
|
39
|
+
persist_details(*response)
|
40
|
+
|
41
|
+
Strobe.ui.info "You are now logged in as #{email}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# we don't need the account for anything yet, so leave it
|
45
|
+
def initialize(json)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def self.ask_for_login
|
50
|
+
Strobe.ui.info "Please enter your Strobe username and password."
|
51
|
+
|
52
|
+
email = Strobe.ui.ask "Email: "
|
53
|
+
pass = Strobe.ui.password "Password: "
|
54
|
+
return email, pass
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.persist_details(user, account = nil, errors = nil)
|
58
|
+
if account && errors
|
59
|
+
raise Strobe::AccountError, "Account creation failed. Please try again.\n#{errors}"
|
60
|
+
elsif errors
|
61
|
+
raise Strobe::AccountError, "Log in failed. Please try again.\n#{errors}"
|
62
|
+
else
|
63
|
+
Strobe.settings[:token] = user.token
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/strobe/cli.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
class Strobe
|
4
|
+
class CLI < Thor
|
5
|
+
check_unknown_options!
|
6
|
+
|
7
|
+
class_option "no-color", :type => :boolean, :banner => "Disable colorization in output"
|
8
|
+
class_option "path", :type => :string, :banner => "The path to your application"
|
9
|
+
|
10
|
+
def initialize(*)
|
11
|
+
super
|
12
|
+
|
13
|
+
opts = {"path" => Strobe.root}.merge(options)
|
14
|
+
self.options = Thor::CoreExt::HashWithIndifferentAccess.new(opts)
|
15
|
+
|
16
|
+
use_shell = options["no-color"] ? Thor::Shell::Basic.new : shell
|
17
|
+
Strobe.ui = UI::Shell.new(use_shell)
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "signup", "sign up for a new Strobe account"
|
21
|
+
def signup
|
22
|
+
Account.signup
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "deploy", "deploy your application to Strobe"
|
26
|
+
def deploy
|
27
|
+
Deploy.new(options[:path]).deploy
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "preview", "preview your application locally"
|
31
|
+
def preview
|
32
|
+
Server.new(options[:path]).start
|
33
|
+
end
|
34
|
+
|
35
|
+
def help(command)
|
36
|
+
case command
|
37
|
+
when "gemfile" then command = "gemfile.5"
|
38
|
+
when nil then command = "bundle"
|
39
|
+
else command = "bundle-#{cli}"
|
40
|
+
end
|
41
|
+
|
42
|
+
root = File.expand_path("../man", __FILE__)
|
43
|
+
|
44
|
+
if manpages.include?(command)
|
45
|
+
if have_groff?
|
46
|
+
groff = "groff -Wall -mtty-char -mandoc -Tascii"
|
47
|
+
pager = ENV['MANPAGER'] || ENV['PAGER'] || 'more'
|
48
|
+
|
49
|
+
Kernel.exec "#{groff} #{root}/#{command} | #{pager}"
|
50
|
+
else
|
51
|
+
puts File.read("#{root}/#{command}.txt")
|
52
|
+
end
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def manpages
|
60
|
+
man_root = File.expand_path("../man", __FILE__)
|
61
|
+
return [] unless File.directory?(man_root)
|
62
|
+
|
63
|
+
man_pages = []
|
64
|
+
Dir["#{man_root}/*"].each do |file|
|
65
|
+
base = File.basename(file)
|
66
|
+
next if base.include?(".")
|
67
|
+
man_pages << base
|
68
|
+
end
|
69
|
+
man_pages
|
70
|
+
end
|
71
|
+
|
72
|
+
def have_groff?
|
73
|
+
`which groff 2>#{NULL}`
|
74
|
+
$? == 0
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require "excon"
|
2
|
+
require "json"
|
3
|
+
require "mime/types"
|
4
|
+
require "active_support/secure_random"
|
5
|
+
|
6
|
+
cache_manifest = MIME::Type.new("text/cache-manifest") { |m| m.extensions = %w(manifest) }
|
7
|
+
MIME::Types.add(cache_manifest)
|
8
|
+
|
9
|
+
class Strobe
|
10
|
+
class Deploy
|
11
|
+
def initialize(path)
|
12
|
+
@path = path
|
13
|
+
@connection, @method = Strobe.settings.app_connection_and_method
|
14
|
+
end
|
15
|
+
|
16
|
+
def deploy
|
17
|
+
Strobe::Account.signup_or_login unless Strobe.settings[:token]
|
18
|
+
|
19
|
+
application, errors = HTTP.new.deploy(multipart)
|
20
|
+
|
21
|
+
if errors
|
22
|
+
raise Strobe::ApplicationError, "Application creation failed. Please try again.\n#{errors}"
|
23
|
+
else
|
24
|
+
# Update this every time for now, in case the app has changed
|
25
|
+
# on the server. This assumes that the server returns JSON
|
26
|
+
Strobe.settings.set_local(:application, application)
|
27
|
+
Strobe.ui.confirm "Your app was successfully deployed on #{application["url"]}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def multipart
|
32
|
+
Multipart.new do |m|
|
33
|
+
if application = Strobe.settings[:application]
|
34
|
+
name = application["name"]
|
35
|
+
url = application["url"]
|
36
|
+
else
|
37
|
+
name = Strobe.ui.ask "Please enter your application's name: "
|
38
|
+
url = Strobe.ui.ask "URL to deploy to: "
|
39
|
+
end
|
40
|
+
|
41
|
+
m.name name
|
42
|
+
m.url url
|
43
|
+
|
44
|
+
Dir["#{@path}/**/*"].each do |file|
|
45
|
+
next unless File.file?(file)
|
46
|
+
|
47
|
+
filename = file.sub(/^#{@path}/, '')
|
48
|
+
mime_type = MIME::Types.type_for(file).first
|
49
|
+
m.file filename, mime_type ? mime_type.to_s : "application/octet-stream", File.read(file)
|
50
|
+
end
|
51
|
+
|
52
|
+
apps = []
|
53
|
+
|
54
|
+
Dir["#{@path}/static/*"].each do |app|
|
55
|
+
next if File.file?(app)
|
56
|
+
|
57
|
+
dir = File.basename(app)
|
58
|
+
index = Dir["#{app}/*/*/index.html"].first
|
59
|
+
|
60
|
+
next if !index || !File.file?(index)
|
61
|
+
|
62
|
+
index = File.read(index)
|
63
|
+
apps << index
|
64
|
+
|
65
|
+
m.file "#{dir}/index.html", "text/html", index
|
66
|
+
end
|
67
|
+
|
68
|
+
m.file "index.html", "text/html", apps.first if apps.size == 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class Multipart
|
73
|
+
def initialize
|
74
|
+
@name = nil
|
75
|
+
@url = nil
|
76
|
+
@files = []
|
77
|
+
yield self if block_given?
|
78
|
+
end
|
79
|
+
|
80
|
+
def name(name)
|
81
|
+
@name = name
|
82
|
+
end
|
83
|
+
|
84
|
+
def url(url)
|
85
|
+
@url = url
|
86
|
+
end
|
87
|
+
|
88
|
+
def file(path, content_type, body)
|
89
|
+
@files << [path, content_type, body]
|
90
|
+
end
|
91
|
+
|
92
|
+
def headers
|
93
|
+
{ "Content-Type" => %[multipart/form-data; boundary="#{boundary}"] }
|
94
|
+
end
|
95
|
+
|
96
|
+
def body
|
97
|
+
b = "--#{boundary}\r\n"
|
98
|
+
b << part("metadata", "application/json", metadata)
|
99
|
+
b << "--#{boundary}\r\n"
|
100
|
+
|
101
|
+
@files.each do |path, type, body|
|
102
|
+
b << part("files[]", type, body)
|
103
|
+
b << "--#{boundary}\r\n"
|
104
|
+
end
|
105
|
+
|
106
|
+
b
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def boundary
|
112
|
+
@boundary ||= ActiveSupport::SecureRandom.hex(25)
|
113
|
+
end
|
114
|
+
|
115
|
+
def metadata
|
116
|
+
hash = { "name" => @name, "url" => @url }
|
117
|
+
hash.merge!( "paths" => @files.map { |path,*| path } )
|
118
|
+
hash.to_json
|
119
|
+
end
|
120
|
+
|
121
|
+
def part(name, type, body)
|
122
|
+
b = ""
|
123
|
+
b << %[Content-Disposition: form-data; name="#{name}"; filename="array"\r\n]
|
124
|
+
b << %[Content-Type: #{type}\r\n]
|
125
|
+
b << %[\r\n]
|
126
|
+
b << body
|
127
|
+
b << %[\r\n]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|