git-coco 0.2.1 โ†’ 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,7 +113,7 @@ Remember, command line flags and environment variables should be defined in `UPP
113
113
  - [x] LangChain integration ๐Ÿฆœ
114
114
  - [ ] Additional tests! ๐Ÿงช
115
115
  - [ ] Conventional commits ๐Ÿ”œ
116
- - [ ] HuggingFace integration ๐Ÿ”œ
116
+ - [x] HuggingFace integration ๐Ÿ”œ
117
117
  - [ ] Google Vertex AI integration (?)
118
118
  - [ ] Automatic changelog generation ๐Ÿซฃ
119
119
  - [ ] Rebase support ๐Ÿ”€
@@ -0,0 +1,16 @@
1
+ import { Argv, CommandBuilder } from 'yargs';
2
+ import { BaseCommandOptions } from '../types';
3
+ export interface CommitOptions extends BaseCommandOptions {
4
+ interactive: boolean;
5
+ tokenLimit: number;
6
+ prompt: string;
7
+ commit: boolean;
8
+ summarizePrompt: string;
9
+ openInEditor: boolean;
10
+ ignoredFiles: string[];
11
+ ignoredExtensions: string[];
12
+ }
13
+ export declare const command: string[];
14
+ export declare const description = "Generate a commit message based on the diff summary";
15
+ export declare const builder: CommandBuilder<CommitOptions>;
16
+ export declare function handler(argv: Argv<CommitOptions>["argv"]): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import * as commit from './commands/commit';
3
+ export { commit };
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import yargs from 'yargs';
2
3
  import { select, editor } from '@inquirer/prompts';
4
+ import { simpleGit } from 'simple-git';
3
5
  import * as fs from 'fs';
4
6
  import * as os from 'os';
5
7
  import * as path from 'path';
6
8
  import path__default from 'path';
7
9
  import * as ini from 'ini';
8
- import yargs from 'yargs';
9
- import { hideBin } from 'yargs/helpers';
10
10
  import { PromptTemplate } from 'langchain/prompts';
11
11
  import pQueue from 'p-queue';
12
- import chalk from 'chalk';
13
- import ora from 'ora';
14
- import now from 'performance-now';
15
- import prettyMilliseconds from 'pretty-ms';
16
12
  import { Document } from 'langchain/document';
13
+ import { HuggingFaceInference } from 'langchain/llms/hf';
17
14
  import { loadSummarizationChain, LLMChain } from 'langchain/chains';
18
15
  import { OpenAI } from 'langchain/llms/openai';
19
16
  import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
20
17
  import { createTwoFilesPatch } from 'diff';
18
+ import chalk from 'chalk';
21
19
  import GPT3NodeTokenizer from 'gpt3-tokenizer';
20
+ import ora from 'ora';
21
+ import now from 'performance-now';
22
+ import prettyMilliseconds from 'pretty-ms';
22
23
  import { minimatch } from 'minimatch';
23
- import { simpleGit } from 'simple-git';
24
24
 
25
25
  /**
26
26
  * Returns a new object with all undefined keys removed
@@ -40,7 +40,9 @@ function removeUndefined(obj) {
40
40
  **/
41
41
  function loadEnvConfig(config) {
42
42
  const envConfig = {
43
+ model: process.env.COCO_MODEL || undefined,
43
44
  openAIApiKey: process.env.OPENAI_API_KEY || undefined,
45
+ huggingFaceHubApiKey: process.env.HUGGINGFACE_HUB_API_KEY || undefined,
44
46
  tokenLimit: process.env.COCO_TOKEN_LIMIT
45
47
  ? parseInt(process.env.COCO_TOKEN_LIMIT)
46
48
  : undefined,
@@ -71,7 +73,9 @@ function loadGitConfig(config) {
71
73
  const gitConfigParsed = ini.parse(gitConfigRaw);
72
74
  config = {
73
75
  ...config,
76
+ model: gitConfigParsed.coco?.model || config.model,
74
77
  openAIApiKey: gitConfigParsed.coco?.openAIApiKey || config.openAIApiKey,
78
+ huggingFaceHubApiKey: gitConfigParsed.coco?.huggingFaceHubApiKey || config.huggingFaceHubApiKey,
75
79
  tokenLimit: parseInt(gitConfigParsed.coco?.tokenLimit) || config.tokenLimit,
76
80
  prompt: gitConfigParsed.coco?.prompt || config.prompt,
77
81
  mode: gitConfigParsed.coco?.mode || config.mode,
@@ -147,72 +151,6 @@ function loadXDGConfig(config) {
147
151
  return config;
148
152
  }
149
153
 
150
- /**
151
- * Command line options via yargs
152
- */
153
- const options = {
154
- openAIApiKey: { type: 'string', description: 'OpenAI API Key' },
155
- tokenLimit: { type: 'number', description: 'Token limit' },
156
- prompt: {
157
- type: 'string',
158
- alias: 'p',
159
- description: 'Commit message prompt',
160
- },
161
- interactive: {
162
- type: 'boolean',
163
- alias: 'i',
164
- description: 'Toggle interactive mode',
165
- },
166
- commit: {
167
- type: 'boolean',
168
- alias: 's',
169
- description: 'Commit staged changes with generated commit message',
170
- default: false,
171
- },
172
- openInEditor: {
173
- type: 'boolean',
174
- alias: 'e',
175
- description: 'Open commit message in editor before proceeding',
176
- },
177
- verbose: {
178
- type: 'boolean',
179
- description: 'Enable verbose logging',
180
- },
181
- summarizePrompt: {
182
- type: 'string',
183
- description: 'Large file summary prompt',
184
- },
185
- ignoredFiles: {
186
- type: 'array',
187
- description: 'Ignored files',
188
- },
189
- ignoredExtensions: {
190
- type: 'array',
191
- description: 'Ignored extensions',
192
- },
193
- };
194
- /**
195
- * Load command line flags via yargs
196
- *
197
- * @returns {Partial<Config>} Updated config
198
- */
199
- const loadArgv = () => {
200
- return yargs(hideBin(process.argv)).options(options).parseSync();
201
- };
202
- /**
203
- * Load command line flags
204
- *
205
- * Note: Arugments are parsed using yargs.
206
- *
207
- * @param {Config} config
208
- * @returns {Config} Updated config
209
- **/
210
- function loadCmdLineFlags(config) {
211
- const argv = loadArgv();
212
- config = { ...config, ...argv };
213
- return config;
214
- }
215
-
216
154
  const template$1 = `Write informative git commit message based on the diffs & file changes provided in the "Diff Summary" section.
217
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.
218
156
  - Write concisely using an informal tone
@@ -248,7 +186,7 @@ const SUMMARIZE_PROMPT = new PromptTemplate({
248
186
  * @type {Config}
249
187
  */
250
188
  const DEFAULT_CONFIG = {
251
- openAIApiKey: '',
189
+ model: 'openai/gpt-3.5-turbo',
252
190
  verbose: false,
253
191
  tokenLimit: 1024,
254
192
  prompt: COMMIT_PROMPT.template,
@@ -258,7 +196,6 @@ const DEFAULT_CONFIG = {
258
196
  ignoredFiles: ['package-lock.json'],
259
197
  ignoredExtensions: ['.map', '.lock'],
260
198
  };
261
-
262
199
  /**
263
200
  * Load application config
264
201
  *
@@ -276,7 +213,7 @@ const DEFAULT_CONFIG = {
276
213
  *
277
214
  * @returns {Config} application config
278
215
  **/
279
- function loadConfig() {
216
+ function loadConfig(argv = {}) {
280
217
  // Default config
281
218
  let config = DEFAULT_CONFIG;
282
219
  config = loadGitignore(config);
@@ -285,60 +222,7 @@ function loadConfig() {
285
222
  config = loadGitConfig(config);
286
223
  config = loadProjectConfig(config);
287
224
  config = loadEnvConfig(config);
288
- config = loadCmdLineFlags(config);
289
- return config;
290
- }
291
- const config = loadConfig();
292
-
293
- class Logger {
294
- constructor(config) {
295
- this.config = config;
296
- this.spinner = null;
297
- }
298
- log(message, options = { color: 'blue' }) {
299
- let outputMessage = message;
300
- if (options.color) {
301
- outputMessage = chalk[options.color](outputMessage);
302
- }
303
- console.log(outputMessage);
304
- return this;
305
- }
306
- verbose(message, options = {}) {
307
- if (!this.config?.verbose) {
308
- return this;
309
- }
310
- this.log(message, options);
311
- return this;
312
- }
313
- startTimer() {
314
- this.timerStart = now();
315
- return this;
316
- }
317
- stopTimer(message, options = { color: 'yellow' }) {
318
- if (!this.config?.verbose || !this.timerStart) {
319
- return this;
320
- }
321
- const elapsedTime = prettyMilliseconds(now() - this.timerStart);
322
- let outputMessage = message
323
- ? `${message} (โฒ ${elapsedTime})`
324
- : `โฒ ${elapsedTime}`;
325
- if (options.color) {
326
- outputMessage = chalk[options.color](outputMessage);
327
- }
328
- console.log(outputMessage);
329
- return this;
330
- }
331
- startSpinner(message, options = { color: 'green' }) {
332
- const spinnerMessage = options.color ? chalk[options.color](message) : message;
333
- this.spinner = ora(spinnerMessage).start();
334
- return this;
335
- }
336
- stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
337
- const spinnerMessage = options?.color ? chalk[options.color](message) : message;
338
- this.spinner?.[options.mode || 'succeed'](spinnerMessage);
339
- this.spinner = null;
340
- return this;
341
- }
225
+ return { ...config, ...argv };
342
226
  }
343
227
 
344
228
  /**
@@ -425,8 +309,7 @@ const defaultOutputCallback = (group) => {
425
309
  }
426
310
  return output;
427
311
  };
428
- async function summarizeDiffs(rootDiffNode, { tokenizer, maxTokens = 2048, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
429
- const logger = new Logger(config);
312
+ async function summarizeDiffs(rootDiffNode, { tokenizer, logger, maxTokens = 2048, textSplitter, chain, handleOutput = defaultOutputCallback, }) {
430
313
  const queue = new pQueue({ concurrency: 8 });
431
314
  logger.startTimer().startSpinner(`Organizing Diffs...`, { color: 'blue' });
432
315
  const directoryDiffs = createDirectoryDiffs(rootDiffNode);
@@ -512,7 +395,7 @@ const createDiffTree = (changes) => {
512
395
  /**
513
396
  * Asynchronously collect diffs for a given node and its children.
514
397
  */
515
- async function collectDiffs(node, getFileDiff, tokenizer, logger = new Logger(config)) {
398
+ async function collectDiffs(node, getFileDiff, tokenizer, logger) {
516
399
  // Collect diffs for the files of the current node
517
400
  const diffPromises = node.files.map(async (nodeFile) => {
518
401
  const diff = await getFileDiff(nodeFile);
@@ -530,7 +413,7 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger = new Logger(co
530
413
  };
531
414
  });
532
415
  // Collect diffs for the children of the current node
533
- const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer));
416
+ const childrenPromises = Array.from(node.children.values()).map(async (child) => collectDiffs(child, getFileDiff, tokenizer, logger));
534
417
  const [diffs, children] = await Promise.all([
535
418
  Promise.all(diffPromises),
536
419
  Promise.all(childrenPromises),
@@ -542,22 +425,67 @@ async function collectDiffs(node, getFileDiff, tokenizer, logger = new Logger(co
542
425
  };
543
426
  }
544
427
 
545
- // TODO: Extend this to support other models! ๐ŸŽ‰
546
- function getModel(fields, configuration) {
547
- return new OpenAI(fields, configuration);
548
- // return new HuggingFaceInference({
549
- // // model: 'gpt2',
550
- // // model: 'bigcode/starcoder',
551
- // model: 'bigscience/bloom',
552
- // apiKey: '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',
553
- // maxConcurrency: 4,
554
- // cache: true,
555
- // // maxTokens: 2046,
556
- // })
428
+ /**
429
+ * Get LLM Model Based on Configuration
430
+ * @param fields
431
+ * @param configuration
432
+ * @returns LLM Model
433
+ */
434
+ function getModel(name, key, fields, configuration) {
435
+ const [llm, model] = name.split(/\/(.*)/s);
436
+ if (!model) {
437
+ throw new Error(`Invalid model: ${name}`);
438
+ }
439
+ switch (llm) {
440
+ case 'huggingface':
441
+ return new HuggingFaceInference({
442
+ model: model,
443
+ apiKey: key,
444
+ maxConcurrency: 4,
445
+ ...fields,
446
+ });
447
+ case 'openai':
448
+ default:
449
+ return new OpenAI({
450
+ openAIApiKey: key,
451
+ modelName: model,
452
+ ...fields,
453
+ }, configuration);
454
+ }
557
455
  }
456
+ /**
457
+ * Retrieve appropriate API key based on selected model
458
+ * @param name
459
+ * @param options
460
+ * @returns
461
+ */
462
+ function getModelAPIKey(name, options) {
463
+ const [llm, model] = name.split(/\/(.*)/s);
464
+ if (!model) {
465
+ throw new Error(`Invalid model: ${name}`);
466
+ }
467
+ switch (llm) {
468
+ case 'huggingface':
469
+ return options.huggingFaceHubApiKey;
470
+ case 'openai':
471
+ default:
472
+ return options.openAIApiKey;
473
+ }
474
+ }
475
+ /**
476
+ * Get Recursive Character Text Splitter
477
+ * @param options
478
+ * @returns
479
+ */
558
480
  function getTextSplitter(options = {}) {
559
481
  return new RecursiveCharacterTextSplitter(options);
560
482
  }
483
+ /**
484
+ * Get Summarization Chain
485
+ * @param model
486
+ * @param options
487
+ * @returns
488
+ */
561
489
  function getChain(model, options = { type: 'map_reduce' }) {
562
490
  return loadSummarizationChain(model, options);
563
491
  }
@@ -571,6 +499,12 @@ function getPrompt({ template, variables, fallback }) {
571
499
  })
572
500
  : fallback);
573
501
  }
502
+ /**
503
+ * Verify template string contains all required input variables
504
+ * @param text template string
505
+ * @param inputVariables template variables
506
+ * @returns boolean or error message
507
+ */
574
508
  function validatePromptTemplate(text, inputVariables) {
575
509
  if (!text) {
576
510
  return 'Prompt template cannot be empty';
@@ -624,8 +558,7 @@ const getDiff = async (nodeFile, { git, logger, }) => {
624
558
  };
625
559
 
626
560
  const MAX_TOKENS_PER_SUMMARY = 2048;
627
- const fileChangeParser = async (changes, { tokenizer, git, model }) => {
628
- const logger = new Logger(config);
561
+ const fileChangeParser = async (changes, { tokenizer, git, model, logger }) => {
629
562
  const textSplitter = getTextSplitter({ chunkSize: 2000, chunkOverlap: 125, });
630
563
  const summarizationChain = getChain(model, {
631
564
  type: 'map_reduce',
@@ -646,6 +579,7 @@ const fileChangeParser = async (changes, { tokenizer, git, model }) => {
646
579
  maxTokens: MAX_TOKENS_PER_SUMMARY,
647
580
  textSplitter,
648
581
  chain: summarizationChain,
582
+ logger
649
583
  });
650
584
  logger.stopTimer(`\nSummary generated for ${changes.length} staged files`, { color: 'green' });
651
585
  return summary;
@@ -681,11 +615,77 @@ const getTokenizer = () => {
681
615
  return tokenizer;
682
616
  };
683
617
 
618
+ class Logger {
619
+ constructor(config) {
620
+ this.config = config;
621
+ this.spinner = null;
622
+ }
623
+ log(message, options = { color: 'blue' }) {
624
+ let outputMessage = message;
625
+ if (options.color) {
626
+ outputMessage = chalk[options.color](outputMessage);
627
+ }
628
+ console.log(outputMessage);
629
+ return this;
630
+ }
631
+ verbose(message, options = {}) {
632
+ if (!this.config?.verbose) {
633
+ return this;
634
+ }
635
+ this.log(message, options);
636
+ return this;
637
+ }
638
+ startTimer() {
639
+ this.timerStart = now();
640
+ return this;
641
+ }
642
+ stopTimer(message, options = { color: 'yellow' }) {
643
+ if (!this.config?.verbose || !this.timerStart) {
644
+ return this;
645
+ }
646
+ const elapsedTime = prettyMilliseconds(now() - this.timerStart);
647
+ let outputMessage = message
648
+ ? `${message} (โฒ ${elapsedTime})`
649
+ : `โฒ ${elapsedTime}`;
650
+ if (options.color) {
651
+ outputMessage = chalk[options.color](outputMessage);
652
+ }
653
+ console.log(outputMessage);
654
+ return this;
655
+ }
656
+ startSpinner(message, options = { color: 'green' }) {
657
+ const spinnerMessage = options.color ? chalk[options.color](message) : message;
658
+ this.spinner = ora(spinnerMessage).start();
659
+ return this;
660
+ }
661
+ stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
662
+ const spinnerMessage = options?.color ? chalk[options.color](message) : message;
663
+ this.spinner?.[options.mode || 'succeed'](spinnerMessage);
664
+ this.spinner = null;
665
+ return this;
666
+ }
667
+ }
668
+
684
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
+ }
685
673
  const chain = new LLMChain({ llm, prompt });
686
- const res = await chain.call(variables);
687
- if (res.error)
688
- throw new Error(res.error);
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
689
  return res.text.trim();
690
690
  };
691
691
 
@@ -723,6 +723,7 @@ const getSummaryText = (file, change) => {
723
723
  return `${status}: ${file.path}`;
724
724
  };
725
725
 
726
+ const config = loadConfig();
726
727
  const DEFAULT_IGNORED_FILES = config?.ignoredFiles?.length ? config.ignoredFiles : [];
727
728
  const DEFAULT_IGNORED_EXTENSIONS = config?.ignoredExtensions?.length ? config.ignoredExtensions : [];
728
729
  async function getChanges(git, options = {}) {
@@ -777,62 +778,117 @@ async function getChanges(git, options = {}) {
777
778
 
778
779
  const noResult = async ({ git, logger }) => {
779
780
  const { staged, unstaged, untracked } = await getChanges(git);
780
- if (staged.length > 0) {
781
+ const hasStaged = staged && staged.length > 0;
782
+ const hasUnstaged = unstaged && unstaged.length > 0;
783
+ const hasUntracked = untracked && untracked.length > 0;
784
+ if (hasStaged) {
781
785
  logger.log(`Staged files detected, but no summary generated...`, { color: 'red' });
782
786
  logger.log(`Files are likely either:\n โ€ข changed files are ignored\n โ€ข file diff is too large.`, { color: 'yellow' });
783
787
  }
784
- else if (unstaged && unstaged.length > 0) {
785
- logger.log('No staged files detected, but unstaged files detected.', { color: 'yellow' });
786
- logger.verbose(`\n Unstaged Changes: \n ${unstaged.map(({ summary }) => summary).join('\n ')}`, {
787
- color: 'yellow',
788
- });
789
- }
790
- else if (untracked && untracked.length > 0) {
791
- logger.log('No staged files detected, but untracked files detected.', { color: 'yellow' });
792
- logger.verbose(`\n Untracked Changes: \n ${untracked.map(({ summary }) => summary).join('\n ')}`, {
793
- color: 'yellow',
794
- });
788
+ else if (hasUnstaged || hasUntracked) {
789
+ logger.log('Forget something? No staged changes found... ๐Ÿ‘ป', { color: 'red' });
790
+ if (hasUnstaged) {
791
+ logger.log('\nDetected unstaged changes', { color: 'yellow' });
792
+ logger.verbose(`\t${unstaged.map(({ summary }) => summary).join('\n\t')}`, {
793
+ color: 'red',
794
+ });
795
+ }
796
+ if (hasUntracked) {
797
+ logger.log('\nDetected untracked changes', { color: 'yellow' });
798
+ logger.verbose(`\t${untracked.map(({ summary }) => summary).join('\n\t')}`, {
799
+ color: 'red',
800
+ });
801
+ }
795
802
  }
796
803
  else {
797
- logger.log('No repo changes detected.', { color: 'yellow' });
804
+ logger.log('No repo changes detected. ๐Ÿ‘€', { color: 'blue' });
798
805
  }
799
- process.exit(0);
800
806
  };
801
807
 
802
808
  async function createCommit(commitMsg, git) {
803
809
  return await git.commit(commitMsg);
804
810
  }
805
811
 
806
- const argv = loadArgv();
812
+ // const argv = loadArgv()
807
813
  const tokenizer = getTokenizer();
808
814
  const git = simpleGit();
809
- async function main(options) {
810
- const logger = new Logger(config);
811
- if (!config.openAIApiKey) {
815
+ const command = ['commit', '$0'];
816
+ const description = 'Generate a commit message based on the diff summary';
817
+ const builder = {
818
+ model: { type: 'string', description: 'LLM/Model-Name' },
819
+ openAIApiKey: {
820
+ type: 'string',
821
+ description: 'OpenAI API Key',
822
+ conflicts: 'huggingFaceHubApiKey',
823
+ },
824
+ huggingFaceHubApiKey: {
825
+ type: 'string',
826
+ description: 'HuggingFace Hub API Key',
827
+ conflicts: 'openAIApiKey',
828
+ },
829
+ tokenLimit: { type: 'number', description: 'Token limit' },
830
+ prompt: {
831
+ type: 'string',
832
+ alias: 'p',
833
+ description: 'Commit message prompt',
834
+ },
835
+ i: {
836
+ type: 'boolean',
837
+ alias: 'interactive',
838
+ description: 'Toggle interactive mode',
839
+ },
840
+ s: {
841
+ type: 'boolean',
842
+ description: 'Automatically commit staged changes with generated commit message',
843
+ default: false,
844
+ },
845
+ e: {
846
+ type: 'boolean',
847
+ alias: 'edit',
848
+ description: 'Open commit message in editor before proceeding',
849
+ },
850
+ summarizePrompt: {
851
+ type: 'string',
852
+ description: 'Large file summary prompt',
853
+ },
854
+ ignoredFiles: {
855
+ type: 'array',
856
+ description: 'Ignored files',
857
+ },
858
+ ignoredExtensions: {
859
+ type: 'array',
860
+ description: 'Ignored extensions',
861
+ },
862
+ };
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) {
812
868
  logger.log(`No API Key found. ๐Ÿ—๏ธ๐Ÿšช`, { color: 'red' });
813
869
  process.exit(1);
814
870
  }
815
- const model = getModel({
871
+ const model = getModel(options.model, key, {
816
872
  temperature: 0.4,
817
873
  maxConcurrency: 10,
818
- openAIApiKey: config.openAIApiKey,
819
874
  });
820
- const INTERACTIVE = config?.mode === 'interactive' || options.interactive;
875
+ const INTERACTIVE = options?.mode === 'interactive' || options.interactive;
821
876
  const { staged: changes } = await getChanges(git);
822
877
  let summary = '';
823
878
  let commitMsg = '';
824
- let promptTemplate = config?.prompt || '';
879
+ let promptTemplate = options?.prompt || '';
825
880
  let modifyPrompt = false;
826
881
  while (true) {
827
882
  if (changes.length !== 0 && !summary.length) {
828
883
  logger.verbose(`\nChanged Files: \n ${changes.map(({ summary }) => summary).join('\n ')}`, {
829
884
  color: 'blue',
830
885
  });
831
- summary = await fileChangeParser(changes, { tokenizer, git, model });
886
+ summary = await fileChangeParser(changes, { tokenizer, git, model, logger });
832
887
  }
833
888
  // Handle empty summary
834
889
  if (!summary.length) {
835
- noResult({ git, logger });
890
+ await noResult({ git, logger });
891
+ process.exit(0);
836
892
  }
837
893
  // Prompt user for commit template prompt, if necessary
838
894
  if (modifyPrompt) {
@@ -908,7 +964,7 @@ async function main(options) {
908
964
  process.exit(0);
909
965
  }
910
966
  if (reviewAnswer === 'edit') {
911
- config.openInEditor = true;
967
+ options.openInEditor = true;
912
968
  }
913
969
  if (reviewAnswer === 'retryFull') {
914
970
  summary = '';
@@ -927,7 +983,7 @@ async function main(options) {
927
983
  continue;
928
984
  }
929
985
  }
930
- if (config.openInEditor) {
986
+ if (options.openInEditor) {
931
987
  commitMsg = await editor({
932
988
  message: 'Edit the commit message',
933
989
  default: commitMsg,
@@ -942,7 +998,7 @@ async function main(options) {
942
998
  }
943
999
  const MODE = (options.interactive && 'interactive') ||
944
1000
  (options.commit && 'interactive') ||
945
- config?.mode ||
1001
+ options?.mode ||
946
1002
  'stdout';
947
1003
  // Handle resulting commit message
948
1004
  switch (MODE) {
@@ -958,5 +1014,28 @@ async function main(options) {
958
1014
  process.exit(0);
959
1015
  }
960
1016
  }
961
- main(argv).catch(console.error);
1017
+
1018
+ var commit = /*#__PURE__*/Object.freeze({
1019
+ __proto__: null,
1020
+ builder: builder,
1021
+ command: command,
1022
+ description: description,
1023
+ handler: handler
1024
+ });
1025
+
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;
1039
+
1040
+ export { commit };
962
1041
  //# sourceMappingURL=index.esm.mjs.map