├── .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 | [](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 | |
---|---|
Ask the user a question2 | 3 |This is great, stole it from 4 | St. On It 5 | 6 |Re-formatted it to be in coffeescript 7 | 8 |Takes9 | 10 |
| 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 Sorter35 | 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 | |
jira-cli.coffee | |
---|---|
Because colors are pretty | color = require('ansi-color').set |
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) | |
JiraHelper12 | 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 |
Constructor15 | 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 Issue32 | 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 Types42 | 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 Issue58 | 59 |Takes60 | 61 |
| 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 Issue80 | 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 Item89 | 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 Transitions101 | 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 Issue109 | 110 |Transitions an issue in Jira 111 | 112 |Takes113 | 114 |
| 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 Jira131 | 132 |Passes a jql formatted query to jira for search 133 | 134 |Takes135 | 136 |
| 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 Issues149 | 150 |Gets a list of issues for the user listed in the config 151 | 152 |Takes153 | 154 |
| 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 Projects165 | 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 | |
jira.coffee | |
---|---|
Jira Command Line Client2 | 3 |This client depends on you having a json file in your home directory 4 | named '.jiraclirc.json' it must contain: 5 | 6 |
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 File19 | 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 Parameter38 | 39 |Optimist returns a | 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 Item47 | 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 Log65 | 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 Projects71 | 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 Project76 | 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 Item91 | 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 point106 | 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 | |
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 Transition23 | 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 Projects29 | 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 | |