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