@kb-labs/commit-core 0.6.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.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @kb-labs/commit-core
2
+
3
+ Core business logic for KB Labs Commit plugin - git analysis, plan generation, and commit application.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @kb-labs/commit-core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { ... } from '@kb-labs/commit-core';
15
+ ```
16
+
17
+ ## API
18
+
19
+ See TypeScript types for detailed API documentation.
20
+
21
+ ## License
22
+
23
+ MIT
@@ -0,0 +1,90 @@
1
+ export { F as FileDiff, j as detectCommitStyle, f as formatFileSummary, a as getAllChangedFiles, b as getCurrentBranch, d as getFileDiff, k as getFileDiffs, c as getFileSummaries, g as getGitStatus, e as getRecentCommits, h as hasChanges, i as isProtectedBranch } from '../recent-commits-DD6LX2OI.js';
2
+ import '@kb-labs/commit-contracts';
3
+
4
+ /**
5
+ * Scope resolver - converts package names to file path patterns
6
+ * Supports:
7
+ * 1. Exact package name: @kb-labs/core, my-package
8
+ * 2. Wildcard pattern: @kb-labs/core-*, packages/*
9
+ * 3. Path pattern: packages/core/src/**
10
+ */
11
+ interface ResolvedScope {
12
+ /** Original scope string */
13
+ original: string;
14
+ /** Type of scope */
15
+ type: "package-name" | "wildcard" | "path-pattern";
16
+ /** Resolved package paths (directories) */
17
+ packagePaths: string[];
18
+ /** Pattern to filter files (glob or regex) */
19
+ filePattern?: string;
20
+ }
21
+ interface PackageInfo {
22
+ name: string;
23
+ path: string;
24
+ }
25
+ /**
26
+ * Resolve scope to package paths and file pattern
27
+ */
28
+ declare function resolveScope(cwd: string, scope: string): Promise<ResolvedScope>;
29
+ /**
30
+ * Check if a file matches the resolved scope
31
+ */
32
+ declare function matchesScope(filePath: string, resolvedScope: ResolvedScope): boolean;
33
+
34
+ /**
35
+ * Secrets detection module
36
+ * Detects files that likely contain secrets and should not be sent to LLM
37
+ */
38
+ /**
39
+ * Custom error class for secrets detection
40
+ * This error should NEVER be caught and fallback to heuristics
41
+ */
42
+ declare class SecretsDetectedError extends Error {
43
+ readonly secretMatches: SecretMatch[];
44
+ constructor(matches: SecretMatch[], message: string);
45
+ }
46
+ /**
47
+ * Detailed information about a detected secret
48
+ */
49
+ interface SecretMatch {
50
+ file: string;
51
+ line: number;
52
+ column: number;
53
+ pattern: string;
54
+ patternName: string;
55
+ snippet: string;
56
+ matchedText: string;
57
+ }
58
+ /**
59
+ * Check if file path matches secret file patterns
60
+ */
61
+ declare function isSecretFile(filePath: string): boolean;
62
+ /**
63
+ * Check if file content contains secret patterns
64
+ */
65
+ declare function containsSecrets(content: string): boolean;
66
+ /**
67
+ * Scan files for potential secrets
68
+ * Returns list of files that likely contain secrets
69
+ */
70
+ declare function detectSecretFiles(files: string[]): string[];
71
+ /**
72
+ * Scan file diffs for secrets
73
+ * Returns map of files that contain secrets
74
+ */
75
+ declare function detectSecretsInDiffs(diffs: Map<string, string>): Map<string, string>;
76
+ /**
77
+ * Create error message for detected secrets
78
+ */
79
+ declare function formatSecretsWarning(secretFiles: string[]): string;
80
+ /**
81
+ * Detect secrets in file diffs with exact location information
82
+ * Returns array of SecretMatch with file, line, column, pattern info
83
+ */
84
+ declare function detectSecretsWithLocation(diffs: Map<string, string>): SecretMatch[];
85
+ /**
86
+ * Format detailed secrets report with locations
87
+ */
88
+ declare function formatSecretsReport(matches: SecretMatch[]): string;
89
+
90
+ export { type PackageInfo, type ResolvedScope, type SecretMatch, SecretsDetectedError, containsSecrets, detectSecretFiles, detectSecretsInDiffs, detectSecretsWithLocation, formatSecretsReport, formatSecretsWarning, isSecretFile, matchesScope, resolveScope };
@@ -0,0 +1,588 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import { readFile } from 'fs/promises';
3
+ import { join, relative } from 'path';
4
+ import globby from 'globby';
5
+ import { minimatch } from 'minimatch';
6
+
7
+ // src/analyzer/git-status.ts
8
+ async function getGitStatus(cwd) {
9
+ const git = simpleGit(cwd);
10
+ const status = await git.status(["--ignore-submodules=all"]);
11
+ return {
12
+ staged: status.staged.filter((f) => !shouldIgnoreFile(f)),
13
+ unstaged: [...status.modified, ...status.deleted].filter((f) => !status.staged.includes(f)).filter((f) => !shouldIgnoreFile(f)),
14
+ untracked: status.not_added.filter((f) => !shouldIgnoreFile(f))
15
+ };
16
+ }
17
+ function shouldIgnoreFile(file) {
18
+ const ignoredPaths = [
19
+ "node_modules/",
20
+ ".git/",
21
+ "dist/",
22
+ "build/",
23
+ ".next/",
24
+ ".turbo/",
25
+ "coverage/",
26
+ ".cache/",
27
+ ".temp/",
28
+ "tmp/"
29
+ ];
30
+ return ignoredPaths.some((path) => file.includes(path));
31
+ }
32
+ function getAllChangedFiles(status) {
33
+ const allFiles = [
34
+ .../* @__PURE__ */ new Set([...status.staged, ...status.unstaged, ...status.untracked])
35
+ ];
36
+ return allFiles.filter((file) => !shouldIgnoreFile(file));
37
+ }
38
+ function hasChanges(status) {
39
+ return status.staged.length > 0 || status.unstaged.length > 0 || status.untracked.length > 0;
40
+ }
41
+ async function getCurrentBranch(cwd) {
42
+ const git = simpleGit(cwd);
43
+ const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
44
+ return branch.trim();
45
+ }
46
+ function isProtectedBranch(branch) {
47
+ const protectedBranches = [
48
+ "main",
49
+ "master",
50
+ "develop",
51
+ "release",
52
+ "production"
53
+ ];
54
+ return protectedBranches.includes(branch.toLowerCase());
55
+ }
56
+ function isTextFile(file) {
57
+ return !file.binary;
58
+ }
59
+ async function isNewFile(cwd, filePath) {
60
+ try {
61
+ const repo = await findGitRepo(cwd, filePath);
62
+ if (!repo) {
63
+ return true;
64
+ }
65
+ const git = simpleGit(repo.repoPath);
66
+ const result = await git.raw(["ls-tree", "HEAD", "--", repo.relativePath]);
67
+ return result.trim().length === 0;
68
+ } catch {
69
+ return true;
70
+ }
71
+ }
72
+ async function getFileSummaries(cwd, files) {
73
+ if (files.length === 0) {
74
+ return [];
75
+ }
76
+ const summaries = [];
77
+ const filesByRepo = /* @__PURE__ */ new Map();
78
+ for (const file of files) {
79
+ const repo = await findGitRepo(cwd, file);
80
+ if (!repo) {
81
+ const group2 = filesByRepo.get(cwd) ?? [];
82
+ group2.push({ repoPath: cwd, relativePath: file, originalPath: file });
83
+ filesByRepo.set(cwd, group2);
84
+ continue;
85
+ }
86
+ const group = filesByRepo.get(repo.repoPath) ?? [];
87
+ group.push({
88
+ repoPath: repo.repoPath,
89
+ relativePath: repo.relativePath,
90
+ originalPath: file
91
+ });
92
+ filesByRepo.set(repo.repoPath, group);
93
+ }
94
+ for (const [repoPath, fileInfos] of filesByRepo) {
95
+ const git = simpleGit(repoPath);
96
+ const relativePaths = fileInfos.map((f) => f.relativePath);
97
+ try {
98
+ const stagedDiff = await git.diffSummary([
99
+ "--cached",
100
+ "--",
101
+ ...relativePaths
102
+ ]);
103
+ const unstagedDiff = await git.diffSummary(["--", ...relativePaths]);
104
+ const allDiffFiles = /* @__PURE__ */ new Map();
105
+ for (const file of unstagedDiff.files) {
106
+ allDiffFiles.set(file.file, file);
107
+ }
108
+ for (const file of stagedDiff.files) {
109
+ allDiffFiles.set(file.file, file);
110
+ }
111
+ for (const file of allDiffFiles.values()) {
112
+ const fileInfo = fileInfos.find((f) => f.relativePath === file.file);
113
+ if (!fileInfo) {
114
+ continue;
115
+ }
116
+ const isNew = await isNewFile(repoPath, file.file);
117
+ if (isTextFile(file)) {
118
+ summaries.push({
119
+ path: fileInfo.originalPath,
120
+ status: mapDiffStatus(file.insertions, file.deletions),
121
+ additions: file.insertions,
122
+ deletions: file.deletions,
123
+ binary: false,
124
+ isNewFile: isNew
125
+ });
126
+ } else {
127
+ summaries.push({
128
+ path: fileInfo.originalPath,
129
+ status: "modified",
130
+ additions: 0,
131
+ deletions: 0,
132
+ binary: true,
133
+ isNewFile: isNew
134
+ });
135
+ }
136
+ }
137
+ const processedPaths = new Set(
138
+ Array.from(allDiffFiles.values()).map((f) => f.file)
139
+ );
140
+ const missingInRepo = fileInfos.filter(
141
+ (f) => !processedPaths.has(f.relativePath)
142
+ );
143
+ for (const fileInfo of missingInRepo) {
144
+ summaries.push({
145
+ path: fileInfo.originalPath,
146
+ status: "added",
147
+ additions: 0,
148
+ deletions: 0,
149
+ binary: false,
150
+ isNewFile: true
151
+ });
152
+ }
153
+ } catch {
154
+ for (const fileInfo of fileInfos) {
155
+ summaries.push({
156
+ path: fileInfo.originalPath,
157
+ status: "modified",
158
+ additions: 0,
159
+ deletions: 0,
160
+ binary: false,
161
+ isNewFile: false
162
+ // Conservative assumption
163
+ });
164
+ }
165
+ }
166
+ }
167
+ return summaries;
168
+ }
169
+ function mapDiffStatus(insertions, deletions) {
170
+ if (deletions === 0 && insertions > 0) {
171
+ return "added";
172
+ }
173
+ if (insertions === 0 && deletions > 0) {
174
+ return "deleted";
175
+ }
176
+ return "modified";
177
+ }
178
+ function formatFileSummary(summary) {
179
+ const stats = summary.binary ? "binary" : `+${summary.additions}/-${summary.deletions}`;
180
+ return `${summary.path} (${summary.status}, ${stats})`;
181
+ }
182
+ async function getFileDiffs(cwd, files) {
183
+ if (files.length === 0) {
184
+ return /* @__PURE__ */ new Map();
185
+ }
186
+ const diffs = /* @__PURE__ */ new Map();
187
+ const filesByRepo = /* @__PURE__ */ new Map();
188
+ for (const file of files) {
189
+ const repo = await findGitRepo(cwd, file);
190
+ if (!repo) {
191
+ const group2 = filesByRepo.get(cwd) ?? [];
192
+ group2.push({ repoPath: cwd, relativePath: file, originalPath: file });
193
+ filesByRepo.set(cwd, group2);
194
+ continue;
195
+ }
196
+ const group = filesByRepo.get(repo.repoPath) ?? [];
197
+ group.push({
198
+ repoPath: repo.repoPath,
199
+ relativePath: repo.relativePath,
200
+ originalPath: file
201
+ });
202
+ filesByRepo.set(repo.repoPath, group);
203
+ }
204
+ for (const [repoPath, fileInfos] of filesByRepo) {
205
+ const git = simpleGit(repoPath);
206
+ for (const { relativePath, originalPath } of fileInfos) {
207
+ try {
208
+ let diff = await git.diff(["--cached", "--", relativePath]);
209
+ if (!diff) {
210
+ diff = await git.diff(["--", relativePath]);
211
+ }
212
+ if (!diff) {
213
+ diff = await git.show([`:${relativePath}`]).catch(() => "");
214
+ }
215
+ if (diff) {
216
+ diffs.set(originalPath, diff);
217
+ }
218
+ } catch {
219
+ }
220
+ }
221
+ }
222
+ return diffs;
223
+ }
224
+ async function existsAsync(path) {
225
+ try {
226
+ const { existsSync } = await import('fs');
227
+ return existsSync(path);
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ async function findGitRepo(basePath, filePath) {
233
+ const segments = filePath.split("/");
234
+ for (let i = segments.length - 1; i > 0; i--) {
235
+ const potentialRepoSegments = segments.slice(0, i);
236
+ const potentialRepoPath = `${basePath}/${potentialRepoSegments.join("/")}`;
237
+ const gitDir = `${potentialRepoPath}/.git`;
238
+ if (await existsAsync(gitDir)) {
239
+ const relativePath = segments.slice(i).join("/");
240
+ return { repoPath: potentialRepoPath, relativePath };
241
+ }
242
+ }
243
+ if (await existsAsync(`${basePath}/.git`)) {
244
+ return { repoPath: basePath, relativePath: filePath };
245
+ }
246
+ return null;
247
+ }
248
+ async function getFileDiff(cwd, filePath) {
249
+ const git = simpleGit(cwd);
250
+ const diffOutput = await git.diff(["HEAD", "--", filePath]);
251
+ const additions = (diffOutput.match(/^\+(?!\+)/gm) || []).length;
252
+ const deletions = (diffOutput.match(/^-(?!-)/gm) || []).length;
253
+ return {
254
+ diff: diffOutput,
255
+ additions,
256
+ deletions
257
+ };
258
+ }
259
+ async function getRecentCommits(cwd, count = 10) {
260
+ const git = simpleGit(cwd);
261
+ try {
262
+ const log = await git.log({
263
+ maxCount: count,
264
+ format: {
265
+ message: "%s"
266
+ // Subject line only
267
+ }
268
+ });
269
+ return log.all.map((commit) => commit.message);
270
+ } catch {
271
+ return [];
272
+ }
273
+ }
274
+ function detectCommitStyle(commits) {
275
+ if (commits.length === 0) {
276
+ return {
277
+ usesConventional: false,
278
+ commonScopes: [],
279
+ avgLength: 50
280
+ };
281
+ }
282
+ const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|ci|perf|build)(\([^)]+\))?!?:/i;
283
+ const conventionalMatches = commits.filter(
284
+ (c) => conventionalPattern.test(c)
285
+ );
286
+ const usesConventional = conventionalMatches.length >= commits.length * 0.5;
287
+ const scopePattern = /^\w+\(([^)]+)\)/;
288
+ const scopes = commits.map((c) => {
289
+ const match = c.match(scopePattern);
290
+ return match ? match[1] : null;
291
+ }).filter((s) => s !== null);
292
+ const scopeCounts = scopes.reduce((acc, scope) => {
293
+ acc[scope] = (acc[scope] || 0) + 1;
294
+ return acc;
295
+ }, {});
296
+ const commonScopes = Object.entries(scopeCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([scope]) => scope);
297
+ const avgLength = Math.round(
298
+ commits.reduce((sum, c) => sum + c.length, 0) / commits.length
299
+ );
300
+ return {
301
+ usesConventional,
302
+ commonScopes,
303
+ avgLength
304
+ };
305
+ }
306
+ async function resolveScope(cwd, scope) {
307
+ const isExactPackageName = !scope.includes("*") && (scope.startsWith("@") || !scope.includes("/"));
308
+ const isWildcardPackageName = scope.includes("*") && (scope.startsWith("@") || !scope.includes("/"));
309
+ scope.includes("/") && !scope.startsWith("@");
310
+ if (isExactPackageName) {
311
+ const packages = await discoverPackages(cwd);
312
+ const matched = packages.filter((pkg) => pkg.name === scope);
313
+ return {
314
+ original: scope,
315
+ type: "package-name",
316
+ packagePaths: matched.map((p) => p.path),
317
+ filePattern: matched.length > 0 ? createGlobPattern(cwd, matched) : void 0
318
+ };
319
+ }
320
+ if (isWildcardPackageName) {
321
+ const packages = await discoverPackages(cwd);
322
+ const regex = createPackageNameRegex(scope);
323
+ const matched = packages.filter((pkg) => regex.test(pkg.name));
324
+ return {
325
+ original: scope,
326
+ type: "wildcard",
327
+ packagePaths: matched.map((p) => p.path),
328
+ filePattern: matched.length > 0 ? createGlobPattern(cwd, matched) : void 0
329
+ };
330
+ }
331
+ const filePattern = !scope.includes("*") && !scope.includes("?") ? `${scope}/**` : scope;
332
+ return {
333
+ original: scope,
334
+ type: "path-pattern",
335
+ packagePaths: [],
336
+ filePattern
337
+ };
338
+ }
339
+ function matchesScope(filePath, resolvedScope) {
340
+ if (resolvedScope.packagePaths.length > 0) {
341
+ return resolvedScope.packagePaths.some((pkgPath) => {
342
+ const normalizedFile = filePath.replace(/\\/g, "/");
343
+ const normalizedPkg = pkgPath.replace(/\\/g, "/");
344
+ return normalizedFile.startsWith(normalizedPkg + "/") || normalizedFile === normalizedPkg;
345
+ });
346
+ }
347
+ return true;
348
+ }
349
+ async function discoverPackages(cwd) {
350
+ const packages = [];
351
+ const packageJsonPaths = await globby("**/package.json", {
352
+ cwd,
353
+ absolute: true,
354
+ onlyFiles: true,
355
+ ignore: [
356
+ "**/node_modules/**",
357
+ "**/dist/**",
358
+ "**/build/**",
359
+ "**/.git/**",
360
+ "**/.*/**"
361
+ ]
362
+ });
363
+ for (const packageJsonPath of packageJsonPaths) {
364
+ try {
365
+ const packagePath = join(packageJsonPath, "..");
366
+ const content = await readFile(packageJsonPath, "utf-8");
367
+ const packageJson = JSON.parse(content);
368
+ if (!packageJson.name) {
369
+ continue;
370
+ }
371
+ packages.push({
372
+ name: packageJson.name,
373
+ path: relative(cwd, packagePath) || "."
374
+ });
375
+ } catch {
376
+ }
377
+ }
378
+ return packages;
379
+ }
380
+ function createPackageNameRegex(pattern) {
381
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
382
+ return new RegExp(`^${escaped}$`);
383
+ }
384
+ function createGlobPattern(cwd, packages) {
385
+ if (packages.length === 1 && packages[0]) {
386
+ return `${packages[0].path}/**`;
387
+ }
388
+ const paths = packages.map((p) => p.path).join(",");
389
+ return `{${paths}}/**`;
390
+ }
391
+ var SecretsDetectedError = class _SecretsDetectedError extends Error {
392
+ secretMatches;
393
+ constructor(matches, message) {
394
+ super(message);
395
+ this.name = "SecretsDetectedError";
396
+ this.secretMatches = matches;
397
+ if (Error.captureStackTrace) {
398
+ Error.captureStackTrace(this, _SecretsDetectedError);
399
+ }
400
+ }
401
+ };
402
+ var SECRET_FILE_PATTERNS = [
403
+ // Environment files
404
+ ".env",
405
+ ".env.*",
406
+ "*.env",
407
+ ".envrc",
408
+ // NPM/Node
409
+ ".npmrc",
410
+ ".yarnrc",
411
+ ".yarnrc.yml",
412
+ // SSH/GPG keys
413
+ "*.key",
414
+ "*.pem",
415
+ "*.p12",
416
+ "*.pfx",
417
+ "id_rsa",
418
+ "id_dsa",
419
+ "id_ecdsa",
420
+ "id_ed25519",
421
+ "*.pub",
422
+ // AWS
423
+ ".aws/**",
424
+ "credentials",
425
+ // Docker
426
+ ".docker/config.json",
427
+ // Git credentials
428
+ ".git-credentials",
429
+ ".netrc",
430
+ // Service account files
431
+ "*-service-account.json",
432
+ "*-serviceaccount.json",
433
+ "service-account*.json",
434
+ "serviceaccount*.json",
435
+ // Kubernetes
436
+ "kubeconfig",
437
+ "*.kubeconfig",
438
+ // Terraform
439
+ "*.tfvars",
440
+ "terraform.tfstate",
441
+ "terraform.tfstate.backup",
442
+ // Other common secrets
443
+ "secrets.yml",
444
+ "secrets.yaml",
445
+ "secret.yml",
446
+ "secret.yaml",
447
+ "passwords.txt",
448
+ "password.txt"
449
+ ];
450
+ var SECRET_CONTENT_PATTERNS = [
451
+ // API keys/tokens
452
+ { pattern: /api[_-]?key[s]?['":\s]*[a-zA-Z0-9_-]{20,}/i, name: "API Key" },
453
+ { pattern: /auth[_-]?token[s]?['":\s]*[a-zA-Z0-9_-]{20,}/i, name: "Auth Token" },
454
+ { pattern: /access[_-]?token[s]?['":\s]*[a-zA-Z0-9_-]{20,}/i, name: "Access Token" },
455
+ // AWS
456
+ { pattern: /AKIA[0-9A-Z]{16}/, name: "AWS Access Key ID" },
457
+ { pattern: /aws[_-]?secret[_-]?access[_-]?key/i, name: "AWS Secret Access Key" },
458
+ // NPM
459
+ { pattern: /\/\/registry\.npmjs\.org\/:_authToken=/, name: "NPM Auth Token" },
460
+ { pattern: /npm_[A-Za-z0-9]{30,}/, name: "NPM Token" },
461
+ // GitHub
462
+ { pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, name: "GitHub Token" },
463
+ // Slack
464
+ { pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{24,}/, name: "Slack Token" },
465
+ // Private keys
466
+ { pattern: /-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----/, name: "Private Key" },
467
+ // Passwords
468
+ { pattern: /password['":\s]*['"]/i, name: "Password" },
469
+ // Generic patterns
470
+ { pattern: /secret['":\s]*['"]/i, name: "Secret" }
471
+ ];
472
+ function isSecretFile(filePath) {
473
+ const normalizedPath = filePath.replace(/\\/g, "/");
474
+ return SECRET_FILE_PATTERNS.some(
475
+ (pattern) => minimatch(normalizedPath, pattern, { matchBase: true })
476
+ );
477
+ }
478
+ function containsSecrets(content) {
479
+ return SECRET_CONTENT_PATTERNS.some(({ pattern }) => pattern.test(content));
480
+ }
481
+ function detectSecretFiles(files) {
482
+ return files.filter(isSecretFile);
483
+ }
484
+ function detectSecretsInDiffs(diffs) {
485
+ const secretDiffs = /* @__PURE__ */ new Map();
486
+ for (const [file, diff] of diffs.entries()) {
487
+ if (containsSecrets(diff)) {
488
+ secretDiffs.set(file, diff);
489
+ }
490
+ }
491
+ return secretDiffs;
492
+ }
493
+ function formatSecretsWarning(secretFiles) {
494
+ const count = secretFiles.length;
495
+ const filesList = secretFiles.map((f) => ` - ${f}`).join("\n");
496
+ return [
497
+ `\u{1F6A8} CRITICAL SECURITY ERROR: Detected ${count} file(s) with potential secrets:`,
498
+ filesList,
499
+ "",
500
+ "\u26D4\uFE0F COMMIT GENERATION ABORTED",
501
+ "",
502
+ "These files contain sensitive data (API keys, tokens, credentials)",
503
+ "that MUST NOT be committed to git or sent to LLM.",
504
+ "",
505
+ "\u2705 Actions to fix:",
506
+ " 1. Add these files to .gitignore",
507
+ " 2. Remove secrets from the files (use environment variables instead)",
508
+ " 3. If already committed, use git filter-branch or BFG to remove from history",
509
+ "",
510
+ "\u26A0\uFE0F If you already ran commit:generate before, the secrets may have been",
511
+ "sent to OpenAI. Rotate your credentials immediately."
512
+ ].join("\n");
513
+ }
514
+ function detectSecretsWithLocation(diffs) {
515
+ const matches = [];
516
+ for (const [file, diff] of diffs.entries()) {
517
+ const lines = diff.split("\n");
518
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
519
+ const line = lines[lineIndex] ?? "";
520
+ for (const { pattern, name } of SECRET_CONTENT_PATTERNS) {
521
+ pattern.lastIndex = 0;
522
+ const match = pattern.exec(line);
523
+ if (match) {
524
+ const matchedText = match[0];
525
+ const column = match.index;
526
+ const snippetStart = Math.max(0, column - 20);
527
+ const snippetEnd = Math.min(line.length, column + matchedText.length + 20);
528
+ const snippet = line.substring(snippetStart, snippetEnd);
529
+ const displayText = matchedText.length > 50 ? matchedText.substring(0, 50) + "..." : matchedText;
530
+ matches.push({
531
+ file,
532
+ line: lineIndex + 1,
533
+ // 1-based line numbers
534
+ column: column + 1,
535
+ // 1-based column numbers
536
+ pattern: pattern.source,
537
+ patternName: name,
538
+ snippet: snippet.trim(),
539
+ matchedText: displayText
540
+ });
541
+ }
542
+ }
543
+ }
544
+ }
545
+ return matches;
546
+ }
547
+ function formatSecretsReport(matches) {
548
+ const count = matches.length;
549
+ const fileCount = new Set(matches.map((m) => m.file)).size;
550
+ const lines = [
551
+ `\u{1F6A8} CRITICAL SECURITY ERROR: Detected ${count} potential secret(s) in ${fileCount} file(s)`,
552
+ "",
553
+ "\u26D4\uFE0F COMMIT GENERATION BLOCKED",
554
+ ""
555
+ ];
556
+ const byFile = /* @__PURE__ */ new Map();
557
+ for (const match of matches) {
558
+ const existing = byFile.get(match.file) ?? [];
559
+ existing.push(match);
560
+ byFile.set(match.file, existing);
561
+ }
562
+ for (const [file, fileMatches] of byFile.entries()) {
563
+ lines.push(`\u{1F4C4} ${file}:`);
564
+ for (const match of fileMatches) {
565
+ lines.push(` Line ${match.line}:${match.column} - ${match.patternName}`);
566
+ lines.push(` Pattern: ${match.pattern.substring(0, 60)}${match.pattern.length > 60 ? "..." : ""}`);
567
+ lines.push(` Matched: ${match.matchedText}`);
568
+ lines.push(` Context: ...${match.snippet}...`);
569
+ lines.push("");
570
+ }
571
+ }
572
+ lines.push(
573
+ "\u{1F512} These files contain sensitive data (API keys, tokens, credentials)",
574
+ "that MUST NOT be committed to git or sent to LLM.",
575
+ "",
576
+ "\u2705 Actions to fix:",
577
+ " 1. Review each match above - some may be false positives (e.g., examples in comments)",
578
+ " 2. If real secrets: Add files to .gitignore and remove secrets (use env vars)",
579
+ " 3. If false positives: Use --allow-secrets flag to proceed with confirmation",
580
+ "",
581
+ "\u26A0\uFE0F If secrets were already sent to LLM in previous runs, rotate credentials immediately!"
582
+ );
583
+ return lines.join("\n");
584
+ }
585
+
586
+ export { SecretsDetectedError, containsSecrets, detectCommitStyle, detectSecretFiles, detectSecretsInDiffs, detectSecretsWithLocation, formatFileSummary, formatSecretsReport, formatSecretsWarning, getAllChangedFiles, getCurrentBranch, getFileDiff, getFileDiffs, getFileSummaries, getGitStatus, getRecentCommits, hasChanges, isProtectedBranch, isSecretFile, matchesScope, resolveScope };
587
+ //# sourceMappingURL=index.js.map
588
+ //# sourceMappingURL=index.js.map