@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.mjs CHANGED
@@ -11,15 +11,75 @@ import {
11
11
  validateWithDetails
12
12
  } from "./chunk-VU2JB66N.mjs";
13
13
  import {
14
- GitHubLabelSync
15
- } from "./chunk-4ZUJQMV7.mjs";
16
- import {
17
- __publicField
18
- } from "./chunk-QZ7TP4HQ.mjs";
14
+ BatchLabelSync,
15
+ GitHubLabelSync,
16
+ Spinner,
17
+ colorize,
18
+ error,
19
+ header,
20
+ info,
21
+ success,
22
+ warning
23
+ } from "./chunk-MM4GCVPE.mjs";
24
+ import "./chunk-QZ7TP4HQ.mjs";
19
25
 
20
26
  // src/cli.ts
21
27
  import { promises as fs } from "fs";
22
28
 
29
+ // src/config/batch-config.ts
30
+ var BatchConfigLoader = class {
31
+ /**
32
+ * バッチ設定ファイルの読み込み
33
+ */
34
+ static async load(filePath) {
35
+ const { promises: fs2 } = await import("fs");
36
+ try {
37
+ const content = await fs2.readFile(filePath, "utf-8");
38
+ const config = JSON.parse(content);
39
+ this.validate(config);
40
+ return config;
41
+ } catch (error2) {
42
+ throw new Error(`Failed to load batch config: ${error2}`);
43
+ }
44
+ }
45
+ /**
46
+ * バッチ設定のバリデーション
47
+ */
48
+ static validate(config) {
49
+ if (!config.version) {
50
+ throw new Error("Batch config version is required");
51
+ }
52
+ if (!config.targets || config.targets.length === 0) {
53
+ throw new Error("At least one target is required");
54
+ }
55
+ config.targets.forEach((target, index) => {
56
+ const hasRepoSpec = target.organization || target.user || target.repositories;
57
+ if (!hasRepoSpec) {
58
+ throw new Error(`Target ${index}: One of organization, user, or repositories is required`);
59
+ }
60
+ const hasLabelSpec = target.template || target.file;
61
+ if (!hasLabelSpec) {
62
+ throw new Error(`Target ${index}: Either template or file is required`);
63
+ }
64
+ });
65
+ }
66
+ /**
67
+ * BatchConfigTargetをBatchSyncOptionsに変換
68
+ */
69
+ static targetToOptions(target, defaults) {
70
+ return {
71
+ organization: target.organization,
72
+ user: target.user,
73
+ repositories: target.repositories,
74
+ template: target.template || defaults?.template,
75
+ mode: target.mode || defaults?.mode || "append",
76
+ parallel: target.parallel || defaults?.parallel || 3,
77
+ filter: target.filter,
78
+ dryRun: false
79
+ };
80
+ }
81
+ };
82
+
23
83
  // src/utils/args.ts
24
84
  function parseArgs(argv) {
25
85
  const result = {
@@ -77,104 +137,6 @@ function getPositional(args, index) {
77
137
  return args.positional[index];
78
138
  }
79
139
 
80
- // src/utils/ui.ts
81
- var colors = {
82
- reset: "\x1B[0m",
83
- bright: "\x1B[1m",
84
- dim: "\x1B[2m",
85
- // Foreground colors
86
- red: "\x1B[31m",
87
- green: "\x1B[32m",
88
- yellow: "\x1B[33m",
89
- blue: "\x1B[34m",
90
- magenta: "\x1B[35m",
91
- cyan: "\x1B[36m",
92
- white: "\x1B[37m",
93
- gray: "\x1B[90m",
94
- // Background colors
95
- bgRed: "\x1B[41m",
96
- bgGreen: "\x1B[42m",
97
- bgYellow: "\x1B[43m",
98
- bgBlue: "\x1B[44m"
99
- };
100
- function supportsColor() {
101
- if (process.env.NO_COLOR) {
102
- return false;
103
- }
104
- if (process.env.FORCE_COLOR) {
105
- return true;
106
- }
107
- if (!process.stdout.isTTY) {
108
- return false;
109
- }
110
- if (process.platform === "win32") {
111
- return true;
112
- }
113
- return true;
114
- }
115
- function colorize(text, color) {
116
- if (!supportsColor()) {
117
- return text;
118
- }
119
- return `${colors[color]}${text}${colors.reset}`;
120
- }
121
- function success(message) {
122
- return colorize("\u2713", "green") + " " + message;
123
- }
124
- function error(message) {
125
- return colorize("\u2717", "red") + " " + message;
126
- }
127
- function warning(message) {
128
- return colorize("\u26A0", "yellow") + " " + message;
129
- }
130
- function info(message) {
131
- return colorize("\u2139", "blue") + " " + message;
132
- }
133
- function header(text) {
134
- return "\n" + colorize(text, "bright") + "\n" + "\u2500".repeat(Math.min(text.length, 50));
135
- }
136
- var Spinner = class {
137
- constructor() {
138
- __publicField(this, "frames", ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]);
139
- __publicField(this, "interval", null);
140
- __publicField(this, "frameIndex", 0);
141
- __publicField(this, "message", "");
142
- }
143
- start(message) {
144
- this.message = message;
145
- if (!process.stdout.isTTY || !supportsColor()) {
146
- console.log(message + "...");
147
- return;
148
- }
149
- this.interval = setInterval(() => {
150
- const frame = this.frames[this.frameIndex];
151
- process.stdout.write(`\r${colorize(frame, "cyan")} ${this.message}`);
152
- this.frameIndex = (this.frameIndex + 1) % this.frames.length;
153
- }, 80);
154
- }
155
- succeed(message) {
156
- this.stop();
157
- console.log(success(message || this.message));
158
- }
159
- fail(message) {
160
- this.stop();
161
- console.log(error(message || this.message));
162
- }
163
- warn(message) {
164
- this.stop();
165
- console.log(warning(message || this.message));
166
- }
167
- stop() {
168
- if (this.interval) {
169
- clearInterval(this.interval);
170
- this.interval = null;
171
- if (process.stdout.isTTY) {
172
- process.stdout.write("\r\x1B[K");
173
- }
174
- }
175
- }
176
- };
177
-
178
140
  // src/cli.ts
179
141
  var rawArgs = process.argv.slice(2);
180
142
  var parsedArgs = parseArgs(rawArgs);
@@ -183,6 +145,8 @@ function printUsage() {
183
145
  console.log(header("Commands"));
184
146
  console.log(" " + colorize("validate", "cyan") + " <file> Validate label configuration file");
185
147
  console.log(" " + colorize("sync", "cyan") + " Sync labels to GitHub repository");
148
+ console.log(" " + colorize("batch-sync", "cyan") + " Sync labels to multiple repositories");
149
+ console.log(" " + colorize("batch-config", "cyan") + " <file> Sync using batch configuration file");
186
150
  console.log(" " + colorize("export", "cyan") + " Export labels from GitHub repository");
187
151
  console.log(" " + colorize("init", "cyan") + " <template> Initialize new configuration");
188
152
  console.log(" " + colorize("help", "cyan") + " Show this help message");
@@ -194,6 +158,13 @@ function printUsage() {
194
158
  console.log(" " + colorize("--owner", "green") + " <owner> Repository owner (required for sync/export)");
195
159
  console.log(" " + colorize("--repo", "green") + " <repo> Repository name (required for sync/export)");
196
160
  console.log(" " + colorize("--file", "green") + " <file> Configuration file path");
161
+ console.log(" " + colorize("--template", "green") + " <name> Template name (for batch-sync)");
162
+ console.log(" " + colorize("--org", "green") + " <org> Organization name (for batch-sync)");
163
+ console.log(" " + colorize("--user", "green") + " <user> User name (for batch-sync)");
164
+ console.log(" " + colorize("--repos", "green") + " <repos> Comma-separated repository list");
165
+ console.log(" " + colorize("--parallel", "green") + " <num> Number of parallel executions (default: 3)");
166
+ console.log(" " + colorize("--filter-lang", "green") + " <lang> Filter by programming language");
167
+ console.log(" " + colorize("--filter-vis", "green") + " <vis> Filter by visibility (public/private/all)");
197
168
  console.log(" " + colorize("--dry-run", "green") + " Dry run mode (don't make changes)");
198
169
  console.log(" " + colorize("--delete-extra", "green") + " Replace mode: delete labels not in config");
199
170
  console.log(" " + colorize("--verbose", "green") + " Verbose output");
@@ -220,6 +191,18 @@ function printUsage() {
220
191
  console.log(" # Sync with dry run");
221
192
  console.log(" " + colorize("labels-config sync --owner user --repo repo --file labels.json --dry-run", "gray"));
222
193
  console.log("");
194
+ console.log(" # Batch sync to all org repositories");
195
+ console.log(" " + colorize("labels-config batch-sync --org your-org --template prod-ja --dry-run", "gray"));
196
+ console.log("");
197
+ console.log(" # Batch sync to specific repositories");
198
+ console.log(" " + colorize("labels-config batch-sync --repos your-org/repo1,your-org/repo2 --file labels.json", "gray"));
199
+ console.log("");
200
+ console.log(" # Batch sync with filters");
201
+ console.log(" " + colorize("labels-config batch-sync --user your-username --template react --filter-lang TypeScript --filter-vis public", "gray"));
202
+ console.log("");
203
+ console.log(" # Batch sync using config file");
204
+ console.log(" " + colorize("labels-config batch-config batch-config.json --dry-run", "gray"));
205
+ console.log("");
223
206
  }
224
207
  async function validateCommand() {
225
208
  const file = getPositional(parsedArgs, 0);
@@ -405,6 +388,170 @@ async function initCommand() {
405
388
  process.exit(1);
406
389
  }
407
390
  }
391
+ async function batchSyncCommand() {
392
+ const spinner = new Spinner();
393
+ try {
394
+ const file = getOption(parsedArgs, "--file");
395
+ const template = getOption(parsedArgs, "--template");
396
+ const org = getOption(parsedArgs, "--org");
397
+ const user = getOption(parsedArgs, "--user");
398
+ const reposOption = getOption(parsedArgs, "--repos");
399
+ const parallel = parseInt(getOption(parsedArgs, "--parallel") || "3");
400
+ const filterLang = getOption(parsedArgs, "--filter-lang");
401
+ const filterVis = getOption(parsedArgs, "--filter-vis");
402
+ const dryRun = hasFlag(parsedArgs, "--dry-run");
403
+ const deleteExtra = hasFlag(parsedArgs, "--delete-extra");
404
+ if (!file && !template) {
405
+ console.error(error("Error: Either --file or --template is required for batch-sync"));
406
+ console.log(info("Available templates: ") + listTemplates().map((t) => colorize(t, "magenta")).join(", "));
407
+ process.exit(1);
408
+ }
409
+ if (!org && !user && !reposOption) {
410
+ console.error(error("Error: One of --org, --user, or --repos is required"));
411
+ console.log(info("Specify target repositories using:"));
412
+ console.log(" --org <organization> (sync all org repos)");
413
+ console.log(" --user <username> (sync all user repos)");
414
+ console.log(" --repos owner/repo1,owner/repo2 (specific repos)");
415
+ process.exit(1);
416
+ }
417
+ let labels;
418
+ if (file) {
419
+ try {
420
+ await fs.access(file);
421
+ } catch {
422
+ console.error(error(`File not found: ${file}`));
423
+ process.exit(1);
424
+ }
425
+ spinner.start(`Loading labels from ${file}`);
426
+ const content = await fs.readFile(file, "utf-8");
427
+ const loader = new ConfigLoader();
428
+ labels = loader.loadFromString(content);
429
+ spinner.succeed(`Loaded ${labels.length} labels from file`);
430
+ } else if (template) {
431
+ if (!listTemplates().includes(template)) {
432
+ console.error(error(`Invalid template "${template}"`));
433
+ console.log(info("Available templates: ") + listTemplates().map((t) => colorize(t, "magenta")).join(", "));
434
+ process.exit(1);
435
+ }
436
+ spinner.start(`Loading "${template}" template`);
437
+ labels = CONFIG_TEMPLATES[template];
438
+ spinner.succeed(`Loaded ${labels.length} labels from "${template}" template`);
439
+ } else {
440
+ throw new Error("Either --file or --template is required");
441
+ }
442
+ const repositories = reposOption ? reposOption.split(",").map((r) => r.trim()) : void 0;
443
+ const batchSync = new BatchLabelSync();
444
+ const options = {
445
+ repositories,
446
+ organization: org,
447
+ user,
448
+ template,
449
+ mode: deleteExtra ? "replace" : "append",
450
+ dryRun,
451
+ parallel,
452
+ filter: {
453
+ visibility: filterVis,
454
+ language: filterLang,
455
+ archived: false
456
+ }
457
+ };
458
+ const modeText = dryRun ? colorize("[DRY RUN] ", "yellow") : "";
459
+ console.log(`
460
+ ${modeText}${header("Batch Sync Configuration")}`);
461
+ console.log(info(`Labels: ${labels.length}`));
462
+ if (org) console.log(info(`Organization: ${org}`));
463
+ if (user) console.log(info(`User: ${user}`));
464
+ if (repositories) console.log(info(`Repositories: ${repositories.length} specified`));
465
+ if (filterLang) console.log(info(`Filter (language): ${filterLang}`));
466
+ if (filterVis) console.log(info(`Filter (visibility): ${filterVis}`));
467
+ console.log(info(`Parallel: ${parallel}`));
468
+ console.log(info(`Mode: ${deleteExtra ? "replace" : "append"}`));
469
+ const results = await batchSync.syncMultiple(labels, options);
470
+ const summary = batchSync.generateSummary(results);
471
+ console.log(summary);
472
+ const hasErrors = results.some((r) => r.status === "failed");
473
+ if (hasErrors) {
474
+ process.exit(1);
475
+ }
476
+ } catch (err) {
477
+ spinner.fail("Batch sync failed");
478
+ console.error(error(err instanceof Error ? err.message : String(err)));
479
+ process.exit(1);
480
+ }
481
+ }
482
+ async function batchConfigCommand() {
483
+ const configFile = getPositional(parsedArgs, 0);
484
+ const dryRun = hasFlag(parsedArgs, "--dry-run");
485
+ const spinner = new Spinner();
486
+ if (!configFile) {
487
+ console.error(error("Configuration file path required"));
488
+ console.error("Usage: labels-config batch-config <file>");
489
+ console.log(info("Example: labels-config batch-config batch-config.json --dry-run"));
490
+ process.exit(1);
491
+ }
492
+ try {
493
+ spinner.start(`Loading batch configuration from ${configFile}`);
494
+ const config = await BatchConfigLoader.load(configFile);
495
+ spinner.succeed(`Loaded batch configuration with ${config.targets.length} targets`);
496
+ const modeText = dryRun ? colorize("[DRY RUN] ", "yellow") : "";
497
+ console.log(`
498
+ ${modeText}${header("Batch Configuration")}`);
499
+ console.log(info(`Version: ${config.version}`));
500
+ if (config.description) console.log(info(`Description: ${config.description}`));
501
+ console.log(info(`Targets: ${config.targets.length}`));
502
+ let totalSuccess = 0;
503
+ let totalFailed = 0;
504
+ for (let i = 0; i < config.targets.length; i++) {
505
+ const target = config.targets[i];
506
+ console.log(`
507
+ ${header(`Target ${i + 1}/${config.targets.length}`)}`);
508
+ let labels;
509
+ if (target.file) {
510
+ spinner.start(`Loading labels from ${target.file}`);
511
+ const content = await fs.readFile(target.file, "utf-8");
512
+ const loader = new ConfigLoader();
513
+ labels = loader.loadFromString(content);
514
+ spinner.succeed(`Loaded ${labels.length} labels`);
515
+ } else if (target.template) {
516
+ const templateName = target.template || config.defaults?.template;
517
+ if (!templateName || !listTemplates().includes(templateName)) {
518
+ console.error(error(`Invalid template "${templateName}"`));
519
+ continue;
520
+ }
521
+ spinner.start(`Loading "${templateName}" template`);
522
+ labels = CONFIG_TEMPLATES[templateName];
523
+ spinner.succeed(`Loaded ${labels.length} labels`);
524
+ } else {
525
+ console.error(error("No template or file specified"));
526
+ continue;
527
+ }
528
+ const batchSync = new BatchLabelSync();
529
+ const options = BatchConfigLoader.targetToOptions(target, config.defaults);
530
+ options.dryRun = dryRun;
531
+ console.log(info(`Mode: ${options.mode}`));
532
+ if (target.organization) console.log(info(`Organization: ${target.organization}`));
533
+ if (target.user) console.log(info(`User: ${target.user}`));
534
+ if (target.repositories) console.log(info(`Repositories: ${target.repositories.length}`));
535
+ const results = await batchSync.syncMultiple(labels, options);
536
+ const successful = results.filter((r) => r.status === "success").length;
537
+ const failed = results.filter((r) => r.status === "failed").length;
538
+ totalSuccess += successful;
539
+ totalFailed += failed;
540
+ console.log(success(`Target ${i + 1}: ${successful} successful, ${failed} failed`));
541
+ }
542
+ console.log(`
543
+ ${header("Overall Summary")}`);
544
+ console.log(success(`Total successful: ${totalSuccess}`));
545
+ if (totalFailed > 0) {
546
+ console.log(error(`Total failed: ${totalFailed}`));
547
+ process.exit(1);
548
+ }
549
+ } catch (err) {
550
+ spinner.fail("Batch config execution failed");
551
+ console.error(error(err instanceof Error ? err.message : String(err)));
552
+ process.exit(1);
553
+ }
554
+ }
408
555
  async function main() {
409
556
  const command = parsedArgs.command;
410
557
  if (!command || command === "help" || hasFlag(parsedArgs, "--help", "-h")) {
@@ -418,6 +565,12 @@ async function main() {
418
565
  case "sync":
419
566
  await syncCommand();
420
567
  break;
568
+ case "batch-sync":
569
+ await batchSyncCommand();
570
+ break;
571
+ case "batch-config":
572
+ await batchConfigCommand();
573
+ break;
421
574
  case "export":
422
575
  await exportCommand();
423
576
  break;
@@ -110,4 +110,52 @@ declare class GitHubLabelSync {
110
110
  updateLabel(name: string, updates: Partial<LabelConfig>): Promise<void>;
111
111
  }
112
112
 
113
- export { GitHubClient, type GitHubClientOptions, type GitHubLabel, GitHubLabelSync, type GitHubSyncOptions };
113
+ interface BatchSyncOptions {
114
+ repositories?: string[];
115
+ organization?: string;
116
+ user?: string;
117
+ template?: string;
118
+ mode?: 'append' | 'replace';
119
+ dryRun?: boolean;
120
+ parallel?: number;
121
+ filter?: {
122
+ visibility?: 'public' | 'private' | 'all';
123
+ language?: string;
124
+ archived?: boolean;
125
+ };
126
+ }
127
+ interface BatchSyncResult {
128
+ repository: string;
129
+ status: 'success' | 'failed' | 'skipped';
130
+ result?: any;
131
+ error?: string;
132
+ }
133
+ declare class BatchLabelSync {
134
+ private static readonly DEFAULT_PARALLEL;
135
+ /**
136
+ * 複数リポジトリへのラベル一括同期
137
+ */
138
+ syncMultiple(labels: LabelConfig[], options: BatchSyncOptions): Promise<BatchSyncResult[]>;
139
+ /**
140
+ * 単一リポジトリへの同期
141
+ */
142
+ private syncSingleRepo;
143
+ /**
144
+ * 対象リポジトリリストの取得
145
+ */
146
+ private getTargetRepositories;
147
+ /**
148
+ * 組織のリポジトリ一覧を取得
149
+ */
150
+ private getOrganizationRepos;
151
+ /**
152
+ * ユーザーのリポジトリ一覧を取得
153
+ */
154
+ private getUserRepos;
155
+ /**
156
+ * 結果サマリーの生成
157
+ */
158
+ generateSummary(results: BatchSyncResult[]): string;
159
+ }
160
+
161
+ export { BatchLabelSync, type BatchSyncOptions, type BatchSyncResult, GitHubClient, type GitHubClientOptions, type GitHubLabel, GitHubLabelSync, type GitHubSyncOptions };
@@ -110,4 +110,52 @@ declare class GitHubLabelSync {
110
110
  updateLabel(name: string, updates: Partial<LabelConfig>): Promise<void>;
111
111
  }
112
112
 
113
- export { GitHubClient, type GitHubClientOptions, type GitHubLabel, GitHubLabelSync, type GitHubSyncOptions };
113
+ interface BatchSyncOptions {
114
+ repositories?: string[];
115
+ organization?: string;
116
+ user?: string;
117
+ template?: string;
118
+ mode?: 'append' | 'replace';
119
+ dryRun?: boolean;
120
+ parallel?: number;
121
+ filter?: {
122
+ visibility?: 'public' | 'private' | 'all';
123
+ language?: string;
124
+ archived?: boolean;
125
+ };
126
+ }
127
+ interface BatchSyncResult {
128
+ repository: string;
129
+ status: 'success' | 'failed' | 'skipped';
130
+ result?: any;
131
+ error?: string;
132
+ }
133
+ declare class BatchLabelSync {
134
+ private static readonly DEFAULT_PARALLEL;
135
+ /**
136
+ * 複数リポジトリへのラベル一括同期
137
+ */
138
+ syncMultiple(labels: LabelConfig[], options: BatchSyncOptions): Promise<BatchSyncResult[]>;
139
+ /**
140
+ * 単一リポジトリへの同期
141
+ */
142
+ private syncSingleRepo;
143
+ /**
144
+ * 対象リポジトリリストの取得
145
+ */
146
+ private getTargetRepositories;
147
+ /**
148
+ * 組織のリポジトリ一覧を取得
149
+ */
150
+ private getOrganizationRepos;
151
+ /**
152
+ * ユーザーのリポジトリ一覧を取得
153
+ */
154
+ private getUserRepos;
155
+ /**
156
+ * 結果サマリーの生成
157
+ */
158
+ generateSummary(results: BatchSyncResult[]): string;
159
+ }
160
+
161
+ export { BatchLabelSync, type BatchSyncOptions, type BatchSyncResult, GitHubClient, type GitHubClientOptions, type GitHubLabel, GitHubLabelSync, type GitHubSyncOptions };