git-coco 0.4.1 → 0.6.0
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/init/handler.d.ts +3 -0
- package/dist/commands/init/index.d.ts +10 -0
- package/dist/commands/init/options.d.ts +15 -0
- package/dist/index.esm.mjs +507 -74
- package/dist/index.esm.mjs.map +1 -1
- package/dist/index.js +505 -74
- package/dist/lib/config/constants.d.ts +13 -0
- package/dist/lib/config/loadConfig.d.ts +0 -6
- package/dist/lib/config/services/env.d.ts +1 -0
- package/dist/lib/config/services/git.d.ts +7 -0
- package/dist/lib/config/services/project.d.ts +1 -0
- package/dist/lib/config/types.d.ts +6 -0
- package/dist/lib/simple-git/getCommitLogCurrentBranch.d.ts +9 -0
- package/dist/lib/simple-git/getCurrentBranchName.d.ts +5 -0
- package/dist/lib/simple-git/getRepo.d.ts +2 -0
- package/dist/lib/ui/logResult.d.ts +1 -1
- package/dist/lib/utils/updateFileSection.d.ts +1 -0
- package/dist/stats.html +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -18,10 +18,10 @@ var prettyMilliseconds = require('pretty-ms');
|
|
|
18
18
|
var path = require('path');
|
|
19
19
|
var minimatch = require('minimatch');
|
|
20
20
|
var fs = require('fs');
|
|
21
|
+
var prompts$1 = require('@inquirer/prompts');
|
|
21
22
|
var os = require('os');
|
|
22
23
|
var ini = require('ini');
|
|
23
24
|
var simpleGit = require('simple-git');
|
|
24
|
-
var prompts$1 = require('@inquirer/prompts');
|
|
25
25
|
|
|
26
26
|
function _interopNamespaceDefault(e) {
|
|
27
27
|
var n = Object.create(null);
|
|
@@ -606,6 +606,82 @@ function removeUndefined(obj) {
|
|
|
606
606
|
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Default Config
|
|
611
|
+
*
|
|
612
|
+
* @type {Config}
|
|
613
|
+
*/
|
|
614
|
+
const DEFAULT_CONFIG = {
|
|
615
|
+
model: 'openai/gpt-4',
|
|
616
|
+
verbose: false,
|
|
617
|
+
tokenLimit: 1024,
|
|
618
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
619
|
+
temperature: 0.4,
|
|
620
|
+
mode: 'stdout',
|
|
621
|
+
ignoredFiles: ['package-lock.json'],
|
|
622
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
623
|
+
defaultBranch: 'main',
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* Config keys
|
|
627
|
+
*
|
|
628
|
+
* @type {string[]}
|
|
629
|
+
*/
|
|
630
|
+
const CONFIG_KEYS = Object.keys({
|
|
631
|
+
...DEFAULT_CONFIG,
|
|
632
|
+
huggingFaceHubApiKey: '',
|
|
633
|
+
openAIApiKey: '',
|
|
634
|
+
prompt: '',
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
async function updateFileSection(filePath, startComment, endComment, getNewContent, confirmUpdate = true) {
|
|
638
|
+
const lines = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
639
|
+
const newLines = [];
|
|
640
|
+
let foundSection = false;
|
|
641
|
+
for (let i = 0; i < lines.length; i++) {
|
|
642
|
+
if (lines[i].trim() === startComment) {
|
|
643
|
+
foundSection = true;
|
|
644
|
+
if (confirmUpdate) {
|
|
645
|
+
const confirmOverwrite = await prompts$1.confirm({
|
|
646
|
+
message: `A section already exists in ${filePath}, do you want to override it?`,
|
|
647
|
+
default: false,
|
|
648
|
+
});
|
|
649
|
+
if (!confirmOverwrite) {
|
|
650
|
+
// keep all lines until the end comment
|
|
651
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
652
|
+
newLines.push(lines[i]);
|
|
653
|
+
i++;
|
|
654
|
+
}
|
|
655
|
+
newLines.push(endComment);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
newLines.push(startComment);
|
|
660
|
+
// Insert the new content
|
|
661
|
+
const newContent = await getNewContent();
|
|
662
|
+
newLines.push(newContent);
|
|
663
|
+
// Skip the existing content of the section
|
|
664
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
665
|
+
i++;
|
|
666
|
+
}
|
|
667
|
+
newLines.push(endComment);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (!foundSection || lines[i].trim() !== endComment) {
|
|
671
|
+
newLines.push(lines[i]);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// If section wasn't found, append it at the end
|
|
675
|
+
if (!foundSection) {
|
|
676
|
+
newLines.push('\n' + startComment);
|
|
677
|
+
const newContent = await getNewContent();
|
|
678
|
+
newLines.push(newContent);
|
|
679
|
+
newLines.push(endComment);
|
|
680
|
+
}
|
|
681
|
+
// Write the updated contents back to the file
|
|
682
|
+
fs.writeFileSync(filePath, newLines.join('\n'));
|
|
683
|
+
}
|
|
684
|
+
|
|
609
685
|
/**
|
|
610
686
|
* Load environment variables
|
|
611
687
|
*
|
|
@@ -613,26 +689,65 @@ function removeUndefined(obj) {
|
|
|
613
689
|
* @returns {Config} Updated config
|
|
614
690
|
**/
|
|
615
691
|
function loadEnvConfig(config) {
|
|
616
|
-
const envConfig = {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
? process.env.COCO_IGNORED_FILES.split(',')
|
|
628
|
-
: undefined,
|
|
629
|
-
ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
|
|
630
|
-
? process.env.COCO_IGNORED_EXTENSIONS.split(',')
|
|
631
|
-
: undefined,
|
|
632
|
-
};
|
|
633
|
-
config = { ...config, ...removeUndefined(envConfig) };
|
|
634
|
-
return config;
|
|
692
|
+
const envConfig = {};
|
|
693
|
+
CONFIG_KEYS.forEach((key) => {
|
|
694
|
+
const envVarName = toEnvVarName(key);
|
|
695
|
+
const envValue = parseEnvValue(key, process.env[envVarName]);
|
|
696
|
+
if (envValue === undefined)
|
|
697
|
+
return;
|
|
698
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
699
|
+
// @ts-ignore
|
|
700
|
+
envConfig[key] = envValue;
|
|
701
|
+
});
|
|
702
|
+
return { ...config, ...removeUndefined(envConfig) };
|
|
635
703
|
}
|
|
704
|
+
function parseEnvValue(key, value) {
|
|
705
|
+
if (value === undefined) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
else if (key === 'tokenLimit' && typeof value === 'string') {
|
|
709
|
+
return parseInt(value);
|
|
710
|
+
}
|
|
711
|
+
else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
|
|
712
|
+
typeof value === 'string' &&
|
|
713
|
+
value.includes(',')) {
|
|
714
|
+
return value.split(',');
|
|
715
|
+
}
|
|
716
|
+
return value;
|
|
717
|
+
}
|
|
718
|
+
function toEnvVarName(key) {
|
|
719
|
+
switch (key) {
|
|
720
|
+
case 'openAIApiKey':
|
|
721
|
+
return 'OPENAI_API_KEY';
|
|
722
|
+
case 'huggingFaceHubApiKey':
|
|
723
|
+
return 'HUGGINGFACE_HUB_API_KEY';
|
|
724
|
+
default:
|
|
725
|
+
return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function formatEnvValue(value) {
|
|
729
|
+
if (typeof value === 'number') {
|
|
730
|
+
return `${value}`;
|
|
731
|
+
}
|
|
732
|
+
else if (Array.isArray(value)) {
|
|
733
|
+
return `${value.join(',')}`;
|
|
734
|
+
}
|
|
735
|
+
else if (typeof value === 'string') {
|
|
736
|
+
// Escape newlines and tabs in strings
|
|
737
|
+
return `${value.replace(/\n/g, '\\n').replace(/\t/g, '\\t')}`;
|
|
738
|
+
}
|
|
739
|
+
return `${value}`;
|
|
740
|
+
}
|
|
741
|
+
const appendToEnvFile = async (filePath, config) => {
|
|
742
|
+
const startComment = '# -- Start coco config --';
|
|
743
|
+
const endComment = '# -- End coco config --';
|
|
744
|
+
const getNewContent = async () => {
|
|
745
|
+
return Object.entries(config)
|
|
746
|
+
.map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
|
|
747
|
+
.join('\n');
|
|
748
|
+
};
|
|
749
|
+
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
750
|
+
};
|
|
636
751
|
|
|
637
752
|
/**
|
|
638
753
|
* Load git profile config (from ~/.gitconfig)
|
|
@@ -657,10 +772,43 @@ function loadGitConfig(config) {
|
|
|
657
772
|
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
658
773
|
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
659
774
|
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
775
|
+
defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
|
|
660
776
|
};
|
|
661
777
|
}
|
|
662
778
|
return config;
|
|
663
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Appends the provided configuration to a git config file.
|
|
782
|
+
*
|
|
783
|
+
* @param filePath - The path to the .gitconfig
|
|
784
|
+
* @param config - The configuration object to append.
|
|
785
|
+
*/
|
|
786
|
+
const appendToGitConfig = async (filePath, config) => {
|
|
787
|
+
if (!fs__namespace.existsSync(filePath)) {
|
|
788
|
+
throw new Error(`File ${filePath} does not exist.`);
|
|
789
|
+
}
|
|
790
|
+
const startComment = '# -- Start coco config --';
|
|
791
|
+
const endComment = '# -- End coco config --';
|
|
792
|
+
const header = '[coco]';
|
|
793
|
+
// Function to generate new content for the coco section
|
|
794
|
+
const getNewContent = async () => {
|
|
795
|
+
const contentLines = [header];
|
|
796
|
+
for (const key in config) {
|
|
797
|
+
// check if string has new lines, if so, wrap in quotes
|
|
798
|
+
if (typeof config[key] === 'string') {
|
|
799
|
+
const value = config[key];
|
|
800
|
+
if (value.includes('\n')) {
|
|
801
|
+
contentLines.push(`\t${key} = ${JSON.stringify(value)}`);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
contentLines.push(`\t${key} = ${config[key]}`);
|
|
806
|
+
}
|
|
807
|
+
return contentLines.join('\n');
|
|
808
|
+
};
|
|
809
|
+
// Use the updateFileSection utility
|
|
810
|
+
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
811
|
+
};
|
|
664
812
|
|
|
665
813
|
/**
|
|
666
814
|
* Load .gitignore in project root
|
|
@@ -702,12 +850,20 @@ function loadIgnore(config) {
|
|
|
702
850
|
* @returns {Config} Updated config
|
|
703
851
|
**/
|
|
704
852
|
function loadProjectConfig(config) {
|
|
853
|
+
// TODO: Add validation based of JSON schema?
|
|
854
|
+
// @see https://github.com/acornejo/jjv
|
|
705
855
|
if (fs__namespace.existsSync('.coco.config.json')) {
|
|
706
856
|
const projectConfig = JSON.parse(fs__namespace.readFileSync('.coco.config.json', 'utf-8'));
|
|
707
857
|
config = { ...config, ...projectConfig };
|
|
708
858
|
}
|
|
709
859
|
return config;
|
|
710
860
|
}
|
|
861
|
+
const appendToProjectConfig = (filePath, config) => {
|
|
862
|
+
fs__namespace.writeFileSync(filePath, JSON.stringify({
|
|
863
|
+
$schema: 'https://git-co.co/schema.json',
|
|
864
|
+
...config,
|
|
865
|
+
}, null, 2));
|
|
866
|
+
};
|
|
711
867
|
|
|
712
868
|
/**
|
|
713
869
|
* Load XDG config
|
|
@@ -725,21 +881,6 @@ function loadXDGConfig(config) {
|
|
|
725
881
|
return config;
|
|
726
882
|
}
|
|
727
883
|
|
|
728
|
-
/**
|
|
729
|
-
* Default Config
|
|
730
|
-
*
|
|
731
|
-
* @type {Config}
|
|
732
|
-
*/
|
|
733
|
-
const DEFAULT_CONFIG = {
|
|
734
|
-
model: 'openai/gpt-4',
|
|
735
|
-
verbose: false,
|
|
736
|
-
tokenLimit: 1024,
|
|
737
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
738
|
-
temperature: 0.4,
|
|
739
|
-
mode: 'stdout',
|
|
740
|
-
ignoredFiles: ['package-lock.json'],
|
|
741
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
742
|
-
};
|
|
743
884
|
/**
|
|
744
885
|
* Load application config
|
|
745
886
|
*
|
|
@@ -859,8 +1000,8 @@ const isInteractive = (argv) => {
|
|
|
859
1000
|
};
|
|
860
1001
|
const SEPERATOR = chalk.blue('----------------');
|
|
861
1002
|
|
|
862
|
-
function logResult(result) {
|
|
863
|
-
console.log(`\n${chalk.bgBlue(chalk.bold(
|
|
1003
|
+
function logResult(label, result) {
|
|
1004
|
+
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
864
1005
|
}
|
|
865
1006
|
|
|
866
1007
|
async function editResult(result, options) {
|
|
@@ -961,7 +1102,7 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
|
|
|
961
1102
|
})
|
|
962
1103
|
.stopTimer();
|
|
963
1104
|
if (options?.interactive) {
|
|
964
|
-
logResult(result);
|
|
1105
|
+
logResult(label, result);
|
|
965
1106
|
const reviewAnswer = await getUserReviewDecision();
|
|
966
1107
|
if (reviewAnswer === 'cancel') {
|
|
967
1108
|
process.exit(0);
|
|
@@ -1039,9 +1180,21 @@ const handleResult = async (result, { mode, git }) => {
|
|
|
1039
1180
|
process.exit(0);
|
|
1040
1181
|
};
|
|
1041
1182
|
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1183
|
+
const getRepo = () => {
|
|
1184
|
+
let git;
|
|
1185
|
+
try {
|
|
1186
|
+
git = simpleGit.simpleGit();
|
|
1187
|
+
}
|
|
1188
|
+
catch (e) {
|
|
1189
|
+
console.log('Error initializing git repo', e);
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
return git;
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
async function handler$2(argv) {
|
|
1196
|
+
const tokenizer = getTokenizer();
|
|
1197
|
+
const git = getRepo();
|
|
1045
1198
|
const options = loadConfig(argv);
|
|
1046
1199
|
const logger = new Logger(options);
|
|
1047
1200
|
const key = getApiKeyForModel(options.model, options);
|
|
@@ -1055,14 +1208,14 @@ async function handler$1(argv) {
|
|
|
1055
1208
|
});
|
|
1056
1209
|
const INTERACTIVE = isInteractive(options);
|
|
1057
1210
|
async function factory() {
|
|
1058
|
-
const changes = await getChanges({ git
|
|
1211
|
+
const changes = await getChanges({ git });
|
|
1059
1212
|
return changes.staged;
|
|
1060
1213
|
}
|
|
1061
1214
|
async function parser(changes) {
|
|
1062
1215
|
return await fileChangeParser({
|
|
1063
1216
|
changes,
|
|
1064
1217
|
commit: '--staged',
|
|
1065
|
-
options: { tokenizer, git
|
|
1218
|
+
options: { tokenizer, git, model, logger },
|
|
1066
1219
|
});
|
|
1067
1220
|
}
|
|
1068
1221
|
const commitMsg = await generateAndReviewLoop({
|
|
@@ -1081,7 +1234,7 @@ async function handler$1(argv) {
|
|
|
1081
1234
|
});
|
|
1082
1235
|
},
|
|
1083
1236
|
noResult: async () => {
|
|
1084
|
-
await noResult({ git
|
|
1237
|
+
await noResult({ git, logger });
|
|
1085
1238
|
process.exit(0);
|
|
1086
1239
|
},
|
|
1087
1240
|
options: {
|
|
@@ -1094,14 +1247,14 @@ async function handler$1(argv) {
|
|
|
1094
1247
|
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1095
1248
|
handleResult(commitMsg, {
|
|
1096
1249
|
mode: MODE,
|
|
1097
|
-
git
|
|
1250
|
+
git,
|
|
1098
1251
|
});
|
|
1099
1252
|
}
|
|
1100
1253
|
|
|
1101
1254
|
/**
|
|
1102
1255
|
* Command line options via yargs
|
|
1103
1256
|
*/
|
|
1104
|
-
const options$
|
|
1257
|
+
const options$2 = {
|
|
1105
1258
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1106
1259
|
openAIApiKey: {
|
|
1107
1260
|
type: 'string',
|
|
@@ -1147,16 +1300,16 @@ const options$1 = {
|
|
|
1147
1300
|
description: 'Ignored extensions',
|
|
1148
1301
|
},
|
|
1149
1302
|
};
|
|
1150
|
-
const builder$
|
|
1151
|
-
return yargs.options(options$
|
|
1303
|
+
const builder$2 = (yargs) => {
|
|
1304
|
+
return yargs.options(options$2);
|
|
1152
1305
|
};
|
|
1153
1306
|
|
|
1154
1307
|
var commit = {
|
|
1155
1308
|
command: 'commit',
|
|
1156
1309
|
desc: 'Generate commit message',
|
|
1157
|
-
builder: builder$
|
|
1158
|
-
handler: handler$
|
|
1159
|
-
options: options$
|
|
1310
|
+
builder: builder$2,
|
|
1311
|
+
handler: handler$2,
|
|
1312
|
+
options: options$2,
|
|
1160
1313
|
};
|
|
1161
1314
|
|
|
1162
1315
|
const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
|
|
@@ -1175,15 +1328,9 @@ const CHANGELOG_PROMPT = new prompts.PromptTemplate({
|
|
|
1175
1328
|
|
|
1176
1329
|
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
1177
1330
|
try {
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
'--pretty=format:%s',
|
|
1182
|
-
// Include '--no-merges' here if you want to exclude merge commits.
|
|
1183
|
-
noMerges ? '--no-merges' : null,
|
|
1184
|
-
].filter(Boolean)); // filter(Boolean) removes any null values from the array
|
|
1185
|
-
const messages = output.split('\n').filter(Boolean);
|
|
1186
|
-
return messages;
|
|
1331
|
+
const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
|
|
1332
|
+
const commitLog = await git.log(logOptions);
|
|
1333
|
+
return commitLog.all.map(({ message, date, body, author_name }) => `[${date}] ${message}\n${body}\n - ${author_name}`);
|
|
1187
1334
|
}
|
|
1188
1335
|
catch (error) {
|
|
1189
1336
|
// If there's an error, handle it appropriately
|
|
@@ -1192,10 +1339,56 @@ async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
|
1192
1339
|
}
|
|
1193
1340
|
}
|
|
1194
1341
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1342
|
+
async function getCurrentBranchName({ git }) {
|
|
1343
|
+
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
|
|
1347
|
+
try {
|
|
1348
|
+
// Get the current branch name
|
|
1349
|
+
const branch = await getCurrentBranchName({ git });
|
|
1350
|
+
// Check if the current branch has any commits
|
|
1351
|
+
const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
|
|
1352
|
+
if (!hasCommits) {
|
|
1353
|
+
logger?.log('No commits on the current branch.');
|
|
1354
|
+
return [];
|
|
1355
|
+
}
|
|
1356
|
+
// Get the list of commits that are unique to the current branch
|
|
1357
|
+
let uniqueCommits;
|
|
1358
|
+
if (comparisonBranch === branch) {
|
|
1359
|
+
// If the comparison branch is the same as the current branch, we compare against the remote.
|
|
1360
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
|
|
1361
|
+
.split('\n')
|
|
1362
|
+
.filter(Boolean)
|
|
1363
|
+
.reverse();
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
// Your existing code for different branches
|
|
1367
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
|
|
1368
|
+
.split('\n')
|
|
1369
|
+
.filter(Boolean)
|
|
1370
|
+
.reverse();
|
|
1371
|
+
}
|
|
1372
|
+
logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branch}"`, { color: 'blue' });
|
|
1373
|
+
const firstCommit = uniqueCommits[0];
|
|
1374
|
+
const lastCommit = uniqueCommits[uniqueCommits.length - 1];
|
|
1375
|
+
if (!firstCommit || !lastCommit) {
|
|
1376
|
+
logger?.log('Unable to determine first and last commit on the current branch', { color: 'yellow' });
|
|
1377
|
+
return [];
|
|
1378
|
+
}
|
|
1379
|
+
// Retrieve commit log with messages
|
|
1380
|
+
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
1381
|
+
}
|
|
1382
|
+
catch (error) {
|
|
1383
|
+
logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
|
|
1384
|
+
}
|
|
1385
|
+
return [];
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async function handler$1(argv) {
|
|
1197
1389
|
const options = loadConfig(argv);
|
|
1198
1390
|
const logger = new Logger(options);
|
|
1391
|
+
const git = getRepo();
|
|
1199
1392
|
const key = getApiKeyForModel(options.model, options);
|
|
1200
1393
|
if (!key) {
|
|
1201
1394
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
@@ -1206,14 +1399,17 @@ async function handler(argv) {
|
|
|
1206
1399
|
maxConcurrency: 10,
|
|
1207
1400
|
});
|
|
1208
1401
|
const INTERACTIVE = isInteractive(options);
|
|
1209
|
-
const [from, to] = options.range?.split(':');
|
|
1210
|
-
if (!from || !to) {
|
|
1211
|
-
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
1212
|
-
process.exit(1);
|
|
1213
|
-
}
|
|
1214
1402
|
async function factory() {
|
|
1215
|
-
|
|
1216
|
-
|
|
1403
|
+
if (options.range) {
|
|
1404
|
+
const [from, to] = options.range?.split(':');
|
|
1405
|
+
if (!from || !to) {
|
|
1406
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
1407
|
+
process.exit(1);
|
|
1408
|
+
}
|
|
1409
|
+
return await getCommitLogRange(from, to, { git, noMerges: true });
|
|
1410
|
+
}
|
|
1411
|
+
logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
|
|
1412
|
+
return await getCommitLogCurrentBranch({ git, logger });
|
|
1217
1413
|
}
|
|
1218
1414
|
async function parser(messages) {
|
|
1219
1415
|
const result = messages.join('\n');
|
|
@@ -1236,7 +1432,11 @@ async function handler(argv) {
|
|
|
1236
1432
|
});
|
|
1237
1433
|
},
|
|
1238
1434
|
noResult: async () => {
|
|
1239
|
-
|
|
1435
|
+
if (options.range) {
|
|
1436
|
+
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
1437
|
+
process.exit(0);
|
|
1438
|
+
}
|
|
1439
|
+
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
1240
1440
|
process.exit(0);
|
|
1241
1441
|
},
|
|
1242
1442
|
options: {
|
|
@@ -1256,12 +1456,11 @@ async function handler(argv) {
|
|
|
1256
1456
|
/**
|
|
1257
1457
|
* Command line options via yargs
|
|
1258
1458
|
*/
|
|
1259
|
-
const options = {
|
|
1459
|
+
const options$1 = {
|
|
1260
1460
|
range: {
|
|
1261
1461
|
type: 'string',
|
|
1262
1462
|
alias: 'r',
|
|
1263
1463
|
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
1264
|
-
demandOption: true,
|
|
1265
1464
|
},
|
|
1266
1465
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1267
1466
|
openAIApiKey: {
|
|
@@ -1303,13 +1502,240 @@ const options = {
|
|
|
1303
1502
|
description: 'Ignored extensions',
|
|
1304
1503
|
},
|
|
1305
1504
|
};
|
|
1306
|
-
const builder = (yargs) => {
|
|
1307
|
-
return yargs.options(options);
|
|
1505
|
+
const builder$1 = (yargs) => {
|
|
1506
|
+
return yargs.options(options$1);
|
|
1308
1507
|
};
|
|
1309
1508
|
|
|
1310
1509
|
var changelog = {
|
|
1311
1510
|
command: 'changelog',
|
|
1312
1511
|
desc: 'Generate a changelog from a commit range',
|
|
1512
|
+
builder: builder$1,
|
|
1513
|
+
handler: handler$1,
|
|
1514
|
+
options: options$1,
|
|
1515
|
+
};
|
|
1516
|
+
|
|
1517
|
+
const handleProjectLevelConfig = async () => {
|
|
1518
|
+
const projectConfiguration = await prompts$1.select({
|
|
1519
|
+
message: 'select type project level configuration:',
|
|
1520
|
+
choices: [
|
|
1521
|
+
{
|
|
1522
|
+
name: '.coco.config.json',
|
|
1523
|
+
value: '.coco.config.json',
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
name: '.env',
|
|
1527
|
+
value: '.env',
|
|
1528
|
+
},
|
|
1529
|
+
],
|
|
1530
|
+
});
|
|
1531
|
+
let configFile = '.coco.config.json';
|
|
1532
|
+
if (projectConfiguration === '.env') {
|
|
1533
|
+
configFile = '.env';
|
|
1534
|
+
if (!fs.existsSync('.env')) {
|
|
1535
|
+
fs.writeFileSync('.env', '');
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return configFile;
|
|
1539
|
+
};
|
|
1540
|
+
const handleSystemLevelConfig = () => {
|
|
1541
|
+
return path.join(os.homedir(), '.gitconfig');
|
|
1542
|
+
};
|
|
1543
|
+
async function handler(argv) {
|
|
1544
|
+
const options = loadConfig(argv);
|
|
1545
|
+
const logger = new Logger(options);
|
|
1546
|
+
const level = await prompts$1.select({
|
|
1547
|
+
message: 'configure coco at the system or project level:',
|
|
1548
|
+
choices: [
|
|
1549
|
+
{
|
|
1550
|
+
name: 'system',
|
|
1551
|
+
value: 'system',
|
|
1552
|
+
description: 'add coco config to your global git config',
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
name: 'project',
|
|
1556
|
+
value: 'project',
|
|
1557
|
+
description: 'add coco config to existing git project',
|
|
1558
|
+
},
|
|
1559
|
+
],
|
|
1560
|
+
});
|
|
1561
|
+
let configFilePath = '';
|
|
1562
|
+
switch (level) {
|
|
1563
|
+
case 'system':
|
|
1564
|
+
configFilePath = await handleSystemLevelConfig();
|
|
1565
|
+
break;
|
|
1566
|
+
case 'project':
|
|
1567
|
+
configFilePath = await handleProjectLevelConfig();
|
|
1568
|
+
break;
|
|
1569
|
+
}
|
|
1570
|
+
// interactive v.s stdout mode
|
|
1571
|
+
const mode = (await prompts$1.select({
|
|
1572
|
+
message: 'select mode:',
|
|
1573
|
+
choices: [
|
|
1574
|
+
{
|
|
1575
|
+
name: 'interactive',
|
|
1576
|
+
value: 'interactive',
|
|
1577
|
+
description: 'interactive prompt for creating, reviewing, and committing',
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
name: 'stdout',
|
|
1581
|
+
value: 'stdout',
|
|
1582
|
+
description: 'print results to stdout',
|
|
1583
|
+
},
|
|
1584
|
+
],
|
|
1585
|
+
}));
|
|
1586
|
+
const apiKey = await prompts$1.password({
|
|
1587
|
+
message: `enter your OpenAI API key:`,
|
|
1588
|
+
validate(input) {
|
|
1589
|
+
return input.length > 0 ? true : 'API key cannot be empty';
|
|
1590
|
+
},
|
|
1591
|
+
});
|
|
1592
|
+
const tokenLimit = await prompts$1.input({
|
|
1593
|
+
message: 'maximum number of tokens for the commit message:',
|
|
1594
|
+
default: '500',
|
|
1595
|
+
});
|
|
1596
|
+
const defaultBranch = await prompts$1.input({
|
|
1597
|
+
message: 'default branch for the repository:',
|
|
1598
|
+
default: 'main',
|
|
1599
|
+
});
|
|
1600
|
+
const advOptions = await prompts$1.confirm({
|
|
1601
|
+
message: 'would you like to configure advanced options?',
|
|
1602
|
+
default: false,
|
|
1603
|
+
});
|
|
1604
|
+
const config = {
|
|
1605
|
+
openAIApiKey: '•••••••••••••••',
|
|
1606
|
+
tokenLimit: parseInt(tokenLimit),
|
|
1607
|
+
defaultBranch,
|
|
1608
|
+
mode,
|
|
1609
|
+
};
|
|
1610
|
+
/**
|
|
1611
|
+
* Prompt for advanced options
|
|
1612
|
+
*
|
|
1613
|
+
* e.g.
|
|
1614
|
+
* - temperature
|
|
1615
|
+
* - verbose logging
|
|
1616
|
+
* - ignored files
|
|
1617
|
+
* - ignored extensions
|
|
1618
|
+
* - commit message prompt
|
|
1619
|
+
*/
|
|
1620
|
+
if (advOptions) {
|
|
1621
|
+
const temperature = await prompts$1.input({
|
|
1622
|
+
message: 'temperature for the model:',
|
|
1623
|
+
default: '0.4',
|
|
1624
|
+
});
|
|
1625
|
+
config.temperature = parseFloat(temperature);
|
|
1626
|
+
config.verbose = await prompts$1.confirm({
|
|
1627
|
+
message: 'enable verbose logging:',
|
|
1628
|
+
default: false,
|
|
1629
|
+
});
|
|
1630
|
+
const promptForIgnores = await prompts$1.confirm({
|
|
1631
|
+
message: 'would you like to configure ignored files and extensions?',
|
|
1632
|
+
default: false,
|
|
1633
|
+
});
|
|
1634
|
+
if (promptForIgnores) {
|
|
1635
|
+
const ignoredFiles = await prompts$1.input({
|
|
1636
|
+
message: 'paths of files to be excluded when generating commit messages (comma-separated):',
|
|
1637
|
+
default: 'package-lock.json',
|
|
1638
|
+
});
|
|
1639
|
+
const ignoredExtensions = await prompts$1.input({
|
|
1640
|
+
message: 'file extensions to be excluded when generating commit messages (comma-separated):',
|
|
1641
|
+
default: '.map, .lock',
|
|
1642
|
+
});
|
|
1643
|
+
config.ignoredFiles =
|
|
1644
|
+
(ignoredFiles && ignoredFiles.split(',').map((file) => file.trim())) || [];
|
|
1645
|
+
config.ignoredExtensions =
|
|
1646
|
+
(ignoredExtensions && ignoredExtensions.split(',').map((ext) => ext.trim())) || [];
|
|
1647
|
+
}
|
|
1648
|
+
const promptForCommitPrompt = await prompts$1.confirm({
|
|
1649
|
+
message: 'would you like to configure the commit message prompt?',
|
|
1650
|
+
default: false,
|
|
1651
|
+
});
|
|
1652
|
+
if (promptForCommitPrompt) {
|
|
1653
|
+
const commitPrompt = await prompts$1.editor({
|
|
1654
|
+
message: 'modify default commit message prompt:',
|
|
1655
|
+
default: COMMIT_PROMPT.template,
|
|
1656
|
+
});
|
|
1657
|
+
config.prompt = commitPrompt;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
logResult('Config', JSON.stringify(config, null, 2));
|
|
1661
|
+
// add to config after logging, so that the API key is not logged
|
|
1662
|
+
config.openAIApiKey = apiKey;
|
|
1663
|
+
const isApproved = await prompts$1.confirm({
|
|
1664
|
+
message: 'look good? (hiding API key for security)',
|
|
1665
|
+
});
|
|
1666
|
+
if (isApproved) {
|
|
1667
|
+
if (configFilePath.endsWith('.gitconfig')) {
|
|
1668
|
+
await appendToGitConfig(configFilePath, config);
|
|
1669
|
+
}
|
|
1670
|
+
else if (configFilePath === '.env') {
|
|
1671
|
+
await appendToEnvFile(configFilePath, config);
|
|
1672
|
+
}
|
|
1673
|
+
else if (configFilePath === '.coco.config.json') {
|
|
1674
|
+
await appendToProjectConfig(configFilePath, config);
|
|
1675
|
+
}
|
|
1676
|
+
logger.log(`init successful! 🦾🤖🎉`, { color: 'green' });
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
logger.log('init cancelled.', { color: 'yellow' });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* Command line options via yargs
|
|
1685
|
+
*/
|
|
1686
|
+
const options = {
|
|
1687
|
+
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1688
|
+
openAIApiKey: {
|
|
1689
|
+
type: 'string',
|
|
1690
|
+
description: 'OpenAI API Key',
|
|
1691
|
+
conflicts: 'huggingFaceHubApiKey',
|
|
1692
|
+
},
|
|
1693
|
+
huggingFaceHubApiKey: {
|
|
1694
|
+
type: 'string',
|
|
1695
|
+
description: 'HuggingFace Hub API Key',
|
|
1696
|
+
conflicts: 'openAIApiKey',
|
|
1697
|
+
},
|
|
1698
|
+
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1699
|
+
prompt: {
|
|
1700
|
+
type: 'string',
|
|
1701
|
+
alias: 'p',
|
|
1702
|
+
description: 'Commit message prompt',
|
|
1703
|
+
},
|
|
1704
|
+
i: {
|
|
1705
|
+
type: 'boolean',
|
|
1706
|
+
alias: 'interactive',
|
|
1707
|
+
description: 'Toggle interactive mode',
|
|
1708
|
+
},
|
|
1709
|
+
s: {
|
|
1710
|
+
type: 'boolean',
|
|
1711
|
+
description: 'Automatically commit staged changes with generated commit message',
|
|
1712
|
+
default: false,
|
|
1713
|
+
},
|
|
1714
|
+
e: {
|
|
1715
|
+
type: 'boolean',
|
|
1716
|
+
alias: 'edit',
|
|
1717
|
+
description: 'Open commit message in editor before proceeding',
|
|
1718
|
+
},
|
|
1719
|
+
summarizePrompt: {
|
|
1720
|
+
type: 'string',
|
|
1721
|
+
description: 'Large file summary prompt',
|
|
1722
|
+
},
|
|
1723
|
+
ignoredFiles: {
|
|
1724
|
+
type: 'array',
|
|
1725
|
+
description: 'Ignored files',
|
|
1726
|
+
},
|
|
1727
|
+
ignoredExtensions: {
|
|
1728
|
+
type: 'array',
|
|
1729
|
+
description: 'Ignored extensions',
|
|
1730
|
+
},
|
|
1731
|
+
};
|
|
1732
|
+
const builder = (yargs) => {
|
|
1733
|
+
return yargs.options(options);
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
var init = {
|
|
1737
|
+
command: 'init',
|
|
1738
|
+
desc: 'Setup coco for a new project or system',
|
|
1313
1739
|
builder,
|
|
1314
1740
|
handler,
|
|
1315
1741
|
options,
|
|
@@ -1328,5 +1754,10 @@ commit.builder, commit.handler)
|
|
|
1328
1754
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1329
1755
|
// @ts-ignore
|
|
1330
1756
|
changelog.builder, changelog.handler)
|
|
1757
|
+
.command(init.command, init.desc,
|
|
1758
|
+
// TODO: fix type on builder
|
|
1759
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1760
|
+
// @ts-ignore
|
|
1761
|
+
init.builder, init.handler)
|
|
1331
1762
|
.demandCommand()
|
|
1332
1763
|
.help().argv;
|