@kb-labs/commit-core 2.94.0 → 2.98.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/index.js CHANGED
@@ -17,20 +17,17 @@ async function getGitStatus(cwd) {
17
17
  untracked: status.not_added.filter((f) => !shouldIgnoreFile(f))
18
18
  };
19
19
  }
20
+ var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
21
+ "node_modules",
22
+ ".git",
23
+ "dist",
24
+ "build",
25
+ ".next",
26
+ ".turbo",
27
+ "coverage"
28
+ ]);
20
29
  function shouldIgnoreFile(file) {
21
- const ignoredPaths = [
22
- "node_modules/",
23
- ".git/",
24
- "dist/",
25
- "build/",
26
- ".next/",
27
- ".turbo/",
28
- "coverage/",
29
- ".cache/",
30
- ".temp/",
31
- "tmp/"
32
- ];
33
- return ignoredPaths.some((path) => file.includes(path));
30
+ return file.split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
34
31
  }
35
32
  function getAllChangedFiles(status) {
36
33
  const allFiles = [
@@ -2769,11 +2766,44 @@ function getErrorType(error) {
2769
2766
  const preview = error.message.substring(0, 50).replace(/\n/g, " ");
2770
2767
  return `error: ${preview}${error.message.length > 50 ? "..." : ""}`;
2771
2768
  }
2769
+ var GIT_HOOK_NAMES = [
2770
+ "pre-commit",
2771
+ "commit-msg",
2772
+ "post-commit",
2773
+ "prepare-commit-msg",
2774
+ "pre-push"
2775
+ ];
2776
+ function cleanGitError(message) {
2777
+ return message.split("\n").filter((line) => !line.trimStart().startsWith("hint:")).join("\n").trim();
2778
+ }
2779
+ function formatCommitError(message) {
2780
+ const cleaned = cleanGitError(message);
2781
+ const isHookError = /hook\s+(failed|exited|returned)/i.test(cleaned) || /husky/i.test(cleaned) || /lint-staged/i.test(cleaned) || GIT_HOOK_NAMES.some((h) => cleaned.includes(h));
2782
+ if (isHookError) {
2783
+ const match = cleaned.match(
2784
+ /(pre-commit|commit-msg|post-commit|prepare-commit-msg|pre-push)/i
2785
+ );
2786
+ const hookName = match?.[1] ?? "git hook";
2787
+ return `${hookName} hook failed:
2788
+ ${cleaned}`;
2789
+ }
2790
+ return cleaned;
2791
+ }
2772
2792
  async function applyCommitPlan(cwd, plan, options) {
2773
2793
  const appliedCommits = [];
2774
2794
  const errors = [];
2795
+ const logger = useLogger();
2796
+ const planFileCount = plan.commits.reduce((sum, c) => sum + c.files.length, 0);
2797
+ await logger.info("apply: start", {
2798
+ scope: options?.scope,
2799
+ cwd,
2800
+ commits: plan.commits.length,
2801
+ planFiles: planFileCount,
2802
+ force: !!options?.force
2803
+ });
2775
2804
  const integrityErrors = validatePlanIntegrity(plan);
2776
2805
  if (integrityErrors.length > 0) {
2806
+ await logger.warn("apply: plan integrity failed", { errors: integrityErrors });
2777
2807
  return {
2778
2808
  success: false,
2779
2809
  appliedCommits: [],
@@ -2799,8 +2829,8 @@ async function applyCommitPlan(cwd, plan, options) {
2799
2829
  message: formatCommitMessage(commit)
2800
2830
  });
2801
2831
  } catch (error) {
2802
- const message = error instanceof Error ? error.message : "Unknown error";
2803
- errors.push(`Failed to apply commit ${commit.id}: ${message}`);
2832
+ const raw = error instanceof Error ? error.message : String(error);
2833
+ errors.push(`Failed to apply commit ${commit.id}: ${formatCommitError(raw)}`);
2804
2834
  break;
2805
2835
  }
2806
2836
  }
@@ -2818,6 +2848,10 @@ function validatePlanIntegrity(plan) {
2818
2848
  errors.push(`Commit ${commit.id} has no files`);
2819
2849
  continue;
2820
2850
  }
2851
+ if (!commit.message.trim()) {
2852
+ errors.push(`Commit ${commit.id} has empty message`);
2853
+ continue;
2854
+ }
2821
2855
  for (const file of commit.files) {
2822
2856
  const firstCommit = seenInCommit.get(file);
2823
2857
  if (firstCommit) {
@@ -2841,9 +2875,44 @@ async function applyCommit(cwd, commit) {
2841
2875
  }
2842
2876
  const [repoPath, fileInfos] = Array.from(filesByRepo.entries())[0];
2843
2877
  const git = simpleGit(repoPath);
2844
- await git.reset(["--"]);
2878
+ const statusBefore = await git.status();
2879
+ if (statusBefore.staged.length > 0) {
2880
+ const restored = await git.raw(["restore", "--staged", "--", ...statusBefore.staged]).then(() => true).catch(() => false);
2881
+ if (!restored) {
2882
+ for (const f of statusBefore.staged) {
2883
+ await git.raw(["rm", "--cached", "--", f]).catch(() => {
2884
+ });
2885
+ }
2886
+ }
2887
+ }
2845
2888
  for (const { relativePath } of fileInfos) {
2846
- await git.add(relativePath);
2889
+ try {
2890
+ await git.add(relativePath);
2891
+ } catch (addErr) {
2892
+ const msg = addErr instanceof Error ? addErr.message : String(addErr);
2893
+ if (msg.includes("ignored by one of your .gitignore files")) {
2894
+ const isTracked = await git.raw(["ls-files", "--error-unmatch", relativePath]).then(() => true).catch(() => false);
2895
+ if (isTracked) {
2896
+ await git.raw(["add", "-f", relativePath]);
2897
+ } else {
2898
+ throw new Error(
2899
+ `File is gitignored and untracked, cannot stage: ${relativePath}`
2900
+ );
2901
+ }
2902
+ } else {
2903
+ throw new Error(cleanGitError(msg));
2904
+ }
2905
+ }
2906
+ }
2907
+ const statusAfterAdd = await git.status();
2908
+ const stagedSet = new Set(statusAfterAdd.staged);
2909
+ const anyStagedForCommit = fileInfos.some(
2910
+ (fi) => stagedSet.has(fi.relativePath)
2911
+ );
2912
+ if (!anyStagedForCommit) {
2913
+ throw new Error(
2914
+ `Nothing staged for this commit \u2014 files may already be committed or have no changes`
2915
+ );
2847
2916
  }
2848
2917
  const message = formatCommitMessage(commit);
2849
2918
  const result = await git.commit(message);
@@ -2893,8 +2962,14 @@ ${commit.body}`;
2893
2962
  }
2894
2963
  return message;
2895
2964
  }
2896
- async function checkStaleness(cwd, plan, _scope) {
2965
+ async function checkStaleness(cwd, plan, scope) {
2966
+ const logger = useLogger();
2897
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
+ });
2898
2973
  if (planFiles.size === 0) {
2899
2974
  return { isStale: false, reason: "" };
2900
2975
  }
@@ -2902,8 +2977,22 @@ async function checkStaleness(cwd, plan, _scope) {
2902
2977
  for (const [repoPath, fileInfos] of filesByRepo) {
2903
2978
  const currentStatus = await getGitStatus(repoPath);
2904
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
+ });
2905
2987
  for (const { relativePath, originalPath } of fileInfos) {
2906
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
+ });
2907
2996
  return {
2908
2997
  isStale: true,
2909
2998
  reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`
@@ -2945,7 +3034,8 @@ async function pushCommits(cwd, options) {
2945
3034
  commitsPushed: commitsToPush
2946
3035
  };
2947
3036
  } catch (error) {
2948
- const message = error instanceof Error ? error.message : "Unknown error";
3037
+ const raw = error instanceof Error ? error.message : String(error);
3038
+ const message = cleanGitError2(raw);
2949
3039
  const branch = await getCurrentBranch(cwd).catch(() => "unknown");
2950
3040
  return {
2951
3041
  success: false,
@@ -2956,6 +3046,9 @@ async function pushCommits(cwd, options) {
2956
3046
  };
2957
3047
  }
2958
3048
  }
3049
+ function cleanGitError2(message) {
3050
+ return message.split("\n").filter((line) => !line.trimStart().startsWith("hint:")).join("\n").trim();
3051
+ }
2959
3052
  async function countCommitsToPush(git, remote, branch) {
2960
3053
  try {
2961
3054
  await git.fetch(remote, branch);
@@ -3021,12 +3114,21 @@ async function loadPlan(cwd, scope = "root") {
3021
3114
  const data = JSON.parse(content);
3022
3115
  const result = CommitPlanSchema.safeParse(data);
3023
3116
  if (!result.success) {
3024
- console.error(`[loadPlan] Zod validation failed for ${planPath}:`, JSON.stringify(result.error.issues));
3025
- return null;
3117
+ throw new Error(
3118
+ `Commit plan is corrupted and cannot be loaded. Run 'kb commit:generate' to create a new plan.
3119
+ ` + result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")
3120
+ );
3026
3121
  }
3027
3122
  return result.data;
3028
3123
  } catch (err) {
3029
- console.error(`[loadPlan] Failed to read ${planPath}:`, err instanceof Error ? err.message : err);
3124
+ if (err instanceof SyntaxError) {
3125
+ throw new Error(
3126
+ `Commit plan file is not valid JSON. Run 'kb commit:generate' to create a new plan.`
3127
+ );
3128
+ }
3129
+ if (err instanceof Error && err.message.startsWith("Commit plan is corrupted")) {
3130
+ throw err;
3131
+ }
3030
3132
  return null;
3031
3133
  }
3032
3134
  }