git-coco 0.3.2 → 0.3.3

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.
@@ -13,4 +13,4 @@ export interface CommitOptions extends BaseCommandOptions {
13
13
  export declare const command: string[];
14
14
  export declare const description = "Generate a commit message based on the diff summary";
15
15
  export declare const builder: CommandBuilder<CommitOptions>;
16
- export declare function handler(argv: Argv<CommitOptions>["argv"]): Promise<void>;
16
+ export declare function handler(argv: Argv<CommitOptions>['argv']): Promise<void>;
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import yargs from 'yargs';
3
- import { select, editor } from '@inquirer/prompts';
4
3
  import { simpleGit } from 'simple-git';
5
4
  import * as fs from 'fs';
6
5
  import * as os from 'os';
@@ -8,6 +7,8 @@ import * as path from 'path';
8
7
  import path__default from 'path';
9
8
  import * as ini from 'ini';
10
9
  import { PromptTemplate } from 'langchain/prompts';
10
+ import chalk from 'chalk';
11
+ import { select, editor } from '@inquirer/prompts';
11
12
  import pQueue from 'p-queue';
12
13
  import { Document } from 'langchain/document';
13
14
  import { HuggingFaceInference } from 'langchain/llms/hf';
@@ -15,12 +16,11 @@ import { loadSummarizationChain, LLMChain } from 'langchain/chains';
15
16
  import { OpenAI } from 'langchain/llms/openai';
16
17
  import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
17
18
  import { createTwoFilesPatch } from 'diff';
18
- import chalk from 'chalk';
19
+ import { minimatch } from 'minimatch';
19
20
  import GPT3NodeTokenizer from 'gpt3-tokenizer';
20
21
  import ora from 'ora';
21
22
  import now from 'performance-now';
22
23
  import prettyMilliseconds from 'pretty-ms';
23
- import { minimatch } from 'minimatch';
24
24
 
25
25
  /**
26
26
  * Returns a new object with all undefined keys removed
@@ -151,11 +151,13 @@ function loadXDGConfig(config) {
151
151
  return config;
152
152
  }
153
153
 
154
- const template$1 = `Write informative git commit message based on the diffs & file changes provided in the "Diff Summary" section.
154
+ const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
155
155
  Commit Messages must have a short description that is less than 50 characters followed by a newline character and then a more verbose detailed description.
156
+
157
+ - Typically a hyphen or asterisk is used for the bullet
156
158
  - Write concisely using an informal tone
157
- - List significant changes
158
- - DO NOT use phrases like "this commit", "this change", etc.
159
+ - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
160
+ - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
159
161
  - DO NOT use specific names or files from the code
160
162
  - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
161
163
 
@@ -186,7 +188,7 @@ const SUMMARIZE_PROMPT = new PromptTemplate({
186
188
  * @type {Config}
187
189
  */
188
190
  const DEFAULT_CONFIG = {
189
- model: 'openai/gpt-3.5-turbo',
191
+ model: 'openai/gpt-4',
190
192
  verbose: false,
191
193
  tokenLimit: 1024,
192
194
  prompt: COMMIT_PROMPT.template,
@@ -371,7 +373,7 @@ const createDiffTree = (changes) => {
371
373
  const root = new DiffTreeNode();
372
374
  for (const change of changes) {
373
375
  let currentParent = root;
374
- const parts = change.filepath.split('/');
376
+ const parts = change.filePath.split('/');
375
377
  parts.pop();
376
378
  for (const part of parts) {
377
379
  let childNode = currentParent.getChild(part);
@@ -383,8 +385,8 @@ const createDiffTree = (changes) => {
383
385
  }
384
386
  // Create a NodeFile object and add it to the parent
385
387
  currentParent.addFile({
386
- filepath: change.filepath,
387
- oldFilepath: change.oldFilepath,
388
+ filePath: change.filePath,
389
+ oldFilePath: change.oldFilePath,
388
390
  summary: change.summary,
389
391
  status: change.status,
390
392
  });
@@ -402,11 +404,11 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
402
404
  // TODO: Swap out the GPT3Tokenizer for LangChain tokenizer
403
405
  const tokenizedDiff = tokenizer.encode(diff).text;
404
406
  const tokenCount = tokenizedDiff.length;
405
- logger.verbose(`Collected diff for ${nodeFile.filepath} (${tokenCount} tokens)`, {
407
+ logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
406
408
  color: 'magenta',
407
409
  });
408
410
  return {
409
- file: nodeFile.filepath,
411
+ file: nodeFile.filePath,
410
412
  summary: nodeFile.summary,
411
413
  diff,
412
414
  tokenCount,
@@ -431,7 +433,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
431
433
  * @param configuration
432
434
  * @returns LLM Model
433
435
  */
434
- function getModel(name, key, fields, configuration) {
436
+ function getModel(name, key, fields) {
435
437
  const [llm, model] = name.split(/\/(.*)/s);
436
438
  if (!model) {
437
439
  throw new Error(`Invalid model: ${name}`);
@@ -450,7 +452,7 @@ function getModel(name, key, fields, configuration) {
450
452
  openAIApiKey: key,
451
453
  modelName: model,
452
454
  ...fields,
453
- }, configuration);
455
+ });
454
456
  }
455
457
  }
456
458
  /**
@@ -517,18 +519,18 @@ function validatePromptTemplate(text, inputVariables) {
517
519
  }
518
520
 
519
521
  const parseDefaultFileDiff = async (nodeFile, git) => {
520
- return await git.diff(['--staged', nodeFile.filepath]);
522
+ return await git.diff(['--staged', nodeFile.filePath]);
521
523
  };
522
524
  const parseRenamedFileDiff = async (nodeFile, git, logger) => {
523
525
  let result = '';
524
- const oldFilepath = nodeFile?.oldFilepath || nodeFile.filepath;
526
+ const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
525
527
  try {
526
528
  const [headContent, indexContent] = await Promise.all([
527
- git.show([`HEAD:${oldFilepath}`]),
528
- git.show([`:${nodeFile.filepath}`]),
529
+ git.show([`HEAD:${oldFilePath}`]),
530
+ git.show([`:${nodeFile.filePath}`]),
529
531
  ]);
530
532
  if (headContent !== indexContent) {
531
- result = createTwoFilesPatch(oldFilepath, nodeFile.filepath, headContent, indexContent, '', '', {
533
+ result = createTwoFilesPatch(oldFilePath, nodeFile.filePath, headContent, indexContent, '', '', {
532
534
  context: 3,
533
535
  });
534
536
  // remove the first 4 lines of the patch (they contain the old and new file names)
@@ -539,7 +541,7 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
539
541
  }
540
542
  }
541
543
  catch (err) {
542
- logger.verbose(`Error comparing file contents for ${nodeFile.filepath}`, { color: 'red' });
544
+ logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
543
545
  result = 'Error comparing file contents.';
544
546
  }
545
547
  return result;
@@ -548,7 +550,7 @@ const getDiff = async (nodeFile, { git, logger, }) => {
548
550
  if (nodeFile.status === 'deleted') {
549
551
  return 'This file has been deleted.';
550
552
  }
551
- if (nodeFile.status === 'renamed' && nodeFile.oldFilepath) {
553
+ if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
552
554
  const renamedDiff = await parseRenamedFileDiff(nodeFile, git, logger);
553
555
  return renamedDiff;
554
556
  }
@@ -585,87 +587,6 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
585
587
  return summary;
586
588
  };
587
589
 
588
- const SEPERATOR = chalk.blue('----------------');
589
- const logCommit = (commit) => {
590
- console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${commit}\n${SEPERATOR}\n`);
591
- };
592
- const logSuccess = () => {
593
- console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
594
- };
595
-
596
- /**
597
- * Wrapper around GPT3NodeTokenizer to handle default export.
598
- *
599
- * @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
600
- *
601
- * @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
602
- */
603
- const getTokenizer = () => {
604
- let tokenizer;
605
- // eslint-disable-next-line
606
- // @ts-ignore
607
- if (GPT3NodeTokenizer.default) {
608
- // eslint-disable-next-line
609
- // @ts-ignore
610
- tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
611
- }
612
- else {
613
- tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
614
- }
615
- return tokenizer;
616
- };
617
-
618
- class Logger {
619
- constructor(config) {
620
- this.config = config;
621
- this.spinner = null;
622
- }
623
- log(message, options = { color: 'blue' }) {
624
- let outputMessage = message;
625
- if (options.color) {
626
- outputMessage = chalk[options.color](outputMessage);
627
- }
628
- console.log(outputMessage);
629
- return this;
630
- }
631
- verbose(message, options = {}) {
632
- if (!this.config?.verbose) {
633
- return this;
634
- }
635
- this.log(message, options);
636
- return this;
637
- }
638
- startTimer() {
639
- this.timerStart = now();
640
- return this;
641
- }
642
- stopTimer(message, options = { color: 'yellow' }) {
643
- if (!this.config?.verbose || !this.timerStart) {
644
- return this;
645
- }
646
- const elapsedTime = prettyMilliseconds(now() - this.timerStart);
647
- let outputMessage = message
648
- ? `${message} (⏲ ${elapsedTime})`
649
- : `⏲ ${elapsedTime}`;
650
- if (options.color) {
651
- outputMessage = chalk[options.color](outputMessage);
652
- }
653
- console.log(outputMessage);
654
- return this;
655
- }
656
- startSpinner(message, options = { color: 'green' }) {
657
- const spinnerMessage = options.color ? chalk[options.color](message) : message;
658
- this.spinner = ora(spinnerMessage).start();
659
- return this;
660
- }
661
- stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
662
- const spinnerMessage = options?.color ? chalk[options.color](message) : message;
663
- this.spinner?.[options.mode || 'succeed'](spinnerMessage);
664
- this.spinner = null;
665
- return this;
666
- }
667
- }
668
-
669
590
  const llm = async ({ llm, prompt, variables }) => {
670
591
  if (!llm || !prompt || !variables) {
671
592
  throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
@@ -690,52 +611,72 @@ const llm = async ({ llm, prompt, variables }) => {
690
611
  };
691
612
 
692
613
  const getStatus = (file, location = 'index') => {
693
- const statusCode = file[location] ? file[location] : file.index;
694
- let status;
695
- switch (statusCode) {
696
- case 'A':
697
- status = 'added';
698
- break;
699
- case 'D':
700
- status = 'deleted';
701
- break;
702
- case 'M':
703
- status = 'modified';
704
- break;
705
- case 'R':
706
- status = 'renamed';
707
- break;
708
- case '?':
709
- status = 'untracked';
710
- break;
711
- default:
712
- status = 'unknown';
713
- break;
614
+ if ('index' in file && 'working_dir' in file) {
615
+ const statusCode = file[location];
616
+ switch (statusCode) {
617
+ case 'A':
618
+ return 'added';
619
+ case 'D':
620
+ return 'deleted';
621
+ case 'M':
622
+ return 'modified';
623
+ case 'R':
624
+ return 'renamed';
625
+ case '?':
626
+ return 'untracked';
627
+ default:
628
+ return 'unknown';
629
+ }
630
+ }
631
+ else if ('changes' in file && 'binary' in file) {
632
+ if (file.changes === 0)
633
+ return 'untracked';
634
+ if (file.file.includes('=>'))
635
+ return 'renamed';
636
+ if (file.deletions === 0 && file.insertions > 0)
637
+ return 'added';
638
+ if (file.insertions === 0 && file.deletions > 0)
639
+ return 'deleted';
640
+ if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
641
+ return 'modified';
642
+ return 'unknown';
643
+ }
644
+ else {
645
+ throw new Error("Invalid file type");
714
646
  }
715
- return status;
716
647
  };
717
648
 
718
649
  const getSummaryText = (file, change) => {
719
650
  const status = change.status || getStatus(file);
720
- if (change.oldFilepath) {
721
- return `${status}: ${change.oldFilepath} -> ${file.path}`;
651
+ let filePath;
652
+ if ('path' in file) {
653
+ filePath = file.path;
654
+ }
655
+ else if ('file' in file) {
656
+ filePath = change?.filePath || file.file;
657
+ }
658
+ else {
659
+ throw new Error("Invalid file type");
660
+ }
661
+ if (change.oldFilePath) {
662
+ return `${status}: ${change.oldFilePath} -> ${filePath}`;
722
663
  }
723
- return `${status}: ${file.path}`;
664
+ return `${status}: ${filePath}`;
724
665
  };
725
666
 
726
667
  const config = loadConfig();
727
668
  const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
728
669
  const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
729
- async function getChanges(git, options = {}) {
730
- const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options;
670
+ async function getChanges({ git, options }) {
671
+ const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
731
672
  const staged = [];
732
673
  const unstaged = [];
733
674
  const untracked = [];
734
675
  const status = await git.status();
735
676
  status.files.forEach((file) => {
736
677
  const fileChange = {
737
- filepath: file.path,
738
- oldFilepath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
678
+ filePath: file.path,
679
+ oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
739
680
  };
740
681
  // Unstaged files
741
682
  if (file.working_dir !== '?' && file.working_dir !== ' ') {
@@ -758,16 +699,19 @@ async function getChanges(git, options = {}) {
758
699
  });
759
700
  const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
760
701
  const filteredStaged = staged.filter((file) => {
761
- const extension = path__default.extname(file.filepath).toLowerCase();
762
- return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch(file.filepath, ignoredPattern));
702
+ const extension = path__default.extname(file.filePath).toLowerCase();
703
+ return (!ignoredExtensionsSet.has(extension) &&
704
+ !ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
763
705
  });
764
706
  const filteredUnstaged = unstaged.filter((file) => {
765
- const extension = path__default.extname(file.filepath).toLowerCase();
766
- return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch(file.filepath, ignoredPattern));
707
+ const extension = path__default.extname(file.filePath).toLowerCase();
708
+ return (!ignoredExtensionsSet.has(extension) &&
709
+ !ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
767
710
  });
768
711
  const filteredUntracked = untracked.filter((file) => {
769
- const extension = path__default.extname(file.filepath).toLowerCase();
770
- return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch(file.filepath, ignoredPattern));
712
+ const extension = path__default.extname(file.filePath).toLowerCase();
713
+ return (!ignoredExtensionsSet.has(extension) &&
714
+ !ignoredFiles.some((ignoredPattern) => minimatch(file.filePath, ignoredPattern)));
771
715
  });
772
716
  return {
773
717
  staged: filteredStaged,
@@ -777,7 +721,7 @@ async function getChanges(git, options = {}) {
777
721
  }
778
722
 
779
723
  const noResult = async ({ git, logger }) => {
780
- const { staged, unstaged, untracked } = await getChanges(git);
724
+ const { staged, unstaged, untracked } = await getChanges({ git });
781
725
  const hasStaged = staged && staged.length > 0;
782
726
  const hasUnstaged = unstaged && unstaged.length > 0;
783
727
  const hasUntracked = untracked && untracked.length > 0;
@@ -809,76 +753,25 @@ async function createCommit(commitMsg, git) {
809
753
  return await git.commit(commitMsg);
810
754
  }
811
755
 
812
- // const argv = loadArgv()
813
- const tokenizer = getTokenizer();
814
- const git = simpleGit();
815
- const command = ['commit', '$0'];
816
- const description = 'Generate a commit message based on the diff summary';
817
- const builder = {
818
- model: { type: 'string', description: 'LLM/Model-Name' },
819
- openAIApiKey: {
820
- type: 'string',
821
- description: 'OpenAI API Key',
822
- conflicts: 'huggingFaceHubApiKey',
823
- },
824
- huggingFaceHubApiKey: {
825
- type: 'string',
826
- description: 'HuggingFace Hub API Key',
827
- conflicts: 'openAIApiKey',
828
- },
829
- tokenLimit: { type: 'number', description: 'Token limit' },
830
- prompt: {
831
- type: 'string',
832
- alias: 'p',
833
- description: 'Commit message prompt',
834
- },
835
- i: {
836
- type: 'boolean',
837
- alias: 'interactive',
838
- description: 'Toggle interactive mode',
839
- },
840
- s: {
841
- type: 'boolean',
842
- description: 'Automatically commit staged changes with generated commit message',
843
- default: false,
844
- },
845
- e: {
846
- type: 'boolean',
847
- alias: 'edit',
848
- description: 'Open commit message in editor before proceeding',
849
- },
850
- summarizePrompt: {
851
- type: 'string',
852
- description: 'Large file summary prompt',
853
- },
854
- ignoredFiles: {
855
- type: 'array',
856
- description: 'Ignored files',
857
- },
858
- ignoredExtensions: {
859
- type: 'array',
860
- description: 'Ignored extensions',
861
- },
756
+ const SEPERATOR = chalk.blue('----------------');
757
+ const isInteractive = (argv) => {
758
+ return argv?.mode === 'interactive' || argv.interactive;
862
759
  };
863
- async function handler(argv) {
864
- const options = loadConfig(argv);
865
- const logger = new Logger(options);
866
- const key = getModelAPIKey(options.model, options);
867
- if (!key) {
868
- logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
869
- process.exit(1);
870
- }
871
- const model = getModel(options.model, key, {
872
- temperature: 0.4,
873
- maxConcurrency: 10,
874
- });
875
- const INTERACTIVE = options?.mode === 'interactive' || options.interactive;
876
- const { staged: changes } = await getChanges(git);
760
+ const logCommit = (commit) => {
761
+ console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${commit}\n${SEPERATOR}\n`);
762
+ };
763
+ const logSuccess = () => {
764
+ console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
765
+ };
766
+ const generateCommitMessageAndReviewLoop = async (changes, options) => {
767
+ const { logger, model, git, tokenizer } = options;
877
768
  let summary = '';
878
769
  let commitMsg = '';
879
770
  let promptTemplate = options?.prompt || '';
880
771
  let modifyPrompt = false;
881
- while (true) {
772
+ // determine if we continue generating commit messages
773
+ let continueLoop = true;
774
+ while (continueLoop) {
882
775
  if (changes.length !== 0 && !summary.length) {
883
776
  logger.verbose(`\nChanged Files: \n ${changes.map(({ summary }) => summary).join('\n ')}`, {
884
777
  color: 'blue',
@@ -924,7 +817,7 @@ async function handler(argv) {
924
817
  mode: 'succeed',
925
818
  })
926
819
  .stopTimer();
927
- if (INTERACTIVE) {
820
+ if (options?.interactive) {
928
821
  logCommit(commitMsg);
929
822
  const reviewAnswer = await select({
930
823
  message: 'Would you like to make any changes to the commit message?',
@@ -996,23 +889,180 @@ async function handler(argv) {
996
889
  },
997
890
  });
998
891
  }
999
- const MODE = (options.interactive && 'interactive') ||
1000
- (options.commit && 'interactive') ||
1001
- options?.mode ||
1002
- 'stdout';
1003
- // Handle resulting commit message
1004
- switch (MODE) {
1005
- case 'interactive':
1006
- await createCommit(commitMsg, git);
1007
- logSuccess();
1008
- break;
1009
- case 'stdout':
1010
- default:
1011
- process.stdout.write(commitMsg, 'utf8');
1012
- break;
892
+ continueLoop = false;
893
+ }
894
+ return commitMsg;
895
+ };
896
+ const handleResult = async (commit, { mode, git }) => {
897
+ // Handle resulting commit message
898
+ switch (mode) {
899
+ case 'interactive':
900
+ await createCommit(commit, git);
901
+ logSuccess();
902
+ break;
903
+ case 'stdout':
904
+ default:
905
+ process.stdout.write(commit, 'utf8');
906
+ break;
907
+ }
908
+ process.exit(0);
909
+ };
910
+
911
+ /**
912
+ * Wrapper around GPT3NodeTokenizer to handle default export.
913
+ *
914
+ * @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
915
+ *
916
+ * @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
917
+ */
918
+ const getTokenizer = () => {
919
+ let tokenizer;
920
+ // eslint-disable-next-line
921
+ // @ts-ignore
922
+ if (GPT3NodeTokenizer.default) {
923
+ // eslint-disable-next-line
924
+ // @ts-ignore
925
+ tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
926
+ }
927
+ else {
928
+ tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
929
+ }
930
+ return tokenizer;
931
+ };
932
+
933
+ class Logger {
934
+ constructor(config) {
935
+ this.config = config;
936
+ this.spinner = null;
937
+ }
938
+ log(message, options = { color: 'blue' }) {
939
+ let outputMessage = message;
940
+ if (options.color) {
941
+ outputMessage = chalk[options.color](outputMessage);
942
+ }
943
+ console.log(outputMessage);
944
+ return this;
945
+ }
946
+ verbose(message, options = {}) {
947
+ if (!this.config?.verbose) {
948
+ return this;
949
+ }
950
+ this.log(message, options);
951
+ return this;
952
+ }
953
+ startTimer() {
954
+ this.timerStart = now();
955
+ return this;
956
+ }
957
+ stopTimer(message, options = { color: 'yellow' }) {
958
+ if (!this.config?.verbose || !this.timerStart) {
959
+ return this;
960
+ }
961
+ const elapsedTime = prettyMilliseconds(now() - this.timerStart);
962
+ let outputMessage = message
963
+ ? `${message} (⏲ ${elapsedTime})`
964
+ : `⏲ ${elapsedTime}`;
965
+ if (options.color) {
966
+ outputMessage = chalk[options.color](outputMessage);
1013
967
  }
1014
- process.exit(0);
968
+ console.log(outputMessage);
969
+ return this;
970
+ }
971
+ startSpinner(message, options = { color: 'green' }) {
972
+ const spinnerMessage = options.color ? chalk[options.color](message) : message;
973
+ this.spinner = ora(spinnerMessage).start();
974
+ return this;
975
+ }
976
+ stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
977
+ const spinnerMessage = options?.color ? chalk[options.color](message) : message;
978
+ this.spinner?.[options.mode || 'succeed'](spinnerMessage);
979
+ this.spinner = null;
980
+ return this;
981
+ }
982
+ }
983
+
984
+ // const argv = loadArgv()
985
+ const tokenizer = getTokenizer();
986
+ const git = simpleGit();
987
+ const command = ['commit', '$0'];
988
+ const description = 'Generate a commit message based on the diff summary';
989
+ const builder = {
990
+ model: { type: 'string', description: 'LLM/Model-Name' },
991
+ openAIApiKey: {
992
+ type: 'string',
993
+ description: 'OpenAI API Key',
994
+ conflicts: 'huggingFaceHubApiKey',
995
+ },
996
+ huggingFaceHubApiKey: {
997
+ type: 'string',
998
+ description: 'HuggingFace Hub API Key',
999
+ conflicts: 'openAIApiKey',
1000
+ },
1001
+ tokenLimit: { type: 'number', description: 'Token limit' },
1002
+ prompt: {
1003
+ type: 'string',
1004
+ alias: 'p',
1005
+ description: 'Commit message prompt',
1006
+ },
1007
+ i: {
1008
+ type: 'boolean',
1009
+ alias: 'interactive',
1010
+ description: 'Toggle interactive mode',
1011
+ },
1012
+ s: {
1013
+ type: 'boolean',
1014
+ description: 'Automatically commit staged changes with generated commit message',
1015
+ default: false,
1016
+ },
1017
+ e: {
1018
+ type: 'boolean',
1019
+ alias: 'edit',
1020
+ description: 'Open commit message in editor before proceeding',
1021
+ },
1022
+ summarizePrompt: {
1023
+ type: 'string',
1024
+ description: 'Large file summary prompt',
1025
+ },
1026
+ ignoredFiles: {
1027
+ type: 'array',
1028
+ description: 'Ignored files',
1029
+ },
1030
+ ignoredExtensions: {
1031
+ type: 'array',
1032
+ description: 'Ignored extensions',
1033
+ },
1034
+ };
1035
+ async function handler(argv) {
1036
+ const options = loadConfig(argv);
1037
+ const logger = new Logger(options);
1038
+ const key = getModelAPIKey(options.model, options);
1039
+ if (!key) {
1040
+ logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1041
+ process.exit(1);
1015
1042
  }
1043
+ const model = getModel(options.model, key, {
1044
+ temperature: 0.4,
1045
+ maxConcurrency: 10,
1046
+ });
1047
+ const INTERACTIVE = isInteractive(options);
1048
+ const { staged: changes } = await getChanges({ git });
1049
+ const commitMsg = await generateCommitMessageAndReviewLoop(changes, {
1050
+ logger,
1051
+ model,
1052
+ git,
1053
+ tokenizer,
1054
+ prompt: options.prompt,
1055
+ interactive: INTERACTIVE,
1056
+ openInEditor: options.openInEditor,
1057
+ });
1058
+ const MODE = (options.interactive && 'interactive') ||
1059
+ (options.commit && 'interactive') ||
1060
+ options?.mode ||
1061
+ 'stdout';
1062
+ handleResult(commitMsg, {
1063
+ mode: MODE,
1064
+ git,
1065
+ });
1016
1066
  }
1017
1067
 
1018
1068
  var commit = /*#__PURE__*/Object.freeze({