contribute-now 0.1.1 → 0.1.2-dev.c209cc7

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 (3) hide show
  1. package/README.md +162 -133
  2. package/dist/index.js +492 -197
  3. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { defineCommand as defineCommand9, runMain } from "citty";
4
+ import { defineCommand as defineCommand11, runMain } from "citty";
5
5
 
6
6
  // src/commands/clean.ts
7
7
  import { defineCommand } from "citty";
@@ -44,12 +44,14 @@ function isGitignored(cwd = process.cwd()) {
44
44
  }
45
45
  function getDefaultConfig() {
46
46
  return {
47
+ workflow: "clean-flow",
47
48
  role: "contributor",
48
49
  mainBranch: "main",
49
50
  devBranch: "dev",
50
51
  upstream: "upstream",
51
52
  origin: "origin",
52
- branchPrefixes: ["feature", "fix", "docs", "chore", "test", "refactor"]
53
+ branchPrefixes: ["feature", "fix", "docs", "chore", "test", "refactor"],
54
+ commitConvention: "clean-commit"
53
55
  };
54
56
  }
55
57
 
@@ -167,9 +169,6 @@ async function createBranch(branch, from) {
167
169
  async function resetHard(ref) {
168
170
  return run(["reset", "--hard", ref]);
169
171
  }
170
- async function pushForceWithLease(remote, branch) {
171
- return run(["push", "--force-with-lease", remote, branch]);
172
- }
173
172
  async function pushSetUpstream(remote, branch) {
174
173
  return run(["push", "-u", remote, branch]);
175
174
  }
@@ -236,6 +235,9 @@ async function getLog(base, head) {
236
235
  return stdout.trim().split(`
237
236
  `).filter(Boolean);
238
237
  }
238
+ async function pullBranch(remote, branch) {
239
+ return run(["pull", remote, branch]);
240
+ }
239
241
 
240
242
  // src/utils/logger.ts
241
243
  import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
@@ -265,6 +267,53 @@ function heading(msg) {
265
267
  ${pc2.bold(msg)}`);
266
268
  }
267
269
 
270
+ // src/utils/workflow.ts
271
+ var WORKFLOW_DESCRIPTIONS = {
272
+ "clean-flow": "Clean Flow — main + dev, squash features into dev, merge dev into main",
273
+ "github-flow": "GitHub Flow — main + feature branches, squash/merge into main",
274
+ "git-flow": "Git Flow — main + develop + release + hotfix branches"
275
+ };
276
+ function getBaseBranch(config) {
277
+ switch (config.workflow) {
278
+ case "clean-flow":
279
+ case "git-flow":
280
+ return config.devBranch ?? "dev";
281
+ case "github-flow":
282
+ return config.mainBranch;
283
+ }
284
+ }
285
+ function hasDevBranch(workflow) {
286
+ return workflow === "clean-flow" || workflow === "git-flow";
287
+ }
288
+ function getSyncSource(config) {
289
+ const { workflow, role, mainBranch, origin, upstream } = config;
290
+ const devBranch = config.devBranch ?? "dev";
291
+ switch (workflow) {
292
+ case "clean-flow":
293
+ if (role === "contributor") {
294
+ return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
295
+ }
296
+ return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
297
+ case "github-flow":
298
+ if (role === "contributor") {
299
+ return { remote: upstream, ref: `${upstream}/${mainBranch}`, strategy: "pull" };
300
+ }
301
+ return { remote: origin, ref: `${origin}/${mainBranch}`, strategy: "pull" };
302
+ case "git-flow":
303
+ if (role === "contributor") {
304
+ return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
305
+ }
306
+ return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
307
+ }
308
+ }
309
+ function getProtectedBranches(config) {
310
+ const branches = [config.mainBranch];
311
+ if (hasDevBranch(config.workflow) && config.devBranch) {
312
+ branches.push(config.devBranch);
313
+ }
314
+ return branches;
315
+ }
316
+
268
317
  // src/commands/clean.ts
269
318
  var clean_default = defineCommand({
270
319
  meta: {
@@ -289,12 +338,13 @@ var clean_default = defineCommand({
289
338
  error("No .contributerc.json found. Run `contrib setup` first.");
290
339
  process.exit(1);
291
340
  }
292
- const { mainBranch, devBranch, origin } = config;
341
+ const { origin } = config;
342
+ const baseBranch = getBaseBranch(config);
293
343
  const currentBranch = await getCurrentBranch();
294
344
  heading("\uD83E\uDDF9 contrib clean");
295
- const mergedBranches = await getMergedBranches(devBranch);
296
- const protected_ = new Set([mainBranch, devBranch, currentBranch ?? ""]);
297
- const candidates = mergedBranches.filter((b) => !protected_.has(b));
345
+ const mergedBranches = await getMergedBranches(baseBranch);
346
+ const protectedBranches = new Set([...getProtectedBranches(config), currentBranch ?? ""]);
347
+ const candidates = mergedBranches.filter((b) => !protectedBranches.has(b));
298
348
  if (candidates.length === 0) {
299
349
  info("No merged branches to clean up.");
300
350
  } else {
@@ -332,8 +382,78 @@ ${pc3.bold("Branches to delete:")}`);
332
382
  import { defineCommand as defineCommand2 } from "citty";
333
383
  import pc4 from "picocolors";
334
384
 
385
+ // src/utils/convention.ts
386
+ var CLEAN_COMMIT_PATTERN = /^(📦|🔧|🗑\uFE0F?|🔒|⚙\uFE0F?|☕|🧪|📖|🚀) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
387
+ var CONVENTIONAL_COMMIT_PATTERN = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(!?)(\([a-zA-Z0-9][a-zA-Z0-9._-]*\))?: .{1,72}$/;
388
+ var CONVENTION_LABELS = {
389
+ conventional: "Conventional Commits",
390
+ "clean-commit": "Clean Commit (by WGTech Labs)",
391
+ none: "No convention"
392
+ };
393
+ var CONVENTION_DESCRIPTIONS = {
394
+ conventional: "Conventional Commits — feat: | fix: | docs: | chore: etc. (conventionalcommits.org)",
395
+ "clean-commit": "Clean Commit — \uD83D\uDCE6 new: | \uD83D\uDD27 update: | \uD83D\uDDD1️ remove: etc. (by WGTech Labs)",
396
+ none: "No commit convention enforcement"
397
+ };
398
+ var CONVENTION_FORMAT_HINTS = {
399
+ conventional: [
400
+ "Format: <type>[!][(<scope>)]: <description>",
401
+ "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
402
+ "Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README"
403
+ ],
404
+ "clean-commit": [
405
+ "Format: <emoji> <type>[!][(<scope>)]: <description>",
406
+ "Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1️ remove | \uD83D\uDD12 security | ⚙️ setup | ☕ chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
407
+ "Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow"
408
+ ]
409
+ };
410
+ function validateCommitMessage(message, convention) {
411
+ if (convention === "none")
412
+ return true;
413
+ if (convention === "clean-commit")
414
+ return CLEAN_COMMIT_PATTERN.test(message);
415
+ if (convention === "conventional")
416
+ return CONVENTIONAL_COMMIT_PATTERN.test(message);
417
+ return true;
418
+ }
419
+ function getValidationError(convention) {
420
+ if (convention === "none")
421
+ return [];
422
+ return [
423
+ `Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
424
+ ...CONVENTION_FORMAT_HINTS[convention]
425
+ ];
426
+ }
427
+
335
428
  // src/utils/copilot.ts
336
429
  import { CopilotClient } from "@github/copilot-sdk";
430
+ var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `You are a git commit message generator. Generate a Conventional Commit message following this exact format:
431
+ <type>[!][(<scope>)]: <description>
432
+
433
+ Types:
434
+ feat – a new feature
435
+ fix – a bug fix
436
+ docs – documentation only changes
437
+ style – changes that do not affect code meaning (whitespace, formatting)
438
+ refactor – code change that neither fixes a bug nor adds a feature
439
+ perf – performance improvement
440
+ test – adding or correcting tests
441
+ build – changes to the build system or external dependencies
442
+ ci – changes to CI configuration files and scripts
443
+ chore – other changes that don't modify src or test files
444
+ revert – reverts a previous commit
445
+
446
+ Rules:
447
+ - Breaking change (!) only for: feat, fix, refactor, perf
448
+ - Description: concise, imperative mood, max 72 chars, lowercase start
449
+ - Scope: optional, camelCase or kebab-case component name
450
+ - Return ONLY the commit message line, nothing else
451
+
452
+ Examples:
453
+ feat: add user authentication system
454
+ fix(auth): resolve token expiry issue
455
+ docs: update contributing guidelines
456
+ feat!: redesign authentication API`;
337
457
  var CLEAN_COMMIT_SYSTEM_PROMPT = `You are a git commit message generator. Generate a Clean Commit message following this exact format:
338
458
  <emoji> <type>[!][(<scope>)]: <description>
339
459
 
@@ -444,7 +564,12 @@ async function callCopilot(systemMessage, userMessage, model) {
444
564
  await client.stop();
445
565
  }
446
566
  }
447
- async function generateCommitMessage(diff, stagedFiles, model) {
567
+ function getCommitSystemPrompt(convention) {
568
+ if (convention === "conventional")
569
+ return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
570
+ return CLEAN_COMMIT_SYSTEM_PROMPT;
571
+ }
572
+ async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
448
573
  try {
449
574
  const userMessage = `Generate a commit message for these staged changes:
450
575
 
@@ -452,7 +577,7 @@ Files: ${stagedFiles.join(", ")}
452
577
 
453
578
  Diff:
454
579
  ${diff.slice(0, 4000)}`;
455
- const result = await callCopilot(CLEAN_COMMIT_SYSTEM_PROMPT, userMessage, model);
580
+ const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
456
581
  return result?.trim() ?? null;
457
582
  } catch {
458
583
  return null;
@@ -498,14 +623,10 @@ ${conflictDiff.slice(0, 4000)}`;
498
623
  }
499
624
 
500
625
  // src/commands/commit.ts
501
- var CLEAN_COMMIT_PATTERN = /^(📦|🔧|🗑\uFE0F?|🔒|⚙\uFE0F?|☕|🧪|📖|🚀) (new|update|remove|security|setup|chore|test|docs|release)(!?)( \([a-zA-Z0-9][a-zA-Z0-9-]*\))?: .{1,72}$/u;
502
- function validateCleanCommit(msg) {
503
- return CLEAN_COMMIT_PATTERN.test(msg);
504
- }
505
626
  var commit_default = defineCommand2({
506
627
  meta: {
507
628
  name: "commit",
508
- description: "Stage changes and create a Clean Commit message (AI-powered)"
629
+ description: "Stage changes and create a commit message (AI-powered)"
509
630
  },
510
631
  args: {
511
632
  model: {
@@ -556,7 +677,7 @@ ${pc4.bold("Changed files:")}`);
556
677
  } else {
557
678
  info("Generating commit message with AI...");
558
679
  const diff = await getStagedDiff();
559
- commitMessage = await generateCommitMessage(diff, stagedFiles, args.model);
680
+ commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
560
681
  if (commitMessage) {
561
682
  console.log(`
562
683
  ${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(commitMessage))}`);
@@ -580,7 +701,7 @@ ${pc4.bold("Changed files:")}`);
580
701
  } else if (action === "Regenerate") {
581
702
  info("Regenerating...");
582
703
  const diff = await getStagedDiff();
583
- const regen = await generateCommitMessage(diff, stagedFiles, args.model);
704
+ const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
584
705
  if (regen) {
585
706
  console.log(`
586
707
  ${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(regen))}`);
@@ -594,19 +715,25 @@ ${pc4.bold("Changed files:")}`);
594
715
  finalMessage = await inputPrompt("Enter commit message");
595
716
  }
596
717
  } else {
597
- console.log();
598
- console.log(pc4.dim("Clean Commit format: <emoji> <type>[!][(<scope>)]: <description>"));
599
- console.log(pc4.dim("Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors"));
600
- console.log();
718
+ const convention2 = config.commitConvention;
719
+ if (convention2 !== "none") {
720
+ console.log();
721
+ for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
722
+ console.log(pc4.dim(hint));
723
+ }
724
+ console.log();
725
+ }
601
726
  finalMessage = await inputPrompt("Enter commit message");
602
727
  }
603
728
  if (!finalMessage) {
604
729
  error("No commit message provided.");
605
730
  process.exit(1);
606
731
  }
607
- if (!validateCleanCommit(finalMessage)) {
608
- warn("Commit message does not follow Clean Commit format.");
609
- warn("Format: <emoji> <type>[!][(<scope>)]: <description>");
732
+ const convention = config.commitConvention;
733
+ if (!validateCommitMessage(finalMessage, convention)) {
734
+ for (const line of getValidationError(convention)) {
735
+ warn(line);
736
+ }
610
737
  const proceed = await confirmPrompt("Commit anyway?");
611
738
  if (!proceed)
612
739
  process.exit(1);
@@ -620,9 +747,117 @@ ${pc4.bold("Changed files:")}`);
620
747
  }
621
748
  });
622
749
 
623
- // src/commands/setup.ts
750
+ // src/commands/hook.ts
751
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
752
+ import { join as join2 } from "node:path";
624
753
  import { defineCommand as defineCommand3 } from "citty";
625
754
  import pc5 from "picocolors";
755
+ var HOOK_MARKER = "# managed by contribute-now";
756
+ function getHooksDir(cwd = process.cwd()) {
757
+ return join2(cwd, ".git", "hooks");
758
+ }
759
+ function getHookPath(cwd = process.cwd()) {
760
+ return join2(getHooksDir(cwd), "commit-msg");
761
+ }
762
+ function generateHookScript() {
763
+ return `#!/bin/sh
764
+ ${HOOK_MARKER}
765
+ # Validates commit messages against your configured convention.
766
+ # Install: contrib hook install
767
+ # Uninstall: contrib hook uninstall
768
+
769
+ commit_msg_file="$1"
770
+ commit_msg=$(head -1 "$commit_msg_file")
771
+
772
+ # Skip merge commits and fixup/squash commits
773
+ case "$commit_msg" in
774
+ Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
775
+ esac
776
+
777
+ # Validate using contrib CLI
778
+ npx contrib validate "$commit_msg"
779
+ `;
780
+ }
781
+ var hook_default = defineCommand3({
782
+ meta: {
783
+ name: "hook",
784
+ description: "Install or uninstall the commit-msg git hook"
785
+ },
786
+ args: {
787
+ action: {
788
+ type: "positional",
789
+ description: "Action to perform: install or uninstall",
790
+ required: true
791
+ }
792
+ },
793
+ async run({ args }) {
794
+ if (!await isGitRepo()) {
795
+ error("Not inside a git repository.");
796
+ process.exit(1);
797
+ }
798
+ const action = args.action;
799
+ if (action !== "install" && action !== "uninstall") {
800
+ error(`Unknown action "${action}". Use "install" or "uninstall".`);
801
+ process.exit(1);
802
+ }
803
+ if (action === "install") {
804
+ await installHook();
805
+ } else {
806
+ await uninstallHook();
807
+ }
808
+ }
809
+ });
810
+ async function installHook() {
811
+ heading("\uD83E\uDE9D hook install");
812
+ const config = readConfig();
813
+ if (!config) {
814
+ error("No .contributerc.json found. Run `contrib setup` first.");
815
+ process.exit(1);
816
+ }
817
+ if (config.commitConvention === "none") {
818
+ warn('Commit convention is set to "none". No hook to install.');
819
+ info("Change your convention with `contrib setup` first.");
820
+ process.exit(0);
821
+ }
822
+ const hookPath = getHookPath();
823
+ const hooksDir = getHooksDir();
824
+ if (existsSync2(hookPath)) {
825
+ const existing = readFileSync2(hookPath, "utf-8");
826
+ if (!existing.includes(HOOK_MARKER)) {
827
+ error("A commit-msg hook already exists and was not installed by contribute-now.");
828
+ warn(`Path: ${hookPath}`);
829
+ warn("Remove it manually or back it up before installing.");
830
+ process.exit(1);
831
+ }
832
+ info("Updating existing contribute-now hook...");
833
+ }
834
+ if (!existsSync2(hooksDir)) {
835
+ mkdirSync(hooksDir, { recursive: true });
836
+ }
837
+ writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
838
+ success(`commit-msg hook installed.`);
839
+ info(`Convention: ${pc5.bold(CONVENTION_LABELS[config.commitConvention])}`);
840
+ info(`Path: ${pc5.dim(hookPath)}`);
841
+ }
842
+ async function uninstallHook() {
843
+ heading("\uD83E\uDE9D hook uninstall");
844
+ const hookPath = getHookPath();
845
+ if (!existsSync2(hookPath)) {
846
+ info("No commit-msg hook found. Nothing to uninstall.");
847
+ return;
848
+ }
849
+ const content = readFileSync2(hookPath, "utf-8");
850
+ if (!content.includes(HOOK_MARKER)) {
851
+ error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
852
+ process.exit(1);
853
+ }
854
+ rmSync(hookPath);
855
+ success("commit-msg hook removed.");
856
+ }
857
+
858
+ // src/commands/setup.ts
859
+ import { defineCommand as defineCommand4 } from "citty";
860
+ import pc6 from "picocolors";
626
861
 
627
862
  // src/utils/gh.ts
628
863
  import { execFile as execFileCb2 } from "node:child_process";
@@ -735,7 +970,7 @@ async function getRepoInfoFromRemote(remote = "origin") {
735
970
  }
736
971
 
737
972
  // src/commands/setup.ts
738
- var setup_default = defineCommand3({
973
+ var setup_default = defineCommand4({
739
974
  meta: {
740
975
  name: "setup",
741
976
  description: "Initialize contribute-now config for this repo (.contributerc.json)"
@@ -746,6 +981,27 @@ var setup_default = defineCommand3({
746
981
  process.exit(1);
747
982
  }
748
983
  heading("\uD83D\uDD27 contribute-now setup");
984
+ const workflowChoice = await selectPrompt("Which git workflow does this project use?", [
985
+ "Clean Flow — main + dev, squash features into dev, merge dev into main (recommended)",
986
+ "GitHub Flow — main + feature branches, squash/merge into main",
987
+ "Git Flow — main + develop + release + hotfix branches"
988
+ ]);
989
+ let workflow = "clean-flow";
990
+ if (workflowChoice.startsWith("GitHub"))
991
+ workflow = "github-flow";
992
+ else if (workflowChoice.startsWith("Git Flow"))
993
+ workflow = "git-flow";
994
+ info(`Workflow: ${pc6.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
995
+ const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
996
+ `${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
997
+ CONVENTION_DESCRIPTIONS.conventional,
998
+ CONVENTION_DESCRIPTIONS.none
999
+ ]);
1000
+ let commitConvention = "clean-commit";
1001
+ if (conventionChoice.includes("Conventional Commits"))
1002
+ commitConvention = "conventional";
1003
+ else if (conventionChoice.includes("No commit"))
1004
+ commitConvention = "none";
749
1005
  const remotes = await getRemotes();
750
1006
  if (remotes.length === 0) {
751
1007
  error("No git remotes found. Add a remote first (e.g., git remote add origin <url>).");
@@ -788,8 +1044,8 @@ var setup_default = defineCommand3({
788
1044
  detectedRole = roleChoice;
789
1045
  detectionSource = "user selection";
790
1046
  } else {
791
- info(`Detected role: ${pc5.bold(detectedRole)} (via ${detectionSource})`);
792
- const confirmed = await confirmPrompt(`Role detected as ${pc5.bold(detectedRole)}. Is this correct?`);
1047
+ info(`Detected role: ${pc6.bold(detectedRole)} (via ${detectionSource})`);
1048
+ const confirmed = await confirmPrompt(`Role detected as ${pc6.bold(detectedRole)}. Is this correct?`);
793
1049
  if (!confirmed) {
794
1050
  const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
795
1051
  detectedRole = roleChoice;
@@ -797,7 +1053,11 @@ var setup_default = defineCommand3({
797
1053
  }
798
1054
  const defaultConfig = getDefaultConfig();
799
1055
  const mainBranch = await inputPrompt("Main branch name", defaultConfig.mainBranch);
800
- const devBranch = await inputPrompt("Dev branch name", defaultConfig.devBranch);
1056
+ let devBranch;
1057
+ if (hasDevBranch(workflow)) {
1058
+ const defaultDev = workflow === "git-flow" ? "develop" : "dev";
1059
+ devBranch = await inputPrompt("Dev/develop branch name", defaultDev);
1060
+ }
801
1061
  const originRemote = await inputPrompt("Origin remote name", defaultConfig.origin);
802
1062
  let upstreamRemote = defaultConfig.upstream;
803
1063
  if (detectedRole === "contributor") {
@@ -814,12 +1074,14 @@ var setup_default = defineCommand3({
814
1074
  }
815
1075
  }
816
1076
  const config = {
1077
+ workflow,
817
1078
  role: detectedRole,
818
1079
  mainBranch,
819
- devBranch,
1080
+ ...devBranch ? { devBranch } : {},
820
1081
  upstream: upstreamRemote,
821
1082
  origin: originRemote,
822
- branchPrefixes: defaultConfig.branchPrefixes
1083
+ branchPrefixes: defaultConfig.branchPrefixes,
1084
+ commitConvention
823
1085
  };
824
1086
  writeConfig(config);
825
1087
  success(`✅ Config written to .contributerc.json`);
@@ -828,15 +1090,21 @@ var setup_default = defineCommand3({
828
1090
  warn(' echo ".contributerc.json" >> .gitignore');
829
1091
  }
830
1092
  console.log();
831
- info(`Role: ${pc5.bold(config.role)}`);
832
- info(`Main: ${pc5.bold(config.mainBranch)} | Dev: ${pc5.bold(config.devBranch)}`);
833
- info(`Origin: ${pc5.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc5.bold(config.upstream)}` : ""}`);
1093
+ info(`Workflow: ${pc6.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
1094
+ info(`Convention: ${pc6.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
1095
+ info(`Role: ${pc6.bold(config.role)}`);
1096
+ if (config.devBranch) {
1097
+ info(`Main: ${pc6.bold(config.mainBranch)} | Dev: ${pc6.bold(config.devBranch)}`);
1098
+ } else {
1099
+ info(`Main: ${pc6.bold(config.mainBranch)}`);
1100
+ }
1101
+ info(`Origin: ${pc6.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc6.bold(config.upstream)}` : ""}`);
834
1102
  }
835
1103
  });
836
1104
 
837
1105
  // src/commands/start.ts
838
- import { defineCommand as defineCommand4 } from "citty";
839
- import pc6 from "picocolors";
1106
+ import { defineCommand as defineCommand5 } from "citty";
1107
+ import pc7 from "picocolors";
840
1108
 
841
1109
  // src/utils/branch.ts
842
1110
  var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
@@ -852,10 +1120,10 @@ function looksLikeNaturalLanguage(input) {
852
1120
  }
853
1121
 
854
1122
  // src/commands/start.ts
855
- var start_default = defineCommand4({
1123
+ var start_default = defineCommand5({
856
1124
  meta: {
857
1125
  name: "start",
858
- description: "Create a new feature branch from the latest dev"
1126
+ description: "Create a new feature branch from the latest base branch"
859
1127
  },
860
1128
  args: {
861
1129
  name: {
@@ -887,7 +1155,9 @@ var start_default = defineCommand4({
887
1155
  error("You have uncommitted changes. Please commit or stash them before creating a branch.");
888
1156
  process.exit(1);
889
1157
  }
890
- const { devBranch, origin, upstream, branchPrefixes, role } = config;
1158
+ const { branchPrefixes } = config;
1159
+ const baseBranch = getBaseBranch(config);
1160
+ const syncSource = getSyncSource(config);
891
1161
  let branchName = args.name;
892
1162
  heading("\uD83C\uDF3F contrib start");
893
1163
  const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
@@ -896,8 +1166,8 @@ var start_default = defineCommand4({
896
1166
  const suggested = await suggestBranchName(branchName, args.model);
897
1167
  if (suggested) {
898
1168
  console.log(`
899
- ${pc6.dim("AI suggestion:")} ${pc6.bold(pc6.cyan(suggested))}`);
900
- const accepted = await confirmPrompt(`Use ${pc6.bold(suggested)} as your branch name?`);
1169
+ ${pc7.dim("AI suggestion:")} ${pc7.bold(pc7.cyan(suggested))}`);
1170
+ const accepted = await confirmPrompt(`Use ${pc7.bold(suggested)} as your branch name?`);
901
1171
  if (accepted) {
902
1172
  branchName = suggested;
903
1173
  } else {
@@ -906,31 +1176,29 @@ var start_default = defineCommand4({
906
1176
  }
907
1177
  }
908
1178
  if (!hasPrefix(branchName, branchPrefixes)) {
909
- const prefix = await selectPrompt(`Choose a branch type for ${pc6.bold(branchName)}:`, branchPrefixes);
1179
+ const prefix = await selectPrompt(`Choose a branch type for ${pc7.bold(branchName)}:`, branchPrefixes);
910
1180
  branchName = formatBranchName(prefix, branchName);
911
1181
  }
912
- info(`Creating branch: ${pc6.bold(branchName)}`);
913
- const remote = role === "contributor" ? upstream : origin;
914
- const remoteDevRef = role === "contributor" ? `${upstream}/${devBranch}` : `${origin}/${devBranch}`;
915
- await fetchRemote(remote);
916
- const resetResult = await resetHard(remoteDevRef);
1182
+ info(`Creating branch: ${pc7.bold(branchName)}`);
1183
+ await fetchRemote(syncSource.remote);
1184
+ const resetResult = await resetHard(syncSource.ref);
917
1185
  if (resetResult.exitCode !== 0) {}
918
- const result = await createBranch(branchName, devBranch);
1186
+ const result = await createBranch(branchName, baseBranch);
919
1187
  if (result.exitCode !== 0) {
920
1188
  error(`Failed to create branch: ${result.stderr}`);
921
1189
  process.exit(1);
922
1190
  }
923
- success(`✅ Created ${pc6.bold(branchName)} from latest ${pc6.bold(devBranch)}`);
1191
+ success(`✅ Created ${pc7.bold(branchName)} from latest ${pc7.bold(baseBranch)}`);
924
1192
  }
925
1193
  });
926
1194
 
927
1195
  // src/commands/status.ts
928
- import { defineCommand as defineCommand5 } from "citty";
929
- import pc7 from "picocolors";
930
- var status_default = defineCommand5({
1196
+ import { defineCommand as defineCommand6 } from "citty";
1197
+ import pc8 from "picocolors";
1198
+ var status_default = defineCommand6({
931
1199
  meta: {
932
1200
  name: "status",
933
- description: "Show sync status of main, dev, and current branch"
1201
+ description: "Show sync status of branches"
934
1202
  },
935
1203
  async run() {
936
1204
  if (!await isGitRepo()) {
@@ -943,60 +1211,57 @@ var status_default = defineCommand5({
943
1211
  process.exit(1);
944
1212
  }
945
1213
  heading("\uD83D\uDCCA contribute-now status");
1214
+ console.log(` ${pc8.dim("Workflow:")} ${pc8.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
1215
+ console.log(` ${pc8.dim("Role:")} ${pc8.bold(config.role)}`);
1216
+ console.log();
946
1217
  await fetchAll();
947
1218
  const currentBranch = await getCurrentBranch();
948
- const { mainBranch, devBranch, origin, upstream } = config;
1219
+ const { mainBranch, origin, upstream, workflow } = config;
1220
+ const baseBranch = getBaseBranch(config);
949
1221
  const isContributor = config.role === "contributor";
950
1222
  const dirty = await hasUncommittedChanges();
951
1223
  if (dirty) {
952
- console.log(` ${pc7.yellow("⚠")} ${pc7.yellow("Uncommitted changes in working tree")}`);
1224
+ console.log(` ${pc8.yellow("⚠")} ${pc8.yellow("Uncommitted changes in working tree")}`);
953
1225
  console.log();
954
1226
  }
955
1227
  const mainRemote = `${origin}/${mainBranch}`;
956
1228
  const mainDiv = await getDivergence(mainBranch, mainRemote);
957
1229
  const mainStatus = formatStatus(mainBranch, mainRemote, mainDiv.ahead, mainDiv.behind);
958
1230
  console.log(mainStatus);
959
- const devRemoteRef = isContributor ? `${upstream}/${devBranch}` : `${origin}/${mainBranch}`;
960
- const devDiv = await getDivergence(devBranch, devRemoteRef);
961
- let devLine = formatStatus(devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
962
- if (!isContributor && devDiv.ahead > 0 && devDiv.behind > 0) {
963
- devLine += pc7.red(" (needs sync! squash-merge divergence detected)");
964
- } else if (devDiv.ahead > 0 && devDiv.behind === 0) {
965
- devLine += pc7.yellow(" (needs sync!)");
966
- }
967
- console.log(devLine);
968
- if (currentBranch && currentBranch !== mainBranch && currentBranch !== devBranch) {
969
- const branchDiv = await getDivergence(currentBranch, devBranch);
970
- const branchLine = formatStatus(currentBranch, devBranch, branchDiv.ahead, branchDiv.behind);
971
- console.log(branchLine + pc7.dim(` (current ${pc7.green("*")})`));
1231
+ if (hasDevBranch(workflow) && config.devBranch) {
1232
+ const devRemoteRef = isContributor ? `${upstream}/${config.devBranch}` : `${origin}/${config.devBranch}`;
1233
+ const devDiv = await getDivergence(config.devBranch, devRemoteRef);
1234
+ const devLine = formatStatus(config.devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
1235
+ console.log(devLine);
1236
+ }
1237
+ if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
1238
+ const branchDiv = await getDivergence(currentBranch, baseBranch);
1239
+ const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
1240
+ console.log(branchLine + pc8.dim(` (current ${pc8.green("*")})`));
972
1241
  } else if (currentBranch) {
973
- if (currentBranch === mainBranch) {
974
- console.log(pc7.dim(` (on ${pc7.bold(mainBranch)} branch)`));
975
- } else if (currentBranch === devBranch) {
976
- console.log(pc7.dim(` (on ${pc7.bold(devBranch)} branch)`));
977
- }
1242
+ console.log(pc8.dim(` (on ${pc8.bold(currentBranch)} branch)`));
978
1243
  }
979
1244
  console.log();
980
1245
  }
981
1246
  });
982
1247
  function formatStatus(branch, base, ahead, behind) {
983
- const label = pc7.bold(branch.padEnd(20));
1248
+ const label = pc8.bold(branch.padEnd(20));
984
1249
  if (ahead === 0 && behind === 0) {
985
- return ` ${pc7.green("✓")} ${label} ${pc7.dim(`in sync with ${base}`)}`;
1250
+ return ` ${pc8.green("✓")} ${label} ${pc8.dim(`in sync with ${base}`)}`;
986
1251
  }
987
1252
  if (ahead > 0 && behind === 0) {
988
- return ` ${pc7.yellow("↑")} ${label} ${pc7.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
1253
+ return ` ${pc8.yellow("↑")} ${label} ${pc8.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
989
1254
  }
990
1255
  if (behind > 0 && ahead === 0) {
991
- return ` ${pc7.red("↓")} ${label} ${pc7.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
1256
+ return ` ${pc8.red("↓")} ${label} ${pc8.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
992
1257
  }
993
- return ` ${pc7.red("⚡")} ${label} ${pc7.yellow(`${ahead} ahead`)}${pc7.dim(", ")}${pc7.red(`${behind} behind`)} ${pc7.dim(base)}`;
1258
+ return ` ${pc8.red("⚡")} ${label} ${pc8.yellow(`${ahead} ahead`)}${pc8.dim(", ")}${pc8.red(`${behind} behind`)} ${pc8.dim(base)}`;
994
1259
  }
995
1260
 
996
1261
  // src/commands/submit.ts
997
- import { defineCommand as defineCommand6 } from "citty";
998
- import pc8 from "picocolors";
999
- var submit_default = defineCommand6({
1262
+ import { defineCommand as defineCommand7 } from "citty";
1263
+ import pc9 from "picocolors";
1264
+ var submit_default = defineCommand7({
1000
1265
  meta: {
1001
1266
  name: "submit",
1002
1267
  description: "Push current branch and create a pull request"
@@ -1027,18 +1292,20 @@ var submit_default = defineCommand6({
1027
1292
  error("No .contributerc.json found. Run `contrib setup` first.");
1028
1293
  process.exit(1);
1029
1294
  }
1030
- const { mainBranch, devBranch, origin } = config;
1295
+ const { origin } = config;
1296
+ const baseBranch = getBaseBranch(config);
1297
+ const protectedBranches = getProtectedBranches(config);
1031
1298
  const currentBranch = await getCurrentBranch();
1032
1299
  if (!currentBranch) {
1033
1300
  error("Could not determine current branch.");
1034
1301
  process.exit(1);
1035
1302
  }
1036
- if (currentBranch === mainBranch || currentBranch === devBranch) {
1037
- error(`Cannot submit ${pc8.bold(mainBranch)} or ${pc8.bold(devBranch)} as a PR. Switch to your feature branch.`);
1303
+ if (protectedBranches.includes(currentBranch)) {
1304
+ error(`Cannot submit ${protectedBranches.map((b) => pc9.bold(b)).join(" or ")} as a PR. Switch to your feature branch.`);
1038
1305
  process.exit(1);
1039
1306
  }
1040
1307
  heading("\uD83D\uDE80 contrib submit");
1041
- info(`Pushing ${pc8.bold(currentBranch)} to ${origin}...`);
1308
+ info(`Pushing ${pc9.bold(currentBranch)} to ${origin}...`);
1042
1309
  const pushResult = await pushSetUpstream(origin, currentBranch);
1043
1310
  if (pushResult.exitCode !== 0) {
1044
1311
  error(`Failed to push: ${pushResult.stderr}`);
@@ -1049,10 +1316,10 @@ var submit_default = defineCommand6({
1049
1316
  if (!ghInstalled || !ghAuthed) {
1050
1317
  const repoInfo = await getRepoInfoFromRemote(origin);
1051
1318
  if (repoInfo) {
1052
- const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${devBranch}...${currentBranch}?expand=1`;
1319
+ const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
1053
1320
  console.log();
1054
1321
  info("Create your PR manually:");
1055
- console.log(` ${pc8.cyan(prUrl)}`);
1322
+ console.log(` ${pc9.cyan(prUrl)}`);
1056
1323
  } else {
1057
1324
  info("gh CLI not available. Create your PR manually on GitHub.");
1058
1325
  }
@@ -1064,17 +1331,17 @@ var submit_default = defineCommand6({
1064
1331
  const copilotError = await checkCopilotAvailable();
1065
1332
  if (!copilotError) {
1066
1333
  info("Generating AI PR description...");
1067
- const commits = await getLog(devBranch, "HEAD");
1068
- const diff = await getLogDiff(devBranch, "HEAD");
1334
+ const commits = await getLog(baseBranch, "HEAD");
1335
+ const diff = await getLogDiff(baseBranch, "HEAD");
1069
1336
  const result = await generatePRDescription(commits, diff, args.model);
1070
1337
  if (result) {
1071
1338
  prTitle = result.title;
1072
1339
  prBody = result.body;
1073
1340
  console.log(`
1074
- ${pc8.dim("AI title:")} ${pc8.bold(pc8.cyan(prTitle))}`);
1341
+ ${pc9.dim("AI title:")} ${pc9.bold(pc9.cyan(prTitle))}`);
1075
1342
  console.log(`
1076
- ${pc8.dim("AI body preview:")}`);
1077
- console.log(pc8.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
1343
+ ${pc9.dim("AI body preview:")}`);
1344
+ console.log(pc9.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
1078
1345
  } else {
1079
1346
  warn("AI did not return a PR description.");
1080
1347
  }
@@ -1095,7 +1362,7 @@ ${pc8.dim("AI body preview:")}`);
1095
1362
  prTitle = await inputPrompt("PR title");
1096
1363
  prBody = await inputPrompt("PR body (markdown)");
1097
1364
  } else {
1098
- const fillResult = await createPRFill(devBranch, args.draft);
1365
+ const fillResult = await createPRFill(baseBranch, args.draft);
1099
1366
  if (fillResult.exitCode !== 0) {
1100
1367
  error(`Failed to create PR: ${fillResult.stderr}`);
1101
1368
  process.exit(1);
@@ -1109,7 +1376,7 @@ ${pc8.dim("AI body preview:")}`);
1109
1376
  prTitle = await inputPrompt("PR title");
1110
1377
  prBody = await inputPrompt("PR body (markdown)");
1111
1378
  } else {
1112
- const fillResult = await createPRFill(devBranch, args.draft);
1379
+ const fillResult = await createPRFill(baseBranch, args.draft);
1113
1380
  if (fillResult.exitCode !== 0) {
1114
1381
  error(`Failed to create PR: ${fillResult.stderr}`);
1115
1382
  process.exit(1);
@@ -1123,7 +1390,7 @@ ${pc8.dim("AI body preview:")}`);
1123
1390
  process.exit(1);
1124
1391
  }
1125
1392
  const prResult = await createPR({
1126
- base: devBranch,
1393
+ base: baseBranch,
1127
1394
  title: prTitle,
1128
1395
  body: prBody ?? "",
1129
1396
  draft: args.draft
@@ -1137,12 +1404,12 @@ ${pc8.dim("AI body preview:")}`);
1137
1404
  });
1138
1405
 
1139
1406
  // src/commands/sync.ts
1140
- import { defineCommand as defineCommand7 } from "citty";
1141
- import pc9 from "picocolors";
1142
- var sync_default = defineCommand7({
1407
+ import { defineCommand as defineCommand8 } from "citty";
1408
+ import pc10 from "picocolors";
1409
+ var sync_default = defineCommand8({
1143
1410
  meta: {
1144
1411
  name: "sync",
1145
- description: "Reset dev branch to match origin/main (maintainer) or upstream/dev (contributor)"
1412
+ description: "Sync your local branches with the remote"
1146
1413
  },
1147
1414
  args: {
1148
1415
  yes: {
@@ -1162,86 +1429,70 @@ var sync_default = defineCommand7({
1162
1429
  error("No .contributerc.json found. Run `contrib setup` first.");
1163
1430
  process.exit(1);
1164
1431
  }
1165
- const { role, mainBranch, devBranch, origin, upstream } = config;
1432
+ const { workflow, role, origin } = config;
1166
1433
  if (await hasUncommittedChanges()) {
1167
1434
  error("You have uncommitted changes. Please commit or stash them before syncing.");
1168
1435
  process.exit(1);
1169
1436
  }
1170
- heading(`\uD83D\uDD04 contrib sync (${role})`);
1171
- if (role === "maintainer") {
1172
- info(`Fetching ${origin}...`);
1173
- const fetchResult = await fetchRemote(origin);
1174
- if (fetchResult.exitCode !== 0) {
1175
- error(`Failed to fetch ${origin}: ${fetchResult.stderr}`);
1176
- process.exit(1);
1177
- }
1178
- const div = await getDivergence(devBranch, `${origin}/${mainBranch}`);
1179
- if (div.ahead > 0 || div.behind > 0) {
1180
- info(`${pc9.bold(devBranch)} is ${pc9.yellow(`${div.ahead} ahead`)} and ${pc9.red(`${div.behind} behind`)} ${origin}/${mainBranch}`);
1181
- } else {
1182
- info(`${pc9.bold(devBranch)} is already in sync with ${origin}/${mainBranch}`);
1183
- }
1184
- if (!args.yes) {
1185
- const ok = await confirmPrompt(`This will reset ${pc9.bold(devBranch)} to match ${pc9.bold(`${origin}/${mainBranch}`)}.`);
1186
- if (!ok)
1187
- process.exit(0);
1188
- }
1189
- const coResult = await checkoutBranch(devBranch);
1190
- if (coResult.exitCode !== 0) {
1191
- error(`Failed to checkout ${devBranch}: ${coResult.stderr}`);
1192
- process.exit(1);
1193
- }
1194
- const resetResult = await resetHard(`${origin}/${mainBranch}`);
1195
- if (resetResult.exitCode !== 0) {
1196
- error(`Failed to reset: ${resetResult.stderr}`);
1197
- process.exit(1);
1198
- }
1199
- const pushResult = await pushForceWithLease(origin, devBranch);
1200
- if (pushResult.exitCode !== 0) {
1201
- error(`Failed to push: ${pushResult.stderr}`);
1202
- process.exit(1);
1203
- }
1204
- success(`✅ ${devBranch} has been reset to match ${origin}/${mainBranch} and pushed.`);
1437
+ heading(`\uD83D\uDD04 contrib sync (${workflow}, ${role})`);
1438
+ const baseBranch = getBaseBranch(config);
1439
+ const syncSource = getSyncSource(config);
1440
+ info(`Fetching ${syncSource.remote}...`);
1441
+ const fetchResult = await fetchRemote(syncSource.remote);
1442
+ if (fetchResult.exitCode !== 0) {
1443
+ error(`Failed to fetch ${syncSource.remote}: ${fetchResult.stderr}`);
1444
+ process.exit(1);
1445
+ }
1446
+ if (role === "contributor" && syncSource.remote !== origin) {
1447
+ await fetchRemote(origin);
1448
+ }
1449
+ const div = await getDivergence(baseBranch, syncSource.ref);
1450
+ if (div.ahead > 0 || div.behind > 0) {
1451
+ info(`${pc10.bold(baseBranch)} is ${pc10.yellow(`${div.ahead} ahead`)} and ${pc10.red(`${div.behind} behind`)} ${syncSource.ref}`);
1205
1452
  } else {
1206
- info(`Fetching ${upstream}...`);
1207
- const fetchResult = await fetchRemote(upstream);
1208
- if (fetchResult.exitCode !== 0) {
1209
- error(`Failed to fetch ${upstream}: ${fetchResult.stderr}`);
1210
- process.exit(1);
1211
- }
1212
- if (!args.yes) {
1213
- const ok = await confirmPrompt(`This will reset local ${pc9.bold(devBranch)} to match ${pc9.bold(`${upstream}/${devBranch}`)}.`);
1214
- if (!ok)
1215
- process.exit(0);
1216
- }
1217
- const coResult = await checkoutBranch(devBranch);
1218
- if (coResult.exitCode !== 0) {
1219
- error(`Failed to checkout ${devBranch}: ${coResult.stderr}`);
1220
- process.exit(1);
1221
- }
1222
- const resetResult = await resetHard(`${upstream}/${devBranch}`);
1223
- if (resetResult.exitCode !== 0) {
1224
- error(`Failed to reset: ${resetResult.stderr}`);
1225
- process.exit(1);
1226
- }
1227
- const pushResult = await pushForceWithLease(origin, devBranch);
1228
- if (pushResult.exitCode !== 0) {
1229
- error(`Failed to push: ${pushResult.stderr}`);
1230
- process.exit(1);
1453
+ info(`${pc10.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
1454
+ }
1455
+ if (!args.yes) {
1456
+ const ok = await confirmPrompt(`This will pull ${pc10.bold(syncSource.ref)} into local ${pc10.bold(baseBranch)}.`);
1457
+ if (!ok)
1458
+ process.exit(0);
1459
+ }
1460
+ const coResult = await checkoutBranch(baseBranch);
1461
+ if (coResult.exitCode !== 0) {
1462
+ error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
1463
+ process.exit(1);
1464
+ }
1465
+ const pullResult = await pullBranch(syncSource.remote, baseBranch);
1466
+ if (pullResult.exitCode !== 0) {
1467
+ error(`Failed to pull: ${pullResult.stderr}`);
1468
+ process.exit(1);
1469
+ }
1470
+ success(`✅ ${baseBranch} is now in sync with ${syncSource.ref}`);
1471
+ if (hasDevBranch(workflow) && role === "maintainer") {
1472
+ const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
1473
+ if (mainDiv.behind > 0) {
1474
+ info(`Also syncing ${pc10.bold(config.mainBranch)}...`);
1475
+ const mainCoResult = await checkoutBranch(config.mainBranch);
1476
+ if (mainCoResult.exitCode === 0) {
1477
+ const mainPullResult = await pullBranch(origin, config.mainBranch);
1478
+ if (mainPullResult.exitCode === 0) {
1479
+ success(`✅ ${config.mainBranch} is now in sync with ${origin}/${config.mainBranch}`);
1480
+ }
1481
+ }
1482
+ await checkoutBranch(baseBranch);
1231
1483
  }
1232
- success(`✅ ${devBranch} has been reset to match ${upstream}/${devBranch} and pushed.`);
1233
1484
  }
1234
1485
  }
1235
1486
  });
1236
1487
 
1237
1488
  // src/commands/update.ts
1238
- import { readFileSync as readFileSync2 } from "node:fs";
1239
- import { defineCommand as defineCommand8 } from "citty";
1240
- import pc10 from "picocolors";
1241
- var update_default = defineCommand8({
1489
+ import { readFileSync as readFileSync3 } from "node:fs";
1490
+ import { defineCommand as defineCommand9 } from "citty";
1491
+ import pc11 from "picocolors";
1492
+ var update_default = defineCommand9({
1242
1493
  meta: {
1243
1494
  name: "update",
1244
- description: "Rebase current branch onto latest dev"
1495
+ description: "Rebase current branch onto the latest base branch"
1245
1496
  },
1246
1497
  args: {
1247
1498
  model: {
@@ -1264,14 +1515,16 @@ var update_default = defineCommand8({
1264
1515
  error("No .contributerc.json found. Run `contrib setup` first.");
1265
1516
  process.exit(1);
1266
1517
  }
1267
- const { mainBranch, devBranch, origin, upstream, role } = config;
1518
+ const baseBranch = getBaseBranch(config);
1519
+ const protectedBranches = getProtectedBranches(config);
1520
+ const syncSource = getSyncSource(config);
1268
1521
  const currentBranch = await getCurrentBranch();
1269
1522
  if (!currentBranch) {
1270
1523
  error("Could not determine current branch.");
1271
1524
  process.exit(1);
1272
1525
  }
1273
- if (currentBranch === mainBranch || currentBranch === devBranch) {
1274
- error(`Use \`contrib sync\` to update ${pc10.bold(mainBranch)} or ${pc10.bold(devBranch)} branches.`);
1526
+ if (protectedBranches.includes(currentBranch)) {
1527
+ error(`Use \`contrib sync\` to update ${protectedBranches.map((b) => pc11.bold(b)).join(" or ")} branches.`);
1275
1528
  process.exit(1);
1276
1529
  }
1277
1530
  if (await hasUncommittedChanges()) {
@@ -1279,12 +1532,10 @@ var update_default = defineCommand8({
1279
1532
  process.exit(1);
1280
1533
  }
1281
1534
  heading("\uD83D\uDD03 contrib update");
1282
- info(`Updating ${pc10.bold(currentBranch)} with latest ${pc10.bold(devBranch)}...`);
1283
- const remote = role === "contributor" ? upstream : origin;
1284
- const remoteDevRef = role === "contributor" ? `${upstream}/${devBranch}` : `${origin}/${devBranch}`;
1285
- await fetchRemote(remote);
1286
- await resetHard(remoteDevRef);
1287
- const rebaseResult = await rebase(devBranch);
1535
+ info(`Updating ${pc11.bold(currentBranch)} with latest ${pc11.bold(baseBranch)}...`);
1536
+ await fetchRemote(syncSource.remote);
1537
+ await resetHard(syncSource.ref);
1538
+ const rebaseResult = await rebase(baseBranch);
1288
1539
  if (rebaseResult.exitCode !== 0) {
1289
1540
  warn("Rebase hit conflicts. Resolve them manually.");
1290
1541
  console.log();
@@ -1296,7 +1547,7 @@ var update_default = defineCommand8({
1296
1547
  let conflictDiff = "";
1297
1548
  for (const file of conflictFiles.slice(0, 3)) {
1298
1549
  try {
1299
- const content = readFileSync2(file, "utf-8");
1550
+ const content = readFileSync3(file, "utf-8");
1300
1551
  if (content.includes("<<<<<<<")) {
1301
1552
  conflictDiff += `
1302
1553
  --- ${file} ---
@@ -1309,34 +1560,73 @@ ${content.slice(0, 2000)}
1309
1560
  const suggestion = await suggestConflictResolution(conflictDiff, args.model);
1310
1561
  if (suggestion) {
1311
1562
  console.log(`
1312
- ${pc10.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
1313
- console.log(pc10.dim("─".repeat(60)));
1563
+ ${pc11.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
1564
+ console.log(pc11.dim("─".repeat(60)));
1314
1565
  console.log(suggestion);
1315
- console.log(pc10.dim("─".repeat(60)));
1566
+ console.log(pc11.dim("─".repeat(60)));
1316
1567
  console.log();
1317
1568
  }
1318
1569
  }
1319
1570
  }
1320
1571
  }
1321
- console.log(pc10.bold("To resolve:"));
1572
+ console.log(pc11.bold("To resolve:"));
1322
1573
  console.log(` 1. Fix conflicts in the affected files`);
1323
- console.log(` 2. ${pc10.cyan("git add <resolved-files>")}`);
1324
- console.log(` 3. ${pc10.cyan("git rebase --continue")}`);
1574
+ console.log(` 2. ${pc11.cyan("git add <resolved-files>")}`);
1575
+ console.log(` 3. ${pc11.cyan("git rebase --continue")}`);
1325
1576
  console.log();
1326
- console.log(` Or abort: ${pc10.cyan("git rebase --abort")}`);
1577
+ console.log(` Or abort: ${pc11.cyan("git rebase --abort")}`);
1327
1578
  process.exit(1);
1328
1579
  }
1329
- success(`✅ ${pc10.bold(currentBranch)} has been rebased onto latest ${pc10.bold(devBranch)}`);
1580
+ success(`✅ ${pc11.bold(currentBranch)} has been rebased onto latest ${pc11.bold(baseBranch)}`);
1581
+ }
1582
+ });
1583
+
1584
+ // src/commands/validate.ts
1585
+ import { defineCommand as defineCommand10 } from "citty";
1586
+ import pc12 from "picocolors";
1587
+ var validate_default = defineCommand10({
1588
+ meta: {
1589
+ name: "validate",
1590
+ description: "Validate a commit message against the configured convention"
1591
+ },
1592
+ args: {
1593
+ message: {
1594
+ type: "positional",
1595
+ description: "The commit message to validate",
1596
+ required: true
1597
+ }
1598
+ },
1599
+ async run({ args }) {
1600
+ const config = readConfig();
1601
+ if (!config) {
1602
+ error("No .contributerc.json found. Run `contrib setup` first.");
1603
+ process.exit(1);
1604
+ }
1605
+ const convention = config.commitConvention;
1606
+ if (convention === "none") {
1607
+ info('Commit convention is set to "none". All messages are accepted.');
1608
+ process.exit(0);
1609
+ }
1610
+ const message = args.message;
1611
+ if (validateCommitMessage(message, convention)) {
1612
+ success(`Valid ${CONVENTION_LABELS[convention]} message.`);
1613
+ process.exit(0);
1614
+ }
1615
+ const errors = getValidationError(convention);
1616
+ for (const line of errors) {
1617
+ console.error(pc12.red(` ✗ ${line}`));
1618
+ }
1619
+ process.exit(1);
1330
1620
  }
1331
1621
  });
1332
1622
 
1333
1623
  // src/ui/banner.ts
1334
1624
  import figlet from "figlet";
1335
- import pc11 from "picocolors";
1625
+ import pc13 from "picocolors";
1336
1626
  // package.json
1337
1627
  var package_default = {
1338
1628
  name: "contribute-now",
1339
- version: "0.1.1",
1629
+ version: "0.1.2-dev.c209cc7",
1340
1630
  description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
1341
1631
  type: "module",
1342
1632
  bin: {
@@ -1353,7 +1643,10 @@ var package_default = {
1353
1643
  lint: "biome check .",
1354
1644
  "lint:fix": "biome check --write .",
1355
1645
  format: "biome format --write .",
1356
- prepare: "husky || true"
1646
+ prepare: "husky || true",
1647
+ "www:dev": "bun run --cwd www dev",
1648
+ "www:build": "bun run --cwd www build",
1649
+ "www:preview": "bun run --cwd www preview"
1357
1650
  },
1358
1651
  engines: {
1359
1652
  node: ">=18",
@@ -1406,15 +1699,15 @@ function getAuthor() {
1406
1699
  return typeof package_default.author === "string" ? package_default.author : "unknown";
1407
1700
  }
1408
1701
  function showBanner(minimal = false) {
1409
- console.log(pc11.cyan(`
1702
+ console.log(pc13.cyan(`
1410
1703
  ${LOGO}`));
1411
- console.log(` ${pc11.dim(`v${getVersion()}`)} ${pc11.dim("—")} ${pc11.dim(`Built by ${getAuthor()}`)}`);
1704
+ console.log(` ${pc13.dim(`v${getVersion()}`)} ${pc13.dim("—")} ${pc13.dim(`Built by ${getAuthor()}`)}`);
1412
1705
  if (!minimal) {
1413
- console.log(` ${pc11.dim(package_default.description)}`);
1706
+ console.log(` ${pc13.dim(package_default.description)}`);
1414
1707
  console.log();
1415
- console.log(` ${pc11.yellow("Star")} ${pc11.cyan("https://github.com/warengonzaga/contribute-now")}`);
1416
- console.log(` ${pc11.green("Contribute")} ${pc11.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
1417
- console.log(` ${pc11.magenta("Sponsor")} ${pc11.cyan("https://warengonzaga.com/sponsor")}`);
1708
+ console.log(` ${pc13.yellow("Star")} ${pc13.cyan("https://github.com/warengonzaga/contribute-now")}`);
1709
+ console.log(` ${pc13.green("Contribute")} ${pc13.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
1710
+ console.log(` ${pc13.magenta("Sponsor")} ${pc13.cyan("https://warengonzaga.com/sponsor")}`);
1418
1711
  }
1419
1712
  console.log();
1420
1713
  }
@@ -1422,11 +1715,11 @@ ${LOGO}`));
1422
1715
  // src/index.ts
1423
1716
  var isHelp = process.argv.includes("--help") || process.argv.includes("-h");
1424
1717
  showBanner(isHelp);
1425
- var main = defineCommand9({
1718
+ var main = defineCommand11({
1426
1719
  meta: {
1427
1720
  name: "contrib",
1428
1721
  version: getVersion(),
1429
- description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges."
1722
+ description: "Git workflow CLI that guides contributors through clean branching, commits, and PRs."
1430
1723
  },
1431
1724
  args: {
1432
1725
  version: {
@@ -1443,7 +1736,9 @@ var main = defineCommand9({
1443
1736
  update: update_default,
1444
1737
  submit: submit_default,
1445
1738
  clean: clean_default,
1446
- status: status_default
1739
+ status: status_default,
1740
+ hook: hook_default,
1741
+ validate: validate_default
1447
1742
  },
1448
1743
  run({ args }) {
1449
1744
  if (args.version) {