├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 | "[1m[91m x Label[1m[96m todo [39m[91m[22m[1malready exists![39m[22m",
6 | ]
7 | `;
8 |
9 | exports[`label exists without string 1`] = `
10 | Array [
11 | "[1m[91m x Label already exists![39m[22m",
12 | ]
13 | `;
14 |
15 | exports[`login failed 1`] = `
16 | Array [
17 | "",
18 | "[91mLogin failed![39m Please try again.",
19 | ]
20 | `;
21 |
22 | exports[`repo not found with string 1`] = `
23 | Array [
24 | "",
25 | "[1m[91mRepository[1m[96m bradgarropy/labman-cli [39m[91m[22m[1mdoes not exist![39m[22m",
26 | ]
27 | `;
28 |
29 | exports[`repo not found without string 1`] = `
30 | Array [
31 | "",
32 | "[1m[91mRepository does not exist![39m[22m",
33 | ]
34 | `;
35 |
36 | exports[`token invalid 1`] = `
37 | Array [
38 | "",
39 | "[91mInvalid token![39m Please run the [96mlogin[39m command again.",
40 | "",
41 | "[96mlabman login [39m",
42 | ]
43 | `;
44 |
45 | exports[`token not found 1`] = `
46 | Array [
47 | "",
48 | "You are not logged in, please run the [96mlogin[39m command.",
49 | "",
50 | "[96mlabman login [39m",
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
--------------------------------------------------------------------------------