@elsikora/git-branch-lint 1.1.0 → 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 (44) 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} +1 -1
  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/errors/branch-creation.error.d.ts +24 -0
  13. package/bin/domain/interface/branch.repository.interface.d.ts +23 -0
  14. package/bin/domain/{interfaces/repositories/iconfig-repository.d.ts → interface/config.repository.interface.d.ts} +2 -2
  15. package/bin/domain/interface/repository.interface.d.ts +2 -0
  16. package/bin/domain/type/branch.type.d.ts +5 -0
  17. package/bin/domain/type/config.type.d.ts +7 -0
  18. package/bin/domain/{interfaces → type}/rules.type.d.ts +1 -1
  19. package/bin/index.d.ts +1 -1
  20. package/bin/index.js +432 -188
  21. package/bin/index.js.map +1 -1
  22. package/bin/infrastructure/config/{cosmiconfig-repository.d.ts → cosmiconfig.repository.d.ts} +4 -4
  23. package/bin/infrastructure/git/git-branch.repository.d.ts +26 -0
  24. package/bin/presentation/cli/controllers/create-branch.controller.d.ts +25 -0
  25. package/bin/presentation/cli/{cli-controller.d.ts → controllers/lint.controller.d.ts} +5 -5
  26. package/bin/presentation/cli/formatters/branch-choice.formatter.d.ts +27 -0
  27. package/bin/presentation/cli/formatters/{hint-formatter.d.ts → hint.formatter.d.ts} +2 -2
  28. package/bin/presentation/cli/prompts/branch-creation.prompt.d.ts +25 -0
  29. package/package.json +25 -18
  30. package/bin/application/create-branch-tool/createBranch.d.ts +0 -1
  31. package/bin/application/create-branch-tool/utils/alignChoices.d.ts +0 -8
  32. package/bin/application/create-branch-tool/utils/branchNameQuiz.d.ts +0 -2
  33. package/bin/application/create-branch-tool/utils/runGitCommand.d.ts +0 -1
  34. package/bin/application/use-cases/get-branch-config-use-case.d.ts +0 -19
  35. package/bin/domain/interfaces/branch.type.d.ts +0 -5
  36. package/bin/domain/interfaces/config.type.d.ts +0 -7
  37. package/bin/domain/interfaces/repositories/ibranch-repository.d.ts +0 -9
  38. package/bin/domain/interfaces/repository-interfaces.d.ts +0 -2
  39. package/bin/domain/repositories/branch-repository.d.ts +0 -10
  40. package/bin/domain/repositories/config-repository.d.ts +0 -12
  41. package/bin/infrastructure/git/git-branch-repository.d.ts +0 -11
  42. /package/bin/domain/{entities/branch.d.ts → entity/branch.entity.d.ts} +0 -0
  43. /package/bin/domain/errors/{lint-errors.d.ts → lint.error.d.ts} +0 -0
  44. /package/bin/presentation/cli/formatters/{error-formatter.d.ts → error.formatter.d.ts} +0 -0
package/bin/index.js CHANGED
@@ -1,191 +1,111 @@
1
1
  #!/usr/bin/env node
2
2
  import yargs from 'yargs';
3
- import { execSync, exec } from 'node:child_process';
4
- import inquirer from 'inquirer';
5
3
  import { cosmiconfig } from 'cosmiconfig';
4
+ import { exec } from 'node:child_process';
6
5
  import { promisify } from 'node:util';
6
+ import inquirer from 'inquirer';
7
7
  import chalk from 'chalk';
8
8
 
9
9
  /**
10
- * Default configuration for branch linting
10
+ * Base error class for branch creation errors
11
11
  */
12
- const DEFAULT_CONFIG = {
13
- branches: {
14
- bugfix: { description: "🐞 Fixing issues in existing functionality", title: "Bugfix" },
15
- feature: { description: "🆕 Integration of new functionality", title: "Feature" },
16
- hotfix: { description: "🚑 Critical fixes for urgent issues", title: "Hotfix" },
17
- release: { description: "📦 Preparing a new release version", title: "Release" },
18
- support: { description: "🛠️ Support and maintenance tasks", title: "Support" },
19
- },
20
- ignore: ["dev"],
21
- rules: {
22
- "branch-max-length": 50,
23
- "branch-min-length": 5,
24
- "branch-pattern": ":type/:name",
25
- "branch-prohibited": ["main", "master", "release"],
26
- "branch-subject-pattern": "[a-z0-9-]+",
27
- },
28
- };
12
+ class BranchCreationError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "BranchCreationError";
16
+ }
17
+ }
29
18
  /**
30
- * Cosmiconfig implementation of ConfigRepository
19
+ * Error thrown when trying to create a branch that already exists
31
20
  */
32
- class CosmiconfigRepository {
33
- /**
34
- * Get the branch configuration
35
- * @param appName The name of the application
36
- * @returns A promise that resolves to the branch configuration
37
- */
38
- async getConfig(appName) {
39
- const configExplorer = cosmiconfig(appName, {
40
- packageProp: `elsikora.${appName}`,
41
- searchPlaces: ["package.json", `.elsikora/.${appName}rc`, `.elsikora/.${appName}rc.json`, `.elsikora/.${appName}rc.yaml`, `.elsikora/.${appName}rc.yml`, `.elsikora/.${appName}rc.js`, `.elsikora/.${appName}rc.ts`, `.elsikora/.${appName}rc.mjs`, `.elsikora/.${appName}rc.cjs`, `.elsikora/${appName}.config.js`, `.elsikora/${appName}.config.ts`, `.elsikora/${appName}.config.mjs`, `.elsikora/${appName}.config.cjs`],
42
- });
43
- const result = await configExplorer.search();
44
- if (!result || result.isEmpty) {
45
- return DEFAULT_CONFIG;
46
- }
47
- // Convert the config to match our interfaces
48
- const providedConfig = result.config;
49
- const mergedConfig = {
50
- ...DEFAULT_CONFIG,
51
- ...providedConfig,
52
- };
53
- return mergedConfig;
21
+ class BranchAlreadyExistsError extends BranchCreationError {
22
+ constructor(branchName) {
23
+ super(`You are already on branch ${branchName}!`);
24
+ this.name = "BranchAlreadyExistsError";
54
25
  }
55
26
  }
56
-
57
- const EMPTY_SPACING_OFFSET = 5; // in spaces
58
- // Функция для выравнивания текста
59
- const alignChoices = (branchList) => {
60
- if (Array.isArray(branchList)) {
61
- return branchList.map((branchName) => {
62
- return {
63
- name: branchName,
64
- short: branchName,
65
- value: branchName,
66
- };
67
- });
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";
68
35
  }
69
- else {
70
- const nameOfBranchesArray = Object.keys(branchList);
71
- const maxNameLength = Math.max(...nameOfBranchesArray.map((branchName) => branchName.length));
72
- // Формируем choices с выравниванием
73
- return nameOfBranchesArray.map((branchName) => {
74
- const padding = " ".repeat(maxNameLength - branchName.length + EMPTY_SPACING_OFFSET);
75
- return {
76
- name: `${branchList[branchName].title}:${padding}${branchList[branchName].description}`,
77
- short: branchList[branchName].title,
78
- value: branchName,
79
- };
80
- });
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";
81
44
  }
82
- };
83
-
84
- const branchNameQuiz = async (branchList) => {
85
- const { branchType } = await inquirer.prompt([
86
- {
87
- choices: alignChoices(branchList),
88
- message: "Select the type of branch you're creating:",
89
- name: "branchType",
90
- type: "list",
91
- },
92
- ]);
93
- const { branchName } = await inquirer.prompt([
94
- {
95
- message: "Enter the branch name (e.g., authorization):",
96
- name: "branchName",
97
- type: "input",
98
- // Валидация названия ветки
99
- // eslint-disable-next-line @elsikora/sonar/function-return-type
100
- validate: (input) => {
101
- if (!input.trim()) {
102
- return "Branch name cannot be empty!";
103
- }
104
- if (!/^[a-z0-9-]+$/.test(input)) {
105
- return "Branch name can only contain lowercase letters, numbers, and hyphens!";
106
- }
107
- return true;
108
- },
109
- },
110
- ]);
111
- const fullBranchName = `${branchType}/${branchName}`;
112
- console.log(`\n⌛️ Creating branch: ${fullBranchName}`);
113
- return fullBranchName;
114
- };
45
+ }
115
46
 
116
- /* eslint-disable @elsikora/sonar/os-command */
117
- /* eslint-disable @elsikora/unicorn/no-process-exit */
118
- // Функция для выполнения Git-команд
119
- const runGitCommand = (command) => {
120
- try {
121
- execSync(command, { stdio: "inherit" });
122
- }
123
- catch (error) {
124
- console.error(`❌ Error executing command: ${command}`);
125
- console.error(error.message);
126
- process.exit(1);
47
+ /**
48
+ * Use case for checking working directory status
49
+ */
50
+ class CheckWorkingDirectoryUseCase {
51
+ branchRepository;
52
+ constructor(branchRepository) {
53
+ this.branchRepository = branchRepository;
127
54
  }
128
- };
129
-
130
- /* eslint-disable @elsikora/sonar/no-os-command-from-path */
131
- /* eslint-disable @elsikora/unicorn/no-process-exit */
132
- /* eslint-disable @elsikora/node/no-extraneous-import */
133
- // Основная функция для создания ветки
134
- const createBranch = async (appName) => {
135
- const configRepository = new CosmiconfigRepository();
136
- const { branches } = await configRepository.getConfig(appName);
137
- // Проверка чистоты рабочей директории (есть ли незакомиченные файлы)
138
- const status = execSync("git status --porcelain", { encoding: "utf8" });
139
- if (status) {
140
- console.log("⚠️ You have uncommitted changes. Please commit or stash them before creating a new branch.");
141
- process.exit(1);
55
+ /**
56
+ * Execute the use case
57
+ * @throws {UncommittedChangesError} When there are uncommitted changes
58
+ */
59
+ async execute() {
60
+ const hasChanges = await this.branchRepository.hasUncommittedChanges();
61
+ if (hasChanges) {
62
+ throw new UncommittedChangesError();
63
+ }
142
64
  }
143
- //
144
- console.log("🌿 Creating a new branch...\n");
145
- const branchName = await branchNameQuiz(branches);
146
- // Проверка, не создается ли дубликат
147
- const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
148
- if (currentBranch === branchName) {
149
- console.log(`⚠️ You are already on branch ${branchName}!`);
150
- process.exit(0);
151
- }
152
- runGitCommand(`git checkout -b ${branchName}`);
153
- const { shouldPush } = await inquirer.prompt([
154
- {
155
- default: false,
156
- message: "Do you want to push the branch to the remote repository?",
157
- name: "shouldPush",
158
- type: "confirm",
159
- },
160
- ]);
161
- if (shouldPush) {
162
- runGitCommand(`git push --set-upstream origin ${branchName}`);
163
- console.log(`✅ Branch ${branchName} pushed to remote repository!`);
65
+ }
66
+
67
+ /**
68
+ * Use case for creating a new branch
69
+ */
70
+ class CreateBranchUseCase {
71
+ branchRepository;
72
+ constructor(branchRepository) {
73
+ this.branchRepository = branchRepository;
164
74
  }
165
- else {
166
- console.log(`✅ Branch ${branchName} created locally!`);
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);
167
86
  }
168
- };
87
+ }
169
88
 
170
89
  /**
171
- * Use case for getting branch configuration
90
+ * Use case for retrieving branch configuration
172
91
  */
173
92
  class GetBranchConfigUseCase {
174
- CONFIG_REPOSITORY;
175
- /**
176
- * Constructor
177
- * @param configRepository The configuration repository
178
- */
93
+ configRepository;
179
94
  constructor(configRepository) {
180
- this.CONFIG_REPOSITORY = configRepository;
95
+ this.configRepository = configRepository;
181
96
  }
182
97
  /**
183
98
  * Execute the use case
184
- * @param appName The application name
99
+ * @param appName - The application name
185
100
  * @returns The branch configuration
186
101
  */
187
102
  async execute(appName) {
188
- 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
+ }
189
109
  }
190
110
  }
191
111
 
@@ -252,7 +172,6 @@ class LintError extends Error {
252
172
  */
253
173
  class BranchTooLongError extends LintError {
254
174
  constructor(branchName, maxLength) {
255
- // eslint-disable-next-line @elsikora/typescript/restrict-template-expressions
256
175
  super(`Branch name "${branchName}" is too long (maximum length: ${maxLength})`);
257
176
  this.name = "BranchTooLongError";
258
177
  }
@@ -262,7 +181,6 @@ class BranchTooLongError extends LintError {
262
181
  */
263
182
  class BranchTooShortError extends LintError {
264
183
  constructor(branchName, minLength) {
265
- // eslint-disable-next-line @elsikora/typescript/restrict-template-expressions
266
184
  super(`Branch name "${branchName}" is too short (minimum length: ${minLength})`);
267
185
  this.name = "BranchTooShortError";
268
186
  }
@@ -287,11 +205,11 @@ class ProhibitedBranchError extends LintError {
287
205
  }
288
206
 
289
207
  /**
290
- * Use case for linting a branch name
208
+ * Use case for linting branch names
291
209
  */
292
210
  class LintBranchNameUseCase {
293
211
  /**
294
- * Lint a branch name against a configuration
212
+ * Execute the use case
295
213
  * @param branchName The branch name to lint
296
214
  * @param config The branch configuration
297
215
  * @throws {ProhibitedBranchError} When branch name is prohibited
@@ -301,7 +219,7 @@ class LintBranchNameUseCase {
301
219
  */
302
220
  execute(branchName, config) {
303
221
  const branch = new Branch(branchName);
304
- const configRules = config.rules;
222
+ const configRules = config.rules ?? {};
305
223
  const ignoreList = config.ignore ?? [];
306
224
  if (configRules?.["branch-prohibited"] && branch.isProhibited(configRules["branch-prohibited"])) {
307
225
  throw new ProhibitedBranchError(branchName);
@@ -330,8 +248,10 @@ class LintBranchNameUseCase {
330
248
  if (!branchNamePattern) {
331
249
  return;
332
250
  }
251
+ // Get branch types - handle both array and object formats
252
+ const branchTypes = Array.isArray(config.branches) ? config.branches : Object.keys(config.branches);
333
253
  const parameters = {
334
- type: Object.keys(config.branches),
254
+ type: branchTypes,
335
255
  // Если branch-name-pattern не определён, не добавляем name в params
336
256
  ...(subjectNamePattern && { name: [subjectNamePattern] }),
337
257
  };
@@ -364,19 +284,342 @@ class LintBranchNameUseCase {
364
284
  }
365
285
  }
366
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
+
304
+ /**
305
+ * Default configuration for branch linting
306
+ */
307
+ const DEFAULT_CONFIG = {
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-]+",
322
+ },
323
+ };
324
+ /**
325
+ * Cosmiconfig implementation of ConfigRepository
326
+ */
327
+ class CosmiconfigRepository {
328
+ /**
329
+ * Get the branch configuration
330
+ * @param appName The name of the application
331
+ * @returns A promise that resolves to the branch configuration
332
+ */
333
+ async getConfig(appName) {
334
+ const configExplorer = cosmiconfig(appName, {
335
+ packageProp: `elsikora.${appName}`,
336
+ searchPlaces: ["package.json", `.elsikora/.${appName}rc`, `.elsikora/.${appName}rc.json`, `.elsikora/.${appName}rc.yaml`, `.elsikora/.${appName}rc.yml`, `.elsikora/.${appName}rc.js`, `.elsikora/.${appName}rc.ts`, `.elsikora/.${appName}rc.mjs`, `.elsikora/.${appName}rc.cjs`, `.elsikora/${appName}.config.js`, `.elsikora/${appName}.config.ts`, `.elsikora/${appName}.config.mjs`, `.elsikora/${appName}.config.cjs`],
337
+ });
338
+ const result = await configExplorer.search();
339
+ if (!result || result.isEmpty) {
340
+ return DEFAULT_CONFIG;
341
+ }
342
+ // Convert the config to match our interfaces
343
+ const providedConfig = result.config;
344
+ const mergedConfig = {
345
+ ...DEFAULT_CONFIG,
346
+ ...providedConfig,
347
+ };
348
+ return mergedConfig;
349
+ }
350
+ }
351
+
367
352
  const execAsync = promisify(exec);
368
353
  /**
369
354
  * Git implementation of BranchRepository
370
355
  */
371
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
+ }
372
370
  /**
373
371
  * Get the current branch name
374
372
  * @returns A promise that resolves to the current branch name
375
373
  */
376
374
  async getCurrentBranchName() {
377
- const command = "git rev-parse --abbrev-ref HEAD";
378
- const { stdout } = await execAsync(command);
379
- 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);
380
623
  }
381
624
  }
382
625
 
@@ -440,9 +683,9 @@ class HintFormatter {
440
683
  }
441
684
 
442
685
  /**
443
- * Controller for CLI operations
686
+ * Controller for linting CLI operations
444
687
  */
445
- class CliController {
688
+ class LintController {
446
689
  ERROR_FORMATTER;
447
690
  GET_BRANCH_CONFIG_USE_CASE;
448
691
  GET_CURRENT_BRANCH_USE_CASE;
@@ -488,7 +731,7 @@ class CliController {
488
731
  // Get the configuration using the service instead of hardcoded values
489
732
  const config = await this.GET_BRANCH_CONFIG_USE_CASE.execute("git-branch-lint");
490
733
  console.error(this.ERROR_FORMATTER.format(error.message));
491
- console.log(this.HINT_FORMATTER.format(error, config));
734
+ console.error(this.HINT_FORMATTER.format(error, config));
492
735
  // Since this is a CLI tool, it's appropriate to exit the process on validation errors
493
736
  // ESLint wants us to throw instead, but this is a CLI application where exit is acceptable
494
737
  // eslint-disable-next-line @elsikora/unicorn/no-process-exit
@@ -509,7 +752,8 @@ class CliController {
509
752
  * Application name used for configuration
510
753
  */
511
754
  const APP_NAME = "git-branch-lint";
512
- const argv = yargs(process.argv.slice(2))
755
+ const ARGS_SLICE_INDEX = 2;
756
+ const argv = yargs(process.argv.slice(ARGS_SLICE_INDEX))
513
757
  .option("branch", {
514
758
  alias: "b",
515
759
  description: "Run branch creation tool",
@@ -522,28 +766,28 @@ const argv = yargs(process.argv.slice(2))
522
766
  * Main function that bootstraps the application
523
767
  */
524
768
  const main = async () => {
525
- if (argv.branch) {
526
- try {
527
- await createBranch(APP_NAME);
528
- }
529
- catch (error) {
530
- console.error("❌ Failed to create branch:", error.message);
531
- // eslint-disable-next-line @elsikora/unicorn/no-process-exit
532
- process.exit(1);
533
- }
769
+ // Infrastructure layer
770
+ const configRepository = new CosmiconfigRepository();
771
+ const branchRepository = new GitBranchRepository();
772
+ // Application layer - common use cases
773
+ const getBranchConfigUseCase = new GetBranchConfigUseCase(configRepository);
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);
534
783
  }
535
784
  else {
536
- // Infrastructure layer
537
- const configRepository = new CosmiconfigRepository();
538
- const branchRepository = new GitBranchRepository();
539
- // Application layer
540
- const getBranchConfigUseCase = new GetBranchConfigUseCase(configRepository);
785
+ // Application layer - linting use cases
541
786
  const getCurrentBranchUseCase = new GetCurrentBranchUseCase(branchRepository);
542
787
  const lintBranchNameUseCase = new LintBranchNameUseCase();
543
788
  // Presentation layer
544
- const cliController = new CliController(getBranchConfigUseCase, getCurrentBranchUseCase, lintBranchNameUseCase);
545
- // Execute the application
546
- await cliController.execute(APP_NAME);
789
+ const lintController = new LintController(getBranchConfigUseCase, getCurrentBranchUseCase, lintBranchNameUseCase);
790
+ await lintController.execute(APP_NAME);
547
791
  }
548
792
  };
549
793
  // Bootstrap the application and handle errors