git-coco 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ import { Argv } from 'yargs';
2
+ import { ChangelogOptions } from './options';
3
+ export declare function handler(argv: Argv<ChangelogOptions>['argv']): Promise<void>;
@@ -0,0 +1,10 @@
1
+ /// <reference types="yargs" />
2
+ import { handler } from './handler';
3
+ declare const _default: {
4
+ command: string;
5
+ desc: string;
6
+ builder: (yargs: import("yargs").Argv<{}>) => import("yargs").Argv<import("yargs").Omit<{}, string> & import("yargs").InferredOptionTypes<Record<string, import("yargs").Options>>>;
7
+ handler: typeof handler;
8
+ options: Record<string, import("yargs").Options>;
9
+ };
10
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { Options, Argv } from 'yargs';
2
+ import { BaseCommandOptions } from '../types';
3
+ export interface ChangelogOptions extends BaseCommandOptions {
4
+ range: string;
5
+ prompt: string;
6
+ commit: boolean;
7
+ summarizePrompt: string;
8
+ openInEditor: boolean;
9
+ ignoredFiles: string[];
10
+ ignoredExtensions: string[];
11
+ }
12
+ /**
13
+ * Command line options via yargs
14
+ */
15
+ export declare const options: Record<string, Options>;
16
+ export declare const builder: (yargs: Argv) => Argv<import("yargs").Omit<{}, string> & import("yargs").InferredOptionTypes<Record<string, Options>>>;
@@ -330,25 +330,25 @@ function validatePromptTemplate(text, inputVariables) {
330
330
  return true;
331
331
  }
332
332
 
333
- const template$1 = `GOAL: Use functional abstractions to summarize the following text
333
+ const template$2 = `GOAL: Use functional abstractions to summarize the following text
334
334
 
335
335
  RULES: Avoid phrases like "this change", "this code", or "this function" etc. Instead refer to the function, variable, or class by name.
336
336
 
337
337
  TEXT:"""{text}"""
338
338
  `;
339
- const inputVariables$1 = ['text'];
339
+ const inputVariables$2 = ['text'];
340
340
  const SUMMARIZE_PROMPT = new PromptTemplate({
341
- template: template$1,
342
- inputVariables: inputVariables$1,
341
+ template: template$2,
342
+ inputVariables: inputVariables$2,
343
343
  });
344
344
 
345
- const parseDefaultFileDiff = async (nodeFile, commit = '--staged', git) => {
345
+ async function parseDefaultFileDiff(nodeFile, commit = '--staged', git) {
346
346
  if (commit !== '--staged') {
347
347
  return await git.diff([`${commit}~1..${commit}`, '--', nodeFile.filePath]);
348
348
  }
349
349
  return await git.diff([commit, nodeFile.filePath]);
350
- };
351
- const parseRenamedFileDiff = async (nodeFile, commit, git, logger) => {
350
+ }
351
+ async function parseRenamedFileDiff(nodeFile, commit, git, logger) {
352
352
  let result = '';
353
353
  const oldFilePath = nodeFile?.oldFilePath || nodeFile.filePath;
354
354
  let previousCommitHash = 'HEAD';
@@ -385,8 +385,8 @@ const parseRenamedFileDiff = async (nodeFile, commit, git, logger) => {
385
385
  result = 'Error comparing file contents.';
386
386
  }
387
387
  return result;
388
- };
389
- const getDiff = async (nodeFile, commit, { git, logger, }) => {
388
+ }
389
+ async function getDiff(nodeFile, commit, { git, logger, }) {
390
390
  if (nodeFile.status === 'deleted') {
391
391
  return 'This file has been deleted.';
392
392
  }
@@ -397,7 +397,7 @@ const getDiff = async (nodeFile, commit, { git, logger, }) => {
397
397
  // If not deleted or renamed, get the diff from the index
398
398
  const defaultDiff = await parseDefaultFileDiff(nodeFile, commit, git);
399
399
  return defaultDiff;
400
- };
400
+ }
401
401
 
402
402
  const MAX_TOKENS_PER_SUMMARY = 2048;
403
403
  async function fileChangeParser({ changes, commit, options: { tokenizer, git, model, logger }, }) {
@@ -500,7 +500,7 @@ class Logger {
500
500
  }
501
501
  }
502
502
 
503
- const template = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
503
+ const template$1 = `Write informative git commit message, in the imperative, based on the diffs & file changes provided in the "Diff Summary" section.
504
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
505
 
506
506
  - Typically a hyphen or asterisk is used for the bullet
@@ -513,13 +513,13 @@ Commit Messages must have a short description that is less than 50 characters fo
513
513
  """{summary}"""
514
514
 
515
515
  Commit:`;
516
- const inputVariables = ['summary'];
516
+ const inputVariables$1 = ['summary'];
517
517
  const COMMIT_PROMPT = new PromptTemplate({
518
- template,
519
- inputVariables,
518
+ template: template$1,
519
+ inputVariables: inputVariables$1,
520
520
  });
521
521
 
522
- const getStatus = (file, location = 'index') => {
522
+ function getStatus(file, location = 'index') {
523
523
  if ('index' in file && 'working_dir' in file) {
524
524
  const statusCode = file[location];
525
525
  switch (statusCode) {
@@ -551,11 +551,11 @@ const getStatus = (file, location = 'index') => {
551
551
  return 'unknown';
552
552
  }
553
553
  else {
554
- throw new Error("Invalid file type");
554
+ throw new Error('Invalid file type');
555
555
  }
556
- };
556
+ }
557
557
 
558
- const getSummaryText = (file, change) => {
558
+ function getSummaryText(file, change) {
559
559
  const status = change.status || getStatus(file);
560
560
  let filePath;
561
561
  if ('path' in file) {
@@ -565,13 +565,13 @@ const getSummaryText = (file, change) => {
565
565
  filePath = change?.filePath || file.file;
566
566
  }
567
567
  else {
568
- throw new Error("Invalid file type");
568
+ throw new Error('Invalid file type');
569
569
  }
570
570
  if (change.oldFilePath) {
571
571
  return `${status}: ${change.oldFilePath} -> ${filePath}`;
572
572
  }
573
573
  return `${status}: ${filePath}`;
574
- };
574
+ }
575
575
 
576
576
  /**
577
577
  * Returns a new object with all undefined keys removed
@@ -711,7 +711,6 @@ const DEFAULT_CONFIG = {
711
711
  model: 'openai/gpt-4',
712
712
  verbose: false,
713
713
  tokenLimit: 1024,
714
- prompt: COMMIT_PROMPT.template,
715
714
  summarizePrompt: SUMMARIZE_PROMPT.template,
716
715
  temperature: 0.4,
717
716
  mode: 'stdout',
@@ -899,7 +898,7 @@ async function editPrompt(options) {
899
898
  });
900
899
  }
901
900
 
902
- async function generateAndReviewLoop({ factory, parser, noResult, agent, options, }) {
901
+ async function generateAndReviewLoop({ label, factory, parser, noResult, agent, options, }) {
903
902
  const { logger } = options;
904
903
  let continueLoop = true;
905
904
  let modifyPrompt = false;
@@ -921,19 +920,19 @@ async function generateAndReviewLoop({ factory, parser, noResult, agent, options
921
920
  if (modifyPrompt) {
922
921
  options.prompt = await editPrompt(options);
923
922
  }
924
- logger.startTimer().startSpinner(`Generating Message\n`, {
923
+ logger.startTimer().startSpinner(`Generating ${label}\n`, {
925
924
  color: 'blue',
926
925
  });
927
926
  result = await agent(context, options);
928
927
  if (!result) {
929
- logger.stopSpinner('💀 Agent failed to generate message.', {
928
+ logger.stopSpinner('💀 Agent failed to return content.', {
930
929
  mode: 'fail',
931
930
  color: 'red',
932
931
  });
933
932
  process.exit(0);
934
933
  }
935
934
  logger
936
- .stopSpinner('Generated Commit Message', {
935
+ .stopSpinner(`Generated ${label}`, {
937
936
  color: 'green',
938
937
  mode: 'succeed',
939
938
  })
@@ -1018,8 +1017,8 @@ const handleResult = async (result, { mode, git }) => {
1018
1017
  };
1019
1018
 
1020
1019
  const tokenizer = getTokenizer();
1021
- const git = simpleGit();
1022
- async function handler(argv) {
1020
+ const git$1 = simpleGit();
1021
+ async function handler$1(argv) {
1023
1022
  const options = loadConfig(argv);
1024
1023
  const logger = new Logger(options);
1025
1024
  const key = getApiKeyForModel(options.model, options);
@@ -1033,17 +1032,18 @@ async function handler(argv) {
1033
1032
  });
1034
1033
  const INTERACTIVE = isInteractive(options);
1035
1034
  async function factory() {
1036
- const changes = await getChanges({ git });
1035
+ const changes = await getChanges({ git: git$1 });
1037
1036
  return changes.staged;
1038
1037
  }
1039
1038
  async function parser(changes) {
1040
1039
  return await fileChangeParser({
1041
1040
  changes,
1042
1041
  commit: '--staged',
1043
- options: { tokenizer, git, model, logger },
1042
+ options: { tokenizer, git: git$1, model, logger },
1044
1043
  });
1045
1044
  }
1046
1045
  const commitMsg = await generateAndReviewLoop({
1046
+ label: 'Commit Message',
1047
1047
  factory,
1048
1048
  parser,
1049
1049
  agent: async (context, options) => {
@@ -1058,11 +1058,12 @@ async function handler(argv) {
1058
1058
  });
1059
1059
  },
1060
1060
  noResult: async () => {
1061
- await noResult({ git, logger });
1061
+ await noResult({ git: git$1, logger });
1062
1062
  process.exit(0);
1063
1063
  },
1064
1064
  options: {
1065
1065
  ...options,
1066
+ prompt: options.prompt || COMMIT_PROMPT.template,
1066
1067
  logger,
1067
1068
  interactive: INTERACTIVE,
1068
1069
  },
@@ -1070,14 +1071,14 @@ async function handler(argv) {
1070
1071
  const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1071
1072
  handleResult(commitMsg, {
1072
1073
  mode: MODE,
1073
- git,
1074
+ git: git$1,
1074
1075
  });
1075
1076
  }
1076
1077
 
1077
1078
  /**
1078
1079
  * Command line options via yargs
1079
1080
  */
1080
- const options = {
1081
+ const options$1 = {
1081
1082
  model: { type: 'string', description: 'LLM/Model-Name' },
1082
1083
  openAIApiKey: {
1083
1084
  type: 'string',
@@ -1123,21 +1124,187 @@ const options = {
1123
1124
  description: 'Ignored extensions',
1124
1125
  },
1125
1126
  };
1126
- const builder = (yargs) => {
1127
- return yargs.options(options);
1127
+ const builder$1 = (yargs) => {
1128
+ return yargs.options(options$1);
1128
1129
  };
1129
1130
 
1130
1131
  var commit = {
1131
1132
  command: 'commit',
1132
1133
  desc: 'Generate commit message',
1134
+ builder: builder$1,
1135
+ handler: handler$1,
1136
+ options: options$1,
1137
+ };
1138
+
1139
+ const template = `Write informative git changelog, in the imperative, based on a series of individual messages.
1140
+
1141
+ - Typically a hyphen or asterisk is used for the bullet
1142
+ - Summarize dependency updates
1143
+
1144
+ """{summary}"""
1145
+
1146
+ Changelog:`;
1147
+ const inputVariables = ['summary'];
1148
+ const CHANGELOG_PROMPT = new PromptTemplate({
1149
+ template,
1150
+ inputVariables,
1151
+ });
1152
+
1153
+ async function getCommitLogRange(from, to, { noMerges, git }) {
1154
+ try {
1155
+ const output = await git.raw([
1156
+ 'log',
1157
+ `${from}..${to}`,
1158
+ '--pretty=format:%s',
1159
+ // Include '--no-merges' here if you want to exclude merge commits.
1160
+ noMerges ? '--no-merges' : null,
1161
+ ].filter(Boolean)); // filter(Boolean) removes any null values from the array
1162
+ const messages = output.split('\n').filter(Boolean);
1163
+ return messages;
1164
+ }
1165
+ catch (error) {
1166
+ // If there's an error, handle it appropriately
1167
+ console.error('Error getting commit messages:', error);
1168
+ throw error;
1169
+ }
1170
+ }
1171
+
1172
+ const git = simpleGit();
1173
+ async function handler(argv) {
1174
+ const options = loadConfig(argv);
1175
+ const logger = new Logger(options);
1176
+ const key = getApiKeyForModel(options.model, options);
1177
+ if (!key) {
1178
+ logger.log(`No API Key found. 🗝️🚪`, { color: 'red' });
1179
+ process.exit(1);
1180
+ }
1181
+ const model = getModel(options.model, key, {
1182
+ temperature: 0.4,
1183
+ maxConcurrency: 10,
1184
+ });
1185
+ const INTERACTIVE = isInteractive(options);
1186
+ const [from, to] = options.range?.split(':');
1187
+ if (!from || !to) {
1188
+ logger.log(`Invalid range provided. Expected format is <from>:<to>`, { color: 'red' });
1189
+ process.exit(1);
1190
+ }
1191
+ async function factory() {
1192
+ const messages = await getCommitLogRange(from, to, { git, noMerges: true });
1193
+ return messages;
1194
+ }
1195
+ async function parser(messages) {
1196
+ const result = messages.join('\n');
1197
+ return result;
1198
+ }
1199
+ const changelogMsg = await generateAndReviewLoop({
1200
+ label: 'Changelog',
1201
+ factory,
1202
+ parser,
1203
+ agent: async (context, options) => {
1204
+ const prompt = getPrompt({
1205
+ template: options.prompt,
1206
+ variables: CHANGELOG_PROMPT.inputVariables,
1207
+ fallback: CHANGELOG_PROMPT,
1208
+ });
1209
+ return await executeChain({
1210
+ llm: model,
1211
+ prompt,
1212
+ variables: { summary: context },
1213
+ });
1214
+ },
1215
+ noResult: async () => {
1216
+ await noResult({ git, logger });
1217
+ process.exit(0);
1218
+ },
1219
+ options: {
1220
+ ...options,
1221
+ prompt: options.prompt || CHANGELOG_PROMPT.template,
1222
+ logger,
1223
+ interactive: INTERACTIVE,
1224
+ },
1225
+ });
1226
+ const MODE = (INTERACTIVE && 'interactive') || (options.commit && 'interactive') || options?.mode || 'stdout';
1227
+ handleResult(changelogMsg, {
1228
+ mode: MODE,
1229
+ git,
1230
+ });
1231
+ }
1232
+
1233
+ /**
1234
+ * Command line options via yargs
1235
+ */
1236
+ const options = {
1237
+ range: {
1238
+ type: 'string',
1239
+ alias: 'r',
1240
+ description: 'Commit range e.g `HEAD~2:HEAD`',
1241
+ demandOption: true,
1242
+ },
1243
+ model: { type: 'string', description: 'LLM/Model-Name' },
1244
+ openAIApiKey: {
1245
+ type: 'string',
1246
+ description: 'OpenAI API Key',
1247
+ conflicts: 'huggingFaceHubApiKey',
1248
+ },
1249
+ huggingFaceHubApiKey: {
1250
+ type: 'string',
1251
+ description: 'HuggingFace Hub API Key',
1252
+ conflicts: 'openAIApiKey',
1253
+ },
1254
+ tokenLimit: { type: 'number', description: 'Token limit' },
1255
+ prompt: {
1256
+ type: 'string',
1257
+ alias: 'p',
1258
+ description: 'Prompt for llm',
1259
+ },
1260
+ i: {
1261
+ type: 'boolean',
1262
+ alias: 'interactive',
1263
+ description: 'Toggle interactive mode',
1264
+ },
1265
+ e: {
1266
+ type: 'boolean',
1267
+ alias: 'edit',
1268
+ description: 'Open generated changelog message in editor before proceeding',
1269
+ },
1270
+ summarizePrompt: {
1271
+ type: 'string',
1272
+ description: 'Prompt for summarizing large files',
1273
+ },
1274
+ ignoredFiles: {
1275
+ type: 'array',
1276
+ description: 'Ignored files',
1277
+ },
1278
+ ignoredExtensions: {
1279
+ type: 'array',
1280
+ description: 'Ignored extensions',
1281
+ },
1282
+ };
1283
+ const builder = (yargs) => {
1284
+ return yargs.options(options);
1285
+ };
1286
+
1287
+ var changelog = {
1288
+ command: 'changelog',
1289
+ desc: 'Generate a changelog from a commit range',
1133
1290
  builder,
1134
1291
  handler,
1135
1292
  options,
1136
1293
  };
1137
1294
 
1138
- yargs.scriptName('coco').usage('$0 <cmd> [args]').command([commit.command, '$0'], commit.desc,
1295
+ yargs
1296
+ .scriptName('coco')
1297
+ .usage('$0 <cmd> [args]')
1298
+ .command([commit.command, '$0'], commit.desc,
1299
+ // TODO: fix type on builder
1300
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1301
+ // @ts-ignore
1302
+ commit.builder, commit.handler)
1303
+ .command(changelog.command, changelog.desc,
1139
1304
  // TODO: fix type on builder
1140
1305
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1141
1306
  // @ts-ignore
1142
- commit.builder, commit.handler).argv;
1307
+ changelog.builder, changelog.handler)
1308
+ .demandCommand()
1309
+ .help().argv;
1143
1310
  //# sourceMappingURL=index.esm.mjs.map