@asagiri-design/labels-config 0.2.2 → 0.3.1

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/dist/cli.js CHANGED
@@ -1,7 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
9
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
5
26
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
27
 
7
28
  // src/cli.ts
@@ -280,6 +301,60 @@ var ConfigLoader = class {
280
301
  }
281
302
  };
282
303
 
304
+ // src/config/batch-config.ts
305
+ var BatchConfigLoader = class {
306
+ /**
307
+ * バッチ設定ファイルの読み込み
308
+ */
309
+ static async load(filePath) {
310
+ const { promises: fs2 } = await import("fs");
311
+ try {
312
+ const content = await fs2.readFile(filePath, "utf-8");
313
+ const config = JSON.parse(content);
314
+ this.validate(config);
315
+ return config;
316
+ } catch (error2) {
317
+ throw new Error(`Failed to load batch config: ${error2}`);
318
+ }
319
+ }
320
+ /**
321
+ * バッチ設定のバリデーション
322
+ */
323
+ static validate(config) {
324
+ if (!config.version) {
325
+ throw new Error("Batch config version is required");
326
+ }
327
+ if (!config.targets || config.targets.length === 0) {
328
+ throw new Error("At least one target is required");
329
+ }
330
+ config.targets.forEach((target, index) => {
331
+ const hasRepoSpec = target.organization || target.user || target.repositories;
332
+ if (!hasRepoSpec) {
333
+ throw new Error(`Target ${index}: One of organization, user, or repositories is required`);
334
+ }
335
+ const hasLabelSpec = target.template || target.file;
336
+ if (!hasLabelSpec) {
337
+ throw new Error(`Target ${index}: Either template or file is required`);
338
+ }
339
+ });
340
+ }
341
+ /**
342
+ * BatchConfigTargetをBatchSyncOptionsに変換
343
+ */
344
+ static targetToOptions(target, defaults) {
345
+ return {
346
+ organization: target.organization,
347
+ user: target.user,
348
+ repositories: target.repositories,
349
+ template: target.template || defaults?.template,
350
+ mode: target.mode || defaults?.mode || "append",
351
+ parallel: target.parallel || defaults?.parallel || 3,
352
+ filter: target.filter,
353
+ dryRun: false
354
+ };
355
+ }
356
+ };
357
+
283
358
  // src/github/client.ts
284
359
  var import_child_process = require("child_process");
285
360
  var GitHubClient = class {
@@ -557,6 +632,262 @@ var _GitHubLabelSync = class _GitHubLabelSync {
557
632
  __publicField(_GitHubLabelSync, "BATCH_SIZE", 5);
558
633
  var GitHubLabelSync = _GitHubLabelSync;
559
634
 
635
+ // src/utils/ui.ts
636
+ var colors = {
637
+ reset: "\x1B[0m",
638
+ bright: "\x1B[1m",
639
+ dim: "\x1B[2m",
640
+ // Foreground colors
641
+ red: "\x1B[31m",
642
+ green: "\x1B[32m",
643
+ yellow: "\x1B[33m",
644
+ blue: "\x1B[34m",
645
+ magenta: "\x1B[35m",
646
+ cyan: "\x1B[36m",
647
+ white: "\x1B[37m",
648
+ gray: "\x1B[90m",
649
+ // Background colors
650
+ bgRed: "\x1B[41m",
651
+ bgGreen: "\x1B[42m",
652
+ bgYellow: "\x1B[43m",
653
+ bgBlue: "\x1B[44m"
654
+ };
655
+ function supportsColor() {
656
+ if (process.env.NO_COLOR) {
657
+ return false;
658
+ }
659
+ if (process.env.FORCE_COLOR) {
660
+ return true;
661
+ }
662
+ if (!process.stdout.isTTY) {
663
+ return false;
664
+ }
665
+ if (process.platform === "win32") {
666
+ return true;
667
+ }
668
+ return true;
669
+ }
670
+ function colorize(text, color) {
671
+ if (!supportsColor()) {
672
+ return text;
673
+ }
674
+ return `${colors[color]}${text}${colors.reset}`;
675
+ }
676
+ function success(message) {
677
+ return colorize("\u2713", "green") + " " + message;
678
+ }
679
+ function error(message) {
680
+ return colorize("\u2717", "red") + " " + message;
681
+ }
682
+ function warning(message) {
683
+ return colorize("\u26A0", "yellow") + " " + message;
684
+ }
685
+ function info(message) {
686
+ return colorize("\u2139", "blue") + " " + message;
687
+ }
688
+ function header(text) {
689
+ return "\n" + colorize(text, "bright") + "\n" + "\u2500".repeat(Math.min(text.length, 50));
690
+ }
691
+ var Spinner = class {
692
+ constructor() {
693
+ __publicField(this, "frames", ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]);
694
+ __publicField(this, "interval", null);
695
+ __publicField(this, "frameIndex", 0);
696
+ __publicField(this, "message", "");
697
+ }
698
+ start(message) {
699
+ this.message = message;
700
+ if (!process.stdout.isTTY || !supportsColor()) {
701
+ console.log(message + "...");
702
+ return;
703
+ }
704
+ this.interval = setInterval(() => {
705
+ const frame = this.frames[this.frameIndex];
706
+ process.stdout.write(`\r${colorize(frame, "cyan")} ${this.message}`);
707
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
708
+ }, 80);
709
+ }
710
+ succeed(message) {
711
+ this.stop();
712
+ console.log(success(message || this.message));
713
+ }
714
+ fail(message) {
715
+ this.stop();
716
+ console.log(error(message || this.message));
717
+ }
718
+ warn(message) {
719
+ this.stop();
720
+ console.log(warning(message || this.message));
721
+ }
722
+ stop() {
723
+ if (this.interval) {
724
+ clearInterval(this.interval);
725
+ this.interval = null;
726
+ if (process.stdout.isTTY) {
727
+ process.stdout.write("\r\x1B[K");
728
+ }
729
+ }
730
+ }
731
+ };
732
+
733
+ // src/github/batch-sync.ts
734
+ var _BatchLabelSync = class _BatchLabelSync {
735
+ /**
736
+ * 複数リポジトリへのラベル一括同期
737
+ */
738
+ async syncMultiple(labels, options) {
739
+ const repos = await this.getTargetRepositories(options);
740
+ const results = [];
741
+ console.log(colorize(`
742
+ \u{1F4CB} Target repositories: ${repos.length}`, "cyan"));
743
+ let completed = 0;
744
+ const parallel = options.parallel || _BatchLabelSync.DEFAULT_PARALLEL;
745
+ for (let i = 0; i < repos.length; i += parallel) {
746
+ const batch = repos.slice(i, i + parallel);
747
+ const batchResults = await Promise.allSettled(
748
+ batch.map((repo) => this.syncSingleRepo(repo, labels, options))
749
+ );
750
+ batchResults.forEach((result, index) => {
751
+ const repo = batch[index];
752
+ if (result.status === "fulfilled") {
753
+ results.push(result.value);
754
+ completed++;
755
+ console.log(colorize(`\u2705 [${completed}/${repos.length}] ${repo}`, "green"));
756
+ } else {
757
+ results.push({
758
+ repository: repo,
759
+ status: "failed",
760
+ error: result.reason?.message || "Unknown error"
761
+ });
762
+ completed++;
763
+ console.log(colorize(`\u274C [${completed}/${repos.length}] ${repo}: ${result.reason}`, "red"));
764
+ }
765
+ });
766
+ }
767
+ return results;
768
+ }
769
+ /**
770
+ * 単一リポジトリへの同期
771
+ */
772
+ async syncSingleRepo(repository, labels, options) {
773
+ try {
774
+ const [owner, repo] = repository.split("/");
775
+ if (!owner || !repo) {
776
+ throw new Error(`Invalid repository format: ${repository}. Expected format: owner/repo`);
777
+ }
778
+ const sync = new GitHubLabelSync({
779
+ owner,
780
+ repo,
781
+ deleteExtra: options.mode === "replace",
782
+ dryRun: options.dryRun || false
783
+ });
784
+ const result = await sync.syncLabels(labels);
785
+ return {
786
+ repository,
787
+ status: "success",
788
+ result
789
+ };
790
+ } catch (error2) {
791
+ return {
792
+ repository,
793
+ status: "failed",
794
+ error: error2 instanceof Error ? error2.message : "Unknown error"
795
+ };
796
+ }
797
+ }
798
+ /**
799
+ * 対象リポジトリリストの取得
800
+ */
801
+ async getTargetRepositories(options) {
802
+ if (options.repositories && options.repositories.length > 0) {
803
+ return options.repositories;
804
+ }
805
+ if (options.organization) {
806
+ return this.getOrganizationRepos(options.organization, options.filter);
807
+ }
808
+ if (options.user) {
809
+ return this.getUserRepos(options.user, options.filter);
810
+ }
811
+ throw new Error("No target repositories specified");
812
+ }
813
+ /**
814
+ * 組織のリポジトリ一覧を取得
815
+ */
816
+ async getOrganizationRepos(org, filter) {
817
+ const { execSync: execSync2 } = await import("child_process");
818
+ try {
819
+ const command = `gh repo list ${org} --json nameWithOwner,visibility,language,isArchived --limit 1000`;
820
+ const output = execSync2(command, { encoding: "utf-8" });
821
+ const repos = JSON.parse(output);
822
+ return repos.filter((repo) => {
823
+ if (filter?.visibility && filter.visibility !== "all" && repo.visibility !== filter.visibility) {
824
+ return false;
825
+ }
826
+ if (filter?.language && repo.language !== filter.language) {
827
+ return false;
828
+ }
829
+ if (filter?.archived !== void 0 && repo.isArchived !== filter.archived) {
830
+ return false;
831
+ }
832
+ return true;
833
+ }).map((repo) => repo.nameWithOwner);
834
+ } catch (error2) {
835
+ throw new Error(`Failed to fetch organization repos: ${error2}`);
836
+ }
837
+ }
838
+ /**
839
+ * ユーザーのリポジトリ一覧を取得
840
+ */
841
+ async getUserRepos(user, filter) {
842
+ const { execSync: execSync2 } = await import("child_process");
843
+ try {
844
+ const command = `gh repo list ${user} --json nameWithOwner,visibility,language,isArchived --limit 1000`;
845
+ const output = execSync2(command, { encoding: "utf-8" });
846
+ const repos = JSON.parse(output);
847
+ return repos.filter((repo) => {
848
+ if (filter?.visibility && filter.visibility !== "all" && repo.visibility !== filter.visibility) {
849
+ return false;
850
+ }
851
+ if (filter?.language && repo.language !== filter.language) {
852
+ return false;
853
+ }
854
+ if (filter?.archived !== void 0 && repo.isArchived !== filter.archived) {
855
+ return false;
856
+ }
857
+ return true;
858
+ }).map((repo) => repo.nameWithOwner);
859
+ } catch (error2) {
860
+ throw new Error(`Failed to fetch user repos: ${error2}`);
861
+ }
862
+ }
863
+ /**
864
+ * 結果サマリーの生成
865
+ */
866
+ generateSummary(results) {
867
+ const successful = results.filter((r) => r.status === "success").length;
868
+ const failed = results.filter((r) => r.status === "failed").length;
869
+ const skipped = results.filter((r) => r.status === "skipped").length;
870
+ let summary = "\n\u{1F4CA} Batch Sync Summary:\n";
871
+ summary += `\u2705 Successful: ${successful}
872
+ `;
873
+ if (failed > 0) summary += `\u274C Failed: ${failed}
874
+ `;
875
+ if (skipped > 0) summary += `\u23ED\uFE0F Skipped: ${skipped}
876
+ `;
877
+ const failedRepos = results.filter((r) => r.status === "failed");
878
+ if (failedRepos.length > 0) {
879
+ summary += "\n\u274C Failed repositories:\n";
880
+ failedRepos.forEach((repo) => {
881
+ summary += ` - ${repo.repository}: ${repo.error}
882
+ `;
883
+ });
884
+ }
885
+ return summary;
886
+ }
887
+ };
888
+ __publicField(_BatchLabelSync, "DEFAULT_PARALLEL", 3);
889
+ var BatchLabelSync = _BatchLabelSync;
890
+
560
891
  // src/config/templates.ts
561
892
  var MINIMAL_TEMPLATE = [
562
893
  {
@@ -1058,104 +1389,6 @@ function getPositional(args, index) {
1058
1389
  return args.positional[index];
1059
1390
  }
1060
1391
 
1061
- // src/utils/ui.ts
1062
- var colors = {
1063
- reset: "\x1B[0m",
1064
- bright: "\x1B[1m",
1065
- dim: "\x1B[2m",
1066
- // Foreground colors
1067
- red: "\x1B[31m",
1068
- green: "\x1B[32m",
1069
- yellow: "\x1B[33m",
1070
- blue: "\x1B[34m",
1071
- magenta: "\x1B[35m",
1072
- cyan: "\x1B[36m",
1073
- white: "\x1B[37m",
1074
- gray: "\x1B[90m",
1075
- // Background colors
1076
- bgRed: "\x1B[41m",
1077
- bgGreen: "\x1B[42m",
1078
- bgYellow: "\x1B[43m",
1079
- bgBlue: "\x1B[44m"
1080
- };
1081
- function supportsColor() {
1082
- if (process.env.NO_COLOR) {
1083
- return false;
1084
- }
1085
- if (process.env.FORCE_COLOR) {
1086
- return true;
1087
- }
1088
- if (!process.stdout.isTTY) {
1089
- return false;
1090
- }
1091
- if (process.platform === "win32") {
1092
- return true;
1093
- }
1094
- return true;
1095
- }
1096
- function colorize(text, color) {
1097
- if (!supportsColor()) {
1098
- return text;
1099
- }
1100
- return `${colors[color]}${text}${colors.reset}`;
1101
- }
1102
- function success(message) {
1103
- return colorize("\u2713", "green") + " " + message;
1104
- }
1105
- function error(message) {
1106
- return colorize("\u2717", "red") + " " + message;
1107
- }
1108
- function warning(message) {
1109
- return colorize("\u26A0", "yellow") + " " + message;
1110
- }
1111
- function info(message) {
1112
- return colorize("\u2139", "blue") + " " + message;
1113
- }
1114
- function header(text) {
1115
- return "\n" + colorize(text, "bright") + "\n" + "\u2500".repeat(Math.min(text.length, 50));
1116
- }
1117
- var Spinner = class {
1118
- constructor() {
1119
- __publicField(this, "frames", ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]);
1120
- __publicField(this, "interval", null);
1121
- __publicField(this, "frameIndex", 0);
1122
- __publicField(this, "message", "");
1123
- }
1124
- start(message) {
1125
- this.message = message;
1126
- if (!process.stdout.isTTY || !supportsColor()) {
1127
- console.log(message + "...");
1128
- return;
1129
- }
1130
- this.interval = setInterval(() => {
1131
- const frame = this.frames[this.frameIndex];
1132
- process.stdout.write(`\r${colorize(frame, "cyan")} ${this.message}`);
1133
- this.frameIndex = (this.frameIndex + 1) % this.frames.length;
1134
- }, 80);
1135
- }
1136
- succeed(message) {
1137
- this.stop();
1138
- console.log(success(message || this.message));
1139
- }
1140
- fail(message) {
1141
- this.stop();
1142
- console.log(error(message || this.message));
1143
- }
1144
- warn(message) {
1145
- this.stop();
1146
- console.log(warning(message || this.message));
1147
- }
1148
- stop() {
1149
- if (this.interval) {
1150
- clearInterval(this.interval);
1151
- this.interval = null;
1152
- if (process.stdout.isTTY) {
1153
- process.stdout.write("\r\x1B[K");
1154
- }
1155
- }
1156
- }
1157
- };
1158
-
1159
1392
  // src/cli.ts
1160
1393
  var rawArgs = process.argv.slice(2);
1161
1394
  var parsedArgs = parseArgs(rawArgs);
@@ -1164,6 +1397,8 @@ function printUsage() {
1164
1397
  console.log(header("Commands"));
1165
1398
  console.log(" " + colorize("validate", "cyan") + " <file> Validate label configuration file");
1166
1399
  console.log(" " + colorize("sync", "cyan") + " Sync labels to GitHub repository");
1400
+ console.log(" " + colorize("batch-sync", "cyan") + " Sync labels to multiple repositories");
1401
+ console.log(" " + colorize("batch-config", "cyan") + " <file> Sync using batch configuration file");
1167
1402
  console.log(" " + colorize("export", "cyan") + " Export labels from GitHub repository");
1168
1403
  console.log(" " + colorize("init", "cyan") + " <template> Initialize new configuration");
1169
1404
  console.log(" " + colorize("help", "cyan") + " Show this help message");
@@ -1175,6 +1410,13 @@ function printUsage() {
1175
1410
  console.log(" " + colorize("--owner", "green") + " <owner> Repository owner (required for sync/export)");
1176
1411
  console.log(" " + colorize("--repo", "green") + " <repo> Repository name (required for sync/export)");
1177
1412
  console.log(" " + colorize("--file", "green") + " <file> Configuration file path");
1413
+ console.log(" " + colorize("--template", "green") + " <name> Template name (for batch-sync)");
1414
+ console.log(" " + colorize("--org", "green") + " <org> Organization name (for batch-sync)");
1415
+ console.log(" " + colorize("--user", "green") + " <user> User name (for batch-sync)");
1416
+ console.log(" " + colorize("--repos", "green") + " <repos> Comma-separated repository list");
1417
+ console.log(" " + colorize("--parallel", "green") + " <num> Number of parallel executions (default: 3)");
1418
+ console.log(" " + colorize("--filter-lang", "green") + " <lang> Filter by programming language");
1419
+ console.log(" " + colorize("--filter-vis", "green") + " <vis> Filter by visibility (public/private/all)");
1178
1420
  console.log(" " + colorize("--dry-run", "green") + " Dry run mode (don't make changes)");
1179
1421
  console.log(" " + colorize("--delete-extra", "green") + " Replace mode: delete labels not in config");
1180
1422
  console.log(" " + colorize("--verbose", "green") + " Verbose output");
@@ -1201,6 +1443,18 @@ function printUsage() {
1201
1443
  console.log(" # Sync with dry run");
1202
1444
  console.log(" " + colorize("labels-config sync --owner user --repo repo --file labels.json --dry-run", "gray"));
1203
1445
  console.log("");
1446
+ console.log(" # Batch sync to all org repositories");
1447
+ console.log(" " + colorize("labels-config batch-sync --org your-org --template prod-ja --dry-run", "gray"));
1448
+ console.log("");
1449
+ console.log(" # Batch sync to specific repositories");
1450
+ console.log(" " + colorize("labels-config batch-sync --repos your-org/repo1,your-org/repo2 --file labels.json", "gray"));
1451
+ console.log("");
1452
+ console.log(" # Batch sync with filters");
1453
+ console.log(" " + colorize("labels-config batch-sync --user your-username --template react --filter-lang TypeScript --filter-vis public", "gray"));
1454
+ console.log("");
1455
+ console.log(" # Batch sync using config file");
1456
+ console.log(" " + colorize("labels-config batch-config batch-config.json --dry-run", "gray"));
1457
+ console.log("");
1204
1458
  }
1205
1459
  async function validateCommand() {
1206
1460
  const file = getPositional(parsedArgs, 0);
@@ -1386,6 +1640,170 @@ async function initCommand() {
1386
1640
  process.exit(1);
1387
1641
  }
1388
1642
  }
1643
+ async function batchSyncCommand() {
1644
+ const spinner = new Spinner();
1645
+ try {
1646
+ const file = getOption(parsedArgs, "--file");
1647
+ const template = getOption(parsedArgs, "--template");
1648
+ const org = getOption(parsedArgs, "--org");
1649
+ const user = getOption(parsedArgs, "--user");
1650
+ const reposOption = getOption(parsedArgs, "--repos");
1651
+ const parallel = parseInt(getOption(parsedArgs, "--parallel") || "3");
1652
+ const filterLang = getOption(parsedArgs, "--filter-lang");
1653
+ const filterVis = getOption(parsedArgs, "--filter-vis");
1654
+ const dryRun = hasFlag(parsedArgs, "--dry-run");
1655
+ const deleteExtra = hasFlag(parsedArgs, "--delete-extra");
1656
+ if (!file && !template) {
1657
+ console.error(error("Error: Either --file or --template is required for batch-sync"));
1658
+ console.log(info("Available templates: ") + listTemplates().map((t) => colorize(t, "magenta")).join(", "));
1659
+ process.exit(1);
1660
+ }
1661
+ if (!org && !user && !reposOption) {
1662
+ console.error(error("Error: One of --org, --user, or --repos is required"));
1663
+ console.log(info("Specify target repositories using:"));
1664
+ console.log(" --org <organization> (sync all org repos)");
1665
+ console.log(" --user <username> (sync all user repos)");
1666
+ console.log(" --repos owner/repo1,owner/repo2 (specific repos)");
1667
+ process.exit(1);
1668
+ }
1669
+ let labels;
1670
+ if (file) {
1671
+ try {
1672
+ await import_fs.promises.access(file);
1673
+ } catch {
1674
+ console.error(error(`File not found: ${file}`));
1675
+ process.exit(1);
1676
+ }
1677
+ spinner.start(`Loading labels from ${file}`);
1678
+ const content = await import_fs.promises.readFile(file, "utf-8");
1679
+ const loader = new ConfigLoader();
1680
+ labels = loader.loadFromString(content);
1681
+ spinner.succeed(`Loaded ${labels.length} labels from file`);
1682
+ } else if (template) {
1683
+ if (!listTemplates().includes(template)) {
1684
+ console.error(error(`Invalid template "${template}"`));
1685
+ console.log(info("Available templates: ") + listTemplates().map((t) => colorize(t, "magenta")).join(", "));
1686
+ process.exit(1);
1687
+ }
1688
+ spinner.start(`Loading "${template}" template`);
1689
+ labels = CONFIG_TEMPLATES[template];
1690
+ spinner.succeed(`Loaded ${labels.length} labels from "${template}" template`);
1691
+ } else {
1692
+ throw new Error("Either --file or --template is required");
1693
+ }
1694
+ const repositories = reposOption ? reposOption.split(",").map((r) => r.trim()) : void 0;
1695
+ const batchSync = new BatchLabelSync();
1696
+ const options = {
1697
+ repositories,
1698
+ organization: org,
1699
+ user,
1700
+ template,
1701
+ mode: deleteExtra ? "replace" : "append",
1702
+ dryRun,
1703
+ parallel,
1704
+ filter: {
1705
+ visibility: filterVis,
1706
+ language: filterLang,
1707
+ archived: false
1708
+ }
1709
+ };
1710
+ const modeText = dryRun ? colorize("[DRY RUN] ", "yellow") : "";
1711
+ console.log(`
1712
+ ${modeText}${header("Batch Sync Configuration")}`);
1713
+ console.log(info(`Labels: ${labels.length}`));
1714
+ if (org) console.log(info(`Organization: ${org}`));
1715
+ if (user) console.log(info(`User: ${user}`));
1716
+ if (repositories) console.log(info(`Repositories: ${repositories.length} specified`));
1717
+ if (filterLang) console.log(info(`Filter (language): ${filterLang}`));
1718
+ if (filterVis) console.log(info(`Filter (visibility): ${filterVis}`));
1719
+ console.log(info(`Parallel: ${parallel}`));
1720
+ console.log(info(`Mode: ${deleteExtra ? "replace" : "append"}`));
1721
+ const results = await batchSync.syncMultiple(labels, options);
1722
+ const summary = batchSync.generateSummary(results);
1723
+ console.log(summary);
1724
+ const hasErrors = results.some((r) => r.status === "failed");
1725
+ if (hasErrors) {
1726
+ process.exit(1);
1727
+ }
1728
+ } catch (err) {
1729
+ spinner.fail("Batch sync failed");
1730
+ console.error(error(err instanceof Error ? err.message : String(err)));
1731
+ process.exit(1);
1732
+ }
1733
+ }
1734
+ async function batchConfigCommand() {
1735
+ const configFile = getPositional(parsedArgs, 0);
1736
+ const dryRun = hasFlag(parsedArgs, "--dry-run");
1737
+ const spinner = new Spinner();
1738
+ if (!configFile) {
1739
+ console.error(error("Configuration file path required"));
1740
+ console.error("Usage: labels-config batch-config <file>");
1741
+ console.log(info("Example: labels-config batch-config batch-config.json --dry-run"));
1742
+ process.exit(1);
1743
+ }
1744
+ try {
1745
+ spinner.start(`Loading batch configuration from ${configFile}`);
1746
+ const config = await BatchConfigLoader.load(configFile);
1747
+ spinner.succeed(`Loaded batch configuration with ${config.targets.length} targets`);
1748
+ const modeText = dryRun ? colorize("[DRY RUN] ", "yellow") : "";
1749
+ console.log(`
1750
+ ${modeText}${header("Batch Configuration")}`);
1751
+ console.log(info(`Version: ${config.version}`));
1752
+ if (config.description) console.log(info(`Description: ${config.description}`));
1753
+ console.log(info(`Targets: ${config.targets.length}`));
1754
+ let totalSuccess = 0;
1755
+ let totalFailed = 0;
1756
+ for (let i = 0; i < config.targets.length; i++) {
1757
+ const target = config.targets[i];
1758
+ console.log(`
1759
+ ${header(`Target ${i + 1}/${config.targets.length}`)}`);
1760
+ let labels;
1761
+ if (target.file) {
1762
+ spinner.start(`Loading labels from ${target.file}`);
1763
+ const content = await import_fs.promises.readFile(target.file, "utf-8");
1764
+ const loader = new ConfigLoader();
1765
+ labels = loader.loadFromString(content);
1766
+ spinner.succeed(`Loaded ${labels.length} labels`);
1767
+ } else if (target.template) {
1768
+ const templateName = target.template || config.defaults?.template;
1769
+ if (!templateName || !listTemplates().includes(templateName)) {
1770
+ console.error(error(`Invalid template "${templateName}"`));
1771
+ continue;
1772
+ }
1773
+ spinner.start(`Loading "${templateName}" template`);
1774
+ labels = CONFIG_TEMPLATES[templateName];
1775
+ spinner.succeed(`Loaded ${labels.length} labels`);
1776
+ } else {
1777
+ console.error(error("No template or file specified"));
1778
+ continue;
1779
+ }
1780
+ const batchSync = new BatchLabelSync();
1781
+ const options = BatchConfigLoader.targetToOptions(target, config.defaults);
1782
+ options.dryRun = dryRun;
1783
+ console.log(info(`Mode: ${options.mode}`));
1784
+ if (target.organization) console.log(info(`Organization: ${target.organization}`));
1785
+ if (target.user) console.log(info(`User: ${target.user}`));
1786
+ if (target.repositories) console.log(info(`Repositories: ${target.repositories.length}`));
1787
+ const results = await batchSync.syncMultiple(labels, options);
1788
+ const successful = results.filter((r) => r.status === "success").length;
1789
+ const failed = results.filter((r) => r.status === "failed").length;
1790
+ totalSuccess += successful;
1791
+ totalFailed += failed;
1792
+ console.log(success(`Target ${i + 1}: ${successful} successful, ${failed} failed`));
1793
+ }
1794
+ console.log(`
1795
+ ${header("Overall Summary")}`);
1796
+ console.log(success(`Total successful: ${totalSuccess}`));
1797
+ if (totalFailed > 0) {
1798
+ console.log(error(`Total failed: ${totalFailed}`));
1799
+ process.exit(1);
1800
+ }
1801
+ } catch (err) {
1802
+ spinner.fail("Batch config execution failed");
1803
+ console.error(error(err instanceof Error ? err.message : String(err)));
1804
+ process.exit(1);
1805
+ }
1806
+ }
1389
1807
  async function main() {
1390
1808
  const command = parsedArgs.command;
1391
1809
  if (!command || command === "help" || hasFlag(parsedArgs, "--help", "-h")) {
@@ -1399,6 +1817,12 @@ async function main() {
1399
1817
  case "sync":
1400
1818
  await syncCommand();
1401
1819
  break;
1820
+ case "batch-sync":
1821
+ await batchSyncCommand();
1822
+ break;
1823
+ case "batch-config":
1824
+ await batchConfigCommand();
1825
+ break;
1402
1826
  case "export":
1403
1827
  await exportCommand();
1404
1828
  break;