contribute-now 0.4.0-dev.d24b735 → 0.4.0-dev.d48d9e6

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 (2) hide show
  1. package/dist/index.js +133 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -970,6 +970,68 @@ function withTimeout(promise, ms) {
970
970
  }
971
971
  var COPILOT_TIMEOUT_MS = 30000;
972
972
  var COPILOT_LONG_TIMEOUT_MS = 90000;
973
+ var BATCH_CONFIG = {
974
+ LARGE_CHANGESET_THRESHOLD: 15,
975
+ COMPACT_PER_FILE_CHARS: 300,
976
+ MAX_COMPACT_PAYLOAD: 1e4,
977
+ FALLBACK_BATCH_SIZE: 15
978
+ };
979
+ function parseDiffByFile(rawDiff) {
980
+ const sections = new Map;
981
+ const headerPattern = /^diff --git a\/(.+?) b\//gm;
982
+ const positions = [];
983
+ for (let match = headerPattern.exec(rawDiff);match !== null; match = headerPattern.exec(rawDiff)) {
984
+ positions.push({ file: match[1], start: match.index });
985
+ }
986
+ for (let i = 0;i < positions.length; i++) {
987
+ const { file, start } = positions[i];
988
+ const end = i + 1 < positions.length ? positions[i + 1].start : rawDiff.length;
989
+ sections.set(file, rawDiff.slice(start, end));
990
+ }
991
+ return sections;
992
+ }
993
+ function extractDiffStats(diffSection) {
994
+ let added = 0;
995
+ let removed = 0;
996
+ for (const line of diffSection.split(`
997
+ `)) {
998
+ if (line.startsWith("+") && !line.startsWith("+++"))
999
+ added++;
1000
+ if (line.startsWith("-") && !line.startsWith("---"))
1001
+ removed++;
1002
+ }
1003
+ return { added, removed };
1004
+ }
1005
+ function createCompactDiff(files, rawDiff, maxTotalChars = BATCH_CONFIG.MAX_COMPACT_PAYLOAD) {
1006
+ if (files.length === 0)
1007
+ return "";
1008
+ const diffSections = parseDiffByFile(rawDiff);
1009
+ const perFileBudget = Math.min(BATCH_CONFIG.COMPACT_PER_FILE_CHARS, Math.floor(maxTotalChars / files.length));
1010
+ const parts = [];
1011
+ for (const file of files) {
1012
+ const section = diffSections.get(file);
1013
+ if (section) {
1014
+ const stats = extractDiffStats(section);
1015
+ const header = `[${file}] (+${stats.added}/-${stats.removed})`;
1016
+ if (section.length <= perFileBudget) {
1017
+ parts.push(`${header}
1018
+ ${section}`);
1019
+ } else {
1020
+ const truncated = section.slice(0, perFileBudget - header.length - 20);
1021
+ parts.push(`${header}
1022
+ ${truncated}
1023
+ ...(truncated)`);
1024
+ }
1025
+ } else {
1026
+ parts.push(`[${file}] (new/binary file — no diff available)`);
1027
+ }
1028
+ }
1029
+ const result = parts.join(`
1030
+
1031
+ `);
1032
+ return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
1033
+ ...(truncated)` : result;
1034
+ }
973
1035
  async function checkCopilotAvailable() {
974
1036
  try {
975
1037
  const client = await getManagedClient();
@@ -1070,16 +1132,18 @@ function extractJson(raw) {
1070
1132
  }
1071
1133
  async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
1072
1134
  try {
1135
+ const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1073
1136
  const multiFileHint = stagedFiles.length > 1 ? `
1074
1137
 
1075
1138
  IMPORTANT: Multiple files are staged. Generate ONE commit message that captures the high-level purpose of ALL changes together. Focus on the overall intent, not individual file changes. Be specific but concise — do not list every file.` : "";
1139
+ const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
1076
1140
  const userMessage = `Generate a commit message for these staged changes:
1077
1141
 
1078
- Files: ${stagedFiles.join(", ")}
1142
+ Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
1079
1143
 
1080
1144
  Diff:
1081
- ${diff.slice(0, 4000)}${multiFileHint}`;
1082
- const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
1145
+ ${diffContent}${multiFileHint}`;
1146
+ const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
1083
1147
  return result?.trim() ?? null;
1084
1148
  } catch {
1085
1149
  return null;
@@ -1128,16 +1192,23 @@ ${conflictDiff.slice(0, 4000)}`;
1128
1192
  }
1129
1193
  }
1130
1194
  async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
1195
+ const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1196
+ const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
1197
+ const largeHint = isLarge ? `
1198
+
1199
+ NOTE: This is a large changeset (${files.length} files). Compact diffs are provided for every file. Focus on creating well-organized logical groups.` : "";
1131
1200
  const userMessage = `Group these changed files into logical atomic commits:
1132
1201
 
1133
1202
  Files:
1134
1203
  ${files.join(`
1135
1204
  `)}
1136
1205
 
1137
- Diffs (truncated):
1138
- ${diffs.slice(0, 6000)}`;
1206
+ Diffs:
1207
+ ${diffContent}${largeHint}`;
1139
1208
  const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
1140
1209
  if (!result) {
1210
+ if (isLarge)
1211
+ return generateCommitGroupsInBatches(files, diffs, model, convention);
1141
1212
  throw new Error("AI returned an empty response");
1142
1213
  }
1143
1214
  const cleaned = extractJson(result);
@@ -1145,10 +1216,14 @@ ${diffs.slice(0, 6000)}`;
1145
1216
  try {
1146
1217
  parsed = JSON.parse(cleaned);
1147
1218
  } catch {
1219
+ if (isLarge)
1220
+ return generateCommitGroupsInBatches(files, diffs, model, convention);
1148
1221
  throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
1149
1222
  }
1150
1223
  const groups = parsed;
1151
1224
  if (!Array.isArray(groups) || groups.length === 0) {
1225
+ if (isLarge)
1226
+ return generateCommitGroupsInBatches(files, diffs, model, convention);
1152
1227
  throw new Error("AI response was not a valid JSON array of commit groups");
1153
1228
  }
1154
1229
  for (const group of groups) {
@@ -1158,7 +1233,51 @@ ${diffs.slice(0, 6000)}`;
1158
1233
  }
1159
1234
  return groups;
1160
1235
  }
1236
+ async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
1237
+ const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
1238
+ const allGroups = [];
1239
+ const diffSections = parseDiffByFile(diffs);
1240
+ for (let i = 0;i < files.length; i += batchSize) {
1241
+ const batchFiles = files.slice(i, i + batchSize);
1242
+ const batchDiff = batchFiles.map((f) => diffSections.get(f) ?? "").filter(Boolean).join(`
1243
+ `);
1244
+ const batchDiffContent = batchFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? createCompactDiff(batchFiles, batchDiff) : batchDiff.slice(0, 6000);
1245
+ const batchNum = Math.floor(i / batchSize) + 1;
1246
+ const totalBatches = Math.ceil(files.length / batchSize);
1247
+ const userMessage = `Group these changed files into logical atomic commits:
1248
+
1249
+ Files:
1250
+ ${batchFiles.join(`
1251
+ `)}
1252
+
1253
+ Diffs:
1254
+ ${batchDiffContent}
1255
+
1256
+ NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group only the files listed above.`;
1257
+ try {
1258
+ const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
1259
+ if (!result)
1260
+ continue;
1261
+ const cleaned = extractJson(result);
1262
+ const parsed = JSON.parse(cleaned);
1263
+ if (Array.isArray(parsed)) {
1264
+ for (const group of parsed) {
1265
+ if (Array.isArray(group.files) && typeof group.message === "string") {
1266
+ allGroups.push(group);
1267
+ }
1268
+ }
1269
+ }
1270
+ } catch {}
1271
+ }
1272
+ if (allGroups.length === 0) {
1273
+ throw new Error("AI could not group any files even with batch processing");
1274
+ }
1275
+ return allGroups;
1276
+ }
1161
1277
  async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
1278
+ const totalFiles = groups.reduce((sum, g) => sum + g.files.length, 0);
1279
+ const isLarge = totalFiles >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1280
+ const diffContent = isLarge ? createCompactDiff(groups.flatMap((g) => g.files), diffs) : diffs.slice(0, 6000);
1162
1281
  const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
1163
1282
  `);
1164
1283
  const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
@@ -1166,8 +1285,8 @@ async function regenerateAllGroupMessages(groups, diffs, model, convention = "cl
1166
1285
  Groups:
1167
1286
  ${groupSummary}
1168
1287
 
1169
- Diffs (truncated):
1170
- ${diffs.slice(0, 6000)}`;
1288
+ Diffs:
1289
+ ${diffContent}`;
1171
1290
  const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
1172
1291
  if (!result)
1173
1292
  return groups;
@@ -1186,12 +1305,14 @@ ${diffs.slice(0, 6000)}`;
1186
1305
  }
1187
1306
  async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
1188
1307
  try {
1308
+ const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
1309
+ const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 4000);
1189
1310
  const userMessage = `Generate a single commit message for these files:
1190
1311
 
1191
1312
  Files: ${files.join(", ")}
1192
1313
 
1193
1314
  Diff:
1194
- ${diffs.slice(0, 4000)}`;
1315
+ ${diffContent}`;
1195
1316
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
1196
1317
  return result?.trim() ?? null;
1197
1318
  } catch {
@@ -1749,7 +1870,8 @@ ${pc6.bold("Changed files:")}`);
1749
1870
  warn(`AI unavailable: ${copilotError}`);
1750
1871
  warn("Falling back to manual commit message entry.");
1751
1872
  } else {
1752
- const spinner = createSpinner("Generating commit message with AI...");
1873
+ const spinnerMsg = stagedFiles.length >= 15 ? `Generating commit message with AI (${stagedFiles.length} files — using optimized batching)...` : "Generating commit message with AI...";
1874
+ const spinner = createSpinner(spinnerMsg);
1753
1875
  commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
1754
1876
  if (commitMessage) {
1755
1877
  spinner.success("AI commit message generated.");
@@ -1840,7 +1962,7 @@ ${pc6.bold("Changed files:")}`);
1840
1962
  for (const f of changedFiles) {
1841
1963
  console.log(` ${pc6.dim("•")} ${f}`);
1842
1964
  }
1843
- const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
1965
+ const spinner = createSpinner(changedFiles.length >= 15 ? `Asking AI to group ${changedFiles.length} file(s) into logical commits (using optimized batching)...` : `Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
1844
1966
  const diffs = await getFullDiffForFiles(changedFiles);
1845
1967
  if (!diffs.trim()) {
1846
1968
  spinner.stop();
@@ -2018,7 +2140,7 @@ import pc7 from "picocolors";
2018
2140
  // package.json
2019
2141
  var package_default = {
2020
2142
  name: "contribute-now",
2021
- version: "0.4.0-dev.d24b735",
2143
+ version: "0.4.0-dev.d48d9e6",
2022
2144
  description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
2023
2145
  type: "module",
2024
2146
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contribute-now",
3
- "version": "0.4.0-dev.d24b735",
3
+ "version": "0.4.0-dev.d48d9e6",
4
4
  "description": "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
5
5
  "type": "module",
6
6
  "bin": {