diff --git a/AGENTS.md b/AGENTS.md index 5061e9a5..e207230a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,7 +88,7 @@ cli/ │ │ │ └── index.ts │ │ ├── consts.ts # Pure constants (NO imports from other core modules) │ │ ├── config.ts # Path helpers (global dir, templates, API URL) -│ │ ├── errors.ts # Error classes +│ │ ├── errors.ts # CLIError hierarchy (UserError, SystemError, etc.) │ │ └── index.ts # Barrel export for all core modules │ └── cli/ │ ├── program.ts # createProgram(context) factory @@ -416,6 +416,128 @@ import { entityResource } from "@/core/resources/entity/index.js"; import { base44Client } from "@/core/api/index.js"; ``` +## Error Handling + +The CLI uses a structured error hierarchy to provide clear, actionable error messages with hints for users and AI agents. + +### Error Hierarchy + +``` +CLIError (abstract base class) +├── UserError (user did something wrong - fixable by user) +│ ├── AuthRequiredError # Not logged in +│ ├── AuthExpiredError # Token expired +│ ├── ConfigNotFoundError # No project found +│ ├── ConfigInvalidError # Invalid config syntax/structure +│ ├── ConfigExistsError # Project already exists +│ ├── SchemaValidationError # Zod validation failed +│ └── InvalidInputError # Bad user input (template not found, etc.) +│ +└── SystemError (something broke - needs investigation) + ├── ApiError # HTTP/network failures + ├── FileNotFoundError # File doesn't exist + ├── FileReadError # Can't read file + └── InternalError # Unexpected errors +``` + +### Error Properties + +All errors extend `CLIError` and have these properties: + +```typescript +interface CLIError { + code: string; // e.g., "AUTH_REQUIRED", "CONFIG_NOT_FOUND" + isUserError: boolean; // true for UserError, false for SystemError + hints: ErrorHint[]; // Actionable suggestions + cause?: Error; // Original error for stack traces +} + +interface ErrorHint { + message: string; // Human-readable hint + command?: string; // Optional command to run (for AI agents) +} +``` + +### Throwing Errors + +Import errors from `@/core/errors.js`: + +```typescript +import { + ConfigNotFoundError, + ConfigExistsError, + SchemaValidationError, + ApiError, + InvalidInputError, +} from "@/core/errors.js"; + +// User errors - provide helpful hints +throw new ConfigNotFoundError(); // Has default hints for create/link + +throw new ConfigExistsError("Project already exists at /path/to/config.jsonc"); + +throw new InvalidInputError(`Template "${templateId}" not found`, { + hints: [ + { message: `Use one of: ${validIds}` }, + ], +}); + +// API errors - include status code for automatic hint generation +throw new ApiError("Failed to sync entities", { statusCode: response.status }); +// 401 → hints to run `base44 login` +// 404 → hints about resource not found +// Other → hints to check network +``` + +### SchemaValidationError with Zod + +`SchemaValidationError` requires a context message and a `ZodError`. It formats the error automatically using `z.prettifyError()`: + +```typescript +import { SchemaValidationError } from "@/core/errors.js"; + +const result = EntitySchema.safeParse(parsed); + +if (!result.success) { + // Pass context message + ZodError - formatting is handled automatically + throw new SchemaValidationError("Invalid entity file at " + entityPath, result.error); +} + +// Output: +// Invalid entity file at /path/to/entity.jsonc: +// ✖ Invalid input: expected string, received number +// → at name +``` + +**Important**: Do NOT manually call `z.prettifyError()` - the class does this internally. + +### Error Code Reference + +| Code | Class | When to use | +| ------------------ | ----------------------- | ------------------------------------- | +| `AUTH_REQUIRED` | `AuthRequiredError` | User not logged in | +| `AUTH_EXPIRED` | `AuthExpiredError` | Token expired, needs re-login | +| `CONFIG_NOT_FOUND` | `ConfigNotFoundError` | No project/config file found | +| `CONFIG_INVALID` | `ConfigInvalidError` | Config file has invalid content | +| `CONFIG_EXISTS` | `ConfigExistsError` | Project already exists at location | +| `SCHEMA_INVALID` | `SchemaValidationError` | Zod validation failed | +| `INVALID_INPUT` | `InvalidInputError` | User provided invalid input | +| `API_ERROR` | `ApiError` | API request failed | +| `FILE_NOT_FOUND` | `FileNotFoundError` | File doesn't exist | +| `FILE_READ_ERROR` | `FileReadError` | Can't read/write file | +| `INTERNAL_ERROR` | `InternalError` | Unexpected error | + +### CLIExitError (Special Case) + +`CLIExitError` in `src/cli/errors.ts` is for controlled exits (e.g., user cancellation). It's NOT reported to telemetry: + +```typescript +import { CLIExitError } from "@/cli/errors.js"; + +// User cancelled a prompt +throw new CLIExitError(0); // Exit code 0 = success (user chose to cancel) +``` + ## Telemetry & Error Reporting The CLI reports errors to PostHog for monitoring. This is handled by the `ErrorReporter` class. @@ -463,6 +585,7 @@ Set the environment variable: `BASE44_DISABLE_TELEMETRY=1` - App ID (if in a project) - System info (Node version, OS, platform) - Error stack traces +- Error code and isUserError (for CLIError instances) ## Important Rules @@ -480,7 +603,9 @@ Set the environment variable: `BASE44_DISABLE_TELEMETRY=1` 12. **Use theme for styling** - Never use `chalk` directly in commands; import `theme` from utils and use semantic color/style names 13. **Use fs.ts utilities** - Always use `@/core/utils/fs.js` for file operations 14. **No direct process.exit()** - Throw `CLIExitError` instead; entry points handle the actual exit -15. **No dynamic imports** - Avoid `await import()` inside functions; use static imports at top of file +15. **Use structured errors** - Never `throw new Error()`; use specific error classes from `@/core/errors.js` with appropriate hints +16. **SchemaValidationError requires ZodError** - Always pass `ZodError`: `new SchemaValidationError("context", result.error)` - don't call `z.prettifyError()` manually +17. **No dynamic imports** - Avoid `await import()` inside functions; use static imports at top of file ## Development diff --git a/bin/dev.js b/bin/dev.js index 6d6c99a3..a4cea9c2 100755 --- a/bin/dev.js +++ b/bin/dev.js @@ -1,5 +1,5 @@ #!/usr/bin/env tsx -import { createProgram, runCLI } from "../src/cli/index.ts"; +import { runCLI } from "../src/cli/index.ts"; // Disable Clack spinners and animations in non-interactive environments. // Clack only checks the CI env var, so we set it when stdin/stdout aren't TTYs. diff --git a/bin/run.js b/bin/run.js index 220a2665..daa3a38d 100755 --- a/bin/run.js +++ b/bin/run.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createProgram, runCLI } from "../dist/index.js"; +import { runCLI } from "../dist/index.js"; // Disable Clack spinners and animations in non-interactive environments. // Clack only checks the CI env var, so we set it when stdin/stdout aren't TTYs. diff --git a/package-lock.json b/package-lock.json index cefc27dd..6d9c3c5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -933,9 +933,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", - "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], diff --git a/src/cli/commands/functions/deploy.ts b/src/cli/commands/functions/deploy.ts index 2886aa82..718bbe30 100644 --- a/src/cli/commands/functions/deploy.ts +++ b/src/cli/commands/functions/deploy.ts @@ -3,6 +3,7 @@ import { log } from "@clack/prompts"; import type { CLIContext } from "@/cli/types.js"; import { pushFunctions } from "@/core/resources/function/index.js"; import { readProjectConfig } from "@/core/index.js"; +import { ApiError } from "@/core/errors.js"; import { runCommand, runTask } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; @@ -41,7 +42,12 @@ async function deployFunctionsAction(): Promise { const errorMessages = result.errors .map((e) => `'${e.name}' function: ${e.message}`) .join("\n"); - throw new Error(`Function deployment errors:\n${errorMessages}`); + throw new ApiError(`Function deployment errors:\n${errorMessages}`, { + hints: [ + { message: "Check the function code for syntax errors" }, + { message: "Ensure all imports are valid" }, + ], + }); } return { outroMessage: "Functions deployed to Base44" }; diff --git a/src/cli/commands/project/create.ts b/src/cli/commands/project/create.ts index 764e50d2..33cc690d 100644 --- a/src/cli/commands/project/create.ts +++ b/src/cli/commands/project/create.ts @@ -8,6 +8,7 @@ import type { CLIContext } from "@/cli/types.js"; import { createProjectFiles, listTemplates, readProjectConfig, setAppConfig } from "@/core/project/index.js"; import type { Template } from "@/core/project/index.js"; import { deploySite, isDirEmpty, pushEntities } from "@/core/index.js"; +import { InvalidInputError } from "@/core/errors.js"; import { runCommand, runTask, @@ -32,7 +33,14 @@ async function getTemplateById(templateId: string): Promise