@kb-labs/commit-core 2.116.14 → 2.117.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/dist/applier/index.js +96 -94
- package/dist/applier/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +94 -94
- package/dist/index.js.map +1 -1
- package/dist/validator/index.d.ts +43 -0
- package/dist/validator/index.js +131 -0
- package/dist/validator/index.js.map +1 -0
- package/package.json +8 -4
package/dist/applier/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { simpleGit } from 'simple-git';
|
|
1
2
|
import { existsSync } from 'fs';
|
|
2
3
|
import { join } from 'path';
|
|
3
|
-
import { simpleGit } from 'simple-git';
|
|
4
4
|
import { useLogger } from '@kb-labs/sdk';
|
|
5
5
|
|
|
6
6
|
// src/applier/apply.ts
|
|
@@ -46,6 +46,100 @@ function isProtectedBranch(branch) {
|
|
|
46
46
|
];
|
|
47
47
|
return protectedBranches.includes(branch.toLowerCase());
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
// src/validator/index.ts
|
|
51
|
+
function groupFilesByRepo(cwd, files) {
|
|
52
|
+
const filesByRepo = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const segments = file.split("/");
|
|
55
|
+
const potentialRepoDir = segments[0];
|
|
56
|
+
if (!potentialRepoDir) {
|
|
57
|
+
const group = filesByRepo.get(cwd) ?? [];
|
|
58
|
+
group.push({ relativePath: file, originalPath: file });
|
|
59
|
+
filesByRepo.set(cwd, group);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const potentialRepoPath = join(cwd, potentialRepoDir);
|
|
63
|
+
const potentialGitDir = join(potentialRepoPath, ".git");
|
|
64
|
+
const isNestedRepo = existsSync(potentialGitDir);
|
|
65
|
+
if (isNestedRepo) {
|
|
66
|
+
const relativePath = segments.slice(1).join("/");
|
|
67
|
+
const group = filesByRepo.get(potentialRepoPath) ?? [];
|
|
68
|
+
group.push({ relativePath, originalPath: file });
|
|
69
|
+
filesByRepo.set(potentialRepoPath, group);
|
|
70
|
+
} else {
|
|
71
|
+
const group = filesByRepo.get(cwd) ?? [];
|
|
72
|
+
group.push({ relativePath: file, originalPath: file });
|
|
73
|
+
filesByRepo.set(cwd, group);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return filesByRepo;
|
|
77
|
+
}
|
|
78
|
+
function validatePlanIntegrity(plan) {
|
|
79
|
+
const errors = [];
|
|
80
|
+
const seenInCommit = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const commit of plan.commits) {
|
|
82
|
+
if (commit.files.length === 0) {
|
|
83
|
+
errors.push(`Commit ${commit.id} has no files`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!commit.message.trim()) {
|
|
87
|
+
errors.push(`Commit ${commit.id} has empty message`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const file of commit.files) {
|
|
91
|
+
const firstCommit = seenInCommit.get(file);
|
|
92
|
+
if (firstCommit) {
|
|
93
|
+
errors.push(
|
|
94
|
+
`File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
seenInCommit.set(file, commit.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return errors;
|
|
102
|
+
}
|
|
103
|
+
async function checkPlanStaleness(cwd, plan, scope) {
|
|
104
|
+
const logger = useLogger();
|
|
105
|
+
const planFiles = new Set(plan.commits.flatMap((c) => c.files));
|
|
106
|
+
await logger.debug("checkPlanStaleness: start", {
|
|
107
|
+
scope,
|
|
108
|
+
cwd,
|
|
109
|
+
planFiles: [...planFiles]
|
|
110
|
+
});
|
|
111
|
+
if (planFiles.size === 0) {
|
|
112
|
+
return { isStale: false, reason: "" };
|
|
113
|
+
}
|
|
114
|
+
const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);
|
|
115
|
+
for (const [repoPath, fileInfos] of filesByRepo) {
|
|
116
|
+
const currentStatus = await getGitStatus(repoPath);
|
|
117
|
+
const currentFiles = new Set(getAllChangedFiles(currentStatus));
|
|
118
|
+
await logger.debug("checkPlanStaleness: repo status", {
|
|
119
|
+
repoPath,
|
|
120
|
+
staged: currentStatus.staged,
|
|
121
|
+
unstaged: currentStatus.unstaged,
|
|
122
|
+
untracked: currentStatus.untracked,
|
|
123
|
+
expected: fileInfos.map((f) => f.relativePath)
|
|
124
|
+
});
|
|
125
|
+
for (const { relativePath, originalPath } of fileInfos) {
|
|
126
|
+
if (!currentFiles.has(relativePath)) {
|
|
127
|
+
await logger.warn("checkPlanStaleness: file not in current changes", {
|
|
128
|
+
scope,
|
|
129
|
+
repoPath,
|
|
130
|
+
originalPath,
|
|
131
|
+
relativePath,
|
|
132
|
+
currentFiles: [...currentFiles]
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
isStale: true,
|
|
136
|
+
reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { isStale: false, reason: "" };
|
|
142
|
+
}
|
|
49
143
|
var GIT_HOOK_NAMES = [
|
|
50
144
|
"pre-commit",
|
|
51
145
|
"commit-msg",
|
|
@@ -91,7 +185,7 @@ async function applyCommitPlan(cwd, plan, options) {
|
|
|
91
185
|
};
|
|
92
186
|
}
|
|
93
187
|
if (!options?.force) {
|
|
94
|
-
const staleness = await
|
|
188
|
+
const staleness = await checkPlanStaleness(cwd, plan, options?.scope);
|
|
95
189
|
if (staleness.isStale) {
|
|
96
190
|
return {
|
|
97
191
|
success: false,
|
|
@@ -120,31 +214,6 @@ async function applyCommitPlan(cwd, plan, options) {
|
|
|
120
214
|
errors
|
|
121
215
|
};
|
|
122
216
|
}
|
|
123
|
-
function validatePlanIntegrity(plan) {
|
|
124
|
-
const errors = [];
|
|
125
|
-
const seenInCommit = /* @__PURE__ */ new Map();
|
|
126
|
-
for (const commit of plan.commits) {
|
|
127
|
-
if (commit.files.length === 0) {
|
|
128
|
-
errors.push(`Commit ${commit.id} has no files`);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (!commit.message.trim()) {
|
|
132
|
-
errors.push(`Commit ${commit.id} has empty message`);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
for (const file of commit.files) {
|
|
136
|
-
const firstCommit = seenInCommit.get(file);
|
|
137
|
-
if (firstCommit) {
|
|
138
|
-
errors.push(
|
|
139
|
-
`File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`
|
|
140
|
-
);
|
|
141
|
-
} else {
|
|
142
|
-
seenInCommit.set(file, commit.id);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return errors;
|
|
147
|
-
}
|
|
148
217
|
async function applyCommit(cwd, commit) {
|
|
149
218
|
const filesByRepo = groupFilesByRepo(cwd, commit.files);
|
|
150
219
|
const repos = Array.from(filesByRepo.keys());
|
|
@@ -198,33 +267,6 @@ async function applyCommit(cwd, commit) {
|
|
|
198
267
|
const result = await git.commit(message);
|
|
199
268
|
return result.commit;
|
|
200
269
|
}
|
|
201
|
-
function groupFilesByRepo(cwd, files) {
|
|
202
|
-
const filesByRepo = /* @__PURE__ */ new Map();
|
|
203
|
-
for (const file of files) {
|
|
204
|
-
const segments = file.split("/");
|
|
205
|
-
const potentialRepoDir = segments[0];
|
|
206
|
-
if (!potentialRepoDir) {
|
|
207
|
-
const group = filesByRepo.get(cwd) ?? [];
|
|
208
|
-
group.push({ relativePath: file, originalPath: file });
|
|
209
|
-
filesByRepo.set(cwd, group);
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
const potentialRepoPath = join(cwd, potentialRepoDir);
|
|
213
|
-
const potentialGitDir = join(potentialRepoPath, ".git");
|
|
214
|
-
const isNestedRepo = existsSync(potentialGitDir);
|
|
215
|
-
if (isNestedRepo) {
|
|
216
|
-
const relativePath = segments.slice(1).join("/");
|
|
217
|
-
const group = filesByRepo.get(potentialRepoPath) ?? [];
|
|
218
|
-
group.push({ relativePath, originalPath: file });
|
|
219
|
-
filesByRepo.set(potentialRepoPath, group);
|
|
220
|
-
} else {
|
|
221
|
-
const group = filesByRepo.get(cwd) ?? [];
|
|
222
|
-
group.push({ relativePath: file, originalPath: file });
|
|
223
|
-
filesByRepo.set(cwd, group);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return filesByRepo;
|
|
227
|
-
}
|
|
228
270
|
var COMMIT_FOOTER = "\n\n\u{1F916} Generated by kb-labs-commit-plugin";
|
|
229
271
|
function formatCommitMessage(commit, options) {
|
|
230
272
|
const type = commit.type;
|
|
@@ -242,46 +284,6 @@ ${commit.body}`;
|
|
|
242
284
|
}
|
|
243
285
|
return message;
|
|
244
286
|
}
|
|
245
|
-
async function checkStaleness(cwd, plan, scope) {
|
|
246
|
-
const logger = useLogger();
|
|
247
|
-
const planFiles = new Set(plan.commits.flatMap((c) => c.files));
|
|
248
|
-
await logger.debug("checkStaleness: start", {
|
|
249
|
-
scope,
|
|
250
|
-
cwd,
|
|
251
|
-
planFiles: [...planFiles]
|
|
252
|
-
});
|
|
253
|
-
if (planFiles.size === 0) {
|
|
254
|
-
return { isStale: false, reason: "" };
|
|
255
|
-
}
|
|
256
|
-
const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);
|
|
257
|
-
for (const [repoPath, fileInfos] of filesByRepo) {
|
|
258
|
-
const currentStatus = await getGitStatus(repoPath);
|
|
259
|
-
const currentFiles = new Set(getAllChangedFiles(currentStatus));
|
|
260
|
-
await logger.debug("checkStaleness: repo status", {
|
|
261
|
-
repoPath,
|
|
262
|
-
staged: currentStatus.staged,
|
|
263
|
-
unstaged: currentStatus.unstaged,
|
|
264
|
-
untracked: currentStatus.untracked,
|
|
265
|
-
expected: fileInfos.map((f) => f.relativePath)
|
|
266
|
-
});
|
|
267
|
-
for (const { relativePath, originalPath } of fileInfos) {
|
|
268
|
-
if (!currentFiles.has(relativePath)) {
|
|
269
|
-
await logger.warn("checkStaleness: file not in current changes", {
|
|
270
|
-
scope,
|
|
271
|
-
repoPath,
|
|
272
|
-
originalPath,
|
|
273
|
-
relativePath,
|
|
274
|
-
currentFiles: [...currentFiles]
|
|
275
|
-
});
|
|
276
|
-
return {
|
|
277
|
-
isStale: true,
|
|
278
|
-
reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return { isStale: false, reason: "" };
|
|
284
|
-
}
|
|
285
287
|
async function pushCommits(cwd, options) {
|
|
286
288
|
const git = simpleGit(cwd);
|
|
287
289
|
const remote = options?.remote || "origin";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/analyzer/git-status.ts","../../src/applier/apply.ts","../../src/applier/push.ts"],"names":["simpleGit","cleanGitError"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Git status analysis\n */\n\nimport { simpleGit, type SimpleGit, type StatusResult } from \"simple-git\";\nimport type { GitStatus } from \"@kb-labs/commit-contracts\";\n\n/**\n * Get current git status (staged, unstaged, untracked files).\n * cwd must already point to the resolved scope directory.\n */\nexport async function getGitStatus(cwd: string): Promise<GitStatus> {\n // --ignore-submodules=all: exclude submodule pointer drift in worktrees\n const git: SimpleGit = simpleGit(cwd);\n const status: StatusResult = await git.status(['--ignore-submodules=all']);\n\n return {\n staged: status.staged.filter((f) => !shouldIgnoreFile(f)),\n unstaged: [...status.modified, ...status.deleted]\n .filter((f) => !status.staged.includes(f))\n .filter((f) => !shouldIgnoreFile(f)),\n untracked: status.not_added.filter((f) => !shouldIgnoreFile(f)),\n };\n}\n\nconst IGNORED_SEGMENTS = new Set([\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \".next\",\n \".turbo\",\n \"coverage\",\n]);\n\n/**\n * Check if file should be ignored (node_modules, dist, etc.)\n * Uses exact path segment matching to avoid false positives like \"my-dist/file.ts\".\n */\nfunction shouldIgnoreFile(file: string): boolean {\n return file.split(\"/\").some((segment) => IGNORED_SEGMENTS.has(segment));\n}\n\n/**\n * Get all changed files (staged + unstaged + untracked)\n * Filters out node_modules and other build artifacts\n */\nexport function getAllChangedFiles(status: GitStatus): string[] {\n const allFiles = [\n ...new Set([...status.staged, ...status.unstaged, ...status.untracked]),\n ];\n return allFiles.filter((file) => !shouldIgnoreFile(file));\n}\n\n/**\n * Check if there are any changes\n */\nexport function hasChanges(status: GitStatus): boolean {\n return (\n status.staged.length > 0 ||\n status.unstaged.length > 0 ||\n status.untracked.length > 0\n );\n}\n\n/**\n * Get current branch name\n */\nexport async function getCurrentBranch(cwd: string): Promise<string> {\n const git: SimpleGit = simpleGit(cwd);\n const branch = await git.revparse([\"--abbrev-ref\", \"HEAD\"]);\n return branch.trim();\n}\n\n/**\n * Check if branch is protected (main/master)\n */\nexport function isProtectedBranch(branch: string): boolean {\n const protectedBranches = [\n \"main\",\n \"master\",\n \"develop\",\n \"release\",\n \"production\",\n ];\n return protectedBranches.includes(branch.toLowerCase());\n}\n","/**\n * Commit plan applier\n */\n\n/* eslint-disable no-await-in-loop -- Sequential git commits required: must stage files and commit one group at a time */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { simpleGit, type SimpleGit } from \"simple-git\";\nimport type {\n CommitPlan,\n ApplyResult,\n CommitGroup,\n} from \"@kb-labs/commit-contracts\";\nimport type { ApplyOptions } from \"../types\";\nimport { getGitStatus, getAllChangedFiles } from \"../analyzer/git-status\";\nimport { useLogger } from \"@kb-labs/sdk\";\n\nconst GIT_HOOK_NAMES = [\n \"pre-commit\",\n \"commit-msg\",\n \"post-commit\",\n \"prepare-commit-msg\",\n \"pre-push\",\n] as const;\n\nfunction cleanGitError(message: string): string {\n return message\n .split(\"\\n\")\n .filter((line) => !line.trimStart().startsWith(\"hint:\"))\n .join(\"\\n\")\n .trim();\n}\n\nfunction formatCommitError(message: string): string {\n const cleaned = cleanGitError(message);\n const isHookError =\n /hook\\s+(failed|exited|returned)/i.test(cleaned) ||\n /husky/i.test(cleaned) ||\n /lint-staged/i.test(cleaned) ||\n GIT_HOOK_NAMES.some((h) => cleaned.includes(h));\n\n if (isHookError) {\n const match = cleaned.match(\n /(pre-commit|commit-msg|post-commit|prepare-commit-msg|pre-push)/i,\n );\n const hookName = match?.[1] ?? \"git hook\";\n return `${hookName} hook failed:\\n${cleaned}`;\n }\n return cleaned;\n}\n\n/**\n * Apply a commit plan - creates local git commits\n *\n * @param cwd - Working directory (repo root)\n * @param plan - Commit plan to apply\n * @param options - Apply options\n * @returns Apply result with created commit SHAs\n */\nexport async function applyCommitPlan(\n cwd: string,\n plan: CommitPlan,\n options?: ApplyOptions,\n): Promise<ApplyResult> {\n const appliedCommits: ApplyResult[\"appliedCommits\"] = [];\n const errors: string[] = [];\n const logger = useLogger();\n const planFileCount = plan.commits.reduce((sum, c) => sum + c.files.length, 0);\n\n await logger.info(\"apply: start\", {\n scope: options?.scope,\n cwd,\n commits: plan.commits.length,\n planFiles: planFileCount,\n force: !!options?.force,\n });\n\n // 0. Validate plan integrity before touching git state.\n const integrityErrors = validatePlanIntegrity(plan);\n if (integrityErrors.length > 0) {\n await logger.warn(\"apply: plan integrity failed\", { errors: integrityErrors });\n return {\n success: false,\n appliedCommits: [],\n errors: integrityErrors,\n };\n }\n\n // 1. Check for staleness (only for files in the plan, not entire repo)\n if (!options?.force) {\n const staleness = await checkStaleness(cwd, plan, options?.scope);\n if (staleness.isStale) {\n return {\n success: false,\n appliedCommits: [],\n errors: [staleness.reason],\n };\n }\n }\n\n // 2. Apply each commit in order\n for (const commit of plan.commits) {\n try {\n const sha = await applyCommit(cwd, commit);\n appliedCommits.push({\n groupId: commit.id,\n sha,\n message: formatCommitMessage(commit),\n });\n } catch (error) {\n const raw = error instanceof Error ? error.message : String(error);\n errors.push(`Failed to apply commit ${commit.id}: ${formatCommitError(raw)}`);\n\n // Stop on first error - don't leave repo in inconsistent state\n break;\n }\n }\n\n return {\n success: errors.length === 0,\n appliedCommits,\n errors,\n };\n}\n\nfunction validatePlanIntegrity(plan: CommitPlan): string[] {\n const errors: string[] = [];\n const seenInCommit = new Map<string, string>();\n\n for (const commit of plan.commits) {\n if (commit.files.length === 0) {\n errors.push(`Commit ${commit.id} has no files`);\n continue;\n }\n\n if (!commit.message.trim()) {\n errors.push(`Commit ${commit.id} has empty message`);\n continue;\n }\n\n for (const file of commit.files) {\n const firstCommit = seenInCommit.get(file);\n if (firstCommit) {\n errors.push(\n `File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`,\n );\n } else {\n seenInCommit.set(file, commit.id);\n }\n }\n }\n\n return errors;\n}\n\n/**\n * Apply a single commit\n * Supports nested git repositories - files with 'nested-repo/...' prefix will be committed in the nested repo\n */\nasync function applyCommit(cwd: string, commit: CommitGroup): Promise<string> {\n // Group files by repository (root vs nested)\n const filesByRepo = groupFilesByRepo(cwd, commit.files);\n\n // For now, we only support commits within a single repo\n // If files span multiple repos, we need to handle that differently\n const repos = Array.from(filesByRepo.keys());\n\n if (repos.length > 1) {\n throw new Error(\n \"Commit spans multiple repositories. Split into separate commits.\",\n );\n }\n\n const [repoPath, fileInfos] = Array.from(filesByRepo.entries())[0]!;\n const git: SimpleGit = simpleGit(repoPath);\n\n // Unstage all currently staged files so only plan files end up in the commit.\n // restore --staged requires HEAD; fall back to rm --cached for fresh repos.\n const statusBefore = await git.status();\n if (statusBefore.staged.length > 0) {\n const restored = await git\n .raw([\"restore\", \"--staged\", \"--\", ...statusBefore.staged])\n .then(() => true)\n .catch(() => false);\n if (!restored) {\n for (const f of statusBefore.staged) {\n await git.raw([\"rm\", \"--cached\", \"--\", f]).catch(() => {});\n }\n }\n }\n\n // Stage the files for this commit, handling gitignored-but-tracked files\n for (const { relativePath } of fileInfos) {\n try {\n await git.add(relativePath);\n } catch (addErr) {\n const msg = addErr instanceof Error ? addErr.message : String(addErr);\n if (msg.includes(\"ignored by one of your .gitignore files\")) {\n // File is inside a gitignored directory — check if it's tracked (previously committed)\n const isTracked = await git\n .raw([\"ls-files\", \"--error-unmatch\", relativePath])\n .then(() => true)\n .catch(() => false);\n if (isTracked) {\n await git.raw([\"add\", \"-f\", relativePath]);\n } else {\n throw new Error(\n `File is gitignored and untracked, cannot stage: ${relativePath}`,\n );\n }\n } else {\n throw new Error(cleanGitError(msg));\n }\n }\n }\n\n // Verify something was actually staged for this commit\n const statusAfterAdd = await git.status();\n const stagedSet = new Set(statusAfterAdd.staged);\n const anyStagedForCommit = fileInfos.some((fi) =>\n stagedSet.has(fi.relativePath),\n );\n if (!anyStagedForCommit) {\n throw new Error(\n `Nothing staged for this commit — files may already be committed or have no changes`,\n );\n }\n\n // Commit only the plan files (staging area contains exclusively those at this point)\n const message = formatCommitMessage(commit);\n const result = await git.commit(message);\n\n return result.commit;\n}\n\n/**\n * Group files by their git repository (root or nested)\n */\nfunction groupFilesByRepo(\n cwd: string,\n files: string[],\n): Map<string, { relativePath: string; originalPath: string }[]> {\n const filesByRepo = new Map<\n string,\n { relativePath: string; originalPath: string }[]\n >();\n\n for (const file of files) {\n // Check if file is in a nested repo (first segment might be a git repo)\n const segments = file.split(\"/\");\n const potentialRepoDir = segments[0];\n\n // Handle edge case: empty file path or no segments\n if (!potentialRepoDir) {\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n continue;\n }\n\n const potentialRepoPath = join(cwd, potentialRepoDir);\n const potentialGitDir = join(potentialRepoPath, \".git\");\n\n // Check if it's actually a nested git repo\n const isNestedRepo = existsSync(potentialGitDir);\n\n if (isNestedRepo) {\n // Use nested repo as git root, strip first segment from path\n const relativePath = segments.slice(1).join(\"/\");\n const group = filesByRepo.get(potentialRepoPath) ?? [];\n group.push({ relativePath, originalPath: file });\n filesByRepo.set(potentialRepoPath, group);\n } else {\n // Use cwd as git root\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n }\n }\n\n return filesByRepo;\n}\n\n/** Commit footer for branding */\nconst COMMIT_FOOTER = \"\\n\\n🤖 Generated by kb-labs-commit-plugin\";\n\n/**\n * Format commit message following conventional commits\n */\nexport function formatCommitMessage(\n commit: CommitGroup,\n options?: { includeFooter?: boolean },\n): string {\n const type = commit.type;\n const scope = commit.scope ? `(${commit.scope})` : \"\";\n const breaking = commit.breaking ? \"!\" : \"\";\n const subject = commit.message;\n\n let message = `${type}${scope}${breaking}: ${subject}`;\n\n // Add body if present\n if (commit.body) {\n message += `\\n\\n${commit.body}`;\n }\n\n // Add branding footer (default: true)\n if (options?.includeFooter !== false) {\n message += COMMIT_FOOTER;\n }\n\n return message;\n}\n\n/**\n * Check if files in the plan have changed since plan generation\n * Only checks files that are part of the plan, ignoring other changes in the repo\n */\nasync function checkStaleness(\n cwd: string,\n plan: CommitPlan,\n scope?: string,\n): Promise<{ isStale: boolean; reason: string }> {\n const logger = useLogger();\n const planFiles = new Set(plan.commits.flatMap((c) => c.files));\n\n await logger.debug(\"checkStaleness: start\", {\n scope,\n cwd,\n planFiles: [...planFiles],\n });\n\n // If no files in plan, nothing to check\n if (planFiles.size === 0) {\n return { isStale: false, reason: \"\" };\n }\n\n // Determine which repo(s) we need to check\n const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);\n\n // Check each repo for staleness\n for (const [repoPath, fileInfos] of filesByRepo) {\n // Get current git status from the repo\n // Note: repoPath already points to the correct git repository root\n const currentStatus = await getGitStatus(repoPath);\n const currentFiles = new Set(getAllChangedFiles(currentStatus));\n\n await logger.debug(\"checkStaleness: repo status\", {\n repoPath,\n staged: currentStatus.staged,\n unstaged: currentStatus.unstaged,\n untracked: currentStatus.untracked,\n expected: fileInfos.map((f) => f.relativePath),\n });\n\n // Check that all expected files are still changed\n for (const { relativePath, originalPath } of fileInfos) {\n if (!currentFiles.has(relativePath)) {\n await logger.warn(\"checkStaleness: file not in current changes\", {\n scope,\n repoPath,\n originalPath,\n relativePath,\n currentFiles: [...currentFiles],\n });\n return {\n isStale: true,\n reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`,\n };\n }\n }\n }\n\n return { isStale: false, reason: \"\" };\n}\n","/**\n * Git push operations\n */\n\nimport { simpleGit, type SimpleGit } from 'simple-git';\nimport type { PushResult } from '@kb-labs/commit-contracts';\nimport type { PushOptions } from '../types';\nimport { getCurrentBranch, isProtectedBranch } from '../analyzer/git-status';\n\n/**\n * Push commits to remote repository\n *\n * @param cwd - Working directory (repo root)\n * @param options - Push options\n * @returns Push result\n */\nexport async function pushCommits(cwd: string, options?: PushOptions): Promise<PushResult> {\n const git: SimpleGit = simpleGit(cwd);\n const remote = options?.remote || 'origin';\n\n try {\n // Get current branch\n const branch = await getCurrentBranch(cwd);\n\n // Warn about protected branches with force push\n if (options?.force && isProtectedBranch(branch)) {\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: `Refusing to force push to protected branch '${branch}'. This is dangerous and disabled by default.`,\n };\n }\n\n // Check how many commits ahead of remote\n const commitsToPush = await countCommitsToPush(git, remote, branch);\n\n if (commitsToPush === 0) {\n return {\n success: true,\n remote,\n branch,\n commitsPushed: 0,\n };\n }\n\n // Push to remote\n const pushOptions = options?.force ? ['--force'] : [];\n await git.push(remote, branch, pushOptions);\n\n return {\n success: true,\n remote,\n branch,\n commitsPushed: commitsToPush,\n };\n } catch (error) {\n const raw = error instanceof Error ? error.message : String(error);\n const message = cleanGitError(raw);\n const branch = await getCurrentBranch(cwd).catch(() => 'unknown');\n\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: message,\n };\n }\n}\n\nfunction cleanGitError(message: string): string {\n return message\n .split('\\n')\n .filter((line) => !line.trimStart().startsWith('hint:'))\n .join('\\n')\n .trim();\n}\n\n/**\n * Count commits ahead of remote\n */\nasync function countCommitsToPush(\n git: SimpleGit,\n remote: string,\n branch: string\n): Promise<number> {\n try {\n // First fetch to ensure we have latest remote refs\n await git.fetch(remote, branch);\n\n // Count commits between remote and local\n const result = await git.raw([\n 'rev-list',\n '--count',\n `${remote}/${branch}..HEAD`,\n ]);\n\n return parseInt(result.trim(), 10) || 0;\n } catch {\n // If remote doesn't have the branch, all local commits need pushing\n try {\n const result = await git.raw(['rev-list', '--count', 'HEAD']);\n return parseInt(result.trim(), 10) || 0;\n } catch {\n return 0;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/analyzer/git-status.ts","../../src/validator/index.ts","../../src/applier/apply.ts","../../src/applier/push.ts"],"names":["useLogger","simpleGit","cleanGitError"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Git status analysis\n */\n\nimport { simpleGit, type SimpleGit, type StatusResult } from \"simple-git\";\nimport type { GitStatus } from \"@kb-labs/commit-contracts\";\n\n/**\n * Get current git status (staged, unstaged, untracked files).\n * cwd must already point to the resolved scope directory.\n */\nexport async function getGitStatus(cwd: string): Promise<GitStatus> {\n // --ignore-submodules=all: exclude submodule pointer drift in worktrees\n const git: SimpleGit = simpleGit(cwd);\n const status: StatusResult = await git.status(['--ignore-submodules=all']);\n\n return {\n staged: status.staged.filter((f) => !shouldIgnoreFile(f)),\n unstaged: [...status.modified, ...status.deleted]\n .filter((f) => !status.staged.includes(f))\n .filter((f) => !shouldIgnoreFile(f)),\n untracked: status.not_added.filter((f) => !shouldIgnoreFile(f)),\n };\n}\n\nconst IGNORED_SEGMENTS = new Set([\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \".next\",\n \".turbo\",\n \"coverage\",\n]);\n\n/**\n * Check if file should be ignored (node_modules, dist, etc.)\n * Uses exact path segment matching to avoid false positives like \"my-dist/file.ts\".\n */\nfunction shouldIgnoreFile(file: string): boolean {\n return file.split(\"/\").some((segment) => IGNORED_SEGMENTS.has(segment));\n}\n\n/**\n * Get all changed files (staged + unstaged + untracked)\n * Filters out node_modules and other build artifacts\n */\nexport function getAllChangedFiles(status: GitStatus): string[] {\n const allFiles = [\n ...new Set([...status.staged, ...status.unstaged, ...status.untracked]),\n ];\n return allFiles.filter((file) => !shouldIgnoreFile(file));\n}\n\n/**\n * Check if there are any changes\n */\nexport function hasChanges(status: GitStatus): boolean {\n return (\n status.staged.length > 0 ||\n status.unstaged.length > 0 ||\n status.untracked.length > 0\n );\n}\n\n/**\n * Get current branch name\n */\nexport async function getCurrentBranch(cwd: string): Promise<string> {\n const git: SimpleGit = simpleGit(cwd);\n const branch = await git.revparse([\"--abbrev-ref\", \"HEAD\"]);\n return branch.trim();\n}\n\n/**\n * Check if branch is protected (main/master)\n */\nexport function isProtectedBranch(branch: string): boolean {\n const protectedBranches = [\n \"main\",\n \"master\",\n \"develop\",\n \"release\",\n \"production\",\n ];\n return protectedBranches.includes(branch.toLowerCase());\n}\n","/**\n * Commit plan validation — shared by applier, REST handlers, CLI, and MCP.\n *\n * Centralizes checks that used to live only inside apply.ts, so that any\n * surface (Studio, CLI, MCP) can proactively report plan integrity/staleness\n * before the user attempts to apply, instead of only failing at apply time.\n *\n * @module @kb-labs/commit-core/validator\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { CommitPlan } from \"@kb-labs/commit-contracts\";\nimport { useLogger } from \"@kb-labs/sdk\";\nimport { getGitStatus, getAllChangedFiles } from \"../analyzer/git-status\";\n\n/**\n * Group files by their git repository (root or nested)\n *\n * Supports nested git repositories: files with a first path segment that is\n * itself a git repo root are grouped under that nested repo instead of cwd.\n */\nexport function groupFilesByRepo(\n cwd: string,\n files: string[],\n): Map<string, { relativePath: string; originalPath: string }[]> {\n const filesByRepo = new Map<\n string,\n { relativePath: string; originalPath: string }[]\n >();\n\n for (const file of files) {\n // Check if file is in a nested repo (first segment might be a git repo)\n const segments = file.split(\"/\");\n const potentialRepoDir = segments[0];\n\n // Handle edge case: empty file path or no segments\n if (!potentialRepoDir) {\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n continue;\n }\n\n const potentialRepoPath = join(cwd, potentialRepoDir);\n const potentialGitDir = join(potentialRepoPath, \".git\");\n\n // Check if it's actually a nested git repo\n const isNestedRepo = existsSync(potentialGitDir);\n\n if (isNestedRepo) {\n // Use nested repo as git root, strip first segment from path\n const relativePath = segments.slice(1).join(\"/\");\n const group = filesByRepo.get(potentialRepoPath) ?? [];\n group.push({ relativePath, originalPath: file });\n filesByRepo.set(potentialRepoPath, group);\n } else {\n // Use cwd as git root\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n }\n }\n\n return filesByRepo;\n}\n\n/**\n * Validate internal consistency of a commit plan: every commit has files and\n * a message, and no file appears in more than one commit.\n *\n * Returns an array of human-readable error strings (empty = valid).\n */\nexport function validatePlanIntegrity(plan: CommitPlan): string[] {\n const errors: string[] = [];\n const seenInCommit = new Map<string, string>();\n\n for (const commit of plan.commits) {\n if (commit.files.length === 0) {\n errors.push(`Commit ${commit.id} has no files`);\n continue;\n }\n\n if (!commit.message.trim()) {\n errors.push(`Commit ${commit.id} has empty message`);\n continue;\n }\n\n for (const file of commit.files) {\n const firstCommit = seenInCommit.get(file);\n if (firstCommit) {\n errors.push(\n `File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`,\n );\n } else {\n seenInCommit.set(file, commit.id);\n }\n }\n }\n\n return errors;\n}\n\n/**\n * Check if files in the plan have changed since plan generation.\n *\n * Only checks files that are part of the plan, ignoring other changes in the\n * repo — cheap even on a large repo. Used both proactively (status handlers,\n * before the user attempts Apply) and as the last-second guard inside\n * applyCommitPlan.\n */\nexport async function checkPlanStaleness(\n cwd: string,\n plan: CommitPlan,\n scope?: string,\n): Promise<{ isStale: boolean; reason: string }> {\n const logger = useLogger();\n const planFiles = new Set(plan.commits.flatMap((c) => c.files));\n\n await logger.debug(\"checkPlanStaleness: start\", {\n scope,\n cwd,\n planFiles: [...planFiles],\n });\n\n // If no files in plan, nothing to check\n if (planFiles.size === 0) {\n return { isStale: false, reason: \"\" };\n }\n\n // Determine which repo(s) we need to check\n const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);\n\n // Check each repo for staleness\n for (const [repoPath, fileInfos] of filesByRepo) {\n // Get current git status from the repo\n // Note: repoPath already points to the correct git repository root\n const currentStatus = await getGitStatus(repoPath);\n const currentFiles = new Set(getAllChangedFiles(currentStatus));\n\n await logger.debug(\"checkPlanStaleness: repo status\", {\n repoPath,\n staged: currentStatus.staged,\n unstaged: currentStatus.unstaged,\n untracked: currentStatus.untracked,\n expected: fileInfos.map((f) => f.relativePath),\n });\n\n // Check that all expected files are still changed\n for (const { relativePath, originalPath } of fileInfos) {\n if (!currentFiles.has(relativePath)) {\n await logger.warn(\"checkPlanStaleness: file not in current changes\", {\n scope,\n repoPath,\n originalPath,\n relativePath,\n currentFiles: [...currentFiles],\n });\n return {\n isStale: true,\n reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`,\n };\n }\n }\n }\n\n return { isStale: false, reason: \"\" };\n}\n","/**\n * Commit plan applier\n */\n\n/* eslint-disable no-await-in-loop -- Sequential git commits required: must stage files and commit one group at a time */\n\nimport { simpleGit, type SimpleGit } from \"simple-git\";\nimport type {\n CommitPlan,\n ApplyResult,\n CommitGroup,\n} from \"@kb-labs/commit-contracts\";\nimport type { ApplyOptions } from \"../types\";\nimport { validatePlanIntegrity, checkPlanStaleness, groupFilesByRepo } from \"../validator\";\nimport { useLogger } from \"@kb-labs/sdk\";\n\nconst GIT_HOOK_NAMES = [\n \"pre-commit\",\n \"commit-msg\",\n \"post-commit\",\n \"prepare-commit-msg\",\n \"pre-push\",\n] as const;\n\nfunction cleanGitError(message: string): string {\n return message\n .split(\"\\n\")\n .filter((line) => !line.trimStart().startsWith(\"hint:\"))\n .join(\"\\n\")\n .trim();\n}\n\nfunction formatCommitError(message: string): string {\n const cleaned = cleanGitError(message);\n const isHookError =\n /hook\\s+(failed|exited|returned)/i.test(cleaned) ||\n /husky/i.test(cleaned) ||\n /lint-staged/i.test(cleaned) ||\n GIT_HOOK_NAMES.some((h) => cleaned.includes(h));\n\n if (isHookError) {\n const match = cleaned.match(\n /(pre-commit|commit-msg|post-commit|prepare-commit-msg|pre-push)/i,\n );\n const hookName = match?.[1] ?? \"git hook\";\n return `${hookName} hook failed:\\n${cleaned}`;\n }\n return cleaned;\n}\n\n/**\n * Apply a commit plan - creates local git commits\n *\n * @param cwd - Working directory (repo root)\n * @param plan - Commit plan to apply\n * @param options - Apply options\n * @returns Apply result with created commit SHAs\n */\nexport async function applyCommitPlan(\n cwd: string,\n plan: CommitPlan,\n options?: ApplyOptions,\n): Promise<ApplyResult> {\n const appliedCommits: ApplyResult[\"appliedCommits\"] = [];\n const errors: string[] = [];\n const logger = useLogger();\n const planFileCount = plan.commits.reduce((sum, c) => sum + c.files.length, 0);\n\n await logger.info(\"apply: start\", {\n scope: options?.scope,\n cwd,\n commits: plan.commits.length,\n planFiles: planFileCount,\n force: !!options?.force,\n });\n\n // 0. Validate plan integrity before touching git state.\n const integrityErrors = validatePlanIntegrity(plan);\n if (integrityErrors.length > 0) {\n await logger.warn(\"apply: plan integrity failed\", { errors: integrityErrors });\n return {\n success: false,\n appliedCommits: [],\n errors: integrityErrors,\n };\n }\n\n // 1. Check for staleness (only for files in the plan, not entire repo)\n if (!options?.force) {\n const staleness = await checkPlanStaleness(cwd, plan, options?.scope);\n if (staleness.isStale) {\n return {\n success: false,\n appliedCommits: [],\n errors: [staleness.reason],\n };\n }\n }\n\n // 2. Apply each commit in order\n for (const commit of plan.commits) {\n try {\n const sha = await applyCommit(cwd, commit);\n appliedCommits.push({\n groupId: commit.id,\n sha,\n message: formatCommitMessage(commit),\n });\n } catch (error) {\n const raw = error instanceof Error ? error.message : String(error);\n errors.push(`Failed to apply commit ${commit.id}: ${formatCommitError(raw)}`);\n\n // Stop on first error - don't leave repo in inconsistent state\n break;\n }\n }\n\n return {\n success: errors.length === 0,\n appliedCommits,\n errors,\n };\n}\n\n/**\n * Apply a single commit\n * Supports nested git repositories - files with 'nested-repo/...' prefix will be committed in the nested repo\n */\nasync function applyCommit(cwd: string, commit: CommitGroup): Promise<string> {\n // Group files by repository (root vs nested)\n const filesByRepo = groupFilesByRepo(cwd, commit.files);\n\n // For now, we only support commits within a single repo\n // If files span multiple repos, we need to handle that differently\n const repos = Array.from(filesByRepo.keys());\n\n if (repos.length > 1) {\n throw new Error(\n \"Commit spans multiple repositories. Split into separate commits.\",\n );\n }\n\n const [repoPath, fileInfos] = Array.from(filesByRepo.entries())[0]!;\n const git: SimpleGit = simpleGit(repoPath);\n\n // Unstage all currently staged files so only plan files end up in the commit.\n // restore --staged requires HEAD; fall back to rm --cached for fresh repos.\n const statusBefore = await git.status();\n if (statusBefore.staged.length > 0) {\n const restored = await git\n .raw([\"restore\", \"--staged\", \"--\", ...statusBefore.staged])\n .then(() => true)\n .catch(() => false);\n if (!restored) {\n for (const f of statusBefore.staged) {\n await git.raw([\"rm\", \"--cached\", \"--\", f]).catch(() => {});\n }\n }\n }\n\n // Stage the files for this commit, handling gitignored-but-tracked files\n for (const { relativePath } of fileInfos) {\n try {\n await git.add(relativePath);\n } catch (addErr) {\n const msg = addErr instanceof Error ? addErr.message : String(addErr);\n if (msg.includes(\"ignored by one of your .gitignore files\")) {\n // File is inside a gitignored directory — check if it's tracked (previously committed)\n const isTracked = await git\n .raw([\"ls-files\", \"--error-unmatch\", relativePath])\n .then(() => true)\n .catch(() => false);\n if (isTracked) {\n await git.raw([\"add\", \"-f\", relativePath]);\n } else {\n throw new Error(\n `File is gitignored and untracked, cannot stage: ${relativePath}`,\n );\n }\n } else {\n throw new Error(cleanGitError(msg));\n }\n }\n }\n\n // Verify something was actually staged for this commit\n const statusAfterAdd = await git.status();\n const stagedSet = new Set(statusAfterAdd.staged);\n const anyStagedForCommit = fileInfos.some((fi) =>\n stagedSet.has(fi.relativePath),\n );\n if (!anyStagedForCommit) {\n throw new Error(\n `Nothing staged for this commit — files may already be committed or have no changes`,\n );\n }\n\n // Commit only the plan files (staging area contains exclusively those at this point)\n const message = formatCommitMessage(commit);\n const result = await git.commit(message);\n\n return result.commit;\n}\n\n/** Commit footer for branding */\nconst COMMIT_FOOTER = \"\\n\\n🤖 Generated by kb-labs-commit-plugin\";\n\n/**\n * Format commit message following conventional commits\n */\nexport function formatCommitMessage(\n commit: CommitGroup,\n options?: { includeFooter?: boolean },\n): string {\n const type = commit.type;\n const scope = commit.scope ? `(${commit.scope})` : \"\";\n const breaking = commit.breaking ? \"!\" : \"\";\n const subject = commit.message;\n\n let message = `${type}${scope}${breaking}: ${subject}`;\n\n // Add body if present\n if (commit.body) {\n message += `\\n\\n${commit.body}`;\n }\n\n // Add branding footer (default: true)\n if (options?.includeFooter !== false) {\n message += COMMIT_FOOTER;\n }\n\n return message;\n}\n","/**\n * Git push operations\n */\n\nimport { simpleGit, type SimpleGit } from 'simple-git';\nimport type { PushResult } from '@kb-labs/commit-contracts';\nimport type { PushOptions } from '../types';\nimport { getCurrentBranch, isProtectedBranch } from '../analyzer/git-status';\n\n/**\n * Push commits to remote repository\n *\n * @param cwd - Working directory (repo root)\n * @param options - Push options\n * @returns Push result\n */\nexport async function pushCommits(cwd: string, options?: PushOptions): Promise<PushResult> {\n const git: SimpleGit = simpleGit(cwd);\n const remote = options?.remote || 'origin';\n\n try {\n // Get current branch\n const branch = await getCurrentBranch(cwd);\n\n // Warn about protected branches with force push\n if (options?.force && isProtectedBranch(branch)) {\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: `Refusing to force push to protected branch '${branch}'. This is dangerous and disabled by default.`,\n };\n }\n\n // Check how many commits ahead of remote\n const commitsToPush = await countCommitsToPush(git, remote, branch);\n\n if (commitsToPush === 0) {\n return {\n success: true,\n remote,\n branch,\n commitsPushed: 0,\n };\n }\n\n // Push to remote\n const pushOptions = options?.force ? ['--force'] : [];\n await git.push(remote, branch, pushOptions);\n\n return {\n success: true,\n remote,\n branch,\n commitsPushed: commitsToPush,\n };\n } catch (error) {\n const raw = error instanceof Error ? error.message : String(error);\n const message = cleanGitError(raw);\n const branch = await getCurrentBranch(cwd).catch(() => 'unknown');\n\n return {\n success: false,\n remote,\n branch,\n commitsPushed: 0,\n error: message,\n };\n }\n}\n\nfunction cleanGitError(message: string): string {\n return message\n .split('\\n')\n .filter((line) => !line.trimStart().startsWith('hint:'))\n .join('\\n')\n .trim();\n}\n\n/**\n * Count commits ahead of remote\n */\nasync function countCommitsToPush(\n git: SimpleGit,\n remote: string,\n branch: string\n): Promise<number> {\n try {\n // First fetch to ensure we have latest remote refs\n await git.fetch(remote, branch);\n\n // Count commits between remote and local\n const result = await git.raw([\n 'rev-list',\n '--count',\n `${remote}/${branch}..HEAD`,\n ]);\n\n return parseInt(result.trim(), 10) || 0;\n } catch {\n // If remote doesn't have the branch, all local commits need pushing\n try {\n const result = await git.raw(['rev-list', '--count', 'HEAD']);\n return parseInt(result.trim(), 10) || 0;\n } catch {\n return 0;\n }\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { A as ApplyOptions, G as GenerateOptions, a as GitStatusWithStaleness, L
|
|
|
2
2
|
export { F as FileDiff, d as detectCommitStyle, f as formatFileSummary, g as getAllChangedFiles, a as getCurrentBranch, b as getFileDiff, c as getFileSummaries, e as getGitStatus, h as getRecentCommits, i as hasChanges, j as isProtectedBranch } from './recent-commits-BnietMgO.js';
|
|
3
3
|
export { S as SYSTEM_PROMPT, b as buildPrompt, g as generateCommitPlan, a as generateHeuristicPlan, p as parseResponse } from './heuristics-AM06lXSo.js';
|
|
4
4
|
export { applyCommitPlan, formatCommitMessage, pushCommits } from './applier/index.js';
|
|
5
|
+
export { checkPlanStaleness, groupFilesByRepo, validatePlanIntegrity } from './validator/index.js';
|
|
5
6
|
export { clearPlan, getCommitStoragePath, getCurrentPlanPath, getCurrentStatusPath, hasPlan, initStorage, listHistory, loadPlan, loadStatus, savePlan, saveToHistory } from './storage/index.js';
|
|
6
7
|
export { ApplyResult, CommitGroup, CommitPlan, ConventionalType, FileSummary, GitStatus, GitStatusSnapshot, PushResult, ReleaseHint } from '@kb-labs/commit-contracts';
|
package/dist/index.js
CHANGED
|
@@ -2766,6 +2766,98 @@ function getErrorType(error) {
|
|
|
2766
2766
|
const preview = error.message.substring(0, 50).replace(/\n/g, " ");
|
|
2767
2767
|
return `error: ${preview}${error.message.length > 50 ? "..." : ""}`;
|
|
2768
2768
|
}
|
|
2769
|
+
function groupFilesByRepo(cwd, files) {
|
|
2770
|
+
const filesByRepo = /* @__PURE__ */ new Map();
|
|
2771
|
+
for (const file of files) {
|
|
2772
|
+
const segments = file.split("/");
|
|
2773
|
+
const potentialRepoDir = segments[0];
|
|
2774
|
+
if (!potentialRepoDir) {
|
|
2775
|
+
const group = filesByRepo.get(cwd) ?? [];
|
|
2776
|
+
group.push({ relativePath: file, originalPath: file });
|
|
2777
|
+
filesByRepo.set(cwd, group);
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
const potentialRepoPath = join(cwd, potentialRepoDir);
|
|
2781
|
+
const potentialGitDir = join(potentialRepoPath, ".git");
|
|
2782
|
+
const isNestedRepo = existsSync(potentialGitDir);
|
|
2783
|
+
if (isNestedRepo) {
|
|
2784
|
+
const relativePath = segments.slice(1).join("/");
|
|
2785
|
+
const group = filesByRepo.get(potentialRepoPath) ?? [];
|
|
2786
|
+
group.push({ relativePath, originalPath: file });
|
|
2787
|
+
filesByRepo.set(potentialRepoPath, group);
|
|
2788
|
+
} else {
|
|
2789
|
+
const group = filesByRepo.get(cwd) ?? [];
|
|
2790
|
+
group.push({ relativePath: file, originalPath: file });
|
|
2791
|
+
filesByRepo.set(cwd, group);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return filesByRepo;
|
|
2795
|
+
}
|
|
2796
|
+
function validatePlanIntegrity(plan) {
|
|
2797
|
+
const errors = [];
|
|
2798
|
+
const seenInCommit = /* @__PURE__ */ new Map();
|
|
2799
|
+
for (const commit of plan.commits) {
|
|
2800
|
+
if (commit.files.length === 0) {
|
|
2801
|
+
errors.push(`Commit ${commit.id} has no files`);
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
if (!commit.message.trim()) {
|
|
2805
|
+
errors.push(`Commit ${commit.id} has empty message`);
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
for (const file of commit.files) {
|
|
2809
|
+
const firstCommit = seenInCommit.get(file);
|
|
2810
|
+
if (firstCommit) {
|
|
2811
|
+
errors.push(
|
|
2812
|
+
`File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`
|
|
2813
|
+
);
|
|
2814
|
+
} else {
|
|
2815
|
+
seenInCommit.set(file, commit.id);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
return errors;
|
|
2820
|
+
}
|
|
2821
|
+
async function checkPlanStaleness(cwd, plan, scope) {
|
|
2822
|
+
const logger = useLogger();
|
|
2823
|
+
const planFiles = new Set(plan.commits.flatMap((c) => c.files));
|
|
2824
|
+
await logger.debug("checkPlanStaleness: start", {
|
|
2825
|
+
scope,
|
|
2826
|
+
cwd,
|
|
2827
|
+
planFiles: [...planFiles]
|
|
2828
|
+
});
|
|
2829
|
+
if (planFiles.size === 0) {
|
|
2830
|
+
return { isStale: false, reason: "" };
|
|
2831
|
+
}
|
|
2832
|
+
const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);
|
|
2833
|
+
for (const [repoPath, fileInfos] of filesByRepo) {
|
|
2834
|
+
const currentStatus = await getGitStatus(repoPath);
|
|
2835
|
+
const currentFiles = new Set(getAllChangedFiles(currentStatus));
|
|
2836
|
+
await logger.debug("checkPlanStaleness: repo status", {
|
|
2837
|
+
repoPath,
|
|
2838
|
+
staged: currentStatus.staged,
|
|
2839
|
+
unstaged: currentStatus.unstaged,
|
|
2840
|
+
untracked: currentStatus.untracked,
|
|
2841
|
+
expected: fileInfos.map((f) => f.relativePath)
|
|
2842
|
+
});
|
|
2843
|
+
for (const { relativePath, originalPath } of fileInfos) {
|
|
2844
|
+
if (!currentFiles.has(relativePath)) {
|
|
2845
|
+
await logger.warn("checkPlanStaleness: file not in current changes", {
|
|
2846
|
+
scope,
|
|
2847
|
+
repoPath,
|
|
2848
|
+
originalPath,
|
|
2849
|
+
relativePath,
|
|
2850
|
+
currentFiles: [...currentFiles]
|
|
2851
|
+
});
|
|
2852
|
+
return {
|
|
2853
|
+
isStale: true,
|
|
2854
|
+
reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
return { isStale: false, reason: "" };
|
|
2860
|
+
}
|
|
2769
2861
|
var GIT_HOOK_NAMES = [
|
|
2770
2862
|
"pre-commit",
|
|
2771
2863
|
"commit-msg",
|
|
@@ -2811,7 +2903,7 @@ async function applyCommitPlan(cwd, plan, options) {
|
|
|
2811
2903
|
};
|
|
2812
2904
|
}
|
|
2813
2905
|
if (!options?.force) {
|
|
2814
|
-
const staleness = await
|
|
2906
|
+
const staleness = await checkPlanStaleness(cwd, plan, options?.scope);
|
|
2815
2907
|
if (staleness.isStale) {
|
|
2816
2908
|
return {
|
|
2817
2909
|
success: false,
|
|
@@ -2840,31 +2932,6 @@ async function applyCommitPlan(cwd, plan, options) {
|
|
|
2840
2932
|
errors
|
|
2841
2933
|
};
|
|
2842
2934
|
}
|
|
2843
|
-
function validatePlanIntegrity(plan) {
|
|
2844
|
-
const errors = [];
|
|
2845
|
-
const seenInCommit = /* @__PURE__ */ new Map();
|
|
2846
|
-
for (const commit of plan.commits) {
|
|
2847
|
-
if (commit.files.length === 0) {
|
|
2848
|
-
errors.push(`Commit ${commit.id} has no files`);
|
|
2849
|
-
continue;
|
|
2850
|
-
}
|
|
2851
|
-
if (!commit.message.trim()) {
|
|
2852
|
-
errors.push(`Commit ${commit.id} has empty message`);
|
|
2853
|
-
continue;
|
|
2854
|
-
}
|
|
2855
|
-
for (const file of commit.files) {
|
|
2856
|
-
const firstCommit = seenInCommit.get(file);
|
|
2857
|
-
if (firstCommit) {
|
|
2858
|
-
errors.push(
|
|
2859
|
-
`File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`
|
|
2860
|
-
);
|
|
2861
|
-
} else {
|
|
2862
|
-
seenInCommit.set(file, commit.id);
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
return errors;
|
|
2867
|
-
}
|
|
2868
2935
|
async function applyCommit(cwd, commit) {
|
|
2869
2936
|
const filesByRepo = groupFilesByRepo(cwd, commit.files);
|
|
2870
2937
|
const repos = Array.from(filesByRepo.keys());
|
|
@@ -2918,33 +2985,6 @@ async function applyCommit(cwd, commit) {
|
|
|
2918
2985
|
const result = await git.commit(message);
|
|
2919
2986
|
return result.commit;
|
|
2920
2987
|
}
|
|
2921
|
-
function groupFilesByRepo(cwd, files) {
|
|
2922
|
-
const filesByRepo = /* @__PURE__ */ new Map();
|
|
2923
|
-
for (const file of files) {
|
|
2924
|
-
const segments = file.split("/");
|
|
2925
|
-
const potentialRepoDir = segments[0];
|
|
2926
|
-
if (!potentialRepoDir) {
|
|
2927
|
-
const group = filesByRepo.get(cwd) ?? [];
|
|
2928
|
-
group.push({ relativePath: file, originalPath: file });
|
|
2929
|
-
filesByRepo.set(cwd, group);
|
|
2930
|
-
continue;
|
|
2931
|
-
}
|
|
2932
|
-
const potentialRepoPath = join(cwd, potentialRepoDir);
|
|
2933
|
-
const potentialGitDir = join(potentialRepoPath, ".git");
|
|
2934
|
-
const isNestedRepo = existsSync(potentialGitDir);
|
|
2935
|
-
if (isNestedRepo) {
|
|
2936
|
-
const relativePath = segments.slice(1).join("/");
|
|
2937
|
-
const group = filesByRepo.get(potentialRepoPath) ?? [];
|
|
2938
|
-
group.push({ relativePath, originalPath: file });
|
|
2939
|
-
filesByRepo.set(potentialRepoPath, group);
|
|
2940
|
-
} else {
|
|
2941
|
-
const group = filesByRepo.get(cwd) ?? [];
|
|
2942
|
-
group.push({ relativePath: file, originalPath: file });
|
|
2943
|
-
filesByRepo.set(cwd, group);
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
return filesByRepo;
|
|
2947
|
-
}
|
|
2948
2988
|
var COMMIT_FOOTER = "\n\n\u{1F916} Generated by kb-labs-commit-plugin";
|
|
2949
2989
|
function formatCommitMessage(commit, options) {
|
|
2950
2990
|
const type = commit.type;
|
|
@@ -2962,46 +3002,6 @@ ${commit.body}`;
|
|
|
2962
3002
|
}
|
|
2963
3003
|
return message;
|
|
2964
3004
|
}
|
|
2965
|
-
async function checkStaleness(cwd, plan, scope) {
|
|
2966
|
-
const logger = useLogger();
|
|
2967
|
-
const planFiles = new Set(plan.commits.flatMap((c) => c.files));
|
|
2968
|
-
await logger.debug("checkStaleness: start", {
|
|
2969
|
-
scope,
|
|
2970
|
-
cwd,
|
|
2971
|
-
planFiles: [...planFiles]
|
|
2972
|
-
});
|
|
2973
|
-
if (planFiles.size === 0) {
|
|
2974
|
-
return { isStale: false, reason: "" };
|
|
2975
|
-
}
|
|
2976
|
-
const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);
|
|
2977
|
-
for (const [repoPath, fileInfos] of filesByRepo) {
|
|
2978
|
-
const currentStatus = await getGitStatus(repoPath);
|
|
2979
|
-
const currentFiles = new Set(getAllChangedFiles(currentStatus));
|
|
2980
|
-
await logger.debug("checkStaleness: repo status", {
|
|
2981
|
-
repoPath,
|
|
2982
|
-
staged: currentStatus.staged,
|
|
2983
|
-
unstaged: currentStatus.unstaged,
|
|
2984
|
-
untracked: currentStatus.untracked,
|
|
2985
|
-
expected: fileInfos.map((f) => f.relativePath)
|
|
2986
|
-
});
|
|
2987
|
-
for (const { relativePath, originalPath } of fileInfos) {
|
|
2988
|
-
if (!currentFiles.has(relativePath)) {
|
|
2989
|
-
await logger.warn("checkStaleness: file not in current changes", {
|
|
2990
|
-
scope,
|
|
2991
|
-
repoPath,
|
|
2992
|
-
originalPath,
|
|
2993
|
-
relativePath,
|
|
2994
|
-
currentFiles: [...currentFiles]
|
|
2995
|
-
});
|
|
2996
|
-
return {
|
|
2997
|
-
isStale: true,
|
|
2998
|
-
reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`
|
|
2999
|
-
};
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
return { isStale: false, reason: "" };
|
|
3004
|
-
}
|
|
3005
3005
|
async function pushCommits(cwd, options) {
|
|
3006
3006
|
const git = simpleGit(cwd);
|
|
3007
3007
|
const remote = options?.remote || "origin";
|
|
@@ -3199,6 +3199,6 @@ async function initStorage(cwd, scope = "root") {
|
|
|
3199
3199
|
}
|
|
3200
3200
|
}
|
|
3201
3201
|
|
|
3202
|
-
export { SYSTEM_PROMPT, applyCommitPlan, buildPrompt, clearPlan, detectCommitStyle, formatCommitMessage, formatFileSummary, generateCommitPlan, generateHeuristicPlan, getAllChangedFiles, getCommitStoragePath, getCurrentBranch, getCurrentPlanPath, getCurrentStatusPath, getFileDiff, getFileSummaries, getGitStatus, getRecentCommits, hasChanges, hasPlan, initStorage, isProtectedBranch, listHistory, loadPlan, loadStatus, parseResponse, pushCommits, savePlan, saveToHistory };
|
|
3202
|
+
export { SYSTEM_PROMPT, applyCommitPlan, buildPrompt, checkPlanStaleness, clearPlan, detectCommitStyle, formatCommitMessage, formatFileSummary, generateCommitPlan, generateHeuristicPlan, getAllChangedFiles, getCommitStoragePath, getCurrentBranch, getCurrentPlanPath, getCurrentStatusPath, getFileDiff, getFileSummaries, getGitStatus, getRecentCommits, groupFilesByRepo, hasChanges, hasPlan, initStorage, isProtectedBranch, listHistory, loadPlan, loadStatus, parseResponse, pushCommits, savePlan, saveToHistory, validatePlanIntegrity };
|
|
3203
3203
|
//# sourceMappingURL=index.js.map
|
|
3204
3204
|
//# sourceMappingURL=index.js.map
|