@asagiri-design/labels-config 0.2.2 → 0.3.0
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 +62 -0
- package/dist/chunk-MM4GCVPE.mjs +549 -0
- package/dist/cli.js +522 -98
- package/dist/cli.mjs +256 -103
- package/dist/github/index.d.mts +49 -1
- package/dist/github/index.d.ts +49 -1
- package/dist/github/index.js +212 -0
- package/dist/github/index.mjs +3 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/BATCH_SYNC.md +396 -0
- package/docs/NPM_SETUP.md +200 -0
- package/docs/QUICK_START.md +457 -0
- package/docs/RELEASE_FLOW.md +436 -0
- package/package.json +2 -6
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 BoxPistols --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 owner/repo1,owner/repo2 --file labels.json", "gray"));
|
|
1451
|
+
console.log("");
|
|
1452
|
+
console.log(" # Batch sync with filters");
|
|
1453
|
+
console.log(" " + colorize("labels-config batch-sync --user BoxPistols --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;
|