contribute-now 0.1.2-staging.c209cc7 → 0.2.0-dev.0de9dbd
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 +2229 -807
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/index.ts
|
|
4
|
-
import { defineCommand as
|
|
6
|
+
import { defineCommand as defineCommand12, runMain } from "citty";
|
|
5
7
|
|
|
6
8
|
// src/commands/clean.ts
|
|
7
9
|
import { defineCommand } from "citty";
|
|
8
|
-
import
|
|
10
|
+
import pc4 from "picocolors";
|
|
11
|
+
|
|
12
|
+
// src/utils/branch.ts
|
|
13
|
+
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
14
|
+
function hasPrefix(branchName, prefixes = DEFAULT_PREFIXES) {
|
|
15
|
+
return prefixes.some((p) => branchName.startsWith(`${p}/`));
|
|
16
|
+
}
|
|
17
|
+
function formatBranchName(prefix, name) {
|
|
18
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
19
|
+
return `${prefix}/${sanitized}`;
|
|
20
|
+
}
|
|
21
|
+
function isValidBranchName(name) {
|
|
22
|
+
return /^[a-zA-Z0-9._/-]+$/.test(name) && !name.startsWith("/") && !name.endsWith("/");
|
|
23
|
+
}
|
|
24
|
+
function looksLikeNaturalLanguage(input) {
|
|
25
|
+
return input.includes(" ") && !input.includes("/");
|
|
26
|
+
}
|
|
9
27
|
|
|
10
28
|
// src/utils/config.ts
|
|
11
29
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -14,13 +32,20 @@ var CONFIG_FILENAME = ".contributerc.json";
|
|
|
14
32
|
function getConfigPath(cwd = process.cwd()) {
|
|
15
33
|
return join(cwd, CONFIG_FILENAME);
|
|
16
34
|
}
|
|
35
|
+
function configExists(cwd = process.cwd()) {
|
|
36
|
+
return existsSync(getConfigPath(cwd));
|
|
37
|
+
}
|
|
17
38
|
function readConfig(cwd = process.cwd()) {
|
|
18
39
|
const path = getConfigPath(cwd);
|
|
19
40
|
if (!existsSync(path))
|
|
20
41
|
return null;
|
|
21
42
|
try {
|
|
22
43
|
const raw = readFileSync(path, "utf-8");
|
|
23
|
-
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
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") {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
24
49
|
} catch {
|
|
25
50
|
return null;
|
|
26
51
|
}
|
|
@@ -56,187 +81,800 @@ function getDefaultConfig() {
|
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
// src/utils/confirm.ts
|
|
84
|
+
import * as clack from "@clack/prompts";
|
|
59
85
|
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;
|
|
86
|
+
function handleCancel(value) {
|
|
87
|
+
if (clack.isCancel(value)) {
|
|
88
|
+
clack.cancel("Cancelled.");
|
|
89
|
+
process.exit(0);
|
|
75
90
|
}
|
|
76
|
-
|
|
91
|
+
}
|
|
92
|
+
async function confirmPrompt(message) {
|
|
93
|
+
const result = await clack.confirm({ message });
|
|
94
|
+
handleCancel(result);
|
|
95
|
+
return result;
|
|
77
96
|
}
|
|
78
97
|
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();
|
|
98
|
+
const result = await clack.select({
|
|
99
|
+
message,
|
|
100
|
+
options: choices.map((choice) => ({ value: choice, label: choice }))
|
|
92
101
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return choices[index];
|
|
96
|
-
}
|
|
97
|
-
return choices[0];
|
|
102
|
+
handleCancel(result);
|
|
103
|
+
return result;
|
|
98
104
|
}
|
|
99
105
|
async function inputPrompt(message, defaultValue) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const result = await clack.text({
|
|
107
|
+
message,
|
|
108
|
+
placeholder: defaultValue,
|
|
109
|
+
defaultValue
|
|
110
|
+
});
|
|
111
|
+
handleCancel(result);
|
|
112
|
+
return result || defaultValue || "";
|
|
113
|
+
}
|
|
114
|
+
async function multiSelectPrompt(message, choices) {
|
|
115
|
+
const result = await clack.multiselect({
|
|
116
|
+
message: `${message} ${pc.dim("(space to toggle, enter to confirm)")}`,
|
|
117
|
+
options: choices.map((choice) => ({ value: choice, label: choice })),
|
|
118
|
+
required: false
|
|
110
119
|
});
|
|
111
|
-
|
|
120
|
+
handleCancel(result);
|
|
121
|
+
return result;
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
// src/utils/
|
|
115
|
-
import {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
// src/utils/copilot.ts
|
|
125
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
126
|
+
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
|
|
127
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
128
|
+
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.
|
|
129
|
+
Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
|
|
130
|
+
var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
131
|
+
Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
|
|
132
|
+
Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
|
|
133
|
+
Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
|
|
134
|
+
Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
|
|
135
|
+
WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
|
|
136
|
+
function getGroupingSystemPrompt(convention) {
|
|
137
|
+
const conventionBlock = convention === "conventional" ? `Use Conventional Commit format: <type>[(<scope>)]: <description>
|
|
138
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert` : `Use Clean Commit format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
139
|
+
Emoji/type table:
|
|
140
|
+
\uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release`;
|
|
141
|
+
return `You are a smart commit grouping assistant. Given a list of changed files and their diffs, group related changes into logical atomic commits.
|
|
142
|
+
|
|
143
|
+
${conventionBlock}
|
|
144
|
+
|
|
145
|
+
Return a JSON array of commit groups with this EXACT structure (no markdown fences, no explanation):
|
|
146
|
+
[
|
|
147
|
+
{
|
|
148
|
+
"files": ["path/to/file1.ts", "path/to/file2.ts"],
|
|
149
|
+
"message": "<commit message following the convention above>"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
Rules:
|
|
154
|
+
- Group files that are logically related (e.g. a utility and its tests, a feature and its types)
|
|
155
|
+
- Each group should represent ONE logical change
|
|
156
|
+
- Every file must appear in exactly one group
|
|
157
|
+
- Commit messages must follow the convention, be concise, imperative, max 72 chars
|
|
158
|
+
- Order groups so foundational changes come first (types, utils) and consumers come after
|
|
159
|
+
- Return ONLY the JSON array, nothing else`;
|
|
160
|
+
}
|
|
161
|
+
var BRANCH_NAME_SYSTEM_PROMPT = `You are a git branch name generator. Your ONLY job is to output a single git branch name. NOTHING ELSE.
|
|
162
|
+
Output format: <prefix>/<kebab-case-name>
|
|
163
|
+
Valid prefixes: feature, fix, docs, chore, test, refactor
|
|
164
|
+
Rules: lowercase, kebab-case, 2-5 words after the prefix, no punctuation.
|
|
165
|
+
CRITICAL: Output ONLY the branch name on a single line. No explanation. No markdown. No questions. No other text.
|
|
166
|
+
Examples: fix/login-timeout | feature/user-profile-page | docs/update-readme | chore/update-pr-title`;
|
|
167
|
+
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..."}
|
|
168
|
+
IMPORTANT: The title must capture the overall theme or goal of the PR — NOT enumerate individual changes. Think: what problem does this PR solve or what capability does it add? Keep it focused and specific but high-level.`;
|
|
169
|
+
function getPRDescriptionSystemPrompt(convention) {
|
|
170
|
+
if (convention === "clean-commit") {
|
|
171
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
172
|
+
CRITICAL: The PR title MUST follow the Clean Commit format exactly: <emoji> <type>: <description>
|
|
173
|
+
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
|
|
174
|
+
Title examples: \uD83D\uDCE6 new: add user authentication | \uD83D\uDD27 update: improve error handling | \uD83D\uDDD1️ remove: drop legacy API
|
|
175
|
+
Rules: title follows convention, present tense, max 72 chars, describes the PR theme not individual commits; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
176
|
+
}
|
|
177
|
+
if (convention === "conventional") {
|
|
178
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
179
|
+
CRITICAL: The PR title MUST follow Conventional Commits format: <type>[(<scope>)]: <description>
|
|
180
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
181
|
+
Title examples: feat: add user authentication | fix(auth): resolve token expiry | docs: update contributing guide
|
|
182
|
+
Rules: title follows convention, present tense, max 72 chars, describes the PR theme not individual commits; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
183
|
+
}
|
|
184
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
185
|
+
Rules: title concise present tense, describes the PR theme not individual commits; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
186
|
+
}
|
|
187
|
+
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `Git merge conflict advisor. Explain each side, suggest resolution strategy. Never auto-resolve — guidance only. Be concise and actionable.`;
|
|
188
|
+
function suppressSubprocessWarnings() {
|
|
189
|
+
process.env.NODE_NO_WARNINGS = "1";
|
|
190
|
+
}
|
|
191
|
+
function withTimeout(promise, ms) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const timer = setTimeout(() => reject(new Error(`Copilot request timed out after ${ms / 1000}s`)), ms);
|
|
194
|
+
promise.then((val) => {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
resolve(val);
|
|
197
|
+
}, (err) => {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
reject(err);
|
|
124
200
|
});
|
|
125
201
|
});
|
|
126
202
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
203
|
+
var COPILOT_TIMEOUT_MS = 30000;
|
|
204
|
+
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
205
|
+
async function checkCopilotAvailable() {
|
|
206
|
+
try {
|
|
207
|
+
const client = await getManagedClient();
|
|
208
|
+
try {
|
|
209
|
+
await client.ping();
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
212
|
+
if (msg.includes("auth") || msg.includes("token") || msg.includes("401") || msg.includes("403")) {
|
|
213
|
+
return "Copilot authentication failed. Run `gh auth login` to refresh your token.";
|
|
214
|
+
}
|
|
215
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("timeout") || msg.includes("network")) {
|
|
216
|
+
return "Could not reach GitHub Copilot service. Check your internet connection.";
|
|
217
|
+
}
|
|
218
|
+
return `Copilot health check failed: ${msg}`;
|
|
219
|
+
}
|
|
134
220
|
return null;
|
|
135
|
-
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
223
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
224
|
+
return "Copilot CLI binary not found. Ensure GitHub Copilot is installed and your gh CLI is up to date.";
|
|
225
|
+
}
|
|
226
|
+
return `Failed to start Copilot service: ${msg}`;
|
|
227
|
+
}
|
|
136
228
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
229
|
+
var _managedClient = null;
|
|
230
|
+
var _clientStarted = false;
|
|
231
|
+
async function getManagedClient() {
|
|
232
|
+
if (!_managedClient || !_clientStarted) {
|
|
233
|
+
suppressSubprocessWarnings();
|
|
234
|
+
_managedClient = new CopilotClient;
|
|
235
|
+
await _managedClient.start();
|
|
236
|
+
_clientStarted = true;
|
|
237
|
+
const cleanup = () => {
|
|
238
|
+
if (_managedClient && _clientStarted) {
|
|
239
|
+
try {
|
|
240
|
+
_managedClient.stop();
|
|
241
|
+
} catch {}
|
|
242
|
+
_clientStarted = false;
|
|
243
|
+
_managedClient = null;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
process.once("exit", cleanup);
|
|
247
|
+
process.once("SIGINT", cleanup);
|
|
248
|
+
process.once("SIGTERM", cleanup);
|
|
249
|
+
}
|
|
250
|
+
return _managedClient;
|
|
143
251
|
}
|
|
144
|
-
async function
|
|
145
|
-
const
|
|
146
|
-
|
|
252
|
+
async function callCopilot(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
|
|
253
|
+
const client = await getManagedClient();
|
|
254
|
+
const sessionConfig = {
|
|
255
|
+
systemMessage: { mode: "replace", content: systemMessage }
|
|
256
|
+
};
|
|
257
|
+
if (model)
|
|
258
|
+
sessionConfig.model = model;
|
|
259
|
+
const session = await client.createSession(sessionConfig);
|
|
260
|
+
try {
|
|
261
|
+
const response = await withTimeout(session.sendAndWait({ prompt: userMessage }), timeoutMs);
|
|
262
|
+
if (!response?.data?.content)
|
|
263
|
+
return null;
|
|
264
|
+
return response.data.content;
|
|
265
|
+
} finally {
|
|
266
|
+
await session.destroy();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function getCommitSystemPrompt(convention) {
|
|
270
|
+
if (convention === "conventional")
|
|
271
|
+
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
272
|
+
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
273
|
+
}
|
|
274
|
+
function extractJson(raw) {
|
|
275
|
+
let text2 = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
276
|
+
if (text2.startsWith("[") || text2.startsWith("{"))
|
|
277
|
+
return text2;
|
|
278
|
+
const arrayStart = text2.indexOf("[");
|
|
279
|
+
const objStart = text2.indexOf("{");
|
|
280
|
+
let start;
|
|
281
|
+
let closeChar;
|
|
282
|
+
if (arrayStart === -1 && objStart === -1)
|
|
283
|
+
return text2;
|
|
284
|
+
if (arrayStart === -1) {
|
|
285
|
+
start = objStart;
|
|
286
|
+
closeChar = "}";
|
|
287
|
+
} else if (objStart === -1) {
|
|
288
|
+
start = arrayStart;
|
|
289
|
+
closeChar = "]";
|
|
290
|
+
} else if (arrayStart < objStart) {
|
|
291
|
+
start = arrayStart;
|
|
292
|
+
closeChar = "]";
|
|
293
|
+
} else {
|
|
294
|
+
start = objStart;
|
|
295
|
+
closeChar = "}";
|
|
296
|
+
}
|
|
297
|
+
const end = text2.lastIndexOf(closeChar);
|
|
298
|
+
if (end > start) {
|
|
299
|
+
text2 = text2.slice(start, end + 1);
|
|
300
|
+
}
|
|
301
|
+
return text2;
|
|
302
|
+
}
|
|
303
|
+
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
304
|
+
try {
|
|
305
|
+
const multiFileHint = stagedFiles.length > 1 ? `
|
|
306
|
+
|
|
307
|
+
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.` : "";
|
|
308
|
+
const userMessage = `Generate a commit message for these staged changes:
|
|
309
|
+
|
|
310
|
+
Files: ${stagedFiles.join(", ")}
|
|
311
|
+
|
|
312
|
+
Diff:
|
|
313
|
+
${diff.slice(0, 4000)}${multiFileHint}`;
|
|
314
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
315
|
+
return result?.trim() ?? null;
|
|
316
|
+
} catch {
|
|
147
317
|
return null;
|
|
148
|
-
|
|
318
|
+
}
|
|
149
319
|
}
|
|
150
|
-
async function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
320
|
+
async function generatePRDescription(commits, diff, model, convention = "clean-commit") {
|
|
321
|
+
try {
|
|
322
|
+
const userMessage = `Generate a PR description for these changes:
|
|
323
|
+
|
|
324
|
+
Commits:
|
|
325
|
+
${commits.join(`
|
|
326
|
+
`)}
|
|
327
|
+
|
|
328
|
+
Diff (truncated):
|
|
329
|
+
${diff.slice(0, 4000)}`;
|
|
330
|
+
const result = await callCopilot(getPRDescriptionSystemPrompt(convention), userMessage, model);
|
|
331
|
+
if (!result)
|
|
332
|
+
return null;
|
|
333
|
+
const cleaned = extractJson(result);
|
|
334
|
+
return JSON.parse(cleaned);
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
155
338
|
}
|
|
156
|
-
async function
|
|
157
|
-
|
|
339
|
+
async function suggestBranchName(description, model) {
|
|
340
|
+
try {
|
|
341
|
+
const result = await callCopilot(BRANCH_NAME_SYSTEM_PROMPT, description, model);
|
|
342
|
+
const trimmed = result?.trim() ?? null;
|
|
343
|
+
if (trimmed && /^[a-z]+\/[a-z0-9-]+$/.test(trimmed)) {
|
|
344
|
+
return trimmed;
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
158
350
|
}
|
|
159
|
-
async function
|
|
160
|
-
|
|
351
|
+
async function suggestConflictResolution(conflictDiff, model) {
|
|
352
|
+
try {
|
|
353
|
+
const userMessage = `Help me resolve this merge conflict:
|
|
354
|
+
|
|
355
|
+
${conflictDiff.slice(0, 4000)}`;
|
|
356
|
+
const result = await callCopilot(CONFLICT_RESOLUTION_SYSTEM_PROMPT, userMessage, model);
|
|
357
|
+
return result?.trim() ?? null;
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
161
361
|
}
|
|
162
|
-
async function
|
|
163
|
-
|
|
362
|
+
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
363
|
+
const userMessage = `Group these changed files into logical atomic commits:
|
|
364
|
+
|
|
365
|
+
Files:
|
|
366
|
+
${files.join(`
|
|
367
|
+
`)}
|
|
368
|
+
|
|
369
|
+
Diffs (truncated):
|
|
370
|
+
${diffs.slice(0, 6000)}`;
|
|
371
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
372
|
+
if (!result) {
|
|
373
|
+
throw new Error("AI returned an empty response");
|
|
374
|
+
}
|
|
375
|
+
const cleaned = extractJson(result);
|
|
376
|
+
let parsed;
|
|
377
|
+
try {
|
|
378
|
+
parsed = JSON.parse(cleaned);
|
|
379
|
+
} catch {
|
|
380
|
+
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
381
|
+
}
|
|
382
|
+
const groups = parsed;
|
|
383
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
384
|
+
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
385
|
+
}
|
|
386
|
+
for (const group of groups) {
|
|
387
|
+
if (!Array.isArray(group.files) || typeof group.message !== "string") {
|
|
388
|
+
throw new Error("AI returned groups with invalid structure (missing files or message)");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return groups;
|
|
164
392
|
}
|
|
165
|
-
async function
|
|
166
|
-
const
|
|
167
|
-
|
|
393
|
+
async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
|
|
394
|
+
const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
|
|
395
|
+
`);
|
|
396
|
+
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
397
|
+
|
|
398
|
+
Groups:
|
|
399
|
+
${groupSummary}
|
|
400
|
+
|
|
401
|
+
Diffs (truncated):
|
|
402
|
+
${diffs.slice(0, 6000)}`;
|
|
403
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
404
|
+
if (!result)
|
|
405
|
+
return groups;
|
|
406
|
+
try {
|
|
407
|
+
const cleaned = extractJson(result);
|
|
408
|
+
const parsed = JSON.parse(cleaned);
|
|
409
|
+
if (!Array.isArray(parsed) || parsed.length !== groups.length)
|
|
410
|
+
return groups;
|
|
411
|
+
return groups.map((g, i) => ({
|
|
412
|
+
files: g.files,
|
|
413
|
+
message: typeof parsed[i]?.message === "string" ? parsed[i].message : g.message
|
|
414
|
+
}));
|
|
415
|
+
} catch {
|
|
416
|
+
return groups;
|
|
417
|
+
}
|
|
168
418
|
}
|
|
169
|
-
async function
|
|
170
|
-
|
|
419
|
+
async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
|
|
420
|
+
try {
|
|
421
|
+
const userMessage = `Generate a single commit message for these files:
|
|
422
|
+
|
|
423
|
+
Files: ${files.join(", ")}
|
|
424
|
+
|
|
425
|
+
Diff:
|
|
426
|
+
${diffs.slice(0, 4000)}`;
|
|
427
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
428
|
+
return result?.trim() ?? null;
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
171
432
|
}
|
|
172
|
-
|
|
173
|
-
|
|
433
|
+
|
|
434
|
+
// src/utils/gh.ts
|
|
435
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
436
|
+
function run(args) {
|
|
437
|
+
return new Promise((resolve) => {
|
|
438
|
+
execFileCb("gh", args, (error, stdout, stderr) => {
|
|
439
|
+
resolve({
|
|
440
|
+
exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
|
|
441
|
+
stdout: stdout ?? "",
|
|
442
|
+
stderr: stderr ?? ""
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
174
446
|
}
|
|
175
|
-
async function
|
|
176
|
-
|
|
447
|
+
async function checkGhInstalled() {
|
|
448
|
+
try {
|
|
449
|
+
const { exitCode } = await run(["--version"]);
|
|
450
|
+
return exitCode === 0;
|
|
451
|
+
} catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
177
454
|
}
|
|
178
|
-
async function
|
|
179
|
-
|
|
180
|
-
|
|
455
|
+
async function checkGhAuth() {
|
|
456
|
+
try {
|
|
457
|
+
const { exitCode } = await run(["auth", "status"]);
|
|
458
|
+
return exitCode === 0;
|
|
459
|
+
} catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
181
462
|
}
|
|
182
|
-
|
|
183
|
-
|
|
463
|
+
var SAFE_SLUG = /^[\w.-]+$/;
|
|
464
|
+
async function checkRepoPermissions(owner, repo) {
|
|
465
|
+
if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
|
|
466
|
+
return null;
|
|
467
|
+
const { exitCode, stdout } = await run(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
184
468
|
if (exitCode !== 0)
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
469
|
+
return null;
|
|
470
|
+
try {
|
|
471
|
+
return JSON.parse(stdout.trim());
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
188
475
|
}
|
|
189
|
-
async function
|
|
190
|
-
const { exitCode, stdout } = await run(["
|
|
476
|
+
async function isRepoFork() {
|
|
477
|
+
const { exitCode, stdout } = await run(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
|
|
191
478
|
if (exitCode !== 0)
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
|
|
479
|
+
return null;
|
|
480
|
+
const val = stdout.trim();
|
|
481
|
+
if (val === "true")
|
|
482
|
+
return true;
|
|
483
|
+
if (val === "false")
|
|
484
|
+
return false;
|
|
485
|
+
return null;
|
|
195
486
|
}
|
|
196
|
-
async function
|
|
487
|
+
async function getCurrentRepoInfo() {
|
|
197
488
|
const { exitCode, stdout } = await run([
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"--
|
|
201
|
-
|
|
489
|
+
"repo",
|
|
490
|
+
"view",
|
|
491
|
+
"--json",
|
|
492
|
+
"nameWithOwner",
|
|
493
|
+
"-q",
|
|
494
|
+
".nameWithOwner"
|
|
202
495
|
]);
|
|
203
496
|
if (exitCode !== 0)
|
|
204
|
-
return
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
497
|
+
return null;
|
|
498
|
+
const nameWithOwner = stdout.trim();
|
|
499
|
+
if (!nameWithOwner)
|
|
500
|
+
return null;
|
|
501
|
+
const [owner, repo] = nameWithOwner.split("/");
|
|
502
|
+
if (!owner || !repo)
|
|
503
|
+
return null;
|
|
504
|
+
return { owner, repo };
|
|
505
|
+
}
|
|
506
|
+
async function createPR(options) {
|
|
507
|
+
const args = [
|
|
508
|
+
"pr",
|
|
509
|
+
"create",
|
|
510
|
+
"--base",
|
|
511
|
+
options.base,
|
|
512
|
+
"--title",
|
|
513
|
+
options.title,
|
|
514
|
+
"--body",
|
|
515
|
+
options.body
|
|
516
|
+
];
|
|
517
|
+
if (options.draft)
|
|
518
|
+
args.push("--draft");
|
|
519
|
+
return run(args);
|
|
520
|
+
}
|
|
521
|
+
async function createPRFill(base, draft) {
|
|
522
|
+
const args = ["pr", "create", "--base", base, "--fill"];
|
|
523
|
+
if (draft)
|
|
524
|
+
args.push("--draft");
|
|
525
|
+
return run(args);
|
|
526
|
+
}
|
|
527
|
+
async function getPRForBranch(headBranch) {
|
|
528
|
+
const { exitCode, stdout } = await run([
|
|
529
|
+
"pr",
|
|
530
|
+
"list",
|
|
531
|
+
"--head",
|
|
532
|
+
headBranch,
|
|
533
|
+
"--state",
|
|
534
|
+
"open",
|
|
535
|
+
"--json",
|
|
536
|
+
"number,url,title,state",
|
|
537
|
+
"--limit",
|
|
538
|
+
"1"
|
|
539
|
+
]);
|
|
540
|
+
if (exitCode !== 0)
|
|
541
|
+
return null;
|
|
542
|
+
try {
|
|
543
|
+
const prs = JSON.parse(stdout.trim());
|
|
544
|
+
return prs.length > 0 ? prs[0] : null;
|
|
545
|
+
} catch {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function getMergedPRForBranch(headBranch) {
|
|
550
|
+
const { exitCode, stdout } = await run([
|
|
551
|
+
"pr",
|
|
552
|
+
"list",
|
|
553
|
+
"--head",
|
|
554
|
+
headBranch,
|
|
555
|
+
"--state",
|
|
556
|
+
"merged",
|
|
557
|
+
"--json",
|
|
558
|
+
"number,url,title,state",
|
|
559
|
+
"--limit",
|
|
560
|
+
"1"
|
|
561
|
+
]);
|
|
562
|
+
if (exitCode !== 0)
|
|
563
|
+
return null;
|
|
564
|
+
try {
|
|
565
|
+
const prs = JSON.parse(stdout.trim());
|
|
566
|
+
return prs.length > 0 ? prs[0] : null;
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/utils/git.ts
|
|
573
|
+
import { execFile as execFileCb2 } from "node:child_process";
|
|
574
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
575
|
+
import { join as join2 } from "node:path";
|
|
576
|
+
function run2(args) {
|
|
577
|
+
return new Promise((resolve) => {
|
|
578
|
+
execFileCb2("git", args, (error, stdout, stderr) => {
|
|
579
|
+
resolve({
|
|
580
|
+
exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
|
|
581
|
+
stdout: stdout ?? "",
|
|
582
|
+
stderr: stderr ?? ""
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
async function isGitRepo() {
|
|
588
|
+
const { exitCode } = await run2(["rev-parse", "--is-inside-work-tree"]);
|
|
589
|
+
return exitCode === 0;
|
|
590
|
+
}
|
|
591
|
+
async function getCurrentBranch() {
|
|
592
|
+
const { exitCode, stdout } = await run2(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
593
|
+
if (exitCode !== 0)
|
|
594
|
+
return null;
|
|
595
|
+
return stdout.trim() || null;
|
|
596
|
+
}
|
|
597
|
+
async function getRemotes() {
|
|
598
|
+
const { exitCode, stdout } = await run2(["remote"]);
|
|
599
|
+
if (exitCode !== 0)
|
|
600
|
+
return [];
|
|
601
|
+
return stdout.trim().split(`
|
|
602
|
+
`).map((r) => r.trim()).filter(Boolean);
|
|
603
|
+
}
|
|
604
|
+
async function getRemoteUrl(remote) {
|
|
605
|
+
const { exitCode, stdout } = await run2(["remote", "get-url", remote]);
|
|
606
|
+
if (exitCode !== 0)
|
|
607
|
+
return null;
|
|
608
|
+
return stdout.trim() || null;
|
|
609
|
+
}
|
|
610
|
+
async function hasUncommittedChanges() {
|
|
611
|
+
const { exitCode, stdout } = await run2(["status", "--porcelain"]);
|
|
612
|
+
if (exitCode !== 0)
|
|
613
|
+
return false;
|
|
614
|
+
return stdout.trim().length > 0;
|
|
615
|
+
}
|
|
616
|
+
async function fetchRemote(remote) {
|
|
617
|
+
return run2(["fetch", remote]);
|
|
618
|
+
}
|
|
619
|
+
async function fetchAll() {
|
|
620
|
+
return run2(["fetch", "--all", "--quiet"]);
|
|
621
|
+
}
|
|
622
|
+
async function checkoutBranch2(branch) {
|
|
623
|
+
return run2(["checkout", branch]);
|
|
624
|
+
}
|
|
625
|
+
async function createBranch(branch, from) {
|
|
626
|
+
const args = from ? ["checkout", "-b", branch, from] : ["checkout", "-b", branch];
|
|
627
|
+
return run2(args);
|
|
628
|
+
}
|
|
629
|
+
async function resetHard(ref) {
|
|
630
|
+
return run2(["reset", "--hard", ref]);
|
|
631
|
+
}
|
|
632
|
+
async function updateLocalBranch(branch, target) {
|
|
633
|
+
const current = await getCurrentBranch();
|
|
634
|
+
if (current === branch) {
|
|
635
|
+
return resetHard(target);
|
|
636
|
+
}
|
|
637
|
+
return run2(["branch", "-f", branch, target]);
|
|
638
|
+
}
|
|
639
|
+
async function pushSetUpstream(remote, branch) {
|
|
640
|
+
return run2(["push", "-u", remote, branch]);
|
|
641
|
+
}
|
|
642
|
+
async function rebase(branch) {
|
|
643
|
+
return run2(["rebase", branch]);
|
|
644
|
+
}
|
|
645
|
+
async function getUpstreamRef() {
|
|
646
|
+
const { exitCode, stdout } = await run2([
|
|
647
|
+
"rev-parse",
|
|
648
|
+
"--abbrev-ref",
|
|
649
|
+
"--symbolic-full-name",
|
|
650
|
+
"@{u}"
|
|
651
|
+
]);
|
|
652
|
+
if (exitCode !== 0)
|
|
653
|
+
return null;
|
|
654
|
+
return stdout.trim() || null;
|
|
655
|
+
}
|
|
656
|
+
async function unsetUpstream() {
|
|
657
|
+
return run2(["branch", "--unset-upstream"]);
|
|
658
|
+
}
|
|
659
|
+
async function rebaseOnto(newBase, oldBase) {
|
|
660
|
+
return run2(["rebase", "--onto", newBase, oldBase]);
|
|
661
|
+
}
|
|
662
|
+
async function getMergeBase(ref1, ref2) {
|
|
663
|
+
const { exitCode, stdout } = await run2(["merge-base", ref1, ref2]);
|
|
664
|
+
if (exitCode !== 0)
|
|
665
|
+
return null;
|
|
666
|
+
return stdout.trim() || null;
|
|
667
|
+
}
|
|
668
|
+
async function getCommitHash(ref) {
|
|
669
|
+
const { exitCode, stdout } = await run2(["rev-parse", ref]);
|
|
670
|
+
if (exitCode !== 0)
|
|
671
|
+
return null;
|
|
672
|
+
return stdout.trim() || null;
|
|
673
|
+
}
|
|
674
|
+
async function determineRebaseStrategy(currentBranch, syncRef) {
|
|
675
|
+
const upstreamRef = await getUpstreamRef();
|
|
676
|
+
if (!upstreamRef) {
|
|
677
|
+
return { strategy: "plain" };
|
|
678
|
+
}
|
|
679
|
+
const upstreamHash = await getCommitHash(upstreamRef);
|
|
680
|
+
if (!upstreamHash) {
|
|
681
|
+
return { strategy: "plain" };
|
|
682
|
+
}
|
|
683
|
+
const slashIdx = upstreamRef.indexOf("/");
|
|
684
|
+
const upstreamBranchName = slashIdx !== -1 ? upstreamRef.slice(slashIdx + 1) : upstreamRef;
|
|
685
|
+
if (upstreamBranchName === currentBranch) {
|
|
686
|
+
return { strategy: "plain" };
|
|
687
|
+
}
|
|
688
|
+
const [forkFromUpstream, forkFromSync] = await Promise.all([
|
|
689
|
+
getMergeBase("HEAD", upstreamRef),
|
|
690
|
+
getMergeBase("HEAD", syncRef)
|
|
691
|
+
]);
|
|
692
|
+
if (forkFromUpstream && forkFromSync && forkFromUpstream === forkFromSync) {
|
|
693
|
+
return { strategy: "plain" };
|
|
694
|
+
}
|
|
695
|
+
if (forkFromUpstream) {
|
|
696
|
+
return { strategy: "onto", ontoOldBase: forkFromUpstream };
|
|
697
|
+
}
|
|
698
|
+
return { strategy: "plain" };
|
|
699
|
+
}
|
|
700
|
+
async function getStagedDiff() {
|
|
701
|
+
const { stdout } = await run2(["diff", "--cached"]);
|
|
702
|
+
return stdout;
|
|
703
|
+
}
|
|
704
|
+
async function getStagedFiles() {
|
|
705
|
+
const { exitCode, stdout } = await run2(["diff", "--cached", "--name-only"]);
|
|
706
|
+
if (exitCode !== 0)
|
|
707
|
+
return [];
|
|
708
|
+
return stdout.trim().split(`
|
|
709
|
+
`).filter(Boolean);
|
|
710
|
+
}
|
|
711
|
+
async function getChangedFiles() {
|
|
712
|
+
const { exitCode, stdout } = await run2(["status", "--porcelain"]);
|
|
713
|
+
if (exitCode !== 0)
|
|
714
|
+
return [];
|
|
715
|
+
return stdout.trimEnd().split(`
|
|
716
|
+
`).filter(Boolean).map((l) => {
|
|
717
|
+
const line = l.replace(/\r$/, "");
|
|
718
|
+
const match = line.match(/^..\s+(.*)/);
|
|
719
|
+
if (!match)
|
|
720
|
+
return "";
|
|
721
|
+
const file = match[1];
|
|
722
|
+
const renameIdx = file.indexOf(" -> ");
|
|
723
|
+
return renameIdx !== -1 ? file.slice(renameIdx + 4) : file;
|
|
724
|
+
}).filter(Boolean);
|
|
725
|
+
}
|
|
726
|
+
async function getDivergence(branch, base) {
|
|
727
|
+
const { exitCode, stdout } = await run2([
|
|
728
|
+
"rev-list",
|
|
729
|
+
"--left-right",
|
|
730
|
+
"--count",
|
|
731
|
+
`${base}...${branch}`
|
|
732
|
+
]);
|
|
733
|
+
if (exitCode !== 0)
|
|
734
|
+
return { ahead: 0, behind: 0 };
|
|
735
|
+
const parts = stdout.trim().split(/\s+/);
|
|
736
|
+
return {
|
|
737
|
+
behind: Number.parseInt(parts[0] ?? "0", 10),
|
|
738
|
+
ahead: Number.parseInt(parts[1] ?? "0", 10)
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
async function getMergedBranches(base) {
|
|
742
|
+
const { exitCode, stdout } = await run2(["branch", "--merged", base]);
|
|
743
|
+
if (exitCode !== 0)
|
|
744
|
+
return [];
|
|
745
|
+
return stdout.trim().split(`
|
|
746
|
+
`).map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean);
|
|
747
|
+
}
|
|
748
|
+
async function getGoneBranches() {
|
|
749
|
+
const { exitCode, stdout } = await run2(["branch", "-vv"]);
|
|
750
|
+
if (exitCode !== 0)
|
|
751
|
+
return [];
|
|
752
|
+
return stdout.trimEnd().split(`
|
|
753
|
+
`).filter((line) => line.includes(": gone]")).map((line) => line.replace(/^\*?\s+/, "").split(/\s+/)[0]).filter(Boolean);
|
|
217
754
|
}
|
|
218
755
|
async function deleteBranch(branch) {
|
|
219
|
-
return
|
|
756
|
+
return run2(["branch", "-d", branch]);
|
|
757
|
+
}
|
|
758
|
+
async function forceDeleteBranch(branch) {
|
|
759
|
+
return run2(["branch", "-D", branch]);
|
|
760
|
+
}
|
|
761
|
+
async function renameBranch(oldName, newName) {
|
|
762
|
+
return run2(["branch", "-m", oldName, newName]);
|
|
763
|
+
}
|
|
764
|
+
async function hasLocalWork(remote, branch) {
|
|
765
|
+
const uncommitted = await hasUncommittedChanges();
|
|
766
|
+
const trackingRef = `${remote}/${branch}`;
|
|
767
|
+
const { exitCode, stdout } = await run2(["rev-list", "--count", `${trackingRef}..${branch}`]);
|
|
768
|
+
const unpushedCommits = exitCode === 0 ? Number.parseInt(stdout.trim(), 10) || 0 : 0;
|
|
769
|
+
return { uncommitted, unpushedCommits };
|
|
770
|
+
}
|
|
771
|
+
async function deleteRemoteBranch(remote, branch) {
|
|
772
|
+
return run2(["push", remote, "--delete", branch]);
|
|
773
|
+
}
|
|
774
|
+
async function mergeSquash(branch) {
|
|
775
|
+
return run2(["merge", "--squash", branch]);
|
|
776
|
+
}
|
|
777
|
+
async function pushBranch(remote, branch) {
|
|
778
|
+
return run2(["push", remote, branch]);
|
|
220
779
|
}
|
|
221
780
|
async function pruneRemote(remote) {
|
|
222
|
-
return
|
|
781
|
+
return run2(["remote", "prune", remote]);
|
|
223
782
|
}
|
|
224
783
|
async function commitWithMessage(message) {
|
|
225
|
-
return
|
|
784
|
+
return run2(["commit", "-m", message]);
|
|
226
785
|
}
|
|
227
786
|
async function getLogDiff(base, head) {
|
|
228
|
-
const { stdout } = await
|
|
787
|
+
const { stdout } = await run2(["diff", `${base}...${head}`]);
|
|
229
788
|
return stdout;
|
|
230
789
|
}
|
|
231
790
|
async function getLog(base, head) {
|
|
232
|
-
const { exitCode, stdout } = await
|
|
791
|
+
const { exitCode, stdout } = await run2(["log", `${base}..${head}`, "--oneline"]);
|
|
233
792
|
if (exitCode !== 0)
|
|
234
793
|
return [];
|
|
235
794
|
return stdout.trim().split(`
|
|
236
795
|
`).filter(Boolean);
|
|
237
796
|
}
|
|
238
797
|
async function pullBranch(remote, branch) {
|
|
239
|
-
return
|
|
798
|
+
return run2(["pull", remote, branch]);
|
|
799
|
+
}
|
|
800
|
+
async function stageFiles(files) {
|
|
801
|
+
return run2(["add", "--", ...files]);
|
|
802
|
+
}
|
|
803
|
+
async function unstageFiles(files) {
|
|
804
|
+
return run2(["reset", "HEAD", "--", ...files]);
|
|
805
|
+
}
|
|
806
|
+
async function stageAll() {
|
|
807
|
+
return run2(["add", "-A"]);
|
|
808
|
+
}
|
|
809
|
+
async function getFullDiffForFiles(files) {
|
|
810
|
+
const [unstaged, staged, untracked] = await Promise.all([
|
|
811
|
+
run2(["diff", "--", ...files]),
|
|
812
|
+
run2(["diff", "--cached", "--", ...files]),
|
|
813
|
+
getUntrackedFiles()
|
|
814
|
+
]);
|
|
815
|
+
const parts = [staged.stdout, unstaged.stdout].filter(Boolean);
|
|
816
|
+
const untrackedSet = new Set(untracked);
|
|
817
|
+
const MAX_FILE_CONTENT = 2000;
|
|
818
|
+
for (const file of files) {
|
|
819
|
+
if (untrackedSet.has(file)) {
|
|
820
|
+
try {
|
|
821
|
+
const content = readFileSync2(join2(process.cwd(), file), "utf-8");
|
|
822
|
+
const truncated = content.length > MAX_FILE_CONTENT ? `${content.slice(0, MAX_FILE_CONTENT)}
|
|
823
|
+
... (truncated)` : content;
|
|
824
|
+
const lines = truncated.split(`
|
|
825
|
+
`).map((l) => `+${l}`);
|
|
826
|
+
parts.push(`diff --git a/${file} b/${file}
|
|
827
|
+
new file
|
|
828
|
+
--- /dev/null
|
|
829
|
+
+++ b/${file}
|
|
830
|
+
${lines.join(`
|
|
831
|
+
`)}`);
|
|
832
|
+
} catch {}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return parts.join(`
|
|
836
|
+
`);
|
|
837
|
+
}
|
|
838
|
+
async function getUntrackedFiles() {
|
|
839
|
+
const { exitCode, stdout } = await run2(["ls-files", "--others", "--exclude-standard"]);
|
|
840
|
+
if (exitCode !== 0)
|
|
841
|
+
return [];
|
|
842
|
+
return stdout.trim().split(`
|
|
843
|
+
`).filter(Boolean);
|
|
844
|
+
}
|
|
845
|
+
async function getFileStatus() {
|
|
846
|
+
const { exitCode, stdout } = await run2(["status", "--porcelain"]);
|
|
847
|
+
if (exitCode !== 0)
|
|
848
|
+
return { staged: [], modified: [], untracked: [] };
|
|
849
|
+
const result = { staged: [], modified: [], untracked: [] };
|
|
850
|
+
const STATUS_LABELS = {
|
|
851
|
+
A: "new file",
|
|
852
|
+
M: "modified",
|
|
853
|
+
D: "deleted",
|
|
854
|
+
R: "renamed",
|
|
855
|
+
C: "copied",
|
|
856
|
+
T: "type changed"
|
|
857
|
+
};
|
|
858
|
+
for (const raw of stdout.trimEnd().split(`
|
|
859
|
+
`).filter(Boolean)) {
|
|
860
|
+
const line = raw.replace(/\r$/, "");
|
|
861
|
+
const indexStatus = line[0];
|
|
862
|
+
const workTreeStatus = line[1];
|
|
863
|
+
const pathPart = line.slice(3);
|
|
864
|
+
const renameIdx = pathPart.indexOf(" -> ");
|
|
865
|
+
const file = renameIdx !== -1 ? pathPart.slice(renameIdx + 4) : pathPart;
|
|
866
|
+
if (indexStatus === "?" && workTreeStatus === "?") {
|
|
867
|
+
result.untracked.push(file);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (indexStatus && indexStatus !== " " && indexStatus !== "?") {
|
|
871
|
+
result.staged.push({ file, status: STATUS_LABELS[indexStatus] ?? indexStatus });
|
|
872
|
+
}
|
|
873
|
+
if (workTreeStatus && workTreeStatus !== " " && workTreeStatus !== "?") {
|
|
874
|
+
result.modified.push({ file, status: STATUS_LABELS[workTreeStatus] ?? workTreeStatus });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return result;
|
|
240
878
|
}
|
|
241
879
|
|
|
242
880
|
// src/utils/logger.ts
|
|
@@ -267,6 +905,53 @@ function heading(msg) {
|
|
|
267
905
|
${pc2.bold(msg)}`);
|
|
268
906
|
}
|
|
269
907
|
|
|
908
|
+
// src/utils/spinner.ts
|
|
909
|
+
import pc3 from "picocolors";
|
|
910
|
+
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
911
|
+
function createSpinner(text2) {
|
|
912
|
+
let frameIdx = 0;
|
|
913
|
+
let currentText = text2;
|
|
914
|
+
let stopped = false;
|
|
915
|
+
const clearLine = () => {
|
|
916
|
+
process.stderr.write("\r\x1B[K");
|
|
917
|
+
};
|
|
918
|
+
const render = () => {
|
|
919
|
+
if (stopped)
|
|
920
|
+
return;
|
|
921
|
+
const frame = pc3.cyan(FRAMES[frameIdx % FRAMES.length]);
|
|
922
|
+
clearLine();
|
|
923
|
+
process.stderr.write(`${frame} ${currentText}`);
|
|
924
|
+
frameIdx++;
|
|
925
|
+
};
|
|
926
|
+
const timer = setInterval(render, 80);
|
|
927
|
+
render();
|
|
928
|
+
const stop = () => {
|
|
929
|
+
if (stopped)
|
|
930
|
+
return;
|
|
931
|
+
stopped = true;
|
|
932
|
+
clearInterval(timer);
|
|
933
|
+
clearLine();
|
|
934
|
+
};
|
|
935
|
+
return {
|
|
936
|
+
update(newText) {
|
|
937
|
+
currentText = newText;
|
|
938
|
+
},
|
|
939
|
+
success(msg) {
|
|
940
|
+
stop();
|
|
941
|
+
process.stderr.write(`${pc3.green("✔")} ${msg}
|
|
942
|
+
`);
|
|
943
|
+
},
|
|
944
|
+
fail(msg) {
|
|
945
|
+
stop();
|
|
946
|
+
process.stderr.write(`${pc3.red("✖")} ${msg}
|
|
947
|
+
`);
|
|
948
|
+
},
|
|
949
|
+
stop() {
|
|
950
|
+
stop();
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
270
955
|
// src/utils/workflow.ts
|
|
271
956
|
var WORKFLOW_DESCRIPTIONS = {
|
|
272
957
|
"clean-flow": "Clean Flow — main + dev, squash features into dev, merge dev into main",
|
|
@@ -315,6 +1000,91 @@ function getProtectedBranches(config) {
|
|
|
315
1000
|
}
|
|
316
1001
|
|
|
317
1002
|
// src/commands/clean.ts
|
|
1003
|
+
async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
1004
|
+
if (!config)
|
|
1005
|
+
return "skipped";
|
|
1006
|
+
const { origin } = config;
|
|
1007
|
+
const localWork = await hasLocalWork(origin, currentBranch);
|
|
1008
|
+
const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
|
|
1009
|
+
if (hasWork) {
|
|
1010
|
+
if (localWork.uncommitted) {
|
|
1011
|
+
warn("You have uncommitted changes in your working tree.");
|
|
1012
|
+
}
|
|
1013
|
+
if (localWork.unpushedCommits > 0) {
|
|
1014
|
+
warn(`You have ${pc4.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
|
|
1015
|
+
}
|
|
1016
|
+
const SAVE_NEW_BRANCH = "Save changes to a new branch";
|
|
1017
|
+
const DISCARD = "Discard all changes and clean up";
|
|
1018
|
+
const CANCEL = "Skip this branch";
|
|
1019
|
+
const action = await selectPrompt(`${pc4.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
|
|
1020
|
+
if (action === CANCEL)
|
|
1021
|
+
return "skipped";
|
|
1022
|
+
if (action === SAVE_NEW_BRANCH) {
|
|
1023
|
+
if (!config)
|
|
1024
|
+
return "skipped";
|
|
1025
|
+
info(pc4.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
1026
|
+
const description = await inputPrompt("What are you working on?");
|
|
1027
|
+
let newBranchName = description;
|
|
1028
|
+
if (looksLikeNaturalLanguage(description)) {
|
|
1029
|
+
const spinner = createSpinner("Generating branch name suggestion...");
|
|
1030
|
+
const suggested = await suggestBranchName(description);
|
|
1031
|
+
if (suggested) {
|
|
1032
|
+
spinner.success("Branch name suggestion ready.");
|
|
1033
|
+
console.log(`
|
|
1034
|
+
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(suggested))}`);
|
|
1035
|
+
const accepted = await confirmPrompt(`Use ${pc4.bold(suggested)} as your branch name?`);
|
|
1036
|
+
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
1037
|
+
} else {
|
|
1038
|
+
spinner.fail("AI did not return a suggestion.");
|
|
1039
|
+
newBranchName = await inputPrompt("Enter branch name", description);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
1043
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc4.bold(newBranchName)}:`, config.branchPrefixes);
|
|
1044
|
+
newBranchName = formatBranchName(prefix, newBranchName);
|
|
1045
|
+
}
|
|
1046
|
+
if (!isValidBranchName(newBranchName)) {
|
|
1047
|
+
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
1048
|
+
return "skipped";
|
|
1049
|
+
}
|
|
1050
|
+
const renameResult = await renameBranch(currentBranch, newBranchName);
|
|
1051
|
+
if (renameResult.exitCode !== 0) {
|
|
1052
|
+
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
1053
|
+
return "skipped";
|
|
1054
|
+
}
|
|
1055
|
+
success(`Renamed ${pc4.bold(currentBranch)} → ${pc4.bold(newBranchName)}`);
|
|
1056
|
+
const syncSource2 = getSyncSource(config);
|
|
1057
|
+
await fetchRemote(syncSource2.remote);
|
|
1058
|
+
const savedUpstreamRef = await getUpstreamRef();
|
|
1059
|
+
const rebaseResult = savedUpstreamRef && savedUpstreamRef !== syncSource2.ref ? await rebaseOnto(syncSource2.ref, savedUpstreamRef) : await rebase(syncSource2.ref);
|
|
1060
|
+
if (rebaseResult.exitCode !== 0) {
|
|
1061
|
+
warn("Rebase encountered conflicts. Resolve them after cleanup:");
|
|
1062
|
+
info(` ${pc4.bold(`git checkout ${newBranchName} && git rebase --continue`)}`);
|
|
1063
|
+
} else {
|
|
1064
|
+
success(`Rebased ${pc4.bold(newBranchName)} onto ${pc4.bold(syncSource2.ref)}.`);
|
|
1065
|
+
}
|
|
1066
|
+
const coResult2 = await checkoutBranch(baseBranch);
|
|
1067
|
+
if (coResult2.exitCode !== 0) {
|
|
1068
|
+
error(`Failed to checkout ${baseBranch}: ${coResult2.stderr}`);
|
|
1069
|
+
return "saved";
|
|
1070
|
+
}
|
|
1071
|
+
await updateLocalBranch(baseBranch, syncSource2.ref);
|
|
1072
|
+
success(`Synced ${pc4.bold(baseBranch)} with ${pc4.bold(syncSource2.ref)}.`);
|
|
1073
|
+
return "saved";
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const syncSource = getSyncSource(config);
|
|
1077
|
+
info(`Switching to ${pc4.bold(baseBranch)} and syncing...`);
|
|
1078
|
+
await fetchRemote(syncSource.remote);
|
|
1079
|
+
const coResult = await checkoutBranch(baseBranch);
|
|
1080
|
+
if (coResult.exitCode !== 0) {
|
|
1081
|
+
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
1082
|
+
return "skipped";
|
|
1083
|
+
}
|
|
1084
|
+
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1085
|
+
success(`Synced ${pc4.bold(baseBranch)} with ${pc4.bold(syncSource.ref)}.`);
|
|
1086
|
+
return "switched";
|
|
1087
|
+
}
|
|
318
1088
|
var clean_default = defineCommand({
|
|
319
1089
|
meta: {
|
|
320
1090
|
name: "clean",
|
|
@@ -340,47 +1110,114 @@ var clean_default = defineCommand({
|
|
|
340
1110
|
}
|
|
341
1111
|
const { origin } = config;
|
|
342
1112
|
const baseBranch = getBaseBranch(config);
|
|
343
|
-
|
|
1113
|
+
let currentBranch = await getCurrentBranch();
|
|
344
1114
|
heading("\uD83E\uDDF9 contrib clean");
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
info("No merged branches to clean up.");
|
|
1115
|
+
info(`Pruning ${origin} remote refs...`);
|
|
1116
|
+
const pruneResult = await pruneRemote(origin);
|
|
1117
|
+
if (pruneResult.exitCode === 0) {
|
|
1118
|
+
success(`Pruned ${origin} remote refs.`);
|
|
350
1119
|
} else {
|
|
1120
|
+
warn(`Could not prune remote: ${pruneResult.stderr.trim()}`);
|
|
1121
|
+
}
|
|
1122
|
+
const protectedBranches = new Set(getProtectedBranches(config));
|
|
1123
|
+
const mergedBranches = await getMergedBranches(baseBranch);
|
|
1124
|
+
const mergedCandidates = mergedBranches.filter((b) => !protectedBranches.has(b));
|
|
1125
|
+
const goneBranches = await getGoneBranches();
|
|
1126
|
+
const goneCandidates = goneBranches.filter((b) => !protectedBranches.has(b) && !mergedCandidates.includes(b));
|
|
1127
|
+
if (currentBranch && !protectedBranches.has(currentBranch) && !mergedCandidates.includes(currentBranch) && !goneCandidates.includes(currentBranch)) {
|
|
1128
|
+
const ghInstalled = await checkGhInstalled();
|
|
1129
|
+
const ghAuthed = ghInstalled && await checkGhAuth();
|
|
1130
|
+
if (ghInstalled && ghAuthed) {
|
|
1131
|
+
const mergedPR = await getMergedPRForBranch(currentBranch);
|
|
1132
|
+
if (mergedPR) {
|
|
1133
|
+
warn(`PR #${mergedPR.number} (${pc4.bold(mergedPR.title)}) has already been merged.`);
|
|
1134
|
+
info(`Link: ${pc4.underline(mergedPR.url)}`);
|
|
1135
|
+
goneCandidates.push(currentBranch);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (mergedCandidates.length > 0) {
|
|
351
1140
|
console.log(`
|
|
352
|
-
${
|
|
353
|
-
for (const b of
|
|
354
|
-
|
|
1141
|
+
${pc4.bold("Merged branches to delete:")}`);
|
|
1142
|
+
for (const b of mergedCandidates) {
|
|
1143
|
+
const marker = b === currentBranch ? pc4.yellow(" (current)") : "";
|
|
1144
|
+
console.log(` ${pc4.dim("•")} ${b}${marker}`);
|
|
355
1145
|
}
|
|
356
1146
|
console.log();
|
|
357
|
-
const ok = args.yes || await confirmPrompt(`Delete ${
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1147
|
+
const ok = args.yes || await confirmPrompt(`Delete ${pc4.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
|
|
1148
|
+
if (ok) {
|
|
1149
|
+
for (const branch of mergedCandidates) {
|
|
1150
|
+
if (branch === currentBranch) {
|
|
1151
|
+
const result2 = await handleCurrentBranchDeletion(currentBranch, baseBranch, config);
|
|
1152
|
+
if (result2 === "skipped") {
|
|
1153
|
+
warn(` Skipped ${branch}.`);
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (result2 === "saved") {
|
|
1157
|
+
currentBranch = baseBranch;
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
currentBranch = baseBranch;
|
|
1161
|
+
}
|
|
362
1162
|
const result = await deleteBranch(branch);
|
|
363
1163
|
if (result.exitCode === 0) {
|
|
364
|
-
success(` Deleted ${
|
|
1164
|
+
success(` Deleted ${pc4.bold(branch)}`);
|
|
1165
|
+
} else {
|
|
1166
|
+
warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
} else {
|
|
1170
|
+
info("Skipped merged branch deletion.");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (goneCandidates.length > 0) {
|
|
1174
|
+
console.log(`
|
|
1175
|
+
${pc4.bold("Stale branches (remote deleted, likely squash-merged):")}`);
|
|
1176
|
+
for (const b of goneCandidates) {
|
|
1177
|
+
const marker = b === currentBranch ? pc4.yellow(" (current)") : "";
|
|
1178
|
+
console.log(` ${pc4.dim("•")} ${b}${marker}`);
|
|
1179
|
+
}
|
|
1180
|
+
console.log();
|
|
1181
|
+
const ok = args.yes || await confirmPrompt(`Delete ${pc4.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
|
|
1182
|
+
if (ok) {
|
|
1183
|
+
for (const branch of goneCandidates) {
|
|
1184
|
+
if (branch === currentBranch) {
|
|
1185
|
+
const result2 = await handleCurrentBranchDeletion(currentBranch, baseBranch, config);
|
|
1186
|
+
if (result2 === "skipped") {
|
|
1187
|
+
warn(` Skipped ${branch}.`);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (result2 === "saved") {
|
|
1191
|
+
currentBranch = baseBranch;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
currentBranch = baseBranch;
|
|
1195
|
+
}
|
|
1196
|
+
const result = await forceDeleteBranch(branch);
|
|
1197
|
+
if (result.exitCode === 0) {
|
|
1198
|
+
success(` Deleted ${pc4.bold(branch)}`);
|
|
365
1199
|
} else {
|
|
366
1200
|
warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
|
|
367
1201
|
}
|
|
368
1202
|
}
|
|
1203
|
+
} else {
|
|
1204
|
+
info("Skipped stale branch deletion.");
|
|
369
1205
|
}
|
|
370
1206
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1207
|
+
if (mergedCandidates.length === 0 && goneCandidates.length === 0) {
|
|
1208
|
+
info("No branches to clean up. Everything is tidy! \uD83E\uDDF9");
|
|
1209
|
+
}
|
|
1210
|
+
const finalBranch = await getCurrentBranch();
|
|
1211
|
+
if (finalBranch && protectedBranches.has(finalBranch)) {
|
|
1212
|
+
console.log();
|
|
1213
|
+
info(`You're on ${pc4.bold(finalBranch)}. Run ${pc4.bold("contrib start")} to begin a new feature.`);
|
|
377
1214
|
}
|
|
378
1215
|
}
|
|
379
1216
|
});
|
|
380
1217
|
|
|
381
1218
|
// src/commands/commit.ts
|
|
382
1219
|
import { defineCommand as defineCommand2 } from "citty";
|
|
383
|
-
import
|
|
1220
|
+
import pc5 from "picocolors";
|
|
384
1221
|
|
|
385
1222
|
// src/utils/convention.ts
|
|
386
1223
|
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;
|
|
@@ -425,339 +1262,734 @@ function getValidationError(convention) {
|
|
|
425
1262
|
];
|
|
426
1263
|
}
|
|
427
1264
|
|
|
428
|
-
// src/
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
Examples:
|
|
453
|
-
feat: add user authentication system
|
|
454
|
-
fix(auth): resolve token expiry issue
|
|
455
|
-
docs: update contributing guidelines
|
|
456
|
-
feat!: redesign authentication API`;
|
|
457
|
-
var CLEAN_COMMIT_SYSTEM_PROMPT = `You are a git commit message generator. Generate a Clean Commit message following this exact format:
|
|
458
|
-
<emoji> <type>[!][(<scope>)]: <description>
|
|
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
|
|
470
|
-
|
|
471
|
-
Rules:
|
|
472
|
-
- Breaking change (!) only for: new, update, remove, security
|
|
473
|
-
- Description: concise, imperative mood, max 72 chars
|
|
474
|
-
- Scope: optional, camelCase or kebab-case component name
|
|
475
|
-
- Return ONLY the commit message line, nothing else
|
|
476
|
-
|
|
477
|
-
Examples:
|
|
478
|
-
\uD83D\uDCE6 new: user authentication system
|
|
479
|
-
\uD83D\uDD27 update (api): improve error handling
|
|
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>
|
|
485
|
-
Prefixes: feature, fix, docs, chore, test, refactor
|
|
486
|
-
|
|
487
|
-
Rules:
|
|
488
|
-
- Use lowercase kebab-case for the name part
|
|
489
|
-
- Keep it short and descriptive (2-5 words max)
|
|
490
|
-
- Return ONLY the branch name, nothing else
|
|
491
|
-
|
|
492
|
-
Examples:
|
|
493
|
-
Input: "fix the login timeout bug" → fix/login-timeout
|
|
494
|
-
Input: "add user profile page" → feature/user-profile-page
|
|
495
|
-
Input: "update readme documentation" → docs/update-readme`;
|
|
496
|
-
var PR_DESCRIPTION_SYSTEM_PROMPT = `You are a GitHub pull request description generator. Create a clear, structured PR description.
|
|
497
|
-
|
|
498
|
-
Return a JSON object with this exact structure:
|
|
499
|
-
{
|
|
500
|
-
"title": "Brief PR title (50 chars max)",
|
|
501
|
-
"body": "## Summary\\n...\\n\\n## Changes\\n...\\n\\n## Test Plan\\n..."
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
Rules:
|
|
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`;
|
|
515
|
-
async function checkCopilotAvailable() {
|
|
516
|
-
let client = null;
|
|
517
|
-
try {
|
|
518
|
-
client = new CopilotClient;
|
|
519
|
-
await client.start();
|
|
520
|
-
} catch (err) {
|
|
521
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
522
|
-
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
523
|
-
return "Copilot CLI binary not found. Ensure GitHub Copilot is installed and your gh CLI is up to date.";
|
|
1265
|
+
// src/commands/commit.ts
|
|
1266
|
+
var commit_default = defineCommand2({
|
|
1267
|
+
meta: {
|
|
1268
|
+
name: "commit",
|
|
1269
|
+
description: "Stage changes and create a commit message (AI-powered)"
|
|
1270
|
+
},
|
|
1271
|
+
args: {
|
|
1272
|
+
model: {
|
|
1273
|
+
type: "string",
|
|
1274
|
+
description: "AI model to use for commit message generation"
|
|
1275
|
+
},
|
|
1276
|
+
"no-ai": {
|
|
1277
|
+
type: "boolean",
|
|
1278
|
+
description: "Skip AI and write commit message manually",
|
|
1279
|
+
default: false
|
|
1280
|
+
},
|
|
1281
|
+
group: {
|
|
1282
|
+
type: "boolean",
|
|
1283
|
+
description: "AI groups related changes into separate atomic commits",
|
|
1284
|
+
default: false
|
|
524
1285
|
}
|
|
525
|
-
|
|
1286
|
+
},
|
|
1287
|
+
async run({ args }) {
|
|
1288
|
+
if (!await isGitRepo()) {
|
|
1289
|
+
error("Not inside a git repository.");
|
|
1290
|
+
process.exit(1);
|
|
1291
|
+
}
|
|
1292
|
+
const config = readConfig();
|
|
1293
|
+
if (!config) {
|
|
1294
|
+
error("No .contributerc.json found. Run `contrib setup` first.");
|
|
1295
|
+
process.exit(1);
|
|
1296
|
+
}
|
|
1297
|
+
heading("\uD83D\uDCBE contrib commit");
|
|
1298
|
+
if (args.group) {
|
|
1299
|
+
await runGroupCommit(args.model, config);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
let stagedFiles = await getStagedFiles();
|
|
1303
|
+
if (stagedFiles.length === 0) {
|
|
1304
|
+
const changedFiles = await getChangedFiles();
|
|
1305
|
+
if (changedFiles.length === 0) {
|
|
1306
|
+
error("No changes to commit.");
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1309
|
+
console.log(`
|
|
1310
|
+
${pc5.bold("Changed files:")}`);
|
|
1311
|
+
for (const f of changedFiles) {
|
|
1312
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
1313
|
+
}
|
|
1314
|
+
const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
|
|
1315
|
+
"Stage all changes",
|
|
1316
|
+
"Select files to stage",
|
|
1317
|
+
"Cancel"
|
|
1318
|
+
]);
|
|
1319
|
+
if (stageAction === "Cancel") {
|
|
1320
|
+
process.exit(0);
|
|
1321
|
+
}
|
|
1322
|
+
if (stageAction === "Stage all changes") {
|
|
1323
|
+
const result2 = await stageAll();
|
|
1324
|
+
if (result2.exitCode !== 0) {
|
|
1325
|
+
error(`Failed to stage files: ${result2.stderr}`);
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
success("Staged all changes.");
|
|
1329
|
+
} else {
|
|
1330
|
+
const selected = await multiSelectPrompt("Select files to stage:", changedFiles);
|
|
1331
|
+
if (selected.length === 0) {
|
|
1332
|
+
error("No files selected.");
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
const result2 = await stageFiles(selected);
|
|
1336
|
+
if (result2.exitCode !== 0) {
|
|
1337
|
+
error(`Failed to stage files: ${result2.stderr}`);
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
}
|
|
1340
|
+
success(`Staged ${selected.length} file(s).`);
|
|
1341
|
+
}
|
|
1342
|
+
stagedFiles = await getStagedFiles();
|
|
1343
|
+
if (stagedFiles.length === 0) {
|
|
1344
|
+
error("No staged changes after staging attempt.");
|
|
1345
|
+
process.exit(1);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
info(`Staged files: ${stagedFiles.join(", ")}`);
|
|
1349
|
+
let commitMessage = null;
|
|
1350
|
+
const useAI = !args["no-ai"];
|
|
1351
|
+
if (useAI) {
|
|
1352
|
+
const [copilotError, diff] = await Promise.all([checkCopilotAvailable(), getStagedDiff()]);
|
|
1353
|
+
if (copilotError) {
|
|
1354
|
+
warn(`AI unavailable: ${copilotError}`);
|
|
1355
|
+
warn("Falling back to manual commit message entry.");
|
|
1356
|
+
} else {
|
|
1357
|
+
const spinner = createSpinner("Generating commit message with AI...");
|
|
1358
|
+
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
1359
|
+
if (commitMessage) {
|
|
1360
|
+
spinner.success("AI commit message generated.");
|
|
1361
|
+
console.log(`
|
|
1362
|
+
${pc5.dim("AI suggestion:")} ${pc5.bold(pc5.cyan(commitMessage))}`);
|
|
1363
|
+
} else {
|
|
1364
|
+
spinner.fail("AI did not return a commit message.");
|
|
1365
|
+
warn("Falling back to manual entry.");
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
let finalMessage = null;
|
|
1370
|
+
if (commitMessage) {
|
|
1371
|
+
const action = await selectPrompt("What would you like to do?", [
|
|
1372
|
+
"Accept this message",
|
|
1373
|
+
"Edit this message",
|
|
1374
|
+
"Regenerate",
|
|
1375
|
+
"Write manually"
|
|
1376
|
+
]);
|
|
1377
|
+
if (action === "Accept this message") {
|
|
1378
|
+
finalMessage = commitMessage;
|
|
1379
|
+
} else if (action === "Edit this message") {
|
|
1380
|
+
finalMessage = await inputPrompt("Edit commit message", commitMessage);
|
|
1381
|
+
} else if (action === "Regenerate") {
|
|
1382
|
+
const spinner = createSpinner("Regenerating commit message...");
|
|
1383
|
+
const diff = await getStagedDiff();
|
|
1384
|
+
const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
1385
|
+
if (regen) {
|
|
1386
|
+
spinner.success("Commit message regenerated.");
|
|
1387
|
+
console.log(`
|
|
1388
|
+
${pc5.dim("AI suggestion:")} ${pc5.bold(pc5.cyan(regen))}`);
|
|
1389
|
+
const ok = await confirmPrompt("Use this message?");
|
|
1390
|
+
finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
|
|
1391
|
+
} else {
|
|
1392
|
+
spinner.fail("Regeneration failed.");
|
|
1393
|
+
finalMessage = await inputPrompt("Enter commit message");
|
|
1394
|
+
}
|
|
1395
|
+
} else {
|
|
1396
|
+
finalMessage = await inputPrompt("Enter commit message");
|
|
1397
|
+
}
|
|
1398
|
+
} else {
|
|
1399
|
+
const convention2 = config.commitConvention;
|
|
1400
|
+
if (convention2 !== "none") {
|
|
1401
|
+
console.log();
|
|
1402
|
+
for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
|
|
1403
|
+
console.log(pc5.dim(hint));
|
|
1404
|
+
}
|
|
1405
|
+
console.log();
|
|
1406
|
+
}
|
|
1407
|
+
finalMessage = await inputPrompt("Enter commit message");
|
|
1408
|
+
}
|
|
1409
|
+
if (!finalMessage) {
|
|
1410
|
+
error("No commit message provided.");
|
|
1411
|
+
process.exit(1);
|
|
1412
|
+
}
|
|
1413
|
+
const convention = config.commitConvention;
|
|
1414
|
+
if (!validateCommitMessage(finalMessage, convention)) {
|
|
1415
|
+
for (const line of getValidationError(convention)) {
|
|
1416
|
+
warn(line);
|
|
1417
|
+
}
|
|
1418
|
+
const proceed = await confirmPrompt("Commit anyway?");
|
|
1419
|
+
if (!proceed)
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
const result = await commitWithMessage(finalMessage);
|
|
1423
|
+
if (result.exitCode !== 0) {
|
|
1424
|
+
error(`Failed to commit: ${result.stderr}`);
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
success(`✅ Committed: ${pc5.bold(finalMessage)}`);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
async function runGroupCommit(model, config) {
|
|
1431
|
+
const [copilotError, changedFiles] = await Promise.all([
|
|
1432
|
+
checkCopilotAvailable(),
|
|
1433
|
+
getChangedFiles()
|
|
1434
|
+
]);
|
|
1435
|
+
if (copilotError) {
|
|
1436
|
+
error(`AI is required for --group mode but unavailable: ${copilotError}`);
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
}
|
|
1439
|
+
if (changedFiles.length === 0) {
|
|
1440
|
+
error("No changes to group-commit.");
|
|
1441
|
+
process.exit(1);
|
|
526
1442
|
}
|
|
1443
|
+
console.log(`
|
|
1444
|
+
${pc5.bold("Changed files:")}`);
|
|
1445
|
+
for (const f of changedFiles) {
|
|
1446
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
1447
|
+
}
|
|
1448
|
+
const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
1449
|
+
const diffs = await getFullDiffForFiles(changedFiles);
|
|
1450
|
+
if (!diffs.trim()) {
|
|
1451
|
+
spinner.stop();
|
|
1452
|
+
warn("Could not retrieve diff context for any files. AI needs diffs to produce groups.");
|
|
1453
|
+
}
|
|
1454
|
+
let groups;
|
|
527
1455
|
try {
|
|
528
|
-
await
|
|
1456
|
+
groups = await generateCommitGroups(changedFiles, diffs, model, config.commitConvention);
|
|
1457
|
+
spinner.success(`AI generated ${groups.length} commit group(s).`);
|
|
529
1458
|
} catch (err) {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
1459
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1460
|
+
spinner.fail(`AI grouping failed: ${reason}`);
|
|
1461
|
+
process.exit(1);
|
|
1462
|
+
}
|
|
1463
|
+
if (groups.length === 0) {
|
|
1464
|
+
error("AI could not produce commit groups. Try committing files manually.");
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
const changedSet = new Set(changedFiles);
|
|
1468
|
+
for (const group of groups) {
|
|
1469
|
+
const invalid = group.files.filter((f) => !changedSet.has(f));
|
|
1470
|
+
if (invalid.length > 0) {
|
|
1471
|
+
warn(`AI suggested unknown file(s): ${invalid.join(", ")} — removed from group.`);
|
|
533
1472
|
}
|
|
534
|
-
|
|
535
|
-
|
|
1473
|
+
group.files = group.files.filter((f) => changedSet.has(f));
|
|
1474
|
+
}
|
|
1475
|
+
let validGroups = groups.filter((g) => g.files.length > 0);
|
|
1476
|
+
if (validGroups.length === 0) {
|
|
1477
|
+
error("No valid groups remain after validation. Try committing files manually.");
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
let proceedToCommit = false;
|
|
1481
|
+
let commitAll = false;
|
|
1482
|
+
while (!proceedToCommit) {
|
|
1483
|
+
console.log(`
|
|
1484
|
+
${pc5.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
1485
|
+
`);
|
|
1486
|
+
for (let i = 0;i < validGroups.length; i++) {
|
|
1487
|
+
const g = validGroups[i];
|
|
1488
|
+
console.log(` ${pc5.cyan(`Group ${i + 1}:`)} ${pc5.bold(g.message)}`);
|
|
1489
|
+
for (const f of g.files) {
|
|
1490
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
1491
|
+
}
|
|
1492
|
+
console.log();
|
|
536
1493
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1494
|
+
const summaryAction = await selectPrompt("What would you like to do?", [
|
|
1495
|
+
"Commit all",
|
|
1496
|
+
"Review each group",
|
|
1497
|
+
"Regenerate all messages",
|
|
1498
|
+
"Cancel"
|
|
1499
|
+
]);
|
|
1500
|
+
if (summaryAction === "Cancel") {
|
|
1501
|
+
warn("Group commit cancelled.");
|
|
1502
|
+
process.exit(0);
|
|
1503
|
+
}
|
|
1504
|
+
if (summaryAction === "Regenerate all messages") {
|
|
1505
|
+
const regenSpinner = createSpinner("Regenerating all commit messages...");
|
|
1506
|
+
try {
|
|
1507
|
+
validGroups = await regenerateAllGroupMessages(validGroups, diffs, model, config.commitConvention);
|
|
1508
|
+
regenSpinner.success("All commit messages regenerated.");
|
|
1509
|
+
} catch {
|
|
1510
|
+
regenSpinner.fail("Failed to regenerate messages. Keeping current ones.");
|
|
1511
|
+
}
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
proceedToCommit = true;
|
|
1515
|
+
commitAll = summaryAction === "Commit all";
|
|
542
1516
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1517
|
+
let committed = 0;
|
|
1518
|
+
if (commitAll) {
|
|
1519
|
+
for (let i = 0;i < validGroups.length; i++) {
|
|
1520
|
+
const group = validGroups[i];
|
|
1521
|
+
const stageResult = await stageFiles(group.files);
|
|
1522
|
+
if (stageResult.exitCode !== 0) {
|
|
1523
|
+
error(`Failed to stage group ${i + 1}: ${stageResult.stderr}`);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
const commitResult = await commitWithMessage(group.message);
|
|
1527
|
+
if (commitResult.exitCode !== 0) {
|
|
1528
|
+
const detail = (commitResult.stderr || commitResult.stdout).trim();
|
|
1529
|
+
error(`Failed to commit group ${i + 1}: ${detail}`);
|
|
1530
|
+
await unstageFiles(group.files);
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
committed++;
|
|
1534
|
+
success(`✅ Committed group ${i + 1}: ${pc5.bold(group.message)}`);
|
|
1535
|
+
}
|
|
1536
|
+
} else {
|
|
1537
|
+
for (let i = 0;i < validGroups.length; i++) {
|
|
1538
|
+
const group = validGroups[i];
|
|
1539
|
+
console.log(pc5.bold(`
|
|
1540
|
+
── Group ${i + 1}/${validGroups.length} ──`));
|
|
1541
|
+
console.log(` ${pc5.cyan(group.message)}`);
|
|
1542
|
+
for (const f of group.files) {
|
|
1543
|
+
console.log(` ${pc5.dim("•")} ${f}`);
|
|
1544
|
+
}
|
|
1545
|
+
let message = group.message;
|
|
1546
|
+
let actionDone = false;
|
|
1547
|
+
while (!actionDone) {
|
|
1548
|
+
const action = await selectPrompt("Action for this group:", [
|
|
1549
|
+
"Commit as-is",
|
|
1550
|
+
"Edit message and commit",
|
|
1551
|
+
"Regenerate message",
|
|
1552
|
+
"Skip this group"
|
|
1553
|
+
]);
|
|
1554
|
+
if (action === "Skip this group") {
|
|
1555
|
+
warn(`Skipped group ${i + 1}.`);
|
|
1556
|
+
actionDone = true;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (action === "Regenerate message") {
|
|
1560
|
+
const regenSpinner = createSpinner("Regenerating commit message for this group...");
|
|
1561
|
+
const newMsg = await regenerateGroupMessage(group.files, diffs, model, config.commitConvention);
|
|
1562
|
+
if (newMsg) {
|
|
1563
|
+
message = newMsg;
|
|
1564
|
+
group.message = newMsg;
|
|
1565
|
+
regenSpinner.success(`New message: ${pc5.bold(message)}`);
|
|
1566
|
+
} else {
|
|
1567
|
+
regenSpinner.fail("AI could not generate a new message. Keeping current one.");
|
|
1568
|
+
}
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (action === "Edit message and commit") {
|
|
1572
|
+
message = await inputPrompt("Edit commit message", message);
|
|
1573
|
+
if (!message) {
|
|
1574
|
+
warn(`Skipped group ${i + 1} (empty message).`);
|
|
1575
|
+
actionDone = true;
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (!validateCommitMessage(message, config.commitConvention)) {
|
|
1580
|
+
for (const line of getValidationError(config.commitConvention)) {
|
|
1581
|
+
warn(line);
|
|
1582
|
+
}
|
|
1583
|
+
const proceed = await confirmPrompt("Commit anyway?");
|
|
1584
|
+
if (!proceed) {
|
|
1585
|
+
warn(`Skipped group ${i + 1}.`);
|
|
1586
|
+
actionDone = true;
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const stageResult = await stageFiles(group.files);
|
|
1591
|
+
if (stageResult.exitCode !== 0) {
|
|
1592
|
+
error(`Failed to stage group ${i + 1}: ${stageResult.stderr}`);
|
|
1593
|
+
actionDone = true;
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const commitResult = await commitWithMessage(message);
|
|
1597
|
+
if (commitResult.exitCode !== 0) {
|
|
1598
|
+
const detail = (commitResult.stderr || commitResult.stdout).trim();
|
|
1599
|
+
error(`Failed to commit group ${i + 1}: ${detail}`);
|
|
1600
|
+
await unstageFiles(group.files);
|
|
1601
|
+
actionDone = true;
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
committed++;
|
|
1605
|
+
success(`✅ Committed group ${i + 1}: ${pc5.bold(message)}`);
|
|
1606
|
+
actionDone = true;
|
|
1607
|
+
}
|
|
562
1608
|
}
|
|
563
|
-
} finally {
|
|
564
|
-
await client.stop();
|
|
565
1609
|
}
|
|
1610
|
+
if (committed === 0) {
|
|
1611
|
+
warn("No groups were committed.");
|
|
1612
|
+
} else {
|
|
1613
|
+
success(`
|
|
1614
|
+
\uD83C\uDF89 ${committed} of ${validGroups.length} group(s) committed successfully.`);
|
|
1615
|
+
}
|
|
1616
|
+
process.exit(0);
|
|
566
1617
|
}
|
|
567
|
-
function getCommitSystemPrompt(convention) {
|
|
568
|
-
if (convention === "conventional")
|
|
569
|
-
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
570
|
-
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
571
|
-
}
|
|
572
|
-
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
573
|
-
try {
|
|
574
|
-
const userMessage = `Generate a commit message for these staged changes:
|
|
575
1618
|
|
|
576
|
-
|
|
1619
|
+
// src/commands/doctor.ts
|
|
1620
|
+
import { execFile as execFileCb3 } from "node:child_process";
|
|
1621
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
1622
|
+
import pc6 from "picocolors";
|
|
1623
|
+
// package.json
|
|
1624
|
+
var package_default = {
|
|
1625
|
+
name: "contribute-now",
|
|
1626
|
+
version: "0.2.0-dev.0de9dbd",
|
|
1627
|
+
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1628
|
+
type: "module",
|
|
1629
|
+
bin: {
|
|
1630
|
+
contrib: "dist/index.js",
|
|
1631
|
+
contribute: "dist/index.js"
|
|
1632
|
+
},
|
|
1633
|
+
files: [
|
|
1634
|
+
"dist"
|
|
1635
|
+
],
|
|
1636
|
+
scripts: {
|
|
1637
|
+
build: "bun build src/index.ts --outfile dist/index.js --target node --packages external",
|
|
1638
|
+
cli: "bun run src/index.ts --",
|
|
1639
|
+
dev: "bun src/index.ts",
|
|
1640
|
+
test: "bun test",
|
|
1641
|
+
lint: "biome check .",
|
|
1642
|
+
"lint:fix": "biome check --write .",
|
|
1643
|
+
format: "biome format --write .",
|
|
1644
|
+
"www:dev": "bun run --cwd www dev",
|
|
1645
|
+
"www:build": "bun run --cwd www build",
|
|
1646
|
+
"www:preview": "bun run --cwd www preview"
|
|
1647
|
+
},
|
|
1648
|
+
engines: {
|
|
1649
|
+
node: ">=18",
|
|
1650
|
+
bun: ">=1.0"
|
|
1651
|
+
},
|
|
1652
|
+
keywords: [
|
|
1653
|
+
"git",
|
|
1654
|
+
"workflow",
|
|
1655
|
+
"squash-merge",
|
|
1656
|
+
"sync",
|
|
1657
|
+
"cli",
|
|
1658
|
+
"contribute",
|
|
1659
|
+
"fork",
|
|
1660
|
+
"dev-branch",
|
|
1661
|
+
"clean-commit"
|
|
1662
|
+
],
|
|
1663
|
+
author: "Waren Gonzaga",
|
|
1664
|
+
license: "GPL-3.0",
|
|
1665
|
+
repository: {
|
|
1666
|
+
type: "git",
|
|
1667
|
+
url: "git+https://github.com/warengonzaga/contribute-now.git"
|
|
1668
|
+
},
|
|
1669
|
+
dependencies: {
|
|
1670
|
+
"@clack/prompts": "^1.0.1",
|
|
1671
|
+
"@github/copilot-sdk": "^0.1.25",
|
|
1672
|
+
"@wgtechlabs/log-engine": "^2.3.1",
|
|
1673
|
+
citty: "^0.1.6",
|
|
1674
|
+
figlet: "^1.10.0",
|
|
1675
|
+
picocolors: "^1.1.1"
|
|
1676
|
+
},
|
|
1677
|
+
devDependencies: {
|
|
1678
|
+
"@biomejs/biome": "^2.4.4",
|
|
1679
|
+
"@types/bun": "latest",
|
|
1680
|
+
"@types/figlet": "^1.7.0",
|
|
1681
|
+
typescript: "^5.7.0"
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
577
1684
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1685
|
+
// src/utils/remote.ts
|
|
1686
|
+
function parseRepoFromUrl(url) {
|
|
1687
|
+
const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
1688
|
+
if (httpsMatch) {
|
|
1689
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
1690
|
+
}
|
|
1691
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
1692
|
+
if (sshMatch) {
|
|
1693
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
1694
|
+
}
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
async function detectForkSetup() {
|
|
1698
|
+
const remotes = await getRemotes();
|
|
1699
|
+
const hasOrigin = remotes.includes("origin");
|
|
1700
|
+
const hasUpstream = remotes.includes("upstream");
|
|
1701
|
+
return {
|
|
1702
|
+
isFork: hasUpstream,
|
|
1703
|
+
originRemote: hasOrigin ? "origin" : null,
|
|
1704
|
+
upstreamRemote: hasUpstream ? "upstream" : null
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
async function getRepoInfoFromRemote(remote = "origin") {
|
|
1708
|
+
const url = await getRemoteUrl(remote);
|
|
1709
|
+
if (!url)
|
|
583
1710
|
return null;
|
|
1711
|
+
return parseRepoFromUrl(url);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/commands/doctor.ts
|
|
1715
|
+
var PASS = ` ${pc6.green("✔")} `;
|
|
1716
|
+
var FAIL = ` ${pc6.red("✗")} `;
|
|
1717
|
+
var WARN = ` ${pc6.yellow("⚠")} `;
|
|
1718
|
+
function printReport(report) {
|
|
1719
|
+
for (const section of report.sections) {
|
|
1720
|
+
console.log(`
|
|
1721
|
+
${pc6.bold(pc6.underline(section.title))}`);
|
|
1722
|
+
for (const check of section.checks) {
|
|
1723
|
+
const prefix = check.ok ? check.warning ? WARN : PASS : FAIL;
|
|
1724
|
+
const text2 = check.detail ? `${check.label} ${pc6.dim(`— ${check.detail}`)}` : check.label;
|
|
1725
|
+
console.log(`${prefix}${text2}`);
|
|
1726
|
+
}
|
|
584
1727
|
}
|
|
1728
|
+
console.log();
|
|
1729
|
+
}
|
|
1730
|
+
function toJson(report) {
|
|
1731
|
+
return JSON.stringify(report.sections.map((s) => ({
|
|
1732
|
+
section: s.title,
|
|
1733
|
+
checks: s.checks.map((c) => ({
|
|
1734
|
+
label: c.label,
|
|
1735
|
+
ok: c.ok,
|
|
1736
|
+
warning: c.warning ?? false,
|
|
1737
|
+
detail: c.detail ?? null
|
|
1738
|
+
}))
|
|
1739
|
+
})), null, 2);
|
|
1740
|
+
}
|
|
1741
|
+
function runCmd(cmd, args) {
|
|
1742
|
+
return new Promise((resolve) => {
|
|
1743
|
+
execFileCb3(cmd, args, (error2, stdout) => {
|
|
1744
|
+
resolve({
|
|
1745
|
+
ok: !error2,
|
|
1746
|
+
stdout: (stdout ?? "").trim()
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
585
1750
|
}
|
|
586
|
-
async function
|
|
1751
|
+
async function toolSection() {
|
|
1752
|
+
const checks = [];
|
|
1753
|
+
checks.push({
|
|
1754
|
+
label: `contrib v${package_default.version ?? "unknown"}`,
|
|
1755
|
+
ok: true
|
|
1756
|
+
});
|
|
1757
|
+
const runtime = typeof globalThis.Bun !== "undefined" ? `Bun ${globalThis.Bun.version ?? "?"}` : `Node ${process.version}`;
|
|
1758
|
+
checks.push({ label: runtime, ok: true, detail: `${process.platform}-${process.arch}` });
|
|
1759
|
+
return { title: "Tool", checks };
|
|
1760
|
+
}
|
|
1761
|
+
async function depsSection() {
|
|
1762
|
+
const checks = [];
|
|
1763
|
+
const git = await runCmd("git", ["--version"]);
|
|
1764
|
+
checks.push({
|
|
1765
|
+
label: git.ok ? git.stdout.replace("git version ", "git ") : "git not found",
|
|
1766
|
+
ok: git.ok
|
|
1767
|
+
});
|
|
1768
|
+
const ghInstalled = await checkGhInstalled();
|
|
1769
|
+
if (ghInstalled) {
|
|
1770
|
+
const ghVer = await runCmd("gh", ["--version"]);
|
|
1771
|
+
const ver = ghVer.stdout.split(`
|
|
1772
|
+
`)[0] ?? "gh";
|
|
1773
|
+
checks.push({ label: ver, ok: true });
|
|
1774
|
+
const ghAuth = await checkGhAuth();
|
|
1775
|
+
checks.push({
|
|
1776
|
+
label: ghAuth ? "gh authenticated" : "gh not authenticated",
|
|
1777
|
+
ok: ghAuth,
|
|
1778
|
+
warning: !ghAuth,
|
|
1779
|
+
detail: ghAuth ? undefined : "run `gh auth login`"
|
|
1780
|
+
});
|
|
1781
|
+
} else {
|
|
1782
|
+
checks.push({
|
|
1783
|
+
label: "gh CLI not installed",
|
|
1784
|
+
ok: false,
|
|
1785
|
+
detail: "install from https://cli.github.com"
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
587
1788
|
try {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
Commits:
|
|
591
|
-
${commits.join(`
|
|
592
|
-
`)}
|
|
593
|
-
|
|
594
|
-
Diff (truncated):
|
|
595
|
-
${diff.slice(0, 4000)}`;
|
|
596
|
-
const result = await callCopilot(PR_DESCRIPTION_SYSTEM_PROMPT, userMessage, model);
|
|
597
|
-
if (!result)
|
|
598
|
-
return null;
|
|
599
|
-
const cleaned = result.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
600
|
-
return JSON.parse(cleaned);
|
|
1789
|
+
await import("@github/copilot-sdk");
|
|
1790
|
+
checks.push({ label: "Copilot SDK importable", ok: true });
|
|
601
1791
|
} catch {
|
|
602
|
-
|
|
1792
|
+
checks.push({
|
|
1793
|
+
label: "Copilot SDK not loadable",
|
|
1794
|
+
ok: false,
|
|
1795
|
+
warning: true,
|
|
1796
|
+
detail: "AI features will be unavailable"
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
return { title: "Dependencies", checks };
|
|
1800
|
+
}
|
|
1801
|
+
async function configSection() {
|
|
1802
|
+
const checks = [];
|
|
1803
|
+
const exists = configExists();
|
|
1804
|
+
if (!exists) {
|
|
1805
|
+
checks.push({
|
|
1806
|
+
label: ".contributerc.json not found",
|
|
1807
|
+
ok: false,
|
|
1808
|
+
detail: "run `contrib setup` to create it"
|
|
1809
|
+
});
|
|
1810
|
+
return { title: "Config", checks };
|
|
1811
|
+
}
|
|
1812
|
+
const config = readConfig();
|
|
1813
|
+
if (!config) {
|
|
1814
|
+
checks.push({ label: ".contributerc.json found but invalid", ok: false });
|
|
1815
|
+
return { title: "Config", checks };
|
|
1816
|
+
}
|
|
1817
|
+
checks.push({ label: ".contributerc.json found and valid", ok: true });
|
|
1818
|
+
const desc = WORKFLOW_DESCRIPTIONS[config.workflow] ?? config.workflow;
|
|
1819
|
+
checks.push({
|
|
1820
|
+
label: `Workflow: ${config.workflow}`,
|
|
1821
|
+
ok: true,
|
|
1822
|
+
detail: desc
|
|
1823
|
+
});
|
|
1824
|
+
checks.push({ label: `Role: ${config.role}`, ok: true });
|
|
1825
|
+
checks.push({ label: `Commit convention: ${config.commitConvention}`, ok: true });
|
|
1826
|
+
if (hasDevBranch(config.workflow)) {
|
|
1827
|
+
checks.push({
|
|
1828
|
+
label: `Dev branch: ${config.devBranch ?? "(not set)"}`,
|
|
1829
|
+
ok: !!config.devBranch
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
const ignored = isGitignored();
|
|
1833
|
+
checks.push({
|
|
1834
|
+
label: ignored ? ".contributerc.json in .gitignore" : ".contributerc.json NOT in .gitignore",
|
|
1835
|
+
ok: true,
|
|
1836
|
+
warning: !ignored,
|
|
1837
|
+
detail: ignored ? undefined : "consider adding it to .gitignore"
|
|
1838
|
+
});
|
|
1839
|
+
return { title: "Config", checks };
|
|
1840
|
+
}
|
|
1841
|
+
async function gitSection() {
|
|
1842
|
+
const checks = [];
|
|
1843
|
+
const inRepo = await isGitRepo();
|
|
1844
|
+
checks.push({
|
|
1845
|
+
label: inRepo ? "Inside a git repository" : "Not inside a git repository",
|
|
1846
|
+
ok: inRepo
|
|
1847
|
+
});
|
|
1848
|
+
if (!inRepo)
|
|
1849
|
+
return { title: "Git Environment", checks };
|
|
1850
|
+
const branch = await getCurrentBranch();
|
|
1851
|
+
const head = await runCmd("git", ["rev-parse", "--short", "HEAD"]);
|
|
1852
|
+
checks.push({
|
|
1853
|
+
label: `Branch: ${branch ?? "(detached)"}`,
|
|
1854
|
+
ok: !!branch,
|
|
1855
|
+
detail: head.ok ? `HEAD ${head.stdout}` : undefined
|
|
1856
|
+
});
|
|
1857
|
+
const remotes = await getRemotes();
|
|
1858
|
+
if (remotes.length === 0) {
|
|
1859
|
+
checks.push({ label: "No remotes configured", ok: false, warning: true });
|
|
1860
|
+
} else {
|
|
1861
|
+
for (const remote of remotes) {
|
|
1862
|
+
const url = await getRemoteUrl(remote);
|
|
1863
|
+
const repoInfo = url ? parseRepoFromUrl(url) : null;
|
|
1864
|
+
const detail = repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : url ?? "unknown URL";
|
|
1865
|
+
checks.push({ label: `Remote: ${remote}`, ok: true, detail });
|
|
1866
|
+
}
|
|
603
1867
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
1868
|
+
const dirty = await hasUncommittedChanges();
|
|
1869
|
+
checks.push({
|
|
1870
|
+
label: dirty ? "Uncommitted changes detected" : "Working tree clean",
|
|
1871
|
+
ok: true,
|
|
1872
|
+
warning: dirty
|
|
1873
|
+
});
|
|
1874
|
+
return { title: "Git Environment", checks };
|
|
1875
|
+
}
|
|
1876
|
+
async function forkSection() {
|
|
1877
|
+
const checks = [];
|
|
1878
|
+
const fork = await detectForkSetup();
|
|
1879
|
+
checks.push({
|
|
1880
|
+
label: fork.isFork ? "Fork detected (upstream remote exists)" : "Not a fork (no upstream remote)",
|
|
1881
|
+
ok: true
|
|
1882
|
+
});
|
|
1883
|
+
if (fork.originRemote) {
|
|
1884
|
+
checks.push({ label: `Origin remote: ${fork.originRemote}`, ok: true });
|
|
1885
|
+
}
|
|
1886
|
+
if (fork.upstreamRemote) {
|
|
1887
|
+
checks.push({ label: `Upstream remote: ${fork.upstreamRemote}`, ok: true });
|
|
611
1888
|
}
|
|
1889
|
+
return { title: "Fork Detection", checks };
|
|
612
1890
|
}
|
|
613
|
-
async function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1891
|
+
async function workflowSection() {
|
|
1892
|
+
const checks = [];
|
|
1893
|
+
const config = readConfig();
|
|
1894
|
+
if (!config) {
|
|
1895
|
+
checks.push({
|
|
1896
|
+
label: "Cannot resolve workflow (no config)",
|
|
1897
|
+
ok: false,
|
|
1898
|
+
detail: "run `contrib setup` first"
|
|
1899
|
+
});
|
|
1900
|
+
return { title: "Workflow Resolution", checks };
|
|
1901
|
+
}
|
|
1902
|
+
const baseBranch = getBaseBranch(config);
|
|
1903
|
+
checks.push({ label: `Base branch: ${baseBranch}`, ok: true });
|
|
1904
|
+
const sync = getSyncSource(config);
|
|
1905
|
+
checks.push({
|
|
1906
|
+
label: `Sync source: ${sync.ref}`,
|
|
1907
|
+
ok: true,
|
|
1908
|
+
detail: `strategy: ${sync.strategy}`
|
|
1909
|
+
});
|
|
1910
|
+
checks.push({
|
|
1911
|
+
label: `Branch prefixes: ${config.branchPrefixes.join(", ")}`,
|
|
1912
|
+
ok: config.branchPrefixes.length > 0
|
|
1913
|
+
});
|
|
1914
|
+
return { title: "Workflow Resolution", checks };
|
|
1915
|
+
}
|
|
1916
|
+
function envSection() {
|
|
1917
|
+
const checks = [];
|
|
1918
|
+
const vars = ["GITHUB_TOKEN", "GH_TOKEN", "COPILOT_AGENT_TOKEN", "NO_COLOR", "FORCE_COLOR", "CI"];
|
|
1919
|
+
for (const name of vars) {
|
|
1920
|
+
const val = process.env[name];
|
|
1921
|
+
if (val !== undefined) {
|
|
1922
|
+
const isSecret = name.toLowerCase().includes("token");
|
|
1923
|
+
const display = isSecret ? `${val.slice(0, 4)}${"*".repeat(Math.min(val.length - 4, 12))}` : val;
|
|
1924
|
+
checks.push({ label: `${name} = ${display}`, ok: true });
|
|
1925
|
+
}
|
|
622
1926
|
}
|
|
1927
|
+
if (checks.length === 0) {
|
|
1928
|
+
checks.push({ label: "No relevant environment variables set", ok: true });
|
|
1929
|
+
}
|
|
1930
|
+
return { title: "Environment", checks };
|
|
623
1931
|
}
|
|
624
|
-
|
|
625
|
-
// src/commands/commit.ts
|
|
626
|
-
var commit_default = defineCommand2({
|
|
1932
|
+
var doctor_default = defineCommand3({
|
|
627
1933
|
meta: {
|
|
628
|
-
name: "
|
|
629
|
-
description: "
|
|
1934
|
+
name: "doctor",
|
|
1935
|
+
description: "Diagnose the contribute-now CLI environment and configuration"
|
|
630
1936
|
},
|
|
631
1937
|
args: {
|
|
632
|
-
|
|
633
|
-
type: "string",
|
|
634
|
-
description: "AI model to use for commit message generation"
|
|
635
|
-
},
|
|
636
|
-
"no-ai": {
|
|
1938
|
+
json: {
|
|
637
1939
|
type: "boolean",
|
|
638
|
-
description: "
|
|
1940
|
+
description: "Output report as JSON",
|
|
639
1941
|
default: false
|
|
640
1942
|
}
|
|
641
1943
|
},
|
|
642
1944
|
async run({ args }) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
console.log(`
|
|
661
|
-
${pc4.bold("Changed files:")}`);
|
|
662
|
-
for (const f of changedFiles) {
|
|
663
|
-
console.log(` ${pc4.dim("•")} ${f}`);
|
|
664
|
-
}
|
|
665
|
-
console.log();
|
|
666
|
-
warn("No staged changes. Stage your files with `git add` and re-run.");
|
|
667
|
-
process.exit(1);
|
|
668
|
-
}
|
|
669
|
-
info(`Staged files: ${stagedFiles.join(", ")}`);
|
|
670
|
-
let commitMessage = null;
|
|
671
|
-
const useAI = !args["no-ai"];
|
|
672
|
-
if (useAI) {
|
|
673
|
-
const copilotError = await checkCopilotAvailable();
|
|
674
|
-
if (copilotError) {
|
|
675
|
-
warn(`AI unavailable: ${copilotError}`);
|
|
676
|
-
warn("Falling back to manual commit message entry.");
|
|
677
|
-
} else {
|
|
678
|
-
info("Generating commit message with AI...");
|
|
679
|
-
const diff = await getStagedDiff();
|
|
680
|
-
commitMessage = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
681
|
-
if (commitMessage) {
|
|
682
|
-
console.log(`
|
|
683
|
-
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(commitMessage))}`);
|
|
684
|
-
} else {
|
|
685
|
-
warn("AI did not return a commit message. Falling back to manual entry.");
|
|
686
|
-
}
|
|
687
|
-
}
|
|
1945
|
+
const isJson = args.json;
|
|
1946
|
+
const [tool, deps, config, git, fork, workflow] = await Promise.all([
|
|
1947
|
+
toolSection(),
|
|
1948
|
+
depsSection(),
|
|
1949
|
+
configSection(),
|
|
1950
|
+
gitSection(),
|
|
1951
|
+
forkSection(),
|
|
1952
|
+
workflowSection()
|
|
1953
|
+
]);
|
|
1954
|
+
const env = envSection();
|
|
1955
|
+
const report = {
|
|
1956
|
+
sections: [tool, deps, config, git, fork, workflow, env]
|
|
1957
|
+
};
|
|
1958
|
+
if (isJson) {
|
|
1959
|
+
console.log(toJson(report));
|
|
1960
|
+
return;
|
|
688
1961
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (action === "Accept this message") {
|
|
698
|
-
finalMessage = commitMessage;
|
|
699
|
-
} else if (action === "Edit this message") {
|
|
700
|
-
finalMessage = await inputPrompt("Edit commit message", commitMessage);
|
|
701
|
-
} else if (action === "Regenerate") {
|
|
702
|
-
info("Regenerating...");
|
|
703
|
-
const diff = await getStagedDiff();
|
|
704
|
-
const regen = await generateCommitMessage(diff, stagedFiles, args.model, config.commitConvention);
|
|
705
|
-
if (regen) {
|
|
706
|
-
console.log(`
|
|
707
|
-
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(regen))}`);
|
|
708
|
-
const ok = await confirmPrompt("Use this message?");
|
|
709
|
-
finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
|
|
710
|
-
} else {
|
|
711
|
-
warn("Regeneration failed. Falling back to manual entry.");
|
|
712
|
-
finalMessage = await inputPrompt("Enter commit message");
|
|
713
|
-
}
|
|
714
|
-
} else {
|
|
715
|
-
finalMessage = await inputPrompt("Enter commit message");
|
|
716
|
-
}
|
|
1962
|
+
heading("\uD83E\uDE7A contribute-now doctor");
|
|
1963
|
+
printReport(report);
|
|
1964
|
+
const total = report.sections.flatMap((s) => s.checks);
|
|
1965
|
+
const failures = total.filter((c) => !c.ok);
|
|
1966
|
+
const warnings = total.filter((c) => c.ok && c.warning);
|
|
1967
|
+
if (failures.length === 0 && warnings.length === 0) {
|
|
1968
|
+
console.log(` ${pc6.green("All checks passed!")} No issues detected.
|
|
1969
|
+
`);
|
|
717
1970
|
} else {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
console.log();
|
|
721
|
-
for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
|
|
722
|
-
console.log(pc4.dim(hint));
|
|
723
|
-
}
|
|
724
|
-
console.log();
|
|
1971
|
+
if (failures.length > 0) {
|
|
1972
|
+
console.log(` ${pc6.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
|
|
725
1973
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (!finalMessage) {
|
|
729
|
-
error("No commit message provided.");
|
|
730
|
-
process.exit(1);
|
|
731
|
-
}
|
|
732
|
-
const convention = config.commitConvention;
|
|
733
|
-
if (!validateCommitMessage(finalMessage, convention)) {
|
|
734
|
-
for (const line of getValidationError(convention)) {
|
|
735
|
-
warn(line);
|
|
1974
|
+
if (warnings.length > 0) {
|
|
1975
|
+
console.log(` ${pc6.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
|
|
736
1976
|
}
|
|
737
|
-
|
|
738
|
-
if (!proceed)
|
|
739
|
-
process.exit(1);
|
|
740
|
-
}
|
|
741
|
-
const result = await commitWithMessage(finalMessage);
|
|
742
|
-
if (result.exitCode !== 0) {
|
|
743
|
-
error(`Failed to commit: ${result.stderr}`);
|
|
744
|
-
process.exit(1);
|
|
1977
|
+
console.log();
|
|
745
1978
|
}
|
|
746
|
-
success(`✅ Committed: ${pc4.bold(finalMessage)}`);
|
|
747
1979
|
}
|
|
748
1980
|
});
|
|
749
1981
|
|
|
750
1982
|
// src/commands/hook.ts
|
|
751
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as
|
|
752
|
-
import { join as
|
|
753
|
-
import { defineCommand as
|
|
754
|
-
import
|
|
1983
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1984
|
+
import { join as join3 } from "node:path";
|
|
1985
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
1986
|
+
import pc7 from "picocolors";
|
|
755
1987
|
var HOOK_MARKER = "# managed by contribute-now";
|
|
756
1988
|
function getHooksDir(cwd = process.cwd()) {
|
|
757
|
-
return
|
|
1989
|
+
return join3(cwd, ".git", "hooks");
|
|
758
1990
|
}
|
|
759
1991
|
function getHookPath(cwd = process.cwd()) {
|
|
760
|
-
return
|
|
1992
|
+
return join3(getHooksDir(cwd), "commit-msg");
|
|
761
1993
|
}
|
|
762
1994
|
function generateHookScript() {
|
|
763
1995
|
return `#!/bin/sh
|
|
@@ -774,11 +2006,22 @@ case "$commit_msg" in
|
|
|
774
2006
|
Merge\\ *|fixup!*|squash!*|amend!*) exit 0 ;;
|
|
775
2007
|
esac
|
|
776
2008
|
|
|
777
|
-
#
|
|
778
|
-
|
|
2009
|
+
# Detect available package runner
|
|
2010
|
+
if command -v contrib >/dev/null 2>&1; then
|
|
2011
|
+
contrib validate "$commit_msg"
|
|
2012
|
+
elif command -v bunx >/dev/null 2>&1; then
|
|
2013
|
+
bunx contrib validate "$commit_msg"
|
|
2014
|
+
elif command -v pnpx >/dev/null 2>&1; then
|
|
2015
|
+
pnpx contrib validate "$commit_msg"
|
|
2016
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
2017
|
+
npx contrib validate "$commit_msg"
|
|
2018
|
+
else
|
|
2019
|
+
echo "Warning: No package runner found. Skipping commit message validation."
|
|
2020
|
+
exit 0
|
|
2021
|
+
fi
|
|
779
2022
|
`;
|
|
780
2023
|
}
|
|
781
|
-
var hook_default =
|
|
2024
|
+
var hook_default = defineCommand4({
|
|
782
2025
|
meta: {
|
|
783
2026
|
name: "hook",
|
|
784
2027
|
description: "Install or uninstall the commit-msg git hook"
|
|
@@ -822,155 +2065,43 @@ async function installHook() {
|
|
|
822
2065
|
const hookPath = getHookPath();
|
|
823
2066
|
const hooksDir = getHooksDir();
|
|
824
2067
|
if (existsSync2(hookPath)) {
|
|
825
|
-
const existing =
|
|
826
|
-
if (!existing.includes(HOOK_MARKER)) {
|
|
827
|
-
error("A commit-msg hook already exists and was not installed by contribute-now.");
|
|
828
|
-
warn(`Path: ${hookPath}`);
|
|
829
|
-
warn("Remove it manually or back it up before installing.");
|
|
830
|
-
process.exit(1);
|
|
831
|
-
}
|
|
832
|
-
info("Updating existing contribute-now hook...");
|
|
833
|
-
}
|
|
834
|
-
if (!existsSync2(hooksDir)) {
|
|
835
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
836
|
-
}
|
|
837
|
-
writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
|
|
838
|
-
success(`commit-msg hook installed.`);
|
|
839
|
-
info(`Convention: ${pc5.bold(CONVENTION_LABELS[config.commitConvention])}`);
|
|
840
|
-
info(`Path: ${pc5.dim(hookPath)}`);
|
|
841
|
-
}
|
|
842
|
-
async function uninstallHook() {
|
|
843
|
-
heading("\uD83E\uDE9D hook uninstall");
|
|
844
|
-
const hookPath = getHookPath();
|
|
845
|
-
if (!existsSync2(hookPath)) {
|
|
846
|
-
info("No commit-msg hook found. Nothing to uninstall.");
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
const content = readFileSync2(hookPath, "utf-8");
|
|
850
|
-
if (!content.includes(HOOK_MARKER)) {
|
|
851
|
-
error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
|
|
852
|
-
process.exit(1);
|
|
853
|
-
}
|
|
854
|
-
rmSync(hookPath);
|
|
855
|
-
success("commit-msg hook removed.");
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// src/commands/setup.ts
|
|
859
|
-
import { defineCommand as defineCommand4 } from "citty";
|
|
860
|
-
import pc6 from "picocolors";
|
|
861
|
-
|
|
862
|
-
// src/utils/gh.ts
|
|
863
|
-
import { execFile as execFileCb2 } from "node:child_process";
|
|
864
|
-
function run2(args) {
|
|
865
|
-
return new Promise((resolve) => {
|
|
866
|
-
execFileCb2("gh", args, (error2, stdout, stderr) => {
|
|
867
|
-
resolve({
|
|
868
|
-
exitCode: error2 ? error2.code != null ? Number(error2.code) : 1 : 0,
|
|
869
|
-
stdout: stdout ?? "",
|
|
870
|
-
stderr: stderr ?? ""
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
async function checkGhInstalled() {
|
|
876
|
-
try {
|
|
877
|
-
const { exitCode } = await run2(["--version"]);
|
|
878
|
-
return exitCode === 0;
|
|
879
|
-
} catch {
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
async function checkGhAuth() {
|
|
884
|
-
try {
|
|
885
|
-
const { exitCode } = await run2(["auth", "status"]);
|
|
886
|
-
return exitCode === 0;
|
|
887
|
-
} catch {
|
|
888
|
-
return false;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
async function checkRepoPermissions(owner, repo) {
|
|
892
|
-
const { exitCode, stdout } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
893
|
-
if (exitCode !== 0)
|
|
894
|
-
return null;
|
|
895
|
-
try {
|
|
896
|
-
return JSON.parse(stdout.trim());
|
|
897
|
-
} catch {
|
|
898
|
-
return null;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
async function isRepoFork() {
|
|
902
|
-
const { exitCode, stdout } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
|
|
903
|
-
if (exitCode !== 0)
|
|
904
|
-
return null;
|
|
905
|
-
const val = stdout.trim();
|
|
906
|
-
if (val === "true")
|
|
907
|
-
return true;
|
|
908
|
-
if (val === "false")
|
|
909
|
-
return false;
|
|
910
|
-
return null;
|
|
911
|
-
}
|
|
912
|
-
async function getCurrentRepoInfo() {
|
|
913
|
-
const { exitCode, stdout } = await run2([
|
|
914
|
-
"repo",
|
|
915
|
-
"view",
|
|
916
|
-
"--json",
|
|
917
|
-
"nameWithOwner",
|
|
918
|
-
"-q",
|
|
919
|
-
".nameWithOwner"
|
|
920
|
-
]);
|
|
921
|
-
if (exitCode !== 0)
|
|
922
|
-
return null;
|
|
923
|
-
const nameWithOwner = stdout.trim();
|
|
924
|
-
if (!nameWithOwner)
|
|
925
|
-
return null;
|
|
926
|
-
const [owner, repo] = nameWithOwner.split("/");
|
|
927
|
-
if (!owner || !repo)
|
|
928
|
-
return null;
|
|
929
|
-
return { owner, repo };
|
|
930
|
-
}
|
|
931
|
-
async function createPR(options) {
|
|
932
|
-
const args = [
|
|
933
|
-
"pr",
|
|
934
|
-
"create",
|
|
935
|
-
"--base",
|
|
936
|
-
options.base,
|
|
937
|
-
"--title",
|
|
938
|
-
options.title,
|
|
939
|
-
"--body",
|
|
940
|
-
options.body
|
|
941
|
-
];
|
|
942
|
-
if (options.draft)
|
|
943
|
-
args.push("--draft");
|
|
944
|
-
return run2(args);
|
|
945
|
-
}
|
|
946
|
-
async function createPRFill(base, draft) {
|
|
947
|
-
const args = ["pr", "create", "--base", base, "--fill"];
|
|
948
|
-
if (draft)
|
|
949
|
-
args.push("--draft");
|
|
950
|
-
return run2(args);
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// src/utils/remote.ts
|
|
954
|
-
function parseRepoFromUrl(url) {
|
|
955
|
-
const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
956
|
-
if (httpsMatch) {
|
|
957
|
-
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
2068
|
+
const existing = readFileSync3(hookPath, "utf-8");
|
|
2069
|
+
if (!existing.includes(HOOK_MARKER)) {
|
|
2070
|
+
error("A commit-msg hook already exists and was not installed by contribute-now.");
|
|
2071
|
+
warn(`Path: ${hookPath}`);
|
|
2072
|
+
warn("Remove it manually or back it up before installing.");
|
|
2073
|
+
process.exit(1);
|
|
2074
|
+
}
|
|
2075
|
+
info("Updating existing contribute-now hook...");
|
|
958
2076
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
2077
|
+
if (!existsSync2(hooksDir)) {
|
|
2078
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
962
2079
|
}
|
|
963
|
-
|
|
2080
|
+
writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
|
|
2081
|
+
success(`commit-msg hook installed.`);
|
|
2082
|
+
info(`Convention: ${pc7.bold(CONVENTION_LABELS[config.commitConvention])}`);
|
|
2083
|
+
info(`Path: ${pc7.dim(hookPath)}`);
|
|
964
2084
|
}
|
|
965
|
-
async function
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
2085
|
+
async function uninstallHook() {
|
|
2086
|
+
heading("\uD83E\uDE9D hook uninstall");
|
|
2087
|
+
const hookPath = getHookPath();
|
|
2088
|
+
if (!existsSync2(hookPath)) {
|
|
2089
|
+
info("No commit-msg hook found. Nothing to uninstall.");
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const content = readFileSync3(hookPath, "utf-8");
|
|
2093
|
+
if (!content.includes(HOOK_MARKER)) {
|
|
2094
|
+
error("The commit-msg hook was not installed by contribute-now. Leaving it untouched.");
|
|
2095
|
+
process.exit(1);
|
|
2096
|
+
}
|
|
2097
|
+
rmSync(hookPath);
|
|
2098
|
+
success("commit-msg hook removed.");
|
|
970
2099
|
}
|
|
971
2100
|
|
|
972
2101
|
// src/commands/setup.ts
|
|
973
|
-
|
|
2102
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
2103
|
+
import pc8 from "picocolors";
|
|
2104
|
+
var setup_default = defineCommand5({
|
|
974
2105
|
meta: {
|
|
975
2106
|
name: "setup",
|
|
976
2107
|
description: "Initialize contribute-now config for this repo (.contributerc.json)"
|
|
@@ -991,7 +2122,7 @@ var setup_default = defineCommand4({
|
|
|
991
2122
|
workflow = "github-flow";
|
|
992
2123
|
else if (workflowChoice.startsWith("Git Flow"))
|
|
993
2124
|
workflow = "git-flow";
|
|
994
|
-
info(`Workflow: ${
|
|
2125
|
+
info(`Workflow: ${pc8.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
|
|
995
2126
|
const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
|
|
996
2127
|
`${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
|
|
997
2128
|
CONVENTION_DESCRIPTIONS.conventional,
|
|
@@ -1044,8 +2175,8 @@ var setup_default = defineCommand4({
|
|
|
1044
2175
|
detectedRole = roleChoice;
|
|
1045
2176
|
detectionSource = "user selection";
|
|
1046
2177
|
} else {
|
|
1047
|
-
info(`Detected role: ${
|
|
1048
|
-
const confirmed = await confirmPrompt(`Role detected as ${
|
|
2178
|
+
info(`Detected role: ${pc8.bold(detectedRole)} (via ${detectionSource})`);
|
|
2179
|
+
const confirmed = await confirmPrompt(`Role detected as ${pc8.bold(detectedRole)}. Is this correct?`);
|
|
1049
2180
|
if (!confirmed) {
|
|
1050
2181
|
const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
|
|
1051
2182
|
detectedRole = roleChoice;
|
|
@@ -1090,37 +2221,22 @@ var setup_default = defineCommand4({
|
|
|
1090
2221
|
warn(' echo ".contributerc.json" >> .gitignore');
|
|
1091
2222
|
}
|
|
1092
2223
|
console.log();
|
|
1093
|
-
info(`Workflow: ${
|
|
1094
|
-
info(`Convention: ${
|
|
1095
|
-
info(`Role: ${
|
|
2224
|
+
info(`Workflow: ${pc8.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
2225
|
+
info(`Convention: ${pc8.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
|
|
2226
|
+
info(`Role: ${pc8.bold(config.role)}`);
|
|
1096
2227
|
if (config.devBranch) {
|
|
1097
|
-
info(`Main: ${
|
|
2228
|
+
info(`Main: ${pc8.bold(config.mainBranch)} | Dev: ${pc8.bold(config.devBranch)}`);
|
|
1098
2229
|
} else {
|
|
1099
|
-
info(`Main: ${
|
|
2230
|
+
info(`Main: ${pc8.bold(config.mainBranch)}`);
|
|
1100
2231
|
}
|
|
1101
|
-
info(`Origin: ${
|
|
2232
|
+
info(`Origin: ${pc8.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc8.bold(config.upstream)}` : ""}`);
|
|
1102
2233
|
}
|
|
1103
2234
|
});
|
|
1104
2235
|
|
|
1105
2236
|
// src/commands/start.ts
|
|
1106
|
-
import { defineCommand as
|
|
1107
|
-
import
|
|
1108
|
-
|
|
1109
|
-
// src/utils/branch.ts
|
|
1110
|
-
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
1111
|
-
function hasPrefix(branchName, prefixes = DEFAULT_PREFIXES) {
|
|
1112
|
-
return prefixes.some((p) => branchName.startsWith(`${p}/`));
|
|
1113
|
-
}
|
|
1114
|
-
function formatBranchName(prefix, name) {
|
|
1115
|
-
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1116
|
-
return `${prefix}/${sanitized}`;
|
|
1117
|
-
}
|
|
1118
|
-
function looksLikeNaturalLanguage(input) {
|
|
1119
|
-
return input.includes(" ") && !input.includes("/");
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// src/commands/start.ts
|
|
1123
|
-
var start_default = defineCommand5({
|
|
2237
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
2238
|
+
import pc9 from "picocolors";
|
|
2239
|
+
var start_default = defineCommand6({
|
|
1124
2240
|
meta: {
|
|
1125
2241
|
name: "start",
|
|
1126
2242
|
description: "Create a new feature branch from the latest base branch"
|
|
@@ -1162,40 +2278,47 @@ var start_default = defineCommand5({
|
|
|
1162
2278
|
heading("\uD83C\uDF3F contrib start");
|
|
1163
2279
|
const useAI = !args["no-ai"] && looksLikeNaturalLanguage(branchName);
|
|
1164
2280
|
if (useAI) {
|
|
1165
|
-
|
|
2281
|
+
const spinner = createSpinner("Generating branch name suggestion...");
|
|
1166
2282
|
const suggested = await suggestBranchName(branchName, args.model);
|
|
1167
2283
|
if (suggested) {
|
|
2284
|
+
spinner.success("Branch name suggestion ready.");
|
|
1168
2285
|
console.log(`
|
|
1169
|
-
${
|
|
1170
|
-
const accepted = await confirmPrompt(`Use ${
|
|
2286
|
+
${pc9.dim("AI suggestion:")} ${pc9.bold(pc9.cyan(suggested))}`);
|
|
2287
|
+
const accepted = await confirmPrompt(`Use ${pc9.bold(suggested)} as your branch name?`);
|
|
1171
2288
|
if (accepted) {
|
|
1172
2289
|
branchName = suggested;
|
|
1173
2290
|
} else {
|
|
1174
2291
|
branchName = await inputPrompt("Enter branch name", branchName);
|
|
1175
2292
|
}
|
|
2293
|
+
} else {
|
|
2294
|
+
spinner.fail("AI did not return a branch name suggestion.");
|
|
1176
2295
|
}
|
|
1177
2296
|
}
|
|
1178
2297
|
if (!hasPrefix(branchName, branchPrefixes)) {
|
|
1179
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
2298
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc9.bold(branchName)}:`, branchPrefixes);
|
|
1180
2299
|
branchName = formatBranchName(prefix, branchName);
|
|
1181
2300
|
}
|
|
1182
|
-
|
|
2301
|
+
if (!isValidBranchName(branchName)) {
|
|
2302
|
+
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
2303
|
+
process.exit(1);
|
|
2304
|
+
}
|
|
2305
|
+
info(`Creating branch: ${pc9.bold(branchName)}`);
|
|
1183
2306
|
await fetchRemote(syncSource.remote);
|
|
1184
|
-
const
|
|
1185
|
-
if (
|
|
2307
|
+
const updateResult = await updateLocalBranch(baseBranch, syncSource.ref);
|
|
2308
|
+
if (updateResult.exitCode !== 0) {}
|
|
1186
2309
|
const result = await createBranch(branchName, baseBranch);
|
|
1187
2310
|
if (result.exitCode !== 0) {
|
|
1188
2311
|
error(`Failed to create branch: ${result.stderr}`);
|
|
1189
2312
|
process.exit(1);
|
|
1190
2313
|
}
|
|
1191
|
-
success(`✅ Created ${
|
|
2314
|
+
success(`✅ Created ${pc9.bold(branchName)} from latest ${pc9.bold(baseBranch)}`);
|
|
1192
2315
|
}
|
|
1193
2316
|
});
|
|
1194
2317
|
|
|
1195
2318
|
// src/commands/status.ts
|
|
1196
|
-
import { defineCommand as
|
|
1197
|
-
import
|
|
1198
|
-
var status_default =
|
|
2319
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
2320
|
+
import pc10 from "picocolors";
|
|
2321
|
+
var status_default = defineCommand7({
|
|
1199
2322
|
meta: {
|
|
1200
2323
|
name: "status",
|
|
1201
2324
|
description: "Show sync status of branches"
|
|
@@ -1211,17 +2334,17 @@ var status_default = defineCommand6({
|
|
|
1211
2334
|
process.exit(1);
|
|
1212
2335
|
}
|
|
1213
2336
|
heading("\uD83D\uDCCA contribute-now status");
|
|
1214
|
-
console.log(` ${
|
|
1215
|
-
console.log(` ${
|
|
2337
|
+
console.log(` ${pc10.dim("Workflow:")} ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
2338
|
+
console.log(` ${pc10.dim("Role:")} ${pc10.bold(config.role)}`);
|
|
1216
2339
|
console.log();
|
|
1217
2340
|
await fetchAll();
|
|
1218
2341
|
const currentBranch = await getCurrentBranch();
|
|
1219
2342
|
const { mainBranch, origin, upstream, workflow } = config;
|
|
1220
2343
|
const baseBranch = getBaseBranch(config);
|
|
1221
2344
|
const isContributor = config.role === "contributor";
|
|
1222
|
-
const dirty = await hasUncommittedChanges();
|
|
2345
|
+
const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
|
|
1223
2346
|
if (dirty) {
|
|
1224
|
-
console.log(` ${
|
|
2347
|
+
console.log(` ${pc10.yellow("⚠")} ${pc10.yellow("Uncommitted changes in working tree")}`);
|
|
1225
2348
|
console.log();
|
|
1226
2349
|
}
|
|
1227
2350
|
const mainRemote = `${origin}/${mainBranch}`;
|
|
@@ -1237,31 +2360,131 @@ var status_default = defineCommand6({
|
|
|
1237
2360
|
if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
1238
2361
|
const branchDiv = await getDivergence(currentBranch, baseBranch);
|
|
1239
2362
|
const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
|
|
1240
|
-
console.log(branchLine +
|
|
2363
|
+
console.log(branchLine + pc10.dim(` (current ${pc10.green("*")})`));
|
|
1241
2364
|
} else if (currentBranch) {
|
|
1242
|
-
console.log(
|
|
2365
|
+
console.log(pc10.dim(` (on ${pc10.bold(currentBranch)} branch)`));
|
|
2366
|
+
}
|
|
2367
|
+
const hasFiles = fileStatus.staged.length > 0 || fileStatus.modified.length > 0 || fileStatus.untracked.length > 0;
|
|
2368
|
+
if (hasFiles) {
|
|
2369
|
+
console.log();
|
|
2370
|
+
if (fileStatus.staged.length > 0) {
|
|
2371
|
+
console.log(` ${pc10.green("Staged for commit:")}`);
|
|
2372
|
+
for (const { file, status } of fileStatus.staged) {
|
|
2373
|
+
console.log(` ${pc10.green("+")} ${pc10.dim(`${status}:`)} ${file}`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (fileStatus.modified.length > 0) {
|
|
2377
|
+
console.log(` ${pc10.yellow("Unstaged changes:")}`);
|
|
2378
|
+
for (const { file, status } of fileStatus.modified) {
|
|
2379
|
+
console.log(` ${pc10.yellow("~")} ${pc10.dim(`${status}:`)} ${file}`);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
if (fileStatus.untracked.length > 0) {
|
|
2383
|
+
console.log(` ${pc10.red("Untracked files:")}`);
|
|
2384
|
+
for (const file of fileStatus.untracked) {
|
|
2385
|
+
console.log(` ${pc10.red("?")} ${file}`);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
} else if (!dirty) {
|
|
2389
|
+
console.log(` ${pc10.green("✓")} ${pc10.dim("Working tree clean")}`);
|
|
2390
|
+
}
|
|
2391
|
+
const tips = [];
|
|
2392
|
+
if (fileStatus.staged.length > 0) {
|
|
2393
|
+
tips.push(`Run ${pc10.bold("contrib commit")} to commit staged changes`);
|
|
2394
|
+
}
|
|
2395
|
+
if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
|
|
2396
|
+
tips.push(`Run ${pc10.bold("contrib commit")} to stage and commit changes`);
|
|
2397
|
+
}
|
|
2398
|
+
if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0 && currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
2399
|
+
const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
|
|
2400
|
+
if (branchDiv.ahead > 0) {
|
|
2401
|
+
tips.push(`Run ${pc10.bold("contrib submit")} to push and create/update your PR`);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
if (tips.length > 0) {
|
|
2405
|
+
console.log();
|
|
2406
|
+
console.log(` ${pc10.dim("\uD83D\uDCA1 Tip:")}`);
|
|
2407
|
+
for (const tip of tips) {
|
|
2408
|
+
console.log(` ${pc10.dim(tip)}`);
|
|
2409
|
+
}
|
|
1243
2410
|
}
|
|
1244
2411
|
console.log();
|
|
1245
2412
|
}
|
|
1246
2413
|
});
|
|
1247
2414
|
function formatStatus(branch, base, ahead, behind) {
|
|
1248
|
-
const label =
|
|
2415
|
+
const label = pc10.bold(branch.padEnd(20));
|
|
1249
2416
|
if (ahead === 0 && behind === 0) {
|
|
1250
|
-
return ` ${
|
|
2417
|
+
return ` ${pc10.green("✓")} ${label} ${pc10.dim(`in sync with ${base}`)}`;
|
|
1251
2418
|
}
|
|
1252
2419
|
if (ahead > 0 && behind === 0) {
|
|
1253
|
-
return ` ${
|
|
2420
|
+
return ` ${pc10.yellow("↑")} ${label} ${pc10.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
|
|
1254
2421
|
}
|
|
1255
2422
|
if (behind > 0 && ahead === 0) {
|
|
1256
|
-
return ` ${
|
|
2423
|
+
return ` ${pc10.red("↓")} ${label} ${pc10.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
|
|
1257
2424
|
}
|
|
1258
|
-
return ` ${
|
|
2425
|
+
return ` ${pc10.red("⚡")} ${label} ${pc10.yellow(`${ahead} ahead`)}${pc10.dim(", ")}${pc10.red(`${behind} behind`)} ${pc10.dim(base)}`;
|
|
1259
2426
|
}
|
|
1260
2427
|
|
|
1261
2428
|
// src/commands/submit.ts
|
|
1262
|
-
import { defineCommand as
|
|
1263
|
-
import
|
|
1264
|
-
|
|
2429
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
2430
|
+
import pc11 from "picocolors";
|
|
2431
|
+
async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
2432
|
+
info(`Checking out ${pc11.bold(baseBranch)}...`);
|
|
2433
|
+
const coResult = await checkoutBranch2(baseBranch);
|
|
2434
|
+
if (coResult.exitCode !== 0) {
|
|
2435
|
+
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2436
|
+
process.exit(1);
|
|
2437
|
+
}
|
|
2438
|
+
info(`Squash merging ${pc11.bold(featureBranch)} into ${pc11.bold(baseBranch)}...`);
|
|
2439
|
+
const mergeResult = await mergeSquash(featureBranch);
|
|
2440
|
+
if (mergeResult.exitCode !== 0) {
|
|
2441
|
+
error(`Squash merge failed: ${mergeResult.stderr}`);
|
|
2442
|
+
process.exit(1);
|
|
2443
|
+
}
|
|
2444
|
+
let message = options?.defaultMsg;
|
|
2445
|
+
if (!message) {
|
|
2446
|
+
const copilotError = await checkCopilotAvailable();
|
|
2447
|
+
if (!copilotError) {
|
|
2448
|
+
const spinner = createSpinner("Generating AI commit message for squash merge...");
|
|
2449
|
+
const [stagedDiff, stagedFiles] = await Promise.all([getStagedDiff(), getStagedFiles()]);
|
|
2450
|
+
const aiMsg = await generateCommitMessage(stagedDiff, stagedFiles, options?.model, options?.convention ?? "clean-commit");
|
|
2451
|
+
if (aiMsg) {
|
|
2452
|
+
message = aiMsg;
|
|
2453
|
+
spinner.success("AI commit message generated.");
|
|
2454
|
+
} else {
|
|
2455
|
+
spinner.fail("AI did not return a commit message.");
|
|
2456
|
+
}
|
|
2457
|
+
} else {
|
|
2458
|
+
warn(`AI unavailable: ${copilotError}`);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
const fallback = message || `squash merge ${featureBranch}`;
|
|
2462
|
+
const finalMsg = await inputPrompt("Commit message", fallback);
|
|
2463
|
+
const commitResult = await commitWithMessage(finalMsg);
|
|
2464
|
+
if (commitResult.exitCode !== 0) {
|
|
2465
|
+
error(`Commit failed: ${commitResult.stderr}`);
|
|
2466
|
+
process.exit(1);
|
|
2467
|
+
}
|
|
2468
|
+
info(`Pushing ${pc11.bold(baseBranch)} to ${origin}...`);
|
|
2469
|
+
const pushResult = await pushBranch(origin, baseBranch);
|
|
2470
|
+
if (pushResult.exitCode !== 0) {
|
|
2471
|
+
error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
|
|
2472
|
+
process.exit(1);
|
|
2473
|
+
}
|
|
2474
|
+
info(`Deleting local branch ${pc11.bold(featureBranch)}...`);
|
|
2475
|
+
const delLocal = await forceDeleteBranch(featureBranch);
|
|
2476
|
+
if (delLocal.exitCode !== 0) {
|
|
2477
|
+
warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
|
|
2478
|
+
}
|
|
2479
|
+
info(`Deleting remote branch ${pc11.bold(featureBranch)}...`);
|
|
2480
|
+
const delRemote = await deleteRemoteBranch(origin, featureBranch);
|
|
2481
|
+
if (delRemote.exitCode !== 0) {
|
|
2482
|
+
warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
|
|
2483
|
+
}
|
|
2484
|
+
success(`✅ Squash merged ${pc11.bold(featureBranch)} into ${pc11.bold(baseBranch)} and pushed.`);
|
|
2485
|
+
info(`Run ${pc11.bold("contrib start")} to begin a new feature.`);
|
|
2486
|
+
}
|
|
2487
|
+
var submit_default = defineCommand8({
|
|
1265
2488
|
meta: {
|
|
1266
2489
|
name: "submit",
|
|
1267
2490
|
description: "Push current branch and create a pull request"
|
|
@@ -1301,89 +2524,251 @@ var submit_default = defineCommand7({
|
|
|
1301
2524
|
process.exit(1);
|
|
1302
2525
|
}
|
|
1303
2526
|
if (protectedBranches.includes(currentBranch)) {
|
|
1304
|
-
error(`Cannot submit ${protectedBranches.map((b) =>
|
|
2527
|
+
error(`Cannot submit ${protectedBranches.map((b) => pc11.bold(b)).join(" or ")} as a PR. Switch to your feature branch.`);
|
|
1305
2528
|
process.exit(1);
|
|
1306
2529
|
}
|
|
1307
2530
|
heading("\uD83D\uDE80 contrib submit");
|
|
1308
|
-
info(`Pushing ${pc9.bold(currentBranch)} to ${origin}...`);
|
|
1309
|
-
const pushResult = await pushSetUpstream(origin, currentBranch);
|
|
1310
|
-
if (pushResult.exitCode !== 0) {
|
|
1311
|
-
error(`Failed to push: ${pushResult.stderr}`);
|
|
1312
|
-
process.exit(1);
|
|
1313
|
-
}
|
|
1314
2531
|
const ghInstalled = await checkGhInstalled();
|
|
1315
2532
|
const ghAuthed = ghInstalled && await checkGhAuth();
|
|
1316
|
-
if (
|
|
1317
|
-
const
|
|
1318
|
-
if (
|
|
1319
|
-
|
|
2533
|
+
if (ghInstalled && ghAuthed) {
|
|
2534
|
+
const mergedPR = await getMergedPRForBranch(currentBranch);
|
|
2535
|
+
if (mergedPR) {
|
|
2536
|
+
warn(`PR #${mergedPR.number} (${pc11.bold(mergedPR.title)}) was already merged.`);
|
|
2537
|
+
const localWork = await hasLocalWork(origin, currentBranch);
|
|
2538
|
+
const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
|
|
2539
|
+
if (hasWork) {
|
|
2540
|
+
if (localWork.uncommitted) {
|
|
2541
|
+
warn("You have uncommitted changes in your working tree.");
|
|
2542
|
+
}
|
|
2543
|
+
if (localWork.unpushedCommits > 0) {
|
|
2544
|
+
warn(`You have ${pc11.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
|
|
2545
|
+
}
|
|
2546
|
+
const SAVE_NEW_BRANCH = "Save changes to a new branch";
|
|
2547
|
+
const DISCARD = "Discard all changes and clean up";
|
|
2548
|
+
const CANCEL2 = "Cancel";
|
|
2549
|
+
const action = await selectPrompt("This branch was merged but you have local changes. What would you like to do?", [SAVE_NEW_BRANCH, DISCARD, CANCEL2]);
|
|
2550
|
+
if (action === CANCEL2) {
|
|
2551
|
+
info("No changes made. You are still on your current branch.");
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (action === SAVE_NEW_BRANCH) {
|
|
2555
|
+
info(pc11.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
2556
|
+
const description = await inputPrompt("What are you working on?");
|
|
2557
|
+
let newBranchName = description;
|
|
2558
|
+
if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
|
|
2559
|
+
const spinner = createSpinner("Generating branch name suggestion...");
|
|
2560
|
+
const suggested = await suggestBranchName(description, args.model);
|
|
2561
|
+
if (suggested) {
|
|
2562
|
+
spinner.success("Branch name suggestion ready.");
|
|
2563
|
+
console.log(`
|
|
2564
|
+
${pc11.dim("AI suggestion:")} ${pc11.bold(pc11.cyan(suggested))}`);
|
|
2565
|
+
const accepted = await confirmPrompt(`Use ${pc11.bold(suggested)} as your branch name?`);
|
|
2566
|
+
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
2567
|
+
} else {
|
|
2568
|
+
spinner.fail("AI did not return a suggestion.");
|
|
2569
|
+
newBranchName = await inputPrompt("Enter branch name", description);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
2573
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc11.bold(newBranchName)}:`, config.branchPrefixes);
|
|
2574
|
+
newBranchName = formatBranchName(prefix, newBranchName);
|
|
2575
|
+
}
|
|
2576
|
+
if (!isValidBranchName(newBranchName)) {
|
|
2577
|
+
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
const staleUpstream = await getUpstreamRef();
|
|
2581
|
+
const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
|
|
2582
|
+
const renameResult = await renameBranch(currentBranch, newBranchName);
|
|
2583
|
+
if (renameResult.exitCode !== 0) {
|
|
2584
|
+
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
2585
|
+
process.exit(1);
|
|
2586
|
+
}
|
|
2587
|
+
success(`Renamed ${pc11.bold(currentBranch)} → ${pc11.bold(newBranchName)}`);
|
|
2588
|
+
await unsetUpstream();
|
|
2589
|
+
const syncSource2 = getSyncSource(config);
|
|
2590
|
+
info(`Syncing ${pc11.bold(newBranchName)} with latest ${pc11.bold(baseBranch)}...`);
|
|
2591
|
+
await fetchRemote(syncSource2.remote);
|
|
2592
|
+
let rebaseResult;
|
|
2593
|
+
if (staleUpstreamHash) {
|
|
2594
|
+
rebaseResult = await rebaseOnto(syncSource2.ref, staleUpstreamHash);
|
|
2595
|
+
} else {
|
|
2596
|
+
const savedStrategy = await determineRebaseStrategy(newBranchName, syncSource2.ref);
|
|
2597
|
+
rebaseResult = savedStrategy.strategy === "onto" && savedStrategy.ontoOldBase ? await rebaseOnto(syncSource2.ref, savedStrategy.ontoOldBase) : await rebase(syncSource2.ref);
|
|
2598
|
+
}
|
|
2599
|
+
if (rebaseResult.exitCode !== 0) {
|
|
2600
|
+
warn("Rebase encountered conflicts. Resolve them manually, then run:");
|
|
2601
|
+
info(` ${pc11.bold("git rebase --continue")}`);
|
|
2602
|
+
} else {
|
|
2603
|
+
success(`Rebased ${pc11.bold(newBranchName)} onto ${pc11.bold(syncSource2.ref)}.`);
|
|
2604
|
+
}
|
|
2605
|
+
info(`All your changes are preserved. Run ${pc11.bold("contrib submit")} when ready to create a new PR.`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
warn("Discarding local changes...");
|
|
2609
|
+
}
|
|
2610
|
+
const syncSource = getSyncSource(config);
|
|
2611
|
+
info(`Switching to ${pc11.bold(baseBranch)} and syncing...`);
|
|
2612
|
+
await fetchRemote(syncSource.remote);
|
|
2613
|
+
const coResult = await checkoutBranch2(baseBranch);
|
|
2614
|
+
if (coResult.exitCode !== 0) {
|
|
2615
|
+
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2616
|
+
process.exit(1);
|
|
2617
|
+
}
|
|
2618
|
+
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
2619
|
+
success(`Synced ${pc11.bold(baseBranch)} with ${pc11.bold(syncSource.ref)}.`);
|
|
2620
|
+
info(`Deleting stale branch ${pc11.bold(currentBranch)}...`);
|
|
2621
|
+
const delResult = await forceDeleteBranch(currentBranch);
|
|
2622
|
+
if (delResult.exitCode === 0) {
|
|
2623
|
+
success(`Deleted ${pc11.bold(currentBranch)}.`);
|
|
2624
|
+
} else {
|
|
2625
|
+
warn(`Could not delete branch: ${delResult.stderr.trim()}`);
|
|
2626
|
+
}
|
|
1320
2627
|
console.log();
|
|
1321
|
-
info(
|
|
1322
|
-
|
|
1323
|
-
} else {
|
|
1324
|
-
info("gh CLI not available. Create your PR manually on GitHub.");
|
|
2628
|
+
info(`You're now on ${pc11.bold(baseBranch)}. Run ${pc11.bold("contrib start")} to begin a new feature.`);
|
|
2629
|
+
return;
|
|
1325
2630
|
}
|
|
1326
|
-
return;
|
|
1327
2631
|
}
|
|
1328
2632
|
let prTitle = null;
|
|
1329
2633
|
let prBody = null;
|
|
1330
|
-
|
|
1331
|
-
const copilotError = await
|
|
2634
|
+
async function tryGenerateAI() {
|
|
2635
|
+
const [copilotError, commits, diff] = await Promise.all([
|
|
2636
|
+
checkCopilotAvailable(),
|
|
2637
|
+
getLog(baseBranch, "HEAD"),
|
|
2638
|
+
getLogDiff(baseBranch, "HEAD")
|
|
2639
|
+
]);
|
|
1332
2640
|
if (!copilotError) {
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
const diff = await getLogDiff(baseBranch, "HEAD");
|
|
1336
|
-
const result = await generatePRDescription(commits, diff, args.model);
|
|
2641
|
+
const spinner = createSpinner("Generating AI PR description...");
|
|
2642
|
+
const result = await generatePRDescription(commits, diff, args.model, config.commitConvention);
|
|
1337
2643
|
if (result) {
|
|
1338
2644
|
prTitle = result.title;
|
|
1339
2645
|
prBody = result.body;
|
|
2646
|
+
spinner.success("PR description generated.");
|
|
1340
2647
|
console.log(`
|
|
1341
|
-
${
|
|
2648
|
+
${pc11.dim("AI title:")} ${pc11.bold(pc11.cyan(prTitle))}`);
|
|
1342
2649
|
console.log(`
|
|
1343
|
-
${
|
|
1344
|
-
console.log(
|
|
2650
|
+
${pc11.dim("AI body preview:")}`);
|
|
2651
|
+
console.log(pc11.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
|
|
1345
2652
|
} else {
|
|
1346
|
-
|
|
2653
|
+
spinner.fail("AI did not return a PR description.");
|
|
1347
2654
|
}
|
|
1348
2655
|
} else {
|
|
1349
2656
|
warn(`AI unavailable: ${copilotError}`);
|
|
1350
2657
|
}
|
|
1351
2658
|
}
|
|
1352
|
-
if (
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
2659
|
+
if (!args["no-ai"]) {
|
|
2660
|
+
await tryGenerateAI();
|
|
2661
|
+
}
|
|
2662
|
+
const CANCEL = "Cancel";
|
|
2663
|
+
const SQUASH_LOCAL = `Squash merge to ${baseBranch} locally (no PR)`;
|
|
2664
|
+
const REGENERATE = "Regenerate AI description";
|
|
2665
|
+
let submitAction = "cancel";
|
|
2666
|
+
const isMaintainer = config.role === "maintainer";
|
|
2667
|
+
let actionResolved = false;
|
|
2668
|
+
while (!actionResolved) {
|
|
2669
|
+
if (prTitle && prBody) {
|
|
2670
|
+
const choices = ["Use AI description"];
|
|
2671
|
+
if (isMaintainer)
|
|
2672
|
+
choices.push(SQUASH_LOCAL);
|
|
2673
|
+
choices.push("Edit title", "Write manually", "Use gh --fill (auto-fill from commits)", REGENERATE, CANCEL);
|
|
2674
|
+
const action = await selectPrompt("What would you like to do with the PR description?", choices);
|
|
2675
|
+
if (action === CANCEL) {
|
|
2676
|
+
submitAction = "cancel";
|
|
2677
|
+
actionResolved = true;
|
|
2678
|
+
} else if (action === REGENERATE) {
|
|
2679
|
+
prTitle = null;
|
|
2680
|
+
prBody = null;
|
|
2681
|
+
await tryGenerateAI();
|
|
2682
|
+
} else if (action === SQUASH_LOCAL) {
|
|
2683
|
+
submitAction = "squash";
|
|
2684
|
+
actionResolved = true;
|
|
2685
|
+
} else if (action === "Use AI description") {
|
|
2686
|
+
submitAction = "create-pr";
|
|
2687
|
+
actionResolved = true;
|
|
2688
|
+
} else if (action === "Edit title") {
|
|
2689
|
+
prTitle = await inputPrompt("PR title", prTitle);
|
|
2690
|
+
submitAction = "create-pr";
|
|
2691
|
+
actionResolved = true;
|
|
2692
|
+
} else if (action === "Write manually") {
|
|
2693
|
+
prTitle = await inputPrompt("PR title");
|
|
2694
|
+
prBody = await inputPrompt("PR body (markdown)");
|
|
2695
|
+
submitAction = "create-pr";
|
|
2696
|
+
actionResolved = true;
|
|
2697
|
+
} else {
|
|
2698
|
+
submitAction = "fill";
|
|
2699
|
+
actionResolved = true;
|
|
2700
|
+
}
|
|
1364
2701
|
} else {
|
|
1365
|
-
const
|
|
1366
|
-
if (
|
|
1367
|
-
|
|
1368
|
-
|
|
2702
|
+
const choices = [];
|
|
2703
|
+
if (isMaintainer)
|
|
2704
|
+
choices.push(SQUASH_LOCAL);
|
|
2705
|
+
if (!args["no-ai"])
|
|
2706
|
+
choices.push(REGENERATE);
|
|
2707
|
+
choices.push("Write title & body manually", "Use gh --fill (auto-fill from commits)", CANCEL);
|
|
2708
|
+
const action = await selectPrompt("How would you like to create the PR?", choices);
|
|
2709
|
+
if (action === CANCEL) {
|
|
2710
|
+
submitAction = "cancel";
|
|
2711
|
+
actionResolved = true;
|
|
2712
|
+
} else if (action === REGENERATE) {
|
|
2713
|
+
await tryGenerateAI();
|
|
2714
|
+
} else if (action === SQUASH_LOCAL) {
|
|
2715
|
+
submitAction = "squash";
|
|
2716
|
+
actionResolved = true;
|
|
2717
|
+
} else if (action === "Write title & body manually") {
|
|
2718
|
+
prTitle = await inputPrompt("PR title");
|
|
2719
|
+
prBody = await inputPrompt("PR body (markdown)");
|
|
2720
|
+
submitAction = "create-pr";
|
|
2721
|
+
actionResolved = true;
|
|
2722
|
+
} else {
|
|
2723
|
+
submitAction = "fill";
|
|
2724
|
+
actionResolved = true;
|
|
1369
2725
|
}
|
|
1370
|
-
success(`✅ PR created: ${fillResult.stdout.trim()}`);
|
|
1371
|
-
return;
|
|
1372
2726
|
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2727
|
+
}
|
|
2728
|
+
if (submitAction === "cancel") {
|
|
2729
|
+
warn("Submit cancelled.");
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
if (submitAction === "squash") {
|
|
2733
|
+
await performSquashMerge(origin, baseBranch, currentBranch, {
|
|
2734
|
+
defaultMsg: prTitle ?? undefined,
|
|
2735
|
+
model: args.model,
|
|
2736
|
+
convention: config.commitConvention
|
|
2737
|
+
});
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
info(`Pushing ${pc11.bold(currentBranch)} to ${origin}...`);
|
|
2741
|
+
const pushResult = await pushSetUpstream(origin, currentBranch);
|
|
2742
|
+
if (pushResult.exitCode !== 0) {
|
|
2743
|
+
error(`Failed to push: ${pushResult.stderr}`);
|
|
2744
|
+
process.exit(1);
|
|
2745
|
+
}
|
|
2746
|
+
if (!ghInstalled || !ghAuthed) {
|
|
2747
|
+
const repoInfo = await getRepoInfoFromRemote(origin);
|
|
2748
|
+
if (repoInfo) {
|
|
2749
|
+
const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
|
|
2750
|
+
console.log();
|
|
2751
|
+
info("Create your PR manually:");
|
|
2752
|
+
console.log(` ${pc11.cyan(prUrl)}`);
|
|
1378
2753
|
} else {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
2754
|
+
info("gh CLI not available. Create your PR manually on GitHub.");
|
|
2755
|
+
}
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const existingPR = await getPRForBranch(currentBranch);
|
|
2759
|
+
if (existingPR) {
|
|
2760
|
+
success(`Pushed changes to existing PR #${existingPR.number}: ${pc11.bold(existingPR.title)}`);
|
|
2761
|
+
console.log(` ${pc11.cyan(existingPR.url)}`);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
if (submitAction === "fill") {
|
|
2765
|
+
const fillResult = await createPRFill(baseBranch, args.draft);
|
|
2766
|
+
if (fillResult.exitCode !== 0) {
|
|
2767
|
+
error(`Failed to create PR: ${fillResult.stderr}`);
|
|
2768
|
+
process.exit(1);
|
|
1386
2769
|
}
|
|
2770
|
+
success(`✅ PR created: ${fillResult.stdout.trim()}`);
|
|
2771
|
+
return;
|
|
1387
2772
|
}
|
|
1388
2773
|
if (!prTitle) {
|
|
1389
2774
|
error("No PR title provided.");
|
|
@@ -1404,9 +2789,9 @@ ${pc9.dim("AI body preview:")}`);
|
|
|
1404
2789
|
});
|
|
1405
2790
|
|
|
1406
2791
|
// src/commands/sync.ts
|
|
1407
|
-
import { defineCommand as
|
|
1408
|
-
import
|
|
1409
|
-
var sync_default =
|
|
2792
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
2793
|
+
import pc12 from "picocolors";
|
|
2794
|
+
var sync_default = defineCommand9({
|
|
1410
2795
|
meta: {
|
|
1411
2796
|
name: "sync",
|
|
1412
2797
|
description: "Sync your local branches with the remote"
|
|
@@ -1448,16 +2833,16 @@ var sync_default = defineCommand8({
|
|
|
1448
2833
|
}
|
|
1449
2834
|
const div = await getDivergence(baseBranch, syncSource.ref);
|
|
1450
2835
|
if (div.ahead > 0 || div.behind > 0) {
|
|
1451
|
-
info(`${
|
|
2836
|
+
info(`${pc12.bold(baseBranch)} is ${pc12.yellow(`${div.ahead} ahead`)} and ${pc12.red(`${div.behind} behind`)} ${syncSource.ref}`);
|
|
1452
2837
|
} else {
|
|
1453
|
-
info(`${
|
|
2838
|
+
info(`${pc12.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
|
|
1454
2839
|
}
|
|
1455
2840
|
if (!args.yes) {
|
|
1456
|
-
const ok = await confirmPrompt(`This will pull ${
|
|
2841
|
+
const ok = await confirmPrompt(`This will pull ${pc12.bold(syncSource.ref)} into local ${pc12.bold(baseBranch)}.`);
|
|
1457
2842
|
if (!ok)
|
|
1458
2843
|
process.exit(0);
|
|
1459
2844
|
}
|
|
1460
|
-
const coResult = await
|
|
2845
|
+
const coResult = await checkoutBranch2(baseBranch);
|
|
1461
2846
|
if (coResult.exitCode !== 0) {
|
|
1462
2847
|
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
1463
2848
|
process.exit(1);
|
|
@@ -1471,25 +2856,25 @@ var sync_default = defineCommand8({
|
|
|
1471
2856
|
if (hasDevBranch(workflow) && role === "maintainer") {
|
|
1472
2857
|
const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
|
|
1473
2858
|
if (mainDiv.behind > 0) {
|
|
1474
|
-
info(`Also syncing ${
|
|
1475
|
-
const mainCoResult = await
|
|
2859
|
+
info(`Also syncing ${pc12.bold(config.mainBranch)}...`);
|
|
2860
|
+
const mainCoResult = await checkoutBranch2(config.mainBranch);
|
|
1476
2861
|
if (mainCoResult.exitCode === 0) {
|
|
1477
2862
|
const mainPullResult = await pullBranch(origin, config.mainBranch);
|
|
1478
2863
|
if (mainPullResult.exitCode === 0) {
|
|
1479
2864
|
success(`✅ ${config.mainBranch} is now in sync with ${origin}/${config.mainBranch}`);
|
|
1480
2865
|
}
|
|
1481
2866
|
}
|
|
1482
|
-
await
|
|
2867
|
+
await checkoutBranch2(baseBranch);
|
|
1483
2868
|
}
|
|
1484
2869
|
}
|
|
1485
2870
|
}
|
|
1486
2871
|
});
|
|
1487
2872
|
|
|
1488
2873
|
// src/commands/update.ts
|
|
1489
|
-
import { readFileSync as
|
|
1490
|
-
import { defineCommand as
|
|
1491
|
-
import
|
|
1492
|
-
var update_default =
|
|
2874
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2875
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
2876
|
+
import pc13 from "picocolors";
|
|
2877
|
+
var update_default = defineCommand10({
|
|
1493
2878
|
meta: {
|
|
1494
2879
|
name: "update",
|
|
1495
2880
|
description: "Rebase current branch onto the latest base branch"
|
|
@@ -1524,7 +2909,7 @@ var update_default = defineCommand9({
|
|
|
1524
2909
|
process.exit(1);
|
|
1525
2910
|
}
|
|
1526
2911
|
if (protectedBranches.includes(currentBranch)) {
|
|
1527
|
-
error(`Use \`contrib sync\` to update ${protectedBranches.map((b) =>
|
|
2912
|
+
error(`Use \`contrib sync\` to update ${protectedBranches.map((b) => pc13.bold(b)).join(" or ")} branches.`);
|
|
1528
2913
|
process.exit(1);
|
|
1529
2914
|
}
|
|
1530
2915
|
if (await hasUncommittedChanges()) {
|
|
@@ -1532,10 +2917,103 @@ var update_default = defineCommand9({
|
|
|
1532
2917
|
process.exit(1);
|
|
1533
2918
|
}
|
|
1534
2919
|
heading("\uD83D\uDD03 contrib update");
|
|
1535
|
-
|
|
2920
|
+
const mergedPR = await getMergedPRForBranch(currentBranch);
|
|
2921
|
+
if (mergedPR) {
|
|
2922
|
+
warn(`PR #${mergedPR.number} (${pc13.bold(mergedPR.title)}) has already been merged.`);
|
|
2923
|
+
info(`Link: ${pc13.underline(mergedPR.url)}`);
|
|
2924
|
+
const localWork = await hasLocalWork(syncSource.remote, currentBranch);
|
|
2925
|
+
const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
|
|
2926
|
+
if (hasWork) {
|
|
2927
|
+
if (localWork.uncommitted) {
|
|
2928
|
+
info("You have uncommitted local changes.");
|
|
2929
|
+
}
|
|
2930
|
+
if (localWork.unpushedCommits > 0) {
|
|
2931
|
+
info(`You have ${localWork.unpushedCommits} unpushed commit(s).`);
|
|
2932
|
+
}
|
|
2933
|
+
const SAVE_NEW_BRANCH = "Save changes to a new branch";
|
|
2934
|
+
const DISCARD = "Discard all changes and clean up";
|
|
2935
|
+
const CANCEL = "Cancel";
|
|
2936
|
+
const action = await selectPrompt(`${pc13.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
|
|
2937
|
+
if (action === CANCEL) {
|
|
2938
|
+
info("No changes made. You are still on your current branch.");
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (action === SAVE_NEW_BRANCH) {
|
|
2942
|
+
info(pc13.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
2943
|
+
const description = await inputPrompt("What are you working on?");
|
|
2944
|
+
let newBranchName = description;
|
|
2945
|
+
if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
|
|
2946
|
+
const spinner = createSpinner("Generating branch name suggestion...");
|
|
2947
|
+
const suggested = await suggestBranchName(description, args.model);
|
|
2948
|
+
if (suggested) {
|
|
2949
|
+
spinner.success("Branch name suggestion ready.");
|
|
2950
|
+
console.log(`
|
|
2951
|
+
${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(suggested))}`);
|
|
2952
|
+
const accepted = await confirmPrompt(`Use ${pc13.bold(suggested)} as your branch name?`);
|
|
2953
|
+
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
2954
|
+
} else {
|
|
2955
|
+
spinner.fail("AI did not return a suggestion.");
|
|
2956
|
+
newBranchName = await inputPrompt("Enter branch name", description);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
2960
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc13.bold(newBranchName)}:`, config.branchPrefixes);
|
|
2961
|
+
newBranchName = formatBranchName(prefix, newBranchName);
|
|
2962
|
+
}
|
|
2963
|
+
if (!isValidBranchName(newBranchName)) {
|
|
2964
|
+
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
2965
|
+
process.exit(1);
|
|
2966
|
+
}
|
|
2967
|
+
const staleUpstream = await getUpstreamRef();
|
|
2968
|
+
const staleUpstreamHash = staleUpstream ? await getCommitHash(staleUpstream) : null;
|
|
2969
|
+
const renameResult = await renameBranch(currentBranch, newBranchName);
|
|
2970
|
+
if (renameResult.exitCode !== 0) {
|
|
2971
|
+
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
2972
|
+
process.exit(1);
|
|
2973
|
+
}
|
|
2974
|
+
success(`Renamed ${pc13.bold(currentBranch)} → ${pc13.bold(newBranchName)}`);
|
|
2975
|
+
await unsetUpstream();
|
|
2976
|
+
await fetchRemote(syncSource.remote);
|
|
2977
|
+
let rebaseResult2;
|
|
2978
|
+
if (staleUpstreamHash) {
|
|
2979
|
+
rebaseResult2 = await rebaseOnto(syncSource.ref, staleUpstreamHash);
|
|
2980
|
+
} else {
|
|
2981
|
+
const savedStrategy = await determineRebaseStrategy(newBranchName, syncSource.ref);
|
|
2982
|
+
rebaseResult2 = savedStrategy.strategy === "onto" && savedStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, savedStrategy.ontoOldBase) : await rebase(syncSource.ref);
|
|
2983
|
+
}
|
|
2984
|
+
if (rebaseResult2.exitCode !== 0) {
|
|
2985
|
+
warn("Rebase encountered conflicts. Resolve them manually, then run:");
|
|
2986
|
+
info(` ${pc13.bold("git rebase --continue")}`);
|
|
2987
|
+
} else {
|
|
2988
|
+
success(`Rebased ${pc13.bold(newBranchName)} onto ${pc13.bold(syncSource.ref)}.`);
|
|
2989
|
+
}
|
|
2990
|
+
info(`All your changes are preserved. Run ${pc13.bold("contrib submit")} when ready to create a new PR.`);
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
warn("Discarding local changes...");
|
|
2994
|
+
}
|
|
2995
|
+
await fetchRemote(syncSource.remote);
|
|
2996
|
+
const coResult = await checkoutBranch2(baseBranch);
|
|
2997
|
+
if (coResult.exitCode !== 0) {
|
|
2998
|
+
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2999
|
+
process.exit(1);
|
|
3000
|
+
}
|
|
3001
|
+
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
3002
|
+
success(`Synced ${pc13.bold(baseBranch)} with ${pc13.bold(syncSource.ref)}.`);
|
|
3003
|
+
info(`Deleting stale branch ${pc13.bold(currentBranch)}...`);
|
|
3004
|
+
await forceDeleteBranch(currentBranch);
|
|
3005
|
+
success(`Deleted ${pc13.bold(currentBranch)}.`);
|
|
3006
|
+
info(`Run ${pc13.bold("contrib start")} to begin a new feature branch.`);
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
info(`Updating ${pc13.bold(currentBranch)} with latest ${pc13.bold(baseBranch)}...`);
|
|
1536
3010
|
await fetchRemote(syncSource.remote);
|
|
1537
|
-
await
|
|
1538
|
-
const
|
|
3011
|
+
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
3012
|
+
const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
|
|
3013
|
+
if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
|
|
3014
|
+
info(pc13.dim(`Using --onto rebase (branch was based on a different ref)`));
|
|
3015
|
+
}
|
|
3016
|
+
const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
|
|
1539
3017
|
if (rebaseResult.exitCode !== 0) {
|
|
1540
3018
|
warn("Rebase hit conflicts. Resolve them manually.");
|
|
1541
3019
|
console.log();
|
|
@@ -1547,7 +3025,7 @@ var update_default = defineCommand9({
|
|
|
1547
3025
|
let conflictDiff = "";
|
|
1548
3026
|
for (const file of conflictFiles.slice(0, 3)) {
|
|
1549
3027
|
try {
|
|
1550
|
-
const content =
|
|
3028
|
+
const content = readFileSync4(file, "utf-8");
|
|
1551
3029
|
if (content.includes("<<<<<<<")) {
|
|
1552
3030
|
conflictDiff += `
|
|
1553
3031
|
--- ${file} ---
|
|
@@ -1557,34 +3035,38 @@ ${content.slice(0, 2000)}
|
|
|
1557
3035
|
} catch {}
|
|
1558
3036
|
}
|
|
1559
3037
|
if (conflictDiff) {
|
|
3038
|
+
const spinner = createSpinner("Analyzing conflicts with AI...");
|
|
1560
3039
|
const suggestion = await suggestConflictResolution(conflictDiff, args.model);
|
|
1561
3040
|
if (suggestion) {
|
|
3041
|
+
spinner.success("AI conflict guidance ready.");
|
|
1562
3042
|
console.log(`
|
|
1563
|
-
${
|
|
1564
|
-
console.log(
|
|
3043
|
+
${pc13.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
3044
|
+
console.log(pc13.dim("─".repeat(60)));
|
|
1565
3045
|
console.log(suggestion);
|
|
1566
|
-
console.log(
|
|
3046
|
+
console.log(pc13.dim("─".repeat(60)));
|
|
1567
3047
|
console.log();
|
|
3048
|
+
} else {
|
|
3049
|
+
spinner.fail("AI could not analyze the conflicts.");
|
|
1568
3050
|
}
|
|
1569
3051
|
}
|
|
1570
3052
|
}
|
|
1571
3053
|
}
|
|
1572
|
-
console.log(
|
|
3054
|
+
console.log(pc13.bold("To resolve:"));
|
|
1573
3055
|
console.log(` 1. Fix conflicts in the affected files`);
|
|
1574
|
-
console.log(` 2. ${
|
|
1575
|
-
console.log(` 3. ${
|
|
3056
|
+
console.log(` 2. ${pc13.cyan("git add <resolved-files>")}`);
|
|
3057
|
+
console.log(` 3. ${pc13.cyan("git rebase --continue")}`);
|
|
1576
3058
|
console.log();
|
|
1577
|
-
console.log(` Or abort: ${
|
|
3059
|
+
console.log(` Or abort: ${pc13.cyan("git rebase --abort")}`);
|
|
1578
3060
|
process.exit(1);
|
|
1579
3061
|
}
|
|
1580
|
-
success(`✅ ${
|
|
3062
|
+
success(`✅ ${pc13.bold(currentBranch)} has been rebased onto latest ${pc13.bold(baseBranch)}`);
|
|
1581
3063
|
}
|
|
1582
3064
|
});
|
|
1583
3065
|
|
|
1584
3066
|
// src/commands/validate.ts
|
|
1585
|
-
import { defineCommand as
|
|
1586
|
-
import
|
|
1587
|
-
var validate_default =
|
|
3067
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
3068
|
+
import pc14 from "picocolors";
|
|
3069
|
+
var validate_default = defineCommand11({
|
|
1588
3070
|
meta: {
|
|
1589
3071
|
name: "validate",
|
|
1590
3072
|
description: "Validate a commit message against the configured convention"
|
|
@@ -1614,7 +3096,7 @@ var validate_default = defineCommand10({
|
|
|
1614
3096
|
}
|
|
1615
3097
|
const errors = getValidationError(convention);
|
|
1616
3098
|
for (const line of errors) {
|
|
1617
|
-
console.error(
|
|
3099
|
+
console.error(pc14.red(` ✗ ${line}`));
|
|
1618
3100
|
}
|
|
1619
3101
|
process.exit(1);
|
|
1620
3102
|
}
|
|
@@ -1622,75 +3104,13 @@ var validate_default = defineCommand10({
|
|
|
1622
3104
|
|
|
1623
3105
|
// src/ui/banner.ts
|
|
1624
3106
|
import figlet from "figlet";
|
|
1625
|
-
import
|
|
1626
|
-
// package.json
|
|
1627
|
-
var package_default = {
|
|
1628
|
-
name: "contribute-now",
|
|
1629
|
-
version: "0.1.2-staging.c209cc7",
|
|
1630
|
-
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1631
|
-
type: "module",
|
|
1632
|
-
bin: {
|
|
1633
|
-
contrib: "dist/index.js",
|
|
1634
|
-
contribute: "dist/index.js"
|
|
1635
|
-
},
|
|
1636
|
-
files: [
|
|
1637
|
-
"dist"
|
|
1638
|
-
],
|
|
1639
|
-
scripts: {
|
|
1640
|
-
build: "bun build src/index.ts --outfile dist/index.js --target node --packages external",
|
|
1641
|
-
dev: "bun src/index.ts",
|
|
1642
|
-
test: "bun test",
|
|
1643
|
-
lint: "biome check .",
|
|
1644
|
-
"lint:fix": "biome check --write .",
|
|
1645
|
-
format: "biome format --write .",
|
|
1646
|
-
prepare: "husky || true",
|
|
1647
|
-
"www:dev": "bun run --cwd www dev",
|
|
1648
|
-
"www:build": "bun run --cwd www build",
|
|
1649
|
-
"www:preview": "bun run --cwd www preview"
|
|
1650
|
-
},
|
|
1651
|
-
engines: {
|
|
1652
|
-
node: ">=18",
|
|
1653
|
-
bun: ">=1.0"
|
|
1654
|
-
},
|
|
1655
|
-
keywords: [
|
|
1656
|
-
"git",
|
|
1657
|
-
"workflow",
|
|
1658
|
-
"squash-merge",
|
|
1659
|
-
"sync",
|
|
1660
|
-
"cli",
|
|
1661
|
-
"contribute",
|
|
1662
|
-
"fork",
|
|
1663
|
-
"dev-branch",
|
|
1664
|
-
"clean-commit"
|
|
1665
|
-
],
|
|
1666
|
-
author: "Waren Gonzaga",
|
|
1667
|
-
license: "GPL-3.0",
|
|
1668
|
-
repository: {
|
|
1669
|
-
type: "git",
|
|
1670
|
-
url: "git+https://github.com/warengonzaga/contribute-now.git"
|
|
1671
|
-
},
|
|
1672
|
-
dependencies: {
|
|
1673
|
-
"@github/copilot-sdk": "^0.1.25",
|
|
1674
|
-
"@wgtechlabs/log-engine": "^2.3.1",
|
|
1675
|
-
citty: "^0.1.6",
|
|
1676
|
-
figlet: "^1.10.0",
|
|
1677
|
-
picocolors: "^1.1.1"
|
|
1678
|
-
},
|
|
1679
|
-
devDependencies: {
|
|
1680
|
-
"@biomejs/biome": "^2.4.4",
|
|
1681
|
-
"@types/bun": "latest",
|
|
1682
|
-
"@types/figlet": "^1.7.0",
|
|
1683
|
-
husky: "^9.1.7",
|
|
1684
|
-
typescript: "^5.7.0"
|
|
1685
|
-
}
|
|
1686
|
-
};
|
|
1687
|
-
|
|
1688
|
-
// src/ui/banner.ts
|
|
3107
|
+
import pc15 from "picocolors";
|
|
1689
3108
|
var LOGO;
|
|
1690
3109
|
try {
|
|
1691
|
-
LOGO = figlet.textSync(
|
|
3110
|
+
LOGO = figlet.textSync(`Contribute
|
|
3111
|
+
Now`, { font: "ANSI Shadow" });
|
|
1692
3112
|
} catch {
|
|
1693
|
-
LOGO = "
|
|
3113
|
+
LOGO = "Contribute Now";
|
|
1694
3114
|
}
|
|
1695
3115
|
function getVersion() {
|
|
1696
3116
|
return package_default.version ?? "unknown";
|
|
@@ -1698,16 +3118,15 @@ function getVersion() {
|
|
|
1698
3118
|
function getAuthor() {
|
|
1699
3119
|
return typeof package_default.author === "string" ? package_default.author : "unknown";
|
|
1700
3120
|
}
|
|
1701
|
-
function showBanner(
|
|
1702
|
-
console.log(
|
|
3121
|
+
function showBanner(showLinks = false) {
|
|
3122
|
+
console.log(pc15.cyan(`
|
|
1703
3123
|
${LOGO}`));
|
|
1704
|
-
console.log(` ${
|
|
1705
|
-
if (
|
|
1706
|
-
console.log(` ${pc13.dim(package_default.description)}`);
|
|
3124
|
+
console.log(` ${pc15.dim(`v${getVersion()}`)} ${pc15.dim("—")} ${pc15.dim(`Built by ${getAuthor()}`)}`);
|
|
3125
|
+
if (showLinks) {
|
|
1707
3126
|
console.log();
|
|
1708
|
-
console.log(` ${
|
|
1709
|
-
console.log(` ${
|
|
1710
|
-
console.log(` ${
|
|
3127
|
+
console.log(` ${pc15.yellow("Star")} ${pc15.cyan("https://github.com/warengonzaga/contribute-now")}`);
|
|
3128
|
+
console.log(` ${pc15.green("Contribute")} ${pc15.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
|
|
3129
|
+
console.log(` ${pc15.magenta("Sponsor")} ${pc15.cyan("https://warengonzaga.com/sponsor")}`);
|
|
1711
3130
|
}
|
|
1712
3131
|
console.log();
|
|
1713
3132
|
}
|
|
@@ -1715,7 +3134,7 @@ ${LOGO}`));
|
|
|
1715
3134
|
// src/index.ts
|
|
1716
3135
|
var isHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
1717
3136
|
showBanner(isHelp);
|
|
1718
|
-
var main =
|
|
3137
|
+
var main = defineCommand12({
|
|
1719
3138
|
meta: {
|
|
1720
3139
|
name: "contrib",
|
|
1721
3140
|
version: getVersion(),
|
|
@@ -1738,7 +3157,8 @@ var main = defineCommand11({
|
|
|
1738
3157
|
clean: clean_default,
|
|
1739
3158
|
status: status_default,
|
|
1740
3159
|
hook: hook_default,
|
|
1741
|
-
validate: validate_default
|
|
3160
|
+
validate: validate_default,
|
|
3161
|
+
doctor: doctor_default
|
|
1742
3162
|
},
|
|
1743
3163
|
run({ args }) {
|
|
1744
3164
|
if (args.version) {
|
|
@@ -1746,4 +3166,6 @@ var main = defineCommand11({
|
|
|
1746
3166
|
}
|
|
1747
3167
|
}
|
|
1748
3168
|
});
|
|
1749
|
-
runMain(main)
|
|
3169
|
+
runMain(main).then(() => {
|
|
3170
|
+
process.exit(0);
|
|
3171
|
+
});
|