contribute-now 0.2.0-dev.0de9dbd → 0.2.0-dev.2621ffa

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