contribute-now 0.2.0-dev.e0cfab8 → 0.2.0-pr.4dd3c30
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 +961 -282
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -20,7 +20,11 @@ function readConfig(cwd = process.cwd()) {
|
|
|
20
20
|
return null;
|
|
21
21
|
try {
|
|
22
22
|
const raw = readFileSync(path, "utf-8");
|
|
23
|
-
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.workflow !== "string" || typeof parsed.role !== "string" || typeof parsed.mainBranch !== "string" || typeof parsed.upstream !== "string" || typeof parsed.origin !== "string" || !Array.isArray(parsed.branchPrefixes) || typeof parsed.commitConvention !== "string") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
24
28
|
} catch {
|
|
25
29
|
return null;
|
|
26
30
|
}
|
|
@@ -56,68 +60,55 @@ function getDefaultConfig() {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
// src/utils/confirm.ts
|
|
63
|
+
import * as clack from "@clack/prompts";
|
|
59
64
|
import pc from "picocolors";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const response = await new Promise((resolve) => {
|
|
65
|
-
process.stdin.setEncoding("utf-8");
|
|
66
|
-
process.stdin.once("data", (data) => {
|
|
67
|
-
process.stdin.pause();
|
|
68
|
-
resolve(data.toString().trim());
|
|
69
|
-
});
|
|
70
|
-
process.stdin.resume();
|
|
71
|
-
});
|
|
72
|
-
if (response.toLowerCase() !== "y") {
|
|
73
|
-
console.log(pc.yellow("Aborted."));
|
|
74
|
-
return false;
|
|
65
|
+
function handleCancel(value) {
|
|
66
|
+
if (clack.isCancel(value)) {
|
|
67
|
+
clack.cancel("Cancelled.");
|
|
68
|
+
process.exit(0);
|
|
75
69
|
}
|
|
76
|
-
|
|
70
|
+
}
|
|
71
|
+
async function confirmPrompt(message) {
|
|
72
|
+
const result = await clack.confirm({ message });
|
|
73
|
+
handleCancel(result);
|
|
74
|
+
return result;
|
|
77
75
|
}
|
|
78
76
|
async function selectPrompt(message, choices) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.log(` ${pc.dim(`${i + 1}.`)} ${choice}`);
|
|
83
|
-
});
|
|
84
|
-
process.stdout.write(pc.dim(`Enter number [1-${choices.length}]: `));
|
|
85
|
-
const response = await new Promise((resolve) => {
|
|
86
|
-
process.stdin.setEncoding("utf-8");
|
|
87
|
-
process.stdin.once("data", (data) => {
|
|
88
|
-
process.stdin.pause();
|
|
89
|
-
resolve(data.toString().trim());
|
|
90
|
-
});
|
|
91
|
-
process.stdin.resume();
|
|
77
|
+
const result = await clack.select({
|
|
78
|
+
message,
|
|
79
|
+
options: choices.map((choice) => ({ value: choice, label: choice }))
|
|
92
80
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return choices[index];
|
|
96
|
-
}
|
|
97
|
-
return choices[0];
|
|
81
|
+
handleCancel(result);
|
|
82
|
+
return result;
|
|
98
83
|
}
|
|
99
84
|
async function inputPrompt(message, defaultValue) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
process.stdin.setEncoding("utf-8");
|
|
105
|
-
process.stdin.once("data", (data) => {
|
|
106
|
-
process.stdin.pause();
|
|
107
|
-
resolve(data.toString().trim());
|
|
108
|
-
});
|
|
109
|
-
process.stdin.resume();
|
|
85
|
+
const result = await clack.text({
|
|
86
|
+
message,
|
|
87
|
+
placeholder: defaultValue,
|
|
88
|
+
defaultValue
|
|
110
89
|
});
|
|
111
|
-
|
|
90
|
+
handleCancel(result);
|
|
91
|
+
return result || defaultValue || "";
|
|
92
|
+
}
|
|
93
|
+
async function multiSelectPrompt(message, choices) {
|
|
94
|
+
const result = await clack.multiselect({
|
|
95
|
+
message: `${message} ${pc.dim("(space to toggle, enter to confirm)")}`,
|
|
96
|
+
options: choices.map((choice) => ({ value: choice, label: choice })),
|
|
97
|
+
required: false
|
|
98
|
+
});
|
|
99
|
+
handleCancel(result);
|
|
100
|
+
return result;
|
|
112
101
|
}
|
|
113
102
|
|
|
114
103
|
// src/utils/git.ts
|
|
115
104
|
import { execFile as execFileCb } from "node:child_process";
|
|
105
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
106
|
+
import { join as join2 } from "node:path";
|
|
116
107
|
function run(args) {
|
|
117
108
|
return new Promise((resolve) => {
|
|
118
109
|
execFileCb("git", args, (error, stdout, stderr) => {
|
|
119
110
|
resolve({
|
|
120
|
-
exitCode: error ? error.code === "ENOENT" ? 127 : error.
|
|
111
|
+
exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
|
|
121
112
|
stdout: stdout ?? "",
|
|
122
113
|
stderr: stderr ?? ""
|
|
123
114
|
});
|
|
@@ -169,6 +160,13 @@ async function createBranch(branch, from) {
|
|
|
169
160
|
async function resetHard(ref) {
|
|
170
161
|
return run(["reset", "--hard", ref]);
|
|
171
162
|
}
|
|
163
|
+
async function updateLocalBranch(branch, target) {
|
|
164
|
+
const current = await getCurrentBranch();
|
|
165
|
+
if (current === branch) {
|
|
166
|
+
return resetHard(target);
|
|
167
|
+
}
|
|
168
|
+
return run(["branch", "-f", branch, target]);
|
|
169
|
+
}
|
|
172
170
|
async function pushSetUpstream(remote, branch) {
|
|
173
171
|
return run(["push", "-u", remote, branch]);
|
|
174
172
|
}
|
|
@@ -190,8 +188,16 @@ async function getChangedFiles() {
|
|
|
190
188
|
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
191
189
|
if (exitCode !== 0)
|
|
192
190
|
return [];
|
|
193
|
-
return stdout.
|
|
194
|
-
`).filter(Boolean).map((l) =>
|
|
191
|
+
return stdout.trimEnd().split(`
|
|
192
|
+
`).filter(Boolean).map((l) => {
|
|
193
|
+
const line = l.replace(/\r$/, "");
|
|
194
|
+
const match = line.match(/^..\s+(.*)/);
|
|
195
|
+
if (!match)
|
|
196
|
+
return "";
|
|
197
|
+
const file = match[1];
|
|
198
|
+
const renameIdx = file.indexOf(" -> ");
|
|
199
|
+
return renameIdx !== -1 ? file.slice(renameIdx + 4) : file;
|
|
200
|
+
}).filter(Boolean);
|
|
195
201
|
}
|
|
196
202
|
async function getDivergence(branch, base) {
|
|
197
203
|
const { exitCode, stdout } = await run([
|
|
@@ -218,6 +224,18 @@ async function getMergedBranches(base) {
|
|
|
218
224
|
async function deleteBranch(branch) {
|
|
219
225
|
return run(["branch", "-d", branch]);
|
|
220
226
|
}
|
|
227
|
+
async function forceDeleteBranch(branch) {
|
|
228
|
+
return run(["branch", "-D", branch]);
|
|
229
|
+
}
|
|
230
|
+
async function deleteRemoteBranch(remote, branch) {
|
|
231
|
+
return run(["push", remote, "--delete", branch]);
|
|
232
|
+
}
|
|
233
|
+
async function mergeSquash(branch) {
|
|
234
|
+
return run(["merge", "--squash", branch]);
|
|
235
|
+
}
|
|
236
|
+
async function pushBranch(remote, branch) {
|
|
237
|
+
return run(["push", remote, branch]);
|
|
238
|
+
}
|
|
221
239
|
async function pruneRemote(remote) {
|
|
222
240
|
return run(["remote", "prune", remote]);
|
|
223
241
|
}
|
|
@@ -238,6 +256,85 @@ async function getLog(base, head) {
|
|
|
238
256
|
async function pullBranch(remote, branch) {
|
|
239
257
|
return run(["pull", remote, branch]);
|
|
240
258
|
}
|
|
259
|
+
async function stageFiles(files) {
|
|
260
|
+
return run(["add", "--", ...files]);
|
|
261
|
+
}
|
|
262
|
+
async function unstageFiles(files) {
|
|
263
|
+
return run(["reset", "HEAD", "--", ...files]);
|
|
264
|
+
}
|
|
265
|
+
async function stageAll() {
|
|
266
|
+
return run(["add", "-A"]);
|
|
267
|
+
}
|
|
268
|
+
async function getFullDiffForFiles(files) {
|
|
269
|
+
const [unstaged, staged, untracked] = await Promise.all([
|
|
270
|
+
run(["diff", "--", ...files]),
|
|
271
|
+
run(["diff", "--cached", "--", ...files]),
|
|
272
|
+
getUntrackedFiles()
|
|
273
|
+
]);
|
|
274
|
+
const parts = [staged.stdout, unstaged.stdout].filter(Boolean);
|
|
275
|
+
const untrackedSet = new Set(untracked);
|
|
276
|
+
const MAX_FILE_CONTENT = 2000;
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
if (untrackedSet.has(file)) {
|
|
279
|
+
try {
|
|
280
|
+
const content = readFileSync2(join2(process.cwd(), file), "utf-8");
|
|
281
|
+
const truncated = content.length > MAX_FILE_CONTENT ? `${content.slice(0, MAX_FILE_CONTENT)}
|
|
282
|
+
... (truncated)` : content;
|
|
283
|
+
const lines = truncated.split(`
|
|
284
|
+
`).map((l) => `+${l}`);
|
|
285
|
+
parts.push(`diff --git a/${file} b/${file}
|
|
286
|
+
new file
|
|
287
|
+
--- /dev/null
|
|
288
|
+
+++ b/${file}
|
|
289
|
+
${lines.join(`
|
|
290
|
+
`)}`);
|
|
291
|
+
} catch {}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return parts.join(`
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
async function getUntrackedFiles() {
|
|
298
|
+
const { exitCode, stdout } = await run(["ls-files", "--others", "--exclude-standard"]);
|
|
299
|
+
if (exitCode !== 0)
|
|
300
|
+
return [];
|
|
301
|
+
return stdout.trim().split(`
|
|
302
|
+
`).filter(Boolean);
|
|
303
|
+
}
|
|
304
|
+
async function getFileStatus() {
|
|
305
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
306
|
+
if (exitCode !== 0)
|
|
307
|
+
return { staged: [], modified: [], untracked: [] };
|
|
308
|
+
const result = { staged: [], modified: [], untracked: [] };
|
|
309
|
+
const STATUS_LABELS = {
|
|
310
|
+
A: "new file",
|
|
311
|
+
M: "modified",
|
|
312
|
+
D: "deleted",
|
|
313
|
+
R: "renamed",
|
|
314
|
+
C: "copied",
|
|
315
|
+
T: "type changed"
|
|
316
|
+
};
|
|
317
|
+
for (const raw of stdout.trimEnd().split(`
|
|
318
|
+
`).filter(Boolean)) {
|
|
319
|
+
const line = raw.replace(/\r$/, "");
|
|
320
|
+
const indexStatus = line[0];
|
|
321
|
+
const workTreeStatus = line[1];
|
|
322
|
+
const pathPart = line.slice(3);
|
|
323
|
+
const renameIdx = pathPart.indexOf(" -> ");
|
|
324
|
+
const file = renameIdx !== -1 ? pathPart.slice(renameIdx + 4) : pathPart;
|
|
325
|
+
if (indexStatus === "?" && workTreeStatus === "?") {
|
|
326
|
+
result.untracked.push(file);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (indexStatus && indexStatus !== " " && indexStatus !== "?") {
|
|
330
|
+
result.staged.push({ file, status: STATUS_LABELS[indexStatus] ?? indexStatus });
|
|
331
|
+
}
|
|
332
|
+
if (workTreeStatus && workTreeStatus !== " " && workTreeStatus !== "?") {
|
|
333
|
+
result.modified.push({ file, status: STATUS_LABELS[workTreeStatus] ?? workTreeStatus });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
241
338
|
|
|
242
339
|
// src/utils/logger.ts
|
|
243
340
|
import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
|
|
@@ -380,7 +477,7 @@ ${pc3.bold("Branches to delete:")}`);
|
|
|
380
477
|
|
|
381
478
|
// src/commands/commit.ts
|
|
382
479
|
import { defineCommand as defineCommand2 } from "citty";
|
|
383
|
-
import
|
|
480
|
+
import pc5 from "picocolors";
|
|
384
481
|
|
|
385
482
|
// src/utils/convention.ts
|
|
386
483
|
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;
|
|
@@ -427,96 +524,98 @@ function getValidationError(convention) {
|
|
|
427
524
|
|
|
428
525
|
// src/utils/copilot.ts
|
|
429
526
|
import { CopilotClient } from "@github/copilot-sdk";
|
|
430
|
-
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
docs
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
ci
|
|
443
|
-
|
|
444
|
-
|
|
527
|
+
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
|
|
528
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
529
|
+
Rules: breaking (!) only for feat/fix/refactor/perf; imperative mood; max 72 chars; lowercase start; scope optional camelCase/kebab-case. Return ONLY the message line.
|
|
530
|
+
Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
|
|
531
|
+
var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
532
|
+
Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
|
|
533
|
+
Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
|
|
534
|
+
Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
|
|
535
|
+
Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
|
|
536
|
+
WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
|
|
537
|
+
function getGroupingSystemPrompt(convention) {
|
|
538
|
+
const conventionBlock = convention === "conventional" ? `Use Conventional Commit format: <type>[(<scope>)]: <description>
|
|
539
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert` : `Use Clean Commit format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
540
|
+
Emoji/type table:
|
|
541
|
+
\uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release`;
|
|
542
|
+
return `You are a smart commit grouping assistant. Given a list of changed files and their diffs, group related changes into logical atomic commits.
|
|
445
543
|
|
|
446
|
-
|
|
447
|
-
- Breaking change (!) only for: feat, fix, refactor, perf
|
|
448
|
-
- Description: concise, imperative mood, max 72 chars, lowercase start
|
|
449
|
-
- Scope: optional, camelCase or kebab-case component name
|
|
450
|
-
- Return ONLY the commit message line, nothing else
|
|
544
|
+
${conventionBlock}
|
|
451
545
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
Emoji and type table:
|
|
461
|
-
\uD83D\uDCE6 new – new features, files, or capabilities
|
|
462
|
-
\uD83D\uDD27 update – changes, refactoring, improvements
|
|
463
|
-
\uD83D\uDDD1️ remove – removing code, files, or dependencies
|
|
464
|
-
\uD83D\uDD12 security – security fixes or patches
|
|
465
|
-
⚙️ setup – configs, CI/CD, tooling, build systems
|
|
466
|
-
☕ chore – maintenance, dependency updates
|
|
467
|
-
\uD83E\uDDEA test – adding or updating tests
|
|
468
|
-
\uD83D\uDCD6 docs – documentation changes
|
|
469
|
-
\uD83D\uDE80 release – version releases
|
|
546
|
+
Return a JSON array of commit groups with this EXACT structure (no markdown fences, no explanation):
|
|
547
|
+
[
|
|
548
|
+
{
|
|
549
|
+
"files": ["path/to/file1.ts", "path/to/file2.ts"],
|
|
550
|
+
"message": "<commit message following the convention above>"
|
|
551
|
+
}
|
|
552
|
+
]
|
|
470
553
|
|
|
471
554
|
Rules:
|
|
472
|
-
-
|
|
473
|
-
-
|
|
474
|
-
-
|
|
475
|
-
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
⚙️ setup (ci): configure github actions workflow
|
|
481
|
-
\uD83D\uDCE6 new!: completely redesign authentication system`;
|
|
482
|
-
var BRANCH_NAME_SYSTEM_PROMPT = `You are a git branch name generator. Convert natural language descriptions into proper git branch names.
|
|
483
|
-
|
|
484
|
-
Format: <prefix>/<kebab-case-name>
|
|
555
|
+
- Group files that are logically related (e.g. a utility and its tests, a feature and its types)
|
|
556
|
+
- Each group should represent ONE logical change
|
|
557
|
+
- Every file must appear in exactly one group
|
|
558
|
+
- Commit messages must follow the convention, be concise, imperative, max 72 chars
|
|
559
|
+
- Order groups so foundational changes come first (types, utils) and consumers come after
|
|
560
|
+
- Return ONLY the JSON array, nothing else`;
|
|
561
|
+
}
|
|
562
|
+
var BRANCH_NAME_SYSTEM_PROMPT = `Git branch name generator. Format: <prefix>/<kebab-case-name>
|
|
485
563
|
Prefixes: feature, fix, docs, chore, test, refactor
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
564
|
+
Rules: lowercase kebab-case, 2-5 words max. Return ONLY the branch name.
|
|
565
|
+
Examples: fix/login-timeout | feature/user-profile-page | docs/update-readme`;
|
|
566
|
+
var PR_DESCRIPTION_SYSTEM_PROMPT_BASE = `GitHub PR description generator. Return JSON: {"title":"<72 chars>","body":"## Summary\\n...\\n\\n## Changes\\n- ...\\n\\n## Test Plan\\n..."}`;
|
|
567
|
+
function getPRDescriptionSystemPrompt(convention) {
|
|
568
|
+
if (convention === "clean-commit") {
|
|
569
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
570
|
+
CRITICAL: The PR title MUST follow the Clean Commit format exactly: <emoji> <type>: <description>
|
|
571
|
+
Emoji/type table: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
|
|
572
|
+
Title examples: \uD83D\uDCE6 new: add user authentication | \uD83D\uDD27 update: improve error handling | \uD83D\uDDD1️ remove: drop legacy API
|
|
573
|
+
Rules: title follows convention, present tense, max 72 chars; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
574
|
+
}
|
|
575
|
+
if (convention === "conventional") {
|
|
576
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
577
|
+
CRITICAL: The PR title MUST follow Conventional Commits format: <type>[(<scope>)]: <description>
|
|
578
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
579
|
+
Title examples: feat: add user authentication | fix(auth): resolve token expiry | docs: update contributing guide
|
|
580
|
+
Rules: title follows convention, present tense, max 72 chars; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
581
|
+
}
|
|
582
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
583
|
+
Rules: title concise present tense; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
584
|
+
}
|
|
585
|
+
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `Git merge conflict advisor. Explain each side, suggest resolution strategy. Never auto-resolve — guidance only. Be concise and actionable.`;
|
|
586
|
+
function suppressSubprocessWarnings() {
|
|
587
|
+
process.env.NODE_NO_WARNINGS = "1";
|
|
588
|
+
}
|
|
589
|
+
function withTimeout(promise, ms) {
|
|
590
|
+
return new Promise((resolve, reject) => {
|
|
591
|
+
const timer = setTimeout(() => reject(new Error(`Copilot request timed out after ${ms / 1000}s`)), ms);
|
|
592
|
+
promise.then((val) => {
|
|
593
|
+
clearTimeout(timer);
|
|
594
|
+
resolve(val);
|
|
595
|
+
}, (err) => {
|
|
596
|
+
clearTimeout(timer);
|
|
597
|
+
reject(err);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
502
600
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
- title: concise, present tense, describes what the PR does
|
|
506
|
-
- body: markdown with Summary, Changes (bullet list), and Test Plan sections
|
|
507
|
-
- Return ONLY the JSON object, no markdown fences, no extra text`;
|
|
508
|
-
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `You are a git merge conflict resolution advisor. Analyze the conflict markers and provide guidance.
|
|
509
|
-
|
|
510
|
-
Rules:
|
|
511
|
-
- Explain what each side of the conflict contains
|
|
512
|
-
- Suggest the most likely correct resolution strategy
|
|
513
|
-
- Never auto-resolve — provide guidance only
|
|
514
|
-
- Be concise and actionable`;
|
|
601
|
+
var COPILOT_TIMEOUT_MS = 30000;
|
|
602
|
+
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
515
603
|
async function checkCopilotAvailable() {
|
|
516
|
-
let client = null;
|
|
517
604
|
try {
|
|
518
|
-
client =
|
|
519
|
-
|
|
605
|
+
const client = await getManagedClient();
|
|
606
|
+
try {
|
|
607
|
+
await client.ping();
|
|
608
|
+
} catch (err) {
|
|
609
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
610
|
+
if (msg.includes("auth") || msg.includes("token") || msg.includes("401") || msg.includes("403")) {
|
|
611
|
+
return "Copilot authentication failed. Run `gh auth login` to refresh your token.";
|
|
612
|
+
}
|
|
613
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("timeout") || msg.includes("network")) {
|
|
614
|
+
return "Could not reach GitHub Copilot service. Check your internet connection.";
|
|
615
|
+
}
|
|
616
|
+
return `Copilot health check failed: ${msg}`;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
520
619
|
} catch (err) {
|
|
521
620
|
const msg = err instanceof Error ? err.message : String(err);
|
|
522
621
|
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
@@ -524,44 +623,45 @@ async function checkCopilotAvailable() {
|
|
|
524
623
|
}
|
|
525
624
|
return `Failed to start Copilot service: ${msg}`;
|
|
526
625
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
626
|
+
}
|
|
627
|
+
var _managedClient = null;
|
|
628
|
+
var _clientStarted = false;
|
|
629
|
+
async function getManagedClient() {
|
|
630
|
+
if (!_managedClient || !_clientStarted) {
|
|
631
|
+
suppressSubprocessWarnings();
|
|
632
|
+
_managedClient = new CopilotClient;
|
|
633
|
+
await _managedClient.start();
|
|
634
|
+
_clientStarted = true;
|
|
635
|
+
const cleanup = () => {
|
|
636
|
+
if (_managedClient && _clientStarted) {
|
|
637
|
+
try {
|
|
638
|
+
_managedClient.stop();
|
|
639
|
+
} catch {}
|
|
640
|
+
_clientStarted = false;
|
|
641
|
+
_managedClient = null;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
process.once("exit", cleanup);
|
|
645
|
+
process.once("SIGINT", cleanup);
|
|
646
|
+
process.once("SIGTERM", cleanup);
|
|
542
647
|
}
|
|
543
|
-
return
|
|
648
|
+
return _managedClient;
|
|
544
649
|
}
|
|
545
|
-
async function callCopilot(systemMessage, userMessage, model) {
|
|
546
|
-
const client =
|
|
547
|
-
|
|
650
|
+
async function callCopilot(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
|
|
651
|
+
const client = await getManagedClient();
|
|
652
|
+
const sessionConfig = {
|
|
653
|
+
systemMessage: { mode: "replace", content: systemMessage }
|
|
654
|
+
};
|
|
655
|
+
if (model)
|
|
656
|
+
sessionConfig.model = model;
|
|
657
|
+
const session = await client.createSession(sessionConfig);
|
|
548
658
|
try {
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
sessionConfig.model = model;
|
|
554
|
-
const session = await client.createSession(sessionConfig);
|
|
555
|
-
try {
|
|
556
|
-
const response = await session.sendAndWait({ content: userMessage });
|
|
557
|
-
if (!response?.data?.content)
|
|
558
|
-
return null;
|
|
559
|
-
return response.data.content;
|
|
560
|
-
} finally {
|
|
561
|
-
await session.destroy();
|
|
562
|
-
}
|
|
659
|
+
const response = await withTimeout(session.sendAndWait({ prompt: userMessage }), timeoutMs);
|
|
660
|
+
if (!response?.data?.content)
|
|
661
|
+
return null;
|
|
662
|
+
return response.data.content;
|
|
563
663
|
} finally {
|
|
564
|
-
await
|
|
664
|
+
await session.destroy();
|
|
565
665
|
}
|
|
566
666
|
}
|
|
567
667
|
function getCommitSystemPrompt(convention) {
|
|
@@ -569,21 +669,53 @@ function getCommitSystemPrompt(convention) {
|
|
|
569
669
|
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
570
670
|
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
571
671
|
}
|
|
672
|
+
function extractJson(raw) {
|
|
673
|
+
let text2 = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
674
|
+
if (text2.startsWith("[") || text2.startsWith("{"))
|
|
675
|
+
return text2;
|
|
676
|
+
const arrayStart = text2.indexOf("[");
|
|
677
|
+
const objStart = text2.indexOf("{");
|
|
678
|
+
let start;
|
|
679
|
+
let closeChar;
|
|
680
|
+
if (arrayStart === -1 && objStart === -1)
|
|
681
|
+
return text2;
|
|
682
|
+
if (arrayStart === -1) {
|
|
683
|
+
start = objStart;
|
|
684
|
+
closeChar = "}";
|
|
685
|
+
} else if (objStart === -1) {
|
|
686
|
+
start = arrayStart;
|
|
687
|
+
closeChar = "]";
|
|
688
|
+
} else if (arrayStart < objStart) {
|
|
689
|
+
start = arrayStart;
|
|
690
|
+
closeChar = "]";
|
|
691
|
+
} else {
|
|
692
|
+
start = objStart;
|
|
693
|
+
closeChar = "}";
|
|
694
|
+
}
|
|
695
|
+
const end = text2.lastIndexOf(closeChar);
|
|
696
|
+
if (end > start) {
|
|
697
|
+
text2 = text2.slice(start, end + 1);
|
|
698
|
+
}
|
|
699
|
+
return text2;
|
|
700
|
+
}
|
|
572
701
|
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
573
702
|
try {
|
|
703
|
+
const multiFileHint = stagedFiles.length > 1 ? `
|
|
704
|
+
|
|
705
|
+
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.` : "";
|
|
574
706
|
const userMessage = `Generate a commit message for these staged changes:
|
|
575
707
|
|
|
576
708
|
Files: ${stagedFiles.join(", ")}
|
|
577
709
|
|
|
578
710
|
Diff:
|
|
579
|
-
${diff.slice(0, 4000)}`;
|
|
711
|
+
${diff.slice(0, 4000)}${multiFileHint}`;
|
|
580
712
|
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
581
713
|
return result?.trim() ?? null;
|
|
582
714
|
} catch {
|
|
583
715
|
return null;
|
|
584
716
|
}
|
|
585
717
|
}
|
|
586
|
-
async function generatePRDescription(commits, diff, model) {
|
|
718
|
+
async function generatePRDescription(commits, diff, model, convention = "clean-commit") {
|
|
587
719
|
try {
|
|
588
720
|
const userMessage = `Generate a PR description for these changes:
|
|
589
721
|
|
|
@@ -593,10 +725,10 @@ ${commits.join(`
|
|
|
593
725
|
|
|
594
726
|
Diff (truncated):
|
|
595
727
|
${diff.slice(0, 4000)}`;
|
|
596
|
-
const result = await callCopilot(
|
|
728
|
+
const result = await callCopilot(getPRDescriptionSystemPrompt(convention), userMessage, model);
|
|
597
729
|
if (!result)
|
|
598
730
|
return null;
|
|
599
|
-
const cleaned = result
|
|
731
|
+
const cleaned = extractJson(result);
|
|
600
732
|
return JSON.parse(cleaned);
|
|
601
733
|
} catch {
|
|
602
734
|
return null;
|
|
@@ -621,6 +753,124 @@ ${conflictDiff.slice(0, 4000)}`;
|
|
|
621
753
|
return null;
|
|
622
754
|
}
|
|
623
755
|
}
|
|
756
|
+
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
757
|
+
const userMessage = `Group these changed files into logical atomic commits:
|
|
758
|
+
|
|
759
|
+
Files:
|
|
760
|
+
${files.join(`
|
|
761
|
+
`)}
|
|
762
|
+
|
|
763
|
+
Diffs (truncated):
|
|
764
|
+
${diffs.slice(0, 6000)}`;
|
|
765
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
766
|
+
if (!result) {
|
|
767
|
+
throw new Error("AI returned an empty response");
|
|
768
|
+
}
|
|
769
|
+
const cleaned = extractJson(result);
|
|
770
|
+
let parsed;
|
|
771
|
+
try {
|
|
772
|
+
parsed = JSON.parse(cleaned);
|
|
773
|
+
} catch {
|
|
774
|
+
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
775
|
+
}
|
|
776
|
+
const groups = parsed;
|
|
777
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
778
|
+
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
779
|
+
}
|
|
780
|
+
for (const group of groups) {
|
|
781
|
+
if (!Array.isArray(group.files) || typeof group.message !== "string") {
|
|
782
|
+
throw new Error("AI returned groups with invalid structure (missing files or message)");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return groups;
|
|
786
|
+
}
|
|
787
|
+
async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
|
|
788
|
+
const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
|
|
789
|
+
`);
|
|
790
|
+
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
791
|
+
|
|
792
|
+
Groups:
|
|
793
|
+
${groupSummary}
|
|
794
|
+
|
|
795
|
+
Diffs (truncated):
|
|
796
|
+
${diffs.slice(0, 6000)}`;
|
|
797
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
798
|
+
if (!result)
|
|
799
|
+
return groups;
|
|
800
|
+
try {
|
|
801
|
+
const cleaned = extractJson(result);
|
|
802
|
+
const parsed = JSON.parse(cleaned);
|
|
803
|
+
if (!Array.isArray(parsed) || parsed.length !== groups.length)
|
|
804
|
+
return groups;
|
|
805
|
+
return groups.map((g, i) => ({
|
|
806
|
+
files: g.files,
|
|
807
|
+
message: typeof parsed[i]?.message === "string" ? parsed[i].message : g.message
|
|
808
|
+
}));
|
|
809
|
+
} catch {
|
|
810
|
+
return groups;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
|
|
814
|
+
try {
|
|
815
|
+
const userMessage = `Generate a single commit message for these files:
|
|
816
|
+
|
|
817
|
+
Files: ${files.join(", ")}
|
|
818
|
+
|
|
819
|
+
Diff:
|
|
820
|
+
${diffs.slice(0, 4000)}`;
|
|
821
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
822
|
+
return result?.trim() ?? null;
|
|
823
|
+
} catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/utils/spinner.ts
|
|
829
|
+
import pc4 from "picocolors";
|
|
830
|
+
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
831
|
+
function createSpinner(text2) {
|
|
832
|
+
let frameIdx = 0;
|
|
833
|
+
let currentText = text2;
|
|
834
|
+
let stopped = false;
|
|
835
|
+
const clearLine = () => {
|
|
836
|
+
process.stderr.write("\r\x1B[K");
|
|
837
|
+
};
|
|
838
|
+
const render = () => {
|
|
839
|
+
if (stopped)
|
|
840
|
+
return;
|
|
841
|
+
const frame = pc4.cyan(FRAMES[frameIdx % FRAMES.length]);
|
|
842
|
+
clearLine();
|
|
843
|
+
process.stderr.write(`${frame} ${currentText}`);
|
|
844
|
+
frameIdx++;
|
|
845
|
+
};
|
|
846
|
+
const timer = setInterval(render, 80);
|
|
847
|
+
render();
|
|
848
|
+
const stop = () => {
|
|
849
|
+
if (stopped)
|
|
850
|
+
return;
|
|
851
|
+
stopped = true;
|
|
852
|
+
clearInterval(timer);
|
|
853
|
+
clearLine();
|
|
854
|
+
};
|
|
855
|
+
return {
|
|
856
|
+
update(newText) {
|
|
857
|
+
currentText = newText;
|
|
858
|
+
},
|
|
859
|
+
success(msg) {
|
|
860
|
+
stop();
|
|
861
|
+
process.stderr.write(`${pc4.green("✔")} ${msg}
|
|
862
|
+
`);
|
|
863
|
+
},
|
|
864
|
+
fail(msg) {
|
|
865
|
+
stop();
|
|
866
|
+
process.stderr.write(`${pc4.red("✖")} ${msg}
|
|
867
|
+
`);
|
|
868
|
+
},
|
|
869
|
+
stop() {
|
|
870
|
+
stop();
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
624
874
|
|
|
625
875
|
// src/commands/commit.ts
|
|
626
876
|
var commit_default = defineCommand2({
|
|
@@ -637,6 +887,11 @@ var commit_default = defineCommand2({
|
|
|
637
887
|
type: "boolean",
|
|
638
888
|
description: "Skip AI and write commit message manually",
|
|
639
889
|
default: false
|
|
890
|
+
},
|
|
891
|
+
group: {
|
|
892
|
+
type: "boolean",
|
|
893
|
+
description: "AI groups related changes into separate atomic commits",
|
|
894
|
+
default: false
|
|
640
895
|
}
|
|
641
896
|
},
|
|
642
897
|
async run({ args }) {
|
|
@@ -650,7 +905,11 @@ var commit_default = defineCommand2({
|
|
|
650
905
|
process.exit(1);
|
|
651
906
|
}
|
|
652
907
|
heading("\uD83D\uDCBE contrib commit");
|
|
653
|
-
|
|
908
|
+
if (args.group) {
|
|
909
|
+
await runGroupCommit(args.model, config);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
let stagedFiles = await getStagedFiles();
|
|
654
913
|
if (stagedFiles.length === 0) {
|
|
655
914
|
const changedFiles = await getChangedFiles();
|
|
656
915
|
if (changedFiles.length === 0) {
|
|
@@ -658,31 +917,62 @@ var commit_default = defineCommand2({
|
|
|
658
917
|
process.exit(1);
|
|
659
918
|
}
|
|
660
919
|
console.log(`
|
|
661
|
-
${
|
|
920
|
+
${pc5.bold("Changed files:")}`);
|
|
662
921
|
for (const f of changedFiles) {
|
|
663
|
-
console.log(` ${
|
|
922
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
923
|
+
}
|
|
924
|
+
const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
|
|
925
|
+
"Stage all changes",
|
|
926
|
+
"Select files to stage",
|
|
927
|
+
"Cancel"
|
|
928
|
+
]);
|
|
929
|
+
if (stageAction === "Cancel") {
|
|
930
|
+
process.exit(0);
|
|
931
|
+
}
|
|
932
|
+
if (stageAction === "Stage all changes") {
|
|
933
|
+
const result2 = await stageAll();
|
|
934
|
+
if (result2.exitCode !== 0) {
|
|
935
|
+
error(`Failed to stage files: ${result2.stderr}`);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
success("Staged all changes.");
|
|
939
|
+
} else {
|
|
940
|
+
const selected = await multiSelectPrompt("Select files to stage:", changedFiles);
|
|
941
|
+
if (selected.length === 0) {
|
|
942
|
+
error("No files selected.");
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
const result2 = await stageFiles(selected);
|
|
946
|
+
if (result2.exitCode !== 0) {
|
|
947
|
+
error(`Failed to stage files: ${result2.stderr}`);
|
|
948
|
+
process.exit(1);
|
|
949
|
+
}
|
|
950
|
+
success(`Staged ${selected.length} file(s).`);
|
|
951
|
+
}
|
|
952
|
+
stagedFiles = await getStagedFiles();
|
|
953
|
+
if (stagedFiles.length === 0) {
|
|
954
|
+
error("No staged changes after staging attempt.");
|
|
955
|
+
process.exit(1);
|
|
664
956
|
}
|
|
665
|
-
console.log();
|
|
666
|
-
warn("No staged changes. Stage your files with `git add` and re-run.");
|
|
667
|
-
process.exit(1);
|
|
668
957
|
}
|
|
669
958
|
info(`Staged files: ${stagedFiles.join(", ")}`);
|
|
670
959
|
let commitMessage = null;
|
|
671
960
|
const useAI = !args["no-ai"];
|
|
672
961
|
if (useAI) {
|
|
673
|
-
const copilotError = await checkCopilotAvailable();
|
|
962
|
+
const [copilotError, diff] = await Promise.all([checkCopilotAvailable(), getStagedDiff()]);
|
|
674
963
|
if (copilotError) {
|
|
675
964
|
warn(`AI unavailable: ${copilotError}`);
|
|
676
965
|
warn("Falling back to manual commit message entry.");
|
|
677
966
|
} else {
|
|
678
|
-
|
|
679
|
-
const diff = await getStagedDiff();
|
|
967
|
+
const spinner = createSpinner("Generating commit message with AI...");
|
|
680
968
|
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
681
969
|
if (commitMessage) {
|
|
970
|
+
spinner.success("AI commit message generated.");
|
|
682
971
|
console.log(`
|
|
683
|
-
${
|
|
972
|
+
${pc5.dim("AI suggestion:")} ${pc5.bold(pc5.cyan(commitMessage))}`);
|
|
684
973
|
} else {
|
|
685
|
-
|
|
974
|
+
spinner.fail("AI did not return a commit message.");
|
|
975
|
+
warn("Falling back to manual entry.");
|
|
686
976
|
}
|
|
687
977
|
}
|
|
688
978
|
}
|
|
@@ -699,16 +989,17 @@ ${pc4.bold("Changed files:")}`);
|
|
|
699
989
|
} else if (action === "Edit this message") {
|
|
700
990
|
finalMessage = await inputPrompt("Edit commit message", commitMessage);
|
|
701
991
|
} else if (action === "Regenerate") {
|
|
702
|
-
|
|
992
|
+
const spinner = createSpinner("Regenerating commit message...");
|
|
703
993
|
const diff = await getStagedDiff();
|
|
704
994
|
const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
705
995
|
if (regen) {
|
|
996
|
+
spinner.success("Commit message regenerated.");
|
|
706
997
|
console.log(`
|
|
707
|
-
${
|
|
998
|
+
${pc5.dim("AI suggestion:")} ${pc5.bold(pc5.cyan(regen))}`);
|
|
708
999
|
const ok = await confirmPrompt("Use this message?");
|
|
709
1000
|
finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
|
|
710
1001
|
} else {
|
|
711
|
-
|
|
1002
|
+
spinner.fail("Regeneration failed.");
|
|
712
1003
|
finalMessage = await inputPrompt("Enter commit message");
|
|
713
1004
|
}
|
|
714
1005
|
} else {
|
|
@@ -719,7 +1010,7 @@ ${pc4.bold("Changed files:")}`);
|
|
|
719
1010
|
if (convention2 !== "none") {
|
|
720
1011
|
console.log();
|
|
721
1012
|
for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
|
|
722
|
-
console.log(
|
|
1013
|
+
console.log(pc5.dim(hint));
|
|
723
1014
|
}
|
|
724
1015
|
console.log();
|
|
725
1016
|
}
|
|
@@ -743,21 +1034,209 @@ ${pc4.bold("Changed files:")}`);
|
|
|
743
1034
|
error(`Failed to commit: ${result.stderr}`);
|
|
744
1035
|
process.exit(1);
|
|
745
1036
|
}
|
|
746
|
-
success(`✅ Committed: ${
|
|
1037
|
+
success(`✅ Committed: ${pc5.bold(finalMessage)}`);
|
|
747
1038
|
}
|
|
748
1039
|
});
|
|
1040
|
+
async function runGroupCommit(model, config) {
|
|
1041
|
+
const [copilotError, changedFiles] = await Promise.all([
|
|
1042
|
+
checkCopilotAvailable(),
|
|
1043
|
+
getChangedFiles()
|
|
1044
|
+
]);
|
|
1045
|
+
if (copilotError) {
|
|
1046
|
+
error(`AI is required for --group mode but unavailable: ${copilotError}`);
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
if (changedFiles.length === 0) {
|
|
1050
|
+
error("No changes to group-commit.");
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
console.log(`
|
|
1054
|
+
${pc5.bold("Changed files:")}`);
|
|
1055
|
+
for (const f of changedFiles) {
|
|
1056
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
1057
|
+
}
|
|
1058
|
+
const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
1059
|
+
const diffs = await getFullDiffForFiles(changedFiles);
|
|
1060
|
+
if (!diffs.trim()) {
|
|
1061
|
+
spinner.stop();
|
|
1062
|
+
warn("Could not retrieve diff context for any files. AI needs diffs to produce groups.");
|
|
1063
|
+
}
|
|
1064
|
+
let groups;
|
|
1065
|
+
try {
|
|
1066
|
+
groups = await generateCommitGroups(changedFiles, diffs, model, config.commitConvention);
|
|
1067
|
+
spinner.success(`AI generated ${groups.length} commit group(s).`);
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1070
|
+
spinner.fail(`AI grouping failed: ${reason}`);
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
if (groups.length === 0) {
|
|
1074
|
+
error("AI could not produce commit groups. Try committing files manually.");
|
|
1075
|
+
process.exit(1);
|
|
1076
|
+
}
|
|
1077
|
+
const changedSet = new Set(changedFiles);
|
|
1078
|
+
for (const group of groups) {
|
|
1079
|
+
const invalid = group.files.filter((f) => !changedSet.has(f));
|
|
1080
|
+
if (invalid.length > 0) {
|
|
1081
|
+
warn(`AI suggested unknown file(s): ${invalid.join(", ")} — removed from group.`);
|
|
1082
|
+
}
|
|
1083
|
+
group.files = group.files.filter((f) => changedSet.has(f));
|
|
1084
|
+
}
|
|
1085
|
+
let validGroups = groups.filter((g) => g.files.length > 0);
|
|
1086
|
+
if (validGroups.length === 0) {
|
|
1087
|
+
error("No valid groups remain after validation. Try committing files manually.");
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
let proceedToCommit = false;
|
|
1091
|
+
let commitAll = false;
|
|
1092
|
+
while (!proceedToCommit) {
|
|
1093
|
+
console.log(`
|
|
1094
|
+
${pc5.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
1095
|
+
`);
|
|
1096
|
+
for (let i = 0;i < validGroups.length; i++) {
|
|
1097
|
+
const g = validGroups[i];
|
|
1098
|
+
console.log(` ${pc5.cyan(`Group ${i + 1}:`)} ${pc5.bold(g.message)}`);
|
|
1099
|
+
for (const f of g.files) {
|
|
1100
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
1101
|
+
}
|
|
1102
|
+
console.log();
|
|
1103
|
+
}
|
|
1104
|
+
const summaryAction = await selectPrompt("What would you like to do?", [
|
|
1105
|
+
"Commit all",
|
|
1106
|
+
"Review each group",
|
|
1107
|
+
"Regenerate all messages",
|
|
1108
|
+
"Cancel"
|
|
1109
|
+
]);
|
|
1110
|
+
if (summaryAction === "Cancel") {
|
|
1111
|
+
warn("Group commit cancelled.");
|
|
1112
|
+
process.exit(0);
|
|
1113
|
+
}
|
|
1114
|
+
if (summaryAction === "Regenerate all messages") {
|
|
1115
|
+
const regenSpinner = createSpinner("Regenerating all commit messages...");
|
|
1116
|
+
try {
|
|
1117
|
+
validGroups = await regenerateAllGroupMessages(validGroups, diffs, model, config.commitConvention);
|
|
1118
|
+
regenSpinner.success("All commit messages regenerated.");
|
|
1119
|
+
} catch {
|
|
1120
|
+
regenSpinner.fail("Failed to regenerate messages. Keeping current ones.");
|
|
1121
|
+
}
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
proceedToCommit = true;
|
|
1125
|
+
commitAll = summaryAction === "Commit all";
|
|
1126
|
+
}
|
|
1127
|
+
let committed = 0;
|
|
1128
|
+
if (commitAll) {
|
|
1129
|
+
for (let i = 0;i < validGroups.length; i++) {
|
|
1130
|
+
const group = validGroups[i];
|
|
1131
|
+
const stageResult = await stageFiles(group.files);
|
|
1132
|
+
if (stageResult.exitCode !== 0) {
|
|
1133
|
+
error(`Failed to stage group ${i + 1}: ${stageResult.stderr}`);
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const commitResult = await commitWithMessage(group.message);
|
|
1137
|
+
if (commitResult.exitCode !== 0) {
|
|
1138
|
+
const detail = (commitResult.stderr || commitResult.stdout).trim();
|
|
1139
|
+
error(`Failed to commit group ${i + 1}: ${detail}`);
|
|
1140
|
+
await unstageFiles(group.files);
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
committed++;
|
|
1144
|
+
success(`✅ Committed group ${i + 1}: ${pc5.bold(group.message)}`);
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
for (let i = 0;i < validGroups.length; i++) {
|
|
1148
|
+
const group = validGroups[i];
|
|
1149
|
+
console.log(pc5.bold(`
|
|
1150
|
+
── Group ${i + 1}/${validGroups.length} ──`));
|
|
1151
|
+
console.log(` ${pc5.cyan(group.message)}`);
|
|
1152
|
+
for (const f of group.files) {
|
|
1153
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
1154
|
+
}
|
|
1155
|
+
let message = group.message;
|
|
1156
|
+
let actionDone = false;
|
|
1157
|
+
while (!actionDone) {
|
|
1158
|
+
const action = await selectPrompt("Action for this group:", [
|
|
1159
|
+
"Commit as-is",
|
|
1160
|
+
"Edit message and commit",
|
|
1161
|
+
"Regenerate message",
|
|
1162
|
+
"Skip this group"
|
|
1163
|
+
]);
|
|
1164
|
+
if (action === "Skip this group") {
|
|
1165
|
+
warn(`Skipped group ${i + 1}.`);
|
|
1166
|
+
actionDone = true;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
if (action === "Regenerate message") {
|
|
1170
|
+
const regenSpinner = createSpinner("Regenerating commit message for this group...");
|
|
1171
|
+
const newMsg = await regenerateGroupMessage(group.files, diffs, model, config.commitConvention);
|
|
1172
|
+
if (newMsg) {
|
|
1173
|
+
message = newMsg;
|
|
1174
|
+
group.message = newMsg;
|
|
1175
|
+
regenSpinner.success(`New message: ${pc5.bold(message)}`);
|
|
1176
|
+
} else {
|
|
1177
|
+
regenSpinner.fail("AI could not generate a new message. Keeping current one.");
|
|
1178
|
+
}
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
if (action === "Edit message and commit") {
|
|
1182
|
+
message = await inputPrompt("Edit commit message", message);
|
|
1183
|
+
if (!message) {
|
|
1184
|
+
warn(`Skipped group ${i + 1} (empty message).`);
|
|
1185
|
+
actionDone = true;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (!validateCommitMessage(message, config.commitConvention)) {
|
|
1190
|
+
for (const line of getValidationError(config.commitConvention)) {
|
|
1191
|
+
warn(line);
|
|
1192
|
+
}
|
|
1193
|
+
const proceed = await confirmPrompt("Commit anyway?");
|
|
1194
|
+
if (!proceed) {
|
|
1195
|
+
warn(`Skipped group ${i + 1}.`);
|
|
1196
|
+
actionDone = true;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const stageResult = await stageFiles(group.files);
|
|
1201
|
+
if (stageResult.exitCode !== 0) {
|
|
1202
|
+
error(`Failed to stage group ${i + 1}: ${stageResult.stderr}`);
|
|
1203
|
+
actionDone = true;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const commitResult = await commitWithMessage(message);
|
|
1207
|
+
if (commitResult.exitCode !== 0) {
|
|
1208
|
+
const detail = (commitResult.stderr || commitResult.stdout).trim();
|
|
1209
|
+
error(`Failed to commit group ${i + 1}: ${detail}`);
|
|
1210
|
+
await unstageFiles(group.files);
|
|
1211
|
+
actionDone = true;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
committed++;
|
|
1215
|
+
success(`✅ Committed group ${i + 1}: ${pc5.bold(message)}`);
|
|
1216
|
+
actionDone = true;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (committed === 0) {
|
|
1221
|
+
warn("No groups were committed.");
|
|
1222
|
+
} else {
|
|
1223
|
+
success(`
|
|
1224
|
+
\uD83C\uDF89 ${committed} of ${validGroups.length} group(s) committed successfully.`);
|
|
1225
|
+
}
|
|
1226
|
+
process.exit(0);
|
|
1227
|
+
}
|
|
749
1228
|
|
|
750
1229
|
// src/commands/hook.ts
|
|
751
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as
|
|
752
|
-
import { join as
|
|
1230
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1231
|
+
import { join as join3 } from "node:path";
|
|
753
1232
|
import { defineCommand as defineCommand3 } from "citty";
|
|
754
|
-
import
|
|
1233
|
+
import pc6 from "picocolors";
|
|
755
1234
|
var HOOK_MARKER = "# managed by contribute-now";
|
|
756
1235
|
function getHooksDir(cwd = process.cwd()) {
|
|
757
|
-
return
|
|
1236
|
+
return join3(cwd, ".git", "hooks");
|
|
758
1237
|
}
|
|
759
1238
|
function getHookPath(cwd = process.cwd()) {
|
|
760
|
-
return
|
|
1239
|
+
return join3(getHooksDir(cwd), "commit-msg");
|
|
761
1240
|
}
|
|
762
1241
|
function generateHookScript() {
|
|
763
1242
|
return `#!/bin/sh
|
|
@@ -774,8 +1253,19 @@ case "$commit_msg" in
|
|
|
774
1253
|
Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
|
|
775
1254
|
esac
|
|
776
1255
|
|
|
777
|
-
#
|
|
778
|
-
|
|
1256
|
+
# Detect available package runner
|
|
1257
|
+
if command -v contrib >/dev/null 2>&1; then
|
|
1258
|
+
contrib validate "$commit_msg"
|
|
1259
|
+
elif command -v bunx >/dev/null 2>&1; then
|
|
1260
|
+
bunx contrib validate "$commit_msg"
|
|
1261
|
+
elif command -v pnpx >/dev/null 2>&1; then
|
|
1262
|
+
pnpx contrib validate "$commit_msg"
|
|
1263
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
1264
|
+
npx contrib validate "$commit_msg"
|
|
1265
|
+
else
|
|
1266
|
+
echo "Warning: No package runner found. Skipping commit message validation."
|
|
1267
|
+
exit 0
|
|
1268
|
+
fi
|
|
779
1269
|
`;
|
|
780
1270
|
}
|
|
781
1271
|
var hook_default = defineCommand3({
|
|
@@ -822,7 +1312,7 @@ async function installHook() {
|
|
|
822
1312
|
const hookPath = getHookPath();
|
|
823
1313
|
const hooksDir = getHooksDir();
|
|
824
1314
|
if (existsSync2(hookPath)) {
|
|
825
|
-
const existing =
|
|
1315
|
+
const existing = readFileSync3(hookPath, "utf-8");
|
|
826
1316
|
if (!existing.includes(HOOK_MARKER)) {
|
|
827
1317
|
error("A commit-msg hook already exists and was not installed by contribute-now.");
|
|
828
1318
|
warn(`Path: ${hookPath}`);
|
|
@@ -836,8 +1326,8 @@ async function installHook() {
|
|
|
836
1326
|
}
|
|
837
1327
|
writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
|
|
838
1328
|
success(`commit-msg hook installed.`);
|
|
839
|
-
info(`Convention: ${
|
|
840
|
-
info(`Path: ${
|
|
1329
|
+
info(`Convention: ${pc6.bold(CONVENTION_LABELS[config.commitConvention])}`);
|
|
1330
|
+
info(`Path: ${pc6.dim(hookPath)}`);
|
|
841
1331
|
}
|
|
842
1332
|
async function uninstallHook() {
|
|
843
1333
|
heading("\uD83E\uDE9D hook uninstall");
|
|
@@ -846,7 +1336,7 @@ async function uninstallHook() {
|
|
|
846
1336
|
info("No commit-msg hook found. Nothing to uninstall.");
|
|
847
1337
|
return;
|
|
848
1338
|
}
|
|
849
|
-
const content =
|
|
1339
|
+
const content = readFileSync3(hookPath, "utf-8");
|
|
850
1340
|
if (!content.includes(HOOK_MARKER)) {
|
|
851
1341
|
error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
|
|
852
1342
|
process.exit(1);
|
|
@@ -857,7 +1347,7 @@ async function uninstallHook() {
|
|
|
857
1347
|
|
|
858
1348
|
// src/commands/setup.ts
|
|
859
1349
|
import { defineCommand as defineCommand4 } from "citty";
|
|
860
|
-
import
|
|
1350
|
+
import pc7 from "picocolors";
|
|
861
1351
|
|
|
862
1352
|
// src/utils/gh.ts
|
|
863
1353
|
import { execFile as execFileCb2 } from "node:child_process";
|
|
@@ -865,7 +1355,7 @@ function run2(args) {
|
|
|
865
1355
|
return new Promise((resolve) => {
|
|
866
1356
|
execFileCb2("gh", args, (error2, stdout, stderr) => {
|
|
867
1357
|
resolve({
|
|
868
|
-
exitCode: error2 ? error2.code
|
|
1358
|
+
exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
|
|
869
1359
|
stdout: stdout ?? "",
|
|
870
1360
|
stderr: stderr ?? ""
|
|
871
1361
|
});
|
|
@@ -888,7 +1378,10 @@ async function checkGhAuth() {
|
|
|
888
1378
|
return false;
|
|
889
1379
|
}
|
|
890
1380
|
}
|
|
1381
|
+
var SAFE_SLUG = /^[\w.-]+$/;
|
|
891
1382
|
async function checkRepoPermissions(owner, repo) {
|
|
1383
|
+
if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
|
|
1384
|
+
return null;
|
|
892
1385
|
const { exitCode, stdout } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
893
1386
|
if (exitCode !== 0)
|
|
894
1387
|
return null;
|
|
@@ -949,6 +1442,28 @@ async function createPRFill(base, draft) {
|
|
|
949
1442
|
args.push("--draft");
|
|
950
1443
|
return run2(args);
|
|
951
1444
|
}
|
|
1445
|
+
async function getPRForBranch(headBranch) {
|
|
1446
|
+
const { exitCode, stdout } = await run2([
|
|
1447
|
+
"pr",
|
|
1448
|
+
"list",
|
|
1449
|
+
"--head",
|
|
1450
|
+
headBranch,
|
|
1451
|
+
"--state",
|
|
1452
|
+
"open",
|
|
1453
|
+
"--json",
|
|
1454
|
+
"number,url,title,state",
|
|
1455
|
+
"--limit",
|
|
1456
|
+
"1"
|
|
1457
|
+
]);
|
|
1458
|
+
if (exitCode !== 0)
|
|
1459
|
+
return null;
|
|
1460
|
+
try {
|
|
1461
|
+
const prs = JSON.parse(stdout.trim());
|
|
1462
|
+
return prs.length > 0 ? prs[0] : null;
|
|
1463
|
+
} catch {
|
|
1464
|
+
return null;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
952
1467
|
|
|
953
1468
|
// src/utils/remote.ts
|
|
954
1469
|
function parseRepoFromUrl(url) {
|
|
@@ -991,7 +1506,7 @@ var setup_default = defineCommand4({
|
|
|
991
1506
|
workflow = "github-flow";
|
|
992
1507
|
else if (workflowChoice.startsWith("Git Flow"))
|
|
993
1508
|
workflow = "git-flow";
|
|
994
|
-
info(`Workflow: ${
|
|
1509
|
+
info(`Workflow: ${pc7.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
|
|
995
1510
|
const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
|
|
996
1511
|
`${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
|
|
997
1512
|
CONVENTION_DESCRIPTIONS.conventional,
|
|
@@ -1044,8 +1559,8 @@ var setup_default = defineCommand4({
|
|
|
1044
1559
|
detectedRole = roleChoice;
|
|
1045
1560
|
detectionSource = "user selection";
|
|
1046
1561
|
} else {
|
|
1047
|
-
info(`Detected role: ${
|
|
1048
|
-
const confirmed = await confirmPrompt(`Role detected as ${
|
|
1562
|
+
info(`Detected role: ${pc7.bold(detectedRole)} (via ${detectionSource})`);
|
|
1563
|
+
const confirmed = await confirmPrompt(`Role detected as ${pc7.bold(detectedRole)}. Is this correct?`);
|
|
1049
1564
|
if (!confirmed) {
|
|
1050
1565
|
const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
|
|
1051
1566
|
detectedRole = roleChoice;
|
|
@@ -1090,21 +1605,21 @@ var setup_default = defineCommand4({
|
|
|
1090
1605
|
warn(' echo ".contributerc.json" >> .gitignore');
|
|
1091
1606
|
}
|
|
1092
1607
|
console.log();
|
|
1093
|
-
info(`Workflow: ${
|
|
1094
|
-
info(`Convention: ${
|
|
1095
|
-
info(`Role: ${
|
|
1608
|
+
info(`Workflow: ${pc7.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
1609
|
+
info(`Convention: ${pc7.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
|
|
1610
|
+
info(`Role: ${pc7.bold(config.role)}`);
|
|
1096
1611
|
if (config.devBranch) {
|
|
1097
|
-
info(`Main: ${
|
|
1612
|
+
info(`Main: ${pc7.bold(config.mainBranch)} | Dev: ${pc7.bold(config.devBranch)}`);
|
|
1098
1613
|
} else {
|
|
1099
|
-
info(`Main: ${
|
|
1614
|
+
info(`Main: ${pc7.bold(config.mainBranch)}`);
|
|
1100
1615
|
}
|
|
1101
|
-
info(`Origin: ${
|
|
1616
|
+
info(`Origin: ${pc7.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc7.bold(config.upstream)}` : ""}`);
|
|
1102
1617
|
}
|
|
1103
1618
|
});
|
|
1104
1619
|
|
|
1105
1620
|
// src/commands/start.ts
|
|
1106
1621
|
import { defineCommand as defineCommand5 } from "citty";
|
|
1107
|
-
import
|
|
1622
|
+
import pc8 from "picocolors";
|
|
1108
1623
|
|
|
1109
1624
|
// src/utils/branch.ts
|
|
1110
1625
|
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
@@ -1115,6 +1630,9 @@ function formatBranchName(prefix, name) {
|
|
|
1115
1630
|
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1116
1631
|
return `${prefix}/${sanitized}`;
|
|
1117
1632
|
}
|
|
1633
|
+
function isValidBranchName(name) {
|
|
1634
|
+
return /^[a-zA-Z0-9._/-]+$/.test(name) && !name.startsWith("/") && !name.endsWith("/");
|
|
1635
|
+
}
|
|
1118
1636
|
function looksLikeNaturalLanguage(input) {
|
|
1119
1637
|
return input.includes(" ") && !input.includes("/");
|
|
1120
1638
|
}
|
|
@@ -1162,39 +1680,46 @@ var start_default = defineCommand5({
|
|
|
1162
1680
|
heading("\uD83C\uDF3F contrib start");
|
|
1163
1681
|
const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
|
|
1164
1682
|
if (useAI) {
|
|
1165
|
-
|
|
1683
|
+
const spinner = createSpinner("Generating branch name suggestion...");
|
|
1166
1684
|
const suggested = await suggestBranchName(branchName, args.model);
|
|
1167
1685
|
if (suggested) {
|
|
1686
|
+
spinner.success("Branch name suggestion ready.");
|
|
1168
1687
|
console.log(`
|
|
1169
|
-
${
|
|
1170
|
-
const accepted = await confirmPrompt(`Use ${
|
|
1688
|
+
${pc8.dim("AI suggestion:")} ${pc8.bold(pc8.cyan(suggested))}`);
|
|
1689
|
+
const accepted = await confirmPrompt(`Use ${pc8.bold(suggested)} as your branch name?`);
|
|
1171
1690
|
if (accepted) {
|
|
1172
1691
|
branchName = suggested;
|
|
1173
1692
|
} else {
|
|
1174
1693
|
branchName = await inputPrompt("Enter branch name", branchName);
|
|
1175
1694
|
}
|
|
1695
|
+
} else {
|
|
1696
|
+
spinner.fail("AI did not return a branch name suggestion.");
|
|
1176
1697
|
}
|
|
1177
1698
|
}
|
|
1178
1699
|
if (!hasPrefix(branchName, branchPrefixes)) {
|
|
1179
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
1700
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc8.bold(branchName)}:`, branchPrefixes);
|
|
1180
1701
|
branchName = formatBranchName(prefix, branchName);
|
|
1181
1702
|
}
|
|
1182
|
-
|
|
1703
|
+
if (!isValidBranchName(branchName)) {
|
|
1704
|
+
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
info(`Creating branch: ${pc8.bold(branchName)}`);
|
|
1183
1708
|
await fetchRemote(syncSource.remote);
|
|
1184
|
-
const
|
|
1185
|
-
if (
|
|
1709
|
+
const updateResult = await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1710
|
+
if (updateResult.exitCode !== 0) {}
|
|
1186
1711
|
const result = await createBranch(branchName, baseBranch);
|
|
1187
1712
|
if (result.exitCode !== 0) {
|
|
1188
1713
|
error(`Failed to create branch: ${result.stderr}`);
|
|
1189
1714
|
process.exit(1);
|
|
1190
1715
|
}
|
|
1191
|
-
success(`✅ Created ${
|
|
1716
|
+
success(`✅ Created ${pc8.bold(branchName)} from latest ${pc8.bold(baseBranch)}`);
|
|
1192
1717
|
}
|
|
1193
1718
|
});
|
|
1194
1719
|
|
|
1195
1720
|
// src/commands/status.ts
|
|
1196
1721
|
import { defineCommand as defineCommand6 } from "citty";
|
|
1197
|
-
import
|
|
1722
|
+
import pc9 from "picocolors";
|
|
1198
1723
|
var status_default = defineCommand6({
|
|
1199
1724
|
meta: {
|
|
1200
1725
|
name: "status",
|
|
@@ -1211,17 +1736,20 @@ var status_default = defineCommand6({
|
|
|
1211
1736
|
process.exit(1);
|
|
1212
1737
|
}
|
|
1213
1738
|
heading("\uD83D\uDCCA contribute-now status");
|
|
1214
|
-
console.log(` ${
|
|
1215
|
-
console.log(` ${
|
|
1739
|
+
console.log(` ${pc9.dim("Workflow:")} ${pc9.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
1740
|
+
console.log(` ${pc9.dim("Role:")} ${pc9.bold(config.role)}`);
|
|
1216
1741
|
console.log();
|
|
1217
1742
|
await fetchAll();
|
|
1218
1743
|
const currentBranch = await getCurrentBranch();
|
|
1219
1744
|
const { mainBranch, origin, upstream, workflow } = config;
|
|
1220
1745
|
const baseBranch = getBaseBranch(config);
|
|
1221
1746
|
const isContributor = config.role === "contributor";
|
|
1222
|
-
const dirty = await
|
|
1747
|
+
const [dirty, fileStatus] = await Promise.all([
|
|
1748
|
+
hasUncommittedChanges(),
|
|
1749
|
+
getFileStatus()
|
|
1750
|
+
]);
|
|
1223
1751
|
if (dirty) {
|
|
1224
|
-
console.log(` ${
|
|
1752
|
+
console.log(` ${pc9.yellow("⚠")} ${pc9.yellow("Uncommitted changes in working tree")}`);
|
|
1225
1753
|
console.log();
|
|
1226
1754
|
}
|
|
1227
1755
|
const mainRemote = `${origin}/${mainBranch}`;
|
|
@@ -1237,30 +1765,130 @@ var status_default = defineCommand6({
|
|
|
1237
1765
|
if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
1238
1766
|
const branchDiv = await getDivergence(currentBranch, baseBranch);
|
|
1239
1767
|
const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
|
|
1240
|
-
console.log(branchLine +
|
|
1768
|
+
console.log(branchLine + pc9.dim(` (current ${pc9.green("*")})`));
|
|
1241
1769
|
} else if (currentBranch) {
|
|
1242
|
-
console.log(
|
|
1770
|
+
console.log(pc9.dim(` (on ${pc9.bold(currentBranch)} branch)`));
|
|
1771
|
+
}
|
|
1772
|
+
const hasFiles = fileStatus.staged.length > 0 || fileStatus.modified.length > 0 || fileStatus.untracked.length > 0;
|
|
1773
|
+
if (hasFiles) {
|
|
1774
|
+
console.log();
|
|
1775
|
+
if (fileStatus.staged.length > 0) {
|
|
1776
|
+
console.log(` ${pc9.green("Staged for commit:")}`);
|
|
1777
|
+
for (const { file, status } of fileStatus.staged) {
|
|
1778
|
+
console.log(` ${pc9.green("+")} ${pc9.dim(`${status}:`)} ${file}`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
if (fileStatus.modified.length > 0) {
|
|
1782
|
+
console.log(` ${pc9.yellow("Unstaged changes:")}`);
|
|
1783
|
+
for (const { file, status } of fileStatus.modified) {
|
|
1784
|
+
console.log(` ${pc9.yellow("~")} ${pc9.dim(`${status}:`)} ${file}`);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (fileStatus.untracked.length > 0) {
|
|
1788
|
+
console.log(` ${pc9.red("Untracked files:")}`);
|
|
1789
|
+
for (const file of fileStatus.untracked) {
|
|
1790
|
+
console.log(` ${pc9.red("?")} ${file}`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
} else if (!dirty) {
|
|
1794
|
+
console.log(` ${pc9.green("✓")} ${pc9.dim("Working tree clean")}`);
|
|
1795
|
+
}
|
|
1796
|
+
const tips = [];
|
|
1797
|
+
if (fileStatus.staged.length > 0) {
|
|
1798
|
+
tips.push(`Run ${pc9.bold("contrib commit")} to commit staged changes`);
|
|
1799
|
+
}
|
|
1800
|
+
if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
|
|
1801
|
+
tips.push(`Run ${pc9.bold("contrib commit")} to stage and commit changes`);
|
|
1802
|
+
}
|
|
1803
|
+
if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0 && currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
1804
|
+
const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
|
|
1805
|
+
if (branchDiv.ahead > 0) {
|
|
1806
|
+
tips.push(`Run ${pc9.bold("contrib submit")} to push and create/update your PR`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (tips.length > 0) {
|
|
1810
|
+
console.log();
|
|
1811
|
+
console.log(` ${pc9.dim("\uD83D\uDCA1 Tip:")}`);
|
|
1812
|
+
for (const tip of tips) {
|
|
1813
|
+
console.log(` ${pc9.dim(tip)}`);
|
|
1814
|
+
}
|
|
1243
1815
|
}
|
|
1244
1816
|
console.log();
|
|
1245
1817
|
}
|
|
1246
1818
|
});
|
|
1247
1819
|
function formatStatus(branch, base, ahead, behind) {
|
|
1248
|
-
const label =
|
|
1820
|
+
const label = pc9.bold(branch.padEnd(20));
|
|
1249
1821
|
if (ahead === 0 && behind === 0) {
|
|
1250
|
-
return ` ${
|
|
1822
|
+
return ` ${pc9.green("✓")} ${label} ${pc9.dim(`in sync with ${base}`)}`;
|
|
1251
1823
|
}
|
|
1252
1824
|
if (ahead > 0 && behind === 0) {
|
|
1253
|
-
return ` ${
|
|
1825
|
+
return ` ${pc9.yellow("↑")} ${label} ${pc9.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
|
|
1254
1826
|
}
|
|
1255
1827
|
if (behind > 0 && ahead === 0) {
|
|
1256
|
-
return ` ${
|
|
1828
|
+
return ` ${pc9.red("↓")} ${label} ${pc9.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
|
|
1257
1829
|
}
|
|
1258
|
-
return ` ${
|
|
1830
|
+
return ` ${pc9.red("⚡")} ${label} ${pc9.yellow(`${ahead} ahead`)}${pc9.dim(", ")}${pc9.red(`${behind} behind`)} ${pc9.dim(base)}`;
|
|
1259
1831
|
}
|
|
1260
1832
|
|
|
1261
1833
|
// src/commands/submit.ts
|
|
1262
1834
|
import { defineCommand as defineCommand7 } from "citty";
|
|
1263
|
-
import
|
|
1835
|
+
import pc10 from "picocolors";
|
|
1836
|
+
async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
1837
|
+
info(`Checking out ${pc10.bold(baseBranch)}...`);
|
|
1838
|
+
const coResult = await checkoutBranch(baseBranch);
|
|
1839
|
+
if (coResult.exitCode !== 0) {
|
|
1840
|
+
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
1841
|
+
process.exit(1);
|
|
1842
|
+
}
|
|
1843
|
+
info(`Squash merging ${pc10.bold(featureBranch)} into ${pc10.bold(baseBranch)}...`);
|
|
1844
|
+
const mergeResult = await mergeSquash(featureBranch);
|
|
1845
|
+
if (mergeResult.exitCode !== 0) {
|
|
1846
|
+
error(`Squash merge failed: ${mergeResult.stderr}`);
|
|
1847
|
+
process.exit(1);
|
|
1848
|
+
}
|
|
1849
|
+
let message = options?.defaultMsg;
|
|
1850
|
+
if (!message) {
|
|
1851
|
+
const copilotError = await checkCopilotAvailable();
|
|
1852
|
+
if (!copilotError) {
|
|
1853
|
+
const spinner = createSpinner("Generating AI commit message for squash merge...");
|
|
1854
|
+
const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
|
|
1855
|
+
const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit");
|
|
1856
|
+
if (aiMsg) {
|
|
1857
|
+
message = aiMsg;
|
|
1858
|
+
spinner.success("AI commit message generated.");
|
|
1859
|
+
} else {
|
|
1860
|
+
spinner.fail("AI did not return a commit message.");
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
warn(`AI unavailable: ${copilotError}`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const fallback = message || `squash merge ${featureBranch}`;
|
|
1867
|
+
const finalMsg = await inputPrompt("Commit message", fallback);
|
|
1868
|
+
const commitResult = await commitWithMessage(finalMsg);
|
|
1869
|
+
if (commitResult.exitCode !== 0) {
|
|
1870
|
+
error(`Commit failed: ${commitResult.stderr}`);
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
1873
|
+
info(`Pushing ${pc10.bold(baseBranch)} to ${origin}...`);
|
|
1874
|
+
const pushResult = await pushBranch(origin, baseBranch);
|
|
1875
|
+
if (pushResult.exitCode !== 0) {
|
|
1876
|
+
error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
|
|
1877
|
+
process.exit(1);
|
|
1878
|
+
}
|
|
1879
|
+
info(`Deleting local branch ${pc10.bold(featureBranch)}...`);
|
|
1880
|
+
const delLocal = await forceDeleteBranch(featureBranch);
|
|
1881
|
+
if (delLocal.exitCode !== 0) {
|
|
1882
|
+
warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
|
|
1883
|
+
}
|
|
1884
|
+
info(`Deleting remote branch ${pc10.bold(featureBranch)}...`);
|
|
1885
|
+
const delRemote = await deleteRemoteBranch(origin, featureBranch);
|
|
1886
|
+
if (delRemote.exitCode !== 0) {
|
|
1887
|
+
warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
|
|
1888
|
+
}
|
|
1889
|
+
success(`✅ Squash merged ${pc10.bold(featureBranch)} into ${pc10.bold(baseBranch)} and pushed.`);
|
|
1890
|
+
info(`Run ${pc10.bold("contrib start")} to begin a new feature.`);
|
|
1891
|
+
}
|
|
1264
1892
|
var submit_default = defineCommand7({
|
|
1265
1893
|
meta: {
|
|
1266
1894
|
name: "submit",
|
|
@@ -1301,11 +1929,11 @@ var submit_default = defineCommand7({
|
|
|
1301
1929
|
process.exit(1);
|
|
1302
1930
|
}
|
|
1303
1931
|
if (protectedBranches.includes(currentBranch)) {
|
|
1304
|
-
error(`Cannot submit ${protectedBranches.map((b) =>
|
|
1932
|
+
error(`Cannot submit ${protectedBranches.map((b) => pc10.bold(b)).join(" or ")} as a PR. Switch to your feature branch.`);
|
|
1305
1933
|
process.exit(1);
|
|
1306
1934
|
}
|
|
1307
1935
|
heading("\uD83D\uDE80 contrib submit");
|
|
1308
|
-
info(`Pushing ${
|
|
1936
|
+
info(`Pushing ${pc10.bold(currentBranch)} to ${origin}...`);
|
|
1309
1937
|
const pushResult = await pushSetUpstream(origin, currentBranch);
|
|
1310
1938
|
if (pushResult.exitCode !== 0) {
|
|
1311
1939
|
error(`Failed to push: ${pushResult.stderr}`);
|
|
@@ -1319,43 +1947,70 @@ var submit_default = defineCommand7({
|
|
|
1319
1947
|
const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
|
|
1320
1948
|
console.log();
|
|
1321
1949
|
info("Create your PR manually:");
|
|
1322
|
-
console.log(` ${
|
|
1950
|
+
console.log(` ${pc10.cyan(prUrl)}`);
|
|
1323
1951
|
} else {
|
|
1324
1952
|
info("gh CLI not available. Create your PR manually on GitHub.");
|
|
1325
1953
|
}
|
|
1326
1954
|
return;
|
|
1327
1955
|
}
|
|
1956
|
+
const existingPR = await getPRForBranch(currentBranch);
|
|
1957
|
+
if (existingPR) {
|
|
1958
|
+
success(`Pushed changes to existing PR #${existingPR.number}: ${pc10.bold(existingPR.title)}`);
|
|
1959
|
+
console.log(` ${pc10.cyan(existingPR.url)}`);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1328
1962
|
let prTitle = null;
|
|
1329
1963
|
let prBody = null;
|
|
1330
1964
|
if (!args["no-ai"]) {
|
|
1331
|
-
const copilotError = await
|
|
1965
|
+
const [copilotError, commits, diff] = await Promise.all([
|
|
1966
|
+
checkCopilotAvailable(),
|
|
1967
|
+
getLog(baseBranch, "HEAD"),
|
|
1968
|
+
getLogDiff(baseBranch, "HEAD")
|
|
1969
|
+
]);
|
|
1332
1970
|
if (!copilotError) {
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
const diff = await getLogDiff(baseBranch, "HEAD");
|
|
1336
|
-
const result = await generatePRDescription(commits, diff, args.model);
|
|
1971
|
+
const spinner = createSpinner("Generating AI PR description...");
|
|
1972
|
+
const result = await generatePRDescription(commits, diff, args.model, config.commitConvention);
|
|
1337
1973
|
if (result) {
|
|
1338
1974
|
prTitle = result.title;
|
|
1339
1975
|
prBody = result.body;
|
|
1976
|
+
spinner.success("PR description generated.");
|
|
1340
1977
|
console.log(`
|
|
1341
|
-
${
|
|
1978
|
+
${pc10.dim("AI title:")} ${pc10.bold(pc10.cyan(prTitle))}`);
|
|
1342
1979
|
console.log(`
|
|
1343
|
-
${
|
|
1344
|
-
console.log(
|
|
1980
|
+
${pc10.dim("AI body preview:")}`);
|
|
1981
|
+
console.log(pc10.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
|
|
1345
1982
|
} else {
|
|
1346
|
-
|
|
1983
|
+
spinner.fail("AI did not return a PR description.");
|
|
1347
1984
|
}
|
|
1348
1985
|
} else {
|
|
1349
1986
|
warn(`AI unavailable: ${copilotError}`);
|
|
1350
1987
|
}
|
|
1351
1988
|
}
|
|
1989
|
+
const CANCEL = "Cancel";
|
|
1990
|
+
const SQUASH_LOCAL = `Squash merge to ${baseBranch} locally (no PR)`;
|
|
1352
1991
|
if (prTitle && prBody) {
|
|
1353
|
-
const
|
|
1992
|
+
const choices = [
|
|
1354
1993
|
"Use AI description",
|
|
1355
1994
|
"Edit title",
|
|
1356
1995
|
"Write manually",
|
|
1357
1996
|
"Use gh --fill (auto-fill from commits)"
|
|
1358
|
-
]
|
|
1997
|
+
];
|
|
1998
|
+
if (config.role === "maintainer")
|
|
1999
|
+
choices.push(SQUASH_LOCAL);
|
|
2000
|
+
choices.push(CANCEL);
|
|
2001
|
+
const action = await selectPrompt("What would you like to do with the PR description?", choices);
|
|
2002
|
+
if (action === CANCEL) {
|
|
2003
|
+
warn("Submit cancelled.");
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
if (action === SQUASH_LOCAL) {
|
|
2007
|
+
await performSquashMerge(origin, baseBranch, currentBranch, {
|
|
2008
|
+
defaultMsg: prTitle ?? undefined,
|
|
2009
|
+
model: args.model,
|
|
2010
|
+
convention: config.commitConvention
|
|
2011
|
+
});
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
1359
2014
|
if (action === "Use AI description") {} else if (action === "Edit title") {
|
|
1360
2015
|
prTitle = await inputPrompt("PR title", prTitle);
|
|
1361
2016
|
} else if (action === "Write manually") {
|
|
@@ -1371,8 +2026,26 @@ ${pc9.dim("AI body preview:")}`);
|
|
|
1371
2026
|
return;
|
|
1372
2027
|
}
|
|
1373
2028
|
} else {
|
|
1374
|
-
const
|
|
1375
|
-
|
|
2029
|
+
const choices = [
|
|
2030
|
+
"Write title & body manually",
|
|
2031
|
+
"Use gh --fill (auto-fill from commits)"
|
|
2032
|
+
];
|
|
2033
|
+
if (config.role === "maintainer")
|
|
2034
|
+
choices.push(SQUASH_LOCAL);
|
|
2035
|
+
choices.push(CANCEL);
|
|
2036
|
+
const action = await selectPrompt("How would you like to create the PR?", choices);
|
|
2037
|
+
if (action === CANCEL) {
|
|
2038
|
+
warn("Submit cancelled.");
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (action === SQUASH_LOCAL) {
|
|
2042
|
+
await performSquashMerge(origin, baseBranch, currentBranch, {
|
|
2043
|
+
model: args.model,
|
|
2044
|
+
convention: config.commitConvention
|
|
2045
|
+
});
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (action === "Write title & body manually") {
|
|
1376
2049
|
prTitle = await inputPrompt("PR title");
|
|
1377
2050
|
prBody = await inputPrompt("PR body (markdown)");
|
|
1378
2051
|
} else {
|
|
@@ -1405,7 +2078,7 @@ ${pc9.dim("AI body preview:")}`);
|
|
|
1405
2078
|
|
|
1406
2079
|
// src/commands/sync.ts
|
|
1407
2080
|
import { defineCommand as defineCommand8 } from "citty";
|
|
1408
|
-
import
|
|
2081
|
+
import pc11 from "picocolors";
|
|
1409
2082
|
var sync_default = defineCommand8({
|
|
1410
2083
|
meta: {
|
|
1411
2084
|
name: "sync",
|
|
@@ -1448,12 +2121,12 @@ var sync_default = defineCommand8({
|
|
|
1448
2121
|
}
|
|
1449
2122
|
const div = await getDivergence(baseBranch, syncSource.ref);
|
|
1450
2123
|
if (div.ahead > 0 || div.behind > 0) {
|
|
1451
|
-
info(`${
|
|
2124
|
+
info(`${pc11.bold(baseBranch)} is ${pc11.yellow(`${div.ahead} ahead`)} and ${pc11.red(`${div.behind} behind`)} ${syncSource.ref}`);
|
|
1452
2125
|
} else {
|
|
1453
|
-
info(`${
|
|
2126
|
+
info(`${pc11.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
|
|
1454
2127
|
}
|
|
1455
2128
|
if (!args.yes) {
|
|
1456
|
-
const ok = await confirmPrompt(`This will pull ${
|
|
2129
|
+
const ok = await confirmPrompt(`This will pull ${pc11.bold(syncSource.ref)} into local ${pc11.bold(baseBranch)}.`);
|
|
1457
2130
|
if (!ok)
|
|
1458
2131
|
process.exit(0);
|
|
1459
2132
|
}
|
|
@@ -1471,7 +2144,7 @@ var sync_default = defineCommand8({
|
|
|
1471
2144
|
if (hasDevBranch(workflow) && role === "maintainer") {
|
|
1472
2145
|
const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
|
|
1473
2146
|
if (mainDiv.behind > 0) {
|
|
1474
|
-
info(`Also syncing ${
|
|
2147
|
+
info(`Also syncing ${pc11.bold(config.mainBranch)}...`);
|
|
1475
2148
|
const mainCoResult = await checkoutBranch(config.mainBranch);
|
|
1476
2149
|
if (mainCoResult.exitCode === 0) {
|
|
1477
2150
|
const mainPullResult = await pullBranch(origin, config.mainBranch);
|
|
@@ -1486,9 +2159,9 @@ var sync_default = defineCommand8({
|
|
|
1486
2159
|
});
|
|
1487
2160
|
|
|
1488
2161
|
// src/commands/update.ts
|
|
1489
|
-
import { readFileSync as
|
|
2162
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1490
2163
|
import { defineCommand as defineCommand9 } from "citty";
|
|
1491
|
-
import
|
|
2164
|
+
import pc12 from "picocolors";
|
|
1492
2165
|
var update_default = defineCommand9({
|
|
1493
2166
|
meta: {
|
|
1494
2167
|
name: "update",
|
|
@@ -1524,7 +2197,7 @@ var update_default = defineCommand9({
|
|
|
1524
2197
|
process.exit(1);
|
|
1525
2198
|
}
|
|
1526
2199
|
if (protectedBranches.includes(currentBranch)) {
|
|
1527
|
-
error(`Use \`contrib sync\` to update ${protectedBranches.map((b) =>
|
|
2200
|
+
error(`Use \`contrib sync\` to update ${protectedBranches.map((b) => pc12.bold(b)).join(" or ")} branches.`);
|
|
1528
2201
|
process.exit(1);
|
|
1529
2202
|
}
|
|
1530
2203
|
if (await hasUncommittedChanges()) {
|
|
@@ -1532,9 +2205,9 @@ var update_default = defineCommand9({
|
|
|
1532
2205
|
process.exit(1);
|
|
1533
2206
|
}
|
|
1534
2207
|
heading("\uD83D\uDD03 contrib update");
|
|
1535
|
-
info(`Updating ${
|
|
2208
|
+
info(`Updating ${pc12.bold(currentBranch)} with latest ${pc12.bold(baseBranch)}...`);
|
|
1536
2209
|
await fetchRemote(syncSource.remote);
|
|
1537
|
-
await
|
|
2210
|
+
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1538
2211
|
const rebaseResult = await rebase(baseBranch);
|
|
1539
2212
|
if (rebaseResult.exitCode !== 0) {
|
|
1540
2213
|
warn("Rebase hit conflicts. Resolve them manually.");
|
|
@@ -1547,7 +2220,7 @@ var update_default = defineCommand9({
|
|
|
1547
2220
|
let conflictDiff = "";
|
|
1548
2221
|
for (const file of conflictFiles.slice(0, 3)) {
|
|
1549
2222
|
try {
|
|
1550
|
-
const content =
|
|
2223
|
+
const content = readFileSync4(file, "utf-8");
|
|
1551
2224
|
if (content.includes("<<<<<<<")) {
|
|
1552
2225
|
conflictDiff += `
|
|
1553
2226
|
--- ${file} ---
|
|
@@ -1557,33 +2230,37 @@ ${content.slice(0, 2000)}
|
|
|
1557
2230
|
} catch {}
|
|
1558
2231
|
}
|
|
1559
2232
|
if (conflictDiff) {
|
|
2233
|
+
const spinner = createSpinner("Analyzing conflicts with AI...");
|
|
1560
2234
|
const suggestion = await suggestConflictResolution(conflictDiff, args.model);
|
|
1561
2235
|
if (suggestion) {
|
|
2236
|
+
spinner.success("AI conflict guidance ready.");
|
|
1562
2237
|
console.log(`
|
|
1563
|
-
${
|
|
1564
|
-
console.log(
|
|
2238
|
+
${pc12.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
2239
|
+
console.log(pc12.dim("─".repeat(60)));
|
|
1565
2240
|
console.log(suggestion);
|
|
1566
|
-
console.log(
|
|
2241
|
+
console.log(pc12.dim("─".repeat(60)));
|
|
1567
2242
|
console.log();
|
|
2243
|
+
} else {
|
|
2244
|
+
spinner.fail("AI could not analyze the conflicts.");
|
|
1568
2245
|
}
|
|
1569
2246
|
}
|
|
1570
2247
|
}
|
|
1571
2248
|
}
|
|
1572
|
-
console.log(
|
|
2249
|
+
console.log(pc12.bold("To resolve:"));
|
|
1573
2250
|
console.log(` 1. Fix conflicts in the affected files`);
|
|
1574
|
-
console.log(` 2. ${
|
|
1575
|
-
console.log(` 3. ${
|
|
2251
|
+
console.log(` 2. ${pc12.cyan("git add <resolved-files>")}`);
|
|
2252
|
+
console.log(` 3. ${pc12.cyan("git rebase --continue")}`);
|
|
1576
2253
|
console.log();
|
|
1577
|
-
console.log(` Or abort: ${
|
|
2254
|
+
console.log(` Or abort: ${pc12.cyan("git rebase --abort")}`);
|
|
1578
2255
|
process.exit(1);
|
|
1579
2256
|
}
|
|
1580
|
-
success(`✅ ${
|
|
2257
|
+
success(`✅ ${pc12.bold(currentBranch)} has been rebased onto latest ${pc12.bold(baseBranch)}`);
|
|
1581
2258
|
}
|
|
1582
2259
|
});
|
|
1583
2260
|
|
|
1584
2261
|
// src/commands/validate.ts
|
|
1585
2262
|
import { defineCommand as defineCommand10 } from "citty";
|
|
1586
|
-
import
|
|
2263
|
+
import pc13 from "picocolors";
|
|
1587
2264
|
var validate_default = defineCommand10({
|
|
1588
2265
|
meta: {
|
|
1589
2266
|
name: "validate",
|
|
@@ -1614,7 +2291,7 @@ var validate_default = defineCommand10({
|
|
|
1614
2291
|
}
|
|
1615
2292
|
const errors = getValidationError(convention);
|
|
1616
2293
|
for (const line of errors) {
|
|
1617
|
-
console.error(
|
|
2294
|
+
console.error(pc13.red(` ✗ ${line}`));
|
|
1618
2295
|
}
|
|
1619
2296
|
process.exit(1);
|
|
1620
2297
|
}
|
|
@@ -1622,11 +2299,11 @@ var validate_default = defineCommand10({
|
|
|
1622
2299
|
|
|
1623
2300
|
// src/ui/banner.ts
|
|
1624
2301
|
import figlet from "figlet";
|
|
1625
|
-
import
|
|
2302
|
+
import pc14 from "picocolors";
|
|
1626
2303
|
// package.json
|
|
1627
2304
|
var package_default = {
|
|
1628
2305
|
name: "contribute-now",
|
|
1629
|
-
version: "0.2.0-
|
|
2306
|
+
version: "0.2.0-pr.4dd3c30",
|
|
1630
2307
|
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1631
2308
|
type: "module",
|
|
1632
2309
|
bin: {
|
|
@@ -1638,12 +2315,12 @@ var package_default = {
|
|
|
1638
2315
|
],
|
|
1639
2316
|
scripts: {
|
|
1640
2317
|
build: "bun build src/index.ts --outfile dist/index.js --target node --packages external",
|
|
2318
|
+
cli: "bun run src/index.ts --",
|
|
1641
2319
|
dev: "bun src/index.ts",
|
|
1642
2320
|
test: "bun test",
|
|
1643
2321
|
lint: "biome check .",
|
|
1644
2322
|
"lint:fix": "biome check --write .",
|
|
1645
2323
|
format: "biome format --write .",
|
|
1646
|
-
prepare: "husky || true",
|
|
1647
2324
|
"www:dev": "bun run --cwd www dev",
|
|
1648
2325
|
"www:build": "bun run --cwd www build",
|
|
1649
2326
|
"www:preview": "bun run --cwd www preview"
|
|
@@ -1670,6 +2347,7 @@ var package_default = {
|
|
|
1670
2347
|
url: "git+https://github.com/warengonzaga/contribute-now.git"
|
|
1671
2348
|
},
|
|
1672
2349
|
dependencies: {
|
|
2350
|
+
"@clack/prompts": "^1.0.1",
|
|
1673
2351
|
"@github/copilot-sdk": "^0.1.25",
|
|
1674
2352
|
"@wgtechlabs/log-engine": "^2.3.1",
|
|
1675
2353
|
citty: "^0.1.6",
|
|
@@ -1680,7 +2358,6 @@ var package_default = {
|
|
|
1680
2358
|
"@biomejs/biome": "^2.4.4",
|
|
1681
2359
|
"@types/bun": "latest",
|
|
1682
2360
|
"@types/figlet": "^1.7.0",
|
|
1683
|
-
husky: "^9.1.7",
|
|
1684
2361
|
typescript: "^5.7.0"
|
|
1685
2362
|
}
|
|
1686
2363
|
};
|
|
@@ -1688,9 +2365,10 @@ var package_default = {
|
|
|
1688
2365
|
// src/ui/banner.ts
|
|
1689
2366
|
var LOGO;
|
|
1690
2367
|
try {
|
|
1691
|
-
LOGO = figlet.textSync(
|
|
2368
|
+
LOGO = figlet.textSync(`Contribute
|
|
2369
|
+
Now`, { font: "ANSI Shadow" });
|
|
1692
2370
|
} catch {
|
|
1693
|
-
LOGO = "
|
|
2371
|
+
LOGO = "Contribute Now";
|
|
1694
2372
|
}
|
|
1695
2373
|
function getVersion() {
|
|
1696
2374
|
return package_default.version ?? "unknown";
|
|
@@ -1698,16 +2376,15 @@ function getVersion() {
|
|
|
1698
2376
|
function getAuthor() {
|
|
1699
2377
|
return typeof package_default.author === "string" ? package_default.author : "unknown";
|
|
1700
2378
|
}
|
|
1701
|
-
function showBanner(
|
|
1702
|
-
console.log(
|
|
2379
|
+
function showBanner(showLinks = false) {
|
|
2380
|
+
console.log(pc14.cyan(`
|
|
1703
2381
|
${LOGO}`));
|
|
1704
|
-
console.log(` ${
|
|
1705
|
-
if (
|
|
1706
|
-
console.log(` ${pc13.dim(package_default.description)}`);
|
|
2382
|
+
console.log(` ${pc14.dim(`v${getVersion()}`)} ${pc14.dim("—")} ${pc14.dim(`Built by ${getAuthor()}`)}`);
|
|
2383
|
+
if (showLinks) {
|
|
1707
2384
|
console.log();
|
|
1708
|
-
console.log(` ${
|
|
1709
|
-
console.log(` ${
|
|
1710
|
-
console.log(` ${
|
|
2385
|
+
console.log(` ${pc14.yellow("Star")} ${pc14.cyan("https://github.com/warengonzaga/contribute-now")}`);
|
|
2386
|
+
console.log(` ${pc14.green("Contribute")} ${pc14.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
|
|
2387
|
+
console.log(` ${pc14.magenta("Sponsor")} ${pc14.cyan("https://warengonzaga.com/sponsor")}`);
|
|
1711
2388
|
}
|
|
1712
2389
|
console.log();
|
|
1713
2390
|
}
|
|
@@ -1746,4 +2423,6 @@ var main = defineCommand11({
|
|
|
1746
2423
|
}
|
|
1747
2424
|
}
|
|
1748
2425
|
});
|
|
1749
|
-
runMain(main)
|
|
2426
|
+
runMain(main).then(() => {
|
|
2427
|
+
process.exit(0);
|
|
2428
|
+
});
|