Skip to content

Commit edeaacd

Browse files
committed
fix: correct readiness hint, detect corrupted config, preserve CRLF, dedupe computation
- Fix getPlatformReadinessHint pointing to /architect instead of /implementation-readiness (guidance.ts, artifact-scan.ts) - Distinguish corrupted config from missing config in status, run, and watch commands — show "bmalph doctor" hint instead of misleading "bmalph init" (status.ts, run.ts, watch.ts, config.ts) - Catch broken JSON in readConfig so parse errors don't bypass the friendly corrupted-config message - Hoist duplicate buildCompletedTitleMap/newTitleMap computation in fix-plan-sync.ts to avoid redundant work - Preserve CRLF line endings in removeGitignoreLines (reset.ts) - Remove exports field from package.json that triggered CLI side effects on import("bmalph")
1 parent 9bc7cc1 commit edeaacd

12 files changed

Lines changed: 106 additions & 15 deletions

File tree

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@
5252
"engines": {
5353
"node": ">=20.0.0"
5454
},
55-
"exports": {
56-
".": "./dist/cli.js"
57-
},
5855
"files": [
5956
"bin/",
6057
"dist/",

src/commands/run.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import chalk from "chalk";
22
import { readConfig } from "../utils/config.js";
33
import { withErrorHandling } from "../utils/errors.js";
4+
import { isInitialized } from "../installer/project-files.js";
45
import { isPlatformId, getPlatform, getFullTierPlatformNames } from "../platform/registry.js";
56
import { validateCursorRuntime } from "../platform/cursor-runtime-checks.js";
67
import {
@@ -32,6 +33,9 @@ async function executeRun(options: RunCommandOptions): Promise<void> {
3233

3334
const config = await readConfig(projectDir);
3435
if (!config) {
36+
if (await isInitialized(projectDir)) {
37+
throw new Error("Config file is corrupted. Run: bmalph doctor");
38+
}
3539
throw new Error("Project not initialized. Run: bmalph init");
3640
}
3741

src/commands/status.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import chalk from "chalk";
22
import { readConfig } from "../utils/config.js";
33
import { readState, readRalphStatus, getPhaseLabel, getPhaseInfo } from "../utils/state.js";
44
import { withErrorHandling } from "../utils/errors.js";
5+
import { isInitialized } from "../installer/project-files.js";
56
import { formatStatus } from "../utils/format-status.js";
67
import { ARTIFACT_DEFINITIONS } from "../utils/artifact-definitions.js";
78
import { resolveProjectPlatform } from "../platform/resolve.js";
@@ -46,7 +47,11 @@ export async function runStatus(options: StatusOptions): Promise<void> {
4647
// Check if project is initialized
4748
const config = await readConfig(projectDir);
4849
if (!config) {
49-
console.log(chalk.red("Project not initialized. Run: bmalph init"));
50+
if (await isInitialized(projectDir)) {
51+
console.log(chalk.red("Config file is corrupted. Run: bmalph doctor"));
52+
} else {
53+
console.log(chalk.red("Project not initialized. Run: bmalph init"));
54+
}
5055
return;
5156
}
5257

src/commands/watch.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import chalk from "chalk";
22
import { readConfig } from "../utils/config.js";
33
import { withErrorHandling } from "../utils/errors.js";
4+
import { isInitialized } from "../installer/project-files.js";
45
import { parseInterval } from "../utils/validate.js";
56
import { startDashboard } from "../watch/dashboard.js";
67
import { getDashboardTerminalSupport } from "../watch/frame-writer.js";
@@ -21,6 +22,9 @@ async function runWatch(options: WatchCommandOptions): Promise<void> {
2122

2223
const config = await readConfig(projectDir);
2324
if (!config) {
25+
if (await isInitialized(projectDir)) {
26+
throw new Error("Config file is corrupted. Run: bmalph doctor");
27+
}
2428
throw new Error("Project not initialized. Run: bmalph init");
2529
}
2630

src/platform/guidance.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ export function getPlatformEpicsStoriesHint(platform: Platform): string {
6262

6363
export function getPlatformReadinessHint(platform: Platform): string {
6464
if (platform.commandDelivery.kind === "directory") {
65-
return "Run /architect to generate readiness report";
65+
return "Run /implementation-readiness to generate readiness report";
6666
}
6767

6868
if (platform.commandDelivery.kind === "skills") {
69-
return getSkillHint(platform, "architect", platform.commandDelivery.dir);
69+
return getSkillHint(platform, "check-implementation-readiness", platform.commandDelivery.dir);
7070
}
7171

7272
return getCommandIndexHint(platform);

src/reset.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,12 @@ export async function executeResetPlan(projectDir: string, plan: ResetPlan): Pro
178178
}
179179
}
180180

181-
function removeGitignoreLines(content: string, linesToRemove: string[]): string {
181+
export function removeGitignoreLines(content: string, linesToRemove: string[]): string {
182182
const removeSet = new Set(linesToRemove);
183+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
183184
const lines = content.split(/\r?\n/);
184185
const filtered = lines.filter((line) => !removeSet.has(line.trim()));
185-
return filtered.join("\n");
186+
return filtered.join(eol);
186187
}
187188

188189
export function planToDryRunActions(plan: ResetPlan): DryRunAction[] {

src/transition/artifact-scan.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export function suggestNext(
126126
if (!foundNames.has("Readiness Report")) {
127127
return platform
128128
? getPlatformReadinessHint(platform)
129-
: "Run /architect to generate readiness report";
129+
: "Run /implementation-readiness to generate readiness report";
130130
}
131131

132132
return "Run: bmalph implement";

src/transition/fix-plan-sync.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export async function syncFixPlan(
5757
}
5858
}
5959

60+
const completedTitles = buildCompletedTitleMap(existingItems);
61+
const newTitleMap = new Map(inputs.stories.map((story) => [story.id, story.title]));
62+
6063
const sprintStatusSource = await resolveSprintStatusSource(projectDir, inputs);
6164
if (sprintStatusSource) {
6265
useTitleBasedMerge = false;
@@ -96,8 +99,6 @@ export async function syncFixPlan(
9699
const newStoryIds = new Set(inputs.stories.map((story) => story.id));
97100
orphanWarnings = detectOrphanedCompletedStories(existingItems, newStoryIds);
98101

99-
const completedTitles = buildCompletedTitleMap(existingItems);
100-
const newTitleMap = new Map(inputs.stories.map((story) => [story.id, story.title]));
101102
const preservedIds = new Set<string>();
102103

103104
for (const [id, title] of newTitleMap) {
@@ -109,9 +110,6 @@ export async function syncFixPlan(
109110
renumberWarnings = detectRenumberedStories(existingItems, inputs.stories, preservedIds);
110111
}
111112

112-
const completedTitles = buildCompletedTitleMap(existingItems);
113-
const newTitleMap = new Map(inputs.stories.map((story) => [story.id, story.title]));
114-
115113
info(`Generating fix plan for ${inputs.stories.length} stories...`);
116114
const newFixPlan = generateFixPlan(inputs.stories, undefined, inputs.planningSpecsSubpath);
117115
const mergedFixPlan = mergeFixPlanProgress(

src/utils/config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ export interface BmalphConfig {
2121
}
2222

2323
export async function readConfig(projectDir: string): Promise<BmalphConfig | null> {
24-
const data = await readJsonFile<unknown>(join(projectDir, CONFIG_FILE));
24+
let data: unknown;
25+
try {
26+
data = await readJsonFile<unknown>(join(projectDir, CONFIG_FILE));
27+
} catch (err) {
28+
warn(`Config file is corrupted, treating as missing: ${formatError(err)}`);
29+
return null;
30+
}
2531
if (data === null) return null;
2632
try {
2733
return validateConfig(data);

tests/commands/status.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,32 @@ describe("status command", () => {
7979
expect(output).toContain("not initialized");
8080
});
8181

82+
it("shows corrupted config message when config exists but has wrong schema", async () => {
83+
await mkdir(join(testDir, "bmalph"), { recursive: true });
84+
await writeFile(join(testDir, "bmalph/config.json"), '{"invalid": true}');
85+
86+
const { runStatus } = await import("../../src/commands/status.js");
87+
await runStatus({ projectDir: testDir });
88+
89+
const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n");
90+
expect(output).toContain("corrupted");
91+
expect(output).toContain("bmalph doctor");
92+
expect(output).not.toContain("bmalph init");
93+
});
94+
95+
it("shows corrupted config message when config contains broken JSON", async () => {
96+
await mkdir(join(testDir, "bmalph"), { recursive: true });
97+
await writeFile(join(testDir, "bmalph/config.json"), "{corrupt");
98+
99+
const { runStatus } = await import("../../src/commands/status.js");
100+
await runStatus({ projectDir: testDir });
101+
102+
const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n");
103+
expect(output).toContain("corrupted");
104+
expect(output).toContain("bmalph doctor");
105+
expect(output).not.toContain("bmalph init");
106+
});
107+
82108
it("shows phase 1 status when in planning", async () => {
83109
await setupProject();
84110
await setupState({ currentPhase: 1, status: "planning" });

0 commit comments

Comments
 (0)