Skip to content

Commit 8c088af

Browse files
committed
fix(ep-admin): harden json failure contracts and error propagation
1 parent 2d35daa commit 8c088af

4 files changed

Lines changed: 88 additions & 96 deletions

File tree

packages/ep-admin/src/__tests__/cli-json-contract.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,33 @@ describe.sequential("CLI JSON machine contract", () => {
118118
expect(ingestFailure.stdout.trim()).toBe("");
119119
expect(ingestFailure.stderr.trim().length).toBeGreaterThan(0);
120120
expect(ingestFailure.stderr).toContain("Run: ep-admin data --help");
121+
122+
const invalidToolFailure = runCli(
123+
["config", "install", "add", "--tool", "badtool", "--json"],
124+
automationEnv
125+
);
126+
expect(invalidToolFailure.status).toBe(1);
127+
expect(invalidToolFailure.stdout.trim()).toBe("");
128+
expect(invalidToolFailure.stderr).toContain("Unsupported tool");
129+
130+
const invalidFormatFailure = runCli(
131+
["config", "install", "skills", "--format", "invalid", "--json"],
132+
automationEnv
133+
);
134+
expect(invalidFormatFailure.status).toBe(1);
135+
expect(invalidFormatFailure.stdout.trim()).toBe("");
136+
expect(invalidFormatFailure.stderr).toContain("Invalid format option");
137+
138+
const searchFailure = runCli(
139+
["pattern", "search", "foo", "--json"],
140+
{
141+
...automationEnv,
142+
DATABASE_URL: "postgresql://postgres:postgres@127.0.0.1:1/effect_patterns",
143+
}
144+
);
145+
expect(searchFailure.status).toBe(1);
146+
expect(searchFailure.stdout.trim()).toBe("");
147+
expect(searchFailure.stderr.trim().length).toBeGreaterThan(0);
121148
} finally {
122149
await rm(tempDir, { recursive: true, force: true });
123150
}

packages/ep-admin/src/install-commands.ts

Lines changed: 59 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -155,22 +155,16 @@ export const installAddCommand = Command.make("add", {
155155
];
156156

157157
if (!supportedTools.includes(tool)) {
158-
if (options.json) {
159-
yield* emitJson({
160-
ok: false,
161-
error: `Unsupported tool: ${tool}`,
162-
supportedTools,
163-
});
164-
return yield* Effect.fail(new Error(`Unsupported tool: ${tool}`));
165-
}
166-
yield* Console.error(
167-
colorize(`\n❌ Error: Tool "${tool}" is not supported\n`, "RED")
168-
);
169-
yield* Console.error(
170-
colorize("Currently supported tools:\n", "BRIGHT")
171-
);
172-
for (const t of supportedTools) {
173-
yield* Console.error(` • ${t}`);
158+
if (!options.json) {
159+
yield* Console.error(
160+
colorize(`\n❌ Error: Tool "${tool}" is not supported\n`, "RED")
161+
);
162+
yield* Console.error(
163+
colorize("Currently supported tools:\n", "BRIGHT")
164+
);
165+
for (const t of supportedTools) {
166+
yield* Console.error(` • ${t}`);
167+
}
174168
}
175169
return yield* Effect.fail(new Error(`Unsupported tool: ${tool}`));
176170
}
@@ -244,20 +238,14 @@ export const installAddCommand = Command.make("add", {
244238
}
245239

246240
if (rules.length === 0) {
247-
if (options.json) {
248-
yield* emitJson({
249-
ok: false,
250-
error: "No rules match the specified filters",
251-
ruleCount: 0,
252-
});
253-
return yield* Effect.fail(
254-
new Error("No rules match the specified filters")
241+
if (!options.json) {
242+
yield* Console.log(
243+
colorize("⚠️ No rules match the specified filters\n", "YELLOW")
255244
);
256245
}
257-
yield* Console.log(
258-
colorize("⚠️ No rules match the specified filters\n", "YELLOW")
246+
return yield* Effect.fail(
247+
new Error("No rules match the specified filters")
259248
);
260-
return;
261249
}
262250

263251
const toolFileMap: Record<string, string> = {
@@ -282,20 +270,14 @@ export const installAddCommand = Command.make("add", {
282270
}
283271

284272
const count = yield* injectRulesIntoFile(targetFile, rules).pipe(
285-
Effect.catchAll((error) =>
286-
Effect.gen(function* () {
287-
if (options.json) {
288-
yield* emitJson({
289-
ok: false,
290-
error: "Failed to inject rules",
291-
details: String(error),
292-
});
293-
} else {
294-
yield* Console.log(
295-
colorize("❌ Failed to inject rules\n", "RED")
296-
);
297-
yield* Console.log(`Error: ${error}\n`);
298-
}
273+
Effect.catchAll((error) =>
274+
Effect.gen(function* () {
275+
if (!options.json) {
276+
yield* Console.log(
277+
colorize("❌ Failed to inject rules\n", "RED")
278+
);
279+
yield* Console.log(`Error: ${error}\n`);
280+
}
299281
return yield* Effect.fail(
300282
new Error("Failed to inject rules")
301283
);
@@ -428,49 +410,38 @@ export const installSkillsCommand = Command.make("skills", {
428410

429411
const formatLower = formatOption.toLowerCase();
430412

431-
if (formatLower === "both") {
432-
generateClaude = true;
433-
generateGemini = true;
434-
generateOpenAI = true;
435-
} else {
413+
if (formatLower === "both") {
414+
generateClaude = true;
415+
generateGemini = true;
416+
generateOpenAI = true;
417+
} else {
436418
const formats = formatLower.split(",").map((f) => f.trim());
437419

438-
for (const fmt of formats) {
439-
if (!validOptions.includes(fmt)) {
440-
if (options.json) {
441-
yield* emitJson({
442-
ok: false,
443-
error: `Invalid format: ${fmt}`,
444-
validOptions,
445-
});
420+
for (const fmt of formats) {
421+
if (!validOptions.includes(fmt)) {
422+
if (!options.json) {
423+
yield* Console.error(
424+
colorize(
425+
`\n❌ Invalid format: ${fmt}\nValid options: ` +
426+
`${validOptions.join(", ")}\n`,
427+
"RED"
428+
)
429+
);
430+
}
446431
return yield* Effect.fail(new Error("Invalid format option"));
447432
}
448-
yield* Console.error(
449-
colorize(
450-
`\n❌ Invalid format: ${fmt}\nValid options: ` +
451-
`${validOptions.join(", ")}\n`,
452-
"RED"
453-
)
454-
);
455-
return yield* Effect.fail(new Error("Invalid format option"));
456-
}
457433

458434
if (fmt === "claude") generateClaude = true;
459435
if (fmt === "gemini") generateGemini = true;
460436
if (fmt === "openai") generateOpenAI = true;
461437
}
462438
}
463439

464-
if (!generateClaude && !generateGemini && !generateOpenAI) {
465-
if (options.json) {
466-
yield* emitJson({
467-
ok: false,
468-
error: "No format option",
469-
});
470-
} else {
471-
yield* Console.error(
472-
colorize(
473-
`\n❌ No formats specified. Valid options: ` +
440+
if (!generateClaude && !generateGemini && !generateOpenAI) {
441+
if (!options.json) {
442+
yield* Console.error(
443+
colorize(
444+
`\n❌ No formats specified. Valid options: ` +
474445
`${validOptions.join(", ")}\n`,
475446
"RED"
476447
)
@@ -532,31 +503,25 @@ export const installSkillsCommand = Command.make("skills", {
532503
let geminiCount = 0;
533504
let openaiCount = 0;
534505

535-
if (options.category._tag === "Some") {
506+
if (options.category._tag === "Some") {
536507
const category = options.category.value
537508
.toLowerCase()
538509
.replace(/\s+/g, "-");
539510
const categoryPatterns = categoryMap.get(category);
540511

541-
if (!categoryPatterns) {
542-
if (options.json) {
543-
yield* emitJson({
544-
ok: false,
545-
error: `Category not found: ${category}`,
546-
availableCategories: Array.from(categoryMap.keys()).sort(),
547-
});
512+
if (!categoryPatterns) {
513+
if (!options.json) {
514+
yield* Console.error(
515+
colorize(`\n❌ Category not found: ${category}\n`, "RED")
516+
);
517+
yield* Console.log(colorize("Available categories:\n", "BRIGHT"));
518+
const sortedCategories = Array.from(categoryMap.keys()).sort();
519+
for (const cat of sortedCategories) {
520+
yield* Console.log(` • ${cat}`);
521+
}
522+
}
548523
return yield* Effect.fail(new Error("Category not found"));
549524
}
550-
yield* Console.error(
551-
colorize(`\n❌ Category not found: ${category}\n`, "RED")
552-
);
553-
yield* Console.log(colorize("Available categories:\n", "BRIGHT"));
554-
const sortedCategories = Array.from(categoryMap.keys()).sort();
555-
for (const cat of sortedCategories) {
556-
yield* Console.log(` • ${cat}`);
557-
}
558-
return yield* Effect.fail(new Error("Category not found"));
559-
}
560525

561526
if (generateClaude) {
562527
const skillName = `effect-patterns-${category}`;
@@ -648,7 +613,7 @@ export const installSkillsCommand = Command.make("skills", {
648613
);
649614

650615
if (writeResult !== null) {
651-
yield* Console.log(
616+
yield* log(
652617
colorize(
653618
` ✓ ${skillName} (${categoryPatterns.length} patterns)`,
654619
"GREEN"
@@ -679,7 +644,7 @@ export const installSkillsCommand = Command.make("skills", {
679644
);
680645

681646
if (writeResult !== null) {
682-
yield* Console.log(
647+
yield* log(
683648
colorize(
684649
` ✓ ${geminiSkill.skillId} (${categoryPatterns.length} ` +
685650
`patterns)`,
@@ -712,7 +677,7 @@ export const installSkillsCommand = Command.make("skills", {
712677
);
713678

714679
if (writeResult !== null) {
715-
yield* Console.log(
680+
yield* log(
716681
colorize(
717682
` ✓ ${skillName} (${categoryPatterns.length} patterns)`,
718683
"GREEN"

packages/ep-admin/src/lock-commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const handleEntityOperation = (
171171
"\n💡 Tip: Make sure PostgreSQL is running and DATABASE_URL " +
172172
"is set correctly.\n"
173173
);
174-
return Effect.fail(error);
174+
return yield* Effect.fail(error);
175175
})
176176
)
177177
);

packages/ep-admin/src/search-commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export const searchCommand = Command.make("search", {
126126
"is set correctly.\n"
127127
);
128128
}
129-
return Effect.fail(error);
129+
return yield* Effect.fail(error);
130130
})
131131
)
132132
)

0 commit comments

Comments
 (0)