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/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
- model: process.env.COCO_MODEL || undefined,
618
- openAIApiKey: process.env.OPENAI_API_KEY || undefined,
619
- huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
620
- tokenLimit: process.env.COCO_TOKEN_LIMIT
621
- ? parseInt(process.env.COCO_TOKEN_LIMIT)
622
- : undefined,
623
- prompt: process.env.COCO_PROMPT,
624
- mode: process.env.COCO_MODE,
625
- summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
626
- ignoredFiles: process.env.COCO_IGNORED_FILES
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('Proposed Commit:'))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
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 tokenizer = getTokenizer();
1043
- const git$1 = simpleGit.simpleGit();
1044
- async function handler$1(argv) {
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: git$1 });
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: git$1, model, logger },
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: git$1, logger });
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: git$1,
1250
+ git,
1098
1251
  });
1099
1252
  }
1100
1253
 
1101
1254
  /**
1102
1255
  * Command line options via yargs
1103
1256
  */
1104
- const options$1 = {
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$1 = (yargs) => {
1151
- return yargs.options(options$1);
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$1,
1158
- handler: handler$1,
1159
- options: options$1,
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 output = await git.raw([
1179
- 'log',
1180
- `${from}..${to}`,
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
- const git = simpleGit.simpleGit();
1196
- async function handler(argv) {
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
- const messages = await getCommitLogRange(from, to, { git, noMerges: true });
1216
- return messages;
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
- await noResult({ git, logger });
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;