contribute-now 0.2.0-dev.70284d0 → 0.2.0-dev.d4b7ede
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 +941 -296
- package/package.json +2 -1
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
|
});
|
|
@@ -197,8 +188,16 @@ async function getChangedFiles() {
|
|
|
197
188
|
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
198
189
|
if (exitCode !== 0)
|
|
199
190
|
return [];
|
|
200
|
-
return stdout.
|
|
201
|
-
`).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);
|
|
202
201
|
}
|
|
203
202
|
async function getDivergence(branch, base) {
|
|
204
203
|
const { exitCode, stdout } = await run([
|
|
@@ -225,6 +224,18 @@ async function getMergedBranches(base) {
|
|
|
225
224
|
async function deleteBranch(branch) {
|
|
226
225
|
return run(["branch", "-d", branch]);
|
|
227
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
|
+
}
|
|
228
239
|
async function pruneRemote(remote) {
|
|
229
240
|
return run(["remote", "prune", remote]);
|
|
230
241
|
}
|
|
@@ -245,6 +256,85 @@ async function getLog(base, head) {
|
|
|
245
256
|
async function pullBranch(remote, branch) {
|
|
246
257
|
return run(["pull", remote, branch]);
|
|
247
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
|
+
}
|
|
248
338
|
|
|
249
339
|
// src/utils/logger.ts
|
|
250
340
|
import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
|
|
@@ -387,7 +477,7 @@ ${pc3.bold("Branches to delete:")}`);
|
|
|
387
477
|
|
|
388
478
|
// src/commands/commit.ts
|
|
389
479
|
import { defineCommand as defineCommand2 } from "citty";
|
|
390
|
-
import
|
|
480
|
+
import pc5 from "picocolors";
|
|
391
481
|
|
|
392
482
|
// src/utils/convention.ts
|
|
393
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;
|
|
@@ -434,121 +524,98 @@ function getValidationError(convention) {
|
|
|
434
524
|
|
|
435
525
|
// src/utils/copilot.ts
|
|
436
526
|
import { CopilotClient } from "@github/copilot-sdk";
|
|
437
|
-
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
docs
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
ci
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
Rules:
|
|
454
|
-
- Breaking change (!) only for: feat, fix, refactor, perf
|
|
455
|
-
- Description: concise, imperative mood, max 72 chars, lowercase start
|
|
456
|
-
- Scope: optional, camelCase or kebab-case component name
|
|
457
|
-
- Return ONLY the commit message line, nothing else
|
|
458
|
-
|
|
459
|
-
Examples:
|
|
460
|
-
feat: add user authentication system
|
|
461
|
-
fix(auth): resolve token expiry issue
|
|
462
|
-
docs: update contributing guidelines
|
|
463
|
-
feat!: redesign authentication API`;
|
|
464
|
-
var CLEAN_COMMIT_SYSTEM_PROMPT = `You are a git commit message generator. Generate a Clean Commit message following this EXACT format:
|
|
465
|
-
<emoji> <type>[!][ (<scope>)]: <description>
|
|
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.
|
|
466
543
|
|
|
467
|
-
|
|
468
|
-
- There MUST be a space between the emoji and the type
|
|
469
|
-
- If a scope is used, there MUST be a space before the opening parenthesis
|
|
470
|
-
- There MUST be a colon and a space after the type or scope before the description
|
|
471
|
-
- Pattern: EMOJI SPACE TYPE SPACE OPENPAREN SCOPE CLOSEPAREN COLON SPACE DESCRIPTION
|
|
544
|
+
${conventionBlock}
|
|
472
545
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
\uD83E\uDDEA test – adding or updating tests
|
|
481
|
-
\uD83D\uDCD6 docs – documentation changes
|
|
482
|
-
\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
|
+
]
|
|
483
553
|
|
|
484
554
|
Rules:
|
|
485
|
-
-
|
|
486
|
-
-
|
|
487
|
-
-
|
|
488
|
-
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
⚙️ setup (ci): configure github actions workflow
|
|
494
|
-
\uD83D\uDCE6 new!: redesign authentication system
|
|
495
|
-
\uD83D\uDDD1️ remove (deps): drop unused lodash dependency
|
|
496
|
-
|
|
497
|
-
WRONG (never do this):
|
|
498
|
-
⚙️setup(ci): ... ← missing spaces
|
|
499
|
-
\uD83D\uDCE6new: ... ← missing space after emoji
|
|
500
|
-
\uD83D\uDD27 update(api): ... ← missing space before scope`;
|
|
501
|
-
var BRANCH_NAME_SYSTEM_PROMPT = `You are a git branch name generator. Convert natural language descriptions into proper git branch names.
|
|
502
|
-
|
|
503
|
-
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>
|
|
504
563
|
Prefixes: feature, fix, docs, chore, test, refactor
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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.`;
|
|
521
584
|
}
|
|
522
|
-
|
|
523
|
-
Rules:
|
|
524
|
-
- title: concise, present tense, describes what the PR does
|
|
525
|
-
- body: markdown with Summary, Changes (bullet list), and Test Plan sections
|
|
526
|
-
- Return ONLY the JSON object, no markdown fences, no extra text`;
|
|
527
|
-
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `You are a git merge conflict resolution advisor. Analyze the conflict markers and provide guidance.
|
|
528
|
-
|
|
529
|
-
Rules:
|
|
530
|
-
- Explain what each side of the conflict contains
|
|
531
|
-
- Suggest the most likely correct resolution strategy
|
|
532
|
-
- Never auto-resolve — provide guidance only
|
|
533
|
-
- Be concise and actionable`;
|
|
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.`;
|
|
534
586
|
function suppressSubprocessWarnings() {
|
|
535
|
-
const prev = process.env.NODE_NO_WARNINGS;
|
|
536
587
|
process.env.NODE_NO_WARNINGS = "1";
|
|
537
|
-
return prev;
|
|
538
588
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
+
});
|
|
545
600
|
}
|
|
601
|
+
var COPILOT_TIMEOUT_MS = 30000;
|
|
602
|
+
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
546
603
|
async function checkCopilotAvailable() {
|
|
547
|
-
let client = null;
|
|
548
|
-
const prev = suppressSubprocessWarnings();
|
|
549
604
|
try {
|
|
550
|
-
client =
|
|
551
|
-
|
|
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;
|
|
552
619
|
} catch (err) {
|
|
553
620
|
const msg = err instanceof Error ? err.message : String(err);
|
|
554
621
|
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
@@ -556,47 +623,45 @@ async function checkCopilotAvailable() {
|
|
|
556
623
|
}
|
|
557
624
|
return `Failed to start Copilot service: ${msg}`;
|
|
558
625
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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);
|
|
575
647
|
}
|
|
576
|
-
return
|
|
648
|
+
return _managedClient;
|
|
577
649
|
}
|
|
578
|
-
async function callCopilot(systemMessage, userMessage, model) {
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
|
|
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);
|
|
582
658
|
try {
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
sessionConfig.model = model;
|
|
588
|
-
const session = await client.createSession(sessionConfig);
|
|
589
|
-
try {
|
|
590
|
-
const response = await session.sendAndWait({ prompt: userMessage });
|
|
591
|
-
if (!response?.data?.content)
|
|
592
|
-
return null;
|
|
593
|
-
return response.data.content;
|
|
594
|
-
} finally {
|
|
595
|
-
await session.destroy();
|
|
596
|
-
}
|
|
659
|
+
const response = await withTimeout(session.sendAndWait({ prompt: userMessage }), timeoutMs);
|
|
660
|
+
if (!response?.data?.content)
|
|
661
|
+
return null;
|
|
662
|
+
return response.data.content;
|
|
597
663
|
} finally {
|
|
598
|
-
|
|
599
|
-
await client.stop();
|
|
664
|
+
await session.destroy();
|
|
600
665
|
}
|
|
601
666
|
}
|
|
602
667
|
function getCommitSystemPrompt(convention) {
|
|
@@ -604,21 +669,53 @@ function getCommitSystemPrompt(convention) {
|
|
|
604
669
|
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
605
670
|
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
606
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
|
+
}
|
|
607
701
|
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
608
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.` : "";
|
|
609
706
|
const userMessage = `Generate a commit message for these staged changes:
|
|
610
707
|
|
|
611
708
|
Files: ${stagedFiles.join(", ")}
|
|
612
709
|
|
|
613
710
|
Diff:
|
|
614
|
-
${diff.slice(0, 4000)}`;
|
|
711
|
+
${diff.slice(0, 4000)}${multiFileHint}`;
|
|
615
712
|
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
616
713
|
return result?.trim() ?? null;
|
|
617
714
|
} catch {
|
|
618
715
|
return null;
|
|
619
716
|
}
|
|
620
717
|
}
|
|
621
|
-
async function generatePRDescription(commits, diff, model) {
|
|
718
|
+
async function generatePRDescription(commits, diff, model, convention = "clean-commit") {
|
|
622
719
|
try {
|
|
623
720
|
const userMessage = `Generate a PR description for these changes:
|
|
624
721
|
|
|
@@ -628,10 +725,10 @@ ${commits.join(`
|
|
|
628
725
|
|
|
629
726
|
Diff (truncated):
|
|
630
727
|
${diff.slice(0, 4000)}`;
|
|
631
|
-
const result = await callCopilot(
|
|
728
|
+
const result = await callCopilot(getPRDescriptionSystemPrompt(convention), userMessage, model);
|
|
632
729
|
if (!result)
|
|
633
730
|
return null;
|
|
634
|
-
const cleaned = result
|
|
731
|
+
const cleaned = extractJson(result);
|
|
635
732
|
return JSON.parse(cleaned);
|
|
636
733
|
} catch {
|
|
637
734
|
return null;
|
|
@@ -656,6 +753,124 @@ ${conflictDiff.slice(0, 4000)}`;
|
|
|
656
753
|
return null;
|
|
657
754
|
}
|
|
658
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
|
+
}
|
|
659
874
|
|
|
660
875
|
// src/commands/commit.ts
|
|
661
876
|
var commit_default = defineCommand2({
|
|
@@ -672,6 +887,11 @@ var commit_default = defineCommand2({
|
|
|
672
887
|
type: "boolean",
|
|
673
888
|
description: "Skip AI and write commit message manually",
|
|
674
889
|
default: false
|
|
890
|
+
},
|
|
891
|
+
group: {
|
|
892
|
+
type: "boolean",
|
|
893
|
+
description: "AI groups related changes into separate atomic commits",
|
|
894
|
+
default: false
|
|
675
895
|
}
|
|
676
896
|
},
|
|
677
897
|
async run({ args }) {
|
|
@@ -685,7 +905,11 @@ var commit_default = defineCommand2({
|
|
|
685
905
|
process.exit(1);
|
|
686
906
|
}
|
|
687
907
|
heading("\uD83D\uDCBE contrib commit");
|
|
688
|
-
|
|
908
|
+
if (args.group) {
|
|
909
|
+
await runGroupCommit(args.model, config);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
let stagedFiles = await getStagedFiles();
|
|
689
913
|
if (stagedFiles.length === 0) {
|
|
690
914
|
const changedFiles = await getChangedFiles();
|
|
691
915
|
if (changedFiles.length === 0) {
|
|
@@ -693,31 +917,62 @@ var commit_default = defineCommand2({
|
|
|
693
917
|
process.exit(1);
|
|
694
918
|
}
|
|
695
919
|
console.log(`
|
|
696
|
-
${
|
|
920
|
+
${pc5.bold("Changed files:")}`);
|
|
697
921
|
for (const f of changedFiles) {
|
|
698
|
-
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);
|
|
699
956
|
}
|
|
700
|
-
console.log();
|
|
701
|
-
warn("No staged changes. Stage your files with `git add` and re-run.");
|
|
702
|
-
process.exit(1);
|
|
703
957
|
}
|
|
704
958
|
info(`Staged files: ${stagedFiles.join(", ")}`);
|
|
705
959
|
let commitMessage = null;
|
|
706
960
|
const useAI = !args["no-ai"];
|
|
707
961
|
if (useAI) {
|
|
708
|
-
const copilotError = await checkCopilotAvailable();
|
|
962
|
+
const [copilotError, diff] = await Promise.all([checkCopilotAvailable(), getStagedDiff()]);
|
|
709
963
|
if (copilotError) {
|
|
710
964
|
warn(`AI unavailable: ${copilotError}`);
|
|
711
965
|
warn("Falling back to manual commit message entry.");
|
|
712
966
|
} else {
|
|
713
|
-
|
|
714
|
-
const diff = await getStagedDiff();
|
|
967
|
+
const spinner = createSpinner("Generating commit message with AI...");
|
|
715
968
|
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
716
969
|
if (commitMessage) {
|
|
970
|
+
spinner.success("AI commit message generated.");
|
|
717
971
|
console.log(`
|
|
718
|
-
${
|
|
972
|
+
${pc5.dim("AI suggestion:")} ${pc5.bold(pc5.cyan(commitMessage))}`);
|
|
719
973
|
} else {
|
|
720
|
-
|
|
974
|
+
spinner.fail("AI did not return a commit message.");
|
|
975
|
+
warn("Falling back to manual entry.");
|
|
721
976
|
}
|
|
722
977
|
}
|
|
723
978
|
}
|
|
@@ -734,16 +989,17 @@ ${pc4.bold("Changed files:")}`);
|
|
|
734
989
|
} else if (action === "Edit this message") {
|
|
735
990
|
finalMessage = await inputPrompt("Edit commit message", commitMessage);
|
|
736
991
|
} else if (action === "Regenerate") {
|
|
737
|
-
|
|
992
|
+
const spinner = createSpinner("Regenerating commit message...");
|
|
738
993
|
const diff = await getStagedDiff();
|
|
739
994
|
const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
740
995
|
if (regen) {
|
|
996
|
+
spinner.success("Commit message regenerated.");
|
|
741
997
|
console.log(`
|
|
742
|
-
${
|
|
998
|
+
${pc5.dim("AI suggestion:")} ${pc5.bold(pc5.cyan(regen))}`);
|
|
743
999
|
const ok = await confirmPrompt("Use this message?");
|
|
744
1000
|
finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
|
|
745
1001
|
} else {
|
|
746
|
-
|
|
1002
|
+
spinner.fail("Regeneration failed.");
|
|
747
1003
|
finalMessage = await inputPrompt("Enter commit message");
|
|
748
1004
|
}
|
|
749
1005
|
} else {
|
|
@@ -754,7 +1010,7 @@ ${pc4.bold("Changed files:")}`);
|
|
|
754
1010
|
if (convention2 !== "none") {
|
|
755
1011
|
console.log();
|
|
756
1012
|
for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
|
|
757
|
-
console.log(
|
|
1013
|
+
console.log(pc5.dim(hint));
|
|
758
1014
|
}
|
|
759
1015
|
console.log();
|
|
760
1016
|
}
|
|
@@ -778,21 +1034,209 @@ ${pc4.bold("Changed files:")}`);
|
|
|
778
1034
|
error(`Failed to commit: ${result.stderr}`);
|
|
779
1035
|
process.exit(1);
|
|
780
1036
|
}
|
|
781
|
-
success(`✅ Committed: ${
|
|
1037
|
+
success(`✅ Committed: ${pc5.bold(finalMessage)}`);
|
|
782
1038
|
}
|
|
783
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
|
+
}
|
|
784
1228
|
|
|
785
1229
|
// src/commands/hook.ts
|
|
786
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as
|
|
787
|
-
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";
|
|
788
1232
|
import { defineCommand as defineCommand3 } from "citty";
|
|
789
|
-
import
|
|
1233
|
+
import pc6 from "picocolors";
|
|
790
1234
|
var HOOK_MARKER = "# managed by contribute-now";
|
|
791
1235
|
function getHooksDir(cwd = process.cwd()) {
|
|
792
|
-
return
|
|
1236
|
+
return join3(cwd, ".git", "hooks");
|
|
793
1237
|
}
|
|
794
1238
|
function getHookPath(cwd = process.cwd()) {
|
|
795
|
-
return
|
|
1239
|
+
return join3(getHooksDir(cwd), "commit-msg");
|
|
796
1240
|
}
|
|
797
1241
|
function generateHookScript() {
|
|
798
1242
|
return `#!/bin/sh
|
|
@@ -809,8 +1253,19 @@ case "$commit_msg" in
|
|
|
809
1253
|
Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
|
|
810
1254
|
esac
|
|
811
1255
|
|
|
812
|
-
#
|
|
813
|
-
|
|
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
|
|
814
1269
|
`;
|
|
815
1270
|
}
|
|
816
1271
|
var hook_default = defineCommand3({
|
|
@@ -857,7 +1312,7 @@ async function installHook() {
|
|
|
857
1312
|
const hookPath = getHookPath();
|
|
858
1313
|
const hooksDir = getHooksDir();
|
|
859
1314
|
if (existsSync2(hookPath)) {
|
|
860
|
-
const existing =
|
|
1315
|
+
const existing = readFileSync3(hookPath, "utf-8");
|
|
861
1316
|
if (!existing.includes(HOOK_MARKER)) {
|
|
862
1317
|
error("A commit-msg hook already exists and was not installed by contribute-now.");
|
|
863
1318
|
warn(`Path: ${hookPath}`);
|
|
@@ -871,8 +1326,8 @@ async function installHook() {
|
|
|
871
1326
|
}
|
|
872
1327
|
writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
|
|
873
1328
|
success(`commit-msg hook installed.`);
|
|
874
|
-
info(`Convention: ${
|
|
875
|
-
info(`Path: ${
|
|
1329
|
+
info(`Convention: ${pc6.bold(CONVENTION_LABELS[config.commitConvention])}`);
|
|
1330
|
+
info(`Path: ${pc6.dim(hookPath)}`);
|
|
876
1331
|
}
|
|
877
1332
|
async function uninstallHook() {
|
|
878
1333
|
heading("\uD83E\uDE9D hook uninstall");
|
|
@@ -881,7 +1336,7 @@ async function uninstallHook() {
|
|
|
881
1336
|
info("No commit-msg hook found. Nothing to uninstall.");
|
|
882
1337
|
return;
|
|
883
1338
|
}
|
|
884
|
-
const content =
|
|
1339
|
+
const content = readFileSync3(hookPath, "utf-8");
|
|
885
1340
|
if (!content.includes(HOOK_MARKER)) {
|
|
886
1341
|
error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
|
|
887
1342
|
process.exit(1);
|
|
@@ -892,7 +1347,7 @@ async function uninstallHook() {
|
|
|
892
1347
|
|
|
893
1348
|
// src/commands/setup.ts
|
|
894
1349
|
import { defineCommand as defineCommand4 } from "citty";
|
|
895
|
-
import
|
|
1350
|
+
import pc7 from "picocolors";
|
|
896
1351
|
|
|
897
1352
|
// src/utils/gh.ts
|
|
898
1353
|
import { execFile as execFileCb2 } from "node:child_process";
|
|
@@ -900,7 +1355,7 @@ function run2(args) {
|
|
|
900
1355
|
return new Promise((resolve) => {
|
|
901
1356
|
execFileCb2("gh", args, (error2, stdout, stderr) => {
|
|
902
1357
|
resolve({
|
|
903
|
-
exitCode: error2 ? error2.code
|
|
1358
|
+
exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
|
|
904
1359
|
stdout: stdout ?? "",
|
|
905
1360
|
stderr: stderr ?? ""
|
|
906
1361
|
});
|
|
@@ -923,7 +1378,10 @@ async function checkGhAuth() {
|
|
|
923
1378
|
return false;
|
|
924
1379
|
}
|
|
925
1380
|
}
|
|
1381
|
+
var SAFE_SLUG = /^[\w.-]+$/;
|
|
926
1382
|
async function checkRepoPermissions(owner, repo) {
|
|
1383
|
+
if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
|
|
1384
|
+
return null;
|
|
927
1385
|
const { exitCode, stdout } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
928
1386
|
if (exitCode !== 0)
|
|
929
1387
|
return null;
|
|
@@ -984,6 +1442,28 @@ async function createPRFill(base, draft) {
|
|
|
984
1442
|
args.push("--draft");
|
|
985
1443
|
return run2(args);
|
|
986
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
|
+
}
|
|
987
1467
|
|
|
988
1468
|
// src/utils/remote.ts
|
|
989
1469
|
function parseRepoFromUrl(url) {
|
|
@@ -1026,7 +1506,7 @@ var setup_default = defineCommand4({
|
|
|
1026
1506
|
workflow = "github-flow";
|
|
1027
1507
|
else if (workflowChoice.startsWith("Git Flow"))
|
|
1028
1508
|
workflow = "git-flow";
|
|
1029
|
-
info(`Workflow: ${
|
|
1509
|
+
info(`Workflow: ${pc7.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
|
|
1030
1510
|
const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
|
|
1031
1511
|
`${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
|
|
1032
1512
|
CONVENTION_DESCRIPTIONS.conventional,
|
|
@@ -1079,8 +1559,8 @@ var setup_default = defineCommand4({
|
|
|
1079
1559
|
detectedRole = roleChoice;
|
|
1080
1560
|
detectionSource = "user selection";
|
|
1081
1561
|
} else {
|
|
1082
|
-
info(`Detected role: ${
|
|
1083
|
-
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?`);
|
|
1084
1564
|
if (!confirmed) {
|
|
1085
1565
|
const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
|
|
1086
1566
|
detectedRole = roleChoice;
|
|
@@ -1125,21 +1605,21 @@ var setup_default = defineCommand4({
|
|
|
1125
1605
|
warn(' echo ".contributerc.json" >> .gitignore');
|
|
1126
1606
|
}
|
|
1127
1607
|
console.log();
|
|
1128
|
-
info(`Workflow: ${
|
|
1129
|
-
info(`Convention: ${
|
|
1130
|
-
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)}`);
|
|
1131
1611
|
if (config.devBranch) {
|
|
1132
|
-
info(`Main: ${
|
|
1612
|
+
info(`Main: ${pc7.bold(config.mainBranch)} | Dev: ${pc7.bold(config.devBranch)}`);
|
|
1133
1613
|
} else {
|
|
1134
|
-
info(`Main: ${
|
|
1614
|
+
info(`Main: ${pc7.bold(config.mainBranch)}`);
|
|
1135
1615
|
}
|
|
1136
|
-
info(`Origin: ${
|
|
1616
|
+
info(`Origin: ${pc7.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc7.bold(config.upstream)}` : ""}`);
|
|
1137
1617
|
}
|
|
1138
1618
|
});
|
|
1139
1619
|
|
|
1140
1620
|
// src/commands/start.ts
|
|
1141
1621
|
import { defineCommand as defineCommand5 } from "citty";
|
|
1142
|
-
import
|
|
1622
|
+
import pc8 from "picocolors";
|
|
1143
1623
|
|
|
1144
1624
|
// src/utils/branch.ts
|
|
1145
1625
|
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
@@ -1150,6 +1630,9 @@ function formatBranchName(prefix, name) {
|
|
|
1150
1630
|
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1151
1631
|
return `${prefix}/${sanitized}`;
|
|
1152
1632
|
}
|
|
1633
|
+
function isValidBranchName(name) {
|
|
1634
|
+
return /^[a-zA-Z0-9._/-]+$/.test(name) && !name.startsWith("/") && !name.endsWith("/");
|
|
1635
|
+
}
|
|
1153
1636
|
function looksLikeNaturalLanguage(input) {
|
|
1154
1637
|
return input.includes(" ") && !input.includes("/");
|
|
1155
1638
|
}
|
|
@@ -1197,24 +1680,31 @@ var start_default = defineCommand5({
|
|
|
1197
1680
|
heading("\uD83C\uDF3F contrib start");
|
|
1198
1681
|
const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
|
|
1199
1682
|
if (useAI) {
|
|
1200
|
-
|
|
1683
|
+
const spinner = createSpinner("Generating branch name suggestion...");
|
|
1201
1684
|
const suggested = await suggestBranchName(branchName, args.model);
|
|
1202
1685
|
if (suggested) {
|
|
1686
|
+
spinner.success("Branch name suggestion ready.");
|
|
1203
1687
|
console.log(`
|
|
1204
|
-
${
|
|
1205
|
-
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?`);
|
|
1206
1690
|
if (accepted) {
|
|
1207
1691
|
branchName = suggested;
|
|
1208
1692
|
} else {
|
|
1209
1693
|
branchName = await inputPrompt("Enter branch name", branchName);
|
|
1210
1694
|
}
|
|
1695
|
+
} else {
|
|
1696
|
+
spinner.fail("AI did not return a branch name suggestion.");
|
|
1211
1697
|
}
|
|
1212
1698
|
}
|
|
1213
1699
|
if (!hasPrefix(branchName, branchPrefixes)) {
|
|
1214
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
1700
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc8.bold(branchName)}:`, branchPrefixes);
|
|
1215
1701
|
branchName = formatBranchName(prefix, branchName);
|
|
1216
1702
|
}
|
|
1217
|
-
|
|
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)}`);
|
|
1218
1708
|
await fetchRemote(syncSource.remote);
|
|
1219
1709
|
const updateResult = await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1220
1710
|
if (updateResult.exitCode !== 0) {}
|
|
@@ -1223,13 +1713,13 @@ var start_default = defineCommand5({
|
|
|
1223
1713
|
error(`Failed to create branch: ${result.stderr}`);
|
|
1224
1714
|
process.exit(1);
|
|
1225
1715
|
}
|
|
1226
|
-
success(`✅ Created ${
|
|
1716
|
+
success(`✅ Created ${pc8.bold(branchName)} from latest ${pc8.bold(baseBranch)}`);
|
|
1227
1717
|
}
|
|
1228
1718
|
});
|
|
1229
1719
|
|
|
1230
1720
|
// src/commands/status.ts
|
|
1231
1721
|
import { defineCommand as defineCommand6 } from "citty";
|
|
1232
|
-
import
|
|
1722
|
+
import pc9 from "picocolors";
|
|
1233
1723
|
var status_default = defineCommand6({
|
|
1234
1724
|
meta: {
|
|
1235
1725
|
name: "status",
|
|
@@ -1246,17 +1736,20 @@ var status_default = defineCommand6({
|
|
|
1246
1736
|
process.exit(1);
|
|
1247
1737
|
}
|
|
1248
1738
|
heading("\uD83D\uDCCA contribute-now status");
|
|
1249
|
-
console.log(` ${
|
|
1250
|
-
console.log(` ${
|
|
1739
|
+
console.log(` ${pc9.dim("Workflow:")} ${pc9.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
1740
|
+
console.log(` ${pc9.dim("Role:")} ${pc9.bold(config.role)}`);
|
|
1251
1741
|
console.log();
|
|
1252
1742
|
await fetchAll();
|
|
1253
1743
|
const currentBranch = await getCurrentBranch();
|
|
1254
1744
|
const { mainBranch, origin, upstream, workflow } = config;
|
|
1255
1745
|
const baseBranch = getBaseBranch(config);
|
|
1256
1746
|
const isContributor = config.role === "contributor";
|
|
1257
|
-
const dirty = await
|
|
1747
|
+
const [dirty, fileStatus] = await Promise.all([
|
|
1748
|
+
hasUncommittedChanges(),
|
|
1749
|
+
getFileStatus()
|
|
1750
|
+
]);
|
|
1258
1751
|
if (dirty) {
|
|
1259
|
-
console.log(` ${
|
|
1752
|
+
console.log(` ${pc9.yellow("⚠")} ${pc9.yellow("Uncommitted changes in working tree")}`);
|
|
1260
1753
|
console.log();
|
|
1261
1754
|
}
|
|
1262
1755
|
const mainRemote = `${origin}/${mainBranch}`;
|
|
@@ -1272,30 +1765,130 @@ var status_default = defineCommand6({
|
|
|
1272
1765
|
if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
1273
1766
|
const branchDiv = await getDivergence(currentBranch, baseBranch);
|
|
1274
1767
|
const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
|
|
1275
|
-
console.log(branchLine +
|
|
1768
|
+
console.log(branchLine + pc9.dim(` (current ${pc9.green("*")})`));
|
|
1276
1769
|
} else if (currentBranch) {
|
|
1277
|
-
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
|
+
}
|
|
1278
1815
|
}
|
|
1279
1816
|
console.log();
|
|
1280
1817
|
}
|
|
1281
1818
|
});
|
|
1282
1819
|
function formatStatus(branch, base, ahead, behind) {
|
|
1283
|
-
const label =
|
|
1820
|
+
const label = pc9.bold(branch.padEnd(20));
|
|
1284
1821
|
if (ahead === 0 && behind === 0) {
|
|
1285
|
-
return ` ${
|
|
1822
|
+
return ` ${pc9.green("✓")} ${label} ${pc9.dim(`in sync with ${base}`)}`;
|
|
1286
1823
|
}
|
|
1287
1824
|
if (ahead > 0 && behind === 0) {
|
|
1288
|
-
return ` ${
|
|
1825
|
+
return ` ${pc9.yellow("↑")} ${label} ${pc9.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
|
|
1289
1826
|
}
|
|
1290
1827
|
if (behind > 0 && ahead === 0) {
|
|
1291
|
-
return ` ${
|
|
1828
|
+
return ` ${pc9.red("↓")} ${label} ${pc9.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
|
|
1292
1829
|
}
|
|
1293
|
-
return ` ${
|
|
1830
|
+
return ` ${pc9.red("⚡")} ${label} ${pc9.yellow(`${ahead} ahead`)}${pc9.dim(", ")}${pc9.red(`${behind} behind`)} ${pc9.dim(base)}`;
|
|
1294
1831
|
}
|
|
1295
1832
|
|
|
1296
1833
|
// src/commands/submit.ts
|
|
1297
1834
|
import { defineCommand as defineCommand7 } from "citty";
|
|
1298
|
-
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
|
+
}
|
|
1299
1892
|
var submit_default = defineCommand7({
|
|
1300
1893
|
meta: {
|
|
1301
1894
|
name: "submit",
|
|
@@ -1336,11 +1929,11 @@ var submit_default = defineCommand7({
|
|
|
1336
1929
|
process.exit(1);
|
|
1337
1930
|
}
|
|
1338
1931
|
if (protectedBranches.includes(currentBranch)) {
|
|
1339
|
-
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.`);
|
|
1340
1933
|
process.exit(1);
|
|
1341
1934
|
}
|
|
1342
1935
|
heading("\uD83D\uDE80 contrib submit");
|
|
1343
|
-
info(`Pushing ${
|
|
1936
|
+
info(`Pushing ${pc10.bold(currentBranch)} to ${origin}...`);
|
|
1344
1937
|
const pushResult = await pushSetUpstream(origin, currentBranch);
|
|
1345
1938
|
if (pushResult.exitCode !== 0) {
|
|
1346
1939
|
error(`Failed to push: ${pushResult.stderr}`);
|
|
@@ -1354,43 +1947,70 @@ var submit_default = defineCommand7({
|
|
|
1354
1947
|
const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
|
|
1355
1948
|
console.log();
|
|
1356
1949
|
info("Create your PR manually:");
|
|
1357
|
-
console.log(` ${
|
|
1950
|
+
console.log(` ${pc10.cyan(prUrl)}`);
|
|
1358
1951
|
} else {
|
|
1359
1952
|
info("gh CLI not available. Create your PR manually on GitHub.");
|
|
1360
1953
|
}
|
|
1361
1954
|
return;
|
|
1362
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
|
+
}
|
|
1363
1962
|
let prTitle = null;
|
|
1364
1963
|
let prBody = null;
|
|
1365
1964
|
if (!args["no-ai"]) {
|
|
1366
|
-
const copilotError = await
|
|
1965
|
+
const [copilotError, commits, diff] = await Promise.all([
|
|
1966
|
+
checkCopilotAvailable(),
|
|
1967
|
+
getLog(baseBranch, "HEAD"),
|
|
1968
|
+
getLogDiff(baseBranch, "HEAD")
|
|
1969
|
+
]);
|
|
1367
1970
|
if (!copilotError) {
|
|
1368
|
-
|
|
1369
|
-
const
|
|
1370
|
-
const diff = await getLogDiff(baseBranch, "HEAD");
|
|
1371
|
-
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);
|
|
1372
1973
|
if (result) {
|
|
1373
1974
|
prTitle = result.title;
|
|
1374
1975
|
prBody = result.body;
|
|
1976
|
+
spinner.success("PR description generated.");
|
|
1375
1977
|
console.log(`
|
|
1376
|
-
${
|
|
1978
|
+
${pc10.dim("AI title:")} ${pc10.bold(pc10.cyan(prTitle))}`);
|
|
1377
1979
|
console.log(`
|
|
1378
|
-
${
|
|
1379
|
-
console.log(
|
|
1980
|
+
${pc10.dim("AI body preview:")}`);
|
|
1981
|
+
console.log(pc10.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
|
|
1380
1982
|
} else {
|
|
1381
|
-
|
|
1983
|
+
spinner.fail("AI did not return a PR description.");
|
|
1382
1984
|
}
|
|
1383
1985
|
} else {
|
|
1384
1986
|
warn(`AI unavailable: ${copilotError}`);
|
|
1385
1987
|
}
|
|
1386
1988
|
}
|
|
1989
|
+
const CANCEL = "Cancel";
|
|
1990
|
+
const SQUASH_LOCAL = `Squash merge to ${baseBranch} locally (no PR)`;
|
|
1387
1991
|
if (prTitle && prBody) {
|
|
1388
|
-
const
|
|
1992
|
+
const choices = [
|
|
1389
1993
|
"Use AI description",
|
|
1390
1994
|
"Edit title",
|
|
1391
1995
|
"Write manually",
|
|
1392
1996
|
"Use gh --fill (auto-fill from commits)"
|
|
1393
|
-
]
|
|
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
|
+
}
|
|
1394
2014
|
if (action === "Use AI description") {} else if (action === "Edit title") {
|
|
1395
2015
|
prTitle = await inputPrompt("PR title", prTitle);
|
|
1396
2016
|
} else if (action === "Write manually") {
|
|
@@ -1406,8 +2026,26 @@ ${pc9.dim("AI body preview:")}`);
|
|
|
1406
2026
|
return;
|
|
1407
2027
|
}
|
|
1408
2028
|
} else {
|
|
1409
|
-
const
|
|
1410
|
-
|
|
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") {
|
|
1411
2049
|
prTitle = await inputPrompt("PR title");
|
|
1412
2050
|
prBody = await inputPrompt("PR body (markdown)");
|
|
1413
2051
|
} else {
|
|
@@ -1440,7 +2078,7 @@ ${pc9.dim("AI body preview:")}`);
|
|
|
1440
2078
|
|
|
1441
2079
|
// src/commands/sync.ts
|
|
1442
2080
|
import { defineCommand as defineCommand8 } from "citty";
|
|
1443
|
-
import
|
|
2081
|
+
import pc11 from "picocolors";
|
|
1444
2082
|
var sync_default = defineCommand8({
|
|
1445
2083
|
meta: {
|
|
1446
2084
|
name: "sync",
|
|
@@ -1483,12 +2121,12 @@ var sync_default = defineCommand8({
|
|
|
1483
2121
|
}
|
|
1484
2122
|
const div = await getDivergence(baseBranch, syncSource.ref);
|
|
1485
2123
|
if (div.ahead > 0 || div.behind > 0) {
|
|
1486
|
-
info(`${
|
|
2124
|
+
info(`${pc11.bold(baseBranch)} is ${pc11.yellow(`${div.ahead} ahead`)} and ${pc11.red(`${div.behind} behind`)} ${syncSource.ref}`);
|
|
1487
2125
|
} else {
|
|
1488
|
-
info(`${
|
|
2126
|
+
info(`${pc11.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
|
|
1489
2127
|
}
|
|
1490
2128
|
if (!args.yes) {
|
|
1491
|
-
const ok = await confirmPrompt(`This will pull ${
|
|
2129
|
+
const ok = await confirmPrompt(`This will pull ${pc11.bold(syncSource.ref)} into local ${pc11.bold(baseBranch)}.`);
|
|
1492
2130
|
if (!ok)
|
|
1493
2131
|
process.exit(0);
|
|
1494
2132
|
}
|
|
@@ -1506,7 +2144,7 @@ var sync_default = defineCommand8({
|
|
|
1506
2144
|
if (hasDevBranch(workflow) && role === "maintainer") {
|
|
1507
2145
|
const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
|
|
1508
2146
|
if (mainDiv.behind > 0) {
|
|
1509
|
-
info(`Also syncing ${
|
|
2147
|
+
info(`Also syncing ${pc11.bold(config.mainBranch)}...`);
|
|
1510
2148
|
const mainCoResult = await checkoutBranch(config.mainBranch);
|
|
1511
2149
|
if (mainCoResult.exitCode === 0) {
|
|
1512
2150
|
const mainPullResult = await pullBranch(origin, config.mainBranch);
|
|
@@ -1521,9 +2159,9 @@ var sync_default = defineCommand8({
|
|
|
1521
2159
|
});
|
|
1522
2160
|
|
|
1523
2161
|
// src/commands/update.ts
|
|
1524
|
-
import { readFileSync as
|
|
2162
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1525
2163
|
import { defineCommand as defineCommand9 } from "citty";
|
|
1526
|
-
import
|
|
2164
|
+
import pc12 from "picocolors";
|
|
1527
2165
|
var update_default = defineCommand9({
|
|
1528
2166
|
meta: {
|
|
1529
2167
|
name: "update",
|
|
@@ -1559,7 +2197,7 @@ var update_default = defineCommand9({
|
|
|
1559
2197
|
process.exit(1);
|
|
1560
2198
|
}
|
|
1561
2199
|
if (protectedBranches.includes(currentBranch)) {
|
|
1562
|
-
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.`);
|
|
1563
2201
|
process.exit(1);
|
|
1564
2202
|
}
|
|
1565
2203
|
if (await hasUncommittedChanges()) {
|
|
@@ -1567,7 +2205,7 @@ var update_default = defineCommand9({
|
|
|
1567
2205
|
process.exit(1);
|
|
1568
2206
|
}
|
|
1569
2207
|
heading("\uD83D\uDD03 contrib update");
|
|
1570
|
-
info(`Updating ${
|
|
2208
|
+
info(`Updating ${pc12.bold(currentBranch)} with latest ${pc12.bold(baseBranch)}...`);
|
|
1571
2209
|
await fetchRemote(syncSource.remote);
|
|
1572
2210
|
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1573
2211
|
const rebaseResult = await rebase(baseBranch);
|
|
@@ -1582,7 +2220,7 @@ var update_default = defineCommand9({
|
|
|
1582
2220
|
let conflictDiff = "";
|
|
1583
2221
|
for (const file of conflictFiles.slice(0, 3)) {
|
|
1584
2222
|
try {
|
|
1585
|
-
const content =
|
|
2223
|
+
const content = readFileSync4(file, "utf-8");
|
|
1586
2224
|
if (content.includes("<<<<<<<")) {
|
|
1587
2225
|
conflictDiff += `
|
|
1588
2226
|
--- ${file} ---
|
|
@@ -1592,33 +2230,37 @@ ${content.slice(0, 2000)}
|
|
|
1592
2230
|
} catch {}
|
|
1593
2231
|
}
|
|
1594
2232
|
if (conflictDiff) {
|
|
2233
|
+
const spinner = createSpinner("Analyzing conflicts with AI...");
|
|
1595
2234
|
const suggestion = await suggestConflictResolution(conflictDiff, args.model);
|
|
1596
2235
|
if (suggestion) {
|
|
2236
|
+
spinner.success("AI conflict guidance ready.");
|
|
1597
2237
|
console.log(`
|
|
1598
|
-
${
|
|
1599
|
-
console.log(
|
|
2238
|
+
${pc12.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
2239
|
+
console.log(pc12.dim("─".repeat(60)));
|
|
1600
2240
|
console.log(suggestion);
|
|
1601
|
-
console.log(
|
|
2241
|
+
console.log(pc12.dim("─".repeat(60)));
|
|
1602
2242
|
console.log();
|
|
2243
|
+
} else {
|
|
2244
|
+
spinner.fail("AI could not analyze the conflicts.");
|
|
1603
2245
|
}
|
|
1604
2246
|
}
|
|
1605
2247
|
}
|
|
1606
2248
|
}
|
|
1607
|
-
console.log(
|
|
2249
|
+
console.log(pc12.bold("To resolve:"));
|
|
1608
2250
|
console.log(` 1. Fix conflicts in the affected files`);
|
|
1609
|
-
console.log(` 2. ${
|
|
1610
|
-
console.log(` 3. ${
|
|
2251
|
+
console.log(` 2. ${pc12.cyan("git add <resolved-files>")}`);
|
|
2252
|
+
console.log(` 3. ${pc12.cyan("git rebase --continue")}`);
|
|
1611
2253
|
console.log();
|
|
1612
|
-
console.log(` Or abort: ${
|
|
2254
|
+
console.log(` Or abort: ${pc12.cyan("git rebase --abort")}`);
|
|
1613
2255
|
process.exit(1);
|
|
1614
2256
|
}
|
|
1615
|
-
success(`✅ ${
|
|
2257
|
+
success(`✅ ${pc12.bold(currentBranch)} has been rebased onto latest ${pc12.bold(baseBranch)}`);
|
|
1616
2258
|
}
|
|
1617
2259
|
});
|
|
1618
2260
|
|
|
1619
2261
|
// src/commands/validate.ts
|
|
1620
2262
|
import { defineCommand as defineCommand10 } from "citty";
|
|
1621
|
-
import
|
|
2263
|
+
import pc13 from "picocolors";
|
|
1622
2264
|
var validate_default = defineCommand10({
|
|
1623
2265
|
meta: {
|
|
1624
2266
|
name: "validate",
|
|
@@ -1649,7 +2291,7 @@ var validate_default = defineCommand10({
|
|
|
1649
2291
|
}
|
|
1650
2292
|
const errors = getValidationError(convention);
|
|
1651
2293
|
for (const line of errors) {
|
|
1652
|
-
console.error(
|
|
2294
|
+
console.error(pc13.red(` ✗ ${line}`));
|
|
1653
2295
|
}
|
|
1654
2296
|
process.exit(1);
|
|
1655
2297
|
}
|
|
@@ -1657,11 +2299,11 @@ var validate_default = defineCommand10({
|
|
|
1657
2299
|
|
|
1658
2300
|
// src/ui/banner.ts
|
|
1659
2301
|
import figlet from "figlet";
|
|
1660
|
-
import
|
|
2302
|
+
import pc14 from "picocolors";
|
|
1661
2303
|
// package.json
|
|
1662
2304
|
var package_default = {
|
|
1663
2305
|
name: "contribute-now",
|
|
1664
|
-
version: "0.2.0-dev.
|
|
2306
|
+
version: "0.2.0-dev.d4b7ede",
|
|
1665
2307
|
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1666
2308
|
type: "module",
|
|
1667
2309
|
bin: {
|
|
@@ -1705,6 +2347,7 @@ var package_default = {
|
|
|
1705
2347
|
url: "git+https://github.com/warengonzaga/contribute-now.git"
|
|
1706
2348
|
},
|
|
1707
2349
|
dependencies: {
|
|
2350
|
+
"@clack/prompts": "^1.0.1",
|
|
1708
2351
|
"@github/copilot-sdk": "^0.1.25",
|
|
1709
2352
|
"@wgtechlabs/log-engine": "^2.3.1",
|
|
1710
2353
|
citty: "^0.1.6",
|
|
@@ -1734,14 +2377,14 @@ function getAuthor() {
|
|
|
1734
2377
|
return typeof package_default.author === "string" ? package_default.author : "unknown";
|
|
1735
2378
|
}
|
|
1736
2379
|
function showBanner(showLinks = false) {
|
|
1737
|
-
console.log(
|
|
2380
|
+
console.log(pc14.cyan(`
|
|
1738
2381
|
${LOGO}`));
|
|
1739
|
-
console.log(` ${
|
|
2382
|
+
console.log(` ${pc14.dim(`v${getVersion()}`)} ${pc14.dim("—")} ${pc14.dim(`Built by ${getAuthor()}`)}`);
|
|
1740
2383
|
if (showLinks) {
|
|
1741
2384
|
console.log();
|
|
1742
|
-
console.log(` ${
|
|
1743
|
-
console.log(` ${
|
|
1744
|
-
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")}`);
|
|
1745
2388
|
}
|
|
1746
2389
|
console.log();
|
|
1747
2390
|
}
|
|
@@ -1780,4 +2423,6 @@ var main = defineCommand11({
|
|
|
1780
2423
|
}
|
|
1781
2424
|
}
|
|
1782
2425
|
});
|
|
1783
|
-
runMain(main)
|
|
2426
|
+
runMain(main).then(() => {
|
|
2427
|
+
process.exit(0);
|
|
2428
|
+
});
|