contribute-now 0.2.0-dev.0de9dbd → 0.2.0-dev.2621ffa
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 +1370 -920
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,27 +3,11 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
import { defineCommand as
|
|
6
|
+
import { defineCommand as defineCommand14, runMain } from "citty";
|
|
7
7
|
|
|
8
|
-
// src/commands/
|
|
8
|
+
// src/commands/branch.ts
|
|
9
9
|
import { defineCommand } from "citty";
|
|
10
|
-
import
|
|
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
|
-
}
|
|
10
|
+
import pc2 from "picocolors";
|
|
27
11
|
|
|
28
12
|
// src/utils/config.ts
|
|
29
13
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -80,736 +64,247 @@ function getDefaultConfig() {
|
|
|
80
64
|
};
|
|
81
65
|
}
|
|
82
66
|
|
|
83
|
-
// src/utils/
|
|
84
|
-
import
|
|
85
|
-
import
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
67
|
+
// src/utils/git.ts
|
|
68
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
69
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
70
|
+
import { join as join2 } from "node:path";
|
|
71
|
+
function run(args) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
execFileCb("git", args, (error, stdout, stderr) => {
|
|
74
|
+
resolve({
|
|
75
|
+
exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
|
|
76
|
+
stdout: stdout ?? "",
|
|
77
|
+
stderr: stderr ?? ""
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
91
81
|
}
|
|
92
|
-
async function
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
return result;
|
|
82
|
+
async function isGitRepo() {
|
|
83
|
+
const { exitCode } = await run(["rev-parse", "--is-inside-work-tree"]);
|
|
84
|
+
return exitCode === 0;
|
|
96
85
|
}
|
|
97
|
-
async function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
handleCancel(result);
|
|
103
|
-
return result;
|
|
86
|
+
async function getCurrentBranch() {
|
|
87
|
+
const { exitCode, stdout } = await run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
88
|
+
if (exitCode !== 0)
|
|
89
|
+
return null;
|
|
90
|
+
return stdout.trim() || null;
|
|
104
91
|
}
|
|
105
|
-
async function
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
handleCancel(result);
|
|
112
|
-
return result || defaultValue || "";
|
|
92
|
+
async function getRemotes() {
|
|
93
|
+
const { exitCode, stdout } = await run(["remote"]);
|
|
94
|
+
if (exitCode !== 0)
|
|
95
|
+
return [];
|
|
96
|
+
return stdout.trim().split(`
|
|
97
|
+
`).map((r) => r.trim()).filter(Boolean);
|
|
113
98
|
}
|
|
114
|
-
async function
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
120
|
-
handleCancel(result);
|
|
121
|
-
return result;
|
|
99
|
+
async function getRemoteUrl(remote) {
|
|
100
|
+
const { exitCode, stdout } = await run(["remote", "get-url", remote]);
|
|
101
|
+
if (exitCode !== 0)
|
|
102
|
+
return null;
|
|
103
|
+
return stdout.trim() || null;
|
|
122
104
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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`;
|
|
105
|
+
async function hasUncommittedChanges() {
|
|
106
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
107
|
+
if (exitCode !== 0)
|
|
108
|
+
return false;
|
|
109
|
+
return stdout.trim().length > 0;
|
|
160
110
|
}
|
|
161
|
-
|
|
162
|
-
|
|
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.`;
|
|
111
|
+
async function fetchRemote(remote) {
|
|
112
|
+
return run(["fetch", remote]);
|
|
186
113
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
process.env.NODE_NO_WARNINGS = "1";
|
|
114
|
+
async function fetchAll() {
|
|
115
|
+
return run(["fetch", "--all", "--quiet"]);
|
|
190
116
|
}
|
|
191
|
-
function
|
|
192
|
-
return
|
|
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);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
117
|
+
async function checkoutBranch2(branch) {
|
|
118
|
+
return run(["checkout", branch]);
|
|
202
119
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
}
|
|
220
|
-
return null;
|
|
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
|
-
}
|
|
120
|
+
async function createBranch(branch, from) {
|
|
121
|
+
const args = from ? ["checkout", "-b", branch, from] : ["checkout", "-b", branch];
|
|
122
|
+
return run(args);
|
|
228
123
|
}
|
|
229
|
-
|
|
230
|
-
|
|
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;
|
|
124
|
+
async function resetHard(ref) {
|
|
125
|
+
return run(["reset", "--hard", ref]);
|
|
251
126
|
}
|
|
252
|
-
async function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
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();
|
|
127
|
+
async function updateLocalBranch(branch, target) {
|
|
128
|
+
const current = await getCurrentBranch();
|
|
129
|
+
if (current === branch) {
|
|
130
|
+
return resetHard(target);
|
|
267
131
|
}
|
|
132
|
+
return run(["branch", "-f", branch, target]);
|
|
268
133
|
}
|
|
269
|
-
function
|
|
270
|
-
|
|
271
|
-
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
272
|
-
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
134
|
+
async function pushSetUpstream(remote, branch) {
|
|
135
|
+
return run(["push", "-u", remote, branch]);
|
|
273
136
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
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;
|
|
137
|
+
async function rebase(branch) {
|
|
138
|
+
return run(["rebase", branch]);
|
|
302
139
|
}
|
|
303
|
-
async function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 {
|
|
140
|
+
async function getUpstreamRef() {
|
|
141
|
+
const { exitCode, stdout } = await run([
|
|
142
|
+
"rev-parse",
|
|
143
|
+
"--abbrev-ref",
|
|
144
|
+
"--symbolic-full-name",
|
|
145
|
+
"@{u}"
|
|
146
|
+
]);
|
|
147
|
+
if (exitCode !== 0)
|
|
317
148
|
return null;
|
|
318
|
-
|
|
149
|
+
return stdout.trim() || null;
|
|
319
150
|
}
|
|
320
|
-
async function
|
|
321
|
-
|
|
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
|
-
}
|
|
151
|
+
async function unsetUpstream() {
|
|
152
|
+
return run(["branch", "--unset-upstream"]);
|
|
338
153
|
}
|
|
339
|
-
async function
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
return null;
|
|
347
|
-
} catch {
|
|
154
|
+
async function rebaseOnto(newBase, oldBase) {
|
|
155
|
+
return run(["rebase", "--onto", newBase, oldBase]);
|
|
156
|
+
}
|
|
157
|
+
async function getMergeBase(ref1, ref2) {
|
|
158
|
+
const { exitCode, stdout } = await run(["merge-base", ref1, ref2]);
|
|
159
|
+
if (exitCode !== 0)
|
|
348
160
|
return null;
|
|
349
|
-
|
|
161
|
+
return stdout.trim() || null;
|
|
350
162
|
}
|
|
351
|
-
async function
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
${conflictDiff.slice(0, 4000)}`;
|
|
356
|
-
const result = await callCopilot(CONFLICT_RESOLUTION_SYSTEM_PROMPT, userMessage, model);
|
|
357
|
-
return result?.trim() ?? null;
|
|
358
|
-
} catch {
|
|
163
|
+
async function getCommitHash(ref) {
|
|
164
|
+
const { exitCode, stdout } = await run(["rev-parse", ref]);
|
|
165
|
+
if (exitCode !== 0)
|
|
359
166
|
return null;
|
|
360
|
-
|
|
167
|
+
return stdout.trim() || null;
|
|
361
168
|
}
|
|
362
|
-
async function
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
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");
|
|
169
|
+
async function determineRebaseStrategy(currentBranch, syncRef) {
|
|
170
|
+
const upstreamRef = await getUpstreamRef();
|
|
171
|
+
if (!upstreamRef) {
|
|
172
|
+
return { strategy: "plain" };
|
|
385
173
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
174
|
+
const upstreamHash = await getCommitHash(upstreamRef);
|
|
175
|
+
if (!upstreamHash) {
|
|
176
|
+
return { strategy: "plain" };
|
|
390
177
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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;
|
|
178
|
+
const slashIdx = upstreamRef.indexOf("/");
|
|
179
|
+
const upstreamBranchName = slashIdx !== -1 ? upstreamRef.slice(slashIdx + 1) : upstreamRef;
|
|
180
|
+
if (upstreamBranchName === currentBranch) {
|
|
181
|
+
return { strategy: "plain" };
|
|
417
182
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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;
|
|
183
|
+
const [forkFromUpstream, forkFromSync] = await Promise.all([
|
|
184
|
+
getMergeBase("HEAD", upstreamRef),
|
|
185
|
+
getMergeBase("HEAD", syncRef)
|
|
186
|
+
]);
|
|
187
|
+
if (forkFromUpstream && forkFromSync && forkFromUpstream === forkFromSync) {
|
|
188
|
+
return { strategy: "plain" };
|
|
431
189
|
}
|
|
432
|
-
|
|
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
|
-
});
|
|
446
|
-
}
|
|
447
|
-
async function checkGhInstalled() {
|
|
448
|
-
try {
|
|
449
|
-
const { exitCode } = await run(["--version"]);
|
|
450
|
-
return exitCode === 0;
|
|
451
|
-
} catch {
|
|
452
|
-
return false;
|
|
190
|
+
if (forkFromUpstream) {
|
|
191
|
+
return { strategy: "onto", ontoOldBase: forkFromUpstream };
|
|
453
192
|
}
|
|
193
|
+
return { strategy: "plain" };
|
|
454
194
|
}
|
|
455
|
-
async function
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return exitCode === 0;
|
|
459
|
-
} catch {
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
195
|
+
async function getStagedDiff() {
|
|
196
|
+
const { stdout } = await run(["diff", "--cached"]);
|
|
197
|
+
return stdout;
|
|
462
198
|
}
|
|
463
|
-
|
|
464
|
-
|
|
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"]);
|
|
199
|
+
async function getStagedFiles() {
|
|
200
|
+
const { exitCode, stdout } = await run(["diff", "--cached", "--name-only"]);
|
|
468
201
|
if (exitCode !== 0)
|
|
469
|
-
return
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
} catch {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
202
|
+
return [];
|
|
203
|
+
return stdout.trim().split(`
|
|
204
|
+
`).filter(Boolean);
|
|
475
205
|
}
|
|
476
|
-
async function
|
|
477
|
-
const { exitCode, stdout } = await run(["
|
|
206
|
+
async function getChangedFiles() {
|
|
207
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
478
208
|
if (exitCode !== 0)
|
|
479
|
-
return
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
209
|
+
return [];
|
|
210
|
+
return stdout.trimEnd().split(`
|
|
211
|
+
`).filter(Boolean).map((l) => {
|
|
212
|
+
const line = l.replace(/\r$/, "");
|
|
213
|
+
const match = line.match(/^..\s+(.*)/);
|
|
214
|
+
if (!match)
|
|
215
|
+
return "";
|
|
216
|
+
const file = match[1];
|
|
217
|
+
const renameIdx = file.indexOf(" -> ");
|
|
218
|
+
return renameIdx !== -1 ? file.slice(renameIdx + 4) : file;
|
|
219
|
+
}).filter(Boolean);
|
|
486
220
|
}
|
|
487
|
-
async function
|
|
221
|
+
async function getDivergence(branch, base) {
|
|
488
222
|
const { exitCode, stdout } = await run([
|
|
489
|
-
"
|
|
490
|
-
"
|
|
491
|
-
"--
|
|
492
|
-
|
|
493
|
-
"-q",
|
|
494
|
-
".nameWithOwner"
|
|
223
|
+
"rev-list",
|
|
224
|
+
"--left-right",
|
|
225
|
+
"--count",
|
|
226
|
+
`${base}...${branch}`
|
|
495
227
|
]);
|
|
496
228
|
if (exitCode !== 0)
|
|
497
|
-
return
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
};
|
|
229
|
+
return { ahead: 0, behind: 0 };
|
|
230
|
+
const parts = stdout.trim().split(/\s+/);
|
|
231
|
+
return {
|
|
232
|
+
behind: Number.parseInt(parts[0] ?? "0", 10),
|
|
233
|
+
ahead: Number.parseInt(parts[1] ?? "0", 10)
|
|
234
|
+
};
|
|
740
235
|
}
|
|
741
236
|
async function getMergedBranches(base) {
|
|
742
|
-
const { exitCode, stdout } = await
|
|
237
|
+
const { exitCode, stdout } = await run(["branch", "--merged", base]);
|
|
743
238
|
if (exitCode !== 0)
|
|
744
239
|
return [];
|
|
745
240
|
return stdout.trim().split(`
|
|
746
241
|
`).map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean);
|
|
747
242
|
}
|
|
748
243
|
async function getGoneBranches() {
|
|
749
|
-
const { exitCode, stdout } = await
|
|
244
|
+
const { exitCode, stdout } = await run(["branch", "-vv"]);
|
|
750
245
|
if (exitCode !== 0)
|
|
751
246
|
return [];
|
|
752
247
|
return stdout.trimEnd().split(`
|
|
753
248
|
`).filter((line) => line.includes(": gone]")).map((line) => line.replace(/^\*?\s+/, "").split(/\s+/)[0]).filter(Boolean);
|
|
754
249
|
}
|
|
755
250
|
async function deleteBranch(branch) {
|
|
756
|
-
return
|
|
251
|
+
return run(["branch", "-d", branch]);
|
|
757
252
|
}
|
|
758
253
|
async function forceDeleteBranch(branch) {
|
|
759
|
-
return
|
|
254
|
+
return run(["branch", "-D", branch]);
|
|
760
255
|
}
|
|
761
256
|
async function renameBranch(oldName, newName) {
|
|
762
|
-
return
|
|
257
|
+
return run(["branch", "-m", oldName, newName]);
|
|
763
258
|
}
|
|
764
259
|
async function hasLocalWork(remote, branch) {
|
|
765
260
|
const uncommitted = await hasUncommittedChanges();
|
|
766
261
|
const trackingRef = `${remote}/${branch}`;
|
|
767
|
-
const { exitCode, stdout } = await
|
|
262
|
+
const { exitCode, stdout } = await run(["rev-list", "--count", `${trackingRef}..${branch}`]);
|
|
768
263
|
const unpushedCommits = exitCode === 0 ? Number.parseInt(stdout.trim(), 10) || 0 : 0;
|
|
769
264
|
return { uncommitted, unpushedCommits };
|
|
770
265
|
}
|
|
771
266
|
async function deleteRemoteBranch(remote, branch) {
|
|
772
|
-
return
|
|
267
|
+
return run(["push", remote, "--delete", branch]);
|
|
773
268
|
}
|
|
774
269
|
async function mergeSquash(branch) {
|
|
775
|
-
return
|
|
270
|
+
return run(["merge", "--squash", branch]);
|
|
776
271
|
}
|
|
777
272
|
async function pushBranch(remote, branch) {
|
|
778
|
-
return
|
|
273
|
+
return run(["push", remote, branch]);
|
|
779
274
|
}
|
|
780
275
|
async function pruneRemote(remote) {
|
|
781
|
-
return
|
|
276
|
+
return run(["remote", "prune", remote]);
|
|
782
277
|
}
|
|
783
278
|
async function commitWithMessage(message) {
|
|
784
|
-
return
|
|
279
|
+
return run(["commit", "-m", message]);
|
|
785
280
|
}
|
|
786
281
|
async function getLogDiff(base, head) {
|
|
787
|
-
const { stdout } = await
|
|
282
|
+
const { stdout } = await run(["diff", `${base}...${head}`]);
|
|
788
283
|
return stdout;
|
|
789
284
|
}
|
|
790
285
|
async function getLog(base, head) {
|
|
791
|
-
const { exitCode, stdout } = await
|
|
286
|
+
const { exitCode, stdout } = await run(["log", `${base}..${head}`, "--oneline"]);
|
|
792
287
|
if (exitCode !== 0)
|
|
793
288
|
return [];
|
|
794
289
|
return stdout.trim().split(`
|
|
795
290
|
`).filter(Boolean);
|
|
796
291
|
}
|
|
797
292
|
async function pullBranch(remote, branch) {
|
|
798
|
-
return
|
|
293
|
+
return run(["pull", remote, branch]);
|
|
799
294
|
}
|
|
800
295
|
async function stageFiles(files) {
|
|
801
|
-
return
|
|
296
|
+
return run(["add", "--", ...files]);
|
|
802
297
|
}
|
|
803
298
|
async function unstageFiles(files) {
|
|
804
|
-
return
|
|
299
|
+
return run(["reset", "HEAD", "--", ...files]);
|
|
805
300
|
}
|
|
806
301
|
async function stageAll() {
|
|
807
|
-
return
|
|
302
|
+
return run(["add", "-A"]);
|
|
808
303
|
}
|
|
809
304
|
async function getFullDiffForFiles(files) {
|
|
810
305
|
const [unstaged, staged, untracked] = await Promise.all([
|
|
811
|
-
|
|
812
|
-
|
|
306
|
+
run(["diff", "--", ...files]),
|
|
307
|
+
run(["diff", "--cached", "--", ...files]),
|
|
813
308
|
getUntrackedFiles()
|
|
814
309
|
]);
|
|
815
310
|
const parts = [staged.stdout, unstaged.stdout].filter(Boolean);
|
|
@@ -836,14 +331,14 @@ ${lines.join(`
|
|
|
836
331
|
`);
|
|
837
332
|
}
|
|
838
333
|
async function getUntrackedFiles() {
|
|
839
|
-
const { exitCode, stdout } = await
|
|
334
|
+
const { exitCode, stdout } = await run(["ls-files", "--others", "--exclude-standard"]);
|
|
840
335
|
if (exitCode !== 0)
|
|
841
336
|
return [];
|
|
842
337
|
return stdout.trim().split(`
|
|
843
338
|
`).filter(Boolean);
|
|
844
339
|
}
|
|
845
340
|
async function getFileStatus() {
|
|
846
|
-
const { exitCode, stdout } = await
|
|
341
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
847
342
|
if (exitCode !== 0)
|
|
848
343
|
return { staged: [], modified: [], untracked: [] };
|
|
849
344
|
const result = { staged: [], modified: [], untracked: [] };
|
|
@@ -876,10 +371,82 @@ async function getFileStatus() {
|
|
|
876
371
|
}
|
|
877
372
|
return result;
|
|
878
373
|
}
|
|
374
|
+
async function getLogGraph(options) {
|
|
375
|
+
const count = options?.count ?? 20;
|
|
376
|
+
const args = [
|
|
377
|
+
"log",
|
|
378
|
+
"--oneline",
|
|
379
|
+
"--graph",
|
|
380
|
+
"--decorate",
|
|
381
|
+
`--max-count=${count}`,
|
|
382
|
+
"--color=never"
|
|
383
|
+
];
|
|
384
|
+
if (options?.all) {
|
|
385
|
+
args.push("--all");
|
|
386
|
+
}
|
|
387
|
+
if (options?.branch) {
|
|
388
|
+
args.push(options.branch);
|
|
389
|
+
}
|
|
390
|
+
const { exitCode, stdout } = await run(args);
|
|
391
|
+
if (exitCode !== 0)
|
|
392
|
+
return [];
|
|
393
|
+
return stdout.trimEnd().split(`
|
|
394
|
+
`);
|
|
395
|
+
}
|
|
396
|
+
async function getLogEntries(options) {
|
|
397
|
+
const count = options?.count ?? 20;
|
|
398
|
+
const args = [
|
|
399
|
+
"log",
|
|
400
|
+
`--format=%h||%s||%D`,
|
|
401
|
+
`--max-count=${count}`
|
|
402
|
+
];
|
|
403
|
+
if (options?.all) {
|
|
404
|
+
args.push("--all");
|
|
405
|
+
}
|
|
406
|
+
if (options?.branch) {
|
|
407
|
+
args.push(options.branch);
|
|
408
|
+
}
|
|
409
|
+
const { exitCode, stdout } = await run(args);
|
|
410
|
+
if (exitCode !== 0)
|
|
411
|
+
return [];
|
|
412
|
+
return stdout.trimEnd().split(`
|
|
413
|
+
`).filter(Boolean).map((line) => {
|
|
414
|
+
const [hash = "", subject = "", refs = ""] = line.split("||");
|
|
415
|
+
return { hash: hash.trim(), subject: subject.trim(), refs: refs.trim() };
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async function getLocalBranches() {
|
|
419
|
+
const { exitCode, stdout } = await run(["branch", "-vv", "--no-color"]);
|
|
420
|
+
if (exitCode !== 0)
|
|
421
|
+
return [];
|
|
422
|
+
return stdout.trimEnd().split(`
|
|
423
|
+
`).filter(Boolean).map((line) => {
|
|
424
|
+
const isCurrent = line.startsWith("*");
|
|
425
|
+
const trimmed = line.slice(2);
|
|
426
|
+
const nameMatch = trimmed.match(/^(\S+)/);
|
|
427
|
+
const name = nameMatch?.[1] ?? "";
|
|
428
|
+
const upstreamMatch = trimmed.match(/\[([^\]]+)\]/);
|
|
429
|
+
let upstream = null;
|
|
430
|
+
let gone = false;
|
|
431
|
+
if (upstreamMatch) {
|
|
432
|
+
const bracketContent = upstreamMatch[1];
|
|
433
|
+
gone = bracketContent.includes(": gone");
|
|
434
|
+
upstream = bracketContent.split(":")[0].trim();
|
|
435
|
+
}
|
|
436
|
+
return { name, isCurrent, upstream, gone };
|
|
437
|
+
}).filter((b) => b.name.length > 0);
|
|
438
|
+
}
|
|
439
|
+
async function getRemoteBranches() {
|
|
440
|
+
const { exitCode, stdout } = await run(["branch", "-r", "--no-color"]);
|
|
441
|
+
if (exitCode !== 0)
|
|
442
|
+
return [];
|
|
443
|
+
return stdout.trimEnd().split(`
|
|
444
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0 && !line.includes(" -> "));
|
|
445
|
+
}
|
|
879
446
|
|
|
880
447
|
// src/utils/logger.ts
|
|
881
448
|
import { LogEngine, LogMode } from "@wgtechlabs/log-engine";
|
|
882
|
-
import
|
|
449
|
+
import pc from "picocolors";
|
|
883
450
|
LogEngine.configure({
|
|
884
451
|
mode: LogMode.INFO,
|
|
885
452
|
format: {
|
|
@@ -891,22 +458,717 @@ LogEngine.configure({
|
|
|
891
458
|
function success(msg) {
|
|
892
459
|
LogEngine.log(msg);
|
|
893
460
|
}
|
|
894
|
-
function error(msg) {
|
|
895
|
-
LogEngine.error(msg);
|
|
461
|
+
function error(msg) {
|
|
462
|
+
LogEngine.error(msg);
|
|
463
|
+
}
|
|
464
|
+
function warn(msg) {
|
|
465
|
+
LogEngine.warn(msg);
|
|
466
|
+
}
|
|
467
|
+
function info(msg) {
|
|
468
|
+
LogEngine.info(msg);
|
|
469
|
+
}
|
|
470
|
+
function heading(msg) {
|
|
471
|
+
console.log(`
|
|
472
|
+
${pc.bold(msg)}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/utils/workflow.ts
|
|
476
|
+
var WORKFLOW_DESCRIPTIONS = {
|
|
477
|
+
"clean-flow": "Clean Flow — main + dev, squash features into dev, merge dev into main",
|
|
478
|
+
"github-flow": "GitHub Flow — main + feature branches, squash/merge into main",
|
|
479
|
+
"git-flow": "Git Flow — main + develop + release + hotfix branches"
|
|
480
|
+
};
|
|
481
|
+
function getBaseBranch(config) {
|
|
482
|
+
switch (config.workflow) {
|
|
483
|
+
case "clean-flow":
|
|
484
|
+
case "git-flow":
|
|
485
|
+
return config.devBranch ?? "dev";
|
|
486
|
+
case "github-flow":
|
|
487
|
+
return config.mainBranch;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function hasDevBranch(workflow) {
|
|
491
|
+
return workflow === "clean-flow" || workflow === "git-flow";
|
|
492
|
+
}
|
|
493
|
+
function getSyncSource(config) {
|
|
494
|
+
const { workflow, role, mainBranch, origin, upstream } = config;
|
|
495
|
+
const devBranch = config.devBranch ?? "dev";
|
|
496
|
+
switch (workflow) {
|
|
497
|
+
case "clean-flow":
|
|
498
|
+
if (role === "contributor") {
|
|
499
|
+
return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
|
|
500
|
+
}
|
|
501
|
+
return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
|
|
502
|
+
case "github-flow":
|
|
503
|
+
if (role === "contributor") {
|
|
504
|
+
return { remote: upstream, ref: `${upstream}/${mainBranch}`, strategy: "pull" };
|
|
505
|
+
}
|
|
506
|
+
return { remote: origin, ref: `${origin}/${mainBranch}`, strategy: "pull" };
|
|
507
|
+
case "git-flow":
|
|
508
|
+
if (role === "contributor") {
|
|
509
|
+
return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
|
|
510
|
+
}
|
|
511
|
+
return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function getProtectedBranches(config) {
|
|
515
|
+
const branches = [config.mainBranch];
|
|
516
|
+
if (hasDevBranch(config.workflow) && config.devBranch) {
|
|
517
|
+
branches.push(config.devBranch);
|
|
518
|
+
}
|
|
519
|
+
return branches;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/commands/branch.ts
|
|
523
|
+
var branch_default = defineCommand({
|
|
524
|
+
meta: {
|
|
525
|
+
name: "branch",
|
|
526
|
+
description: "List branches with workflow-aware labels and status"
|
|
527
|
+
},
|
|
528
|
+
args: {
|
|
529
|
+
all: {
|
|
530
|
+
type: "boolean",
|
|
531
|
+
alias: "a",
|
|
532
|
+
description: "Show both local and remote branches",
|
|
533
|
+
default: false
|
|
534
|
+
},
|
|
535
|
+
remote: {
|
|
536
|
+
type: "boolean",
|
|
537
|
+
alias: "r",
|
|
538
|
+
description: "Show only remote branches",
|
|
539
|
+
default: false
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
async run({ args }) {
|
|
543
|
+
if (!await isGitRepo()) {
|
|
544
|
+
error("Not inside a git repository.");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
const config = readConfig();
|
|
548
|
+
const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
|
|
549
|
+
const currentBranch = await getCurrentBranch();
|
|
550
|
+
const showRemoteOnly = args.remote;
|
|
551
|
+
const showAll = args.all;
|
|
552
|
+
heading("\uD83C\uDF3F branches");
|
|
553
|
+
console.log();
|
|
554
|
+
if (!showRemoteOnly) {
|
|
555
|
+
const localBranches = await getLocalBranches();
|
|
556
|
+
if (localBranches.length === 0) {
|
|
557
|
+
console.log(pc2.dim(" No local branches found."));
|
|
558
|
+
} else {
|
|
559
|
+
console.log(` ${pc2.bold("Local")}`);
|
|
560
|
+
console.log();
|
|
561
|
+
for (const branch of localBranches) {
|
|
562
|
+
const parts = [];
|
|
563
|
+
if (branch.isCurrent) {
|
|
564
|
+
parts.push(pc2.green("* "));
|
|
565
|
+
} else {
|
|
566
|
+
parts.push(" ");
|
|
567
|
+
}
|
|
568
|
+
const nameStr = colorBranchName(branch.name, protectedBranches, currentBranch);
|
|
569
|
+
parts.push(nameStr.padEnd(30));
|
|
570
|
+
if (branch.gone) {
|
|
571
|
+
parts.push(pc2.red(" ✗ remote gone"));
|
|
572
|
+
} else if (branch.upstream) {
|
|
573
|
+
parts.push(pc2.dim(` → ${branch.upstream}`));
|
|
574
|
+
} else {
|
|
575
|
+
parts.push(pc2.dim(" (no remote)"));
|
|
576
|
+
}
|
|
577
|
+
const labels = getBranchLabels(branch.name, protectedBranches, config);
|
|
578
|
+
if (labels.length > 0) {
|
|
579
|
+
parts.push(` ${labels.join(" ")}`);
|
|
580
|
+
}
|
|
581
|
+
console.log(` ${parts.join("")}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (showRemoteOnly || showAll) {
|
|
586
|
+
const remoteBranches = await getRemoteBranches();
|
|
587
|
+
if (!showRemoteOnly) {
|
|
588
|
+
console.log();
|
|
589
|
+
}
|
|
590
|
+
if (remoteBranches.length === 0) {
|
|
591
|
+
console.log(pc2.dim(" No remote branches found."));
|
|
592
|
+
} else {
|
|
593
|
+
const grouped = groupByRemote(remoteBranches);
|
|
594
|
+
for (const [remote, branches] of Object.entries(grouped)) {
|
|
595
|
+
console.log(` ${pc2.bold(`Remote: ${remote}`)}`);
|
|
596
|
+
console.log();
|
|
597
|
+
for (const fullRef of branches) {
|
|
598
|
+
const branchName = fullRef.slice(remote.length + 1);
|
|
599
|
+
const nameStr = colorBranchName(branchName, protectedBranches, currentBranch);
|
|
600
|
+
const remotePrefix = pc2.dim(`${remote}/`);
|
|
601
|
+
console.log(` ${remotePrefix}${nameStr}`);
|
|
602
|
+
}
|
|
603
|
+
console.log();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const tips = [];
|
|
608
|
+
if (!showAll && !showRemoteOnly) {
|
|
609
|
+
tips.push(`Use ${pc2.bold("contrib branch -a")} to include remote branches`);
|
|
610
|
+
}
|
|
611
|
+
if (!showRemoteOnly) {
|
|
612
|
+
tips.push(`Use ${pc2.bold("contrib start")} to create a new feature branch`);
|
|
613
|
+
tips.push(`Use ${pc2.bold("contrib clean")} to remove merged/stale branches`);
|
|
614
|
+
}
|
|
615
|
+
if (tips.length > 0) {
|
|
616
|
+
console.log(` ${pc2.dim("\uD83D\uDCA1 Tip:")}`);
|
|
617
|
+
for (const tip of tips) {
|
|
618
|
+
console.log(` ${pc2.dim(tip)}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
console.log();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
function colorBranchName(name, protectedBranches, currentBranch) {
|
|
625
|
+
if (name === currentBranch) {
|
|
626
|
+
return pc2.bold(pc2.green(name));
|
|
627
|
+
}
|
|
628
|
+
if (protectedBranches.includes(name)) {
|
|
629
|
+
return pc2.bold(pc2.red(name));
|
|
630
|
+
}
|
|
631
|
+
return name;
|
|
632
|
+
}
|
|
633
|
+
function getBranchLabels(name, protectedBranches, config) {
|
|
634
|
+
const labels = [];
|
|
635
|
+
if (protectedBranches.includes(name)) {
|
|
636
|
+
labels.push(pc2.dim(pc2.red("[protected]")));
|
|
637
|
+
}
|
|
638
|
+
if (config) {
|
|
639
|
+
if (name === config.mainBranch) {
|
|
640
|
+
labels.push(pc2.dim(pc2.cyan("[main]")));
|
|
641
|
+
}
|
|
642
|
+
if (config.devBranch && name === config.devBranch) {
|
|
643
|
+
labels.push(pc2.dim(pc2.cyan("[dev]")));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return labels;
|
|
647
|
+
}
|
|
648
|
+
function groupByRemote(branches) {
|
|
649
|
+
const grouped = {};
|
|
650
|
+
for (const ref of branches) {
|
|
651
|
+
const slashIdx = ref.indexOf("/");
|
|
652
|
+
const remote = slashIdx !== -1 ? ref.slice(0, slashIdx) : "unknown";
|
|
653
|
+
if (!grouped[remote]) {
|
|
654
|
+
grouped[remote] = [];
|
|
655
|
+
}
|
|
656
|
+
grouped[remote].push(ref);
|
|
657
|
+
}
|
|
658
|
+
return grouped;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/commands/clean.ts
|
|
662
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
663
|
+
import pc5 from "picocolors";
|
|
664
|
+
|
|
665
|
+
// src/utils/branch.ts
|
|
666
|
+
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
667
|
+
function hasPrefix(branchName, prefixes = DEFAULT_PREFIXES) {
|
|
668
|
+
return prefixes.some((p) => branchName.startsWith(`${p}/`));
|
|
669
|
+
}
|
|
670
|
+
function formatBranchName(prefix, name) {
|
|
671
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
672
|
+
return `${prefix}/${sanitized}`;
|
|
673
|
+
}
|
|
674
|
+
function isValidBranchName(name) {
|
|
675
|
+
return /^[a-zA-Z0-9._/-]+$/.test(name) && !name.startsWith("/") && !name.endsWith("/");
|
|
676
|
+
}
|
|
677
|
+
function looksLikeNaturalLanguage(input) {
|
|
678
|
+
return input.includes(" ") && !input.includes("/");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/utils/confirm.ts
|
|
682
|
+
import * as clack from "@clack/prompts";
|
|
683
|
+
import pc3 from "picocolors";
|
|
684
|
+
function handleCancel(value) {
|
|
685
|
+
if (clack.isCancel(value)) {
|
|
686
|
+
clack.cancel("Cancelled.");
|
|
687
|
+
process.exit(0);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function confirmPrompt(message) {
|
|
691
|
+
const result = await clack.confirm({ message });
|
|
692
|
+
handleCancel(result);
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
async function selectPrompt(message, choices) {
|
|
696
|
+
const result = await clack.select({
|
|
697
|
+
message,
|
|
698
|
+
options: choices.map((choice) => ({ value: choice, label: choice }))
|
|
699
|
+
});
|
|
700
|
+
handleCancel(result);
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
async function inputPrompt(message, defaultValue) {
|
|
704
|
+
const result = await clack.text({
|
|
705
|
+
message,
|
|
706
|
+
placeholder: defaultValue,
|
|
707
|
+
defaultValue
|
|
708
|
+
});
|
|
709
|
+
handleCancel(result);
|
|
710
|
+
return result || defaultValue || "";
|
|
711
|
+
}
|
|
712
|
+
async function multiSelectPrompt(message, choices) {
|
|
713
|
+
const result = await clack.multiselect({
|
|
714
|
+
message: `${message} ${pc3.dim("(space to toggle, enter to confirm)")}`,
|
|
715
|
+
options: choices.map((choice) => ({ value: choice, label: choice })),
|
|
716
|
+
required: false
|
|
717
|
+
});
|
|
718
|
+
handleCancel(result);
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/utils/copilot.ts
|
|
723
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
724
|
+
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
|
|
725
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
726
|
+
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.
|
|
727
|
+
Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
|
|
728
|
+
var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
729
|
+
Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
|
|
730
|
+
Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
|
|
731
|
+
Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
|
|
732
|
+
Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
|
|
733
|
+
WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
|
|
734
|
+
function getGroupingSystemPrompt(convention) {
|
|
735
|
+
const conventionBlock = convention === "conventional" ? `Use Conventional Commit format: <type>[(<scope>)]: <description>
|
|
736
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert` : `Use Clean Commit format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
737
|
+
Emoji/type table:
|
|
738
|
+
\uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release`;
|
|
739
|
+
return `You are a smart commit grouping assistant. Given a list of changed files and their diffs, group related changes into logical atomic commits.
|
|
740
|
+
|
|
741
|
+
${conventionBlock}
|
|
742
|
+
|
|
743
|
+
Return a JSON array of commit groups with this EXACT structure (no markdown fences, no explanation):
|
|
744
|
+
[
|
|
745
|
+
{
|
|
746
|
+
"files": ["path/to/file1.ts", "path/to/file2.ts"],
|
|
747
|
+
"message": "<commit message following the convention above>"
|
|
748
|
+
}
|
|
749
|
+
]
|
|
750
|
+
|
|
751
|
+
Rules:
|
|
752
|
+
- Group files that are logically related (e.g. a utility and its tests, a feature and its types)
|
|
753
|
+
- Each group should represent ONE logical change
|
|
754
|
+
- Every file must appear in exactly one group
|
|
755
|
+
- Commit messages must follow the convention, be concise, imperative, max 72 chars
|
|
756
|
+
- Order groups so foundational changes come first (types, utils) and consumers come after
|
|
757
|
+
- Return ONLY the JSON array, nothing else`;
|
|
758
|
+
}
|
|
759
|
+
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.
|
|
760
|
+
Output format: <prefix>/<kebab-case-name>
|
|
761
|
+
Valid prefixes: feature, fix, docs, chore, test, refactor
|
|
762
|
+
Rules: lowercase, kebab-case, 2-5 words after the prefix, no punctuation.
|
|
763
|
+
CRITICAL: Output ONLY the branch name on a single line. No explanation. No markdown. No questions. No other text.
|
|
764
|
+
Examples: fix/login-timeout | feature/user-profile-page | docs/update-readme | chore/update-pr-title`;
|
|
765
|
+
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..."}
|
|
766
|
+
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.`;
|
|
767
|
+
function getPRDescriptionSystemPrompt(convention) {
|
|
768
|
+
if (convention === "clean-commit") {
|
|
769
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
770
|
+
CRITICAL: The PR title MUST follow the Clean Commit format exactly: <emoji> <type>: <description>
|
|
771
|
+
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
|
|
772
|
+
Title examples: \uD83D\uDCE6 new: add user authentication | \uD83D\uDD27 update: improve error handling | \uD83D\uDDD1️ remove: drop legacy API
|
|
773
|
+
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.`;
|
|
774
|
+
}
|
|
775
|
+
if (convention === "conventional") {
|
|
776
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
777
|
+
CRITICAL: The PR title MUST follow Conventional Commits format: <type>[(<scope>)]: <description>
|
|
778
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
779
|
+
Title examples: feat: add user authentication | fix(auth): resolve token expiry | docs: update contributing guide
|
|
780
|
+
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.`;
|
|
781
|
+
}
|
|
782
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
783
|
+
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.`;
|
|
784
|
+
}
|
|
785
|
+
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `Git merge conflict advisor. Explain each side, suggest resolution strategy. Never auto-resolve — guidance only. Be concise and actionable.`;
|
|
786
|
+
function suppressSubprocessWarnings() {
|
|
787
|
+
process.env.NODE_NO_WARNINGS = "1";
|
|
788
|
+
}
|
|
789
|
+
function withTimeout(promise, ms) {
|
|
790
|
+
return new Promise((resolve, reject) => {
|
|
791
|
+
const timer = setTimeout(() => reject(new Error(`Copilot request timed out after ${ms / 1000}s`)), ms);
|
|
792
|
+
promise.then((val) => {
|
|
793
|
+
clearTimeout(timer);
|
|
794
|
+
resolve(val);
|
|
795
|
+
}, (err) => {
|
|
796
|
+
clearTimeout(timer);
|
|
797
|
+
reject(err);
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
var COPILOT_TIMEOUT_MS = 30000;
|
|
802
|
+
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
803
|
+
async function checkCopilotAvailable() {
|
|
804
|
+
try {
|
|
805
|
+
const client = await getManagedClient();
|
|
806
|
+
try {
|
|
807
|
+
await client.ping();
|
|
808
|
+
} catch (err) {
|
|
809
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
810
|
+
if (msg.includes("auth") || msg.includes("token") || msg.includes("401") || msg.includes("403")) {
|
|
811
|
+
return "Copilot authentication failed. Run `gh auth login` to refresh your token.";
|
|
812
|
+
}
|
|
813
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("timeout") || msg.includes("network")) {
|
|
814
|
+
return "Could not reach GitHub Copilot service. Check your internet connection.";
|
|
815
|
+
}
|
|
816
|
+
return `Copilot health check failed: ${msg}`;
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
} catch (err) {
|
|
820
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
821
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
822
|
+
return "Copilot CLI binary not found. Ensure GitHub Copilot is installed and your gh CLI is up to date.";
|
|
823
|
+
}
|
|
824
|
+
return `Failed to start Copilot service: ${msg}`;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
var _managedClient = null;
|
|
828
|
+
var _clientStarted = false;
|
|
829
|
+
async function getManagedClient() {
|
|
830
|
+
if (!_managedClient || !_clientStarted) {
|
|
831
|
+
suppressSubprocessWarnings();
|
|
832
|
+
_managedClient = new CopilotClient;
|
|
833
|
+
await _managedClient.start();
|
|
834
|
+
_clientStarted = true;
|
|
835
|
+
const cleanup = () => {
|
|
836
|
+
if (_managedClient && _clientStarted) {
|
|
837
|
+
try {
|
|
838
|
+
_managedClient.stop();
|
|
839
|
+
} catch {}
|
|
840
|
+
_clientStarted = false;
|
|
841
|
+
_managedClient = null;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
process.once("exit", cleanup);
|
|
845
|
+
process.once("SIGINT", cleanup);
|
|
846
|
+
process.once("SIGTERM", cleanup);
|
|
847
|
+
}
|
|
848
|
+
return _managedClient;
|
|
849
|
+
}
|
|
850
|
+
async function callCopilot(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
|
|
851
|
+
const client = await getManagedClient();
|
|
852
|
+
const sessionConfig = {
|
|
853
|
+
systemMessage: { mode: "replace", content: systemMessage }
|
|
854
|
+
};
|
|
855
|
+
if (model)
|
|
856
|
+
sessionConfig.model = model;
|
|
857
|
+
const session = await client.createSession(sessionConfig);
|
|
858
|
+
try {
|
|
859
|
+
const response = await withTimeout(session.sendAndWait({ prompt: userMessage }), timeoutMs);
|
|
860
|
+
if (!response?.data?.content)
|
|
861
|
+
return null;
|
|
862
|
+
return response.data.content;
|
|
863
|
+
} finally {
|
|
864
|
+
await session.destroy();
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function getCommitSystemPrompt(convention) {
|
|
868
|
+
if (convention === "conventional")
|
|
869
|
+
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
870
|
+
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
871
|
+
}
|
|
872
|
+
function extractJson(raw) {
|
|
873
|
+
let text2 = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
874
|
+
if (text2.startsWith("[") || text2.startsWith("{"))
|
|
875
|
+
return text2;
|
|
876
|
+
const arrayStart = text2.indexOf("[");
|
|
877
|
+
const objStart = text2.indexOf("{");
|
|
878
|
+
let start;
|
|
879
|
+
let closeChar;
|
|
880
|
+
if (arrayStart === -1 && objStart === -1)
|
|
881
|
+
return text2;
|
|
882
|
+
if (arrayStart === -1) {
|
|
883
|
+
start = objStart;
|
|
884
|
+
closeChar = "}";
|
|
885
|
+
} else if (objStart === -1) {
|
|
886
|
+
start = arrayStart;
|
|
887
|
+
closeChar = "]";
|
|
888
|
+
} else if (arrayStart < objStart) {
|
|
889
|
+
start = arrayStart;
|
|
890
|
+
closeChar = "]";
|
|
891
|
+
} else {
|
|
892
|
+
start = objStart;
|
|
893
|
+
closeChar = "}";
|
|
894
|
+
}
|
|
895
|
+
const end = text2.lastIndexOf(closeChar);
|
|
896
|
+
if (end > start) {
|
|
897
|
+
text2 = text2.slice(start, end + 1);
|
|
898
|
+
}
|
|
899
|
+
return text2;
|
|
900
|
+
}
|
|
901
|
+
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
902
|
+
try {
|
|
903
|
+
const multiFileHint = stagedFiles.length > 1 ? `
|
|
904
|
+
|
|
905
|
+
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.` : "";
|
|
906
|
+
const userMessage = `Generate a commit message for these staged changes:
|
|
907
|
+
|
|
908
|
+
Files: ${stagedFiles.join(", ")}
|
|
909
|
+
|
|
910
|
+
Diff:
|
|
911
|
+
${diff.slice(0, 4000)}${multiFileHint}`;
|
|
912
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
913
|
+
return result?.trim() ?? null;
|
|
914
|
+
} catch {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function generatePRDescription(commits, diff, model, convention = "clean-commit") {
|
|
919
|
+
try {
|
|
920
|
+
const userMessage = `Generate a PR description for these changes:
|
|
921
|
+
|
|
922
|
+
Commits:
|
|
923
|
+
${commits.join(`
|
|
924
|
+
`)}
|
|
925
|
+
|
|
926
|
+
Diff (truncated):
|
|
927
|
+
${diff.slice(0, 4000)}`;
|
|
928
|
+
const result = await callCopilot(getPRDescriptionSystemPrompt(convention), userMessage, model);
|
|
929
|
+
if (!result)
|
|
930
|
+
return null;
|
|
931
|
+
const cleaned = extractJson(result);
|
|
932
|
+
return JSON.parse(cleaned);
|
|
933
|
+
} catch {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async function suggestBranchName(description, model) {
|
|
938
|
+
try {
|
|
939
|
+
const result = await callCopilot(BRANCH_NAME_SYSTEM_PROMPT, description, model);
|
|
940
|
+
const trimmed = result?.trim() ?? null;
|
|
941
|
+
if (trimmed && /^[a-z]+\/[a-z0-9-]+$/.test(trimmed)) {
|
|
942
|
+
return trimmed;
|
|
943
|
+
}
|
|
944
|
+
return null;
|
|
945
|
+
} catch {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async function suggestConflictResolution(conflictDiff, model) {
|
|
950
|
+
try {
|
|
951
|
+
const userMessage = `Help me resolve this merge conflict:
|
|
952
|
+
|
|
953
|
+
${conflictDiff.slice(0, 4000)}`;
|
|
954
|
+
const result = await callCopilot(CONFLICT_RESOLUTION_SYSTEM_PROMPT, userMessage, model);
|
|
955
|
+
return result?.trim() ?? null;
|
|
956
|
+
} catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
961
|
+
const userMessage = `Group these changed files into logical atomic commits:
|
|
962
|
+
|
|
963
|
+
Files:
|
|
964
|
+
${files.join(`
|
|
965
|
+
`)}
|
|
966
|
+
|
|
967
|
+
Diffs (truncated):
|
|
968
|
+
${diffs.slice(0, 6000)}`;
|
|
969
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
970
|
+
if (!result) {
|
|
971
|
+
throw new Error("AI returned an empty response");
|
|
972
|
+
}
|
|
973
|
+
const cleaned = extractJson(result);
|
|
974
|
+
let parsed;
|
|
975
|
+
try {
|
|
976
|
+
parsed = JSON.parse(cleaned);
|
|
977
|
+
} catch {
|
|
978
|
+
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
979
|
+
}
|
|
980
|
+
const groups = parsed;
|
|
981
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
982
|
+
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
983
|
+
}
|
|
984
|
+
for (const group of groups) {
|
|
985
|
+
if (!Array.isArray(group.files) || typeof group.message !== "string") {
|
|
986
|
+
throw new Error("AI returned groups with invalid structure (missing files or message)");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return groups;
|
|
990
|
+
}
|
|
991
|
+
async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
|
|
992
|
+
const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
|
|
993
|
+
`);
|
|
994
|
+
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
995
|
+
|
|
996
|
+
Groups:
|
|
997
|
+
${groupSummary}
|
|
998
|
+
|
|
999
|
+
Diffs (truncated):
|
|
1000
|
+
${diffs.slice(0, 6000)}`;
|
|
1001
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
1002
|
+
if (!result)
|
|
1003
|
+
return groups;
|
|
1004
|
+
try {
|
|
1005
|
+
const cleaned = extractJson(result);
|
|
1006
|
+
const parsed = JSON.parse(cleaned);
|
|
1007
|
+
if (!Array.isArray(parsed) || parsed.length !== groups.length)
|
|
1008
|
+
return groups;
|
|
1009
|
+
return groups.map((g, i) => ({
|
|
1010
|
+
files: g.files,
|
|
1011
|
+
message: typeof parsed[i]?.message === "string" ? parsed[i].message : g.message
|
|
1012
|
+
}));
|
|
1013
|
+
} catch {
|
|
1014
|
+
return groups;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
|
|
1018
|
+
try {
|
|
1019
|
+
const userMessage = `Generate a single commit message for these files:
|
|
1020
|
+
|
|
1021
|
+
Files: ${files.join(", ")}
|
|
1022
|
+
|
|
1023
|
+
Diff:
|
|
1024
|
+
${diffs.slice(0, 4000)}`;
|
|
1025
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
1026
|
+
return result?.trim() ?? null;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/utils/gh.ts
|
|
1033
|
+
import { execFile as execFileCb2 } from "node:child_process";
|
|
1034
|
+
function run2(args) {
|
|
1035
|
+
return new Promise((resolve) => {
|
|
1036
|
+
execFileCb2("gh", args, (error2, stdout, stderr) => {
|
|
1037
|
+
resolve({
|
|
1038
|
+
exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
|
|
1039
|
+
stdout: stdout ?? "",
|
|
1040
|
+
stderr: stderr ?? ""
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
async function checkGhInstalled() {
|
|
1046
|
+
try {
|
|
1047
|
+
const { exitCode } = await run2(["--version"]);
|
|
1048
|
+
return exitCode === 0;
|
|
1049
|
+
} catch {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function checkGhAuth() {
|
|
1054
|
+
try {
|
|
1055
|
+
const { exitCode } = await run2(["auth", "status"]);
|
|
1056
|
+
return exitCode === 0;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
var SAFE_SLUG = /^[\w.-]+$/;
|
|
1062
|
+
async function checkRepoPermissions(owner, repo) {
|
|
1063
|
+
if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
|
|
1064
|
+
return null;
|
|
1065
|
+
const { exitCode, stdout } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
1066
|
+
if (exitCode !== 0)
|
|
1067
|
+
return null;
|
|
1068
|
+
try {
|
|
1069
|
+
return JSON.parse(stdout.trim());
|
|
1070
|
+
} catch {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function isRepoFork() {
|
|
1075
|
+
const { exitCode, stdout } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
|
|
1076
|
+
if (exitCode !== 0)
|
|
1077
|
+
return null;
|
|
1078
|
+
const val = stdout.trim();
|
|
1079
|
+
if (val === "true")
|
|
1080
|
+
return true;
|
|
1081
|
+
if (val === "false")
|
|
1082
|
+
return false;
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
async function getCurrentRepoInfo() {
|
|
1086
|
+
const { exitCode, stdout } = await run2([
|
|
1087
|
+
"repo",
|
|
1088
|
+
"view",
|
|
1089
|
+
"--json",
|
|
1090
|
+
"nameWithOwner",
|
|
1091
|
+
"-q",
|
|
1092
|
+
".nameWithOwner"
|
|
1093
|
+
]);
|
|
1094
|
+
if (exitCode !== 0)
|
|
1095
|
+
return null;
|
|
1096
|
+
const nameWithOwner = stdout.trim();
|
|
1097
|
+
if (!nameWithOwner)
|
|
1098
|
+
return null;
|
|
1099
|
+
const [owner, repo] = nameWithOwner.split("/");
|
|
1100
|
+
if (!owner || !repo)
|
|
1101
|
+
return null;
|
|
1102
|
+
return { owner, repo };
|
|
1103
|
+
}
|
|
1104
|
+
async function createPR(options) {
|
|
1105
|
+
const args = [
|
|
1106
|
+
"pr",
|
|
1107
|
+
"create",
|
|
1108
|
+
"--base",
|
|
1109
|
+
options.base,
|
|
1110
|
+
"--title",
|
|
1111
|
+
options.title,
|
|
1112
|
+
"--body",
|
|
1113
|
+
options.body
|
|
1114
|
+
];
|
|
1115
|
+
if (options.draft)
|
|
1116
|
+
args.push("--draft");
|
|
1117
|
+
return run2(args);
|
|
896
1118
|
}
|
|
897
|
-
function
|
|
898
|
-
|
|
1119
|
+
async function createPRFill(base, draft) {
|
|
1120
|
+
const args = ["pr", "create", "--base", base, "--fill"];
|
|
1121
|
+
if (draft)
|
|
1122
|
+
args.push("--draft");
|
|
1123
|
+
return run2(args);
|
|
899
1124
|
}
|
|
900
|
-
function
|
|
901
|
-
|
|
1125
|
+
async function getPRForBranch(headBranch) {
|
|
1126
|
+
const { exitCode, stdout } = await run2([
|
|
1127
|
+
"pr",
|
|
1128
|
+
"list",
|
|
1129
|
+
"--head",
|
|
1130
|
+
headBranch,
|
|
1131
|
+
"--state",
|
|
1132
|
+
"open",
|
|
1133
|
+
"--json",
|
|
1134
|
+
"number,url,title,state",
|
|
1135
|
+
"--limit",
|
|
1136
|
+
"1"
|
|
1137
|
+
]);
|
|
1138
|
+
if (exitCode !== 0)
|
|
1139
|
+
return null;
|
|
1140
|
+
try {
|
|
1141
|
+
const prs = JSON.parse(stdout.trim());
|
|
1142
|
+
return prs.length > 0 ? prs[0] : null;
|
|
1143
|
+
} catch {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
902
1146
|
}
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
|
|
1147
|
+
async function getMergedPRForBranch(headBranch) {
|
|
1148
|
+
const { exitCode, stdout } = await run2([
|
|
1149
|
+
"pr",
|
|
1150
|
+
"list",
|
|
1151
|
+
"--head",
|
|
1152
|
+
headBranch,
|
|
1153
|
+
"--state",
|
|
1154
|
+
"merged",
|
|
1155
|
+
"--json",
|
|
1156
|
+
"number,url,title,state",
|
|
1157
|
+
"--limit",
|
|
1158
|
+
"1"
|
|
1159
|
+
]);
|
|
1160
|
+
if (exitCode !== 0)
|
|
1161
|
+
return null;
|
|
1162
|
+
try {
|
|
1163
|
+
const prs = JSON.parse(stdout.trim());
|
|
1164
|
+
return prs.length > 0 ? prs[0] : null;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
906
1168
|
}
|
|
907
1169
|
|
|
908
1170
|
// src/utils/spinner.ts
|
|
909
|
-
import
|
|
1171
|
+
import pc4 from "picocolors";
|
|
910
1172
|
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
911
1173
|
function createSpinner(text2) {
|
|
912
1174
|
let frameIdx = 0;
|
|
@@ -918,7 +1180,7 @@ function createSpinner(text2) {
|
|
|
918
1180
|
const render = () => {
|
|
919
1181
|
if (stopped)
|
|
920
1182
|
return;
|
|
921
|
-
const frame =
|
|
1183
|
+
const frame = pc4.cyan(FRAMES[frameIdx % FRAMES.length]);
|
|
922
1184
|
clearLine();
|
|
923
1185
|
process.stderr.write(`${frame} ${currentText}`);
|
|
924
1186
|
frameIdx++;
|
|
@@ -938,12 +1200,12 @@ function createSpinner(text2) {
|
|
|
938
1200
|
},
|
|
939
1201
|
success(msg) {
|
|
940
1202
|
stop();
|
|
941
|
-
process.stderr.write(`${
|
|
1203
|
+
process.stderr.write(`${pc4.green("✔")} ${msg}
|
|
942
1204
|
`);
|
|
943
1205
|
},
|
|
944
1206
|
fail(msg) {
|
|
945
1207
|
stop();
|
|
946
|
-
process.stderr.write(`${
|
|
1208
|
+
process.stderr.write(`${pc4.red("✖")} ${msg}
|
|
947
1209
|
`);
|
|
948
1210
|
},
|
|
949
1211
|
stop() {
|
|
@@ -952,53 +1214,6 @@ function createSpinner(text2) {
|
|
|
952
1214
|
};
|
|
953
1215
|
}
|
|
954
1216
|
|
|
955
|
-
// src/utils/workflow.ts
|
|
956
|
-
var WORKFLOW_DESCRIPTIONS = {
|
|
957
|
-
"clean-flow": "Clean Flow — main + dev, squash features into dev, merge dev into main",
|
|
958
|
-
"github-flow": "GitHub Flow — main + feature branches, squash/merge into main",
|
|
959
|
-
"git-flow": "Git Flow — main + develop + release + hotfix branches"
|
|
960
|
-
};
|
|
961
|
-
function getBaseBranch(config) {
|
|
962
|
-
switch (config.workflow) {
|
|
963
|
-
case "clean-flow":
|
|
964
|
-
case "git-flow":
|
|
965
|
-
return config.devBranch ?? "dev";
|
|
966
|
-
case "github-flow":
|
|
967
|
-
return config.mainBranch;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
function hasDevBranch(workflow) {
|
|
971
|
-
return workflow === "clean-flow" || workflow === "git-flow";
|
|
972
|
-
}
|
|
973
|
-
function getSyncSource(config) {
|
|
974
|
-
const { workflow, role, mainBranch, origin, upstream } = config;
|
|
975
|
-
const devBranch = config.devBranch ?? "dev";
|
|
976
|
-
switch (workflow) {
|
|
977
|
-
case "clean-flow":
|
|
978
|
-
if (role === "contributor") {
|
|
979
|
-
return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
|
|
980
|
-
}
|
|
981
|
-
return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
|
|
982
|
-
case "github-flow":
|
|
983
|
-
if (role === "contributor") {
|
|
984
|
-
return { remote: upstream, ref: `${upstream}/${mainBranch}`, strategy: "pull" };
|
|
985
|
-
}
|
|
986
|
-
return { remote: origin, ref: `${origin}/${mainBranch}`, strategy: "pull" };
|
|
987
|
-
case "git-flow":
|
|
988
|
-
if (role === "contributor") {
|
|
989
|
-
return { remote: upstream, ref: `${upstream}/${devBranch}`, strategy: "pull" };
|
|
990
|
-
}
|
|
991
|
-
return { remote: origin, ref: `${origin}/${devBranch}`, strategy: "pull" };
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
function getProtectedBranches(config) {
|
|
995
|
-
const branches = [config.mainBranch];
|
|
996
|
-
if (hasDevBranch(config.workflow) && config.devBranch) {
|
|
997
|
-
branches.push(config.devBranch);
|
|
998
|
-
}
|
|
999
|
-
return branches;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
1217
|
// src/commands/clean.ts
|
|
1003
1218
|
async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
1004
1219
|
if (!config)
|
|
@@ -1011,18 +1226,18 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1011
1226
|
warn("You have uncommitted changes in your working tree.");
|
|
1012
1227
|
}
|
|
1013
1228
|
if (localWork.unpushedCommits > 0) {
|
|
1014
|
-
warn(`You have ${
|
|
1229
|
+
warn(`You have ${pc5.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
|
|
1015
1230
|
}
|
|
1016
1231
|
const SAVE_NEW_BRANCH = "Save changes to a new branch";
|
|
1017
1232
|
const DISCARD = "Discard all changes and clean up";
|
|
1018
1233
|
const CANCEL = "Skip this branch";
|
|
1019
|
-
const action = await selectPrompt(`${
|
|
1234
|
+
const action = await selectPrompt(`${pc5.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
|
|
1020
1235
|
if (action === CANCEL)
|
|
1021
1236
|
return "skipped";
|
|
1022
1237
|
if (action === SAVE_NEW_BRANCH) {
|
|
1023
1238
|
if (!config)
|
|
1024
1239
|
return "skipped";
|
|
1025
|
-
info(
|
|
1240
|
+
info(pc5.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
1026
1241
|
const description = await inputPrompt("What are you working on?");
|
|
1027
1242
|
let newBranchName = description;
|
|
1028
1243
|
if (looksLikeNaturalLanguage(description)) {
|
|
@@ -1031,8 +1246,8 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1031
1246
|
if (suggested) {
|
|
1032
1247
|
spinner.success("Branch name suggestion ready.");
|
|
1033
1248
|
console.log(`
|
|
1034
|
-
${
|
|
1035
|
-
const accepted = await confirmPrompt(`Use ${
|
|
1249
|
+
${pc5.dim("AI suggestion:")} ${pc5.bold(pc5.cyan(suggested))}`);
|
|
1250
|
+
const accepted = await confirmPrompt(`Use ${pc5.bold(suggested)} as your branch name?`);
|
|
1036
1251
|
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
1037
1252
|
} else {
|
|
1038
1253
|
spinner.fail("AI did not return a suggestion.");
|
|
@@ -1040,7 +1255,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1040
1255
|
}
|
|
1041
1256
|
}
|
|
1042
1257
|
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
1043
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
1258
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc5.bold(newBranchName)}:`, config.branchPrefixes);
|
|
1044
1259
|
newBranchName = formatBranchName(prefix, newBranchName);
|
|
1045
1260
|
}
|
|
1046
1261
|
if (!isValidBranchName(newBranchName)) {
|
|
@@ -1052,16 +1267,16 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1052
1267
|
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
1053
1268
|
return "skipped";
|
|
1054
1269
|
}
|
|
1055
|
-
success(`Renamed ${
|
|
1270
|
+
success(`Renamed ${pc5.bold(currentBranch)} → ${pc5.bold(newBranchName)}`);
|
|
1056
1271
|
const syncSource2 = getSyncSource(config);
|
|
1057
1272
|
await fetchRemote(syncSource2.remote);
|
|
1058
1273
|
const savedUpstreamRef = await getUpstreamRef();
|
|
1059
1274
|
const rebaseResult = savedUpstreamRef && savedUpstreamRef !== syncSource2.ref ? await rebaseOnto(syncSource2.ref, savedUpstreamRef) : await rebase(syncSource2.ref);
|
|
1060
1275
|
if (rebaseResult.exitCode !== 0) {
|
|
1061
1276
|
warn("Rebase encountered conflicts. Resolve them after cleanup:");
|
|
1062
|
-
info(` ${
|
|
1277
|
+
info(` ${pc5.bold(`git checkout ${newBranchName} && git rebase --continue`)}`);
|
|
1063
1278
|
} else {
|
|
1064
|
-
success(`Rebased ${
|
|
1279
|
+
success(`Rebased ${pc5.bold(newBranchName)} onto ${pc5.bold(syncSource2.ref)}.`);
|
|
1065
1280
|
}
|
|
1066
1281
|
const coResult2 = await checkoutBranch(baseBranch);
|
|
1067
1282
|
if (coResult2.exitCode !== 0) {
|
|
@@ -1069,12 +1284,12 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1069
1284
|
return "saved";
|
|
1070
1285
|
}
|
|
1071
1286
|
await updateLocalBranch(baseBranch, syncSource2.ref);
|
|
1072
|
-
success(`Synced ${
|
|
1287
|
+
success(`Synced ${pc5.bold(baseBranch)} with ${pc5.bold(syncSource2.ref)}.`);
|
|
1073
1288
|
return "saved";
|
|
1074
1289
|
}
|
|
1075
1290
|
}
|
|
1076
1291
|
const syncSource = getSyncSource(config);
|
|
1077
|
-
info(`Switching to ${
|
|
1292
|
+
info(`Switching to ${pc5.bold(baseBranch)} and syncing...`);
|
|
1078
1293
|
await fetchRemote(syncSource.remote);
|
|
1079
1294
|
const coResult = await checkoutBranch(baseBranch);
|
|
1080
1295
|
if (coResult.exitCode !== 0) {
|
|
@@ -1082,10 +1297,10 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1082
1297
|
return "skipped";
|
|
1083
1298
|
}
|
|
1084
1299
|
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1085
|
-
success(`Synced ${
|
|
1300
|
+
success(`Synced ${pc5.bold(baseBranch)} with ${pc5.bold(syncSource.ref)}.`);
|
|
1086
1301
|
return "switched";
|
|
1087
1302
|
}
|
|
1088
|
-
var clean_default =
|
|
1303
|
+
var clean_default = defineCommand2({
|
|
1089
1304
|
meta: {
|
|
1090
1305
|
name: "clean",
|
|
1091
1306
|
description: "Delete merged branches and prune remote refs"
|
|
@@ -1130,21 +1345,21 @@ var clean_default = defineCommand({
|
|
|
1130
1345
|
if (ghInstalled && ghAuthed) {
|
|
1131
1346
|
const mergedPR = await getMergedPRForBranch(currentBranch);
|
|
1132
1347
|
if (mergedPR) {
|
|
1133
|
-
warn(`PR #${mergedPR.number} (${
|
|
1134
|
-
info(`Link: ${
|
|
1348
|
+
warn(`PR #${mergedPR.number} (${pc5.bold(mergedPR.title)}) has already been merged.`);
|
|
1349
|
+
info(`Link: ${pc5.underline(mergedPR.url)}`);
|
|
1135
1350
|
goneCandidates.push(currentBranch);
|
|
1136
1351
|
}
|
|
1137
1352
|
}
|
|
1138
1353
|
}
|
|
1139
1354
|
if (mergedCandidates.length > 0) {
|
|
1140
1355
|
console.log(`
|
|
1141
|
-
${
|
|
1356
|
+
${pc5.bold("Merged branches to delete:")}`);
|
|
1142
1357
|
for (const b of mergedCandidates) {
|
|
1143
|
-
const marker = b === currentBranch ?
|
|
1144
|
-
console.log(` ${
|
|
1358
|
+
const marker = b === currentBranch ? pc5.yellow(" (current)") : "";
|
|
1359
|
+
console.log(` ${pc5.dim("•")} ${b}${marker}`);
|
|
1145
1360
|
}
|
|
1146
1361
|
console.log();
|
|
1147
|
-
const ok = args.yes || await confirmPrompt(`Delete ${
|
|
1362
|
+
const ok = args.yes || await confirmPrompt(`Delete ${pc5.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
|
|
1148
1363
|
if (ok) {
|
|
1149
1364
|
for (const branch of mergedCandidates) {
|
|
1150
1365
|
if (branch === currentBranch) {
|
|
@@ -1161,7 +1376,7 @@ ${pc4.bold("Merged branches to delete:")}`);
|
|
|
1161
1376
|
}
|
|
1162
1377
|
const result = await deleteBranch(branch);
|
|
1163
1378
|
if (result.exitCode === 0) {
|
|
1164
|
-
success(` Deleted ${
|
|
1379
|
+
success(` Deleted ${pc5.bold(branch)}`);
|
|
1165
1380
|
} else {
|
|
1166
1381
|
warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
|
|
1167
1382
|
}
|
|
@@ -1172,13 +1387,13 @@ ${pc4.bold("Merged branches to delete:")}`);
|
|
|
1172
1387
|
}
|
|
1173
1388
|
if (goneCandidates.length > 0) {
|
|
1174
1389
|
console.log(`
|
|
1175
|
-
${
|
|
1390
|
+
${pc5.bold("Stale branches (remote deleted, likely squash-merged):")}`);
|
|
1176
1391
|
for (const b of goneCandidates) {
|
|
1177
|
-
const marker = b === currentBranch ?
|
|
1178
|
-
console.log(` ${
|
|
1392
|
+
const marker = b === currentBranch ? pc5.yellow(" (current)") : "";
|
|
1393
|
+
console.log(` ${pc5.dim("•")} ${b}${marker}`);
|
|
1179
1394
|
}
|
|
1180
1395
|
console.log();
|
|
1181
|
-
const ok = args.yes || await confirmPrompt(`Delete ${
|
|
1396
|
+
const ok = args.yes || await confirmPrompt(`Delete ${pc5.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
|
|
1182
1397
|
if (ok) {
|
|
1183
1398
|
for (const branch of goneCandidates) {
|
|
1184
1399
|
if (branch === currentBranch) {
|
|
@@ -1195,7 +1410,7 @@ ${pc4.bold("Stale branches (remote deleted, likely squash-merged):")}`);
|
|
|
1195
1410
|
}
|
|
1196
1411
|
const result = await forceDeleteBranch(branch);
|
|
1197
1412
|
if (result.exitCode === 0) {
|
|
1198
|
-
success(` Deleted ${
|
|
1413
|
+
success(` Deleted ${pc5.bold(branch)}`);
|
|
1199
1414
|
} else {
|
|
1200
1415
|
warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
|
|
1201
1416
|
}
|
|
@@ -1210,14 +1425,14 @@ ${pc4.bold("Stale branches (remote deleted, likely squash-merged):")}`);
|
|
|
1210
1425
|
const finalBranch = await getCurrentBranch();
|
|
1211
1426
|
if (finalBranch && protectedBranches.has(finalBranch)) {
|
|
1212
1427
|
console.log();
|
|
1213
|
-
info(`You're on ${
|
|
1428
|
+
info(`You're on ${pc5.bold(finalBranch)}. Run ${pc5.bold("contrib start")} to begin a new feature.`);
|
|
1214
1429
|
}
|
|
1215
1430
|
}
|
|
1216
1431
|
});
|
|
1217
1432
|
|
|
1218
1433
|
// src/commands/commit.ts
|
|
1219
|
-
import { defineCommand as
|
|
1220
|
-
import
|
|
1434
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
1435
|
+
import pc6 from "picocolors";
|
|
1221
1436
|
|
|
1222
1437
|
// src/utils/convention.ts
|
|
1223
1438
|
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;
|
|
@@ -1263,7 +1478,7 @@ function getValidationError(convention) {
|
|
|
1263
1478
|
}
|
|
1264
1479
|
|
|
1265
1480
|
// src/commands/commit.ts
|
|
1266
|
-
var commit_default =
|
|
1481
|
+
var commit_default = defineCommand3({
|
|
1267
1482
|
meta: {
|
|
1268
1483
|
name: "commit",
|
|
1269
1484
|
description: "Stage changes and create a commit message (AI-powered)"
|
|
@@ -1307,9 +1522,9 @@ var commit_default = defineCommand2({
|
|
|
1307
1522
|
process.exit(1);
|
|
1308
1523
|
}
|
|
1309
1524
|
console.log(`
|
|
1310
|
-
${
|
|
1525
|
+
${pc6.bold("Changed files:")}`);
|
|
1311
1526
|
for (const f of changedFiles) {
|
|
1312
|
-
console.log(` ${
|
|
1527
|
+
console.log(` ${pc6.dim("•")} ${f}`);
|
|
1313
1528
|
}
|
|
1314
1529
|
const stageAction = await selectPrompt("No staged changes. How would you like to stage?", [
|
|
1315
1530
|
"Stage all changes",
|
|
@@ -1359,7 +1574,7 @@ ${pc5.bold("Changed files:")}`);
|
|
|
1359
1574
|
if (commitMessage) {
|
|
1360
1575
|
spinner.success("AI commit message generated.");
|
|
1361
1576
|
console.log(`
|
|
1362
|
-
${
|
|
1577
|
+
${pc6.dim("AI suggestion:")} ${pc6.bold(pc6.cyan(commitMessage))}`);
|
|
1363
1578
|
} else {
|
|
1364
1579
|
spinner.fail("AI did not return a commit message.");
|
|
1365
1580
|
warn("Falling back to manual entry.");
|
|
@@ -1385,7 +1600,7 @@ ${pc5.bold("Changed files:")}`);
|
|
|
1385
1600
|
if (regen) {
|
|
1386
1601
|
spinner.success("Commit message regenerated.");
|
|
1387
1602
|
console.log(`
|
|
1388
|
-
${
|
|
1603
|
+
${pc6.dim("AI suggestion:")} ${pc6.bold(pc6.cyan(regen))}`);
|
|
1389
1604
|
const ok = await confirmPrompt("Use this message?");
|
|
1390
1605
|
finalMessage = ok ? regen : await inputPrompt("Enter commit message manually");
|
|
1391
1606
|
} else {
|
|
@@ -1400,7 +1615,7 @@ ${pc5.bold("Changed files:")}`);
|
|
|
1400
1615
|
if (convention2 !== "none") {
|
|
1401
1616
|
console.log();
|
|
1402
1617
|
for (const hint of CONVENTION_FORMAT_HINTS[convention2]) {
|
|
1403
|
-
console.log(
|
|
1618
|
+
console.log(pc6.dim(hint));
|
|
1404
1619
|
}
|
|
1405
1620
|
console.log();
|
|
1406
1621
|
}
|
|
@@ -1424,7 +1639,7 @@ ${pc5.bold("Changed files:")}`);
|
|
|
1424
1639
|
error(`Failed to commit: ${result.stderr}`);
|
|
1425
1640
|
process.exit(1);
|
|
1426
1641
|
}
|
|
1427
|
-
success(`✅ Committed: ${
|
|
1642
|
+
success(`✅ Committed: ${pc6.bold(finalMessage)}`);
|
|
1428
1643
|
}
|
|
1429
1644
|
});
|
|
1430
1645
|
async function runGroupCommit(model, config) {
|
|
@@ -1441,9 +1656,9 @@ async function runGroupCommit(model, config) {
|
|
|
1441
1656
|
process.exit(1);
|
|
1442
1657
|
}
|
|
1443
1658
|
console.log(`
|
|
1444
|
-
${
|
|
1659
|
+
${pc6.bold("Changed files:")}`);
|
|
1445
1660
|
for (const f of changedFiles) {
|
|
1446
|
-
console.log(` ${
|
|
1661
|
+
console.log(` ${pc6.dim("•")} ${f}`);
|
|
1447
1662
|
}
|
|
1448
1663
|
const spinner = createSpinner(`Asking AI to group ${changedFiles.length} file(s) into logical commits...`);
|
|
1449
1664
|
const diffs = await getFullDiffForFiles(changedFiles);
|
|
@@ -1481,13 +1696,13 @@ ${pc5.bold("Changed files:")}`);
|
|
|
1481
1696
|
let commitAll = false;
|
|
1482
1697
|
while (!proceedToCommit) {
|
|
1483
1698
|
console.log(`
|
|
1484
|
-
${
|
|
1699
|
+
${pc6.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
1485
1700
|
`);
|
|
1486
1701
|
for (let i = 0;i < validGroups.length; i++) {
|
|
1487
1702
|
const g = validGroups[i];
|
|
1488
|
-
console.log(` ${
|
|
1703
|
+
console.log(` ${pc6.cyan(`Group ${i + 1}:`)} ${pc6.bold(g.message)}`);
|
|
1489
1704
|
for (const f of g.files) {
|
|
1490
|
-
console.log(` ${
|
|
1705
|
+
console.log(` ${pc6.dim("•")} ${f}`);
|
|
1491
1706
|
}
|
|
1492
1707
|
console.log();
|
|
1493
1708
|
}
|
|
@@ -1531,16 +1746,16 @@ ${pc5.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
|
1531
1746
|
continue;
|
|
1532
1747
|
}
|
|
1533
1748
|
committed++;
|
|
1534
|
-
success(`✅ Committed group ${i + 1}: ${
|
|
1749
|
+
success(`✅ Committed group ${i + 1}: ${pc6.bold(group.message)}`);
|
|
1535
1750
|
}
|
|
1536
1751
|
} else {
|
|
1537
1752
|
for (let i = 0;i < validGroups.length; i++) {
|
|
1538
1753
|
const group = validGroups[i];
|
|
1539
|
-
console.log(
|
|
1754
|
+
console.log(pc6.bold(`
|
|
1540
1755
|
── Group ${i + 1}/${validGroups.length} ──`));
|
|
1541
|
-
console.log(` ${
|
|
1756
|
+
console.log(` ${pc6.cyan(group.message)}`);
|
|
1542
1757
|
for (const f of group.files) {
|
|
1543
|
-
console.log(` ${
|
|
1758
|
+
console.log(` ${pc6.dim("•")} ${f}`);
|
|
1544
1759
|
}
|
|
1545
1760
|
let message = group.message;
|
|
1546
1761
|
let actionDone = false;
|
|
@@ -1562,7 +1777,7 @@ ${pc5.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
|
1562
1777
|
if (newMsg) {
|
|
1563
1778
|
message = newMsg;
|
|
1564
1779
|
group.message = newMsg;
|
|
1565
|
-
regenSpinner.success(`New message: ${
|
|
1780
|
+
regenSpinner.success(`New message: ${pc6.bold(message)}`);
|
|
1566
1781
|
} else {
|
|
1567
1782
|
regenSpinner.fail("AI could not generate a new message. Keeping current one.");
|
|
1568
1783
|
}
|
|
@@ -1602,7 +1817,7 @@ ${pc5.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
|
1602
1817
|
continue;
|
|
1603
1818
|
}
|
|
1604
1819
|
committed++;
|
|
1605
|
-
success(`✅ Committed group ${i + 1}: ${
|
|
1820
|
+
success(`✅ Committed group ${i + 1}: ${pc6.bold(message)}`);
|
|
1606
1821
|
actionDone = true;
|
|
1607
1822
|
}
|
|
1608
1823
|
}
|
|
@@ -1618,12 +1833,12 @@ ${pc5.bold(`AI suggested ${validGroups.length} commit group(s):`)}
|
|
|
1618
1833
|
|
|
1619
1834
|
// src/commands/doctor.ts
|
|
1620
1835
|
import { execFile as execFileCb3 } from "node:child_process";
|
|
1621
|
-
import { defineCommand as
|
|
1622
|
-
import
|
|
1836
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
1837
|
+
import pc7 from "picocolors";
|
|
1623
1838
|
// package.json
|
|
1624
1839
|
var package_default = {
|
|
1625
1840
|
name: "contribute-now",
|
|
1626
|
-
version: "0.2.0-dev.
|
|
1841
|
+
version: "0.2.0-dev.2621ffa",
|
|
1627
1842
|
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
1628
1843
|
type: "module",
|
|
1629
1844
|
bin: {
|
|
@@ -1712,16 +1927,16 @@ async function getRepoInfoFromRemote(remote = "origin") {
|
|
|
1712
1927
|
}
|
|
1713
1928
|
|
|
1714
1929
|
// src/commands/doctor.ts
|
|
1715
|
-
var PASS = ` ${
|
|
1716
|
-
var FAIL = ` ${
|
|
1717
|
-
var WARN = ` ${
|
|
1930
|
+
var PASS = ` ${pc7.green("✔")} `;
|
|
1931
|
+
var FAIL = ` ${pc7.red("✗")} `;
|
|
1932
|
+
var WARN = ` ${pc7.yellow("⚠")} `;
|
|
1718
1933
|
function printReport(report) {
|
|
1719
1934
|
for (const section of report.sections) {
|
|
1720
1935
|
console.log(`
|
|
1721
|
-
${
|
|
1936
|
+
${pc7.bold(pc7.underline(section.title))}`);
|
|
1722
1937
|
for (const check of section.checks) {
|
|
1723
1938
|
const prefix = check.ok ? check.warning ? WARN : PASS : FAIL;
|
|
1724
|
-
const text2 = check.detail ? `${check.label} ${
|
|
1939
|
+
const text2 = check.detail ? `${check.label} ${pc7.dim(`— ${check.detail}`)}` : check.label;
|
|
1725
1940
|
console.log(`${prefix}${text2}`);
|
|
1726
1941
|
}
|
|
1727
1942
|
}
|
|
@@ -1929,7 +2144,7 @@ function envSection() {
|
|
|
1929
2144
|
}
|
|
1930
2145
|
return { title: "Environment", checks };
|
|
1931
2146
|
}
|
|
1932
|
-
var doctor_default =
|
|
2147
|
+
var doctor_default = defineCommand4({
|
|
1933
2148
|
meta: {
|
|
1934
2149
|
name: "doctor",
|
|
1935
2150
|
description: "Diagnose the contribute-now CLI environment and configuration"
|
|
@@ -1965,14 +2180,14 @@ var doctor_default = defineCommand3({
|
|
|
1965
2180
|
const failures = total.filter((c) => !c.ok);
|
|
1966
2181
|
const warnings = total.filter((c) => c.ok && c.warning);
|
|
1967
2182
|
if (failures.length === 0 && warnings.length === 0) {
|
|
1968
|
-
console.log(` ${
|
|
2183
|
+
console.log(` ${pc7.green("All checks passed!")} No issues detected.
|
|
1969
2184
|
`);
|
|
1970
2185
|
} else {
|
|
1971
2186
|
if (failures.length > 0) {
|
|
1972
|
-
console.log(` ${
|
|
2187
|
+
console.log(` ${pc7.red(`${failures.length} issue${failures.length !== 1 ? "s" : ""} found.`)}`);
|
|
1973
2188
|
}
|
|
1974
2189
|
if (warnings.length > 0) {
|
|
1975
|
-
console.log(` ${
|
|
2190
|
+
console.log(` ${pc7.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}.`)}`);
|
|
1976
2191
|
}
|
|
1977
2192
|
console.log();
|
|
1978
2193
|
}
|
|
@@ -1982,8 +2197,8 @@ var doctor_default = defineCommand3({
|
|
|
1982
2197
|
// src/commands/hook.ts
|
|
1983
2198
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1984
2199
|
import { join as join3 } from "node:path";
|
|
1985
|
-
import { defineCommand as
|
|
1986
|
-
import
|
|
2200
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
2201
|
+
import pc8 from "picocolors";
|
|
1987
2202
|
var HOOK_MARKER = "# managed by contribute-now";
|
|
1988
2203
|
function getHooksDir(cwd = process.cwd()) {
|
|
1989
2204
|
return join3(cwd, ".git", "hooks");
|
|
@@ -2021,7 +2236,7 @@ else
|
|
|
2021
2236
|
fi
|
|
2022
2237
|
`;
|
|
2023
2238
|
}
|
|
2024
|
-
var hook_default =
|
|
2239
|
+
var hook_default = defineCommand5({
|
|
2025
2240
|
meta: {
|
|
2026
2241
|
name: "hook",
|
|
2027
2242
|
description: "Install or uninstall the commit-msg git hook"
|
|
@@ -2079,8 +2294,8 @@ async function installHook() {
|
|
|
2079
2294
|
}
|
|
2080
2295
|
writeFileSync2(hookPath, generateHookScript(), { mode: 493 });
|
|
2081
2296
|
success(`commit-msg hook installed.`);
|
|
2082
|
-
info(`Convention: ${
|
|
2083
|
-
info(`Path: ${
|
|
2297
|
+
info(`Convention: ${pc8.bold(CONVENTION_LABELS[config.commitConvention])}`);
|
|
2298
|
+
info(`Path: ${pc8.dim(hookPath)}`);
|
|
2084
2299
|
}
|
|
2085
2300
|
async function uninstallHook() {
|
|
2086
2301
|
heading("\uD83E\uDE9D hook uninstall");
|
|
@@ -2098,10 +2313,165 @@ async function uninstallHook() {
|
|
|
2098
2313
|
success("commit-msg hook removed.");
|
|
2099
2314
|
}
|
|
2100
2315
|
|
|
2316
|
+
// src/commands/log.ts
|
|
2317
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
2318
|
+
import pc9 from "picocolors";
|
|
2319
|
+
var log_default = defineCommand6({
|
|
2320
|
+
meta: {
|
|
2321
|
+
name: "log",
|
|
2322
|
+
description: "Show a colorized, workflow-aware commit log with graph"
|
|
2323
|
+
},
|
|
2324
|
+
args: {
|
|
2325
|
+
count: {
|
|
2326
|
+
type: "string",
|
|
2327
|
+
alias: "n",
|
|
2328
|
+
description: "Number of commits to show (default: 20)"
|
|
2329
|
+
},
|
|
2330
|
+
all: {
|
|
2331
|
+
type: "boolean",
|
|
2332
|
+
alias: "a",
|
|
2333
|
+
description: "Show all branches, not just current",
|
|
2334
|
+
default: false
|
|
2335
|
+
},
|
|
2336
|
+
graph: {
|
|
2337
|
+
type: "boolean",
|
|
2338
|
+
alias: "g",
|
|
2339
|
+
description: "Show graph view with branch lines",
|
|
2340
|
+
default: true
|
|
2341
|
+
},
|
|
2342
|
+
branch: {
|
|
2343
|
+
type: "string",
|
|
2344
|
+
alias: "b",
|
|
2345
|
+
description: "Show log for a specific branch"
|
|
2346
|
+
}
|
|
2347
|
+
},
|
|
2348
|
+
async run({ args }) {
|
|
2349
|
+
if (!await isGitRepo()) {
|
|
2350
|
+
error("Not inside a git repository.");
|
|
2351
|
+
process.exit(1);
|
|
2352
|
+
}
|
|
2353
|
+
const config = readConfig();
|
|
2354
|
+
const count = args.count ? Number.parseInt(args.count, 10) : 20;
|
|
2355
|
+
const showAll = args.all;
|
|
2356
|
+
const showGraph = args.graph;
|
|
2357
|
+
const targetBranch = args.branch;
|
|
2358
|
+
const protectedBranches = config ? getProtectedBranches(config) : ["main", "master"];
|
|
2359
|
+
const currentBranch = await getCurrentBranch();
|
|
2360
|
+
heading("\uD83D\uDCDC commit log");
|
|
2361
|
+
if (showGraph) {
|
|
2362
|
+
const lines = await getLogGraph({ count, all: showAll, branch: targetBranch });
|
|
2363
|
+
if (lines.length === 0) {
|
|
2364
|
+
console.log(pc9.dim(" No commits found."));
|
|
2365
|
+
console.log();
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
console.log();
|
|
2369
|
+
for (const line of lines) {
|
|
2370
|
+
console.log(` ${colorizeGraphLine(line, protectedBranches, currentBranch)}`);
|
|
2371
|
+
}
|
|
2372
|
+
} else {
|
|
2373
|
+
const entries = await getLogEntries({ count, all: showAll, branch: targetBranch });
|
|
2374
|
+
if (entries.length === 0) {
|
|
2375
|
+
console.log(pc9.dim(" No commits found."));
|
|
2376
|
+
console.log();
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
console.log();
|
|
2380
|
+
for (const entry of entries) {
|
|
2381
|
+
const hashStr = pc9.yellow(entry.hash);
|
|
2382
|
+
const refsStr = entry.refs ? ` ${colorizeRefs(entry.refs, protectedBranches, currentBranch)}` : "";
|
|
2383
|
+
const subjectStr = colorizeSubject(entry.subject);
|
|
2384
|
+
console.log(` ${hashStr}${refsStr} ${subjectStr}`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
console.log();
|
|
2388
|
+
console.log(pc9.dim(` Showing ${count} most recent commits${showAll ? " (all branches)" : targetBranch ? ` (${targetBranch})` : ""}`));
|
|
2389
|
+
console.log(pc9.dim(` Use ${pc9.bold("contrib log -n 50")} for more, or ${pc9.bold("contrib log --all")} for all branches`));
|
|
2390
|
+
console.log();
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
function colorizeGraphLine(line, protectedBranches, currentBranch) {
|
|
2394
|
+
const match = line.match(/^([|/\\*\s_.-]*)([a-f0-9]{7,12})(\s+\(([^)]+)\))?\s*(.*)/);
|
|
2395
|
+
if (!match) {
|
|
2396
|
+
return pc9.cyan(line);
|
|
2397
|
+
}
|
|
2398
|
+
const [, graphPart = "", hash, , refs, subject = ""] = match;
|
|
2399
|
+
const parts = [];
|
|
2400
|
+
if (graphPart) {
|
|
2401
|
+
parts.push(colorizeGraphChars(graphPart));
|
|
2402
|
+
}
|
|
2403
|
+
parts.push(pc9.yellow(hash));
|
|
2404
|
+
if (refs) {
|
|
2405
|
+
parts.push(` (${colorizeRefs(refs, protectedBranches, currentBranch)})`);
|
|
2406
|
+
}
|
|
2407
|
+
parts.push(` ${colorizeSubject(subject)}`);
|
|
2408
|
+
return parts.join("");
|
|
2409
|
+
}
|
|
2410
|
+
function colorizeGraphChars(graphPart) {
|
|
2411
|
+
return graphPart.split("").map((ch) => {
|
|
2412
|
+
switch (ch) {
|
|
2413
|
+
case "*":
|
|
2414
|
+
return pc9.green(ch);
|
|
2415
|
+
case "|":
|
|
2416
|
+
return pc9.cyan(ch);
|
|
2417
|
+
case "/":
|
|
2418
|
+
case "\\":
|
|
2419
|
+
return pc9.cyan(ch);
|
|
2420
|
+
case "-":
|
|
2421
|
+
case "_":
|
|
2422
|
+
return pc9.cyan(ch);
|
|
2423
|
+
default:
|
|
2424
|
+
return ch;
|
|
2425
|
+
}
|
|
2426
|
+
}).join("");
|
|
2427
|
+
}
|
|
2428
|
+
function colorizeRefs(refs, protectedBranches, currentBranch) {
|
|
2429
|
+
return refs.split(",").map((ref) => {
|
|
2430
|
+
const trimmed = ref.trim();
|
|
2431
|
+
if (trimmed.startsWith("HEAD ->") || trimmed === "HEAD") {
|
|
2432
|
+
const branchName = trimmed.replace("HEAD -> ", "");
|
|
2433
|
+
if (trimmed === "HEAD") {
|
|
2434
|
+
return pc9.bold(pc9.cyan("HEAD"));
|
|
2435
|
+
}
|
|
2436
|
+
return `${pc9.bold(pc9.cyan("HEAD"))} ${pc9.dim("->")} ${colorizeRefName(branchName, protectedBranches, currentBranch)}`;
|
|
2437
|
+
}
|
|
2438
|
+
if (trimmed.startsWith("tag:")) {
|
|
2439
|
+
return pc9.bold(pc9.magenta(trimmed));
|
|
2440
|
+
}
|
|
2441
|
+
return colorizeRefName(trimmed, protectedBranches, currentBranch);
|
|
2442
|
+
}).join(pc9.dim(", "));
|
|
2443
|
+
}
|
|
2444
|
+
function colorizeRefName(name, protectedBranches, currentBranch) {
|
|
2445
|
+
const isRemote = name.includes("/");
|
|
2446
|
+
const localName = isRemote ? name.split("/").slice(1).join("/") : name;
|
|
2447
|
+
if (protectedBranches.includes(localName)) {
|
|
2448
|
+
return isRemote ? pc9.bold(pc9.red(name)) : pc9.bold(pc9.red(name));
|
|
2449
|
+
}
|
|
2450
|
+
if (localName === currentBranch) {
|
|
2451
|
+
return pc9.bold(pc9.green(name));
|
|
2452
|
+
}
|
|
2453
|
+
if (isRemote) {
|
|
2454
|
+
return pc9.blue(name);
|
|
2455
|
+
}
|
|
2456
|
+
return pc9.green(name);
|
|
2457
|
+
}
|
|
2458
|
+
function colorizeSubject(subject) {
|
|
2459
|
+
const emojiMatch = subject.match(/^([\p{Emoji_Presentation}\p{Emoji}\uFE0F]+\s*)/u);
|
|
2460
|
+
if (emojiMatch) {
|
|
2461
|
+
const emoji = emojiMatch[1];
|
|
2462
|
+
const rest = subject.slice(emoji.length);
|
|
2463
|
+
return `${emoji}${pc9.white(rest)}`;
|
|
2464
|
+
}
|
|
2465
|
+
if (subject.startsWith("Merge ")) {
|
|
2466
|
+
return pc9.dim(subject);
|
|
2467
|
+
}
|
|
2468
|
+
return pc9.white(subject);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2101
2471
|
// src/commands/setup.ts
|
|
2102
|
-
import { defineCommand as
|
|
2103
|
-
import
|
|
2104
|
-
var setup_default =
|
|
2472
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
2473
|
+
import pc10 from "picocolors";
|
|
2474
|
+
var setup_default = defineCommand7({
|
|
2105
2475
|
meta: {
|
|
2106
2476
|
name: "setup",
|
|
2107
2477
|
description: "Initialize contribute-now config for this repo (.contributerc.json)"
|
|
@@ -2122,7 +2492,7 @@ var setup_default = defineCommand5({
|
|
|
2122
2492
|
workflow = "github-flow";
|
|
2123
2493
|
else if (workflowChoice.startsWith("Git Flow"))
|
|
2124
2494
|
workflow = "git-flow";
|
|
2125
|
-
info(`Workflow: ${
|
|
2495
|
+
info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[workflow])}`);
|
|
2126
2496
|
const conventionChoice = await selectPrompt("Which commit convention should this project use?", [
|
|
2127
2497
|
`${CONVENTION_DESCRIPTIONS["clean-commit"]} (recommended)`,
|
|
2128
2498
|
CONVENTION_DESCRIPTIONS.conventional,
|
|
@@ -2175,8 +2545,8 @@ var setup_default = defineCommand5({
|
|
|
2175
2545
|
detectedRole = roleChoice;
|
|
2176
2546
|
detectionSource = "user selection";
|
|
2177
2547
|
} else {
|
|
2178
|
-
info(`Detected role: ${
|
|
2179
|
-
const confirmed = await confirmPrompt(`Role detected as ${
|
|
2548
|
+
info(`Detected role: ${pc10.bold(detectedRole)} (via ${detectionSource})`);
|
|
2549
|
+
const confirmed = await confirmPrompt(`Role detected as ${pc10.bold(detectedRole)}. Is this correct?`);
|
|
2180
2550
|
if (!confirmed) {
|
|
2181
2551
|
const roleChoice = await selectPrompt("Select your role:", ["maintainer", "contributor"]);
|
|
2182
2552
|
detectedRole = roleChoice;
|
|
@@ -2221,22 +2591,22 @@ var setup_default = defineCommand5({
|
|
|
2221
2591
|
warn(' echo ".contributerc.json" >> .gitignore');
|
|
2222
2592
|
}
|
|
2223
2593
|
console.log();
|
|
2224
|
-
info(`Workflow: ${
|
|
2225
|
-
info(`Convention: ${
|
|
2226
|
-
info(`Role: ${
|
|
2594
|
+
info(`Workflow: ${pc10.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
2595
|
+
info(`Convention: ${pc10.bold(CONVENTION_DESCRIPTIONS[config.commitConvention])}`);
|
|
2596
|
+
info(`Role: ${pc10.bold(config.role)}`);
|
|
2227
2597
|
if (config.devBranch) {
|
|
2228
|
-
info(`Main: ${
|
|
2598
|
+
info(`Main: ${pc10.bold(config.mainBranch)} | Dev: ${pc10.bold(config.devBranch)}`);
|
|
2229
2599
|
} else {
|
|
2230
|
-
info(`Main: ${
|
|
2600
|
+
info(`Main: ${pc10.bold(config.mainBranch)}`);
|
|
2231
2601
|
}
|
|
2232
|
-
info(`Origin: ${
|
|
2602
|
+
info(`Origin: ${pc10.bold(config.origin)}${config.role === "contributor" ? ` | Upstream: ${pc10.bold(config.upstream)}` : ""}`);
|
|
2233
2603
|
}
|
|
2234
2604
|
});
|
|
2235
2605
|
|
|
2236
2606
|
// src/commands/start.ts
|
|
2237
|
-
import { defineCommand as
|
|
2238
|
-
import
|
|
2239
|
-
var start_default =
|
|
2607
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
2608
|
+
import pc11 from "picocolors";
|
|
2609
|
+
var start_default = defineCommand8({
|
|
2240
2610
|
meta: {
|
|
2241
2611
|
name: "start",
|
|
2242
2612
|
description: "Create a new feature branch from the latest base branch"
|
|
@@ -2283,8 +2653,8 @@ var start_default = defineCommand6({
|
|
|
2283
2653
|
if (suggested) {
|
|
2284
2654
|
spinner.success("Branch name suggestion ready.");
|
|
2285
2655
|
console.log(`
|
|
2286
|
-
${
|
|
2287
|
-
const accepted = await confirmPrompt(`Use ${
|
|
2656
|
+
${pc11.dim("AI suggestion:")} ${pc11.bold(pc11.cyan(suggested))}`);
|
|
2657
|
+
const accepted = await confirmPrompt(`Use ${pc11.bold(suggested)} as your branch name?`);
|
|
2288
2658
|
if (accepted) {
|
|
2289
2659
|
branchName = suggested;
|
|
2290
2660
|
} else {
|
|
@@ -2295,14 +2665,14 @@ var start_default = defineCommand6({
|
|
|
2295
2665
|
}
|
|
2296
2666
|
}
|
|
2297
2667
|
if (!hasPrefix(branchName, branchPrefixes)) {
|
|
2298
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
2668
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc11.bold(branchName)}:`, branchPrefixes);
|
|
2299
2669
|
branchName = formatBranchName(prefix, branchName);
|
|
2300
2670
|
}
|
|
2301
2671
|
if (!isValidBranchName(branchName)) {
|
|
2302
2672
|
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
2303
2673
|
process.exit(1);
|
|
2304
2674
|
}
|
|
2305
|
-
info(`Creating branch: ${
|
|
2675
|
+
info(`Creating branch: ${pc11.bold(branchName)}`);
|
|
2306
2676
|
await fetchRemote(syncSource.remote);
|
|
2307
2677
|
const updateResult = await updateLocalBranch(baseBranch, syncSource.ref);
|
|
2308
2678
|
if (updateResult.exitCode !== 0) {}
|
|
@@ -2311,14 +2681,14 @@ var start_default = defineCommand6({
|
|
|
2311
2681
|
error(`Failed to create branch: ${result.stderr}`);
|
|
2312
2682
|
process.exit(1);
|
|
2313
2683
|
}
|
|
2314
|
-
success(`✅ Created ${
|
|
2684
|
+
success(`✅ Created ${pc11.bold(branchName)} from latest ${pc11.bold(baseBranch)}`);
|
|
2315
2685
|
}
|
|
2316
2686
|
});
|
|
2317
2687
|
|
|
2318
2688
|
// src/commands/status.ts
|
|
2319
|
-
import { defineCommand as
|
|
2320
|
-
import
|
|
2321
|
-
var status_default =
|
|
2689
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
2690
|
+
import pc12 from "picocolors";
|
|
2691
|
+
var status_default = defineCommand9({
|
|
2322
2692
|
meta: {
|
|
2323
2693
|
name: "status",
|
|
2324
2694
|
description: "Show sync status of branches"
|
|
@@ -2334,8 +2704,8 @@ var status_default = defineCommand7({
|
|
|
2334
2704
|
process.exit(1);
|
|
2335
2705
|
}
|
|
2336
2706
|
heading("\uD83D\uDCCA contribute-now status");
|
|
2337
|
-
console.log(` ${
|
|
2338
|
-
console.log(` ${
|
|
2707
|
+
console.log(` ${pc12.dim("Workflow:")} ${pc12.bold(WORKFLOW_DESCRIPTIONS[config.workflow])}`);
|
|
2708
|
+
console.log(` ${pc12.dim("Role:")} ${pc12.bold(config.role)}`);
|
|
2339
2709
|
console.log();
|
|
2340
2710
|
await fetchAll();
|
|
2341
2711
|
const currentBranch = await getCurrentBranch();
|
|
@@ -2344,7 +2714,7 @@ var status_default = defineCommand7({
|
|
|
2344
2714
|
const isContributor = config.role === "contributor";
|
|
2345
2715
|
const [dirty, fileStatus] = await Promise.all([hasUncommittedChanges(), getFileStatus()]);
|
|
2346
2716
|
if (dirty) {
|
|
2347
|
-
console.log(` ${
|
|
2717
|
+
console.log(` ${pc12.yellow("⚠")} ${pc12.yellow("Uncommitted changes in working tree")}`);
|
|
2348
2718
|
console.log();
|
|
2349
2719
|
}
|
|
2350
2720
|
const mainRemote = `${origin}/${mainBranch}`;
|
|
@@ -2360,82 +2730,82 @@ var status_default = defineCommand7({
|
|
|
2360
2730
|
if (currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
2361
2731
|
const branchDiv = await getDivergence(currentBranch, baseBranch);
|
|
2362
2732
|
const branchLine = formatStatus(currentBranch, baseBranch, branchDiv.ahead, branchDiv.behind);
|
|
2363
|
-
console.log(branchLine +
|
|
2733
|
+
console.log(branchLine + pc12.dim(` (current ${pc12.green("*")})`));
|
|
2364
2734
|
} else if (currentBranch) {
|
|
2365
|
-
console.log(
|
|
2735
|
+
console.log(pc12.dim(` (on ${pc12.bold(currentBranch)} branch)`));
|
|
2366
2736
|
}
|
|
2367
2737
|
const hasFiles = fileStatus.staged.length > 0 || fileStatus.modified.length > 0 || fileStatus.untracked.length > 0;
|
|
2368
2738
|
if (hasFiles) {
|
|
2369
2739
|
console.log();
|
|
2370
2740
|
if (fileStatus.staged.length > 0) {
|
|
2371
|
-
console.log(` ${
|
|
2741
|
+
console.log(` ${pc12.green("Staged for commit:")}`);
|
|
2372
2742
|
for (const { file, status } of fileStatus.staged) {
|
|
2373
|
-
console.log(` ${
|
|
2743
|
+
console.log(` ${pc12.green("+")} ${pc12.dim(`${status}:`)} ${file}`);
|
|
2374
2744
|
}
|
|
2375
2745
|
}
|
|
2376
2746
|
if (fileStatus.modified.length > 0) {
|
|
2377
|
-
console.log(` ${
|
|
2747
|
+
console.log(` ${pc12.yellow("Unstaged changes:")}`);
|
|
2378
2748
|
for (const { file, status } of fileStatus.modified) {
|
|
2379
|
-
console.log(` ${
|
|
2749
|
+
console.log(` ${pc12.yellow("~")} ${pc12.dim(`${status}:`)} ${file}`);
|
|
2380
2750
|
}
|
|
2381
2751
|
}
|
|
2382
2752
|
if (fileStatus.untracked.length > 0) {
|
|
2383
|
-
console.log(` ${
|
|
2753
|
+
console.log(` ${pc12.red("Untracked files:")}`);
|
|
2384
2754
|
for (const file of fileStatus.untracked) {
|
|
2385
|
-
console.log(` ${
|
|
2755
|
+
console.log(` ${pc12.red("?")} ${file}`);
|
|
2386
2756
|
}
|
|
2387
2757
|
}
|
|
2388
2758
|
} else if (!dirty) {
|
|
2389
|
-
console.log(` ${
|
|
2759
|
+
console.log(` ${pc12.green("✓")} ${pc12.dim("Working tree clean")}`);
|
|
2390
2760
|
}
|
|
2391
2761
|
const tips = [];
|
|
2392
2762
|
if (fileStatus.staged.length > 0) {
|
|
2393
|
-
tips.push(`Run ${
|
|
2763
|
+
tips.push(`Run ${pc12.bold("contrib commit")} to commit staged changes`);
|
|
2394
2764
|
}
|
|
2395
2765
|
if (fileStatus.modified.length > 0 || fileStatus.untracked.length > 0) {
|
|
2396
|
-
tips.push(`Run ${
|
|
2766
|
+
tips.push(`Run ${pc12.bold("contrib commit")} to stage and commit changes`);
|
|
2397
2767
|
}
|
|
2398
2768
|
if (fileStatus.staged.length === 0 && fileStatus.modified.length === 0 && fileStatus.untracked.length === 0 && currentBranch && currentBranch !== mainBranch && currentBranch !== config.devBranch) {
|
|
2399
2769
|
const branchDiv = await getDivergence(currentBranch, `${origin}/${currentBranch}`);
|
|
2400
2770
|
if (branchDiv.ahead > 0) {
|
|
2401
|
-
tips.push(`Run ${
|
|
2771
|
+
tips.push(`Run ${pc12.bold("contrib submit")} to push and create/update your PR`);
|
|
2402
2772
|
}
|
|
2403
2773
|
}
|
|
2404
2774
|
if (tips.length > 0) {
|
|
2405
2775
|
console.log();
|
|
2406
|
-
console.log(` ${
|
|
2776
|
+
console.log(` ${pc12.dim("\uD83D\uDCA1 Tip:")}`);
|
|
2407
2777
|
for (const tip of tips) {
|
|
2408
|
-
console.log(` ${
|
|
2778
|
+
console.log(` ${pc12.dim(tip)}`);
|
|
2409
2779
|
}
|
|
2410
2780
|
}
|
|
2411
2781
|
console.log();
|
|
2412
2782
|
}
|
|
2413
2783
|
});
|
|
2414
2784
|
function formatStatus(branch, base, ahead, behind) {
|
|
2415
|
-
const label =
|
|
2785
|
+
const label = pc12.bold(branch.padEnd(20));
|
|
2416
2786
|
if (ahead === 0 && behind === 0) {
|
|
2417
|
-
return ` ${
|
|
2787
|
+
return ` ${pc12.green("✓")} ${label} ${pc12.dim(`in sync with ${base}`)}`;
|
|
2418
2788
|
}
|
|
2419
2789
|
if (ahead > 0 && behind === 0) {
|
|
2420
|
-
return ` ${
|
|
2790
|
+
return ` ${pc12.yellow("↑")} ${label} ${pc12.yellow(`${ahead} commit${ahead !== 1 ? "s" : ""} ahead of ${base}`)}`;
|
|
2421
2791
|
}
|
|
2422
2792
|
if (behind > 0 && ahead === 0) {
|
|
2423
|
-
return ` ${
|
|
2793
|
+
return ` ${pc12.red("↓")} ${label} ${pc12.red(`${behind} commit${behind !== 1 ? "s" : ""} behind ${base}`)}`;
|
|
2424
2794
|
}
|
|
2425
|
-
return ` ${
|
|
2795
|
+
return ` ${pc12.red("⚡")} ${label} ${pc12.yellow(`${ahead} ahead`)}${pc12.dim(", ")}${pc12.red(`${behind} behind`)} ${pc12.dim(base)}`;
|
|
2426
2796
|
}
|
|
2427
2797
|
|
|
2428
2798
|
// src/commands/submit.ts
|
|
2429
|
-
import { defineCommand as
|
|
2430
|
-
import
|
|
2799
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
2800
|
+
import pc13 from "picocolors";
|
|
2431
2801
|
async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
2432
|
-
info(`Checking out ${
|
|
2802
|
+
info(`Checking out ${pc13.bold(baseBranch)}...`);
|
|
2433
2803
|
const coResult = await checkoutBranch2(baseBranch);
|
|
2434
2804
|
if (coResult.exitCode !== 0) {
|
|
2435
2805
|
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2436
2806
|
process.exit(1);
|
|
2437
2807
|
}
|
|
2438
|
-
info(`Squash merging ${
|
|
2808
|
+
info(`Squash merging ${pc13.bold(featureBranch)} into ${pc13.bold(baseBranch)}...`);
|
|
2439
2809
|
const mergeResult = await mergeSquash(featureBranch);
|
|
2440
2810
|
if (mergeResult.exitCode !== 0) {
|
|
2441
2811
|
error(`Squash merge failed: ${mergeResult.stderr}`);
|
|
@@ -2465,26 +2835,26 @@ async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
|
2465
2835
|
error(`Commit failed: ${commitResult.stderr}`);
|
|
2466
2836
|
process.exit(1);
|
|
2467
2837
|
}
|
|
2468
|
-
info(`Pushing ${
|
|
2838
|
+
info(`Pushing ${pc13.bold(baseBranch)} to ${origin}...`);
|
|
2469
2839
|
const pushResult = await pushBranch(origin, baseBranch);
|
|
2470
2840
|
if (pushResult.exitCode !== 0) {
|
|
2471
2841
|
error(`Failed to push ${baseBranch}: ${pushResult.stderr}`);
|
|
2472
2842
|
process.exit(1);
|
|
2473
2843
|
}
|
|
2474
|
-
info(`Deleting local branch ${
|
|
2844
|
+
info(`Deleting local branch ${pc13.bold(featureBranch)}...`);
|
|
2475
2845
|
const delLocal = await forceDeleteBranch(featureBranch);
|
|
2476
2846
|
if (delLocal.exitCode !== 0) {
|
|
2477
2847
|
warn(`Could not delete local branch: ${delLocal.stderr.trim()}`);
|
|
2478
2848
|
}
|
|
2479
|
-
info(`Deleting remote branch ${
|
|
2849
|
+
info(`Deleting remote branch ${pc13.bold(featureBranch)}...`);
|
|
2480
2850
|
const delRemote = await deleteRemoteBranch(origin, featureBranch);
|
|
2481
2851
|
if (delRemote.exitCode !== 0) {
|
|
2482
2852
|
warn(`Could not delete remote branch: ${delRemote.stderr.trim()}`);
|
|
2483
2853
|
}
|
|
2484
|
-
success(`✅ Squash merged ${
|
|
2485
|
-
info(`Run ${
|
|
2854
|
+
success(`✅ Squash merged ${pc13.bold(featureBranch)} into ${pc13.bold(baseBranch)} and pushed.`);
|
|
2855
|
+
info(`Run ${pc13.bold("contrib start")} to begin a new feature.`);
|
|
2486
2856
|
}
|
|
2487
|
-
var submit_default =
|
|
2857
|
+
var submit_default = defineCommand10({
|
|
2488
2858
|
meta: {
|
|
2489
2859
|
name: "submit",
|
|
2490
2860
|
description: "Push current branch and create a pull request"
|
|
@@ -2524,8 +2894,73 @@ var submit_default = defineCommand8({
|
|
|
2524
2894
|
process.exit(1);
|
|
2525
2895
|
}
|
|
2526
2896
|
if (protectedBranches.includes(currentBranch)) {
|
|
2527
|
-
|
|
2528
|
-
|
|
2897
|
+
heading("\uD83D\uDE80 contrib submit");
|
|
2898
|
+
warn(`You're on ${pc13.bold(currentBranch)}, which is a protected branch. PRs should come from feature branches.`);
|
|
2899
|
+
await fetchAll();
|
|
2900
|
+
const remoteRef = `${origin}/${currentBranch}`;
|
|
2901
|
+
const localWork = await hasLocalWork(origin, currentBranch);
|
|
2902
|
+
const dirty = await hasUncommittedChanges();
|
|
2903
|
+
const hasCommits = localWork.unpushedCommits > 0;
|
|
2904
|
+
const hasAnything = hasCommits || dirty;
|
|
2905
|
+
if (!hasAnything) {
|
|
2906
|
+
error("No local changes or commits to move. Switch to a feature branch first.");
|
|
2907
|
+
info(` Run ${pc13.bold("contrib start")} to create a new feature branch.`);
|
|
2908
|
+
process.exit(1);
|
|
2909
|
+
}
|
|
2910
|
+
if (hasCommits) {
|
|
2911
|
+
info(`Found ${pc13.bold(String(localWork.unpushedCommits))} unpushed commit${localWork.unpushedCommits !== 1 ? "s" : ""} on ${pc13.bold(currentBranch)}.`);
|
|
2912
|
+
}
|
|
2913
|
+
if (dirty) {
|
|
2914
|
+
info("You also have uncommitted changes in the working tree.");
|
|
2915
|
+
}
|
|
2916
|
+
console.log();
|
|
2917
|
+
const MOVE_BRANCH = "Move my changes to a new feature branch";
|
|
2918
|
+
const CANCEL2 = "Cancel (stay on this branch)";
|
|
2919
|
+
const action = await selectPrompt("Let's get you back on track. What would you like to do?", [MOVE_BRANCH, CANCEL2]);
|
|
2920
|
+
if (action === CANCEL2) {
|
|
2921
|
+
info("No changes made. You are still on your current branch.");
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
info(pc13.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
2925
|
+
const description = await inputPrompt("What are you working on?");
|
|
2926
|
+
let newBranchName = description;
|
|
2927
|
+
if (looksLikeNaturalLanguage(description)) {
|
|
2928
|
+
const copilotError = await checkCopilotAvailable();
|
|
2929
|
+
if (!copilotError) {
|
|
2930
|
+
const spinner = createSpinner("Generating branch name suggestion...");
|
|
2931
|
+
const suggested = await suggestBranchName(description, args.model);
|
|
2932
|
+
if (suggested) {
|
|
2933
|
+
spinner.success("Branch name suggestion ready.");
|
|
2934
|
+
console.log(`
|
|
2935
|
+
${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(suggested))}`);
|
|
2936
|
+
const accepted = await confirmPrompt(`Use ${pc13.bold(suggested)} as your branch name?`);
|
|
2937
|
+
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
2938
|
+
} else {
|
|
2939
|
+
spinner.fail("AI did not return a suggestion.");
|
|
2940
|
+
newBranchName = await inputPrompt("Enter branch name", description);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
2945
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc13.bold(newBranchName)}:`, config.branchPrefixes);
|
|
2946
|
+
newBranchName = formatBranchName(prefix, newBranchName);
|
|
2947
|
+
}
|
|
2948
|
+
if (!isValidBranchName(newBranchName)) {
|
|
2949
|
+
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
2950
|
+
process.exit(1);
|
|
2951
|
+
}
|
|
2952
|
+
const branchResult = await createBranch(newBranchName);
|
|
2953
|
+
if (branchResult.exitCode !== 0) {
|
|
2954
|
+
error(`Failed to create branch: ${branchResult.stderr}`);
|
|
2955
|
+
process.exit(1);
|
|
2956
|
+
}
|
|
2957
|
+
success(`Created ${pc13.bold(newBranchName)} with your changes.`);
|
|
2958
|
+
await updateLocalBranch(currentBranch, remoteRef);
|
|
2959
|
+
info(`Reset ${pc13.bold(currentBranch)} back to ${pc13.bold(remoteRef)} — no damage done.`);
|
|
2960
|
+
console.log();
|
|
2961
|
+
success(`You're now on ${pc13.bold(newBranchName)} with all your work intact.`);
|
|
2962
|
+
info(`Run ${pc13.bold("contrib submit")} again to push and create your PR.`);
|
|
2963
|
+
return;
|
|
2529
2964
|
}
|
|
2530
2965
|
heading("\uD83D\uDE80 contrib submit");
|
|
2531
2966
|
const ghInstalled = await checkGhInstalled();
|
|
@@ -2533,7 +2968,7 @@ var submit_default = defineCommand8({
|
|
|
2533
2968
|
if (ghInstalled && ghAuthed) {
|
|
2534
2969
|
const mergedPR = await getMergedPRForBranch(currentBranch);
|
|
2535
2970
|
if (mergedPR) {
|
|
2536
|
-
warn(`PR #${mergedPR.number} (${
|
|
2971
|
+
warn(`PR #${mergedPR.number} (${pc13.bold(mergedPR.title)}) was already merged.`);
|
|
2537
2972
|
const localWork = await hasLocalWork(origin, currentBranch);
|
|
2538
2973
|
const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
|
|
2539
2974
|
if (hasWork) {
|
|
@@ -2541,7 +2976,7 @@ var submit_default = defineCommand8({
|
|
|
2541
2976
|
warn("You have uncommitted changes in your working tree.");
|
|
2542
2977
|
}
|
|
2543
2978
|
if (localWork.unpushedCommits > 0) {
|
|
2544
|
-
warn(`You have ${
|
|
2979
|
+
warn(`You have ${pc13.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not in the merged PR.`);
|
|
2545
2980
|
}
|
|
2546
2981
|
const SAVE_NEW_BRANCH = "Save changes to a new branch";
|
|
2547
2982
|
const DISCARD = "Discard all changes and clean up";
|
|
@@ -2552,7 +2987,7 @@ var submit_default = defineCommand8({
|
|
|
2552
2987
|
return;
|
|
2553
2988
|
}
|
|
2554
2989
|
if (action === SAVE_NEW_BRANCH) {
|
|
2555
|
-
info(
|
|
2990
|
+
info(pc13.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
2556
2991
|
const description = await inputPrompt("What are you working on?");
|
|
2557
2992
|
let newBranchName = description;
|
|
2558
2993
|
if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
|
|
@@ -2561,8 +2996,8 @@ var submit_default = defineCommand8({
|
|
|
2561
2996
|
if (suggested) {
|
|
2562
2997
|
spinner.success("Branch name suggestion ready.");
|
|
2563
2998
|
console.log(`
|
|
2564
|
-
${
|
|
2565
|
-
const accepted = await confirmPrompt(`Use ${
|
|
2999
|
+
${pc13.dim("AI suggestion:")} ${pc13.bold(pc13.cyan(suggested))}`);
|
|
3000
|
+
const accepted = await confirmPrompt(`Use ${pc13.bold(suggested)} as your branch name?`);
|
|
2566
3001
|
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
2567
3002
|
} else {
|
|
2568
3003
|
spinner.fail("AI did not return a suggestion.");
|
|
@@ -2570,7 +3005,7 @@ var submit_default = defineCommand8({
|
|
|
2570
3005
|
}
|
|
2571
3006
|
}
|
|
2572
3007
|
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
2573
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
3008
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc13.bold(newBranchName)}:`, config.branchPrefixes);
|
|
2574
3009
|
newBranchName = formatBranchName(prefix, newBranchName);
|
|
2575
3010
|
}
|
|
2576
3011
|
if (!isValidBranchName(newBranchName)) {
|
|
@@ -2584,10 +3019,10 @@ var submit_default = defineCommand8({
|
|
|
2584
3019
|
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
2585
3020
|
process.exit(1);
|
|
2586
3021
|
}
|
|
2587
|
-
success(`Renamed ${
|
|
3022
|
+
success(`Renamed ${pc13.bold(currentBranch)} → ${pc13.bold(newBranchName)}`);
|
|
2588
3023
|
await unsetUpstream();
|
|
2589
3024
|
const syncSource2 = getSyncSource(config);
|
|
2590
|
-
info(`Syncing ${
|
|
3025
|
+
info(`Syncing ${pc13.bold(newBranchName)} with latest ${pc13.bold(baseBranch)}...`);
|
|
2591
3026
|
await fetchRemote(syncSource2.remote);
|
|
2592
3027
|
let rebaseResult;
|
|
2593
3028
|
if (staleUpstreamHash) {
|
|
@@ -2598,17 +3033,17 @@ var submit_default = defineCommand8({
|
|
|
2598
3033
|
}
|
|
2599
3034
|
if (rebaseResult.exitCode !== 0) {
|
|
2600
3035
|
warn("Rebase encountered conflicts. Resolve them manually, then run:");
|
|
2601
|
-
info(` ${
|
|
3036
|
+
info(` ${pc13.bold("git rebase --continue")}`);
|
|
2602
3037
|
} else {
|
|
2603
|
-
success(`Rebased ${
|
|
3038
|
+
success(`Rebased ${pc13.bold(newBranchName)} onto ${pc13.bold(syncSource2.ref)}.`);
|
|
2604
3039
|
}
|
|
2605
|
-
info(`All your changes are preserved. Run ${
|
|
3040
|
+
info(`All your changes are preserved. Run ${pc13.bold("contrib submit")} when ready to create a new PR.`);
|
|
2606
3041
|
return;
|
|
2607
3042
|
}
|
|
2608
3043
|
warn("Discarding local changes...");
|
|
2609
3044
|
}
|
|
2610
3045
|
const syncSource = getSyncSource(config);
|
|
2611
|
-
info(`Switching to ${
|
|
3046
|
+
info(`Switching to ${pc13.bold(baseBranch)} and syncing...`);
|
|
2612
3047
|
await fetchRemote(syncSource.remote);
|
|
2613
3048
|
const coResult = await checkoutBranch2(baseBranch);
|
|
2614
3049
|
if (coResult.exitCode !== 0) {
|
|
@@ -2616,16 +3051,16 @@ var submit_default = defineCommand8({
|
|
|
2616
3051
|
process.exit(1);
|
|
2617
3052
|
}
|
|
2618
3053
|
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
2619
|
-
success(`Synced ${
|
|
2620
|
-
info(`Deleting stale branch ${
|
|
3054
|
+
success(`Synced ${pc13.bold(baseBranch)} with ${pc13.bold(syncSource.ref)}.`);
|
|
3055
|
+
info(`Deleting stale branch ${pc13.bold(currentBranch)}...`);
|
|
2621
3056
|
const delResult = await forceDeleteBranch(currentBranch);
|
|
2622
3057
|
if (delResult.exitCode === 0) {
|
|
2623
|
-
success(`Deleted ${
|
|
3058
|
+
success(`Deleted ${pc13.bold(currentBranch)}.`);
|
|
2624
3059
|
} else {
|
|
2625
3060
|
warn(`Could not delete branch: ${delResult.stderr.trim()}`);
|
|
2626
3061
|
}
|
|
2627
3062
|
console.log();
|
|
2628
|
-
info(`You're now on ${
|
|
3063
|
+
info(`You're now on ${pc13.bold(baseBranch)}. Run ${pc13.bold("contrib start")} to begin a new feature.`);
|
|
2629
3064
|
return;
|
|
2630
3065
|
}
|
|
2631
3066
|
}
|
|
@@ -2645,10 +3080,10 @@ var submit_default = defineCommand8({
|
|
|
2645
3080
|
prBody = result.body;
|
|
2646
3081
|
spinner.success("PR description generated.");
|
|
2647
3082
|
console.log(`
|
|
2648
|
-
${
|
|
3083
|
+
${pc13.dim("AI title:")} ${pc13.bold(pc13.cyan(prTitle))}`);
|
|
2649
3084
|
console.log(`
|
|
2650
|
-
${
|
|
2651
|
-
console.log(
|
|
3085
|
+
${pc13.dim("AI body preview:")}`);
|
|
3086
|
+
console.log(pc13.dim(prBody.slice(0, 300) + (prBody.length > 300 ? "..." : "")));
|
|
2652
3087
|
} else {
|
|
2653
3088
|
spinner.fail("AI did not return a PR description.");
|
|
2654
3089
|
}
|
|
@@ -2737,7 +3172,7 @@ ${pc11.dim("AI body preview:")}`);
|
|
|
2737
3172
|
});
|
|
2738
3173
|
return;
|
|
2739
3174
|
}
|
|
2740
|
-
info(`Pushing ${
|
|
3175
|
+
info(`Pushing ${pc13.bold(currentBranch)} to ${origin}...`);
|
|
2741
3176
|
const pushResult = await pushSetUpstream(origin, currentBranch);
|
|
2742
3177
|
if (pushResult.exitCode !== 0) {
|
|
2743
3178
|
error(`Failed to push: ${pushResult.stderr}`);
|
|
@@ -2749,7 +3184,7 @@ ${pc11.dim("AI body preview:")}`);
|
|
|
2749
3184
|
const prUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/compare/${baseBranch}...${currentBranch}?expand=1`;
|
|
2750
3185
|
console.log();
|
|
2751
3186
|
info("Create your PR manually:");
|
|
2752
|
-
console.log(` ${
|
|
3187
|
+
console.log(` ${pc13.cyan(prUrl)}`);
|
|
2753
3188
|
} else {
|
|
2754
3189
|
info("gh CLI not available. Create your PR manually on GitHub.");
|
|
2755
3190
|
}
|
|
@@ -2757,8 +3192,8 @@ ${pc11.dim("AI body preview:")}`);
|
|
|
2757
3192
|
}
|
|
2758
3193
|
const existingPR = await getPRForBranch(currentBranch);
|
|
2759
3194
|
if (existingPR) {
|
|
2760
|
-
success(`Pushed changes to existing PR #${existingPR.number}: ${
|
|
2761
|
-
console.log(` ${
|
|
3195
|
+
success(`Pushed changes to existing PR #${existingPR.number}: ${pc13.bold(existingPR.title)}`);
|
|
3196
|
+
console.log(` ${pc13.cyan(existingPR.url)}`);
|
|
2762
3197
|
return;
|
|
2763
3198
|
}
|
|
2764
3199
|
if (submitAction === "fill") {
|
|
@@ -2789,9 +3224,9 @@ ${pc11.dim("AI body preview:")}`);
|
|
|
2789
3224
|
});
|
|
2790
3225
|
|
|
2791
3226
|
// src/commands/sync.ts
|
|
2792
|
-
import { defineCommand as
|
|
2793
|
-
import
|
|
2794
|
-
var sync_default =
|
|
3227
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
3228
|
+
import pc14 from "picocolors";
|
|
3229
|
+
var sync_default = defineCommand11({
|
|
2795
3230
|
meta: {
|
|
2796
3231
|
name: "sync",
|
|
2797
3232
|
description: "Sync your local branches with the remote"
|
|
@@ -2833,12 +3268,12 @@ var sync_default = defineCommand9({
|
|
|
2833
3268
|
}
|
|
2834
3269
|
const div = await getDivergence(baseBranch, syncSource.ref);
|
|
2835
3270
|
if (div.ahead > 0 || div.behind > 0) {
|
|
2836
|
-
info(`${
|
|
3271
|
+
info(`${pc14.bold(baseBranch)} is ${pc14.yellow(`${div.ahead} ahead`)} and ${pc14.red(`${div.behind} behind`)} ${syncSource.ref}`);
|
|
2837
3272
|
} else {
|
|
2838
|
-
info(`${
|
|
3273
|
+
info(`${pc14.bold(baseBranch)} is already in sync with ${syncSource.ref}`);
|
|
2839
3274
|
}
|
|
2840
3275
|
if (!args.yes) {
|
|
2841
|
-
const ok = await confirmPrompt(`This will pull ${
|
|
3276
|
+
const ok = await confirmPrompt(`This will pull ${pc14.bold(syncSource.ref)} into local ${pc14.bold(baseBranch)}.`);
|
|
2842
3277
|
if (!ok)
|
|
2843
3278
|
process.exit(0);
|
|
2844
3279
|
}
|
|
@@ -2856,7 +3291,7 @@ var sync_default = defineCommand9({
|
|
|
2856
3291
|
if (hasDevBranch(workflow) && role === "maintainer") {
|
|
2857
3292
|
const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
|
|
2858
3293
|
if (mainDiv.behind > 0) {
|
|
2859
|
-
info(`Also syncing ${
|
|
3294
|
+
info(`Also syncing ${pc14.bold(config.mainBranch)}...`);
|
|
2860
3295
|
const mainCoResult = await checkoutBranch2(config.mainBranch);
|
|
2861
3296
|
if (mainCoResult.exitCode === 0) {
|
|
2862
3297
|
const mainPullResult = await pullBranch(origin, config.mainBranch);
|
|
@@ -2872,9 +3307,9 @@ var sync_default = defineCommand9({
|
|
|
2872
3307
|
|
|
2873
3308
|
// src/commands/update.ts
|
|
2874
3309
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2875
|
-
import { defineCommand as
|
|
2876
|
-
import
|
|
2877
|
-
var update_default =
|
|
3310
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
3311
|
+
import pc15 from "picocolors";
|
|
3312
|
+
var update_default = defineCommand12({
|
|
2878
3313
|
meta: {
|
|
2879
3314
|
name: "update",
|
|
2880
3315
|
description: "Rebase current branch onto the latest base branch"
|
|
@@ -2909,7 +3344,7 @@ var update_default = defineCommand10({
|
|
|
2909
3344
|
process.exit(1);
|
|
2910
3345
|
}
|
|
2911
3346
|
if (protectedBranches.includes(currentBranch)) {
|
|
2912
|
-
error(`Use \`contrib sync\` to update ${protectedBranches.map((b) =>
|
|
3347
|
+
error(`Use \`contrib sync\` to update ${protectedBranches.map((b) => pc15.bold(b)).join(" or ")} branches.`);
|
|
2913
3348
|
process.exit(1);
|
|
2914
3349
|
}
|
|
2915
3350
|
if (await hasUncommittedChanges()) {
|
|
@@ -2919,8 +3354,8 @@ var update_default = defineCommand10({
|
|
|
2919
3354
|
heading("\uD83D\uDD03 contrib update");
|
|
2920
3355
|
const mergedPR = await getMergedPRForBranch(currentBranch);
|
|
2921
3356
|
if (mergedPR) {
|
|
2922
|
-
warn(`PR #${mergedPR.number} (${
|
|
2923
|
-
info(`Link: ${
|
|
3357
|
+
warn(`PR #${mergedPR.number} (${pc15.bold(mergedPR.title)}) has already been merged.`);
|
|
3358
|
+
info(`Link: ${pc15.underline(mergedPR.url)}`);
|
|
2924
3359
|
const localWork = await hasLocalWork(syncSource.remote, currentBranch);
|
|
2925
3360
|
const hasWork = localWork.uncommitted || localWork.unpushedCommits > 0;
|
|
2926
3361
|
if (hasWork) {
|
|
@@ -2933,13 +3368,13 @@ var update_default = defineCommand10({
|
|
|
2933
3368
|
const SAVE_NEW_BRANCH = "Save changes to a new branch";
|
|
2934
3369
|
const DISCARD = "Discard all changes and clean up";
|
|
2935
3370
|
const CANCEL = "Cancel";
|
|
2936
|
-
const action = await selectPrompt(`${
|
|
3371
|
+
const action = await selectPrompt(`${pc15.bold(currentBranch)} is stale but has local work. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
|
|
2937
3372
|
if (action === CANCEL) {
|
|
2938
3373
|
info("No changes made. You are still on your current branch.");
|
|
2939
3374
|
return;
|
|
2940
3375
|
}
|
|
2941
3376
|
if (action === SAVE_NEW_BRANCH) {
|
|
2942
|
-
info(
|
|
3377
|
+
info(pc15.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
2943
3378
|
const description = await inputPrompt("What are you working on?");
|
|
2944
3379
|
let newBranchName = description;
|
|
2945
3380
|
if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
|
|
@@ -2948,8 +3383,8 @@ var update_default = defineCommand10({
|
|
|
2948
3383
|
if (suggested) {
|
|
2949
3384
|
spinner.success("Branch name suggestion ready.");
|
|
2950
3385
|
console.log(`
|
|
2951
|
-
${
|
|
2952
|
-
const accepted = await confirmPrompt(`Use ${
|
|
3386
|
+
${pc15.dim("AI suggestion:")} ${pc15.bold(pc15.cyan(suggested))}`);
|
|
3387
|
+
const accepted = await confirmPrompt(`Use ${pc15.bold(suggested)} as your branch name?`);
|
|
2953
3388
|
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
2954
3389
|
} else {
|
|
2955
3390
|
spinner.fail("AI did not return a suggestion.");
|
|
@@ -2957,7 +3392,7 @@ var update_default = defineCommand10({
|
|
|
2957
3392
|
}
|
|
2958
3393
|
}
|
|
2959
3394
|
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
2960
|
-
const prefix = await selectPrompt(`Choose a branch type for ${
|
|
3395
|
+
const prefix = await selectPrompt(`Choose a branch type for ${pc15.bold(newBranchName)}:`, config.branchPrefixes);
|
|
2961
3396
|
newBranchName = formatBranchName(prefix, newBranchName);
|
|
2962
3397
|
}
|
|
2963
3398
|
if (!isValidBranchName(newBranchName)) {
|
|
@@ -2971,7 +3406,7 @@ var update_default = defineCommand10({
|
|
|
2971
3406
|
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
2972
3407
|
process.exit(1);
|
|
2973
3408
|
}
|
|
2974
|
-
success(`Renamed ${
|
|
3409
|
+
success(`Renamed ${pc15.bold(currentBranch)} → ${pc15.bold(newBranchName)}`);
|
|
2975
3410
|
await unsetUpstream();
|
|
2976
3411
|
await fetchRemote(syncSource.remote);
|
|
2977
3412
|
let rebaseResult2;
|
|
@@ -2983,11 +3418,11 @@ var update_default = defineCommand10({
|
|
|
2983
3418
|
}
|
|
2984
3419
|
if (rebaseResult2.exitCode !== 0) {
|
|
2985
3420
|
warn("Rebase encountered conflicts. Resolve them manually, then run:");
|
|
2986
|
-
info(` ${
|
|
3421
|
+
info(` ${pc15.bold("git rebase --continue")}`);
|
|
2987
3422
|
} else {
|
|
2988
|
-
success(`Rebased ${
|
|
3423
|
+
success(`Rebased ${pc15.bold(newBranchName)} onto ${pc15.bold(syncSource.ref)}.`);
|
|
2989
3424
|
}
|
|
2990
|
-
info(`All your changes are preserved. Run ${
|
|
3425
|
+
info(`All your changes are preserved. Run ${pc15.bold("contrib submit")} when ready to create a new PR.`);
|
|
2991
3426
|
return;
|
|
2992
3427
|
}
|
|
2993
3428
|
warn("Discarding local changes...");
|
|
@@ -2999,19 +3434,19 @@ var update_default = defineCommand10({
|
|
|
2999
3434
|
process.exit(1);
|
|
3000
3435
|
}
|
|
3001
3436
|
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
3002
|
-
success(`Synced ${
|
|
3003
|
-
info(`Deleting stale branch ${
|
|
3437
|
+
success(`Synced ${pc15.bold(baseBranch)} with ${pc15.bold(syncSource.ref)}.`);
|
|
3438
|
+
info(`Deleting stale branch ${pc15.bold(currentBranch)}...`);
|
|
3004
3439
|
await forceDeleteBranch(currentBranch);
|
|
3005
|
-
success(`Deleted ${
|
|
3006
|
-
info(`Run ${
|
|
3440
|
+
success(`Deleted ${pc15.bold(currentBranch)}.`);
|
|
3441
|
+
info(`Run ${pc15.bold("contrib start")} to begin a new feature branch.`);
|
|
3007
3442
|
return;
|
|
3008
3443
|
}
|
|
3009
|
-
info(`Updating ${
|
|
3444
|
+
info(`Updating ${pc15.bold(currentBranch)} with latest ${pc15.bold(baseBranch)}...`);
|
|
3010
3445
|
await fetchRemote(syncSource.remote);
|
|
3011
3446
|
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
3012
3447
|
const rebaseStrategy = await determineRebaseStrategy(currentBranch, syncSource.ref);
|
|
3013
3448
|
if (rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase) {
|
|
3014
|
-
info(
|
|
3449
|
+
info(pc15.dim(`Using --onto rebase (branch was based on a different ref)`));
|
|
3015
3450
|
}
|
|
3016
3451
|
const rebaseResult = rebaseStrategy.strategy === "onto" && rebaseStrategy.ontoOldBase ? await rebaseOnto(syncSource.ref, rebaseStrategy.ontoOldBase) : await rebase(syncSource.ref);
|
|
3017
3452
|
if (rebaseResult.exitCode !== 0) {
|
|
@@ -3040,10 +3475,10 @@ ${content.slice(0, 2000)}
|
|
|
3040
3475
|
if (suggestion) {
|
|
3041
3476
|
spinner.success("AI conflict guidance ready.");
|
|
3042
3477
|
console.log(`
|
|
3043
|
-
${
|
|
3044
|
-
console.log(
|
|
3478
|
+
${pc15.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
3479
|
+
console.log(pc15.dim("─".repeat(60)));
|
|
3045
3480
|
console.log(suggestion);
|
|
3046
|
-
console.log(
|
|
3481
|
+
console.log(pc15.dim("─".repeat(60)));
|
|
3047
3482
|
console.log();
|
|
3048
3483
|
} else {
|
|
3049
3484
|
spinner.fail("AI could not analyze the conflicts.");
|
|
@@ -3051,22 +3486,22 @@ ${pc13.bold("\uD83D\uDCA1 AI Conflict Resolution Guidance:")}`);
|
|
|
3051
3486
|
}
|
|
3052
3487
|
}
|
|
3053
3488
|
}
|
|
3054
|
-
console.log(
|
|
3489
|
+
console.log(pc15.bold("To resolve:"));
|
|
3055
3490
|
console.log(` 1. Fix conflicts in the affected files`);
|
|
3056
|
-
console.log(` 2. ${
|
|
3057
|
-
console.log(` 3. ${
|
|
3491
|
+
console.log(` 2. ${pc15.cyan("git add <resolved-files>")}`);
|
|
3492
|
+
console.log(` 3. ${pc15.cyan("git rebase --continue")}`);
|
|
3058
3493
|
console.log();
|
|
3059
|
-
console.log(` Or abort: ${
|
|
3494
|
+
console.log(` Or abort: ${pc15.cyan("git rebase --abort")}`);
|
|
3060
3495
|
process.exit(1);
|
|
3061
3496
|
}
|
|
3062
|
-
success(`✅ ${
|
|
3497
|
+
success(`✅ ${pc15.bold(currentBranch)} has been rebased onto latest ${pc15.bold(baseBranch)}`);
|
|
3063
3498
|
}
|
|
3064
3499
|
});
|
|
3065
3500
|
|
|
3066
3501
|
// src/commands/validate.ts
|
|
3067
|
-
import { defineCommand as
|
|
3068
|
-
import
|
|
3069
|
-
var validate_default =
|
|
3502
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
3503
|
+
import pc16 from "picocolors";
|
|
3504
|
+
var validate_default = defineCommand13({
|
|
3070
3505
|
meta: {
|
|
3071
3506
|
name: "validate",
|
|
3072
3507
|
description: "Validate a commit message against the configured convention"
|
|
@@ -3096,7 +3531,7 @@ var validate_default = defineCommand11({
|
|
|
3096
3531
|
}
|
|
3097
3532
|
const errors = getValidationError(convention);
|
|
3098
3533
|
for (const line of errors) {
|
|
3099
|
-
console.error(
|
|
3534
|
+
console.error(pc16.red(` ✗ ${line}`));
|
|
3100
3535
|
}
|
|
3101
3536
|
process.exit(1);
|
|
3102
3537
|
}
|
|
@@ -3104,13 +3539,19 @@ var validate_default = defineCommand11({
|
|
|
3104
3539
|
|
|
3105
3540
|
// src/ui/banner.ts
|
|
3106
3541
|
import figlet from "figlet";
|
|
3107
|
-
import
|
|
3108
|
-
var
|
|
3542
|
+
import pc17 from "picocolors";
|
|
3543
|
+
var LOGO_BIG;
|
|
3109
3544
|
try {
|
|
3110
|
-
|
|
3545
|
+
LOGO_BIG = figlet.textSync(`Contribute
|
|
3111
3546
|
Now`, { font: "ANSI Shadow" });
|
|
3112
3547
|
} catch {
|
|
3113
|
-
|
|
3548
|
+
LOGO_BIG = "Contribute Now";
|
|
3549
|
+
}
|
|
3550
|
+
var LOGO_SMALL;
|
|
3551
|
+
try {
|
|
3552
|
+
LOGO_SMALL = figlet.textSync("Contribute Now", { font: "Slant" });
|
|
3553
|
+
} catch {
|
|
3554
|
+
LOGO_SMALL = "Contribute Now";
|
|
3114
3555
|
}
|
|
3115
3556
|
function getVersion() {
|
|
3116
3557
|
return package_default.version ?? "unknown";
|
|
@@ -3118,23 +3559,30 @@ function getVersion() {
|
|
|
3118
3559
|
function getAuthor() {
|
|
3119
3560
|
return typeof package_default.author === "string" ? package_default.author : "unknown";
|
|
3120
3561
|
}
|
|
3121
|
-
function showBanner(
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3562
|
+
function showBanner(variant = "small") {
|
|
3563
|
+
const logo = variant === "big" ? LOGO_BIG : LOGO_SMALL;
|
|
3564
|
+
console.log(pc17.cyan(`
|
|
3565
|
+
${logo}`));
|
|
3566
|
+
console.log(` ${pc17.dim(`v${getVersion()}`)} ${pc17.dim("—")} ${pc17.dim(`Built by ${getAuthor()}`)}`);
|
|
3567
|
+
if (variant === "big") {
|
|
3126
3568
|
console.log();
|
|
3127
|
-
console.log(` ${
|
|
3128
|
-
console.log(` ${
|
|
3129
|
-
console.log(` ${
|
|
3569
|
+
console.log(` ${pc17.yellow("Star")} ${pc17.cyan("https://github.com/warengonzaga/contribute-now")}`);
|
|
3570
|
+
console.log(` ${pc17.green("Contribute")} ${pc17.cyan("https://github.com/warengonzaga/contribute-now/blob/main/CONTRIBUTING.md")}`);
|
|
3571
|
+
console.log(` ${pc17.magenta("Sponsor")} ${pc17.cyan("https://warengonzaga.com/sponsor")}`);
|
|
3130
3572
|
}
|
|
3131
3573
|
console.log();
|
|
3132
3574
|
}
|
|
3133
3575
|
|
|
3134
3576
|
// src/index.ts
|
|
3135
|
-
var
|
|
3136
|
-
|
|
3137
|
-
|
|
3577
|
+
var isVersion = process.argv.includes("--version") || process.argv.includes("-v");
|
|
3578
|
+
if (!isVersion) {
|
|
3579
|
+
const subCommands = ["setup", "sync", "start", "commit", "update", "submit", "clean", "status", "log", "branch", "hook", "validate", "doctor"];
|
|
3580
|
+
const isHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
3581
|
+
const hasSubCommand = subCommands.some((cmd) => process.argv.includes(cmd));
|
|
3582
|
+
const useBigBanner = isHelp || !hasSubCommand;
|
|
3583
|
+
showBanner(useBigBanner ? "big" : "small");
|
|
3584
|
+
}
|
|
3585
|
+
var main = defineCommand14({
|
|
3138
3586
|
meta: {
|
|
3139
3587
|
name: "contrib",
|
|
3140
3588
|
version: getVersion(),
|
|
@@ -3154,8 +3602,10 @@ var main = defineCommand12({
|
|
|
3154
3602
|
commit: commit_default,
|
|
3155
3603
|
update: update_default,
|
|
3156
3604
|
submit: submit_default,
|
|
3605
|
+
branch: branch_default,
|
|
3157
3606
|
clean: clean_default,
|
|
3158
3607
|
status: status_default,
|
|
3608
|
+
log: log_default,
|
|
3159
3609
|
hook: hook_default,
|
|
3160
3610
|
validate: validate_default,
|
|
3161
3611
|
doctor: doctor_default
|