@elsikora/git-branch-lint 1.0.1 → 1.1.1-dev.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.
Files changed (40) hide show
  1. package/README.md +74 -15
  2. package/bin/application/use-cases/check-working-directory-use-case.d.ts +13 -0
  3. package/bin/application/use-cases/check-working-directory.use-case.d.ts +13 -0
  4. package/bin/application/use-cases/create-branch-use-case.d.ts +14 -0
  5. package/bin/application/use-cases/create-branch.use-case.d.ts +14 -0
  6. package/bin/application/use-cases/get-branch-config.use-case.d.ts +15 -0
  7. package/bin/application/use-cases/{get-current-branch-use-case.d.ts → get-current-branch.use-case.d.ts} +2 -3
  8. package/bin/application/use-cases/{lint-branch-name-use-case.d.ts → lint-branch-name.use-case.d.ts} +4 -4
  9. package/bin/application/use-cases/push-branch.use-case.d.ts +13 -0
  10. package/bin/application/use-cases/validate-branch-name-use-case.d.ts +15 -0
  11. package/bin/application/use-cases/validate-branch-name.use-case.d.ts +15 -0
  12. package/bin/domain/{entities/branch.d.ts → entity/branch.entity.d.ts} +2 -3
  13. package/bin/domain/errors/branch-creation.error.d.ts +24 -0
  14. package/bin/domain/interface/branch.repository.interface.d.ts +23 -0
  15. package/bin/domain/{interfaces/repositories/iconfig-repository.d.ts → interface/config.repository.interface.d.ts} +2 -2
  16. package/bin/domain/interface/repository.interface.d.ts +2 -0
  17. package/bin/domain/type/branch.type.d.ts +5 -0
  18. package/bin/domain/type/config.type.d.ts +7 -0
  19. package/bin/domain/type/rules.type.d.ts +7 -0
  20. package/bin/index.d.ts +1 -1
  21. package/bin/index.js +455 -103
  22. package/bin/index.js.map +1 -1
  23. package/bin/infrastructure/config/cosmiconfig.repository.d.ts +17 -0
  24. package/bin/infrastructure/git/git-branch.repository.d.ts +26 -0
  25. package/bin/presentation/cli/controllers/create-branch.controller.d.ts +25 -0
  26. package/bin/presentation/cli/{cli-controller.d.ts → controllers/lint.controller.d.ts} +5 -5
  27. package/bin/presentation/cli/formatters/branch-choice.formatter.d.ts +27 -0
  28. package/bin/presentation/cli/formatters/{hint-formatter.d.ts → hint.formatter.d.ts} +2 -2
  29. package/bin/presentation/cli/prompts/branch-creation.prompt.d.ts +25 -0
  30. package/package.json +34 -16
  31. package/bin/application/use-cases/get-branch-config-use-case.d.ts +0 -19
  32. package/bin/domain/interfaces/branch-interfaces.d.ts +0 -22
  33. package/bin/domain/interfaces/repositories/ibranch-repository.d.ts +0 -10
  34. package/bin/domain/interfaces/repository-interfaces.d.ts +0 -2
  35. package/bin/domain/repositories/branch-repository.d.ts +0 -11
  36. package/bin/domain/repositories/config-repository.d.ts +0 -12
  37. package/bin/infrastructure/config/cosmiconfig-repository.d.ts +0 -13
  38. package/bin/infrastructure/git/git-branch-repository.d.ts +0 -12
  39. /package/bin/domain/errors/{lint-errors.d.ts → lint.error.d.ts} +0 -0
  40. /package/bin/presentation/cli/formatters/{error-formatter.d.ts → error.formatter.d.ts} +0 -0
package/bin/index.js CHANGED
@@ -1,28 +1,111 @@
1
1
  #!/usr/bin/env node
2
+ import yargs from 'yargs';
2
3
  import { cosmiconfig } from 'cosmiconfig';
3
4
  import { exec } from 'node:child_process';
4
5
  import { promisify } from 'node:util';
6
+ import inquirer from 'inquirer';
5
7
  import chalk from 'chalk';
6
8
 
7
9
  /**
8
- * Use case for getting branch configuration
10
+ * Base error class for branch creation errors
9
11
  */
10
- class GetBranchConfigUseCase {
11
- CONFIG_REPOSITORY;
12
+ class BranchCreationError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "BranchCreationError";
16
+ }
17
+ }
18
+ /**
19
+ * Error thrown when trying to create a branch that already exists
20
+ */
21
+ class BranchAlreadyExistsError extends BranchCreationError {
22
+ constructor(branchName) {
23
+ super(`You are already on branch ${branchName}!`);
24
+ this.name = "BranchAlreadyExistsError";
25
+ }
26
+ }
27
+ /**
28
+ * Error thrown when git operation fails
29
+ */
30
+ class GitOperationError extends BranchCreationError {
31
+ constructor(operation, details) {
32
+ const message = details ? `Git operation failed: ${operation} - ${details}` : `Git operation failed: ${operation}`;
33
+ super(message);
34
+ this.name = "GitOperationError";
35
+ }
36
+ }
37
+ /**
38
+ * Error thrown when working directory has uncommitted changes
39
+ */
40
+ class UncommittedChangesError extends BranchCreationError {
41
+ constructor() {
42
+ super("You have uncommitted changes. Please commit or stash them before creating a new branch.");
43
+ this.name = "UncommittedChangesError";
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Use case for checking working directory status
49
+ */
50
+ class CheckWorkingDirectoryUseCase {
51
+ branchRepository;
52
+ constructor(branchRepository) {
53
+ this.branchRepository = branchRepository;
54
+ }
12
55
  /**
13
- * Constructor
14
- * @param configRepository The configuration repository
56
+ * Execute the use case
57
+ * @throws {UncommittedChangesError} When there are uncommitted changes
15
58
  */
59
+ async execute() {
60
+ const hasChanges = await this.branchRepository.hasUncommittedChanges();
61
+ if (hasChanges) {
62
+ throw new UncommittedChangesError();
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Use case for creating a new branch
69
+ */
70
+ class CreateBranchUseCase {
71
+ branchRepository;
72
+ constructor(branchRepository) {
73
+ this.branchRepository = branchRepository;
74
+ }
75
+ /**
76
+ * Execute the use case
77
+ * @param branchName The name of the branch to create
78
+ * @throws {BranchAlreadyExistsError} When trying to create current branch
79
+ */
80
+ async execute(branchName) {
81
+ const currentBranch = await this.branchRepository.getCurrentBranchName();
82
+ if (currentBranch === branchName) {
83
+ throw new BranchAlreadyExistsError(branchName);
84
+ }
85
+ await this.branchRepository.createBranch(branchName);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Use case for retrieving branch configuration
91
+ */
92
+ class GetBranchConfigUseCase {
93
+ configRepository;
16
94
  constructor(configRepository) {
17
- this.CONFIG_REPOSITORY = configRepository;
95
+ this.configRepository = configRepository;
18
96
  }
19
97
  /**
20
98
  * Execute the use case
21
- * @param appName The application name
99
+ * @param appName - The application name
22
100
  * @returns The branch configuration
23
101
  */
24
102
  async execute(appName) {
25
- return this.CONFIG_REPOSITORY.getConfig(appName);
103
+ try {
104
+ return await this.configRepository.getConfig(appName);
105
+ }
106
+ catch (error) {
107
+ throw new Error(`Failed to get branch config: ${error.message}`);
108
+ }
26
109
  }
27
110
  }
28
111
 
@@ -89,7 +172,6 @@ class LintError extends Error {
89
172
  */
90
173
  class BranchTooLongError extends LintError {
91
174
  constructor(branchName, maxLength) {
92
- // eslint-disable-next-line @elsikora-typescript/restrict-template-expressions
93
175
  super(`Branch name "${branchName}" is too long (maximum length: ${maxLength})`);
94
176
  this.name = "BranchTooLongError";
95
177
  }
@@ -99,7 +181,6 @@ class BranchTooLongError extends LintError {
99
181
  */
100
182
  class BranchTooShortError extends LintError {
101
183
  constructor(branchName, minLength) {
102
- // eslint-disable-next-line @elsikora-typescript/restrict-template-expressions
103
184
  super(`Branch name "${branchName}" is too short (minimum length: ${minLength})`);
104
185
  this.name = "BranchTooShortError";
105
186
  }
@@ -124,11 +205,11 @@ class ProhibitedBranchError extends LintError {
124
205
  }
125
206
 
126
207
  /**
127
- * Use case for linting a branch name
208
+ * Use case for linting branch names
128
209
  */
129
210
  class LintBranchNameUseCase {
130
211
  /**
131
- * Lint a branch name against a configuration
212
+ * Execute the use case
132
213
  * @param branchName The branch name to lint
133
214
  * @param config The branch configuration
134
215
  * @throws {ProhibitedBranchError} When branch name is prohibited
@@ -138,18 +219,19 @@ class LintBranchNameUseCase {
138
219
  */
139
220
  execute(branchName, config) {
140
221
  const branch = new Branch(branchName);
141
- if (branch.isProhibited(config.PROHIBITED)) {
222
+ const configRules = config.rules ?? {};
223
+ const ignoreList = config.ignore ?? [];
224
+ if (configRules?.["branch-prohibited"] && branch.isProhibited(configRules["branch-prohibited"])) {
142
225
  throw new ProhibitedBranchError(branchName);
143
226
  }
144
- // @ts-ignore
145
- if (branch.isTooShort(config.MINLENGTH)) {
146
- // @ts-ignore
147
- throw new BranchTooShortError(branchName, config.MINLENGTH);
227
+ if (ignoreList.length > 0 && ignoreList.includes(branchName)) {
228
+ return;
229
+ }
230
+ if (configRules?.["branch-min-length"] && branch.isTooShort(configRules["branch-min-length"])) {
231
+ throw new BranchTooShortError(branchName, configRules["branch-min-length"]);
148
232
  }
149
- // @ts-ignore
150
- if (branch.isTooLong(config.MAXLENGTH)) {
151
- // @ts-ignore
152
- throw new BranchTooLongError(branchName, config.MAXLENGTH);
233
+ if (configRules?.["branch-max-length"] && branch.isTooLong(configRules["branch-max-length"])) {
234
+ throw new BranchTooLongError(branchName, configRules["branch-max-length"]);
153
235
  }
154
236
  this.validatePattern(branch.getName(), config);
155
237
  }
@@ -161,16 +243,24 @@ class LintBranchNameUseCase {
161
243
  */
162
244
  validatePattern(branchName, config) {
163
245
  // Start with original pattern
164
- let pattern = config.PATTERN;
165
- // Process each parameter in the configuration
166
- for (const [key, values] of Object.entries(config.PARAMS)) {
167
- // Create the placeholder - IMPORTANT: Convert to lowercase to match pattern
246
+ let branchNamePattern = config.rules?.["branch-pattern"];
247
+ const subjectNamePattern = config.rules?.["branch-subject-pattern"];
248
+ if (!branchNamePattern) {
249
+ return;
250
+ }
251
+ // Get branch types - handle both array and object formats
252
+ const branchTypes = Array.isArray(config.branches) ? config.branches : Object.keys(config.branches);
253
+ const parameters = {
254
+ type: branchTypes,
255
+ // Если branch-name-pattern не определён, не добавляем name в params
256
+ ...(subjectNamePattern && { name: [subjectNamePattern] }),
257
+ };
258
+ // Обрабатываем параметры, если они есть
259
+ for (const [key, values] of Object.entries(parameters)) {
168
260
  const placeholder = `:${key.toLowerCase()}`;
169
- const parameterValues = values;
170
261
  let replacement = "(";
171
- for (let index = 0; index < parameterValues.length; index++) {
172
- const value = parameterValues[index];
173
- // Add the value to the replacement pattern
262
+ for (let index = 0; index < values.length; index++) {
263
+ const value = values[index];
174
264
  if (value.startsWith("[")) {
175
265
  replacement += value;
176
266
  }
@@ -178,17 +268,15 @@ class LintBranchNameUseCase {
178
268
  const escapedValue = value.replaceAll(/[-/\\^$*+?.()|[\]{}]/g, String.raw `\$&`);
179
269
  replacement += escapedValue;
180
270
  }
181
- // Add OR separator if not the last value
182
- if (index < parameterValues.length - 1) {
271
+ if (index < values.length - 1) {
183
272
  replacement += "|";
184
273
  }
185
274
  }
186
275
  replacement += ")";
187
- // Replace the placeholder in the pattern
188
- pattern = pattern.replaceAll(new RegExp(placeholder, "g"), replacement);
276
+ branchNamePattern = branchNamePattern.replaceAll(new RegExp(placeholder, "g"), replacement);
189
277
  }
190
278
  // Create the regular expression
191
- const regexp = new RegExp(`^${pattern}$`);
279
+ const regexp = new RegExp(`^${branchNamePattern}$`);
192
280
  // Test the branch name against the pattern
193
281
  if (!regexp.test(branchName)) {
194
282
  throw new PatternMatchError(branchName);
@@ -196,20 +284,42 @@ class LintBranchNameUseCase {
196
284
  }
197
285
  }
198
286
 
287
+ /**
288
+ * Use case for pushing a branch to remote repository
289
+ */
290
+ class PushBranchUseCase {
291
+ branchRepository;
292
+ constructor(branchRepository) {
293
+ this.branchRepository = branchRepository;
294
+ }
295
+ /**
296
+ * Execute the use case
297
+ * @param branchName The name of the branch to push
298
+ */
299
+ async execute(branchName) {
300
+ await this.branchRepository.pushBranch(branchName);
301
+ }
302
+ }
303
+
199
304
  /**
200
305
  * Default configuration for branch linting
201
306
  */
202
307
  const DEFAULT_CONFIG = {
203
- // eslint-disable-next-line @elsikora-typescript/no-magic-numbers
204
- MAXLENGTH: 50,
205
- // eslint-disable-next-line @elsikora-typescript/no-magic-numbers
206
- MINLENGTH: 5,
207
- PARAMS: {
208
- NAME: ["[a-z0-9-]+"],
209
- TYPE: ["feature", "hotfix", "support", "bugfix", "release"],
308
+ branches: {
309
+ bugfix: { description: "🐞 Fixing issues in existing functionality", title: "Bugfix" },
310
+ feature: { description: "🆕 Integration of new functionality", title: "Feature" },
311
+ hotfix: { description: "🚑 Critical fixes for urgent issues", title: "Hotfix" },
312
+ release: { description: "📦 Preparing a new release version", title: "Release" },
313
+ support: { description: "🛠️ Support and maintenance tasks", title: "Support" },
314
+ },
315
+ ignore: ["dev"],
316
+ rules: {
317
+ "branch-max-length": 50,
318
+ "branch-min-length": 5,
319
+ "branch-pattern": ":type/:name",
320
+ "branch-prohibited": ["main", "master", "release"],
321
+ "branch-subject-pattern": "[a-z0-9-]+",
210
322
  },
211
- PATTERN: ":type/:name",
212
- PROHIBITED: ["ci", "wip", "main", "test", "build", "master", "release", "dev", "develop"],
213
323
  };
214
324
  /**
215
325
  * Cosmiconfig implementation of ConfigRepository
@@ -231,47 +341,10 @@ class CosmiconfigRepository {
231
341
  }
232
342
  // Convert the config to match our interfaces
233
343
  const providedConfig = result.config;
234
- const uppercasedConfig = {};
235
- // Create a new object with uppercase keys instead of modifying the original
236
- for (const key of Object.keys(providedConfig)) {
237
- const uppercaseKey = key.toUpperCase();
238
- const value = providedConfig[key];
239
- if (Array.isArray(value)) {
240
- // Preserve arrays
241
- // eslint-disable-next-line @elsikora-typescript/no-unsafe-assignment
242
- uppercasedConfig[uppercaseKey] = [...value];
243
- }
244
- else if (typeof value === "object" && value !== null) {
245
- // Handle nested objects
246
- const nestedObject = {};
247
- for (const subKey of Object.keys(value)) {
248
- const subValue = value[subKey];
249
- if (Array.isArray(subValue)) {
250
- // Preserve nested arrays
251
- // eslint-disable-next-line @elsikora-typescript/no-unsafe-assignment
252
- nestedObject[subKey.toUpperCase()] = [...subValue];
253
- }
254
- else {
255
- nestedObject[subKey.toUpperCase()] = subValue;
256
- }
257
- }
258
- uppercasedConfig[uppercaseKey] = nestedObject;
259
- }
260
- else {
261
- // Handle primitive values
262
- uppercasedConfig[uppercaseKey] = value;
263
- }
264
- }
265
344
  const mergedConfig = {
266
345
  ...DEFAULT_CONFIG,
267
- ...uppercasedConfig,
346
+ ...providedConfig,
268
347
  };
269
- if (uppercasedConfig.PARAMS && DEFAULT_CONFIG.PARAMS) {
270
- mergedConfig.PARAMS = {
271
- ...DEFAULT_CONFIG.PARAMS,
272
- ...uppercasedConfig.PARAMS,
273
- };
274
- }
275
348
  return mergedConfig;
276
349
  }
277
350
  }
@@ -281,14 +354,272 @@ const execAsync = promisify(exec);
281
354
  * Git implementation of BranchRepository
282
355
  */
283
356
  class GitBranchRepository {
357
+ /**
358
+ * Create a new branch
359
+ * @param branchName The name of the branch to create
360
+ */
361
+ async createBranch(branchName) {
362
+ try {
363
+ const command = `git checkout -b ${branchName}`;
364
+ await execAsync(command);
365
+ }
366
+ catch (error) {
367
+ throw new GitOperationError("create branch", error.message);
368
+ }
369
+ }
284
370
  /**
285
371
  * Get the current branch name
286
372
  * @returns A promise that resolves to the current branch name
287
373
  */
288
374
  async getCurrentBranchName() {
289
- const command = "git rev-parse --abbrev-ref HEAD";
290
- const { stdout } = await execAsync(command);
291
- return stdout.trim();
375
+ try {
376
+ const command = "git rev-parse --abbrev-ref HEAD";
377
+ const { stdout } = await execAsync(command);
378
+ return stdout.trim();
379
+ }
380
+ catch (error) {
381
+ throw new GitOperationError("get current branch", error.message);
382
+ }
383
+ }
384
+ /**
385
+ * Check if working directory has uncommitted changes
386
+ * @returns A promise that resolves to true if there are uncommitted changes
387
+ */
388
+ async hasUncommittedChanges() {
389
+ try {
390
+ const command = "git status --porcelain";
391
+ const { stdout } = await execAsync(command);
392
+ return stdout.trim().length > 0;
393
+ }
394
+ catch (error) {
395
+ throw new GitOperationError("check working directory status", error.message);
396
+ }
397
+ }
398
+ /**
399
+ * Push branch to remote repository
400
+ * @param branchName The name of the branch to push
401
+ */
402
+ async pushBranch(branchName) {
403
+ try {
404
+ const command = `git push --set-upstream origin ${branchName}`;
405
+ await execAsync(command);
406
+ }
407
+ catch (error) {
408
+ throw new GitOperationError("push branch", error.message);
409
+ }
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Use case for validating branch name format
415
+ */
416
+ class ValidateBranchNameUseCase {
417
+ BRANCH_NAME_PATTERN = /^[a-z0-9-]+$/;
418
+ /**
419
+ * Execute the use case
420
+ * @param branchName The branch name to validate
421
+ * @returns Validation result with error message if invalid
422
+ */
423
+ execute(branchName) {
424
+ if (!branchName.trim()) {
425
+ return {
426
+ errorMessage: "Branch name cannot be empty!",
427
+ isValid: false,
428
+ };
429
+ }
430
+ if (!this.BRANCH_NAME_PATTERN.test(branchName)) {
431
+ return {
432
+ errorMessage: "Branch name can only contain lowercase letters, numbers, and hyphens!",
433
+ isValid: false,
434
+ };
435
+ }
436
+ return { isValid: true };
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Formatter for branch choices in CLI prompts
442
+ */
443
+ class BranchChoiceFormatter {
444
+ // eslint-disable-next-line @elsikora/typescript/no-magic-numbers
445
+ EMPTY_SPACING_OFFSET = 5;
446
+ /**
447
+ * Format branch list for CLI selection
448
+ * @param branchList The list of branches to format
449
+ * @returns Formatted choices for inquirer prompt
450
+ */
451
+ format(branchList) {
452
+ if (Array.isArray(branchList)) {
453
+ return this.formatSimpleList(branchList);
454
+ }
455
+ return this.formatDetailedList(branchList);
456
+ }
457
+ /**
458
+ * Format detailed branch list (object with descriptions)
459
+ */
460
+ formatDetailedList(branchList) {
461
+ const branchNames = Object.keys(branchList);
462
+ const maxNameLength = Math.max(...branchNames.map((name) => name.length));
463
+ return branchNames.map((branchName) => {
464
+ const branch = branchList[branchName];
465
+ const padding = " ".repeat(maxNameLength - branchName.length + this.EMPTY_SPACING_OFFSET);
466
+ return {
467
+ name: `${branch.title}:${padding}${branch.description}`,
468
+ short: branch.title,
469
+ value: branchName,
470
+ };
471
+ });
472
+ }
473
+ /**
474
+ * Format simple branch list (array of strings)
475
+ */
476
+ formatSimpleList(branchList) {
477
+ return branchList.map((branchName) => ({
478
+ name: branchName,
479
+ short: branchName,
480
+ value: branchName,
481
+ }));
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Prompt service for branch creation interactions
487
+ */
488
+ class BranchCreationPrompt {
489
+ BRANCH_CHOICE_FORMATTER;
490
+ VALIDATE_BRANCH_NAME_USE_CASE;
491
+ constructor() {
492
+ this.BRANCH_CHOICE_FORMATTER = new BranchChoiceFormatter();
493
+ this.VALIDATE_BRANCH_NAME_USE_CASE = new ValidateBranchNameUseCase();
494
+ }
495
+ /**
496
+ * Prompt for branch name
497
+ * @returns Branch name
498
+ */
499
+ async promptBranchName() {
500
+ const result = await inquirer.prompt([
501
+ {
502
+ message: "Enter the branch name (e.g., authorization):",
503
+ name: "branchName",
504
+ type: "input",
505
+ // eslint-disable-next-line @elsikora/sonar/function-return-type
506
+ validate: (input) => {
507
+ const validation = this.VALIDATE_BRANCH_NAME_USE_CASE.execute(input);
508
+ if (validation.isValid) {
509
+ return true;
510
+ }
511
+ return validation.errorMessage ?? "Invalid branch name";
512
+ },
513
+ },
514
+ ]);
515
+ return result.branchName;
516
+ }
517
+ /**
518
+ * Prompt for branch type selection
519
+ * @param branches Available branch types
520
+ * @returns Selected branch type
521
+ */
522
+ async promptBranchType(branches) {
523
+ const choices = this.BRANCH_CHOICE_FORMATTER.format(branches);
524
+ const result = await inquirer.prompt([
525
+ {
526
+ choices,
527
+ message: "Select the type of branch you're creating:",
528
+ name: "branchType",
529
+ type: "list",
530
+ },
531
+ ]);
532
+ return result.branchType;
533
+ }
534
+ /**
535
+ * Prompt to push branch to remote
536
+ * @returns Whether to push the branch
537
+ */
538
+ async promptPushBranch() {
539
+ const result = await inquirer.prompt([
540
+ {
541
+ // eslint-disable-next-line @elsikora/typescript/naming-convention
542
+ default: false,
543
+ message: "Do you want to push the branch to the remote repository?",
544
+ name: "shouldPush",
545
+ type: "confirm",
546
+ },
547
+ ]);
548
+ return result.shouldPush;
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Controller for branch creation CLI operations
554
+ */
555
+ class CreateBranchController {
556
+ BRANCH_CREATION_PROMPT;
557
+ CHECK_WORKING_DIRECTORY_USE_CASE;
558
+ CREATE_BRANCH_USE_CASE;
559
+ GET_BRANCH_CONFIG_USE_CASE;
560
+ PUSH_BRANCH_USE_CASE;
561
+ constructor(checkWorkingDirectoryUseCase, createBranchUseCase, getBranchConfigUseCase, pushBranchUseCase) {
562
+ this.CHECK_WORKING_DIRECTORY_USE_CASE = checkWorkingDirectoryUseCase;
563
+ this.CREATE_BRANCH_USE_CASE = createBranchUseCase;
564
+ this.GET_BRANCH_CONFIG_USE_CASE = getBranchConfigUseCase;
565
+ this.PUSH_BRANCH_USE_CASE = pushBranchUseCase;
566
+ this.BRANCH_CREATION_PROMPT = new BranchCreationPrompt();
567
+ }
568
+ /**
569
+ * Execute the branch creation flow
570
+ * @param appName The application name for configuration
571
+ */
572
+ async execute(appName) {
573
+ try {
574
+ // Check working directory
575
+ await this.CHECK_WORKING_DIRECTORY_USE_CASE.execute();
576
+ console.error("🌿 Creating a new branch...\n");
577
+ // Get configuration
578
+ const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute(appName);
579
+ // Prompt for branch details
580
+ const branchType = await this.BRANCH_CREATION_PROMPT.promptBranchType(config.branches);
581
+ const branchName = await this.BRANCH_CREATION_PROMPT.promptBranchName();
582
+ const fullBranchName = `${branchType}/${branchName}`;
583
+ console.error(`\n⌛️ Creating branch: ${fullBranchName}`);
584
+ // Create the branch
585
+ await this.CREATE_BRANCH_USE_CASE.execute(fullBranchName);
586
+ // Ask about pushing to remote
587
+ const shouldPush = await this.BRANCH_CREATION_PROMPT.promptPushBranch();
588
+ if (shouldPush) {
589
+ await this.PUSH_BRANCH_USE_CASE.execute(fullBranchName);
590
+ console.error(`✅ Branch ${fullBranchName} pushed to remote repository!`);
591
+ }
592
+ else {
593
+ console.error(`✅ Branch ${fullBranchName} created locally!`);
594
+ }
595
+ }
596
+ catch (error) {
597
+ this.handleError(error);
598
+ }
599
+ }
600
+ /**
601
+ * Handle errors that occur during branch creation
602
+ * @param error The error that occurred
603
+ */
604
+ handleError(error) {
605
+ if (error instanceof UncommittedChangesError) {
606
+ console.error(`⚠️ ${error.message}`);
607
+ // eslint-disable-next-line @elsikora/unicorn/no-process-exit
608
+ process.exit(1);
609
+ }
610
+ if (error instanceof BranchAlreadyExistsError) {
611
+ console.error(`⚠️ ${error.message}`);
612
+ // eslint-disable-next-line @elsikora/unicorn/no-process-exit
613
+ process.exit(0);
614
+ }
615
+ if (error instanceof BranchCreationError) {
616
+ console.error(`❌ ${error.message}`);
617
+ // eslint-disable-next-line @elsikora/unicorn/no-process-exit
618
+ process.exit(1);
619
+ }
620
+ console.error(`❌ Failed to create branch: ${error.message}`);
621
+ // eslint-disable-next-line @elsikora/unicorn/no-process-exit
622
+ process.exit(1);
292
623
  }
293
624
  }
294
625
 
@@ -333,12 +664,11 @@ class HintFormatter {
333
664
  */
334
665
  formatPatternMatchHint(config) {
335
666
  let output = "";
336
- output += `${chalk.blue("Expected pattern:")} ${chalk.yellow(config.PATTERN)}\n`;
337
- // Format the parameters
338
- for (const [parameterName, parameterValues] of Object.entries(config.PARAMS)) {
339
- const valuesList = parameterValues.map((value) => chalk.yellow(value)).join(", ");
340
- output += chalk.blue(`Valid ${parameterName} values:`) + " " + valuesList + "\n";
341
- }
667
+ const branchNamePattern = config.rules?.["branch-pattern"];
668
+ const branchTypeList = Array.isArray(config.branches) ? config.branches : Object.keys(config.branches);
669
+ output += branchNamePattern ? `${chalk.blue("Expected pattern:")} ${chalk.yellow(branchNamePattern)}\n` : "";
670
+ const valuesList = branchTypeList.map((value) => chalk.yellow(value)).join(", ");
671
+ output += chalk.blue(`Valid branch types:`) + " " + valuesList + "\n";
342
672
  return output.trim();
343
673
  }
344
674
  /**
@@ -347,15 +677,15 @@ class HintFormatter {
347
677
  * @returns The formatted hint
348
678
  */
349
679
  formatProhibitedBranchHint(config) {
350
- const prohibitedList = config.PROHIBITED.map((name) => chalk.yellow(name)).join(", ");
680
+ const prohibitedList = config.rules?.["branch-prohibited"]?.map((name) => chalk.yellow(name)).join(", ") ?? "";
351
681
  return `${chalk.blue("Prohibited branch names:")} ${prohibitedList}`;
352
682
  }
353
683
  }
354
684
 
355
685
  /**
356
- * Controller for CLI operations
686
+ * Controller for linting CLI operations
357
687
  */
358
- class CliController {
688
+ class LintController {
359
689
  ERROR_FORMATTER;
360
690
  GET_BRANCH_CONFIG_USE_CASE;
361
691
  GET_CURRENT_BRANCH_USE_CASE;
@@ -401,16 +731,16 @@ class CliController {
401
731
  // Get the configuration using the service instead of hardcoded values
402
732
  const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute("git-branch-lint");
403
733
  console.error(this.ERROR_FORMATTER.format(error.message));
404
- console.log(this.HINT_FORMATTER.format(error, config));
734
+ console.error(this.HINT_FORMATTER.format(error, config));
405
735
  // Since this is a CLI tool, it's appropriate to exit the process on validation errors
406
736
  // ESLint wants us to throw instead, but this is a CLI application where exit is acceptable
407
- // eslint-disable-next-line elsikora-node/no-process-exit, @elsikora-unicorn/no-process-exit
737
+ // eslint-disable-next-line @elsikora/unicorn/no-process-exit
408
738
  process.exit(1);
409
739
  }
410
740
  catch {
411
741
  console.error(this.ERROR_FORMATTER.format("Failed to load configuration for error hint"));
412
742
  console.error(this.ERROR_FORMATTER.format(error.message));
413
- // eslint-disable-next-line elsikora-node/no-process-exit, @elsikora-unicorn/no-process-exit
743
+ // eslint-disable-next-line @elsikora/unicorn/no-process-exit
414
744
  process.exit(1);
415
745
  }
416
746
  }
@@ -422,6 +752,16 @@ class CliController {
422
752
  * Application name used for configuration
423
753
  */
424
754
  const APP_NAME = "git-branch-lint";
755
+ const ARGS_SLICE_INDEX = 2;
756
+ const argv = yargs(process.argv.slice(ARGS_SLICE_INDEX))
757
+ .option("branch", {
758
+ alias: "b",
759
+ description: "Run branch creation tool",
760
+ type: "boolean",
761
+ })
762
+ .help()
763
+ .usage("Usage: $0 [-b] to create a branch or lint the current branch")
764
+ .parseSync();
425
765
  /**
426
766
  * Main function that bootstraps the application
427
767
  */
@@ -429,14 +769,26 @@ const main = async () => {
429
769
  // Infrastructure layer
430
770
  const configRepository = new CosmiconfigRepository();
431
771
  const branchRepository = new GitBranchRepository();
432
- // Application layer
772
+ // Application layer - common use cases
433
773
  const getBranchConfigUseCase = new GetBranchConfigUseCase(configRepository);
434
- const getCurrentBranchUseCase = new GetCurrentBranchUseCase(branchRepository);
435
- const lintBranchNameUseCase = new LintBranchNameUseCase();
436
- // Presentation layer
437
- const cliController = new CliController(getBranchConfigUseCase, getCurrentBranchUseCase, lintBranchNameUseCase);
438
- // Execute the application
439
- await cliController.execute(APP_NAME);
774
+ const shouldRunBranch = Boolean(argv.branch);
775
+ if (shouldRunBranch) {
776
+ // Application layer - branch creation use cases
777
+ const checkWorkingDirectoryUseCase = new CheckWorkingDirectoryUseCase(branchRepository);
778
+ const createBranchUseCase = new CreateBranchUseCase(branchRepository);
779
+ const pushBranchUseCase = new PushBranchUseCase(branchRepository);
780
+ // Presentation layer
781
+ const createBranchController = new CreateBranchController(checkWorkingDirectoryUseCase, createBranchUseCase, getBranchConfigUseCase, pushBranchUseCase);
782
+ await createBranchController.execute(APP_NAME);
783
+ }
784
+ else {
785
+ // Application layer - linting use cases
786
+ const getCurrentBranchUseCase = new GetCurrentBranchUseCase(branchRepository);
787
+ const lintBranchNameUseCase = new LintBranchNameUseCase();
788
+ // Presentation layer
789
+ const lintController = new LintController(getBranchConfigUseCase, getCurrentBranchUseCase, lintBranchNameUseCase);
790
+ await lintController.execute(APP_NAME);
791
+ }
440
792
  };
441
793
  // Bootstrap the application and handle errors
442
794
  main().catch((error) => {