maestro 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/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
|
+
|