diff --git a/src/core/auth/config.ts b/src/core/auth/config.ts index e08731ec..e155f9a8 100644 --- a/src/core/auth/config.ts +++ b/src/core/auth/config.ts @@ -31,7 +31,7 @@ export async function readAuth(): Promise { const result = AuthDataSchema.safeParse(parsed); if (!result.success) { - throw new SchemaValidationError("Invalid authentication data", result.error); + throw new SchemaValidationError("Invalid authentication data", result.error, getAuthFilePath()); } return result.data; @@ -52,7 +52,7 @@ export async function writeAuth(authData: AuthData): Promise { const result = AuthDataSchema.safeParse(authData); if (!result.success) { - throw new SchemaValidationError("Invalid authentication data", result.error); + throw new SchemaValidationError("Invalid authentication data", result.error, getAuthFilePath()); } try { diff --git a/src/core/errors.ts b/src/core/errors.ts index 2b17d1e6..b7989b72 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -189,16 +189,24 @@ export class ConfigExistsError extends UserError { * @example * const result = schema.safeParse(data); * if (!result.success) { - * throw new SchemaValidationError("Invalid entity file", result.error); + * throw new SchemaValidationError("Invalid entity file", result.error, entityPath); * } */ export class SchemaValidationError extends UserError { readonly code = "SCHEMA_INVALID"; + readonly filePath?: string; - constructor(context: string, zodError: z.ZodError) { - super(`${context}:\n${z.prettifyError(zodError)}`, { - hints: [{ message: "Fix the schema/data structure errors above" }], - }); + constructor(context: string, zodError: z.ZodError, filePath?: string) { + const message = filePath + ? `${context} in ${filePath}:\n${z.prettifyError(zodError)}` + : `${context}:\n${z.prettifyError(zodError)}`; + + const hints: ErrorHint[] = filePath + ? [{ message: `Fix the schema/data structure errors in ${filePath}` }] + : [{ message: "Fix the schema/data structure errors above" }]; + + super(message, { hints }); + this.filePath = filePath; } } diff --git a/src/core/project/app-config.ts b/src/core/project/app-config.ts index adfa4721..ed9e4eef 100644 --- a/src/core/project/app-config.ts +++ b/src/core/project/app-config.ts @@ -147,7 +147,7 @@ async function readAppConfig( const result = AppConfigSchema.safeParse(parsed); if (!result.success) { - throw new SchemaValidationError(`Invalid app configuration in ${configPath}`, result.error); + throw new SchemaValidationError("Invalid app configuration", result.error, configPath); } return result.data; diff --git a/src/core/project/config.ts b/src/core/project/config.ts index e2847267..024d0353 100644 --- a/src/core/project/config.ts +++ b/src/core/project/config.ts @@ -81,7 +81,7 @@ export async function readProjectConfig( const result = ProjectConfigSchema.safeParse(parsed); if (!result.success) { - throw new SchemaValidationError("Invalid project configuration", result.error); + throw new SchemaValidationError("Invalid project configuration", result.error, configPath); } const project = result.data; diff --git a/src/core/project/schema.ts b/src/core/project/schema.ts index 18b65045..431ae2f2 100644 --- a/src/core/project/schema.ts +++ b/src/core/project/schema.ts @@ -22,7 +22,9 @@ const SiteConfigSchema = z.object({ }); export const ProjectConfigSchema = z.object({ - name: z.string().min(1, "App name cannot be empty"), + name: z.string({ + error: "App name cannot be empty" + }).min(1, "App name cannot be empty"), description: z.string().optional(), site: SiteConfigSchema.optional(), entitiesDir: z.string().optional().default("entities"), diff --git a/src/core/project/template.ts b/src/core/project/template.ts index dd888d66..c464d0a2 100644 --- a/src/core/project/template.ts +++ b/src/core/project/template.ts @@ -23,7 +23,7 @@ export async function listTemplates(): Promise { const result = TemplatesConfigSchema.safeParse(parsed); if (!result.success) { - throw new SchemaValidationError("Invalid templates configuration", result.error); + throw new SchemaValidationError("Invalid templates configuration", result.error, getTemplatesIndexPath()); } return result.data.templates; diff --git a/src/core/resources/agent/config.ts b/src/core/resources/agent/config.ts index 048507cd..ae607241 100644 --- a/src/core/resources/agent/config.ts +++ b/src/core/resources/agent/config.ts @@ -30,7 +30,7 @@ async function readAgentFile(agentPath: string): Promise { const result = AgentConfigSchema.safeParse(parsed); if (!result.success) { - throw new SchemaValidationError(`Invalid agent file at ${agentPath}`, result.error); + throw new SchemaValidationError("Invalid agent file", result.error, agentPath); } return result.data; diff --git a/src/core/resources/entity/config.ts b/src/core/resources/entity/config.ts index 70f62435..861b96f8 100644 --- a/src/core/resources/entity/config.ts +++ b/src/core/resources/entity/config.ts @@ -10,7 +10,7 @@ async function readEntityFile(entityPath: string): Promise { const result = EntitySchema.safeParse(parsed); if (!result.success) { - throw new SchemaValidationError(`Invalid entity file at ${entityPath}`, result.error); + throw new SchemaValidationError("Invalid entity file", result.error, entityPath); } return result.data; diff --git a/src/core/resources/function/config.ts b/src/core/resources/function/config.ts index c47de2aa..e9e4e309 100644 --- a/src/core/resources/function/config.ts +++ b/src/core/resources/function/config.ts @@ -13,7 +13,7 @@ export async function readFunctionConfig( const result = FunctionConfigSchema.safeParse(parsed); if (!result.success) { - throw new SchemaValidationError(`Invalid function configuration in ${configPath}`, result.error); + throw new SchemaValidationError("Invalid function configuration", result.error, configPath); } return result.data; @@ -38,7 +38,7 @@ export async function readFunction(configPath: string): Promise { const functionData = { ...config, entryPath, files }; const result = FunctionSchema.safeParse(functionData); if (!result.success) { - throw new SchemaValidationError(`Invalid function in ${configPath}`, result.error); + throw new SchemaValidationError("Invalid function", result.error, configPath); } return result.data; diff --git a/tests/core/errors.spec.ts b/tests/core/errors.spec.ts index 107e4f51..c80ca886 100644 --- a/tests/core/errors.spec.ts +++ b/tests/core/errors.spec.ts @@ -81,7 +81,7 @@ describe("UserError subclasses", () => { expect(error.message).toBe("Project already exists"); }); - it("SchemaValidationError formats ZodError automatically", async () => { + it("SchemaValidationError formats ZodError without filePath", async () => { const { z } = await import("zod"); const schema = z.object({ name: z.string() }); const result = schema.safeParse({ name: 123 }); @@ -93,6 +93,24 @@ describe("UserError subclasses", () => { // Zod prettifyError uses lowercase expect(error.message).toContain("expected string"); expect(error.message).toContain("name"); + expect(error.filePath).toBeUndefined(); + } else { + throw new Error("Expected parse to fail"); + } + }); + + it("SchemaValidationError includes filePath in message and hints when provided", async () => { + const { z } = await import("zod"); + const schema = z.object({ name: z.string() }); + const result = schema.safeParse({ name: 123 }); + + if (!result.success) { + const error = new SchemaValidationError("Invalid entity file", result.error, "/path/to/entity.jsonc"); + expect(error.code).toBe("SCHEMA_INVALID"); + expect(error.message).toContain("Invalid entity file in /path/to/entity.jsonc"); + expect(error.message).toContain("expected string"); + expect(error.filePath).toBe("/path/to/entity.jsonc"); + expect(error.hints[0].message).toContain("/path/to/entity.jsonc"); } else { throw new Error("Expected parse to fail"); }