git-coco 0.3.2 → 0.3.4

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.
Files changed (38) hide show
  1. package/dist/commands/commit/handler.d.ts +3 -0
  2. package/dist/commands/commit/index.d.ts +10 -0
  3. package/dist/commands/commit/options.d.ts +15 -0
  4. package/dist/commands/types.d.ts +14 -0
  5. package/dist/index.d.ts +1 -3
  6. package/dist/index.esm.mjs +588 -486
  7. package/dist/index.esm.mjs.map +1 -1
  8. package/dist/index.js +586 -485
  9. package/dist/lib/config/types.d.ts +1 -1
  10. package/dist/lib/langchain/executeChain.d.ts +6 -0
  11. package/dist/lib/langchain/prompts/commitDefault.d.ts +1 -1
  12. package/dist/lib/langchain/prompts/summarize.d.ts +1 -1
  13. package/dist/lib/langchain/utils.d.ts +4 -7
  14. package/dist/lib/parsers/default/index.d.ts +2 -2
  15. package/dist/lib/parsers/default/utils/createDiffTree.d.ts +1 -0
  16. package/dist/lib/parsers/noResult.d.ts +4 -2
  17. package/dist/lib/simple-git/getChanges.d.ts +7 -4
  18. package/dist/lib/simple-git/getChangesByCommit.d.ts +13 -0
  19. package/dist/lib/simple-git/getDiff.d.ts +1 -1
  20. package/dist/lib/simple-git/getDiffFromCommmit.d.ts +10 -0
  21. package/dist/lib/simple-git/getStatus.d.ts +2 -2
  22. package/dist/lib/simple-git/getSummaryText.d.ts +2 -2
  23. package/dist/lib/simple-git/helpers.d.ts +6 -0
  24. package/dist/lib/types.d.ts +21 -10
  25. package/dist/lib/ui/editPrompt.d.ts +2 -0
  26. package/dist/lib/ui/editResult.d.ts +2 -0
  27. package/dist/lib/ui/generateAndReviewLoop.d.ts +15 -0
  28. package/dist/lib/ui/getUserReviewDecision.d.ts +2 -0
  29. package/dist/lib/ui/handleResult.d.ts +5 -0
  30. package/dist/lib/ui/helpers.d.ts +3 -0
  31. package/dist/lib/ui/logResult.d.ts +1 -0
  32. package/dist/lib/ui/logSuccess.d.ts +1 -0
  33. package/dist/stats.html +1 -1
  34. package/package.json +5 -5
  35. package/dist/commands/commit.d.ts +0 -16
  36. package/dist/lib/langchain/chains/llm.d.ts +0 -6
  37. package/dist/lib/ui.d.ts +0 -2
  38. package/dist/types.d.ts +0 -10
package/dist/index.js CHANGED
@@ -2,26 +2,26 @@
2
2
  'use strict';
3
3
 
4
4
  var yargs = require('yargs');
5
- var prompts$1 = require('@inquirer/prompts');
6
- var simpleGit = require('simple-git');
7
- var fs = require('fs');
8
- var os = require('os');
9
- var path = require('path');
10
- var ini = require('ini');
11
- var prompts = require('langchain/prompts');
12
5
  var pQueue = require('p-queue');
13
6
  var document = require('langchain/document');
14
7
  var hf = require('langchain/llms/hf');
8
+ var prompts = require('langchain/prompts');
15
9
  var chains = require('langchain/chains');
16
10
  var openai = require('langchain/llms/openai');
17
11
  var text_splitter = require('langchain/text_splitter');
18
12
  var diff = require('diff');
19
- var chalk = require('chalk');
20
13
  var GPT3NodeTokenizer = require('gpt3-tokenizer');
14
+ var chalk = require('chalk');
21
15
  var ora = require('ora');
22
16
  var now = require('performance-now');
23
17
  var prettyMilliseconds = require('pretty-ms');
18
+ var path = require('path');
24
19
  var minimatch = require('minimatch');
20
+ var fs = require('fs');
21
+ var os = require('os');
22
+ var ini = require('ini');
23
+ 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);
@@ -40,214 +40,11 @@ function _interopNamespaceDefault(e) {
40
40
  return Object.freeze(n);
41
41
  }
42
42
 
43
+ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
43
44
  var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
44
45
  var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
45
- var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
46
46
  var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
47
47
 
48
- /**
49
- * Returns a new object with all undefined keys removed
50
- *
51
- * @param obj Object to remove undefined keys from
52
- * @returns
53
- */
54
- function removeUndefined(obj) {
55
- return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
56
- }
57
-
58
- /**
59
- * Load environment variables
60
- *
61
- * @param {Config} config
62
- * @returns {Config} Updated config
63
- **/
64
- function loadEnvConfig(config) {
65
- const envConfig = {
66
- model: process.env.COCO_MODEL || undefined,
67
- openAIApiKey: process.env.OPENAI_API_KEY || undefined,
68
- huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
69
- tokenLimit: process.env.COCO_TOKEN_LIMIT
70
- ? parseInt(process.env.COCO_TOKEN_LIMIT)
71
- : undefined,
72
- prompt: process.env.COCO_PROMPT,
73
- mode: process.env.COCO_MODE,
74
- summarizePrompt: process.env.COCO_SUMMARIZE_PROMPT,
75
- ignoredFiles: process.env.COCO_IGNORED_FILES
76
- ? process.env.COCO_IGNORED_FILES.split(',')
77
- : undefined,
78
- ignoredExtensions: process.env.COCO_IGNORED_EXTENSIONS
79
- ? process.env.COCO_IGNORED_EXTENSIONS.split(',')
80
- : undefined,
81
- };
82
- config = { ...config, ...removeUndefined(envConfig) };
83
- return config;
84
- }
85
-
86
- /**
87
- * Load git profile config (from ~/.gitconfig)
88
- *
89
- * @param {Config} config
90
- * @returns {Config} Updated config
91
- **/
92
- function loadGitConfig(config) {
93
- const gitConfigPath = path__namespace.join(os__namespace.homedir(), '.gitconfig');
94
- if (fs__namespace.existsSync(gitConfigPath)) {
95
- const gitConfigRaw = fs__namespace.readFileSync(gitConfigPath, 'utf-8');
96
- const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
97
- config = {
98
- ...config,
99
- model: gitConfigParsed.coco?.model || config.model,
100
- openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
101
- huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
102
- tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
103
- prompt: gitConfigParsed.coco?.prompt || config.prompt,
104
- mode: gitConfigParsed.coco?.mode || config.mode,
105
- temperature: gitConfigParsed.coco?.temperature || config.temperature,
106
- summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
107
- ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
108
- ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
109
- };
110
- }
111
- return config;
112
- }
113
-
114
- /**
115
- * Load .gitignore in project root
116
- *
117
- * @param {Config} config
118
- * @returns
119
- */
120
- function loadGitignore(config) {
121
- if (fs__namespace.existsSync('.gitignore')) {
122
- const gitignoreContent = fs__namespace.readFileSync('.gitignore', 'utf-8');
123
- config.ignoredFiles = [
124
- ...(config?.ignoredFiles || []),
125
- ...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
126
- ];
127
- }
128
- return config;
129
- }
130
- /**
131
- * Load .ignore in project root
132
- *
133
- * @param {Config} config
134
- * @returns
135
- */
136
- function loadIgnore(config) {
137
- if (fs__namespace.existsSync('.ignore')) {
138
- const ignoreContent = fs__namespace.readFileSync('.ignore', 'utf-8');
139
- config.ignoredFiles = [
140
- ...(config?.ignoredFiles || []),
141
- ...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
142
- ];
143
- }
144
- return config;
145
- }
146
-
147
- /**
148
- * Load project config
149
- *
150
- * @param {Config} config
151
- * @returns {Config} Updated config
152
- **/
153
- function loadProjectConfig(config) {
154
- if (fs__namespace.existsSync('.coco.config.json')) {
155
- const projectConfig = JSON.parse(fs__namespace.readFileSync('.coco.config.json', 'utf-8'));
156
- config = { ...config, ...projectConfig };
157
- }
158
- return config;
159
- }
160
-
161
- /**
162
- * Load XDG config
163
- *
164
- * @param {Config} config
165
- * @returns {Config} Updated config
166
- */
167
- function loadXDGConfig(config) {
168
- const xdgConfigHome = process.env.XDG_CONFIG_HOME || path__namespace.join(os__namespace.homedir(), '.config');
169
- const xdgConfigPath = path__namespace.join(xdgConfigHome, 'coco', 'config.json');
170
- if (fs__namespace.existsSync(xdgConfigPath)) {
171
- const xdgConfig = JSON.parse(fs__namespace.readFileSync(xdgConfigPath, 'utf-8'));
172
- config = { ...config, ...xdgConfig };
173
- }
174
- return config;
175
- }
176
-
177
- const template$1 = `Write informative git commit message based on the diffs & file changes provided in the "Diff Summary" section.
178
- Commit Messages must have a short description that is less than 50 characters followed by a newline character and then a more verbose detailed description.
179
- - Write concisely using an informal tone
180
- - List significant changes
181
- - DO NOT use phrases like "this commit", "this change", etc.
182
- - DO NOT use specific names or files from the code
183
- - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
184
-
185
- """{summary}"""
186
-
187
- Commit:`;
188
- const inputVariables$1 = ['summary'];
189
- const COMMIT_PROMPT = new prompts.PromptTemplate({
190
- template: template$1,
191
- inputVariables: inputVariables$1,
192
- });
193
-
194
- const template = `GOAL: Use functional abstractions to summarize the following text
195
-
196
- RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
197
-
198
- TEXT:"""{text}"""
199
- `;
200
- const inputVariables = ['text'];
201
- const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
202
- template,
203
- inputVariables,
204
- });
205
-
206
- /**
207
- * Default Config
208
- *
209
- * @type {Config}
210
- */
211
- const DEFAULT_CONFIG = {
212
- model: 'openai/gpt-3.5-turbo',
213
- verbose: false,
214
- tokenLimit: 1024,
215
- prompt: COMMIT_PROMPT.template,
216
- summarizePrompt: SUMMARIZE_PROMPT.template,
217
- temperature: 0.4,
218
- mode: 'stdout',
219
- ignoredFiles: ['package-lock.json'],
220
- ignoredExtensions: ['.map', '.lock'],
221
- };
222
- /**
223
- * Load application config
224
- *
225
- * Merge config from multiple sources.
226
- *
227
- * \* Order of precedence:
228
- * \* 1. Command line flags
229
- * \* 2. Environment variables
230
- * \* 3. Project config
231
- * \* 4. Git config
232
- * \* 5. XDG config
233
- * \* 6. .gitignore
234
- * \* 7. .ignore
235
- * \* 8. Default config
236
- *
237
- * @returns {Config} application config
238
- **/
239
- function loadConfig(argv = {}) {
240
- // Default config
241
- let config = DEFAULT_CONFIG;
242
- config = loadGitignore(config);
243
- config = loadIgnore(config);
244
- config = loadXDGConfig(config);
245
- config = loadGitConfig(config);
246
- config = loadProjectConfig(config);
247
- config = loadEnvConfig(config);
248
- return { ...config, ...argv };
249
- }
250
-
251
48
  /**
252
49
  * Extract the path from a file path string.
253
50
  * @param {string} filePath - The full file path.
@@ -389,12 +186,29 @@ class DiffTreeNode {
389
186
  getPath() {
390
187
  return this.path.join('/');
391
188
  }
189
+ print(indentation = 0) {
190
+ const indent = ' '.repeat(indentation);
191
+ let output = `${indent}- Path: ${this.getPath()}\n`;
192
+ if (this.files.length > 0) {
193
+ output += `${indent} Files:\n`;
194
+ for (const file of this.files) {
195
+ output += `${indent} - ${file.summary}\n`;
196
+ }
197
+ }
198
+ if (this.children.size > 0) {
199
+ output += `${indent} Children:\n`;
200
+ for (const [, child] of this.children) {
201
+ output += child.print(indentation + 4);
202
+ }
203
+ }
204
+ return output;
205
+ }
392
206
  }
393
207
  const createDiffTree = (changes) => {
394
208
  const root = new DiffTreeNode();
395
209
  for (const change of changes) {
396
210
  let currentParent = root;
397
- const parts = change.filepath.split('/');
211
+ const parts = change.filePath.split('/');
398
212
  parts.pop();
399
213
  for (const part of parts) {
400
214
  let childNode = currentParent.getChild(part);
@@ -406,8 +220,8 @@ const createDiffTree = (changes) => {
406
220
  }
407
221
  // Create a NodeFile object and add it to the parent
408
222
  currentParent.addFile({
409
- filepath: change.filepath,
410
- oldFilepath: change.oldFilepath,
223
+ filePath: change.filePath,
224
+ oldFilePath: change.oldFilePath,
411
225
  summary: change.summary,
412
226
  status: change.status,
413
227
  });
@@ -425,11 +239,11 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
425
239
  // TODO: Swap out the GPT3Tokenizer for LangChain tokenizer
426
240
  const tokenizedDiff = tokenizer.encode(diff).text;
427
241
  const tokenCount = tokenizedDiff.length;
428
- logger.verbose(`Collected diff for ${nodeFile.filepath} (${tokenCount} tokens)`, {
242
+ logger.verbose(`Collected diff for ${nodeFile.filePath} (${tokenCount} tokens)`, {
429
243
  color: 'magenta',
430
244
  });
431
245
  return {
432
- file: nodeFile.filepath,
246
+ file: nodeFile.filePath,
433
247
  summary: nodeFile.summary,
434
248
  diff,
435
249
  tokenCount,
@@ -454,7 +268,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
454
268
  * @param configuration
455
269
  * @returns LLM Model
456
270
  */
457
- function getModel(name, key, fields, configuration) {
271
+ function getModel(name, key, fields) {
458
272
  const [llm, model] = name.split(/\/(.*)/s);
459
273
  if (!model) {
460
274
  throw new Error(`Invalid model: ${name}`);
@@ -473,7 +287,7 @@ function getModel(name, key, fields, configuration) {
473
287
  openAIApiKey: key,
474
288
  modelName: model,
475
289
  ...fields,
476
- }, configuration);
290
+ });
477
291
  }
478
292
  }
479
293
  /**
@@ -482,7 +296,7 @@ function getModel(name, key, fields, configuration) {
482
296
  * @param options
483
297
  * @returns
484
298
  */
485
- function getModelAPIKey(name, options) {
299
+ function getApiKeyForModel(name, options) {
486
300
  const [llm, model] = name.split(/\/(.*)/s);
487
301
  if (!model) {
488
302
  throw new Error(`Invalid model: ${name}`);
@@ -539,19 +353,47 @@ function validatePromptTemplate(text, inputVariables) {
539
353
  return true;
540
354
  }
541
355
 
542
- const parseDefaultFileDiff = async (nodeFile, git) => {
543
- return await git.diff(['--staged', nodeFile.filepath]);
356
+ const template$1 = `GOAL: Use functional abstractions to summarize the following text
357
+
358
+ RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
359
+
360
+ TEXT:"""{text}"""
361
+ `;
362
+ const inputVariables$1 = ['text'];
363
+ const SUMMARIZE_PROMPT = new prompts.PromptTemplate({
364
+ template: template$1,
365
+ inputVariables: inputVariables$1,
366
+ });
367
+
368
+ const parseDefaultFileDiff = async (nodeFile, commit = '--staged', git) => {
369
+ if (commit !== '--staged') {
370
+ return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
371
+ }
372
+ return await git.diff([commit, nodeFile.filePath]);
544
373
  };
545
- const parseRenamedFileDiff = async (nodeFile, git, logger) => {
374
+ const parseRenamedFileDiff = async (nodeFile, commit, git, logger) => {
546
375
  let result = '';
547
- const oldFilepath = nodeFile?.oldFilepath || nodeFile.filepath;
376
+ const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
377
+ let previousCommitHash = 'HEAD';
378
+ let newCommitHash = '';
379
+ if (commit !== '--staged') {
380
+ try {
381
+ previousCommitHash = await git.revparse([`${commit}~1`]);
382
+ }
383
+ catch (err) {
384
+ logger.verbose(`Error getting previous commit hash for ${nodeFile.filePath}`, {
385
+ color: 'red',
386
+ });
387
+ }
388
+ newCommitHash = commit;
389
+ }
548
390
  try {
549
- const [headContent, indexContent] = await Promise.all([
550
- git.show([`HEAD:${oldFilepath}`]),
551
- git.show([`:${nodeFile.filepath}`]),
391
+ const [previousContent, newContent] = await Promise.all([
392
+ git.show([`${previousCommitHash}:${oldFilePath}`]),
393
+ git.show([`${newCommitHash}:${nodeFile.filePath}`]),
552
394
  ]);
553
- if (headContent !== indexContent) {
554
- result = diff.createTwoFilesPatch(oldFilepath, nodeFile.filepath, headContent, indexContent, '', '', {
395
+ if (previousContent !== newContent) {
396
+ result = diff.createTwoFilesPatch(oldFilePath, nodeFile.filePath, previousContent, newContent, '', '', {
555
397
  context: 3,
556
398
  });
557
399
  // remove the first 4 lines of the patch (they contain the old and new file names)
@@ -562,27 +404,27 @@ const parseRenamedFileDiff = async (nodeFile, git, logger) => {
562
404
  }
563
405
  }
564
406
  catch (err) {
565
- logger.verbose(`Error comparing file contents for ${nodeFile.filepath}`, { color: 'red' });
407
+ logger.verbose(`Error comparing file contents for ${nodeFile.filePath}`, { color: 'red' });
566
408
  result = 'Error comparing file contents.';
567
409
  }
568
410
  return result;
569
411
  };
570
- const getDiff = async (nodeFile, { git, logger, }) => {
412
+ const getDiff = async (nodeFile, commit, { git, logger, }) => {
571
413
  if (nodeFile.status === 'deleted') {
572
414
  return 'This file has been deleted.';
573
415
  }
574
- if (nodeFile.status === 'renamed' && nodeFile.oldFilepath) {
575
- const renamedDiff = await parseRenamedFileDiff(nodeFile, git, logger);
416
+ if (nodeFile.status === 'renamed' && nodeFile.oldFilePath) {
417
+ const renamedDiff = await parseRenamedFileDiff(nodeFile, commit, git, logger);
576
418
  return renamedDiff;
577
419
  }
578
420
  // If not deleted or renamed, get the diff from the index
579
- const defaultDiff = await parseDefaultFileDiff(nodeFile, git);
421
+ const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
580
422
  return defaultDiff;
581
423
  };
582
424
 
583
425
  const MAX_TOKENS_PER_SUMMARY = 2048;
584
- const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
585
- const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125, });
426
+ async function fileChangeParser({ changes, commit, options: { tokenizer, git, model, logger }, }) {
427
+ const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125 });
586
428
  const summarizationChain = getChain(model, {
587
429
  type: 'map_reduce',
588
430
  combineMapPrompt: SUMMARIZE_PROMPT,
@@ -593,7 +435,7 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
593
435
  logger.stopTimer('Created file hierarchy');
594
436
  // Collect diffs
595
437
  logger.startTimer().startSpinner(`Collecting Diffs...\n`, { color: 'blue' });
596
- const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, { git, logger }), tokenizer, logger);
438
+ const diffs = await collectDiffs(rootTreeNode, (path) => getDiff(path, commit, { git, logger }), tokenizer, logger);
597
439
  logger.stopSpinner('Diffs Collected').stopTimer();
598
440
  // Summarize diffs
599
441
  logger.startTimer();
@@ -602,19 +444,11 @@ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
602
444
  maxTokens: MAX_TOKENS_PER_SUMMARY,
603
445
  textSplitter,
604
446
  chain: summarizationChain,
605
- logger
447
+ logger,
606
448
  });
607
449
  logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
608
450
  return summary;
609
- };
610
-
611
- const SEPERATOR = chalk.blue('----------------');
612
- const logCommit = (commit) => {
613
- console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${commit}\n${SEPERATOR}\n`);
614
- };
615
- const logSuccess = () => {
616
- console.log(chalk.green(chalk.bold('\nAll set! ðŸĶūðŸĪ–')));
617
- };
451
+ }
618
452
 
619
453
  /**
620
454
  * Wrapper around GPT3NodeTokenizer to handle default export.
@@ -689,76 +523,266 @@ class Logger {
689
523
  }
690
524
  }
691
525
 
692
- const llm = async ({ llm, prompt, variables }) => {
693
- if (!llm || !prompt || !variables) {
694
- throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
695
- }
696
- const chain = new chains.LLMChain({ llm, prompt });
697
- let res;
698
- try {
699
- res = await chain.call(variables);
700
- }
701
- catch (error) {
702
- if (error instanceof Error) {
703
- throw new Error(`LLMChain call error: ${error.message}`);
526
+ const template = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
527
+ Commit Messages must have a short description that is less than 50 characters followed by a newline character and then a more verbose detailed description.
528
+
529
+ - Typically a hyphen or asterisk is used for the bullet
530
+ - Write concisely using an informal tone
531
+ - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
532
+ - DO NOT use phrases like "this commit", "this change", "this function", etc. Instead refer to the function, variable, or class by name
533
+ - DO NOT use specific names or files from the code
534
+ - Wrap variable, class, function, components, and dependency names in back ticks e.g. \`variable\`
535
+
536
+ """{summary}"""
537
+
538
+ Commit:`;
539
+ const inputVariables = ['summary'];
540
+ const COMMIT_PROMPT = new prompts.PromptTemplate({
541
+ template,
542
+ inputVariables,
543
+ });
544
+
545
+ const getStatus = (file, location = 'index') => {
546
+ if ('index' in file && 'working_dir' in file) {
547
+ const statusCode = file[location];
548
+ switch (statusCode) {
549
+ case 'A':
550
+ return 'added';
551
+ case 'D':
552
+ return 'deleted';
553
+ case 'M':
554
+ return 'modified';
555
+ case 'R':
556
+ return 'renamed';
557
+ case '?':
558
+ return 'untracked';
559
+ default:
560
+ return 'unknown';
704
561
  }
705
562
  }
706
- if (!res) {
707
- throw new Error('Empty response from LLMChain call');
563
+ else if ('changes' in file && 'binary' in file) {
564
+ if (file.changes === 0)
565
+ return 'untracked';
566
+ if (file.file.includes('=>'))
567
+ return 'renamed';
568
+ if (file.deletions === 0 && file.insertions > 0)
569
+ return 'added';
570
+ if (file.insertions === 0 && file.deletions > 0)
571
+ return 'deleted';
572
+ if ((file.insertions > 0 && file.deletions > 0) || file.changes > 0)
573
+ return 'modified';
574
+ return 'unknown';
708
575
  }
709
- if (res.error) {
710
- throw new Error(`LLMChain response error: ${res.error}`);
576
+ else {
577
+ throw new Error("Invalid file type");
711
578
  }
712
- return res.text.trim();
713
579
  };
714
580
 
715
- const getStatus = (file, location = 'index') => {
716
- const statusCode = file[location] ? file[location] : file.index;
717
- let status;
718
- switch (statusCode) {
719
- case 'A':
720
- status = 'added';
721
- break;
722
- case 'D':
723
- status = 'deleted';
724
- break;
725
- case 'M':
726
- status = 'modified';
727
- break;
728
- case 'R':
729
- status = 'renamed';
730
- break;
731
- case '?':
732
- status = 'untracked';
733
- break;
734
- default:
735
- status = 'unknown';
736
- break;
581
+ const getSummaryText = (file, change) => {
582
+ const status = change.status || getStatus(file);
583
+ let filePath;
584
+ if ('path' in file) {
585
+ filePath = file.path;
586
+ }
587
+ else if ('file' in file) {
588
+ filePath = change?.filePath || file.file;
589
+ }
590
+ else {
591
+ throw new Error("Invalid file type");
592
+ }
593
+ if (change.oldFilePath) {
594
+ return `${status}: ${change.oldFilePath} -> ${filePath}`;
737
595
  }
738
- return status;
596
+ return `${status}: ${filePath}`;
739
597
  };
740
598
 
741
- const getSummaryText = (file, change) => {
742
- const status = change.status || getStatus(file);
743
- if (change.oldFilepath) {
744
- return `${status}: ${change.oldFilepath} -> ${file.path}`;
599
+ /**
600
+ * Returns a new object with all undefined keys removed
601
+ *
602
+ * @param obj Object to remove undefined keys from
603
+ * @returns
604
+ */
605
+ function removeUndefined(obj) {
606
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
607
+ }
608
+
609
+ /**
610
+ * Load environment variables
611
+ *
612
+ * @param {Config} config
613
+ * @returns {Config} Updated config
614
+ **/
615
+ 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;
635
+ }
636
+
637
+ /**
638
+ * Load git profile config (from ~/.gitconfig)
639
+ *
640
+ * @param {Config} config
641
+ * @returns {Config} Updated config
642
+ **/
643
+ function loadGitConfig(config) {
644
+ const gitConfigPath = path__namespace.join(os__namespace.homedir(), '.gitconfig');
645
+ if (fs__namespace.existsSync(gitConfigPath)) {
646
+ const gitConfigRaw = fs__namespace.readFileSync(gitConfigPath, 'utf-8');
647
+ const gitConfigParsed = ini__namespace.parse(gitConfigRaw);
648
+ config = {
649
+ ...config,
650
+ model: gitConfigParsed.coco?.model || config.model,
651
+ openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
652
+ huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
653
+ tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
654
+ prompt: gitConfigParsed.coco?.prompt || config.prompt,
655
+ mode: gitConfigParsed.coco?.mode || config.mode,
656
+ temperature: gitConfigParsed.coco?.temperature || config.temperature,
657
+ summarizePrompt: gitConfigParsed.coco?.summarizePrompt || config.summarizePrompt,
658
+ ignoredFiles: gitConfigParsed.coco?.ignoredFiles || config.ignoredFiles,
659
+ ignoredExtensions: gitConfigParsed.coco?.ignoredExtensions || config.ignoredExtensions,
660
+ };
661
+ }
662
+ return config;
663
+ }
664
+
665
+ /**
666
+ * Load .gitignore in project root
667
+ *
668
+ * @param {Config} config
669
+ * @returns
670
+ */
671
+ function loadGitignore(config) {
672
+ if (fs__namespace.existsSync('.gitignore')) {
673
+ const gitignoreContent = fs__namespace.readFileSync('.gitignore', 'utf-8');
674
+ config.ignoredFiles = [
675
+ ...(config?.ignoredFiles || []),
676
+ ...gitignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
677
+ ];
678
+ }
679
+ return config;
680
+ }
681
+ /**
682
+ * Load .ignore in project root
683
+ *
684
+ * @param {Config} config
685
+ * @returns
686
+ */
687
+ function loadIgnore(config) {
688
+ if (fs__namespace.existsSync('.ignore')) {
689
+ const ignoreContent = fs__namespace.readFileSync('.ignore', 'utf-8');
690
+ config.ignoredFiles = [
691
+ ...(config?.ignoredFiles || []),
692
+ ...ignoreContent.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#')),
693
+ ];
694
+ }
695
+ return config;
696
+ }
697
+
698
+ /**
699
+ * Load project config
700
+ *
701
+ * @param {Config} config
702
+ * @returns {Config} Updated config
703
+ **/
704
+ function loadProjectConfig(config) {
705
+ if (fs__namespace.existsSync('.coco.config.json')) {
706
+ const projectConfig = JSON.parse(fs__namespace.readFileSync('.coco.config.json', 'utf-8'));
707
+ config = { ...config, ...projectConfig };
745
708
  }
746
- return `${status}: ${file.path}`;
709
+ return config;
710
+ }
711
+
712
+ /**
713
+ * Load XDG config
714
+ *
715
+ * @param {Config} config
716
+ * @returns {Config} Updated config
717
+ */
718
+ function loadXDGConfig(config) {
719
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME || path__namespace.join(os__namespace.homedir(), '.config');
720
+ const xdgConfigPath = path__namespace.join(xdgConfigHome, 'coco', 'config.json');
721
+ if (fs__namespace.existsSync(xdgConfigPath)) {
722
+ const xdgConfig = JSON.parse(fs__namespace.readFileSync(xdgConfigPath, 'utf-8'));
723
+ config = { ...config, ...xdgConfig };
724
+ }
725
+ return config;
726
+ }
727
+
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
+ prompt: COMMIT_PROMPT.template,
738
+ summarizePrompt: SUMMARIZE_PROMPT.template,
739
+ temperature: 0.4,
740
+ mode: 'stdout',
741
+ ignoredFiles: ['package-lock.json'],
742
+ ignoredExtensions: ['.map', '.lock'],
747
743
  };
744
+ /**
745
+ * Load application config
746
+ *
747
+ * Merge config from multiple sources.
748
+ *
749
+ * \* Order of precedence:
750
+ * \* 1. Command line flags
751
+ * \* 2. Environment variables
752
+ * \* 3. Project config
753
+ * \* 4. Git config
754
+ * \* 5. XDG config
755
+ * \* 6. .gitignore
756
+ * \* 7. .ignore
757
+ * \* 8. Default config
758
+ *
759
+ * @returns {Config} application config
760
+ **/
761
+ function loadConfig(argv = {}) {
762
+ // Default config
763
+ let config = DEFAULT_CONFIG;
764
+ config = loadGitignore(config);
765
+ config = loadIgnore(config);
766
+ config = loadXDGConfig(config);
767
+ config = loadGitConfig(config);
768
+ config = loadProjectConfig(config);
769
+ config = loadEnvConfig(config);
770
+ return { ...config, ...argv };
771
+ }
748
772
 
749
773
  const config = loadConfig();
750
774
  const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
751
775
  const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
752
- async function getChanges(git, options = {}) {
753
- const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options;
776
+ async function getChanges({ git, options }) {
777
+ const { ignoredFiles = DEFAULT_IGNORED_FILES, ignoredExtensions = DEFAULT_IGNORED_EXTENSIONS } = options || {};
754
778
  const staged = [];
755
779
  const unstaged = [];
756
780
  const untracked = [];
757
781
  const status = await git.status();
758
782
  status.files.forEach((file) => {
759
783
  const fileChange = {
760
- filepath: file.path,
761
- oldFilepath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
784
+ filePath: file.path,
785
+ oldFilePath: status.renamed.filter((renamed) => renamed.to === file.path)[0]?.from,
762
786
  };
763
787
  // Unstaged files
764
788
  if (file.working_dir !== '?' && file.working_dir !== ' ') {
@@ -781,16 +805,19 @@ async function getChanges(git, options = {}) {
781
805
  });
782
806
  const ignoredExtensionsSet = new Set(ignoredExtensions.map((extension) => extension.toLowerCase()));
783
807
  const filteredStaged = staged.filter((file) => {
784
- const extension = path.extname(file.filepath).toLowerCase();
785
- return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch.minimatch(file.filepath, ignoredPattern));
808
+ const extension = path.extname(file.filePath).toLowerCase();
809
+ return (!ignoredExtensionsSet.has(extension) &&
810
+ !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
786
811
  });
787
812
  const filteredUnstaged = unstaged.filter((file) => {
788
- const extension = path.extname(file.filepath).toLowerCase();
789
- return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch.minimatch(file.filepath, ignoredPattern));
813
+ const extension = path.extname(file.filePath).toLowerCase();
814
+ return (!ignoredExtensionsSet.has(extension) &&
815
+ !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
790
816
  });
791
817
  const filteredUntracked = untracked.filter((file) => {
792
- const extension = path.extname(file.filepath).toLowerCase();
793
- return !ignoredExtensionsSet.has(extension) && !ignoredFiles.some(ignoredPattern => minimatch.minimatch(file.filepath, ignoredPattern));
818
+ const extension = path.extname(file.filePath).toLowerCase();
819
+ return (!ignoredExtensionsSet.has(extension) &&
820
+ !ignoredFiles.some((ignoredPattern) => minimatch.minimatch(file.filePath, ignoredPattern)));
794
821
  });
795
822
  return {
796
823
  staged: filteredStaged,
@@ -799,8 +826,8 @@ async function getChanges(git, options = {}) {
799
826
  };
800
827
  }
801
828
 
802
- const noResult = async ({ git, logger }) => {
803
- const { staged, unstaged, untracked } = await getChanges(git);
829
+ async function noResult({ git, logger }) {
830
+ const { staged, unstaged, untracked } = await getChanges({ git });
804
831
  const hasStaged = staged && staged.length > 0;
805
832
  const hasUnstaged = unstaged && unstaged.length > 0;
806
833
  const hasUntracked = untracked && untracked.length > 0;
@@ -826,18 +853,254 @@ const noResult = async ({ git, logger }) => {
826
853
  else {
827
854
  logger.log('No repo changes detected. 👀', { color: 'blue' });
828
855
  }
856
+ }
857
+
858
+ const isInteractive = (argv) => {
859
+ return argv?.mode === 'interactive' || argv.interactive;
860
+ };
861
+ const SEPERATOR = chalk.blue('----------------');
862
+
863
+ function logResult(result) {
864
+ console.log(`\n${chalk.bgBlue(chalk.bold('Proposed Commit:'))}\n${SEPERATOR}\n${result}\n${SEPERATOR}\n`);
865
+ }
866
+
867
+ async function editResult(result, options) {
868
+ if (options.openInEditor) {
869
+ return await prompts$1.editor({
870
+ message: 'Edit the commit message',
871
+ default: result,
872
+ waitForUseInput: false,
873
+ validate: (text) => (text ? true : 'Commit message cannot be empty'),
874
+ });
875
+ }
876
+ return result;
877
+ }
878
+
879
+ async function getUserReviewDecision() {
880
+ return await prompts$1.select({
881
+ message: 'Would you like to make any changes to the commit message?',
882
+ choices: [
883
+ {
884
+ name: 'âœĻ Looks good!',
885
+ value: 'approve',
886
+ description: 'Commit staged changes with generated commit message',
887
+ },
888
+ {
889
+ name: '📝 Edit',
890
+ value: 'edit',
891
+ description: 'Edit the commit message before proceeding',
892
+ },
893
+ {
894
+ name: 'ðŸŠķ Modify Prompt',
895
+ value: 'modifyPrompt',
896
+ description: 'Modify the prompt template and regenerate the commit message',
897
+ },
898
+ {
899
+ name: '🔄 Retry - Message Only',
900
+ value: 'retryMessageOnly',
901
+ description: 'Restart the function execution from generating the commit message',
902
+ },
903
+ {
904
+ name: '🔄 Retry - Full',
905
+ value: 'retryFull',
906
+ description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
907
+ },
908
+ {
909
+ name: 'ðŸ’Ģ Cancel',
910
+ value: 'cancel',
911
+ },
912
+ ],
913
+ });
914
+ }
915
+
916
+ async function editPrompt(options) {
917
+ return await prompts$1.editor({
918
+ message: 'Edit the prompt',
919
+ default: options.prompt?.length ? options.prompt : COMMIT_PROMPT.template,
920
+ waitForUseInput: false,
921
+ validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
922
+ });
923
+ }
924
+
925
+ async function generateAndReviewLoop({ factory, parser, noResult, agent, options, }) {
926
+ const { logger } = options;
927
+ let continueLoop = true;
928
+ let modifyPrompt = false;
929
+ let context = '';
930
+ let result = '';
931
+ const changes = await factory();
932
+ // if we don't have any changes, bail.
933
+ if (!changes || !changes.length) {
934
+ await noResult(options);
935
+ }
936
+ while (continueLoop) {
937
+ if (!context.length) {
938
+ context = await parser(changes, result, options);
939
+ }
940
+ // if we still don't have a context, bail.
941
+ if (!context.length) {
942
+ await noResult(options);
943
+ }
944
+ if (modifyPrompt) {
945
+ options.prompt = await editPrompt(options);
946
+ }
947
+ logger.startTimer().startSpinner(`Generating Message\n`, {
948
+ color: 'blue',
949
+ });
950
+ result = await agent(context, options);
951
+ if (!result) {
952
+ logger.stopSpinner('💀 Agent failed to generate message.', {
953
+ mode: 'fail',
954
+ color: 'red',
955
+ });
956
+ process.exit(0);
957
+ }
958
+ logger
959
+ .stopSpinner('Generated Commit Message', {
960
+ color: 'green',
961
+ mode: 'succeed',
962
+ })
963
+ .stopTimer();
964
+ if (options?.interactive) {
965
+ logResult(result);
966
+ const reviewAnswer = await getUserReviewDecision();
967
+ if (reviewAnswer === 'cancel') {
968
+ process.exit(0);
969
+ }
970
+ if (reviewAnswer === 'edit') {
971
+ options.openInEditor = true;
972
+ }
973
+ if (reviewAnswer === 'retryFull') {
974
+ context = '';
975
+ result = '';
976
+ options.prompt = '';
977
+ continue;
978
+ }
979
+ if (reviewAnswer === 'retryMessageOnly') {
980
+ modifyPrompt = false;
981
+ result = '';
982
+ continue;
983
+ }
984
+ if (reviewAnswer === 'modifyPrompt') {
985
+ modifyPrompt = true;
986
+ result = '';
987
+ continue;
988
+ }
989
+ }
990
+ // if we're here, we're done.
991
+ result = await editResult(result, options);
992
+ continueLoop = false;
993
+ }
994
+ return result;
995
+ }
996
+
997
+ const executeChain = async ({ llm, prompt, variables }) => {
998
+ if (!llm || !prompt || !variables) {
999
+ throw new Error('The input parameters "llm", "prompt", and "variables" are all required.');
1000
+ }
1001
+ const chain = new chains.LLMChain({ llm, prompt });
1002
+ let res;
1003
+ try {
1004
+ res = await chain.call(variables);
1005
+ }
1006
+ catch (error) {
1007
+ if (error instanceof Error) {
1008
+ throw new Error(`LLMChain call error: ${error.message}`);
1009
+ }
1010
+ }
1011
+ if (!res) {
1012
+ throw new Error('Empty response from LLMChain call');
1013
+ }
1014
+ if (res.error) {
1015
+ throw new Error(`LLMChain response error: ${res.error}`);
1016
+ }
1017
+ return res.text.trim();
829
1018
  };
830
1019
 
831
1020
  async function createCommit(commitMsg, git) {
832
1021
  return await git.commit(commitMsg);
833
1022
  }
834
1023
 
835
- // const argv = loadArgv()
1024
+ const logSuccess = () => {
1025
+ console.log(chalk.green(chalk.bold('\nAll set! ðŸĶūðŸĪ–')));
1026
+ };
1027
+
1028
+ const handleResult = async (result, { mode, git }) => {
1029
+ // Handle resulting commit message
1030
+ switch (mode) {
1031
+ case 'interactive':
1032
+ await createCommit(result, git);
1033
+ logSuccess();
1034
+ break;
1035
+ case 'stdout':
1036
+ default:
1037
+ process.stdout.write(result, 'utf8');
1038
+ break;
1039
+ }
1040
+ process.exit(0);
1041
+ };
1042
+
836
1043
  const tokenizer = getTokenizer();
837
1044
  const git = simpleGit.simpleGit();
838
- const command = ['commit', '$0'];
839
- const description = 'Generate a commit message based on the diff summary';
840
- const builder = {
1045
+ async function handler(argv) {
1046
+ const options = loadConfig(argv);
1047
+ const logger = new Logger(options);
1048
+ const key = getApiKeyForModel(options.model, options);
1049
+ if (!key) {
1050
+ logger.log(`No API Key found. 🗝ïļðŸšŠ`, { color: 'red' });
1051
+ process.exit(1);
1052
+ }
1053
+ const model = getModel(options.model, key, {
1054
+ temperature: 0.4,
1055
+ maxConcurrency: 10,
1056
+ });
1057
+ const INTERACTIVE = isInteractive(options);
1058
+ async function factory() {
1059
+ const changes = await getChanges({ git });
1060
+ return changes.staged;
1061
+ }
1062
+ async function parser(changes) {
1063
+ return await fileChangeParser({
1064
+ changes,
1065
+ commit: '--staged',
1066
+ options: { tokenizer, git, model, logger },
1067
+ });
1068
+ }
1069
+ const commitMsg = await generateAndReviewLoop({
1070
+ factory,
1071
+ parser,
1072
+ agent: async (context, options) => {
1073
+ return await executeChain({
1074
+ llm: model,
1075
+ prompt: getPrompt({
1076
+ template: options.prompt,
1077
+ variables: COMMIT_PROMPT.inputVariables,
1078
+ fallback: COMMIT_PROMPT,
1079
+ }),
1080
+ variables: { summary: context },
1081
+ });
1082
+ },
1083
+ noResult: async () => {
1084
+ await noResult({ git, logger });
1085
+ process.exit(0);
1086
+ },
1087
+ options: {
1088
+ ...options,
1089
+ logger,
1090
+ interactive: INTERACTIVE,
1091
+ },
1092
+ });
1093
+ const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1094
+ handleResult(commitMsg, {
1095
+ mode: MODE,
1096
+ git,
1097
+ });
1098
+ }
1099
+
1100
+ /**
1101
+ * Command line options via yargs
1102
+ */
1103
+ const options = {
841
1104
  model: { type: 'string', description: 'LLM/Model-Name' },
842
1105
  openAIApiKey: {
843
1106
  type: 'string',
@@ -883,182 +1146,20 @@ const builder = {
883
1146
  description: 'Ignored extensions',
884
1147
  },
885
1148
  };
886
- async function handler(argv) {
887
- const options = loadConfig(argv);
888
- const logger = new Logger(options);
889
- const key = getModelAPIKey(options.model, options);
890
- if (!key) {
891
- logger.log(`No API Key found. 🗝ïļðŸšŠ`, { color: 'red' });
892
- process.exit(1);
893
- }
894
- const model = getModel(options.model, key, {
895
- temperature: 0.4,
896
- maxConcurrency: 10,
897
- });
898
- const INTERACTIVE = options?.mode === 'interactive' || options.interactive;
899
- const { staged: changes } = await getChanges(git);
900
- let summary = '';
901
- let commitMsg = '';
902
- let promptTemplate = options?.prompt || '';
903
- let modifyPrompt = false;
904
- while (true) {
905
- if (changes.length !== 0 && !summary.length) {
906
- logger.verbose(`\nChanged Files: \n ${changes.map(({ summary }) => summary).join('\n ')}`, {
907
- color: 'blue',
908
- });
909
- summary = await fileChangeParser(changes, { tokenizer, git, model, logger });
910
- }
911
- // Handle empty summary
912
- if (!summary.length) {
913
- await noResult({ git, logger });
914
- process.exit(0);
915
- }
916
- // Prompt user for commit template prompt, if necessary
917
- if (modifyPrompt) {
918
- promptTemplate = await prompts$1.editor({
919
- message: 'Edit the prompt',
920
- default: promptTemplate.length ? promptTemplate : COMMIT_PROMPT.template,
921
- waitForUseInput: false,
922
- validate: (text) => validatePromptTemplate(text, COMMIT_PROMPT.inputVariables),
923
- });
924
- }
925
- logger.startTimer().startSpinner(`Generating Commit Message\n`, {
926
- color: 'blue',
927
- });
928
- commitMsg = await llm({
929
- llm: model,
930
- prompt: getPrompt({
931
- template: promptTemplate,
932
- variables: COMMIT_PROMPT.inputVariables,
933
- fallback: COMMIT_PROMPT,
934
- }),
935
- variables: { summary },
936
- });
937
- if (!commitMsg) {
938
- logger.stopSpinner('💀 Failed to generate commit message.', {
939
- mode: 'fail',
940
- color: 'red',
941
- });
942
- process.exit(0);
943
- }
944
- logger
945
- .stopSpinner('Generated Commit Message', {
946
- color: 'green',
947
- mode: 'succeed',
948
- })
949
- .stopTimer();
950
- if (INTERACTIVE) {
951
- logCommit(commitMsg);
952
- const reviewAnswer = await prompts$1.select({
953
- message: 'Would you like to make any changes to the commit message?',
954
- choices: [
955
- {
956
- name: 'âœĻ Looks good!',
957
- value: 'approve',
958
- description: 'Commit staged changes with generated commit message',
959
- },
960
- {
961
- name: '📝 Edit',
962
- value: 'edit',
963
- description: 'Edit the commit message before proceeding',
964
- },
965
- {
966
- name: 'ðŸŠķ Modify Prompt',
967
- value: 'modifyPrompt',
968
- description: 'Modify the prompt template and regenerate the commit message',
969
- },
970
- {
971
- name: '🔄 Retry - Message Only',
972
- value: 'retryMessageOnly',
973
- description: 'Restart the function execution from generating the commit message',
974
- },
975
- {
976
- name: '🔄 Retry - Full',
977
- value: 'retryFull',
978
- description: 'Restart the function execution from the beginning, regenerating both the diff summary and commit message',
979
- },
980
- {
981
- name: 'ðŸ’Ģ Cancel',
982
- value: 'cancel',
983
- },
984
- ],
985
- });
986
- if (reviewAnswer === 'cancel') {
987
- process.exit(0);
988
- }
989
- if (reviewAnswer === 'edit') {
990
- options.openInEditor = true;
991
- }
992
- if (reviewAnswer === 'retryFull') {
993
- summary = '';
994
- commitMsg = '';
995
- promptTemplate = '';
996
- continue;
997
- }
998
- if (reviewAnswer === 'retryMessageOnly') {
999
- modifyPrompt = false;
1000
- commitMsg = '';
1001
- continue;
1002
- }
1003
- if (reviewAnswer === 'modifyPrompt') {
1004
- modifyPrompt = true;
1005
- commitMsg = '';
1006
- continue;
1007
- }
1008
- }
1009
- if (options.openInEditor) {
1010
- commitMsg = await prompts$1.editor({
1011
- message: 'Edit the commit message',
1012
- default: commitMsg,
1013
- waitForUseInput: false,
1014
- validate: (text) => {
1015
- if (!text) {
1016
- return 'Commit message cannot be empty';
1017
- }
1018
- return true;
1019
- },
1020
- });
1021
- }
1022
- const MODE = (options.interactive && 'interactive') ||
1023
- (options.commit && 'interactive') ||
1024
- options?.mode ||
1025
- 'stdout';
1026
- // Handle resulting commit message
1027
- switch (MODE) {
1028
- case 'interactive':
1029
- await createCommit(commitMsg, git);
1030
- logSuccess();
1031
- break;
1032
- case 'stdout':
1033
- default:
1034
- process.stdout.write(commitMsg, 'utf8');
1035
- break;
1036
- }
1037
- process.exit(0);
1038
- }
1039
- }
1040
-
1041
- var commit = /*#__PURE__*/Object.freeze({
1042
- __proto__: null,
1043
- builder: builder,
1044
- command: command,
1045
- description: description,
1046
- handler: handler
1047
- });
1149
+ const builder = (yargs) => {
1150
+ return yargs.options(options);
1151
+ };
1048
1152
 
1049
- yargs
1050
- .scriptName('coco')
1051
- .commandDir('./commands', {
1052
- extensions: ['ts'],
1053
- })
1054
- .demandCommand()
1055
- .strict()
1056
- .option('h', { alias: 'help' })
1057
- .option('v', {
1058
- alias: 'verbose',
1059
- type: 'boolean',
1060
- description: 'Run with verbose logging',
1061
- }).argv;
1153
+ var commit = {
1154
+ command: 'commit',
1155
+ desc: 'Generate commit message',
1156
+ builder,
1157
+ handler,
1158
+ options,
1159
+ };
1062
1160
 
1063
- exports.commit = commit;
1064
- exports.loadConfig = loadConfig;
1161
+ yargs.scriptName('coco').usage('$0 <cmd> [args]').command([commit.command, '$0'], commit.desc,
1162
+ // TODO: fix type on builder
1163
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1164
+ // @ts-ignore
1165
+ commit.builder, commit.handler).argv;