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.
- package/README.md +162 -133
- package/dist/index.js +492 -197
- 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
|
|
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 {
|
|
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(
|
|
296
|
-
const
|
|
297
|
-
const candidates = mergedBranches.filter((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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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/
|
|
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 =
|
|
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: ${
|
|
792
|
-
const confirmed = await confirmPrompt(`Role detected as ${
|
|
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
|
-
|
|
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(`
|
|
832
|
-
info(`
|
|
833
|
-
info(`
|
|
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
|
|
839
|
-
import
|
|
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 =
|
|
1123
|
+
var start_default = defineCommand5({
|
|
856
1124
|
meta: {
|
|
857
1125
|
name: "start",
|
|
858
|
-
description: "Create a new feature branch from the latest
|
|
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 {
|
|
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
|
-
${
|
|
900
|
-
const accepted = await confirmPrompt(`Use ${
|
|
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 ${
|
|
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: ${
|
|
913
|
-
|
|
914
|
-
const
|
|
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,
|
|
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 ${
|
|
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
|
|
929
|
-
import
|
|
930
|
-
var status_default =
|
|
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
|
|
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,
|
|
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(` ${
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
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 =
|
|
1248
|
+
const label = pc8.bold(branch.padEnd(20));
|
|
984
1249
|
if (ahead === 0 && behind === 0) {
|
|
985
|
-
return ` ${
|
|
1250
|
+
return ` ${pc8.green("✓")} ${label} ${pc8.dim(`in sync with ${base}`)}`;
|
|
986
1251
|
}
|
|
987
1252
|
if (ahead > 0 && behind === 0) {
|
|
988
|
-
return ` ${
|
|
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 ` ${
|
|
1256
|
+
return ` ${pc8.red("↓")} ${label} ${pc8.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
|
|
992
1257
|
}
|
|
993
|
-
return ` ${
|
|
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
|
|
998
|
-
import
|
|
999
|
-
var submit_default =
|
|
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 {
|
|
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
|
|
1037
|
-
error(`Cannot submit ${
|
|
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 ${
|
|
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/${
|
|
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(` ${
|
|
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(
|
|
1068
|
-
const diff = await getLogDiff(
|
|
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
|
-
${
|
|
1341
|
+
${pc9.dim("AI title:")} ${pc9.bold(pc9.cyan(prTitle))}`);
|
|
1075
1342
|
console.log(`
|
|
1076
|
-
${
|
|
1077
|
-
console.log(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
1141
|
-
import
|
|
1142
|
-
var sync_default =
|
|
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: "
|
|
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 {
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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(
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
|
1239
|
-
import { defineCommand as
|
|
1240
|
-
import
|
|
1241
|
-
var update_default =
|
|
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
|
|
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
|
|
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
|
|
1274
|
-
error(`Use \`contrib sync\` to update ${
|
|
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 ${
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
await
|
|
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 =
|
|
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
|
-
${
|
|
1313
|
-
console.log(
|
|
1563
|
+
${pc11.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
1564
|
+
console.log(pc11.dim("─".repeat(60)));
|
|
1314
1565
|
console.log(suggestion);
|
|
1315
|
-
console.log(
|
|
1566
|
+
console.log(pc11.dim("─".repeat(60)));
|
|
1316
1567
|
console.log();
|
|
1317
1568
|
}
|
|
1318
1569
|
}
|
|
1319
1570
|
}
|
|
1320
1571
|
}
|
|
1321
|
-
console.log(
|
|
1572
|
+
console.log(pc11.bold("To resolve:"));
|
|
1322
1573
|
console.log(` 1. Fix conflicts in the affected files`);
|
|
1323
|
-
console.log(` 2. ${
|
|
1324
|
-
console.log(` 3. ${
|
|
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: ${
|
|
1577
|
+
console.log(` Or abort: ${pc11.cyan("git rebase --abort")}`);
|
|
1327
1578
|
process.exit(1);
|
|
1328
1579
|
}
|
|
1329
|
-
success(`✅ ${
|
|
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
|
|
1625
|
+
import pc13 from "picocolors";
|
|
1336
1626
|
// package.json
|
|
1337
1627
|
var package_default = {
|
|
1338
1628
|
name: "contribute-now",
|
|
1339
|
-
version: "0.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(
|
|
1702
|
+
console.log(pc13.cyan(`
|
|
1410
1703
|
${LOGO}`));
|
|
1411
|
-
console.log(` ${
|
|
1704
|
+
console.log(` ${pc13.dim(`v${getVersion()}`)} ${pc13.dim("—")} ${pc13.dim(`Built by ${getAuthor()}`)}`);
|
|
1412
1705
|
if (!minimal) {
|
|
1413
|
-
console.log(` ${
|
|
1706
|
+
console.log(` ${pc13.dim(package_default.description)}`);
|
|
1414
1707
|
console.log();
|
|
1415
|
-
console.log(` ${
|
|
1416
|
-
console.log(` ${
|
|
1417
|
-
console.log(` ${
|
|
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 =
|
|
1718
|
+
var main = defineCommand11({
|
|
1426
1719
|
meta: {
|
|
1427
1720
|
name: "contrib",
|
|
1428
1721
|
version: getVersion(),
|
|
1429
|
-
description: "Git workflow CLI
|
|
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) {
|