@asagiri-design/labels-config 0.2.2
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.ja.md +387 -0
- package/README.md +387 -0
- package/dist/chunk-4ZUJQMV7.mjs +285 -0
- package/dist/chunk-DGUMSQAI.mjs +496 -0
- package/dist/chunk-DSI7SDAM.mjs +161 -0
- package/dist/chunk-QJLMZSVA.mjs +496 -0
- package/dist/chunk-QZ7TP4HQ.mjs +7 -0
- package/dist/chunk-VU2JB66N.mjs +103 -0
- package/dist/chunk-ZYHIDOG2.mjs +247 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1417 -0
- package/dist/cli.mjs +436 -0
- package/dist/config/index.d.mts +49 -0
- package/dist/config/index.d.ts +49 -0
- package/dist/config/index.js +554 -0
- package/dist/config/index.mjs +10 -0
- package/dist/github/index.d.mts +113 -0
- package/dist/github/index.d.ts +113 -0
- package/dist/github/index.js +310 -0
- package/dist/github/index.mjs +9 -0
- package/dist/index.d.mts +309 -0
- package/dist/index.d.ts +309 -0
- package/dist/index.js +306 -0
- package/dist/index.mjs +44 -0
- package/dist/types-CkwsO1Iu.d.mts +50 -0
- package/dist/types-CkwsO1Iu.d.ts +50 -0
- package/docs/API.md +309 -0
- package/docs/GETTING_STARTED.md +305 -0
- package/package.json +87 -0
- package/templates/prod-labels.json +106 -0
package/README.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# @boxpistols/labels-config
|
|
2
|
+
|
|
3
|
+
Terminal-first GitHub label management - Simple, fast, no token needed.
|
|
4
|
+
|
|
5
|
+
Manage GitHub labels from your terminal using gh CLI. No manual token setup required.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 1. Install gh CLI and authenticate (one-time setup)
|
|
13
|
+
brew install gh # macOS
|
|
14
|
+
gh auth login
|
|
15
|
+
|
|
16
|
+
# 2. Install labels-config
|
|
17
|
+
npm install -g @boxpistols/labels-config
|
|
18
|
+
|
|
19
|
+
# 3. Initialize from template
|
|
20
|
+
labels-config init minimal --file labels.json
|
|
21
|
+
|
|
22
|
+
# 4. Sync to your repository
|
|
23
|
+
labels-config sync --owner your-name --repo your-repo --file labels.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Done! Your labels are synced.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Terminal-First**: No token management - uses gh CLI authentication
|
|
33
|
+
- **Simple CLI**: 5 commands, straightforward usage
|
|
34
|
+
- **Pre-built Templates**: 9 ready-to-use label sets
|
|
35
|
+
- **Validation**: Check your config before syncing
|
|
36
|
+
- **Dry Run**: Preview changes before applying
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### Prerequisites
|
|
43
|
+
|
|
44
|
+
Install and authenticate gh CLI:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# macOS
|
|
48
|
+
brew install gh
|
|
49
|
+
|
|
50
|
+
# Linux (Debian/Ubuntu)
|
|
51
|
+
sudo apt install gh
|
|
52
|
+
|
|
53
|
+
# Windows
|
|
54
|
+
winget install --id GitHub.cli
|
|
55
|
+
|
|
56
|
+
# Authenticate
|
|
57
|
+
gh auth login
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Install labels-config
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install -g @boxpistols/labels-config
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### Understanding init vs sync
|
|
71
|
+
|
|
72
|
+
**Important:** The `init` command creates a local configuration file - it does NOT sync to GitHub.
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
|---------|--------------|
|
|
76
|
+
| `init` | Creates `labels.json` locally (no GitHub changes) |
|
|
77
|
+
| `sync` | Applies `labels.json` to your GitHub repository |
|
|
78
|
+
|
|
79
|
+
**Example workflow:**
|
|
80
|
+
```bash
|
|
81
|
+
# Step 1: Create local config file (no GitHub changes yet)
|
|
82
|
+
labels-config init prod-ja --file labels.json
|
|
83
|
+
|
|
84
|
+
# Step 2: Check what's in the file
|
|
85
|
+
cat labels.json
|
|
86
|
+
|
|
87
|
+
# Step 3: Apply to GitHub (this actually changes your labels)
|
|
88
|
+
labels-config sync --owner your-name --repo your-repo --file labels.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 1. Create label configuration
|
|
92
|
+
|
|
93
|
+
**From template:**
|
|
94
|
+
```bash
|
|
95
|
+
labels-config init minimal --file labels.json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Available templates:**
|
|
99
|
+
- `minimal` - Basic 3-label set (bug, feature, documentation)
|
|
100
|
+
- `github` - GitHub standard labels
|
|
101
|
+
- `prod-ja` - Production project (Japanese, 14 labels)
|
|
102
|
+
- `prod-en` - Production project (English, 14 labels)
|
|
103
|
+
- `agile` - Agile/Scrum workflow
|
|
104
|
+
- `react`, `vue`, `frontend` - Framework-specific
|
|
105
|
+
|
|
106
|
+
### 2. Validate configuration
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
labels-config validate labels.json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Preview changes (dry run)
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
labels-config sync \
|
|
116
|
+
--owner your-name \
|
|
117
|
+
--repo your-repo \
|
|
118
|
+
--file labels.json \
|
|
119
|
+
--dry-run \
|
|
120
|
+
--verbose
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Sync to GitHub
|
|
124
|
+
|
|
125
|
+
**Append mode** (default - keeps existing labels):
|
|
126
|
+
```bash
|
|
127
|
+
labels-config sync --owner your-name --repo your-repo --file labels.json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Replace mode** (removes unlisted labels):
|
|
131
|
+
```bash
|
|
132
|
+
labels-config sync --owner your-name --repo your-repo --file labels.json --delete-extra
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 5. Export existing labels
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
labels-config export --owner your-name --repo your-repo --file exported.json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## CLI Commands
|
|
144
|
+
|
|
145
|
+
| Command | Description |
|
|
146
|
+
|---------|-------------|
|
|
147
|
+
| `init <template>` | Create label config from template |
|
|
148
|
+
| `validate <file>` | Validate label configuration |
|
|
149
|
+
| `sync` | Sync labels to GitHub |
|
|
150
|
+
| `export` | Export labels from GitHub |
|
|
151
|
+
| `help` | Show help |
|
|
152
|
+
|
|
153
|
+
### Options
|
|
154
|
+
|
|
155
|
+
| Option | Description |
|
|
156
|
+
|--------|-------------|
|
|
157
|
+
| `--owner <name>` | Repository owner |
|
|
158
|
+
| `--repo <name>` | Repository name |
|
|
159
|
+
| `--file <path>` | Config file path |
|
|
160
|
+
| `--dry-run` | Preview changes only |
|
|
161
|
+
| `--delete-extra` | Delete unlisted labels (replace mode) |
|
|
162
|
+
| `--verbose` | Show detailed output |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Label Configuration Format
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"version": "1.0.0",
|
|
171
|
+
"labels": [
|
|
172
|
+
{
|
|
173
|
+
"name": "bug",
|
|
174
|
+
"color": "d73a4a",
|
|
175
|
+
"description": "Something isn't working"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "feature",
|
|
179
|
+
"color": "0e8a16",
|
|
180
|
+
"description": "New feature request"
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Requirements:**
|
|
187
|
+
- `name`: 1-50 characters
|
|
188
|
+
- `color`: 3 or 6 character hex code (without #)
|
|
189
|
+
- `description`: 1-200 characters
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Sync Modes
|
|
194
|
+
|
|
195
|
+
### Append Mode (Default)
|
|
196
|
+
Adds new labels and updates existing ones. Keeps labels not in your config.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
labels-config sync --owner user --repo repo --file labels.json
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Replace Mode
|
|
203
|
+
Deletes all labels not in your config. Complete control.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
labels-config sync --owner user --repo repo --file labels.json --delete-extra
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
⚠️ **Warning**: Replace mode removes labels from all issues and PRs. Always use `--dry-run` first!
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Multi-Repository Sync
|
|
214
|
+
|
|
215
|
+
Sync the same labels to multiple repositories:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
#!/bin/bash
|
|
219
|
+
REPOS=("org/repo1" "org/repo2" "org/repo3")
|
|
220
|
+
|
|
221
|
+
for REPO in "${REPOS[@]}"; do
|
|
222
|
+
OWNER=$(echo $REPO | cut -d'/' -f1)
|
|
223
|
+
REPO_NAME=$(echo $REPO | cut -d'/' -f2)
|
|
224
|
+
|
|
225
|
+
labels-config sync \
|
|
226
|
+
--owner $OWNER \
|
|
227
|
+
--repo $REPO_NAME \
|
|
228
|
+
--file labels.json \
|
|
229
|
+
--verbose
|
|
230
|
+
done
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Workflow Integration
|
|
236
|
+
|
|
237
|
+
### GitHub Actions
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
name: Sync Labels
|
|
241
|
+
|
|
242
|
+
on:
|
|
243
|
+
push:
|
|
244
|
+
paths:
|
|
245
|
+
- 'labels.json'
|
|
246
|
+
branches:
|
|
247
|
+
- main
|
|
248
|
+
|
|
249
|
+
jobs:
|
|
250
|
+
sync:
|
|
251
|
+
runs-on: ubuntu-latest
|
|
252
|
+
steps:
|
|
253
|
+
- uses: actions/checkout@v4
|
|
254
|
+
|
|
255
|
+
- name: Setup Node.js
|
|
256
|
+
uses: actions/setup-node@v4
|
|
257
|
+
with:
|
|
258
|
+
node-version: '18'
|
|
259
|
+
|
|
260
|
+
- name: Install labels-config
|
|
261
|
+
run: npm install -g @boxpistols/labels-config
|
|
262
|
+
|
|
263
|
+
- name: Install gh CLI
|
|
264
|
+
run: |
|
|
265
|
+
sudo apt update
|
|
266
|
+
sudo apt install gh -y
|
|
267
|
+
|
|
268
|
+
- name: Authenticate gh CLI
|
|
269
|
+
env:
|
|
270
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
271
|
+
run: echo "$GITHUB_TOKEN" | gh auth login --with-token
|
|
272
|
+
|
|
273
|
+
- name: Sync labels
|
|
274
|
+
run: |
|
|
275
|
+
labels-config sync \
|
|
276
|
+
--owner ${{ github.repository_owner }} \
|
|
277
|
+
--repo ${{ github.event.repository.name }} \
|
|
278
|
+
--file labels.json \
|
|
279
|
+
--verbose
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Troubleshooting
|
|
285
|
+
|
|
286
|
+
### Authentication failed
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Check gh CLI status
|
|
290
|
+
gh auth status
|
|
291
|
+
|
|
292
|
+
# Re-authenticate
|
|
293
|
+
gh auth login
|
|
294
|
+
|
|
295
|
+
# Refresh authentication
|
|
296
|
+
gh auth refresh
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Validation errors
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Run validation to see specific errors
|
|
303
|
+
labels-config validate labels.json
|
|
304
|
+
|
|
305
|
+
# Common issues:
|
|
306
|
+
# - Duplicate label names
|
|
307
|
+
# - Invalid hex colors (must be 3 or 6 chars without #)
|
|
308
|
+
# - Name too long (max 50 chars)
|
|
309
|
+
# - Description too long (max 200 chars)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Labels not syncing
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Check with verbose output
|
|
316
|
+
labels-config sync --owner user --repo repo --file labels.json --verbose
|
|
317
|
+
|
|
318
|
+
# Try dry run to see what would change
|
|
319
|
+
labels-config sync --owner user --repo repo --file labels.json --dry-run --verbose
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Rate limit exceeded
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
# Check rate limit status
|
|
326
|
+
gh api rate_limit
|
|
327
|
+
|
|
328
|
+
# Wait for reset (typically 60 minutes)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Best Practices
|
|
334
|
+
|
|
335
|
+
**✅ DO:**
|
|
336
|
+
- Keep `labels.json` in version control
|
|
337
|
+
- Run `--dry-run` before actual sync
|
|
338
|
+
- Use semantic commit messages
|
|
339
|
+
- Document label purposes in your project
|
|
340
|
+
- Use consistent colors across projects
|
|
341
|
+
|
|
342
|
+
**❌ DON'T:**
|
|
343
|
+
- Delete labels without checking issue/PR usage
|
|
344
|
+
- Change label names frequently
|
|
345
|
+
- Skip validation before syncing
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Advanced Usage
|
|
350
|
+
|
|
351
|
+
### As npm Package
|
|
352
|
+
|
|
353
|
+
You can also use this as a library in your code:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { GitHubLabelSync } from '@boxpistols/labels-config/github'
|
|
357
|
+
import { CONFIG_TEMPLATES } from '@boxpistols/labels-config'
|
|
358
|
+
|
|
359
|
+
const sync = new GitHubLabelSync({
|
|
360
|
+
owner: 'your-org',
|
|
361
|
+
repo: 'your-repo'
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const labels = CONFIG_TEMPLATES.minimal
|
|
365
|
+
await sync.syncLabels(labels)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Install in your project:
|
|
369
|
+
```bash
|
|
370
|
+
npm install @boxpistols/labels-config
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
MIT
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Related
|
|
382
|
+
|
|
383
|
+
- [日本語 README](./README.ja.md)
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
**Made for terminal users who love gh CLI** 🚀
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__publicField
|
|
3
|
+
} from "./chunk-QZ7TP4HQ.mjs";
|
|
4
|
+
|
|
5
|
+
// src/github/client.ts
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
var GitHubClient = class {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
__publicField(this, "owner");
|
|
10
|
+
__publicField(this, "repo");
|
|
11
|
+
__publicField(this, "repoPath");
|
|
12
|
+
this.owner = options.owner;
|
|
13
|
+
this.repo = options.repo;
|
|
14
|
+
this.repoPath = `${this.owner}/${this.repo}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Execute gh CLI command
|
|
18
|
+
*/
|
|
19
|
+
exec(command) {
|
|
20
|
+
try {
|
|
21
|
+
return execSync(command, {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
24
|
+
}).trim();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`gh CLI command failed: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fetch all labels from repository
|
|
31
|
+
*/
|
|
32
|
+
async fetchLabels() {
|
|
33
|
+
try {
|
|
34
|
+
const output = this.exec(
|
|
35
|
+
`gh label list --repo ${this.repoPath} --json name,color,description --limit 1000`
|
|
36
|
+
);
|
|
37
|
+
if (!output) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const labels = JSON.parse(output);
|
|
41
|
+
return labels.map((label) => ({
|
|
42
|
+
name: label.name,
|
|
43
|
+
color: label.color,
|
|
44
|
+
description: label.description || ""
|
|
45
|
+
}));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`Failed to fetch labels from ${this.repoPath}: ${error}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a new label
|
|
52
|
+
*/
|
|
53
|
+
async createLabel(label) {
|
|
54
|
+
try {
|
|
55
|
+
const name = label.name.replace(/"/g, '\\"');
|
|
56
|
+
const description = label.description.replace(/"/g, '\\"');
|
|
57
|
+
const color = label.color.replace("#", "");
|
|
58
|
+
this.exec(
|
|
59
|
+
`gh label create "${name}" --color "${color}" --description "${description}" --repo ${this.repoPath}`
|
|
60
|
+
);
|
|
61
|
+
return {
|
|
62
|
+
name: label.name,
|
|
63
|
+
color: label.color,
|
|
64
|
+
description: label.description
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`Failed to create label "${label.name}": ${error}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Update an existing label
|
|
72
|
+
*/
|
|
73
|
+
async updateLabel(currentName, label) {
|
|
74
|
+
try {
|
|
75
|
+
const escapedCurrentName = currentName.replace(/"/g, '\\"');
|
|
76
|
+
const args = [];
|
|
77
|
+
if (label.name && label.name !== currentName) {
|
|
78
|
+
args.push(`--name "${label.name.replace(/"/g, '\\"')}"`);
|
|
79
|
+
}
|
|
80
|
+
if (label.color) {
|
|
81
|
+
args.push(`--color "${label.color.replace("#", "")}"`);
|
|
82
|
+
}
|
|
83
|
+
if (label.description !== void 0) {
|
|
84
|
+
args.push(`--description "${label.description.replace(/"/g, '\\"')}"`);
|
|
85
|
+
}
|
|
86
|
+
if (args.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
name: currentName,
|
|
89
|
+
color: label.color || "",
|
|
90
|
+
description: label.description || ""
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
this.exec(
|
|
94
|
+
`gh label edit "${escapedCurrentName}" ${args.join(" ")} --repo ${this.repoPath}`
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
name: label.name || currentName,
|
|
98
|
+
color: label.color || "",
|
|
99
|
+
description: label.description || ""
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw new Error(`Failed to update label "${currentName}": ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Delete a label
|
|
107
|
+
*/
|
|
108
|
+
async deleteLabel(name) {
|
|
109
|
+
try {
|
|
110
|
+
const escapedName = name.replace(/"/g, '\\"');
|
|
111
|
+
this.exec(`gh label delete "${escapedName}" --repo ${this.repoPath} --yes`);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new Error(`Failed to delete label "${name}": ${error}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if label exists
|
|
118
|
+
*/
|
|
119
|
+
async hasLabel(name) {
|
|
120
|
+
try {
|
|
121
|
+
const labels = await this.fetchLabels();
|
|
122
|
+
return labels.some((label) => label.name.toLowerCase() === name.toLowerCase());
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/github/sync.ts
|
|
130
|
+
var _GitHubLabelSync = class _GitHubLabelSync {
|
|
131
|
+
constructor(options) {
|
|
132
|
+
__publicField(this, "client");
|
|
133
|
+
__publicField(this, "options");
|
|
134
|
+
this.client = new GitHubClient(options);
|
|
135
|
+
this.options = options;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Log message if verbose mode is enabled
|
|
139
|
+
*/
|
|
140
|
+
log(message) {
|
|
141
|
+
if (this.options.verbose) {
|
|
142
|
+
console.log(`[labels-config] ${message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sync labels to GitHub repository with batch operations for better performance
|
|
147
|
+
*/
|
|
148
|
+
async syncLabels(localLabels) {
|
|
149
|
+
this.log("Fetching remote labels...");
|
|
150
|
+
const remoteLabels = await this.client.fetchLabels();
|
|
151
|
+
const result = {
|
|
152
|
+
created: [],
|
|
153
|
+
updated: [],
|
|
154
|
+
deleted: [],
|
|
155
|
+
unchanged: [],
|
|
156
|
+
errors: []
|
|
157
|
+
};
|
|
158
|
+
const remoteLabelMap = new Map(remoteLabels.map((label) => [label.name.toLowerCase(), label]));
|
|
159
|
+
const localLabelMap = new Map(localLabels.map((label) => [label.name.toLowerCase(), label]));
|
|
160
|
+
const toCreate = [];
|
|
161
|
+
const toUpdate = [];
|
|
162
|
+
const toDelete = [];
|
|
163
|
+
for (const label of localLabels) {
|
|
164
|
+
const remoteLabel = remoteLabelMap.get(label.name.toLowerCase());
|
|
165
|
+
if (!remoteLabel) {
|
|
166
|
+
toCreate.push(label);
|
|
167
|
+
} else if (this.hasChanges(label, remoteLabel)) {
|
|
168
|
+
toUpdate.push({ current: remoteLabel.name, updated: label });
|
|
169
|
+
} else {
|
|
170
|
+
result.unchanged.push(label);
|
|
171
|
+
this.log(`Unchanged label: ${label.name}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (this.options.deleteExtra) {
|
|
175
|
+
for (const remoteLabel of remoteLabels) {
|
|
176
|
+
if (!localLabelMap.has(remoteLabel.name.toLowerCase())) {
|
|
177
|
+
toDelete.push(remoteLabel.name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!this.options.dryRun) {
|
|
182
|
+
for (let i = 0; i < toCreate.length; i += _GitHubLabelSync.BATCH_SIZE) {
|
|
183
|
+
const batch = toCreate.slice(i, i + _GitHubLabelSync.BATCH_SIZE);
|
|
184
|
+
const promises = batch.map(async (label) => {
|
|
185
|
+
try {
|
|
186
|
+
await this.client.createLabel(label);
|
|
187
|
+
result.created.push(label);
|
|
188
|
+
this.log(`Created label: ${label.name}`);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
result.errors.push({
|
|
191
|
+
name: label.name,
|
|
192
|
+
error: error instanceof Error ? error.message : String(error)
|
|
193
|
+
});
|
|
194
|
+
this.log(`Error creating label "${label.name}": ${error}`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
await Promise.all(promises);
|
|
198
|
+
}
|
|
199
|
+
for (let i = 0; i < toUpdate.length; i += _GitHubLabelSync.BATCH_SIZE) {
|
|
200
|
+
const batch = toUpdate.slice(i, i + _GitHubLabelSync.BATCH_SIZE);
|
|
201
|
+
const promises = batch.map(async ({ current, updated }) => {
|
|
202
|
+
try {
|
|
203
|
+
await this.client.updateLabel(current, updated);
|
|
204
|
+
result.updated.push(updated);
|
|
205
|
+
this.log(`Updated label: ${updated.name}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
result.errors.push({
|
|
208
|
+
name: updated.name,
|
|
209
|
+
error: error instanceof Error ? error.message : String(error)
|
|
210
|
+
});
|
|
211
|
+
this.log(`Error updating label "${updated.name}": ${error}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
await Promise.all(promises);
|
|
215
|
+
}
|
|
216
|
+
for (let i = 0; i < toDelete.length; i += _GitHubLabelSync.BATCH_SIZE) {
|
|
217
|
+
const batch = toDelete.slice(i, i + _GitHubLabelSync.BATCH_SIZE);
|
|
218
|
+
const promises = batch.map(async (name) => {
|
|
219
|
+
try {
|
|
220
|
+
await this.client.deleteLabel(name);
|
|
221
|
+
result.deleted.push(name);
|
|
222
|
+
this.log(`Deleted label: ${name}`);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
result.errors.push({
|
|
225
|
+
name,
|
|
226
|
+
error: error instanceof Error ? error.message : String(error)
|
|
227
|
+
});
|
|
228
|
+
this.log(`Error deleting label "${name}": ${error}`);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
await Promise.all(promises);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
result.created.push(...toCreate);
|
|
235
|
+
result.updated.push(...toUpdate.map((op) => op.updated));
|
|
236
|
+
result.deleted.push(...toDelete);
|
|
237
|
+
toCreate.forEach((label) => this.log(`[DRY RUN] Would create label: ${label.name}`));
|
|
238
|
+
toUpdate.forEach(({ updated }) => this.log(`[DRY RUN] Would update label: ${updated.name}`));
|
|
239
|
+
toDelete.forEach((name) => this.log(`[DRY RUN] Would delete label: ${name}`));
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if label has changes
|
|
245
|
+
*/
|
|
246
|
+
hasChanges(local, remote) {
|
|
247
|
+
return local.color.toLowerCase() !== remote.color.toLowerCase() || local.description !== (remote.description || "");
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Fetch labels from GitHub
|
|
251
|
+
*/
|
|
252
|
+
async fetchLabels() {
|
|
253
|
+
const labels = await this.client.fetchLabels();
|
|
254
|
+
return labels.map((label) => ({
|
|
255
|
+
name: label.name,
|
|
256
|
+
color: label.color,
|
|
257
|
+
description: label.description || ""
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Delete a single label
|
|
262
|
+
*/
|
|
263
|
+
async deleteLabel(name) {
|
|
264
|
+
if (!this.options.dryRun) {
|
|
265
|
+
await this.client.deleteLabel(name);
|
|
266
|
+
}
|
|
267
|
+
this.log(`Deleted label: ${name}`);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Update a single label
|
|
271
|
+
*/
|
|
272
|
+
async updateLabel(name, updates) {
|
|
273
|
+
if (!this.options.dryRun) {
|
|
274
|
+
await this.client.updateLabel(name, updates);
|
|
275
|
+
}
|
|
276
|
+
this.log(`Updated label: ${name}`);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
__publicField(_GitHubLabelSync, "BATCH_SIZE", 5);
|
|
280
|
+
var GitHubLabelSync = _GitHubLabelSync;
|
|
281
|
+
|
|
282
|
+
export {
|
|
283
|
+
GitHubClient,
|
|
284
|
+
GitHubLabelSync
|
|
285
|
+
};
|