├── .eslintrc ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── jest.config.js ├── license ├── package-lock.json ├── package.json ├── readme.md ├── src ├── cli │ ├── default.js │ ├── index.js │ ├── login.js │ └── logout.js ├── config.js ├── errors.js ├── github │ ├── index.js │ ├── labels.js │ ├── octokit.js │ └── validate.js ├── tests │ ├── __snapshots__ │ │ └── errors.test.js.snap │ ├── cli │ │ ├── default.test.js │ │ ├── login.test.js │ │ └── logout.test.js │ ├── config.test.js │ ├── errors.test.js │ ├── github │ │ ├── labels.test.js │ │ ├── octokit.test.js │ │ └── validate.test.js │ └── utils.test.js └── utils.js └── usage.gif /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "bradgarropy" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "🛠 build" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | name: "🛠 build" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: "📚 checkout" 14 | uses: actions/checkout@v2.0.0 15 | - name: "🟢 node" 16 | uses: actions/setup-node@v1.1.0 17 | with: 18 | node-version: 13 19 | registry-url: https://registry.npmjs.org/ 20 | - name: "📦 install" 21 | run: npm install 22 | - name: "🧪 test" 23 | run: npm run test 24 | - name: "👖 coveralls" 25 | uses: coverallsapp/github-action@v1.0.1 26 | with: 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "🚀 release" 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: "🚀 release" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "📚 checkout" 13 | uses: actions/checkout@v2.0.0 14 | - name: "🟢 node" 15 | uses: actions/setup-node@v1.1.0 16 | with: 17 | node-version: 13 18 | registry-url: https://registry.npmjs.org/ 19 | - name: "📦 install" 20 | run: npm install 21 | - name: "🧪 test" 22 | run: npm run test 23 | - name: "👖 coveralls" 24 | uses: coverallsapp/github-action@v1.0.1 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | - name: "🚀 publish" 28 | run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor 2 | .vscode 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # tests 8 | coverage 9 | 10 | # secrets 11 | .env* 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # editor 2 | .vscode 3 | .eslintrc 4 | .prettierrc 5 | 6 | # dependencies 7 | node_modules 8 | 9 | # secrets 10 | .env* 11 | 12 | # build 13 | .github 14 | 15 | # tests 16 | src/tests 17 | jest.config.js 18 | coverage 19 | 20 | # images 21 | usage.gif 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": false, 7 | "quoteProps": "consistent", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": false, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "avoid", 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | collectCoverage: true, 3 | coverageReporters: ["text", "lcov"], 4 | clearMocks: true, 5 | verbose: true, 6 | } 7 | 8 | module.exports = config 9 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brad Garropy 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "labman", 3 | "version": "1.0.1", 4 | "description": "👨🏼‍🔬 github label manager cli", 5 | "keywords": [ 6 | "github", 7 | "issues", 8 | "labels", 9 | "octokit", 10 | "javascript", 11 | "nodejs", 12 | "cli" 13 | ], 14 | "homepage": "https://npmjs.com/package/labman", 15 | "bugs": { 16 | "url": "https://github.com/bradgarropy/labman-cli/issues" 17 | }, 18 | "license": "MIT", 19 | "author": { 20 | "name": "Brad Garropy", 21 | "email": "bradgarropy@gmail.com", 22 | "url": "https://bradgarropy.com" 23 | }, 24 | "bin": { 25 | "labman": "src/cli/index.js" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/bradgarropy/labman-cli" 30 | }, 31 | "scripts": { 32 | "start": "node src/cli/index.js", 33 | "dev": "npm run test -- --watch", 34 | "test": "jest --passWithNoTests --silent --color" 35 | }, 36 | "dependencies": { 37 | "@octokit/rest": "^16.43.1", 38 | "chalk": "^3.0.0", 39 | "conf": "^6.2.0", 40 | "yargs": "^15.1.0" 41 | }, 42 | "devDependencies": { 43 | "babel-eslint": "^10.0.3", 44 | "eslint": "^6.8.0", 45 | "eslint-config-bradgarropy": "^1.3.1", 46 | "eslint-plugin-jsx-a11y": "^6.2.3", 47 | "eslint-plugin-react": "^7.18.3", 48 | "eslint-plugin-react-hooks": "^2.3.0", 49 | "jest": "^25.1.0", 50 | "prettier": "^1.19.1" 51 | }, 52 | "peerDependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 👨🏼‍🔬 github label manager cli 2 | 3 | 4 | npm 5 | 6 | 7 | 8 | npm 9 | 10 | 11 | 12 | coveralls 13 | 14 | 15 | _Command line tool for managing issue labels across GitHub repositories._ 16 | 17 | ![usage][gif] 18 | 19 | ## 📦 Installation 20 | 21 | Installation is optional, as you can run this package with [`npx`][npx]. 22 | 23 | ``` 24 | npm install labman --global 25 | ``` 26 | 27 | ## 🥑 Usage 28 | 29 | If you have `labman` installed globally you can run it as shown below. 30 | 31 | ``` 32 | labman --help 33 | ``` 34 | 35 | Alternatively, you can run it with [`npx`][npx]. 36 | 37 | ``` 38 | npx labman --help 39 | ``` 40 | 41 | ## 👨🏼‍🏫 Commands 42 | 43 | ### `login` 44 | 45 | Validates and stores your GitHub [personal access token][token]. 46 | 47 | #### Options 48 | 49 | | Name | Description | Default | 50 | | --------------- | -------------------------------------------------------- | ------- | 51 | | `username` | GitHub username. | | 52 | | `token` | GitHub [personal access token][token] with `repo` scope. | | 53 | | `--force`, `-f` | Overwrite existing login. | `false` | 54 | 55 | #### Examples 56 | 57 | Login as `bradgarropy` with a [personal access token][token]. 58 | 59 | ``` 60 | labman login bradgarropy 123456 61 | ``` 62 | 63 | Login as `gabygarropy`, overwriting the `bradgarropy` login. 64 | 65 | ``` 66 | labman login gabygarropy 456789 67 | ``` 68 | 69 | ### `labman` 70 | 71 | Copies labels from one repository to another. 72 | 73 | The `source` and `destination` can be provided in two ways. 74 | 75 | - `owner/repo`, like `bradgarropy/labman-cli` 76 | - `repo`, like `labman-cli` 77 | 78 | If an `owner` is not specified, `labman` will assume the `username` you provided during `login`. 79 | 80 | If `labels` is not provided, `labman` will copy all labels from `source` to `destination`. 81 | 82 | #### Options 83 | 84 | | Name | Description | Default | 85 | | ----------------- | -------------------------------------------------------------------- | ------- | 86 | | `source` | GitHub repository. | | 87 | | `destination` | GitHub repository. | | 88 | | `labels` | Space separated list of label names to copy. | `[]` | 89 | | `--clobber`, `-c` | Remove all labels from `destination` before copying `source` labels. | `false` | 90 | 91 | #### Examples 92 | 93 | Copy all labels from `bradgarropy/label-source` to `bradgarropy/label-destination`. 94 | 95 | ``` 96 | labman bradgarropy/label-source bradgarropy/label-destination 97 | ``` 98 | 99 | Delete all labels from `bradgarropy/label-destionation`, then copy all labels from `bradgarropy/label-source` to `bradgarropy/label-destination`. 100 | 101 | ``` 102 | labman bradgarropy/label-source bradgarropy/label-destination --clobber 103 | ``` 104 | 105 | Copy only the `bug` and `todo` labels from `bradgarropy/label-source` to `bradgarropy/label-destination`. 106 | 107 | ``` 108 | labman bradgarropy/label-source bradgarropy/label-destination bug todo 109 | ``` 110 | 111 | Use the shorthand method for providing `source` and `destination` repositories. 112 | 113 | ``` 114 | labman login bradgarropy 123456 115 | labman label-source label-destination 116 | ``` 117 | 118 | ### `logout` 119 | 120 | Removes your GitHub [personal access token][token]. 121 | 122 | #### Examples 123 | 124 | ``` 125 | labman logout 126 | ``` 127 | 128 | ## ❔ Questions 129 | 130 | If you have any trouble, definitely [open an issue][issue] and I'll take a look. 131 | 132 | If all else fails, you can ask me directly on [Twitter][twitter] or my [AMA][ama]. 133 | 134 | [gif]: https://raw.githubusercontent.com/bradgarropy/labman-cli/master/usage.gif 135 | [npx]: https://www.npmjs.com/package/npx 136 | [token]: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line 137 | [issue]: https://github.com/bradgarropy/labman-cli/issues 138 | [twitter]: https://twitter.com/bradgarropy 139 | [ama]: https://github.com/bradgarropy/ama 140 | -------------------------------------------------------------------------------- /src/cli/default.js: -------------------------------------------------------------------------------- 1 | const config = require("../config") 2 | const {repoAutocomplete} = require("../utils") 3 | const { 4 | errorTokenNotFound, 5 | errorInvalidToken, 6 | errorRepoNotFound, 7 | } = require("../errors") 8 | const { 9 | createOctokit, 10 | validToken, 11 | validRepo, 12 | getLabels, 13 | deleteLabels, 14 | createLabels, 15 | } = require("../github") 16 | 17 | const command = "* [labels...]" 18 | const description = "Copy issue labels from one repo to another" 19 | 20 | const builder = { 21 | clobber: { 22 | alias: "c", 23 | type: "boolean", 24 | default: false, 25 | description: "Clobber destination labels", 26 | }, 27 | } 28 | 29 | const handler = async argv => { 30 | const token = config.get("token") 31 | 32 | // validate token 33 | if (!token) { 34 | errorTokenNotFound() 35 | return 36 | } 37 | 38 | const isValidToken = await validToken(token) 39 | 40 | // validate token 41 | if (!isValidToken) { 42 | errorInvalidToken() 43 | return 44 | } 45 | 46 | const source = repoAutocomplete(argv.source) 47 | const destination = repoAutocomplete(argv.destination) 48 | 49 | const isValidSource = await validRepo(source) 50 | 51 | // validate source 52 | if (!isValidSource) { 53 | errorRepoNotFound(source) 54 | return 55 | } 56 | 57 | const isValidDestination = await validRepo(destination) 58 | 59 | // validate destination 60 | if (!isValidDestination) { 61 | errorRepoNotFound(destination) 62 | return 63 | } 64 | 65 | createOctokit(token) 66 | const {labels, clobber} = argv 67 | 68 | // delete existing labels 69 | if (clobber) { 70 | const oldLabels = await getLabels(destination) 71 | await deleteLabels(oldLabels, destination) 72 | } 73 | 74 | // get new labels 75 | const sourceLabels = await getLabels(source) 76 | 77 | const newLabels = labels.length 78 | ? sourceLabels.filter(label => labels.includes(label.name)) 79 | : sourceLabels 80 | 81 | // create new labels 82 | await createLabels(newLabels, destination) 83 | } 84 | 85 | module.exports = { 86 | command, 87 | description, 88 | builder, 89 | handler, 90 | } 91 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const yargs = require("yargs") 4 | const {name} = require("../../package.json") 5 | 6 | yargs 7 | .scriptName(name) 8 | .command(require("./login")) 9 | .command(require("./logout")) 10 | .command(require("./default")) 11 | .help() 12 | .alias("help", "h") 13 | .alias("version", "v").argv 14 | -------------------------------------------------------------------------------- /src/cli/login.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk") 2 | const config = require("../config") 3 | const {validToken} = require("../github") 4 | const {errorLoginFailed} = require("../errors") 5 | 6 | const command = "login " 7 | const description = "Persist GitHub credentials" 8 | 9 | const builder = { 10 | force: { 11 | alias: "f", 12 | type: "boolean", 13 | default: false, 14 | description: "Force login", 15 | }, 16 | } 17 | 18 | const handler = async argv => { 19 | const {username, token, force} = argv 20 | const storedToken = config.get("token") 21 | 22 | if (!force && storedToken) { 23 | console.log("") 24 | console.log("You are already logged in!") 25 | return 26 | } 27 | 28 | const valid = await validToken(token) 29 | 30 | if (!valid) { 31 | errorLoginFailed() 32 | return 33 | } 34 | 35 | config.set({username, token}) 36 | 37 | console.log("") 38 | console.log(chalk.greenBright("Login successful!")) 39 | 40 | return 41 | } 42 | 43 | module.exports = { 44 | command, 45 | description, 46 | builder, 47 | handler, 48 | } 49 | -------------------------------------------------------------------------------- /src/cli/logout.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk") 2 | const config = require("../config") 3 | 4 | const command = "logout" 5 | const description = "Remove GitHub credentials" 6 | const builder = {} 7 | 8 | const handler = () => { 9 | config.clear() 10 | 11 | console.log("") 12 | console.log(chalk.greenBright("Logout successful!")) 13 | 14 | return 15 | } 16 | 17 | module.exports = { 18 | command, 19 | description, 20 | builder, 21 | handler, 22 | } 23 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const conf = require("conf") 2 | const {name} = require("../package.json") 3 | 4 | const options = { 5 | projectName: name, 6 | } 7 | 8 | const config = new conf(options) 9 | 10 | module.exports = config 11 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk") 2 | 3 | const errorTokenNotFound = () => { 4 | const commandText = chalk.cyanBright("login") 5 | 6 | console.log("") 7 | console.log(`You are not logged in, please run the ${commandText} command.`) 8 | console.log("") 9 | console.log(chalk.cyanBright("labman login ")) 10 | } 11 | 12 | const errorInvalidToken = () => { 13 | const errorText = chalk.redBright("Invalid token!") 14 | const commandText = chalk.cyanBright("login") 15 | 16 | console.log("") 17 | console.log(`${errorText} Please run the ${commandText} command again.`) 18 | console.log("") 19 | console.log(chalk.cyanBright("labman login ")) 20 | } 21 | 22 | const errorLoginFailed = () => { 23 | const errorText = chalk.redBright("Login failed!") 24 | 25 | console.log("") 26 | console.log(`${errorText} Please try again.`) 27 | } 28 | 29 | const errorRepoNotFound = repo => { 30 | const repoText = repo ? chalk.bold.cyanBright(` ${repo} `) : " " 31 | const errorText = chalk.bold.redBright( 32 | `Repository${repoText}does not exist!`, 33 | ) 34 | 35 | console.log("") 36 | console.log(errorText) 37 | } 38 | 39 | const errorLabelExists = label => { 40 | const labelText = label ? chalk.bold.cyanBright(` ${label} `) : " " 41 | const errorText = chalk.bold.redBright( 42 | ` x Label${labelText}already exists!`, 43 | ) 44 | 45 | console.log(errorText) 46 | } 47 | 48 | module.exports = { 49 | errorTokenNotFound, 50 | errorInvalidToken, 51 | errorLoginFailed, 52 | errorRepoNotFound, 53 | errorLabelExists, 54 | } 55 | -------------------------------------------------------------------------------- /src/github/index.js: -------------------------------------------------------------------------------- 1 | const labels = require("./labels") 2 | const octokit = require("./octokit") 3 | const validate = require("./validate") 4 | 5 | module.exports = { 6 | ...labels, 7 | ...octokit, 8 | ...validate, 9 | } 10 | -------------------------------------------------------------------------------- /src/github/labels.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk") 2 | const {repoObject} = require("../utils") 3 | const {getOctokit} = require("./octokit") 4 | const {errorLabelExists} = require("../errors") 5 | 6 | const getLabels = async repo => { 7 | const octokit = getOctokit() 8 | 9 | const parameters = repoObject(repo) 10 | const response = await octokit.issues.listLabelsForRepo(parameters) 11 | 12 | const labels = response.data 13 | return labels 14 | } 15 | 16 | const deleteLabels = async (labels, repo) => { 17 | console.log("") 18 | console.log(`Deleting labels from ${chalk.cyanBright(repo)}`) 19 | console.log("") 20 | 21 | const octokit = getOctokit() 22 | 23 | labels.forEach(label => { 24 | const {name} = label 25 | console.log(` ${chalk.bold.redBright("-")} ${name}`) 26 | 27 | const parameters = { 28 | ...repoObject(repo), 29 | name, 30 | } 31 | 32 | octokit.issues.deleteLabel(parameters) 33 | }) 34 | } 35 | 36 | const createLabels = async (labels, repo) => { 37 | console.log("") 38 | console.log(`Creating labels in ${chalk.cyanBright(repo)}`) 39 | console.log("") 40 | 41 | const octokit = getOctokit() 42 | 43 | labels.forEach(async label => { 44 | const {name, color, description} = label 45 | 46 | const parameters = { 47 | ...repoObject(repo), 48 | name, 49 | color, 50 | description, 51 | } 52 | 53 | try { 54 | await octokit.issues.createLabel(parameters) 55 | console.log(` ${chalk.bold.greenBright("+")} ${name}`) 56 | } catch (error) { 57 | errorLabelExists(name) 58 | } 59 | }) 60 | } 61 | 62 | module.exports = { 63 | getLabels, 64 | deleteLabels, 65 | createLabels, 66 | } 67 | -------------------------------------------------------------------------------- /src/github/octokit.js: -------------------------------------------------------------------------------- 1 | const {Octokit} = require("@octokit/rest") 2 | 3 | let octokit 4 | 5 | const createOctokit = token => { 6 | const options = {auth: token} 7 | octokit = new Octokit(options) 8 | return octokit 9 | } 10 | 11 | const getOctokit = () => octokit 12 | 13 | module.exports = { 14 | getOctokit, 15 | createOctokit, 16 | } 17 | -------------------------------------------------------------------------------- /src/github/validate.js: -------------------------------------------------------------------------------- 1 | const {repoObject} = require("../utils") 2 | const {createOctokit, getOctokit} = require("./octokit") 3 | 4 | const validToken = async token => { 5 | const octokit = createOctokit(token) 6 | 7 | try { 8 | await octokit.users.getAuthenticated() 9 | } catch (error) { 10 | return false 11 | } 12 | 13 | return true 14 | } 15 | 16 | const validRepo = async repo => { 17 | const octokit = getOctokit() 18 | const parameters = repoObject(repo) 19 | 20 | try { 21 | await octokit.repos.get(parameters) 22 | } catch (error) { 23 | return false 24 | } 25 | 26 | return true 27 | } 28 | 29 | module.exports = { 30 | validToken, 31 | validRepo, 32 | } 33 | -------------------------------------------------------------------------------- /src/tests/__snapshots__/errors.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`label exists with string 1`] = ` 4 | Array [ 5 | " x Label todo already exists!", 6 | ] 7 | `; 8 | 9 | exports[`label exists without string 1`] = ` 10 | Array [ 11 | " x Label already exists!", 12 | ] 13 | `; 14 | 15 | exports[`login failed 1`] = ` 16 | Array [ 17 | "", 18 | "Login failed! Please try again.", 19 | ] 20 | `; 21 | 22 | exports[`repo not found with string 1`] = ` 23 | Array [ 24 | "", 25 | "Repository bradgarropy/labman-cli does not exist!", 26 | ] 27 | `; 28 | 29 | exports[`repo not found without string 1`] = ` 30 | Array [ 31 | "", 32 | "Repository does not exist!", 33 | ] 34 | `; 35 | 36 | exports[`token invalid 1`] = ` 37 | Array [ 38 | "", 39 | "Invalid token! Please run the login command again.", 40 | "", 41 | "labman login ", 42 | ] 43 | `; 44 | 45 | exports[`token not found 1`] = ` 46 | Array [ 47 | "", 48 | "You are not logged in, please run the login command.", 49 | "", 50 | "labman login ", 51 | ] 52 | `; 53 | -------------------------------------------------------------------------------- /src/tests/cli/default.test.js: -------------------------------------------------------------------------------- 1 | const config = require("../../config") 2 | const {handler: logoutHandler} = require("../../cli/logout") 3 | const {handler: defaultHandler} = require("../../cli/default") 4 | const { 5 | errorTokenNotFound, 6 | errorInvalidToken, 7 | errorRepoNotFound, 8 | } = require("../../errors") 9 | const { 10 | validToken, 11 | validRepo, 12 | getLabels, 13 | deleteLabels, 14 | createLabels, 15 | } = require("../../github") 16 | 17 | jest.mock("../../errors") 18 | jest.mock("../../github") 19 | 20 | beforeEach(() => logoutHandler()) 21 | 22 | describe("default", () => { 23 | test("token not found", async () => { 24 | const args = { 25 | source: "bradgarropy/label-source", 26 | destination: "bradgarropy/label-destination", 27 | labels: [], 28 | clobber: false, 29 | } 30 | 31 | await defaultHandler(args) 32 | expect(errorTokenNotFound).toHaveBeenCalledTimes(1) 33 | }) 34 | 35 | test("invalid token", async () => { 36 | validToken.mockImplementation(() => false) 37 | 38 | const stored = { 39 | username: "bradgarropy", 40 | token: "123456", 41 | } 42 | 43 | config.set(stored) 44 | 45 | const args = { 46 | source: "bradgarropy/label-source", 47 | destination: "bradgarropy/label-destination", 48 | labels: [], 49 | clobber: false, 50 | } 51 | 52 | await defaultHandler(args) 53 | expect(errorInvalidToken).toHaveBeenCalledTimes(1) 54 | }) 55 | 56 | test("without owner", async () => { 57 | validToken.mockImplementation(() => true) 58 | validRepo.mockImplementation(() => true) 59 | 60 | const stored = { 61 | username: "bradgarropy", 62 | token: "123456", 63 | } 64 | 65 | config.set(stored) 66 | 67 | const args = { 68 | source: "label-source", 69 | destination: "label-destination", 70 | labels: [], 71 | clobber: false, 72 | } 73 | 74 | await defaultHandler(args) 75 | 76 | expect(validRepo).toHaveBeenCalledTimes(2) 77 | expect(validRepo).toHaveBeenCalledWith("bradgarropy/label-source") 78 | expect(validRepo).toHaveBeenCalledWith("bradgarropy/label-destination") 79 | }) 80 | 81 | test("invalid source", async () => { 82 | validToken.mockImplementation(() => true) 83 | validRepo.mockImplementationOnce(() => false) 84 | 85 | const stored = { 86 | username: "bradgarropy", 87 | token: "123456", 88 | } 89 | 90 | config.set(stored) 91 | 92 | const args = { 93 | source: "bradgarropy/invalid-source", 94 | destination: "bradgarropy/label-destination", 95 | labels: [], 96 | clobber: false, 97 | } 98 | 99 | await defaultHandler(args) 100 | 101 | expect(validRepo).toHaveBeenCalledTimes(1) 102 | expect(validRepo).toHaveBeenCalledWith(args.source) 103 | expect(errorRepoNotFound).toHaveBeenCalledTimes(1) 104 | }) 105 | 106 | test("invalid destination", async () => { 107 | validToken.mockImplementation(() => true) 108 | validRepo 109 | .mockImplementationOnce(() => true) 110 | .mockImplementationOnce(() => false) 111 | 112 | const stored = { 113 | username: "bradgarropy", 114 | token: "123456", 115 | } 116 | 117 | config.set(stored) 118 | 119 | const args = { 120 | source: "bradgarropy/label-source", 121 | destination: "bradgarropy/invalid-destination", 122 | labels: [], 123 | clobber: false, 124 | } 125 | 126 | await defaultHandler(args) 127 | 128 | expect(validRepo).toHaveBeenCalledTimes(2) 129 | expect(validRepo).toHaveBeenNthCalledWith(2, args.destination) 130 | expect(errorRepoNotFound).toHaveBeenCalledTimes(1) 131 | }) 132 | 133 | test("with labels", async () => { 134 | const argLabels = ["bug", "todo"] 135 | 136 | const existingLabels = [ 137 | {name: "bug"}, 138 | {name: "enhance"}, 139 | {name: "explore"}, 140 | {name: "todo"}, 141 | ] 142 | 143 | const newLabels = [{name: "bug"}, {name: "todo"}] 144 | 145 | validToken.mockImplementation(() => true) 146 | validRepo.mockImplementation(() => true) 147 | getLabels.mockImplementation(() => existingLabels) 148 | 149 | const stored = { 150 | username: "bradgarropy", 151 | token: "123456", 152 | } 153 | 154 | config.set(stored) 155 | 156 | const args = { 157 | source: "bradgarropy/label-source", 158 | destination: "bradgarropy/label-destination", 159 | labels: argLabels, 160 | clobber: false, 161 | } 162 | 163 | await defaultHandler(args) 164 | 165 | expect(getLabels).toHaveBeenCalledTimes(1) 166 | expect(getLabels).toHaveBeenNthCalledWith(1, args.source) 167 | expect(createLabels).toHaveBeenCalledTimes(1) 168 | expect(createLabels).toHaveBeenNthCalledWith( 169 | 1, 170 | newLabels, 171 | args.destination, 172 | ) 173 | }) 174 | 175 | test("clobber", async () => { 176 | const labels = [ 177 | {name: "bug"}, 178 | {name: "enhance"}, 179 | {name: "explore"}, 180 | {name: "todo"}, 181 | ] 182 | 183 | validToken.mockImplementation(() => true) 184 | validRepo.mockImplementation(() => true) 185 | getLabels.mockImplementation(() => labels) 186 | 187 | const stored = { 188 | username: "bradgarropy", 189 | token: "123456", 190 | } 191 | 192 | config.set(stored) 193 | 194 | const args = { 195 | source: "bradgarropy/label-source", 196 | destination: "bradgarropy/label-destination", 197 | labels: [], 198 | clobber: true, 199 | } 200 | 201 | await defaultHandler(args) 202 | 203 | expect(getLabels).toHaveBeenCalledTimes(2) 204 | expect(getLabels).toHaveBeenNthCalledWith(1, args.destination) 205 | expect(deleteLabels).toHaveBeenCalledTimes(1) 206 | expect(deleteLabels).toHaveBeenNthCalledWith( 207 | 1, 208 | labels, 209 | args.destination, 210 | ) 211 | }) 212 | }) 213 | -------------------------------------------------------------------------------- /src/tests/cli/login.test.js: -------------------------------------------------------------------------------- 1 | const config = require("../../config") 2 | const {validToken} = require("../../github") 3 | const {errorLoginFailed} = require("../../errors") 4 | const {handler: loginHandler} = require("../../cli/login") 5 | const {handler: logoutHandler} = require("../../cli/logout") 6 | 7 | jest.mock("../../errors") 8 | jest.mock("../../github") 9 | 10 | beforeEach(() => logoutHandler()) 11 | 12 | describe("login", () => { 13 | test("valid token", async () => { 14 | validToken.mockImplementation(() => true) 15 | 16 | const args = { 17 | username: "bradgarropy", 18 | token: "123456", 19 | force: false, 20 | } 21 | 22 | await loginHandler(args) 23 | 24 | expect(validToken).toHaveBeenCalled() 25 | expect(errorLoginFailed).not.toHaveBeenCalled() 26 | expect(config.get("username")).toEqual(args.username) 27 | expect(config.get("token")).toEqual(args.token) 28 | }) 29 | 30 | test("invalid token", async () => { 31 | validToken.mockImplementation(() => false) 32 | 33 | const args = { 34 | username: "bradgarropy", 35 | token: "123456", 36 | force: false, 37 | } 38 | 39 | await loginHandler(args) 40 | 41 | expect(validToken).toHaveBeenCalled() 42 | expect(errorLoginFailed).toHaveBeenCalled() 43 | expect(config.get("username")).toBeUndefined() 44 | expect(config.get("token")).toBeUndefined() 45 | }) 46 | 47 | test("existing token", async () => { 48 | const stored = { 49 | username: "bradgarropy", 50 | token: "456789", 51 | } 52 | 53 | config.set(stored) 54 | 55 | const args = { 56 | username: "bradgarropy", 57 | token: "123456", 58 | force: false, 59 | } 60 | 61 | await loginHandler(args) 62 | 63 | expect(validToken).not.toHaveBeenCalled() 64 | expect(errorLoginFailed).not.toHaveBeenCalled() 65 | expect(config.get("username")).toEqual(stored.username) 66 | expect(config.get("token")).toEqual(stored.token) 67 | }) 68 | 69 | test("force", async () => { 70 | validToken.mockImplementation(() => true) 71 | 72 | const stored = { 73 | username: "bradgarropy", 74 | token: "456789", 75 | } 76 | 77 | config.set(stored) 78 | 79 | const args = { 80 | username: "bradgarropy", 81 | token: "123456", 82 | force: true, 83 | } 84 | 85 | await loginHandler(args) 86 | 87 | expect(validToken).toHaveBeenCalled() 88 | expect(errorLoginFailed).not.toHaveBeenCalled() 89 | expect(config.get("username")).toEqual(args.username) 90 | expect(config.get("token")).toEqual(args.token) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /src/tests/cli/logout.test.js: -------------------------------------------------------------------------------- 1 | const config = require("../../config") 2 | const {handler} = require("../../cli/logout") 3 | 4 | describe("logout", () => { 5 | test("logout", () => { 6 | const stored = { 7 | username: "bradgarropy", 8 | token: "456789", 9 | } 10 | 11 | config.set(stored) 12 | 13 | handler() 14 | expect(config.get("username")).toBeUndefined() 15 | expect(config.get("token")).toBeUndefined() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/tests/config.test.js: -------------------------------------------------------------------------------- 1 | const config = require("../config") 2 | const {name} = require("../../package.json") 3 | 4 | describe("config", () => { 5 | test("config", () => { 6 | expect(config).toHaveProperty("path", expect.stringContaining(name)) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/tests/errors.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | errorTokenNotFound, 3 | errorInvalidToken, 4 | errorLoginFailed, 5 | errorRepoNotFound, 6 | errorLabelExists, 7 | } = require("../errors") 8 | 9 | let logs 10 | const consoleLog = console.log 11 | 12 | beforeEach(() => { 13 | logs = [] 14 | console.log = message => logs.push(message) 15 | }) 16 | 17 | afterEach(() => (console.log = consoleLog)) 18 | 19 | describe("token", () => { 20 | test("not found", () => { 21 | errorTokenNotFound() 22 | expect(logs).toMatchSnapshot() 23 | }) 24 | 25 | test("invalid", () => { 26 | errorInvalidToken() 27 | expect(logs).toMatchSnapshot() 28 | }) 29 | }) 30 | 31 | describe("login ", () => { 32 | test("failed", () => { 33 | errorLoginFailed() 34 | expect(logs).toMatchSnapshot() 35 | }) 36 | }) 37 | 38 | describe("repo not found", () => { 39 | test("with string", () => { 40 | errorRepoNotFound("bradgarropy/labman-cli") 41 | expect(logs).toMatchSnapshot() 42 | }) 43 | 44 | test("without string", () => { 45 | errorRepoNotFound("") 46 | expect(logs).toMatchSnapshot() 47 | }) 48 | }) 49 | 50 | describe("label exists", () => { 51 | test("with string", () => { 52 | errorLabelExists("todo") 53 | expect(logs).toMatchSnapshot() 54 | }) 55 | 56 | test("without string", () => { 57 | errorLabelExists("") 58 | expect(logs).toMatchSnapshot() 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/tests/github/labels.test.js: -------------------------------------------------------------------------------- 1 | const {getOctokit} = require("../../github/octokit") 2 | const {getLabels, deleteLabels, createLabels} = require("../../github/labels") 3 | 4 | jest.mock("../../github/octokit") 5 | 6 | describe("labels", () => { 7 | test("get", async () => { 8 | const expectedLabels = [ 9 | {name: "bug"}, 10 | {name: "enhance"}, 11 | {name: "todo"}, 12 | ] 13 | 14 | getOctokit.mockImplementation(() => ({ 15 | issues: { 16 | listLabelsForRepo: () => { 17 | const labels = {data: expectedLabels} 18 | return labels 19 | }, 20 | }, 21 | })) 22 | 23 | const labels = await getLabels("bradgarropy/label-source") 24 | 25 | expect(labels).toEqual(expectedLabels) 26 | }) 27 | 28 | test("delete", async () => { 29 | getOctokit.mockImplementation(() => ({ 30 | issues: { 31 | deleteLabel: jest.fn(), 32 | }, 33 | })) 34 | 35 | const actual = await deleteLabels( 36 | ["bug", "enhance", "todo"], 37 | "bradgarropy/label-destination", 38 | ) 39 | 40 | expect(actual).toBeUndefined() 41 | }) 42 | 43 | test("create", async () => { 44 | getOctokit.mockImplementation(() => ({ 45 | issues: { 46 | createLabel: jest.fn(), 47 | }, 48 | })) 49 | 50 | const actual = await createLabels( 51 | ["bug", "enhance", "todo"], 52 | "bradgarropy/label-destination", 53 | ) 54 | 55 | expect(actual).toBeUndefined() 56 | }) 57 | 58 | test("label exists", async () => { 59 | getOctokit.mockImplementation(() => ({ 60 | issues: { 61 | createLabel: () => { 62 | throw "Label exists!" 63 | }, 64 | }, 65 | })) 66 | 67 | const actual = await createLabels( 68 | ["bug", "enhance", "todo"], 69 | "bradgarropy/label-destination", 70 | ) 71 | 72 | expect(actual).toBeUndefined() 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/tests/github/octokit.test.js: -------------------------------------------------------------------------------- 1 | const {createOctokit, getOctokit} = require("../../github") 2 | 3 | describe("octokit", () => { 4 | test("create", () => { 5 | const octokit = createOctokit("123456") 6 | 7 | const keys = Object.keys(octokit) 8 | 9 | expect(keys).toContainEqual("users") 10 | expect(keys).toContainEqual("repos") 11 | expect(keys).toContainEqual("issues") 12 | }) 13 | 14 | test("get", () => { 15 | const octokit = getOctokit() 16 | 17 | const keys = Object.keys(octokit) 18 | 19 | expect(keys).toContainEqual("users") 20 | expect(keys).toContainEqual("repos") 21 | expect(keys).toContainEqual("issues") 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/tests/github/validate.test.js: -------------------------------------------------------------------------------- 1 | const {validToken, validRepo} = require("../../github/validate") 2 | const {createOctokit, getOctokit} = require("../../github/octokit") 3 | 4 | jest.mock("../../github/octokit") 5 | 6 | describe("validate", () => { 7 | test("valid token", async () => { 8 | createOctokit.mockImplementation(() => ({ 9 | users: { 10 | getAuthenticated: () => true, 11 | }, 12 | })) 13 | 14 | const isValidToken = await validToken("123456") 15 | expect(isValidToken).toBeTruthy() 16 | }) 17 | 18 | test("invalid token", async () => { 19 | createOctokit.mockImplementation(() => ({ 20 | users: { 21 | getAuthenticated: () => { 22 | throw "Invalid token!" 23 | }, 24 | }, 25 | })) 26 | 27 | const isValidToken = await validToken("123456") 28 | expect(isValidToken).not.toBeTruthy() 29 | }) 30 | 31 | test("valid repo", async () => { 32 | getOctokit.mockImplementation(() => ({ 33 | repos: { 34 | get: () => true, 35 | }, 36 | })) 37 | 38 | const isValidRepo = await validRepo("bradgarropy/labman-cli") 39 | expect(isValidRepo).toBeTruthy() 40 | }) 41 | 42 | test("invalid repo", async () => { 43 | getOctokit.mockImplementation(() => ({ 44 | repos: { 45 | get: () => { 46 | throw "Invalid token!" 47 | }, 48 | }, 49 | })) 50 | 51 | const isValidRepo = await validRepo("bradgarropy/invalid-repo") 52 | expect(isValidRepo).not.toBeTruthy() 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/tests/utils.test.js: -------------------------------------------------------------------------------- 1 | const config = require("../config") 2 | const {repoPath, repoObject, repoAutocomplete} = require("../utils") 3 | 4 | describe("repo path", () => { 5 | test("object", () => { 6 | const object = { 7 | owner: "bradgarropy", 8 | repo: "labman-cli", 9 | } 10 | 11 | const path = repoPath(object) 12 | expect(path).toEqual("bradgarropy/labman-cli") 13 | }) 14 | 15 | test("empty object", () => { 16 | const object = {} 17 | 18 | const path = repoPath(object) 19 | expect(path).toEqual("") 20 | }) 21 | 22 | test("no owner", () => { 23 | const object = { 24 | owner: "", 25 | repo: "labman-cli", 26 | } 27 | 28 | const path = repoPath(object) 29 | expect(path).toEqual("") 30 | }) 31 | 32 | test("no repo", () => { 33 | const object = { 34 | owner: "bradgarropy", 35 | repo: "", 36 | } 37 | 38 | const path = repoPath(object) 39 | expect(path).toEqual("") 40 | }) 41 | }) 42 | 43 | describe("repo object", () => { 44 | test("string", () => { 45 | const expected = { 46 | owner: "bradgarropy", 47 | repo: "labman-cli", 48 | } 49 | 50 | const object = repoObject("bradgarropy/labman-cli") 51 | expect(object).toEqual(expected) 52 | }) 53 | 54 | test("empty string", () => { 55 | const expected = { 56 | owner: "", 57 | repo: "", 58 | } 59 | 60 | const object = repoObject("") 61 | expect(object).toEqual(expected) 62 | }) 63 | 64 | test("no owner", () => { 65 | const expected = { 66 | owner: "", 67 | repo: "", 68 | } 69 | 70 | const object = repoObject("") 71 | expect(object).toEqual(expected) 72 | }) 73 | 74 | test("no repo", () => { 75 | const expected = { 76 | owner: "", 77 | repo: "", 78 | } 79 | 80 | const object = repoObject("") 81 | expect(object).toEqual(expected) 82 | }) 83 | }) 84 | 85 | describe("repo autocomplete", () => { 86 | const stored = { 87 | username: "bradgarropy", 88 | token: "123456", 89 | } 90 | 91 | config.set(stored) 92 | 93 | test("with owner", () => { 94 | const repo = repoAutocomplete("bradgarropy/labman-cli") 95 | expect(repo).toEqual("bradgarropy/labman-cli") 96 | }) 97 | 98 | test("without owner", () => { 99 | const repo = repoAutocomplete("labman-cli") 100 | expect(repo).toEqual("bradgarropy/labman-cli") 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const config = require("./config") 2 | 3 | const repoPath = object => { 4 | const {owner, repo} = object 5 | 6 | if (!owner || !repo) { 7 | return "" 8 | } 9 | 10 | const path = `${owner}/${repo}` 11 | return path 12 | } 13 | 14 | const repoObject = string => { 15 | const [owner, repo] = string.split("/") 16 | 17 | if (!owner || !repo) { 18 | return { 19 | owner: "", 20 | repo: "", 21 | } 22 | } 23 | 24 | const object = { 25 | owner, 26 | repo, 27 | } 28 | 29 | return object 30 | } 31 | 32 | const repoAutocomplete = repo => { 33 | if (repo.includes("/")) { 34 | return repo 35 | } 36 | 37 | const owner = config.get("username") 38 | 39 | const autocompleted = `${owner}/${repo}` 40 | return autocompleted 41 | } 42 | 43 | module.exports = {repoPath, repoObject, repoAutocomplete} 44 | -------------------------------------------------------------------------------- /usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradgarropy/labman-cli/b50b8d4985ec7901d4625866f66127ff5161fb73/usage.gif --------------------------------------------------------------------------------