git-coco 0.14.7 → 0.14.9

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
@@ -83,6 +83,9 @@ coco changelog -r HEAD~5:HEAD
83
83
 
84
84
  # For a target branch
85
85
  coco changelog -b other-branch
86
+
87
+ # For all commits since the last tag
88
+ coco changelog -t
86
89
  ```
87
90
 
88
91
  ### **`coco recap`**
package/dist/index.d.ts CHANGED
@@ -150,6 +150,7 @@ interface BaseCommandOptions extends BaseArgvOptions {
150
150
  interface ChangelogOptions extends BaseCommandOptions {
151
151
  range: string;
152
152
  branch: string;
153
+ sinceLastTag: boolean;
153
154
  }
154
155
  type ChangelogArgv = Arguments<ChangelogOptions>;
155
156
 
@@ -166,6 +167,7 @@ interface CommitOptions extends BaseCommandOptions {
166
167
  openInEditor: boolean;
167
168
  ignoredFiles: string[];
168
169
  ignoredExtensions: string[];
170
+ withPreviousCommits: number;
169
171
  }
170
172
  type CommitArgv = Arguments<CommitOptions>;
171
173
 
@@ -239,12 +241,14 @@ interface SpinnerOptions {
239
241
  }
240
242
  interface Config {
241
243
  verbose?: boolean;
244
+ silent?: boolean;
242
245
  }
243
246
  declare class Logger {
244
247
  private config;
245
248
  private timerStart;
246
249
  private spinner;
247
250
  constructor(config: Config);
251
+ setConfig(config: Config): Logger;
248
252
  log(message: string, options?: LoggerOptions): Logger;
249
253
  verbose(message: string, options?: LoggerOptions): Logger;
250
254
  startTimer(): Logger;
@@ -18,7 +18,7 @@ import prettyMilliseconds from 'pretty-ms';
18
18
  import { ChatAnthropic } from '@langchain/anthropic';
19
19
  import { ChatOllama } from '@langchain/ollama';
20
20
  import { ChatOpenAI } from '@langchain/openai';
21
- import { JsonOutputParser, BaseOutputParser } from '@langchain/core/output_parsers';
21
+ import { JsonOutputParser, BaseOutputParser, StringOutputParser } from '@langchain/core/output_parsers';
22
22
  import { simpleGit } from 'simple-git';
23
23
  import pQueue from 'p-queue';
24
24
  import { Document, BaseDocumentTransformer } from '@langchain/core/documents';
@@ -55,7 +55,7 @@ import * as readline from 'readline';
55
55
  /**
56
56
  * Current build version from package.json
57
57
  */
58
- const BUILD_VERSION = "0.14.7";
58
+ const BUILD_VERSION = "0.14.9";
59
59
 
60
60
  const isInteractive = (config) => {
61
61
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1769,7 +1769,17 @@ class Logger {
1769
1769
  this.config = config;
1770
1770
  this.spinner = null;
1771
1771
  }
1772
+ setConfig(config) {
1773
+ this.config = {
1774
+ ...this.config,
1775
+ ...config,
1776
+ };
1777
+ return this;
1778
+ }
1772
1779
  log(message, options = { color: 'blue' }) {
1780
+ if (this.config?.silent) {
1781
+ return this;
1782
+ }
1773
1783
  let outputMessage = message;
1774
1784
  if (options.color) {
1775
1785
  outputMessage = chalk[options.color](outputMessage);
@@ -1778,7 +1788,7 @@ class Logger {
1778
1788
  return this;
1779
1789
  }
1780
1790
  verbose(message, options = {}) {
1781
- if (!this.config?.verbose) {
1791
+ if (!this.config?.verbose || this.config?.silent) {
1782
1792
  return this;
1783
1793
  }
1784
1794
  this.log(message, options);
@@ -1789,13 +1799,11 @@ class Logger {
1789
1799
  return this;
1790
1800
  }
1791
1801
  stopTimer(message, options = { color: 'yellow' }) {
1792
- if (!this.config?.verbose || !this.timerStart) {
1802
+ if (!this.config?.verbose || !this.timerStart || this.config?.silent) {
1793
1803
  return this;
1794
1804
  }
1795
1805
  const elapsedTime = prettyMilliseconds(now() - this.timerStart);
1796
- let outputMessage = message
1797
- ? `${message} (⏲ ${elapsedTime})`
1798
- : `⏲ ${elapsedTime}`;
1806
+ let outputMessage = message ? `${message} (⏲ ${elapsedTime})` : `⏲ ${elapsedTime}`;
1799
1807
  if (options.color) {
1800
1808
  outputMessage = chalk[options.color](outputMessage);
1801
1809
  }
@@ -1803,11 +1811,17 @@ class Logger {
1803
1811
  return this;
1804
1812
  }
1805
1813
  startSpinner(message, options = { color: 'green' }) {
1814
+ if (this.config?.silent) {
1815
+ return this;
1816
+ }
1806
1817
  const spinnerMessage = options.color ? chalk[options.color](message) : message;
1807
1818
  this.spinner = ora(spinnerMessage).start();
1808
1819
  return this;
1809
1820
  }
1810
1821
  stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
1822
+ if (this.config?.silent) {
1823
+ return this;
1824
+ }
1811
1825
  const spinnerMessage = options?.color ? chalk[options.color](message) : message;
1812
1826
  this.spinner?.[options.mode || 'succeed'](spinnerMessage);
1813
1827
  this.spinner = null;
@@ -1846,6 +1860,12 @@ const options$4 = {
1846
1860
  alias: 'b',
1847
1861
  description: 'Target branch to compare against',
1848
1862
  },
1863
+ sinceLastTag: {
1864
+ type: 'boolean',
1865
+ alias: 't',
1866
+ description: 'Generate changelog for all commits since the last tag',
1867
+ default: false,
1868
+ },
1849
1869
  i: {
1850
1870
  type: 'boolean',
1851
1871
  alias: 'interactive',
@@ -1898,6 +1918,7 @@ function getPrompt({ template, variables, fallback }) {
1898
1918
  ? new PromptTemplate({
1899
1919
  template,
1900
1920
  inputVariables: variables,
1921
+ templateFormat: 'mustache',
1901
1922
  })
1902
1923
  : fallback);
1903
1924
  }
@@ -1928,6 +1949,35 @@ function extractTicketIdFromBranchName(branchName) {
1928
1949
  return match ? match[0] : null;
1929
1950
  }
1930
1951
 
1952
+ /**
1953
+ * Formats a commit log into a readable string format.
1954
+ *
1955
+ * @param commitLog - The commit log result containing an array of commit details.
1956
+ * @returns An array of formatted commit log strings.
1957
+ *
1958
+ * Each formatted string includes:
1959
+ * - The date of the commit in square brackets.
1960
+ * - The commit message.
1961
+ * - The commit body.
1962
+ * - The commit hash in parentheses.
1963
+ * - The author's name and email in angle brackets.
1964
+ */
1965
+ const formatCommitLog = (commitLog) => {
1966
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
1967
+ };
1968
+
1969
+ const getChangesSinceLastTag = async ({ git }) => {
1970
+ const tags = await git.tags();
1971
+ if (tags.all.length > 0) {
1972
+ const lastTag = tags.latest;
1973
+ const commitLog = await git.log({ from: lastTag });
1974
+ return formatCommitLog(commitLog);
1975
+ }
1976
+ else {
1977
+ return ['No tags found in the repository.'];
1978
+ }
1979
+ };
1980
+
1931
1981
  /**
1932
1982
  * Retrieves the commit log range between two specified commits.
1933
1983
  *
@@ -2061,8 +2111,17 @@ const getRepo = () => {
2061
2111
  return git;
2062
2112
  };
2063
2113
 
2064
- const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2065
- Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
2114
+ /**
2115
+ * Template for generating git commit messages based on code changes
2116
+ *
2117
+ * Variables:
2118
+ * - summary: Contains the diff summary of staged changes
2119
+ * - format_instructions: Instructions for the output format (JSON with title and body)
2120
+ * - additional_context: Optional user-provided context to guide the commit message generation
2121
+ * - commit_history: Optional history of previous commits for context
2122
+ */
2123
+ const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2124
+ Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
2066
2125
 
2067
2126
  Please follow the guidelines below when writing your commit message:
2068
2127
 
@@ -2074,13 +2133,16 @@ Please follow the guidelines below when writing your commit message:
2074
2133
 
2075
2134
  {format_instructions}
2076
2135
 
2136
+ {commit_history}
2137
+
2077
2138
  """"""
2078
2139
  {summary}
2079
2140
  """"""
2080
2141
 
2081
- {additional}
2142
+ {additional_context}
2082
2143
  `;
2083
- const inputVariables$3 = ['summary', 'format_instructions', 'additional'];
2144
+ // Define the variables that will be passed to the prompt template
2145
+ const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history'];
2084
2146
  const COMMIT_PROMPT = new PromptTemplate({
2085
2147
  template: template$4,
2086
2148
  inputVariables: inputVariables$3,
@@ -2246,9 +2308,16 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
2246
2308
  result = '';
2247
2309
  continue;
2248
2310
  }
2311
+ // Only edit the result in interactive mode if approved
2312
+ result = await editResult(result, options);
2313
+ }
2314
+ else {
2315
+ // In non-interactive mode, we return the result as is to be output to stdout by the caller.
2316
+ const displayResult = reviewParser ? reviewParser(result, options) : result;
2317
+ // In non-interactive mode, ensure we return the properly formatted result
2318
+ result = displayResult;
2249
2319
  }
2250
2320
  // if we're here, we're done.
2251
- result = await editResult(result, options);
2252
2321
  continueLoop = false;
2253
2322
  }
2254
2323
  return result;
@@ -2271,7 +2340,8 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
2271
2340
  break;
2272
2341
  case 'stdout':
2273
2342
  default:
2274
- process.stdout.write(result, 'utf8');
2343
+ // Ensure we write the result to stdout in non-interactive mode
2344
+ process.stdout.write(result + '\n', 'utf8');
2275
2345
  break;
2276
2346
  }
2277
2347
  process.exit(0);
@@ -2310,6 +2380,13 @@ const handler$4 = async (argv, logger) => {
2310
2380
  }
2311
2381
  async function factory() {
2312
2382
  const branchName = await getCurrentBranchName({ git });
2383
+ if (config.sinceLastTag) {
2384
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
2385
+ return {
2386
+ branch: branchName,
2387
+ commits: await getChangesSinceLastTag({ git, logger }),
2388
+ };
2389
+ }
2313
2390
  if (config.range && config.range.includes(':')) {
2314
2391
  const [from, to] = config.range.split(':');
2315
2392
  if (!from || !to) {
@@ -2328,7 +2405,7 @@ const handler$4 = async (argv, logger) => {
2328
2405
  commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
2329
2406
  };
2330
2407
  }
2331
- logger.verbose(`No range or branch provided. Defaulting to current branch`, { color: 'yellow' });
2408
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, { color: 'yellow' });
2332
2409
  return {
2333
2410
  branch: branchName,
2334
2411
  commits: await getCommitLogCurrentBranch({ git, logger }),
@@ -2400,7 +2477,7 @@ const handler$4 = async (argv, logger) => {
2400
2477
 
2401
2478
  var changelog = {
2402
2479
  command: command$4,
2403
- desc: 'Generate a changelog from current or target branch or provided commit range.',
2480
+ desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
2404
2481
  builder: builder$4,
2405
2482
  handler: commandExecutor(handler$4),
2406
2483
  options: options$4,
@@ -2438,6 +2515,12 @@ const options$3 = {
2438
2515
  type: 'string',
2439
2516
  alias: 'a',
2440
2517
  },
2518
+ withPreviousCommits: {
2519
+ description: 'Include previous commits as context (specify number of commits, 0 for none)',
2520
+ type: 'number',
2521
+ default: 0,
2522
+ alias: 'p',
2523
+ },
2441
2524
  };
2442
2525
  const builder$3 = (yargs) => {
2443
2526
  return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
@@ -6175,6 +6258,48 @@ async function getChanges({ git, options }) {
6175
6258
  };
6176
6259
  }
6177
6260
 
6261
+ /**
6262
+ * Format a single commit log entry into a readable string
6263
+ * @param commit - The commit log entry
6264
+ * @returns Formatted commit log string
6265
+ */
6266
+ function formatSingleCommit(commit) {
6267
+ const { hash, date, message, body, author_name } = commit;
6268
+ const shortHash = hash.substring(0, 7);
6269
+ return `Commit: ${shortHash}
6270
+ Author: ${author_name}
6271
+ Date: ${date}
6272
+ Message: ${message}
6273
+ ${body ? `\nDetails: ${body}` : ''}`;
6274
+ }
6275
+
6276
+ /**
6277
+ * Get the specified number of previous commits
6278
+ * @param options - Options for getting previous commits
6279
+ * @returns Formatted commit logs
6280
+ */
6281
+ async function getPreviousCommits(options) {
6282
+ const { git, count = 1 } = options;
6283
+ if (count <= 0) {
6284
+ return '';
6285
+ }
6286
+ try {
6287
+ const logs = await git.log({ maxCount: count });
6288
+ if (!logs || logs.total === 0) {
6289
+ return '';
6290
+ }
6291
+ // Format the commit logs
6292
+ const formattedLogs = logs.all.map((commit) => {
6293
+ return formatSingleCommit(commit);
6294
+ }).join('\n\n');
6295
+ return formattedLogs;
6296
+ }
6297
+ catch (error) {
6298
+ console.error(`Error getting previous commits: ${error.message}`);
6299
+ return '';
6300
+ }
6301
+ }
6302
+
6178
6303
  /**
6179
6304
  * Retrieves a TikToken for the specified model.
6180
6305
  *
@@ -6237,10 +6362,13 @@ const handler$3 = async (argv, logger) => {
6237
6362
  }
6238
6363
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
6239
6364
  const llm = getLlm(provider, model, config);
6240
- const INTERACTIVE = isInteractive(config);
6365
+ const INTERACTIVE = argv.interactive || isInteractive(config);
6241
6366
  if (INTERACTIVE) {
6242
6367
  logger.log(LOGO);
6243
6368
  }
6369
+ else {
6370
+ logger.setConfig({ silent: true });
6371
+ }
6244
6372
  async function factory() {
6245
6373
  const changes = await getChanges({
6246
6374
  git,
@@ -6285,14 +6413,30 @@ const handler$3 = async (argv, logger) => {
6285
6413
  fallback: COMMIT_PROMPT,
6286
6414
  });
6287
6415
  const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body', both strings.";
6288
- const additionalContext = argv.additional ? `${argv.additional}` : '';
6416
+ // Get additional context if provided
6417
+ let additional_context = '';
6418
+ if (argv.additional) {
6419
+ additional_context = `## Additional Context\n${argv.additional}`;
6420
+ }
6421
+ // Get commit history if requested
6422
+ let commit_history = '';
6423
+ if (argv.withPreviousCommits > 0) {
6424
+ const commitHistoryData = await getPreviousCommits({
6425
+ git,
6426
+ count: argv.withPreviousCommits
6427
+ });
6428
+ if (commitHistoryData) {
6429
+ commit_history = `## Commit History\n${commitHistoryData}`;
6430
+ }
6431
+ }
6289
6432
  const commitMsg = await executeChain({
6290
6433
  llm,
6291
6434
  prompt,
6292
6435
  variables: {
6293
6436
  summary: context,
6294
6437
  format_instructions: formatInstructions,
6295
- additional: additionalContext,
6438
+ additional_context: additional_context,
6439
+ commit_history: commit_history,
6296
6440
  },
6297
6441
  parser,
6298
6442
  });
@@ -6816,40 +6960,11 @@ const builder$1 = (yargs) => {
6816
6960
  return yargs.options(options$1).usage(getCommandUsageHeader(command$1));
6817
6961
  };
6818
6962
 
6819
- /**
6820
- * Formats a commit log into a readable string format.
6821
- *
6822
- * @param commitLog - The commit log result containing an array of commit details.
6823
- * @returns An array of formatted commit log strings.
6824
- *
6825
- * Each formatted string includes:
6826
- * - The date of the commit in square brackets.
6827
- * - The commit message.
6828
- * - The commit body.
6829
- * - The commit hash in parentheses.
6830
- * - The author's name and email in angle brackets.
6831
- */
6832
- const formatCommitLog = (commitLog) => {
6833
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6834
- };
6835
-
6836
6963
  const getChangesByTimestamp = async ({ since, git }) => {
6837
6964
  const commitLog = await git.log({ '--since': since });
6838
6965
  return formatCommitLog(commitLog);
6839
6966
  };
6840
6967
 
6841
- const getChangesSinceLastTag = async ({ git }) => {
6842
- const tags = await git.tags();
6843
- if (tags.all.length > 0) {
6844
- const lastTag = tags.latest;
6845
- const commitLog = await git.log({ from: lastTag });
6846
- return formatCommitLog(commitLog);
6847
- }
6848
- else {
6849
- return ['No tags found in the repository.'];
6850
- }
6851
- };
6852
-
6853
6968
  async function noResult$1({ logger }) {
6854
6969
  logger.log('No repo changes detected. 👀', { color: 'blue' });
6855
6970
  }
@@ -6859,12 +6974,12 @@ The summarization should descibe in a general sense what has changed in the repo
6859
6974
 
6860
6975
  Breaking down the changes into categories (e.g. bug fixes, new features, etc.) with markdown headings is encouraged.
6861
6976
 
6862
- {timeframe}
6977
+ {{timeframe}}
6863
6978
 
6864
- {format_instructions}
6979
+ {{format_instructions}}
6865
6980
 
6866
- """{changes}"""`;
6867
- const inputVariables$1 = ['format_instructions', 'changes', 'timeframe'];
6981
+ """{{changes}}"""`;
6982
+ const inputVariables$1 = ['timeframe', 'format_instructions', 'changes'];
6868
6983
  const RECAP_PROMPT = new PromptTemplate({
6869
6984
  template: template$1,
6870
6985
  inputVariables: inputVariables$1,
@@ -6881,10 +6996,13 @@ const handler$1 = async (argv, logger) => {
6881
6996
  }
6882
6997
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
6883
6998
  const llm = getLlm(provider, model, config);
6884
- const INTERACTIVE = isInteractive(config);
6999
+ const INTERACTIVE = argv.interactive || isInteractive(config);
6885
7000
  if (INTERACTIVE) {
6886
7001
  logger.log(LOGO);
6887
7002
  }
7003
+ else {
7004
+ logger.setConfig({ silent: true });
7005
+ }
6888
7006
  const { 'last-month': lastMonth, 'last-tag': lastTag, yesterday, 'last-week': lastWeek } = argv;
6889
7007
  const timeframe = lastMonth
6890
7008
  ? 'last-month'
@@ -6943,7 +7061,7 @@ const handler$1 = async (argv, logger) => {
6943
7061
  async function parser(changes) {
6944
7062
  return changes.join('\n');
6945
7063
  }
6946
- await generateAndReviewLoop({
7064
+ const recapResult = await generateAndReviewLoop({
6947
7065
  label: 'recap',
6948
7066
  options: {
6949
7067
  ...config,
@@ -6968,30 +7086,61 @@ const handler$1 = async (argv, logger) => {
6968
7086
  factory,
6969
7087
  parser,
6970
7088
  agent: async (context, options) => {
6971
- const parser = new JsonOutputParser();
6972
- const formatInstructions = "Respond with a valid JSON object, containing one field: 'summary', a string.";
7089
+ const formatInstructions = 'Respond in a readable format. Include both high level and detailed information. Use markdown to format the response.';
6973
7090
  const prompt = getPrompt({
6974
7091
  template: options.prompt,
6975
7092
  variables: RECAP_PROMPT.inputVariables,
6976
7093
  fallback: RECAP_PROMPT,
6977
7094
  });
6978
- const response = await executeChain({
6979
- llm,
6980
- prompt,
6981
- variables: {
6982
- changes: context,
6983
- format_instructions: formatInstructions,
6984
- timeframe,
6985
- },
6986
- parser,
6987
- });
6988
- return `${response.summary || 'no response'}`;
7095
+ try {
7096
+ const stringParser = new StringOutputParser();
7097
+ const response = (await executeChain({
7098
+ llm,
7099
+ prompt,
7100
+ variables: {
7101
+ changes: context,
7102
+ format_instructions: formatInstructions,
7103
+ timeframe,
7104
+ },
7105
+ // NOTE: parser is not optional and JSONOutputParser is expected, however making a union type for `executeChain` breaks type generation downstream.
7106
+ // In the future, we should consider making the parser optional in `executeChain` and better handle parser types.
7107
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7108
+ // @ts-expect-error - parser is not optional and JSONOutputParser is expected
7109
+ parser: stringParser,
7110
+ }));
7111
+ return `${response || 'no response'}`;
7112
+ }
7113
+ catch (error) {
7114
+ const errorMessage = error instanceof Error ? error.message : String(error);
7115
+ // Log the error but don't exit
7116
+ logger.log(`Error parsing LLM response: ${errorMessage}`, { color: 'red' });
7117
+ // Always return a fallback message instead of exiting
7118
+ const fallbackMessage = `
7119
+ ## Failed to parse the response [timeframe: ${timeframe}]
7120
+ - There are changes in the codebase that couldn't be properly summarized due to a technical issue.
7121
+ - LLM encountered issues when parsing the changes.
7122
+
7123
+ ### Error encountered
7124
+
7125
+ ${errorMessage}
7126
+ `;
7127
+ return fallbackMessage;
7128
+ }
6989
7129
  },
6990
7130
  noResult: async () => {
6991
7131
  await noResult$1({ git, logger });
6992
7132
  process.exit(0);
6993
7133
  },
6994
7134
  });
7135
+ // Handle the result based on the mode (interactive or stdout)
7136
+ const MODE = (INTERACTIVE && 'interactive') || (config.recap && 'interactive') || config?.mode || 'stdout'; // Default to stdout
7137
+ handleResult({
7138
+ result: recapResult,
7139
+ interactiveModeCallback: async () => {
7140
+ logSuccess();
7141
+ },
7142
+ mode: MODE,
7143
+ });
6995
7144
  };
6996
7145
 
6997
7146
  var recap = {
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline$1);
77
77
  /**
78
78
  * Current build version from package.json
79
79
  */
80
- const BUILD_VERSION = "0.14.7";
80
+ const BUILD_VERSION = "0.14.9";
81
81
 
82
82
  const isInteractive = (config) => {
83
83
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -1791,7 +1791,17 @@ class Logger {
1791
1791
  this.config = config;
1792
1792
  this.spinner = null;
1793
1793
  }
1794
+ setConfig(config) {
1795
+ this.config = {
1796
+ ...this.config,
1797
+ ...config,
1798
+ };
1799
+ return this;
1800
+ }
1794
1801
  log(message, options = { color: 'blue' }) {
1802
+ if (this.config?.silent) {
1803
+ return this;
1804
+ }
1795
1805
  let outputMessage = message;
1796
1806
  if (options.color) {
1797
1807
  outputMessage = chalk[options.color](outputMessage);
@@ -1800,7 +1810,7 @@ class Logger {
1800
1810
  return this;
1801
1811
  }
1802
1812
  verbose(message, options = {}) {
1803
- if (!this.config?.verbose) {
1813
+ if (!this.config?.verbose || this.config?.silent) {
1804
1814
  return this;
1805
1815
  }
1806
1816
  this.log(message, options);
@@ -1811,13 +1821,11 @@ class Logger {
1811
1821
  return this;
1812
1822
  }
1813
1823
  stopTimer(message, options = { color: 'yellow' }) {
1814
- if (!this.config?.verbose || !this.timerStart) {
1824
+ if (!this.config?.verbose || !this.timerStart || this.config?.silent) {
1815
1825
  return this;
1816
1826
  }
1817
1827
  const elapsedTime = prettyMilliseconds(now() - this.timerStart);
1818
- let outputMessage = message
1819
- ? `${message} (⏲ ${elapsedTime})`
1820
- : `⏲ ${elapsedTime}`;
1828
+ let outputMessage = message ? `${message} (⏲ ${elapsedTime})` : `⏲ ${elapsedTime}`;
1821
1829
  if (options.color) {
1822
1830
  outputMessage = chalk[options.color](outputMessage);
1823
1831
  }
@@ -1825,11 +1833,17 @@ class Logger {
1825
1833
  return this;
1826
1834
  }
1827
1835
  startSpinner(message, options = { color: 'green' }) {
1836
+ if (this.config?.silent) {
1837
+ return this;
1838
+ }
1828
1839
  const spinnerMessage = options.color ? chalk[options.color](message) : message;
1829
1840
  this.spinner = ora(spinnerMessage).start();
1830
1841
  return this;
1831
1842
  }
1832
1843
  stopSpinner(message = '', options = { mode: 'succeed', color: 'green' }) {
1844
+ if (this.config?.silent) {
1845
+ return this;
1846
+ }
1833
1847
  const spinnerMessage = options?.color ? chalk[options.color](message) : message;
1834
1848
  this.spinner?.[options.mode || 'succeed'](spinnerMessage);
1835
1849
  this.spinner = null;
@@ -1868,6 +1882,12 @@ const options$4 = {
1868
1882
  alias: 'b',
1869
1883
  description: 'Target branch to compare against',
1870
1884
  },
1885
+ sinceLastTag: {
1886
+ type: 'boolean',
1887
+ alias: 't',
1888
+ description: 'Generate changelog for all commits since the last tag',
1889
+ default: false,
1890
+ },
1871
1891
  i: {
1872
1892
  type: 'boolean',
1873
1893
  alias: 'interactive',
@@ -1920,6 +1940,7 @@ function getPrompt({ template, variables, fallback }) {
1920
1940
  ? new prompts$1.PromptTemplate({
1921
1941
  template,
1922
1942
  inputVariables: variables,
1943
+ templateFormat: 'mustache',
1923
1944
  })
1924
1945
  : fallback);
1925
1946
  }
@@ -1950,6 +1971,35 @@ function extractTicketIdFromBranchName(branchName) {
1950
1971
  return match ? match[0] : null;
1951
1972
  }
1952
1973
 
1974
+ /**
1975
+ * Formats a commit log into a readable string format.
1976
+ *
1977
+ * @param commitLog - The commit log result containing an array of commit details.
1978
+ * @returns An array of formatted commit log strings.
1979
+ *
1980
+ * Each formatted string includes:
1981
+ * - The date of the commit in square brackets.
1982
+ * - The commit message.
1983
+ * - The commit body.
1984
+ * - The commit hash in parentheses.
1985
+ * - The author's name and email in angle brackets.
1986
+ */
1987
+ const formatCommitLog = (commitLog) => {
1988
+ return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
1989
+ };
1990
+
1991
+ const getChangesSinceLastTag = async ({ git }) => {
1992
+ const tags = await git.tags();
1993
+ if (tags.all.length > 0) {
1994
+ const lastTag = tags.latest;
1995
+ const commitLog = await git.log({ from: lastTag });
1996
+ return formatCommitLog(commitLog);
1997
+ }
1998
+ else {
1999
+ return ['No tags found in the repository.'];
2000
+ }
2001
+ };
2002
+
1953
2003
  /**
1954
2004
  * Retrieves the commit log range between two specified commits.
1955
2005
  *
@@ -2083,8 +2133,17 @@ const getRepo = () => {
2083
2133
  return git;
2084
2134
  };
2085
2135
 
2086
- const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2087
- Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
2136
+ /**
2137
+ * Template for generating git commit messages based on code changes
2138
+ *
2139
+ * Variables:
2140
+ * - summary: Contains the diff summary of staged changes
2141
+ * - format_instructions: Instructions for the output format (JSON with title and body)
2142
+ * - additional_context: Optional user-provided context to guide the commit message generation
2143
+ * - commit_history: Optional history of previous commits for context
2144
+ */
2145
+ const template$4 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
2146
+ Commit Messages must have a short description that is less than 50 characters and a longer detailed summary around 300 characters, the shorter and more concise the better.
2088
2147
 
2089
2148
  Please follow the guidelines below when writing your commit message:
2090
2149
 
@@ -2096,13 +2155,16 @@ Please follow the guidelines below when writing your commit message:
2096
2155
 
2097
2156
  {format_instructions}
2098
2157
 
2158
+ {commit_history}
2159
+
2099
2160
  """"""
2100
2161
  {summary}
2101
2162
  """"""
2102
2163
 
2103
- {additional}
2164
+ {additional_context}
2104
2165
  `;
2105
- const inputVariables$3 = ['summary', 'format_instructions', 'additional'];
2166
+ // Define the variables that will be passed to the prompt template
2167
+ const inputVariables$3 = ['summary', 'format_instructions', 'additional_context', 'commit_history'];
2106
2168
  const COMMIT_PROMPT = new prompts$1.PromptTemplate({
2107
2169
  template: template$4,
2108
2170
  inputVariables: inputVariables$3,
@@ -2268,9 +2330,16 @@ async function generateAndReviewLoop({ label, factory, parser, noResult, agent,
2268
2330
  result = '';
2269
2331
  continue;
2270
2332
  }
2333
+ // Only edit the result in interactive mode if approved
2334
+ result = await editResult(result, options);
2335
+ }
2336
+ else {
2337
+ // In non-interactive mode, we return the result as is to be output to stdout by the caller.
2338
+ const displayResult = reviewParser ? reviewParser(result, options) : result;
2339
+ // In non-interactive mode, ensure we return the properly formatted result
2340
+ result = displayResult;
2271
2341
  }
2272
2342
  // if we're here, we're done.
2273
- result = await editResult(result, options);
2274
2343
  continueLoop = false;
2275
2344
  }
2276
2345
  return result;
@@ -2293,7 +2362,8 @@ async function handleResult({ result, mode, interactiveModeCallback }) {
2293
2362
  break;
2294
2363
  case 'stdout':
2295
2364
  default:
2296
- process.stdout.write(result, 'utf8');
2365
+ // Ensure we write the result to stdout in non-interactive mode
2366
+ process.stdout.write(result + '\n', 'utf8');
2297
2367
  break;
2298
2368
  }
2299
2369
  process.exit(0);
@@ -2332,6 +2402,13 @@ const handler$4 = async (argv, logger) => {
2332
2402
  }
2333
2403
  async function factory() {
2334
2404
  const branchName = await getCurrentBranchName({ git });
2405
+ if (config.sinceLastTag) {
2406
+ logger.verbose(`Generating commit log since the last tag`, { color: 'yellow' });
2407
+ return {
2408
+ branch: branchName,
2409
+ commits: await getChangesSinceLastTag({ git, logger }),
2410
+ };
2411
+ }
2335
2412
  if (config.range && config.range.includes(':')) {
2336
2413
  const [from, to] = config.range.split(':');
2337
2414
  if (!from || !to) {
@@ -2350,7 +2427,7 @@ const handler$4 = async (argv, logger) => {
2350
2427
  commits: await getCommitLogAgainstBranch({ git, logger, targetBranch: argv.branch }),
2351
2428
  };
2352
2429
  }
2353
- logger.verbose(`No range or branch provided. Defaulting to current branch`, { color: 'yellow' });
2430
+ logger.verbose(`No range, branch, or tag option provided. Defaulting to current branch`, { color: 'yellow' });
2354
2431
  return {
2355
2432
  branch: branchName,
2356
2433
  commits: await getCommitLogCurrentBranch({ git, logger }),
@@ -2422,7 +2499,7 @@ const handler$4 = async (argv, logger) => {
2422
2499
 
2423
2500
  var changelog = {
2424
2501
  command: command$4,
2425
- desc: 'Generate a changelog from current or target branch or provided commit range.',
2502
+ desc: 'Generate a changelog from current or target branch, provided commit range, or since the last tag.',
2426
2503
  builder: builder$4,
2427
2504
  handler: commandExecutor(handler$4),
2428
2505
  options: options$4,
@@ -2460,6 +2537,12 @@ const options$3 = {
2460
2537
  type: 'string',
2461
2538
  alias: 'a',
2462
2539
  },
2540
+ withPreviousCommits: {
2541
+ description: 'Include previous commits as context (specify number of commits, 0 for none)',
2542
+ type: 'number',
2543
+ default: 0,
2544
+ alias: 'p',
2545
+ },
2463
2546
  };
2464
2547
  const builder$3 = (yargs) => {
2465
2548
  return yargs.options(options$3).usage(getCommandUsageHeader(command$3));
@@ -6197,6 +6280,48 @@ async function getChanges({ git, options }) {
6197
6280
  };
6198
6281
  }
6199
6282
 
6283
+ /**
6284
+ * Format a single commit log entry into a readable string
6285
+ * @param commit - The commit log entry
6286
+ * @returns Formatted commit log string
6287
+ */
6288
+ function formatSingleCommit(commit) {
6289
+ const { hash, date, message, body, author_name } = commit;
6290
+ const shortHash = hash.substring(0, 7);
6291
+ return `Commit: ${shortHash}
6292
+ Author: ${author_name}
6293
+ Date: ${date}
6294
+ Message: ${message}
6295
+ ${body ? `\nDetails: ${body}` : ''}`;
6296
+ }
6297
+
6298
+ /**
6299
+ * Get the specified number of previous commits
6300
+ * @param options - Options for getting previous commits
6301
+ * @returns Formatted commit logs
6302
+ */
6303
+ async function getPreviousCommits(options) {
6304
+ const { git, count = 1 } = options;
6305
+ if (count <= 0) {
6306
+ return '';
6307
+ }
6308
+ try {
6309
+ const logs = await git.log({ maxCount: count });
6310
+ if (!logs || logs.total === 0) {
6311
+ return '';
6312
+ }
6313
+ // Format the commit logs
6314
+ const formattedLogs = logs.all.map((commit) => {
6315
+ return formatSingleCommit(commit);
6316
+ }).join('\n\n');
6317
+ return formattedLogs;
6318
+ }
6319
+ catch (error) {
6320
+ console.error(`Error getting previous commits: ${error.message}`);
6321
+ return '';
6322
+ }
6323
+ }
6324
+
6200
6325
  /**
6201
6326
  * Retrieves a TikToken for the specified model.
6202
6327
  *
@@ -6259,10 +6384,13 @@ const handler$3 = async (argv, logger) => {
6259
6384
  }
6260
6385
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
6261
6386
  const llm = getLlm(provider, model, config);
6262
- const INTERACTIVE = isInteractive(config);
6387
+ const INTERACTIVE = argv.interactive || isInteractive(config);
6263
6388
  if (INTERACTIVE) {
6264
6389
  logger.log(LOGO);
6265
6390
  }
6391
+ else {
6392
+ logger.setConfig({ silent: true });
6393
+ }
6266
6394
  async function factory() {
6267
6395
  const changes = await getChanges({
6268
6396
  git,
@@ -6307,14 +6435,30 @@ const handler$3 = async (argv, logger) => {
6307
6435
  fallback: COMMIT_PROMPT,
6308
6436
  });
6309
6437
  const formatInstructions = "Respond with a valid JSON object, containing two fields: 'title' and 'body', both strings.";
6310
- const additionalContext = argv.additional ? `${argv.additional}` : '';
6438
+ // Get additional context if provided
6439
+ let additional_context = '';
6440
+ if (argv.additional) {
6441
+ additional_context = `## Additional Context\n${argv.additional}`;
6442
+ }
6443
+ // Get commit history if requested
6444
+ let commit_history = '';
6445
+ if (argv.withPreviousCommits > 0) {
6446
+ const commitHistoryData = await getPreviousCommits({
6447
+ git,
6448
+ count: argv.withPreviousCommits
6449
+ });
6450
+ if (commitHistoryData) {
6451
+ commit_history = `## Commit History\n${commitHistoryData}`;
6452
+ }
6453
+ }
6311
6454
  const commitMsg = await executeChain({
6312
6455
  llm,
6313
6456
  prompt,
6314
6457
  variables: {
6315
6458
  summary: context,
6316
6459
  format_instructions: formatInstructions,
6317
- additional: additionalContext,
6460
+ additional_context: additional_context,
6461
+ commit_history: commit_history,
6318
6462
  },
6319
6463
  parser,
6320
6464
  });
@@ -6838,40 +6982,11 @@ const builder$1 = (yargs) => {
6838
6982
  return yargs.options(options$1).usage(getCommandUsageHeader(command$1));
6839
6983
  };
6840
6984
 
6841
- /**
6842
- * Formats a commit log into a readable string format.
6843
- *
6844
- * @param commitLog - The commit log result containing an array of commit details.
6845
- * @returns An array of formatted commit log strings.
6846
- *
6847
- * Each formatted string includes:
6848
- * - The date of the commit in square brackets.
6849
- * - The commit message.
6850
- * - The commit body.
6851
- * - The commit hash in parentheses.
6852
- * - The author's name and email in angle brackets.
6853
- */
6854
- const formatCommitLog = (commitLog) => {
6855
- return commitLog.all.map(({ message, date, body, author_name, hash, author_email }) => `[${date}] ${message}\n${body}\n(${hash}) - ${author_name}<${author_email}>`);
6856
- };
6857
-
6858
6985
  const getChangesByTimestamp = async ({ since, git }) => {
6859
6986
  const commitLog = await git.log({ '--since': since });
6860
6987
  return formatCommitLog(commitLog);
6861
6988
  };
6862
6989
 
6863
- const getChangesSinceLastTag = async ({ git }) => {
6864
- const tags = await git.tags();
6865
- if (tags.all.length > 0) {
6866
- const lastTag = tags.latest;
6867
- const commitLog = await git.log({ from: lastTag });
6868
- return formatCommitLog(commitLog);
6869
- }
6870
- else {
6871
- return ['No tags found in the repository.'];
6872
- }
6873
- };
6874
-
6875
6990
  async function noResult$1({ logger }) {
6876
6991
  logger.log('No repo changes detected. 👀', { color: 'blue' });
6877
6992
  }
@@ -6881,12 +6996,12 @@ The summarization should descibe in a general sense what has changed in the repo
6881
6996
 
6882
6997
  Breaking down the changes into categories (e.g. bug fixes, new features, etc.) with markdown headings is encouraged.
6883
6998
 
6884
- {timeframe}
6999
+ {{timeframe}}
6885
7000
 
6886
- {format_instructions}
7001
+ {{format_instructions}}
6887
7002
 
6888
- """{changes}"""`;
6889
- const inputVariables$1 = ['format_instructions', 'changes', 'timeframe'];
7003
+ """{{changes}}"""`;
7004
+ const inputVariables$1 = ['timeframe', 'format_instructions', 'changes'];
6890
7005
  const RECAP_PROMPT = new prompts$1.PromptTemplate({
6891
7006
  template: template$1,
6892
7007
  inputVariables: inputVariables$1,
@@ -6903,10 +7018,13 @@ const handler$1 = async (argv, logger) => {
6903
7018
  }
6904
7019
  const tokenizer = await getTokenCounter(provider === 'openai' ? model : 'gpt-4o');
6905
7020
  const llm = getLlm(provider, model, config);
6906
- const INTERACTIVE = isInteractive(config);
7021
+ const INTERACTIVE = argv.interactive || isInteractive(config);
6907
7022
  if (INTERACTIVE) {
6908
7023
  logger.log(LOGO);
6909
7024
  }
7025
+ else {
7026
+ logger.setConfig({ silent: true });
7027
+ }
6910
7028
  const { 'last-month': lastMonth, 'last-tag': lastTag, yesterday, 'last-week': lastWeek } = argv;
6911
7029
  const timeframe = lastMonth
6912
7030
  ? 'last-month'
@@ -6965,7 +7083,7 @@ const handler$1 = async (argv, logger) => {
6965
7083
  async function parser(changes) {
6966
7084
  return changes.join('\n');
6967
7085
  }
6968
- await generateAndReviewLoop({
7086
+ const recapResult = await generateAndReviewLoop({
6969
7087
  label: 'recap',
6970
7088
  options: {
6971
7089
  ...config,
@@ -6990,30 +7108,61 @@ const handler$1 = async (argv, logger) => {
6990
7108
  factory,
6991
7109
  parser,
6992
7110
  agent: async (context, options) => {
6993
- const parser = new output_parsers.JsonOutputParser();
6994
- const formatInstructions = "Respond with a valid JSON object, containing one field: 'summary', a string.";
7111
+ const formatInstructions = 'Respond in a readable format. Include both high level and detailed information. Use markdown to format the response.';
6995
7112
  const prompt = getPrompt({
6996
7113
  template: options.prompt,
6997
7114
  variables: RECAP_PROMPT.inputVariables,
6998
7115
  fallback: RECAP_PROMPT,
6999
7116
  });
7000
- const response = await executeChain({
7001
- llm,
7002
- prompt,
7003
- variables: {
7004
- changes: context,
7005
- format_instructions: formatInstructions,
7006
- timeframe,
7007
- },
7008
- parser,
7009
- });
7010
- return `${response.summary || 'no response'}`;
7117
+ try {
7118
+ const stringParser = new output_parsers.StringOutputParser();
7119
+ const response = (await executeChain({
7120
+ llm,
7121
+ prompt,
7122
+ variables: {
7123
+ changes: context,
7124
+ format_instructions: formatInstructions,
7125
+ timeframe,
7126
+ },
7127
+ // NOTE: parser is not optional and JSONOutputParser is expected, however making a union type for `executeChain` breaks type generation downstream.
7128
+ // In the future, we should consider making the parser optional in `executeChain` and better handle parser types.
7129
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7130
+ // @ts-expect-error - parser is not optional and JSONOutputParser is expected
7131
+ parser: stringParser,
7132
+ }));
7133
+ return `${response || 'no response'}`;
7134
+ }
7135
+ catch (error) {
7136
+ const errorMessage = error instanceof Error ? error.message : String(error);
7137
+ // Log the error but don't exit
7138
+ logger.log(`Error parsing LLM response: ${errorMessage}`, { color: 'red' });
7139
+ // Always return a fallback message instead of exiting
7140
+ const fallbackMessage = `
7141
+ ## Failed to parse the response [timeframe: ${timeframe}]
7142
+ - There are changes in the codebase that couldn't be properly summarized due to a technical issue.
7143
+ - LLM encountered issues when parsing the changes.
7144
+
7145
+ ### Error encountered
7146
+
7147
+ ${errorMessage}
7148
+ `;
7149
+ return fallbackMessage;
7150
+ }
7011
7151
  },
7012
7152
  noResult: async () => {
7013
7153
  await noResult$1({ git, logger });
7014
7154
  process.exit(0);
7015
7155
  },
7016
7156
  });
7157
+ // Handle the result based on the mode (interactive or stdout)
7158
+ const MODE = (INTERACTIVE && 'interactive') || (config.recap && 'interactive') || config?.mode || 'stdout'; // Default to stdout
7159
+ handleResult({
7160
+ result: recapResult,
7161
+ interactiveModeCallback: async () => {
7162
+ logSuccess();
7163
+ },
7164
+ mode: MODE,
7165
+ });
7017
7166
  };
7018
7167
 
7019
7168
  var recap = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.14.7",
3
+ "version": "0.14.9",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",
@@ -52,8 +52,8 @@
52
52
  "@types/async": "^3.2.20",
53
53
  "@types/chunk-text": "^1.0.0",
54
54
  "@types/common-tags": "^1.8.1",
55
- "@types/diff": "^5.0.3",
56
- "@types/ini": "^1.3.31",
55
+ "@types/diff": "^7.0.1",
56
+ "@types/ini": "^4.1.1",
57
57
  "@types/inquirer": "^9.0.3",
58
58
  "@types/jest": "^29.5.10",
59
59
  "@types/node": "^22.7.5",
@@ -93,14 +93,14 @@
93
93
  "ajv": "^8.16.0",
94
94
  "ajv-formats": "^3.0.1",
95
95
  "chalk": "4.1.2",
96
- "diff": "5.2.0",
97
- "ini": "4.1.1",
96
+ "diff": "7.0.0",
97
+ "ini": "5.0.0",
98
98
  "minimatch": "10.0.1",
99
99
  "ora": "5.4.1",
100
100
  "p-queue": "5.0.0",
101
101
  "performance-now": "2.1.0",
102
102
  "pretty-ms": "7.0.1",
103
- "simple-git": "3.25.0",
103
+ "simple-git": "3.27.0",
104
104
  "tiktoken": "^1.0.17",
105
105
  "yargs": "17.7.2"
106
106
  }