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.
- package/dist/commands/commit.d.ts +1 -1
- package/dist/index.esm.mjs +269 -219
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +269 -219
- package/dist/lib/config/types.d.ts +1 -1
- package/dist/lib/langchain/prompts/commitDefault.d.ts +1 -1
- package/dist/lib/langchain/prompts/summarize.d.ts +1 -1
- package/dist/lib/langchain/utils.d.ts +2 -5
- package/dist/lib/simple-git/getChanges.d.ts +6 -3
- package/dist/lib/simple-git/getChangesByCommit.d.ts +13 -0
- package/dist/lib/simple-git/getDiffFromCommmit.d.ts +10 -0
- package/dist/lib/simple-git/getStatus.d.ts +2 -2
- package/dist/lib/simple-git/getSummaryText.d.ts +2 -2
- package/dist/lib/simple-git/helpers.d.ts +6 -0
- package/dist/lib/types.d.ts +2 -2
- package/dist/lib/ui.d.ts +22 -0
- package/dist/stats.html +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var yargs = require('yargs');
|
|
5
|
-
var prompts$1 = require('@inquirer/prompts');
|
|
6
5
|
var simpleGit = require('simple-git');
|
|
7
6
|
var fs = require('fs');
|
|
8
7
|
var os = require('os');
|
|
9
8
|
var path = require('path');
|
|
10
9
|
var ini = require('ini');
|
|
11
10
|
var prompts = require('langchain/prompts');
|
|
11
|
+
var chalk = require('chalk');
|
|
12
|
+
var prompts$1 = require('@inquirer/prompts');
|
|
12
13
|
var pQueue = require('p-queue');
|
|
13
14
|
var document = require('langchain/document');
|
|
14
15
|
var hf = require('langchain/llms/hf');
|
|
@@ -16,12 +17,11 @@ var chains = require('langchain/chains');
|
|
|
16
17
|
var openai = require('langchain/llms/openai');
|
|
17
18
|
var text_splitter = require('langchain/text_splitter');
|
|
18
19
|
var diff = require('diff');
|
|
19
|
-
var
|
|
20
|
+
var minimatch = require('minimatch');
|
|
20
21
|
var GPT3NodeTokenizer = require('gpt3-tokenizer');
|
|
21
22
|
var ora = require('ora');
|
|
22
23
|
var now = require('performance-now');
|
|
23
24
|
var prettyMilliseconds = require('pretty-ms');
|
|
24
|
-
var minimatch = require('minimatch');
|
|
25
25
|
|
|
26
26
|
function _interopNamespaceDefault(e) {
|
|
27
27
|
var n = Object.create(null);
|
|
@@ -174,11 +174,13 @@ function loadXDGConfig(config) {
|
|
|
174
174
|
return config;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
const template$1 = `Write informative git commit message based on the diffs & file changes provided in the "Diff Summary" section.
|
|
177
|
+
const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
|
|
178
178
|
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.
|
|
179
|
+
|
|
180
|
+
- Typically a hyphen or asterisk is used for the bullet
|
|
179
181
|
- Write concisely using an informal tone
|
|
180
|
-
-
|
|
181
|
-
- DO NOT use phrases like "this commit", "this change", etc.
|
|
182
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
183
|
+
- DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
|
|
182
184
|
- DO NOT use specific names or files from the code
|
|
183
185
|
- Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
|
|
184
186
|
|
|
@@ -209,7 +211,7 @@ const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
|
|
|
209
211
|
* @type {Config}
|
|
210
212
|
*/
|
|
211
213
|
const DEFAULT_CONFIG = {
|
|
212
|
-
model: 'openai/gpt-
|
|
214
|
+
model: 'openai/gpt-4',
|
|
213
215
|
verbose: false,
|
|
214
216
|
tokenLimit: 1024,
|
|
215
217
|
prompt: COMMIT_PROMPT.template,
|
|
@@ -394,7 +396,7 @@ const createDiffTree = (changes) => {
|
|
|
394
396
|
const root = new DiffTreeNode();
|
|
395
397
|
for (const change of changes) {
|
|
396
398
|
let currentParent = root;
|
|
397
|
-
const parts = change.
|
|
399
|
+
const parts = change.filePath.split('/');
|
|
398
400
|
parts.pop();
|
|
399
401
|
for (const part of parts) {
|
|
400
402
|
let childNode = currentParent.getChild(part);
|
|
@@ -406,8 +408,8 @@ const createDiffTree = (changes) => {
|
|
|
406
408
|
}
|
|
407
409
|
// Create a NodeFile object and add it to the parent
|
|
408
410
|
currentParent.addFile({
|
|
409
|
-
|
|
410
|
-
|
|
411
|
+
filePath: change.filePath,
|
|
412
|
+
oldFilePath: change.oldFilePath,
|
|
411
413
|
summary: change.summary,
|
|
412
414
|
status: change.status,
|
|
413
415
|
});
|
|
@@ -425,11 +427,11 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
425
427
|
// TODO: Swap out the GPT3Tokenizer for LangChain tokenizer
|
|
426
428
|
const tokenizedDiff = tokenizer.encode(diff).text;
|
|
427
429
|
const tokenCount = tokenizedDiff.length;
|
|
428
|
-
logger.verbose(`Collected diff for ${nodeFile.
|
|
430
|
+
logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
|
|
429
431
|
color: 'magenta',
|
|
430
432
|
});
|
|
431
433
|
return {
|
|
432
|
-
file: nodeFile.
|
|
434
|
+
file: nodeFile.filePath,
|
|
433
435
|
summary: nodeFile.summary,
|
|
434
436
|
diff,
|
|
435
437
|
tokenCount,
|
|
@@ -454,7 +456,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
|
|
|
454
456
|
* @param configuration
|
|
455
457
|
* @returns LLM Model
|
|
456
458
|
*/
|
|
457
|
-
function getModel(name, key, fields
|
|
459
|
+
function getModel(name, key, fields) {
|
|
458
460
|
const [llm, model] = name.split(/\/(.*)/s);
|
|
459
461
|
if (!model) {
|
|
460
462
|
throw new Error(`Invalid model: ${name}`);
|
|
@@ -473,7 +475,7 @@ function getModel(name, key, fields, configuration) {
|
|
|
473
475
|
openAIApiKey: key,
|
|
474
476
|
modelName: model,
|
|
475
477
|
...fields,
|
|
476
|
-
}
|
|
478
|
+
});
|
|
477
479
|
}
|
|
478
480
|
}
|
|
479
481
|
/**
|
|
@@ -540,18 +542,18 @@ function validatePromptTemplate(text, inputVariables) {
|
|
|
540
542
|
}
|
|
541
543
|
|
|
542
544
|
const parseDefaultFileDiff = async (nodeFile, git) => {
|
|
543
|
-
return await git.diff(['--staged', nodeFile.
|
|
545
|
+
return await git.diff(['--staged', nodeFile.filePath]);
|
|
544
546
|
};
|
|
545
547
|
const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
546
548
|
let result = '';
|
|
547
|
-
const
|
|
549
|
+
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
548
550
|
try {
|
|
549
551
|
const [headContent, indexContent] = await Promise.all([
|
|
550
|
-
git.show([`HEAD:${
|
|
551
|
-
git.show([`:${nodeFile.
|
|
552
|
+
git.show([`HEAD:${oldFilePath}`]),
|
|
553
|
+
git.show([`:${nodeFile.filePath}`]),
|
|
552
554
|
]);
|
|
553
555
|
if (headContent !== indexContent) {
|
|
554
|
-
result = diff.createTwoFilesPatch(
|
|
556
|
+
result = diff.createTwoFilesPatch(oldFilePath, nodeFile.filePath, headContent, indexContent, '', '', {
|
|
555
557
|
context: 3,
|
|
556
558
|
});
|
|
557
559
|
// remove the first 4 lines of the patch (they contain the old and new file names)
|
|
@@ -562,7 +564,7 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
|
562
564
|
}
|
|
563
565
|
}
|
|
564
566
|
catch (err) {
|
|
565
|
-
logger.verbose(`Error comparing file contents for ${nodeFile.
|
|
567
|
+
logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
|
|
566
568
|
result = 'Error comparing file contents.';
|
|
567
569
|
}
|
|
568
570
|
return result;
|
|
@@ -571,7 +573,7 @@ const getDiff = async (nodeFile, { git, logger, }) => {
|
|
|
571
573
|
if (nodeFile.status === 'deleted') {
|
|
572
574
|
return 'This file has been deleted.';
|
|
573
575
|
}
|
|
574
|
-
if (nodeFile.status === 'renamed' && nodeFile.
|
|
576
|
+
if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
|
|
575
577
|
const renamedDiff = await parseRenamedFileDiff(nodeFile, git, logger);
|
|
576
578
|
return renamedDiff;
|
|
577
579
|
}
|
|
@@ -608,87 +610,6 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
|
|
|
608
610
|
return summary;
|
|
609
611
|
};
|
|
610
612
|
|
|
611
|
-
const SEPERATOR = chalk.blue('----------------');
|
|
612
|
-
const logCommit = (commit) => {
|
|
613
|
-
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${commit}\n${SEPERATOR}\n`);
|
|
614
|
-
};
|
|
615
|
-
const logSuccess = () => {
|
|
616
|
-
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Wrapper around GPT3NodeTokenizer to handle default export.
|
|
621
|
-
*
|
|
622
|
-
* @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
|
|
623
|
-
*
|
|
624
|
-
* @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
|
|
625
|
-
*/
|
|
626
|
-
const getTokenizer = () => {
|
|
627
|
-
let tokenizer;
|
|
628
|
-
// eslint-disable-next-line
|
|
629
|
-
// @ts-ignore
|
|
630
|
-
if (GPT3NodeTokenizer.default) {
|
|
631
|
-
// eslint-disable-next-line
|
|
632
|
-
// @ts-ignore
|
|
633
|
-
tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
|
|
637
|
-
}
|
|
638
|
-
return tokenizer;
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
class Logger {
|
|
642
|
-
constructor(config) {
|
|
643
|
-
this.config = config;
|
|
644
|
-
this.spinner = null;
|
|
645
|
-
}
|
|
646
|
-
log(message, options = { color: 'blue' }) {
|
|
647
|
-
let outputMessage = message;
|
|
648
|
-
if (options.color) {
|
|
649
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
650
|
-
}
|
|
651
|
-
console.log(outputMessage);
|
|
652
|
-
return this;
|
|
653
|
-
}
|
|
654
|
-
verbose(message, options = {}) {
|
|
655
|
-
if (!this.config?.verbose) {
|
|
656
|
-
return this;
|
|
657
|
-
}
|
|
658
|
-
this.log(message, options);
|
|
659
|
-
return this;
|
|
660
|
-
}
|
|
661
|
-
startTimer() {
|
|
662
|
-
this.timerStart = now();
|
|
663
|
-
return this;
|
|
664
|
-
}
|
|
665
|
-
stopTimer(message, options = { color: 'yellow' }) {
|
|
666
|
-
if (!this.config?.verbose || !this.timerStart) {
|
|
667
|
-
return this;
|
|
668
|
-
}
|
|
669
|
-
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
670
|
-
let outputMessage = message
|
|
671
|
-
? `${message} (⏲ ${elapsedTime})`
|
|
672
|
-
: `⏲ ${elapsedTime}`;
|
|
673
|
-
if (options.color) {
|
|
674
|
-
outputMessage = chalk[options.color](outputMessage);
|
|
675
|
-
}
|
|
676
|
-
console.log(outputMessage);
|
|
677
|
-
return this;
|
|
678
|
-
}
|
|
679
|
-
startSpinner(message, options = { color: 'green' }) {
|
|
680
|
-
const spinnerMessage = options.color ? chalk[options.color](message) : message;
|
|
681
|
-
this.spinner = ora(spinnerMessage).start();
|
|
682
|
-
return this;
|
|
683
|
-
}
|
|
684
|
-
stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
|
|
685
|
-
const spinnerMessage = options?.color ? chalk[options.color](message) : message;
|
|
686
|
-
this.spinner?.[options.mode || 'succeed'](spinnerMessage);
|
|
687
|
-
this.spinner = null;
|
|
688
|
-
return this;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
613
|
const llm = async ({ llm, prompt, variables }) => {
|
|
693
614
|
if (!llm || !prompt || !variables) {
|
|
694
615
|
throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
|
|
@@ -713,52 +634,72 @@ const llm = async ({ llm, prompt, variables }) => {
|
|
|
713
634
|
};
|
|
714
635
|
|
|
715
636
|
const getStatus = (file, location = 'index') => {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
637
|
+
if ('index' in file && 'working_dir' in file) {
|
|
638
|
+
const statusCode = file[location];
|
|
639
|
+
switch (statusCode) {
|
|
640
|
+
case 'A':
|
|
641
|
+
return 'added';
|
|
642
|
+
case 'D':
|
|
643
|
+
return 'deleted';
|
|
644
|
+
case 'M':
|
|
645
|
+
return 'modified';
|
|
646
|
+
case 'R':
|
|
647
|
+
return 'renamed';
|
|
648
|
+
case '?':
|
|
649
|
+
return 'untracked';
|
|
650
|
+
default:
|
|
651
|
+
return 'unknown';
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else if ('changes' in file && 'binary' in file) {
|
|
655
|
+
if (file.changes === 0)
|
|
656
|
+
return 'untracked';
|
|
657
|
+
if (file.file.includes('=>'))
|
|
658
|
+
return 'renamed';
|
|
659
|
+
if (file.deletions === 0 && file.insertions > 0)
|
|
660
|
+
return 'added';
|
|
661
|
+
if (file.insertions === 0 && file.deletions > 0)
|
|
662
|
+
return 'deleted';
|
|
663
|
+
if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
|
|
664
|
+
return 'modified';
|
|
665
|
+
return 'unknown';
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
throw new Error("Invalid file type");
|
|
737
669
|
}
|
|
738
|
-
return status;
|
|
739
670
|
};
|
|
740
671
|
|
|
741
672
|
const getSummaryText = (file, change) => {
|
|
742
673
|
const status = change.status || getStatus(file);
|
|
743
|
-
|
|
744
|
-
|
|
674
|
+
let filePath;
|
|
675
|
+
if ('path' in file) {
|
|
676
|
+
filePath = file.path;
|
|
677
|
+
}
|
|
678
|
+
else if ('file' in file) {
|
|
679
|
+
filePath = change?.filePath || file.file;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
throw new Error("Invalid file type");
|
|
683
|
+
}
|
|
684
|
+
if (change.oldFilePath) {
|
|
685
|
+
return `${status}: ${change.oldFilePath} -> ${filePath}`;
|
|
745
686
|
}
|
|
746
|
-
return `${status}: ${
|
|
687
|
+
return `${status}: ${filePath}`;
|
|
747
688
|
};
|
|
748
689
|
|
|
749
690
|
const config = loadConfig();
|
|
750
691
|
const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
|
|
751
692
|
const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
|
|
752
|
-
async function getChanges(git, options
|
|
753
|
-
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options;
|
|
693
|
+
async function getChanges({ git, options }) {
|
|
694
|
+
const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
|
|
754
695
|
const staged = [];
|
|
755
696
|
const unstaged = [];
|
|
756
697
|
const untracked = [];
|
|
757
698
|
const status = await git.status();
|
|
758
699
|
status.files.forEach((file) => {
|
|
759
700
|
const fileChange = {
|
|
760
|
-
|
|
761
|
-
|
|
701
|
+
filePath: file.path,
|
|
702
|
+
oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
|
|
762
703
|
};
|
|
763
704
|
// Unstaged files
|
|
764
705
|
if (file.working_dir !== '?' && file.working_dir !== ' ') {
|
|
@@ -781,16 +722,19 @@ async function getChanges(git, options = {}) {
|
|
|
781
722
|
});
|
|
782
723
|
const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
|
|
783
724
|
const filteredStaged = staged.filter((file) => {
|
|
784
|
-
const extension = path.extname(file.
|
|
785
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
725
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
726
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
727
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
786
728
|
});
|
|
787
729
|
const filteredUnstaged = unstaged.filter((file) => {
|
|
788
|
-
const extension = path.extname(file.
|
|
789
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
730
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
731
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
732
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
790
733
|
});
|
|
791
734
|
const filteredUntracked = untracked.filter((file) => {
|
|
792
|
-
const extension = path.extname(file.
|
|
793
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
735
|
+
const extension = path.extname(file.filePath).toLowerCase();
|
|
736
|
+
return (!ignoredExtensionsSet.has(extension) &&
|
|
737
|
+
!ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
|
|
794
738
|
});
|
|
795
739
|
return {
|
|
796
740
|
staged: filteredStaged,
|
|
@@ -800,7 +744,7 @@ async function getChanges(git, options = {}) {
|
|
|
800
744
|
}
|
|
801
745
|
|
|
802
746
|
const noResult = async ({ git, logger }) => {
|
|
803
|
-
const { staged, unstaged, untracked } = await getChanges(git);
|
|
747
|
+
const { staged, unstaged, untracked } = await getChanges({ git });
|
|
804
748
|
const hasStaged = staged && staged.length > 0;
|
|
805
749
|
const hasUnstaged = unstaged && unstaged.length > 0;
|
|
806
750
|
const hasUntracked = untracked && untracked.length > 0;
|
|
@@ -832,76 +776,25 @@ async function createCommit(commitMsg, git) {
|
|
|
832
776
|
return await git.commit(commitMsg);
|
|
833
777
|
}
|
|
834
778
|
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
const command = ['commit', '$0'];
|
|
839
|
-
const description = 'Generate a commit message based on the diff summary';
|
|
840
|
-
const builder = {
|
|
841
|
-
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
842
|
-
openAIApiKey: {
|
|
843
|
-
type: 'string',
|
|
844
|
-
description: 'OpenAI API Key',
|
|
845
|
-
conflicts: 'huggingFaceHubApiKey',
|
|
846
|
-
},
|
|
847
|
-
huggingFaceHubApiKey: {
|
|
848
|
-
type: 'string',
|
|
849
|
-
description: 'HuggingFace Hub API Key',
|
|
850
|
-
conflicts: 'openAIApiKey',
|
|
851
|
-
},
|
|
852
|
-
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
853
|
-
prompt: {
|
|
854
|
-
type: 'string',
|
|
855
|
-
alias: 'p',
|
|
856
|
-
description: 'Commit message prompt',
|
|
857
|
-
},
|
|
858
|
-
i: {
|
|
859
|
-
type: 'boolean',
|
|
860
|
-
alias: 'interactive',
|
|
861
|
-
description: 'Toggle interactive mode',
|
|
862
|
-
},
|
|
863
|
-
s: {
|
|
864
|
-
type: 'boolean',
|
|
865
|
-
description: 'Automatically commit staged changes with generated commit message',
|
|
866
|
-
default: false,
|
|
867
|
-
},
|
|
868
|
-
e: {
|
|
869
|
-
type: 'boolean',
|
|
870
|
-
alias: 'edit',
|
|
871
|
-
description: 'Open commit message in editor before proceeding',
|
|
872
|
-
},
|
|
873
|
-
summarizePrompt: {
|
|
874
|
-
type: 'string',
|
|
875
|
-
description: 'Large file summary prompt',
|
|
876
|
-
},
|
|
877
|
-
ignoredFiles: {
|
|
878
|
-
type: 'array',
|
|
879
|
-
description: 'Ignored files',
|
|
880
|
-
},
|
|
881
|
-
ignoredExtensions: {
|
|
882
|
-
type: 'array',
|
|
883
|
-
description: 'Ignored extensions',
|
|
884
|
-
},
|
|
779
|
+
const SEPERATOR = chalk.blue('----------------');
|
|
780
|
+
const isInteractive = (argv) => {
|
|
781
|
+
return argv?.mode === 'interactive' || argv.interactive;
|
|
885
782
|
};
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
const model = getModel(options.model, key, {
|
|
895
|
-
temperature: 0.4,
|
|
896
|
-
maxConcurrency: 10,
|
|
897
|
-
});
|
|
898
|
-
const INTERACTIVE = options?.mode === 'interactive' || options.interactive;
|
|
899
|
-
const { staged: changes } = await getChanges(git);
|
|
783
|
+
const logCommit = (commit) => {
|
|
784
|
+
console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${commit}\n${SEPERATOR}\n`);
|
|
785
|
+
};
|
|
786
|
+
const logSuccess = () => {
|
|
787
|
+
console.log(chalk.green(chalk.bold('\nAll set! 🦾🤖')));
|
|
788
|
+
};
|
|
789
|
+
const generateCommitMessageAndReviewLoop = async (changes, options) => {
|
|
790
|
+
const { logger, model, git, tokenizer } = options;
|
|
900
791
|
let summary = '';
|
|
901
792
|
let commitMsg = '';
|
|
902
793
|
let promptTemplate = options?.prompt || '';
|
|
903
794
|
let modifyPrompt = false;
|
|
904
|
-
|
|
795
|
+
// determine if we continue generating commit messages
|
|
796
|
+
let continueLoop = true;
|
|
797
|
+
while (continueLoop) {
|
|
905
798
|
if (changes.length !== 0 && !summary.length) {
|
|
906
799
|
logger.verbose(`\nChanged Files: \n ${changes.map(({ summary }) => summary).join('\n ')}`, {
|
|
907
800
|
color: 'blue',
|
|
@@ -947,7 +840,7 @@ async function handler(argv) {
|
|
|
947
840
|
mode: 'succeed',
|
|
948
841
|
})
|
|
949
842
|
.stopTimer();
|
|
950
|
-
if (
|
|
843
|
+
if (options?.interactive) {
|
|
951
844
|
logCommit(commitMsg);
|
|
952
845
|
const reviewAnswer = await prompts$1.select({
|
|
953
846
|
message: 'Would you like to make any changes to the commit message?',
|
|
@@ -1019,23 +912,180 @@ async function handler(argv) {
|
|
|
1019
912
|
},
|
|
1020
913
|
});
|
|
1021
914
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
915
|
+
continueLoop = false;
|
|
916
|
+
}
|
|
917
|
+
return commitMsg;
|
|
918
|
+
};
|
|
919
|
+
const handleResult = async (commit, { mode, git }) => {
|
|
920
|
+
// Handle resulting commit message
|
|
921
|
+
switch (mode) {
|
|
922
|
+
case 'interactive':
|
|
923
|
+
await createCommit(commit, git);
|
|
924
|
+
logSuccess();
|
|
925
|
+
break;
|
|
926
|
+
case 'stdout':
|
|
927
|
+
default:
|
|
928
|
+
process.stdout.write(commit, 'utf8');
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
process.exit(0);
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Wrapper around GPT3NodeTokenizer to handle default export.
|
|
936
|
+
*
|
|
937
|
+
* @see https://github.com/botisan-ai/gpt3-tokenizer/issues/18
|
|
938
|
+
*
|
|
939
|
+
* @returns {GPT3NodeTokenizer} The GPT3NodeTokenizer instance.
|
|
940
|
+
*/
|
|
941
|
+
const getTokenizer = () => {
|
|
942
|
+
let tokenizer;
|
|
943
|
+
// eslint-disable-next-line
|
|
944
|
+
// @ts-ignore
|
|
945
|
+
if (GPT3NodeTokenizer.default) {
|
|
946
|
+
// eslint-disable-next-line
|
|
947
|
+
// @ts-ignore
|
|
948
|
+
tokenizer = new GPT3NodeTokenizer.default({ type: 'gpt3' });
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
tokenizer = new GPT3NodeTokenizer({ type: 'gpt3' });
|
|
952
|
+
}
|
|
953
|
+
return tokenizer;
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
class Logger {
|
|
957
|
+
constructor(config) {
|
|
958
|
+
this.config = config;
|
|
959
|
+
this.spinner = null;
|
|
960
|
+
}
|
|
961
|
+
log(message, options = { color: 'blue' }) {
|
|
962
|
+
let outputMessage = message;
|
|
963
|
+
if (options.color) {
|
|
964
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
965
|
+
}
|
|
966
|
+
console.log(outputMessage);
|
|
967
|
+
return this;
|
|
968
|
+
}
|
|
969
|
+
verbose(message, options = {}) {
|
|
970
|
+
if (!this.config?.verbose) {
|
|
971
|
+
return this;
|
|
972
|
+
}
|
|
973
|
+
this.log(message, options);
|
|
974
|
+
return this;
|
|
975
|
+
}
|
|
976
|
+
startTimer() {
|
|
977
|
+
this.timerStart = now();
|
|
978
|
+
return this;
|
|
979
|
+
}
|
|
980
|
+
stopTimer(message, options = { color: 'yellow' }) {
|
|
981
|
+
if (!this.config?.verbose || !this.timerStart) {
|
|
982
|
+
return this;
|
|
983
|
+
}
|
|
984
|
+
const elapsedTime = prettyMilliseconds(now() - this.timerStart);
|
|
985
|
+
let outputMessage = message
|
|
986
|
+
? `${message} (⏲ ${elapsedTime})`
|
|
987
|
+
: `⏲ ${elapsedTime}`;
|
|
988
|
+
if (options.color) {
|
|
989
|
+
outputMessage = chalk[options.color](outputMessage);
|
|
1036
990
|
}
|
|
1037
|
-
|
|
991
|
+
console.log(outputMessage);
|
|
992
|
+
return this;
|
|
993
|
+
}
|
|
994
|
+
startSpinner(message, options = { color: 'green' }) {
|
|
995
|
+
const spinnerMessage = options.color ? chalk[options.color](message) : message;
|
|
996
|
+
this.spinner = ora(spinnerMessage).start();
|
|
997
|
+
return this;
|
|
998
|
+
}
|
|
999
|
+
stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
|
|
1000
|
+
const spinnerMessage = options?.color ? chalk[options.color](message) : message;
|
|
1001
|
+
this.spinner?.[options.mode || 'succeed'](spinnerMessage);
|
|
1002
|
+
this.spinner = null;
|
|
1003
|
+
return this;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// const argv = loadArgv()
|
|
1008
|
+
const tokenizer = getTokenizer();
|
|
1009
|
+
const git = simpleGit.simpleGit();
|
|
1010
|
+
const command = ['commit', '$0'];
|
|
1011
|
+
const description = 'Generate a commit message based on the diff summary';
|
|
1012
|
+
const builder = {
|
|
1013
|
+
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1014
|
+
openAIApiKey: {
|
|
1015
|
+
type: 'string',
|
|
1016
|
+
description: 'OpenAI API Key',
|
|
1017
|
+
conflicts: 'huggingFaceHubApiKey',
|
|
1018
|
+
},
|
|
1019
|
+
huggingFaceHubApiKey: {
|
|
1020
|
+
type: 'string',
|
|
1021
|
+
description: 'HuggingFace Hub API Key',
|
|
1022
|
+
conflicts: 'openAIApiKey',
|
|
1023
|
+
},
|
|
1024
|
+
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1025
|
+
prompt: {
|
|
1026
|
+
type: 'string',
|
|
1027
|
+
alias: 'p',
|
|
1028
|
+
description: 'Commit message prompt',
|
|
1029
|
+
},
|
|
1030
|
+
i: {
|
|
1031
|
+
type: 'boolean',
|
|
1032
|
+
alias: 'interactive',
|
|
1033
|
+
description: 'Toggle interactive mode',
|
|
1034
|
+
},
|
|
1035
|
+
s: {
|
|
1036
|
+
type: 'boolean',
|
|
1037
|
+
description: 'Automatically commit staged changes with generated commit message',
|
|
1038
|
+
default: false,
|
|
1039
|
+
},
|
|
1040
|
+
e: {
|
|
1041
|
+
type: 'boolean',
|
|
1042
|
+
alias: 'edit',
|
|
1043
|
+
description: 'Open commit message in editor before proceeding',
|
|
1044
|
+
},
|
|
1045
|
+
summarizePrompt: {
|
|
1046
|
+
type: 'string',
|
|
1047
|
+
description: 'Large file summary prompt',
|
|
1048
|
+
},
|
|
1049
|
+
ignoredFiles: {
|
|
1050
|
+
type: 'array',
|
|
1051
|
+
description: 'Ignored files',
|
|
1052
|
+
},
|
|
1053
|
+
ignoredExtensions: {
|
|
1054
|
+
type: 'array',
|
|
1055
|
+
description: 'Ignored extensions',
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
async function handler(argv) {
|
|
1059
|
+
const options = loadConfig(argv);
|
|
1060
|
+
const logger = new Logger(options);
|
|
1061
|
+
const key = getModelAPIKey(options.model, options);
|
|
1062
|
+
if (!key) {
|
|
1063
|
+
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
1064
|
+
process.exit(1);
|
|
1038
1065
|
}
|
|
1066
|
+
const model = getModel(options.model, key, {
|
|
1067
|
+
temperature: 0.4,
|
|
1068
|
+
maxConcurrency: 10,
|
|
1069
|
+
});
|
|
1070
|
+
const INTERACTIVE = isInteractive(options);
|
|
1071
|
+
const { staged: changes } = await getChanges({ git });
|
|
1072
|
+
const commitMsg = await generateCommitMessageAndReviewLoop(changes, {
|
|
1073
|
+
logger,
|
|
1074
|
+
model,
|
|
1075
|
+
git,
|
|
1076
|
+
tokenizer,
|
|
1077
|
+
prompt: options.prompt,
|
|
1078
|
+
interactive: INTERACTIVE,
|
|
1079
|
+
openInEditor: options.openInEditor,
|
|
1080
|
+
});
|
|
1081
|
+
const MODE = (options.interactive && 'interactive') ||
|
|
1082
|
+
(options.commit && 'interactive') ||
|
|
1083
|
+
options?.mode ||
|
|
1084
|
+
'stdout';
|
|
1085
|
+
handleResult(commitMsg, {
|
|
1086
|
+
mode: MODE,
|
|
1087
|
+
git,
|
|
1088
|
+
});
|
|
1039
1089
|
}
|
|
1040
1090
|
|
|
1041
1091
|
var commit = /*#__PURE__*/Object.freeze({
|