├── .gitignore ├── .npmignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE-MIT ├── README.md ├── docs ├── data-utils.html ├── docco.css ├── jira-cli.html ├── jira.html └── pretty-printer.html ├── lib ├── data-utils.js ├── jira-cli.js ├── jira.js └── pretty-printer.js ├── package.json ├── spec └── jira-cli.spec.coffee └── src ├── data-utils.coffee ├── jira-cli.coffee ├── jira.coffee └── pretty-printer.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.9" 4 | - "0.8" 5 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | jasmine_node: 4 | projectRoot: "." 5 | requirejs: false 6 | forceExit: true 7 | extensions: 'coffee' 8 | coffee: 9 | compile: 10 | expand: true 11 | flatten: true 12 | cwd: '.' 13 | src: ['src/*.coffee'] 14 | dest: 'lib/' 15 | ext: '.js' 16 | docco: 17 | compile: 18 | src: ['src/*.coffee'] 19 | options: 20 | output: 'docs/' 21 | coffeelint: 22 | app: ['src/*.coffee', 'Gruntfile.coffee'] 23 | options: 24 | indentation: 25 | value: 4 26 | concat: 27 | options: 28 | stripBanners: true 29 | banner: '#!/usr/bin/env node\n' 30 | dist: 31 | src: ['lib/jira.js'] 32 | dest: 'lib/jira.js' 33 | 34 | 35 | grunt.loadNpmTasks 'grunt-jasmine-node' 36 | grunt.loadNpmTasks 'grunt-contrib-coffee' 37 | grunt.loadNpmTasks 'grunt-docco' 38 | grunt.loadNpmTasks 'grunt-bump' 39 | grunt.loadNpmTasks 'grunt-coffeelint' 40 | grunt.loadNpmTasks 'grunt-contrib-concat' 41 | 42 | grunt.registerTask 'default', 43 | ['coffeelint', 'coffee', 'jasmine_node', 'concat'] 44 | grunt.registerTask 'test', 45 | ['coffeelint', 'coffee', 'jasmine_node'] 46 | grunt.registerTask 'prepare', 47 | ['coffeelint', 'coffee', 'jasmine_node', 'docco', 'concat', 'bump'] 48 | grunt.registerTask 'force', 49 | ['coffeelint', 'coffee', 'jasmine_node', 'docco', 'concat'] 50 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Chris Moultrie 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is abandoned. The NPM module is likely to be changed. The maintainer of https://github.com/danshumaker/jira-cmd approached me asking for the name within npm, and I've granted access. So there will be a whole new package in npm unrelated to this project. 2 | 3 | # jira-cli 4 | 5 | [![Build Status](https://travis-ci.org/tebriel/jira-cli.png?branch=master)](https://travis-ci.org/tebriel/jira-cli) 6 | 7 | This is a command line client for jira, because no one likes their terrible 8 | interface. 9 | 10 | ## Getting Started 11 | 12 | * Install the module with: `npm install -g jira-cli` 13 | * Run it with `jira` 14 | 15 | ## What does it do? 16 | 17 | * Lists all a user's issues 18 | * List all a user's projects 19 | * Finds an issue by Key (AB-123) or Id (123456) 20 | * Opens an issue 21 | * Allows user to add a new ticket to different projects 22 | * Transitions an issue (shows all available transition states) 23 | * Adds a worklog to an issue 24 | * Allow searching to be limited by project id 25 | 26 | ## TODO 27 | 28 | * PROFIT? 29 | * MOAR testing 30 | 31 | ## Documentation ## 32 | 33 | [GitHub Documentation](http://tebriel.github.com/jira-cli/) 34 | 35 | ## Examples ## 36 | 37 | `jira -l` 38 | 39 | `jira -f AB-123` 40 | 41 | ## Notes ## 42 | 43 | If you use `https:` for jira, add `"protocol": "https:"` to your .jiraclirc.json 44 | If your ssl certs are also self-signed add: `"strictSSL": false` to your .jiraclirc.json 45 | 46 | ## Testing ## 47 | 48 | Using jasmine-node with grunt currently. Tests are a bit silly, but they helped 49 | me learn jasmine spies. So that's good. 50 | 51 | ## Contributing 52 | 53 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [grunt](https://github.com/gruntjs/grunt). 54 | 55 | 56 | ## Release History 57 | 58 | * _0.5.0 Changing list (`-l`) to to use the query `resolution = unresolved` (Thanks 59 | to [zowens](https://github.com/zowens))_ 60 | * _0.4.1 Accidentally quitting before callback was finished_ 61 | * _0.4.0 Now able to ignore self-signed SSL Certs and specify protocol_ 62 | * _0.3.1 Fixed some bugs_ 63 | * _0.3.0 Updated version of jira_ 64 | * _0.2.9 Fixed an issue where special characters were in the username_ 65 | * _0.2.8 Fixed issue with create that would prevent you from creating_ 66 | * _0.2.7 Fixed typo that prevented -w from working_ 67 | * _0.2.6 Now takes -o to limit to specific project(s)_ 68 | * _0.2.5 Now normalizing event types and item types_ 69 | * _0.2.4 I did something here, don't remember_ 70 | * _0.2.3 Fixed an issue where invalid input caused an exception_ 71 | * _0.2.2 Added wordrap to -d so that the text is easier to grok_ 72 | * _0.2.1 Added -d flag to show details for list/find_ 73 | * _0.2.0 Refactored organization. Creates config file if not present_ 74 | * _0.1.9 Defaults for project in config, lists others if desired_ 75 | * _0.1.8 Now allows entry of worklog when transitioning items, or by itself_ 76 | * _0.1.7 Now requiring my custom npm module for node-jira-devel_ 77 | * _0.1.6 Transitioning now shows all available options_ 78 | * _0.1.5 Listing Id for project_ 79 | * _0.1.4 Listing Types in Create_ 80 | * _0.1.3 Listing Projects_ 81 | * _0.1.2 Moar Minor Doc Changes_ 82 | * _0.1.1 Minor Doc Changes_ 83 | * _0.1.0 Initial Release_ 84 | 85 | ## License 86 | 87 | Copyright (c) 2012 Chris Moultrie 88 | Licensed under the MIT license. 89 | -------------------------------------------------------------------------------- /docs/data-utils.html: -------------------------------------------------------------------------------- 1 | data-utils.coffee
Jump To …

data-utils.coffee

Ask the user a question

2 | 3 |

This is great, stole it from 4 | St. On It

5 | 6 |

Re-formatted it to be in coffeescript

7 | 8 |

Takes

9 | 10 |
    11 |
  • question: (text for the user)
  • 12 |
  • format: RegExp which determines if the input was valid
  • 13 |
  • callback: for when we have proper input
  • 14 |
  • range: integer array that specifies allowed input
  • 15 |
ask = (question, format, callback, range) ->
16 |     stdin = process.stdin
17 |     stdout = process.stdout
18 |  
19 |     stdin.resume()
20 |     stdout.write(question + ": ")
21 |  
22 |     stdin.once 'data', (data) ->
23 |         data = data.toString().trim()
24 |  
25 |         if range?
26 |             if parseInt(data) in range
27 |                 callback data
28 |                 return
29 |         else if format.test data
30 |             callback data
31 |             return
32 | 
33 |         stdout.write("It should match: " + format + "\n")
34 |         ask(question, format, callback, range)

Item Sorter

35 | 36 |

Function for JS .Sort() which sorts items by id in ascending order

itemSorter = (a, b)->
37 |     first = parseInt a.id
38 |     second = parseInt b.id
39 |     return -1 if first < second
40 |     return 0 if first is second
41 |     return 1 if first > second
42 | 
43 | module.exports = {
44 |     ask
45 |     itemSorter
46 | }
47 | 
48 | 
-------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | body { 3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 4 | font-size: 15px; 5 | line-height: 22px; 6 | color: #252519; 7 | margin: 0; padding: 0; 8 | } 9 | a { 10 | color: #261a3b; 11 | } 12 | a:visited { 13 | color: #261a3b; 14 | } 15 | p { 16 | margin: 0 0 15px 0; 17 | } 18 | h1, h2, h3, h4, h5, h6 { 19 | margin: 0px 0 15px 0; 20 | } 21 | h1 { 22 | margin-top: 40px; 23 | } 24 | hr { 25 | border: 0 none; 26 | border-top: 1px solid #e5e5ee; 27 | height: 1px; 28 | margin: 20px 0; 29 | } 30 | #container { 31 | position: relative; 32 | } 33 | #background { 34 | position: fixed; 35 | top: 0; left: 525px; right: 0; bottom: 0; 36 | background: #f5f5ff; 37 | border-left: 1px solid #e5e5ee; 38 | z-index: -1; 39 | } 40 | #jump_to, #jump_page { 41 | background: white; 42 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 43 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 44 | font: 10px Arial; 45 | text-transform: uppercase; 46 | cursor: pointer; 47 | text-align: right; 48 | } 49 | #jump_to, #jump_wrapper { 50 | position: fixed; 51 | right: 0; top: 0; 52 | padding: 5px 10px; 53 | } 54 | #jump_wrapper { 55 | padding: 0; 56 | display: none; 57 | } 58 | #jump_to:hover #jump_wrapper { 59 | display: block; 60 | } 61 | #jump_page { 62 | padding: 5px 0 3px; 63 | margin: 0 0 25px 25px; 64 | } 65 | #jump_page .source { 66 | display: block; 67 | padding: 5px 10px; 68 | text-decoration: none; 69 | border-top: 1px solid #eee; 70 | } 71 | #jump_page .source:hover { 72 | background: #f5f5ff; 73 | } 74 | #jump_page .source:first-child { 75 | } 76 | table td { 77 | border: 0; 78 | outline: 0; 79 | } 80 | td.docs, th.docs { 81 | max-width: 450px; 82 | min-width: 450px; 83 | min-height: 5px; 84 | padding: 10px 25px 1px 50px; 85 | overflow-x: hidden; 86 | vertical-align: top; 87 | text-align: left; 88 | } 89 | .docs pre { 90 | margin: 15px 0 15px; 91 | padding-left: 15px; 92 | } 93 | .docs p tt, .docs p code { 94 | background: #f8f8ff; 95 | border: 1px solid #dedede; 96 | font-size: 12px; 97 | padding: 0 0.2em; 98 | } 99 | .pilwrap { 100 | position: relative; 101 | } 102 | .pilcrow { 103 | font: 12px Arial; 104 | text-decoration: none; 105 | color: #454545; 106 | position: absolute; 107 | top: 3px; left: -20px; 108 | padding: 1px 2px; 109 | opacity: 0; 110 | -webkit-transition: opacity 0.2s linear; 111 | } 112 | td.docs:hover .pilcrow { 113 | opacity: 1; 114 | } 115 | td.code, th.code { 116 | padding: 14px 15px 16px 25px; 117 | width: 100%; 118 | vertical-align: top; 119 | background: #f5f5ff; 120 | border-left: 1px solid #e5e5ee; 121 | } 122 | pre, tt, code { 123 | font-size: 12px; line-height: 18px; 124 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 125 | margin: 0; padding: 0; 126 | } 127 | 128 | 129 | /*---------------------- Syntax Highlighting -----------------------------*/ 130 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 131 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 132 | body .hll { background-color: #ffffcc } 133 | body .c { color: #408080; font-style: italic } /* Comment */ 134 | body .err { border: 1px solid #FF0000 } /* Error */ 135 | body .k { color: #954121 } /* Keyword */ 136 | body .o { color: #666666 } /* Operator */ 137 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 138 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 139 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 140 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 141 | body .gd { color: #A00000 } /* Generic.Deleted */ 142 | body .ge { font-style: italic } /* Generic.Emph */ 143 | body .gr { color: #FF0000 } /* Generic.Error */ 144 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 145 | body .gi { color: #00A000 } /* Generic.Inserted */ 146 | body .go { color: #808080 } /* Generic.Output */ 147 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 148 | body .gs { font-weight: bold } /* Generic.Strong */ 149 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 150 | body .gt { color: #0040D0 } /* Generic.Traceback */ 151 | body .kc { color: #954121 } /* Keyword.Constant */ 152 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ 153 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ 154 | body .kp { color: #954121 } /* Keyword.Pseudo */ 155 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 156 | body .kt { color: #B00040 } /* Keyword.Type */ 157 | body .m { color: #666666 } /* Literal.Number */ 158 | body .s { color: #219161 } /* Literal.String */ 159 | body .na { color: #7D9029 } /* Name.Attribute */ 160 | body .nb { color: #954121 } /* Name.Builtin */ 161 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 162 | body .no { color: #880000 } /* Name.Constant */ 163 | body .nd { color: #AA22FF } /* Name.Decorator */ 164 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 165 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 166 | body .nf { color: #0000FF } /* Name.Function */ 167 | body .nl { color: #A0A000 } /* Name.Label */ 168 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 169 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 170 | body .nv { color: #19469D } /* Name.Variable */ 171 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 172 | body .w { color: #bbbbbb } /* Text.Whitespace */ 173 | body .mf { color: #666666 } /* Literal.Number.Float */ 174 | body .mh { color: #666666 } /* Literal.Number.Hex */ 175 | body .mi { color: #666666 } /* Literal.Number.Integer */ 176 | body .mo { color: #666666 } /* Literal.Number.Oct */ 177 | body .sb { color: #219161 } /* Literal.String.Backtick */ 178 | body .sc { color: #219161 } /* Literal.String.Char */ 179 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 180 | body .s2 { color: #219161 } /* Literal.String.Double */ 181 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 182 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 183 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 184 | body .sx { color: #954121 } /* Literal.String.Other */ 185 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 186 | body .s1 { color: #219161 } /* Literal.String.Single */ 187 | body .ss { color: #19469D } /* Literal.String.Symbol */ 188 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 189 | body .vc { color: #19469D } /* Name.Variable.Class */ 190 | body .vg { color: #19469D } /* Name.Variable.Global */ 191 | body .vi { color: #19469D } /* Name.Variable.Instance */ 192 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/jira-cli.html: -------------------------------------------------------------------------------- 1 | jira-cli.coffee
Jump To …

jira-cli.coffee

Because colors are pretty

color = require('ansi-color').set

PrettyPrinter Sourc/Doc

PrettyPrinter = require('./pretty-printer').PrettyPrinter
  2 | JiraApi = require('jira').JiraApi
  3 | 
  4 | 
  5 | class Logger
  6 |     error: (text) ->
  7 |         @log text, "red"
  8 |     log: (text, textColor) ->
  9 |         unless textColor?
 10 |             textColor = 'white'
 11 |         console.log color(text, textColor)

JiraHelper

12 | 13 |

This does the fancy talking to JiraApi for us. It formats the objects the way 14 | that Jira expects them to come in. Basically a wrapper for node-jira-devel

class JiraHelper

Constructor

15 | 16 |

Builds a new JiraCli with the config settings

    constructor: (@config)->
 17 |         unless @config.strictSSL?
 18 |             @config.strictSSL = true
 19 |         unless @config.protocol?
 20 |             @config.protocol = 'http:'
 21 | 
 22 |         @jira = new JiraApi(@config.protocol, @config.host,
 23 |             @config.port, @config.user, @config.password, '2',
 24 |             false, @config.strictSSL)
 25 |         @response = null
 26 |         @error = null
 27 |         @pp = new PrettyPrinter
 28 |         @log = new Logger
 29 | 
 30 |     dieWithFire: ->
 31 |         process.exit()

Get Issue

32 | 33 |

Searches Jira for the issue number requested 34 | this can be either a key AB-123 or just the number 123456

    getIssue: (issueNum, details)->
 35 |         @jira.findIssue issueNum, (error, response) =>
 36 |             if response?
 37 |                 @response = response
 38 |                 @pp.prettyPrintIssue response, details
 39 |             else
 40 |                 @error = error if error?
 41 |                 @log.error "Error finding issue: #{error}"

Get Issue Types

42 | 43 |

Gets a list of all the available issue types

    getIssueTypes: (callback)->
 44 |         @jira.listIssueTypes (error, response) =>
 45 |             if response?
 46 |                 callback response
 47 |             else
 48 |                 @log.error "Error listing issueTypes: #{error}"
 49 |                 @dieWithFire()
 50 | 
 51 |     createIssueObject: (project, summary, issueType, description) ->
 52 |         fields:
 53 |             project: { id:project }
 54 |             summary: summary
 55 |             issuetype: { id:issueType }
 56 |             assignee: { name:@config.user }
 57 |             description: description

Add Issue

58 | 59 |

Takes

60 | 61 |
    62 |
  • summary: details for the title of the issue
  • 63 |
  • description: more detailed than summary
  • 64 |
  • issue type: Id of the type (types are like bug, feature)
  • 65 |
  • project: this is the id of the project that you're assigning the issue 66 | to
  • 67 |
    addIssue: (summary, description, issueType, project) ->
 68 |         newIssue = @createIssueObject project, summary, issueType, description
 69 | 
 70 |         @jira.addNewIssue newIssue, (error, response) =>
 71 |             if response?
 72 |                 @response = response if response?
 73 |                 @log.log "Issue #{response.key} has " +
 74 |                     "been #{color("created", "green")}"
 75 |             else

The error object is non-standard here from Jira, I'll parse 76 | it better later

                @error = error if error?
 77 |                 @log.error "Error creating issue: #{JSON.stringify(error)}"
 78 | 
 79 |             @dieWithFire()

Delete an Issue

80 | 81 |

Deletes an issue (if you have permissions) from Jira. I haven't tested 82 | this successfully because I don't have permissions.

    deleteIssue: (issueNum)->

Don't have permissions currently

        @jira.deleteIssue issueNum, (error, response) =>
 83 |             if response?
 84 |                 @response = response
 85 |                 @log.log "Issue #{issueNum} was #{color("deleted", "green")}"
 86 |             else
 87 |                 @error = error if error?
 88 |                 @log.error "Error deleting issue: #{error}"

Add Worklog Item

89 | 90 |

Adds a simple worklog to an issue

    addWorklog: (issueId, comment, timeSpent, exit)->
 91 |         worklog =
 92 |             comment:comment
 93 |             timeSpent:timeSpent
 94 |         @jira.addWorklog issueId, worklog, (error, response)=>
 95 |             if response?
 96 |                 @log.log "Worklog was #{color("added", "green")}"
 97 |             else
 98 |                 @error = error if error?
 99 |                 @log.error "Error adding worklog: #{error}"
100 |             @dieWithFire() if exit

List Transitions

101 | 102 |

List the transitions available for an issue

    listTransitions: (issueNum, callback) ->
103 |         @jira.listTransitions issueNum, (error, transitions)=>
104 |             if transitions?
105 |                 callback transitions
106 |             else
107 |                 @log.error "Error getting transitions: #{error}"
108 |                 @dieWithFire()

Transition Issue

109 | 110 |

Transitions an issue in Jira

111 | 112 |

Takes

113 | 114 |
    115 |
  • issueNum: the Id of the issue (either the AB-123 or the 123456)
  • 116 |
  • transitionNum: this is the id of the transition to apply to the issue
  • 117 |
    transitionIssue: (issueNum, transitionNum)->
118 |         issueUpdate =
119 |             transition:
120 |                 id:transitionNum
121 |         @jira.transitionIssue issueNum, issueUpdate, (error, response) =>
122 |             if response?
123 |                 @response = response
124 |                 @log.log "Issue #{issueNum} " +
125 |                     "was #{color("transitioned", "green")}"
126 |             else
127 |                 @error = error if error?
128 |                 @log.error "Error transitioning issue: #{error}"
129 | 
130 |             @dieWithFire()

Search Jira

131 | 132 |

Passes a jql formatted query to jira for search

133 | 134 |

Takes

135 | 136 |
    137 |
  • searchQuery: a jql formatted search query string 138 | shows all otherwise
  • 139 |
    searchJira: (searchQuery, details)->
140 |         fields = ["summary", "status", "assignee"]
141 |         @jira.searchJira searchQuery, fields, (error, issueList) =>
142 |             if issueList?
143 |                 @myIssues = issueList
144 |                 for issue in issueList.issues
145 |                     @pp.prettyPrintIssue issue, details
146 |             else
147 |                 @error = error if error?
148 |                 @log.error "Error retreiving issues list: #{error}"

Get My Issues

149 | 150 |

Gets a list of issues for the user listed in the config

151 | 152 |

Takes

153 | 154 |
    155 |
  • open: boolean which indicates if only open items should be shown, 156 | shows all otherwise
  • 157 |
    getMyIssues: (open, details, projects)->
158 |         jql = "assignee = \"#{@config.user}\""
159 |         if open
160 |             jql += ' AND resolution = unresolved'
161 |         jql += projects if projects?
162 | 
163 |         @searchJira jql, details
164 |         return

List all Projects

165 | 166 |

This lists all the projects viewable with your account

    getMyProjects: (callback)->
167 |         @jira.listProjects (error, projectList) =>
168 |             if projectList?
169 |                 callback projectList
170 |             else
171 |                 @log.error "Error listing projects: #{error}"
172 |                 @dieWithFire()
173 | 
174 | 
175 | module.exports = {
176 |     JiraHelper
177 | }
178 | 
179 | 
-------------------------------------------------------------------------------- /docs/jira.html: -------------------------------------------------------------------------------- 1 | jira.coffee
Jump To …

jira.coffee

Jira Command Line Client

2 | 3 |

This client depends on you having a json file in your home directory 4 | named '.jiraclirc.json' it must contain:

5 | 6 |
{
  7 |     "user": "USERNAME",
  8 |     "password":"PASSWORD",
  9 |     "host":"www.jira.com",
 10 |     "port":80,
 11 |     "project": 10100
 12 | }
 13 | 
14 | 15 |

If not present, it will enter an interactive mode to create it with you

16 | 17 |

JiraCli is on github

fs = require 'fs'
 18 | path = require 'path'

JiraHelper docs/source

JiraHelper = require('./jira-cli').JiraHelper

dutils docs/source

dutils = require('./data-utils')

Create Config File

19 | 20 |

Creates a config file when one doesn't exist

createConfigFile = (aConfigFile) ->
 21 |     console.log "No config file found, answer these questions to create one!"
 22 |     dutils.ask "Username", /.+/, (username) ->
 23 |         dutils.ask "Password", /.+/, (password) ->
 24 |             dutils.ask "Jira Host", /.+/, (host) ->
 25 |                 dutils.ask "Jira Port", /.+/, (port) ->
 26 |                     dutils.ask "Default Project", /.*/, (project) ->
 27 |                         config =
 28 |                             user:username
 29 |                             password:password
 30 |                             host:host
 31 |                             port:port
 32 |                             project:project
 33 | 
 34 |                         fs.writeFileSync aConfigFile,
 35 |                             JSON.stringify(config), 'utf8'
 36 |                         console.log "File created and saved as #{aConfigFile}"
 37 |                         process.exit()

Check for Text Parameter

38 | 39 |

Optimist returns a bool if the param is given but with nothing following it

paramIsText = (param)->
 40 |     if typeof(param) is "boolean"
 41 |         argv.showHelp()
 42 |         return false
 43 |     true

Load the Config File

loadConfigFile = (configFilePath) ->
 44 |     configFile = fs.readFileSync configFilePath
 45 | 
 46 |     JSON.parse configFile

Transition Item

47 | 48 |

This takes the issueId, lists the transitions available for the item and then 49 | lets the user apply that transition to the item. Optionally the user can 50 | specify a comment which will then prompt for time spent. This adds a work log 51 | item to the item before the transition.

transitionItem = (issueId) ->
 52 |     jiraCli.listTransitions issueId, (transitions) ->
 53 |         transitions.sort dutils.itemSorter
 54 |         for transition, index in transitions
 55 |             jiraCli.pp.prettyPrintTransition transition, index + 1
 56 |         allowedTypes = [1..transitions.length]

allowedTypes = new RegExp "[#{allowedTypes.join '|'}]"

        dutils.ask "Transtion Type ", allowedTypes, (type)->
 57 |             dutils.ask "Comment for worklog (blank to skip)", /.*/, (comment)->
 58 |                 if comment.length is 0
 59 |                     jiraCli.transitionIssue issueId, transitions[type - 1].id
 60 |                     return
 61 |                 dutils.ask "Time Spent (for worklog)", /.+/, (timeSpent)->
 62 |                     jiraCli.addWorklog issueId, comment, timeSpent, false
 63 |                     jiraCli.transitionIssue issueId, transitions[type - 1].id
 64 |         , allowedTypes

Add Work Log

65 | 66 |

This will add a comment and time spent as a worklog item attached to the 67 | issue

addWorklog = (issueId) ->
 68 |     dutils.ask "Comment for worklog", /.+/, (comment)->
 69 |         dutils.ask "Time Spent (for worklog)", /.+/, (timeSpent)->
 70 |             jiraCli.addWorklog issueId, comment, timeSpent, true

List Projects

71 | 72 |

This will list all the projects available to you

listProjects = ->
 73 |     projects = jiraCli.getMyProjects (projects)=>
 74 |         for project in projects
 75 |             jiraCli.pp.prettyPrintProject project

Get Project

76 | 77 |

Here we ask the user for their project, giving them an option for the 78 | default, ? for a list, or they can type in a number directly

79 | 80 |

It calls itself if we list the projects, so that it can still be used to for 81 | what it was called to do

getProject = (callback, defaultProj)->
 82 |     dutils.ask "Project (Enter for Default/? for list) [#{defaultProj}] ",/.*/,
 83 |         (project) ->
 84 |             unless project is '?'
 85 |                 callback configFile.project
 86 |                 return
 87 |             projects = jiraCli.getMyProjects (projects)=>
 88 |                 for project in projects
 89 |                     jiraCli.pp.prettyPrintProject project
 90 |                 getProject callback, defaultProj

Add Item

91 | 92 |

Adds an item to Jira. The project passed in comes from getProject currently. 93 | Takes a summary and a description then lists the issue types for the user to 94 | choose from

addItem = (project)->

Gather the summary, description, an type

    dutils.ask "Summary", /.+/, (summary)->
 95 |         dutils.ask "Description", /.+/, (description)->
 96 |             jiraCli.getIssueTypes (issueTypes)->
 97 |                 issueTypes.sort dutils.itemSorter
 98 |                 for type, index in issueTypes
 99 |                     jiraCli.pp.prettyPrintIssueTypes type, index + 1
100 |                     
101 |                 allowedTypes = [1..issueTypes.length]
102 |                 addIssueCallback = (type)->
103 |                     jiraCli.addIssue summary, description,
104 |                         issueTypes[type - 1].id, project
105 |                 dutils.ask "Type ", allowedTypes, addIssueCallback, allowedTypes

Main entry point

106 | 107 |

Parses the arguments and then calls a function above

if require.main is module
108 |     argv = (require 'optimist')
109 |         .options('f',
110 |             alias:'find'
111 |             describe:'Finds the specified Jira ID'
112 |         ).options('a',
113 |             alias:'add'
114 |             describe:'Allows you to add a new Jira Task'
115 |         ).options('t',
116 |             alias:'transition'
117 |             describe:'Allows you to resolve a specific Jira ID'
118 |         ).options('l',
119 |             alias:'list'
120 |             describe:'Lists all your open issues'
121 |         ).options('c',
122 |             alias:'list-all'
123 |             describe:'Lists all your issues'
124 |         ).options('d',
125 |             alias:'details'
126 |             describe:'Shows extra details (currently only for list)'
127 |         ).options('p',
128 |             alias:'projects'
129 |             describe:'Lists all your viewable projects'
130 |         ).options('o',
131 |             describe:'Limits list to only this project'
132 |         ).options('w',
133 |             alias:'worklog'
134 |             describe:'Adds work to your task'
135 |         ).options('s',
136 |             alias:'search'
137 |             describe:'Pass a jql string to jira'
138 |         ).options('h',
139 |             alias:'help'
140 |             describe:'Shows this help message'
141 |         ).usage('Usage:\n\tjira -f EG-143\n\tjira -r EG-143')
142 |         .boolean('d')
143 |         .string('s')
144 |         .string('f')
145 |         .string('t')
146 |         .string('w')
147 | 
148 |     if argv.argv.help
149 |         argv.showHelp()
150 |         return
151 |     args = argv.argv
152 | 
153 |     configFilePath = path.join process.env.HOME, '.jiraclirc.json'
154 |     unless fs.existsSync configFilePath
155 |         createConfigFile configFilePath
156 |         return
157 | 
158 |     configFile = loadConfigFile(configFilePath)
159 |     jiraCli = new JiraHelper configFile
160 | 
161 |     if args.o?
162 |         if args.o instanceof Array
163 |             args.o = args.o.join ','
164 |         args.o = " AND project in (#{args.o})"
165 | 
166 |     if args.l
167 |         jiraCli.getMyIssues true, args.d, args.o
168 |     else if args.c
169 |         jiraCli.getMyIssues false, args.d, args.o
170 |     else if args.s
171 |         return unless paramIsText args.s
172 |         if args.o?
173 |             args.s += args.o
174 |         jiraCli.searchJira args.s, args.d
175 |     else if args.p
176 |         listProjects()
177 |     else if args.a
178 |         getProject addItem, configFile.project
179 |     else if args.f?
180 |         return unless paramIsText args.f
181 |         jiraCli.getIssue args.f, args.d
182 |     else if args.w?
183 |         return unless paramIsText args.w
184 |         addWorklog args.w
185 |     else if args.t?
186 |         return unless paramIsText args.t
187 |         transitionItem args.t
188 |     else
189 |         argv.showHelp()
190 | 
191 | 
-------------------------------------------------------------------------------- /docs/pretty-printer.html: -------------------------------------------------------------------------------- 1 | pretty-printer.coffee
Jump To …

pretty-printer.coffee

color = require('ansi-color').set
 2 | wrap = require('wordwrap')(5, 65)
 3 | 
 4 | class PrettyPrinter

Because I like colors, and I don't want to format them any more than this 5 | TODO: Don't hardcode 5 and 6 anymore

    prettyPrintIssue: (issue, detail)->
 6 |         sumColor = "green"
 7 |         sumColor = "red" if +issue.fields.status.id in [5,6]
 8 |         process.stdout.write color(issue.key, sumColor + "+bold")

I don't think this could happen, but maybe....

        issue.fields.summary = "None" unless issue.fields.summary?
 9 |         process.stdout.write " - "
10 |         process.stdout.write issue.fields.summary
11 |         process.stdout.write "\n"
12 |         if detail and issue.fields.description?
13 |             process.stdout.write color("Description:\n", "white+bold")
14 |             process.stdout.write wrap(issue.fields.description)
15 |             process.stdout.write "\n\n"

Do some fancy formatting on issue types

    prettyPrintIssueTypes: (issueType, index)->
16 |         process.stdout.write color(index, "white+bold")
17 |         process.stdout.write " - "
18 |         process.stdout.write issueType.name
19 |         if issueType.description.length > 0
20 |             process.stdout.write " - "
21 |             process.stdout.write issueType.description
22 |         process.stdout.write "\n"

Pretty Print Transition

23 | 24 |

Show a transition with the ID in bold followed by the name

    prettyPrintTransition: (transition, index) ->
25 |         process.stdout.write color(index, "white+bold")
26 |         process.stdout.write " - "
27 |         process.stdout.write transition.name
28 |         process.stdout.write "\n"

Pretty Print Projects

29 | 30 |

Prints the project list in a non-awful format

    prettyPrintProject: (project) ->
31 |         key = project.key
32 |         while key.length < 12
33 |             key = ' ' + key
34 |         process.stdout.write color(key, "white+bold")
35 |         process.stdout.write " - "
36 |         process.stdout.write project.id
37 |         process.stdout.write " - "
38 |         process.stdout.write project.name
39 |         process.stdout.write "\n"
40 | 
41 | module.exports = {
42 |     PrettyPrinter
43 | }
44 | 
45 | 
-------------------------------------------------------------------------------- /lib/data-utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var ask, itemSorter, 3 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 4 | 5 | ask = function(question, format, callback, range) { 6 | var stdin, stdout; 7 | stdin = process.stdin; 8 | stdout = process.stdout; 9 | stdin.resume(); 10 | stdout.write(question + ": "); 11 | return stdin.once('data', function(data) { 12 | var _ref; 13 | data = data.toString().trim(); 14 | if (range != null) { 15 | if (_ref = parseInt(data), __indexOf.call(range, _ref) >= 0) { 16 | callback(data); 17 | return; 18 | } 19 | } else if (format.test(data)) { 20 | callback(data); 21 | return; 22 | } 23 | stdout.write("It should match: " + format + "\n"); 24 | return ask(question, format, callback, range); 25 | }); 26 | }; 27 | 28 | itemSorter = function(a, b) { 29 | var first, second; 30 | first = parseInt(a.id); 31 | second = parseInt(b.id); 32 | if (first < second) { 33 | return -1; 34 | } 35 | if (first === second) { 36 | return 0; 37 | } 38 | if (first > second) { 39 | return 1; 40 | } 41 | }; 42 | 43 | module.exports = { 44 | ask: ask, 45 | itemSorter: itemSorter 46 | }; 47 | 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /lib/jira-cli.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var JiraApi, JiraHelper, Logger, PrettyPrinter, color; 3 | 4 | color = require('ansi-color').set; 5 | 6 | PrettyPrinter = require('./pretty-printer').PrettyPrinter; 7 | 8 | JiraApi = require('jira').JiraApi; 9 | 10 | Logger = (function() { 11 | function Logger() {} 12 | 13 | Logger.prototype.error = function(text) { 14 | return this.log(text, "red"); 15 | }; 16 | 17 | Logger.prototype.log = function(text, textColor) { 18 | if (textColor == null) { 19 | textColor = 'white'; 20 | } 21 | return console.log(color(text, textColor)); 22 | }; 23 | 24 | return Logger; 25 | 26 | })(); 27 | 28 | JiraHelper = (function() { 29 | function JiraHelper(config) { 30 | this.config = config; 31 | if (this.config.strictSSL == null) { 32 | this.config.strictSSL = true; 33 | } 34 | if (this.config.protocol == null) { 35 | this.config.protocol = 'http:'; 36 | } 37 | this.jira = new JiraApi(this.config.protocol, this.config.host, this.config.port, this.config.user, this.config.password, '2', false, this.config.strictSSL); 38 | this.response = null; 39 | this.error = null; 40 | this.pp = new PrettyPrinter; 41 | this.log = new Logger; 42 | } 43 | 44 | JiraHelper.prototype.dieWithFire = function() { 45 | return process.exit(); 46 | }; 47 | 48 | JiraHelper.prototype.getIssue = function(issueNum, details) { 49 | var _this = this; 50 | return this.jira.findIssue(issueNum, function(error, response) { 51 | if (response != null) { 52 | _this.response = response; 53 | return _this.pp.prettyPrintIssue(response, details); 54 | } else { 55 | if (error != null) { 56 | _this.error = error; 57 | } 58 | return _this.log.error("Error finding issue: " + error); 59 | } 60 | }); 61 | }; 62 | 63 | JiraHelper.prototype.getIssueTypes = function(callback) { 64 | var _this = this; 65 | return this.jira.listIssueTypes(function(error, response) { 66 | if (response != null) { 67 | return callback(response); 68 | } else { 69 | _this.log.error("Error listing issueTypes: " + error); 70 | return _this.dieWithFire(); 71 | } 72 | }); 73 | }; 74 | 75 | JiraHelper.prototype.createIssueObject = function(project, summary, issueType, description) { 76 | return { 77 | fields: { 78 | project: { 79 | id: project 80 | }, 81 | summary: summary, 82 | issuetype: { 83 | id: issueType 84 | }, 85 | assignee: { 86 | name: this.config.user 87 | }, 88 | description: description 89 | } 90 | }; 91 | }; 92 | 93 | JiraHelper.prototype.addIssue = function(summary, description, issueType, project) { 94 | var newIssue, 95 | _this = this; 96 | newIssue = this.createIssueObject(project, summary, issueType, description); 97 | return this.jira.addNewIssue(newIssue, function(error, response) { 98 | if (response != null) { 99 | if (response != null) { 100 | _this.response = response; 101 | } 102 | _this.log.log(("Issue " + response.key + " has ") + ("been " + (color("created", "green")))); 103 | } else { 104 | if (error != null) { 105 | _this.error = error; 106 | } 107 | _this.log.error("Error creating issue: " + (JSON.stringify(error))); 108 | } 109 | return _this.dieWithFire(); 110 | }); 111 | }; 112 | 113 | JiraHelper.prototype.deleteIssue = function(issueNum) { 114 | var _this = this; 115 | return this.jira.deleteIssue(issueNum, function(error, response) { 116 | if (response != null) { 117 | _this.response = response; 118 | return _this.log.log("Issue " + issueNum + " was " + (color("deleted", "green"))); 119 | } else { 120 | if (error != null) { 121 | _this.error = error; 122 | } 123 | return _this.log.error("Error deleting issue: " + error); 124 | } 125 | }); 126 | }; 127 | 128 | JiraHelper.prototype.addWorklog = function(issueId, comment, timeSpent, exit) { 129 | var worklog, 130 | _this = this; 131 | worklog = { 132 | comment: comment, 133 | timeSpent: timeSpent 134 | }; 135 | return this.jira.addWorklog(issueId, worklog, function(error, response) { 136 | if (response != null) { 137 | _this.log.log("Worklog was " + (color("added", "green"))); 138 | } else { 139 | if (error != null) { 140 | _this.error = error; 141 | } 142 | _this.log.error("Error adding worklog: " + error); 143 | } 144 | if (exit) { 145 | return _this.dieWithFire(); 146 | } 147 | }); 148 | }; 149 | 150 | JiraHelper.prototype.listTransitions = function(issueNum, callback) { 151 | var _this = this; 152 | return this.jira.listTransitions(issueNum, function(error, transitions) { 153 | if (transitions != null) { 154 | return callback(transitions); 155 | } else { 156 | _this.log.error("Error getting transitions: " + error); 157 | return _this.dieWithFire(); 158 | } 159 | }); 160 | }; 161 | 162 | JiraHelper.prototype.transitionIssue = function(issueNum, transitionNum) { 163 | var issueUpdate, 164 | _this = this; 165 | issueUpdate = { 166 | transition: { 167 | id: transitionNum 168 | } 169 | }; 170 | return this.jira.transitionIssue(issueNum, issueUpdate, function(error, response) { 171 | if (response != null) { 172 | _this.response = response; 173 | _this.log.log(("Issue " + issueNum + " ") + ("was " + (color("transitioned", "green")))); 174 | } else { 175 | if (error != null) { 176 | _this.error = error; 177 | } 178 | _this.log.error("Error transitioning issue: " + error); 179 | } 180 | return _this.dieWithFire(); 181 | }); 182 | }; 183 | 184 | JiraHelper.prototype.searchJira = function(searchQuery, details) { 185 | var fields, 186 | _this = this; 187 | fields = ["summary", "status", "assignee"]; 188 | return this.jira.searchJira(searchQuery, fields, function(error, issueList) { 189 | var issue, _i, _len, _ref, _results; 190 | if (issueList != null) { 191 | _this.myIssues = issueList; 192 | _ref = issueList.issues; 193 | _results = []; 194 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 195 | issue = _ref[_i]; 196 | _results.push(_this.pp.prettyPrintIssue(issue, details)); 197 | } 198 | return _results; 199 | } else { 200 | if (error != null) { 201 | _this.error = error; 202 | } 203 | return _this.log.error("Error retreiving issues list: " + error); 204 | } 205 | }); 206 | }; 207 | 208 | JiraHelper.prototype.getMyIssues = function(open, details, projects) { 209 | var jql; 210 | jql = "assignee = \"" + this.config.user + "\""; 211 | if (open) { 212 | jql += ' AND resolution = unresolved'; 213 | } 214 | if (projects != null) { 215 | jql += projects; 216 | } 217 | this.searchJira(jql, details); 218 | }; 219 | 220 | JiraHelper.prototype.getMyProjects = function(callback) { 221 | var _this = this; 222 | return this.jira.listProjects(function(error, projectList) { 223 | if (projectList != null) { 224 | return callback(projectList); 225 | } else { 226 | _this.log.error("Error listing projects: " + error); 227 | return _this.dieWithFire(); 228 | } 229 | }); 230 | }; 231 | 232 | return JiraHelper; 233 | 234 | })(); 235 | 236 | module.exports = { 237 | JiraHelper: JiraHelper 238 | }; 239 | 240 | }).call(this); 241 | -------------------------------------------------------------------------------- /lib/jira.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | (function() { 3 | var JiraHelper, addItem, addWorklog, args, argv, configFile, configFilePath, createConfigFile, dutils, fs, getProject, jiraCli, listProjects, loadConfigFile, paramIsText, path, transitionItem; 4 | 5 | fs = require('fs'); 6 | 7 | path = require('path'); 8 | 9 | JiraHelper = require('./jira-cli').JiraHelper; 10 | 11 | dutils = require('./data-utils'); 12 | 13 | createConfigFile = function(aConfigFile) { 14 | console.log("No config file found, answer these questions to create one!"); 15 | return dutils.ask("Username", /.+/, function(username) { 16 | return dutils.ask("Password", /.+/, function(password) { 17 | return dutils.ask("Jira Host", /.+/, function(host) { 18 | return dutils.ask("Jira Port", /.+/, function(port) { 19 | return dutils.ask("Default Project", /.*/, function(project) { 20 | var config; 21 | config = { 22 | user: username, 23 | password: password, 24 | host: host, 25 | port: port, 26 | project: project 27 | }; 28 | fs.writeFileSync(aConfigFile, JSON.stringify(config), 'utf8'); 29 | console.log("File created and saved as " + aConfigFile); 30 | return process.exit(); 31 | }); 32 | }); 33 | }); 34 | }); 35 | }); 36 | }; 37 | 38 | paramIsText = function(param) { 39 | if (typeof param === "boolean") { 40 | argv.showHelp(); 41 | return false; 42 | } 43 | return true; 44 | }; 45 | 46 | loadConfigFile = function(configFilePath) { 47 | var configFile; 48 | configFile = fs.readFileSync(configFilePath); 49 | return JSON.parse(configFile); 50 | }; 51 | 52 | transitionItem = function(issueId) { 53 | return jiraCli.listTransitions(issueId, function(transitions) { 54 | var allowedTypes, index, transition, _i, _j, _len, _ref, _results; 55 | transitions.sort(dutils.itemSorter); 56 | for (index = _i = 0, _len = transitions.length; _i < _len; index = ++_i) { 57 | transition = transitions[index]; 58 | jiraCli.pp.prettyPrintTransition(transition, index + 1); 59 | } 60 | allowedTypes = (function() { 61 | _results = []; 62 | for (var _j = 1, _ref = transitions.length; 1 <= _ref ? _j <= _ref : _j >= _ref; 1 <= _ref ? _j++ : _j--){ _results.push(_j); } 63 | return _results; 64 | }).apply(this); 65 | return dutils.ask("Transtion Type ", allowedTypes, function(type) { 66 | return dutils.ask("Comment for worklog (blank to skip)", /.*/, function(comment) { 67 | if (comment.length === 0) { 68 | jiraCli.transitionIssue(issueId, transitions[type - 1].id); 69 | return; 70 | } 71 | return dutils.ask("Time Spent (for worklog)", /.+/, function(timeSpent) { 72 | jiraCli.addWorklog(issueId, comment, timeSpent, false); 73 | return jiraCli.transitionIssue(issueId, transitions[type - 1].id); 74 | }); 75 | }); 76 | }, allowedTypes); 77 | }); 78 | }; 79 | 80 | addWorklog = function(issueId) { 81 | return dutils.ask("Comment for worklog", /.+/, function(comment) { 82 | return dutils.ask("Time Spent (for worklog)", /.+/, function(timeSpent) { 83 | return jiraCli.addWorklog(issueId, comment, timeSpent, true); 84 | }); 85 | }); 86 | }; 87 | 88 | listProjects = function() { 89 | var projects, 90 | _this = this; 91 | return projects = jiraCli.getMyProjects(function(projects) { 92 | var project, _i, _len, _results; 93 | _results = []; 94 | for (_i = 0, _len = projects.length; _i < _len; _i++) { 95 | project = projects[_i]; 96 | _results.push(jiraCli.pp.prettyPrintProject(project)); 97 | } 98 | return _results; 99 | }); 100 | }; 101 | 102 | getProject = function(callback, defaultProj) { 103 | return dutils.ask("Project (Enter for Default/? for list) [" + defaultProj + "] ", /.*/, function(project) { 104 | var projects, 105 | _this = this; 106 | if (project !== '?') { 107 | callback(configFile.project); 108 | return; 109 | } 110 | return projects = jiraCli.getMyProjects(function(projects) { 111 | var _i, _len; 112 | for (_i = 0, _len = projects.length; _i < _len; _i++) { 113 | project = projects[_i]; 114 | jiraCli.pp.prettyPrintProject(project); 115 | } 116 | return getProject(callback, defaultProj); 117 | }); 118 | }); 119 | }; 120 | 121 | addItem = function(project) { 122 | return dutils.ask("Summary", /.+/, function(summary) { 123 | return dutils.ask("Description", /.+/, function(description) { 124 | return jiraCli.getIssueTypes(function(issueTypes) { 125 | var addIssueCallback, allowedTypes, index, type, _i, _j, _len, _ref, _results; 126 | issueTypes.sort(dutils.itemSorter); 127 | for (index = _i = 0, _len = issueTypes.length; _i < _len; index = ++_i) { 128 | type = issueTypes[index]; 129 | jiraCli.pp.prettyPrintIssueTypes(type, index + 1); 130 | } 131 | allowedTypes = (function() { 132 | _results = []; 133 | for (var _j = 1, _ref = issueTypes.length; 1 <= _ref ? _j <= _ref : _j >= _ref; 1 <= _ref ? _j++ : _j--){ _results.push(_j); } 134 | return _results; 135 | }).apply(this); 136 | addIssueCallback = function(type) { 137 | return jiraCli.addIssue(summary, description, issueTypes[type - 1].id, project); 138 | }; 139 | return dutils.ask("Type ", allowedTypes, addIssueCallback, allowedTypes); 140 | }); 141 | }); 142 | }); 143 | }; 144 | 145 | if (require.main === module) { 146 | argv = (require('optimist')).options('f', { 147 | alias: 'find', 148 | describe: 'Finds the specified Jira ID' 149 | }).options('a', { 150 | alias: 'add', 151 | describe: 'Allows you to add a new Jira Task' 152 | }).options('t', { 153 | alias: 'transition', 154 | describe: 'Allows you to resolve a specific Jira ID' 155 | }).options('l', { 156 | alias: 'list', 157 | describe: 'Lists all your open issues' 158 | }).options('c', { 159 | alias: 'list-all', 160 | describe: 'Lists all your issues' 161 | }).options('d', { 162 | alias: 'details', 163 | describe: 'Shows extra details (currently only for list)' 164 | }).options('p', { 165 | alias: 'projects', 166 | describe: 'Lists all your viewable projects' 167 | }).options('o', { 168 | describe: 'Limits list to only this project' 169 | }).options('w', { 170 | alias: 'worklog', 171 | describe: 'Adds work to your task' 172 | }).options('s', { 173 | alias: 'search', 174 | describe: 'Pass a jql string to jira' 175 | }).options('h', { 176 | alias: 'help', 177 | describe: 'Shows this help message' 178 | }).usage('Usage:\n\tjira -f EG-143\n\tjira -r EG-143').boolean('d').string('s').string('f').string('t').string('w'); 179 | if (argv.argv.help) { 180 | argv.showHelp(); 181 | return; 182 | } 183 | args = argv.argv; 184 | configFilePath = path.join(process.env.HOME, '.jiraclirc.json'); 185 | if (!fs.existsSync(configFilePath)) { 186 | createConfigFile(configFilePath); 187 | return; 188 | } 189 | configFile = loadConfigFile(configFilePath); 190 | jiraCli = new JiraHelper(configFile); 191 | if (args.o != null) { 192 | if (args.o instanceof Array) { 193 | args.o = args.o.join(','); 194 | } 195 | args.o = " AND project in (" + args.o + ")"; 196 | } 197 | if (args.l) { 198 | jiraCli.getMyIssues(true, args.d, args.o); 199 | } else if (args.c) { 200 | jiraCli.getMyIssues(false, args.d, args.o); 201 | } else if (args.s) { 202 | if (!paramIsText(args.s)) { 203 | return; 204 | } 205 | if (args.o != null) { 206 | args.s += args.o; 207 | } 208 | jiraCli.searchJira(args.s, args.d); 209 | } else if (args.p) { 210 | listProjects(); 211 | } else if (args.a) { 212 | getProject(addItem, configFile.project); 213 | } else if (args.f != null) { 214 | if (!paramIsText(args.f)) { 215 | return; 216 | } 217 | jiraCli.getIssue(args.f, args.d); 218 | } else if (args.w != null) { 219 | if (!paramIsText(args.w)) { 220 | return; 221 | } 222 | addWorklog(args.w); 223 | } else if (args.t != null) { 224 | if (!paramIsText(args.t)) { 225 | return; 226 | } 227 | transitionItem(args.t); 228 | } else { 229 | argv.showHelp(); 230 | } 231 | } 232 | 233 | }).call(this); 234 | -------------------------------------------------------------------------------- /lib/pretty-printer.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var PrettyPrinter, color, wrap; 3 | 4 | color = require('ansi-color').set; 5 | 6 | wrap = require('wordwrap')(5, 65); 7 | 8 | PrettyPrinter = (function() { 9 | function PrettyPrinter() {} 10 | 11 | PrettyPrinter.prototype.prettyPrintIssue = function(issue, detail) { 12 | var sumColor, _ref; 13 | sumColor = "green"; 14 | if ((_ref = +issue.fields.status.id) === 5 || _ref === 6) { 15 | sumColor = "red"; 16 | } 17 | process.stdout.write(color(issue.key, sumColor + "+bold")); 18 | if (issue.fields.summary == null) { 19 | issue.fields.summary = "None"; 20 | } 21 | process.stdout.write(" - "); 22 | process.stdout.write(issue.fields.summary); 23 | process.stdout.write("\n"); 24 | if (detail && (issue.fields.description != null)) { 25 | process.stdout.write(color("Description:\n", "white+bold")); 26 | process.stdout.write(wrap(issue.fields.description)); 27 | return process.stdout.write("\n\n"); 28 | } 29 | }; 30 | 31 | PrettyPrinter.prototype.prettyPrintIssueTypes = function(issueType, index) { 32 | process.stdout.write(color(index, "white+bold")); 33 | process.stdout.write(" - "); 34 | process.stdout.write(issueType.name); 35 | if (issueType.description.length > 0) { 36 | process.stdout.write(" - "); 37 | process.stdout.write(issueType.description); 38 | } 39 | return process.stdout.write("\n"); 40 | }; 41 | 42 | PrettyPrinter.prototype.prettyPrintTransition = function(transition, index) { 43 | process.stdout.write(color(index, "white+bold")); 44 | process.stdout.write(" - "); 45 | process.stdout.write(transition.name); 46 | return process.stdout.write("\n"); 47 | }; 48 | 49 | PrettyPrinter.prototype.prettyPrintProject = function(project) { 50 | var key; 51 | key = project.key; 52 | while (key.length < 12) { 53 | key = ' ' + key; 54 | } 55 | process.stdout.write(color(key, "white+bold")); 56 | process.stdout.write(" - "); 57 | process.stdout.write(project.id); 58 | process.stdout.write(" - "); 59 | process.stdout.write(project.name); 60 | return process.stdout.write("\n"); 61 | }; 62 | 63 | return PrettyPrinter; 64 | 65 | })(); 66 | 67 | module.exports = { 68 | PrettyPrinter: PrettyPrinter 69 | }; 70 | 71 | }).call(this); 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira-cli", 3 | "description": "Command line interface for Jira", 4 | "version": "0.5.0", 5 | "homepage": "https://tebriel.github.com/jira-cli", 6 | "author": { 7 | "name": "Chris Moultrie", 8 | "email": "tebriel@gmail.com" 9 | }, 10 | "bin": { 11 | "jira": "./lib/jira.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/tebriel/jira-cli.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/tebriel/jira-cli/issues" 19 | }, 20 | "licenses": [ 21 | { 22 | "type": "MIT", 23 | "url": "https://github.com/tebriel/jira-cli/blob/master/LICENSE-MIT" 24 | } 25 | ], 26 | "main": "lib/jira-cli", 27 | "engines": { 28 | "node": ">= 0.6.0" 29 | }, 30 | "scripts": { 31 | "test": "node -e 'require(\"grunt\").cli()'" 32 | }, 33 | "dependencies": { 34 | "jira": ">= 0.0.5", 35 | "optimist": "~0.3.5", 36 | "ansi-color": "~0.2.1", 37 | "wordwrap": "0.0.2" 38 | }, 39 | "keywords": [ 40 | "jira", 41 | "awesomeness", 42 | "win", 43 | "whisky" 44 | ], 45 | "devDependencies": { 46 | "grunt": "~0.4.1", 47 | "grunt-bump": "0.0.2", 48 | "grunt-contrib-coffee": "~0.7.0", 49 | "grunt-coffeelint": "0.0.6", 50 | "grunt-jasmine-node": "~0.1.0", 51 | "grunt-docco": "~0.2.0", 52 | "grunt-contrib-concat": "~0.3.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/jira-cli.spec.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | color = require('ansi-color').set 5 | 6 | jira = require '../src/jira-cli.coffee' 7 | 8 | # These seem to be a bit silly, gets me more familiar with spies though, so I 9 | # guess that's a good thing. 10 | 11 | describe "JiraCli", -> 12 | beforeEach -> 13 | config = 14 | host: 'localhost' 15 | port: 80 16 | user: 'test' 17 | password: 'test' 18 | 19 | @jiraCli = new jira.JiraHelper config 20 | @cb = jasmine.createSpy 'callback' 21 | spyOn @jiraCli.pp, 'prettyPrintIssue' 22 | spyOn @jiraCli.log, 'error' 23 | spyOn @jiraCli.log, 'log' 24 | spyOn @jiraCli, 'dieWithFire' 25 | 26 | it "Gets the requested issue", -> 27 | spyOn @jiraCli.jira, 'findIssue' 28 | @jiraCli.getIssue 1, false 29 | expect(@jiraCli.jira.findIssue) 30 | .toHaveBeenCalledWith 1, jasmine.any Function 31 | 32 | @jiraCli.jira.findIssue.mostRecentCall.args[1] null, "response" 33 | expect(@jiraCli.pp.prettyPrintIssue) 34 | .toHaveBeenCalledWith "response", false 35 | 36 | @jiraCli.jira.findIssue.mostRecentCall.args[1] "error" 37 | expect(@jiraCli.log.error) 38 | .toHaveBeenCalledWith "Error finding issue: error" 39 | 40 | it "Gets the issue types", -> 41 | spyOn @jiraCli.jira, 'listIssueTypes' 42 | @jiraCli.getIssueTypes @cb 43 | 44 | @jiraCli.jira.listIssueTypes.mostRecentCall.args[0] null, "response" 45 | expect(@cb).toHaveBeenCalledWith "response" 46 | 47 | @jiraCli.jira.listIssueTypes.mostRecentCall.args[0] "error" 48 | expect(@jiraCli.log.error) 49 | .toHaveBeenCalledWith "Error listing issueTypes: error" 50 | expect(@jiraCli.dieWithFire).toHaveBeenCalled() 51 | 52 | it "Adds a new issue", -> 53 | issue = @jiraCli.createIssueObject 'project', 'summary', 'issueType', 54 | 'description' 55 | spyOn @jiraCli.jira, 'addNewIssue' 56 | @jiraCli.addIssue 'summary', 'description', 'issueType', 'project' 57 | expect(@jiraCli.jira.addNewIssue) 58 | .toHaveBeenCalledWith issue, jasmine.any Function 59 | 60 | @jiraCli.jira.addNewIssue.mostRecentCall.args[1] null, 61 | key: 'response' 62 | expect(@jiraCli.log.log) 63 | .toHaveBeenCalledWith "Issue response has been 64 | #{color('created', 'green')}" 65 | 66 | @jiraCli.jira.addNewIssue.mostRecentCall.args[1] "error" 67 | expect(@jiraCli.log.error) 68 | .toHaveBeenCalledWith "Error creating issue: \"error\"" 69 | 70 | it "Deletes an Issue", -> 71 | spyOn @jiraCli.jira, 'deleteIssue' 72 | @jiraCli.deleteIssue 1 73 | 74 | expect(@jiraCli.jira.deleteIssue) 75 | .toHaveBeenCalledWith 1, jasmine.any Function 76 | 77 | @jiraCli.jira.deleteIssue.mostRecentCall.args[1] "error" 78 | expect(@jiraCli.log.error) 79 | .toHaveBeenCalledWith "Error deleting issue: error" 80 | 81 | @jiraCli.jira.deleteIssue.mostRecentCall.args[1] null, "success" 82 | expect(@jiraCli.log.log) 83 | .toHaveBeenCalledWith "Issue 1 was #{color('deleted', 'green')}" 84 | 85 | it "Adds a worklog", -> 86 | worklog = 87 | comment: 'comment' 88 | timeSpent: 'timeSpent' 89 | spyOn @jiraCli.jira, 'addWorklog' 90 | @jiraCli.addWorklog 1, 'comment', 'timeSpent', true 91 | 92 | expect(@jiraCli.jira.addWorklog) 93 | .toHaveBeenCalledWith 1, worklog, jasmine.any Function 94 | 95 | @jiraCli.jira.addWorklog.mostRecentCall.args[2] null, "response" 96 | expect(@jiraCli.log.log) 97 | .toHaveBeenCalledWith "Worklog was #{color("added", "green")}" 98 | 99 | @jiraCli.jira.addWorklog.mostRecentCall.args[2] "error" 100 | expect(@jiraCli.log.error) 101 | .toHaveBeenCalledWith "Error adding worklog: error" 102 | 103 | expect(@jiraCli.dieWithFire).toHaveBeenCalled() 104 | 105 | it "Adds a worklog, but doesn't quit", -> 106 | spyOn @jiraCli.jira, 'addWorklog' 107 | @jiraCli.addWorklog 1, 'comment', 'timeSpent', false 108 | @jiraCli.jira.addWorklog.mostRecentCall.args[2] null, "response" 109 | expect(@jiraCli.dieWithFire).not.toHaveBeenCalled() 110 | 111 | it "Lists transitions", -> 112 | spyOn @jiraCli.jira, 'listTransitions' 113 | 114 | @jiraCli.listTransitions 1, @cb 115 | expect(@jiraCli.jira.listTransitions) 116 | .toHaveBeenCalledWith 1, jasmine.any Function 117 | 118 | @jiraCli.jira.listTransitions.mostRecentCall.args[1] null, "transitions" 119 | expect(@cb).toHaveBeenCalledWith "transitions" 120 | 121 | @jiraCli.jira.listTransitions.mostRecentCall.args[1] "error" 122 | expect(@jiraCli.log.error) 123 | .toHaveBeenCalledWith "Error getting transitions: error" 124 | expect(@jiraCli.dieWithFire).toHaveBeenCalled() 125 | 126 | it "Transitions an Issue", -> 127 | issueUpdate = 128 | transition: 129 | id: 'transition' 130 | spyOn @jiraCli.jira, 'transitionIssue' 131 | @jiraCli.transitionIssue 1, 'transition' 132 | expect(@jiraCli.jira.transitionIssue) 133 | .toHaveBeenCalledWith 1, issueUpdate, jasmine.any Function 134 | 135 | @jiraCli.jira.transitionIssue.mostRecentCall.args[2] null, "response" 136 | expect(@jiraCli.log.log) 137 | .toHaveBeenCalledWith "Issue 1 was #{color "transitioned", 'green'}" 138 | 139 | @jiraCli.jira.transitionIssue.mostRecentCall.args[2] "error" 140 | expect(@jiraCli.log.error) 141 | .toHaveBeenCalledWith "Error transitioning issue: error" 142 | 143 | expect(@jiraCli.dieWithFire).toHaveBeenCalled() 144 | 145 | it "Searches Jira", -> 146 | fields = ["summary", "status", "assignee"] 147 | spyOn @jiraCli.jira, 'searchJira' 148 | 149 | @jiraCli.searchJira 'query', true 150 | expect(@jiraCli.jira.searchJira) 151 | .toHaveBeenCalledWith 'query', fields, jasmine.any Function 152 | 153 | expect(@jiraCli.jira.searchJira.mostRecentCall.args[1]) 154 | .toEqual fields 155 | 156 | @jiraCli.jira.searchJira.mostRecentCall.args[2] null, issues: [1] 157 | expect(@jiraCli.pp.prettyPrintIssue).toHaveBeenCalledWith 1, true 158 | 159 | @jiraCli.jira.searchJira.mostRecentCall.args[2] "error" 160 | expect(@jiraCli.log.error) 161 | .toHaveBeenCalledWith "Error retreiving issues list: error" 162 | 163 | it "Gets the user's OPEN issues", -> 164 | jql = "assignee = \"test\" AND resolution = unresolved" 165 | spyOn @jiraCli, 'searchJira' 166 | 167 | @jiraCli.getMyIssues true, true 168 | 169 | expect(@jiraCli.searchJira).toHaveBeenCalledWith jql, true 170 | 171 | it "Gets ALL the user's issues", -> 172 | jql = "assignee = \"test\"" 173 | spyOn @jiraCli, 'searchJira' 174 | 175 | @jiraCli.getMyIssues false, true 176 | 177 | expect(@jiraCli.searchJira).toHaveBeenCalledWith jql, true 178 | 179 | it "Gets the user's projects", -> 180 | spyOn @jiraCli.jira, 'listProjects' 181 | @jiraCli.getMyProjects @cb 182 | 183 | @jiraCli.jira.listProjects.mostRecentCall.args[0] null, "list" 184 | expect(@cb).toHaveBeenCalledWith "list" 185 | 186 | @jiraCli.jira.listProjects.mostRecentCall.args[0] "error" 187 | expect(@jiraCli.log.error) 188 | .toHaveBeenCalledWith "Error listing projects: error" 189 | 190 | expect(@jiraCli.dieWithFire).toHaveBeenCalled() 191 | -------------------------------------------------------------------------------- /src/data-utils.coffee: -------------------------------------------------------------------------------- 1 | # ## Ask the user a question ## 2 | # This is great, stole it from 3 | # [St. On It](http://is.gd/JZj6V2) 4 | # 5 | # Re-formatted it to be in coffeescript 6 | # 7 | # ### Takes ### 8 | # * question: (text for the user) 9 | # * format: RegExp which determines if the input was valid 10 | # * callback: for when we have proper input 11 | # * range: integer array that specifies allowed input 12 | ask = (question, format, callback, range) -> 13 | stdin = process.stdin 14 | stdout = process.stdout 15 | 16 | stdin.resume() 17 | stdout.write(question + ": ") 18 | 19 | stdin.once 'data', (data) -> 20 | data = data.toString().trim() 21 | 22 | if range? 23 | if parseInt(data) in range 24 | callback data 25 | return 26 | else if format.test data 27 | callback data 28 | return 29 | 30 | stdout.write("It should match: " + format + "\n") 31 | ask(question, format, callback, range) 32 | 33 | # ## Item Sorter ## 34 | # 35 | # Function for JS .Sort() which sorts items by id in ascending order 36 | itemSorter = (a, b)-> 37 | first = parseInt a.id 38 | second = parseInt b.id 39 | return -1 if first < second 40 | return 0 if first is second 41 | return 1 if first > second 42 | 43 | module.exports = { 44 | ask 45 | itemSorter 46 | } 47 | -------------------------------------------------------------------------------- /src/jira-cli.coffee: -------------------------------------------------------------------------------- 1 | # Because colors are pretty 2 | color = require('ansi-color').set 3 | # [PrettyPrinter Sourc/Doc](pretty-printer.html) 4 | PrettyPrinter = require('./pretty-printer').PrettyPrinter 5 | JiraApi = require('jira').JiraApi 6 | 7 | 8 | class Logger 9 | error: (text) -> 10 | @log text, "red" 11 | log: (text, textColor) -> 12 | unless textColor? 13 | textColor = 'white' 14 | console.log color(text, textColor) 15 | 16 | # ## JiraHelper ## 17 | # 18 | # This does the fancy talking to JiraApi for us. It formats the objects the way 19 | # that Jira expects them to come in. Basically a wrapper for node-jira-devel 20 | class JiraHelper 21 | # ## Constructor ## 22 | # 23 | # Builds a new JiraCli with the config settings 24 | constructor: (@config)-> 25 | unless @config.strictSSL? 26 | @config.strictSSL = true 27 | unless @config.protocol? 28 | @config.protocol = 'http:' 29 | 30 | @jira = new JiraApi(@config.protocol, @config.host, 31 | @config.port, @config.user, @config.password, '2', 32 | false, @config.strictSSL) 33 | @response = null 34 | @error = null 35 | @pp = new PrettyPrinter 36 | @log = new Logger 37 | 38 | dieWithFire: -> 39 | process.exit() 40 | 41 | # ## Get Issue ## 42 | # 43 | # Searches Jira for the issue number requested 44 | # this can be either a key AB-123 or just the number 123456 45 | getIssue: (issueNum, details)-> 46 | @jira.findIssue issueNum, (error, response) => 47 | if response? 48 | @response = response 49 | @pp.prettyPrintIssue response, details 50 | else 51 | @error = error if error? 52 | @log.error "Error finding issue: #{error}" 53 | 54 | # ## Get Issue Types ## 55 | # 56 | # Gets a list of all the available issue types 57 | getIssueTypes: (callback)-> 58 | @jira.listIssueTypes (error, response) => 59 | if response? 60 | callback response 61 | else 62 | @log.error "Error listing issueTypes: #{error}" 63 | @dieWithFire() 64 | 65 | createIssueObject: (project, summary, issueType, description) -> 66 | fields: 67 | project: { id:project } 68 | summary: summary 69 | issuetype: { id:issueType } 70 | assignee: { name:@config.user } 71 | description: description 72 | 73 | # ## Add Issue ## 74 | # 75 | # ### Takes ### 76 | # * summary: details for the title of the issue 77 | # * description: more detailed than summary 78 | # * issue type: Id of the type (types are like bug, feature) 79 | # * project: this is the id of the project that you're assigning the issue 80 | # to 81 | addIssue: (summary, description, issueType, project) -> 82 | newIssue = @createIssueObject project, summary, issueType, description 83 | 84 | @jira.addNewIssue newIssue, (error, response) => 85 | if response? 86 | @response = response if response? 87 | @log.log "Issue #{response.key} has " + 88 | "been #{color("created", "green")}" 89 | else 90 | # The error object is non-standard here from Jira, I'll parse 91 | # it better later 92 | @error = error if error? 93 | @log.error "Error creating issue: #{JSON.stringify(error)}" 94 | 95 | @dieWithFire() 96 | 97 | # ## Delete an Issue ## 98 | # 99 | # Deletes an issue (if you have permissions) from Jira. I haven't tested 100 | # this successfully because I don't have permissions. 101 | deleteIssue: (issueNum)-> 102 | # Don't have permissions currently 103 | @jira.deleteIssue issueNum, (error, response) => 104 | if response? 105 | @response = response 106 | @log.log "Issue #{issueNum} was #{color("deleted", "green")}" 107 | else 108 | @error = error if error? 109 | @log.error "Error deleting issue: #{error}" 110 | 111 | # ## Add Worklog Item ## 112 | # 113 | # Adds a simple worklog to an issue 114 | addWorklog: (issueId, comment, timeSpent, exit)-> 115 | worklog = 116 | comment:comment 117 | timeSpent:timeSpent 118 | @jira.addWorklog issueId, worklog, (error, response)=> 119 | if response? 120 | @log.log "Worklog was #{color("added", "green")}" 121 | else 122 | @error = error if error? 123 | @log.error "Error adding worklog: #{error}" 124 | @dieWithFire() if exit 125 | 126 | 127 | # ## List Transitions ## 128 | # 129 | # List the transitions available for an issue 130 | listTransitions: (issueNum, callback) -> 131 | @jira.listTransitions issueNum, (error, transitions)=> 132 | if transitions? 133 | callback transitions 134 | else 135 | @log.error "Error getting transitions: #{error}" 136 | @dieWithFire() 137 | 138 | # ## Transition Issue ## 139 | # 140 | # Transitions an issue in Jira 141 | # 142 | # ### Takes ### 143 | # 144 | # * issueNum: the Id of the issue (either the AB-123 or the 123456) 145 | # * transitionNum: this is the id of the transition to apply to the issue 146 | transitionIssue: (issueNum, transitionNum)-> 147 | issueUpdate = 148 | transition: 149 | id:transitionNum 150 | @jira.transitionIssue issueNum, issueUpdate, (error, response) => 151 | if response? 152 | @response = response 153 | @log.log "Issue #{issueNum} " + 154 | "was #{color("transitioned", "green")}" 155 | else 156 | @error = error if error? 157 | @log.error "Error transitioning issue: #{error}" 158 | 159 | @dieWithFire() 160 | 161 | # ## Search Jira ## 162 | # 163 | # Passes a jql formatted query to jira for search 164 | # 165 | # ### Takes ### 166 | # 167 | # * searchQuery: a jql formatted search query string 168 | # shows all otherwise 169 | searchJira: (searchQuery, details)-> 170 | fields = ["summary", "status", "assignee"] 171 | @jira.searchJira searchQuery, fields, (error, issueList) => 172 | if issueList? 173 | @myIssues = issueList 174 | for issue in issueList.issues 175 | @pp.prettyPrintIssue issue, details 176 | else 177 | @error = error if error? 178 | @log.error "Error retreiving issues list: #{error}" 179 | 180 | # ## Get My Issues ## 181 | # 182 | # Gets a list of issues for the user listed in the config 183 | # 184 | # ### Takes ### 185 | # 186 | # * open: `boolean` which indicates if only open items should be shown, 187 | # shows all otherwise 188 | getMyIssues: (open, details, projects)-> 189 | jql = "assignee = \"#{@config.user}\"" 190 | if open 191 | jql += ' AND resolution = unresolved' 192 | jql += projects if projects? 193 | 194 | @searchJira jql, details 195 | return 196 | 197 | # ## List all Projects ## 198 | # 199 | # This lists all the projects viewable with your account 200 | getMyProjects: (callback)-> 201 | @jira.listProjects (error, projectList) => 202 | if projectList? 203 | callback projectList 204 | else 205 | @log.error "Error listing projects: #{error}" 206 | @dieWithFire() 207 | 208 | 209 | module.exports = { 210 | JiraHelper 211 | } 212 | -------------------------------------------------------------------------------- /src/jira.coffee: -------------------------------------------------------------------------------- 1 | # #Jira Command Line Client# 2 | # 3 | # This client depends on you having a json file in your home directory 4 | # named '.jiraclirc.json' it must contain: 5 | # 6 | # { 7 | # "user": "USERNAME", 8 | # "password":"PASSWORD", 9 | # "host":"www.jira.com", 10 | # "port":80, 11 | # "project": 10100 12 | # } 13 | # 14 | # If not present, it will enter an interactive mode to create it with you 15 | # 16 | # JiraCli is on [github](https://github.com/tebriel/jira-cli) 17 | fs = require 'fs' 18 | path = require 'path' 19 | # ## [JiraHelper docs/source](jira-cli.html) 20 | JiraHelper = require('./jira-cli').JiraHelper 21 | # ## [dutils docs/source](data-utils.html) 22 | dutils = require('./data-utils') 23 | 24 | # ## Create Config File ## 25 | # 26 | # Creates a config file when one doesn't exist 27 | createConfigFile = (aConfigFile) -> 28 | console.log "No config file found, answer these questions to create one!" 29 | dutils.ask "Username", /.+/, (username) -> 30 | dutils.ask "Password", /.+/, (password) -> 31 | dutils.ask "Jira Host", /.+/, (host) -> 32 | dutils.ask "Jira Port", /.+/, (port) -> 33 | dutils.ask "Default Project", /.*/, (project) -> 34 | config = 35 | user:username 36 | password:password 37 | host:host 38 | port:port 39 | project:project 40 | 41 | fs.writeFileSync aConfigFile, 42 | JSON.stringify(config), 'utf8' 43 | console.log "File created and saved as #{aConfigFile}" 44 | process.exit() 45 | 46 | 47 | # ## Check for Text Parameter ## 48 | # 49 | # Optimist returns a `bool` if the param is given but with nothing following it 50 | paramIsText = (param)-> 51 | if typeof(param) is "boolean" 52 | argv.showHelp() 53 | return false 54 | true 55 | 56 | # ## Load the Config File ## 57 | # 58 | loadConfigFile = (configFilePath) -> 59 | configFile = fs.readFileSync configFilePath 60 | 61 | JSON.parse configFile 62 | 63 | # ## Transition Item ## 64 | # 65 | # This takes the issueId, lists the transitions available for the item and then 66 | # lets the user apply that transition to the item. Optionally the user can 67 | # specify a comment which will then prompt for time spent. This adds a work log 68 | # item to the item before the transition. 69 | transitionItem = (issueId) -> 70 | jiraCli.listTransitions issueId, (transitions) -> 71 | transitions.sort dutils.itemSorter 72 | for transition, index in transitions 73 | jiraCli.pp.prettyPrintTransition transition, index + 1 74 | allowedTypes = [1..transitions.length] 75 | #allowedTypes = new RegExp "[#{allowedTypes.join '|'}]" 76 | dutils.ask "Transtion Type ", allowedTypes, (type)-> 77 | dutils.ask "Comment for worklog (blank to skip)", /.*/, (comment)-> 78 | if comment.length is 0 79 | jiraCli.transitionIssue issueId, transitions[type - 1].id 80 | return 81 | dutils.ask "Time Spent (for worklog)", /.+/, (timeSpent)-> 82 | jiraCli.addWorklog issueId, comment, timeSpent, false 83 | jiraCli.transitionIssue issueId, transitions[type - 1].id 84 | , allowedTypes 85 | 86 | # ## Add Work Log ## 87 | # 88 | # This will add a comment and time spent as a worklog item attached to the 89 | # issue 90 | addWorklog = (issueId) -> 91 | dutils.ask "Comment for worklog", /.+/, (comment)-> 92 | dutils.ask "Time Spent (for worklog)", /.+/, (timeSpent)-> 93 | jiraCli.addWorklog issueId, comment, timeSpent, true 94 | 95 | # ## List Projects ## 96 | # 97 | # This will list all the projects available to you 98 | listProjects = -> 99 | projects = jiraCli.getMyProjects (projects)=> 100 | for project in projects 101 | jiraCli.pp.prettyPrintProject project 102 | 103 | # ## Get Project ## 104 | # 105 | # Here we ask the user for their project, giving them an option for the 106 | # default, ? for a list, or they can type in a number directly 107 | # 108 | # It calls itself if we list the projects, so that it can still be used to for 109 | # what it was called to do 110 | getProject = (callback, defaultProj)-> 111 | dutils.ask "Project (Enter for Default/? for list) [#{defaultProj}] ",/.*/, 112 | (project) -> 113 | unless project is '?' 114 | callback configFile.project 115 | return 116 | projects = jiraCli.getMyProjects (projects)=> 117 | for project in projects 118 | jiraCli.pp.prettyPrintProject project 119 | getProject callback, defaultProj 120 | 121 | # ## Add Item ## 122 | # 123 | # Adds an item to Jira. The project passed in comes from getProject currently. 124 | # Takes a summary and a description then lists the issue types for the user to 125 | # choose from 126 | addItem = (project)-> 127 | # Gather the summary, description, an type 128 | dutils.ask "Summary", /.+/, (summary)-> 129 | dutils.ask "Description", /.+/, (description)-> 130 | jiraCli.getIssueTypes (issueTypes)-> 131 | issueTypes.sort dutils.itemSorter 132 | for type, index in issueTypes 133 | jiraCli.pp.prettyPrintIssueTypes type, index + 1 134 | 135 | allowedTypes = [1..issueTypes.length] 136 | addIssueCallback = (type)-> 137 | jiraCli.addIssue summary, description, 138 | issueTypes[type - 1].id, project 139 | dutils.ask "Type ", allowedTypes, addIssueCallback, allowedTypes 140 | 141 | # ## Main entry point ## 142 | # 143 | # Parses the arguments and then calls a function above 144 | if require.main is module 145 | argv = (require 'optimist') 146 | .options('f', 147 | alias:'find' 148 | describe:'Finds the specified Jira ID' 149 | ).options('a', 150 | alias:'add' 151 | describe:'Allows you to add a new Jira Task' 152 | ).options('t', 153 | alias:'transition' 154 | describe:'Allows you to resolve a specific Jira ID' 155 | ).options('l', 156 | alias:'list' 157 | describe:'Lists all your open issues' 158 | ).options('c', 159 | alias:'list-all' 160 | describe:'Lists all your issues' 161 | ).options('d', 162 | alias:'details' 163 | describe:'Shows extra details (currently only for list)' 164 | ).options('p', 165 | alias:'projects' 166 | describe:'Lists all your viewable projects' 167 | ).options('o', 168 | describe:'Limits list to only this project' 169 | ).options('w', 170 | alias:'worklog' 171 | describe:'Adds work to your task' 172 | ).options('s', 173 | alias:'search' 174 | describe:'Pass a jql string to jira' 175 | ).options('h', 176 | alias:'help' 177 | describe:'Shows this help message' 178 | ).usage('Usage:\n\tjira -f EG-143\n\tjira -r EG-143') 179 | .boolean('d') 180 | .string('s') 181 | .string('f') 182 | .string('t') 183 | .string('w') 184 | 185 | if argv.argv.help 186 | argv.showHelp() 187 | return 188 | args = argv.argv 189 | 190 | configFilePath = path.join process.env.HOME, '.jiraclirc.json' 191 | unless fs.existsSync configFilePath 192 | createConfigFile configFilePath 193 | return 194 | 195 | configFile = loadConfigFile(configFilePath) 196 | jiraCli = new JiraHelper configFile 197 | 198 | if args.o? 199 | if args.o instanceof Array 200 | args.o = args.o.join ',' 201 | args.o = " AND project in (#{args.o})" 202 | 203 | if args.l 204 | jiraCli.getMyIssues true, args.d, args.o 205 | else if args.c 206 | jiraCli.getMyIssues false, args.d, args.o 207 | else if args.s 208 | return unless paramIsText args.s 209 | if args.o? 210 | args.s += args.o 211 | jiraCli.searchJira args.s, args.d 212 | else if args.p 213 | listProjects() 214 | else if args.a 215 | getProject addItem, configFile.project 216 | else if args.f? 217 | return unless paramIsText args.f 218 | jiraCli.getIssue args.f, args.d 219 | else if args.w? 220 | return unless paramIsText args.w 221 | addWorklog args.w 222 | else if args.t? 223 | return unless paramIsText args.t 224 | transitionItem args.t 225 | else 226 | argv.showHelp() 227 | -------------------------------------------------------------------------------- /src/pretty-printer.coffee: -------------------------------------------------------------------------------- 1 | color = require('ansi-color').set 2 | wrap = require('wordwrap')(5, 65) 3 | 4 | class PrettyPrinter 5 | # Because I like colors, and I don't want to format them any more than this 6 | # TODO: Don't hardcode 5 and 6 anymore 7 | prettyPrintIssue: (issue, detail)-> 8 | sumColor = "green" 9 | sumColor = "red" if +issue.fields.status.id in [5,6] 10 | process.stdout.write color(issue.key, sumColor + "+bold") 11 | # I don't think this could happen, but maybe.... 12 | issue.fields.summary = "None" unless issue.fields.summary? 13 | process.stdout.write " - " 14 | process.stdout.write issue.fields.summary 15 | process.stdout.write "\n" 16 | if detail and issue.fields.description? 17 | process.stdout.write color("Description:\n", "white+bold") 18 | process.stdout.write wrap(issue.fields.description) 19 | process.stdout.write "\n\n" 20 | 21 | # ## Do some fancy formatting on issue types ## 22 | prettyPrintIssueTypes: (issueType, index)-> 23 | process.stdout.write color(index, "white+bold") 24 | process.stdout.write " - " 25 | process.stdout.write issueType.name 26 | if issueType.description.length > 0 27 | process.stdout.write " - " 28 | process.stdout.write issueType.description 29 | process.stdout.write "\n" 30 | 31 | # ## Pretty Print Transition ## 32 | # 33 | # Show a transition with the ID in bold followed by the name 34 | prettyPrintTransition: (transition, index) -> 35 | process.stdout.write color(index, "white+bold") 36 | process.stdout.write " - " 37 | process.stdout.write transition.name 38 | process.stdout.write "\n" 39 | 40 | # ## Pretty Print Projects ## 41 | # 42 | # Prints the project list in a non-awful format 43 | prettyPrintProject: (project) -> 44 | key = project.key 45 | while key.length < 12 46 | key = ' ' + key 47 | process.stdout.write color(key, "white+bold") 48 | process.stdout.write " - " 49 | process.stdout.write project.id 50 | process.stdout.write " - " 51 | process.stdout.write project.name 52 | process.stdout.write "\n" 53 | 54 | module.exports = { 55 | PrettyPrinter 56 | } 57 | --------------------------------------------------------------------------------