├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── bin └── jira.js ├── lib ├── actions │ ├── create-issue.js │ ├── issue-link.js │ ├── issue-status.js │ ├── issue.js │ ├── jql.js │ └── open.js ├── aliases │ ├── all.js │ ├── my-issues.js │ ├── my-not-released.js │ ├── my-stage.js │ ├── sprint.js │ └── stage.js ├── configuration.js ├── dao │ ├── issue-types-dao.js │ ├── issues-dao.js │ └── projects-dao.js ├── inquirer │ ├── configuration-inquirer.js │ ├── issues-inquirer.js │ └── projects-inquirer.js ├── mappers │ ├── comments-mapper.js │ ├── issue-types-mapper.js │ ├── issues-mapper.js │ └── projects-mapper.js ├── printer │ ├── comments-printer.js │ └── issues-printer.js └── utils │ ├── action-utils.js │ ├── alias-utils.js │ ├── browser-utils.js │ ├── configuration-utils.js │ ├── dao-utils.js │ ├── date-utils.js │ ├── error-handle-utils.js │ ├── error-print-utils.js │ ├── file-utils.js │ ├── json-utils.js │ ├── object-utils.js │ ├── print-utils.js │ ├── require-utils.js │ └── string-utils.js ├── package.json ├── test ├── actions.spec.js ├── actions │ ├── create-issue.spec.js │ ├── issue-link.spec.js │ ├── issue-status.spec.js │ ├── issue.spec.js │ ├── jql.spec.js │ └── open.spec.js ├── aliases.spec.js ├── aliases │ ├── all.spec.js │ ├── my-issues.spec.js │ ├── my-not-released.spec.js │ ├── my-stage.spec.js │ ├── sprint.spec.js │ └── stage.spec.js ├── mocha.opts ├── setup.js └── utils │ ├── action-utils.spec.js │ ├── alias-utils.spec.js │ ├── configuration-utils.spec.js │ ├── error-handle-utils.spec.js │ ├── object-utils.spec.js │ ├── require-utils.spec.js │ └── string-utils.spec.js └── types ├── Comment.d.ts ├── Issue.d.ts ├── IssueDetails.d.ts ├── IssueType.d.ts ├── Project.d.ts └── cli ├── Action.d.ts └── Alias.d.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:7.10 10 | working_directory: ~/repo 11 | 12 | steps: 13 | - checkout 14 | 15 | # Download and cache dependencies 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "package.json" }} 19 | # fallback to using the latest cache if no exact match is found 20 | - v1-dependencies- 21 | 22 | - run: yarn install 23 | 24 | - save_cache: 25 | paths: 26 | - node_modules 27 | key: v1-dependencies-{{ checksum "package.json" }} 28 | 29 | # run tests! 30 | - run: yarn test 31 | - run: yarn check-coverage 32 | - run: yarn lint 33 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-undef": 0, 4 | "no-console": 0 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | .idea/ 63 | tmp/ 64 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Łukasz Usarz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Love jira-node-cli? Please consider [donating](https://www.paypal.me/lusarz/5) to sustain our activities 2 | 3 | # JIRA CLI   [![Build Status](https://circleci.com/gh/lusarz/jira-node-cli/tree/master.png?style=shield)](https://circleci.com/gh/lusarz/jira-node-cli)   [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Interesting%20JIRA%20Command%20Line%20Interface%20written%20in%20node&url=https://github.com/lusarz/jira-node-cli&hashtags=jira,cli,node) 4 | 5 | Usage: jira [options] [command] 6 | 7 | Options: 8 | 9 | -h, --help output usage information 10 | 11 | Commands: 12 | 13 | create-issue Create new issue 14 | issue-link [issueName] Copy issue link to clipboard 15 | issue-status [issueName] Show issue status 16 | issue [issueName] Show issue details 17 | jql [jql] Show issues using JQL 18 | all Alias => jira jql '' 19 | my-issues Alias => jira jql 'assignee in (currentUser()) ORDER BY status ASC' 20 | my-not-released Alias => jira jql 'status not in (Released) AND assignee in (currentUser()) ORDER BY status ASC' 21 | my-stage Alias => jira jql 'status in ('Deployed to Stage') AND assignee in (currentUser()) ORDER BY status ASC' 22 | sprint Alias => jira jql 'sprint in openSprints() ORDER BY status ASC' 23 | stage Alias => jira jql 'status in ('Deployed to Stage') ORDER BY status ASC' 24 | 25 | 26 | ![jira-all](https://user-images.githubusercontent.com/10059264/37196314-a3a24e6a-2376-11e8-8711-5de5a1f353d0.png) 27 | 28 | ![jira-issue](https://user-images.githubusercontent.com/10059264/37196325-b12e75c2-2376-11e8-9e20-4dcdbacde2cc.png) 29 | 30 | 31 | ## Tips 32 | Commands like `jira all`, `jira my-not-released` are implemented as [aliases](https://github.com/lusarz/jira-node-cli/blob/master/lib/aliases/my-stage.js). I'm planning to allow user to create his own aliases but currently there is a few hardcoded, so you may meet error like: 33 | 34 | JIRA respond with '400 Bad Request' status. 35 | ERROR MESSAGE: 36 | The value 'Released' does not exist for the field 'status'. 37 | Please try again later 38 | 39 | As a workaround please use plain `jira jql` queries, for example: 40 | 41 | jira jql 'project=ABC AND status in (New)' 42 | jira jql 'assignee in (currentUser()) ORDER BY status ASC' 43 | jira jql 'project=XYZ AND assignee in (currentUser()) ORDER BY status ASC' . 44 | 45 | 46 | ## Install 47 | 48 | npm install -g jira-node-cli 49 | 50 | 51 | ## Usage 52 | 53 | Type: 54 | 55 | jira 56 | 57 | and you will be asked about credentials to your JIRA account. Credentials will be saved in `~/.jirarc` file. 58 | -------------------------------------------------------------------------------- /bin/jira.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const RequireUtils = require('../lib/utils/require-utils'); 5 | const ActionUtils = require('../lib/utils/action-utils'); 6 | const ConfigurationUtils = require('../lib/utils/configuration-utils'); 7 | const AliasUtils = require('../lib/utils/alias-utils'); 8 | 9 | if (!ConfigurationUtils.configurationFileExists()) { 10 | ConfigurationUtils.setupConfigurationFile() 11 | } else { 12 | registerActions(); 13 | registerAliases(); 14 | runProgram(); 15 | } 16 | 17 | function registerActions () { 18 | RequireUtils 19 | .readAvailableActions() 20 | .forEach(action/*: Action*/ => { 21 | program 22 | .command(ActionUtils.buildCommand(action.name, action.props)) 23 | .description(action.description) 24 | .action(params => action.run(params).catch(console.error)); 25 | }); 26 | } 27 | 28 | function registerAliases () { 29 | RequireUtils 30 | .readAvailableAliases() 31 | .forEach(({ alias/*: Alias*/, action/*: Action*/ }) => { 32 | program 33 | .command(alias.name) 34 | .description(AliasUtils.prepareDescription(alias, action)) 35 | .action(() => action.run(AliasUtils.fillParameters(alias, action)).catch(console.error)); 36 | }); 37 | } 38 | 39 | function runProgram() { 40 | program.parse(process.argv); 41 | if (!program.args.length) program.help(); 42 | } 43 | -------------------------------------------------------------------------------- /lib/actions/create-issue.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Create new issue', 3 | props: [], 4 | async run () { 5 | const IssuesDAO = require('../dao/issues-dao'); 6 | const ProjectsInquirer = require('../inquirer/projects-inquirer'); 7 | const IssuesInquirer = require('../inquirer/issues-inquirer'); 8 | const PrintUtils = require('../utils/print-utils'); 9 | 10 | const projectId = await ProjectsInquirer.chooseProject(); 11 | const parameters = await IssuesInquirer.getIssueParameters(projectId); 12 | const issue = await IssuesDAO.createIssue(Object.assign(parameters, { projectId })); 13 | PrintUtils.printLine(`Issue '${issue.key}' successfully created`, 'green') 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/actions/issue-link.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Copy issue link to clipboard', 3 | props: ['issueName'], 4 | run (issueName) { 5 | const clipboardy = require('clipboardy'); 6 | const ConfigurationUtils = require('../utils/configuration-utils'); 7 | const { API_URL } = ConfigurationUtils.readConfiguration(); 8 | 9 | return clipboardy.write(`${API_URL}/browse/${issueName}`) 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/actions/issue-status.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Show issue status', 3 | props: ['issueName'], 4 | async run (issueName) { 5 | const IssuesDAO = require('../dao/issues-dao'); 6 | const IssuesPrinter = require('../printer/issues-printer'); 7 | 8 | const issue = await IssuesDAO.findIssueByName(issueName); 9 | IssuesPrinter.printIssueStatus(issue); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/actions/issue.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Show issue details', 3 | props: ['issueName'], 4 | async run (issueName) { 5 | const IssuesDAO = require('../dao/issues-dao'); 6 | const IssuesPrinter = require('../printer/issues-printer'); 7 | 8 | const issue = await IssuesDAO.findIssueByName(issueName); 9 | IssuesPrinter.printIssueDetails(issue); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/actions/jql.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Show issues using JQL', 3 | props: ['jql'], 4 | async run (jql) { 5 | const IssuesDAO = require('../dao/issues-dao'); 6 | const IssuesPrinter = require('../printer/issues-printer'); 7 | 8 | const issues = await IssuesDAO.fetchIssuesUsingJQL(jql); 9 | IssuesPrinter.printIssues(issues) 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/actions/open.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Open issue in the browser', 3 | props: ['issueName'], 4 | async run (issueName) { 5 | const BrowserUtils = require('../utils/browser-utils'); 6 | const ConfigurationUtils = require('../utils/configuration-utils'); 7 | const { API_URL } = ConfigurationUtils.readConfiguration(); 8 | 9 | return BrowserUtils.openUrl(`${API_URL}/browse/${issueName}`) 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/aliases/all.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | actionName: 'jql', 3 | propsData: { 4 | jql: '' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/aliases/my-issues.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | actionName: 'jql', 3 | propsData: { 4 | jql: 'assignee in (currentUser()) ORDER BY status ASC' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/aliases/my-not-released.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | actionName: 'jql', 3 | propsData: { 4 | jql: 'status not in (Released) AND assignee in (currentUser()) ORDER BY status ASC' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/aliases/my-stage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | actionName: 'jql', 3 | propsData: { 4 | jql: `status in ('Deployed to Stage') AND assignee in (currentUser()) ORDER BY status ASC` 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/aliases/sprint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | actionName: 'jql', 3 | propsData: { 4 | jql: 'sprint in openSprints() ORDER BY status ASC' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/aliases/stage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | actionName: 'jql', 3 | propsData: { 4 | jql: `status in ('Deployed to Stage') ORDER BY status ASC` 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/configuration.js: -------------------------------------------------------------------------------- 1 | // It's done this way to prevent read from .jirarc in unit test 2 | module.exports = require('./utils/configuration-utils').readConfiguration(); 3 | -------------------------------------------------------------------------------- /lib/dao/issue-types-dao.js: -------------------------------------------------------------------------------- 1 | const DAOUtils = require('../utils/dao-utils'); 2 | const IssueTypesMapper = require('../mappers/issue-types-mapper'); 3 | 4 | module.exports = { 5 | async getIssueTypesExcludingSubtasks (projectId) { 6 | const { issueTypes } = await DAOUtils.fetch(`/project/${projectId}`); 7 | 8 | return issueTypes 9 | .filter(issue => !issue.subtask) 10 | .map(IssueTypesMapper.mapIssueType) 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/dao/issues-dao.js: -------------------------------------------------------------------------------- 1 | const DAOUtils = require('../utils/dao-utils'); 2 | const IssuesMapper = require('../mappers/issues-mapper'); 3 | 4 | module.exports = { 5 | async findIssueByName (issueName) { 6 | const issue = await DAOUtils.fetch(`/issue/${issueName}`); 7 | return IssuesMapper.mapIssueDetails(issue); 8 | }, 9 | 10 | async createIssue ({ summary, description, issueTypeId, projectId }) { 11 | return DAOUtils.post('/issue', { 12 | summary, 13 | description, 14 | issuetype: { id: issueTypeId }, 15 | project: { id: projectId }, 16 | }) 17 | }, 18 | 19 | async fetchIssuesUsingJQL (jql) { 20 | const { issues } = await DAOUtils.fetch(`/search?jql=${jql}&maxResults=200`); 21 | return issues.map(IssuesMapper.mapIssue); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/dao/projects-dao.js: -------------------------------------------------------------------------------- 1 | const DAOUtils = require('../utils/dao-utils'); 2 | const ProjectsMapper = require('../mappers/projects-mapper'); 3 | 4 | module.exports = { 5 | async getProjects () { 6 | const projects = await DAOUtils.fetch(`/project`); 7 | return projects.map(ProjectsMapper.mapProject); 8 | }, 9 | 10 | async getProject (projectId) { 11 | return DAOUtils.fetch(`/project/${projectId}`); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/inquirer/configuration-inquirer.js: -------------------------------------------------------------------------------- 1 | const prompt = require('inquirer').prompt; 2 | const PrintUtils = require('../utils/print-utils'); 3 | 4 | module.exports = { 5 | getConfigurationParameters () { 6 | PrintUtils.printLine('Please provide configuration parameters:'); 7 | return prompt([ 8 | { 9 | type: 'input', 10 | name: 'host', 11 | message: 'Host: ', 12 | default: 'example.atlassian.net' 13 | }, { 14 | type: 'input', 15 | name: 'username', 16 | message: 'User name or email :' 17 | }, { 18 | type: 'password', 19 | name: 'password', 20 | message: 'Password:' 21 | } 22 | ]) 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/inquirer/issues-inquirer.js: -------------------------------------------------------------------------------- 1 | const prompt = require('inquirer').prompt; 2 | const IssueTypesDAO = require('../dao/issue-types-dao'); 3 | 4 | module.exports = { 5 | async getIssueParameters (projectId) { 6 | const issueTypes = await IssueTypesDAO.getIssueTypesExcludingSubtasks(projectId); 7 | return prompt([{ 8 | type: 'list', 9 | name: 'issueTypeId', 10 | message: 'Type: ', 11 | choices: issueTypes.map(issueType => ({ value: issueType.id, name: issueType.name })) 12 | }, { 13 | type: 'input', 14 | name: 'summary', 15 | message: 'Name: ' 16 | }, { 17 | type: 'input', 18 | name: 'description', 19 | message: 'Description: ' 20 | }]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lib/inquirer/projects-inquirer.js: -------------------------------------------------------------------------------- 1 | const prompt = require('inquirer').prompt; 2 | const ProjectsDAO = require('../dao/projects-dao'); 3 | 4 | module.exports = { 5 | async chooseProject () { 6 | const projects = await ProjectsDAO.getProjects(); 7 | 8 | const { projectId } = await prompt({ 9 | type: 'list', 10 | name: 'projectId', 11 | message: 'Choose project: ', 12 | choices: projects.map(project => ({ value: project.id, name: project.name })) 13 | }); 14 | 15 | return projectId; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/mappers/comments-mapper.js: -------------------------------------------------------------------------------- 1 | const DateUtils = require('../utils/date-utils'); 2 | 3 | const CommentsMapper = { 4 | mapComment (serverComment) { 5 | 6 | const comment/*: Comment*/ = { 7 | author: serverComment.author.name, 8 | content: serverComment.body, 9 | updateTimeInfo: DateUtils.formatDateTimeWithFromNow(serverComment.updated) 10 | }; 11 | return comment; 12 | } 13 | }; 14 | 15 | module.exports = CommentsMapper; 16 | -------------------------------------------------------------------------------- /lib/mappers/issue-types-mapper.js: -------------------------------------------------------------------------------- 1 | const IssueTypeMapper = { 2 | mapIssueType (serverIssueType) { 3 | const issueType/*: IssueType*/ = { 4 | id: serverIssueType.id, 5 | name: serverIssueType.name 6 | }; 7 | 8 | return issueType; 9 | } 10 | }; 11 | 12 | module.exports = IssueTypeMapper; 13 | -------------------------------------------------------------------------------- /lib/mappers/issues-mapper.js: -------------------------------------------------------------------------------- 1 | const CommentsMapper = require('./comments-mapper'); 2 | const ConfigurationUtils = require('../utils/configuration-utils'); 3 | const { API_URL } = ConfigurationUtils.readConfiguration(); 4 | 5 | const IssueMapper = { 6 | mapIssue (serverIssue) { 7 | const assignee = serverIssue.fields.assignee && serverIssue.fields.assignee.key; 8 | 9 | const issue/*: Issue*/ = { 10 | statusColor: serverIssue.fields.status.statusCategory.colorName.split('-')[0], 11 | statusName: serverIssue.fields.status.name, 12 | key: serverIssue.key, 13 | summary: serverIssue.fields.summary, 14 | assignee, 15 | summaryColor: assignee ? 'white' : 'green', 16 | link: `${API_URL}/browse/${serverIssue.key}` 17 | }; 18 | 19 | return issue; 20 | }, 21 | 22 | mapIssueDetails (serverIssue) { 23 | const issue/*: IssueDetails*/ = IssueMapper.mapIssue(serverIssue); 24 | issue.commentsCount = serverIssue.fields.comment.total; 25 | issue.comments = (serverIssue.fields.comment.comments || []).map(CommentsMapper.mapComment); 26 | return issue; 27 | } 28 | }; 29 | 30 | module.exports = IssueMapper; 31 | -------------------------------------------------------------------------------- /lib/mappers/projects-mapper.js: -------------------------------------------------------------------------------- 1 | const ProjectMapper = { 2 | mapProject (serverProject) { 3 | const project/*: Project*/ = { 4 | id: serverProject.id, 5 | name: serverProject.name, 6 | key: serverProject.key 7 | }; 8 | 9 | return project; 10 | }, 11 | 12 | mapProjects (projects) { 13 | return projects.map(ProjectMapper.mapProject); 14 | } 15 | }; 16 | 17 | module.exports = ProjectMapper; 18 | -------------------------------------------------------------------------------- /lib/printer/comments-printer.js: -------------------------------------------------------------------------------- 1 | const PrintUtils = require('../utils/print-utils'); 2 | 3 | module.exports = { 4 | printComments (comments/*: Array*/) { 5 | if (comments.length) { 6 | PrintUtils.printField('Last comment details', comments[comments.length - 1].updateTimeInfo, 'green'); 7 | 8 | PrintUtils.printNewLine(); 9 | comments.forEach((comment, index ) => { 10 | PrintUtils.printLine(`${comment.author} - ${comment.content}`, index % 2 ? 'magenta' : 'blue'); 11 | PrintUtils.printNewLine(); 12 | }) 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/printer/issues-printer.js: -------------------------------------------------------------------------------- 1 | const PrintUtils = require('../utils/print-utils'); 2 | const CommentsPrinter = require('./comments-printer'); 3 | 4 | function printIssue (issue/*: Issue*/) { 5 | PrintUtils.printFixedWidth(`[${issue.statusName}]`, issue.statusColor, 20); 6 | PrintUtils.print(`${issue.key}: ${issue.summary} - ${issue.assignee || 'not assigned'}`, issue.summaryColor); 7 | PrintUtils.printNewLine(); 8 | } 9 | 10 | module.exports = { 11 | 12 | printIssues (issues) { 13 | issues.forEach(printIssue); 14 | }, 15 | 16 | printIssueStatus (issue/*: Issue*/) { 17 | PrintUtils.printLine(`[${issue.statusName}]`, issue.statusColor); 18 | }, 19 | 20 | printIssueDetails (issue/*: IssueDetails*/) { 21 | PrintUtils.printLine(issue.key + ': ' + issue.summary); 22 | PrintUtils.printLine(`[${issue.statusName}]`, issue.statusColor); 23 | PrintUtils.printField('Comments', issue.commentsCount, 'green'); 24 | CommentsPrinter.printComments(issue.comments); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/utils/action-utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | buildCommand (action, props = []) { 3 | return action + props.map(prop => ` [${prop}]`).join(''); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /lib/utils/alias-utils.js: -------------------------------------------------------------------------------- 1 | const AliasUtils = { 2 | prepareDescription (alias/*: Alias*/, action/*: Action*/) { 3 | return `Alias => jira ${action.name} '${AliasUtils.fillParameters(alias, action)}'`; 4 | }, 5 | 6 | fillParameters(alias/*: Alias*/, action/*: Action*/) { 7 | return action.props.map(prop => alias.propsData[prop]) 8 | } 9 | }; 10 | 11 | 12 | module.exports = AliasUtils; 13 | -------------------------------------------------------------------------------- /lib/utils/browser-utils.js: -------------------------------------------------------------------------------- 1 | const open = require('opener'); 2 | 3 | function openUrl (url) { 4 | return new Promise((resolve, reject) => { 5 | open(url, {}, (err) => { 6 | if (err) { 7 | reject(err); 8 | } else { 9 | resolve(); 10 | } 11 | }) 12 | }); 13 | } 14 | 15 | module.exports = { 16 | openUrl 17 | }; 18 | -------------------------------------------------------------------------------- /lib/utils/configuration-utils.js: -------------------------------------------------------------------------------- 1 | const JSONUtils = require('./json-utils'); 2 | const FileUtils = require('./file-utils'); 3 | const ConfigurationInquirer = require('../inquirer/configuration-inquirer'); 4 | const PrintUtils = require('./print-utils'); 5 | const path = require('path'); 6 | const toBase64 = require('./string-utils').toBase64; 7 | 8 | const CONFIGURATION_FILE_PATH = path.join(process.env.HOME, '.jirarc'); 9 | 10 | function saveConfiguration ({ host, username, password }) { 11 | JSONUtils.saveAsJSONFile(CONFIGURATION_FILE_PATH, { 12 | API_URL: `https://${host}`, 13 | TOKEN: toBase64(`${username}:${password}`) 14 | }) 15 | } 16 | 17 | const ConfigurationUtils = { 18 | CONFIGURATION_FILE_PATH, 19 | 20 | configurationFileExists: () => FileUtils.fileExists(CONFIGURATION_FILE_PATH), 21 | readConfiguration: () => JSONUtils.readJSONFile(CONFIGURATION_FILE_PATH), 22 | 23 | printConfigurationFile () { 24 | PrintUtils.printNewLine(); 25 | PrintUtils.printJSON(ConfigurationUtils.readConfiguration()); 26 | PrintUtils.printNewLine(); 27 | }, 28 | 29 | async setupConfigurationFile () { 30 | const parameters = await ConfigurationInquirer.getConfigurationParameters(); 31 | saveConfiguration(parameters); 32 | ConfigurationUtils.printConfigurationFile(); 33 | PrintUtils.printLine(`Configuration JSON stored in '${CONFIGURATION_FILE_PATH}' file`, 'yellow') 34 | } 35 | }; 36 | 37 | module.exports = ConfigurationUtils; 38 | -------------------------------------------------------------------------------- /lib/utils/dao-utils.js: -------------------------------------------------------------------------------- 1 | const getConfiguration = () => require('../configuration'); 2 | 3 | function buildUrl (url) { 4 | return `${getConfiguration().API_URL}/rest/api/latest${url}`; 5 | } 6 | 7 | function buildRequestParams () { 8 | return { headers: { 'Authorization': `Basic ${getConfiguration().TOKEN}` } }; 9 | } 10 | 11 | function readDataFromResponse (response) { 12 | return response.data; 13 | } 14 | 15 | const DAOUtils = { 16 | async fetch (url) { 17 | const axios = require('axios'); 18 | const ErrorHandleUtils = require('./error-handle-utils'); 19 | 20 | try { 21 | const response = await axios.get(buildUrl(url), buildRequestParams()); 22 | return readDataFromResponse(response); 23 | } catch (error) { 24 | ErrorHandleUtils.handleResponseError(error); 25 | } 26 | }, 27 | 28 | async post (url, fields) { 29 | const axios = require('axios'); 30 | const ErrorHandleUtils = require('./error-handle-utils'); 31 | 32 | try { 33 | const response = await axios.post(buildUrl(url), { fields }, buildRequestParams()); 34 | return readDataFromResponse(response); 35 | } catch (error) { 36 | ErrorHandleUtils.handleResponseError(error); 37 | } 38 | } 39 | }; 40 | 41 | module.exports = DAOUtils; 42 | -------------------------------------------------------------------------------- /lib/utils/date-utils.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | function formatDateTime (date) { 4 | return moment(date).format('DD.MM.YYYY, hh:mm:ss'); 5 | } 6 | 7 | function formatFromNow (date) { 8 | return moment(date).fromNow(); 9 | } 10 | 11 | module.exports = { 12 | formatDateTimeWithFromNow (date) { 13 | return `${formatDateTime(date)} (${formatFromNow(date)})`; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/utils/error-handle-utils.js: -------------------------------------------------------------------------------- 1 | const ObjectUtils = require('./object-utils'); 2 | const ErrorPrintUtils = require('./error-print-utils'); 3 | 4 | const hasAuthenticationError = response => response.status == '401'; 5 | const containsErrorMessage = response => !!ObjectUtils.readPropertyDeeply(response, 'data.errorMessages.length'); 6 | 7 | 8 | module.exports = { 9 | handleResponseError (err) { 10 | const response = err.response; 11 | 12 | if (!response) { 13 | ErrorPrintUtils.printUnexpectedError(err); 14 | } else { 15 | ErrorPrintUtils.printStatus(response); 16 | if (hasAuthenticationError(response)) { 17 | ErrorPrintUtils.printInvalidCredentialsError(); 18 | } else if (containsErrorMessage(response)) { 19 | ErrorPrintUtils.printErrorMessage(response); 20 | } 21 | } 22 | process.exit(); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/utils/error-print-utils.js: -------------------------------------------------------------------------------- 1 | const ConfigurationUtils = require('./configuration-utils'); 2 | const PrintUtils = require('./print-utils'); 3 | 4 | module.exports = { 5 | printUnexpectedError (err) { 6 | PrintUtils.printLine('An unexpected error occurred during communication with JIRA, details: ', 'red'); 7 | if (err.code || err.syscall || err.errno) { 8 | PrintUtils.printField('Code', err.code); 9 | PrintUtils.printField('Syscall', err.syscall); 10 | PrintUtils.printField('Errno', err.errno); 11 | } else { 12 | console.error(err); 13 | } 14 | PrintUtils.printNewLine(); 15 | PrintUtils.printLine(`Please check your configuration file (${ConfigurationUtils.CONFIGURATION_FILE_PATH}):`, 'yellow'); 16 | ConfigurationUtils.printConfigurationFile(); 17 | }, 18 | 19 | printStatus(response) { 20 | console.log(`JIRA respond with '${response.status} ${response.statusText}' status.`); 21 | }, 22 | 23 | printInvalidCredentialsError () { 24 | console.log('Seems like your credentials are invalid'); 25 | console.log(`Please remove '${ConfigurationUtils.CONFIGURATION_FILE_PATH}' file and run tool again`); 26 | }, 27 | 28 | printErrorMessage (response) { 29 | console.log('Error message is: ' + response.data.errorMessages[0]) 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /lib/utils/file-utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = { 4 | readFile: path => fs.readFileSync(path), 5 | fileExists: path => fs.existsSync(path), 6 | saveFile: (path, content) => fs.writeFileSync(path, content) 7 | }; 8 | -------------------------------------------------------------------------------- /lib/utils/json-utils.js: -------------------------------------------------------------------------------- 1 | const FileUtils = require('./file-utils'); 2 | 3 | module.exports = { 4 | readJSONFile: path => JSON.parse(FileUtils.readFile(path)), 5 | saveAsJSONFile: (path, object) => FileUtils.saveFile(path, JSON.stringify(object, null, 2)) 6 | }; 7 | -------------------------------------------------------------------------------- /lib/utils/object-utils.js: -------------------------------------------------------------------------------- 1 | const ObjectUtils = { 2 | readPropertyDeeply (object, path) { 3 | let current = object; 4 | path.split('.').forEach(p => { 5 | if (current != null) current = current[p]; 6 | }); 7 | return current; 8 | } 9 | }; 10 | 11 | module.exports = ObjectUtils; 12 | -------------------------------------------------------------------------------- /lib/utils/print-utils.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors/safe'); 2 | const StringUtils = require('./string-utils'); 3 | 4 | const PrintUtils = { 5 | print (text, color) { 6 | process.stdout.write(color ? colors[color](text) : text); 7 | }, 8 | 9 | printFixedWidth (text, color, places) { 10 | PrintUtils.print(StringUtils.fillWithSpaces(text, places), color); 11 | }, 12 | 13 | printLine (text, color) { 14 | PrintUtils.print(text, color); 15 | PrintUtils.printNewLine(); 16 | }, 17 | 18 | printField (label, field, color) { 19 | if (field === 0 || field) { 20 | PrintUtils.printLine(label + ': ' + field, color); 21 | } 22 | }, 23 | 24 | printJSON (object, color) { 25 | PrintUtils.printLine(JSON.stringify(object, null, 2), color); 26 | }, 27 | 28 | printNewLine () { 29 | process.stdout.write('\n'); 30 | } 31 | }; 32 | 33 | module.exports = PrintUtils; 34 | -------------------------------------------------------------------------------- /lib/utils/require-utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | function readDir (dir) { 4 | return fs 5 | .readdirSync(__dirname + '/../' + dir) 6 | .map(file => file.replace('.js', '')) 7 | .map(fileName => readFile(fileName, dir)) 8 | } 9 | 10 | function readFile (name, directory) { 11 | return Object.assign(require(`../${directory}/${name}`), { name }); 12 | } 13 | 14 | function readAction (actionName) { 15 | return readFile(actionName, 'actions'); 16 | } 17 | 18 | module.exports = { 19 | readAvailableActions () { 20 | return readDir('actions') 21 | }, 22 | 23 | readAvailableAliases () { 24 | return readDir('aliases').map(alias => ({ alias, action: readAction(alias.actionName) })) 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/utils/string-utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fillWithSpaces (text, requiredLength) { 3 | return text + ' '.repeat(Math.max(requiredLength - text.length, 0)); 4 | }, 5 | 6 | toBase64: text => Buffer.from(text).toString('base64') 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira-node-cli", 3 | "version": "1.2.0", 4 | "description": "JIRA command line interface", 5 | "main": "index.js", 6 | "scripts": { 7 | "test:unit": "mocha", 8 | "test": "nyc --reporter=text mocha", 9 | "check-coverage": "nyc check-coverage --lines 62", 10 | "lint": "eslint . --ext js" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.17.1", 14 | "clipboardy": "^1.2.2", 15 | "colors": "^1.1.2", 16 | "commander": "^2.13.0", 17 | "inquirer": "^5.1.0", 18 | "moment": "^2.20.1", 19 | "opener": "^1.5.1" 20 | }, 21 | "devDependencies": { 22 | "chai": "^4.1.2", 23 | "eslint": "^4.19.1", 24 | "mocha": "^5.0.1", 25 | "nyc": "^11.6.0", 26 | "sinon": "^4.4.4" 27 | }, 28 | "bin": { 29 | "jira": "bin/jira.js" 30 | }, 31 | "author": { 32 | "name": "Łukasz Usarz", 33 | "email": "lukasz.usarz@gmail.com" 34 | }, 35 | "license": "MIT", 36 | "keywords": [ 37 | "jira", 38 | "cli", 39 | "node", 40 | "issues", 41 | "command", 42 | "line" 43 | ], 44 | "engines": { 45 | "node": ">= 8" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git://github.com/lusarz/jira-node-cli.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/lusarz/jira-node-cli/issues" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/actions.spec.js: -------------------------------------------------------------------------------- 1 | const RequireUtils = require('../lib/utils/require-utils'); 2 | 3 | it('every action has name, description, props array, print method and fetch method', () => { 4 | RequireUtils 5 | .readAvailableActions() 6 | .forEach(action => { 7 | expect(action.run).to.be.an('function'); 8 | expect(action.props).to.be.an('array'); 9 | expect(action.description).to.be.a('string'); 10 | expect(action.name).to.be.a('string'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/actions/create-issue.spec.js: -------------------------------------------------------------------------------- 1 | const CreateIssueAction/*:Action*/ = require('../../lib/actions/create-issue'); 2 | 3 | const IssuesDAO = require('../../lib/dao/issues-dao'); 4 | const ProjectsInquirer = require('../../lib/inquirer/projects-inquirer'); 5 | const IssuesInquirer = require('../../lib/inquirer/issues-inquirer'); 6 | const PrintUtils = require('../../lib/utils/print-utils'); 7 | 8 | describe('jira create-issue', () => { 9 | let sandbox; 10 | before(() => { 11 | sandbox = sinon.sandbox.create(); 12 | sandbox.stub(ProjectsInquirer, 'chooseProject').resolves(6); 13 | sandbox.stub(IssuesInquirer, 'getIssueParameters').resolves({ 14 | summary: 'Sample summary', 15 | description: 'Sample description', 16 | issueTypeId: 16 17 | }); 18 | sandbox.stub(IssuesDAO, 'createIssue').resolves({ key: 'ABC-16'}); 19 | sandbox.stub(PrintUtils, 'printLine'); 20 | CreateIssueAction.run(); 21 | }); 22 | 23 | after(() => { sandbox.restore() }); 24 | 25 | it('not require parameters', () => { 26 | expect(CreateIssueAction.props).to.be.empty; 27 | }); 28 | 29 | it('asks about the project', () => { 30 | sinon.assert.calledOnce(ProjectsInquirer.chooseProject); 31 | }); 32 | 33 | it('asks about issue type', () => { 34 | sinon.assert.calledWith(IssuesInquirer.getIssueParameters, 6); 35 | }); 36 | 37 | it('send appropriate data to JIRA server', () => { 38 | sinon.assert.calledWith(IssuesDAO.createIssue, { 39 | summary: 'Sample summary', 40 | description: 'Sample description', 41 | issueTypeId: 16, 42 | projectId: 6 43 | }); 44 | }); 45 | 46 | it('print information about created issue', () => { 47 | sinon.assert.calledWith(PrintUtils.printLine, `Issue 'ABC-16' successfully created`, 'green'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/actions/issue-link.spec.js: -------------------------------------------------------------------------------- 1 | const IssueLinkAction/*:Action*/ = require('../../lib/actions/issue-link'); 2 | const ConfigurationUtils = require('../../lib/utils/configuration-utils'); 3 | const clipboardy = require('clipboardy'); 4 | 5 | describe('jira issue-link', () => { 6 | let sandbox; 7 | 8 | before(() => { 9 | sandbox = sinon.sandbox.create(); 10 | sandbox.stub(ConfigurationUtils, 'readConfiguration').returns({ API_URL: 'https://example.atlassian.net'}); 11 | sandbox.stub(clipboardy, 'write'); 12 | IssueLinkAction.run('ABC-16'); 13 | }); 14 | 15 | after(() => { sandbox.restore() }); 16 | 17 | it('write link to issue into clipboard', () => { 18 | sinon.assert.calledWith(clipboardy.write, 'https://example.atlassian.net/browse/ABC-16'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/actions/issue-status.spec.js: -------------------------------------------------------------------------------- 1 | const IssueStatusAction/*:Action*/ = require('../../lib/actions/issue-status'); 2 | const IssuesDAO = require('../../lib/dao/issues-dao'); 3 | const IssuesPrinter = require('../../lib/printer/issues-printer'); 4 | 5 | let sandbox; 6 | 7 | describe('jira issue-status', () => { 8 | before(() => { 9 | sandbox = sinon.sandbox.create(); 10 | sandbox.stub(IssuesDAO, 'findIssueByName').resolves({ key: 'ABC-1' }); 11 | sandbox.stub(IssuesPrinter, 'printIssueStatus'); 12 | IssueStatusAction.run('ABC-1'); 13 | }); 14 | 15 | after(() => { sandbox.restore() }); 16 | 17 | it('fetches issue data', () => { 18 | sinon.assert.calledWith(IssuesDAO.findIssueByName, 'ABC-1'); 19 | }); 20 | 21 | it('prints issue status', () => { 22 | sinon.assert.calledWith(IssuesPrinter.printIssueStatus, { key: 'ABC-1' }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/actions/issue.spec.js: -------------------------------------------------------------------------------- 1 | const IssueAction/*:Action*/ = require('../../lib/actions/issue'); 2 | const IssuesDAO = require('../../lib/dao/issues-dao'); 3 | const IssuesPrinter = require('../../lib/printer/issues-printer'); 4 | 5 | let sandbox; 6 | 7 | describe('jira issue', () => { 8 | before(() => { 9 | sandbox = sinon.sandbox.create(); 10 | sandbox.stub(IssuesDAO, 'findIssueByName').resolves({ 11 | key: 'ABC-1', 12 | summary: 'Initialize project', 13 | statusName: 'New', 14 | statusColor: 'red', 15 | comments: [] 16 | }); 17 | sandbox.stub(IssuesPrinter, 'printIssueDetails'); 18 | IssueAction.run('ABC-1'); 19 | }); 20 | 21 | after(() => { sandbox.restore() }); 22 | 23 | it('fetches issue data', () => { 24 | sinon.assert.calledWith(IssuesDAO.findIssueByName, 'ABC-1'); 25 | }); 26 | 27 | it('print issue details', () => { 28 | sinon.assert.calledWith(IssuesPrinter.printIssueDetails, { 29 | key: 'ABC-1', 30 | summary: 'Initialize project', 31 | statusName: 'New', 32 | statusColor: 'red', 33 | comments: [] 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/actions/jql.spec.js: -------------------------------------------------------------------------------- 1 | const JQLAction/*:Action*/ = require('../../lib/actions/jql'); 2 | const IssuesDAO = require('../../lib/dao/issues-dao'); 3 | const IssuesPrinter = require('../../lib/printer/issues-printer'); 4 | 5 | let sandbox; 6 | 7 | describe('jira jql', () => { 8 | before(() => { 9 | sandbox = sinon.sandbox.create(); 10 | sandbox.stub(IssuesDAO, 'fetchIssuesUsingJQL').resolves([ { key: 'ABC-1' }, { key: 'ABC-2' } ]); 11 | sandbox.stub(IssuesPrinter, 'printIssues'); 12 | JQLAction.run('sprint in openSprints() ORDER BY status ASC'); 13 | }); 14 | 15 | after(() => { sandbox.restore() }); 16 | 17 | it('fetches issues using specified jql', () => { 18 | sinon.assert.calledWith(IssuesDAO.fetchIssuesUsingJQL, 'sprint in openSprints() ORDER BY status ASC'); 19 | }); 20 | 21 | it('issues are printed', () => { 22 | sinon.assert.calledWith(IssuesPrinter.printIssues, [ { key: 'ABC-1' }, { key: 'ABC-2' } ]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/actions/open.spec.js: -------------------------------------------------------------------------------- 1 | const OpenAction/*:Action*/ = require('../../lib/actions/open'); 2 | const ConfigurationUtils = require('../../lib/utils/configuration-utils'); 3 | const BrowserUtils = require('../../lib/utils/browser-utils'); 4 | 5 | describe('jira open', () => { 6 | let sandbox; 7 | 8 | before(() => { 9 | sandbox = sinon.sandbox.create(); 10 | sandbox.stub(ConfigurationUtils, 'readConfiguration').returns({ API_URL: 'https://example.atlassian.net'}); 11 | sandbox.stub(BrowserUtils, 'openUrl'); 12 | OpenAction.run('ABC-16'); 13 | }); 14 | 15 | after(() => { sandbox.restore() }); 16 | 17 | it('opens link in browser', () => { 18 | sinon.assert.calledWith(BrowserUtils.openUrl, 'https://example.atlassian.net/browse/ABC-16'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/aliases.spec.js: -------------------------------------------------------------------------------- 1 | const RequireUtils = require('../lib/utils/require-utils'); 2 | 3 | it('every alias has description, action and propsData', () => { 4 | RequireUtils 5 | .readAvailableAliases() 6 | .forEach(({ alias }) => { 7 | expect(alias.propsData).to.be.an('object'); 8 | expect(alias.actionName).to.be.a('string'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/aliases/all.spec.js: -------------------------------------------------------------------------------- 1 | const AllAlias = require('../../lib/aliases/all'); 2 | 3 | describe('jira all', () => { 4 | it(`alias to: jira jql ''`, () => { 5 | expect(AllAlias).to.deep.equal({ 6 | actionName: 'jql', 7 | name: 'all', 8 | propsData: { 9 | jql: '' 10 | } 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/aliases/my-issues.spec.js: -------------------------------------------------------------------------------- 1 | const MyIssuesAlias = require('../../lib/aliases/my-issues'); 2 | 3 | describe('jira my-issues', () => { 4 | it(`alias to: jira jql 'assignee in (currentUser()) ORDER BY status ASC'`, () => { 5 | expect(MyIssuesAlias).to.deep.equal({ 6 | actionName: 'jql', 7 | name: 'my-issues', 8 | propsData: { 9 | jql: 'assignee in (currentUser()) ORDER BY status ASC' 10 | } 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/aliases/my-not-released.spec.js: -------------------------------------------------------------------------------- 1 | const MyNotReleasedAlias = require('../../lib/aliases/my-not-released'); 2 | 3 | describe('jira my-not-released', () => { 4 | it(`alias to: jira jql status not in (Released) AND assignee in (currentUser()) ORDER BY status ASC'`, () => { 5 | expect(MyNotReleasedAlias).to.deep.equal({ 6 | actionName: 'jql', 7 | name: 'my-not-released', 8 | propsData: { 9 | jql: 'status not in (Released) AND assignee in (currentUser()) ORDER BY status ASC' 10 | } 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/aliases/my-stage.spec.js: -------------------------------------------------------------------------------- 1 | const MyStageAlias = require('../../lib/aliases/my-stage'); 2 | 3 | describe('jira my-stage', () => { 4 | it(`alias to: jira jql 'status in ('Deployed to Stage') AND assignee in (currentUser()) ORDER BY status ASC'`, () => { 5 | expect(MyStageAlias).to.deep.equal({ 6 | actionName: 'jql', 7 | name: 'my-stage', 8 | propsData: { 9 | jql: `status in ('Deployed to Stage') AND assignee in (currentUser()) ORDER BY status ASC` 10 | } 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/aliases/sprint.spec.js: -------------------------------------------------------------------------------- 1 | const SprintAlias = require('../../lib/aliases/sprint'); 2 | 3 | describe('jira sprint', () => { 4 | it(`alias to: jira jql 'sprint in openSprints() ORDER BY status ASC'`, () => { 5 | expect(SprintAlias).to.deep.equal({ 6 | actionName: 'jql', 7 | name: 'sprint', 8 | propsData: { 9 | jql: 'sprint in openSprints() ORDER BY status ASC' 10 | } 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/aliases/stage.spec.js: -------------------------------------------------------------------------------- 1 | const StageAlias = require('../../lib/aliases/stage'); 2 | 3 | describe('jira stage', () => { 4 | it(`alias to: jira jql 'status in ('Deployed to Stage') ORDER BY status ASC'`, () => { 5 | expect(StageAlias).to.deep.equal({ 6 | actionName: 'jql', 7 | name: 'stage', 8 | propsData: { 9 | jql: `status in ('Deployed to Stage') ORDER BY status ASC` 10 | } 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --require test/setup 3 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.expect = require('chai').expect; 2 | global.sinon = require('sinon'); 3 | -------------------------------------------------------------------------------- /test/utils/action-utils.spec.js: -------------------------------------------------------------------------------- 1 | const ActionUtils = require('../../lib/utils/action-utils'); 2 | 3 | describe('ActionUtils.buildCommand', () => { 4 | it('return properly formatted command that may be registered in commander', () => { 5 | expect(ActionUtils.buildCommand('jql', ['jql'])).to.equal('jql [jql]'); 6 | expect(ActionUtils.buildCommand('jql', ['jql', 'another'])).to.equal('jql [jql] [another]'); 7 | }); 8 | 9 | it('return action when no params are provided', () => { 10 | expect(ActionUtils.buildCommand('sprint')).to.equal('sprint'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/utils/alias-utils.spec.js: -------------------------------------------------------------------------------- 1 | const AliasUtils = require('../../lib/utils/alias-utils'); 2 | 3 | describe('Alias command description', () => { 4 | const alias/*: Alias*/ = { 5 | propsData: { 6 | jql: 'assignee in (currentUser())' 7 | } 8 | }; 9 | 10 | const action/*: Action*/ = { 11 | name: 'jql', 12 | description: 'Show issues using JQL', 13 | props: ['jql'] 14 | }; 15 | 16 | it(`contains original action invocation`, () => { 17 | expect(AliasUtils.prepareDescription(alias, action)).to.includes(`jira jql 'assignee in (currentUser())'`); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/utils/configuration-utils.spec.js: -------------------------------------------------------------------------------- 1 | const ConfigurationUtils = require('../../lib/utils/configuration-utils'); 2 | 3 | describe('Configuration', () => { 4 | it('should be stored in .jirarc file', () => { 5 | expect(ConfigurationUtils.CONFIGURATION_FILE_PATH).to.equal(`${process.env.HOME}/.jirarc`); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/utils/error-handle-utils.spec.js: -------------------------------------------------------------------------------- 1 | const ErrorHandleUtils = require('../../lib/utils/error-handle-utils'); 2 | const ErrorPrintUtils = require('../../lib/utils/error-print-utils'); 3 | 4 | describe('When error not contains response', () => { 5 | before(() => { 6 | sinon.stub(process, 'exit'); 7 | }); 8 | 9 | it('unexpected error information should be printed', () => { 10 | const spy = sinon.stub(ErrorPrintUtils, 'printUnexpectedError'); 11 | ErrorHandleUtils.handleResponseError('NetworkError'); 12 | sinon.assert.calledWith(spy, 'NetworkError'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/utils/object-utils.spec.js: -------------------------------------------------------------------------------- 1 | const ObjectUtils = require('../../lib/utils/object-utils'); 2 | 3 | describe('ObjectUtils.readPropertyDeeply function', () => { 4 | const object = { 5 | a: { 6 | b: { 7 | c: 'some-value' 8 | } 9 | } 10 | }; 11 | 12 | it('return value when path exists in object', () => { 13 | expect(ObjectUtils.readPropertyDeeply(object, 'a.b.c')).to.equal('some-value'); 14 | }); 15 | 16 | it('return value when path exists in object', () => { 17 | expect(ObjectUtils.readPropertyDeeply(object, 'a')).to.deep.equal({ b: { c: 'some-value' }}); 18 | }); 19 | 20 | it('return undefined when path not exists in object', () => { 21 | expect(ObjectUtils.readPropertyDeeply(object, 'a.x.y')).to.be.undefined; 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/utils/require-utils.spec.js: -------------------------------------------------------------------------------- 1 | const RequireUtils = require('../../lib/utils/require-utils'); 2 | 3 | describe('RequireUtils.readAvailableActions', () => { 4 | it('return all actions', () => { 5 | expect(RequireUtils 6 | .readAvailableActions() 7 | .map(action => action.name) 8 | .sort() 9 | ).to.deep.equal(['create-issue', 'issue', 'issue-link', 'issue-status', 'jql', 'open']); 10 | }); 11 | }); 12 | 13 | describe('RequireUtils.readAvailableAliases', () => { 14 | it('return all aliases', () => { 15 | expect(RequireUtils 16 | .readAvailableAliases() 17 | .map(({ alias }) => alias.name) 18 | .sort() 19 | ).to.deep.equal(['all', 'my-issues', 'my-not-released', 'my-stage', 'sprint', 'stage']); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/utils/string-utils.spec.js: -------------------------------------------------------------------------------- 1 | const StringUtils = require('../../lib/utils/string-utils'); 2 | 3 | describe('StringUtils.fillWithSpaces', () => { 4 | it('return properly formatted text', () => { 5 | expect(StringUtils.fillWithSpaces('x', 5)).to.equal('x '); 6 | }); 7 | 8 | it('return original text when is longer that required length', () => { 9 | expect(StringUtils.fillWithSpaces('abcdefgh', 5)).to.equal('abcdefgh'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /types/Comment.d.ts: -------------------------------------------------------------------------------- 1 | export default interface Comment { 2 | author: string; 3 | content: string; 4 | updatedAt: string; 5 | updatedAgo: string; 6 | }; 7 | -------------------------------------------------------------------------------- /types/Issue.d.ts: -------------------------------------------------------------------------------- 1 | export default interface Issue { 2 | statusColor: string; 3 | statusName: string; 4 | key: string; 5 | summary: string; 6 | summaryColor: string; 7 | assignee: string; 8 | link: string; 9 | }; 10 | -------------------------------------------------------------------------------- /types/IssueDetails.d.ts: -------------------------------------------------------------------------------- 1 | import Issue from "./Issue"; 2 | 3 | export default interface IssueDetails extends Issue { 4 | commentsCount: number; 5 | comments: Array; 6 | }; 7 | -------------------------------------------------------------------------------- /types/IssueType.d.ts: -------------------------------------------------------------------------------- 1 | export default interface IssueType { 2 | id: string; 3 | name: string; 4 | }; 5 | -------------------------------------------------------------------------------- /types/Project.d.ts: -------------------------------------------------------------------------------- 1 | export default interface Project { 2 | id: string; 3 | name: string; 4 | key: string; 5 | }; 6 | -------------------------------------------------------------------------------- /types/cli/Action.d.ts: -------------------------------------------------------------------------------- 1 | export default interface Action { 2 | name: string; 3 | description: string; 4 | props: Array; 5 | run: Function; 6 | }; 7 | -------------------------------------------------------------------------------- /types/cli/Alias.d.ts: -------------------------------------------------------------------------------- 1 | export default interface Alias { 2 | actionName: string; 3 | propsData: Object 4 | }; 5 | --------------------------------------------------------------------------------