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.
@@ -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
- model: process.env.COCO_MODEL || undefined,
595
- openAIApiKey: process.env.OPENAI_API_KEY || undefined,
596
- huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
597
- tokenLimit: process.env.COCO_TOKEN_LIMIT
598
- ? parseInt(process.env.COCO_TOKEN_LIMIT)
599
- : undefined,
600
- prompt: process.env.COCO_PROMPT,
601
- mode: process.env.COCO_MODE,
602
- summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
603
- ignoredFiles: process.env.COCO_IGNORED_FILES
604
- ? process.env.COCO_IGNORED_FILES.split(',')
605
- : undefined,
606
- ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
607
- ? process.env.COCO_IGNORED_EXTENSIONS.split(',')
608
- : undefined,
609
- };
610
- config = { ...config, ...removeUndefined(envConfig) };
611
- return config;
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('Proposed Commit:'))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
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 tokenizer = getTokenizer();
1020
- const git$1 = simpleGit();
1021
- async function handler$1(argv) {
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: git$1 });
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: git$1, model, logger },
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: git$1, logger });
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: git$1,
1229
+ git,
1075
1230
  });
1076
1231
  }
1077
1232
 
1078
1233
  /**
1079
1234
  * Command line options via yargs
1080
1235
  */
1081
- const options$1 = {
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$1 = (yargs) => {
1128
- return yargs.options(options$1);
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$1,
1135
- handler: handler$1,
1136
- options: options$1,
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 output = await git.raw([
1156
- 'log',
1157
- `${from}..${to}`,
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
- const git = simpleGit();
1173
- async function handler(argv) {
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
- const messages = await getCommitLogRange(from, to, { git, noMerges: true });
1193
- return messages;
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
- await noResult({ git, logger });
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