maestro 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'maestro'
4
+ Maestro::CLI.run
@@ -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
@@ -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
@@ -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,5 @@
1
+ module Maestro
2
+ class Project < ActiveResource::Base
3
+ self.site = Maestro::Config[:host] + "/:context"
4
+ end
5
+ end
@@ -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
@@ -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
+