contribute-now 0.6.2-dev.38e14f5 → 0.6.2-dev.3d69403

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 +150 -37
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10103,12 +10103,12 @@ async function multiSelectPrompt(message, choices) {
10103
10103
  init_dist();
10104
10104
  var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
10105
10105
  Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
10106
- Rules: breaking (!) only for feat/fix/refactor/perf; imperative mood; max 72 chars; lowercase start; scope optional camelCase/kebab-case. Return ONLY the message line.
10106
+ Rules: breaking (!) only for feat/fix/refactor/perf; imperative mood; max 72 chars; lowercase start; scope optional camelCase/kebab-case. Do NOT use backticks, quotes, or markdown formatting around filenames, functions, or identifiers. Return ONLY the message line.
10107
10107
  Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
10108
10108
  var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
10109
10109
  Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
10110
10110
  Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
10111
- Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
10111
+ Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Do NOT use backticks, quotes, or markdown formatting around filenames, functions, or identifiers. Return ONLY the message line.
10112
10112
  Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
10113
10113
  WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
10114
10114
  function getGroupingSystemPrompt(convention) {
@@ -10133,6 +10133,7 @@ Rules:
10133
10133
  - Each group should represent ONE logical change
10134
10134
  - Every file must appear in exactly one group
10135
10135
  - Commit messages must follow the convention, be concise, imperative, max 72 chars
10136
+ - Do not use backticks, quotes, or markdown formatting in commit messages
10136
10137
  - Order groups so foundational changes come first (types, utils) and consumers come after
10137
10138
  - Return ONLY the JSON array, nothing else`;
10138
10139
  }
@@ -10351,6 +10352,9 @@ function extractJson(raw) {
10351
10352
  }
10352
10353
  return text;
10353
10354
  }
10355
+ function sanitizeGeneratedCommitMessage(message) {
10356
+ return message.replace(/`+/g, "").replace(/\s+/g, " ").trim();
10357
+ }
10354
10358
  async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit", context) {
10355
10359
  try {
10356
10360
  const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
@@ -10368,7 +10372,7 @@ Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
10368
10372
  Diff:
10369
10373
  ${diffContent}${multiFileHint}${squashHint}`;
10370
10374
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
10371
- return result?.trim() ?? null;
10375
+ return result ? sanitizeGeneratedCommitMessage(result) : null;
10372
10376
  } catch {
10373
10377
  return null;
10374
10378
  }
@@ -10415,6 +10419,40 @@ ${conflictDiff.slice(0, 4000)}`;
10415
10419
  return null;
10416
10420
  }
10417
10421
  }
10422
+ function normalizeCommitGroups(changedFiles, groups) {
10423
+ const changedSet = new Set(changedFiles);
10424
+ const assignedFiles = new Set;
10425
+ const unknownFiles = new Set;
10426
+ const duplicateFiles = new Set;
10427
+ const normalizedGroups = groups.map((group) => {
10428
+ const uniqueFiles = new Set;
10429
+ const files = [];
10430
+ for (const file of group.files) {
10431
+ if (!changedSet.has(file)) {
10432
+ unknownFiles.add(file);
10433
+ continue;
10434
+ }
10435
+ if (uniqueFiles.has(file) || assignedFiles.has(file)) {
10436
+ duplicateFiles.add(file);
10437
+ continue;
10438
+ }
10439
+ uniqueFiles.add(file);
10440
+ assignedFiles.add(file);
10441
+ files.push(file);
10442
+ }
10443
+ return {
10444
+ ...group,
10445
+ files
10446
+ };
10447
+ }).filter((group) => group.files.length > 0);
10448
+ const unassignedFiles = changedFiles.filter((file) => !assignedFiles.has(file));
10449
+ return {
10450
+ groups: normalizedGroups,
10451
+ unknownFiles: [...unknownFiles],
10452
+ duplicateFiles: [...duplicateFiles],
10453
+ unassignedFiles
10454
+ };
10455
+ }
10418
10456
  async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
10419
10457
  const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
10420
10458
  const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
@@ -10455,7 +10493,10 @@ ${diffContent}${largeHint}`;
10455
10493
  throw new Error("AI returned groups with invalid structure (missing files or message)");
10456
10494
  }
10457
10495
  }
10458
- return groups;
10496
+ return groups.map((group) => ({
10497
+ ...group,
10498
+ message: sanitizeGeneratedCommitMessage(group.message)
10499
+ }));
10459
10500
  }
10460
10501
  async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
10461
10502
  const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
@@ -10490,7 +10531,11 @@ NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group o
10490
10531
  const batchFileSet = new Set(batchFiles);
10491
10532
  const filteredFiles = group.files.filter((f3) => batchFileSet.has(f3));
10492
10533
  if (filteredFiles.length > 0) {
10493
- allGroups.push({ ...group, files: filteredFiles });
10534
+ allGroups.push({
10535
+ ...group,
10536
+ files: filteredFiles,
10537
+ message: sanitizeGeneratedCommitMessage(group.message)
10538
+ });
10494
10539
  }
10495
10540
  }
10496
10541
  }
@@ -10533,7 +10578,7 @@ ${diffContent}`;
10533
10578
  return groups;
10534
10579
  return groups.map((g3, i2) => ({
10535
10580
  files: g3.files,
10536
- message: typeof parsed[i2]?.message === "string" ? parsed[i2].message : g3.message
10581
+ message: typeof parsed[i2]?.message === "string" ? sanitizeGeneratedCommitMessage(parsed[i2].message) : g3.message
10537
10582
  }));
10538
10583
  } catch {
10539
10584
  return groups;
@@ -10550,7 +10595,7 @@ Files: ${files.join(", ")}
10550
10595
  Diff:
10551
10596
  ${diffContent}`;
10552
10597
  const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
10553
- return result?.trim() ?? null;
10598
+ return result ? sanitizeGeneratedCommitMessage(result) : null;
10554
10599
  } catch {
10555
10600
  return null;
10556
10601
  }
@@ -10986,17 +11031,24 @@ var CONVENTION_FORMAT_HINTS = {
10986
11031
  conventional: [
10987
11032
  "Format: <type>[!][(<scope>)]: <description>",
10988
11033
  "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
10989
- "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README"
11034
+ "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README",
11035
+ "Do not use backticks or markdown formatting in the message."
10990
11036
  ],
10991
11037
  "clean-commit": [
10992
11038
  "Format: <emoji> <type>[!][(<scope>)]: <description>",
10993
11039
  "Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1️ remove | \uD83D\uDD12 security | ⚙️ setup | ☕ chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
10994
- "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow"
11040
+ "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow",
11041
+ "Do not use backticks or markdown formatting in the message."
10995
11042
  ]
10996
11043
  };
11044
+ function hasUnsupportedCommitMessageChars(message) {
11045
+ return message.includes("`");
11046
+ }
10997
11047
  function validateCommitMessage(message, convention) {
10998
11048
  if (convention === "none")
10999
11049
  return true;
11050
+ if (hasUnsupportedCommitMessageChars(message))
11051
+ return false;
11000
11052
  if (convention === "clean-commit")
11001
11053
  return CLEAN_COMMIT_PATTERN.test(message);
11002
11054
  if (convention === "conventional")
@@ -11008,6 +11060,7 @@ function getValidationError(convention) {
11008
11060
  return [];
11009
11061
  return [
11010
11062
  `Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
11063
+ "Do not use backticks or markdown formatting in commit messages.",
11011
11064
  ...CONVENTION_FORMAT_HINTS[convention]
11012
11065
  ];
11013
11066
  }
@@ -11200,6 +11253,15 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11200
11253
  success(`Committed: ${import_picocolors8.default.bold(finalMessage)}`);
11201
11254
  }
11202
11255
  });
11256
+ function getFallbackGroupMessage(convention) {
11257
+ if (convention === "conventional") {
11258
+ return "chore: commit remaining changes";
11259
+ }
11260
+ if (convention === "clean-commit") {
11261
+ return "☕ chore: commit remaining changes";
11262
+ }
11263
+ return "commit remaining changes";
11264
+ }
11203
11265
  async function runGroupCommit(model, config) {
11204
11266
  const [copilotError, changedFiles] = await Promise.all([
11205
11267
  checkCopilotAvailable(),
@@ -11237,15 +11299,25 @@ ${import_picocolors8.default.bold("Changed files:")}`);
11237
11299
  error("AI could not produce commit groups. Try committing files manually.");
11238
11300
  process.exit(1);
11239
11301
  }
11240
- const changedSet = new Set(changedFiles);
11241
- for (const group of groups) {
11242
- const invalid = group.files.filter((f3) => !changedSet.has(f3));
11243
- if (invalid.length > 0) {
11244
- warn(`AI suggested unknown file(s): ${invalid.join(", ")} — removed from group.`);
11245
- }
11246
- group.files = group.files.filter((f3) => changedSet.has(f3));
11302
+ const normalized = normalizeCommitGroups(changedFiles, groups);
11303
+ if (normalized.unknownFiles.length > 0) {
11304
+ warn(`AI suggested unknown file(s): ${normalized.unknownFiles.join(", ")} removed from groups.`);
11305
+ }
11306
+ if (normalized.duplicateFiles.length > 0) {
11307
+ warn(`AI assigned duplicate file(s) across groups: ${normalized.duplicateFiles.join(", ")} — keeping the first assignment only.`);
11308
+ }
11309
+ let validGroups = normalized.groups;
11310
+ if (normalized.unassignedFiles.length > 0) {
11311
+ warn(`AI left ${normalized.unassignedFiles.length} file(s) ungrouped: ${normalized.unassignedFiles.join(", ")}. Creating a fallback group.`);
11312
+ const fallbackMessage = await regenerateGroupMessage(normalized.unassignedFiles, diffs, model, config.commitConvention) ?? getFallbackGroupMessage(config.commitConvention);
11313
+ validGroups = [
11314
+ ...validGroups,
11315
+ {
11316
+ files: normalized.unassignedFiles,
11317
+ message: fallbackMessage
11318
+ }
11319
+ ];
11247
11320
  }
11248
- let validGroups = groups.filter((g3) => g3.files.length > 0);
11249
11321
  if (validGroups.length === 0) {
11250
11322
  error("No valid groups remain after validation. Try committing files manually.");
11251
11323
  process.exit(1);
@@ -11291,7 +11363,17 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11291
11363
  if (commitAll) {
11292
11364
  for (let i2 = 0;i2 < validGroups.length; i2++) {
11293
11365
  const group = validGroups[i2];
11294
- const stageResult = await stageFiles(group.files);
11366
+ const remainingChangedFiles = new Set(await getChangedFiles());
11367
+ const stageableFiles = group.files.filter((file) => remainingChangedFiles.has(file));
11368
+ const skippedFiles = group.files.filter((file) => !remainingChangedFiles.has(file));
11369
+ if (skippedFiles.length > 0) {
11370
+ warn(`Group ${i2 + 1} file(s) no longer have changes: ${skippedFiles.join(", ")}`);
11371
+ }
11372
+ if (stageableFiles.length === 0) {
11373
+ warn(`Skipped group ${i2 + 1}: no files remain to commit.`);
11374
+ continue;
11375
+ }
11376
+ const stageResult = await stageFiles(stageableFiles);
11295
11377
  if (stageResult.exitCode !== 0) {
11296
11378
  error(`Failed to stage group ${i2 + 1}: ${stageResult.stderr}`);
11297
11379
  continue;
@@ -11300,7 +11382,7 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11300
11382
  if (commitResult.exitCode !== 0) {
11301
11383
  const detail = (commitResult.stderr || commitResult.stdout).trim();
11302
11384
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11303
- await unstageFiles(group.files);
11385
+ await unstageFiles(stageableFiles);
11304
11386
  continue;
11305
11387
  }
11306
11388
  committed++;
@@ -11360,7 +11442,18 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11360
11442
  continue;
11361
11443
  }
11362
11444
  }
11363
- const stageResult = await stageFiles(group.files);
11445
+ const remainingChangedFiles = new Set(await getChangedFiles());
11446
+ const stageableFiles = group.files.filter((file) => remainingChangedFiles.has(file));
11447
+ const skippedFiles = group.files.filter((file) => !remainingChangedFiles.has(file));
11448
+ if (skippedFiles.length > 0) {
11449
+ warn(`Group ${i2 + 1} file(s) no longer have changes: ${skippedFiles.join(", ")}`);
11450
+ }
11451
+ if (stageableFiles.length === 0) {
11452
+ warn(`Skipped group ${i2 + 1}: no files remain to commit.`);
11453
+ actionDone = true;
11454
+ continue;
11455
+ }
11456
+ const stageResult = await stageFiles(stageableFiles);
11364
11457
  if (stageResult.exitCode !== 0) {
11365
11458
  error(`Failed to stage group ${i2 + 1}: ${stageResult.stderr}`);
11366
11459
  actionDone = true;
@@ -11370,7 +11463,7 @@ ${import_picocolors8.default.bold(`AI suggested ${validGroups.length} commit gro
11370
11463
  if (commitResult.exitCode !== 0) {
11371
11464
  const detail = (commitResult.stderr || commitResult.stdout).trim();
11372
11465
  error(`Failed to commit group ${i2 + 1}: ${detail}`);
11373
- await unstageFiles(group.files);
11466
+ await unstageFiles(stageableFiles);
11374
11467
  actionDone = true;
11375
11468
  continue;
11376
11469
  }
@@ -11395,7 +11488,7 @@ var import_picocolors9 = __toESM(require_picocolors(), 1);
11395
11488
  // package.json
11396
11489
  var package_default = {
11397
11490
  name: "contribute-now",
11398
- version: "0.6.2-dev.38e14f5",
11491
+ version: "0.6.2-dev.3d69403",
11399
11492
  description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
11400
11493
  type: "module",
11401
11494
  bin: {
@@ -11806,13 +11899,13 @@ esac
11806
11899
 
11807
11900
  # Detect available package runner
11808
11901
  if command -v contrib >/dev/null 2>&1; then
11809
- contrib validate "$commit_msg"
11902
+ contrib validate --file "$commit_msg_file"
11810
11903
  elif command -v bunx >/dev/null 2>&1; then
11811
- bunx contrib validate "$commit_msg"
11904
+ bunx contrib validate --file "$commit_msg_file"
11812
11905
  elif command -v pnpx >/dev/null 2>&1; then
11813
- pnpx contrib validate "$commit_msg"
11906
+ pnpx contrib validate --file "$commit_msg_file"
11814
11907
  elif command -v npx >/dev/null 2>&1; then
11815
- npx contrib validate "$commit_msg"
11908
+ npx contrib validate --file "$commit_msg_file"
11816
11909
  else
11817
11910
  echo "Warning: No package runner found. Skipping commit message validation."
11818
11911
  exit 0
@@ -12965,16 +13058,27 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
12965
13058
  if (!message) {
12966
13059
  const copilotError = await checkCopilotAvailable();
12967
13060
  if (!copilotError) {
12968
- const spinner = createSpinner("Generating AI commit message for squash merge...");
12969
- const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
12970
- const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
12971
- if (aiMsg) {
12972
- message = aiMsg;
12973
- spinner.success("AI commit message generated.");
12974
- console.log(`
13061
+ while (!message) {
13062
+ const spinner = createSpinner("Generating AI commit message for squash merge...");
13063
+ const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
13064
+ const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
13065
+ if (aiMsg) {
13066
+ message = aiMsg;
13067
+ spinner.success("AI commit message generated.");
13068
+ console.log(`
12975
13069
  ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(message))}`);
12976
- } else {
13070
+ break;
13071
+ }
12977
13072
  spinner.fail("AI did not return a commit message.");
13073
+ const retryAction = await selectPrompt("AI could not generate a commit message. What would you like to do?", ["Try again with AI", "Write manually", "Cancel"]);
13074
+ if (retryAction === "Try again with AI") {
13075
+ continue;
13076
+ }
13077
+ if (retryAction === "Cancel") {
13078
+ warn("Squash merge commit cancelled.");
13079
+ process.exit(0);
13080
+ }
13081
+ break;
12978
13082
  }
12979
13083
  } else {
12980
13084
  warn(`AI unavailable: ${copilotError}`);
@@ -13004,7 +13108,7 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
13004
13108
  ${import_picocolors16.default.dim("AI suggestion:")} ${import_picocolors16.default.bold(import_picocolors16.default.cyan(regen))}`);
13005
13109
  } else {
13006
13110
  spinner.fail("Regeneration failed.");
13007
- finalMsg = await inputPrompt("Enter commit message");
13111
+ continue;
13008
13112
  }
13009
13113
  } else {
13010
13114
  finalMsg = await inputPrompt("Enter commit message");
@@ -14040,6 +14144,7 @@ ${import_picocolors19.default.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance
14040
14144
 
14041
14145
  // src/commands/validate.ts
14042
14146
  var import_picocolors20 = __toESM(require_picocolors(), 1);
14147
+ import { readFileSync as readFileSync5 } from "node:fs";
14043
14148
  var validate_default = defineCommand({
14044
14149
  meta: {
14045
14150
  name: "validate",
@@ -14049,7 +14154,11 @@ var validate_default = defineCommand({
14049
14154
  message: {
14050
14155
  type: "positional",
14051
14156
  description: "The commit message to validate",
14052
- required: true
14157
+ required: false
14158
+ },
14159
+ file: {
14160
+ type: "string",
14161
+ description: "Path to a commit message file; only the first line is validated"
14053
14162
  }
14054
14163
  },
14055
14164
  async run({ args }) {
@@ -14063,7 +14172,11 @@ var validate_default = defineCommand({
14063
14172
  info('Commit convention is set to "none". All messages are accepted.');
14064
14173
  process.exit(0);
14065
14174
  }
14066
- const message = args.message;
14175
+ const message = args.file ? readFileSync5(args.file, "utf-8").split(/\r?\n/, 1)[0] ?? "" : args.message;
14176
+ if (!message) {
14177
+ error("No commit message provided. Pass a message or use --file <path>.");
14178
+ process.exit(1);
14179
+ }
14067
14180
  if (validateCommitMessage(message, convention)) {
14068
14181
  success(`Valid ${CONVENTION_LABELS[convention]} message.`);
14069
14182
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contribute-now",
3
- "version": "0.6.2-dev.38e14f5",
3
+ "version": "0.6.2-dev.3d69403",
4
4
  "description": "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
5
5
  "type": "module",
6
6
  "bin": {