git-coco 0.3.1 → 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.d.ts +2 -1
- package/dist/index.esm.mjs +270 -220
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +270 -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
|
@@ -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>[
|
|
16
|
+
export declare function handler(argv: Argv<CommitOptions>['argv']): Promise<void>;
|
package/dist/index.d.ts
CHANGED
package/dist/index.esm.mjs
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
387
|
-
|
|
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.
|
|
407
|
+
logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
|
|
406
408
|
color: 'magenta',
|
|
407
409
|
});
|
|
408
410
|
return {
|
|
409
|
-
file: nodeFile.
|
|
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
|
|
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
|
-
}
|
|
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.
|
|
522
|
+
return await git.diff(['--staged', nodeFile.filePath]);
|
|
521
523
|
};
|
|
522
524
|
const parseRenamedFileDiff = async (nodeFile, git, logger) => {
|
|
523
525
|
let result = '';
|
|
524
|
-
const
|
|
526
|
+
const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
|
|
525
527
|
try {
|
|
526
528
|
const [headContent, indexContent] = await Promise.all([
|
|
527
|
-
git.show([`HEAD:${
|
|
528
|
-
git.show([`:${nodeFile.
|
|
529
|
+
git.show([`HEAD:${oldFilePath}`]),
|
|
530
|
+
git.show([`:${nodeFile.filePath}`]),
|
|
529
531
|
]);
|
|
530
532
|
if (headContent !== indexContent) {
|
|
531
|
-
result = createTwoFilesPatch(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
721
|
-
|
|
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}: ${
|
|
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
|
-
|
|
738
|
-
|
|
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.
|
|
762
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
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.
|
|
766
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
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.
|
|
770
|
-
return !ignoredExtensionsSet.has(extension) &&
|
|
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
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
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({
|
|
@@ -1037,5 +1087,5 @@ yargs
|
|
|
1037
1087
|
description: 'Run with verbose logging',
|
|
1038
1088
|
}).argv;
|
|
1039
1089
|
|
|
1040
|
-
export { commit };
|
|
1090
|
+
export { commit, loadConfig };
|
|
1041
1091
|
//# sourceMappingURL=index.esm.mjs.map
|