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.
Files changed (2) hide show
  1. package/dist/index.js +706 -771
  2. 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 pc4 from "picocolors";
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/copilot.ts
120
- import { CopilotClient } from "@github/copilot-sdk";
121
- var CONVENTIONAL_COMMIT_SYSTEM_PROMPT = `Git commit message generator. Format: <type>[!][(<scope>)]: <description>
122
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
123
- 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.
124
- Examples: feat: add user auth | fix(auth): resolve token expiry | feat!: redesign auth API`;
125
- var CLEAN_COMMIT_SYSTEM_PROMPT = `Git commit message generator. EXACT format: <emoji> <type>[!][ (<scope>)]: <description>
126
- Spacing: EMOJI SPACE TYPE [SPACE OPENPAREN SCOPE CLOSEPAREN] COLON SPACE DESCRIPTION
127
- Types: \uD83D\uDCE6 new, \uD83D\uDD27 update, \uD83D\uDDD1️ remove, \uD83D\uDD12 security, ⚙️ setup, chore, \uD83E\uDDEA test, \uD83D\uDCD6 docs, \uD83D\uDE80 release
128
- Rules: breaking (!) only for new/update/remove/security; imperative mood; max 72 chars; lowercase start; scope optional. Return ONLY the message line.
129
- Correct: \uD83D\uDCE6 new: add user auth | \uD83D\uDD27 update (api): improve error handling | ⚙️ setup (ci): configure github actions
130
- WRONG: ⚙️setup(ci): ... | \uD83D\uDD27 update(api): ... ← always space before scope parenthesis`;
131
- function getGroupingSystemPrompt(convention) {
132
- const conventionBlock = convention === "conventional" ? `Use Conventional Commit format: <type>[(<scope>)]: <description>
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
- 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.
157
- Output format: <prefix>/<kebab-case-name>
158
- Valid prefixes: feature, fix, docs, chore, test, refactor
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
- var CONFLICT_RESOLUTION_SYSTEM_PROMPT = `Git merge conflict advisor. Explain each side, suggest resolution strategy. Never auto-resolve — guidance only. Be concise and actionable.`;
183
- function suppressSubprocessWarnings() {
184
- process.env.NODE_NO_WARNINGS = "1";
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 withTimeout(promise, ms) {
187
- return new Promise((resolve, reject) => {
188
- const timer = setTimeout(() => reject(new Error(`Copilot request timed out after ${ms / 1000}s`)), ms);
189
- promise.then((val) => {
190
- clearTimeout(timer);
191
- resolve(val);
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
- var COPILOT_TIMEOUT_MS = 30000;
199
- var COPILOT_LONG_TIMEOUT_MS = 90000;
200
- async function checkCopilotAvailable() {
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
- } catch (err) {
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
- var _managedClient = null;
225
- var _clientStarted = false;
226
- async function getManagedClient() {
227
- if (!_managedClient || !_clientStarted) {
228
- suppressSubprocessWarnings();
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 callCopilot(systemMessage, userMessage, model, timeoutMs = COPILOT_TIMEOUT_MS) {
248
- const client = await getManagedClient();
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 getCommitSystemPrompt(convention) {
265
- if (convention === "conventional")
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 extractJson(raw) {
270
- let text2 = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
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 generateCommitMessage(diff, stagedFiles, model, convention = "clean-commit") {
299
- try {
300
- const multiFileHint = stagedFiles.length > 1 ? `
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 generatePRDescription(commits, diff, model, convention = "clean-commit") {
316
- try {
317
- const userMessage = `Generate a PR description for these changes:
318
-
319
- Commits:
320
- ${commits.join(`
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 suggestBranchName(description, model) {
335
- try {
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 regenerateAllGroupMessages(groups, diffs, model, convention = "clean-commit") {
389
- const groupSummary = groups.map((g, i) => `Group ${i + 1}: [${g.files.join(", ")}]`).join(`
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 regenerateGroupMessage(files, diffs, model, convention = "clean-commit") {
415
- try {
416
- const userMessage = `Generate a single commit message for these files:
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 checkGhInstalled() {
443
- try {
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 checkGhAuth() {
451
- try {
452
- const { exitCode } = await run(["auth", "status"]);
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
- var SAFE_SLUG = /^[\w.-]+$/;
459
- async function checkRepoPermissions(owner, repo) {
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 null;
465
- try {
466
- return JSON.parse(stdout.trim());
467
- } catch {
468
- return null;
469
- }
192
+ return [];
193
+ return stdout.trim().split(`
194
+ `).filter(Boolean);
470
195
  }
471
- async function isRepoFork() {
472
- const { exitCode, stdout } = await run(["repo", "view", "--json", "isFork", "-q", ".isFork"]);
196
+ async function getChangedFiles() {
197
+ const { exitCode, stdout } = await run(["status", "--porcelain"]);
473
198
  if (exitCode !== 0)
474
- return null;
475
- const val = stdout.trim();
476
- if (val === "true")
477
- return true;
478
- if (val === "false")
479
- return false;
480
- return null;
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 getCurrentRepoInfo() {
211
+ async function getDivergence(branch, base) {
483
212
  const { exitCode, stdout } = await run([
484
- "repo",
485
- "view",
486
- "--json",
487
- "nameWithOwner",
488
- "-q",
489
- ".nameWithOwner"
213
+ "rev-list",
214
+ "--left-right",
215
+ "--count",
216
+ `${base}...${branch}`
490
217
  ]);
491
218
  if (exitCode !== 0)
492
- return null;
493
- const nameWithOwner = stdout.trim();
494
- if (!nameWithOwner)
495
- return null;
496
- const [owner, repo] = nameWithOwner.split("/");
497
- if (!owner || !repo)
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 getPRForBranch(headBranch) {
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 null;
537
- try {
538
- const prs = JSON.parse(stdout.trim());
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 getMergedPRForBranch(headBranch) {
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 null;
559
- try {
560
- const prs = JSON.parse(stdout.trim());
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 run2(["branch", "-d", branch]);
241
+ return run(["branch", "-d", branch]);
706
242
  }
707
243
  async function forceDeleteBranch(branch) {
708
- return run2(["branch", "-D", branch]);
244
+ return run(["branch", "-D", branch]);
709
245
  }
710
246
  async function renameBranch(oldName, newName) {
711
- return run2(["branch", "-m", oldName, newName]);
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 run2(["rev-list", "--count", `${trackingRef}..${branch}`]);
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 run2(["push", remote, "--delete", branch]);
257
+ return run(["push", remote, "--delete", branch]);
722
258
  }
723
259
  async function mergeSquash(branch) {
724
- return run2(["merge", "--squash", branch]);
260
+ return run(["merge", "--squash", branch]);
725
261
  }
726
262
  async function pushBranch(remote, branch) {
727
- return run2(["push", remote, branch]);
263
+ return run(["push", remote, branch]);
728
264
  }
729
265
  async function pruneRemote(remote) {
730
- return run2(["remote", "prune", remote]);
266
+ return run(["remote", "prune", remote]);
731
267
  }
732
268
  async function commitWithMessage(message) {
733
- return run2(["commit", "-m", message]);
269
+ return run(["commit", "-m", message]);
734
270
  }
735
271
  async function getLogDiff(base, head) {
736
- const { stdout } = await run2(["diff", `${base}...${head}`]);
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 run2(["log", `${base}..${head}`, "--oneline"]);
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 run2(["pull", remote, branch]);
283
+ return run(["pull", remote, branch]);
748
284
  }
749
285
  async function stageFiles(files) {
750
- return run2(["add", "--", ...files]);
286
+ return run(["add", "--", ...files]);
751
287
  }
752
288
  async function unstageFiles(files) {
753
- return run2(["reset", "HEAD", "--", ...files]);
289
+ return run(["reset", "HEAD", "--", ...files]);
754
290
  }
755
291
  async function stageAll() {
756
- return run2(["add", "-A"]);
292
+ return run(["add", "-A"]);
757
293
  }
758
294
  async function getFullDiffForFiles(files) {
759
295
  const [unstaged, staged, untracked] = await Promise.all([
760
- run2(["diff", "--", ...files]),
761
- run2(["diff", "--cached", "--", ...files]),
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 run2(["ls-files", "--others", "--exclude-standard"]);
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 run2(["status", "--porcelain"]);
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 ${pc4.bold(String(localWork.unpushedCommits))} local commit${localWork.unpushedCommits !== 1 ? "s" : ""} not pushed.`);
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(`${pc4.bold(currentBranch)} has local changes. What would you like to do?`, [SAVE_NEW_BRANCH, DISCARD, CANCEL]);
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
- if (!config)
973
- return "skipped";
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 ${pc4.bold(currentBranch)} → ${pc4.bold(newBranchName)}`);
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(` ${pc4.bold(`git checkout ${newBranchName} && git rebase --continue`)}`);
475
+ info(` ${pc3.bold(`git checkout ${newBranchName} && git rebase --continue`)}`);
1012
476
  } else {
1013
- success(`Rebased ${pc4.bold(newBranchName)} onto ${pc4.bold(syncSource2.ref)}.`);
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 ${pc4.bold(baseBranch)} with ${pc4.bold(syncSource2.ref)}.`);
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 ${pc4.bold(baseBranch)} and syncing...`);
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 ${pc4.bold(baseBranch)} with ${pc4.bold(syncSource.ref)}.`);
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
- ${pc4.bold("Merged branches to delete:")}`);
542
+ ${pc3.bold("Merged branches to delete:")}`);
1091
543
  for (const b of mergedCandidates) {
1092
- const marker = b === currentBranch ? pc4.yellow(" (current)") : "";
1093
- console.log(` ${pc4.dim("•")} ${b}${marker}`);
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 ${pc4.bold(String(mergedCandidates.length))} merged branch${mergedCandidates.length !== 1 ? "es" : ""}?`);
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 ${pc4.bold(branch)}`);
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
- ${pc4.bold("Stale branches (remote deleted, likely squash-merged):")}`);
576
+ ${pc3.bold("Stale branches (remote deleted, likely squash-merged):")}`);
1125
577
  for (const b of goneCandidates) {
1126
- const marker = b === currentBranch ? pc4.yellow(" (current)") : "";
1127
- console.log(` ${pc4.dim("•")} ${b}${marker}`);
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 ${pc4.bold(String(goneCandidates.length))} stale branch${goneCandidates.length !== 1 ? "es" : ""}?`);
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 ${pc4.bold(branch)}`);
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 ${pc4.bold(finalBranch)}. Run ${pc4.bold("contrib start")} to begin a new feature.`);
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
- // src/commands/commit.ts
1168
- import { defineCommand as defineCommand2 } from "citty";
1169
- import pc5 from "picocolors";
958
+ Files: ${files.join(", ")}
1170
959
 
1171
- // src/utils/convention.ts
1172
- 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;
1173
- 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}$/;
1174
- var CONVENTION_LABELS = {
1175
- conventional: "Conventional Commits",
1176
- "clean-commit": "Clean Commit (by WGTech Labs)",
1177
- none: "No convention"
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
- function getValidationError(convention) {
1206
- if (convention === "none")
1207
- return [];
1208
- return [
1209
- `Commit message does not follow ${CONVENTION_LABELS[convention]} format.`,
1210
- ...CONVENTION_FORMAT_HINTS[convention]
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 checkoutBranch2(baseBranch);
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
- info(pc10.dim("Tip: Describe what you're working on in plain English and we'll generate a branch name."));
2166
- const description = await inputPrompt("What are you working on?");
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 checkoutBranch2(baseBranch);
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 checkoutBranch2(baseBranch);
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 checkoutBranch2(config.mainBranch);
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 checkoutBranch2(baseBranch);
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 checkoutBranch2(baseBranch);
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.33be40f",
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: {