@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 +23 -0
- package/dist/analyzer/index.d.ts +90 -0
- package/dist/analyzer/index.js +588 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/applier/index.d.ts +37 -0
- package/dist/applier/index.js +265 -0
- package/dist/applier/index.js.map +1 -0
- package/dist/generator/index.d.ts +26 -0
- package/dist/generator/index.js +2694 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/heuristics-B6EsN9yD.d.ts +80 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3093 -0
- package/dist/index.js.map +1 -0
- package/dist/recent-commits-DD6LX2OI.d.ts +85 -0
- package/dist/storage/index.d.ts +59 -0
- package/dist/storage/index.js +280 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-CEjOl41u.d.ts +67 -0
- package/package.json +69 -0
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
|