contribute-now 0.1.0-staging.7136fcc
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 +284 -0
- package/dist/index.js +1454 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { defineCommand as defineCommand9, runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/commands/clean.ts
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import pc3 from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/utils/config.ts
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
var CONFIG_FILENAME = ".contributerc.json";
|
|
14
|
+
function getConfigPath(cwd = process.cwd()) {
|
|
15
|
+
return join(cwd, CONFIG_FILENAME);
|
|
16
|
+
}
|
|
17
|
+
function readConfig(cwd = process.cwd()) {
|
|
18
|
+
const path = getConfigPath(cwd);
|
|
19
|
+
if (!existsSync(path))
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(path, "utf-8");
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeConfig(config, cwd = process.cwd()) {
|
|
29
|
+
const path = getConfigPath(cwd);
|
|
30
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
31
|
+
`, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
function isGitignored(cwd = process.cwd()) {
|
|
34
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
35
|
+
if (!existsSync(gitignorePath))
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
39
|
+
return content.split(`
|
|
40
|
+
`).some((line) => line.trim() === CONFIG_FILENAME);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getDefaultConfig() {
|
|
46
|
+
return {
|
|
47
|
+
role: "contributor",
|
|
48
|
+
mainBranch: "main",
|
|
49
|
+
devBranch: "dev",
|
|
50
|
+
upstream: "upstream",
|
|
51
|
+
origin: "origin",
|
|
52
|
+
branchPrefixes: ["feature", "fix", "docs", "chore", "test", "refactor"]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/utils/confirm.ts
|
|
57
|
+
import pc from "picocolors";
|
|
58
|
+
async function confirmPrompt(message) {
|
|
59
|
+
console.log(`
|
|
60
|
+
${message}`);
|
|
61
|
+
process.stdout.write(`${pc.dim("Continue? [y/N] ")}`);
|
|
62
|
+
const response = await new Promise((resolve) => {
|
|
63
|
+
process.stdin.setEncoding("utf-8");
|
|
64
|
+
process.stdin.once("data", (data) => {
|
|
65
|
+
process.stdin.pause();
|
|
66
|
+
resolve(data.toString().trim());
|
|
67
|
+
});
|
|
68
|
+
process.stdin.resume();
|
|
69
|
+
});
|
|
70
|
+
if (response.toLowerCase() !== "y") {
|
|
71
|
+
console.log(pc.yellow("Aborted."));
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
async function selectPrompt(message, choices) {
|
|
77
|
+
console.log(`
|
|
78
|
+
${message}`);
|
|
79
|
+
choices.forEach((choice, i) => {
|
|
80
|
+
console.log(` ${pc.dim(`${i + 1}.`)} ${choice}`);
|
|
81
|
+
});
|
|
82
|
+
process.stdout.write(pc.dim(`Enter number [1-${choices.length}]: `));
|
|
83
|
+
const response = await new Promise((resolve) => {
|
|
84
|
+
process.stdin.setEncoding("utf-8");
|
|
85
|
+
process.stdin.once("data", (data) => {
|
|
86
|
+
process.stdin.pause();
|
|
87
|
+
resolve(data.toString().trim());
|
|
88
|
+
});
|
|
89
|
+
process.stdin.resume();
|
|
90
|
+
});
|
|
91
|
+
const index = Number.parseInt(response, 10) - 1;
|
|
92
|
+
if (index >= 0 && index < choices.length) {
|
|
93
|
+
return choices[index];
|
|
94
|
+
}
|
|
95
|
+
return choices[0];
|
|
96
|
+
}
|
|
97
|
+
async function inputPrompt(message, defaultValue) {
|
|
98
|
+
const hint = defaultValue ? ` ${pc.dim(`[${defaultValue}]`)}` : "";
|
|
99
|
+
process.stdout.write(`
|
|
100
|
+
${message}${hint}: `);
|
|
101
|
+
const response = await new Promise((resolve) => {
|
|
102
|
+
process.stdin.setEncoding("utf-8");
|
|
103
|
+
process.stdin.once("data", (data) => {
|
|
104
|
+
process.stdin.pause();
|
|
105
|
+
resolve(data.toString().trim());
|
|
106
|
+
});
|
|
107
|
+
process.stdin.resume();
|
|
108
|
+
});
|
|
109
|
+
return response || defaultValue || "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/utils/git.ts
|
|
113
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
114
|
+
function run(args) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
execFileCb("git", args, (error, stdout, stderr) => {
|
|
117
|
+
resolve({
|
|
118
|
+
exitCode: error ? error.code === "ENOENT" ? 127 : error.code != null ? Number(error.code) : 1 : 0,
|
|
119
|
+
stdout: stdout ?? "",
|
|
120
|
+
stderr: stderr ?? ""
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function isGitRepo() {
|
|
126
|
+
const { exitCode } = await run(["rev-parse", "--is-inside-work-tree"]);
|
|
127
|
+
return exitCode === 0;
|
|
128
|
+
}
|
|
129
|
+
async function getCurrentBranch() {
|
|
130
|
+
const { exitCode, stdout } = await run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
131
|
+
if (exitCode !== 0)
|
|
132
|
+
return null;
|
|
133
|
+
return stdout.trim() || null;
|
|
134
|
+
}
|
|
135
|
+
async function getRemotes() {
|
|
136
|
+
const { exitCode, stdout } = await run(["remote"]);
|
|
137
|
+
if (exitCode !== 0)
|
|
138
|
+
return [];
|
|
139
|
+
return stdout.trim().split(`
|
|
140
|
+
`).map((r) => r.trim()).filter(Boolean);
|
|
141
|
+
}
|
|
142
|
+
async function getRemoteUrl(remote) {
|
|
143
|
+
const { exitCode, stdout } = await run(["remote", "get-url", remote]);
|
|
144
|
+
if (exitCode !== 0)
|
|
145
|
+
return null;
|
|
146
|
+
return stdout.trim() || null;
|
|
147
|
+
}
|
|
148
|
+
async function hasUncommittedChanges() {
|
|
149
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
150
|
+
if (exitCode !== 0)
|
|
151
|
+
return false;
|
|
152
|
+
return stdout.trim().length > 0;
|
|
153
|
+
}
|
|
154
|
+
async function fetchRemote(remote) {
|
|
155
|
+
return run(["fetch", remote]);
|
|
156
|
+
}
|
|
157
|
+
async function fetchAll() {
|
|
158
|
+
return run(["fetch", "--all", "--quiet"]);
|
|
159
|
+
}
|
|
160
|
+
async function checkoutBranch(branch) {
|
|
161
|
+
return run(["checkout", branch]);
|
|
162
|
+
}
|
|
163
|
+
async function createBranch(branch, from) {
|
|
164
|
+
const args = from ? ["checkout", "-b", branch, from] : ["checkout", "-b", branch];
|
|
165
|
+
return run(args);
|
|
166
|
+
}
|
|
167
|
+
async function resetHard(ref) {
|
|
168
|
+
return run(["reset", "--hard", ref]);
|
|
169
|
+
}
|
|
170
|
+
async function pushForceWithLease(remote, branch) {
|
|
171
|
+
return run(["push", "--force-with-lease", remote, branch]);
|
|
172
|
+
}
|
|
173
|
+
async function pushSetUpstream(remote, branch) {
|
|
174
|
+
return run(["push", "-u", remote, branch]);
|
|
175
|
+
}
|
|
176
|
+
async function rebase(branch) {
|
|
177
|
+
return run(["rebase", branch]);
|
|
178
|
+
}
|
|
179
|
+
async function getStagedDiff() {
|
|
180
|
+
const { stdout } = await run(["diff", "--cached"]);
|
|
181
|
+
return stdout;
|
|
182
|
+
}
|
|
183
|
+
async function getStagedFiles() {
|
|
184
|
+
const { exitCode, stdout } = await run(["diff", "--cached", "--name-only"]);
|
|
185
|
+
if (exitCode !== 0)
|
|
186
|
+
return [];
|
|
187
|
+
return stdout.trim().split(`
|
|
188
|
+
`).filter(Boolean);
|
|
189
|
+
}
|
|
190
|
+
async function getChangedFiles() {
|
|
191
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
192
|
+
if (exitCode !== 0)
|
|
193
|
+
return [];
|
|
194
|
+
return stdout.trim().split(`
|
|
195
|
+
`).filter(Boolean).map((l) => l.slice(3));
|
|
196
|
+
}
|
|
197
|
+
async function getDivergence(branch, base) {
|
|
198
|
+
const { exitCode, stdout } = await run([
|
|
199
|
+
"rev-list",
|
|
200
|
+
"--left-right",
|
|
201
|
+
"--count",
|
|
202
|
+
`${base}...${branch}`
|
|
203
|
+
]);
|
|
204
|
+
if (exitCode !== 0)
|
|
205
|
+
return { ahead: 0, behind: 0 };
|
|
206
|
+
const parts = stdout.trim().split(/\s+/);
|
|
207
|
+
return {
|
|
208
|
+
behind: Number.parseInt(parts[0] ?? "0", 10),
|
|
209
|
+
ahead: Number.parseInt(parts[1] ?? "0", 10)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function getMergedBranches(base) {
|
|
213
|
+
const { exitCode, stdout } = await run(["branch", "--merged", base]);
|
|
214
|
+
if (exitCode !== 0)
|
|
215
|
+
return [];
|
|
216
|
+
return stdout.trim().split(`
|
|
217
|
+
`).map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean);
|
|
218
|
+
}
|
|
219
|
+
async function deleteBranch(branch) {
|
|
220
|
+
return run(["branch", "-d", branch]);
|
|
221
|
+
}
|
|
222
|
+
async function pruneRemote(remote) {
|
|
223
|
+
return run(["remote", "prune", remote]);
|
|
224
|
+
}
|
|
225
|
+
async function commitWithMessage(message) {
|
|
226
|
+
return run(["commit", "-m", message]);
|
|
227
|
+
}
|
|
228
|
+
async function getLogDiff(base, head) {
|
|
229
|
+
const { stdout } = await run(["diff", `${base}...${head}`]);
|
|
230
|
+
return stdout;
|
|
231
|
+
}
|
|
232
|
+
async function getLog(base, head) {
|
|
233
|
+
const { exitCode, stdout } = await run(["log", `${base}..${head}`, "--oneline"]);
|
|
234
|
+
if (exitCode !== 0)
|
|
235
|
+
return [];
|
|
236
|
+
return stdout.trim().split(`
|
|
237
|
+
`).filter(Boolean);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/utils/logger.ts
|
|
241
|
+
import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
|
|
242
|
+
import pc2 from "picocolors";
|
|
243
|
+
LogEngine.configure({
|
|
244
|
+
mode: LogMode.INFO,
|
|
245
|
+
format: {
|
|
246
|
+
includeIsoTimestamp: false,
|
|
247
|
+
includeLocalTime: true,
|
|
248
|
+
includeEmoji: true
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
function success(msg) {
|
|
252
|
+
LogEngine.log(msg);
|
|
253
|
+
}
|
|
254
|
+
function error(msg) {
|
|
255
|
+
LogEngine.error(msg);
|
|
256
|
+
}
|
|
257
|
+
function warn(msg) {
|
|
258
|
+
LogEngine.warn(msg);
|
|
259
|
+
}
|
|
260
|
+
function info(msg) {
|
|
261
|
+
LogEngine.info(msg);
|
|
262
|
+
}
|
|
263
|
+
function heading(msg) {
|
|
264
|
+
console.log(`
|
|
265
|
+
${pc2.bold(msg)}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/commands/clean.ts
|
|
269
|
+
var clean_default = defineCommand({
|
|
270
|
+
meta: {
|
|
271
|
+
name: "clean",
|
|
272
|
+
description: "Delete merged branches and prune remote refs"
|
|
273
|
+
},
|
|
274
|
+
args: {
|
|
275
|
+
yes: {
|
|
276
|
+
type: "boolean",
|
|
277
|
+
alias: "y",
|
|
278
|
+
description: "Skip confirmation prompt",
|
|
279
|
+
default: false
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
async run({ args }) {
|
|
283
|
+
if (!await isGitRepo()) {
|
|
284
|
+
error("Not inside a git repository.");
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
const config = readConfig();
|
|
288
|
+
if (!config) {
|
|
289
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const { mainBranch, devBranch, origin } = config;
|
|
293
|
+
const currentBranch = await getCurrentBranch();
|
|
294
|
+
heading("\uD83E\uDDF9 contrib clean");
|
|
295
|
+
const mergedBranches = await getMergedBranches(devBranch);
|
|
296
|
+
const protected_ = new Set([mainBranch, devBranch, currentBranch ?? ""]);
|
|
297
|
+
const candidates = mergedBranches.filter((b) => !protected_.has(b));
|
|
298
|
+
if (candidates.length === 0) {
|
|
299
|
+
info("No merged branches to clean up.");
|
|
300
|
+
} else {
|
|
301
|
+
console.log(`
|
|
302
|
+
${pc3.bold("Branches to delete:")}`);
|
|
303
|
+
for (const b of candidates) {
|
|
304
|
+
console.log(` ${pc3.dim("•")} ${b}`);
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
const ok = args.yes || await confirmPrompt(`Delete ${pc3.bold(String(candidates.length))} merged branch${candidates.length !== 1 ? "es" : ""}?`);
|
|
308
|
+
if (!ok) {
|
|
309
|
+
info("Skipped branch deletion.");
|
|
310
|
+
} else {
|
|
311
|
+
for (const branch of candidates) {
|
|
312
|
+
const result = await deleteBranch(branch);
|
|
313
|
+
if (result.exitCode === 0) {
|
|
314
|
+
success(` Deleted ${pc3.bold(branch)}`);
|
|
315
|
+
} else {
|
|
316
|
+
warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
info(`Pruning ${origin} remote refs...`);
|
|
322
|
+
const pruneResult = await pruneRemote(origin);
|
|
323
|
+
if (pruneResult.exitCode === 0) {
|
|
324
|
+
success(`✅ Pruned ${origin} remote refs.`);
|
|
325
|
+
} else {
|
|
326
|
+
warn(`Could not prune remote: ${pruneResult.stderr.trim()}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// src/commands/commit.ts
|
|
332
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
333
|
+
import pc4 from "picocolors";
|
|
334
|
+
|
|
335
|
+
// src/utils/copilot.ts
|
|
336
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
337
|
+
var CLEAN_COMMIT_SYSTEM_PROMPT = `You are a git commit message generator. Generate a Clean Commit message following this exact format:
|
|
338
|
+
<emoji> <type>[!][(<scope>)]: <description>
|
|
339
|
+
|
|
340
|
+
Emoji and type table:
|
|
341
|
+
\uD83D\uDCE6 new – new features, files, or capabilities
|
|
342
|
+
\uD83D\uDD27 update – changes, refactoring, improvements
|
|
343
|
+
\uD83D\uDDD1️ remove – removing code, files, or dependencies
|
|
344
|
+
\uD83D\uDD12 security – security fixes or patches
|
|
345
|
+
⚙️ setup – configs, CI/CD, tooling, build systems
|
|
346
|
+
☕ chore – maintenance, dependency updates
|
|
347
|
+
\uD83E\uDDEA test – adding or updating tests
|
|
348
|
+
\uD83D\uDCD6 docs – documentation changes
|
|
349
|
+
\uD83D\uDE80 release – version releases
|
|
350
|
+
|
|
351
|
+
Rules:
|
|
352
|
+
- Breaking change (!) only for: new, update, remove, security
|
|
353
|
+
- Description: concise, imperative mood, max 72 chars
|
|
354
|
+
- Scope: optional, camelCase or kebab-case component name
|
|
355
|
+
- Return ONLY the commit message line, nothing else
|
|
356
|
+
|
|
357
|
+
Examples:
|
|
358
|
+
\uD83D\uDCE6 new: user authentication system
|
|
359
|
+
\uD83D\uDD27 update (api): improve error handling
|
|
360
|
+
⚙️ setup (ci): configure github actions workflow
|
|
361
|
+
\uD83D\uDCE6 new!: completely redesign authentication system`;
|
|
362
|
+
var BRANCH_NAME_SYSTEM_PROMPT = `You are a git branch name generator. Convert natural language descriptions into proper git branch names.
|
|
363
|
+
|
|
364
|
+
Format: <prefix>/<kebab-case-name>
|
|
365
|
+
Prefixes: feature, fix, docs, chore, test, refactor
|
|
366
|
+
|
|
367
|
+
Rules:
|
|
368
|
+
- Use lowercase kebab-case for the name part
|
|
369
|
+
- Keep it short and descriptive (2-5 words max)
|
|
370
|
+
- Return ONLY the branch name, nothing else
|
|
371
|
+
|
|
372
|
+
Examples:
|
|
373
|
+
Input: "fix the login timeout bug" → fix/login-timeout
|
|
374
|
+
Input: "add user profile page" → feature/user-profile-page
|
|
375
|
+
Input: "update readme documentation" → docs/update-readme`;
|
|
376
|
+
var PR_DESCRIPTION_SYSTEM_PROMPT = `You are a GitHub pull request description generator. Create a clear, structured PR description.
|
|
377
|
+
|
|
378
|
+
Return a JSON object with this exact structure:
|
|
379
|
+
{
|
|
380
|
+
"title": "Brief PR title (50 chars max)",
|
|
381
|
+
"body": "## Summary\\n...\\n\\n## Changes\\n...\\n\\n## Test Plan\\n..."
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
Rules:
|
|
385
|
+
- title: concise, present tense, describes what the PR does
|
|
386
|
+
- body: markdown with Summary, Changes (bullet list), and Test Plan sections
|
|
387
|
+
- Return ONLY the JSON object, no markdown fences, no extra text`;
|
|
388
|
+
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `You are a git merge conflict resolution advisor. Analyze the conflict markers and provide guidance.
|
|
389
|
+
|
|
390
|
+
Rules:
|
|
391
|
+
- Explain what each side of the conflict contains
|
|
392
|
+
- Suggest the most likely correct resolution strategy
|
|
393
|
+
- Never auto-resolve — provide guidance only
|
|
394
|
+
- Be concise and actionable`;
|
|
395
|
+
async function checkCopilotAvailable() {
|
|
396
|
+
let client = null;
|
|
397
|
+
try {
|
|
398
|
+
client = new CopilotClient;
|
|
399
|
+
await client.start();
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
402
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
403
|
+
return "Copilot CLI binary not found. Ensure GitHub Copilot is installed and your gh CLI is up to date.";
|
|
404
|
+
}
|
|
405
|
+
return `Failed to start Copilot service: ${msg}`;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
await client.ping();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
411
|
+
if (msg.includes("auth") || msg.includes("token") || msg.includes("401") || msg.includes("403")) {
|
|
412
|
+
return "Copilot authentication failed. Run `gh auth login` to refresh your token.";
|
|
413
|
+
}
|
|
414
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("timeout") || msg.includes("network")) {
|
|
415
|
+
return "Could not reach GitHub Copilot service. Check your internet connection.";
|
|
416
|
+
}
|
|
417
|
+
return `Copilot health check failed: ${msg}`;
|
|
418
|
+
} finally {
|
|
419
|
+
try {
|
|
420
|
+
await client.stop();
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
async function callCopilot(systemMessage, userMessage, model) {
|
|
426
|
+
const client = new CopilotClient;
|
|
427
|
+
await client.start();
|
|
428
|
+
try {
|
|
429
|
+
const sessionConfig = {
|
|
430
|
+
systemMessage: { content: systemMessage }
|
|
431
|
+
};
|
|
432
|
+
if (model)
|
|
433
|
+
sessionConfig.model = model;
|
|
434
|
+
const session = await client.createSession(sessionConfig);
|
|
435
|
+
try {
|
|
436
|
+
const response = await session.sendAndWait({ content: userMessage });
|
|
437
|
+
if (!response?.data?.content)
|
|
438
|
+
return null;
|
|
439
|
+
return response.data.content;
|
|
440
|
+
} finally {
|
|
441
|
+
await session.destroy();
|
|
442
|
+
}
|
|
443
|
+
} finally {
|
|
444
|
+
await client.stop();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function generateCommitMessage(diff, stagedFiles, model) {
|
|
448
|
+
try {
|
|
449
|
+
const userMessage = `Generate a commit message for these staged changes:
|
|
450
|
+
|
|
451
|
+
Files: ${stagedFiles.join(", ")}
|
|
452
|
+
|
|
453
|
+
Diff:
|
|
454
|
+
${diff.slice(0, 4000)}`;
|
|
455
|
+
const result = await callCopilot(CLEAN_COMMIT_SYSTEM_PROMPT, userMessage, model);
|
|
456
|
+
return result?.trim() ?? null;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function generatePRDescription(commits, diff, model) {
|
|
462
|
+
try {
|
|
463
|
+
const userMessage = `Generate a PR description for these changes:
|
|
464
|
+
|
|
465
|
+
Commits:
|
|
466
|
+
${commits.join(`
|
|
467
|
+
`)}
|
|
468
|
+
|
|
469
|
+
Diff (truncated):
|
|
470
|
+
${diff.slice(0, 4000)}`;
|
|
471
|
+
const result = await callCopilot(PR_DESCRIPTION_SYSTEM_PROMPT, userMessage, model);
|
|
472
|
+
if (!result)
|
|
473
|
+
return null;
|
|
474
|
+
const cleaned = result.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
475
|
+
return JSON.parse(cleaned);
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function suggestBranchName(description, model) {
|
|
481
|
+
try {
|
|
482
|
+
const result = await callCopilot(BRANCH_NAME_SYSTEM_PROMPT, description, model);
|
|
483
|
+
return result?.trim() ?? null;
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async function suggestConflictResolution(conflictDiff, model) {
|
|
489
|
+
try {
|
|
490
|
+
const userMessage = `Help me resolve this merge conflict:
|
|
491
|
+
|
|
492
|
+
${conflictDiff.slice(0, 4000)}`;
|
|
493
|
+
const result = await callCopilot(CONFLICT_RESOLUTION_SYSTEM_PROMPT, userMessage, model);
|
|
494
|
+
return result?.trim() ?? null;
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 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
|
+
var commit_default = defineCommand2({
|
|
506
|
+
meta: {
|
|
507
|
+
name: "commit",
|
|
508
|
+
description: "Stage changes and create a Clean Commit message (AI-powered)"
|
|
509
|
+
},
|
|
510
|
+
args: {
|
|
511
|
+
model: {
|
|
512
|
+
type: "string",
|
|
513
|
+
description: "AI model to use for commit message generation"
|
|
514
|
+
},
|
|
515
|
+
"no-ai": {
|
|
516
|
+
type: "boolean",
|
|
517
|
+
description: "Skip AI and write commit message manually",
|
|
518
|
+
default: false
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
async run({ args }) {
|
|
522
|
+
if (!await isGitRepo()) {
|
|
523
|
+
error("Not inside a git repository.");
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
const config = readConfig();
|
|
527
|
+
if (!config) {
|
|
528
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
heading("\uD83D\uDCBE contrib commit");
|
|
532
|
+
const stagedFiles = await getStagedFiles();
|
|
533
|
+
if (stagedFiles.length === 0) {
|
|
534
|
+
const changedFiles = await getChangedFiles();
|
|
535
|
+
if (changedFiles.length === 0) {
|
|
536
|
+
error("No changes to commit.");
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
console.log(`
|
|
540
|
+
${pc4.bold("Changed files:")}`);
|
|
541
|
+
for (const f of changedFiles) {
|
|
542
|
+
console.log(` ${pc4.dim("•")} ${f}`);
|
|
543
|
+
}
|
|
544
|
+
console.log();
|
|
545
|
+
warn("No staged changes. Stage your files with `git add` and re-run.");
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
info(`Staged files: ${stagedFiles.join(", ")}`);
|
|
549
|
+
let commitMessage = null;
|
|
550
|
+
const useAI = !args["no-ai"];
|
|
551
|
+
if (useAI) {
|
|
552
|
+
const copilotError = await checkCopilotAvailable();
|
|
553
|
+
if (copilotError) {
|
|
554
|
+
warn(`AI unavailable: ${copilotError}`);
|
|
555
|
+
warn("Falling back to manual commit message entry.");
|
|
556
|
+
} else {
|
|
557
|
+
info("Generating commit message with AI...");
|
|
558
|
+
const diff = await getStagedDiff();
|
|
559
|
+
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model);
|
|
560
|
+
if (commitMessage) {
|
|
561
|
+
console.log(`
|
|
562
|
+
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(commitMessage))}`);
|
|
563
|
+
} else {
|
|
564
|
+
warn("AI did not return a commit message. Falling back to manual entry.");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
let finalMessage = null;
|
|
569
|
+
if (commitMessage) {
|
|
570
|
+
const action = await selectPrompt("What would you like to do?", [
|
|
571
|
+
"Accept this message",
|
|
572
|
+
"Edit this message",
|
|
573
|
+
"Regenerate",
|
|
574
|
+
"Write manually"
|
|
575
|
+
]);
|
|
576
|
+
if (action === "Accept this message") {
|
|
577
|
+
finalMessage = commitMessage;
|
|
578
|
+
} else if (action === "Edit this message") {
|
|
579
|
+
finalMessage = await inputPrompt("Edit commit message", commitMessage);
|
|
580
|
+
} else if (action === "Regenerate") {
|
|
581
|
+
info("Regenerating...");
|
|
582
|
+
const diff = await getStagedDiff();
|
|
583
|
+
const regen = await generateCommitMessage(diff, stagedFiles, args.model);
|
|
584
|
+
if (regen) {
|
|
585
|
+
console.log(`
|
|
586
|
+
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(regen))}`);
|
|
587
|
+
const ok = await confirmPrompt("Use this message?");
|
|
588
|
+
finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
|
|
589
|
+
} else {
|
|
590
|
+
warn("Regeneration failed. Falling back to manual entry.");
|
|
591
|
+
finalMessage = await inputPrompt("Enter commit message");
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
finalMessage = await inputPrompt("Enter commit message");
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
console.log();
|
|
598
|
+
console.log(pc4.dim("Clean Commit format: <emoji> <type>[!][(<scope>)]: <description>"));
|
|
599
|
+
console.log(pc4.dim("Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors"));
|
|
600
|
+
console.log();
|
|
601
|
+
finalMessage = await inputPrompt("Enter commit message");
|
|
602
|
+
}
|
|
603
|
+
if (!finalMessage) {
|
|
604
|
+
error("No commit message provided.");
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
if (!validateCleanCommit(finalMessage)) {
|
|
608
|
+
warn("Commit message does not follow Clean Commit format.");
|
|
609
|
+
warn("Format: <emoji> <type>[!][(<scope>)]: <description>");
|
|
610
|
+
const proceed = await confirmPrompt("Commit anyway?");
|
|
611
|
+
if (!proceed)
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
const result = await commitWithMessage(finalMessage);
|
|
615
|
+
if (result.exitCode !== 0) {
|
|
616
|
+
error(`Failed to commit: ${result.stderr}`);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
success(`✅ Committed: ${pc4.bold(finalMessage)}`);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// src/commands/setup.ts
|
|
624
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
625
|
+
import pc5 from "picocolors";
|
|
626
|
+
|
|
627
|
+
// src/utils/gh.ts
|
|
628
|
+
import { execFile as execFileCb2 } from "node:child_process";
|
|
629
|
+
function run2(args) {
|
|
630
|
+
return new Promise((resolve) => {
|
|
631
|
+
execFileCb2("gh", args, (error2, stdout, stderr) => {
|
|
632
|
+
resolve({
|
|
633
|
+
exitCode: error2 ? error2.code != null ? Number(error2.code) : 1 : 0,
|
|
634
|
+
stdout: stdout ?? "",
|
|
635
|
+
stderr: stderr ?? ""
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
async function checkGhInstalled() {
|
|
641
|
+
try {
|
|
642
|
+
const { exitCode } = await run2(["--version"]);
|
|
643
|
+
return exitCode === 0;
|
|
644
|
+
} catch {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async function checkGhAuth() {
|
|
649
|
+
try {
|
|
650
|
+
const { exitCode } = await run2(["auth", "status"]);
|
|
651
|
+
return exitCode === 0;
|
|
652
|
+
} catch {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function checkRepoPermissions(owner, repo) {
|
|
657
|
+
const { exitCode, stdout } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
658
|
+
if (exitCode !== 0)
|
|
659
|
+
return null;
|
|
660
|
+
try {
|
|
661
|
+
return JSON.parse(stdout.trim());
|
|
662
|
+
} catch {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function isRepoFork() {
|
|
667
|
+
const { exitCode, stdout } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
|
|
668
|
+
if (exitCode !== 0)
|
|
669
|
+
return null;
|
|
670
|
+
const val = stdout.trim();
|
|
671
|
+
if (val === "true")
|
|
672
|
+
return true;
|
|
673
|
+
if (val === "false")
|
|
674
|
+
return false;
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
async function getCurrentRepoInfo() {
|
|
678
|
+
const { exitCode, stdout } = await run2([
|
|
679
|
+
"repo",
|
|
680
|
+
"view",
|
|
681
|
+
"--json",
|
|
682
|
+
"nameWithOwner",
|
|
683
|
+
"-q",
|
|
684
|
+
".nameWithOwner"
|
|
685
|
+
]);
|
|
686
|
+
if (exitCode !== 0)
|
|
687
|
+
return null;
|
|
688
|
+
const nameWithOwner = stdout.trim();
|
|
689
|
+
if (!nameWithOwner)
|
|
690
|
+
return null;
|
|
691
|
+
const [owner, repo] = nameWithOwner.split("/");
|
|
692
|
+
if (!owner || !repo)
|
|
693
|
+
return null;
|
|
694
|
+
return { owner, repo };
|
|
695
|
+
}
|
|
696
|
+
async function createPR(options) {
|
|
697
|
+
const args = [
|
|
698
|
+
"pr",
|
|
699
|
+
"create",
|
|
700
|
+
"--base",
|
|
701
|
+
options.base,
|
|
702
|
+
"--title",
|
|
703
|
+
options.title,
|
|
704
|
+
"--body",
|
|
705
|
+
options.body
|
|
706
|
+
];
|
|
707
|
+
if (options.draft)
|
|
708
|
+
args.push("--draft");
|
|
709
|
+
return run2(args);
|
|
710
|
+
}
|
|
711
|
+
async function createPRFill(base, draft) {
|
|
712
|
+
const args = ["pr", "create", "--base", base, "--fill"];
|
|
713
|
+
if (draft)
|
|
714
|
+
args.push("--draft");
|
|
715
|
+
return run2(args);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/utils/remote.ts
|
|
719
|
+
function parseRepoFromUrl(url) {
|
|
720
|
+
const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
721
|
+
if (httpsMatch) {
|
|
722
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
723
|
+
}
|
|
724
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
725
|
+
if (sshMatch) {
|
|
726
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
async function getRepoInfoFromRemote(remote = "origin") {
|
|
731
|
+
const url = await getRemoteUrl(remote);
|
|
732
|
+
if (!url)
|
|
733
|
+
return null;
|
|
734
|
+
return parseRepoFromUrl(url);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/commands/setup.ts
|
|
738
|
+
var setup_default = defineCommand3({
|
|
739
|
+
meta: {
|
|
740
|
+
name: "setup",
|
|
741
|
+
description: "Initialize contribute-now config for this repo (.contributerc.json)"
|
|
742
|
+
},
|
|
743
|
+
async run() {
|
|
744
|
+
if (!await isGitRepo()) {
|
|
745
|
+
error("Not inside a git repository. Run this command from within a git repo.");
|
|
746
|
+
process.exit(1);
|
|
747
|
+
}
|
|
748
|
+
heading("\uD83D\uDD27 contribute-now setup");
|
|
749
|
+
const remotes = await getRemotes();
|
|
750
|
+
if (remotes.length === 0) {
|
|
751
|
+
error("No git remotes found. Add a remote first (e.g., git remote add origin <url>).");
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
info(`Found remotes: ${remotes.join(", ")}`);
|
|
755
|
+
let detectedRole = null;
|
|
756
|
+
let detectionSource = "";
|
|
757
|
+
const ghInstalled = await checkGhInstalled();
|
|
758
|
+
if (ghInstalled && await checkGhAuth()) {
|
|
759
|
+
const isFork = await isRepoFork();
|
|
760
|
+
if (isFork === true) {
|
|
761
|
+
detectedRole = "contributor";
|
|
762
|
+
detectionSource = "gh CLI (fork detected)";
|
|
763
|
+
} else if (isFork === false) {
|
|
764
|
+
const repoInfo = await getCurrentRepoInfo();
|
|
765
|
+
if (repoInfo) {
|
|
766
|
+
const perms = await checkRepoPermissions(repoInfo.owner, repoInfo.repo);
|
|
767
|
+
if (perms?.admin || perms?.push) {
|
|
768
|
+
detectedRole = "maintainer";
|
|
769
|
+
detectionSource = "gh CLI (admin/push permissions)";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (detectedRole === null) {
|
|
775
|
+
if (remotes.includes("upstream")) {
|
|
776
|
+
detectedRole = "contributor";
|
|
777
|
+
detectionSource = "heuristic (upstream remote exists)";
|
|
778
|
+
} else if (remotes.includes("origin") && remotes.length === 1) {
|
|
779
|
+
detectedRole = "maintainer";
|
|
780
|
+
detectionSource = "heuristic (only origin remote)";
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (detectedRole === null) {
|
|
784
|
+
const roleChoice = await selectPrompt("What is your role in this project?", [
|
|
785
|
+
"maintainer",
|
|
786
|
+
"contributor"
|
|
787
|
+
]);
|
|
788
|
+
detectedRole = roleChoice;
|
|
789
|
+
detectionSource = "user selection";
|
|
790
|
+
} else {
|
|
791
|
+
info(`Detected role: ${pc5.bold(detectedRole)} (via ${detectionSource})`);
|
|
792
|
+
const confirmed = await confirmPrompt(`Role detected as ${pc5.bold(detectedRole)}. Is this correct?`);
|
|
793
|
+
if (!confirmed) {
|
|
794
|
+
const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
|
|
795
|
+
detectedRole = roleChoice;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const defaultConfig = getDefaultConfig();
|
|
799
|
+
const mainBranch = await inputPrompt("Main branch name", defaultConfig.mainBranch);
|
|
800
|
+
const devBranch = await inputPrompt("Dev branch name", defaultConfig.devBranch);
|
|
801
|
+
const originRemote = await inputPrompt("Origin remote name", defaultConfig.origin);
|
|
802
|
+
let upstreamRemote = defaultConfig.upstream;
|
|
803
|
+
if (detectedRole === "contributor") {
|
|
804
|
+
upstreamRemote = await inputPrompt("Upstream remote name", defaultConfig.upstream);
|
|
805
|
+
if (!remotes.includes(upstreamRemote)) {
|
|
806
|
+
warn(`Remote "${upstreamRemote}" not found.`);
|
|
807
|
+
const originUrl = await getRemoteUrl(originRemote);
|
|
808
|
+
const repoInfo = originUrl ? parseRepoFromUrl(originUrl) : null;
|
|
809
|
+
const upstreamUrl = await inputPrompt("Enter upstream repository URL to add", repoInfo ? `https://github.com/${repoInfo.owner}/${repoInfo.repo}` : undefined);
|
|
810
|
+
if (upstreamUrl) {
|
|
811
|
+
info(`Run: git remote add ${upstreamRemote} ${upstreamUrl}`);
|
|
812
|
+
warn("Please add the upstream remote and re-run setup, or add it manually.");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const config = {
|
|
817
|
+
role: detectedRole,
|
|
818
|
+
mainBranch,
|
|
819
|
+
devBranch,
|
|
820
|
+
upstream: upstreamRemote,
|
|
821
|
+
origin: originRemote,
|
|
822
|
+
branchPrefixes: defaultConfig.branchPrefixes
|
|
823
|
+
};
|
|
824
|
+
writeConfig(config);
|
|
825
|
+
success(`✅ Config written to .contributerc.json`);
|
|
826
|
+
if (!isGitignored()) {
|
|
827
|
+
warn(".contributerc.json is not in .gitignore. Add it to avoid committing personal config.");
|
|
828
|
+
warn(' echo ".contributerc.json" >> .gitignore');
|
|
829
|
+
}
|
|
830
|
+
console.log();
|
|
831
|
+
info(`Role: ${pc5.bold(config.role)}`);
|
|
832
|
+
info(`Main: ${pc5.bold(config.mainBranch)} | Dev: ${pc5.bold(config.devBranch)}`);
|
|
833
|
+
info(`Origin: ${pc5.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc5.bold(config.upstream)}` : ""}`);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// src/commands/start.ts
|
|
838
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
839
|
+
import pc6 from "picocolors";
|
|
840
|
+
|
|
841
|
+
// src/utils/branch.ts
|
|
842
|
+
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
843
|
+
function hasPrefix(branchName, prefixes = DEFAULT_PREFIXES) {
|
|
844
|
+
return prefixes.some((p) => branchName.startsWith(`${p}/`));
|
|
845
|
+
}
|
|
846
|
+
function formatBranchName(prefix, name) {
|
|
847
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
848
|
+
return `${prefix}/${sanitized}`;
|
|
849
|
+
}
|
|
850
|
+
function looksLikeNaturalLanguage(input) {
|
|
851
|
+
return input.includes(" ") && !input.includes("/");
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/commands/start.ts
|
|
855
|
+
var start_default = defineCommand4({
|
|
856
|
+
meta: {
|
|
857
|
+
name: "start",
|
|
858
|
+
description: "Create a new feature branch from the latest dev"
|
|
859
|
+
},
|
|
860
|
+
args: {
|
|
861
|
+
name: {
|
|
862
|
+
type: "positional",
|
|
863
|
+
description: "Branch name or description",
|
|
864
|
+
required: true
|
|
865
|
+
},
|
|
866
|
+
model: {
|
|
867
|
+
type: "string",
|
|
868
|
+
description: "AI model to use for branch name suggestion"
|
|
869
|
+
},
|
|
870
|
+
"no-ai": {
|
|
871
|
+
type: "boolean",
|
|
872
|
+
description: "Skip AI branch name suggestion",
|
|
873
|
+
default: false
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
async run({ args }) {
|
|
877
|
+
if (!await isGitRepo()) {
|
|
878
|
+
error("Not inside a git repository.");
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
const config = readConfig();
|
|
882
|
+
if (!config) {
|
|
883
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
if (await hasUncommittedChanges()) {
|
|
887
|
+
error("You have uncommitted changes. Please commit or stash them before creating a branch.");
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
const { devBranch, origin, upstream, branchPrefixes, role } = config;
|
|
891
|
+
let branchName = args.name;
|
|
892
|
+
heading("\uD83C\uDF3F contrib start");
|
|
893
|
+
const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
|
|
894
|
+
if (useAI) {
|
|
895
|
+
info("Generating branch name suggestion from description...");
|
|
896
|
+
const suggested = await suggestBranchName(branchName, args.model);
|
|
897
|
+
if (suggested) {
|
|
898
|
+
console.log(`
|
|
899
|
+
${pc6.dim("AI suggestion:")} ${pc6.bold(pc6.cyan(suggested))}`);
|
|
900
|
+
const accepted = await confirmPrompt(`Use ${pc6.bold(suggested)} as your branch name?`);
|
|
901
|
+
if (accepted) {
|
|
902
|
+
branchName = suggested;
|
|
903
|
+
} else {
|
|
904
|
+
branchName = await inputPrompt("Enter branch name", branchName);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (!hasPrefix(branchName, branchPrefixes)) {
|
|
909
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc6.bold(branchName)}:`, branchPrefixes);
|
|
910
|
+
branchName = formatBranchName(prefix, branchName);
|
|
911
|
+
}
|
|
912
|
+
info(`Creating branch: ${pc6.bold(branchName)}`);
|
|
913
|
+
const remote = role === "contributor" ? upstream : origin;
|
|
914
|
+
const remoteDevRef = role === "contributor" ? `${upstream}/${devBranch}` : `${origin}/${devBranch}`;
|
|
915
|
+
await fetchRemote(remote);
|
|
916
|
+
const resetResult = await resetHard(remoteDevRef);
|
|
917
|
+
if (resetResult.exitCode !== 0) {}
|
|
918
|
+
const result = await createBranch(branchName, devBranch);
|
|
919
|
+
if (result.exitCode !== 0) {
|
|
920
|
+
error(`Failed to create branch: ${result.stderr}`);
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
success(`✅ Created ${pc6.bold(branchName)} from latest ${pc6.bold(devBranch)}`);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// src/commands/status.ts
|
|
928
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
929
|
+
import pc7 from "picocolors";
|
|
930
|
+
var status_default = defineCommand5({
|
|
931
|
+
meta: {
|
|
932
|
+
name: "status",
|
|
933
|
+
description: "Show sync status of main, dev, and current branch"
|
|
934
|
+
},
|
|
935
|
+
async run() {
|
|
936
|
+
if (!await isGitRepo()) {
|
|
937
|
+
error("Not inside a git repository.");
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
const config = readConfig();
|
|
941
|
+
if (!config) {
|
|
942
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
heading("\uD83D\uDCCA contribute-now status");
|
|
946
|
+
await fetchAll();
|
|
947
|
+
const currentBranch = await getCurrentBranch();
|
|
948
|
+
const { mainBranch, devBranch, origin, upstream } = config;
|
|
949
|
+
const isContributor = config.role === "contributor";
|
|
950
|
+
const dirty = await hasUncommittedChanges();
|
|
951
|
+
if (dirty) {
|
|
952
|
+
console.log(` ${pc7.yellow("⚠")} ${pc7.yellow("Uncommitted changes in working tree")}`);
|
|
953
|
+
console.log();
|
|
954
|
+
}
|
|
955
|
+
const mainRemote = `${origin}/${mainBranch}`;
|
|
956
|
+
const mainDiv = await getDivergence(mainBranch, mainRemote);
|
|
957
|
+
const mainStatus = formatStatus(mainBranch, mainRemote, mainDiv.ahead, mainDiv.behind);
|
|
958
|
+
console.log(mainStatus);
|
|
959
|
+
const devRemoteRef = isContributor ? `${upstream}/${devBranch}` : `${origin}/${mainBranch}`;
|
|
960
|
+
const devDiv = await getDivergence(devBranch, devRemoteRef);
|
|
961
|
+
let devLine = formatStatus(devBranch, devRemoteRef, devDiv.ahead, devDiv.behind);
|
|
962
|
+
if (!isContributor && devDiv.ahead > 0 && devDiv.behind > 0) {
|
|
963
|
+
devLine += pc7.red(" (needs sync! squash-merge divergence detected)");
|
|
964
|
+
} else if (devDiv.ahead > 0 && devDiv.behind === 0) {
|
|
965
|
+
devLine += pc7.yellow(" (needs sync!)");
|
|
966
|
+
}
|
|
967
|
+
console.log(devLine);
|
|
968
|
+
if (currentBranch && currentBranch !== mainBranch && currentBranch !== devBranch) {
|
|
969
|
+
const branchDiv = await getDivergence(currentBranch, devBranch);
|
|
970
|
+
const branchLine = formatStatus(currentBranch, devBranch, branchDiv.ahead, branchDiv.behind);
|
|
971
|
+
console.log(branchLine + pc7.dim(` (current ${pc7.green("*")})`));
|
|
972
|
+
} else if (currentBranch) {
|
|
973
|
+
if (currentBranch === mainBranch) {
|
|
974
|
+
console.log(pc7.dim(` (on ${pc7.bold(mainBranch)} branch)`));
|
|
975
|
+
} else if (currentBranch === devBranch) {
|
|
976
|
+
console.log(pc7.dim(` (on ${pc7.bold(devBranch)} branch)`));
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
console.log();
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
function formatStatus(branch, base, ahead, behind) {
|
|
983
|
+
const label = pc7.bold(branch.padEnd(20));
|
|
984
|
+
if (ahead === 0 && behind === 0) {
|
|
985
|
+
return ` ${pc7.green("✓")} ${label} ${pc7.dim(`in sync with ${base}`)}`;
|
|
986
|
+
}
|
|
987
|
+
if (ahead > 0 && behind === 0) {
|
|
988
|
+
return ` ${pc7.yellow("↑")} ${label} ${pc7.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
|
|
989
|
+
}
|
|
990
|
+
if (behind > 0 && ahead === 0) {
|
|
991
|
+
return ` ${pc7.red("↓")} ${label} ${pc7.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
|
|
992
|
+
}
|
|
993
|
+
return ` ${pc7.red("⚡")} ${label} ${pc7.yellow(`${ahead} ahead`)}${pc7.dim(", ")}${pc7.red(`${behind} behind`)} ${pc7.dim(base)}`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/commands/submit.ts
|
|
997
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
998
|
+
import pc8 from "picocolors";
|
|
999
|
+
var submit_default = defineCommand6({
|
|
1000
|
+
meta: {
|
|
1001
|
+
name: "submit",
|
|
1002
|
+
description: "Push current branch and create a pull request"
|
|
1003
|
+
},
|
|
1004
|
+
args: {
|
|
1005
|
+
draft: {
|
|
1006
|
+
type: "boolean",
|
|
1007
|
+
description: "Create PR as draft",
|
|
1008
|
+
default: false
|
|
1009
|
+
},
|
|
1010
|
+
"no-ai": {
|
|
1011
|
+
type: "boolean",
|
|
1012
|
+
description: "Skip AI PR description generation",
|
|
1013
|
+
default: false
|
|
1014
|
+
},
|
|
1015
|
+
model: {
|
|
1016
|
+
type: "string",
|
|
1017
|
+
description: "AI model to use for PR description generation"
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
async run({ args }) {
|
|
1021
|
+
if (!await isGitRepo()) {
|
|
1022
|
+
error("Not inside a git repository.");
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
const config = readConfig();
|
|
1026
|
+
if (!config) {
|
|
1027
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
const { mainBranch, devBranch, origin } = config;
|
|
1031
|
+
const currentBranch = await getCurrentBranch();
|
|
1032
|
+
if (!currentBranch) {
|
|
1033
|
+
error("Could not determine current branch.");
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
if (currentBranch === mainBranch || currentBranch === devBranch) {
|
|
1037
|
+
error(`Cannot submit ${pc8.bold(mainBranch)} or ${pc8.bold(devBranch)} as a PR. Switch to your feature branch.`);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
heading("\uD83D\uDE80 contrib submit");
|
|
1041
|
+
info(`Pushing ${pc8.bold(currentBranch)} to ${origin}...`);
|
|
1042
|
+
const pushResult = await pushSetUpstream(origin, currentBranch);
|
|
1043
|
+
if (pushResult.exitCode !== 0) {
|
|
1044
|
+
error(`Failed to push: ${pushResult.stderr}`);
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
const ghInstalled = await checkGhInstalled();
|
|
1048
|
+
const ghAuthed = ghInstalled && await checkGhAuth();
|
|
1049
|
+
if (!ghInstalled || !ghAuthed) {
|
|
1050
|
+
const repoInfo = await getRepoInfoFromRemote(origin);
|
|
1051
|
+
if (repoInfo) {
|
|
1052
|
+
const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${devBranch}...${currentBranch}?expand=1`;
|
|
1053
|
+
console.log();
|
|
1054
|
+
info("Create your PR manually:");
|
|
1055
|
+
console.log(` ${pc8.cyan(prUrl)}`);
|
|
1056
|
+
} else {
|
|
1057
|
+
info("gh CLI not available. Create your PR manually on GitHub.");
|
|
1058
|
+
}
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
let prTitle = null;
|
|
1062
|
+
let prBody = null;
|
|
1063
|
+
if (!args["no-ai"]) {
|
|
1064
|
+
const copilotError = await checkCopilotAvailable();
|
|
1065
|
+
if (!copilotError) {
|
|
1066
|
+
info("Generating AI PR description...");
|
|
1067
|
+
const commits = await getLog(devBranch, "HEAD");
|
|
1068
|
+
const diff = await getLogDiff(devBranch, "HEAD");
|
|
1069
|
+
const result = await generatePRDescription(commits, diff, args.model);
|
|
1070
|
+
if (result) {
|
|
1071
|
+
prTitle = result.title;
|
|
1072
|
+
prBody = result.body;
|
|
1073
|
+
console.log(`
|
|
1074
|
+
${pc8.dim("AI title:")} ${pc8.bold(pc8.cyan(prTitle))}`);
|
|
1075
|
+
console.log(`
|
|
1076
|
+
${pc8.dim("AI body preview:")}`);
|
|
1077
|
+
console.log(pc8.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
|
|
1078
|
+
} else {
|
|
1079
|
+
warn("AI did not return a PR description.");
|
|
1080
|
+
}
|
|
1081
|
+
} else {
|
|
1082
|
+
warn(`AI unavailable: ${copilotError}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (prTitle && prBody) {
|
|
1086
|
+
const action = await selectPrompt("What would you like to do with the PR description?", [
|
|
1087
|
+
"Use AI description",
|
|
1088
|
+
"Edit title",
|
|
1089
|
+
"Write manually",
|
|
1090
|
+
"Use gh --fill (auto-fill from commits)"
|
|
1091
|
+
]);
|
|
1092
|
+
if (action === "Use AI description") {} else if (action === "Edit title") {
|
|
1093
|
+
prTitle = await inputPrompt("PR title", prTitle);
|
|
1094
|
+
} else if (action === "Write manually") {
|
|
1095
|
+
prTitle = await inputPrompt("PR title");
|
|
1096
|
+
prBody = await inputPrompt("PR body (markdown)");
|
|
1097
|
+
} else {
|
|
1098
|
+
const fillResult = await createPRFill(devBranch, args.draft);
|
|
1099
|
+
if (fillResult.exitCode !== 0) {
|
|
1100
|
+
error(`Failed to create PR: ${fillResult.stderr}`);
|
|
1101
|
+
process.exit(1);
|
|
1102
|
+
}
|
|
1103
|
+
success(`✅ PR created: ${fillResult.stdout.trim()}`);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
} else {
|
|
1107
|
+
const useManual = await confirmPrompt("Create PR with manual title/body?");
|
|
1108
|
+
if (useManual) {
|
|
1109
|
+
prTitle = await inputPrompt("PR title");
|
|
1110
|
+
prBody = await inputPrompt("PR body (markdown)");
|
|
1111
|
+
} else {
|
|
1112
|
+
const fillResult = await createPRFill(devBranch, args.draft);
|
|
1113
|
+
if (fillResult.exitCode !== 0) {
|
|
1114
|
+
error(`Failed to create PR: ${fillResult.stderr}`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
success(`✅ PR created: ${fillResult.stdout.trim()}`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (!prTitle) {
|
|
1122
|
+
error("No PR title provided.");
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
const prResult = await createPR({
|
|
1126
|
+
base: devBranch,
|
|
1127
|
+
title: prTitle,
|
|
1128
|
+
body: prBody ?? "",
|
|
1129
|
+
draft: args.draft
|
|
1130
|
+
});
|
|
1131
|
+
if (prResult.exitCode !== 0) {
|
|
1132
|
+
error(`Failed to create PR: ${prResult.stderr}`);
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
success(`✅ PR created: ${prResult.stdout.trim()}`);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// src/commands/sync.ts
|
|
1140
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1141
|
+
import pc9 from "picocolors";
|
|
1142
|
+
var sync_default = defineCommand7({
|
|
1143
|
+
meta: {
|
|
1144
|
+
name: "sync",
|
|
1145
|
+
description: "Reset dev branch to match origin/main (maintainer) or upstream/dev (contributor)"
|
|
1146
|
+
},
|
|
1147
|
+
args: {
|
|
1148
|
+
yes: {
|
|
1149
|
+
type: "boolean",
|
|
1150
|
+
alias: "y",
|
|
1151
|
+
description: "Skip confirmation prompt",
|
|
1152
|
+
default: false
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
async run({ args }) {
|
|
1156
|
+
if (!await isGitRepo()) {
|
|
1157
|
+
error("Not inside a git repository.");
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
const config = readConfig();
|
|
1161
|
+
if (!config) {
|
|
1162
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
const { role, mainBranch, devBranch, origin, upstream } = config;
|
|
1166
|
+
if (await hasUncommittedChanges()) {
|
|
1167
|
+
error("You have uncommitted changes. Please commit or stash them before syncing.");
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
heading(`\uD83D\uDD04 contrib sync (${role})`);
|
|
1171
|
+
if (role === "maintainer") {
|
|
1172
|
+
info(`Fetching ${origin}...`);
|
|
1173
|
+
const fetchResult = await fetchRemote(origin);
|
|
1174
|
+
if (fetchResult.exitCode !== 0) {
|
|
1175
|
+
error(`Failed to fetch ${origin}: ${fetchResult.stderr}`);
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
const div = await getDivergence(devBranch, `${origin}/${mainBranch}`);
|
|
1179
|
+
if (div.ahead > 0 || div.behind > 0) {
|
|
1180
|
+
info(`${pc9.bold(devBranch)} is ${pc9.yellow(`${div.ahead} ahead`)} and ${pc9.red(`${div.behind} behind`)} ${origin}/${mainBranch}`);
|
|
1181
|
+
} else {
|
|
1182
|
+
info(`${pc9.bold(devBranch)} is already in sync with ${origin}/${mainBranch}`);
|
|
1183
|
+
}
|
|
1184
|
+
if (!args.yes) {
|
|
1185
|
+
const ok = await confirmPrompt(`This will reset ${pc9.bold(devBranch)} to match ${pc9.bold(`${origin}/${mainBranch}`)}.`);
|
|
1186
|
+
if (!ok)
|
|
1187
|
+
process.exit(0);
|
|
1188
|
+
}
|
|
1189
|
+
const coResult = await checkoutBranch(devBranch);
|
|
1190
|
+
if (coResult.exitCode !== 0) {
|
|
1191
|
+
error(`Failed to checkout ${devBranch}: ${coResult.stderr}`);
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
const resetResult = await resetHard(`${origin}/${mainBranch}`);
|
|
1195
|
+
if (resetResult.exitCode !== 0) {
|
|
1196
|
+
error(`Failed to reset: ${resetResult.stderr}`);
|
|
1197
|
+
process.exit(1);
|
|
1198
|
+
}
|
|
1199
|
+
const pushResult = await pushForceWithLease(origin, devBranch);
|
|
1200
|
+
if (pushResult.exitCode !== 0) {
|
|
1201
|
+
error(`Failed to push: ${pushResult.stderr}`);
|
|
1202
|
+
process.exit(1);
|
|
1203
|
+
}
|
|
1204
|
+
success(`✅ ${devBranch} has been reset to match ${origin}/${mainBranch} and pushed.`);
|
|
1205
|
+
} else {
|
|
1206
|
+
info(`Fetching ${upstream}...`);
|
|
1207
|
+
const fetchResult = await fetchRemote(upstream);
|
|
1208
|
+
if (fetchResult.exitCode !== 0) {
|
|
1209
|
+
error(`Failed to fetch ${upstream}: ${fetchResult.stderr}`);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
if (!args.yes) {
|
|
1213
|
+
const ok = await confirmPrompt(`This will reset local ${pc9.bold(devBranch)} to match ${pc9.bold(`${upstream}/${devBranch}`)}.`);
|
|
1214
|
+
if (!ok)
|
|
1215
|
+
process.exit(0);
|
|
1216
|
+
}
|
|
1217
|
+
const coResult = await checkoutBranch(devBranch);
|
|
1218
|
+
if (coResult.exitCode !== 0) {
|
|
1219
|
+
error(`Failed to checkout ${devBranch}: ${coResult.stderr}`);
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
const resetResult = await resetHard(`${upstream}/${devBranch}`);
|
|
1223
|
+
if (resetResult.exitCode !== 0) {
|
|
1224
|
+
error(`Failed to reset: ${resetResult.stderr}`);
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
}
|
|
1227
|
+
const pushResult = await pushForceWithLease(origin, devBranch);
|
|
1228
|
+
if (pushResult.exitCode !== 0) {
|
|
1229
|
+
error(`Failed to push: ${pushResult.stderr}`);
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
success(`✅ ${devBranch} has been reset to match ${upstream}/${devBranch} and pushed.`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// src/commands/update.ts
|
|
1238
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1239
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1240
|
+
import pc10 from "picocolors";
|
|
1241
|
+
var update_default = defineCommand8({
|
|
1242
|
+
meta: {
|
|
1243
|
+
name: "update",
|
|
1244
|
+
description: "Rebase current branch onto latest dev"
|
|
1245
|
+
},
|
|
1246
|
+
args: {
|
|
1247
|
+
model: {
|
|
1248
|
+
type: "string",
|
|
1249
|
+
description: "AI model to use for conflict resolution suggestions"
|
|
1250
|
+
},
|
|
1251
|
+
"no-ai": {
|
|
1252
|
+
type: "boolean",
|
|
1253
|
+
description: "Skip AI conflict resolution suggestions",
|
|
1254
|
+
default: false
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
async run({ args }) {
|
|
1258
|
+
if (!await isGitRepo()) {
|
|
1259
|
+
error("Not inside a git repository.");
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
}
|
|
1262
|
+
const config = readConfig();
|
|
1263
|
+
if (!config) {
|
|
1264
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
const { mainBranch, devBranch, origin, upstream, role } = config;
|
|
1268
|
+
const currentBranch = await getCurrentBranch();
|
|
1269
|
+
if (!currentBranch) {
|
|
1270
|
+
error("Could not determine current branch.");
|
|
1271
|
+
process.exit(1);
|
|
1272
|
+
}
|
|
1273
|
+
if (currentBranch === mainBranch || currentBranch === devBranch) {
|
|
1274
|
+
error(`Use \`contrib sync\` to update ${pc10.bold(mainBranch)} or ${pc10.bold(devBranch)} branches.`);
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1277
|
+
if (await hasUncommittedChanges()) {
|
|
1278
|
+
error("You have uncommitted changes. Please commit or stash them first.");
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1281
|
+
heading("\uD83D\uDD03 contrib update");
|
|
1282
|
+
info(`Updating ${pc10.bold(currentBranch)} with latest ${pc10.bold(devBranch)}...`);
|
|
1283
|
+
const remote = role === "contributor" ? upstream : origin;
|
|
1284
|
+
const remoteDevRef = role === "contributor" ? `${upstream}/${devBranch}` : `${origin}/${devBranch}`;
|
|
1285
|
+
await fetchRemote(remote);
|
|
1286
|
+
await resetHard(remoteDevRef);
|
|
1287
|
+
const rebaseResult = await rebase(devBranch);
|
|
1288
|
+
if (rebaseResult.exitCode !== 0) {
|
|
1289
|
+
warn("Rebase hit conflicts. Resolve them manually.");
|
|
1290
|
+
console.log();
|
|
1291
|
+
if (!args["no-ai"]) {
|
|
1292
|
+
const copilotError = await checkCopilotAvailable();
|
|
1293
|
+
if (!copilotError) {
|
|
1294
|
+
info("Fetching AI conflict resolution suggestions...");
|
|
1295
|
+
const conflictFiles = await getChangedFiles();
|
|
1296
|
+
let conflictDiff = "";
|
|
1297
|
+
for (const file of conflictFiles.slice(0, 3)) {
|
|
1298
|
+
try {
|
|
1299
|
+
const content = readFileSync2(file, "utf-8");
|
|
1300
|
+
if (content.includes("<<<<<<<")) {
|
|
1301
|
+
conflictDiff += `
|
|
1302
|
+
--- ${file} ---
|
|
1303
|
+
${content.slice(0, 2000)}
|
|
1304
|
+
`;
|
|
1305
|
+
}
|
|
1306
|
+
} catch {}
|
|
1307
|
+
}
|
|
1308
|
+
if (conflictDiff) {
|
|
1309
|
+
const suggestion = await suggestConflictResolution(conflictDiff, args.model);
|
|
1310
|
+
if (suggestion) {
|
|
1311
|
+
console.log(`
|
|
1312
|
+
${pc10.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
1313
|
+
console.log(pc10.dim("─".repeat(60)));
|
|
1314
|
+
console.log(suggestion);
|
|
1315
|
+
console.log(pc10.dim("─".repeat(60)));
|
|
1316
|
+
console.log();
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
console.log(pc10.bold("To resolve:"));
|
|
1322
|
+
console.log(` 1. Fix conflicts in the affected files`);
|
|
1323
|
+
console.log(` 2. ${pc10.cyan("git add <resolved-files>")}`);
|
|
1324
|
+
console.log(` 3. ${pc10.cyan("git rebase --continue")}`);
|
|
1325
|
+
console.log();
|
|
1326
|
+
console.log(` Or abort: ${pc10.cyan("git rebase --abort")}`);
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
success(`✅ ${pc10.bold(currentBranch)} has been rebased onto latest ${pc10.bold(devBranch)}`);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
// src/ui/banner.ts
|
|
1334
|
+
import figlet from "figlet";
|
|
1335
|
+
import pc11 from "picocolors";
|
|
1336
|
+
// package.json
|
|
1337
|
+
var package_default = {
|
|
1338
|
+
name: "contribute-now",
|
|
1339
|
+
version: "0.1.0-staging.7136fcc",
|
|
1340
|
+
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1341
|
+
type: "module",
|
|
1342
|
+
bin: {
|
|
1343
|
+
contrib: "dist/index.js",
|
|
1344
|
+
contribute: "dist/index.js"
|
|
1345
|
+
},
|
|
1346
|
+
files: [
|
|
1347
|
+
"dist"
|
|
1348
|
+
],
|
|
1349
|
+
scripts: {
|
|
1350
|
+
build: "bun build src/index.ts --outfile dist/index.js --target node --packages external",
|
|
1351
|
+
dev: "bun src/index.ts",
|
|
1352
|
+
test: "bun test",
|
|
1353
|
+
lint: "biome check .",
|
|
1354
|
+
"lint:fix": "biome check --write .",
|
|
1355
|
+
format: "biome format --write .",
|
|
1356
|
+
prepare: "husky || true"
|
|
1357
|
+
},
|
|
1358
|
+
engines: {
|
|
1359
|
+
node: ">=18",
|
|
1360
|
+
bun: ">=1.0"
|
|
1361
|
+
},
|
|
1362
|
+
keywords: [
|
|
1363
|
+
"git",
|
|
1364
|
+
"workflow",
|
|
1365
|
+
"squash-merge",
|
|
1366
|
+
"sync",
|
|
1367
|
+
"cli",
|
|
1368
|
+
"contribute",
|
|
1369
|
+
"fork",
|
|
1370
|
+
"dev-branch",
|
|
1371
|
+
"clean-commit"
|
|
1372
|
+
],
|
|
1373
|
+
author: "Waren Gonzaga",
|
|
1374
|
+
license: "GPL-3.0",
|
|
1375
|
+
repository: {
|
|
1376
|
+
type: "git",
|
|
1377
|
+
url: "git+https://github.com/warengonzaga/contribute-now.git"
|
|
1378
|
+
},
|
|
1379
|
+
dependencies: {
|
|
1380
|
+
"@github/copilot-sdk": "^0.1.25",
|
|
1381
|
+
"@wgtechlabs/log-engine": "^2.3.1",
|
|
1382
|
+
citty: "^0.1.6",
|
|
1383
|
+
figlet: "^1.10.0",
|
|
1384
|
+
picocolors: "^1.1.1"
|
|
1385
|
+
},
|
|
1386
|
+
devDependencies: {
|
|
1387
|
+
"@biomejs/biome": "^2.4.4",
|
|
1388
|
+
"@types/bun": "latest",
|
|
1389
|
+
"@types/figlet": "^1.7.0",
|
|
1390
|
+
husky: "^9.1.7",
|
|
1391
|
+
typescript: "^5.7.0"
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// src/ui/banner.ts
|
|
1396
|
+
var LOGO;
|
|
1397
|
+
try {
|
|
1398
|
+
LOGO = figlet.textSync("contrib", { font: "ANSI Shadow" });
|
|
1399
|
+
} catch {
|
|
1400
|
+
LOGO = "contribute-now";
|
|
1401
|
+
}
|
|
1402
|
+
function getVersion() {
|
|
1403
|
+
return package_default.version ?? "unknown";
|
|
1404
|
+
}
|
|
1405
|
+
function getAuthor() {
|
|
1406
|
+
return typeof package_default.author === "string" ? package_default.author : "unknown";
|
|
1407
|
+
}
|
|
1408
|
+
function showBanner(minimal = false) {
|
|
1409
|
+
console.log(pc11.cyan(`
|
|
1410
|
+
${LOGO}`));
|
|
1411
|
+
console.log(` ${pc11.dim(`v${getVersion()}`)} ${pc11.dim("—")} ${pc11.dim(`Built by ${getAuthor()}`)}`);
|
|
1412
|
+
if (!minimal) {
|
|
1413
|
+
console.log(` ${pc11.dim(package_default.description)}`);
|
|
1414
|
+
console.log();
|
|
1415
|
+
console.log(` ${pc11.yellow("Star")} ${pc11.cyan("https://github.com/warengonzaga/contribute-now")}`);
|
|
1416
|
+
console.log(` ${pc11.green("Contribute")} ${pc11.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
|
|
1417
|
+
console.log(` ${pc11.magenta("Sponsor")} ${pc11.cyan("https://warengonzaga.com/sponsor")}`);
|
|
1418
|
+
}
|
|
1419
|
+
console.log();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/index.ts
|
|
1423
|
+
var isHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
1424
|
+
showBanner(isHelp);
|
|
1425
|
+
var main = defineCommand9({
|
|
1426
|
+
meta: {
|
|
1427
|
+
name: "contrib",
|
|
1428
|
+
version: getVersion(),
|
|
1429
|
+
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges."
|
|
1430
|
+
},
|
|
1431
|
+
args: {
|
|
1432
|
+
version: {
|
|
1433
|
+
type: "boolean",
|
|
1434
|
+
alias: "v",
|
|
1435
|
+
description: "Show version number"
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
subCommands: {
|
|
1439
|
+
setup: setup_default,
|
|
1440
|
+
sync: sync_default,
|
|
1441
|
+
start: start_default,
|
|
1442
|
+
commit: commit_default,
|
|
1443
|
+
update: update_default,
|
|
1444
|
+
submit: submit_default,
|
|
1445
|
+
clean: clean_default,
|
|
1446
|
+
status: status_default
|
|
1447
|
+
},
|
|
1448
|
+
run({ args }) {
|
|
1449
|
+
if (args.version) {
|
|
1450
|
+
console.log(`contrib v${getVersion()}`);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
runMain(main);
|