contribute-now 0.3.0 → 0.4.0-dev.6151897
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/dist/index.js +272 -40
- package/package.json +1 -1
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",
|
|
@@ -205,6 +222,13 @@ async function branchExists(branch) {
|
|
|
205
222
|
const { exitCode } = await run(["rev-parse", "--verify", branch]);
|
|
206
223
|
return exitCode === 0;
|
|
207
224
|
}
|
|
225
|
+
async function countCommitsAhead(branch, upstream) {
|
|
226
|
+
const { exitCode, stdout } = await run(["rev-list", "--count", `${upstream}..${branch}`]);
|
|
227
|
+
if (exitCode !== 0)
|
|
228
|
+
return 0;
|
|
229
|
+
const count = Number.parseInt(stdout.trim(), 10);
|
|
230
|
+
return Number.isNaN(count) ? 0 : count;
|
|
231
|
+
}
|
|
208
232
|
async function fetchRemote(remote) {
|
|
209
233
|
return run(["fetch", remote]);
|
|
210
234
|
}
|
|
@@ -946,6 +970,68 @@ function withTimeout(promise, ms) {
|
|
|
946
970
|
}
|
|
947
971
|
var COPILOT_TIMEOUT_MS = 30000;
|
|
948
972
|
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
973
|
+
var BATCH_CONFIG = {
|
|
974
|
+
LARGE_CHANGESET_THRESHOLD: 15,
|
|
975
|
+
COMPACT_PER_FILE_CHARS: 300,
|
|
976
|
+
MAX_COMPACT_PAYLOAD: 1e4,
|
|
977
|
+
FALLBACK_BATCH_SIZE: 15
|
|
978
|
+
};
|
|
979
|
+
function parseDiffByFile(rawDiff) {
|
|
980
|
+
const sections = new Map;
|
|
981
|
+
const headerPattern = /^diff --git a\/(.+?) b\//gm;
|
|
982
|
+
const positions = [];
|
|
983
|
+
for (let match = headerPattern.exec(rawDiff);match !== null; match = headerPattern.exec(rawDiff)) {
|
|
984
|
+
positions.push({ file: match[1], start: match.index });
|
|
985
|
+
}
|
|
986
|
+
for (let i = 0;i < positions.length; i++) {
|
|
987
|
+
const { file, start } = positions[i];
|
|
988
|
+
const end = i + 1 < positions.length ? positions[i + 1].start : rawDiff.length;
|
|
989
|
+
sections.set(file, rawDiff.slice(start, end));
|
|
990
|
+
}
|
|
991
|
+
return sections;
|
|
992
|
+
}
|
|
993
|
+
function extractDiffStats(diffSection) {
|
|
994
|
+
let added = 0;
|
|
995
|
+
let removed = 0;
|
|
996
|
+
for (const line of diffSection.split(`
|
|
997
|
+
`)) {
|
|
998
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
999
|
+
added++;
|
|
1000
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
1001
|
+
removed++;
|
|
1002
|
+
}
|
|
1003
|
+
return { added, removed };
|
|
1004
|
+
}
|
|
1005
|
+
function createCompactDiff(files, rawDiff, maxTotalChars = BATCH_CONFIG.MAX_COMPACT_PAYLOAD) {
|
|
1006
|
+
if (files.length === 0)
|
|
1007
|
+
return "";
|
|
1008
|
+
const diffSections = parseDiffByFile(rawDiff);
|
|
1009
|
+
const perFileBudget = Math.min(BATCH_CONFIG.COMPACT_PER_FILE_CHARS, Math.floor(maxTotalChars / files.length));
|
|
1010
|
+
const parts = [];
|
|
1011
|
+
for (const file of files) {
|
|
1012
|
+
const section = diffSections.get(file);
|
|
1013
|
+
if (section) {
|
|
1014
|
+
const stats = extractDiffStats(section);
|
|
1015
|
+
const header = `[${file}] (+${stats.added}/-${stats.removed})`;
|
|
1016
|
+
if (section.length <= perFileBudget) {
|
|
1017
|
+
parts.push(`${header}
|
|
1018
|
+
${section}`);
|
|
1019
|
+
} else {
|
|
1020
|
+
const truncated = section.slice(0, perFileBudget - header.length - 20);
|
|
1021
|
+
parts.push(`${header}
|
|
1022
|
+
${truncated}
|
|
1023
|
+
...(truncated)`);
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
parts.push(`[${file}] (new/binary file — no diff available)`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const result = parts.join(`
|
|
1030
|
+
|
|
1031
|
+
`);
|
|
1032
|
+
return result.length > maxTotalChars ? `${result.slice(0, maxTotalChars - 15)}
|
|
1033
|
+
...(truncated)` : result;
|
|
1034
|
+
}
|
|
949
1035
|
async function checkCopilotAvailable() {
|
|
950
1036
|
try {
|
|
951
1037
|
const client = await getManagedClient();
|
|
@@ -1046,16 +1132,18 @@ function extractJson(raw) {
|
|
|
1046
1132
|
}
|
|
1047
1133
|
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
1048
1134
|
try {
|
|
1135
|
+
const isLarge = stagedFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1049
1136
|
const multiFileHint = stagedFiles.length > 1 ? `
|
|
1050
1137
|
|
|
1051
1138
|
IMPORTANT: Multiple files are staged. Generate ONE commit message that captures the high-level purpose of ALL changes together. Focus on the overall intent, not individual file changes. Be specific but concise — do not list every file.` : "";
|
|
1139
|
+
const diffContent = isLarge ? createCompactDiff(stagedFiles, diff) : diff.slice(0, 4000);
|
|
1052
1140
|
const userMessage = `Generate a commit message for these staged changes:
|
|
1053
1141
|
|
|
1054
|
-
Files: ${stagedFiles.join(", ")}
|
|
1142
|
+
Files (${stagedFiles.length}): ${stagedFiles.join(", ")}
|
|
1055
1143
|
|
|
1056
1144
|
Diff:
|
|
1057
|
-
${
|
|
1058
|
-
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
1145
|
+
${diffContent}${multiFileHint}`;
|
|
1146
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model, isLarge ? COPILOT_LONG_TIMEOUT_MS : COPILOT_TIMEOUT_MS);
|
|
1059
1147
|
return result?.trim() ?? null;
|
|
1060
1148
|
} catch {
|
|
1061
1149
|
return null;
|
|
@@ -1104,16 +1192,23 @@ ${conflictDiff.slice(0, 4000)}`;
|
|
|
1104
1192
|
}
|
|
1105
1193
|
}
|
|
1106
1194
|
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
1195
|
+
const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1196
|
+
const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 6000);
|
|
1197
|
+
const largeHint = isLarge ? `
|
|
1198
|
+
|
|
1199
|
+
NOTE: This is a large changeset (${files.length} files). Compact diffs are provided for every file. Focus on creating well-organized logical groups.` : "";
|
|
1107
1200
|
const userMessage = `Group these changed files into logical atomic commits:
|
|
1108
1201
|
|
|
1109
1202
|
Files:
|
|
1110
1203
|
${files.join(`
|
|
1111
1204
|
`)}
|
|
1112
1205
|
|
|
1113
|
-
Diffs
|
|
1114
|
-
${
|
|
1206
|
+
Diffs:
|
|
1207
|
+
${diffContent}${largeHint}`;
|
|
1115
1208
|
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1116
1209
|
if (!result) {
|
|
1210
|
+
if (isLarge)
|
|
1211
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1117
1212
|
throw new Error("AI returned an empty response");
|
|
1118
1213
|
}
|
|
1119
1214
|
const cleaned = extractJson(result);
|
|
@@ -1121,10 +1216,14 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1121
1216
|
try {
|
|
1122
1217
|
parsed = JSON.parse(cleaned);
|
|
1123
1218
|
} catch {
|
|
1219
|
+
if (isLarge)
|
|
1220
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1124
1221
|
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
1125
1222
|
}
|
|
1126
1223
|
const groups = parsed;
|
|
1127
1224
|
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1225
|
+
if (isLarge)
|
|
1226
|
+
return generateCommitGroupsInBatches(files, diffs, model, convention);
|
|
1128
1227
|
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
1129
1228
|
}
|
|
1130
1229
|
for (const group of groups) {
|
|
@@ -1134,7 +1233,51 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1134
1233
|
}
|
|
1135
1234
|
return groups;
|
|
1136
1235
|
}
|
|
1236
|
+
async function generateCommitGroupsInBatches(files, diffs, model, convention = "clean-commit") {
|
|
1237
|
+
const batchSize = BATCH_CONFIG.FALLBACK_BATCH_SIZE;
|
|
1238
|
+
const allGroups = [];
|
|
1239
|
+
const diffSections = parseDiffByFile(diffs);
|
|
1240
|
+
for (let i = 0;i < files.length; i += batchSize) {
|
|
1241
|
+
const batchFiles = files.slice(i, i + batchSize);
|
|
1242
|
+
const batchDiff = batchFiles.map((f) => diffSections.get(f) ?? "").filter(Boolean).join(`
|
|
1243
|
+
`);
|
|
1244
|
+
const batchDiffContent = batchFiles.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD ? createCompactDiff(batchFiles, batchDiff) : batchDiff.slice(0, 6000);
|
|
1245
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
1246
|
+
const totalBatches = Math.ceil(files.length / batchSize);
|
|
1247
|
+
const userMessage = `Group these changed files into logical atomic commits:
|
|
1248
|
+
|
|
1249
|
+
Files:
|
|
1250
|
+
${batchFiles.join(`
|
|
1251
|
+
`)}
|
|
1252
|
+
|
|
1253
|
+
Diffs:
|
|
1254
|
+
${batchDiffContent}
|
|
1255
|
+
|
|
1256
|
+
NOTE: Processing batch ${batchNum}/${totalBatches} of a large changeset. Group only the files listed above.`;
|
|
1257
|
+
try {
|
|
1258
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1259
|
+
if (!result)
|
|
1260
|
+
continue;
|
|
1261
|
+
const cleaned = extractJson(result);
|
|
1262
|
+
const parsed = JSON.parse(cleaned);
|
|
1263
|
+
if (Array.isArray(parsed)) {
|
|
1264
|
+
for (const group of parsed) {
|
|
1265
|
+
if (Array.isArray(group.files) && typeof group.message === "string") {
|
|
1266
|
+
allGroups.push(group);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
} catch {}
|
|
1271
|
+
}
|
|
1272
|
+
if (allGroups.length === 0) {
|
|
1273
|
+
throw new Error("AI could not group any files even with batch processing");
|
|
1274
|
+
}
|
|
1275
|
+
return allGroups;
|
|
1276
|
+
}
|
|
1137
1277
|
async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
|
|
1278
|
+
const totalFiles = groups.reduce((sum, g) => sum + g.files.length, 0);
|
|
1279
|
+
const isLarge = totalFiles >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1280
|
+
const diffContent = isLarge ? createCompactDiff(groups.flatMap((g) => g.files), diffs) : diffs.slice(0, 6000);
|
|
1138
1281
|
const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
|
|
1139
1282
|
`);
|
|
1140
1283
|
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
@@ -1142,8 +1285,8 @@ async function regenerateAllGroupMessages(groups, diffs, model, convention = "cl
|
|
|
1142
1285
|
Groups:
|
|
1143
1286
|
${groupSummary}
|
|
1144
1287
|
|
|
1145
|
-
Diffs
|
|
1146
|
-
${
|
|
1288
|
+
Diffs:
|
|
1289
|
+
${diffContent}`;
|
|
1147
1290
|
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1148
1291
|
if (!result)
|
|
1149
1292
|
return groups;
|
|
@@ -1162,12 +1305,14 @@ ${diffs.slice(0, 6000)}`;
|
|
|
1162
1305
|
}
|
|
1163
1306
|
async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
|
|
1164
1307
|
try {
|
|
1308
|
+
const isLarge = files.length >= BATCH_CONFIG.LARGE_CHANGESET_THRESHOLD;
|
|
1309
|
+
const diffContent = isLarge ? createCompactDiff(files, diffs) : diffs.slice(0, 4000);
|
|
1165
1310
|
const userMessage = `Generate a single commit message for these files:
|
|
1166
1311
|
|
|
1167
1312
|
Files: ${files.join(", ")}
|
|
1168
1313
|
|
|
1169
1314
|
Diff:
|
|
1170
|
-
${
|
|
1315
|
+
${diffContent}`;
|
|
1171
1316
|
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
1172
1317
|
return result?.trim() ?? null;
|
|
1173
1318
|
} catch {
|
|
@@ -1725,7 +1870,8 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1725
1870
|
warn(`AI unavailable: ${copilotError}`);
|
|
1726
1871
|
warn("Falling back to manual commit message entry.");
|
|
1727
1872
|
} else {
|
|
1728
|
-
const
|
|
1873
|
+
const spinnerMsg = stagedFiles.length >= 15 ? `Generating commit message with AI (${stagedFiles.length} files — using optimized batching)...` : "Generating commit message with AI...";
|
|
1874
|
+
const spinner = createSpinner(spinnerMsg);
|
|
1729
1875
|
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
1730
1876
|
if (commitMessage) {
|
|
1731
1877
|
spinner.success("AI commit message generated.");
|
|
@@ -1816,7 +1962,7 @@ ${pc6.bold("Changed files:")}`);
|
|
|
1816
1962
|
for (const f of changedFiles) {
|
|
1817
1963
|
console.log(` ${pc6.dim("•")} ${f}`);
|
|
1818
1964
|
}
|
|
1819
|
-
const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
1965
|
+
const spinner = createSpinner(changedFiles.length >= 15 ? `Asking AI to group ${changedFiles.length} file(s) into logical commits (using optimized batching)...` : `Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
1820
1966
|
const diffs = await getFullDiffForFiles(changedFiles);
|
|
1821
1967
|
if (!diffs.trim()) {
|
|
1822
1968
|
spinner.stop();
|
|
@@ -1994,7 +2140,7 @@ import pc7 from "picocolors";
|
|
|
1994
2140
|
// package.json
|
|
1995
2141
|
var package_default = {
|
|
1996
2142
|
name: "contribute-now",
|
|
1997
|
-
version: "0.
|
|
2143
|
+
version: "0.4.0-dev.6151897",
|
|
1998
2144
|
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1999
2145
|
type: "module",
|
|
2000
2146
|
bin: {
|
|
@@ -2653,6 +2799,40 @@ function colorizeSubject(subject) {
|
|
|
2653
2799
|
// src/commands/setup.ts
|
|
2654
2800
|
import { defineCommand as defineCommand7 } from "citty";
|
|
2655
2801
|
import pc10 from "picocolors";
|
|
2802
|
+
async function shouldContinueSetupWithExistingConfig(options) {
|
|
2803
|
+
const {
|
|
2804
|
+
existingConfig,
|
|
2805
|
+
hasConfigFile,
|
|
2806
|
+
confirm: confirm2,
|
|
2807
|
+
ensureIgnored,
|
|
2808
|
+
onInfo,
|
|
2809
|
+
onWarn,
|
|
2810
|
+
onSuccess,
|
|
2811
|
+
summary
|
|
2812
|
+
} = options;
|
|
2813
|
+
if (existingConfig) {
|
|
2814
|
+
onInfo("Existing .contributerc.json detected:");
|
|
2815
|
+
summary(existingConfig);
|
|
2816
|
+
const shouldContinue = await confirm2("Continue setup and overwrite existing config?");
|
|
2817
|
+
if (!shouldContinue) {
|
|
2818
|
+
if (ensureIgnored()) {
|
|
2819
|
+
onInfo("Added .contributerc.json to .gitignore to avoid committing personal config.");
|
|
2820
|
+
}
|
|
2821
|
+
onSuccess("Keeping existing setup.");
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
return true;
|
|
2825
|
+
}
|
|
2826
|
+
if (hasConfigFile) {
|
|
2827
|
+
onWarn("Found .contributerc.json but it appears invalid.");
|
|
2828
|
+
const shouldContinue = await confirm2("Continue setup and overwrite invalid config?");
|
|
2829
|
+
if (!shouldContinue) {
|
|
2830
|
+
onInfo("Keeping existing file. Run setup again when ready to repair it.");
|
|
2831
|
+
return false;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
return true;
|
|
2835
|
+
}
|
|
2656
2836
|
var setup_default = defineCommand7({
|
|
2657
2837
|
meta: {
|
|
2658
2838
|
name: "setup",
|
|
@@ -2664,6 +2844,20 @@ var setup_default = defineCommand7({
|
|
|
2664
2844
|
process.exit(1);
|
|
2665
2845
|
}
|
|
2666
2846
|
heading("\uD83D\uDD27 contribute-now setup");
|
|
2847
|
+
const existingConfig = readConfig();
|
|
2848
|
+
const shouldContinue = await shouldContinueSetupWithExistingConfig({
|
|
2849
|
+
existingConfig,
|
|
2850
|
+
hasConfigFile: configExists(),
|
|
2851
|
+
confirm: confirmPrompt,
|
|
2852
|
+
ensureIgnored: ensureGitignored,
|
|
2853
|
+
onInfo: info,
|
|
2854
|
+
onWarn: warn,
|
|
2855
|
+
onSuccess: success,
|
|
2856
|
+
summary: logConfigSummary
|
|
2857
|
+
});
|
|
2858
|
+
if (!shouldContinue) {
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2667
2861
|
const workflowChoice = await selectPrompt("Which git workflow does this project use?", [
|
|
2668
2862
|
"Clean Flow — main + dev, squash features into dev, merge dev into main (recommended)",
|
|
2669
2863
|
"GitHub Flow — main + feature branches, squash/merge into main",
|
|
@@ -2693,31 +2887,42 @@ var setup_default = defineCommand7({
|
|
|
2693
2887
|
info(`Found remotes: ${remotes.join(", ")}`);
|
|
2694
2888
|
let detectedRole = null;
|
|
2695
2889
|
let detectionSource = "";
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2890
|
+
const roleSpinner = createSpinner("Detecting your role...");
|
|
2891
|
+
try {
|
|
2892
|
+
roleSpinner.update("Checking GitHub CLI and auth...");
|
|
2893
|
+
const ghInstalled = await checkGhInstalled();
|
|
2894
|
+
if (ghInstalled && await checkGhAuth()) {
|
|
2895
|
+
roleSpinner.update("Inspecting repository relationship (fork/permissions)...");
|
|
2896
|
+
const isFork = await isRepoFork();
|
|
2897
|
+
if (isFork === true) {
|
|
2898
|
+
detectedRole = "contributor";
|
|
2899
|
+
detectionSource = "gh CLI (fork detected)";
|
|
2900
|
+
} else if (isFork === false) {
|
|
2901
|
+
const repoInfo = await getCurrentRepoInfo();
|
|
2902
|
+
if (repoInfo) {
|
|
2903
|
+
const perms = await checkRepoPermissions(repoInfo.owner, repoInfo.repo);
|
|
2904
|
+
if (perms?.admin || perms?.push) {
|
|
2905
|
+
detectedRole = "maintainer";
|
|
2906
|
+
detectionSource = "gh CLI (admin/push permissions)";
|
|
2907
|
+
}
|
|
2709
2908
|
}
|
|
2710
2909
|
}
|
|
2711
2910
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2911
|
+
if (detectedRole === null) {
|
|
2912
|
+
roleSpinner.update("Analyzing git remotes...");
|
|
2913
|
+
if (remotes.includes("upstream")) {
|
|
2914
|
+
detectedRole = "contributor";
|
|
2915
|
+
detectionSource = "heuristic (upstream remote exists)";
|
|
2916
|
+
} else if (remotes.includes("origin") && remotes.length === 1) {
|
|
2917
|
+
detectedRole = "maintainer";
|
|
2918
|
+
detectionSource = "heuristic (only origin remote)";
|
|
2919
|
+
}
|
|
2720
2920
|
}
|
|
2921
|
+
roleSpinner.success("Role detection complete.");
|
|
2922
|
+
} catch {
|
|
2923
|
+
roleSpinner.fail("Role detection failed; falling back to manual selection.");
|
|
2924
|
+
detectedRole = null;
|
|
2925
|
+
detectionSource = "";
|
|
2721
2926
|
}
|
|
2722
2927
|
if (detectedRole === null) {
|
|
2723
2928
|
const roleChoice = await selectPrompt("What is your role in this project?", [
|
|
@@ -2735,16 +2940,20 @@ var setup_default = defineCommand7({
|
|
|
2735
2940
|
}
|
|
2736
2941
|
}
|
|
2737
2942
|
const defaultConfig = getDefaultConfig();
|
|
2738
|
-
|
|
2943
|
+
info(pc10.dim("Tip: press Enter to keep the default branch name shown in each prompt."));
|
|
2944
|
+
const mainBranchDefault = defaultConfig.mainBranch;
|
|
2945
|
+
const mainBranch = await inputPrompt(`Main branch name (default: ${mainBranchDefault} — press Enter to keep)`, mainBranchDefault);
|
|
2739
2946
|
let devBranch;
|
|
2740
2947
|
if (hasDevBranch(workflow)) {
|
|
2741
2948
|
const defaultDev = workflow === "git-flow" ? "develop" : "dev";
|
|
2742
|
-
devBranch = await inputPrompt(
|
|
2949
|
+
devBranch = await inputPrompt(`Dev/develop branch name (default: ${defaultDev} — press Enter to keep)`, defaultDev);
|
|
2743
2950
|
}
|
|
2744
|
-
const
|
|
2951
|
+
const originRemoteDefault = defaultConfig.origin;
|
|
2952
|
+
const originRemote = await inputPrompt(`Origin remote name (default: ${originRemoteDefault} — press Enter to keep)`, originRemoteDefault);
|
|
2745
2953
|
let upstreamRemote = defaultConfig.upstream;
|
|
2746
2954
|
if (detectedRole === "contributor") {
|
|
2747
|
-
|
|
2955
|
+
const upstreamRemoteDefault = defaultConfig.upstream;
|
|
2956
|
+
upstreamRemote = await inputPrompt(`Upstream remote name (default: ${upstreamRemoteDefault} — press Enter to keep)`, upstreamRemoteDefault);
|
|
2748
2957
|
if (!remotes.includes(upstreamRemote)) {
|
|
2749
2958
|
warn(`Remote "${upstreamRemote}" not found.`);
|
|
2750
2959
|
const originUrl = await getRemoteUrl(originRemote);
|
|
@@ -2792,9 +3001,8 @@ var setup_default = defineCommand7({
|
|
|
2792
3001
|
warn("Config was saved — verify the branch name and re-run setup if needed.");
|
|
2793
3002
|
}
|
|
2794
3003
|
}
|
|
2795
|
-
if (
|
|
2796
|
-
|
|
2797
|
-
warn(' echo ".contributerc.json" >> .gitignore');
|
|
3004
|
+
if (ensureGitignored()) {
|
|
3005
|
+
info("Added .contributerc.json to .gitignore to avoid committing personal config.");
|
|
2798
3006
|
}
|
|
2799
3007
|
console.log();
|
|
2800
3008
|
info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
@@ -2808,6 +3016,17 @@ var setup_default = defineCommand7({
|
|
|
2808
3016
|
info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
|
|
2809
3017
|
}
|
|
2810
3018
|
});
|
|
3019
|
+
function logConfigSummary(config) {
|
|
3020
|
+
info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
3021
|
+
info(`Convention: ${pc10.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
|
|
3022
|
+
info(`Role: ${pc10.bold(config.role)}`);
|
|
3023
|
+
if (config.devBranch) {
|
|
3024
|
+
info(`Main: ${pc10.bold(config.mainBranch)} | Dev: ${pc10.bold(config.devBranch)}`);
|
|
3025
|
+
} else {
|
|
3026
|
+
info(`Main: ${pc10.bold(config.mainBranch)}`);
|
|
3027
|
+
}
|
|
3028
|
+
info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
|
|
3029
|
+
}
|
|
2811
3030
|
|
|
2812
3031
|
// src/commands/start.ts
|
|
2813
3032
|
import { defineCommand as defineCommand8 } from "citty";
|
|
@@ -2897,6 +3116,19 @@ var start_default = defineCommand8({
|
|
|
2897
3116
|
if (!await refExists(syncSource.ref)) {
|
|
2898
3117
|
warn(`Remote ref ${pc11.bold(syncSource.ref)} not found. Creating branch from local ${pc11.bold(baseBranch)}.`);
|
|
2899
3118
|
}
|
|
3119
|
+
const currentBranch = await getCurrentBranch();
|
|
3120
|
+
if (currentBranch === baseBranch && await refExists(syncSource.ref)) {
|
|
3121
|
+
const ahead = await countCommitsAhead(baseBranch, syncSource.ref);
|
|
3122
|
+
if (ahead > 0) {
|
|
3123
|
+
warn(`You are on ${pc11.bold(baseBranch)} with ${pc11.bold(String(ahead))} local commit${ahead > 1 ? "s" : ""} not in ${pc11.bold(syncSource.ref)}.`);
|
|
3124
|
+
info(" Syncing will discard those commits. Consider backing them up first (e.g. create a branch).");
|
|
3125
|
+
const proceed = await confirmPrompt("Discard local commits and sync to remote?");
|
|
3126
|
+
if (!proceed) {
|
|
3127
|
+
info("Aborted. Your local commits are untouched.");
|
|
3128
|
+
process.exit(0);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
2900
3132
|
const updateResult = await updateLocalBranch(baseBranch, syncSource.ref);
|
|
2901
3133
|
if (updateResult.exitCode !== 0) {
|
|
2902
3134
|
if (await refExists(syncSource.ref)) {
|