@airscript/ghitgud 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.env.base +2 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/FUNDING.yml +2 -0
  4. package/.github/ISSUE_TEMPLATE/build.md +9 -0
  5. package/.github/ISSUE_TEMPLATE/chore.md +9 -0
  6. package/.github/ISSUE_TEMPLATE/ci.md +9 -0
  7. package/.github/ISSUE_TEMPLATE/documentation.md +9 -0
  8. package/.github/ISSUE_TEMPLATE/feature.md +9 -0
  9. package/.github/ISSUE_TEMPLATE/fix.md +9 -0
  10. package/.github/ISSUE_TEMPLATE/performance.md +9 -0
  11. package/.github/ISSUE_TEMPLATE/refactor.md +9 -0
  12. package/.github/ISSUE_TEMPLATE/style.md +9 -0
  13. package/.github/ISSUE_TEMPLATE/test.md +9 -0
  14. package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
  15. package/.github/workflows/tests.yml +36 -0
  16. package/CHANGELOG.md +4 -0
  17. package/CITATION.cff +13 -0
  18. package/CODE_OF_CONDUCT.md +121 -0
  19. package/CONTRIBUTING.md +2 -0
  20. package/LICENSE +674 -0
  21. package/README.md +54 -0
  22. package/SECURITY.md +9 -0
  23. package/VERSION +1 -0
  24. package/app/api.ts +136 -0
  25. package/app/ascii.ts +18 -0
  26. package/app/commands.ts +117 -0
  27. package/app/config.ts +9 -0
  28. package/app/functions.ts +43 -0
  29. package/app/ghitgud.ts +18 -0
  30. package/app/library.ts +158 -0
  31. package/app/types.ts +8 -0
  32. package/dist/app/api.js +127 -0
  33. package/dist/app/ascii.js +20 -0
  34. package/dist/app/commands.js +89 -0
  35. package/dist/app/config.js +12 -0
  36. package/dist/app/functions.js +37 -0
  37. package/dist/app/ghitgud.js +19 -0
  38. package/dist/app/library.js +123 -0
  39. package/dist/app/types.js +2 -0
  40. package/dist/tests/library.test.js +84 -0
  41. package/package.json +38 -0
  42. package/scripts/clean.sh +2 -0
  43. package/templates/base.json +12 -0
  44. package/templates/conventional.json +53 -0
  45. package/templates/github.json +47 -0
  46. package/tests/library.test.ts +78 -0
  47. package/tsconfig.json +113 -0
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Ghitgud
2
+ A simple CLI to give superpowers to GitHub.
3
+
4
+ ## Table of Contents
5
+ - [Installation](#installation)
6
+ - [Usage](#usage)
7
+ - [Contributing](#contributing)
8
+ - [Support](#support)
9
+ - [License](#license)
10
+
11
+ ## Installation
12
+ Follow the steps below to make use of Ghitgud.
13
+
14
+ Clone this repository:
15
+ ```bash
16
+ git clone https://github.com/airscripts/ghitgud.git
17
+ ```
18
+
19
+ ## Usage
20
+ After cloning the project just hit these few commands:
21
+ ```bash
22
+ pnpm install
23
+ pnpm run build
24
+ pnpm link
25
+ ```
26
+
27
+ Then you'll be able to access the CLI and its relative help command:
28
+ ```bash
29
+ ghitgud help
30
+ ```
31
+
32
+ Remember that to use the CLI you have to set a token and a repo with the format `username/repository` (e.g. airscripts/ghitgud):
33
+ ```bash
34
+ ghitgud config set token `your-token-here`
35
+ ghitgud config set repo `username/repository`
36
+ ```
37
+
38
+ ## Contributing
39
+ Contributions and suggestions about how to improve this project are welcome!
40
+ Please follow [our contribution guidelines](https://github.com/airscripts/ghitgud/blob/main/CONTRIBUTING.md).
41
+
42
+ ## Support
43
+ If you want to support my work you can do it by following me, leaving a star, sharing my projects or also donating at the links below.
44
+ Choose what you find more suitable for you:
45
+
46
+ <a href="https://sponsor.airscript.it" target="blank">
47
+ <img src="https://raw.githubusercontent.com/airscripts/assets/main/images/github-sponsors.svg" alt="GitHub Sponsors" width="30px" />
48
+ </a>&nbsp;
49
+ <a href="https://kofi.airscript.it" target="blank">
50
+ <img src="https://raw.githubusercontent.com/airscripts/assets/main/images/kofi.svg" alt="Kofi" width="30px" />
51
+ </a>
52
+
53
+ ## License
54
+ This repository is licensed under [GPL-3.0 License](https://github.com/airscripts/ghitgud/blob/main/LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,9 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+ | Version | Supported |
5
+ | ------- | ------------------ |
6
+ | 1.0.x | :white_check_mark: |
7
+
8
+ ## Reporting Vulnerability
9
+ To report a vulnerability, open an [issue](https://github.com/airscripts/ghitgud/issues/new/choose).
package/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
package/app/api.ts ADDED
@@ -0,0 +1,136 @@
1
+ import config from "./config";
2
+ import { Label } from "./types";
3
+ import functions from "./functions";
4
+ import "dotenv/config";
5
+
6
+ const VERSION = "2022-11-28";
7
+ const BASE_URL = "https://api.github.com";
8
+ const ACCEPT = "application/vnd.github+json";
9
+ const REPO = `${config.repo}`;
10
+ const AUTHORIZATION = `Bearer ${config.token}`;
11
+
12
+ const ERROR_UNAUTHORIZED = "Unauthorized.";
13
+ const ERROR_UNPROCESSABLE = "Content is unprocessable.";
14
+ const ERROR_NO_REPO = "You must set the GHITGUD_GITHUB_REPO environment variable.";
15
+ const ERROR_NO_TOKEN = "You must set the GHITGUD_GITHUB_TOKEN environment variable.";
16
+
17
+ const labels = {
18
+ fetch: async () => {
19
+ if (!functions.environment.hasRepo()) throw new Error(ERROR_NO_REPO);
20
+ if (!functions.environment.hasToken()) throw new Error(ERROR_NO_TOKEN);
21
+
22
+ const response = await fetch(`${BASE_URL}/repos/${REPO}/labels`, {
23
+ headers: {
24
+ Accept: ACCEPT,
25
+ Authorization: AUTHORIZATION,
26
+ "X-GitHub-Api-Version": VERSION,
27
+ },
28
+ });
29
+
30
+ if (functions.http.isNotAuthorized(response.status))
31
+ throw new Error(ERROR_UNAUTHORIZED);
32
+
33
+ return response;
34
+ },
35
+
36
+ get: async (name: string) => {
37
+ if (!functions.environment.hasRepo()) throw new Error(ERROR_NO_REPO);
38
+ if (!functions.environment.hasToken()) throw new Error(ERROR_NO_TOKEN);
39
+
40
+ const response = await fetch(`${BASE_URL}/repos/${REPO}/labels/${name}`, {
41
+ method: "GET",
42
+ headers: {
43
+ Accept: ACCEPT,
44
+ Authorization: AUTHORIZATION,
45
+ "X-GitHub-Api-Version": VERSION,
46
+ },
47
+ });
48
+
49
+ if (functions.http.isNotAuthorized(response.status))
50
+ throw new Error(ERROR_UNAUTHORIZED);
51
+
52
+ return response;
53
+ },
54
+
55
+ create: async (label: Label) => {
56
+ if (!functions.environment.hasRepo()) throw new Error(ERROR_NO_REPO);
57
+ if (!functions.environment.hasToken()) throw new Error(ERROR_NO_TOKEN);
58
+
59
+ const response = await fetch(`${BASE_URL}/repos/${REPO}/labels`, {
60
+ method: "POST",
61
+
62
+ body: JSON.stringify({
63
+ name: label.name,
64
+ color: label.color,
65
+ description: label.description,
66
+ }),
67
+
68
+ headers: {
69
+ Accept: ACCEPT,
70
+ Authorization: AUTHORIZATION,
71
+ "X-GitHub-Api-Version": VERSION,
72
+ },
73
+ });
74
+
75
+ if (functions.http.isUnprocessable(response.status))
76
+ throw new Error(ERROR_UNPROCESSABLE);
77
+
78
+ if (functions.http.isNotAuthorized(response.status))
79
+ throw new Error(ERROR_UNAUTHORIZED);
80
+
81
+ return response;
82
+ },
83
+
84
+ patch: async (label: Label) => {
85
+ if (!functions.environment.hasRepo()) throw new Error(ERROR_NO_REPO);
86
+ if (!functions.environment.hasToken()) throw new Error(ERROR_NO_TOKEN);
87
+
88
+ const response = await fetch(
89
+ `${BASE_URL}/repos/${REPO}/labels/${label.name}`,
90
+ {
91
+ method: "PATCH",
92
+
93
+ body: JSON.stringify({
94
+ color: label.color,
95
+ description: label.description,
96
+ new_name: label.newName || label.name,
97
+ }),
98
+
99
+ headers: {
100
+ Accept: ACCEPT,
101
+ Authorization: AUTHORIZATION,
102
+ "X-GitHub-Api-Version": VERSION,
103
+ },
104
+ }
105
+ );
106
+
107
+ if (functions.http.isNotAuthorized(response.status))
108
+ throw new Error(ERROR_UNAUTHORIZED);
109
+
110
+ return response;
111
+ },
112
+
113
+ delete: async (name: string) => {
114
+ if (!functions.environment.hasRepo()) throw new Error(ERROR_NO_REPO);
115
+ if (!functions.environment.hasToken()) throw new Error(ERROR_NO_TOKEN);
116
+
117
+ const response = await fetch(`${BASE_URL}/repos/${REPO}/labels/${name}`, {
118
+ method: "DELETE",
119
+
120
+ headers: {
121
+ Accept: ACCEPT,
122
+ Authorization: AUTHORIZATION,
123
+ "X-GitHub-Api-Version": VERSION,
124
+ },
125
+ });
126
+
127
+ if (functions.http.isNotAuthorized(response.status))
128
+ throw new Error(ERROR_UNAUTHORIZED);
129
+
130
+ return response;
131
+ },
132
+ };
133
+
134
+ export default {
135
+ labels,
136
+ };
package/app/ascii.ts ADDED
@@ -0,0 +1,18 @@
1
+ import figlet from "figlet";
2
+
3
+ const WIDTH = 80;
4
+ const TITLE = "Ghitgud";
5
+ const FONT = "Standard";
6
+ const WHITESPACE_BREAK = true;
7
+ const VERTICAL_LAYOUT = "default";
8
+ const HORIZONTAL_LAYOUT = "default";
9
+
10
+ const ascii = figlet.textSync(TITLE, {
11
+ font: FONT,
12
+ width: WIDTH,
13
+ verticalLayout: VERTICAL_LAYOUT,
14
+ whitespaceBreak: WHITESPACE_BREAK,
15
+ horizontalLayout: HORIZONTAL_LAYOUT,
16
+ });
17
+
18
+ export default ascii;
@@ -0,0 +1,117 @@
1
+ import { program, Command } from "commander";
2
+ import library from "./library";
3
+
4
+ const COMMANDS = {
5
+ ping: {
6
+ name: "ping",
7
+ action: () => void library.ping(),
8
+ description: "Check if the CLI is working.",
9
+ },
10
+
11
+ labels: {
12
+ name: "labels",
13
+ description: "Manage labels for a repository.",
14
+
15
+ commands: {
16
+ list: {
17
+ name: "list",
18
+ description: "List all labels for a repository.",
19
+ action: () => void library.labels.list(),
20
+ },
21
+
22
+ pull: {
23
+ name: "pull",
24
+ description: "Pull all related labels for a repository.",
25
+ action: () => void library.labels.pull(),
26
+ },
27
+
28
+ push: {
29
+ name: "push",
30
+ description: "Push all related labels for a repository.",
31
+ action: () => void library.labels.push(),
32
+ },
33
+
34
+ prune: {
35
+ name: "prune",
36
+ description: "Prune all related labels for a repository.",
37
+ action: () => void library.labels.prune(),
38
+ },
39
+ },
40
+ },
41
+
42
+ config: {
43
+ name: "config",
44
+ description: "Set CLI configurations.",
45
+
46
+ commands: {
47
+ set: {
48
+ name: "set",
49
+ description: "Set configuration.",
50
+
51
+ action: (key: string, value: string) =>
52
+ void library.config.set(key, value),
53
+ },
54
+ },
55
+ },
56
+ };
57
+
58
+ const ping = () => {
59
+ program
60
+ .command(COMMANDS.ping.name)
61
+ .description(COMMANDS.ping.description)
62
+ .action(COMMANDS.ping.action);
63
+ };
64
+
65
+ const labels = () => {
66
+ const labels = program
67
+ .command(COMMANDS.labels.name)
68
+ .description(COMMANDS.labels.description);
69
+
70
+ labels.addCommand(
71
+ new Command(COMMANDS.labels.commands.list.name)
72
+ .description(COMMANDS.labels.commands.list.description)
73
+ .action(COMMANDS.labels.commands.list.action)
74
+ );
75
+
76
+ labels.addCommand(
77
+ new Command(COMMANDS.labels.commands.pull.name)
78
+ .description(COMMANDS.labels.commands.pull.description)
79
+ .action(COMMANDS.labels.commands.pull.action)
80
+ );
81
+
82
+ labels.addCommand(
83
+ new Command(COMMANDS.labels.commands.push.name)
84
+ .description(COMMANDS.labels.commands.push.description)
85
+ .action(COMMANDS.labels.commands.push.action)
86
+ );
87
+
88
+ labels.addCommand(
89
+ new Command(COMMANDS.labels.commands.prune.name)
90
+ .description(COMMANDS.labels.commands.prune.description)
91
+ .action(COMMANDS.labels.commands.prune.action)
92
+ );
93
+ };
94
+
95
+ const config = () => {
96
+ const config = program
97
+ .command(COMMANDS.config.name)
98
+ .description(COMMANDS.config.description);
99
+
100
+ config.addCommand(
101
+ new Command(COMMANDS.config.commands.set.name)
102
+ .description(COMMANDS.config.commands.set.description)
103
+ .arguments("<key> <value>")
104
+
105
+ .action((key: string, value: string) =>
106
+ COMMANDS.config.commands.set.action(key, value)
107
+ )
108
+ );
109
+ };
110
+
111
+ const init = () => {
112
+ ping();
113
+ labels();
114
+ config();
115
+ };
116
+
117
+ export default init;
package/app/config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import functions from "./functions";
2
+ import "dotenv/config";
3
+
4
+ const config = {
5
+ repo: process.env.GHITGUD_GITHUB_REPO || functions?.config.read("repo"),
6
+ token: process.env.GHITGUD_GITHUB_TOKEN || functions?.config.read("token"),
7
+ };
8
+
9
+ export default config;
@@ -0,0 +1,43 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ import conf from "./config";
6
+ import "dotenv/config";
7
+
8
+ const STATUS_OK = 200;
9
+ const STATUS_UNAUTHORIZED = 401;
10
+ const STATUS_NOT_FOUND = 404;
11
+ const STATUS_UNPROCESSABLE = 422;
12
+
13
+ const ENCODING = "utf8";
14
+ const CREDENTIALS_FILE = "credentials.json";
15
+ const GHITGUD_FOLDER = path.join(os.homedir(), ".config", "ghitgud");
16
+
17
+ const http = {
18
+ isOk: (status: number) => status === STATUS_OK,
19
+ isNotFound: (status: number) => status === STATUS_NOT_FOUND,
20
+ isNotAuthorized: (status: number) => status === STATUS_UNAUTHORIZED,
21
+ isUnprocessable: (status: number) => status === STATUS_UNPROCESSABLE,
22
+ };
23
+
24
+ const environment = {
25
+ hasRepo: () => (conf.repo ? true : false),
26
+ hasToken: () => (conf.token ? true : false),
27
+ };
28
+
29
+ const config = {
30
+ read: (key: string) => {
31
+ if (!fs.existsSync(`${GHITGUD_FOLDER}/${CREDENTIALS_FILE}`)) return null;
32
+
33
+ const data = fs.readFileSync(
34
+ `${GHITGUD_FOLDER}/${CREDENTIALS_FILE}`,
35
+ ENCODING
36
+ );
37
+
38
+ const content = JSON.parse(data);
39
+ return content[key];
40
+ },
41
+ };
42
+
43
+ export default { http, environment, config };
package/app/ghitgud.ts ADDED
@@ -0,0 +1,18 @@
1
+ import process from "process";
2
+ import { program } from "commander";
3
+
4
+ import ascii from "./ascii";
5
+ import commands from "./commands";
6
+
7
+ const NAME = "ghitgud";
8
+ const VERSION = "1.0.0";
9
+ const DESCRIPTION = "A simple CLI to give superpowers to GitHub.";
10
+
11
+ program
12
+ .name(NAME)
13
+ .description(DESCRIPTION)
14
+ .version(VERSION);
15
+
16
+ commands();
17
+ program.addHelpText("before", ascii);
18
+ program.parse(process.argv);
package/app/library.ts ADDED
@@ -0,0 +1,158 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ import api from "./api";
6
+ import { Label } from "./types";
7
+ import functions from "./functions";
8
+
9
+ const ENCODING = "utf8";
10
+ const PING_RESPONSE = "pong";
11
+ const METADATA_FOLDER = "metadata";
12
+ const METADATA_FILE = "labels.json";
13
+ const ERROR_NO_METADATA = "No metadata file found.";
14
+
15
+ const CREDENTIALS_FILE = "credentials.json";
16
+ const ERROR_UNSUPPORTED_KEY = "Trying to set unsupported key.";
17
+ const GHITGUD_FOLDER = path.join(os.homedir(), ".config", "ghitgud");
18
+
19
+ const ping = () => {
20
+ console.info(PING_RESPONSE);
21
+ return { success: true };
22
+ };
23
+
24
+ const labels = {
25
+ list: async () => {
26
+ const response = await api.labels.fetch();
27
+ const data = await response.json();
28
+
29
+ const labels = data.map((label: Label) => ({
30
+ name: label.name,
31
+ color: label.color,
32
+ description: label.description,
33
+ }));
34
+
35
+ const result = { success: true, metadata: labels };
36
+ console.info(result);
37
+ return result;
38
+ },
39
+
40
+ pull: async () => {
41
+ const response = await api.labels.fetch();
42
+ const data = await response.json();
43
+
44
+ const labels = data.map((label: Label) => ({
45
+ name: label.name,
46
+ color: label.color,
47
+ description: label.description,
48
+ }));
49
+
50
+ try {
51
+ fs.mkdirSync(METADATA_FOLDER, { recursive: true });
52
+ } catch (error) {
53
+ throw new Error(error instanceof Error ? error.message : String(error));
54
+ }
55
+
56
+ try {
57
+ fs.writeFileSync(
58
+ `${METADATA_FOLDER}/${METADATA_FILE}`,
59
+ JSON.stringify(labels, null, 2)
60
+ );
61
+ } catch (error) {
62
+ throw new Error(error instanceof Error ? error.message : String(error));
63
+ }
64
+
65
+ const result = { success: true };
66
+ console.info(result);
67
+ return result;
68
+ },
69
+
70
+ push: async () => {
71
+ if (!fs.existsSync(`${METADATA_FOLDER}/${METADATA_FILE}`))
72
+ throw new Error(ERROR_NO_METADATA);
73
+
74
+ const data = fs.readFileSync(
75
+ `${METADATA_FOLDER}/${METADATA_FILE}`,
76
+ ENCODING
77
+ );
78
+
79
+ const labels = JSON.parse(data);
80
+
81
+ await Promise.all(
82
+ labels.map(async (label: Label) => {
83
+ const response = await api.labels.get(label.name);
84
+ if (functions.http.isOk(response.status)) await api.labels.patch(label);
85
+
86
+ if (functions.http.isNotFound(response.status))
87
+ await api.labels.create(label);
88
+ })
89
+ );
90
+
91
+ const result = { success: true };
92
+ console.info(result);
93
+ return result;
94
+ },
95
+
96
+ prune: async () => {
97
+ if (!fs.existsSync(`${METADATA_FOLDER}/${METADATA_FILE}`))
98
+ throw new Error(ERROR_NO_METADATA);
99
+
100
+ const data = fs.readFileSync(
101
+ `${METADATA_FOLDER}/${METADATA_FILE}`,
102
+ ENCODING
103
+ );
104
+
105
+ const labels = JSON.parse(data);
106
+ labels.map(async (label: Label) => await api.labels.delete(label.name));
107
+
108
+ const result = { success: true };
109
+ console.info(result);
110
+ return result;
111
+ },
112
+ };
113
+
114
+ const config = {
115
+ set: (key: string, value: string) => {
116
+ const knowns = ["token", "repo"];
117
+
118
+ if (!knowns.includes(key)) throw new Error(ERROR_UNSUPPORTED_KEY);
119
+
120
+ if (!fs.existsSync(`${GHITGUD_FOLDER}/${CREDENTIALS_FILE}`)) {
121
+ const credentials = { [key]: value };
122
+
123
+ try {
124
+ fs.mkdirSync(GHITGUD_FOLDER, { recursive: true });
125
+ } catch (error) {
126
+ throw new Error(error instanceof Error ? error.message : String(error));
127
+ }
128
+
129
+ fs.writeFileSync(
130
+ `${GHITGUD_FOLDER}/${CREDENTIALS_FILE}`,
131
+ JSON.stringify(credentials, null, 2)
132
+ );
133
+
134
+ return { success: true };
135
+ }
136
+
137
+ const data = fs.readFileSync(
138
+ `${GHITGUD_FOLDER}/${CREDENTIALS_FILE}`,
139
+ ENCODING
140
+ );
141
+
142
+ const credentials = JSON.parse(data);
143
+ credentials[key] = value;
144
+
145
+ fs.writeFileSync(
146
+ `${GHITGUD_FOLDER}/${CREDENTIALS_FILE}`,
147
+ JSON.stringify(credentials, null, 2)
148
+ );
149
+
150
+ return { success: true };
151
+ },
152
+ };
153
+
154
+ export default {
155
+ ping,
156
+ labels,
157
+ config,
158
+ };
package/app/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ interface Label {
2
+ name: string;
3
+ color: string;
4
+ newName?: string;
5
+ description: string;
6
+ }
7
+
8
+ export type { Label };