contribute-now 0.4.0 → 0.4.1-dev.3325fdb
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 +88 -8
- package/dist/index.js +691 -85
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ bun install -g contribute-now
|
|
|
64
64
|
## Prerequisites
|
|
65
65
|
|
|
66
66
|
- **[Git](https://git-scm.com/)** — required
|
|
67
|
-
- **[GitHub CLI](https://cli.github.com)** (`gh`) —
|
|
67
|
+
- **[GitHub CLI](https://cli.github.com)** (`gh`) — recommended; required for PR creation, role detection, and merge status checks
|
|
68
68
|
- **[GitHub Copilot](https://github.com/features/copilot)** — optional; enables AI features
|
|
69
69
|
|
|
70
70
|
---
|
|
@@ -129,10 +129,13 @@ Stage your changes and create a validated, AI-generated commit message matching
|
|
|
129
129
|
contrib commit # AI-generated message
|
|
130
130
|
contrib commit --no-ai # manual entry, still validated
|
|
131
131
|
contrib commit --model gpt-4.1 # specific AI model
|
|
132
|
+
contrib commit --group # AI groups changes into atomic commits
|
|
132
133
|
```
|
|
133
134
|
|
|
134
135
|
After the AI generates a message, you can **accept**, **edit**, **regenerate**, or **write manually**. Messages are always validated against your convention — with a soft warning if they don't match (you can still commit).
|
|
135
136
|
|
|
137
|
+
**Group commit mode** (`--group`): AI analyzes all staged and unstaged changes, groups related files into logical atomic commits, and generates a commit message for each group. Great for splitting a large set of changes into clean, reviewable commits.
|
|
138
|
+
|
|
136
139
|
---
|
|
137
140
|
|
|
138
141
|
### `contrib update`
|
|
@@ -180,6 +183,57 @@ contrib status
|
|
|
180
183
|
|
|
181
184
|
---
|
|
182
185
|
|
|
186
|
+
### `contrib doctor`
|
|
187
|
+
|
|
188
|
+
Diagnose the contribute-now CLI environment and configuration. Checks tools, dependencies, config, git state, fork setup, workflow, and environment.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
contrib doctor # pretty-printed report
|
|
192
|
+
contrib doctor --json # machine-readable JSON output
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Checks include:
|
|
196
|
+
- CLI version and runtime (Bun/Node)
|
|
197
|
+
- git and GitHub CLI availability and authentication
|
|
198
|
+
- `.contributerc.json` validity and `.gitignore` status
|
|
199
|
+
- Git repo state (uncommitted changes, lock files, shallow clone)
|
|
200
|
+
- Fork and remote configuration
|
|
201
|
+
- Workflow and branch setup
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `contrib log`
|
|
206
|
+
|
|
207
|
+
Show a colorized, workflow-aware commit log. By default it shows only **local unpushed commits** — the changes you've made since the last push (or since branching off the base branch). Use flags to switch between different views.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
contrib log # local unpushed commits (default)
|
|
211
|
+
contrib log --remote # commits on remote not yet pulled
|
|
212
|
+
contrib log --full # full history for the current branch
|
|
213
|
+
contrib log --all # commits across all branches
|
|
214
|
+
contrib log -n 50 # change the commit limit (default: 20)
|
|
215
|
+
contrib log -b feature/x # log for a specific branch
|
|
216
|
+
contrib log --no-graph # flat view without graph lines
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
When no upstream tracking is set (branch hasn't been pushed yet), the command automatically compares against the base branch from your config (e.g., `origin/dev`). Protected branches are highlighted, and the current branch is color-coded for quick orientation.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### `contrib branch`
|
|
224
|
+
|
|
225
|
+
List branches with workflow-aware labels and tracking status.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
contrib branch # local branches
|
|
229
|
+
contrib branch --all # local + remote branches
|
|
230
|
+
contrib branch --remote # remote branches only
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Branches are annotated with workflow labels (e.g., base, dev, feature) and tracking info (upstream, gone, no remote).
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
183
237
|
### `contrib hook`
|
|
184
238
|
|
|
185
239
|
Install or uninstall a `commit-msg` git hook that validates every commit against your configured convention — no Husky or lint-staged needed.
|
|
@@ -210,8 +264,9 @@ contrib validate "added stuff" # exit 1
|
|
|
210
264
|
All AI features are powered by **GitHub Copilot** via `@github/copilot-sdk` and are entirely **optional** — every command has a manual fallback.
|
|
211
265
|
|
|
212
266
|
| Command | AI Feature | Fallback |
|
|
213
|
-
|
|
267
|
+
|---------|------------|----------|
|
|
214
268
|
| `commit` | Generate commit message from staged diff | Type manually |
|
|
269
|
+
| `commit --group` | Group related changes into atomic commits | Manual staging + commit |
|
|
215
270
|
| `start` | Suggest branch name from natural language | Prefix picker + manual |
|
|
216
271
|
| `update` | Conflict resolution guidance | Standard git instructions |
|
|
217
272
|
| `submit` | Generate PR title and body | `gh pr create --fill` or manual |
|
|
@@ -299,15 +354,40 @@ bun test # run tests
|
|
|
299
354
|
bun run lint # check code quality
|
|
300
355
|
```
|
|
301
356
|
|
|
302
|
-
## Contributing
|
|
357
|
+
## 🎯 Contributing
|
|
303
358
|
|
|
304
|
-
Contributions are welcome
|
|
359
|
+
Contributions are welcome, create a pull request to this repo and I will review your code. Please consider to submit your pull request to the `dev` branch. Thank you!
|
|
305
360
|
|
|
306
|
-
|
|
361
|
+
Read the project's [contributing guide](./CONTRIBUTING.md) for more info.
|
|
307
362
|
|
|
308
|
-
|
|
363
|
+
## 🐛 Issues
|
|
309
364
|
|
|
310
|
-
|
|
365
|
+
Please report any issues and bugs by [creating a new issue here](https://github.com/warengonzaga/contribute-now/issues/new/choose), also make sure you're reporting an issue that doesn't exist. Any help to improve the project would be appreciated. Thanks! 🙏✨
|
|
366
|
+
|
|
367
|
+
## 🙏 Sponsor
|
|
368
|
+
|
|
369
|
+
Like this project? **Leave a star**! ⭐⭐⭐⭐⭐
|
|
370
|
+
|
|
371
|
+
Want to support my work and get some perks? [Become a sponsor](https://github.com/sponsors/warengonzaga)! 💖
|
|
372
|
+
|
|
373
|
+
Or, you just love what I do? [Buy me a coffee](https://buymeacoffee.com/warengonzaga)! ☕
|
|
311
374
|
|
|
312
|
-
|
|
375
|
+
Recognized my open-source contributions? [Nominate me](https://stars.github.com/nominate) as GitHub Star! 💫
|
|
376
|
+
|
|
377
|
+
## 📋 Code of Conduct
|
|
378
|
+
|
|
379
|
+
Read the project's [code of conduct](./CODE_OF_CONDUCT.md).
|
|
380
|
+
|
|
381
|
+
## 📃 License
|
|
382
|
+
|
|
383
|
+
This project is licensed under [GNU General Public License v3.0](https://opensource.org/licenses/GPL-3.0).
|
|
384
|
+
|
|
385
|
+
## 📝 Author
|
|
386
|
+
|
|
387
|
+
This project is created by **[Waren Gonzaga](https://github.com/warengonzaga)**, with the help of awesome [contributors](https://github.com/warengonzaga/contribute-now/graphs/contributors).
|
|
388
|
+
|
|
389
|
+
[](https://github.com/warengonzaga/contribute-now/graphs/contributors)
|
|
390
|
+
|
|
391
|
+
---
|
|
313
392
|
|
|
393
|
+
💻💖☕ by [Waren Gonzaga](https://warengonzaga.com) & [YHWH](https://www.youtube.com/watch?v=VOZbswniA-g) 🙏 — Without *Him*, none of this exists, *even me*.
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { defineCommand } from "citty";
|
|
|
10
10
|
import pc2 from "picocolors";
|
|
11
11
|
|
|
12
12
|
// src/utils/config.ts
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
var CONFIG_FILENAME = ".contributerc.json";
|
|
16
16
|
function getConfigPath(cwd = process.cwd()) {
|
|
@@ -86,6 +86,23 @@ function isGitignored(cwd = process.cwd()) {
|
|
|
86
86
|
return false;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
function ensureGitignored(cwd = process.cwd()) {
|
|
90
|
+
if (isGitignored(cwd))
|
|
91
|
+
return false;
|
|
92
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
93
|
+
const line = `${CONFIG_FILENAME}
|
|
94
|
+
`;
|
|
95
|
+
if (!existsSync(gitignorePath)) {
|
|
96
|
+
writeFileSync(gitignorePath, line, "utf-8");
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
100
|
+
const needsLeadingNewline = content.length > 0 && !content.endsWith(`
|
|
101
|
+
`);
|
|
102
|
+
appendFileSync(gitignorePath, `${needsLeadingNewline ? `
|
|
103
|
+
` : ""}${line}`, "utf-8");
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
89
106
|
function getDefaultConfig() {
|
|
90
107
|
return {
|
|
91
108
|
workflow: "clean-flow",
|
|
@@ -530,6 +547,76 @@ async function getLogEntries(options) {
|
|
|
530
547
|
return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
|
|
531
548
|
});
|
|
532
549
|
}
|
|
550
|
+
async function getLocalCommitsGraph(options) {
|
|
551
|
+
const count = options?.count ?? 20;
|
|
552
|
+
const upstream = options?.upstream;
|
|
553
|
+
if (!upstream)
|
|
554
|
+
return [];
|
|
555
|
+
const args = [
|
|
556
|
+
"log",
|
|
557
|
+
"--oneline",
|
|
558
|
+
"--graph",
|
|
559
|
+
"--decorate",
|
|
560
|
+
`--max-count=${count}`,
|
|
561
|
+
"--color=never",
|
|
562
|
+
`${upstream}..HEAD`
|
|
563
|
+
];
|
|
564
|
+
const { exitCode, stdout } = await run(args);
|
|
565
|
+
if (exitCode !== 0)
|
|
566
|
+
return [];
|
|
567
|
+
return stdout.trimEnd().split(`
|
|
568
|
+
`).filter(Boolean);
|
|
569
|
+
}
|
|
570
|
+
async function getLocalCommitsEntries(options) {
|
|
571
|
+
const count = options?.count ?? 20;
|
|
572
|
+
const upstream = options?.upstream;
|
|
573
|
+
if (!upstream)
|
|
574
|
+
return [];
|
|
575
|
+
const args = ["log", `--format=%h||%s||%D`, `--max-count=${count}`, `${upstream}..HEAD`];
|
|
576
|
+
const { exitCode, stdout } = await run(args);
|
|
577
|
+
if (exitCode !== 0)
|
|
578
|
+
return [];
|
|
579
|
+
return stdout.trimEnd().split(`
|
|
580
|
+
`).filter(Boolean).map((line) => {
|
|
581
|
+
const [hash = "", subject = "", refs = ""] = line.split("||");
|
|
582
|
+
return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
async function getRemoteOnlyCommitsGraph(options) {
|
|
586
|
+
const count = options?.count ?? 20;
|
|
587
|
+
const upstream = options?.upstream;
|
|
588
|
+
if (!upstream)
|
|
589
|
+
return [];
|
|
590
|
+
const args = [
|
|
591
|
+
"log",
|
|
592
|
+
"--oneline",
|
|
593
|
+
"--graph",
|
|
594
|
+
"--decorate",
|
|
595
|
+
`--max-count=${count}`,
|
|
596
|
+
"--color=never",
|
|
597
|
+
`HEAD..${upstream}`
|
|
598
|
+
];
|
|
599
|
+
const { exitCode, stdout } = await run(args);
|
|
600
|
+
if (exitCode !== 0)
|
|
601
|
+
return [];
|
|
602
|
+
return stdout.trimEnd().split(`
|
|
603
|
+
`).filter(Boolean);
|
|
604
|
+
}
|
|
605
|
+
async function getRemoteOnlyCommitsEntries(options) {
|
|
606
|
+
const count = options?.count ?? 20;
|
|
607
|
+
const upstream = options?.upstream;
|
|
608
|
+
if (!upstream)
|
|
609
|
+
return [];
|
|
610
|
+
const args = ["log", `--format=%h||%s||%D`, `--max-count=${count}`, `HEAD..${upstream}`];
|
|
611
|
+
const { exitCode, stdout } = await run(args);
|
|
612
|
+
if (exitCode !== 0)
|
|
613
|
+
return [];
|
|
614
|
+
return stdout.trimEnd().split(`
|
|
615
|
+
`).filter(Boolean).map((line) => {
|
|
616
|
+
const [hash = "", subject = "", refs = ""] = line.split("||");
|
|
617
|
+
return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
|
|
618
|
+
});
|
|
619
|
+
}
|
|
533
620
|
async function getLocalBranches() {
|
|
534
621
|
const { exitCode, stdout } = await run(["branch", "-vv", "--no-color"]);
|
|
535
622
|
if (exitCode !== 0)
|
|
@@ -558,6 +645,17 @@ async function getRemoteBranches() {
|
|
|
558
645
|
return stdout.trimEnd().split(`
|
|
559
646
|
`).map((line) => line.trim()).filter((line) => line.length > 0 && !line.includes(" -> "));
|
|
560
647
|
}
|
|
648
|
+
async function isBranchMergedInto(branch, base) {
|
|
649
|
+
const { exitCode } = await run(["merge-base", "--is-ancestor", branch, base]);
|
|
650
|
+
return exitCode === 0;
|
|
651
|
+
}
|
|
652
|
+
async function getLastCommitDate(branch) {
|
|
653
|
+
const { exitCode, stdout } = await run(["log", "-1", "--format=%aI", branch]);
|
|
654
|
+
if (exitCode !== 0)
|
|
655
|
+
return null;
|
|
656
|
+
const date = stdout.trim();
|
|
657
|
+
return date || null;
|
|
658
|
+
}
|
|
561
659
|
|
|
562
660
|
// src/utils/logger.ts
|
|
563
661
|
import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
|
|
@@ -953,6 +1051,79 @@ function withTimeout(promise, ms) {
|
|
|
953
1051
|
}
|
|
954
1052
|
var COPILOT_TIMEOUT_MS = 30000;
|
|
955
1053
|
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
1054
|
+
var BATCH_CONFIG = {
|
|
1055
|
+
LARGE_CHANGESET_THRESHOLD: 15,
|
|
1056
|
+
COMPACT_PER_FILE_CHARS: 300,
|
|
1057
|
+
MAX_COMPACT_PAYLOAD: 1e4,
|
|
1058
|
+
FALLBACK_BATCH_SIZE: 15
|
|
1059
|
+
};
|
|
1060
|
+
function parseDiffByFile(rawDiff) {
|
|
1061
|
+
const sections = new Map;
|
|
1062
|
+
const headerPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
|
|
1063
|
+
const positions = [];
|
|
1064
|
+
for (let match = headerPattern.exec(rawDiff);match !== null; match = headerPattern.exec(rawDiff)) {
|
|
1065
|
+
const aFile = match[1];
|
|
1066
|
+
const bFile = match[2] ?? aFile;
|
|
1067
|
+
positions.push({ aFile, bFile, start: match.index });
|
|
1068
|
+
}
|
|
1069
|
+
for (let i = 0;i < positions.length; i++) {
|
|
1070
|
+
const { aFile, bFile, start } = positions[i];
|
|
1071
|
+
const end = i + 1 < positions.length ? positions[i + 1].start : rawDiff.length;
|
|
1072
|
+
const section = rawDiff.slice(start, end);
|
|
1073
|
+
sections.set(aFile, section);
|
|
1074
|
+
if (bFile && bFile !== aFile) {
|
|
1075
|
+
sections.set(bFile, section);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return sections;
|
|
1079
|
+
}
|
|
1080
|
+
function extractDiffStats(diffSection) {
|
|
1081
|
+
let added = 0;
|
|
1082
|
+
let removed = 0;
|
|
1083
|
+
for (const line of diffSection.split(`
|
|
1084
|
+
`)) {
|
|
1085
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
1086
|
+
added++;
|
|
1087
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
1088
|
+
removed++;
|
|
1089
|
+
}
|
|
1090
|
+
return { added, removed };
|
|
1091
|
+
}
|
|
1092
|
+
function createCompactDiff(files, rawDiff, maxTotalChars = BATCH_CONFIG.MAX_COMPACT_PAYLOAD) {
|
|
1093
|
+
if (files.length === 0)
|
|
1094
|
+
return "";
|
|
1095
|
+
const diffSections = parseDiffByFile(rawDiff);
|
|
1096
|
+
const perFileBudget = Math.min(BATCH_CONFIG.COMPACT_PER_FILE_CHARS, Math.floor(maxTotalChars / files.length));
|
|
1097
|
+
const parts = [];
|
|
1098
|
+
for (const file of files) {
|
|
1099
|
+
const section = diffSections.get(file);
|
|
1100
|
+
if (section) {
|
|
1101
|
+
const stats = extractDiffStats(section);
|
|
1102
|
+
const header = `[${file}] (+${stats.added}/-${stats.removed})`;
|
|
1103
|
+
if (section.length <= perFileBudget) {
|
|
1104
|
+
parts.push(`${header}
|
|
1105
|
+
${section}`);
|
|
1106
|
+
} else {
|
|
1107
|
+
const availableForBody = perFileBudget - header.length - 20;
|
|
1108
|
+
if (availableForBody <= 0) {
|
|
1109
|
+
parts.push(header);
|
|
1110
|
+
} else {
|
|
1111
|
+
const truncated = section.slice(0, availableForBody);
|
|
1112
|
+
parts.push(`${header}
|
|
1113
|
+
${truncated}
|
|
1114
|
+
...(truncated)`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
parts.push(`[${file}] (new/binary file — no diff available)`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const result = parts.join(`
|
|
1122
|
+
|
|
1123
|
+
`);
|
|
1124
|
+
return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
|
|
1125
|
+
...(truncated)` : result;
|
|
1126
|
+
}
|
|
956
1127
|
async function checkCopilotAvailable() {
|
|
957
1128
|
try {
|
|
958
1129
|
const client = await getManagedClient();
|
|
@@ -1051,18 +1222,23 @@ function extractJson(raw) {
|
|
|
1051
1222
|
}
|
|
1052
1223
|
return text2;
|
|
1053
1224
|
}
|
|
1054
|
-
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
1225
|
+
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit", context) {
|
|
1055
1226
|
try {
|
|
1227
|
+
const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1056
1228
|
const multiFileHint = stagedFiles.length > 1 ? `
|
|
1057
1229
|
|
|
1058
1230
|
IMPORTANT: Multiple files are staged. Generate ONE commit message that captures the high-level purpose of ALL changes together. Focus on the overall intent, not individual file changes. Be specific but concise — do not list every file.` : "";
|
|
1231
|
+
const squashHint = context === "squash-merge" ? `
|
|
1232
|
+
|
|
1233
|
+
CONTEXT: This is a squash merge of an entire feature branch into the base branch. All commits are being combined into ONE single commit. Generate a single high-level summary that describes the overall feature or change — NOT a list of individual commits. Think: what capability was added or what problem was solved? Be specific but concise.` : "";
|
|
1234
|
+
const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
|
|
1059
1235
|
const userMessage = `Generate a commit message for these staged changes:
|
|
1060
1236
|
|
|
1061
|
-
Files: ${stagedFiles.join(", ")}
|
|
1237
|
+
Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
|
|
1062
1238
|
|
|
1063
1239
|
Diff:
|
|
1064
|
-
${
|
|
1065
|
-
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
1240
|
+
${diffContent}${multiFileHint}${squashHint}`;
|
|
1241
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
|
|
1066
1242
|
return result?.trim() ?? null;
|
|
1067
1243
|
} catch {
|
|
1068
1244
|
return null;
|
|
@@ -1111,16 +1287,23 @@ ${conflictDiff.slice(0, 4000)}`;
|
|
|
1111
1287
|
}
|
|
1112
1288
|
}
|
|
1113
1289
|
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
1290
|
+
const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1291
|
+
const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
|
|
1292
|
+
const largeHint = isLarge ? `
|
|
1293
|
+
|
|
1294
|
+
NOTE: This is a large changeset (${files.length} files). Compact diffs are provided for every file. Focus on creating well-organized logical groups.` : "";
|
|
1114
1295
|
const userMessage = `Group these changed files into logical atomic commits:
|
|
1115
1296
|
|
|
1116
1297
|
Files:
|
|
1117
1298
|
${files.join(`
|
|
1118
1299
|
`)}
|
|
1119
1300
|
|
|
1120
|
-
Diffs
|
|
1121
|
-
${
|
|
1301
|
+
Diffs:
|
|
1302
|
+
${diffContent}${largeHint}`;
|
|
1122
1303
|
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1123
1304
|
if (!result) {
|
|
1305
|
+
if (isLarge)
|
|
1306
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1124
1307
|
throw new Error("AI returned an empty response");
|
|
1125
1308
|
}
|
|
1126
1309
|
const cleaned = extractJson(result);
|
|
@@ -1128,10 +1311,14 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1128
1311
|
try {
|
|
1129
1312
|
parsed = JSON.parse(cleaned);
|
|
1130
1313
|
} catch {
|
|
1314
|
+
if (isLarge)
|
|
1315
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1131
1316
|
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
1132
1317
|
}
|
|
1133
1318
|
const groups = parsed;
|
|
1134
1319
|
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1320
|
+
if (isLarge)
|
|
1321
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1135
1322
|
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
1136
1323
|
}
|
|
1137
1324
|
for (const group of groups) {
|
|
@@ -1141,7 +1328,63 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1141
1328
|
}
|
|
1142
1329
|
return groups;
|
|
1143
1330
|
}
|
|
1331
|
+
async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
|
|
1332
|
+
const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
|
|
1333
|
+
const allGroups = [];
|
|
1334
|
+
const diffSections = parseDiffByFile(diffs);
|
|
1335
|
+
for (let i = 0;i < files.length; i += batchSize) {
|
|
1336
|
+
const batchFiles = files.slice(i, i + batchSize);
|
|
1337
|
+
const batchDiff = batchFiles.map((f) => diffSections.get(f) ?? "").filter(Boolean).join(`
|
|
1338
|
+
`);
|
|
1339
|
+
const batchDiffContent = batchFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? createCompactDiff(batchFiles, batchDiff) : batchDiff.slice(0, 6000);
|
|
1340
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
1341
|
+
const totalBatches = Math.ceil(files.length / batchSize);
|
|
1342
|
+
const userMessage = `Group these changed files into logical atomic commits:
|
|
1343
|
+
|
|
1344
|
+
Files:
|
|
1345
|
+
${batchFiles.join(`
|
|
1346
|
+
`)}
|
|
1347
|
+
|
|
1348
|
+
Diffs:
|
|
1349
|
+
${batchDiffContent}
|
|
1350
|
+
|
|
1351
|
+
NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group only the files listed above.`;
|
|
1352
|
+
try {
|
|
1353
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1354
|
+
if (!result)
|
|
1355
|
+
continue;
|
|
1356
|
+
const cleaned = extractJson(result);
|
|
1357
|
+
const parsed = JSON.parse(cleaned);
|
|
1358
|
+
if (Array.isArray(parsed)) {
|
|
1359
|
+
for (const group of parsed) {
|
|
1360
|
+
if (Array.isArray(group.files) && typeof group.message === "string") {
|
|
1361
|
+
const batchFileSet = new Set(batchFiles);
|
|
1362
|
+
const filteredFiles = group.files.filter((f) => batchFileSet.has(f));
|
|
1363
|
+
if (filteredFiles.length > 0) {
|
|
1364
|
+
allGroups.push({ ...group, files: filteredFiles });
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} catch {}
|
|
1370
|
+
}
|
|
1371
|
+
const groupedFiles = new Set(allGroups.flatMap((g) => g.files));
|
|
1372
|
+
const ungrouped = files.filter((f) => !groupedFiles.has(f));
|
|
1373
|
+
if (ungrouped.length > 0) {
|
|
1374
|
+
allGroups.push({
|
|
1375
|
+
files: ungrouped,
|
|
1376
|
+
message: `chore: update ${ungrouped.length} remaining file${ungrouped.length !== 1 ? "s" : ""}`
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
if (allGroups.length === 0) {
|
|
1380
|
+
throw new Error("AI could not group any files even with batch processing");
|
|
1381
|
+
}
|
|
1382
|
+
return allGroups;
|
|
1383
|
+
}
|
|
1144
1384
|
async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
|
|
1385
|
+
const totalFiles = groups.reduce((sum, g) => sum + g.files.length, 0);
|
|
1386
|
+
const isLarge = totalFiles >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1387
|
+
const diffContent = isLarge ? createCompactDiff(groups.flatMap((g) => g.files), diffs) : diffs.slice(0, 6000);
|
|
1145
1388
|
const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
|
|
1146
1389
|
`);
|
|
1147
1390
|
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
@@ -1149,8 +1392,8 @@ async function regenerateAllGroupMessages(groups, diffs, model, convention = "cl
|
|
|
1149
1392
|
Groups:
|
|
1150
1393
|
${groupSummary}
|
|
1151
1394
|
|
|
1152
|
-
Diffs
|
|
1153
|
-
${
|
|
1395
|
+
Diffs:
|
|
1396
|
+
${diffContent}`;
|
|
1154
1397
|
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1155
1398
|
if (!result)
|
|
1156
1399
|
return groups;
|
|
@@ -1169,12 +1412,14 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1169
1412
|
}
|
|
1170
1413
|
async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
|
|
1171
1414
|
try {
|
|
1415
|
+
const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1416
|
+
const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 4000);
|
|
1172
1417
|
const userMessage = `Generate a single commit message for these files:
|
|
1173
1418
|
|
|
1174
1419
|
Files: ${files.join(", ")}
|
|
1175
1420
|
|
|
1176
1421
|
Diff:
|
|
1177
|
-
${
|
|
1422
|
+
${diffContent}`;
|
|
1178
1423
|
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
1179
1424
|
return result?.trim() ?? null;
|
|
1180
1425
|
} catch {
|
|
@@ -1724,6 +1969,27 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1724
1969
|
}
|
|
1725
1970
|
}
|
|
1726
1971
|
info(`Staged files: ${stagedFiles.join(", ")}`);
|
|
1972
|
+
const LARGE_COMMIT_THRESHOLD = 10;
|
|
1973
|
+
if (stagedFiles.length >= LARGE_COMMIT_THRESHOLD && !args.group) {
|
|
1974
|
+
const dirs = new Set(stagedFiles.map((f) => f.split("/")[0]));
|
|
1975
|
+
if (dirs.size > 1) {
|
|
1976
|
+
console.log();
|
|
1977
|
+
warn(`You're staging ${pc6.bold(String(stagedFiles.length))} files across ${pc6.bold(String(dirs.size))} directories in a single commit.`);
|
|
1978
|
+
info(pc6.dim("Large commits mixing different topics make history harder to read and bisect. " + "For cleaner history, consider splitting into atomic commits."));
|
|
1979
|
+
const choice = await selectPrompt("How would you like to proceed?", [
|
|
1980
|
+
"Continue as single commit",
|
|
1981
|
+
"Switch to group mode (AI splits into atomic commits)",
|
|
1982
|
+
"Cancel"
|
|
1983
|
+
]);
|
|
1984
|
+
if (choice === "Cancel") {
|
|
1985
|
+
process.exit(0);
|
|
1986
|
+
}
|
|
1987
|
+
if (choice === "Switch to group mode (AI splits into atomic commits)") {
|
|
1988
|
+
await runGroupCommit(args.model, config);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1727
1993
|
let commitMessage = null;
|
|
1728
1994
|
const useAI = !args["no-ai"];
|
|
1729
1995
|
if (useAI) {
|
|
@@ -1732,7 +1998,8 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1732
1998
|
warn(`AI unavailable: ${copilotError}`);
|
|
1733
1999
|
warn("Falling back to manual commit message entry.");
|
|
1734
2000
|
} else {
|
|
1735
|
-
const
|
|
2001
|
+
const spinnerMsg = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? `Generating commit message with AI (${stagedFiles.length} files — using optimized batching)...` : "Generating commit message with AI...";
|
|
2002
|
+
const spinner = createSpinner(spinnerMsg);
|
|
1736
2003
|
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
1737
2004
|
if (commitMessage) {
|
|
1738
2005
|
spinner.success("AI commit message generated.");
|
|
@@ -1823,7 +2090,7 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1823
2090
|
for (const f of changedFiles) {
|
|
1824
2091
|
console.log(` ${pc6.dim("•")} ${f}`);
|
|
1825
2092
|
}
|
|
1826
|
-
const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
2093
|
+
const spinner = createSpinner(changedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? `Asking AI to group ${changedFiles.length} file(s) into logical commits (using optimized batching)...` : `Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
1827
2094
|
const diffs = await getFullDiffForFiles(changedFiles);
|
|
1828
2095
|
if (!diffs.trim()) {
|
|
1829
2096
|
spinner.stop();
|
|
@@ -2001,8 +2268,8 @@ import pc7 from "picocolors";
|
|
|
2001
2268
|
// package.json
|
|
2002
2269
|
var package_default = {
|
|
2003
2270
|
name: "contribute-now",
|
|
2004
|
-
version: "0.4.
|
|
2005
|
-
description: "
|
|
2271
|
+
version: "0.4.1-dev.3325fdb",
|
|
2272
|
+
description: "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
|
|
2006
2273
|
type: "module",
|
|
2007
2274
|
bin: {
|
|
2008
2275
|
contrib: "dist/index.js",
|
|
@@ -2019,9 +2286,10 @@ var package_default = {
|
|
|
2019
2286
|
lint: "biome check .",
|
|
2020
2287
|
"lint:fix": "biome check --write .",
|
|
2021
2288
|
format: "biome format --write .",
|
|
2022
|
-
"
|
|
2023
|
-
"
|
|
2024
|
-
"
|
|
2289
|
+
"landing:install": "bun install --cwd landing",
|
|
2290
|
+
"landing:dev": "bun run --cwd landing dev",
|
|
2291
|
+
"landing:build": "bun run --cwd landing build",
|
|
2292
|
+
"landing:preview": "bun run --cwd landing preview"
|
|
2025
2293
|
},
|
|
2026
2294
|
engines: {
|
|
2027
2295
|
node: ">=18",
|
|
@@ -2519,7 +2787,19 @@ var log_default = defineCommand6({
|
|
|
2519
2787
|
all: {
|
|
2520
2788
|
type: "boolean",
|
|
2521
2789
|
alias: "a",
|
|
2522
|
-
description: "Show all branches
|
|
2790
|
+
description: "Show commits from all branches",
|
|
2791
|
+
default: false
|
|
2792
|
+
},
|
|
2793
|
+
remote: {
|
|
2794
|
+
type: "boolean",
|
|
2795
|
+
alias: "r",
|
|
2796
|
+
description: "Show only remote commits not yet pulled locally",
|
|
2797
|
+
default: false
|
|
2798
|
+
},
|
|
2799
|
+
full: {
|
|
2800
|
+
type: "boolean",
|
|
2801
|
+
alias: "f",
|
|
2802
|
+
description: "Show full commit history for the current branch",
|
|
2523
2803
|
default: false
|
|
2524
2804
|
},
|
|
2525
2805
|
graph: {
|
|
@@ -2541,44 +2821,197 @@ var log_default = defineCommand6({
|
|
|
2541
2821
|
}
|
|
2542
2822
|
const config = readConfig();
|
|
2543
2823
|
const count = args.count ? Number.parseInt(args.count, 10) : 20;
|
|
2544
|
-
const showAll = args.all;
|
|
2545
2824
|
const showGraph = args.graph;
|
|
2546
2825
|
const targetBranch = args.branch;
|
|
2826
|
+
let mode = "local";
|
|
2827
|
+
if (args.all)
|
|
2828
|
+
mode = "all";
|
|
2829
|
+
else if (args.remote)
|
|
2830
|
+
mode = "remote";
|
|
2831
|
+
else if (args.full || targetBranch)
|
|
2832
|
+
mode = "full";
|
|
2547
2833
|
const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
|
|
2548
2834
|
const currentBranch = await getCurrentBranch();
|
|
2835
|
+
const upstream = await getUpstreamRef();
|
|
2836
|
+
let compareRef = upstream;
|
|
2837
|
+
let usingFallback = false;
|
|
2838
|
+
if (!compareRef) {
|
|
2839
|
+
const fallback = await resolveBaseBranchRef(config);
|
|
2840
|
+
if (fallback) {
|
|
2841
|
+
compareRef = fallback;
|
|
2842
|
+
usingFallback = true;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2549
2845
|
heading("\uD83D\uDCDC commit log");
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
if (
|
|
2553
|
-
console.log(pc9.dim(" No commits found."));
|
|
2846
|
+
printModeHeader(mode, currentBranch, compareRef, usingFallback);
|
|
2847
|
+
if (mode === "local" || mode === "remote") {
|
|
2848
|
+
if (!compareRef) {
|
|
2554
2849
|
console.log();
|
|
2850
|
+
console.log(pc9.yellow(" ⚠ Could not determine a comparison branch."));
|
|
2851
|
+
console.log(pc9.dim(" No upstream tracking set and no remote base branch found."));
|
|
2852
|
+
console.log(pc9.dim(` Use ${pc9.bold("contrib log --full")} to see the full commit history instead.`));
|
|
2853
|
+
console.log();
|
|
2854
|
+
printGuidance();
|
|
2555
2855
|
return;
|
|
2556
2856
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2857
|
+
const hasCommits = await renderScopedLog({ mode, count, upstream: compareRef, showGraph, protectedBranches, currentBranch });
|
|
2858
|
+
if (!hasCommits) {
|
|
2859
|
+
printGuidance();
|
|
2860
|
+
return;
|
|
2560
2861
|
}
|
|
2561
2862
|
} else {
|
|
2562
|
-
const
|
|
2563
|
-
if (
|
|
2564
|
-
|
|
2565
|
-
console.log();
|
|
2863
|
+
const hasCommits = await renderFullLog({ count, all: mode === "all", showGraph, targetBranch, protectedBranches, currentBranch });
|
|
2864
|
+
if (!hasCommits) {
|
|
2865
|
+
printGuidance();
|
|
2566
2866
|
return;
|
|
2567
2867
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2868
|
+
}
|
|
2869
|
+
printFooter(mode, count, targetBranch);
|
|
2870
|
+
printGuidance();
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
async function resolveBaseBranchRef(config) {
|
|
2874
|
+
if (!config) {
|
|
2875
|
+
for (const candidate2 of ["origin/main", "origin/master"]) {
|
|
2876
|
+
if (await branchExists(candidate2))
|
|
2877
|
+
return candidate2;
|
|
2878
|
+
}
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
const baseBranch = getBaseBranch(config);
|
|
2882
|
+
const remote = config.origin ?? "origin";
|
|
2883
|
+
const candidate = `${remote}/${baseBranch}`;
|
|
2884
|
+
if (await branchExists(candidate))
|
|
2885
|
+
return candidate;
|
|
2886
|
+
for (const fallback of ["origin/main", "origin/master"]) {
|
|
2887
|
+
if (fallback !== candidate && await branchExists(fallback))
|
|
2888
|
+
return fallback;
|
|
2889
|
+
}
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2892
|
+
function printModeHeader(mode, currentBranch, compareRef, usingFallback = false) {
|
|
2893
|
+
const branch = currentBranch ?? "HEAD";
|
|
2894
|
+
const fallbackNote = usingFallback ? pc9.yellow(" (no upstream — comparing against base branch)") : "";
|
|
2895
|
+
console.log();
|
|
2896
|
+
switch (mode) {
|
|
2897
|
+
case "local":
|
|
2898
|
+
console.log(pc9.dim(` mode: ${pc9.bold("local")} — unpushed commits on ${pc9.bold(branch)}`) + fallbackNote);
|
|
2899
|
+
if (compareRef) {
|
|
2900
|
+
console.log(pc9.dim(` comparing: ${pc9.bold(compareRef)} ➜ ${pc9.bold("HEAD")}`));
|
|
2574
2901
|
}
|
|
2902
|
+
break;
|
|
2903
|
+
case "remote":
|
|
2904
|
+
console.log(pc9.dim(` mode: ${pc9.bold("remote")} — commits on remote not yet pulled into ${pc9.bold(branch)}`) + fallbackNote);
|
|
2905
|
+
if (compareRef) {
|
|
2906
|
+
console.log(pc9.dim(` comparing: ${pc9.bold("HEAD")} ➜ ${pc9.bold(compareRef)}`));
|
|
2907
|
+
}
|
|
2908
|
+
break;
|
|
2909
|
+
case "full":
|
|
2910
|
+
console.log(pc9.dim(` mode: ${pc9.bold("full")} — complete commit history for ${pc9.bold(branch)}`));
|
|
2911
|
+
break;
|
|
2912
|
+
case "all":
|
|
2913
|
+
console.log(pc9.dim(` mode: ${pc9.bold("all")} — commits across all branches`));
|
|
2914
|
+
break;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
async function renderScopedLog(options) {
|
|
2918
|
+
const { mode, count, upstream, showGraph, protectedBranches, currentBranch } = options;
|
|
2919
|
+
if (showGraph) {
|
|
2920
|
+
const graphFn = mode === "local" ? getLocalCommitsGraph : getRemoteOnlyCommitsGraph;
|
|
2921
|
+
const lines = await graphFn({ count, upstream });
|
|
2922
|
+
if (lines.length === 0) {
|
|
2923
|
+
printEmptyState(mode);
|
|
2924
|
+
return false;
|
|
2575
2925
|
}
|
|
2576
2926
|
console.log();
|
|
2577
|
-
|
|
2578
|
-
|
|
2927
|
+
for (const line of lines) {
|
|
2928
|
+
console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
|
|
2929
|
+
}
|
|
2930
|
+
} else {
|
|
2931
|
+
const entryFn = mode === "local" ? getLocalCommitsEntries : getRemoteOnlyCommitsEntries;
|
|
2932
|
+
const entries = await entryFn({ count, upstream });
|
|
2933
|
+
if (entries.length === 0) {
|
|
2934
|
+
printEmptyState(mode);
|
|
2935
|
+
return false;
|
|
2936
|
+
}
|
|
2579
2937
|
console.log();
|
|
2938
|
+
for (const entry of entries) {
|
|
2939
|
+
const hashStr = pc9.yellow(entry.hash);
|
|
2940
|
+
const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
|
|
2941
|
+
const subjectStr = colorizeSubject(entry.subject);
|
|
2942
|
+
console.log(` ${hashStr}${refsStr} ${subjectStr}`);
|
|
2943
|
+
}
|
|
2580
2944
|
}
|
|
2581
|
-
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
function printEmptyState(mode) {
|
|
2948
|
+
console.log();
|
|
2949
|
+
if (mode === "local") {
|
|
2950
|
+
console.log(pc9.dim(" No local unpushed commits — you're up to date with remote!"));
|
|
2951
|
+
} else {
|
|
2952
|
+
console.log(pc9.dim(" No remote-only commits — your local branch is up to date!"));
|
|
2953
|
+
}
|
|
2954
|
+
console.log();
|
|
2955
|
+
}
|
|
2956
|
+
async function renderFullLog(options) {
|
|
2957
|
+
const { count, all, showGraph, targetBranch, protectedBranches, currentBranch } = options;
|
|
2958
|
+
if (showGraph) {
|
|
2959
|
+
const lines = await getLogGraph({ count, all, branch: targetBranch });
|
|
2960
|
+
if (lines.length === 0) {
|
|
2961
|
+
console.log(pc9.dim(" No commits found."));
|
|
2962
|
+
console.log();
|
|
2963
|
+
return false;
|
|
2964
|
+
}
|
|
2965
|
+
console.log();
|
|
2966
|
+
for (const line of lines) {
|
|
2967
|
+
console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
|
|
2968
|
+
}
|
|
2969
|
+
} else {
|
|
2970
|
+
const entries = await getLogEntries({ count, all, branch: targetBranch });
|
|
2971
|
+
if (entries.length === 0) {
|
|
2972
|
+
console.log(pc9.dim(" No commits found."));
|
|
2973
|
+
console.log();
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
console.log();
|
|
2977
|
+
for (const entry of entries) {
|
|
2978
|
+
const hashStr = pc9.yellow(entry.hash);
|
|
2979
|
+
const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
|
|
2980
|
+
const subjectStr = colorizeSubject(entry.subject);
|
|
2981
|
+
console.log(` ${hashStr}${refsStr} ${subjectStr}`);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
function printFooter(mode, count, targetBranch) {
|
|
2987
|
+
console.log();
|
|
2988
|
+
switch (mode) {
|
|
2989
|
+
case "local":
|
|
2990
|
+
console.log(pc9.dim(` Showing up to ${count} unpushed commits`));
|
|
2991
|
+
break;
|
|
2992
|
+
case "remote":
|
|
2993
|
+
console.log(pc9.dim(` Showing up to ${count} remote-only commits`));
|
|
2994
|
+
break;
|
|
2995
|
+
case "full":
|
|
2996
|
+
console.log(pc9.dim(` Showing ${count} most recent commits${targetBranch ? ` (${targetBranch})` : ""}`));
|
|
2997
|
+
break;
|
|
2998
|
+
case "all":
|
|
2999
|
+
console.log(pc9.dim(` Showing ${count} most recent commits (all branches)`));
|
|
3000
|
+
break;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function printGuidance() {
|
|
3004
|
+
console.log();
|
|
3005
|
+
console.log(pc9.dim(" ─── quick guide ───"));
|
|
3006
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log")} local unpushed commits (default)`));
|
|
3007
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --remote")} commits on remote not yet pulled`));
|
|
3008
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --full")} full history for the current branch`));
|
|
3009
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --all")} commits across all branches`));
|
|
3010
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log -n 50")} change the commit limit (default: 20)`));
|
|
3011
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log -b dev")} view log for a specific branch`));
|
|
3012
|
+
console.log(pc9.dim(` ${pc9.bold("contrib log --no-graph")} flat list without graph lines`));
|
|
3013
|
+
console.log();
|
|
3014
|
+
}
|
|
2582
3015
|
function colorizeGraphLine(line, protectedBranches, currentBranch) {
|
|
2583
3016
|
const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
|
|
2584
3017
|
if (!match) {
|
|
@@ -2660,6 +3093,43 @@ function colorizeSubject(subject) {
|
|
|
2660
3093
|
// src/commands/setup.ts
|
|
2661
3094
|
import { defineCommand as defineCommand7 } from "citty";
|
|
2662
3095
|
import pc10 from "picocolors";
|
|
3096
|
+
async function shouldContinueSetupWithExistingConfig(options) {
|
|
3097
|
+
const {
|
|
3098
|
+
existingConfig,
|
|
3099
|
+
hasConfigFile,
|
|
3100
|
+
confirm: confirm2,
|
|
3101
|
+
ensureIgnored,
|
|
3102
|
+
onInfo,
|
|
3103
|
+
onWarn,
|
|
3104
|
+
onSuccess,
|
|
3105
|
+
summary
|
|
3106
|
+
} = options;
|
|
3107
|
+
if (existingConfig) {
|
|
3108
|
+
onInfo("Existing .contributerc.json detected:");
|
|
3109
|
+
summary(existingConfig);
|
|
3110
|
+
const shouldContinue = await confirm2("Continue setup and overwrite existing config?");
|
|
3111
|
+
if (!shouldContinue) {
|
|
3112
|
+
if (ensureIgnored()) {
|
|
3113
|
+
onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
|
|
3114
|
+
}
|
|
3115
|
+
onSuccess("Keeping existing setup.");
|
|
3116
|
+
return false;
|
|
3117
|
+
}
|
|
3118
|
+
return true;
|
|
3119
|
+
}
|
|
3120
|
+
if (hasConfigFile) {
|
|
3121
|
+
onWarn("Found .contributerc.json but it appears invalid.");
|
|
3122
|
+
const shouldContinue = await confirm2("Continue setup and overwrite invalid config?");
|
|
3123
|
+
if (!shouldContinue) {
|
|
3124
|
+
if (ensureIgnored()) {
|
|
3125
|
+
onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
|
|
3126
|
+
}
|
|
3127
|
+
onInfo("Keeping existing file. Run setup again when ready to repair it.");
|
|
3128
|
+
return false;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
return true;
|
|
3132
|
+
}
|
|
2663
3133
|
var setup_default = defineCommand7({
|
|
2664
3134
|
meta: {
|
|
2665
3135
|
name: "setup",
|
|
@@ -2671,6 +3141,20 @@ var setup_default = defineCommand7({
|
|
|
2671
3141
|
process.exit(1);
|
|
2672
3142
|
}
|
|
2673
3143
|
heading("\uD83D\uDD27 contribute-now setup");
|
|
3144
|
+
const existingConfig = readConfig();
|
|
3145
|
+
const shouldContinue = await shouldContinueSetupWithExistingConfig({
|
|
3146
|
+
existingConfig,
|
|
3147
|
+
hasConfigFile: configExists(),
|
|
3148
|
+
confirm: confirmPrompt,
|
|
3149
|
+
ensureIgnored: ensureGitignored,
|
|
3150
|
+
onInfo: info,
|
|
3151
|
+
onWarn: warn,
|
|
3152
|
+
onSuccess: success,
|
|
3153
|
+
summary: logConfigSummary
|
|
3154
|
+
});
|
|
3155
|
+
if (!shouldContinue) {
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
2674
3158
|
const workflowChoice = await selectPrompt("Which git workflow does this project use?", [
|
|
2675
3159
|
"Clean Flow — main + dev, squash features into dev, merge dev into main (recommended)",
|
|
2676
3160
|
"GitHub Flow — main + feature branches, squash/merge into main",
|
|
@@ -2700,31 +3184,42 @@ var setup_default = defineCommand7({
|
|
|
2700
3184
|
info(`Found remotes: ${remotes.join(", ")}`);
|
|
2701
3185
|
let detectedRole = null;
|
|
2702
3186
|
let detectionSource = "";
|
|
2703
|
-
const
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
3187
|
+
const roleSpinner = createSpinner("Detecting your role...");
|
|
3188
|
+
try {
|
|
3189
|
+
roleSpinner.update("Checking GitHub CLI and auth...");
|
|
3190
|
+
const ghInstalled = await checkGhInstalled();
|
|
3191
|
+
if (ghInstalled && await checkGhAuth()) {
|
|
3192
|
+
roleSpinner.update("Inspecting repository relationship (fork/permissions)...");
|
|
3193
|
+
const isFork = await isRepoFork();
|
|
3194
|
+
if (isFork === true) {
|
|
3195
|
+
detectedRole = "contributor";
|
|
3196
|
+
detectionSource = "gh CLI (fork detected)";
|
|
3197
|
+
} else if (isFork === false) {
|
|
3198
|
+
const repoInfo = await getCurrentRepoInfo();
|
|
3199
|
+
if (repoInfo) {
|
|
3200
|
+
const perms = await checkRepoPermissions(repoInfo.owner, repoInfo.repo);
|
|
3201
|
+
if (perms?.admin || perms?.push) {
|
|
3202
|
+
detectedRole = "maintainer";
|
|
3203
|
+
detectionSource = "gh CLI (admin/push permissions)";
|
|
3204
|
+
}
|
|
2716
3205
|
}
|
|
2717
3206
|
}
|
|
2718
3207
|
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
3208
|
+
if (detectedRole === null) {
|
|
3209
|
+
roleSpinner.update("Analyzing git remotes...");
|
|
3210
|
+
if (remotes.includes("upstream")) {
|
|
3211
|
+
detectedRole = "contributor";
|
|
3212
|
+
detectionSource = "heuristic (upstream remote exists)";
|
|
3213
|
+
} else if (remotes.includes("origin") && remotes.length === 1) {
|
|
3214
|
+
detectedRole = "maintainer";
|
|
3215
|
+
detectionSource = "heuristic (only origin remote)";
|
|
3216
|
+
}
|
|
2727
3217
|
}
|
|
3218
|
+
roleSpinner.success("Role detection complete.");
|
|
3219
|
+
} catch {
|
|
3220
|
+
roleSpinner.fail("Role detection failed; falling back to manual selection.");
|
|
3221
|
+
detectedRole = null;
|
|
3222
|
+
detectionSource = "";
|
|
2728
3223
|
}
|
|
2729
3224
|
if (detectedRole === null) {
|
|
2730
3225
|
const roleChoice = await selectPrompt("What is your role in this project?", [
|
|
@@ -2742,16 +3237,20 @@ var setup_default = defineCommand7({
|
|
|
2742
3237
|
}
|
|
2743
3238
|
}
|
|
2744
3239
|
const defaultConfig = getDefaultConfig();
|
|
2745
|
-
|
|
3240
|
+
info(pc10.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
|
|
3241
|
+
const mainBranchDefault = defaultConfig.mainBranch;
|
|
3242
|
+
const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} — press Enter to keep)`, mainBranchDefault);
|
|
2746
3243
|
let devBranch;
|
|
2747
3244
|
if (hasDevBranch(workflow)) {
|
|
2748
3245
|
const defaultDev = workflow === "git-flow" ? "develop" : "dev";
|
|
2749
|
-
devBranch = await inputPrompt(
|
|
3246
|
+
devBranch = await inputPrompt(`Dev/develop branch name (default: ${defaultDev} — press Enter to keep)`, defaultDev);
|
|
2750
3247
|
}
|
|
2751
|
-
const
|
|
3248
|
+
const originRemoteDefault = defaultConfig.origin;
|
|
3249
|
+
const originRemote = await inputPrompt(`Origin remote name (default: ${originRemoteDefault} — press Enter to keep)`, originRemoteDefault);
|
|
2752
3250
|
let upstreamRemote = defaultConfig.upstream;
|
|
2753
3251
|
if (detectedRole === "contributor") {
|
|
2754
|
-
|
|
3252
|
+
const upstreamRemoteDefault = defaultConfig.upstream;
|
|
3253
|
+
upstreamRemote = await inputPrompt(`Upstream remote name (default: ${upstreamRemoteDefault} — press Enter to keep)`, upstreamRemoteDefault);
|
|
2755
3254
|
if (!remotes.includes(upstreamRemote)) {
|
|
2756
3255
|
warn(`Remote "${upstreamRemote}" not found.`);
|
|
2757
3256
|
const originUrl = await getRemoteUrl(originRemote);
|
|
@@ -2799,9 +3298,8 @@ var setup_default = defineCommand7({
|
|
|
2799
3298
|
warn("Config was saved — verify the branch name and re-run setup if needed.");
|
|
2800
3299
|
}
|
|
2801
3300
|
}
|
|
2802
|
-
if (
|
|
2803
|
-
|
|
2804
|
-
warn(' echo ".contributerc.json" >> .gitignore');
|
|
3301
|
+
if (ensureGitignored()) {
|
|
3302
|
+
info("Added .contributerc.json to .gitignore to avoid committing personal config.");
|
|
2805
3303
|
}
|
|
2806
3304
|
console.log();
|
|
2807
3305
|
info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
@@ -2815,6 +3313,17 @@ var setup_default = defineCommand7({
|
|
|
2815
3313
|
info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
|
|
2816
3314
|
}
|
|
2817
3315
|
});
|
|
3316
|
+
function logConfigSummary(config) {
|
|
3317
|
+
info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
3318
|
+
info(`Convention: ${pc10.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
|
|
3319
|
+
info(`Role: ${pc10.bold(config.role)}`);
|
|
3320
|
+
if (config.devBranch) {
|
|
3321
|
+
info(`Main: ${pc10.bold(config.mainBranch)} | Dev: ${pc10.bold(config.devBranch)}`);
|
|
3322
|
+
} else {
|
|
3323
|
+
info(`Main: ${pc10.bold(config.mainBranch)}`);
|
|
3324
|
+
}
|
|
3325
|
+
info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
|
|
3326
|
+
}
|
|
2818
3327
|
|
|
2819
3328
|
// src/commands/start.ts
|
|
2820
3329
|
import { defineCommand as defineCommand8 } from "citty";
|
|
@@ -2983,10 +3492,20 @@ var status_default = defineCommand9({
|
|
|
2983
3492
|
const devLine = formatStatus(config.devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
|
|
2984
3493
|
console.log(devLine);
|
|
2985
3494
|
}
|
|
2986
|
-
|
|
3495
|
+
const protectedBranches = getProtectedBranches(config);
|
|
3496
|
+
const isFeatureBranch = currentBranch && !protectedBranches.includes(currentBranch);
|
|
3497
|
+
let branchStatus = null;
|
|
3498
|
+
if (isFeatureBranch) {
|
|
2987
3499
|
const branchDiv = await getDivergence(currentBranch, baseBranch);
|
|
2988
3500
|
const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
|
|
2989
3501
|
console.log(branchLine + pc12.dim(` (current ${pc12.green("*")})`));
|
|
3502
|
+
branchStatus = await detectBranchStatus(currentBranch, baseBranch);
|
|
3503
|
+
if (branchStatus.merged) {
|
|
3504
|
+
console.log(` ${pc12.green("✓")} ${pc12.green("Branch merged")} — ${pc12.dim(branchStatus.mergedReason ?? "all commits reachable from base")}`);
|
|
3505
|
+
}
|
|
3506
|
+
if (branchStatus.stale) {
|
|
3507
|
+
console.log(` ${pc12.yellow("⏳")} ${pc12.yellow("Branch is stale")} — ${pc12.dim(`last commit ${branchStatus.staleDaysAgo} days ago`)}`);
|
|
3508
|
+
}
|
|
2990
3509
|
} else if (currentBranch) {
|
|
2991
3510
|
console.log(pc12.dim(` (on ${pc12.bold(currentBranch)} branch)`));
|
|
2992
3511
|
}
|
|
@@ -3021,10 +3540,16 @@ var status_default = defineCommand9({
|
|
|
3021
3540
|
if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
|
|
3022
3541
|
tips.push(`Run ${pc12.bold("contrib commit")} to stage and commit changes`);
|
|
3023
3542
|
}
|
|
3024
|
-
if (
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3543
|
+
if (isFeatureBranch && branchStatus) {
|
|
3544
|
+
if (branchStatus.merged) {
|
|
3545
|
+
tips.push(`Run ${pc12.bold("contrib clean")} to delete this merged branch`);
|
|
3546
|
+
} else if (branchStatus.stale) {
|
|
3547
|
+
tips.push(`Run ${pc12.bold("contrib sync")} to rebase on latest changes, or ${pc12.bold("contrib clean")} if no longer needed`);
|
|
3548
|
+
} else if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0) {
|
|
3549
|
+
const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
|
|
3550
|
+
if (branchDiv.ahead > 0) {
|
|
3551
|
+
tips.push(`Run ${pc12.bold("contrib submit")} to push and create/update your PR`);
|
|
3552
|
+
}
|
|
3028
3553
|
}
|
|
3029
3554
|
}
|
|
3030
3555
|
if (tips.length > 0) {
|
|
@@ -3050,6 +3575,48 @@ function formatStatus(branch, base, ahead, behind) {
|
|
|
3050
3575
|
}
|
|
3051
3576
|
return ` ${pc12.red("⚡")} ${label} ${pc12.yellow(`${ahead} ahead`)}${pc12.dim(", ")}${pc12.red(`${behind} behind`)} ${pc12.dim(base)}`;
|
|
3052
3577
|
}
|
|
3578
|
+
var STALE_THRESHOLD_DAYS = 14;
|
|
3579
|
+
async function detectBranchStatus(branch, baseBranch) {
|
|
3580
|
+
const result = { merged: false, mergedReason: null, stale: false, staleDaysAgo: null };
|
|
3581
|
+
const div = await getDivergence(branch, baseBranch);
|
|
3582
|
+
const hasWork = div.ahead > 0;
|
|
3583
|
+
if (hasWork) {
|
|
3584
|
+
if (await isBranchMergedInto(branch, baseBranch)) {
|
|
3585
|
+
result.merged = true;
|
|
3586
|
+
result.mergedReason = `all commits reachable from ${baseBranch}`;
|
|
3587
|
+
return result;
|
|
3588
|
+
}
|
|
3589
|
+
const mergedBranches = await getMergedBranches(baseBranch);
|
|
3590
|
+
if (mergedBranches.includes(branch)) {
|
|
3591
|
+
result.merged = true;
|
|
3592
|
+
result.mergedReason = `listed in merged branches of ${baseBranch}`;
|
|
3593
|
+
return result;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
const goneBranches = await getGoneBranches();
|
|
3597
|
+
if (goneBranches.includes(branch)) {
|
|
3598
|
+
result.merged = true;
|
|
3599
|
+
result.mergedReason = "remote branch deleted (likely squash-merged)";
|
|
3600
|
+
return result;
|
|
3601
|
+
}
|
|
3602
|
+
if (await checkGhInstalled()) {
|
|
3603
|
+
const mergedPR = await getMergedPRForBranch(branch);
|
|
3604
|
+
if (mergedPR) {
|
|
3605
|
+
result.merged = true;
|
|
3606
|
+
result.mergedReason = `PR #${mergedPR.number} was merged`;
|
|
3607
|
+
return result;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
const lastDate = await getLastCommitDate(branch);
|
|
3611
|
+
if (lastDate) {
|
|
3612
|
+
const daysAgo = Math.floor((Date.now() - new Date(lastDate).getTime()) / (1000 * 60 * 60 * 24));
|
|
3613
|
+
if (daysAgo >= STALE_THRESHOLD_DAYS) {
|
|
3614
|
+
result.stale = true;
|
|
3615
|
+
result.staleDaysAgo = daysAgo;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return result;
|
|
3619
|
+
}
|
|
3053
3620
|
|
|
3054
3621
|
// src/commands/submit.ts
|
|
3055
3622
|
import { defineCommand as defineCommand10 } from "citty";
|
|
@@ -3073,10 +3640,12 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
|
3073
3640
|
if (!copilotError) {
|
|
3074
3641
|
const spinner = createSpinner("Generating AI commit message for squash merge...");
|
|
3075
3642
|
const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
|
|
3076
|
-
const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit");
|
|
3643
|
+
const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
|
|
3077
3644
|
if (aiMsg) {
|
|
3078
3645
|
message = aiMsg;
|
|
3079
3646
|
spinner.success("AI commit message generated.");
|
|
3647
|
+
console.log(`
|
|
3648
|
+
${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(message))}`);
|
|
3080
3649
|
} else {
|
|
3081
3650
|
spinner.fail("AI did not return a commit message.");
|
|
3082
3651
|
}
|
|
@@ -3084,13 +3653,38 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
|
3084
3653
|
warn(`AI unavailable: ${copilotError}`);
|
|
3085
3654
|
}
|
|
3086
3655
|
}
|
|
3087
|
-
|
|
3088
|
-
let finalMsg;
|
|
3656
|
+
let finalMsg = null;
|
|
3089
3657
|
if (message) {
|
|
3090
|
-
|
|
3091
|
-
|
|
3658
|
+
while (!finalMsg) {
|
|
3659
|
+
const action = await selectPrompt("What would you like to do?", [
|
|
3660
|
+
"Accept this message",
|
|
3661
|
+
"Edit this message",
|
|
3662
|
+
"Regenerate",
|
|
3663
|
+
"Write manually"
|
|
3664
|
+
]);
|
|
3665
|
+
if (action === "Accept this message") {
|
|
3666
|
+
finalMsg = message;
|
|
3667
|
+
} else if (action === "Edit this message") {
|
|
3668
|
+
finalMsg = await inputPrompt("Edit commit message", message);
|
|
3669
|
+
} else if (action === "Regenerate") {
|
|
3670
|
+
const spinner = createSpinner("Regenerating commit message...");
|
|
3671
|
+
const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
|
|
3672
|
+
const regen = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit", "squash-merge");
|
|
3673
|
+
if (regen) {
|
|
3674
|
+
message = regen;
|
|
3675
|
+
spinner.success("Commit message regenerated.");
|
|
3676
|
+
console.log(`
|
|
3677
|
+
${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(regen))}`);
|
|
3678
|
+
} else {
|
|
3679
|
+
spinner.fail("Regeneration failed.");
|
|
3680
|
+
finalMsg = await inputPrompt("Enter commit message");
|
|
3681
|
+
}
|
|
3682
|
+
} else {
|
|
3683
|
+
finalMsg = await inputPrompt("Enter commit message");
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3092
3686
|
} else {
|
|
3093
|
-
finalMsg = await inputPrompt("Commit message",
|
|
3687
|
+
finalMsg = await inputPrompt("Commit message", `squash merge ${featureBranch}`);
|
|
3094
3688
|
}
|
|
3095
3689
|
const commitResult = await commitWithMessage(finalMsg);
|
|
3096
3690
|
if (commitResult.exitCode !== 0) {
|
|
@@ -3343,6 +3937,25 @@ var submit_default = defineCommand10({
|
|
|
3343
3937
|
return;
|
|
3344
3938
|
}
|
|
3345
3939
|
}
|
|
3940
|
+
if (ghInstalled && ghAuthed) {
|
|
3941
|
+
const existingPR = await getPRForBranch(currentBranch);
|
|
3942
|
+
if (existingPR) {
|
|
3943
|
+
info(`Pushing ${pc13.bold(currentBranch)} to ${origin}...`);
|
|
3944
|
+
const pushResult2 = await pushSetUpstream(origin, currentBranch);
|
|
3945
|
+
if (pushResult2.exitCode !== 0) {
|
|
3946
|
+
error(`Failed to push: ${pushResult2.stderr}`);
|
|
3947
|
+
if (pushResult2.stderr.includes("rejected") || pushResult2.stderr.includes("non-fast-forward")) {
|
|
3948
|
+
warn("The remote branch has diverged. Try:");
|
|
3949
|
+
info(` git pull --rebase ${origin} ${currentBranch}`);
|
|
3950
|
+
info(" Then run `contrib submit` again.");
|
|
3951
|
+
}
|
|
3952
|
+
process.exit(1);
|
|
3953
|
+
}
|
|
3954
|
+
success(`Pushed changes to existing PR #${existingPR.number}: ${pc13.bold(existingPR.title)}`);
|
|
3955
|
+
console.log(` ${pc13.cyan(existingPR.url)}`);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3346
3959
|
let prTitle = null;
|
|
3347
3960
|
let prBody = null;
|
|
3348
3961
|
async function tryGenerateAI() {
|
|
@@ -3445,7 +4058,6 @@ ${pc13.dim("AI body preview:")}`);
|
|
|
3445
4058
|
}
|
|
3446
4059
|
if (submitAction === "squash") {
|
|
3447
4060
|
await performSquashMerge(origin, baseBranch, currentBranch, {
|
|
3448
|
-
defaultMsg: prTitle ?? undefined,
|
|
3449
4061
|
model: args.model,
|
|
3450
4062
|
convention: config.commitConvention
|
|
3451
4063
|
});
|
|
@@ -3476,12 +4088,6 @@ ${pc13.dim("AI body preview:")}`);
|
|
|
3476
4088
|
}
|
|
3477
4089
|
return;
|
|
3478
4090
|
}
|
|
3479
|
-
const existingPR = await getPRForBranch(currentBranch);
|
|
3480
|
-
if (existingPR) {
|
|
3481
|
-
success(`Pushed changes to existing PR #${existingPR.number}: ${pc13.bold(existingPR.title)}`);
|
|
3482
|
-
console.log(` ${pc13.cyan(existingPR.url)}`);
|
|
3483
|
-
return;
|
|
3484
|
-
}
|
|
3485
4091
|
if (submitAction === "fill") {
|
|
3486
4092
|
const fillResult = await createPRFill(baseBranch, args.draft);
|
|
3487
4093
|
if (fillResult.exitCode !== 0) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contribute-now",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.1-dev.3325fdb",
|
|
4
|
+
"description": "Developer CLI that automates git workflows — branching, syncing, committing, and PRs — with multi-workflow and commit convention support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"contrib": "dist/index.js",
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
"lint": "biome check .",
|
|
19
19
|
"lint:fix": "biome check --write .",
|
|
20
20
|
"format": "biome format --write .",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
21
|
+
"landing:install": "bun install --cwd landing",
|
|
22
|
+
"landing:dev": "bun run --cwd landing dev",
|
|
23
|
+
"landing:build": "bun run --cwd landing build",
|
|
24
|
+
"landing:preview": "bun run --cwd landing preview"
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=18",
|