contribute-now 0.2.0-dev.33be40f → 0.2.0-dev.7c81c96
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 +706 -771
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,23 +5,7 @@ import { defineCommand as defineCommand11, runMain } from "citty";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/clean.ts
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
// src/utils/branch.ts
|
|
11
|
-
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
12
|
-
function hasPrefix(branchName, prefixes = DEFAULT_PREFIXES) {
|
|
13
|
-
return prefixes.some((p) => branchName.startsWith(`${p}/`));
|
|
14
|
-
}
|
|
15
|
-
function formatBranchName(prefix, name) {
|
|
16
|
-
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
17
|
-
return `${prefix}/${sanitized}`;
|
|
18
|
-
}
|
|
19
|
-
function isValidBranchName(name) {
|
|
20
|
-
return /^[a-zA-Z0-9._/-]+$/.test(name) && !name.startsWith("/") && !name.endsWith("/");
|
|
21
|
-
}
|
|
22
|
-
function looksLikeNaturalLanguage(input) {
|
|
23
|
-
return input.includes(" ") && !input.includes("/");
|
|
24
|
-
}
|
|
8
|
+
import pc3 from "picocolors";
|
|
25
9
|
|
|
26
10
|
// src/utils/config.ts
|
|
27
11
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -116,649 +100,201 @@ async function multiSelectPrompt(message, choices) {
|
|
|
116
100
|
return result;
|
|
117
101
|
}
|
|
118
102
|
|
|
119
|
-
// src/utils/
|
|
120
|
-
import {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert` : `Use Clean Commit format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
134
|
-
Emoji/type table:
|
|
135
|
-
\uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release`;
|
|
136
|
-
return `You are a smart commit grouping assistant. Given a list of changed files and their diffs, group related changes into logical atomic commits.
|
|
137
|
-
|
|
138
|
-
${conventionBlock}
|
|
139
|
-
|
|
140
|
-
Return a JSON array of commit groups with this EXACT structure (no markdown fences, no explanation):
|
|
141
|
-
[
|
|
142
|
-
{
|
|
143
|
-
"files": ["path/to/file1.ts", "path/to/file2.ts"],
|
|
144
|
-
"message": "<commit message following the convention above>"
|
|
145
|
-
}
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
Rules:
|
|
149
|
-
- Group files that are logically related (e.g. a utility and its tests, a feature and its types)
|
|
150
|
-
- Each group should represent ONE logical change
|
|
151
|
-
- Every file must appear in exactly one group
|
|
152
|
-
- Commit messages must follow the convention, be concise, imperative, max 72 chars
|
|
153
|
-
- Order groups so foundational changes come first (types, utils) and consumers come after
|
|
154
|
-
- Return ONLY the JSON array, nothing else`;
|
|
103
|
+
// src/utils/git.ts
|
|
104
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
105
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
106
|
+
import { join as join2 } from "node:path";
|
|
107
|
+
function run(args) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
execFileCb("git", args, (error, stdout, stderr) => {
|
|
110
|
+
resolve({
|
|
111
|
+
exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
|
|
112
|
+
stdout: stdout ?? "",
|
|
113
|
+
stderr: stderr ?? ""
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
155
117
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Rules: lowercase, kebab-case, 2-5 words after the prefix, no punctuation.
|
|
160
|
-
CRITICAL: Output ONLY the branch name on a single line. No explanation. No markdown. No questions. No other text.
|
|
161
|
-
Examples: fix/login-timeout | feature/user-profile-page | docs/update-readme | chore/update-pr-title`;
|
|
162
|
-
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..."}
|
|
163
|
-
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.`;
|
|
164
|
-
function getPRDescriptionSystemPrompt(convention) {
|
|
165
|
-
if (convention === "clean-commit") {
|
|
166
|
-
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
167
|
-
CRITICAL: The PR title MUST follow the Clean Commit format exactly: <emoji> <type>: <description>
|
|
168
|
-
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
|
|
169
|
-
Title examples: \uD83D\uDCE6 new: add user authentication | \uD83D\uDD27 update: improve error handling | \uD83D\uDDD1️ remove: drop legacy API
|
|
170
|
-
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.`;
|
|
171
|
-
}
|
|
172
|
-
if (convention === "conventional") {
|
|
173
|
-
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
174
|
-
CRITICAL: The PR title MUST follow Conventional Commits format: <type>[(<scope>)]: <description>
|
|
175
|
-
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
176
|
-
Title examples: feat: add user authentication | fix(auth): resolve token expiry | docs: update contributing guide
|
|
177
|
-
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.`;
|
|
178
|
-
}
|
|
179
|
-
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
180
|
-
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.`;
|
|
118
|
+
async function isGitRepo() {
|
|
119
|
+
const { exitCode } = await run(["rev-parse", "--is-inside-work-tree"]);
|
|
120
|
+
return exitCode === 0;
|
|
181
121
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
122
|
+
async function getCurrentBranch() {
|
|
123
|
+
const { exitCode, stdout } = await run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
124
|
+
if (exitCode !== 0)
|
|
125
|
+
return null;
|
|
126
|
+
return stdout.trim() || null;
|
|
185
127
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}, (err) => {
|
|
193
|
-
clearTimeout(timer);
|
|
194
|
-
reject(err);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
128
|
+
async function getRemotes() {
|
|
129
|
+
const { exitCode, stdout } = await run(["remote"]);
|
|
130
|
+
if (exitCode !== 0)
|
|
131
|
+
return [];
|
|
132
|
+
return stdout.trim().split(`
|
|
133
|
+
`).map((r) => r.trim()).filter(Boolean);
|
|
197
134
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
const client = await getManagedClient();
|
|
203
|
-
try {
|
|
204
|
-
await client.ping();
|
|
205
|
-
} catch (err) {
|
|
206
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
207
|
-
if (msg.includes("auth") || msg.includes("token") || msg.includes("401") || msg.includes("403")) {
|
|
208
|
-
return "Copilot authentication failed. Run `gh auth login` to refresh your token.";
|
|
209
|
-
}
|
|
210
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("timeout") || msg.includes("network")) {
|
|
211
|
-
return "Could not reach GitHub Copilot service. Check your internet connection.";
|
|
212
|
-
}
|
|
213
|
-
return `Copilot health check failed: ${msg}`;
|
|
214
|
-
}
|
|
135
|
+
async function getRemoteUrl(remote) {
|
|
136
|
+
const { exitCode, stdout } = await run(["remote", "get-url", remote]);
|
|
137
|
+
if (exitCode !== 0)
|
|
215
138
|
return null;
|
|
216
|
-
|
|
217
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
218
|
-
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
219
|
-
return "Copilot CLI binary not found. Ensure GitHub Copilot is installed and your gh CLI is up to date.";
|
|
220
|
-
}
|
|
221
|
-
return `Failed to start Copilot service: ${msg}`;
|
|
222
|
-
}
|
|
139
|
+
return stdout.trim() || null;
|
|
223
140
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
_managedClient = new CopilotClient;
|
|
230
|
-
await _managedClient.start();
|
|
231
|
-
_clientStarted = true;
|
|
232
|
-
const cleanup = () => {
|
|
233
|
-
if (_managedClient && _clientStarted) {
|
|
234
|
-
try {
|
|
235
|
-
_managedClient.stop();
|
|
236
|
-
} catch {}
|
|
237
|
-
_clientStarted = false;
|
|
238
|
-
_managedClient = null;
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
process.once("exit", cleanup);
|
|
242
|
-
process.once("SIGINT", cleanup);
|
|
243
|
-
process.once("SIGTERM", cleanup);
|
|
244
|
-
}
|
|
245
|
-
return _managedClient;
|
|
141
|
+
async function hasUncommittedChanges() {
|
|
142
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
143
|
+
if (exitCode !== 0)
|
|
144
|
+
return false;
|
|
145
|
+
return stdout.trim().length > 0;
|
|
246
146
|
}
|
|
247
|
-
async function
|
|
248
|
-
|
|
249
|
-
const sessionConfig = {
|
|
250
|
-
systemMessage: { mode: "replace", content: systemMessage }
|
|
251
|
-
};
|
|
252
|
-
if (model)
|
|
253
|
-
sessionConfig.model = model;
|
|
254
|
-
const session = await client.createSession(sessionConfig);
|
|
255
|
-
try {
|
|
256
|
-
const response = await withTimeout(session.sendAndWait({ prompt: userMessage }), timeoutMs);
|
|
257
|
-
if (!response?.data?.content)
|
|
258
|
-
return null;
|
|
259
|
-
return response.data.content;
|
|
260
|
-
} finally {
|
|
261
|
-
await session.destroy();
|
|
262
|
-
}
|
|
147
|
+
async function fetchRemote(remote) {
|
|
148
|
+
return run(["fetch", remote]);
|
|
263
149
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
267
|
-
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
150
|
+
async function fetchAll() {
|
|
151
|
+
return run(["fetch", "--all", "--quiet"]);
|
|
268
152
|
}
|
|
269
|
-
function
|
|
270
|
-
|
|
271
|
-
if (text2.startsWith("[") || text2.startsWith("{"))
|
|
272
|
-
return text2;
|
|
273
|
-
const arrayStart = text2.indexOf("[");
|
|
274
|
-
const objStart = text2.indexOf("{");
|
|
275
|
-
let start;
|
|
276
|
-
let closeChar;
|
|
277
|
-
if (arrayStart === -1 && objStart === -1)
|
|
278
|
-
return text2;
|
|
279
|
-
if (arrayStart === -1) {
|
|
280
|
-
start = objStart;
|
|
281
|
-
closeChar = "}";
|
|
282
|
-
} else if (objStart === -1) {
|
|
283
|
-
start = arrayStart;
|
|
284
|
-
closeChar = "]";
|
|
285
|
-
} else if (arrayStart < objStart) {
|
|
286
|
-
start = arrayStart;
|
|
287
|
-
closeChar = "]";
|
|
288
|
-
} else {
|
|
289
|
-
start = objStart;
|
|
290
|
-
closeChar = "}";
|
|
291
|
-
}
|
|
292
|
-
const end = text2.lastIndexOf(closeChar);
|
|
293
|
-
if (end > start) {
|
|
294
|
-
text2 = text2.slice(start, end + 1);
|
|
295
|
-
}
|
|
296
|
-
return text2;
|
|
153
|
+
async function checkoutBranch(branch) {
|
|
154
|
+
return run(["checkout", branch]);
|
|
297
155
|
}
|
|
298
|
-
async function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
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.` : "";
|
|
303
|
-
const userMessage = `Generate a commit message for these staged changes:
|
|
304
|
-
|
|
305
|
-
Files: ${stagedFiles.join(", ")}
|
|
306
|
-
|
|
307
|
-
Diff:
|
|
308
|
-
${diff.slice(0, 4000)}${multiFileHint}`;
|
|
309
|
-
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
310
|
-
return result?.trim() ?? null;
|
|
311
|
-
} catch {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
156
|
+
async function createBranch(branch, from) {
|
|
157
|
+
const args = from ? ["checkout", "-b", branch, from] : ["checkout", "-b", branch];
|
|
158
|
+
return run(args);
|
|
314
159
|
}
|
|
315
|
-
async function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
Diff (truncated):
|
|
324
|
-
${diff.slice(0, 4000)}`;
|
|
325
|
-
const result = await callCopilot(getPRDescriptionSystemPrompt(convention), userMessage, model);
|
|
326
|
-
if (!result)
|
|
327
|
-
return null;
|
|
328
|
-
const cleaned = extractJson(result);
|
|
329
|
-
return JSON.parse(cleaned);
|
|
330
|
-
} catch {
|
|
331
|
-
return null;
|
|
160
|
+
async function resetHard(ref) {
|
|
161
|
+
return run(["reset", "--hard", ref]);
|
|
162
|
+
}
|
|
163
|
+
async function updateLocalBranch(branch, target) {
|
|
164
|
+
const current = await getCurrentBranch();
|
|
165
|
+
if (current === branch) {
|
|
166
|
+
return resetHard(target);
|
|
332
167
|
}
|
|
168
|
+
return run(["branch", "-f", branch, target]);
|
|
333
169
|
}
|
|
334
|
-
async function
|
|
335
|
-
|
|
336
|
-
const result = await callCopilot(BRANCH_NAME_SYSTEM_PROMPT, description, model);
|
|
337
|
-
const trimmed = result?.trim() ?? null;
|
|
338
|
-
if (trimmed && /^[a-z]+\/[a-z0-9-]+$/.test(trimmed)) {
|
|
339
|
-
return trimmed;
|
|
340
|
-
}
|
|
341
|
-
return null;
|
|
342
|
-
} catch {
|
|
343
|
-
return null;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
async function suggestConflictResolution(conflictDiff, model) {
|
|
347
|
-
try {
|
|
348
|
-
const userMessage = `Help me resolve this merge conflict:
|
|
349
|
-
|
|
350
|
-
${conflictDiff.slice(0, 4000)}`;
|
|
351
|
-
const result = await callCopilot(CONFLICT_RESOLUTION_SYSTEM_PROMPT, userMessage, model);
|
|
352
|
-
return result?.trim() ?? null;
|
|
353
|
-
} catch {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
358
|
-
const userMessage = `Group these changed files into logical atomic commits:
|
|
359
|
-
|
|
360
|
-
Files:
|
|
361
|
-
${files.join(`
|
|
362
|
-
`)}
|
|
363
|
-
|
|
364
|
-
Diffs (truncated):
|
|
365
|
-
${diffs.slice(0, 6000)}`;
|
|
366
|
-
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
367
|
-
if (!result) {
|
|
368
|
-
throw new Error("AI returned an empty response");
|
|
369
|
-
}
|
|
370
|
-
const cleaned = extractJson(result);
|
|
371
|
-
let parsed;
|
|
372
|
-
try {
|
|
373
|
-
parsed = JSON.parse(cleaned);
|
|
374
|
-
} catch {
|
|
375
|
-
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
376
|
-
}
|
|
377
|
-
const groups = parsed;
|
|
378
|
-
if (!Array.isArray(groups) || groups.length === 0) {
|
|
379
|
-
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
380
|
-
}
|
|
381
|
-
for (const group of groups) {
|
|
382
|
-
if (!Array.isArray(group.files) || typeof group.message !== "string") {
|
|
383
|
-
throw new Error("AI returned groups with invalid structure (missing files or message)");
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return groups;
|
|
170
|
+
async function pushSetUpstream(remote, branch) {
|
|
171
|
+
return run(["push", "-u", remote, branch]);
|
|
387
172
|
}
|
|
388
|
-
async function
|
|
389
|
-
|
|
390
|
-
`);
|
|
391
|
-
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
392
|
-
|
|
393
|
-
Groups:
|
|
394
|
-
${groupSummary}
|
|
395
|
-
|
|
396
|
-
Diffs (truncated):
|
|
397
|
-
${diffs.slice(0, 6000)}`;
|
|
398
|
-
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
399
|
-
if (!result)
|
|
400
|
-
return groups;
|
|
401
|
-
try {
|
|
402
|
-
const cleaned = extractJson(result);
|
|
403
|
-
const parsed = JSON.parse(cleaned);
|
|
404
|
-
if (!Array.isArray(parsed) || parsed.length !== groups.length)
|
|
405
|
-
return groups;
|
|
406
|
-
return groups.map((g, i) => ({
|
|
407
|
-
files: g.files,
|
|
408
|
-
message: typeof parsed[i]?.message === "string" ? parsed[i].message : g.message
|
|
409
|
-
}));
|
|
410
|
-
} catch {
|
|
411
|
-
return groups;
|
|
412
|
-
}
|
|
173
|
+
async function rebase(branch) {
|
|
174
|
+
return run(["rebase", branch]);
|
|
413
175
|
}
|
|
414
|
-
async function
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
Files: ${files.join(", ")}
|
|
419
|
-
|
|
420
|
-
Diff:
|
|
421
|
-
${diffs.slice(0, 4000)}`;
|
|
422
|
-
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
423
|
-
return result?.trim() ?? null;
|
|
424
|
-
} catch {
|
|
176
|
+
async function getUpstreamRef() {
|
|
177
|
+
const { exitCode, stdout } = await run(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
178
|
+
if (exitCode !== 0)
|
|
425
179
|
return null;
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/utils/gh.ts
|
|
430
|
-
import { execFile as execFileCb } from "node:child_process";
|
|
431
|
-
function run(args) {
|
|
432
|
-
return new Promise((resolve) => {
|
|
433
|
-
execFileCb("gh", args, (error, stdout, stderr) => {
|
|
434
|
-
resolve({
|
|
435
|
-
exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
|
|
436
|
-
stdout: stdout ?? "",
|
|
437
|
-
stderr: stderr ?? ""
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
});
|
|
180
|
+
return stdout.trim() || null;
|
|
441
181
|
}
|
|
442
|
-
async function
|
|
443
|
-
|
|
444
|
-
const { exitCode } = await run(["--version"]);
|
|
445
|
-
return exitCode === 0;
|
|
446
|
-
} catch {
|
|
447
|
-
return false;
|
|
448
|
-
}
|
|
182
|
+
async function rebaseOnto(newBase, oldBase) {
|
|
183
|
+
return run(["rebase", "--onto", newBase, oldBase]);
|
|
449
184
|
}
|
|
450
|
-
async function
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return exitCode === 0;
|
|
454
|
-
} catch {
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
185
|
+
async function getStagedDiff() {
|
|
186
|
+
const { stdout } = await run(["diff", "--cached"]);
|
|
187
|
+
return stdout;
|
|
457
188
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
|
|
461
|
-
return null;
|
|
462
|
-
const { exitCode, stdout } = await run(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
189
|
+
async function getStagedFiles() {
|
|
190
|
+
const { exitCode, stdout } = await run(["diff", "--cached", "--name-only"]);
|
|
463
191
|
if (exitCode !== 0)
|
|
464
|
-
return
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
} catch {
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
192
|
+
return [];
|
|
193
|
+
return stdout.trim().split(`
|
|
194
|
+
`).filter(Boolean);
|
|
470
195
|
}
|
|
471
|
-
async function
|
|
472
|
-
const { exitCode, stdout } = await run(["
|
|
196
|
+
async function getChangedFiles() {
|
|
197
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
473
198
|
if (exitCode !== 0)
|
|
474
|
-
return
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
199
|
+
return [];
|
|
200
|
+
return stdout.trimEnd().split(`
|
|
201
|
+
`).filter(Boolean).map((l) => {
|
|
202
|
+
const line = l.replace(/\r$/, "");
|
|
203
|
+
const match = line.match(/^..\s+(.*)/);
|
|
204
|
+
if (!match)
|
|
205
|
+
return "";
|
|
206
|
+
const file = match[1];
|
|
207
|
+
const renameIdx = file.indexOf(" -> ");
|
|
208
|
+
return renameIdx !== -1 ? file.slice(renameIdx + 4) : file;
|
|
209
|
+
}).filter(Boolean);
|
|
481
210
|
}
|
|
482
|
-
async function
|
|
211
|
+
async function getDivergence(branch, base) {
|
|
483
212
|
const { exitCode, stdout } = await run([
|
|
484
|
-
"
|
|
485
|
-
"
|
|
486
|
-
"--
|
|
487
|
-
|
|
488
|
-
"-q",
|
|
489
|
-
".nameWithOwner"
|
|
213
|
+
"rev-list",
|
|
214
|
+
"--left-right",
|
|
215
|
+
"--count",
|
|
216
|
+
`${base}...${branch}`
|
|
490
217
|
]);
|
|
491
218
|
if (exitCode !== 0)
|
|
492
|
-
return
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return null;
|
|
499
|
-
return { owner, repo };
|
|
500
|
-
}
|
|
501
|
-
async function createPR(options) {
|
|
502
|
-
const args = [
|
|
503
|
-
"pr",
|
|
504
|
-
"create",
|
|
505
|
-
"--base",
|
|
506
|
-
options.base,
|
|
507
|
-
"--title",
|
|
508
|
-
options.title,
|
|
509
|
-
"--body",
|
|
510
|
-
options.body
|
|
511
|
-
];
|
|
512
|
-
if (options.draft)
|
|
513
|
-
args.push("--draft");
|
|
514
|
-
return run(args);
|
|
515
|
-
}
|
|
516
|
-
async function createPRFill(base, draft) {
|
|
517
|
-
const args = ["pr", "create", "--base", base, "--fill"];
|
|
518
|
-
if (draft)
|
|
519
|
-
args.push("--draft");
|
|
520
|
-
return run(args);
|
|
219
|
+
return { ahead: 0, behind: 0 };
|
|
220
|
+
const parts = stdout.trim().split(/\s+/);
|
|
221
|
+
return {
|
|
222
|
+
behind: Number.parseInt(parts[0] ?? "0", 10),
|
|
223
|
+
ahead: Number.parseInt(parts[1] ?? "0", 10)
|
|
224
|
+
};
|
|
521
225
|
}
|
|
522
|
-
async function
|
|
523
|
-
const { exitCode, stdout } = await run([
|
|
524
|
-
"pr",
|
|
525
|
-
"list",
|
|
526
|
-
"--head",
|
|
527
|
-
headBranch,
|
|
528
|
-
"--state",
|
|
529
|
-
"open",
|
|
530
|
-
"--json",
|
|
531
|
-
"number,url,title,state",
|
|
532
|
-
"--limit",
|
|
533
|
-
"1"
|
|
534
|
-
]);
|
|
226
|
+
async function getMergedBranches(base) {
|
|
227
|
+
const { exitCode, stdout } = await run(["branch", "--merged", base]);
|
|
535
228
|
if (exitCode !== 0)
|
|
536
|
-
return
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return prs.length > 0 ? prs[0] : null;
|
|
540
|
-
} catch {
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
229
|
+
return [];
|
|
230
|
+
return stdout.trim().split(`
|
|
231
|
+
`).map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean);
|
|
543
232
|
}
|
|
544
|
-
async function
|
|
545
|
-
const { exitCode, stdout } = await run([
|
|
546
|
-
"pr",
|
|
547
|
-
"list",
|
|
548
|
-
"--head",
|
|
549
|
-
headBranch,
|
|
550
|
-
"--state",
|
|
551
|
-
"merged",
|
|
552
|
-
"--json",
|
|
553
|
-
"number,url,title,state",
|
|
554
|
-
"--limit",
|
|
555
|
-
"1"
|
|
556
|
-
]);
|
|
233
|
+
async function getGoneBranches() {
|
|
234
|
+
const { exitCode, stdout } = await run(["branch", "-vv"]);
|
|
557
235
|
if (exitCode !== 0)
|
|
558
|
-
return
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
return prs.length > 0 ? prs[0] : null;
|
|
562
|
-
} catch {
|
|
563
|
-
return null;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// src/utils/git.ts
|
|
568
|
-
import { execFile as execFileCb2 } from "node:child_process";
|
|
569
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
570
|
-
import { join as join2 } from "node:path";
|
|
571
|
-
function run2(args) {
|
|
572
|
-
return new Promise((resolve) => {
|
|
573
|
-
execFileCb2("git", args, (error, stdout, stderr) => {
|
|
574
|
-
resolve({
|
|
575
|
-
exitCode: error ? error.code === "ENOENT" ? 127 : error.status ?? 1 : 0,
|
|
576
|
-
stdout: stdout ?? "",
|
|
577
|
-
stderr: stderr ?? ""
|
|
578
|
-
});
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
async function isGitRepo() {
|
|
583
|
-
const { exitCode } = await run2(["rev-parse", "--is-inside-work-tree"]);
|
|
584
|
-
return exitCode === 0;
|
|
585
|
-
}
|
|
586
|
-
async function getCurrentBranch() {
|
|
587
|
-
const { exitCode, stdout } = await run2(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
588
|
-
if (exitCode !== 0)
|
|
589
|
-
return null;
|
|
590
|
-
return stdout.trim() || null;
|
|
591
|
-
}
|
|
592
|
-
async function getRemotes() {
|
|
593
|
-
const { exitCode, stdout } = await run2(["remote"]);
|
|
594
|
-
if (exitCode !== 0)
|
|
595
|
-
return [];
|
|
596
|
-
return stdout.trim().split(`
|
|
597
|
-
`).map((r) => r.trim()).filter(Boolean);
|
|
598
|
-
}
|
|
599
|
-
async function getRemoteUrl(remote) {
|
|
600
|
-
const { exitCode, stdout } = await run2(["remote", "get-url", remote]);
|
|
601
|
-
if (exitCode !== 0)
|
|
602
|
-
return null;
|
|
603
|
-
return stdout.trim() || null;
|
|
604
|
-
}
|
|
605
|
-
async function hasUncommittedChanges() {
|
|
606
|
-
const { exitCode, stdout } = await run2(["status", "--porcelain"]);
|
|
607
|
-
if (exitCode !== 0)
|
|
608
|
-
return false;
|
|
609
|
-
return stdout.trim().length > 0;
|
|
610
|
-
}
|
|
611
|
-
async function fetchRemote(remote) {
|
|
612
|
-
return run2(["fetch", remote]);
|
|
613
|
-
}
|
|
614
|
-
async function fetchAll() {
|
|
615
|
-
return run2(["fetch", "--all", "--quiet"]);
|
|
616
|
-
}
|
|
617
|
-
async function checkoutBranch2(branch) {
|
|
618
|
-
return run2(["checkout", branch]);
|
|
619
|
-
}
|
|
620
|
-
async function createBranch(branch, from) {
|
|
621
|
-
const args = from ? ["checkout", "-b", branch, from] : ["checkout", "-b", branch];
|
|
622
|
-
return run2(args);
|
|
623
|
-
}
|
|
624
|
-
async function resetHard(ref) {
|
|
625
|
-
return run2(["reset", "--hard", ref]);
|
|
626
|
-
}
|
|
627
|
-
async function updateLocalBranch(branch, target) {
|
|
628
|
-
const current = await getCurrentBranch();
|
|
629
|
-
if (current === branch) {
|
|
630
|
-
return resetHard(target);
|
|
631
|
-
}
|
|
632
|
-
return run2(["branch", "-f", branch, target]);
|
|
633
|
-
}
|
|
634
|
-
async function pushSetUpstream(remote, branch) {
|
|
635
|
-
return run2(["push", "-u", remote, branch]);
|
|
636
|
-
}
|
|
637
|
-
async function rebase(branch) {
|
|
638
|
-
return run2(["rebase", branch]);
|
|
639
|
-
}
|
|
640
|
-
async function getUpstreamRef() {
|
|
641
|
-
const { exitCode, stdout } = await run2(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
642
|
-
if (exitCode !== 0)
|
|
643
|
-
return null;
|
|
644
|
-
return stdout.trim() || null;
|
|
645
|
-
}
|
|
646
|
-
async function rebaseOnto(newBase, oldBase) {
|
|
647
|
-
return run2(["rebase", "--onto", newBase, oldBase]);
|
|
648
|
-
}
|
|
649
|
-
async function getStagedDiff() {
|
|
650
|
-
const { stdout } = await run2(["diff", "--cached"]);
|
|
651
|
-
return stdout;
|
|
652
|
-
}
|
|
653
|
-
async function getStagedFiles() {
|
|
654
|
-
const { exitCode, stdout } = await run2(["diff", "--cached", "--name-only"]);
|
|
655
|
-
if (exitCode !== 0)
|
|
656
|
-
return [];
|
|
657
|
-
return stdout.trim().split(`
|
|
658
|
-
`).filter(Boolean);
|
|
659
|
-
}
|
|
660
|
-
async function getChangedFiles() {
|
|
661
|
-
const { exitCode, stdout } = await run2(["status", "--porcelain"]);
|
|
662
|
-
if (exitCode !== 0)
|
|
663
|
-
return [];
|
|
664
|
-
return stdout.trimEnd().split(`
|
|
665
|
-
`).filter(Boolean).map((l) => {
|
|
666
|
-
const line = l.replace(/\r$/, "");
|
|
667
|
-
const match = line.match(/^..\s+(.*)/);
|
|
668
|
-
if (!match)
|
|
669
|
-
return "";
|
|
670
|
-
const file = match[1];
|
|
671
|
-
const renameIdx = file.indexOf(" -> ");
|
|
672
|
-
return renameIdx !== -1 ? file.slice(renameIdx + 4) : file;
|
|
673
|
-
}).filter(Boolean);
|
|
674
|
-
}
|
|
675
|
-
async function getDivergence(branch, base) {
|
|
676
|
-
const { exitCode, stdout } = await run2([
|
|
677
|
-
"rev-list",
|
|
678
|
-
"--left-right",
|
|
679
|
-
"--count",
|
|
680
|
-
`${base}...${branch}`
|
|
681
|
-
]);
|
|
682
|
-
if (exitCode !== 0)
|
|
683
|
-
return { ahead: 0, behind: 0 };
|
|
684
|
-
const parts = stdout.trim().split(/\s+/);
|
|
685
|
-
return {
|
|
686
|
-
behind: Number.parseInt(parts[0] ?? "0", 10),
|
|
687
|
-
ahead: Number.parseInt(parts[1] ?? "0", 10)
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
async function getMergedBranches(base) {
|
|
691
|
-
const { exitCode, stdout } = await run2(["branch", "--merged", base]);
|
|
692
|
-
if (exitCode !== 0)
|
|
693
|
-
return [];
|
|
694
|
-
return stdout.trim().split(`
|
|
695
|
-
`).map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean);
|
|
696
|
-
}
|
|
697
|
-
async function getGoneBranches() {
|
|
698
|
-
const { exitCode, stdout } = await run2(["branch", "-vv"]);
|
|
699
|
-
if (exitCode !== 0)
|
|
700
|
-
return [];
|
|
701
|
-
return stdout.trimEnd().split(`
|
|
702
|
-
`).filter((line) => line.includes(": gone]")).map((line) => line.replace(/^\*?\s+/, "").split(/\s+/)[0]).filter(Boolean);
|
|
236
|
+
return [];
|
|
237
|
+
return stdout.trimEnd().split(`
|
|
238
|
+
`).filter((line) => line.includes(": gone]")).map((line) => line.replace(/^\*?\s+/, "").split(/\s+/)[0]).filter(Boolean);
|
|
703
239
|
}
|
|
704
240
|
async function deleteBranch(branch) {
|
|
705
|
-
return
|
|
241
|
+
return run(["branch", "-d", branch]);
|
|
706
242
|
}
|
|
707
243
|
async function forceDeleteBranch(branch) {
|
|
708
|
-
return
|
|
244
|
+
return run(["branch", "-D", branch]);
|
|
709
245
|
}
|
|
710
246
|
async function renameBranch(oldName, newName) {
|
|
711
|
-
return
|
|
247
|
+
return run(["branch", "-m", oldName, newName]);
|
|
712
248
|
}
|
|
713
249
|
async function hasLocalWork(remote, branch) {
|
|
714
250
|
const uncommitted = await hasUncommittedChanges();
|
|
715
251
|
const trackingRef = `${remote}/${branch}`;
|
|
716
|
-
const { exitCode, stdout } = await
|
|
252
|
+
const { exitCode, stdout } = await run(["rev-list", "--count", `${trackingRef}..${branch}`]);
|
|
717
253
|
const unpushedCommits = exitCode === 0 ? Number.parseInt(stdout.trim(), 10) || 0 : 0;
|
|
718
254
|
return { uncommitted, unpushedCommits };
|
|
719
255
|
}
|
|
720
256
|
async function deleteRemoteBranch(remote, branch) {
|
|
721
|
-
return
|
|
257
|
+
return run(["push", remote, "--delete", branch]);
|
|
722
258
|
}
|
|
723
259
|
async function mergeSquash(branch) {
|
|
724
|
-
return
|
|
260
|
+
return run(["merge", "--squash", branch]);
|
|
725
261
|
}
|
|
726
262
|
async function pushBranch(remote, branch) {
|
|
727
|
-
return
|
|
263
|
+
return run(["push", remote, branch]);
|
|
728
264
|
}
|
|
729
265
|
async function pruneRemote(remote) {
|
|
730
|
-
return
|
|
266
|
+
return run(["remote", "prune", remote]);
|
|
731
267
|
}
|
|
732
268
|
async function commitWithMessage(message) {
|
|
733
|
-
return
|
|
269
|
+
return run(["commit", "-m", message]);
|
|
734
270
|
}
|
|
735
271
|
async function getLogDiff(base, head) {
|
|
736
|
-
const { stdout } = await
|
|
272
|
+
const { stdout } = await run(["diff", `${base}...${head}`]);
|
|
737
273
|
return stdout;
|
|
738
274
|
}
|
|
739
275
|
async function getLog(base, head) {
|
|
740
|
-
const { exitCode, stdout } = await
|
|
276
|
+
const { exitCode, stdout } = await run(["log", `${base}..${head}`, "--oneline"]);
|
|
741
277
|
if (exitCode !== 0)
|
|
742
278
|
return [];
|
|
743
279
|
return stdout.trim().split(`
|
|
744
280
|
`).filter(Boolean);
|
|
745
281
|
}
|
|
746
282
|
async function pullBranch(remote, branch) {
|
|
747
|
-
return
|
|
283
|
+
return run(["pull", remote, branch]);
|
|
748
284
|
}
|
|
749
285
|
async function stageFiles(files) {
|
|
750
|
-
return
|
|
286
|
+
return run(["add", "--", ...files]);
|
|
751
287
|
}
|
|
752
288
|
async function unstageFiles(files) {
|
|
753
|
-
return
|
|
289
|
+
return run(["reset", "HEAD", "--", ...files]);
|
|
754
290
|
}
|
|
755
291
|
async function stageAll() {
|
|
756
|
-
return
|
|
292
|
+
return run(["add", "-A"]);
|
|
757
293
|
}
|
|
758
294
|
async function getFullDiffForFiles(files) {
|
|
759
295
|
const [unstaged, staged, untracked] = await Promise.all([
|
|
760
|
-
|
|
761
|
-
|
|
296
|
+
run(["diff", "--", ...files]),
|
|
297
|
+
run(["diff", "--cached", "--", ...files]),
|
|
762
298
|
getUntrackedFiles()
|
|
763
299
|
]);
|
|
764
300
|
const parts = [staged.stdout, unstaged.stdout].filter(Boolean);
|
|
@@ -785,14 +321,14 @@ ${lines.join(`
|
|
|
785
321
|
`);
|
|
786
322
|
}
|
|
787
323
|
async function getUntrackedFiles() {
|
|
788
|
-
const { exitCode, stdout } = await
|
|
324
|
+
const { exitCode, stdout } = await run(["ls-files", "--others", "--exclude-standard"]);
|
|
789
325
|
if (exitCode !== 0)
|
|
790
326
|
return [];
|
|
791
327
|
return stdout.trim().split(`
|
|
792
328
|
`).filter(Boolean);
|
|
793
329
|
}
|
|
794
330
|
async function getFileStatus() {
|
|
795
|
-
const { exitCode, stdout } = await
|
|
331
|
+
const { exitCode, stdout } = await run(["status", "--porcelain"]);
|
|
796
332
|
if (exitCode !== 0)
|
|
797
333
|
return { staged: [], modified: [], untracked: [] };
|
|
798
334
|
const result = { staged: [], modified: [], untracked: [] };
|
|
@@ -854,53 +390,6 @@ function heading(msg) {
|
|
|
854
390
|
${pc2.bold(msg)}`);
|
|
855
391
|
}
|
|
856
392
|
|
|
857
|
-
// src/utils/spinner.ts
|
|
858
|
-
import pc3 from "picocolors";
|
|
859
|
-
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
860
|
-
function createSpinner(text2) {
|
|
861
|
-
let frameIdx = 0;
|
|
862
|
-
let currentText = text2;
|
|
863
|
-
let stopped = false;
|
|
864
|
-
const clearLine = () => {
|
|
865
|
-
process.stderr.write("\r\x1B[K");
|
|
866
|
-
};
|
|
867
|
-
const render = () => {
|
|
868
|
-
if (stopped)
|
|
869
|
-
return;
|
|
870
|
-
const frame = pc3.cyan(FRAMES[frameIdx % FRAMES.length]);
|
|
871
|
-
clearLine();
|
|
872
|
-
process.stderr.write(`${frame} ${currentText}`);
|
|
873
|
-
frameIdx++;
|
|
874
|
-
};
|
|
875
|
-
const timer = setInterval(render, 80);
|
|
876
|
-
render();
|
|
877
|
-
const stop = () => {
|
|
878
|
-
if (stopped)
|
|
879
|
-
return;
|
|
880
|
-
stopped = true;
|
|
881
|
-
clearInterval(timer);
|
|
882
|
-
clearLine();
|
|
883
|
-
};
|
|
884
|
-
return {
|
|
885
|
-
update(newText) {
|
|
886
|
-
currentText = newText;
|
|
887
|
-
},
|
|
888
|
-
success(msg) {
|
|
889
|
-
stop();
|
|
890
|
-
process.stderr.write(`${pc3.green("✔")} ${msg}
|
|
891
|
-
`);
|
|
892
|
-
},
|
|
893
|
-
fail(msg) {
|
|
894
|
-
stop();
|
|
895
|
-
process.stderr.write(`${pc3.red("✖")} ${msg}
|
|
896
|
-
`);
|
|
897
|
-
},
|
|
898
|
-
stop() {
|
|
899
|
-
stop();
|
|
900
|
-
}
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
|
|
904
393
|
// src/utils/workflow.ts
|
|
905
394
|
var WORKFLOW_DESCRIPTIONS = {
|
|
906
395
|
"clean-flow": "Clean Flow — main + dev, squash features into dev, merge dev into main",
|
|
@@ -960,57 +449,32 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
960
449
|
warn("You have uncommitted changes in your working tree.");
|
|
961
450
|
}
|
|
962
451
|
if (localWork.unpushedCommits > 0) {
|
|
963
|
-
warn(`You have ${
|
|
452
|
+
warn(`You have ${pc3.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
|
|
964
453
|
}
|
|
965
454
|
const SAVE_NEW_BRANCH = "Save changes to a new branch";
|
|
966
455
|
const DISCARD = "Discard all changes and clean up";
|
|
967
456
|
const CANCEL = "Skip this branch";
|
|
968
|
-
const action = await selectPrompt(`${
|
|
457
|
+
const action = await selectPrompt(`${pc3.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
|
|
969
458
|
if (action === CANCEL)
|
|
970
459
|
return "skipped";
|
|
971
460
|
if (action === SAVE_NEW_BRANCH) {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
info(pc4.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
|
|
975
|
-
const description = await inputPrompt("What are you working on?");
|
|
976
|
-
let newBranchName = description;
|
|
977
|
-
if (looksLikeNaturalLanguage(description)) {
|
|
978
|
-
const spinner = createSpinner("Generating branch name suggestion...");
|
|
979
|
-
const suggested = await suggestBranchName(description);
|
|
980
|
-
if (suggested) {
|
|
981
|
-
spinner.success("Branch name suggestion ready.");
|
|
982
|
-
console.log(`
|
|
983
|
-
${pc4.dim("AI suggestion:")} ${pc4.bold(pc4.cyan(suggested))}`);
|
|
984
|
-
const accepted = await confirmPrompt(`Use ${pc4.bold(suggested)} as your branch name?`);
|
|
985
|
-
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
986
|
-
} else {
|
|
987
|
-
spinner.fail("AI did not return a suggestion.");
|
|
988
|
-
newBranchName = await inputPrompt("Enter branch name", description);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
992
|
-
const prefix = await selectPrompt(`Choose a branch type for ${pc4.bold(newBranchName)}:`, config.branchPrefixes);
|
|
993
|
-
newBranchName = formatBranchName(prefix, newBranchName);
|
|
994
|
-
}
|
|
995
|
-
if (!isValidBranchName(newBranchName)) {
|
|
996
|
-
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
997
|
-
return "skipped";
|
|
998
|
-
}
|
|
461
|
+
const suggestedName = currentBranch.replace(/^(feature|fix|docs|chore|test|refactor)\//, "$1/new-");
|
|
462
|
+
const newBranchName = await inputPrompt("New branch name", suggestedName !== currentBranch ? suggestedName : `${currentBranch}-v2`);
|
|
999
463
|
const renameResult = await renameBranch(currentBranch, newBranchName);
|
|
1000
464
|
if (renameResult.exitCode !== 0) {
|
|
1001
465
|
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
1002
466
|
return "skipped";
|
|
1003
467
|
}
|
|
1004
|
-
success(`Renamed ${
|
|
468
|
+
success(`Renamed ${pc3.bold(currentBranch)} → ${pc3.bold(newBranchName)}`);
|
|
1005
469
|
const syncSource2 = getSyncSource(config);
|
|
1006
470
|
await fetchRemote(syncSource2.remote);
|
|
1007
471
|
const savedUpstreamRef = await getUpstreamRef();
|
|
1008
472
|
const rebaseResult = savedUpstreamRef && savedUpstreamRef !== syncSource2.ref ? await rebaseOnto(syncSource2.ref, savedUpstreamRef) : await rebase(syncSource2.ref);
|
|
1009
473
|
if (rebaseResult.exitCode !== 0) {
|
|
1010
474
|
warn("Rebase encountered conflicts. Resolve them after cleanup:");
|
|
1011
|
-
info(` ${
|
|
475
|
+
info(` ${pc3.bold(`git checkout ${newBranchName} && git rebase --continue`)}`);
|
|
1012
476
|
} else {
|
|
1013
|
-
success(`Rebased ${
|
|
477
|
+
success(`Rebased ${pc3.bold(newBranchName)} onto ${pc3.bold(syncSource2.ref)}.`);
|
|
1014
478
|
}
|
|
1015
479
|
const coResult2 = await checkoutBranch(baseBranch);
|
|
1016
480
|
if (coResult2.exitCode !== 0) {
|
|
@@ -1018,12 +482,12 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1018
482
|
return "saved";
|
|
1019
483
|
}
|
|
1020
484
|
await updateLocalBranch(baseBranch, syncSource2.ref);
|
|
1021
|
-
success(`Synced ${
|
|
485
|
+
success(`Synced ${pc3.bold(baseBranch)} with ${pc3.bold(syncSource2.ref)}.`);
|
|
1022
486
|
return "saved";
|
|
1023
487
|
}
|
|
1024
488
|
}
|
|
1025
489
|
const syncSource = getSyncSource(config);
|
|
1026
|
-
info(`Switching to ${
|
|
490
|
+
info(`Switching to ${pc3.bold(baseBranch)} and syncing...`);
|
|
1027
491
|
await fetchRemote(syncSource.remote);
|
|
1028
492
|
const coResult = await checkoutBranch(baseBranch);
|
|
1029
493
|
if (coResult.exitCode !== 0) {
|
|
@@ -1031,7 +495,7 @@ async function handleCurrentBranchDeletion(currentBranch, baseBranch, config) {
|
|
|
1031
495
|
return "skipped";
|
|
1032
496
|
}
|
|
1033
497
|
await updateLocalBranch(baseBranch, syncSource.ref);
|
|
1034
|
-
success(`Synced ${
|
|
498
|
+
success(`Synced ${pc3.bold(baseBranch)} with ${pc3.bold(syncSource.ref)}.`);
|
|
1035
499
|
return "switched";
|
|
1036
500
|
}
|
|
1037
501
|
var clean_default = defineCommand({
|
|
@@ -1073,27 +537,15 @@ var clean_default = defineCommand({
|
|
|
1073
537
|
const mergedCandidates = mergedBranches.filter((b) => !protectedBranches.has(b));
|
|
1074
538
|
const goneBranches = await getGoneBranches();
|
|
1075
539
|
const goneCandidates = goneBranches.filter((b) => !protectedBranches.has(b) && !mergedCandidates.includes(b));
|
|
1076
|
-
if (currentBranch && !protectedBranches.has(currentBranch) && !mergedCandidates.includes(currentBranch) && !goneCandidates.includes(currentBranch)) {
|
|
1077
|
-
const ghInstalled = await checkGhInstalled();
|
|
1078
|
-
const ghAuthed = ghInstalled && await checkGhAuth();
|
|
1079
|
-
if (ghInstalled && ghAuthed) {
|
|
1080
|
-
const mergedPR = await getMergedPRForBranch(currentBranch);
|
|
1081
|
-
if (mergedPR) {
|
|
1082
|
-
warn(`PR #${mergedPR.number} (${pc4.bold(mergedPR.title)}) has already been merged.`);
|
|
1083
|
-
info(`Link: ${pc4.underline(mergedPR.url)}`);
|
|
1084
|
-
goneCandidates.push(currentBranch);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
540
|
if (mergedCandidates.length > 0) {
|
|
1089
541
|
console.log(`
|
|
1090
|
-
${
|
|
542
|
+
${pc3.bold("Merged branches to delete:")}`);
|
|
1091
543
|
for (const b of mergedCandidates) {
|
|
1092
|
-
const marker = b === currentBranch ?
|
|
1093
|
-
console.log(` ${
|
|
544
|
+
const marker = b === currentBranch ? pc3.yellow(" (current)") : "";
|
|
545
|
+
console.log(` ${pc3.dim("•")} ${b}${marker}`);
|
|
1094
546
|
}
|
|
1095
547
|
console.log();
|
|
1096
|
-
const ok = args.yes || await confirmPrompt(`Delete ${
|
|
548
|
+
const ok = args.yes || await confirmPrompt(`Delete ${pc3.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
|
|
1097
549
|
if (ok) {
|
|
1098
550
|
for (const branch of mergedCandidates) {
|
|
1099
551
|
if (branch === currentBranch) {
|
|
@@ -1110,7 +562,7 @@ ${pc4.bold("Merged branches to delete:")}`);
|
|
|
1110
562
|
}
|
|
1111
563
|
const result = await deleteBranch(branch);
|
|
1112
564
|
if (result.exitCode === 0) {
|
|
1113
|
-
success(` Deleted ${
|
|
565
|
+
success(` Deleted ${pc3.bold(branch)}`);
|
|
1114
566
|
} else {
|
|
1115
567
|
warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
|
|
1116
568
|
}
|
|
@@ -1121,13 +573,13 @@ ${pc4.bold("Merged branches to delete:")}`);
|
|
|
1121
573
|
}
|
|
1122
574
|
if (goneCandidates.length > 0) {
|
|
1123
575
|
console.log(`
|
|
1124
|
-
${
|
|
576
|
+
${pc3.bold("Stale branches (remote deleted, likely squash-merged):")}`);
|
|
1125
577
|
for (const b of goneCandidates) {
|
|
1126
|
-
const marker = b === currentBranch ?
|
|
1127
|
-
console.log(` ${
|
|
578
|
+
const marker = b === currentBranch ? pc3.yellow(" (current)") : "";
|
|
579
|
+
console.log(` ${pc3.dim("•")} ${b}${marker}`);
|
|
1128
580
|
}
|
|
1129
581
|
console.log();
|
|
1130
|
-
const ok = args.yes || await confirmPrompt(`Delete ${
|
|
582
|
+
const ok = args.yes || await confirmPrompt(`Delete ${pc3.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
|
|
1131
583
|
if (ok) {
|
|
1132
584
|
for (const branch of goneCandidates) {
|
|
1133
585
|
if (branch === currentBranch) {
|
|
@@ -1144,7 +596,7 @@ ${pc4.bold("Stale branches (remote deleted, likely squash-merged):")}`);
|
|
|
1144
596
|
}
|
|
1145
597
|
const result = await forceDeleteBranch(branch);
|
|
1146
598
|
if (result.exitCode === 0) {
|
|
1147
|
-
success(` Deleted ${
|
|
599
|
+
success(` Deleted ${pc3.bold(branch)}`);
|
|
1148
600
|
} else {
|
|
1149
601
|
warn(` Failed to delete ${branch}: ${result.stderr.trim()}`);
|
|
1150
602
|
}
|
|
@@ -1159,56 +611,406 @@ ${pc4.bold("Stale branches (remote deleted, likely squash-merged):")}`);
|
|
|
1159
611
|
const finalBranch = await getCurrentBranch();
|
|
1160
612
|
if (finalBranch && protectedBranches.has(finalBranch)) {
|
|
1161
613
|
console.log();
|
|
1162
|
-
info(`You're on ${
|
|
614
|
+
info(`You're on ${pc3.bold(finalBranch)}. Run ${pc3.bold("contrib start")} to begin a new feature.`);
|
|
1163
615
|
}
|
|
1164
616
|
}
|
|
1165
|
-
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// src/commands/commit.ts
|
|
620
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
621
|
+
import pc5 from "picocolors";
|
|
622
|
+
|
|
623
|
+
// src/utils/convention.ts
|
|
624
|
+
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;
|
|
625
|
+
var CONVENTIONAL_COMMIT_PATTERN = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(!?)(\([a-zA-Z0-9][a-zA-Z0-9._-]*\))?: .{1,72}$/;
|
|
626
|
+
var CONVENTION_LABELS = {
|
|
627
|
+
conventional: "Conventional Commits",
|
|
628
|
+
"clean-commit": "Clean Commit (by WGTech Labs)",
|
|
629
|
+
none: "No convention"
|
|
630
|
+
};
|
|
631
|
+
var CONVENTION_DESCRIPTIONS = {
|
|
632
|
+
conventional: "Conventional Commits — feat: | fix: | docs: | chore: etc. (conventionalcommits.org)",
|
|
633
|
+
"clean-commit": "Clean Commit — \uD83D\uDCE6 new: | \uD83D\uDD27 update: | \uD83D\uDDD1️ remove: etc. (by WGTech Labs)",
|
|
634
|
+
none: "No commit convention enforcement"
|
|
635
|
+
};
|
|
636
|
+
var CONVENTION_FORMAT_HINTS = {
|
|
637
|
+
conventional: [
|
|
638
|
+
"Format: <type>[!][(<scope>)]: <description>",
|
|
639
|
+
"Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
|
|
640
|
+
"Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README"
|
|
641
|
+
],
|
|
642
|
+
"clean-commit": [
|
|
643
|
+
"Format: <emoji> <type>[!][(<scope>)]: <description>",
|
|
644
|
+
"Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1️ remove | \uD83D\uDD12 security | ⚙️ setup | ☕ chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
|
|
645
|
+
"Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow"
|
|
646
|
+
]
|
|
647
|
+
};
|
|
648
|
+
function validateCommitMessage(message, convention) {
|
|
649
|
+
if (convention === "none")
|
|
650
|
+
return true;
|
|
651
|
+
if (convention === "clean-commit")
|
|
652
|
+
return CLEAN_COMMIT_PATTERN.test(message);
|
|
653
|
+
if (convention === "conventional")
|
|
654
|
+
return CONVENTIONAL_COMMIT_PATTERN.test(message);
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
function getValidationError(convention) {
|
|
658
|
+
if (convention === "none")
|
|
659
|
+
return [];
|
|
660
|
+
return [
|
|
661
|
+
`Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
|
|
662
|
+
...CONVENTION_FORMAT_HINTS[convention]
|
|
663
|
+
];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/utils/copilot.ts
|
|
667
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
668
|
+
var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
|
|
669
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
670
|
+
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.
|
|
671
|
+
Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
|
|
672
|
+
var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
673
|
+
Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
|
|
674
|
+
Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
|
|
675
|
+
Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
|
|
676
|
+
Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
|
|
677
|
+
WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
|
|
678
|
+
function getGroupingSystemPrompt(convention) {
|
|
679
|
+
const conventionBlock = convention === "conventional" ? `Use Conventional Commit format: <type>[(<scope>)]: <description>
|
|
680
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert` : `Use Clean Commit format: <emoji> <type>[!][ (<scope>)]: <description>
|
|
681
|
+
Emoji/type table:
|
|
682
|
+
\uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, ☕ chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release`;
|
|
683
|
+
return `You are a smart commit grouping assistant. Given a list of changed files and their diffs, group related changes into logical atomic commits.
|
|
684
|
+
|
|
685
|
+
${conventionBlock}
|
|
686
|
+
|
|
687
|
+
Return a JSON array of commit groups with this EXACT structure (no markdown fences, no explanation):
|
|
688
|
+
[
|
|
689
|
+
{
|
|
690
|
+
"files": ["path/to/file1.ts", "path/to/file2.ts"],
|
|
691
|
+
"message": "<commit message following the convention above>"
|
|
692
|
+
}
|
|
693
|
+
]
|
|
694
|
+
|
|
695
|
+
Rules:
|
|
696
|
+
- Group files that are logically related (e.g. a utility and its tests, a feature and its types)
|
|
697
|
+
- Each group should represent ONE logical change
|
|
698
|
+
- Every file must appear in exactly one group
|
|
699
|
+
- Commit messages must follow the convention, be concise, imperative, max 72 chars
|
|
700
|
+
- Order groups so foundational changes come first (types, utils) and consumers come after
|
|
701
|
+
- Return ONLY the JSON array, nothing else`;
|
|
702
|
+
}
|
|
703
|
+
var BRANCH_NAME_SYSTEM_PROMPT = `Git branch name generator. Format: <prefix>/<kebab-case-name>
|
|
704
|
+
Prefixes: feature, fix, docs, chore, test, refactor
|
|
705
|
+
Rules: lowercase kebab-case, 2-5 words max. Return ONLY the branch name.
|
|
706
|
+
Examples: fix/login-timeout | feature/user-profile-page | docs/update-readme`;
|
|
707
|
+
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..."}`;
|
|
708
|
+
function getPRDescriptionSystemPrompt(convention) {
|
|
709
|
+
if (convention === "clean-commit") {
|
|
710
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
711
|
+
CRITICAL: The PR title MUST follow the Clean Commit format exactly: <emoji> <type>: <description>
|
|
712
|
+
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
|
|
713
|
+
Title examples: \uD83D\uDCE6 new: add user authentication | \uD83D\uDD27 update: improve error handling | \uD83D\uDDD1️ remove: drop legacy API
|
|
714
|
+
Rules: title follows convention, present tense, max 72 chars; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
715
|
+
}
|
|
716
|
+
if (convention === "conventional") {
|
|
717
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
718
|
+
CRITICAL: The PR title MUST follow Conventional Commits format: <type>[(<scope>)]: <description>
|
|
719
|
+
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
720
|
+
Title examples: feat: add user authentication | fix(auth): resolve token expiry | docs: update contributing guide
|
|
721
|
+
Rules: title follows convention, present tense, max 72 chars; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
722
|
+
}
|
|
723
|
+
return `${PR_DESCRIPTION_SYSTEM_PROMPT_BASE}
|
|
724
|
+
Rules: title concise present tense; body has Summary, Changes (bullets), Test Plan sections. Return ONLY the JSON object, no fences.`;
|
|
725
|
+
}
|
|
726
|
+
var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `Git merge conflict advisor. Explain each side, suggest resolution strategy. Never auto-resolve — guidance only. Be concise and actionable.`;
|
|
727
|
+
function suppressSubprocessWarnings() {
|
|
728
|
+
process.env.NODE_NO_WARNINGS = "1";
|
|
729
|
+
}
|
|
730
|
+
function withTimeout(promise, ms) {
|
|
731
|
+
return new Promise((resolve, reject) => {
|
|
732
|
+
const timer = setTimeout(() => reject(new Error(`Copilot request timed out after ${ms / 1000}s`)), ms);
|
|
733
|
+
promise.then((val) => {
|
|
734
|
+
clearTimeout(timer);
|
|
735
|
+
resolve(val);
|
|
736
|
+
}, (err) => {
|
|
737
|
+
clearTimeout(timer);
|
|
738
|
+
reject(err);
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
var COPILOT_TIMEOUT_MS = 30000;
|
|
743
|
+
var COPILOT_LONG_TIMEOUT_MS = 90000;
|
|
744
|
+
async function checkCopilotAvailable() {
|
|
745
|
+
try {
|
|
746
|
+
const client = await getManagedClient();
|
|
747
|
+
try {
|
|
748
|
+
await client.ping();
|
|
749
|
+
} catch (err) {
|
|
750
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
751
|
+
if (msg.includes("auth") || msg.includes("token") || msg.includes("401") || msg.includes("403")) {
|
|
752
|
+
return "Copilot authentication failed. Run `gh auth login` to refresh your token.";
|
|
753
|
+
}
|
|
754
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("timeout") || msg.includes("network")) {
|
|
755
|
+
return "Could not reach GitHub Copilot service. Check your internet connection.";
|
|
756
|
+
}
|
|
757
|
+
return `Copilot health check failed: ${msg}`;
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
} catch (err) {
|
|
761
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
762
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
763
|
+
return "Copilot CLI binary not found. Ensure GitHub Copilot is installed and your gh CLI is up to date.";
|
|
764
|
+
}
|
|
765
|
+
return `Failed to start Copilot service: ${msg}`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
var _managedClient = null;
|
|
769
|
+
var _clientStarted = false;
|
|
770
|
+
async function getManagedClient() {
|
|
771
|
+
if (!_managedClient || !_clientStarted) {
|
|
772
|
+
suppressSubprocessWarnings();
|
|
773
|
+
_managedClient = new CopilotClient;
|
|
774
|
+
await _managedClient.start();
|
|
775
|
+
_clientStarted = true;
|
|
776
|
+
const cleanup = () => {
|
|
777
|
+
if (_managedClient && _clientStarted) {
|
|
778
|
+
try {
|
|
779
|
+
_managedClient.stop();
|
|
780
|
+
} catch {}
|
|
781
|
+
_clientStarted = false;
|
|
782
|
+
_managedClient = null;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
process.once("exit", cleanup);
|
|
786
|
+
process.once("SIGINT", cleanup);
|
|
787
|
+
process.once("SIGTERM", cleanup);
|
|
788
|
+
}
|
|
789
|
+
return _managedClient;
|
|
790
|
+
}
|
|
791
|
+
async function callCopilot(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
|
|
792
|
+
const client = await getManagedClient();
|
|
793
|
+
const sessionConfig = {
|
|
794
|
+
systemMessage: { mode: "replace", content: systemMessage }
|
|
795
|
+
};
|
|
796
|
+
if (model)
|
|
797
|
+
sessionConfig.model = model;
|
|
798
|
+
const session = await client.createSession(sessionConfig);
|
|
799
|
+
try {
|
|
800
|
+
const response = await withTimeout(session.sendAndWait({ prompt: userMessage }), timeoutMs);
|
|
801
|
+
if (!response?.data?.content)
|
|
802
|
+
return null;
|
|
803
|
+
return response.data.content;
|
|
804
|
+
} finally {
|
|
805
|
+
await session.destroy();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function getCommitSystemPrompt(convention) {
|
|
809
|
+
if (convention === "conventional")
|
|
810
|
+
return CONVENTIONAL_COMMIT_SYSTEM_PROMPT;
|
|
811
|
+
return CLEAN_COMMIT_SYSTEM_PROMPT;
|
|
812
|
+
}
|
|
813
|
+
function extractJson(raw) {
|
|
814
|
+
let text2 = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
815
|
+
if (text2.startsWith("[") || text2.startsWith("{"))
|
|
816
|
+
return text2;
|
|
817
|
+
const arrayStart = text2.indexOf("[");
|
|
818
|
+
const objStart = text2.indexOf("{");
|
|
819
|
+
let start;
|
|
820
|
+
let closeChar;
|
|
821
|
+
if (arrayStart === -1 && objStart === -1)
|
|
822
|
+
return text2;
|
|
823
|
+
if (arrayStart === -1) {
|
|
824
|
+
start = objStart;
|
|
825
|
+
closeChar = "}";
|
|
826
|
+
} else if (objStart === -1) {
|
|
827
|
+
start = arrayStart;
|
|
828
|
+
closeChar = "]";
|
|
829
|
+
} else if (arrayStart < objStart) {
|
|
830
|
+
start = arrayStart;
|
|
831
|
+
closeChar = "]";
|
|
832
|
+
} else {
|
|
833
|
+
start = objStart;
|
|
834
|
+
closeChar = "}";
|
|
835
|
+
}
|
|
836
|
+
const end = text2.lastIndexOf(closeChar);
|
|
837
|
+
if (end > start) {
|
|
838
|
+
text2 = text2.slice(start, end + 1);
|
|
839
|
+
}
|
|
840
|
+
return text2;
|
|
841
|
+
}
|
|
842
|
+
async function generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
|
|
843
|
+
try {
|
|
844
|
+
const multiFileHint = stagedFiles.length > 1 ? `
|
|
845
|
+
|
|
846
|
+
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.` : "";
|
|
847
|
+
const userMessage = `Generate a commit message for these staged changes:
|
|
848
|
+
|
|
849
|
+
Files: ${stagedFiles.join(", ")}
|
|
850
|
+
|
|
851
|
+
Diff:
|
|
852
|
+
${diff.slice(0, 4000)}${multiFileHint}`;
|
|
853
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
854
|
+
return result?.trim() ?? null;
|
|
855
|
+
} catch {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function generatePRDescription(commits, diff, model, convention = "clean-commit") {
|
|
860
|
+
try {
|
|
861
|
+
const userMessage = `Generate a PR description for these changes:
|
|
862
|
+
|
|
863
|
+
Commits:
|
|
864
|
+
${commits.join(`
|
|
865
|
+
`)}
|
|
866
|
+
|
|
867
|
+
Diff (truncated):
|
|
868
|
+
${diff.slice(0, 4000)}`;
|
|
869
|
+
const result = await callCopilot(getPRDescriptionSystemPrompt(convention), userMessage, model);
|
|
870
|
+
if (!result)
|
|
871
|
+
return null;
|
|
872
|
+
const cleaned = extractJson(result);
|
|
873
|
+
return JSON.parse(cleaned);
|
|
874
|
+
} catch {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async function suggestBranchName(description, model) {
|
|
879
|
+
try {
|
|
880
|
+
const result = await callCopilot(BRANCH_NAME_SYSTEM_PROMPT, description, model);
|
|
881
|
+
return result?.trim() ?? null;
|
|
882
|
+
} catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async function suggestConflictResolution(conflictDiff, model) {
|
|
887
|
+
try {
|
|
888
|
+
const userMessage = `Help me resolve this merge conflict:
|
|
889
|
+
|
|
890
|
+
${conflictDiff.slice(0, 4000)}`;
|
|
891
|
+
const result = await callCopilot(CONFLICT_RESOLUTION_SYSTEM_PROMPT, userMessage, model);
|
|
892
|
+
return result?.trim() ?? null;
|
|
893
|
+
} catch {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function generateCommitGroups(files, diffs, model, convention = "clean-commit") {
|
|
898
|
+
const userMessage = `Group these changed files into logical atomic commits:
|
|
899
|
+
|
|
900
|
+
Files:
|
|
901
|
+
${files.join(`
|
|
902
|
+
`)}
|
|
903
|
+
|
|
904
|
+
Diffs (truncated):
|
|
905
|
+
${diffs.slice(0, 6000)}`;
|
|
906
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
907
|
+
if (!result) {
|
|
908
|
+
throw new Error("AI returned an empty response");
|
|
909
|
+
}
|
|
910
|
+
const cleaned = extractJson(result);
|
|
911
|
+
let parsed;
|
|
912
|
+
try {
|
|
913
|
+
parsed = JSON.parse(cleaned);
|
|
914
|
+
} catch {
|
|
915
|
+
throw new Error(`AI response is not valid JSON. Raw start: "${result.slice(0, 120)}..."`);
|
|
916
|
+
}
|
|
917
|
+
const groups = parsed;
|
|
918
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
919
|
+
throw new Error("AI response was not a valid JSON array of commit groups");
|
|
920
|
+
}
|
|
921
|
+
for (const group of groups) {
|
|
922
|
+
if (!Array.isArray(group.files) || typeof group.message !== "string") {
|
|
923
|
+
throw new Error("AI returned groups with invalid structure (missing files or message)");
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return groups;
|
|
927
|
+
}
|
|
928
|
+
async function regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
|
|
929
|
+
const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
|
|
930
|
+
`);
|
|
931
|
+
const userMessage = `Regenerate ONLY the commit messages for these pre-defined file groups. Do NOT change the file groupings.
|
|
932
|
+
|
|
933
|
+
Groups:
|
|
934
|
+
${groupSummary}
|
|
935
|
+
|
|
936
|
+
Diffs (truncated):
|
|
937
|
+
${diffs.slice(0, 6000)}`;
|
|
938
|
+
const result = await callCopilot(getGroupingSystemPrompt(convention), userMessage, model, COPILOT_LONG_TIMEOUT_MS);
|
|
939
|
+
if (!result)
|
|
940
|
+
return groups;
|
|
941
|
+
try {
|
|
942
|
+
const cleaned = extractJson(result);
|
|
943
|
+
const parsed = JSON.parse(cleaned);
|
|
944
|
+
if (!Array.isArray(parsed) || parsed.length !== groups.length)
|
|
945
|
+
return groups;
|
|
946
|
+
return groups.map((g, i) => ({
|
|
947
|
+
files: g.files,
|
|
948
|
+
message: typeof parsed[i]?.message === "string" ? parsed[i].message : g.message
|
|
949
|
+
}));
|
|
950
|
+
} catch {
|
|
951
|
+
return groups;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
|
|
955
|
+
try {
|
|
956
|
+
const userMessage = `Generate a single commit message for these files:
|
|
1166
957
|
|
|
1167
|
-
|
|
1168
|
-
import { defineCommand as defineCommand2 } from "citty";
|
|
1169
|
-
import pc5 from "picocolors";
|
|
958
|
+
Files: ${files.join(", ")}
|
|
1170
959
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
};
|
|
1179
|
-
var CONVENTION_DESCRIPTIONS = {
|
|
1180
|
-
conventional: "Conventional Commits — feat: | fix: | docs: | chore: etc. (conventionalcommits.org)",
|
|
1181
|
-
"clean-commit": "Clean Commit — \uD83D\uDCE6 new: | \uD83D\uDD27 update: | \uD83D\uDDD1️ remove: etc. (by WGTech Labs)",
|
|
1182
|
-
none: "No commit convention enforcement"
|
|
1183
|
-
};
|
|
1184
|
-
var CONVENTION_FORMAT_HINTS = {
|
|
1185
|
-
conventional: [
|
|
1186
|
-
"Format: <type>[!][(<scope>)]: <description>",
|
|
1187
|
-
"Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert",
|
|
1188
|
-
"Examples: feat: add login page | fix(auth): resolve token expiry | docs: update README"
|
|
1189
|
-
],
|
|
1190
|
-
"clean-commit": [
|
|
1191
|
-
"Format: <emoji> <type>[!][(<scope>)]: <description>",
|
|
1192
|
-
"Types: \uD83D\uDCE6 new | \uD83D\uDD27 update | \uD83D\uDDD1️ remove | \uD83D\uDD12 security | ⚙️ setup | ☕ chore | \uD83E\uDDEA test | \uD83D\uDCD6 docs | \uD83D\uDE80 release",
|
|
1193
|
-
"Examples: \uD83D\uDCE6 new: user auth | \uD83D\uDD27 update (api): improve errors | ⚙️ setup (ci): add workflow"
|
|
1194
|
-
]
|
|
1195
|
-
};
|
|
1196
|
-
function validateCommitMessage(message, convention) {
|
|
1197
|
-
if (convention === "none")
|
|
1198
|
-
return true;
|
|
1199
|
-
if (convention === "clean-commit")
|
|
1200
|
-
return CLEAN_COMMIT_PATTERN.test(message);
|
|
1201
|
-
if (convention === "conventional")
|
|
1202
|
-
return CONVENTIONAL_COMMIT_PATTERN.test(message);
|
|
1203
|
-
return true;
|
|
960
|
+
Diff:
|
|
961
|
+
${diffs.slice(0, 4000)}`;
|
|
962
|
+
const result = await callCopilot(getCommitSystemPrompt(convention), userMessage, model);
|
|
963
|
+
return result?.trim() ?? null;
|
|
964
|
+
} catch {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
1204
967
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
968
|
+
|
|
969
|
+
// src/utils/spinner.ts
|
|
970
|
+
import pc4 from "picocolors";
|
|
971
|
+
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
972
|
+
function createSpinner(text2) {
|
|
973
|
+
let frameIdx = 0;
|
|
974
|
+
let currentText = text2;
|
|
975
|
+
let stopped = false;
|
|
976
|
+
const clearLine = () => {
|
|
977
|
+
process.stderr.write("\r\x1B[K");
|
|
978
|
+
};
|
|
979
|
+
const render = () => {
|
|
980
|
+
if (stopped)
|
|
981
|
+
return;
|
|
982
|
+
const frame = pc4.cyan(FRAMES[frameIdx % FRAMES.length]);
|
|
983
|
+
clearLine();
|
|
984
|
+
process.stderr.write(`${frame} ${currentText}`);
|
|
985
|
+
frameIdx++;
|
|
986
|
+
};
|
|
987
|
+
const timer = setInterval(render, 80);
|
|
988
|
+
render();
|
|
989
|
+
const stop = () => {
|
|
990
|
+
if (stopped)
|
|
991
|
+
return;
|
|
992
|
+
stopped = true;
|
|
993
|
+
clearInterval(timer);
|
|
994
|
+
clearLine();
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
update(newText) {
|
|
998
|
+
currentText = newText;
|
|
999
|
+
},
|
|
1000
|
+
success(msg) {
|
|
1001
|
+
stop();
|
|
1002
|
+
process.stderr.write(`${pc4.green("✔")} ${msg}
|
|
1003
|
+
`);
|
|
1004
|
+
},
|
|
1005
|
+
fail(msg) {
|
|
1006
|
+
stop();
|
|
1007
|
+
process.stderr.write(`${pc4.red("✖")} ${msg}
|
|
1008
|
+
`);
|
|
1009
|
+
},
|
|
1010
|
+
stop() {
|
|
1011
|
+
stop();
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1212
1014
|
}
|
|
1213
1015
|
|
|
1214
1016
|
// src/commands/commit.ts
|
|
@@ -1688,6 +1490,144 @@ async function uninstallHook() {
|
|
|
1688
1490
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1689
1491
|
import pc7 from "picocolors";
|
|
1690
1492
|
|
|
1493
|
+
// src/utils/gh.ts
|
|
1494
|
+
import { execFile as execFileCb2 } from "node:child_process";
|
|
1495
|
+
function run2(args) {
|
|
1496
|
+
return new Promise((resolve) => {
|
|
1497
|
+
execFileCb2("gh", args, (error2, stdout, stderr) => {
|
|
1498
|
+
resolve({
|
|
1499
|
+
exitCode: error2 ? error2.code === "ENOENT" ? 127 : error2.status ?? 1 : 0,
|
|
1500
|
+
stdout: stdout ?? "",
|
|
1501
|
+
stderr: stderr ?? ""
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
async function checkGhInstalled() {
|
|
1507
|
+
try {
|
|
1508
|
+
const { exitCode } = await run2(["--version"]);
|
|
1509
|
+
return exitCode === 0;
|
|
1510
|
+
} catch {
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async function checkGhAuth() {
|
|
1515
|
+
try {
|
|
1516
|
+
const { exitCode } = await run2(["auth", "status"]);
|
|
1517
|
+
return exitCode === 0;
|
|
1518
|
+
} catch {
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
var SAFE_SLUG = /^[\w.-]+$/;
|
|
1523
|
+
async function checkRepoPermissions(owner, repo) {
|
|
1524
|
+
if (!SAFE_SLUG.test(owner) || !SAFE_SLUG.test(repo))
|
|
1525
|
+
return null;
|
|
1526
|
+
const { exitCode, stdout } = await run2(["api", `repos/${owner}/${repo}`, "--jq", ".permissions"]);
|
|
1527
|
+
if (exitCode !== 0)
|
|
1528
|
+
return null;
|
|
1529
|
+
try {
|
|
1530
|
+
return JSON.parse(stdout.trim());
|
|
1531
|
+
} catch {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async function isRepoFork() {
|
|
1536
|
+
const { exitCode, stdout } = await run2(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
|
|
1537
|
+
if (exitCode !== 0)
|
|
1538
|
+
return null;
|
|
1539
|
+
const val = stdout.trim();
|
|
1540
|
+
if (val === "true")
|
|
1541
|
+
return true;
|
|
1542
|
+
if (val === "false")
|
|
1543
|
+
return false;
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
async function getCurrentRepoInfo() {
|
|
1547
|
+
const { exitCode, stdout } = await run2([
|
|
1548
|
+
"repo",
|
|
1549
|
+
"view",
|
|
1550
|
+
"--json",
|
|
1551
|
+
"nameWithOwner",
|
|
1552
|
+
"-q",
|
|
1553
|
+
".nameWithOwner"
|
|
1554
|
+
]);
|
|
1555
|
+
if (exitCode !== 0)
|
|
1556
|
+
return null;
|
|
1557
|
+
const nameWithOwner = stdout.trim();
|
|
1558
|
+
if (!nameWithOwner)
|
|
1559
|
+
return null;
|
|
1560
|
+
const [owner, repo] = nameWithOwner.split("/");
|
|
1561
|
+
if (!owner || !repo)
|
|
1562
|
+
return null;
|
|
1563
|
+
return { owner, repo };
|
|
1564
|
+
}
|
|
1565
|
+
async function createPR(options) {
|
|
1566
|
+
const args = [
|
|
1567
|
+
"pr",
|
|
1568
|
+
"create",
|
|
1569
|
+
"--base",
|
|
1570
|
+
options.base,
|
|
1571
|
+
"--title",
|
|
1572
|
+
options.title,
|
|
1573
|
+
"--body",
|
|
1574
|
+
options.body
|
|
1575
|
+
];
|
|
1576
|
+
if (options.draft)
|
|
1577
|
+
args.push("--draft");
|
|
1578
|
+
return run2(args);
|
|
1579
|
+
}
|
|
1580
|
+
async function createPRFill(base, draft) {
|
|
1581
|
+
const args = ["pr", "create", "--base", base, "--fill"];
|
|
1582
|
+
if (draft)
|
|
1583
|
+
args.push("--draft");
|
|
1584
|
+
return run2(args);
|
|
1585
|
+
}
|
|
1586
|
+
async function getPRForBranch(headBranch) {
|
|
1587
|
+
const { exitCode, stdout } = await run2([
|
|
1588
|
+
"pr",
|
|
1589
|
+
"list",
|
|
1590
|
+
"--head",
|
|
1591
|
+
headBranch,
|
|
1592
|
+
"--state",
|
|
1593
|
+
"open",
|
|
1594
|
+
"--json",
|
|
1595
|
+
"number,url,title,state",
|
|
1596
|
+
"--limit",
|
|
1597
|
+
"1"
|
|
1598
|
+
]);
|
|
1599
|
+
if (exitCode !== 0)
|
|
1600
|
+
return null;
|
|
1601
|
+
try {
|
|
1602
|
+
const prs = JSON.parse(stdout.trim());
|
|
1603
|
+
return prs.length > 0 ? prs[0] : null;
|
|
1604
|
+
} catch {
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
async function getMergedPRForBranch(headBranch) {
|
|
1609
|
+
const { exitCode, stdout } = await run2([
|
|
1610
|
+
"pr",
|
|
1611
|
+
"list",
|
|
1612
|
+
"--head",
|
|
1613
|
+
headBranch,
|
|
1614
|
+
"--state",
|
|
1615
|
+
"merged",
|
|
1616
|
+
"--json",
|
|
1617
|
+
"number,url,title,state",
|
|
1618
|
+
"--limit",
|
|
1619
|
+
"1"
|
|
1620
|
+
]);
|
|
1621
|
+
if (exitCode !== 0)
|
|
1622
|
+
return null;
|
|
1623
|
+
try {
|
|
1624
|
+
const prs = JSON.parse(stdout.trim());
|
|
1625
|
+
return prs.length > 0 ? prs[0] : null;
|
|
1626
|
+
} catch {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1691
1631
|
// src/utils/remote.ts
|
|
1692
1632
|
function parseRepoFromUrl(url) {
|
|
1693
1633
|
const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
@@ -1843,6 +1783,24 @@ var setup_default = defineCommand4({
|
|
|
1843
1783
|
// src/commands/start.ts
|
|
1844
1784
|
import { defineCommand as defineCommand5 } from "citty";
|
|
1845
1785
|
import pc8 from "picocolors";
|
|
1786
|
+
|
|
1787
|
+
// src/utils/branch.ts
|
|
1788
|
+
var DEFAULT_PREFIXES = ["feature", "fix", "docs", "chore", "test", "refactor"];
|
|
1789
|
+
function hasPrefix(branchName, prefixes = DEFAULT_PREFIXES) {
|
|
1790
|
+
return prefixes.some((p) => branchName.startsWith(`${p}/`));
|
|
1791
|
+
}
|
|
1792
|
+
function formatBranchName(prefix, name) {
|
|
1793
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1794
|
+
return `${prefix}/${sanitized}`;
|
|
1795
|
+
}
|
|
1796
|
+
function isValidBranchName(name) {
|
|
1797
|
+
return /^[a-zA-Z0-9._/-]+$/.test(name) && !name.startsWith("/") && !name.endsWith("/");
|
|
1798
|
+
}
|
|
1799
|
+
function looksLikeNaturalLanguage(input) {
|
|
1800
|
+
return input.includes(" ") && !input.includes("/");
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/commands/start.ts
|
|
1846
1804
|
var start_default = defineCommand5({
|
|
1847
1805
|
meta: {
|
|
1848
1806
|
name: "start",
|
|
@@ -2040,7 +1998,7 @@ import { defineCommand as defineCommand7 } from "citty";
|
|
|
2040
1998
|
import pc10 from "picocolors";
|
|
2041
1999
|
async function performSquashMerge(origin, baseBranch, featureBranch, options) {
|
|
2042
2000
|
info(`Checking out ${pc10.bold(baseBranch)}...`);
|
|
2043
|
-
const coResult = await
|
|
2001
|
+
const coResult = await checkoutBranch(baseBranch);
|
|
2044
2002
|
if (coResult.exitCode !== 0) {
|
|
2045
2003
|
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2046
2004
|
process.exit(1);
|
|
@@ -2162,31 +2120,8 @@ var submit_default = defineCommand7({
|
|
|
2162
2120
|
return;
|
|
2163
2121
|
}
|
|
2164
2122
|
if (action === SAVE_NEW_BRANCH) {
|
|
2165
|
-
|
|
2166
|
-
const
|
|
2167
|
-
let newBranchName = description;
|
|
2168
|
-
if (!args["no-ai"] && looksLikeNaturalLanguage(description)) {
|
|
2169
|
-
const spinner = createSpinner("Generating branch name suggestion...");
|
|
2170
|
-
const suggested = await suggestBranchName(description, args.model);
|
|
2171
|
-
if (suggested) {
|
|
2172
|
-
spinner.success("Branch name suggestion ready.");
|
|
2173
|
-
console.log(`
|
|
2174
|
-
${pc10.dim("AI suggestion:")} ${pc10.bold(pc10.cyan(suggested))}`);
|
|
2175
|
-
const accepted = await confirmPrompt(`Use ${pc10.bold(suggested)} as your branch name?`);
|
|
2176
|
-
newBranchName = accepted ? suggested : await inputPrompt("Enter branch name", description);
|
|
2177
|
-
} else {
|
|
2178
|
-
spinner.fail("AI did not return a suggestion.");
|
|
2179
|
-
newBranchName = await inputPrompt("Enter branch name", description);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
if (!hasPrefix(newBranchName, config.branchPrefixes)) {
|
|
2183
|
-
const prefix = await selectPrompt(`Choose a branch type for ${pc10.bold(newBranchName)}:`, config.branchPrefixes);
|
|
2184
|
-
newBranchName = formatBranchName(prefix, newBranchName);
|
|
2185
|
-
}
|
|
2186
|
-
if (!isValidBranchName(newBranchName)) {
|
|
2187
|
-
error("Invalid branch name. Use only alphanumeric characters, dots, hyphens, underscores, and slashes.");
|
|
2188
|
-
process.exit(1);
|
|
2189
|
-
}
|
|
2123
|
+
const suggestedName = currentBranch.replace(/^(feature|fix|docs|chore|test|refactor)\//, "$1/new-");
|
|
2124
|
+
const newBranchName = await inputPrompt("New branch name", suggestedName !== currentBranch ? suggestedName : `${currentBranch}-v2`);
|
|
2190
2125
|
const renameResult = await renameBranch(currentBranch, newBranchName);
|
|
2191
2126
|
if (renameResult.exitCode !== 0) {
|
|
2192
2127
|
error(`Failed to rename branch: ${renameResult.stderr}`);
|
|
@@ -2212,7 +2147,7 @@ var submit_default = defineCommand7({
|
|
|
2212
2147
|
const syncSource = getSyncSource(config);
|
|
2213
2148
|
info(`Switching to ${pc10.bold(baseBranch)} and syncing...`);
|
|
2214
2149
|
await fetchRemote(syncSource.remote);
|
|
2215
|
-
const coResult = await
|
|
2150
|
+
const coResult = await checkoutBranch(baseBranch);
|
|
2216
2151
|
if (coResult.exitCode !== 0) {
|
|
2217
2152
|
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2218
2153
|
process.exit(1);
|
|
@@ -2426,7 +2361,7 @@ var sync_default = defineCommand8({
|
|
|
2426
2361
|
if (!ok)
|
|
2427
2362
|
process.exit(0);
|
|
2428
2363
|
}
|
|
2429
|
-
const coResult = await
|
|
2364
|
+
const coResult = await checkoutBranch(baseBranch);
|
|
2430
2365
|
if (coResult.exitCode !== 0) {
|
|
2431
2366
|
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2432
2367
|
process.exit(1);
|
|
@@ -2441,14 +2376,14 @@ var sync_default = defineCommand8({
|
|
|
2441
2376
|
const mainDiv = await getDivergence(config.mainBranch, `${origin}/${config.mainBranch}`);
|
|
2442
2377
|
if (mainDiv.behind > 0) {
|
|
2443
2378
|
info(`Also syncing ${pc11.bold(config.mainBranch)}...`);
|
|
2444
|
-
const mainCoResult = await
|
|
2379
|
+
const mainCoResult = await checkoutBranch(config.mainBranch);
|
|
2445
2380
|
if (mainCoResult.exitCode === 0) {
|
|
2446
2381
|
const mainPullResult = await pullBranch(origin, config.mainBranch);
|
|
2447
2382
|
if (mainPullResult.exitCode === 0) {
|
|
2448
2383
|
success(`✅ ${config.mainBranch} is now in sync with ${origin}/${config.mainBranch}`);
|
|
2449
2384
|
}
|
|
2450
2385
|
}
|
|
2451
|
-
await
|
|
2386
|
+
await checkoutBranch(baseBranch);
|
|
2452
2387
|
}
|
|
2453
2388
|
}
|
|
2454
2389
|
}
|
|
@@ -2569,7 +2504,7 @@ var update_default = defineCommand9({
|
|
|
2569
2504
|
warn("Discarding local changes...");
|
|
2570
2505
|
}
|
|
2571
2506
|
await fetchRemote(syncSource.remote);
|
|
2572
|
-
const coResult = await
|
|
2507
|
+
const coResult = await checkoutBranch(baseBranch);
|
|
2573
2508
|
if (coResult.exitCode !== 0) {
|
|
2574
2509
|
error(`Failed to checkout ${baseBranch}: ${coResult.stderr}`);
|
|
2575
2510
|
process.exit(1);
|
|
@@ -2681,7 +2616,7 @@ import pc14 from "picocolors";
|
|
|
2681
2616
|
// package.json
|
|
2682
2617
|
var package_default = {
|
|
2683
2618
|
name: "contribute-now",
|
|
2684
|
-
version: "0.2.0-dev.
|
|
2619
|
+
version: "0.2.0-dev.7c81c96",
|
|
2685
2620
|
description: "Git workflow CLI for squash-merge two-branch models. Keeps dev in sync with main after squash merges.",
|
|
2686
2621
|
type: "module",
|
|
2687
2622
|
bin: {
|