genai-commit 0.0.0

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.
@@ -0,0 +1,829 @@
1
+ // src/utils/exec.ts
2
+ import { spawn } from "child_process";
3
+ async function execCommand(command, args, options = {}) {
4
+ const { input, timeout = 12e4, cwd } = options;
5
+ return new Promise((resolve, reject) => {
6
+ const proc = spawn(command, args, {
7
+ cwd,
8
+ stdio: ["pipe", "pipe", "pipe"]
9
+ });
10
+ let stdout = "";
11
+ let stderr = "";
12
+ let killed = false;
13
+ const timer = setTimeout(() => {
14
+ killed = true;
15
+ proc.kill("SIGTERM");
16
+ reject(new Error(`Command timed out after ${timeout}ms`));
17
+ }, timeout);
18
+ proc.stdout.on("data", (data) => {
19
+ stdout += data.toString();
20
+ });
21
+ proc.stderr.on("data", (data) => {
22
+ stderr += data.toString();
23
+ });
24
+ proc.on("close", (code) => {
25
+ clearTimeout(timer);
26
+ if (!killed) {
27
+ resolve({
28
+ stdout,
29
+ stderr,
30
+ exitCode: code ?? 0
31
+ });
32
+ }
33
+ });
34
+ proc.on("error", (err) => {
35
+ clearTimeout(timer);
36
+ reject(err);
37
+ });
38
+ if (input) {
39
+ proc.stdin.write(input);
40
+ proc.stdin.end();
41
+ }
42
+ });
43
+ }
44
+ async function execSimple(command, args, options = {}) {
45
+ const result = await execCommand(command, args, options);
46
+ if (result.exitCode !== 0) {
47
+ throw new Error(`Command failed with exit code ${result.exitCode}: ${result.stderr}`);
48
+ }
49
+ return result.stdout;
50
+ }
51
+
52
+ // src/prompts/templates.ts
53
+ var CLAUDE_COMMIT_PROMPT = `You are a commit message generator.
54
+
55
+ Analyze the git diff and generate commit messages.
56
+
57
+ Rules:
58
+ - Follow Conventional Commits: type(scope): description
59
+ - Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build
60
+ - Title under 72 characters
61
+ - Split into multiple commits when changes are logically separate
62
+ - Group related file changes into single commit when appropriate
63
+ - NEVER include Jira ticket numbers (like AS-123, PROJ-456) in titles or messages
64
+ - Jira tickets are assigned separately via the [t] option
65
+
66
+ Language settings (check the input for TITLE_LANG and MESSAGE_LANG):
67
+ - TITLE_LANG: Language for commit title (after the type(scope): prefix)
68
+ - MESSAGE_LANG: Language for detailed message
69
+ - Default: title in English, message in Korean
70
+
71
+ Examples:
72
+ - Title (en): "feat(auth): add OAuth login support"
73
+ - Title (ko): "feat(auth): OAuth \uB85C\uADF8\uC778 \uC9C0\uC6D0 \uCD94\uAC00"
74
+ - Message (en): "Implemented OAuth 2.0 flow with Google provider"
75
+ - Message (ko): "Google OAuth 2.0 \uC778\uC99D \uD750\uB984 \uAD6C\uD604"
76
+
77
+ Output ONLY valid JSON matching the required schema. No other text.`;
78
+ var CLAUDE_REGROUP_PROMPT = `You are a commit message regrouper.
79
+
80
+ Your task is to merge commits that share the same Jira ticket URL into a single commit.
81
+
82
+ Rules:
83
+ 1. Commits with the SAME Jira URL must be merged into ONE commit
84
+ 2. Combine all files from the merged commits
85
+ 3. Create a new summarized title that covers all merged changes
86
+ 4. Create a new summarized message that describes all combined changes
87
+ 5. Keep the Jira URL in the merged commit (jira_url field)
88
+ 6. Commits WITHOUT a Jira URL should remain unchanged
89
+ 7. Follow Conventional Commits: type(scope): description
90
+ 8. Title under 72 characters
91
+
92
+ Language settings (check the input for TITLE_LANG and MESSAGE_LANG):
93
+ - TITLE_LANG: Language for commit title
94
+ - MESSAGE_LANG: Language for detailed message
95
+
96
+ Output ONLY valid JSON matching the required schema. No other text.`;
97
+ var CURSOR_COMMIT_PROMPT = `You are a commit message generator. Analyze git changes and generate commit messages.
98
+
99
+ IMPORTANT: You MUST reply ONLY in the EXACT format below. No markdown, no explanation, no other text.
100
+
101
+ ===COMMIT===
102
+ FILES: file1.ts, file2.ts
103
+ TITLE: type(scope): description
104
+ MESSAGE: detailed message here
105
+
106
+ RULES:
107
+ 1. Each commit block MUST start with ===COMMIT=== on its own line
108
+ 2. FILES: comma-separated file paths (use ONLY files from the input, NEVER invent files)
109
+ 3. TITLE: follow Conventional Commits format, under 72 characters
110
+ 4. MESSAGE: detailed description in specified language
111
+ 5. You may output multiple ===COMMIT=== blocks for separate logical changes
112
+ 6. Group related files into the same commit
113
+ 7. NEVER include Jira ticket numbers in titles or messages
114
+
115
+ Conventional Commit Types:
116
+ - feat: new feature
117
+ - fix: bug fix
118
+ - docs: documentation
119
+ - style: formatting (no code change)
120
+ - refactor: code restructuring
121
+ - test: adding tests
122
+ - chore: maintenance
123
+ - perf: performance improvement
124
+ - ci: CI/CD changes
125
+ - build: build system changes
126
+
127
+ Language Settings (check input for TITLE_LANG and MESSAGE_LANG):
128
+ - TITLE_LANG: en = English title, ko = Korean title
129
+ - MESSAGE_LANG: en = English message, ko = Korean message
130
+
131
+ Example Output:
132
+ ===COMMIT===
133
+ FILES: src/auth/login.ts, src/auth/logout.ts
134
+ TITLE: feat(auth): add OAuth login support
135
+ MESSAGE: OAuth 2.0 \uC778\uC99D \uD750\uB984\uC744 \uAD6C\uD604\uD558\uACE0 \uB85C\uADF8\uC544\uC6C3 \uCC98\uB9AC\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.
136
+ ===COMMIT===
137
+ FILES: src/utils/helper.ts
138
+ TITLE: chore(utils): add helper functions
139
+ MESSAGE: \uACF5\uD1B5 \uC720\uD2F8\uB9AC\uD2F0 \uD568\uC218\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`;
140
+ var CURSOR_REGROUP_PROMPT = `You are a commit message regrouper. Merge commits with the same Jira ticket into a single commit.
141
+
142
+ IMPORTANT: You MUST reply ONLY in the EXACT format below. No markdown, no explanation, no other text.
143
+
144
+ ===COMMIT===
145
+ FILES: file1.ts, file2.ts
146
+ TITLE: type(scope): description (JIRA-123)
147
+ MESSAGE: detailed message here
148
+
149
+ RULES:
150
+ 1. Merge all commits with the SAME Jira key into ONE commit
151
+ 2. Combine all files from merged commits (no duplicates)
152
+ 3. Create a summarized title covering all merged changes
153
+ 4. Add the Jira key at the end of the title: "description (AS-123)"
154
+ 5. Create a summarized message describing all combined changes
155
+ 6. Follow Conventional Commits format
156
+ 7. Title under 72 characters
157
+
158
+ Language Settings (check input for TITLE_LANG and MESSAGE_LANG):
159
+ - TITLE_LANG: en = English title, ko = Korean title
160
+ - MESSAGE_LANG: en = English message, ko = Korean message
161
+
162
+ Example Output:
163
+ ===COMMIT===
164
+ FILES: src/components/Button.tsx, src/components/Input.tsx
165
+ TITLE: feat(ui): add Button and Input components (AS-123)
166
+ MESSAGE: Button\uACFC Input \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4.`;
167
+ var COMMIT_SCHEMA = {
168
+ type: "object",
169
+ properties: {
170
+ commits: {
171
+ type: "array",
172
+ items: {
173
+ type: "object",
174
+ properties: {
175
+ files: {
176
+ type: "array",
177
+ items: { type: "string" }
178
+ },
179
+ title: { type: "string" },
180
+ message: { type: "string" },
181
+ jira_key: { type: "string" }
182
+ },
183
+ required: ["files", "title", "message"]
184
+ }
185
+ }
186
+ },
187
+ required: ["commits"]
188
+ };
189
+ function getPromptTemplate(provider, category) {
190
+ if (provider === "claude") {
191
+ return category === "commit" ? CLAUDE_COMMIT_PROMPT : CLAUDE_REGROUP_PROMPT;
192
+ } else {
193
+ return category === "commit" ? CURSOR_COMMIT_PROMPT : CURSOR_REGROUP_PROMPT;
194
+ }
195
+ }
196
+ function getJsonSchema() {
197
+ return COMMIT_SCHEMA;
198
+ }
199
+
200
+ // src/parser/json.ts
201
+ function parseJsonResponse(raw) {
202
+ try {
203
+ const parsed = JSON.parse(raw);
204
+ if (!parsed.commits || !Array.isArray(parsed.commits)) {
205
+ throw new Error("Response does not contain commits array");
206
+ }
207
+ const commits = parsed.commits.map((c) => ({
208
+ files: Array.isArray(c.files) ? c.files : [],
209
+ title: String(c.title || ""),
210
+ message: String(c.message || ""),
211
+ jiraKey: c.jira_key ? String(c.jira_key) : void 0
212
+ }));
213
+ const validCommits = commits.filter(
214
+ (c) => c.files.length > 0 && c.title.length > 0
215
+ );
216
+ if (validCommits.length === 0) {
217
+ throw new Error("No valid commits found in response");
218
+ }
219
+ return { commits: validCommits };
220
+ } catch (error) {
221
+ if (error instanceof SyntaxError) {
222
+ throw new Error(`Invalid JSON response: ${raw.substring(0, 200)}...`);
223
+ }
224
+ throw error;
225
+ }
226
+ }
227
+
228
+ // src/config/defaults.ts
229
+ var DEFAULT_CONFIG = {
230
+ maxInputSize: 3e4,
231
+ maxDiffSize: 15e3,
232
+ timeout: 12e4,
233
+ // 120 seconds
234
+ treeDepth: 3,
235
+ maxRetries: 2,
236
+ titleLang: "en",
237
+ messageLang: "ko"
238
+ };
239
+ var CURSOR_DEFAULT_MODEL = "gemini-3-flash";
240
+ var CLAUDE_DEFAULT_MODEL = "haiku";
241
+
242
+ // src/providers/claude.ts
243
+ var ClaudeCodeProvider = class {
244
+ name = "claude-code";
245
+ sessionId;
246
+ timeout;
247
+ model;
248
+ constructor(options) {
249
+ this.timeout = options?.timeout ?? 12e4;
250
+ this.model = options?.model ?? CLAUDE_DEFAULT_MODEL;
251
+ }
252
+ async generate(input, promptType) {
253
+ const prompt = getPromptTemplate("claude", promptType);
254
+ const schema = getJsonSchema();
255
+ const args = [
256
+ "-p",
257
+ "--model",
258
+ this.model,
259
+ "--output-format",
260
+ "json",
261
+ "--json-schema",
262
+ JSON.stringify(schema),
263
+ "--append-system-prompt",
264
+ prompt
265
+ ];
266
+ if (this.sessionId) {
267
+ args.push("--resume", this.sessionId);
268
+ }
269
+ const result = await execCommand("claude", args, {
270
+ input,
271
+ timeout: this.timeout
272
+ });
273
+ if (result.exitCode !== 0) {
274
+ throw new Error(`Claude CLI failed: ${result.stderr}`);
275
+ }
276
+ try {
277
+ const parsed = JSON.parse(result.stdout);
278
+ this.sessionId = parsed.session_id;
279
+ return {
280
+ raw: JSON.stringify(parsed.structured_output),
281
+ sessionId: this.sessionId
282
+ };
283
+ } catch (error) {
284
+ throw new Error(`Failed to parse Claude response: ${result.stdout}`);
285
+ }
286
+ }
287
+ parseResponse(response) {
288
+ return parseJsonResponse(response.raw);
289
+ }
290
+ async login() {
291
+ console.log("Setting up Claude Code authentication...");
292
+ await execCommand("claude", ["setup-token"], { timeout: 6e4 });
293
+ }
294
+ async status() {
295
+ try {
296
+ const version = await execSimple("claude", ["--version"], { timeout: 1e4 });
297
+ return {
298
+ available: true,
299
+ version: version.trim(),
300
+ details: "Claude Code CLI is available"
301
+ };
302
+ } catch {
303
+ return {
304
+ available: false,
305
+ details: "Claude Code CLI not found. Install it first."
306
+ };
307
+ }
308
+ }
309
+ getSessionId() {
310
+ return this.sessionId;
311
+ }
312
+ clearSession() {
313
+ this.sessionId = void 0;
314
+ }
315
+ };
316
+
317
+ // src/parser/delimiter.ts
318
+ var COMMIT_DELIMITER = "===COMMIT===";
319
+ function parseCommitBlock(block) {
320
+ const lines = block.split("\n");
321
+ let files = "";
322
+ let title = "";
323
+ let message = "";
324
+ for (const line of lines) {
325
+ const trimmedLine = line.trim();
326
+ if (trimmedLine.startsWith("FILES:")) {
327
+ files = trimmedLine.substring(6).trim();
328
+ } else if (trimmedLine.startsWith("TITLE:")) {
329
+ title = trimmedLine.substring(6).trim();
330
+ } else if (trimmedLine.startsWith("MESSAGE:")) {
331
+ message = trimmedLine.substring(8).trim();
332
+ }
333
+ }
334
+ if (!files || !title) {
335
+ return null;
336
+ }
337
+ const fileList = files.split(",").map((f) => f.trim()).filter((f) => f.length > 0);
338
+ if (fileList.length === 0) {
339
+ return null;
340
+ }
341
+ return {
342
+ files: fileList,
343
+ title,
344
+ message: message || ""
345
+ };
346
+ }
347
+ function parseDelimiterResponse(raw) {
348
+ const commits = [];
349
+ const blocks = raw.split(COMMIT_DELIMITER).slice(1);
350
+ for (const block of blocks) {
351
+ const trimmedBlock = block.trim();
352
+ if (trimmedBlock) {
353
+ const commit = parseCommitBlock(trimmedBlock);
354
+ if (commit) {
355
+ commits.push(commit);
356
+ }
357
+ }
358
+ }
359
+ if (commits.length === 0) {
360
+ throw new Error(
361
+ `No valid commits found in response. Raw response:
362
+ ${raw.substring(0, 500)}...`
363
+ );
364
+ }
365
+ return { commits };
366
+ }
367
+ function toDelimiterFormat(commits) {
368
+ return commits.map(
369
+ (c) => `${COMMIT_DELIMITER}
370
+ FILES: ${c.files.join(", ")}
371
+ TITLE: ${c.title}
372
+ MESSAGE: ${c.message}`
373
+ ).join("\n");
374
+ }
375
+
376
+ // src/providers/cursor.ts
377
+ var CursorCLIProvider = class {
378
+ name = "cursor-cli";
379
+ timeout;
380
+ model;
381
+ constructor(options) {
382
+ this.timeout = options?.timeout ?? 12e4;
383
+ this.model = options?.model ?? CURSOR_DEFAULT_MODEL;
384
+ }
385
+ async generate(input, promptType) {
386
+ const prompt = getPromptTemplate("cursor", promptType);
387
+ const fullInput = `${prompt}
388
+
389
+ ---
390
+
391
+ ${input}`;
392
+ const result = await execCommand(
393
+ "cursor",
394
+ ["agent", "-p", "--model", this.model, "--output-format", "text"],
395
+ {
396
+ input: fullInput,
397
+ timeout: this.timeout
398
+ }
399
+ );
400
+ if (result.exitCode !== 0) {
401
+ throw new Error(`Cursor CLI failed: ${result.stderr}`);
402
+ }
403
+ return { raw: result.stdout };
404
+ }
405
+ parseResponse(response) {
406
+ return parseDelimiterResponse(response.raw);
407
+ }
408
+ async login() {
409
+ console.log("Logging in to Cursor...");
410
+ await execCommand("cursor", ["agent", "login"], { timeout: 6e4 });
411
+ }
412
+ async status() {
413
+ try {
414
+ const result = await execCommand("cursor", ["agent", "status"], { timeout: 1e4 });
415
+ return {
416
+ available: true,
417
+ details: result.stdout.trim() || "Cursor CLI is available"
418
+ };
419
+ } catch {
420
+ return {
421
+ available: false,
422
+ details: "Cursor CLI not available. Install it first."
423
+ };
424
+ }
425
+ }
426
+ getSessionId() {
427
+ return void 0;
428
+ }
429
+ clearSession() {
430
+ }
431
+ };
432
+
433
+ // src/providers/index.ts
434
+ function createProvider(type, options) {
435
+ switch (type) {
436
+ case "claude-code":
437
+ return new ClaudeCodeProvider(options);
438
+ case "cursor-cli":
439
+ return new CursorCLIProvider(options);
440
+ default:
441
+ throw new Error(`Unknown provider: ${type}`);
442
+ }
443
+ }
444
+ function isValidProviderType(type) {
445
+ return type === "claude-code" || type === "cursor-cli";
446
+ }
447
+
448
+ // src/git/status.ts
449
+ import simpleGit from "simple-git";
450
+ var gitInstance = null;
451
+ function getGit(cwd) {
452
+ if (!gitInstance || cwd) {
453
+ gitInstance = simpleGit(cwd);
454
+ }
455
+ return gitInstance;
456
+ }
457
+ async function isGitRepository(cwd) {
458
+ try {
459
+ const git = getGit(cwd);
460
+ await git.revparse(["--is-inside-work-tree"]);
461
+ return true;
462
+ } catch {
463
+ return false;
464
+ }
465
+ }
466
+ async function getCurrentBranch(cwd) {
467
+ const git = getGit(cwd);
468
+ const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
469
+ return branch.trim();
470
+ }
471
+ async function getGitStatus(cwd) {
472
+ const git = getGit(cwd);
473
+ const status = await git.status();
474
+ const changes = [];
475
+ const stats = {
476
+ added: 0,
477
+ modified: 0,
478
+ deleted: 0,
479
+ renamed: 0,
480
+ untracked: 0,
481
+ total: 0
482
+ };
483
+ for (const file of status.created) {
484
+ changes.push({ file, status: "A" });
485
+ stats.added++;
486
+ }
487
+ for (const file of status.modified) {
488
+ changes.push({ file, status: "M" });
489
+ stats.modified++;
490
+ }
491
+ for (const file of status.deleted) {
492
+ changes.push({ file, status: "D" });
493
+ stats.deleted++;
494
+ }
495
+ for (const file of status.renamed) {
496
+ changes.push({ file: file.to, status: "R" });
497
+ stats.renamed++;
498
+ }
499
+ for (const file of status.not_added) {
500
+ if (!changes.some((c) => c.file === file)) {
501
+ changes.push({ file, status: "M" });
502
+ stats.modified++;
503
+ }
504
+ }
505
+ for (const file of status.files) {
506
+ if (file.index === "?" && file.working_dir === "?") {
507
+ changes.push({ file: file.path, status: "?" });
508
+ stats.untracked++;
509
+ }
510
+ }
511
+ stats.total = stats.added + stats.modified + stats.deleted + stats.renamed + stats.untracked;
512
+ return { changes, stats };
513
+ }
514
+ async function getAllChangedFiles(cwd) {
515
+ const { changes } = await getGitStatus(cwd);
516
+ return new Set(changes.map((c) => c.file));
517
+ }
518
+ async function hasChanges(cwd) {
519
+ const { stats } = await getGitStatus(cwd);
520
+ return stats.total > 0;
521
+ }
522
+
523
+ // src/git/tree.ts
524
+ var DEFAULT_OPTIONS = {
525
+ treeDepth: 3,
526
+ compressionThreshold: 10
527
+ };
528
+ function getExtension(file) {
529
+ const match = file.match(/\.([^./]+)$/);
530
+ return match ? match[1] : "";
531
+ }
532
+ function generateTreeSummary(files, changeType, options = {}) {
533
+ const opts = { ...DEFAULT_OPTIONS, ...options };
534
+ if (files.length === 0) {
535
+ return "";
536
+ }
537
+ if (files.length <= opts.compressionThreshold) {
538
+ return files.map((f) => `${changeType} ${f}`).join("\n");
539
+ }
540
+ const dirGroups = /* @__PURE__ */ new Map();
541
+ const directFiles = [];
542
+ for (const file of files) {
543
+ const parts = file.split("/");
544
+ if (parts.length <= opts.treeDepth) {
545
+ directFiles.push(`${changeType} ${file}`);
546
+ } else {
547
+ const dir = parts.slice(0, opts.treeDepth).join("/");
548
+ const ext = getExtension(file);
549
+ if (!dirGroups.has(dir)) {
550
+ dirGroups.set(dir, { count: 0, extensions: /* @__PURE__ */ new Map() });
551
+ }
552
+ const group = dirGroups.get(dir);
553
+ group.count++;
554
+ if (ext) {
555
+ group.extensions.set(ext, (group.extensions.get(ext) ?? 0) + 1);
556
+ }
557
+ }
558
+ }
559
+ const compressed = [...dirGroups.entries()].map(([dir, group]) => {
560
+ const extSummary = [...group.extensions.entries()].map(([ext, count]) => `${count} *.${ext}`).join(", ");
561
+ if (extSummary) {
562
+ return `${changeType} ${dir}/ [${group.count} files: ${extSummary}]`;
563
+ } else {
564
+ return `${changeType} ${dir}/ [${group.count} files]`;
565
+ }
566
+ });
567
+ return [...directFiles, ...compressed].join("\n");
568
+ }
569
+ function generateFullTreeSummary(branch, changes, options = {}) {
570
+ const opts = { ...DEFAULT_OPTIONS, ...options };
571
+ const added = changes.filter((c) => c.status === "A").map((c) => c.file);
572
+ const modified = changes.filter((c) => c.status === "M").map((c) => c.file);
573
+ const deleted = changes.filter((c) => c.status === "D").map((c) => c.file);
574
+ const renamed = changes.filter((c) => c.status === "R").map((c) => c.file);
575
+ const untracked = changes.filter((c) => c.status === "?").map((c) => c.file);
576
+ const total = added.length + modified.length + deleted.length + renamed.length + untracked.length;
577
+ let output = `=== CHANGE SUMMARY ===
578
+ Branch: ${branch}
579
+ Total: ${total} files
580
+ - Added (A): ${added.length}
581
+ - Modified (M): ${modified.length}
582
+ - Deleted (D): ${deleted.length}
583
+ - Renamed (R): ${renamed.length}
584
+ - Untracked (?): ${untracked.length}
585
+
586
+ === FILE TREE ===
587
+ `;
588
+ if (modified.length > 0) {
589
+ output += `
590
+ --- Modified (${modified.length}) ---
591
+ `;
592
+ output += generateTreeSummary(modified, "M", opts);
593
+ output += "\n";
594
+ }
595
+ if (added.length > 0) {
596
+ output += `
597
+ --- Added (${added.length}) ---
598
+ `;
599
+ output += generateTreeSummary(added, "A", opts);
600
+ output += "\n";
601
+ }
602
+ if (deleted.length > 0) {
603
+ output += `
604
+ --- Deleted (${deleted.length}) ---
605
+ `;
606
+ output += generateTreeSummary(deleted, "D", opts);
607
+ output += "\n";
608
+ }
609
+ if (renamed.length > 0) {
610
+ output += `
611
+ --- Renamed (${renamed.length}) ---
612
+ `;
613
+ output += generateTreeSummary(renamed, "R", opts);
614
+ output += "\n";
615
+ }
616
+ if (untracked.length > 0) {
617
+ output += `
618
+ --- Untracked (${untracked.length}) ---
619
+ `;
620
+ output += generateTreeSummary(untracked, "?", opts);
621
+ output += "\n";
622
+ }
623
+ return output;
624
+ }
625
+
626
+ // src/git/diff.ts
627
+ var DEFAULT_OPTIONS2 = {
628
+ maxInputSize: 3e4,
629
+ maxDiffSize: 15e3,
630
+ treeDepth: 3
631
+ };
632
+ async function getModifiedDiffs(maxSize, cwd) {
633
+ const git = getGit(cwd);
634
+ const diffSummary = await git.diffSummary(["HEAD"]);
635
+ const modifiedFiles = diffSummary.files.filter((f) => !f.binary && "changes" in f && f.changes > 0).map((f) => f.file);
636
+ if (modifiedFiles.length === 0) {
637
+ return "";
638
+ }
639
+ let output = `
640
+ === MODIFIED FILE DIFFS (${modifiedFiles.length} files) ===`;
641
+ let currentSize = output.length;
642
+ for (const file of modifiedFiles) {
643
+ try {
644
+ const fileDiff = await git.diff(["HEAD", "--", file]);
645
+ const diffSize = fileDiff.length;
646
+ if (currentSize + diffSize + 100 > maxSize) {
647
+ const remaining = modifiedFiles.length - modifiedFiles.indexOf(file);
648
+ output += `
649
+
650
+ [... ${remaining} more files truncated due to size limit]`;
651
+ break;
652
+ }
653
+ if (fileDiff) {
654
+ output += `
655
+
656
+ --- ${file} ---
657
+ ${fileDiff}`;
658
+ currentSize += diffSize + file.length + 10;
659
+ }
660
+ } catch {
661
+ }
662
+ }
663
+ return output;
664
+ }
665
+ async function getDiffContent(treeSummary, options = {}, cwd) {
666
+ const opts = { ...DEFAULT_OPTIONS2, ...options };
667
+ const treeSize = treeSummary.length;
668
+ const remainingSize = opts.maxInputSize - treeSize - 500;
669
+ let diffContent = "";
670
+ if (remainingSize > 1e3) {
671
+ const maxDiff = Math.min(remainingSize, opts.maxDiffSize);
672
+ diffContent = await getModifiedDiffs(maxDiff, cwd);
673
+ }
674
+ return diffContent;
675
+ }
676
+
677
+ // src/utils/logger.ts
678
+ import chalk from "chalk";
679
+ var logger = {
680
+ info: (message) => console.log(chalk.cyan(message)),
681
+ success: (message) => console.log(chalk.green(message)),
682
+ warning: (message) => console.log(chalk.yellow(message)),
683
+ error: (message) => console.error(chalk.red(message)),
684
+ highlight: (message) => console.log(chalk.magenta(message)),
685
+ dim: (message) => console.log(chalk.dim(message))
686
+ };
687
+ var colors = {
688
+ red: chalk.red,
689
+ green: chalk.green,
690
+ yellow: chalk.yellow,
691
+ cyan: chalk.cyan,
692
+ blue: chalk.blue,
693
+ magenta: chalk.magenta,
694
+ dim: chalk.dim,
695
+ bold: chalk.bold
696
+ };
697
+
698
+ // src/git/executor.ts
699
+ import fs from "fs";
700
+ async function stageFiles(files, cwd) {
701
+ const git = getGit(cwd);
702
+ for (const file of files) {
703
+ try {
704
+ if (fs.existsSync(file)) {
705
+ await git.add(file);
706
+ } else {
707
+ try {
708
+ await git.rm(file);
709
+ } catch {
710
+ await git.add(["-A", file]);
711
+ }
712
+ }
713
+ } catch (error) {
714
+ logger.warning(`Failed to stage file: ${file}`);
715
+ }
716
+ }
717
+ }
718
+ async function executeCommit(commit, cwd) {
719
+ const git = getGit(cwd);
720
+ try {
721
+ let title = commit.title;
722
+ if (commit.jiraKey && !title.includes(`(${commit.jiraKey})`)) {
723
+ title = `${title} (${commit.jiraKey})`;
724
+ }
725
+ logger.info("Staging files...");
726
+ await stageFiles(commit.files, cwd);
727
+ logger.success(`Committing: ${title}`);
728
+ await git.commit([title, commit.message]);
729
+ return true;
730
+ } catch (error) {
731
+ logger.error(`Commit failed: ${error}`);
732
+ return false;
733
+ }
734
+ }
735
+ async function executeCommits(commits, cwd) {
736
+ for (let i = 0; i < commits.length; i++) {
737
+ const commit = commits[i];
738
+ console.log(colors.yellow(`
739
+ Staging files for commit ${i + 1}/${commits.length}...`));
740
+ const success = await executeCommit(commit, cwd);
741
+ if (!success) {
742
+ logger.error("Commit failed. Aborting.");
743
+ return false;
744
+ }
745
+ console.log("");
746
+ }
747
+ logger.success("All commits completed successfully!");
748
+ return true;
749
+ }
750
+
751
+ // src/jira/extractor.ts
752
+ var JIRA_KEY_PATTERN = /[A-Z]+-\d+/g;
753
+ function extractJiraKeys(input) {
754
+ const matches = input.match(JIRA_KEY_PATTERN);
755
+ if (!matches) {
756
+ return [];
757
+ }
758
+ return [...new Set(matches)];
759
+ }
760
+ function formatJiraKeys(keys) {
761
+ return keys.join(", ");
762
+ }
763
+ function hasJiraKeys(input) {
764
+ return JIRA_KEY_PATTERN.test(input);
765
+ }
766
+
767
+ // src/utils/validation.ts
768
+ var MAX_TITLE_LENGTH = 72;
769
+ function validateTitleLength(commits) {
770
+ commits.forEach((commit, i) => {
771
+ if (commit.title.length > MAX_TITLE_LENGTH) {
772
+ logger.warning(
773
+ `Commit ${i + 1} title exceeds ${MAX_TITLE_LENGTH} chars (${commit.title.length} chars)`
774
+ );
775
+ }
776
+ });
777
+ }
778
+ function validateFilesExist(commits, validFiles) {
779
+ commits.forEach((commit) => {
780
+ commit.files.forEach((file) => {
781
+ if (!validFiles.has(file)) {
782
+ logger.warning(
783
+ `File '${file}' not in change list (may be AI hallucination or deleted file)`
784
+ );
785
+ }
786
+ });
787
+ });
788
+ }
789
+ function isValidConventionalCommit(title) {
790
+ const pattern = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\([^)]+\))?:\s.+/;
791
+ return pattern.test(title);
792
+ }
793
+
794
+ export {
795
+ execCommand,
796
+ execSimple,
797
+ getPromptTemplate,
798
+ getJsonSchema,
799
+ parseJsonResponse,
800
+ DEFAULT_CONFIG,
801
+ CURSOR_DEFAULT_MODEL,
802
+ CLAUDE_DEFAULT_MODEL,
803
+ ClaudeCodeProvider,
804
+ parseDelimiterResponse,
805
+ toDelimiterFormat,
806
+ CursorCLIProvider,
807
+ createProvider,
808
+ isValidProviderType,
809
+ isGitRepository,
810
+ getCurrentBranch,
811
+ getGitStatus,
812
+ getAllChangedFiles,
813
+ hasChanges,
814
+ generateTreeSummary,
815
+ generateFullTreeSummary,
816
+ getModifiedDiffs,
817
+ getDiffContent,
818
+ logger,
819
+ colors,
820
+ stageFiles,
821
+ executeCommits,
822
+ extractJiraKeys,
823
+ formatJiraKeys,
824
+ hasJiraKeys,
825
+ validateTitleLength,
826
+ validateFilesExist,
827
+ isValidConventionalCommit
828
+ };
829
+ //# sourceMappingURL=chunk-PPSTCEXT.js.map