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.
Files changed (3) hide show
  1. package/README.md +49 -7
  2. package/dist/index.js +363 -24
  3. 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
- - ๐Ÿงน **Wipe Command**: Remove all existing labels with a confirmation prompt
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 existing labels from a repository
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 defineCommand5, runMain } from "citty";
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
- if (!labelFilter && !categoryFilter) {
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
- const entries = Object.entries(allLabels).map(([category, categoryLabels]) => {
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 confirmed = await confirmPrompt(pc3.bold(pc3.red(`This will delete all ${existing.length} labels from ${repo}.`)));
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 existing) {
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 pc6 from "picocolors";
1235
+ import pc8 from "picocolors";
899
1236
  // package.json
900
1237
  var package_default = {
901
1238
  name: "github-labels-template",
902
- version: "0.6.1",
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(pc6.cyan(`
1304
+ console.log(pc8.cyan(`
968
1305
  ` + LOGO));
969
- console.log(` ${pc6.dim("v" + getVersion())} ${pc6.dim("โ€”")} ${pc6.dim("Built by " + getAuthor())}`);
1306
+ console.log(` ${pc8.dim("v" + getVersion())} ${pc8.dim("โ€”")} ${pc8.dim("Built by " + getAuthor())}`);
970
1307
  if (!minimal) {
971
- console.log(` ${pc6.dim(package_default.description)}`);
1308
+ console.log(` ${pc8.dim(package_default.description)}`);
972
1309
  console.log();
973
- console.log(` ${pc6.yellow("Star")} ${pc6.cyan("https://gh.waren.build/github-labels-template")}`);
974
- console.log(` ${pc6.green("Contribute")} ${pc6.cyan("https://gh.waren.build/github-labels-template/blob/main/CONTRIBUTING.md")}`);
975
- console.log(` ${pc6.magenta("Sponsor")} ${pc6.cyan("https://warengonzaga.com/sponsor")}`);
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 = defineCommand5({
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-labels-template",
3
- "version": "0.6.1",
3
+ "version": "0.7.0-staging.63849e1",
4
4
  "description": "A CLI tool to apply a curated GitHub labels template to any repository using gh CLI.",
5
5
  "type": "module",
6
6
  "bin": {