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.esm.mjs
CHANGED
|
@@ -17,10 +17,12 @@ import * as path from 'path';
|
|
|
17
17
|
import path__default from 'path';
|
|
18
18
|
import { minimatch } from 'minimatch';
|
|
19
19
|
import * as fs from 'fs';
|
|
20
|
+
import fs__default from 'fs';
|
|
21
|
+
import { confirm, editor, select, password, input } from '@inquirer/prompts';
|
|
20
22
|
import * as os from 'os';
|
|
23
|
+
import os__default from 'os';
|
|
21
24
|
import * as ini from 'ini';
|
|
22
25
|
import { simpleGit } from 'simple-git';
|
|
23
|
-
import { editor, select } from '@inquirer/prompts';
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Extract the path from a file path string.
|
|
@@ -583,6 +585,82 @@ function removeUndefined(obj) {
|
|
|
583
585
|
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
584
586
|
}
|
|
585
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Default Config
|
|
590
|
+
*
|
|
591
|
+
* @type {Config}
|
|
592
|
+
*/
|
|
593
|
+
const DEFAULT_CONFIG = {
|
|
594
|
+
model: 'openai/gpt-4',
|
|
595
|
+
verbose: false,
|
|
596
|
+
tokenLimit: 1024,
|
|
597
|
+
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
598
|
+
temperature: 0.4,
|
|
599
|
+
mode: 'stdout',
|
|
600
|
+
ignoredFiles: ['package-lock.json'],
|
|
601
|
+
ignoredExtensions: ['.map', '.lock'],
|
|
602
|
+
defaultBranch: 'main',
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* Config keys
|
|
606
|
+
*
|
|
607
|
+
* @type {string[]}
|
|
608
|
+
*/
|
|
609
|
+
const CONFIG_KEYS = Object.keys({
|
|
610
|
+
...DEFAULT_CONFIG,
|
|
611
|
+
huggingFaceHubApiKey: '',
|
|
612
|
+
openAIApiKey: '',
|
|
613
|
+
prompt: '',
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
async function updateFileSection(filePath, startComment, endComment, getNewContent, confirmUpdate = true) {
|
|
617
|
+
const lines = fs__default.existsSync(filePath) ? fs__default.readFileSync(filePath, 'utf-8').split(/\r?\n/) : [];
|
|
618
|
+
const newLines = [];
|
|
619
|
+
let foundSection = false;
|
|
620
|
+
for (let i = 0; i < lines.length; i++) {
|
|
621
|
+
if (lines[i].trim() === startComment) {
|
|
622
|
+
foundSection = true;
|
|
623
|
+
if (confirmUpdate) {
|
|
624
|
+
const confirmOverwrite = await confirm({
|
|
625
|
+
message: `A section already exists in ${filePath}, do you want to override it?`,
|
|
626
|
+
default: false,
|
|
627
|
+
});
|
|
628
|
+
if (!confirmOverwrite) {
|
|
629
|
+
// keep all lines until the end comment
|
|
630
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
631
|
+
newLines.push(lines[i]);
|
|
632
|
+
i++;
|
|
633
|
+
}
|
|
634
|
+
newLines.push(endComment);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
newLines.push(startComment);
|
|
639
|
+
// Insert the new content
|
|
640
|
+
const newContent = await getNewContent();
|
|
641
|
+
newLines.push(newContent);
|
|
642
|
+
// Skip the existing content of the section
|
|
643
|
+
while (i < lines.length && lines[i].trim() !== endComment) {
|
|
644
|
+
i++;
|
|
645
|
+
}
|
|
646
|
+
newLines.push(endComment);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (!foundSection || lines[i].trim() !== endComment) {
|
|
650
|
+
newLines.push(lines[i]);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// If section wasn't found, append it at the end
|
|
654
|
+
if (!foundSection) {
|
|
655
|
+
newLines.push('\n' + startComment);
|
|
656
|
+
const newContent = await getNewContent();
|
|
657
|
+
newLines.push(newContent);
|
|
658
|
+
newLines.push(endComment);
|
|
659
|
+
}
|
|
660
|
+
// Write the updated contents back to the file
|
|
661
|
+
fs__default.writeFileSync(filePath, newLines.join('\n'));
|
|
662
|
+
}
|
|
663
|
+
|
|
586
664
|
/**
|
|
587
665
|
* Load environment variables
|
|
588
666
|
*
|
|
@@ -590,26 +668,65 @@ function removeUndefined(obj) {
|
|
|
590
668
|
* @returns {Config} Updated config
|
|
591
669
|
**/
|
|
592
670
|
function loadEnvConfig(config) {
|
|
593
|
-
const envConfig = {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
671
|
+
const envConfig = {};
|
|
672
|
+
CONFIG_KEYS.forEach((key) => {
|
|
673
|
+
const envVarName = toEnvVarName(key);
|
|
674
|
+
const envValue = parseEnvValue(key, process.env[envVarName]);
|
|
675
|
+
if (envValue === undefined)
|
|
676
|
+
return;
|
|
677
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
678
|
+
// @ts-ignore
|
|
679
|
+
envConfig[key] = envValue;
|
|
680
|
+
});
|
|
681
|
+
return { ...config, ...removeUndefined(envConfig) };
|
|
682
|
+
}
|
|
683
|
+
function parseEnvValue(key, value) {
|
|
684
|
+
if (value === undefined) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
else if (key === 'tokenLimit' && typeof value === 'string') {
|
|
688
|
+
return parseInt(value);
|
|
689
|
+
}
|
|
690
|
+
else if ((key === 'ignoredFiles' || key === 'ignoredExtensions') &&
|
|
691
|
+
typeof value === 'string' &&
|
|
692
|
+
value.includes(',')) {
|
|
693
|
+
return value.split(',');
|
|
694
|
+
}
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
function toEnvVarName(key) {
|
|
698
|
+
switch (key) {
|
|
699
|
+
case 'openAIApiKey':
|
|
700
|
+
return 'OPENAI_API_KEY';
|
|
701
|
+
case 'huggingFaceHubApiKey':
|
|
702
|
+
return 'HUGGINGFACE_HUB_API_KEY';
|
|
703
|
+
default:
|
|
704
|
+
return 'COCO_' + key.replace(/([A-Z])/g, '_$1').toUpperCase();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function formatEnvValue(value) {
|
|
708
|
+
if (typeof value === 'number') {
|
|
709
|
+
return `${value}`;
|
|
710
|
+
}
|
|
711
|
+
else if (Array.isArray(value)) {
|
|
712
|
+
return `${value.join(',')}`;
|
|
713
|
+
}
|
|
714
|
+
else if (typeof value === 'string') {
|
|
715
|
+
// Escape newlines and tabs in strings
|
|
716
|
+
return `${value.replace(/\n/g, '\\n').replace(/\t/g, '\\t')}`;
|
|
717
|
+
}
|
|
718
|
+
return `${value}`;
|
|
612
719
|
}
|
|
720
|
+
const appendToEnvFile = async (filePath, config) => {
|
|
721
|
+
const startComment = '# -- Start coco config --';
|
|
722
|
+
const endComment = '# -- End coco config --';
|
|
723
|
+
const getNewContent = async () => {
|
|
724
|
+
return Object.entries(config)
|
|
725
|
+
.map(([key, value]) => `${toEnvVarName(key)}=${formatEnvValue(value)}`)
|
|
726
|
+
.join('\n');
|
|
727
|
+
};
|
|
728
|
+
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
729
|
+
};
|
|
613
730
|
|
|
614
731
|
/**
|
|
615
732
|
* Load git profile config (from ~/.gitconfig)
|
|
@@ -634,10 +751,43 @@ function loadGitConfig(config) {
|
|
|
634
751
|
summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
|
|
635
752
|
ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
|
|
636
753
|
ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
|
|
754
|
+
defaultBranch: gitConfigParsed.coco?.defaultBranch || config.defaultBranch,
|
|
637
755
|
};
|
|
638
756
|
}
|
|
639
757
|
return config;
|
|
640
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Appends the provided configuration to a git config file.
|
|
761
|
+
*
|
|
762
|
+
* @param filePath - The path to the .gitconfig
|
|
763
|
+
* @param config - The configuration object to append.
|
|
764
|
+
*/
|
|
765
|
+
const appendToGitConfig = async (filePath, config) => {
|
|
766
|
+
if (!fs.existsSync(filePath)) {
|
|
767
|
+
throw new Error(`File ${filePath} does not exist.`);
|
|
768
|
+
}
|
|
769
|
+
const startComment = '# -- Start coco config --';
|
|
770
|
+
const endComment = '# -- End coco config --';
|
|
771
|
+
const header = '[coco]';
|
|
772
|
+
// Function to generate new content for the coco section
|
|
773
|
+
const getNewContent = async () => {
|
|
774
|
+
const contentLines = [header];
|
|
775
|
+
for (const key in config) {
|
|
776
|
+
// check if string has new lines, if so, wrap in quotes
|
|
777
|
+
if (typeof config[key] === 'string') {
|
|
778
|
+
const value = config[key];
|
|
779
|
+
if (value.includes('\n')) {
|
|
780
|
+
contentLines.push(`\t${key} = ${JSON.stringify(value)}`);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
contentLines.push(`\t${key} = ${config[key]}`);
|
|
785
|
+
}
|
|
786
|
+
return contentLines.join('\n');
|
|
787
|
+
};
|
|
788
|
+
// Use the updateFileSection utility
|
|
789
|
+
await updateFileSection(filePath, startComment, endComment, getNewContent);
|
|
790
|
+
};
|
|
641
791
|
|
|
642
792
|
/**
|
|
643
793
|
* Load .gitignore in project root
|
|
@@ -679,12 +829,20 @@ function loadIgnore(config) {
|
|
|
679
829
|
* @returns {Config} Updated config
|
|
680
830
|
**/
|
|
681
831
|
function loadProjectConfig(config) {
|
|
832
|
+
// TODO: Add validation based of JSON schema?
|
|
833
|
+
// @see https://github.com/acornejo/jjv
|
|
682
834
|
if (fs.existsSync('.coco.config.json')) {
|
|
683
835
|
const projectConfig = JSON.parse(fs.readFileSync('.coco.config.json', 'utf-8'));
|
|
684
836
|
config = { ...config, ...projectConfig };
|
|
685
837
|
}
|
|
686
838
|
return config;
|
|
687
839
|
}
|
|
840
|
+
const appendToProjectConfig = (filePath, config) => {
|
|
841
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
842
|
+
$schema: 'https://git-co.co/schema.json',
|
|
843
|
+
...config,
|
|
844
|
+
}, null, 2));
|
|
845
|
+
};
|
|
688
846
|
|
|
689
847
|
/**
|
|
690
848
|
* Load XDG config
|
|
@@ -702,21 +860,6 @@ function loadXDGConfig(config) {
|
|
|
702
860
|
return config;
|
|
703
861
|
}
|
|
704
862
|
|
|
705
|
-
/**
|
|
706
|
-
* Default Config
|
|
707
|
-
*
|
|
708
|
-
* @type {Config}
|
|
709
|
-
*/
|
|
710
|
-
const DEFAULT_CONFIG = {
|
|
711
|
-
model: 'openai/gpt-4',
|
|
712
|
-
verbose: false,
|
|
713
|
-
tokenLimit: 1024,
|
|
714
|
-
summarizePrompt: SUMMARIZE_PROMPT.template,
|
|
715
|
-
temperature: 0.4,
|
|
716
|
-
mode: 'stdout',
|
|
717
|
-
ignoredFiles: ['package-lock.json'],
|
|
718
|
-
ignoredExtensions: ['.map', '.lock'],
|
|
719
|
-
};
|
|
720
863
|
/**
|
|
721
864
|
* Load application config
|
|
722
865
|
*
|
|
@@ -836,8 +979,8 @@ const isInteractive = (argv) => {
|
|
|
836
979
|
};
|
|
837
980
|
const SEPERATOR = chalk.blue('----------------');
|
|
838
981
|
|
|
839
|
-
function logResult(result) {
|
|
840
|
-
console.log(`\n${chalk.bgBlue(chalk.bold(
|
|
982
|
+
function logResult(label, result) {
|
|
983
|
+
console.log(`\n${chalk.bgBlue(chalk.bold(`Proposed ${label}:`))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
|
|
841
984
|
}
|
|
842
985
|
|
|
843
986
|
async function editResult(result, options) {
|
|
@@ -938,7 +1081,7 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
|
|
|
938
1081
|
})
|
|
939
1082
|
.stopTimer();
|
|
940
1083
|
if (options?.interactive) {
|
|
941
|
-
logResult(result);
|
|
1084
|
+
logResult(label, result);
|
|
942
1085
|
const reviewAnswer = await getUserReviewDecision();
|
|
943
1086
|
if (reviewAnswer === 'cancel') {
|
|
944
1087
|
process.exit(0);
|
|
@@ -1016,9 +1159,21 @@ const handleResult = async (result, { mode, git }) => {
|
|
|
1016
1159
|
process.exit(0);
|
|
1017
1160
|
};
|
|
1018
1161
|
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1162
|
+
const getRepo = () => {
|
|
1163
|
+
let git;
|
|
1164
|
+
try {
|
|
1165
|
+
git = simpleGit();
|
|
1166
|
+
}
|
|
1167
|
+
catch (e) {
|
|
1168
|
+
console.log('Error initializing git repo', e);
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
return git;
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
async function handler$2(argv) {
|
|
1175
|
+
const tokenizer = getTokenizer();
|
|
1176
|
+
const git = getRepo();
|
|
1022
1177
|
const options = loadConfig(argv);
|
|
1023
1178
|
const logger = new Logger(options);
|
|
1024
1179
|
const key = getApiKeyForModel(options.model, options);
|
|
@@ -1032,14 +1187,14 @@ async function handler$1(argv) {
|
|
|
1032
1187
|
});
|
|
1033
1188
|
const INTERACTIVE = isInteractive(options);
|
|
1034
1189
|
async function factory() {
|
|
1035
|
-
const changes = await getChanges({ git
|
|
1190
|
+
const changes = await getChanges({ git });
|
|
1036
1191
|
return changes.staged;
|
|
1037
1192
|
}
|
|
1038
1193
|
async function parser(changes) {
|
|
1039
1194
|
return await fileChangeParser({
|
|
1040
1195
|
changes,
|
|
1041
1196
|
commit: '--staged',
|
|
1042
|
-
options: { tokenizer, git
|
|
1197
|
+
options: { tokenizer, git, model, logger },
|
|
1043
1198
|
});
|
|
1044
1199
|
}
|
|
1045
1200
|
const commitMsg = await generateAndReviewLoop({
|
|
@@ -1058,7 +1213,7 @@ async function handler$1(argv) {
|
|
|
1058
1213
|
});
|
|
1059
1214
|
},
|
|
1060
1215
|
noResult: async () => {
|
|
1061
|
-
await noResult({ git
|
|
1216
|
+
await noResult({ git, logger });
|
|
1062
1217
|
process.exit(0);
|
|
1063
1218
|
},
|
|
1064
1219
|
options: {
|
|
@@ -1071,14 +1226,14 @@ async function handler$1(argv) {
|
|
|
1071
1226
|
const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
|
|
1072
1227
|
handleResult(commitMsg, {
|
|
1073
1228
|
mode: MODE,
|
|
1074
|
-
git
|
|
1229
|
+
git,
|
|
1075
1230
|
});
|
|
1076
1231
|
}
|
|
1077
1232
|
|
|
1078
1233
|
/**
|
|
1079
1234
|
* Command line options via yargs
|
|
1080
1235
|
*/
|
|
1081
|
-
const options$
|
|
1236
|
+
const options$2 = {
|
|
1082
1237
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1083
1238
|
openAIApiKey: {
|
|
1084
1239
|
type: 'string',
|
|
@@ -1124,16 +1279,16 @@ const options$1 = {
|
|
|
1124
1279
|
description: 'Ignored extensions',
|
|
1125
1280
|
},
|
|
1126
1281
|
};
|
|
1127
|
-
const builder$
|
|
1128
|
-
return yargs.options(options$
|
|
1282
|
+
const builder$2 = (yargs) => {
|
|
1283
|
+
return yargs.options(options$2);
|
|
1129
1284
|
};
|
|
1130
1285
|
|
|
1131
1286
|
var commit = {
|
|
1132
1287
|
command: 'commit',
|
|
1133
1288
|
desc: 'Generate commit message',
|
|
1134
|
-
builder: builder$
|
|
1135
|
-
handler: handler$
|
|
1136
|
-
options: options$
|
|
1289
|
+
builder: builder$2,
|
|
1290
|
+
handler: handler$2,
|
|
1291
|
+
options: options$2,
|
|
1137
1292
|
};
|
|
1138
1293
|
|
|
1139
1294
|
const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
|
|
@@ -1152,15 +1307,9 @@ const CHANGELOG_PROMPT = new PromptTemplate({
|
|
|
1152
1307
|
|
|
1153
1308
|
async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
1154
1309
|
try {
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
'--pretty=format:%s',
|
|
1159
|
-
// Include '--no-merges' here if you want to exclude merge commits.
|
|
1160
|
-
noMerges ? '--no-merges' : null,
|
|
1161
|
-
].filter(Boolean)); // filter(Boolean) removes any null values from the array
|
|
1162
|
-
const messages = output.split('\n').filter(Boolean);
|
|
1163
|
-
return messages;
|
|
1310
|
+
const logOptions = { from: `${from}^1`, to, '--no-merges': noMerges };
|
|
1311
|
+
const commitLog = await git.log(logOptions);
|
|
1312
|
+
return commitLog.all.map(({ message, date, body, author_name }) => `[${date}] ${message}\n${body}\n - ${author_name}`);
|
|
1164
1313
|
}
|
|
1165
1314
|
catch (error) {
|
|
1166
1315
|
// If there's an error, handle it appropriately
|
|
@@ -1169,10 +1318,56 @@ async function getCommitLogRange(from, to, { noMerges, git }) {
|
|
|
1169
1318
|
}
|
|
1170
1319
|
}
|
|
1171
1320
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1321
|
+
async function getCurrentBranchName({ git }) {
|
|
1322
|
+
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function getCommitLogCurrentBranch({ git, logger, comparisonBranch = 'main', comparisonRemote = 'origin', }) {
|
|
1326
|
+
try {
|
|
1327
|
+
// Get the current branch name
|
|
1328
|
+
const branch = await getCurrentBranchName({ git });
|
|
1329
|
+
// Check if the current branch has any commits
|
|
1330
|
+
const hasCommits = (await git.raw(['rev-list', '--count', branch])) !== '0';
|
|
1331
|
+
if (!hasCommits) {
|
|
1332
|
+
logger?.log('No commits on the current branch.');
|
|
1333
|
+
return [];
|
|
1334
|
+
}
|
|
1335
|
+
// Get the list of commits that are unique to the current branch
|
|
1336
|
+
let uniqueCommits;
|
|
1337
|
+
if (comparisonBranch === branch) {
|
|
1338
|
+
// If the comparison branch is the same as the current branch, we compare against the remote.
|
|
1339
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonRemote}/${comparisonBranch}..${branch}`]))
|
|
1340
|
+
.split('\n')
|
|
1341
|
+
.filter(Boolean)
|
|
1342
|
+
.reverse();
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
// Your existing code for different branches
|
|
1346
|
+
uniqueCommits = (await git.raw(['rev-list', `${comparisonBranch}..${branch}`]))
|
|
1347
|
+
.split('\n')
|
|
1348
|
+
.filter(Boolean)
|
|
1349
|
+
.reverse();
|
|
1350
|
+
}
|
|
1351
|
+
logger?.verbose(`Found ${uniqueCommits.length} unique commits on "${branch}"`, { color: 'blue' });
|
|
1352
|
+
const firstCommit = uniqueCommits[0];
|
|
1353
|
+
const lastCommit = uniqueCommits[uniqueCommits.length - 1];
|
|
1354
|
+
if (!firstCommit || !lastCommit) {
|
|
1355
|
+
logger?.log('Unable to determine first and last commit on the current branch', { color: 'yellow' });
|
|
1356
|
+
return [];
|
|
1357
|
+
}
|
|
1358
|
+
// Retrieve commit log with messages
|
|
1359
|
+
return await getCommitLogRange(firstCommit, lastCommit, { git, noMerges: true });
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
logger?.log('Encountered an error getting commit log from current branch', { color: 'red' });
|
|
1363
|
+
}
|
|
1364
|
+
return [];
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
async function handler$1(argv) {
|
|
1174
1368
|
const options = loadConfig(argv);
|
|
1175
1369
|
const logger = new Logger(options);
|
|
1370
|
+
const git = getRepo();
|
|
1176
1371
|
const key = getApiKeyForModel(options.model, options);
|
|
1177
1372
|
if (!key) {
|
|
1178
1373
|
logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
|
|
@@ -1183,14 +1378,17 @@ async function handler(argv) {
|
|
|
1183
1378
|
maxConcurrency: 10,
|
|
1184
1379
|
});
|
|
1185
1380
|
const INTERACTIVE = isInteractive(options);
|
|
1186
|
-
const [from, to] = options.range?.split(':');
|
|
1187
|
-
if (!from || !to) {
|
|
1188
|
-
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
1189
|
-
process.exit(1);
|
|
1190
|
-
}
|
|
1191
1381
|
async function factory() {
|
|
1192
|
-
|
|
1193
|
-
|
|
1382
|
+
if (options.range) {
|
|
1383
|
+
const [from, to] = options.range?.split(':');
|
|
1384
|
+
if (!from || !to) {
|
|
1385
|
+
logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
|
|
1386
|
+
process.exit(1);
|
|
1387
|
+
}
|
|
1388
|
+
return await getCommitLogRange(from, to, { git, noMerges: true });
|
|
1389
|
+
}
|
|
1390
|
+
logger.verbose(`No range provided. Defaulting to current branch`, { color: 'yellow' });
|
|
1391
|
+
return await getCommitLogCurrentBranch({ git, logger });
|
|
1194
1392
|
}
|
|
1195
1393
|
async function parser(messages) {
|
|
1196
1394
|
const result = messages.join('\n');
|
|
@@ -1213,7 +1411,11 @@ async function handler(argv) {
|
|
|
1213
1411
|
});
|
|
1214
1412
|
},
|
|
1215
1413
|
noResult: async () => {
|
|
1216
|
-
|
|
1414
|
+
if (options.range) {
|
|
1415
|
+
logger.log(`No commits found in the provided range.`, { color: 'red' });
|
|
1416
|
+
process.exit(0);
|
|
1417
|
+
}
|
|
1418
|
+
logger.log(`No commits found in the current branch.`, { color: 'red' });
|
|
1217
1419
|
process.exit(0);
|
|
1218
1420
|
},
|
|
1219
1421
|
options: {
|
|
@@ -1233,12 +1435,11 @@ async function handler(argv) {
|
|
|
1233
1435
|
/**
|
|
1234
1436
|
* Command line options via yargs
|
|
1235
1437
|
*/
|
|
1236
|
-
const options = {
|
|
1438
|
+
const options$1 = {
|
|
1237
1439
|
range: {
|
|
1238
1440
|
type: 'string',
|
|
1239
1441
|
alias: 'r',
|
|
1240
1442
|
description: 'Commit range e.g `HEAD~2:HEAD`',
|
|
1241
|
-
demandOption: true,
|
|
1242
1443
|
},
|
|
1243
1444
|
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1244
1445
|
openAIApiKey: {
|
|
@@ -1280,13 +1481,240 @@ const options = {
|
|
|
1280
1481
|
description: 'Ignored extensions',
|
|
1281
1482
|
},
|
|
1282
1483
|
};
|
|
1283
|
-
const builder = (yargs) => {
|
|
1284
|
-
return yargs.options(options);
|
|
1484
|
+
const builder$1 = (yargs) => {
|
|
1485
|
+
return yargs.options(options$1);
|
|
1285
1486
|
};
|
|
1286
1487
|
|
|
1287
1488
|
var changelog = {
|
|
1288
1489
|
command: 'changelog',
|
|
1289
1490
|
desc: 'Generate a changelog from a commit range',
|
|
1491
|
+
builder: builder$1,
|
|
1492
|
+
handler: handler$1,
|
|
1493
|
+
options: options$1,
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
const handleProjectLevelConfig = async () => {
|
|
1497
|
+
const projectConfiguration = await select({
|
|
1498
|
+
message: 'select type project level configuration:',
|
|
1499
|
+
choices: [
|
|
1500
|
+
{
|
|
1501
|
+
name: '.coco.config.json',
|
|
1502
|
+
value: '.coco.config.json',
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
name: '.env',
|
|
1506
|
+
value: '.env',
|
|
1507
|
+
},
|
|
1508
|
+
],
|
|
1509
|
+
});
|
|
1510
|
+
let configFile = '.coco.config.json';
|
|
1511
|
+
if (projectConfiguration === '.env') {
|
|
1512
|
+
configFile = '.env';
|
|
1513
|
+
if (!fs__default.existsSync('.env')) {
|
|
1514
|
+
fs__default.writeFileSync('.env', '');
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return configFile;
|
|
1518
|
+
};
|
|
1519
|
+
const handleSystemLevelConfig = () => {
|
|
1520
|
+
return path__default.join(os__default.homedir(), '.gitconfig');
|
|
1521
|
+
};
|
|
1522
|
+
async function handler(argv) {
|
|
1523
|
+
const options = loadConfig(argv);
|
|
1524
|
+
const logger = new Logger(options);
|
|
1525
|
+
const level = await select({
|
|
1526
|
+
message: 'configure coco at the system or project level:',
|
|
1527
|
+
choices: [
|
|
1528
|
+
{
|
|
1529
|
+
name: 'system',
|
|
1530
|
+
value: 'system',
|
|
1531
|
+
description: 'add coco config to your global git config',
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
name: 'project',
|
|
1535
|
+
value: 'project',
|
|
1536
|
+
description: 'add coco config to existing git project',
|
|
1537
|
+
},
|
|
1538
|
+
],
|
|
1539
|
+
});
|
|
1540
|
+
let configFilePath = '';
|
|
1541
|
+
switch (level) {
|
|
1542
|
+
case 'system':
|
|
1543
|
+
configFilePath = await handleSystemLevelConfig();
|
|
1544
|
+
break;
|
|
1545
|
+
case 'project':
|
|
1546
|
+
configFilePath = await handleProjectLevelConfig();
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
// interactive v.s stdout mode
|
|
1550
|
+
const mode = (await select({
|
|
1551
|
+
message: 'select mode:',
|
|
1552
|
+
choices: [
|
|
1553
|
+
{
|
|
1554
|
+
name: 'interactive',
|
|
1555
|
+
value: 'interactive',
|
|
1556
|
+
description: 'interactive prompt for creating, reviewing, and committing',
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
name: 'stdout',
|
|
1560
|
+
value: 'stdout',
|
|
1561
|
+
description: 'print results to stdout',
|
|
1562
|
+
},
|
|
1563
|
+
],
|
|
1564
|
+
}));
|
|
1565
|
+
const apiKey = await password({
|
|
1566
|
+
message: `enter your OpenAI API key:`,
|
|
1567
|
+
validate(input) {
|
|
1568
|
+
return input.length > 0 ? true : 'API key cannot be empty';
|
|
1569
|
+
},
|
|
1570
|
+
});
|
|
1571
|
+
const tokenLimit = await input({
|
|
1572
|
+
message: 'maximum number of tokens for the commit message:',
|
|
1573
|
+
default: '500',
|
|
1574
|
+
});
|
|
1575
|
+
const defaultBranch = await input({
|
|
1576
|
+
message: 'default branch for the repository:',
|
|
1577
|
+
default: 'main',
|
|
1578
|
+
});
|
|
1579
|
+
const advOptions = await confirm({
|
|
1580
|
+
message: 'would you like to configure advanced options?',
|
|
1581
|
+
default: false,
|
|
1582
|
+
});
|
|
1583
|
+
const config = {
|
|
1584
|
+
openAIApiKey: '•••••••••••••••',
|
|
1585
|
+
tokenLimit: parseInt(tokenLimit),
|
|
1586
|
+
defaultBranch,
|
|
1587
|
+
mode,
|
|
1588
|
+
};
|
|
1589
|
+
/**
|
|
1590
|
+
* Prompt for advanced options
|
|
1591
|
+
*
|
|
1592
|
+
* e.g.
|
|
1593
|
+
* - temperature
|
|
1594
|
+
* - verbose logging
|
|
1595
|
+
* - ignored files
|
|
1596
|
+
* - ignored extensions
|
|
1597
|
+
* - commit message prompt
|
|
1598
|
+
*/
|
|
1599
|
+
if (advOptions) {
|
|
1600
|
+
const temperature = await input({
|
|
1601
|
+
message: 'temperature for the model:',
|
|
1602
|
+
default: '0.4',
|
|
1603
|
+
});
|
|
1604
|
+
config.temperature = parseFloat(temperature);
|
|
1605
|
+
config.verbose = await confirm({
|
|
1606
|
+
message: 'enable verbose logging:',
|
|
1607
|
+
default: false,
|
|
1608
|
+
});
|
|
1609
|
+
const promptForIgnores = await confirm({
|
|
1610
|
+
message: 'would you like to configure ignored files and extensions?',
|
|
1611
|
+
default: false,
|
|
1612
|
+
});
|
|
1613
|
+
if (promptForIgnores) {
|
|
1614
|
+
const ignoredFiles = await input({
|
|
1615
|
+
message: 'paths of files to be excluded when generating commit messages (comma-separated):',
|
|
1616
|
+
default: 'package-lock.json',
|
|
1617
|
+
});
|
|
1618
|
+
const ignoredExtensions = await input({
|
|
1619
|
+
message: 'file extensions to be excluded when generating commit messages (comma-separated):',
|
|
1620
|
+
default: '.map, .lock',
|
|
1621
|
+
});
|
|
1622
|
+
config.ignoredFiles =
|
|
1623
|
+
(ignoredFiles && ignoredFiles.split(',').map((file) => file.trim())) || [];
|
|
1624
|
+
config.ignoredExtensions =
|
|
1625
|
+
(ignoredExtensions && ignoredExtensions.split(',').map((ext) => ext.trim())) || [];
|
|
1626
|
+
}
|
|
1627
|
+
const promptForCommitPrompt = await confirm({
|
|
1628
|
+
message: 'would you like to configure the commit message prompt?',
|
|
1629
|
+
default: false,
|
|
1630
|
+
});
|
|
1631
|
+
if (promptForCommitPrompt) {
|
|
1632
|
+
const commitPrompt = await editor({
|
|
1633
|
+
message: 'modify default commit message prompt:',
|
|
1634
|
+
default: COMMIT_PROMPT.template,
|
|
1635
|
+
});
|
|
1636
|
+
config.prompt = commitPrompt;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
logResult('Config', JSON.stringify(config, null, 2));
|
|
1640
|
+
// add to config after logging, so that the API key is not logged
|
|
1641
|
+
config.openAIApiKey = apiKey;
|
|
1642
|
+
const isApproved = await confirm({
|
|
1643
|
+
message: 'look good? (hiding API key for security)',
|
|
1644
|
+
});
|
|
1645
|
+
if (isApproved) {
|
|
1646
|
+
if (configFilePath.endsWith('.gitconfig')) {
|
|
1647
|
+
await appendToGitConfig(configFilePath, config);
|
|
1648
|
+
}
|
|
1649
|
+
else if (configFilePath === '.env') {
|
|
1650
|
+
await appendToEnvFile(configFilePath, config);
|
|
1651
|
+
}
|
|
1652
|
+
else if (configFilePath === '.coco.config.json') {
|
|
1653
|
+
await appendToProjectConfig(configFilePath, config);
|
|
1654
|
+
}
|
|
1655
|
+
logger.log(`init successful! 🦾🤖🎉`, { color: 'green' });
|
|
1656
|
+
}
|
|
1657
|
+
else {
|
|
1658
|
+
logger.log('init cancelled.', { color: 'yellow' });
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Command line options via yargs
|
|
1664
|
+
*/
|
|
1665
|
+
const options = {
|
|
1666
|
+
model: { type: 'string', description: 'LLM/Model-Name' },
|
|
1667
|
+
openAIApiKey: {
|
|
1668
|
+
type: 'string',
|
|
1669
|
+
description: 'OpenAI API Key',
|
|
1670
|
+
conflicts: 'huggingFaceHubApiKey',
|
|
1671
|
+
},
|
|
1672
|
+
huggingFaceHubApiKey: {
|
|
1673
|
+
type: 'string',
|
|
1674
|
+
description: 'HuggingFace Hub API Key',
|
|
1675
|
+
conflicts: 'openAIApiKey',
|
|
1676
|
+
},
|
|
1677
|
+
tokenLimit: { type: 'number', description: 'Token limit' },
|
|
1678
|
+
prompt: {
|
|
1679
|
+
type: 'string',
|
|
1680
|
+
alias: 'p',
|
|
1681
|
+
description: 'Commit message prompt',
|
|
1682
|
+
},
|
|
1683
|
+
i: {
|
|
1684
|
+
type: 'boolean',
|
|
1685
|
+
alias: 'interactive',
|
|
1686
|
+
description: 'Toggle interactive mode',
|
|
1687
|
+
},
|
|
1688
|
+
s: {
|
|
1689
|
+
type: 'boolean',
|
|
1690
|
+
description: 'Automatically commit staged changes with generated commit message',
|
|
1691
|
+
default: false,
|
|
1692
|
+
},
|
|
1693
|
+
e: {
|
|
1694
|
+
type: 'boolean',
|
|
1695
|
+
alias: 'edit',
|
|
1696
|
+
description: 'Open commit message in editor before proceeding',
|
|
1697
|
+
},
|
|
1698
|
+
summarizePrompt: {
|
|
1699
|
+
type: 'string',
|
|
1700
|
+
description: 'Large file summary prompt',
|
|
1701
|
+
},
|
|
1702
|
+
ignoredFiles: {
|
|
1703
|
+
type: 'array',
|
|
1704
|
+
description: 'Ignored files',
|
|
1705
|
+
},
|
|
1706
|
+
ignoredExtensions: {
|
|
1707
|
+
type: 'array',
|
|
1708
|
+
description: 'Ignored extensions',
|
|
1709
|
+
},
|
|
1710
|
+
};
|
|
1711
|
+
const builder = (yargs) => {
|
|
1712
|
+
return yargs.options(options);
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
var init = {
|
|
1716
|
+
command: 'init',
|
|
1717
|
+
desc: 'Setup coco for a new project or system',
|
|
1290
1718
|
builder,
|
|
1291
1719
|
handler,
|
|
1292
1720
|
options,
|
|
@@ -1305,6 +1733,11 @@ commit.builder, commit.handler)
|
|
|
1305
1733
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1306
1734
|
// @ts-ignore
|
|
1307
1735
|
changelog.builder, changelog.handler)
|
|
1736
|
+
.command(init.command, init.desc,
|
|
1737
|
+
// TODO: fix type on builder
|
|
1738
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1739
|
+
// @ts-ignore
|
|
1740
|
+
init.builder, init.handler)
|
|
1308
1741
|
.demandCommand()
|
|
1309
1742
|
.help().argv;
|
|
1310
1743
|
//# sourceMappingURL=index.esm.mjs.map
|