github-labels-template 0.6.0 โ†’ 0.6.1-staging.3ace40c

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 +189 -96
  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
@@ -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)`);
@@ -483,85 +608,8 @@ var wipe_default = defineCommand2({
483
608
  }
484
609
  });
485
610
 
486
- // src/commands/preview.ts
487
- import { defineCommand as defineCommand3 } from "citty";
488
- import { resolve as resolve2, join } from "path";
489
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
490
- import { createServer } from "http";
491
- var MIME_TYPES = {
492
- ".html": "text/html",
493
- ".css": "text/css",
494
- ".js": "application/javascript",
495
- ".json": "application/json",
496
- ".png": "image/png",
497
- ".jpg": "image/jpeg",
498
- ".svg": "image/svg+xml",
499
- ".ico": "image/x-icon",
500
- ".woff": "font/woff",
501
- ".woff2": "font/woff2"
502
- };
503
- function getMimeType(filePath) {
504
- const ext = filePath.slice(filePath.lastIndexOf("."));
505
- return MIME_TYPES[ext] || "application/octet-stream";
506
- }
507
- var preview_default = defineCommand3({
508
- meta: {
509
- name: "preview",
510
- description: "Preview the landing page locally in your browser"
511
- },
512
- args: {
513
- port: {
514
- type: "string",
515
- alias: "p",
516
- default: "3000",
517
- description: "Port to serve on (default: 3000)"
518
- }
519
- },
520
- async run({ args }) {
521
- const docsDir = resolve2(import.meta.dirname ?? ".", "..", "..", "docs");
522
- if (!existsSync2(docsDir)) {
523
- error("docs/ directory not found. Nothing to preview.");
524
- process.exit(1);
525
- }
526
- const indexPath = join(docsDir, "index.html");
527
- if (!existsSync2(indexPath)) {
528
- error("docs/index.html not found. Nothing to preview.");
529
- process.exit(1);
530
- }
531
- const port = parseInt(args.port, 10) || 3000;
532
- const server = createServer((req, res) => {
533
- const url = req.url === "/" ? "/index.html" : req.url || "/index.html";
534
- const filePath = join(docsDir, url);
535
- if (!filePath.startsWith(docsDir)) {
536
- res.writeHead(403);
537
- res.end("Forbidden");
538
- return;
539
- }
540
- if (!existsSync2(filePath)) {
541
- res.writeHead(404);
542
- res.end("Not Found");
543
- return;
544
- }
545
- try {
546
- const content = readFileSync2(filePath);
547
- res.writeHead(200, { "Content-Type": getMimeType(filePath) });
548
- res.end(content);
549
- } catch {
550
- res.writeHead(500);
551
- res.end("Internal Server Error");
552
- }
553
- });
554
- server.listen(port, () => {
555
- const url = `http://localhost:${port}`;
556
- success(`Landing page preview running at ${url}`);
557
- info("Press Ctrl+C to stop the server");
558
- });
559
- await new Promise(() => {});
560
- }
561
- });
562
-
563
611
  // src/commands/generate.ts
564
- import { defineCommand as defineCommand4 } from "citty";
612
+ import { defineCommand as defineCommand3 } from "citty";
565
613
  import { select, input, confirm } from "@inquirer/prompts";
566
614
 
567
615
  // src/utils/copilot.ts
@@ -705,7 +753,7 @@ var CATEGORIES = [
705
753
  function formatLabelChoice(label, index) {
706
754
  return `${pc4.bold(label.name)} ${pc4.dim(`#${label.color}`)} โ€” ${label.description}`;
707
755
  }
708
- var generate_default = defineCommand4({
756
+ var generate_default = defineCommand3({
709
757
  meta: {
710
758
  name: "generate",
711
759
  description: "Generate custom labels using AI (requires GitHub Copilot subscription)"
@@ -865,9 +913,9 @@ var generate_default = defineCommand4({
865
913
  });
866
914
 
867
915
  // src/commands/migrate.ts
868
- import { defineCommand as defineCommand5 } from "citty";
916
+ import { defineCommand as defineCommand4 } from "citty";
869
917
  import pc5 from "picocolors";
870
- var migrate_default = defineCommand5({
918
+ var migrate_default = defineCommand4({
871
919
  meta: {
872
920
  name: "migrate",
873
921
  description: "Wipe all existing labels and apply the template (clean slate)"
@@ -970,13 +1018,58 @@ var migrate_default = defineCommand5({
970
1018
  }
971
1019
  });
972
1020
 
1021
+ // src/commands/list.ts
1022
+ import { defineCommand as defineCommand5 } from "citty";
1023
+ import pc6 from "picocolors";
1024
+ var list_default = defineCommand5({
1025
+ meta: {
1026
+ name: "list",
1027
+ description: "List all labels in a repository"
1028
+ },
1029
+ args: {
1030
+ repo: {
1031
+ type: "string",
1032
+ alias: "r",
1033
+ description: "Target repository (owner/repo). Defaults to current repo."
1034
+ }
1035
+ },
1036
+ async run({ args }) {
1037
+ if (!await checkGhInstalled()) {
1038
+ error("gh CLI is not installed. Install it from https://cli.github.com");
1039
+ process.exit(1);
1040
+ }
1041
+ if (!await checkGhAuth()) {
1042
+ error("Not authenticated. Run `gh auth login` first.");
1043
+ process.exit(1);
1044
+ }
1045
+ const repo = args.repo || await detectRepo();
1046
+ if (!repo) {
1047
+ error("Could not detect repository. Use --repo <owner/repo> or run inside a git repo.");
1048
+ process.exit(1);
1049
+ }
1050
+ info(`Target: ${repo}`);
1051
+ const labels = await listLabelsDetailed(repo);
1052
+ if (labels.length === 0) {
1053
+ info("No labels found.");
1054
+ return;
1055
+ }
1056
+ heading(`Labels (${labels.length} total)`);
1057
+ for (const label of labels) {
1058
+ const name = pc6.bold(label.name.padEnd(30));
1059
+ const color = pc6.dim(`#${label.color}`);
1060
+ const desc = label.description ? pc6.dim(label.description) : "";
1061
+ console.log(` ${name} ${color} ${desc}`);
1062
+ }
1063
+ }
1064
+ });
1065
+
973
1066
  // src/ui/banner.ts
974
1067
  import figlet from "figlet";
975
- import pc6 from "picocolors";
1068
+ import pc7 from "picocolors";
976
1069
  // package.json
977
1070
  var package_default = {
978
1071
  name: "github-labels-template",
979
- version: "0.6.0",
1072
+ version: "0.6.1-staging.3ace40c",
980
1073
  description: "A CLI tool to apply a curated GitHub labels template to any repository using gh CLI.",
981
1074
  type: "module",
982
1075
  bin: {
@@ -1041,15 +1134,15 @@ function getAuthor() {
1041
1134
  return package_default.author ?? "unknown";
1042
1135
  }
1043
1136
  function showBanner(minimal = false) {
1044
- console.log(pc6.cyan(`
1137
+ console.log(pc7.cyan(`
1045
1138
  ` + LOGO));
1046
- console.log(` ${pc6.dim("v" + getVersion())} ${pc6.dim("โ€”")} ${pc6.dim("Built by " + getAuthor())}`);
1139
+ console.log(` ${pc7.dim("v" + getVersion())} ${pc7.dim("โ€”")} ${pc7.dim("Built by " + getAuthor())}`);
1047
1140
  if (!minimal) {
1048
- console.log(` ${pc6.dim(package_default.description)}`);
1141
+ console.log(` ${pc7.dim(package_default.description)}`);
1049
1142
  console.log();
1050
- console.log(` ${pc6.yellow("Star")} ${pc6.cyan("https://gh.waren.build/github-labels-template")}`);
1051
- console.log(` ${pc6.green("Contribute")} ${pc6.cyan("https://gh.waren.build/github-labels-template/blob/main/CONTRIBUTING.md")}`);
1052
- console.log(` ${pc6.magenta("Sponsor")} ${pc6.cyan("https://warengonzaga.com/sponsor")}`);
1143
+ console.log(` ${pc7.yellow("Star")} ${pc7.cyan("https://gh.waren.build/github-labels-template")}`);
1144
+ console.log(` ${pc7.green("Contribute")} ${pc7.cyan("https://gh.waren.build/github-labels-template/blob/main/CONTRIBUTING.md")}`);
1145
+ console.log(` ${pc7.magenta("Sponsor")} ${pc7.cyan("https://warengonzaga.com/sponsor")}`);
1053
1146
  }
1054
1147
  console.log();
1055
1148
  }
@@ -1074,8 +1167,8 @@ var main = defineCommand6({
1074
1167
  apply: apply_default,
1075
1168
  wipe: wipe_default,
1076
1169
  migrate: migrate_default,
1077
- preview: preview_default,
1078
- generate: generate_default
1170
+ generate: generate_default,
1171
+ list: list_default
1079
1172
  },
1080
1173
  run({ args }) {
1081
1174
  if (args.version) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-labels-template",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-staging.3ace40c",
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": {