duostack 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +1 -0
- data/bin/duostack +244 -135
- metadata +5 -5
data/Rakefile
CHANGED
@@ -8,6 +8,7 @@ Jeweler::Tasks.new do |gem|
|
|
8
8
|
gem.version = `bin/duostack version`.chomp
|
9
9
|
gem.summary = %Q{Duostack command line client}
|
10
10
|
gem.description = %Q{Duostack command line client: create and manage Duostack apps}
|
11
|
+
gem.homepage = "http://www.duostack.com/"
|
11
12
|
gem.email = "todd@toddeichel.com"
|
12
13
|
gem.authors = "Todd Eichel"
|
13
14
|
gem.require_paths = ['.'] # default is ["lib"] but we don't have that
|
data/bin/duostack
CHANGED
@@ -28,12 +28,16 @@ $client = File.basename(__FILE__)
|
|
28
28
|
module Duostack
|
29
29
|
class Client
|
30
30
|
|
31
|
-
VERSION = '0.
|
31
|
+
VERSION = '0.2.0'
|
32
32
|
DEPENDENCIES_LAST_MODIFIED = 1297154481
|
33
|
-
DEFAULT_CREDENTIALS_LOCATION = '~/.duostack'
|
34
33
|
USER_AGENT = "duostack-#{VERSION}"
|
35
34
|
|
36
|
-
|
35
|
+
DEFAULTS = {
|
36
|
+
:credentials_location => '~/.duostack',
|
37
|
+
:remote_name => 'duostack'
|
38
|
+
}
|
39
|
+
|
40
|
+
FLAGS = [ # app and remote get special handling
|
37
41
|
'confirm',
|
38
42
|
'long',
|
39
43
|
'skip-dependency-checks'
|
@@ -56,7 +60,8 @@ module Duostack
|
|
56
60
|
'rake',
|
57
61
|
'config',
|
58
62
|
'env',
|
59
|
-
'access'
|
63
|
+
'access',
|
64
|
+
'domains'
|
60
65
|
],
|
61
66
|
:compound => [ # mult-part commands that expect subsequent arguments, must validate extra args on their own
|
62
67
|
'help',
|
@@ -64,7 +69,8 @@ module Duostack
|
|
64
69
|
'rake',
|
65
70
|
'config',
|
66
71
|
'env',
|
67
|
-
'access'
|
72
|
+
'access',
|
73
|
+
'domains'
|
68
74
|
]
|
69
75
|
}
|
70
76
|
|
@@ -74,8 +80,10 @@ module Duostack
|
|
74
80
|
end
|
75
81
|
|
76
82
|
def run
|
77
|
-
@creds_file = get_flag_arg('creds')
|
78
|
-
@
|
83
|
+
@creds_file = get_flag_arg('creds') || DEFAULTS[:credentials_location]
|
84
|
+
@remote = get_flag_arg('remote') || DEFAULTS[:remote_name]
|
85
|
+
# get app from args if we can, otherwise it will be extracted from git remotes during require_app
|
86
|
+
@app_name = get_flag_arg('app')
|
79
87
|
@flags = parse_flags
|
80
88
|
@command = @args.shift || COMMANDS[:default]
|
81
89
|
|
@@ -179,12 +187,6 @@ module Duostack
|
|
179
187
|
# curl SSL
|
180
188
|
# handled inside api_host method
|
181
189
|
|
182
|
-
# expect (only if running console)
|
183
|
-
# TODO: just use ruby expect lib
|
184
|
-
if @command == 'console' && `which expect`.empty?
|
185
|
-
exit_with "missing dependency, please install Expect"
|
186
|
-
end
|
187
|
-
|
188
190
|
# touch .duostack so we know when deps were last checked (but don't create it if it doesn't exist)
|
189
191
|
require 'fileutils'
|
190
192
|
FileUtils.touch(filename) if File.exist?(filename)
|
@@ -194,7 +196,7 @@ module Duostack
|
|
194
196
|
# ensures the app can be identified from inspecting the git repo or command line args
|
195
197
|
def require_app
|
196
198
|
require_user # all app commands require a user
|
197
|
-
@app_name ||=
|
199
|
+
@app_name ||= app_name_from_git_remotes
|
198
200
|
unless @app_name
|
199
201
|
exit_with "run this command from a Duostack app folder or pass an app name with the --app argument"
|
200
202
|
end
|
@@ -209,7 +211,7 @@ module Duostack
|
|
209
211
|
ssh_key = require_ssh_key # all user commands require an SSH key
|
210
212
|
|
211
213
|
puts "First-time Duostack client setup"
|
212
|
-
print "Email
|
214
|
+
print "Email: "
|
213
215
|
username = $stdin.gets.chomp
|
214
216
|
password = `bash -c 'read -sp "Password: " passwd; echo $passwd'`.chomp
|
215
217
|
puts '' # clears the line after
|
@@ -245,7 +247,7 @@ module Duostack
|
|
245
247
|
# NOTE: @confirmed is set above in 'run' so we can be assured the app_name came from the --app flag
|
246
248
|
# if assessed confirmation here, the app could have been extracted from the git repo
|
247
249
|
unless @confirmed
|
248
|
-
exit_with "command requires confirmation, run again with '--confirm --app
|
250
|
+
exit_with "command requires confirmation, run again with '--confirm --app #{@app_name}'"
|
249
251
|
end
|
250
252
|
end
|
251
253
|
|
@@ -253,9 +255,103 @@ module Duostack
|
|
253
255
|
# utility methods
|
254
256
|
#########################################################################
|
255
257
|
|
256
|
-
def
|
257
|
-
|
258
|
-
|
258
|
+
def process_crud_command(opts)
|
259
|
+
|
260
|
+
# get action
|
261
|
+
action = @args.shift || 'list' # list is the default
|
262
|
+
action = 'list' if action == 'ls' # normalize shorthand for list
|
263
|
+
action = 'remove' if action == 'rm' # normalize shorthand for remove
|
264
|
+
|
265
|
+
# extract important things from opts, otherwise merge defaults
|
266
|
+
resource = opts[:resource]
|
267
|
+
resources = "#{resource}s" # may eventually need a better pluralizer
|
268
|
+
|
269
|
+
# default settings for opts. priority is:
|
270
|
+
# 1. hash under each action (e.g. opts[:actions][:list][:param])
|
271
|
+
# 2. general opts (e.g. opts[:param])
|
272
|
+
# 3. generic defaults specified here (e.g. `resources`, the pluralized resource name)
|
273
|
+
defaults = {
|
274
|
+
:error_message => opts[:error_message] || "<#{resource}>",
|
275
|
+
:param => opts[:param] || resources,
|
276
|
+
:additional_params => opts[:additonal_params] || nil,
|
277
|
+
:args_processor => opts[:args_processor] || lambda { |args| args }
|
278
|
+
}
|
279
|
+
|
280
|
+
# actions can be either a hash (containing action-specific options) or an array (accept the defaults)
|
281
|
+
if opts[:actions].respond_to?(:keys)
|
282
|
+
valid_actions = opts[:actions].keys.map { |x| x.to_s }
|
283
|
+
opts = defaults.merge(opts[:actions][action.to_sym])
|
284
|
+
else
|
285
|
+
valid_actions = opts[:actions]
|
286
|
+
opts = defaults.merge(opts)
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
# ensure action is valid
|
291
|
+
unless valid_actions.include?(action)
|
292
|
+
exit_with "invalid argument for '#{@command}', try #{sentencize(valid_actions)}"
|
293
|
+
end
|
294
|
+
|
295
|
+
# ensure we have an argument for add/remove actions which require it
|
296
|
+
if %w(add remove).include?(action)
|
297
|
+
# gather up and compose remaining args for add/remove operations
|
298
|
+
argument = opts[:args_processor].call(@args).join(' ')
|
299
|
+
@args.clear # clean up, since we processed every remaining arg
|
300
|
+
|
301
|
+
# warn and exit unless we have an argument to pass
|
302
|
+
if argument.empty?
|
303
|
+
exit_with "'#{@command} #{action}' requires an argument, try '#{@command} #{action} #{opts[:error_message]}'"
|
304
|
+
end
|
305
|
+
|
306
|
+
# clean up argument
|
307
|
+
argument = CGI::escape(argument)
|
308
|
+
end
|
309
|
+
|
310
|
+
# finally, process action
|
311
|
+
case action
|
312
|
+
when 'list'
|
313
|
+
print api_get("list_#{resources}", opts[:additional_params])
|
314
|
+
when 'add'
|
315
|
+
puts api_get("add_#{resource}", ["#{opts[:param]}=#{argument}", opts[:additional_params]].compact.join('&'))
|
316
|
+
when 'remove'
|
317
|
+
puts api_get("remove_#{resource}", ["#{opts[:param]}=#{argument}", opts[:additional_params]].compact.join('&'))
|
318
|
+
when 'clear'
|
319
|
+
require_confirmation
|
320
|
+
puts api_get("clear_#{resources}", opts[:additional_params])
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
|
325
|
+
# attempts to get the app name out of git remotes
|
326
|
+
# if --remote is specified, it will use that assuming it's valid
|
327
|
+
# if not, it will look for any remote name that references duostack.net
|
328
|
+
def app_name_from_git_remotes
|
329
|
+
remotes = `git remote -v 2>/dev/null`.split("\n")
|
330
|
+
remotes.reject! { |line| line.split.last == '(fetch)' } # filter out fetch remotes (careful, does not exist in older git versions)
|
331
|
+
|
332
|
+
# find url of remote of specified name
|
333
|
+
valid_remotes = remotes.reject do |line|
|
334
|
+
line.split.first != @remote
|
335
|
+
end
|
336
|
+
remote_url = valid_remotes.first.split[1] unless valid_remotes.empty?
|
337
|
+
|
338
|
+
# correct url will be in the form "git@duostack.net:appname.git"
|
339
|
+
# if remote of specified name is a duostack remote, set app_name
|
340
|
+
if !remote_url.to_s.empty? && remote_url[0..16] == "git@duostack.net:" && remote_url[-4..-1] == ".git"
|
341
|
+
app_name = remote_url[17..-5]
|
342
|
+
else # the specified remote isn't for duostack; look for another (take first match)
|
343
|
+
remotes.each do |line|
|
344
|
+
result = line.scan(/git@duostack\.net:(\w+)\.git/)
|
345
|
+
if result.length > 0 && result[0].length > 0
|
346
|
+
app_name = result[0][0]
|
347
|
+
remote_name = line.split.first
|
348
|
+
warn_with "remote '#{@remote}' does not refer to Duostack, using remote '#{remote_name}' instead"
|
349
|
+
break
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
return app_name ||= nil
|
259
355
|
end
|
260
356
|
|
261
357
|
def ssh_key_location
|
@@ -324,6 +420,18 @@ module Duostack
|
|
324
420
|
end
|
325
421
|
end
|
326
422
|
|
423
|
+
def sentencize(array, conjunction='or')
|
424
|
+
# http://stackoverflow.com/questions/2038787/join-array-contents-into-an-english-list
|
425
|
+
case array.length
|
426
|
+
when 0, 1
|
427
|
+
array.first.to_s
|
428
|
+
when 2
|
429
|
+
"#{array.first} #{conjunction} #{array.last}"
|
430
|
+
else
|
431
|
+
"#{array[0..-2].join(', ')}, #{conjunction} #{array.last}"
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
327
435
|
def debug(message)
|
328
436
|
puts message if ENV['DSDEBUG']
|
329
437
|
end
|
@@ -374,16 +482,15 @@ module Duostack
|
|
374
482
|
exit_with "current directory is not a Git repository, run 'git init' first"
|
375
483
|
end
|
376
484
|
|
377
|
-
#
|
378
|
-
if
|
379
|
-
exit_with "
|
485
|
+
# make sure we're not going to step on an existing git remote
|
486
|
+
if `git remote`.chomp.split("\n").include?(@remote)
|
487
|
+
exit_with "there is already a Git remote named '#{@remote}', please remove it or pass a \ndifferent name with the --remote argument"
|
380
488
|
end
|
381
489
|
|
382
|
-
# create
|
383
|
-
# TODO: ensure there is not already a remote named duostack
|
490
|
+
# create, add remote
|
384
491
|
puts api_get('create_app', "app_name=#{name}")
|
385
|
-
`git remote add
|
386
|
-
puts "Git remote added, to push: 'git push
|
492
|
+
`git remote add #{@remote} git@duostack.net:#{name}.git 2>/dev/null`
|
493
|
+
puts "Git remote added, to push: 'git push #{@remote} master'"
|
387
494
|
end
|
388
495
|
|
389
496
|
|
@@ -420,6 +527,13 @@ module Duostack
|
|
420
527
|
|
421
528
|
|
422
529
|
def console
|
530
|
+
# TODO: just use ruby expect lib
|
531
|
+
|
532
|
+
# first check for expect dependency
|
533
|
+
if `which expect`.empty?
|
534
|
+
exit_with "missing dependency, please install Expect (http://expect.sourceforge.net/)"
|
535
|
+
end
|
536
|
+
|
423
537
|
exec("#{$dir}/.duostack-console-expect #{@app_name}")
|
424
538
|
end
|
425
539
|
|
@@ -454,104 +568,59 @@ module Duostack
|
|
454
568
|
|
455
569
|
|
456
570
|
def env
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
exit_with "'env add' requires an argument, try 'env add <name>=<value>'"
|
486
|
-
when 'remove', 'rm'
|
487
|
-
exit_with "'env #{command}' requires an argument, try 'env #{command} <name>'"
|
488
|
-
end
|
489
|
-
end
|
490
|
-
|
491
|
-
# clean up argument
|
492
|
-
argument = CGI::escape(argument)
|
493
|
-
end
|
494
|
-
|
495
|
-
# finally, process command
|
496
|
-
case command
|
497
|
-
when 'list', 'ls'
|
498
|
-
truncate = !@flags.include?('long')
|
499
|
-
print api_get("list_envs", "truncate=#{truncate}")
|
500
|
-
when 'add'
|
501
|
-
puts api_get("add_env", "input=#{argument}")
|
502
|
-
when 'remove', 'rm'
|
503
|
-
puts api_get("remove_env", "name=#{argument}")
|
504
|
-
when 'clear'
|
505
|
-
require_confirmation
|
506
|
-
puts api_get("clear_envs")
|
507
|
-
end
|
571
|
+
process_crud_command({
|
572
|
+
:resource => 'env',
|
573
|
+
:actions => {
|
574
|
+
:list => {
|
575
|
+
:additional_params => "truncate=#{!@flags.include?('long')}"
|
576
|
+
},
|
577
|
+
:add => {
|
578
|
+
:error_message => '<name>=<value>',
|
579
|
+
:param => 'input',
|
580
|
+
:args_processor => lambda { |args|
|
581
|
+
# recompose strings and quote values because ruby strips out quotation marks in the args.
|
582
|
+
args.collect do |arg|
|
583
|
+
if arg.include?('=')
|
584
|
+
result = arg.split('=',2)
|
585
|
+
%Q(#{result[0]}="#{result[1].gsub('"', '\"')}")
|
586
|
+
else
|
587
|
+
arg
|
588
|
+
end
|
589
|
+
end
|
590
|
+
}
|
591
|
+
},
|
592
|
+
:remove => {
|
593
|
+
:error_message => '<name>',
|
594
|
+
:param => 'name'
|
595
|
+
},
|
596
|
+
:clear => {}
|
597
|
+
}
|
598
|
+
})
|
508
599
|
end
|
509
600
|
|
510
601
|
|
511
602
|
def access
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
unless %w(add list ls).include?(command)
|
519
|
-
exit_with "invalid argument for 'access', try list or add"
|
520
|
-
end
|
521
|
-
|
522
|
-
# ensure we have an argument for add/remove commands which require it
|
523
|
-
if %w(add).include?(command)
|
524
|
-
# gather up and compose remaining args for add/remove operations
|
525
|
-
argument = @args.join(' ')
|
526
|
-
@args.clear # clean up, since we processed every remaining arg
|
527
|
-
|
528
|
-
# warn and exit unless we have an argument to pass
|
529
|
-
if argument.empty?
|
530
|
-
case command
|
531
|
-
when 'add'
|
532
|
-
exit_with "'access add' requires an argument, try 'access add <email>'"
|
533
|
-
end
|
534
|
-
end
|
535
|
-
|
536
|
-
# clean up argument
|
537
|
-
argument = CGI::escape(argument)
|
538
|
-
end
|
539
|
-
|
540
|
-
# finally, process command
|
541
|
-
case command
|
542
|
-
when 'list', 'ls'
|
543
|
-
print api_get("list_collabs")
|
544
|
-
when 'add'
|
545
|
-
puts api_get("add_collab", "emails=#{argument}")
|
546
|
-
end
|
603
|
+
process_crud_command({
|
604
|
+
:resource => 'collab',
|
605
|
+
:param => 'emails',
|
606
|
+
:error_message => '<email>',
|
607
|
+
:actions => %w(list add)
|
608
|
+
})
|
547
609
|
end
|
548
610
|
|
549
611
|
|
612
|
+
def domains
|
613
|
+
process_crud_command({
|
614
|
+
:resource => 'domain',
|
615
|
+
:actions => %w(list add remove)
|
616
|
+
})
|
617
|
+
end
|
618
|
+
|
550
619
|
|
551
620
|
module Help
|
552
621
|
|
553
|
-
# available help sections. first will be used as the default.
|
554
|
-
SECTIONS =
|
622
|
+
# available help sections. first will be used as the default, so unshift 'help' into first position.
|
623
|
+
SECTIONS = (COMMANDS[:general] + COMMANDS[:user] + COMMANDS[:app]).unshift('help')
|
555
624
|
|
556
625
|
class << self
|
557
626
|
|
@@ -569,33 +638,39 @@ module Duostack
|
|
569
638
|
def help
|
570
639
|
<<-EOF
|
571
640
|
|
572
|
-
Usage: #{$client} <command> [<args>] [--app <appname>]
|
641
|
+
Usage: #{$client} <command> [<args>] [--app <appname>] [--remote <remotename>]
|
573
642
|
|
574
643
|
App commands must be either run from an app folder (a Git repository with a
|
575
644
|
Duostack remote) or have the app specified with the "--app <appname>" flag. If
|
576
645
|
both are present, the "--app" flag takes precedence.
|
577
646
|
|
578
|
-
|
579
|
-
|
647
|
+
You can also use the "--remote" flag to set the app based on a specific Git
|
648
|
+
remote name. This is common when you run two Duostack Apps (e.g. staging and
|
649
|
+
production) from the same folder. This flag can also be used when running
|
650
|
+
"create" to have the client set up a custom remote name (default is "duostack").
|
651
|
+
|
652
|
+
The most common commands are listed below. For additional information on any of
|
653
|
+
them, run: #{$client} help <command>
|
580
654
|
|
581
655
|
General Commands:
|
582
|
-
help [<command>]
|
583
|
-
create <appname>
|
584
|
-
list
|
585
|
-
version
|
656
|
+
help [<command>] Show this help, or detailed help on <command>
|
657
|
+
create <appname> Initialize a Git repository as a Duostack App
|
658
|
+
list Show all apps associated with your account
|
659
|
+
version Show version of this Duostack client
|
586
660
|
|
587
661
|
App Commands:
|
588
|
-
logs
|
589
|
-
restart
|
590
|
-
ps
|
591
|
-
destroy
|
592
|
-
config [<name> [<setting>]]
|
593
|
-
env [<operation>]
|
594
|
-
access [<operation>]
|
662
|
+
logs Retrieve server logs
|
663
|
+
restart Restart instances
|
664
|
+
ps List instances with current status
|
665
|
+
destroy Destroy Duostack App and associated data
|
666
|
+
config [<name> [<setting>]] Show or set configuration options
|
667
|
+
env [<operation>] Manage environment variables
|
668
|
+
access [<operation>] Manage app collaborator access
|
669
|
+
domains [<operation>] Manage custom domains
|
595
670
|
|
596
671
|
App Commands - Ruby:
|
597
|
-
console
|
598
|
-
rake [<command>]
|
672
|
+
console Connect to IRB/Rails console
|
673
|
+
rake [<command>] Run a Rake command
|
599
674
|
|
600
675
|
EOF
|
601
676
|
end
|
@@ -603,18 +678,25 @@ module Duostack
|
|
603
678
|
def create
|
604
679
|
<<-EOF
|
605
680
|
|
606
|
-
Usage:
|
607
|
-
|
681
|
+
Usage:
|
682
|
+
#{$client} create <appname>
|
683
|
+
#{$client} create <appname> --remote <remotename>
|
684
|
+
|
685
|
+
Example:
|
686
|
+
#{$client} create myappname
|
687
|
+
#{$client} create myappname --remote staging
|
608
688
|
|
609
689
|
Arguments:
|
610
690
|
<appname> A name for your app (restrictions apply, see below)
|
691
|
+
<remotename> An optional name for the created Git remote
|
611
692
|
|
612
693
|
Creates a new Duostack App from the current directory with the given name.
|
613
694
|
|
614
695
|
The current directory must be initialized as a Git repository (run 'git init').
|
615
696
|
Upon running 'create', the app will be created on Duostack and a Git remote with
|
616
697
|
the name 'duostack' will be added to the local repository, which you can push to
|
617
|
-
to deploy your app: git push duostack master.
|
698
|
+
to deploy your app: git push duostack master. If a custom remote name is
|
699
|
+
specified with the '--remote' flag, that will be used instead.
|
618
700
|
|
619
701
|
The app name must be 4-16 characters long, alphanumeric, and must not start with
|
620
702
|
a number. The use of hyphens, underscores, or any other special characters is
|
@@ -767,8 +849,7 @@ module Duostack
|
|
767
849
|
#{$client} access lists app collaborators
|
768
850
|
#{$client} access add name@example.com grants access for name@example.com
|
769
851
|
|
770
|
-
Lists and adds app collaborator access for users (identified by
|
771
|
-
emails).
|
852
|
+
Lists and adds app collaborator access for users (identified by emails).
|
772
853
|
|
773
854
|
List: Lists collaborator emails who currently have access to the app. If none
|
774
855
|
are set, output will be blank.
|
@@ -779,6 +860,34 @@ module Duostack
|
|
779
860
|
EOF
|
780
861
|
end
|
781
862
|
|
863
|
+
def domains
|
864
|
+
<<-EOF
|
865
|
+
|
866
|
+
Usage:
|
867
|
+
#{$client} domains lists all custom domains
|
868
|
+
#{$client} domains add <domain> adds <domain> as a custom domain
|
869
|
+
#{$client} domains remove <domain> removes custom domain <domain>
|
870
|
+
|
871
|
+
Examples:
|
872
|
+
#{$client} domains lists all env vars
|
873
|
+
#{$client} domains add app.example.com adds app.example.com to app
|
874
|
+
#{$client} domains remove app.example.com removes app.example.com from app
|
875
|
+
|
876
|
+
Lists, adds, and removes custom domains
|
877
|
+
(http://docs.duostack.com/custom-domains).
|
878
|
+
|
879
|
+
List: Lists custom domains currently set on the app. If none are set, output
|
880
|
+
will be blank.
|
881
|
+
|
882
|
+
Add: Add one or more custom domains to the app. Add multiple by supplying
|
883
|
+
additional <domain> arguments separated by spaces.
|
884
|
+
|
885
|
+
Remove: Removes one or more custom domains from the app. Remove multiple by
|
886
|
+
supplying additional <domain> arguments separated by spaces.
|
887
|
+
|
888
|
+
EOF
|
889
|
+
end
|
890
|
+
|
782
891
|
def console
|
783
892
|
<<-EOF
|
784
893
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: duostack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Todd Eichel
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-02-
|
18
|
+
date: 2011-02-17 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -34,7 +34,7 @@ files:
|
|
34
34
|
- bin/bash/.duostack-console-expect
|
35
35
|
- bin/duostack
|
36
36
|
has_rdoc: true
|
37
|
-
homepage:
|
37
|
+
homepage: http://www.duostack.com/
|
38
38
|
licenses: []
|
39
39
|
|
40
40
|
post_install_message:
|