github-labels-template 0.6.1 โ 0.7.0-staging.63849e1
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 +49 -7
- package/dist/index.js +363 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ A CLI tool to apply a curated set of GitHub labels to any repository using `gh`
|
|
|
14
14
|
- ๐ **One Command Setup**: Apply all labels to any repo with `ghlt apply`
|
|
15
15
|
- ๐ **Auto-Detect Repo**: Automatically detects the current repository from git remote
|
|
16
16
|
- ๐ **Smart Conflict Handling**: Skips existing labels by default, `--force` to update
|
|
17
|
-
-
|
|
17
|
+
- ๐ **List Command**: View all labels on any repo โ name, color, and description at a glance
|
|
18
|
+
- ๐งน **Wipe Command**: Remove all or specific labels with a confirmation prompt
|
|
19
|
+
- ๐ซ **Apply with Exclusions**: Skip specific labels or entire categories with `--exclude` / `--exclude-category`
|
|
18
20
|
- โ
**Pre-Flight Checks**: Validates `gh` CLI is installed and authenticated before doing anything
|
|
19
21
|
- ๐ **Clear Output**: Structured logging powered by [@wgtechlabs/log-engine](https://github.com/wgtechlabs/log-engine) with color-coded levels and emoji
|
|
20
22
|
- ๐จ **ASCII Banner**: Beautiful ANSI Shadow figlet banner with version and author info
|
|
@@ -83,6 +85,15 @@ ghlt apply --category community --label bug
|
|
|
83
85
|
# Combine with force and repo
|
|
84
86
|
ghlt apply --category type --force --repo owner/repo
|
|
85
87
|
|
|
88
|
+
# Apply all labels except specific ones
|
|
89
|
+
ghlt apply --exclude "bug,enhancement"
|
|
90
|
+
|
|
91
|
+
# Apply all labels except an entire category
|
|
92
|
+
ghlt apply --exclude-category type
|
|
93
|
+
|
|
94
|
+
# Combine: apply all community labels except hacktoberfest
|
|
95
|
+
ghlt apply --category community --exclude hacktoberfest
|
|
96
|
+
|
|
86
97
|
# Include custom labels from labels-custom.json
|
|
87
98
|
ghlt apply --custom
|
|
88
99
|
|
|
@@ -90,6 +101,16 @@ ghlt apply --custom
|
|
|
90
101
|
ghlt apply --custom --category type
|
|
91
102
|
```
|
|
92
103
|
|
|
104
|
+
### List Labels
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# List all labels on the current repo
|
|
108
|
+
ghlt list
|
|
109
|
+
|
|
110
|
+
# List labels on a specific repo
|
|
111
|
+
ghlt list --repo owner/repo
|
|
112
|
+
```
|
|
113
|
+
|
|
93
114
|
### Generate Labels (AI)
|
|
94
115
|
|
|
95
116
|
Generate custom labels using GitHub Copilot โ following the Clean Labels convention. Requires a [GitHub Copilot](https://github.com/features/copilot) subscription.
|
|
@@ -145,6 +166,18 @@ ghlt wipe --repo owner/repo
|
|
|
145
166
|
|
|
146
167
|
# Skip confirmation prompt
|
|
147
168
|
ghlt wipe --yes
|
|
169
|
+
|
|
170
|
+
# Remove specific labels
|
|
171
|
+
ghlt wipe --label "bug,enhancement"
|
|
172
|
+
|
|
173
|
+
# Remove all labels from a category
|
|
174
|
+
ghlt wipe --category type
|
|
175
|
+
|
|
176
|
+
# Remove labels from multiple categories
|
|
177
|
+
ghlt wipe --category "type,status"
|
|
178
|
+
|
|
179
|
+
# Include custom labels in the selective wipe scope
|
|
180
|
+
ghlt wipe --category type --custom
|
|
148
181
|
```
|
|
149
182
|
|
|
150
183
|
### Preview Landing Page
|
|
@@ -243,24 +276,30 @@ Broad software layers โ universal across any project.
|
|
|
243
276
|
ghlt โ GitHub Labels Template CLI
|
|
244
277
|
|
|
245
278
|
USAGE
|
|
246
|
-
ghlt [OPTIONS] apply|wipe|migrate|generate|preview
|
|
279
|
+
ghlt [OPTIONS] apply|wipe|migrate|generate|list|preview
|
|
247
280
|
|
|
248
281
|
OPTIONS
|
|
249
282
|
-v, --version Show version number
|
|
250
283
|
|
|
251
284
|
COMMANDS
|
|
252
285
|
apply Apply labels from the template to a repository
|
|
253
|
-
wipe Remove all
|
|
286
|
+
wipe Remove all or specific labels from a repository
|
|
254
287
|
migrate Wipe all existing labels and apply the template (clean slate)
|
|
255
288
|
generate Generate custom labels using AI (requires GitHub Copilot)
|
|
289
|
+
list List all labels in a repository
|
|
256
290
|
preview Preview the landing page locally in your browser
|
|
257
291
|
|
|
258
292
|
OPTIONS (apply)
|
|
293
|
+
-r, --repo <owner/repo> Target repository (default: auto-detect)
|
|
294
|
+
-f, --force Overwrite existing labels
|
|
295
|
+
-l, --label <name> Apply specific label(s) by name (comma-separated)
|
|
296
|
+
-c, --category <name> Apply labels from specific category(ies) (comma-separated)
|
|
297
|
+
-e, --exclude <name> Exclude specific label(s) by name (comma-separated)
|
|
298
|
+
--exclude-category <name> Exclude labels from specific category(ies) (comma-separated)
|
|
299
|
+
--custom Include custom labels from labels-custom.json
|
|
300
|
+
|
|
301
|
+
OPTIONS (list)
|
|
259
302
|
-r, --repo <owner/repo> Target repository (default: auto-detect)
|
|
260
|
-
-f, --force Overwrite existing labels
|
|
261
|
-
-l, --label <name> Apply specific label(s) by name (comma-separated)
|
|
262
|
-
-c, --category <name> Apply labels from specific category(ies) (comma-separated)
|
|
263
|
-
--custom Include custom labels from labels-custom.json
|
|
264
303
|
|
|
265
304
|
OPTIONS (migrate)
|
|
266
305
|
-r, --repo <owner/repo> Target repository (default: auto-detect)
|
|
@@ -275,6 +314,9 @@ OPTIONS (generate)
|
|
|
275
314
|
OPTIONS (wipe)
|
|
276
315
|
-r, --repo <owner/repo> Target repository (default: auto-detect)
|
|
277
316
|
-y, --yes Skip confirmation prompt
|
|
317
|
+
-l, --label <name> Remove specific label(s) by name (comma-separated)
|
|
318
|
+
-c, --category <name> Remove labels from specific category(ies) (comma-separated)
|
|
319
|
+
--custom Include custom labels when using --label or --category
|
|
278
320
|
|
|
279
321
|
OPTIONS (preview)
|
|
280
322
|
-p, --port <number> Port to serve on (default: 3000)
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { defineCommand as
|
|
4
|
+
import { defineCommand as defineCommand7, runMain } from "citty";
|
|
5
5
|
|
|
6
6
|
// src/commands/apply.ts
|
|
7
7
|
import { defineCommand } from "citty";
|
|
@@ -52,6 +52,28 @@ async function detectRepo() {
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
async function listLabelsDetailed(repo) {
|
|
56
|
+
const { exitCode, stdout } = await run([
|
|
57
|
+
"label",
|
|
58
|
+
"list",
|
|
59
|
+
"--repo",
|
|
60
|
+
repo,
|
|
61
|
+
"--json",
|
|
62
|
+
"name,color,description",
|
|
63
|
+
"--limit",
|
|
64
|
+
"100"
|
|
65
|
+
]);
|
|
66
|
+
if (exitCode !== 0)
|
|
67
|
+
return [];
|
|
68
|
+
const text = stdout.trim();
|
|
69
|
+
if (!text)
|
|
70
|
+
return [];
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(text);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
55
77
|
async function listLabels(repo) {
|
|
56
78
|
const { exitCode, stdout } = await run([
|
|
57
79
|
"label",
|
|
@@ -161,7 +183,9 @@ function filterLabels(allLabels, options) {
|
|
|
161
183
|
const warnings = [];
|
|
162
184
|
const labelFilter = options.label ? options.label.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean) : null;
|
|
163
185
|
const categoryFilter = options.category ? options.category.split(",").map((c) => c.trim().toLowerCase()).filter(Boolean) : null;
|
|
164
|
-
|
|
186
|
+
const excludeLabelFilter = options.excludeLabel ? options.excludeLabel.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean) : null;
|
|
187
|
+
const excludeCategoryFilter = options.excludeCategory ? options.excludeCategory.split(",").map((c) => c.trim().toLowerCase()).filter(Boolean) : null;
|
|
188
|
+
if (!labelFilter && !categoryFilter && !excludeLabelFilter && !excludeCategoryFilter) {
|
|
165
189
|
return {
|
|
166
190
|
entries: Object.entries(allLabels),
|
|
167
191
|
warnings
|
|
@@ -183,7 +207,25 @@ function filterLabels(allLabels, options) {
|
|
|
183
207
|
}
|
|
184
208
|
}
|
|
185
209
|
}
|
|
186
|
-
|
|
210
|
+
if (excludeCategoryFilter) {
|
|
211
|
+
for (const cat of excludeCategoryFilter) {
|
|
212
|
+
if (!validCategories.includes(cat)) {
|
|
213
|
+
warnings.push(`Unknown exclude category "${cat}". Valid categories: ${validCategories.join(", ")}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (excludeLabelFilter) {
|
|
218
|
+
const allLabelNames = Object.values(allLabels).flat().map((l) => l.name.toLowerCase());
|
|
219
|
+
for (const name of excludeLabelFilter) {
|
|
220
|
+
if (!allLabelNames.includes(name)) {
|
|
221
|
+
warnings.push(`Exclude label "${name}" not found in the template.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const included = Object.entries(allLabels).map(([category, categoryLabels]) => {
|
|
226
|
+
if (!labelFilter && !categoryFilter) {
|
|
227
|
+
return [category, categoryLabels];
|
|
228
|
+
}
|
|
187
229
|
const categoryMatches = categoryFilter?.includes(category.toLowerCase()) ?? false;
|
|
188
230
|
if (categoryMatches) {
|
|
189
231
|
return [category, categoryLabels];
|
|
@@ -194,6 +236,14 @@ function filterLabels(allLabels, options) {
|
|
|
194
236
|
}
|
|
195
237
|
return [category, []];
|
|
196
238
|
}).filter(([, categoryLabels]) => categoryLabels.length > 0);
|
|
239
|
+
const entries = included.filter(([category]) => excludeCategoryFilter ? !excludeCategoryFilter.includes(category.toLowerCase()) : true).map(([category, categoryLabels]) => {
|
|
240
|
+
if (!excludeLabelFilter)
|
|
241
|
+
return [category, categoryLabels];
|
|
242
|
+
return [
|
|
243
|
+
category,
|
|
244
|
+
categoryLabels.filter((l) => !excludeLabelFilter.includes(l.name.toLowerCase()))
|
|
245
|
+
];
|
|
246
|
+
}).filter(([, categoryLabels]) => categoryLabels.length > 0);
|
|
197
247
|
return { entries, warnings };
|
|
198
248
|
}
|
|
199
249
|
|
|
@@ -306,6 +356,15 @@ var apply_default = defineCommand({
|
|
|
306
356
|
type: "boolean",
|
|
307
357
|
default: false,
|
|
308
358
|
description: "Include custom labels from labels-custom.json (generated via ghlt generate)"
|
|
359
|
+
},
|
|
360
|
+
exclude: {
|
|
361
|
+
type: "string",
|
|
362
|
+
alias: "e",
|
|
363
|
+
description: 'Exclude specific label(s) by name. Comma-separated for multiple (e.g., --exclude "bug,enhancement")'
|
|
364
|
+
},
|
|
365
|
+
"exclude-category": {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: 'Exclude labels from specific category(ies). Comma-separated for multiple (e.g., --exclude-category "type,status")'
|
|
309
368
|
}
|
|
310
369
|
},
|
|
311
370
|
async run({ args }) {
|
|
@@ -350,7 +409,9 @@ var apply_default = defineCommand({
|
|
|
350
409
|
}
|
|
351
410
|
const { entries: filteredEntries, warnings } = filterLabels(labelPool, {
|
|
352
411
|
label: args.label,
|
|
353
|
-
category: args.category
|
|
412
|
+
category: args.category,
|
|
413
|
+
excludeLabel: args.exclude,
|
|
414
|
+
excludeCategory: args["exclude-category"]
|
|
354
415
|
});
|
|
355
416
|
for (const w of warnings) {
|
|
356
417
|
warn(w);
|
|
@@ -361,6 +422,12 @@ var apply_default = defineCommand({
|
|
|
361
422
|
if (args.label) {
|
|
362
423
|
info(`Applying specific labels: ${args.label}`);
|
|
363
424
|
}
|
|
425
|
+
if (args["exclude-category"]) {
|
|
426
|
+
info(`Excluding categories: ${args["exclude-category"]}`);
|
|
427
|
+
}
|
|
428
|
+
if (args.exclude) {
|
|
429
|
+
info(`Excluding labels: ${args.exclude}`);
|
|
430
|
+
}
|
|
364
431
|
if (filteredEntries.length === 0) {
|
|
365
432
|
warn("No labels matched the specified filter(s).");
|
|
366
433
|
return;
|
|
@@ -440,6 +507,21 @@ var wipe_default = defineCommand2({
|
|
|
440
507
|
alias: "y",
|
|
441
508
|
default: false,
|
|
442
509
|
description: "Skip confirmation prompt"
|
|
510
|
+
},
|
|
511
|
+
label: {
|
|
512
|
+
type: "string",
|
|
513
|
+
alias: "l",
|
|
514
|
+
description: 'Remove specific label(s) by name. Comma-separated for multiple (e.g., --label "bug,enhancement")'
|
|
515
|
+
},
|
|
516
|
+
category: {
|
|
517
|
+
type: "string",
|
|
518
|
+
alias: "c",
|
|
519
|
+
description: 'Remove labels from specific category(ies). Comma-separated for multiple (e.g., --category "type,status")'
|
|
520
|
+
},
|
|
521
|
+
custom: {
|
|
522
|
+
type: "boolean",
|
|
523
|
+
default: false,
|
|
524
|
+
description: "Include custom labels from labels-custom.json when using --label or --category"
|
|
443
525
|
}
|
|
444
526
|
},
|
|
445
527
|
async run({ args }) {
|
|
@@ -462,14 +544,57 @@ var wipe_default = defineCommand2({
|
|
|
462
544
|
info("No labels found. Nothing to wipe.");
|
|
463
545
|
return;
|
|
464
546
|
}
|
|
547
|
+
const isSelective = !!(args.label || args.category);
|
|
548
|
+
let toDelete;
|
|
549
|
+
if (isSelective) {
|
|
550
|
+
const labelPool = {
|
|
551
|
+
...labels_default
|
|
552
|
+
};
|
|
553
|
+
if (args.custom) {
|
|
554
|
+
const custom = loadCustomLabels();
|
|
555
|
+
for (const [cat, catLabels] of Object.entries(custom)) {
|
|
556
|
+
if (!labelPool[cat])
|
|
557
|
+
labelPool[cat] = [];
|
|
558
|
+
for (const label of catLabels) {
|
|
559
|
+
const exists = labelPool[cat].some((l) => l.name.toLowerCase() === label.name.toLowerCase());
|
|
560
|
+
if (!exists)
|
|
561
|
+
labelPool[cat].push(label);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const { entries: filteredEntries, warnings } = filterLabels(labelPool, {
|
|
566
|
+
label: args.label,
|
|
567
|
+
category: args.category
|
|
568
|
+
});
|
|
569
|
+
for (const w of warnings) {
|
|
570
|
+
warn(w);
|
|
571
|
+
}
|
|
572
|
+
if (filteredEntries.length === 0) {
|
|
573
|
+
warn("No labels matched the specified filter(s).");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const existingSet = new Set(existing.map((n) => n.toLowerCase()));
|
|
577
|
+
toDelete = filteredEntries.flatMap(([, categoryLabels]) => categoryLabels.map((l) => l.name)).filter((name) => existingSet.has(name.toLowerCase()));
|
|
578
|
+
if (toDelete.length === 0) {
|
|
579
|
+
info("None of the specified labels exist on the repo. Nothing to remove.");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (args.label)
|
|
583
|
+
info(`Removing specific labels: ${args.label}`);
|
|
584
|
+
if (args.category)
|
|
585
|
+
info(`Removing labels from category: ${args.category}`);
|
|
586
|
+
} else {
|
|
587
|
+
toDelete = existing;
|
|
588
|
+
}
|
|
465
589
|
if (!args.yes) {
|
|
466
|
-
const
|
|
590
|
+
const message = isSelective ? pc3.bold(pc3.red(`This will delete ${toDelete.length} label(s) from ${repo}.`)) : pc3.bold(pc3.red(`This will delete all ${toDelete.length} labels from ${repo}.`));
|
|
591
|
+
const confirmed = await confirmPrompt(message);
|
|
467
592
|
if (!confirmed)
|
|
468
593
|
return;
|
|
469
594
|
}
|
|
470
595
|
heading("Deleting Labels");
|
|
471
596
|
const counts = { deleted: 0, failed: 0 };
|
|
472
|
-
for (const name of
|
|
597
|
+
for (const name of toDelete) {
|
|
473
598
|
const ok = await deleteLabel(repo, name);
|
|
474
599
|
if (ok) {
|
|
475
600
|
success(`${name} (deleted)`);
|
|
@@ -496,21 +621,24 @@ var CATEGORY_NAMES = {
|
|
|
496
621
|
resolution: "Resolution",
|
|
497
622
|
area: "Area"
|
|
498
623
|
};
|
|
499
|
-
function buildSystemPrompt(category, count) {
|
|
624
|
+
function buildSystemPrompt(category, count, attempt = 1) {
|
|
500
625
|
const categoryTitle = CATEGORY_NAMES[category] ?? category;
|
|
501
626
|
const existingLabels = labels_default[category] ?? [];
|
|
502
627
|
const existingList = existingLabels.map((l) => ` - "${l.name}" (${l.color}) โ ${l.description}`).join(`
|
|
503
628
|
`);
|
|
629
|
+
const variationHint = attempt > 1 ? `IMPORTANT: This is attempt #${attempt}. You MUST generate completely different label names, colors, and descriptions than any previous suggestions. Be creative and explore new angles.` : null;
|
|
504
630
|
return [
|
|
505
|
-
`You are a GitHub label generator. Generate exactly ${count} label suggestions for the "${categoryTitle}" category.`,
|
|
631
|
+
`You are a GitHub label generator. The user will describe the kind of label they need. Generate exactly ${count} label suggestions for the "${categoryTitle}" category that DIRECTLY match what the user is asking for.`,
|
|
506
632
|
"",
|
|
633
|
+
"CRITICAL: Every suggestion MUST be relevant to the user's description. Do NOT generate generic or unrelated labels. Focus on what the user specifically asked for.",
|
|
634
|
+
...variationHint ? ["", variationHint, ""] : [""],
|
|
507
635
|
"Each label must follow this exact JSON format:",
|
|
508
636
|
"[",
|
|
509
637
|
` { "name": "label-name", "color": "hex123", "description": "[${categoryTitle}] Description text [scope]" }`,
|
|
510
638
|
"]",
|
|
511
639
|
"",
|
|
512
640
|
"Rules:",
|
|
513
|
-
"- name: lowercase, concise (1-3 words), use spaces for multi-word names",
|
|
641
|
+
"- name: lowercase, concise (1-3 words), use spaces for multi-word names, must reflect the user's request",
|
|
514
642
|
"- color: 6-character hex without #, choose colors that are visually distinct from existing labels",
|
|
515
643
|
`- description: MUST start with [${categoryTitle}] and end with [issues], [PRs], or [issues, PRs]`,
|
|
516
644
|
"- Do NOT duplicate any of these existing labels:",
|
|
@@ -520,12 +648,17 @@ function buildSystemPrompt(category, count) {
|
|
|
520
648
|
].join(`
|
|
521
649
|
`);
|
|
522
650
|
}
|
|
523
|
-
function buildUserPrompt(description, refinement) {
|
|
651
|
+
function buildUserPrompt(description, refinement, attempt = 1) {
|
|
524
652
|
let prompt = `I need a label for: ${description}`;
|
|
525
653
|
if (refinement) {
|
|
526
654
|
prompt += `
|
|
527
655
|
|
|
528
656
|
Refinement feedback: ${refinement}`;
|
|
657
|
+
}
|
|
658
|
+
if (attempt > 1) {
|
|
659
|
+
prompt += `
|
|
660
|
+
|
|
661
|
+
Generate different suggestions from previous attempts. This is attempt #${attempt}, so provide fresh and unique alternatives.`;
|
|
529
662
|
}
|
|
530
663
|
return prompt;
|
|
531
664
|
}
|
|
@@ -558,13 +691,13 @@ function parseLabelsResponse(text) {
|
|
|
558
691
|
});
|
|
559
692
|
}
|
|
560
693
|
async function generateLabels(options) {
|
|
561
|
-
const { category, description, count = 3, refinement, model } = options;
|
|
694
|
+
const { category, description, count = 3, refinement, model, attempt = 1 } = options;
|
|
562
695
|
const client = new CopilotClient;
|
|
563
696
|
await client.start();
|
|
564
697
|
try {
|
|
565
698
|
const sessionConfig = {
|
|
566
699
|
systemMessage: {
|
|
567
|
-
content: buildSystemPrompt(category, count)
|
|
700
|
+
content: buildSystemPrompt(category, count, attempt)
|
|
568
701
|
}
|
|
569
702
|
};
|
|
570
703
|
if (model) {
|
|
@@ -572,7 +705,7 @@ async function generateLabels(options) {
|
|
|
572
705
|
}
|
|
573
706
|
const session = await client.createSession(sessionConfig);
|
|
574
707
|
try {
|
|
575
|
-
const userPrompt = buildUserPrompt(description, refinement);
|
|
708
|
+
const userPrompt = buildUserPrompt(description, refinement, attempt);
|
|
576
709
|
const response = await session.sendAndWait({ content: userPrompt });
|
|
577
710
|
if (!response || !response.data?.content) {
|
|
578
711
|
throw new Error("No response received from Copilot");
|
|
@@ -693,6 +826,7 @@ var generate_default = defineCommand3({
|
|
|
693
826
|
});
|
|
694
827
|
let selectedLabel = null;
|
|
695
828
|
let refinement;
|
|
829
|
+
let attempt = 1;
|
|
696
830
|
while (!selectedLabel) {
|
|
697
831
|
info(refinement ? "Regenerating with your feedback..." : "Generating label suggestions...");
|
|
698
832
|
let suggestions;
|
|
@@ -702,7 +836,8 @@ var generate_default = defineCommand3({
|
|
|
702
836
|
description,
|
|
703
837
|
count: 3,
|
|
704
838
|
refinement,
|
|
705
|
-
model: args.model
|
|
839
|
+
model: args.model,
|
|
840
|
+
attempt
|
|
706
841
|
});
|
|
707
842
|
} catch (err) {
|
|
708
843
|
error(`Failed to generate labels: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -712,6 +847,7 @@ var generate_default = defineCommand3({
|
|
|
712
847
|
});
|
|
713
848
|
if (retry) {
|
|
714
849
|
refinement = undefined;
|
|
850
|
+
attempt = 1;
|
|
715
851
|
continue;
|
|
716
852
|
}
|
|
717
853
|
return;
|
|
@@ -748,10 +884,12 @@ var generate_default = defineCommand3({
|
|
|
748
884
|
message: "What would you like to change?",
|
|
749
885
|
validate: (value) => value.trim().length > 0 || "Please provide feedback."
|
|
750
886
|
});
|
|
887
|
+
attempt++;
|
|
751
888
|
continue;
|
|
752
889
|
}
|
|
753
890
|
if (choice === "regenerate") {
|
|
754
891
|
refinement = undefined;
|
|
892
|
+
attempt++;
|
|
755
893
|
continue;
|
|
756
894
|
}
|
|
757
895
|
const pickIndex = parseInt(choice.replace("pick:", ""), 10);
|
|
@@ -893,13 +1031,212 @@ var migrate_default = defineCommand4({
|
|
|
893
1031
|
}
|
|
894
1032
|
});
|
|
895
1033
|
|
|
1034
|
+
// src/commands/list.ts
|
|
1035
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1036
|
+
import pc6 from "picocolors";
|
|
1037
|
+
var list_default = defineCommand5({
|
|
1038
|
+
meta: {
|
|
1039
|
+
name: "list",
|
|
1040
|
+
description: "List all labels in a repository"
|
|
1041
|
+
},
|
|
1042
|
+
args: {
|
|
1043
|
+
repo: {
|
|
1044
|
+
type: "string",
|
|
1045
|
+
alias: "r",
|
|
1046
|
+
description: "Target repository (owner/repo). Defaults to current repo."
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
async run({ args }) {
|
|
1050
|
+
if (!await checkGhInstalled()) {
|
|
1051
|
+
error("gh CLI is not installed. Install it from https://cli.github.com");
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
if (!await checkGhAuth()) {
|
|
1055
|
+
error("Not authenticated. Run `gh auth login` first.");
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
const repo = args.repo || await detectRepo();
|
|
1059
|
+
if (!repo) {
|
|
1060
|
+
error("Could not detect repository. Use --repo <owner/repo> or run inside a git repo.");
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
info(`Target: ${repo}`);
|
|
1064
|
+
const labels = await listLabelsDetailed(repo);
|
|
1065
|
+
if (labels.length === 0) {
|
|
1066
|
+
info("No labels found.");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
heading(`Labels (${labels.length} total)`);
|
|
1070
|
+
for (const label of labels) {
|
|
1071
|
+
const name = pc6.bold(label.name.padEnd(30));
|
|
1072
|
+
const color = pc6.dim(`#${label.color}`);
|
|
1073
|
+
const desc = label.description ? pc6.dim(label.description) : "";
|
|
1074
|
+
console.log(` ${name} ${color} ${desc}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// src/commands/check.ts
|
|
1080
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1081
|
+
import pc7 from "picocolors";
|
|
1082
|
+
function statusIcon(status) {
|
|
1083
|
+
if (status === "match")
|
|
1084
|
+
return pc7.green("โ");
|
|
1085
|
+
if (status === "missing")
|
|
1086
|
+
return pc7.red("โ");
|
|
1087
|
+
return pc7.yellow("~");
|
|
1088
|
+
}
|
|
1089
|
+
function statusLabel(status) {
|
|
1090
|
+
switch (status) {
|
|
1091
|
+
case "match":
|
|
1092
|
+
return pc7.dim("match");
|
|
1093
|
+
case "color-mismatch":
|
|
1094
|
+
return pc7.yellow("color mismatch");
|
|
1095
|
+
case "desc-mismatch":
|
|
1096
|
+
return pc7.yellow("description mismatch");
|
|
1097
|
+
case "both-mismatch":
|
|
1098
|
+
return pc7.yellow("color + description mismatch");
|
|
1099
|
+
case "missing":
|
|
1100
|
+
return pc7.red("missing");
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function isFailing(status, strict) {
|
|
1104
|
+
if (status === "missing")
|
|
1105
|
+
return true;
|
|
1106
|
+
if (strict && status !== "match")
|
|
1107
|
+
return true;
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
var check_default = defineCommand6({
|
|
1111
|
+
meta: {
|
|
1112
|
+
name: "check",
|
|
1113
|
+
description: "Check if a repository is using the Clean Label template"
|
|
1114
|
+
},
|
|
1115
|
+
args: {
|
|
1116
|
+
repo: {
|
|
1117
|
+
type: "string",
|
|
1118
|
+
alias: "r",
|
|
1119
|
+
description: "Target repository (owner/repo). Defaults to current repo."
|
|
1120
|
+
},
|
|
1121
|
+
category: {
|
|
1122
|
+
type: "string",
|
|
1123
|
+
alias: "c",
|
|
1124
|
+
description: 'Check specific category(ies) only. Comma-separated (e.g., --category "type,status")'
|
|
1125
|
+
},
|
|
1126
|
+
strict: {
|
|
1127
|
+
type: "boolean",
|
|
1128
|
+
alias: "s",
|
|
1129
|
+
default: false,
|
|
1130
|
+
description: "Strict mode โ also flag labels with mismatched color or description"
|
|
1131
|
+
}
|
|
1132
|
+
},
|
|
1133
|
+
async run({ args }) {
|
|
1134
|
+
if (!await checkGhInstalled()) {
|
|
1135
|
+
error("gh CLI is not installed. Install it from https://cli.github.com");
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
if (!await checkGhAuth()) {
|
|
1139
|
+
error("Not authenticated. Run `gh auth login` first.");
|
|
1140
|
+
process.exit(1);
|
|
1141
|
+
}
|
|
1142
|
+
const repo = args.repo || await detectRepo();
|
|
1143
|
+
if (!repo) {
|
|
1144
|
+
error("Could not detect repository. Use --repo <owner/repo> or run inside a git repo.");
|
|
1145
|
+
process.exit(1);
|
|
1146
|
+
}
|
|
1147
|
+
const strict = args.strict ?? false;
|
|
1148
|
+
info(`Target: ${repo}`);
|
|
1149
|
+
if (strict)
|
|
1150
|
+
info("Mode: strict (name + color + description)");
|
|
1151
|
+
const repoLabels = await listLabelsDetailed(repo);
|
|
1152
|
+
const repoMap = new Map(repoLabels.map((l) => [l.name.toLowerCase(), l]));
|
|
1153
|
+
const { entries: templateEntries } = filterLabels(labels_default, {
|
|
1154
|
+
category: args.category
|
|
1155
|
+
});
|
|
1156
|
+
const results = [];
|
|
1157
|
+
for (const [category, categoryLabels] of templateEntries) {
|
|
1158
|
+
for (const tmpl of categoryLabels) {
|
|
1159
|
+
const existing = repoMap.get(tmpl.name.toLowerCase());
|
|
1160
|
+
let status;
|
|
1161
|
+
let repoColor;
|
|
1162
|
+
let repoDesc;
|
|
1163
|
+
if (!existing) {
|
|
1164
|
+
status = "missing";
|
|
1165
|
+
} else {
|
|
1166
|
+
const colorMatch = existing.color.toLowerCase() === tmpl.color.toLowerCase();
|
|
1167
|
+
const descMatch = existing.description.trim() === tmpl.description.trim();
|
|
1168
|
+
if (colorMatch && descMatch) {
|
|
1169
|
+
status = "match";
|
|
1170
|
+
} else if (!colorMatch && !descMatch) {
|
|
1171
|
+
status = "both-mismatch";
|
|
1172
|
+
repoColor = existing.color;
|
|
1173
|
+
repoDesc = existing.description;
|
|
1174
|
+
} else if (!colorMatch) {
|
|
1175
|
+
status = "color-mismatch";
|
|
1176
|
+
repoColor = existing.color;
|
|
1177
|
+
} else {
|
|
1178
|
+
status = "desc-mismatch";
|
|
1179
|
+
repoDesc = existing.description;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
results.push({ template: tmpl, category, status, repoColor, repoDesc });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const byCategory = new Map;
|
|
1186
|
+
for (const result of results) {
|
|
1187
|
+
if (!byCategory.has(result.category))
|
|
1188
|
+
byCategory.set(result.category, []);
|
|
1189
|
+
byCategory.get(result.category).push(result);
|
|
1190
|
+
}
|
|
1191
|
+
console.log("");
|
|
1192
|
+
for (const [category, catResults] of byCategory) {
|
|
1193
|
+
const catTitle = category.charAt(0).toUpperCase() + category.slice(1);
|
|
1194
|
+
const catFailing = catResults.filter((r) => isFailing(r.status, strict)).length;
|
|
1195
|
+
const catStatus = catFailing > 0 ? pc7.red(`${catFailing} issue${catFailing !== 1 ? "s" : ""}`) : pc7.green("all good");
|
|
1196
|
+
console.log(`${pc7.bold(catTitle)} ${pc7.dim(`(${catResults.length})`)} โ ${catStatus}`);
|
|
1197
|
+
for (const result of catResults) {
|
|
1198
|
+
const icon = statusIcon(result.status);
|
|
1199
|
+
const name = result.template.name.padEnd(28);
|
|
1200
|
+
const color = pc7.dim(`#${result.template.color}`);
|
|
1201
|
+
const lbl = statusLabel(result.status);
|
|
1202
|
+
let line = ` ${icon} ${name} ${color} ${lbl}`;
|
|
1203
|
+
if (result.repoColor) {
|
|
1204
|
+
line += pc7.dim(` (repo: #${result.repoColor})`);
|
|
1205
|
+
}
|
|
1206
|
+
if (result.repoDesc) {
|
|
1207
|
+
line += pc7.dim(`
|
|
1208
|
+
repo desc: "${result.repoDesc}"`);
|
|
1209
|
+
}
|
|
1210
|
+
console.log(line);
|
|
1211
|
+
}
|
|
1212
|
+
console.log("");
|
|
1213
|
+
}
|
|
1214
|
+
const matched = results.filter((r) => r.status === "match").length;
|
|
1215
|
+
const missing = results.filter((r) => r.status === "missing").length;
|
|
1216
|
+
const mismatched = results.filter((r) => r.status !== "match" && r.status !== "missing").length;
|
|
1217
|
+
const total = results.length;
|
|
1218
|
+
const failing = results.filter((r) => isFailing(r.status, strict)).length;
|
|
1219
|
+
const mismatchedStr = mismatched > 0 ? pc7.yellow(`, ${mismatched} mismatched`) : "";
|
|
1220
|
+
const missingStr = missing > 0 ? pc7.red(`, ${missing} missing`) : "";
|
|
1221
|
+
const scoreStr = pc7.bold(`${matched}/${total}`);
|
|
1222
|
+
console.log(`${pc7.bold("Result:")} ${scoreStr} labels matched${mismatchedStr}${missingStr}`);
|
|
1223
|
+
if (failing === 0) {
|
|
1224
|
+
const mismatchHint = !strict && mismatched > 0 ? pc7.dim(` (${mismatched} mismatch${mismatched !== 1 ? "es" : ""} โ use --strict to enforce)`) : "";
|
|
1225
|
+
console.log(`${pc7.bold("Compatible:")} ${pc7.green("โ Yes")}${strict ? " (strict)" : ""}${mismatchHint}`);
|
|
1226
|
+
} else {
|
|
1227
|
+
console.log(`${pc7.bold("Compatible:")} ${pc7.red("โ No")} โ run ${pc7.bold("ghlt apply")} to fix`);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
|
|
896
1233
|
// src/ui/banner.ts
|
|
897
1234
|
import figlet from "figlet";
|
|
898
|
-
import
|
|
1235
|
+
import pc8 from "picocolors";
|
|
899
1236
|
// package.json
|
|
900
1237
|
var package_default = {
|
|
901
1238
|
name: "github-labels-template",
|
|
902
|
-
version: "0.
|
|
1239
|
+
version: "0.7.0-staging.63849e1",
|
|
903
1240
|
description: "A CLI tool to apply a curated GitHub labels template to any repository using gh CLI.",
|
|
904
1241
|
type: "module",
|
|
905
1242
|
bin: {
|
|
@@ -964,15 +1301,15 @@ function getAuthor() {
|
|
|
964
1301
|
return package_default.author ?? "unknown";
|
|
965
1302
|
}
|
|
966
1303
|
function showBanner(minimal = false) {
|
|
967
|
-
console.log(
|
|
1304
|
+
console.log(pc8.cyan(`
|
|
968
1305
|
` + LOGO));
|
|
969
|
-
console.log(` ${
|
|
1306
|
+
console.log(` ${pc8.dim("v" + getVersion())} ${pc8.dim("โ")} ${pc8.dim("Built by " + getAuthor())}`);
|
|
970
1307
|
if (!minimal) {
|
|
971
|
-
console.log(` ${
|
|
1308
|
+
console.log(` ${pc8.dim(package_default.description)}`);
|
|
972
1309
|
console.log();
|
|
973
|
-
console.log(` ${
|
|
974
|
-
console.log(` ${
|
|
975
|
-
console.log(` ${
|
|
1310
|
+
console.log(` ${pc8.yellow("Star")} ${pc8.cyan("https://gh.waren.build/github-labels-template")}`);
|
|
1311
|
+
console.log(` ${pc8.green("Contribute")} ${pc8.cyan("https://gh.waren.build/github-labels-template/blob/main/CONTRIBUTING.md")}`);
|
|
1312
|
+
console.log(` ${pc8.magenta("Sponsor")} ${pc8.cyan("https://warengonzaga.com/sponsor")}`);
|
|
976
1313
|
}
|
|
977
1314
|
console.log();
|
|
978
1315
|
}
|
|
@@ -980,7 +1317,7 @@ function showBanner(minimal = false) {
|
|
|
980
1317
|
// src/index.ts
|
|
981
1318
|
var isHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
982
1319
|
showBanner(isHelp);
|
|
983
|
-
var main =
|
|
1320
|
+
var main = defineCommand7({
|
|
984
1321
|
meta: {
|
|
985
1322
|
name: "ghlt",
|
|
986
1323
|
version: getVersion(),
|
|
@@ -997,7 +1334,9 @@ var main = defineCommand5({
|
|
|
997
1334
|
apply: apply_default,
|
|
998
1335
|
wipe: wipe_default,
|
|
999
1336
|
migrate: migrate_default,
|
|
1000
|
-
generate: generate_default
|
|
1337
|
+
generate: generate_default,
|
|
1338
|
+
list: list_default,
|
|
1339
|
+
check: check_default
|
|
1001
1340
|
},
|
|
1002
1341
|
run({ args }) {
|
|
1003
1342
|
if (args.version) {
|