hyouji 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/index.cjs +4 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 koji
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Hyouji(表示) GitHub Label Manager
|
|
2
|
+
|
|
3
|
+
### article
|
|
4
|
+
|
|
5
|
+
https://levelup.gitconnected.com/create-github-labels-from-terminal-158d4868fab
|
|
6
|
+
|
|
7
|
+
[](https://github.com/koji/github-label-manager/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/koji/github-label-manager/actions/workflows/publish.yml)
|
|
9
|
+
[](https://badge.fury.io/js/github-label-manager)
|
|
10
|
+
|
|
11
|
+
A simple CLI tool to create/delete labels with GitHub Labels API. Now available as a global npm package with persistent configuration storage.
|
|
12
|
+
|
|
13
|
+
### article
|
|
14
|
+
|
|
15
|
+
https://levelup.gitconnected.com/create-github-labels-from-terminal-158d4868fab
|
|
16
|
+
|
|
17
|
+
<img width="846" alt="Screen Shot 2021-08-23 at 1 02 53 AM" src="https://user-images.githubusercontent.com/474225/130393065-3f2a6fed-f6a3-4b1b-8e5f-ee4fee43d70f.png">
|
|
18
|
+
|
|
19
|
+
https://user-images.githubusercontent.com/474225/130368605-b5c6410f-53f6-4ef0-b321-8950edeebf7d.mov
|
|
20
|
+
|
|
21
|
+
### Labels API
|
|
22
|
+
|
|
23
|
+
https://docs.github.com/en/rest/reference/issues#labels
|
|
24
|
+
|
|
25
|
+
`label data format`
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
// label format
|
|
29
|
+
{
|
|
30
|
+
"id": 3218144327,
|
|
31
|
+
"node_id": "MDU6TGFiZWwzMjE4MTQ0MzI3",
|
|
32
|
+
"url": "https://api.github.com/repos/koji/frontend-tools/labels/wontfix",
|
|
33
|
+
"name": "wontfix",
|
|
34
|
+
"color": "ffffff",
|
|
35
|
+
"default": true,
|
|
36
|
+
"description": "This will not be worked on"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Install globally via npm:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g github-label-manager
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or use with npx (no installation required):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx github-label-manager
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
This tool provides the following functionality:
|
|
57
|
+
|
|
58
|
+
1. Create a single label on a specific repo
|
|
59
|
+
2. Create multiple labels on a specific repo
|
|
60
|
+
3. Delete a single label from a specific repo
|
|
61
|
+
4. Delete all labels from a specific repo
|
|
62
|
+
5. Import labels from JSON file
|
|
63
|
+
6. **Display your saved settings** - View your stored GitHub configuration
|
|
64
|
+
7. **Persistent configuration** - Save your GitHub token and username for future use
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
After installation, run the tool from anywhere:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
github-label-manager
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or use the short alias:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
glm
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### First Time Setup
|
|
81
|
+
|
|
82
|
+
On your first run, you'll be prompted to enter:
|
|
83
|
+
|
|
84
|
+
- **GitHub Personal Token** - Generate one [here](https://github.com/settings/tokens) with `repo` scope
|
|
85
|
+
- **GitHub Username** - Your GitHub account name
|
|
86
|
+
|
|
87
|
+
These credentials will be securely saved and reused for future sessions.
|
|
88
|
+
|
|
89
|
+
### Menu Options
|
|
90
|
+
|
|
91
|
+
1. **Create a single label on a specific repo**
|
|
92
|
+
2. **Create multiple labels on a specific repo**
|
|
93
|
+
3. **Delete a single label from a specific repo**
|
|
94
|
+
4. **Delete all labels from a specific repo**
|
|
95
|
+
5. **Import labels from JSON file**
|
|
96
|
+
6. **Display your settings** - View your saved configuration
|
|
97
|
+
7. **Exit**
|
|
98
|
+
|
|
99
|
+
### Settings Management
|
|
100
|
+
|
|
101
|
+
The tool now includes persistent configuration storage with enhanced security:
|
|
102
|
+
|
|
103
|
+
- **Automatic saving**: Your GitHub token and username are saved after first use
|
|
104
|
+
- **Settings display**: Use option 6 to view your current configuration
|
|
105
|
+
- **Secure storage**: Configuration is stored in `~/.config/github-label-manager/config.json`
|
|
106
|
+
- **Token encryption**: Your personal token is automatically encrypted using machine-specific keys
|
|
107
|
+
- **Automatic migration**: Existing plain text configurations are automatically upgraded to encrypted format
|
|
108
|
+
- **Token security**: Your personal token is never displayed in plain text, only an obfuscated preview is shown
|
|
109
|
+
|
|
110
|
+
### Security Features
|
|
111
|
+
|
|
112
|
+
**Token Encryption**:
|
|
113
|
+
|
|
114
|
+
- All GitHub personal tokens are automatically encrypted before being saved to disk
|
|
115
|
+
- Encryption uses machine-specific keys derived from your system information
|
|
116
|
+
- Existing plain text configurations are automatically migrated to encrypted format on first run
|
|
117
|
+
- Even if someone gains access to your configuration file, the token remains protected
|
|
118
|
+
|
|
119
|
+
**Privacy Protection**:
|
|
120
|
+
|
|
121
|
+
- Tokens are never displayed in plain text in the interface
|
|
122
|
+
- Only an obfuscated preview (e.g., `ghp_****...****3456`) is shown in settings
|
|
123
|
+
- The settings display shows whether your token is encrypted or in plain text format
|
|
124
|
+
|
|
125
|
+
If you want to create/delete a single label, you need to type the followings.
|
|
126
|
+
|
|
127
|
+
#### create
|
|
128
|
+
|
|
129
|
+
- label name
|
|
130
|
+
- label color (technically optional)
|
|
131
|
+
- label description (technically optional)
|
|
132
|
+
|
|
133
|
+
#### delete
|
|
134
|
+
|
|
135
|
+
- label name
|
|
136
|
+
|
|
137
|
+
In terms of multiple labels, this script is using `label.js` to define name, color and description. The format is very simple.
|
|
138
|
+
If you want to put your own labels, you will need to modify `label.js` file.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
module.exports = Object.freeze([
|
|
142
|
+
{
|
|
143
|
+
name: "Type: Bug Fix",
|
|
144
|
+
color: "FF8A65",
|
|
145
|
+
description: "Fix features that are not working",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "Type: Enhancement",
|
|
149
|
+
color: "64B5F7",
|
|
150
|
+
description: "Add new features",
|
|
151
|
+
},
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Quick Start
|
|
155
|
+
|
|
156
|
+
1. Install the package globally:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm install -g github-label-manager
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
2. Run the tool:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
github-label-manager
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
3. On first run, enter your GitHub credentials when prompted
|
|
169
|
+
|
|
170
|
+
4. Select your desired operation from the menu
|
|
171
|
+
|
|
172
|
+
5. Follow the prompts to manage your repository labels
|
|
173
|
+
|
|
174
|
+
### Example Usage
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Install globally
|
|
178
|
+
npm install -g github-label-manager
|
|
179
|
+
|
|
180
|
+
# Run the tool
|
|
181
|
+
github-label-manager
|
|
182
|
+
|
|
183
|
+
# Or use the short alias
|
|
184
|
+
glm
|
|
185
|
+
|
|
186
|
+
# Or run without installing
|
|
187
|
+
npx github-label-manager
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
If you want to contribute or run from source:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
git clone https://github.com/koji/github-label-manager.git
|
|
196
|
+
cd github-label-manager
|
|
197
|
+
npm install
|
|
198
|
+
npm run build
|
|
199
|
+
npm start
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### CI/CD Pipeline
|
|
203
|
+
|
|
204
|
+
This project uses GitHub Actions for continuous integration and deployment:
|
|
205
|
+
|
|
206
|
+
#### Continuous Integration (CI)
|
|
207
|
+
|
|
208
|
+
- **Trigger**: Runs on every push and pull request
|
|
209
|
+
- **Node.js Version**: 22 (latest)
|
|
210
|
+
- **Steps**:
|
|
211
|
+
1. Install dependencies with caching
|
|
212
|
+
2. Run ESLint for code quality
|
|
213
|
+
3. Run Prettier for code formatting
|
|
214
|
+
4. Execute comprehensive test suite
|
|
215
|
+
5. Build package and verify CLI functionality
|
|
216
|
+
6. Generate test coverage reports
|
|
217
|
+
|
|
218
|
+
#### Continuous Deployment (CD)
|
|
219
|
+
|
|
220
|
+
- **Trigger**: Runs when a new release is published
|
|
221
|
+
- **Process**:
|
|
222
|
+
1. Runs all CI validation steps
|
|
223
|
+
2. Builds the package using Vite
|
|
224
|
+
3. Publishes to npm registry automatically
|
|
225
|
+
4. Verifies successful publication
|
|
226
|
+
|
|
227
|
+
#### Workflow Status
|
|
228
|
+
|
|
229
|
+
- **CI Workflow**: Ensures code quality and functionality on every change
|
|
230
|
+
- **Publish Workflow**: Automates npm package releases
|
|
231
|
+
- **Caching**: Dependencies are cached for faster build times
|
|
232
|
+
- **Security**: npm authentication uses encrypted repository secrets
|
|
233
|
+
|
|
234
|
+
For maintainers publishing new versions:
|
|
235
|
+
|
|
236
|
+
1. Update version in `package.json`
|
|
237
|
+
2. Create and publish a GitHub release
|
|
238
|
+
3. The CD pipeline will automatically publish to npm
|
|
239
|
+
|
|
240
|
+
See [NPM_TOKEN_SETUP.md](./NPM_TOKEN_SETUP.md) for detailed setup instructions.
|
|
241
|
+
|
|
242
|
+
### Predefined Labels
|
|
243
|
+
|
|
244
|
+
The "Create multiple labels" option uses predefined labels from `src/constant.ts`. These include common labels for project management:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
{
|
|
248
|
+
name: 'Type: Bug Fix',
|
|
249
|
+
color: 'FF8A65',
|
|
250
|
+
description: 'Fix features that are not working',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'Type: Enhancement',
|
|
254
|
+
color: '64B5F7',
|
|
255
|
+
description: 'Add new features',
|
|
256
|
+
},
|
|
257
|
+
// ... and many more
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Configuration
|
|
261
|
+
|
|
262
|
+
### Configuration File Location
|
|
263
|
+
|
|
264
|
+
Your settings are stored in:
|
|
265
|
+
|
|
266
|
+
- **Primary**: `~/.config/github-label-manager/config.json`
|
|
267
|
+
- **Fallback**: `~/.github-label-manager-config.json`
|
|
268
|
+
|
|
269
|
+
### Viewing Your Settings
|
|
270
|
+
|
|
271
|
+
Use the "Display your settings" menu option to:
|
|
272
|
+
|
|
273
|
+
- See your configuration file path
|
|
274
|
+
- View your stored GitHub username
|
|
275
|
+
- Check if a token is saved (without revealing the actual token)
|
|
276
|
+
- See when your configuration was last updated
|
|
277
|
+
|
|
278
|
+
### Clearing Configuration
|
|
279
|
+
|
|
280
|
+
If you need to reset your configuration, you can:
|
|
281
|
+
|
|
282
|
+
1. Delete the configuration file manually
|
|
283
|
+
2. The tool will prompt for new credentials on the next run
|
|
284
|
+
|
|
285
|
+
## Troubleshooting
|
|
286
|
+
|
|
287
|
+
### Invalid Token Error
|
|
288
|
+
|
|
289
|
+
If you see authentication errors:
|
|
290
|
+
|
|
291
|
+
1. Check that your token has the correct `repo` scope
|
|
292
|
+
2. Verify the token hasn't expired
|
|
293
|
+
3. The tool will automatically prompt for a new token if validation fails
|
|
294
|
+
|
|
295
|
+
### Permission Issues
|
|
296
|
+
|
|
297
|
+
If you encounter file permission errors:
|
|
298
|
+
|
|
299
|
+
- Ensure you have write access to your home directory
|
|
300
|
+
- The tool will attempt to use fallback locations if needed
|
|
301
|
+
|
|
302
|
+
## Requirements
|
|
303
|
+
|
|
304
|
+
- Node.js 10 or higher
|
|
305
|
+
- GitHub Personal Access Token with `repo` scope
|
|
306
|
+
|
|
307
|
+
https://user-images.githubusercontent.com/474225/130368605-b5c6410f-53f6-4ef0-b321-8950edeebf7d.mov
|
|
308
|
+
|
|
309
|
+
## Articles
|
|
310
|
+
|
|
311
|
+
- [Create GitHub Labels from Terminal](https://levelup.gitconnected.com/create-github-labels-from-terminal-158d4868fab)
|
|
312
|
+
- [Logical Colorful GitHub Labels](https://seantrane.com/posts/logical-colorful-github-labels-18230/)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var U=Object.create;var S=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,q=Object.prototype.hasOwnProperty;var H=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of j(e))!q.call(r,i)&&i!==t&&S(r,i,{get:()=>e[i],enumerable:!(o=G(e,i))||o.enumerable});return r};var V=(r,e,t)=>(t=r!=null?U(W(r)):{},H(e||!r||!r.__esModule?S(t,"default",{value:r,enumerable:!0}):t,r));const B=require("chalk"),J=require("figlet"),u=require("fs"),N=require("os"),F=require("path"),v=require("crypto"),K=require("prompts"),D=require("@octokit/core"),O=r=>r&&r.__esModule?r:{default:r};function z(r){if(r&&r.__esModule)return r;const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(r){for(const t in r)if(t!=="default"){const o=Object.getOwnPropertyDescriptor(r,t);Object.defineProperty(e,t,o.get?o:{enumerable:!0,get:()=>r[t]})}}return e.default=r,Object.freeze(e)}const n=O(B),Y=O(J),R=z(u),E=O(K),Q=[{type:"password",name:"octokit",message:"Please type your personal token"},{type:"text",name:"owner",message:"Please type your GitHub account"},{type:"text",name:"repo",message:"Please type your target repo name"}],X=[{type:"text",name:"name",message:"Please type new label name"},{type:"text",name:"color",message:'Please type label color without "#" '},{type:"text",name:"description",message:"Please type label description"}],Z={type:"text",name:"name",message:"Please type label name you want to delete"},ee={type:"text",name:"filePath",message:"Please type the path to your JSON file"},te={type:"multiselect",name:"action",message:"Please select an action",choices:[{title:"create a label",value:0},{title:"create multiple labels",value:1},{title:"delete a label",value:2},{title:"delete all labels",value:3},{title:"import JSON",value:4},{title:"Display your settings",value:5},{title:"exit",value:6}]},oe={type:"confirm",name:"value",message:"Do you have a personal token?",initial:!0},re=[{name:"Type: Bug Fix",color:"FF8A65",description:"Fix features that are not working"},{name:"Type: Enhancement",color:"64B5F7",description:"Add new features"},{name:"Type: Improvement",color:"4DB6AC",description:"Improve existing functionality"},{name:"Type: Modification",color:"4DD0E1",description:"Modify existing functionality"},{name:"Type: Optimization",color:"BA68C8",description:"Optimized existing functionality"},{name:"Type: Security Fix",color:"FF8A65",description:"Fix security issue"},{name:"Status: Available",color:"81C784",description:"Waiting for working on it"},{name:"Status: In Progress",color:"64B5F7",description:"Currently working on it"},{name:"Status: Completed",color:"4DB6AC",description:"Worked on it and completed"},{name:"Status: Canceled",color:"E57373",description:"Worked on it, but canceled"},{name:"Status: Inactive (Abandoned)",color:"90A4AF",description:"For now, there is no plan to work on it"},{name:"Status: Inactive (Duplicate)",color:"90A4AF",description:"This issue is duplicated"},{name:"Status: Inactive (Invalid)",color:"90A4AF",description:"This issue is invalid"},{name:"Status: Inactive (Won't Fix)",color:"90A4AF",description:"There is no plan to fix this issue"},{name:"Status: Pending",color:"A2887F",description:"Worked on it, but suspended"},{name:"Priority: ASAP",color:"FF8A65",description:"We must work on it asap"},{name:"Priority: High",color:"FFB74D",description:"We must work on it"},{name:"Priority: Medium",color:"FFF177",description:"We need to work on it"},{name:"Priority: Low",color:"DCE775",description:"We should work on it"},{name:"Priority: Safe",color:"81C784",description:"We would work on it"},{name:"Effort Effortless",color:"81C784",description:"No efforts are expected"},{name:"Effort Heavy",color:"FFB74D",description:"Heavy efforts are expected"},{name:"Effort Normal",color:"FFF177",description:"Normal efforts are expected"},{name:"Effort Light",color:"DCE775",description:"Light efforts are expected"},{name:"Effort Painful",color:"FF8A65",description:"Painful efforts are expected"},{name:"Feedback Discussion",color:"F06293",description:"A discussion about features"},{name:"Feedback Question",color:"F06293",description:"A question about features"},{name:"Feedback Suggestion",color:"F06293",description:"A suggestion about features"},{name:"Docs",color:"000",description:"Documentation"}],ne="Please input your GitHub info",ae=Y.default.textSync("Hyouji",{font:"Small",horizontalLayout:"default",verticalLayout:"default",width:180,whitespaceBreak:!0}),x="If you don't see action selector, please hit space key.",ie="https://github.com/settings/tokens",w=console.log,$=async(r,e)=>{const t=await r.octokit.request("POST /repos/{owner}/{repo}/labels",{owner:r.owner,repo:r.repo,name:e.name,color:e.color,description:e.description});switch(t.status){case 201:w(n.default.green(`${t.status}: Created ${e.name}`));break;case 404:w(n.default.red(`${t.status}: Resource not found`));break;case 422:w(n.default.red(`${t.status}: Validation failed`));break;default:w(n.default.yellow(`${t.status}: Something wrong`));break}},se=async r=>{re.forEach(async e=>{$(r,e)}),w("Created all labels"),w(n.default.bgBlueBright(x))},ce=(r,e)=>{e.forEach(async t=>{await r.octokit.request("DELETE /repos/{owner}/{repo}/labels/{name}",{owner:r.owner,repo:r.repo,name:t})})},le=async r=>{const e=await r.octokit.request("GET /repos/{owner}/{repo}/labels",{owner:r.owner,repo:r.repo});return e.status===200?await e.data.map(o=>o.name):(w(n.default.red("something wrong")),[])},de=async r=>{const e=await le(r);e.forEach(async t=>{await r.octokit.request("DELETE /repos/{owner}/{repo}/labels/{name}",{owner:r.owner,repo:r.repo,name:t})}),w(""),e.forEach(t=>w(n.default.bgGreen(`deleted ${t}`))),w(n.default.bgBlueBright(x))},P=class P{static generateMachineKey(){const e=[N.homedir(),process.platform,process.arch,process.env.USER||process.env.USERNAME||"default"].join("|");return v.createHash("sha256").update(e).digest()}static encryptToken(e){try{const t=this.generateMachineKey(),o=v.randomBytes(16),i=v.createCipheriv(this.ALGORITHM,t,o);let c=i.update(e,"utf8",this.ENCODING);return c+=i.final(this.ENCODING),o.toString(this.ENCODING)+":"+c}catch{return console.warn("⚠️ Token encryption failed, storing in plain text"),e}}static decryptToken(e){try{if(!e.includes(":"))return e;const[t,o]=e.split(":");if(!t||!o)return e;const i=this.generateMachineKey(),c=Buffer.from(t,this.ENCODING),g=v.createDecipheriv(this.ALGORITHM,i,c);let a=g.update(o,this.ENCODING,"utf8");return a+=g.final("utf8"),a}catch{return console.warn("⚠️ Token decryption failed, using as plain text"),e}}static isTokenEncrypted(e){return e.includes(":")&&e.length>50}static obfuscateToken(e){if(!e||e.length<8)return"***";const t=e.substring(0,4),o=e.substring(e.length-4),i="*".repeat(Math.min(e.length-8,20));return`${t}${i}${o}`}};P.ALGORITHM="aes-256-cbc",P.ENCODING="hex";let h=P;class l extends Error{constructor(e,t,o){super(t),this.type=e,this.originalError=o,this.name="ConfigError"}}class b{constructor(){this.configDir=F.join(N.homedir(),".config","github-label-manager"),this.configPath=F.join(this.configDir,"config.json"),this.fallbackConfigPath=F.join(N.homedir(),".github-label-manager-config.json")}async loadConfig(){const e=[{path:this.configPath,name:"primary"},{path:this.fallbackConfigPath,name:"fallback"}];for(const t of e)try{if(await this.fileExists(t.path)){const o=await this.loadConfigFromPath(t.path);if(o)return o}}catch(o){await this.handleConfigLoadError(o,t.path,t.name)}return null}async loadConfigFromPath(e){try{const t=await u.promises.readFile(e,"utf-8");if(!t.trim())throw new l("CORRUPTED_FILE","Configuration file is empty");let o;try{o=JSON.parse(t)}catch(i){throw new l("CORRUPTED_FILE","Configuration file contains invalid JSON",i)}if(await this.validateConfig(o))return{...o,token:h.decryptToken(o.token)};throw new l("INVALID_FORMAT","Configuration file has invalid format or missing required fields")}catch(t){if(t instanceof l)throw t;const o=t;throw o.code==="EACCES"||o.code==="EPERM"?new l("PERMISSION_DENIED",`Permission denied accessing configuration file: ${e}`,o):o.code==="ENOENT"?new l("FILE_NOT_FOUND",`Configuration file not found: ${e}`,o):new l("UNKNOWN_ERROR",`Unexpected error loading configuration: ${o.message}`,o)}}async handleConfigLoadError(e,t,o){if(e instanceof l)switch(e.type){case"CORRUPTED_FILE":console.warn(`⚠️ Configuration file at ${o} location is corrupted: ${e.message}`),console.warn(` File: ${t}`),console.warn(" The file will be ignored and you'll be prompted for credentials."),await this.backupCorruptedFile(t);break;case"PERMISSION_DENIED":console.warn(`⚠️ Permission denied accessing configuration file at ${o} location.`),console.warn(` File: ${t}`),console.warn(" Please check file permissions or run with appropriate privileges.");break;case"INVALID_FORMAT":console.warn(`⚠️ Configuration file at ${o} location has invalid format.`),console.warn(` File: ${t}`),console.warn(" The file will be ignored and you'll be prompted for credentials."),await this.backupCorruptedFile(t);break;default:console.warn(`⚠️ Failed to load configuration from ${o} location: ${e.message}`),console.warn(` File: ${t}`)}else console.warn(`⚠️ Unexpected error loading configuration from ${o} location.`),console.warn(` File: ${t}`)}async backupCorruptedFile(e){try{const t=`${e}.backup.${Date.now()}`;await u.promises.copyFile(e,t),console.warn(` Corrupted file backed up to: ${t}`)}catch(t){console.warn(` Could not backup corrupted file: ${t instanceof Error?t.message:"Unknown error"}`)}}async saveConfig(e){const t={...e,token:h.encryptToken(e.token),lastUpdated:new Date().toISOString()},o=JSON.stringify(t,null,2);try{if(await this.ensureConfigDirectory(),await u.promises.writeFile(this.configPath,o,{mode:384}),await this.fileExists(this.fallbackConfigPath))try{await u.promises.unlink(this.fallbackConfigPath)}catch{console.warn(`⚠️ Could not remove old fallback configuration file: ${this.fallbackConfigPath}`)}return}catch(i){const c=i;if(c.code==="EACCES"||c.code==="EPERM")console.warn("⚠️ Permission denied writing to primary configuration location."),console.warn(` Attempted path: ${this.configPath}`),console.warn(" Trying fallback location...");else{if(c.code==="ENOSPC")throw new l("UNKNOWN_ERROR","Insufficient disk space to save configuration");console.warn(`⚠️ Failed to save configuration to primary location: ${c.message}`),console.warn(" Trying fallback location...")}try{await u.promises.writeFile(this.fallbackConfigPath,o,{mode:384}),console.warn(`✓ Configuration saved to fallback location: ${this.fallbackConfigPath}`);return}catch(g){const a=g;throw a.code==="EACCES"||a.code==="EPERM"?new l("PERMISSION_DENIED","Permission denied: Cannot save configuration to any location. Please check file permissions or run with appropriate privileges.",a):a.code==="ENOSPC"?new l("UNKNOWN_ERROR","Insufficient disk space to save configuration",a):new l("UNKNOWN_ERROR",`Failed to save configuration to any location. Primary error: ${c.message}. Fallback error: ${a.message}`,a)}}}configExists(){try{return u.existsSync(this.configPath)||u.existsSync(this.fallbackConfigPath)}catch{return!1}}getConfigPath(){return u.existsSync(this.configPath)?this.configPath:u.existsSync(this.fallbackConfigPath)?this.fallbackConfigPath:this.configPath}async validateConfig(e){if(!e||typeof e!="object"||!e.token||typeof e.token!="string"||e.token.trim()===""||!e.owner||typeof e.owner!="string"||e.owner.trim()==="")return!1;const t=h.decryptToken(e.token);return!!/^(ghp_|gho_|ghu_|ghs_)[a-zA-Z0-9]{36}$/.test(t)}async validateCredentials(e){try{const{Octokit:t}=await import("@octokit/core"),o=h.decryptToken(e.token),c=await new t({auth:o}).request("GET /user");return c.data.login.toLowerCase()!==e.owner.toLowerCase()?{isValid:!1,error:new l("INVALID_FORMAT",`Token belongs to user '${c.data.login}' but configuration is for '${e.owner}'`)}:{isValid:!0}}catch(t){const o=t;return o.status===401?{isValid:!1,error:new l("INVALID_FORMAT","GitHub token is invalid or has expired")}:o.status===403?{isValid:!1,error:new l("INVALID_FORMAT","GitHub token has insufficient permissions or rate limit exceeded")}:o.code==="ENOTFOUND"||o.code==="ECONNREFUSED"||o.code==="ETIMEDOUT"?{isValid:!1,error:new l("NETWORK_ERROR","Unable to connect to GitHub API. Please check your internet connection.")}:{isValid:!1,error:new l("UNKNOWN_ERROR",`Failed to validate credentials: ${o.message||"Unknown error"}`)}}}async migrateToEncrypted(){const e=await this.loadConfig();if(e&&!h.isTokenEncrypted(e.token)){console.log("🔒 Migrating configuration to encrypted format...");try{await this.saveConfig(e),console.log("✓ Configuration successfully encrypted")}catch(t){console.warn("⚠️ Failed to encrypt existing configuration:",t instanceof Error?t.message:"Unknown error")}}}async loadValidatedConfig(){const e=await this.loadConfig();if(!e)return{config:null,shouldPromptForCredentials:!0};const t=await this.validateCredentials(e);if(t.isValid)return{config:e,shouldPromptForCredentials:!1};const o={};return t.error&&(console.warn(`⚠️ ${b.getErrorMessage(t.error)}`),t.error.type==="INVALID_FORMAT"&&!t.error.message.includes("Token belongs to user")&&(o.owner=e.owner,console.warn(` Your GitHub username '${e.owner}' will be preserved.`))),{config:null,shouldPromptForCredentials:!0,preservedData:Object.keys(o).length>0?o:void 0}}async clearConfig(){const e=[];if(await this.fileExists(this.configPath))try{await u.promises.unlink(this.configPath)}catch(t){const o=t;o.code==="EACCES"||o.code==="EPERM"?e.push(`Permission denied removing primary config file: ${this.configPath}`):e.push(`Failed to remove primary config file: ${o.message}`)}if(await this.fileExists(this.fallbackConfigPath))try{await u.promises.unlink(this.fallbackConfigPath)}catch(t){const o=t;o.code==="EACCES"||o.code==="EPERM"?e.push(`Permission denied removing fallback config file: ${this.fallbackConfigPath}`):e.push(`Failed to remove fallback config file: ${o.message}`)}if(e.length>0)throw new l("PERMISSION_DENIED",`Failed to clear configuration: ${e.join("; ")}`)}async ensureConfigDirectory(){try{await u.promises.mkdir(this.configDir,{recursive:!0,mode:448})}catch(e){const t=e;if(t.code==="EEXIST")return;throw t.code==="EACCES"||t.code==="EPERM"?new l("PERMISSION_DENIED",`Permission denied creating configuration directory: ${this.configDir}`,t):t.code==="ENOSPC"?new l("UNKNOWN_ERROR","Insufficient disk space to create configuration directory",t):new l("UNKNOWN_ERROR",`Failed to create configuration directory: ${t.message}`,t)}}async fileExists(e){try{return await u.promises.access(e),!0}catch{return!1}}static getErrorMessage(e){switch(e.type){case"FILE_NOT_FOUND":return"Configuration file not found. You will be prompted to enter your credentials.";case"PERMISSION_DENIED":return"Permission denied accessing configuration file. Please check file permissions or run with appropriate privileges.";case"CORRUPTED_FILE":return"Configuration file is corrupted or contains invalid data. A backup has been created and you will be prompted for new credentials.";case"INVALID_FORMAT":return"Configuration file has invalid format. You will be prompted to enter your credentials again.";case"NETWORK_ERROR":return"Network error occurred while validating credentials. Please check your internet connection.";case"UNKNOWN_ERROR":default:return`An unexpected error occurred: ${e.message}`}}static isRecoverableError(e){return["FILE_NOT_FOUND","CORRUPTED_FILE","INVALID_FORMAT"].includes(e.type)}}const fe=async()=>(await E.default(oe)).value,s=console.log,ue=async(r,e)=>{try{if(!R.existsSync(e)){s(n.default.red(`Error: File not found at path: ${e}`));return}const t=R.readFileSync(e,"utf8");let o;try{o=JSON.parse(t)}catch(a){s(n.default.red(`Error: Invalid JSON syntax in file: ${e}`)),s(n.default.red(`Parse error: ${a instanceof Error?a.message:"Unknown error"}`));return}if(!Array.isArray(o)){s(n.default.red("Error: JSON file must contain an array of label objects"));return}const i=[];for(let a=0;a<o.length;a++){const m=o[a];if(typeof m!="object"||m===null){s(n.default.red(`Error: Item at index ${a} is not a valid object`));continue}const d=m;if(!d.name){s(n.default.red(`Error: Item at index ${a} is missing required 'name' field`));continue}if(typeof d.name!="string"){s(n.default.red(`Error: Item at index ${a} has invalid 'name' field (must be a non-empty string)`));continue}if(d.name.trim()===""){s(n.default.red(`Error: Item at index ${a} has empty 'name' field (name cannot be empty)`));continue}if(d.color!==void 0){if(typeof d.color!="string"){s(n.default.red(`Error: Item at index ${a} has invalid 'color' field (must be a string)`));continue}if(d.color.trim()===""){s(n.default.red(`Error: Item at index ${a} has empty 'color' field (color cannot be empty if provided)`));continue}}if(d.description!==void 0&&typeof d.description!="string"){s(n.default.red(`Error: Item at index ${a} has invalid 'description' field (must be a string)`));continue}const C=["name","color","description"],I=Object.keys(d).filter(M=>!C.includes(M));I.length>0&&s(n.default.yellow(`Warning: Item at index ${a} contains unknown fields that will be ignored: ${I.join(", ")}`));const L={name:d.name.trim(),...d.color!==void 0&&{color:d.color.trim()},...d.description!==void 0&&{description:d.description}};i.push(L)}if(i.length===0){s(n.default.red("Error: No valid labels found in JSON file"));return}s(n.default.blue(`Starting import of ${i.length} labels...`)),s("");let c=0,g=0;for(let a=0;a<i.length;a++){const m=i[a],d=`[${a+1}/${i.length}]`;try{s(n.default.cyan(`${d} Processing: ${m.name}`)),await $(r,m),c++}catch(C){g++,s(n.default.red(`${d} Failed to create label "${m.name}": ${C instanceof Error?C.message:"Unknown error"}`))}}s(""),g===0?s(n.default.green(`✅ Import completed successfully! Created ${c} labels.`)):(s(n.default.yellow("⚠️ Import completed with some errors:")),s(n.default.green(` • Successfully created: ${c} labels`)),s(n.default.red(` • Failed to create: ${g} labels`)),s(n.default.blue(` • Total processed: ${i.length} labels`)))}catch(t){s(n.default.red(`Error reading file: ${t instanceof Error?t.message:"Unknown error"}`))}},pe=async()=>[(await E.default(Z)).name],ge=async()=>{var c,g;const r=new b;let e={config:null,shouldPromptForCredentials:!0,preservedData:void 0};try{const a=await r.loadValidatedConfig();a&&(e=a)}catch{e={config:null,shouldPromptForCredentials:!0,preservedData:void 0}}if(e.config&&!e.shouldPromptForCredentials){const a=await E.default([{type:"text",name:"repo",message:"Please type your target repo name"}]);return{octokit:new D.Octokit({auth:e.config.token}),owner:e.config.owner,repo:a.repo,fromSavedConfig:!0}}const t=[...Q];if((c=e.preservedData)!=null&&c.owner){const a=t.findIndex(m=>m.name==="owner");a!==-1&&(t[a]={...t[a],initial:e.preservedData.owner})}const o=await E.default(t);if(o.octokit&&o.owner)try{await r.saveConfig({token:o.octokit,owner:o.owner,lastUpdated:new Date().toISOString()}),(g=e.preservedData)!=null&&g.owner&&e.preservedData.owner!==o.owner?console.log("✓ Configuration updated with new credentials"):console.log("✓ Configuration saved successfully")}catch(a){a instanceof l?(console.error(`❌ ${b.getErrorMessage(a)}`),b.isRecoverableError(a)||console.error(" This may affect future sessions. Please resolve the issue or contact support.")):console.warn("⚠️ Failed to save configuration:",a instanceof Error?a.message:"Unknown error")}return{octokit:new D.Octokit({auth:o.octokit}),owner:o.owner,repo:o.repo,fromSavedConfig:!1}},me=async()=>(await E.default(ee)).filePath,he=async()=>await E.default(X),T=async()=>{const r=await E.default(te),{action:e}=r;return e[0]!==void 0?e[0]:99},f=console.log;let p=!0;const k=new b,A=async()=>{console.log(ne),p&&await k.migrateToEncrypted();const r=await ge();if(!r.octokit||!r.owner||!r.repo)throw new Error("Invalid configuration: missing required fields");try{await r.octokit.request("GET /user")}catch(e){if(r.fromSavedConfig)return console.log(n.default.yellow("Saved credentials are invalid. Please provide new credentials.")),await k.clearConfig(),A();throw new Error(`GitHub API authentication failed: ${e instanceof Error?e.message:"Unknown error"}`)}return r},we=async()=>{f(n.default.cyan(`
|
|
3
|
+
=== Current Settings ===`));const r=k.getConfigPath();if(f(n.default.blue(`Configuration file path: ${r}`)),!k.configExists()){f(n.default.yellow("No configuration file exists. You will be prompted for credentials on next action."));return}try{const e=await k.loadConfig();if(!e){f(n.default.yellow("Configuration file exists but contains invalid data."));return}if(f(n.default.green(`GitHub account: ${e.owner}`)),e.token){const o=h.isTokenEncrypted(e.token)?"✓ Saved and encrypted":"✓ Saved (plain text)";f(n.default.green(`Personal token: ${o}`));const i=h.decryptToken(e.token),c=h.obfuscateToken(i);f(n.default.blue(`Token preview: ${c}`))}else f(n.default.red("Personal token: ✗ Not saved"));if(e.lastUpdated){const t=new Date(e.lastUpdated);f(n.default.blue(`Last updated: ${t.toLocaleString()}`))}}catch(e){f(n.default.red(`Error reading configuration: ${e instanceof Error?e.message:"Unknown error"}`))}f(n.default.cyan(`========================
|
|
4
|
+
`))};let y;const _=async()=>{if(!await fe()){f(n.default.redBright(`Please go to ${ie} and generate a personal token!`));return}if(p){f(ae);try{y=await A(),y.fromSavedConfig&&f(n.default.green(`Using saved configuration for ${y.owner}`))}catch(t){f(n.default.red(`Configuration error: ${t instanceof Error?t.message:"Unknown error"}`));return}}let e=await T();for(;e==99;)e=await T();switch(e){case 0:{const t=await he();$(y,t),p=p&&!1;break}case 1:{se(y),p=p&&!1;break}case 2:{const t=await pe();ce(y,t),p=p&&!1;break}case 3:{de(y),p=p&&!1;break}case 4:{try{const t=await me();t?await ue(y,t):f(n.default.yellow("No file path provided. Returning to main menu."))}catch(t){f(n.default.red(`Error during JSON import: ${t instanceof Error?t.message:"Unknown error"}`))}p=p&&!1;break}case 5:{await we(),p=p&&!1;break}case 6:console.log("exit"),process.exit(0);default:{console.log("invalid input");break}}_()};_();
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hyouji",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Hyouji (表示) — A command-line tool for organizing and displaying GitHub labels with clarity and harmony.",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hyouji": "./dist/index.cjs"
|
|
8
|
+
},
|
|
9
|
+
"repository": "https://github.com/koji/Hyouji",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"github lables",
|
|
13
|
+
"labels api",
|
|
14
|
+
"github",
|
|
15
|
+
"labels"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node dist/index.cjs",
|
|
19
|
+
"build": "vite build",
|
|
20
|
+
"dev": "vite build --watch",
|
|
21
|
+
"fix": "run-s fix:*",
|
|
22
|
+
"fix:prettier": "prettier \"src/**/*.ts\" --write",
|
|
23
|
+
"fix:lint": "eslint src --ext .ts --fix",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"test:ui": "vitest --ui",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
28
|
+
"test:lint": "eslint src --ext .ts",
|
|
29
|
+
"test:prettier": "prettier \"src/**/*.ts\" --list-different",
|
|
30
|
+
"check-cli": "run-s test diff-integration-tests check-integration-tests",
|
|
31
|
+
"check-integration-tests": "run-s check-integration-test:*",
|
|
32
|
+
"diff-integration-tests": "mkdir -p diff && rm -rf diff/test && cp -r test diff/test && rm -rf diff/test/test-*/.git && cd diff && git init --quiet && git add -A && git commit --quiet --no-verify --allow-empty -m 'WIP' && echo '\\n\\nCommitted most recent integration test output in the \"diff\" directory. Review the changes with \"cd diff && git diff HEAD\" or your preferred git diff viewer.'",
|
|
33
|
+
"watch:build": "tsc -p tsconfig.json -w",
|
|
34
|
+
"cov:send": "run-s cov:lcov && codecov",
|
|
35
|
+
"version": "standard-version",
|
|
36
|
+
"reset-hard": "git clean -dfx && git reset --hard && yarn",
|
|
37
|
+
"prepare-release": "run-s reset-hard test cov:check doc:html version doc:publish"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=10"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@octokit/core": "^7.0.3",
|
|
44
|
+
"chalk": "^5.4.1",
|
|
45
|
+
"figlet": "^1.8.1",
|
|
46
|
+
"prompts": "^2.4.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/figlet": "^1.7.0",
|
|
50
|
+
"@types/node": "^24.0.13",
|
|
51
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
52
|
+
"@vitest/ui": "^3.2.4",
|
|
53
|
+
"eslint": "^9.30.1",
|
|
54
|
+
"eslint-config-prettier": "^10.1.5",
|
|
55
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
56
|
+
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
57
|
+
"eslint-plugin-import": "^2.32.0",
|
|
58
|
+
"globals": "^16.3.0",
|
|
59
|
+
"npm-run-all": "^4.1.5",
|
|
60
|
+
"prettier": "^3.6.2",
|
|
61
|
+
"standard-version": "^9.5.0",
|
|
62
|
+
"ts-node": "^10.9.2",
|
|
63
|
+
"typescript": "^5.8.3",
|
|
64
|
+
"typescript-eslint": "^8.36.0",
|
|
65
|
+
"vite": "^7.1.2",
|
|
66
|
+
"vitest": "^3.2.4"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist/**/*",
|
|
70
|
+
"README.md",
|
|
71
|
+
"LICENSE"
|
|
72
|
+
],
|
|
73
|
+
"prettier": {
|
|
74
|
+
"singleQuote": true
|
|
75
|
+
},
|
|
76
|
+
"type": "module"
|
|
77
|
+
}
|