taskmapper 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.document +5 -0
  2. data/.travis.yml +4 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +52 -0
  5. data/LICENSE +20 -0
  6. data/NOTES +18 -0
  7. data/README.md +296 -0
  8. data/Rakefile +40 -0
  9. data/TODO +1 -0
  10. data/VERSION +1 -0
  11. data/bin/tm +7 -0
  12. data/examples/tm_example.rb +11 -0
  13. data/examples/tm_example_2.rb +11 -0
  14. data/examples/tm_example_3.rb +21 -0
  15. data/examples/tm_example_4.rb +17 -0
  16. data/lib/taskmapper.rb +53 -0
  17. data/lib/taskmapper/authenticator.rb +2 -0
  18. data/lib/taskmapper/cli/commands/config.rb +100 -0
  19. data/lib/taskmapper/cli/commands/console.rb +35 -0
  20. data/lib/taskmapper/cli/commands/generate.rb +112 -0
  21. data/lib/taskmapper/cli/commands/generate/provider.rb +5 -0
  22. data/lib/taskmapper/cli/commands/generate/provider/comment.rb +14 -0
  23. data/lib/taskmapper/cli/commands/generate/provider/project.rb +26 -0
  24. data/lib/taskmapper/cli/commands/generate/provider/provider.rb +25 -0
  25. data/lib/taskmapper/cli/commands/generate/provider/ticket.rb +12 -0
  26. data/lib/taskmapper/cli/commands/help.rb +9 -0
  27. data/lib/taskmapper/cli/commands/help/config +27 -0
  28. data/lib/taskmapper/cli/commands/help/console +13 -0
  29. data/lib/taskmapper/cli/commands/help/generate +19 -0
  30. data/lib/taskmapper/cli/commands/help/help +7 -0
  31. data/lib/taskmapper/cli/commands/help/project +13 -0
  32. data/lib/taskmapper/cli/commands/help/ticket +14 -0
  33. data/lib/taskmapper/cli/commands/project.rb +140 -0
  34. data/lib/taskmapper/cli/commands/ticket.rb +145 -0
  35. data/lib/taskmapper/cli/common.rb +28 -0
  36. data/lib/taskmapper/cli/init.rb +77 -0
  37. data/lib/taskmapper/comment.rb +97 -0
  38. data/lib/taskmapper/common.rb +81 -0
  39. data/lib/taskmapper/dummy/comment.rb +27 -0
  40. data/lib/taskmapper/dummy/dummy.rb +28 -0
  41. data/lib/taskmapper/dummy/project.rb +42 -0
  42. data/lib/taskmapper/dummy/ticket.rb +43 -0
  43. data/lib/taskmapper/exception.rb +2 -0
  44. data/lib/taskmapper/helper.rb +72 -0
  45. data/lib/taskmapper/project.rb +145 -0
  46. data/lib/taskmapper/provider.rb +82 -0
  47. data/lib/taskmapper/tester/comment.rb +18 -0
  48. data/lib/taskmapper/tester/project.rb +19 -0
  49. data/lib/taskmapper/tester/tester.rb +28 -0
  50. data/lib/taskmapper/tester/ticket.rb +19 -0
  51. data/lib/taskmapper/ticket.rb +154 -0
  52. data/spec/project_spec.rb +84 -0
  53. data/spec/rcov.opts +1 -0
  54. data/spec/spec.opts +1 -0
  55. data/spec/spec_helper.rb +9 -0
  56. data/spec/taskmapper-cli_spec.rb +60 -0
  57. data/spec/taskmapper-exception_spec.rb +160 -0
  58. data/spec/taskmapper_spec.rb +13 -0
  59. data/spec/ticket_spec.rb +56 -0
  60. data/taskmapper.gemspec +118 -0
  61. metadata +189 -0
@@ -0,0 +1,12 @@
1
+ module TaskMapper::Provider
2
+ module Yoursystem
3
+ # Ticket class for taskmapper-yoursystem
4
+ #
5
+
6
+ class Ticket < TaskMapper::Provider::Base::Ticket
7
+ #API = Yoursystem::Ticket # The class to access the api's tickets
8
+ # declare needed overloaded methods here
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # The help command.
2
+ def help(options)
3
+ cmd = ARGV.shift || 'help'
4
+ page = File.dirname(__FILE__) + '/help/' + cmd
5
+ if File.exist?(page)
6
+ puts File.read(page)
7
+ puts "\nFor parameter listing and details, try executing the command with --help.\n\ttm #{cmd} --help"
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ This command is used to configure taskmapper settings for use in your terminal.
2
+
3
+ By default, taskmapper searches for the config information in a file named 'taskmapper.yml' in the current directory. Next it searches for 'config/taskmapper.yml' and if that fails, it uses the home directory's ~/.taskmapper.yml (note the . in front on this one). There is also the TASKMAPPER environment variable that you can set to point to another configuration file anywhere if necessary.
4
+
5
+ This command helps facilitate the creation of a taskmapper.yml.
6
+
7
+ Warning: Due to the way the authentication is parsed, if any keys or values contain a comma (,) or colin (:), it can not be parsed through the -A command. Usually this isn't a problem, and if it is, it can be resolved though putting the values in a config file.
8
+
9
+ Example:
10
+ tm -c ~/.taskmapper.yml -p dummy -A username:cheese,password:cakes -P 555 config --add
11
+ tm -p dummy config --set-default-provider
12
+
13
+ The format for taskmapper.yml is:
14
+ default: <default provider>
15
+ <provider name>:
16
+ authentication:
17
+ <authentication information>
18
+ project: <project>
19
+ [...]
20
+
21
+ For example, a taskmapper.yml with the Dummy provider would look something like this:
22
+ default: dummy
23
+ dummy:
24
+ authentication:
25
+ username: name
26
+ password: doesnt-matter
27
+ project: 555
@@ -0,0 +1,13 @@
1
+ This command is used to open an irb session with the taskmapper.
2
+
3
+ The configuration for this command should mostly be dependent on the taskmapper.yml files.
4
+ See taskmapper help config for more information on how to set it up.
5
+
6
+ Example:
7
+ tm console
8
+ tm -p dummy console
9
+ tm -p dummy console -d --tracer
10
+
11
+ By default, it attempts to load all providers listed in the config unless a provider is explicitly given.
12
+
13
+ All options passed to console is passed to the irb session. See irb --help for those options and their details
@@ -0,0 +1,19 @@
1
+ This command is used to generate a new provider.
2
+
3
+ It generates some basic files to get you started on creating your own provider.
4
+
5
+ NOTE: This command, in an attempt to keep provider names consistent, will prepend 'taskmapper-' to the given provider name. You can cancel this by prepending a _ before your provider name, which will be removed if found.
6
+
7
+ If you have not created a gem directory or skeleton and want to use Jeweler (http://github.com/technicalpickles/jeweler) you can execute:
8
+
9
+ $ tm generate myprovider --jeweler [JEWELER ARGS]
10
+
11
+ And it will create your whole directory and skeleton using jeweler.
12
+
13
+ If you like the old "classic" gem creation process or are using some other gem processor, you can create the directory to store your files, cd into it and run this command to put the skeleton files inside the directory's lib/.
14
+
15
+ Example:
16
+ mkdir myprovider myprovider/lib
17
+ cd myprovider
18
+ [...]
19
+ tm generate myprovider
@@ -0,0 +1,7 @@
1
+ usage: tm help COMMAND
2
+
3
+ This is the help command. You can use this command to get more information on other commands.
4
+
5
+ Example:
6
+ tm help config
7
+ tm help console
@@ -0,0 +1,13 @@
1
+ This command is used to work with projects.
2
+
3
+ It can be used to do any of the CRUD actions with projects that are provided by the provider.
4
+
5
+ It will attempt to load data through the config files if they are available. See 'tm help config' for more information.
6
+
7
+ Examples:
8
+ tm -p lighthouse -A account:taskmapper,token:abc project --create name "new project"
9
+ tm project --read 946
10
+ tm --project 946 project --read
11
+ tm -p dummy -A "user:common coder,pass:w3rd out" project --destroy 712
12
+ tm -p dummy -P 712 project --destroy
13
+ tm -p dummy project --update name "new project name" description "this is the project description"
@@ -0,0 +1,14 @@
1
+ This command is used to work with tickets.
2
+
3
+ It can be used to do any of the CRUD actions with projects that are provided by the provider.
4
+
5
+ It will attempt to load data through the config files if they are available. See 'tm help config' for more information.
6
+
7
+ If any of the keys or values contain a space, you will have to enclose that key or value in quotes. For example, if you set name to ProjectName it does not need to be quoted, but if you set name to Project Name it will have to be quoted to "Project Name" or 'Project Name'
8
+
9
+ Examples:
10
+ tm -p lighthouse -A account:taskmapper,token:abc ticket --create name "new ticket" description "this is a new ticket"
11
+ tm --project 946 ticket --read --ticket 2
12
+ tm -p dummy -A "user:common coder,pass: w3rd out" ticket --destroy --ticket 12
13
+ tm -p dummy -P 712 ticket --destroy --ticket 4
14
+ tm -p dummy project --ticket 6 --update attribute value name "free dummies"
@@ -0,0 +1,140 @@
1
+ require 'rubygems'
2
+ # The command method call for project
3
+ # This sets the option parser and passes the parsed options to the subcommands
4
+ def project(options)
5
+ ARGV << '--help' if ARGV.length == 0
6
+ begin
7
+ OptionParser.new do |opts|
8
+ opts.banner = 'Usage: tm -p PROVIDER [options] project [project_options]'
9
+ opts.separator ''
10
+ opts.separator 'Options:'
11
+
12
+ opts.on('-C', '--create ATTRIBUTES', 'Create a new project') do |attribute|
13
+ options[:project_attributes] = {attribute => ARGV.shift}.merge(attributes_hash(ARGV))
14
+ options[:subcommand] = 'create'
15
+ end
16
+
17
+ opts.on('-R', '--read [PROJECT]', 'Read out project and its attributes') do |id|
18
+ options[:project] = id if id
19
+ options[:subcommand] = 'read'
20
+ end
21
+
22
+ opts.on('-U', '--update ATTRIBUTES', 'Update project information') do |attribute|
23
+ options[:project_attributes] = {attribute => ARGV.shift}.merge(attributes_hash(ARGV))
24
+ options[:subcommand] = 'update'
25
+ end
26
+
27
+ opts.on('-D', '--destroy [PROJECT]', 'Destroy the project. Not reversible!') do |id|
28
+ options[:project] = id if id
29
+ options[:subcommand] = 'destroy'
30
+ end
31
+
32
+ opts.on('-I', '--info [PROJECT_ID]', 'Get project info. Same as --read. ') do |id|
33
+ options[:project] = id if id
34
+ options[:subcommand] = 'read'
35
+ end
36
+
37
+ opts.on('-S', '--search [ATTRIBUTES]', 'Search for a project based on attributes') do |attribute|
38
+ options[:project_attributes] = attribute ? {attribute => ARGV.shift}.merge(attributes_hash(ARGV)) : {}
39
+ options[:subcommand] = 'search'
40
+ end
41
+
42
+ opts.on('-L', '--list-all', 'List all projects. Same as --search without any parameters') do
43
+ options[:project_attributes] = {}
44
+ options[:subcommand] = 'search'
45
+ end
46
+
47
+ opts.on('-P', '--project [PROJECT_ID]', 'Set the project id') do |id|
48
+ options[:project] = id
49
+ end
50
+
51
+ opts.separator ''
52
+ opts.separator 'Other options:'
53
+
54
+ opts.on_tail('-h', '--help', 'Show this message') do
55
+ puts opts
56
+ exit
57
+ end
58
+ end.order!
59
+ rescue OptionParser::MissingArgument => exception
60
+ puts "tm #{options[:original_argv].join(' ')}\n\n"
61
+ puts "Error: An option was called that requires an argument, but was not given one"
62
+ puts exception.message
63
+ end
64
+ parse_config!(options)
65
+ begin
66
+ require 'taskmapper'
67
+ require "taskmapper-#{options[:provider]}"
68
+ rescue
69
+ require options[:provider]
70
+ end
71
+ send(options[:subcommand], options)
72
+ end
73
+
74
+
75
+ # The create subcommand
76
+ def create(options)
77
+ tm = TaskMapper.new(options[:provider], options[:authentication])
78
+ project = tm.project.create(options[:project_attributes])
79
+ read_project project
80
+ exit
81
+ end
82
+
83
+ # The read subcommand
84
+ def read(options)
85
+ tm = TaskMapper.new(options[:provider], options[:authentication])
86
+ project = tm.project.find(options[:project])
87
+ read_project project
88
+ exit
89
+ end
90
+
91
+ # The update subcommand
92
+ def update(options)
93
+ tm = TaskMapper.new(options[:provider], options[:authentication])
94
+ project = tm.project.find(options[:project])
95
+ if project.update!(options[:project_attributes])
96
+ puts "Successfully updated Project #{project.name} (#{project.id})"
97
+ else
98
+ puts "Sorry, it seems there was an error when trying to update the attributes"
99
+ end
100
+ read_project project
101
+ exit
102
+ end
103
+
104
+ # The destroy subcommand.
105
+ def destroy(options)
106
+ tm = TaskMapper.new(options[:provider], options[:authentication])
107
+ project = tm.project.find(options[:project])
108
+ puts "Are you sure you want to delete Project #{project.name} (#{project.id})? (yes/no) [no]"
109
+ ARGV.clear
110
+ confirm = readline.chomp.downcase
111
+ if confirm != 'y' and confirm != 'yes'
112
+ puts "Did not receive a 'yes' confirmation. Exiting..."
113
+ exit
114
+ elsif project.destroy
115
+ puts "Successfully deleted Project #{project.name} (#{project.id})"
116
+ else
117
+ puts "Sorry, it seems there was an error when trying to delete the project"
118
+ end
119
+ exit
120
+ end
121
+
122
+ # The search and list subcommands
123
+ def search(options)
124
+ tm = TaskMapper.new(options[:provider], options[:authentication])
125
+ projects = tm.projects(options[:project_attributes])
126
+ puts "Found #{projects.length} projects"
127
+ projects.each_with_index do |project, index|
128
+ puts "#{index+1}) Project #{project.name} (#{project.id})"
129
+ #read_project project
130
+ #puts
131
+ end
132
+ exit
133
+ end
134
+
135
+ # A utility method used to output project attributes
136
+ def read_project(project)
137
+ project.system_data[:client].attributes.sort.each do |key, value|
138
+ puts "#{key} : #{value}"
139
+ end
140
+ end
@@ -0,0 +1,145 @@
1
+ require 'rubygems'
2
+ # The command method call for project
3
+ # This sets the option parser and passes the parsed options to the subcommands
4
+ def ticket(options)
5
+ ARGV << '--help' if ARGV.length == 0
6
+ begin
7
+ OptionParser.new do |opts|
8
+ opts.banner = 'Usage: tm -p PROVIDER -P PROJECT [options] ticket [ticket_options]'
9
+ opts.separator ''
10
+ opts.separator 'Options:'
11
+
12
+ opts.on('-T', '--ticket TICKET', 'Sets the working ticket') do |id|
13
+ options[:ticket] = id
14
+ end
15
+ opts.on('-C', '--create ATTRIBUTES', 'Create a new ticket') do |attribute|
16
+ options[:ticket_attributes] = {attribute => ARGV.shift}.merge(attributes_hash(ARGV))
17
+ options[:subcommand] = 'create'
18
+ end
19
+
20
+ opts.on('-R', '--read [TICKET]', 'Read out ticket and its attributes. Requires --ticket to be set') do |id|
21
+ options[:ticket] = id if id
22
+ options[:subcommand] = 'read'
23
+ end
24
+
25
+ opts.on('-U', '--update ATTRIBUTES', 'Update ticket information. Requires --ticket to be set') do |attribute|
26
+ options[:ticket_attributes] = {attribute => ARGV.shift}.merge(attributes_hash(ARGV))
27
+ options[:subcommand] = 'update'
28
+ end
29
+
30
+ opts.on('-D', '--destroy', 'Destroy/Delete the ticket. Not reversible! Requires --ticket to be set') do
31
+ options[:subcommand] = 'destroy'
32
+ end
33
+
34
+ opts.on('-I', '--info', 'Get ticket info. Same as --read. ') do
35
+ options[:subcommand] = 'read'
36
+ end
37
+
38
+ opts.on('-S', '--search [ATTRIBUTES]', 'Search for a ticket based on attributes') do |attribute|
39
+ options[:ticket_attributes] = attribute ? {attribute => ARGV.shift}.merge(attributes_hash(ARGV)) : {}
40
+ options[:subcommand] = 'search'
41
+ end
42
+
43
+ opts.on('-L', '--list-all', 'List all tickets. Same as --search without any parameters') do
44
+ options[:ticket_attributes] = {}
45
+ options[:subcommand] = 'search'
46
+ end
47
+
48
+ opts.on('-P', '--project [PROJECT_ID]', 'Set the project id') do |id|
49
+ options[:project] = id
50
+ end
51
+
52
+ opts.separator ''
53
+ opts.separator 'Other options:'
54
+
55
+ opts.on_tail('-h', '--help', 'Show this message') do
56
+ puts opts
57
+ exit
58
+ end
59
+ end.order!
60
+ rescue OptionParser::MissingArgument => exception
61
+ puts "tm #{options[:original_argv].join(' ')}\n\n"
62
+ puts "Error: An option was called that requires an argument, but was not given one"
63
+ puts exception.message
64
+ end
65
+ parse_config!(options)
66
+ begin
67
+ require 'taskmapper'
68
+ require "taskmapper-#{options[:provider]}"
69
+ rescue
70
+ require options[:provider]
71
+ end
72
+ send(options[:subcommand], options)
73
+ end
74
+
75
+
76
+ # The create subcommand
77
+ def create(options)
78
+ tm = TaskMapper.new(options[:provider], options[:authentication])
79
+ ticket = tm.ticket.create(options[:ticket_attributes].merge({:project_id => options[:project]}))
80
+ read_ticket ticket
81
+ exit
82
+ end
83
+
84
+ # The read subcommand
85
+ def read(options)
86
+ tm = TaskMapper.new(options[:provider], options[:authentication])
87
+ project = tm.project(options[:project])
88
+ ticket = project.ticket(options[:ticket])
89
+ read_ticket ticket
90
+ exit
91
+ end
92
+
93
+ # The update subcommand
94
+ def update(options)
95
+ tm = TaskMapper.new(options[:provider], options[:authentication])
96
+ project = tm.project(options[:project])
97
+ ticket = project.ticket(options[:ticket])
98
+ if ticket.update!(options[:ticket_attributes])
99
+ puts "Successfully updated Ticket #{ticket.title} (#{ticket.id})"
100
+ else
101
+ puts "Sorry, it seems there was an error when trying to update the attributes"
102
+ end
103
+ read_ticket ticket
104
+ exit
105
+ end
106
+
107
+ # The destroy subcommand.
108
+ def destroy(options)
109
+ tm = TaskMapper.new(options[:provider], options[:authentication])
110
+ project = tm.project(options[:project])
111
+ ticket = project.ticket(options[:ticket])
112
+ puts "Are you sure you want to delete Ticket #{ticket.title} (#{ticket.id})? (yes/no) [no]"
113
+ ARGV.clear
114
+ confirm = readline.chomp.downcase
115
+ if confirm != 'y' and confirm != 'yes'
116
+ puts "Did not receive a 'yes' confirmation. Exiting..."
117
+ exit
118
+ elsif ticket.destroy
119
+ puts "Successfully deleted Ticket #{ticket.title} (#{ticket.id})"
120
+ else
121
+ puts "Sorry, it seems there was an error when trying to delete the project"
122
+ end
123
+ exit
124
+ end
125
+
126
+ # The search and list subcommands
127
+ def search(options)
128
+ tm = TaskMapper.new(options[:provider], options[:authentication])
129
+ project = tm.project(options[:project])
130
+ tickets = project.tickets(options[:ticket_attributes])
131
+ puts "Found #{tickets.length} tickets"
132
+ tickets.each_with_index do |ticket, index|
133
+ puts "#{index+1}) Ticket #{ticket.title} (#{ticket.id})"
134
+ #read_ticket ticket
135
+ #puts
136
+ end
137
+ exit
138
+ end
139
+
140
+ # A utility method used to output project attributes
141
+ def read_ticket(ticket)
142
+ ticket.system_data[:client].attributes.sort.each do |key, value|
143
+ puts "#{key} : #{value}"
144
+ end
145
+ end
@@ -0,0 +1,28 @@
1
+ # Parses the configuration information and puts it into options
2
+ def parse_config!(options)
3
+ config = YAML.load_file File.expand_path(options[:config])
4
+ provider = (options[:provider] ||= config['default'] || config.keys.first)
5
+ if provider and provider.length > 0
6
+ options[:project] ||= config[provider]['project']
7
+ options[:authentication] ||= config[provider]['authentication']
8
+ end
9
+ options
10
+ end
11
+
12
+ # A utility method used to separate name:value pairs
13
+ def attributes_hash(kvlist)
14
+ require 'enumerator' if RUBY_VERSION < "1.8.7"
15
+ if kvlist.is_a?(String)
16
+ kvlist.split(',').inject({}) do |mem, kv|
17
+ key, value = kv.split(':')
18
+ mem[key] = value
19
+ mem
20
+ end
21
+ elsif kvlist.is_a?(Array)
22
+ mem = {}
23
+ kvlist.each_slice(2) do |k, v|
24
+ mem[k] = v
25
+ end
26
+ mem
27
+ end
28
+ end
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'yaml'
5
+ require File.dirname(__FILE__) + '/common.rb'
6
+
7
+
8
+ commands ={ 'help' => 'Get the help text for a particular command',
9
+ 'console' => 'Open up a taskmapper console session',
10
+ 'config' => 'Setup and configure a taskmapper.yml file',
11
+ 'ticket' => 'Work with tickets (create, edit, delete, etc)',
12
+ 'project' => 'Work with projects (create, edit, delete, etc)',
13
+ 'generate' => 'Generate skeleton library files for a new provider',
14
+ }
15
+
16
+
17
+
18
+ helptext = lambda {
19
+ helpmsg = "\nAvailable commands:\n"
20
+ commands.sort.inject(helpmsg) { |mem, cmd| mem << "\t#{cmd.join(" \t")}\n" }
21
+ helpmsg << "\nSee 'tm help COMMAND' for more information on a specific command."
22
+ }
23
+
24
+ ARGV << '--help' if ARGV.length == 0
25
+
26
+ options = {:original_argv => ARGV.dup}
27
+
28
+ if File.exist?(options[:config] = File.expand_path('taskmapper.yml'))
29
+ elsif File.exist?(options[:config] = File.expand_path('config/taskmapper.yml'))
30
+ elsif ENV['TASKMAPPER_CONFIG'] and File.exist?(options[:config] = File.expand_path(ENV['TASKMAPPER_CONFIG']))
31
+ else
32
+ options[:config] = File.expand_path('~/.taskmapper.yml')
33
+ end
34
+
35
+ begin
36
+ OptionParser.new do |opts|
37
+ opts.banner = 'Usage: tm [options] COMMAND [command_options]'
38
+ opts.separator ''
39
+ opts.separator 'Options:'
40
+
41
+ opts.on('-c', '--config CONFIG', 'Use CONFIG as the configuration file. default: ~/.taskmapper.yml') do |c|
42
+ options[:config] = c
43
+ end
44
+
45
+ opts.on('-p', '--provider PROVIDER', 'Specifies the provider') { |p| options[:provider] = p }
46
+
47
+ opts.on('-A', '--authentication AUTH',
48
+ 'Specifies authentication information, comma-separated list of name:value pairs.',
49
+ 'Note: The whole list must be enclosed in quotes if there are any spaces.') do |a|
50
+ options[:authentication] = attributes_hash(a)
51
+ end
52
+
53
+ opts.on('-P', '--project PROJECT', 'Specifies the working project') { |p| options[:project] = p }
54
+
55
+ opts.separator ''
56
+ opts.separator 'Other options:'
57
+
58
+ opts.on_tail('-h', '--help', 'Show this message') do
59
+ puts opts
60
+ puts helptext.call
61
+ exit
62
+ end
63
+ end.order!
64
+ rescue OptionParser::MissingArgument => exception
65
+ puts "tm #{ARGV.join(' ')}\n\n"
66
+ puts "Error: An option was called that requires an argument, but was not given one"
67
+ puts exception.message
68
+ end
69
+
70
+ command = ARGV.shift
71
+ if commands[command]
72
+ require File.dirname(__FILE__) + '/commands/' + command
73
+ send(command, options)
74
+ else
75
+ puts "'#{command}' is not a taskmapper command\n\n", helptext.call
76
+ exit
77
+ end