@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/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
+ };