maestro 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/maestro +4 -0
- data/lib/cli.rb +75 -0
- data/lib/commands/command.rb +33 -0
- data/lib/commands/project_commands.rb +26 -0
- data/lib/commands/story_commands.rb +97 -0
- data/lib/dispatcher.rb +24 -0
- data/lib/maestro.rb +18 -0
- data/lib/resources/project.rb +5 -0
- data/lib/resources/story.rb +17 -0
- data/maestro.gemspec +17 -0
- metadata +74 -0
data/bin/maestro
ADDED
data/lib/cli.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'dispatcher'
|
2
|
+
require 'commands/command'
|
3
|
+
|
4
|
+
module Maestro
|
5
|
+
class CLI
|
6
|
+
class << self
|
7
|
+
def run
|
8
|
+
parse_options
|
9
|
+
read_config
|
10
|
+
|
11
|
+
ActiveResource::Base.logger = Logger.new($stdout) if Config[:verbose]
|
12
|
+
abort("API Key is required") unless Config[:api_key]
|
13
|
+
|
14
|
+
[ Story, Project ].each do |resource|
|
15
|
+
resource.headers['X-APIKey'] = Config[:api_key]
|
16
|
+
end
|
17
|
+
|
18
|
+
puts Maestro::Dispatcher.invoke(ARGV.shift, ARGV)
|
19
|
+
rescue Maestro::Command::RuntimeError => e
|
20
|
+
puts e.message
|
21
|
+
rescue Maestro::Command::ArgumentError => e
|
22
|
+
puts e.message
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def read_config
|
28
|
+
maestro = nil
|
29
|
+
if File.exist?('.maestro.yml')
|
30
|
+
verbose "Reading options from .maestro.yml"
|
31
|
+
maestro = YAML::load(File.read('.maestro.yml'))
|
32
|
+
|
33
|
+
Config[:project_id] = maestro['project_id']
|
34
|
+
else
|
35
|
+
verbose "No options file .maestro.yml"
|
36
|
+
end
|
37
|
+
verbose "Using project ID #{Config[:project_id]}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_options
|
41
|
+
option_parser = OptionParser.new do |opts|
|
42
|
+
opts.banner = "Usage: maestro [options] <command> [arguments]"
|
43
|
+
opts.on("-a", "--api-key=abc123", "Specify an API key to use for access (default: ENV['MAESTRO_API_KEY'])") { |o| Config[:api_key] = o }
|
44
|
+
opts.on("-p", "--project=13", "Specify project ID (default: read from ./.maestro.yml)") { |o| Config[:project_id] = o }
|
45
|
+
opts.on("-v", "--[no-]verbose", "Verbose output") { |o| Config[:verbose] = o }
|
46
|
+
|
47
|
+
opts.on_tail("--version", "Show version") do
|
48
|
+
puts 'Maestro CLI: 0.0.1'
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
53
|
+
puts opts
|
54
|
+
puts ""
|
55
|
+
Dispatcher.commands.each do |name, command|
|
56
|
+
puts command.describe
|
57
|
+
puts ""
|
58
|
+
end
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
begin
|
64
|
+
option_parser.parse!
|
65
|
+
rescue OptionParser::ParseError
|
66
|
+
abort("Error: #{$!}\noption_parser")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def verbose(message)
|
71
|
+
puts messages if Config[:verbose]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Maestro
|
2
|
+
class Command
|
3
|
+
# to differentiate between where the error originated, we only want to
|
4
|
+
# rescue from ones that are raised from a Maestro::Command
|
5
|
+
class ArgumentError < ::ArgumentError;end;
|
6
|
+
class RuntimeError < ::RuntimeError;end;
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :description
|
11
|
+
attr_reader :arguments
|
12
|
+
|
13
|
+
def command(description=nil, arguments=nil)
|
14
|
+
@name = self.to_s.split("::").last.downcase.to_sym
|
15
|
+
@description = description || ''
|
16
|
+
@arguments = arguments || {}
|
17
|
+
|
18
|
+
Maestro::Dispatcher.register self
|
19
|
+
end
|
20
|
+
|
21
|
+
def describe
|
22
|
+
"#{name} #{arguments.collect{ |a, d| "<#{a}>"}}\n\t#{description}\n\t#{arguments.collect{ |a, desc| "<#{a}> - #{desc}" }.join(", ")}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(*args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def invoke
|
30
|
+
raise RuntimeError.new("#{self.class} has no invocation!")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Maestro::Command::Info < Maestro::Command
|
2
|
+
command "Show info about the project"
|
3
|
+
|
4
|
+
def invoke
|
5
|
+
project = Maestro::Project.find(Maestro::Config[:project_id], :params => { :context => 'user' })
|
6
|
+
[ "#{project.name}", "#{project.email}" ].join("\n")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Maestro::Command::Init < Maestro::Command
|
11
|
+
command "Initialize the given project in the current directory", { "project" => "Name of project to initialize" }
|
12
|
+
|
13
|
+
def initialize(project)
|
14
|
+
@project = project
|
15
|
+
end
|
16
|
+
|
17
|
+
def invoke
|
18
|
+
project = Maestro::Project.find(@project, :params => { :context => 'user' })
|
19
|
+
File.open(".maestro.yml", "w") do |io|
|
20
|
+
io.puts "project_id: #{project.id}"
|
21
|
+
end
|
22
|
+
Dir.pwd + "/.maestro.yml written successfully"
|
23
|
+
rescue ActiveResource::ResourceNotFound
|
24
|
+
raise RuntimeError.new("Could not find project \"#{@project}\". Please double-check the name on Maestro.")
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
class Maestro::Command::List < Maestro::Command
|
2
|
+
command "List project stories"
|
3
|
+
|
4
|
+
def invoke
|
5
|
+
Maestro::Story.find(:all, :params => { :context => 'user', :project_id => Maestro::Config[:project_id] }).collect do |story|
|
6
|
+
story.to_s
|
7
|
+
end.join("\n")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Maestro::Command::Find < Maestro::Command
|
12
|
+
command "Find stories matching a regular expression", { 'query' => "Regular expression to use" }
|
13
|
+
|
14
|
+
def initialize(query)
|
15
|
+
@query = query
|
16
|
+
end
|
17
|
+
|
18
|
+
def invoke
|
19
|
+
stories = Maestro::Story.find(:all, :params => { :context => 'user', :project_id => Maestro::Config[:project_id] })
|
20
|
+
stories.select{ |s| s.title =~ Regexp.new(@query) }.collect do |story|
|
21
|
+
story.to_s
|
22
|
+
end.join("\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Maestro::Command::StoryUpdater < Maestro::Command
|
27
|
+
private
|
28
|
+
|
29
|
+
def update(attribute, value)
|
30
|
+
story = Maestro::Story.find(@id, :params => { :context => 'user', :project_id => Maestro::Config[:project_id] })
|
31
|
+
story.update_attributes(attribute => value)
|
32
|
+
story
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Maestro::Command::Time < Maestro::Command::StoryUpdater
|
37
|
+
command "Update the actual time on the story", { 'time' => "Time spent working on story (HH:MM)" }
|
38
|
+
|
39
|
+
def initialize(id, time)
|
40
|
+
@id = id
|
41
|
+
@time = time
|
42
|
+
end
|
43
|
+
|
44
|
+
def invoke
|
45
|
+
hours, minutes = @time.split(":")
|
46
|
+
story = update :time, (hours.to_i * 60 + (minutes.to_i || 0))
|
47
|
+
story.to_s + " time: #{(story.time / 60).to_i}:#{(story.time % 60).to_i}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Maestro::Command::StateUpdater < Maestro::Command::StoryUpdater
|
52
|
+
class << self;attr_reader :state;end;
|
53
|
+
|
54
|
+
def initialize(id)
|
55
|
+
@id = id
|
56
|
+
end
|
57
|
+
|
58
|
+
def invoke
|
59
|
+
update_state
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def update_state
|
65
|
+
update :state, self.class.state
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Maestro::Command::Start < Maestro::Command::StateUpdater
|
70
|
+
command "Start the story", { 'id' => "ID of story to start" }
|
71
|
+
@state = 'started'
|
72
|
+
end
|
73
|
+
|
74
|
+
class Maestro::Command::Finish < Maestro::Command::StateUpdater
|
75
|
+
command "Finish the story", { 'id' => "ID of story to finish" }
|
76
|
+
@state = 'finished'
|
77
|
+
end
|
78
|
+
|
79
|
+
class Maestro::Command::Deliver < Maestro::Command::StateUpdater
|
80
|
+
command "Deliver the story", { 'id' => "ID of story to deliver" }
|
81
|
+
@state = 'delivered'
|
82
|
+
end
|
83
|
+
|
84
|
+
class Maestro::Command::Accept < Maestro::Command::StateUpdater
|
85
|
+
command "Accept the story", { 'id' => "ID of story to accept" }
|
86
|
+
@state = 'accepted'
|
87
|
+
end
|
88
|
+
|
89
|
+
class Maestro::Command::Reject < Maestro::Command::StateUpdater
|
90
|
+
command "Reject the story", { 'id' => "ID of story to reject" }
|
91
|
+
@state = 'rejected'
|
92
|
+
end
|
93
|
+
|
94
|
+
class Maestro::Command::Cancel < Maestro::Command::StateUpdater
|
95
|
+
command "Cancel the story", { 'id' => "ID of story to cancel" }
|
96
|
+
@state = 'cancelled'
|
97
|
+
end
|
data/lib/dispatcher.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Maestro
|
2
|
+
class Dispatcher
|
3
|
+
cattr_reader :commands
|
4
|
+
@@commands = {}
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def register(command)
|
8
|
+
commands[command.name] = command
|
9
|
+
end
|
10
|
+
|
11
|
+
def invoke(name, args)
|
12
|
+
return false unless commands.has_key?(name.to_sym)
|
13
|
+
commands[name.to_sym].new(*args).invoke
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Dir.glob(File.dirname(__FILE__) + '/commands/*.rb').each do |requirement|
|
20
|
+
require requirement
|
21
|
+
end
|
22
|
+
Dir.glob(File.dirname(__FILE__) + '/resources/*.rb').each do |requirement|
|
23
|
+
require requirement
|
24
|
+
end
|
data/lib/maestro.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'logger'
|
4
|
+
require 'activeresource'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
$: << File.dirname(__FILE__)
|
8
|
+
|
9
|
+
module Maestro
|
10
|
+
Config = {
|
11
|
+
:api_key => ENV['MAESTRO_API_KEY'],
|
12
|
+
:project_id => nil,
|
13
|
+
:verbose => false,
|
14
|
+
:host => 'http://maestro.citrusbyte.com'
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'cli'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Maestro
|
2
|
+
class Story < ActiveResource::Base
|
3
|
+
self.site = Maestro::Config[:host] + "/:context/projects/:project_id"
|
4
|
+
|
5
|
+
def update_attributes(attributes={})
|
6
|
+
prefix_options[:context] = 'writer'
|
7
|
+
attributes.each do |attribute, value|
|
8
|
+
send "#{attribute}=", value
|
9
|
+
end
|
10
|
+
save
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#{id}\t#{title} (#{state})" + (owner_id ? " (#{self.owner_id})" : "")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/maestro.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "maestro"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.summary = "A command line interface for Maestro project management."
|
5
|
+
s.description = "A command line interface for Maestro project management."
|
6
|
+
s.authors = ["Ben Alavi"]
|
7
|
+
s.email = ["ben.alavi@citrusbyte.com"]
|
8
|
+
s.homepage = "http://maestro.citrusbyte.com/"
|
9
|
+
|
10
|
+
s.rubyforge_project = "maestro"
|
11
|
+
|
12
|
+
s.executables << "maestro"
|
13
|
+
|
14
|
+
s.add_dependency("activeresource")
|
15
|
+
|
16
|
+
s.files = ["bin/maestro", "lib/cli.rb", "lib/commands/command.rb", "lib/commands/project_commands.rb", "lib/commands/story_commands.rb", "lib/dispatcher.rb", "lib/maestro.rb", "lib/resources/project.rb", "lib/resources/story.rb", "maestro.gemspec"]
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: maestro
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Alavi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-01 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activeresource
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: A command line interface for Maestro project management.
|
26
|
+
email:
|
27
|
+
- ben.alavi@citrusbyte.com
|
28
|
+
executables:
|
29
|
+
- maestro
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files: []
|
33
|
+
|
34
|
+
files:
|
35
|
+
- bin/maestro
|
36
|
+
- lib/cli.rb
|
37
|
+
- lib/commands/command.rb
|
38
|
+
- lib/commands/project_commands.rb
|
39
|
+
- lib/commands/story_commands.rb
|
40
|
+
- lib/dispatcher.rb
|
41
|
+
- lib/maestro.rb
|
42
|
+
- lib/resources/project.rb
|
43
|
+
- lib/resources/story.rb
|
44
|
+
- maestro.gemspec
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://maestro.citrusbyte.com/
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project: maestro
|
69
|
+
rubygems_version: 1.3.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: A command line interface for Maestro project management.
|
73
|
+
test_files: []
|
74
|
+
|