From 40069032fcebebbd621987aa437bd6912fe535be Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 2 Feb 2026 10:52:54 +0200 Subject: [PATCH 01/13] first implementationg of types support --- package.json | 1 + src/cli/commands/types/generate.ts | 87 +++++++++ src/cli/program.ts | 4 + src/core/types/generator.ts | 169 ++++++++++++++++++ src/core/types/index.ts | 28 +++ src/core/types/json-schema-generator.ts | 169 ++++++++++++++++++ src/core/types/template.ts | 109 +++++++++++ src/core/types/write.ts | 99 ++++++++++ tests/fixtures/basic/base44/config.jsonc | 4 + .../fixtures/full-project/base44/config.jsonc | 6 + .../full-project/base44/entities/task.json | 8 + .../base44/functions/hello/function.jsonc | 4 + .../base44/functions/hello/index.ts | 3 + .../invalid-agent/base44/agents/broken.json | 4 + .../invalid-entity/base44/config.jsonc | 4 + .../base44/entities/broken.json | 4 + .../base44/agents/customer_support.json | 12 ++ .../base44/agents/data_analyst.jsonc | 12 ++ .../base44/agents/order_assistant.json | 11 ++ .../fixtures/with-agents/base44/config.jsonc | 3 + .../with-entities/base44/config.jsonc | 3 + .../base44/entities/customer.json | 15 ++ .../base44/entities/product.json | 15 ++ .../base44/config.jsonc | 3 + .../base44/entities/order.json | 19 ++ .../functions/process-order/function.jsonc | 4 + .../base44/functions/process-order/helper.ts | 3 + .../base44/functions/process-order/index.ts | 10 ++ tests/fixtures/with-site/base44/config.jsonc | 6 + 29 files changed, 819 insertions(+) create mode 100644 src/cli/commands/types/generate.ts create mode 100644 src/core/types/generator.ts create mode 100644 src/core/types/index.ts create mode 100644 src/core/types/json-schema-generator.ts create mode 100644 src/core/types/template.ts create mode 100644 src/core/types/write.ts create mode 100644 tests/fixtures/basic/base44/config.jsonc create mode 100644 tests/fixtures/full-project/base44/config.jsonc create mode 100644 tests/fixtures/full-project/base44/entities/task.json create mode 100644 tests/fixtures/full-project/base44/functions/hello/function.jsonc create mode 100644 tests/fixtures/full-project/base44/functions/hello/index.ts create mode 100644 tests/fixtures/invalid-agent/base44/agents/broken.json create mode 100644 tests/fixtures/invalid-entity/base44/config.jsonc create mode 100644 tests/fixtures/invalid-entity/base44/entities/broken.json create mode 100644 tests/fixtures/with-agents/base44/agents/customer_support.json create mode 100644 tests/fixtures/with-agents/base44/agents/data_analyst.jsonc create mode 100644 tests/fixtures/with-agents/base44/agents/order_assistant.json create mode 100644 tests/fixtures/with-agents/base44/config.jsonc create mode 100644 tests/fixtures/with-entities/base44/config.jsonc create mode 100644 tests/fixtures/with-entities/base44/entities/customer.json create mode 100644 tests/fixtures/with-entities/base44/entities/product.json create mode 100644 tests/fixtures/with-functions-and-entities/base44/config.jsonc create mode 100644 tests/fixtures/with-functions-and-entities/base44/entities/order.json create mode 100644 tests/fixtures/with-functions-and-entities/base44/functions/process-order/function.jsonc create mode 100644 tests/fixtures/with-functions-and-entities/base44/functions/process-order/helper.ts create mode 100644 tests/fixtures/with-functions-and-entities/base44/functions/process-order/index.ts create mode 100644 tests/fixtures/with-site/base44/config.jsonc diff --git a/package.json b/package.json index 13b3a794..41108b53 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "execa": "^9.6.1", "front-matter": "^4.0.2", "globby": "^16.1.0", + "json-schema-to-typescript": "^15.0.4", "json5": "^2.2.3", "ky": "^1.14.2", "lodash.kebabcase": "^4.1.1", diff --git a/src/cli/commands/types/generate.ts b/src/cli/commands/types/generate.ts new file mode 100644 index 00000000..2bd477ef --- /dev/null +++ b/src/cli/commands/types/generate.ts @@ -0,0 +1,87 @@ +import { join, dirname } from "node:path"; +import { Command } from "commander"; +import { log } from "@clack/prompts"; +import type { CLIContext } from "@/cli/types.js"; +import { readProjectConfig } from "@/core/index.js"; +import { writeAllTypesFiles } from "@/core/types/index.js"; +import { runCommand, runTask, theme } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/utils/runCommand.js"; + +interface TypesCommandOptions { + output?: string; +} + +async function generateTypesAction(options: TypesCommandOptions): Promise { + const { entities, functions, agents, project } = await readProjectConfig(); + + // Determine output directory + // Default: base44/ directory next to config.jsonc + const configDir = dirname(project.configPath); + const outputDir = options.output ?? join(configDir, ""); + + // Log what we found + const resourceCounts: string[] = []; + if (entities.length > 0) { + resourceCounts.push(`${entities.length} ${entities.length === 1 ? "entity" : "entities"}`); + } + if (functions.length > 0) { + resourceCounts.push(`${functions.length} ${functions.length === 1 ? "function" : "functions"}`); + } + if (agents.length > 0) { + resourceCounts.push(`${agents.length} ${agents.length === 1 ? "agent" : "agents"}`); + } + + if (resourceCounts.length === 0) { + log.warn("No entities, functions, or agents found in project"); + log.info("Add resources and run 'base44 types' again"); + return { outroMessage: "No types generated" }; + } + + log.info(`Found ${resourceCounts.join(", ")}`); + + const result = await runTask( + "Generating types", + async () => { + return await writeAllTypesFiles( + { entities, functions, agents }, + { outputDir } + ); + }, + { + successMessage: theme.colors.base44Orange("Types generated successfully"), + errorMessage: "Failed to generate types", + } + ); + + // Log generated files + log.success(`Generated ${result.files.length} files:`); + for (const file of result.files) { + log.message(` ${theme.styles.dim("•")} ${file}`); + } + + // Provide setup hints + log.info(""); + log.info(theme.styles.header("Setup instructions:")); + log.message(` Add to ${theme.styles.bold("tsconfig.json")}:`); + log.message(` { "include": ["src", "base44/types.d.ts"] }`); + log.message(""); + log.message(` Add $schema to config files for IDE autocomplete:`); + log.message(` { "$schema": "./schemas/agent.schema.json", ... }`); + + return { + outroMessage: `Generated types for ${result.entityCount} entities, ${result.functionCount} functions, ${result.agentCount} agents`, + }; +} + +export function getTypesCommand(context: CLIContext): Command { + return new Command("types") + .description("Generate TypeScript declaration files from project schemas") + .option("-o, --output ", "Output directory (default: base44/)") + .action(async (options: TypesCommandOptions) => { + await runCommand( + () => generateTypesAction(options), + { requireAuth: false, requireAppConfig: false }, + context + ); + }); +} diff --git a/src/cli/program.ts b/src/cli/program.ts index 8d01d274..0d251e43 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -10,6 +10,7 @@ import { getCreateCommand } from "@/cli/commands/project/create.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; import { getSiteCommand } from "@/cli/commands/site/index.js"; +import { getTypesCommand } from "@/cli/commands/types/generate.js"; import packageJson from "../../package.json"; import type { CLIContext } from "./types.js"; @@ -50,5 +51,8 @@ export function createProgram(context: CLIContext): Command { // Register site commands program.addCommand(getSiteCommand(context)); + // Register types command + program.addCommand(getTypesCommand(context)); + return program; } diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts new file mode 100644 index 00000000..1f54cbc9 --- /dev/null +++ b/src/core/types/generator.ts @@ -0,0 +1,169 @@ +import { compile } from "json-schema-to-typescript"; +import type { Entity } from "@/core/resources/entity/index.js"; +import type { Function } from "@/core/resources/function/index.js"; +import type { AgentConfig } from "@/core/resources/agent/index.js"; + +/** + * Input for type generation containing all project resources. + */ +export interface TypesInput { + entities: Entity[]; + functions: Function[]; + agents: AgentConfig[]; +} + +/** + * Options for type generation. + */ +export interface TypesOptions { + outputDir: string; +} + +/** + * Result of type generation. + */ +export interface TypesResult { + files: string[]; + entityCount: number; + functionCount: number; + agentCount: number; +} + +/** + * Convert a CLI entity schema to JSON Schema format for json-schema-to-typescript. + */ +function entityToJsonSchema(entity: Entity): object { + const properties: Record = {}; + + for (const [propName, propDef] of Object.entries(entity.properties)) { + properties[propName] = propertyToJsonSchema(propDef); + } + + return { + type: "object", + title: entity.name, + description: entity.description, + properties, + required: entity.required ?? [], + additionalProperties: false, + }; +} + +/** + * Convert a property definition to JSON Schema format. + */ +function propertyToJsonSchema(prop: Record): object { + const result: Record = {}; + + // Handle type + if (prop.type === "integer") { + result.type = "number"; + } else if (prop.type === "binary") { + result.type = "string"; + result.format = "binary"; + } else { + result.type = prop.type; + } + + // Handle description + if (prop.description) { + result.description = prop.description; + } + + // Handle enum + if (prop.enum && Array.isArray(prop.enum)) { + result.enum = prop.enum; + } + + // Handle array items + if (prop.type === "array" && prop.items) { + result.items = propertyToJsonSchema(prop.items as Record); + } + + // Handle nested object properties + if (prop.type === "object" && prop.properties) { + const nestedProps: Record = {}; + for (const [nestedName, nestedDef] of Object.entries( + prop.properties as Record> + )) { + nestedProps[nestedName] = propertyToJsonSchema(nestedDef); + } + result.properties = nestedProps; + if (prop.required) { + result.required = prop.required; + } + result.additionalProperties = false; + } + + return result; +} + +/** + * Generate a TypeScript interface for a single entity. + */ +export async function generateEntityInterface(entity: Entity): Promise { + const jsonSchema = entityToJsonSchema(entity); + + const ts = await compile(jsonSchema as Parameters[0], entity.name, { + bannerComment: "", + additionalProperties: false, + strictIndexSignatures: true, + enableConstEnums: false, + declareExternallyReferenced: false, + }); + + // Remove the export statement and clean up the output + // json-schema-to-typescript outputs "export interface Name {...}" + // We want just "export interface Name {...}" but formatted nicely + return ts.trim(); +} + +/** + * Generate all entity interfaces. + */ +export async function generateAllEntityInterfaces(entities: Entity[]): Promise { + if (entities.length === 0) { + return ""; + } + + const interfaces: string[] = []; + for (const entity of entities) { + const iface = await generateEntityInterface(entity); + interfaces.push(iface); + } + + return interfaces.join("\n\n"); +} + +/** + * Generate registry entries for entities. + */ +export function generateEntityRegistryEntries(entities: Entity[]): string { + if (entities.length === 0) { + return ""; + } + + return entities.map((e) => ` ${e.name}: ${e.name};`).join("\n"); +} + +/** + * Generate registry entries for function names. + */ +export function generateFunctionRegistryEntries(functions: Function[]): string { + if (functions.length === 0) { + return ""; + } + + return functions.map((f) => ` ${f.name}: true;`).join("\n"); +} + +/** + * Generate registry entries for agent names. + */ +export function generateAgentRegistryEntries(agents: AgentConfig[]): string { + if (agents.length === 0) { + return ""; + } + + return agents.map((a) => ` ${a.name}: true;`).join("\n"); +} diff --git a/src/core/types/index.ts b/src/core/types/index.ts new file mode 100644 index 00000000..5c4d5f84 --- /dev/null +++ b/src/core/types/index.ts @@ -0,0 +1,28 @@ +export { + generateEntityInterface, + generateAllEntityInterfaces, + generateEntityRegistryEntries, + generateFunctionRegistryEntries, + generateAgentRegistryEntries, + type TypesInput, + type TypesOptions, + type TypesResult, +} from "./generator.js"; + +export { generateTypesFileContent, type TemplateInput } from "./template.js"; + +export { + generateAgentJsonSchema, + generateEntityJsonSchema, + generateFunctionJsonSchema, + generateAllJsonSchemas, + type SchemaGeneratorInput, + type GeneratedSchemas, +} from "./json-schema-generator.js"; + +export { + writeAllTypesFiles, + type WriteTypesInput, + type WriteTypesOptions, + type WriteTypesResult, +} from "./write.js"; diff --git a/src/core/types/json-schema-generator.ts b/src/core/types/json-schema-generator.ts new file mode 100644 index 00000000..0af8f842 --- /dev/null +++ b/src/core/types/json-schema-generator.ts @@ -0,0 +1,169 @@ +import { z } from "zod"; + +/** + * Input for JSON Schema generation with resource names. + */ +export interface SchemaGeneratorInput { + entityNames: string[]; + functionNames: string[]; +} + +/** + * Creates a dynamic Zod schema for agent config with actual resource names. + */ +function createDynamicAgentSchema(input: SchemaGeneratorInput) { + // Create enum schemas for entity and function names + // Fall back to string if no resources exist + const EntityNameSchema = + input.entityNames.length > 0 + ? z.enum(input.entityNames as [string, ...string[]]) + : z.string(); + + const FunctionNameSchema = + input.functionNames.length > 0 + ? z.enum(input.functionNames as [string, ...string[]]) + : z.string(); + + // Entity tool config + const EntityToolConfigSchema = z.object({ + entity_name: EntityNameSchema, + allowed_operations: z.array(z.enum(["create", "update", "delete", "read"])), + }); + + // Backend function tool config + const BackendFunctionToolConfigSchema = z.object({ + function_name: FunctionNameSchema, + description: z.string().optional(), + }); + + // Tool config union + const ToolConfigSchema = z.union([EntityToolConfigSchema, BackendFunctionToolConfigSchema]); + + // Full agent config schema + return z.object({ + name: z.string().regex(/^[a-z0-9_]+$/), + description: z.string(), + instructions: z.string(), + tool_configs: z.array(ToolConfigSchema).optional(), + whatsapp_greeting: z.string().nullable().optional(), + }); +} + +/** + * Creates the Zod schema for entity config files. + */ +function createEntitySchema() { + const PropertyTypeSchema = z.enum([ + "string", + "number", + "integer", + "boolean", + "array", + "object", + "binary", + ]); + + const StringFormatSchema = z.enum([ + "date", + "date-time", + "time", + "email", + "uri", + "hostname", + "ipv4", + "ipv6", + "uuid", + "file", + "regex", + ]); + + // Simplified property definition (non-recursive for JSON Schema generation) + const PropertyDefinitionSchema = z.object({ + type: PropertyTypeSchema, + title: z.string().optional(), + description: z.string().optional(), + minLength: z.number().int().min(0).optional(), + maxLength: z.number().int().min(0).optional(), + pattern: z.string().optional(), + format: StringFormatSchema.optional(), + minimum: z.number().optional(), + maximum: z.number().optional(), + enum: z.array(z.string()).optional(), + enumNames: z.array(z.string()).optional(), + default: z.unknown().optional(), + }); + + return z.object({ + type: z.literal("object"), + name: z.string().regex(/^[a-zA-Z0-9]+$/), + title: z.string().optional(), + description: z.string().optional(), + properties: z.record(z.string(), PropertyDefinitionSchema), + required: z.array(z.string()).optional(), + }); +} + +/** + * Creates the Zod schema for function config files. + */ +function createFunctionSchema() { + return z.object({ + name: z.string().regex(/^[^.]+$/), + entry: z.string(), + }); +} + +/** + * Convert a Zod schema to JSON Schema using Zod v4's native support. + */ +function zodToJson(schema: z.ZodType): object { + const jsonSchema = z.toJSONSchema(schema, { + target: "draft-07", + }); + + return jsonSchema; +} + +/** + * Generate JSON Schema for agent config files. + */ +export function generateAgentJsonSchema(input: SchemaGeneratorInput): object { + const schema = createDynamicAgentSchema(input); + return zodToJson(schema); +} + +/** + * Generate JSON Schema for entity config files. + */ +export function generateEntityJsonSchema(): object { + const schema = createEntitySchema(); + return zodToJson(schema); +} + +/** + * Generate JSON Schema for function config files. + */ +export function generateFunctionJsonSchema(): object { + const schema = createFunctionSchema(); + return zodToJson(schema); +} + +/** + * All generated JSON schemas. + */ +export interface GeneratedSchemas { + "agent.schema.json": object; + "entity.schema.json": object; + "function.schema.json": object; +} + +/** + * Generate all JSON Schema files. + */ +export function generateAllJsonSchemas(input: SchemaGeneratorInput): GeneratedSchemas { + return { + "agent.schema.json": generateAgentJsonSchema(input), + "entity.schema.json": generateEntityJsonSchema(), + "function.schema.json": generateFunctionJsonSchema(), + }; +} diff --git a/src/core/types/template.ts b/src/core/types/template.ts new file mode 100644 index 00000000..bb68c0df --- /dev/null +++ b/src/core/types/template.ts @@ -0,0 +1,109 @@ +import type { Entity } from "@/core/resources/entity/index.js"; +import type { Function } from "@/core/resources/function/index.js"; +import type { AgentConfig } from "@/core/resources/agent/index.js"; +import { + generateAllEntityInterfaces, + generateEntityRegistryEntries, + generateFunctionRegistryEntries, + generateAgentRegistryEntries, +} from "./generator.js"; + +/** + * Input for generating the types file. + */ +export interface TemplateInput { + entities: Entity[]; + functions: Function[]; + agents: AgentConfig[]; +} + +/** + * Generate the complete types.d.ts file content. + */ +export async function generateTypesFileContent(input: TemplateInput): Promise { + const { entities, functions, agents } = input; + + // Generate entity interfaces + const entityInterfaces = await generateAllEntityInterfaces(entities); + + // Generate registry entries + const entityRegistryEntries = generateEntityRegistryEntries(entities); + const functionRegistryEntries = generateFunctionRegistryEntries(functions); + const agentRegistryEntries = generateAgentRegistryEntries(agents); + + // Check if we have any content to generate + const hasEntities = entities.length > 0; + const hasFunctions = functions.length > 0; + const hasAgents = agents.length > 0; + + if (!hasEntities && !hasFunctions && !hasAgents) { + return generateEmptyTypesFile(); + } + + const sections: string[] = []; + + // Header + sections.push(`// Auto-generated by Base44 CLI - DO NOT EDIT +// Regenerate with: base44 types +// +// Setup: Add to tsconfig.json: +// { "include": ["src", "base44/types.d.ts"] }`); + + // Entity interfaces section + if (hasEntities) { + sections.push(` +// ─── Entity Types ──────────────────────────────────────────────── + +${entityInterfaces}`); + } + + // SDK Type Augmentation section + sections.push(` +// ─── SDK Type Augmentation ─────────────────────────────────────── + +declare module '@base44/sdk' {`); + + // Entity registry + if (hasEntities) { + sections.push(` interface EntityTypeRegistry { +${entityRegistryEntries} + }`); + } + + // Function registry + if (hasFunctions) { + sections.push(` + interface FunctionNameRegistry { +${functionRegistryEntries} + }`); + } + + // Agent registry + if (hasAgents) { + sections.push(` + interface AgentNameRegistry { +${agentRegistryEntries} + }`); + } + + sections.push(`}`); + + return sections.join("\n"); +} + +/** + * Generate an empty types file when no resources exist. + */ +function generateEmptyTypesFile(): string { + return `// Auto-generated by Base44 CLI - DO NOT EDIT +// Regenerate with: base44 types +// +// No entities, functions, or agents found in project. +// Add resources to base44/entities/, base44/functions/, or base44/agents/ +// and run \`base44 types\` again. + +declare module '@base44/sdk' { + // No types to augment - add resources and regenerate +} +`; +} diff --git a/src/core/types/write.ts b/src/core/types/write.ts new file mode 100644 index 00000000..4f37fdc5 --- /dev/null +++ b/src/core/types/write.ts @@ -0,0 +1,99 @@ +import { join } from "node:path"; +import { mkdir, writeFile } from "node:fs/promises"; +import type { Entity } from "@/core/resources/entity/index.js"; +import type { Function } from "@/core/resources/function/index.js"; +import type { AgentConfig } from "@/core/resources/agent/index.js"; +import { generateTypesFileContent } from "./template.js"; +import { generateAllJsonSchemas } from "./json-schema-generator.js"; + +/** + * Input for writing all type files. + */ +export interface WriteTypesInput { + entities: Entity[]; + functions: Function[]; + agents: AgentConfig[]; +} + +/** + * Options for writing type files. + */ +export interface WriteTypesOptions { + outputDir: string; +} + +/** + * Result of writing type files. + */ +export interface WriteTypesResult { + files: string[]; + entityCount: number; + functionCount: number; + agentCount: number; +} + +/** + * Write the .d.ts types file. + */ +async function writeTypesDeclarationFile( + input: WriteTypesInput, + outputDir: string +): Promise { + const content = await generateTypesFileContent(input); + const filePath = join(outputDir, "types.d.ts"); + await writeFile(filePath, content, "utf-8"); + return "types.d.ts"; +} + +/** + * Write JSON Schema files for config IDE autocomplete. + */ +async function writeJsonSchemaFiles( + input: WriteTypesInput, + outputDir: string +): Promise { + const schemasDir = join(outputDir, "schemas"); + await mkdir(schemasDir, { recursive: true }); + + const schemaInput = { + entityNames: input.entities.map((e) => e.name), + functionNames: input.functions.map((f) => f.name), + }; + + const schemas = generateAllJsonSchemas(schemaInput); + const files: string[] = []; + + for (const [filename, schema] of Object.entries(schemas)) { + const filePath = join(schemasDir, filename); + await writeFile(filePath, JSON.stringify(schema, null, 2), "utf-8"); + files.push(`schemas/${filename}`); + } + + return files; +} + +/** + * Write all type files (.d.ts and JSON Schemas). + */ +export async function writeAllTypesFiles( + input: WriteTypesInput, + options: WriteTypesOptions +): Promise { + const { outputDir } = options; + + // Ensure output directory exists + await mkdir(outputDir, { recursive: true }); + + // Write .d.ts file + const dtsFile = await writeTypesDeclarationFile(input, outputDir); + + // Write JSON Schema files + const schemaFiles = await writeJsonSchemaFiles(input, outputDir); + + return { + files: [dtsFile, ...schemaFiles], + entityCount: input.entities.length, + functionCount: input.functions.length, + agentCount: input.agents.length, + }; +} diff --git a/tests/fixtures/basic/base44/config.jsonc b/tests/fixtures/basic/base44/config.jsonc new file mode 100644 index 00000000..1c5d75ef --- /dev/null +++ b/tests/fixtures/basic/base44/config.jsonc @@ -0,0 +1,4 @@ +{ + "name": "Basic Test Project", +} + diff --git a/tests/fixtures/full-project/base44/config.jsonc b/tests/fixtures/full-project/base44/config.jsonc new file mode 100644 index 00000000..002205e2 --- /dev/null +++ b/tests/fixtures/full-project/base44/config.jsonc @@ -0,0 +1,6 @@ +{ + "name": "Full Project", + "site": { + "outputDirectory": "site-output" + } +} diff --git a/tests/fixtures/full-project/base44/entities/task.json b/tests/fixtures/full-project/base44/entities/task.json new file mode 100644 index 00000000..5a33687a --- /dev/null +++ b/tests/fixtures/full-project/base44/entities/task.json @@ -0,0 +1,8 @@ +{ + "name": "Task", + "type": "object", + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" } + } +} diff --git a/tests/fixtures/full-project/base44/functions/hello/function.jsonc b/tests/fixtures/full-project/base44/functions/hello/function.jsonc new file mode 100644 index 00000000..a8484502 --- /dev/null +++ b/tests/fixtures/full-project/base44/functions/hello/function.jsonc @@ -0,0 +1,4 @@ +{ + "name": "hello", + "entry": "index.ts" +} diff --git a/tests/fixtures/full-project/base44/functions/hello/index.ts b/tests/fixtures/full-project/base44/functions/hello/index.ts new file mode 100644 index 00000000..f3350963 --- /dev/null +++ b/tests/fixtures/full-project/base44/functions/hello/index.ts @@ -0,0 +1,3 @@ +export default function hello() { + return "Hello, World!"; +} diff --git a/tests/fixtures/invalid-agent/base44/agents/broken.json b/tests/fixtures/invalid-agent/base44/agents/broken.json new file mode 100644 index 00000000..bd75d157 --- /dev/null +++ b/tests/fixtures/invalid-agent/base44/agents/broken.json @@ -0,0 +1,4 @@ +{ + "name": "INVALID-NAME!", + "description": "" +} diff --git a/tests/fixtures/invalid-entity/base44/config.jsonc b/tests/fixtures/invalid-entity/base44/config.jsonc new file mode 100644 index 00000000..ed56e88b --- /dev/null +++ b/tests/fixtures/invalid-entity/base44/config.jsonc @@ -0,0 +1,4 @@ +{ + "name": "Invalid Entity Project", +} + diff --git a/tests/fixtures/invalid-entity/base44/entities/broken.json b/tests/fixtures/invalid-entity/base44/entities/broken.json new file mode 100644 index 00000000..e1a8346a --- /dev/null +++ b/tests/fixtures/invalid-entity/base44/entities/broken.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "properties": {} +} diff --git a/tests/fixtures/with-agents/base44/agents/customer_support.json b/tests/fixtures/with-agents/base44/agents/customer_support.json new file mode 100644 index 00000000..72314e85 --- /dev/null +++ b/tests/fixtures/with-agents/base44/agents/customer_support.json @@ -0,0 +1,12 @@ +{ + "name": "customer_support", + "description": "A helpful customer support agent that assists users with common questions and issues", + "instructions": "You are a friendly customer support agent. Help users resolve their issues politely and efficiently. If you cannot help, escalate to a human agent. Always be professional and empathetic.", + "tool_configs": [ + { + "entity_name": "task", + "allowed_operations": ["read", "create", "update"] + } + ], + "whatsapp_greeting": "Hi! I'm your support assistant. How can I help you today?" +} diff --git a/tests/fixtures/with-agents/base44/agents/data_analyst.jsonc b/tests/fixtures/with-agents/base44/agents/data_analyst.jsonc new file mode 100644 index 00000000..b694e056 --- /dev/null +++ b/tests/fixtures/with-agents/base44/agents/data_analyst.jsonc @@ -0,0 +1,12 @@ +{ + // A simple agent with minimal config + "name": "data_analyst", + "description": "Analyzes task data and provides insights", + "instructions": "You are a data analyst. When asked about tasks, query the task entity and provide clear, actionable insights.", + "tool_configs": [ + { + "entity_name": "task", + "allowed_operations": ["read"] + } + ] +} diff --git a/tests/fixtures/with-agents/base44/agents/order_assistant.json b/tests/fixtures/with-agents/base44/agents/order_assistant.json new file mode 100644 index 00000000..67be67ea --- /dev/null +++ b/tests/fixtures/with-agents/base44/agents/order_assistant.json @@ -0,0 +1,11 @@ +{ + "name": "order_assistant", + "description": "An agent that helps customers track and manage their tasks", + "instructions": "You help customers with task-related inquiries. You can look up task status and help manage tasks. Be concise and helpful.", + "tool_configs": [ + { + "entity_name": "task", + "allowed_operations": ["read", "update", "delete"] + } + ] +} diff --git a/tests/fixtures/with-agents/base44/config.jsonc b/tests/fixtures/with-agents/base44/config.jsonc new file mode 100644 index 00000000..f588b78f --- /dev/null +++ b/tests/fixtures/with-agents/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Agents Test Project" +} diff --git a/tests/fixtures/with-entities/base44/config.jsonc b/tests/fixtures/with-entities/base44/config.jsonc new file mode 100644 index 00000000..67e52750 --- /dev/null +++ b/tests/fixtures/with-entities/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Entities Test Project" +} diff --git a/tests/fixtures/with-entities/base44/entities/customer.json b/tests/fixtures/with-entities/base44/entities/customer.json new file mode 100644 index 00000000..fddd9585 --- /dev/null +++ b/tests/fixtures/with-entities/base44/entities/customer.json @@ -0,0 +1,15 @@ +{ + "name": "Customer", + "type": "object", + "properties": { + "company": { + "type": "string", + "description": "Company name" + }, + "contact": { + "type": "string", + "description": "Contact person" + } + }, + "required": ["company"] +} diff --git a/tests/fixtures/with-entities/base44/entities/product.json b/tests/fixtures/with-entities/base44/entities/product.json new file mode 100644 index 00000000..43b3828d --- /dev/null +++ b/tests/fixtures/with-entities/base44/entities/product.json @@ -0,0 +1,15 @@ +{ + "name": "Product", + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Product title" + }, + "price": { + "type": "number", + "description": "Product price" + } + }, + "required": ["title"] +} diff --git a/tests/fixtures/with-functions-and-entities/base44/config.jsonc b/tests/fixtures/with-functions-and-entities/base44/config.jsonc new file mode 100644 index 00000000..3ada9605 --- /dev/null +++ b/tests/fixtures/with-functions-and-entities/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Full Test Project" +} diff --git a/tests/fixtures/with-functions-and-entities/base44/entities/order.json b/tests/fixtures/with-functions-and-entities/base44/entities/order.json new file mode 100644 index 00000000..10baa54c --- /dev/null +++ b/tests/fixtures/with-functions-and-entities/base44/entities/order.json @@ -0,0 +1,19 @@ +{ + "name": "Order", + "type": "object", + "properties": { + "orderId": { + "type": "string", + "description": "Order ID" + }, + "status": { + "type": "string", + "description": "Order status" + }, + "total": { + "type": "number", + "description": "Order total" + } + }, + "required": ["orderId"] +} diff --git a/tests/fixtures/with-functions-and-entities/base44/functions/process-order/function.jsonc b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/function.jsonc new file mode 100644 index 00000000..ea0445a7 --- /dev/null +++ b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/function.jsonc @@ -0,0 +1,4 @@ +{ + "name": "process-order", + "entry": "index.ts" +} diff --git a/tests/fixtures/with-functions-and-entities/base44/functions/process-order/helper.ts b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/helper.ts new file mode 100644 index 00000000..2a599770 --- /dev/null +++ b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/helper.ts @@ -0,0 +1,3 @@ +export function foo(text: string) { + console.log(text) +} diff --git a/tests/fixtures/with-functions-and-entities/base44/functions/process-order/index.ts b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/index.ts new file mode 100644 index 00000000..10191359 --- /dev/null +++ b/tests/fixtures/with-functions-and-entities/base44/functions/process-order/index.ts @@ -0,0 +1,10 @@ +import { foo } from "./helper.ts"; + + +Deno.serve(async (req: Request) => { + const body = await req.json(); + + const order = foo(body); + + return new Response(JSON.stringify({ success: true, order: body })); +}) diff --git a/tests/fixtures/with-site/base44/config.jsonc b/tests/fixtures/with-site/base44/config.jsonc new file mode 100644 index 00000000..fb2f5a3a --- /dev/null +++ b/tests/fixtures/with-site/base44/config.jsonc @@ -0,0 +1,6 @@ +{ + "name": "Site Test Project", + "site": { + "outputDirectory": "site-output" + } +} From a4777a49c1e39ee71773b8db12753ce52dac7af7 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Wed, 4 Feb 2026 19:50:30 +0200 Subject: [PATCH 02/13] types changes' --- package.json | 2 +- src/cli/commands/types/generate.ts | 74 +++-------- src/cli/utils/version-check.ts | 5 +- src/core/config.ts | 10 +- src/core/consts.ts | 4 + src/core/types/generator.ts | 61 +++++---- src/core/types/index.ts | 28 +--- src/core/types/json-schema-generator.ts | 169 ------------------------ src/core/types/template.ts | 141 ++++++++------------ src/core/types/write.ts | 99 -------------- 10 files changed, 126 insertions(+), 467 deletions(-) delete mode 100644 src/core/types/json-schema-generator.ts delete mode 100644 src/core/types/write.ts diff --git a/package.json b/package.json index 41108b53..66829a77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base44", - "version": "0.0.28", + "version": "0.0.25", "description": "Base44 CLI - Unified interface for managing Base44 applications", "type": "module", "bin": { diff --git a/src/cli/commands/types/generate.ts b/src/cli/commands/types/generate.ts index 2bd477ef..939a0de4 100644 --- a/src/cli/commands/types/generate.ts +++ b/src/cli/commands/types/generate.ts @@ -1,51 +1,18 @@ -import { join, dirname } from "node:path"; -import { Command } from "commander"; import { log } from "@clack/prompts"; +import { Command } from "commander"; import type { CLIContext } from "@/cli/types.js"; -import { readProjectConfig } from "@/core/index.js"; -import { writeAllTypesFiles } from "@/core/types/index.js"; import { runCommand, runTask, theme } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { readProjectConfig } from "@/core/index.js"; +import { generateBase44TypesFile } from "@/core/types/index.js"; -interface TypesCommandOptions { - output?: string; -} - -async function generateTypesAction(options: TypesCommandOptions): Promise { - const { entities, functions, agents, project } = await readProjectConfig(); - - // Determine output directory - // Default: base44/ directory next to config.jsonc - const configDir = dirname(project.configPath); - const outputDir = options.output ?? join(configDir, ""); - - // Log what we found - const resourceCounts: string[] = []; - if (entities.length > 0) { - resourceCounts.push(`${entities.length} ${entities.length === 1 ? "entity" : "entities"}`); - } - if (functions.length > 0) { - resourceCounts.push(`${functions.length} ${functions.length === 1 ? "function" : "functions"}`); - } - if (agents.length > 0) { - resourceCounts.push(`${agents.length} ${agents.length === 1 ? "agent" : "agents"}`); - } - - if (resourceCounts.length === 0) { - log.warn("No entities, functions, or agents found in project"); - log.info("Add resources and run 'base44 types' again"); - return { outroMessage: "No types generated" }; - } - - log.info(`Found ${resourceCounts.join(", ")}`); +async function generateTypesAction(): Promise { + const { entities, functions, agents } = await readProjectConfig(); - const result = await runTask( + await runTask( "Generating types", async () => { - return await writeAllTypesFiles( - { entities, functions, agents }, - { outputDir } - ); + await generateBase44TypesFile({ entities, functions, agents }); }, { successMessage: theme.colors.base44Orange("Types generated successfully"), @@ -53,34 +20,27 @@ async function generateTypesAction(options: TypesCommandOptions): Promise", "Output directory (default: base44/)") - .action(async (options: TypesCommandOptions) => { + .description( + "Generate TypeScript declaration file (types.d.ts) from project resources" + ) + .action(async () => { await runCommand( - () => generateTypesAction(options), - { requireAuth: false, requireAppConfig: false }, + () => generateTypesAction(), + { requireAuth: false }, context ); }); diff --git a/src/cli/utils/version-check.ts b/src/cli/utils/version-check.ts index fcecb503..15d2c64d 100644 --- a/src/cli/utils/version-check.ts +++ b/src/cli/utils/version-check.ts @@ -22,7 +22,7 @@ export async function checkForUpgrade(): Promise { try { const { stdout } = await execa("npm", ["view", "base44", "version"], { - timeout: 500, + timeout: 1000, shell: true, env: { CI: "1" }, }); @@ -33,7 +33,8 @@ export async function checkForUpgrade(): Promise { return { currentVersion, latestVersion }; } return null; - } catch { + } catch (error) { + console.log(error) return null; } } diff --git a/src/core/config.ts b/src/core/config.ts index 2f7b7c10..e9b3ecee 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -1,7 +1,11 @@ import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { PROJECT_SUBDIR } from "@/core/consts.js"; +import { + PROJECT_SUBDIR, + TYPES_FILENAME, + TYPES_OUTPUT_SUBDIR, +} from "@/core/consts.js"; import { type TestOverrides, TestOverridesSchema, @@ -31,6 +35,10 @@ export function getAppConfigPath(projectRoot: string): string { return join(projectRoot, PROJECT_SUBDIR, ".app.jsonc"); } +export function getTypesOutputPath(projectRoot: string): string { + return join(projectRoot, PROJECT_SUBDIR, TYPES_OUTPUT_SUBDIR, TYPES_FILENAME); +} + export function getBase44ApiUrl(): string { return process.env.BASE44_API_URL || "https://app.base44.com"; } diff --git a/src/core/consts.ts b/src/core/consts.ts index fc7b28e3..7aec471d 100644 --- a/src/core/consts.ts +++ b/src/core/consts.ts @@ -12,5 +12,9 @@ export const PROJECT_CONFIG_PATTERNS = [ `config.${CONFIG_FILE_EXTENSION_GLOB}`, ]; +// Types generation +export const TYPES_OUTPUT_SUBDIR = ".types"; +export const TYPES_FILENAME = "types.d.ts"; + // Auth export const AUTH_CLIENT_ID = "base44_cli"; diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index 1f54cbc9..8c527b42 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -1,32 +1,31 @@ import { compile } from "json-schema-to-typescript"; -import type { Entity } from "@/core/resources/entity/index.js"; -import type { Function } from "@/core/resources/function/index.js"; +import { getTypesOutputPath } from "@/core/config.js"; +import { getAppConfig } from "@/core/project/app-config.js"; import type { AgentConfig } from "@/core/resources/agent/index.js"; +import type { Entity } from "@/core/resources/entity/index.js"; +import type { BackendFunction } from "@/core/resources/function/index.js"; +import { writeFile } from "@/core/utils/fs.js"; +import { generateTypesFileContent } from "./template.js"; /** - * Input for type generation containing all project resources. + * Input for generating the Base44 types file. */ -export interface TypesInput { +export interface GenerateBase44TypesInput { entities: Entity[]; - functions: Function[]; + functions: BackendFunction[]; agents: AgentConfig[]; } /** - * Options for type generation. - */ -export interface TypesOptions { - outputDir: string; -} - -/** - * Result of type generation. + * Generate and write the types.d.ts file to /base44/.types/types.d.ts. */ -export interface TypesResult { - files: string[]; - entityCount: number; - functionCount: number; - agentCount: number; +export async function generateBase44TypesFile( + input: GenerateBase44TypesInput +): Promise { + const { projectRoot } = getAppConfig(); + const content = await generateTypesFileContent(input); + const filePath = getTypesOutputPath(projectRoot); + await writeFile(filePath, content); } /** @@ -104,13 +103,17 @@ function propertyToJsonSchema(prop: Record): object { export async function generateEntityInterface(entity: Entity): Promise { const jsonSchema = entityToJsonSchema(entity); - const ts = await compile(jsonSchema as Parameters[0], entity.name, { - bannerComment: "", - additionalProperties: false, - strictIndexSignatures: true, - enableConstEnums: false, - declareExternallyReferenced: false, - }); + const ts = await compile( + jsonSchema as Parameters[0], + entity.name, + { + bannerComment: "", + additionalProperties: false, + strictIndexSignatures: true, + enableConstEnums: false, + declareExternallyReferenced: false, + } + ); // Remove the export statement and clean up the output // json-schema-to-typescript outputs "export interface Name {...}" @@ -121,7 +124,9 @@ export async function generateEntityInterface(entity: Entity): Promise { /** * Generate all entity interfaces. */ -export async function generateAllEntityInterfaces(entities: Entity[]): Promise { +export async function generateAllEntityInterfaces( + entities: Entity[] +): Promise { if (entities.length === 0) { return ""; } @@ -149,7 +154,9 @@ export function generateEntityRegistryEntries(entities: Entity[]): string { /** * Generate registry entries for function names. */ -export function generateFunctionRegistryEntries(functions: Function[]): string { +export function generateFunctionRegistryEntries( + functions: BackendFunction[] +): string { if (functions.length === 0) { return ""; } diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 5c4d5f84..39d68957 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -1,28 +1,4 @@ export { - generateEntityInterface, - generateAllEntityInterfaces, - generateEntityRegistryEntries, - generateFunctionRegistryEntries, - generateAgentRegistryEntries, - type TypesInput, - type TypesOptions, - type TypesResult, + type GenerateBase44TypesInput, + generateBase44TypesFile, } from "./generator.js"; - -export { generateTypesFileContent, type TemplateInput } from "./template.js"; - -export { - generateAgentJsonSchema, - generateEntityJsonSchema, - generateFunctionJsonSchema, - generateAllJsonSchemas, - type SchemaGeneratorInput, - type GeneratedSchemas, -} from "./json-schema-generator.js"; - -export { - writeAllTypesFiles, - type WriteTypesInput, - type WriteTypesOptions, - type WriteTypesResult, -} from "./write.js"; diff --git a/src/core/types/json-schema-generator.ts b/src/core/types/json-schema-generator.ts deleted file mode 100644 index 0af8f842..00000000 --- a/src/core/types/json-schema-generator.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { z } from "zod"; - -/** - * Input for JSON Schema generation with resource names. - */ -export interface SchemaGeneratorInput { - entityNames: string[]; - functionNames: string[]; -} - -/** - * Creates a dynamic Zod schema for agent config with actual resource names. - */ -function createDynamicAgentSchema(input: SchemaGeneratorInput) { - // Create enum schemas for entity and function names - // Fall back to string if no resources exist - const EntityNameSchema = - input.entityNames.length > 0 - ? z.enum(input.entityNames as [string, ...string[]]) - : z.string(); - - const FunctionNameSchema = - input.functionNames.length > 0 - ? z.enum(input.functionNames as [string, ...string[]]) - : z.string(); - - // Entity tool config - const EntityToolConfigSchema = z.object({ - entity_name: EntityNameSchema, - allowed_operations: z.array(z.enum(["create", "update", "delete", "read"])), - }); - - // Backend function tool config - const BackendFunctionToolConfigSchema = z.object({ - function_name: FunctionNameSchema, - description: z.string().optional(), - }); - - // Tool config union - const ToolConfigSchema = z.union([EntityToolConfigSchema, BackendFunctionToolConfigSchema]); - - // Full agent config schema - return z.object({ - name: z.string().regex(/^[a-z0-9_]+$/), - description: z.string(), - instructions: z.string(), - tool_configs: z.array(ToolConfigSchema).optional(), - whatsapp_greeting: z.string().nullable().optional(), - }); -} - -/** - * Creates the Zod schema for entity config files. - */ -function createEntitySchema() { - const PropertyTypeSchema = z.enum([ - "string", - "number", - "integer", - "boolean", - "array", - "object", - "binary", - ]); - - const StringFormatSchema = z.enum([ - "date", - "date-time", - "time", - "email", - "uri", - "hostname", - "ipv4", - "ipv6", - "uuid", - "file", - "regex", - ]); - - // Simplified property definition (non-recursive for JSON Schema generation) - const PropertyDefinitionSchema = z.object({ - type: PropertyTypeSchema, - title: z.string().optional(), - description: z.string().optional(), - minLength: z.number().int().min(0).optional(), - maxLength: z.number().int().min(0).optional(), - pattern: z.string().optional(), - format: StringFormatSchema.optional(), - minimum: z.number().optional(), - maximum: z.number().optional(), - enum: z.array(z.string()).optional(), - enumNames: z.array(z.string()).optional(), - default: z.unknown().optional(), - }); - - return z.object({ - type: z.literal("object"), - name: z.string().regex(/^[a-zA-Z0-9]+$/), - title: z.string().optional(), - description: z.string().optional(), - properties: z.record(z.string(), PropertyDefinitionSchema), - required: z.array(z.string()).optional(), - }); -} - -/** - * Creates the Zod schema for function config files. - */ -function createFunctionSchema() { - return z.object({ - name: z.string().regex(/^[^.]+$/), - entry: z.string(), - }); -} - -/** - * Convert a Zod schema to JSON Schema using Zod v4's native support. - */ -function zodToJson(schema: z.ZodType): object { - const jsonSchema = z.toJSONSchema(schema, { - target: "draft-07", - }); - - return jsonSchema; -} - -/** - * Generate JSON Schema for agent config files. - */ -export function generateAgentJsonSchema(input: SchemaGeneratorInput): object { - const schema = createDynamicAgentSchema(input); - return zodToJson(schema); -} - -/** - * Generate JSON Schema for entity config files. - */ -export function generateEntityJsonSchema(): object { - const schema = createEntitySchema(); - return zodToJson(schema); -} - -/** - * Generate JSON Schema for function config files. - */ -export function generateFunctionJsonSchema(): object { - const schema = createFunctionSchema(); - return zodToJson(schema); -} - -/** - * All generated JSON schemas. - */ -export interface GeneratedSchemas { - "agent.schema.json": object; - "entity.schema.json": object; - "function.schema.json": object; -} - -/** - * Generate all JSON Schema files. - */ -export function generateAllJsonSchemas(input: SchemaGeneratorInput): GeneratedSchemas { - return { - "agent.schema.json": generateAgentJsonSchema(input), - "entity.schema.json": generateEntityJsonSchema(), - "function.schema.json": generateFunctionJsonSchema(), - }; -} diff --git a/src/core/types/template.ts b/src/core/types/template.ts index bb68c0df..6bfe728f 100644 --- a/src/core/types/template.ts +++ b/src/core/types/template.ts @@ -1,109 +1,80 @@ -import type { Entity } from "@/core/resources/entity/index.js"; -import type { Function } from "@/core/resources/function/index.js"; +import { source, stripIndent } from "common-tags"; import type { AgentConfig } from "@/core/resources/agent/index.js"; +import type { Entity } from "@/core/resources/entity/index.js"; +import type { BackendFunction } from "@/core/resources/function/index.js"; import { + generateAgentRegistryEntries, generateAllEntityInterfaces, generateEntityRegistryEntries, generateFunctionRegistryEntries, - generateAgentRegistryEntries, } from "./generator.js"; -/** - * Input for generating the types file. - */ export interface TemplateInput { entities: Entity[]; - functions: Function[]; + functions: BackendFunction[]; agents: AgentConfig[]; } -/** - * Generate the complete types.d.ts file content. - */ -export async function generateTypesFileContent(input: TemplateInput): Promise { - const { entities, functions, agents } = input; - - // Generate entity interfaces - const entityInterfaces = await generateAllEntityInterfaces(entities); - - // Generate registry entries - const entityRegistryEntries = generateEntityRegistryEntries(entities); - const functionRegistryEntries = generateFunctionRegistryEntries(functions); - const agentRegistryEntries = generateAgentRegistryEntries(agents); - - // Check if we have any content to generate - const hasEntities = entities.length > 0; - const hasFunctions = functions.length > 0; - const hasAgents = agents.length > 0; - - if (!hasEntities && !hasFunctions && !hasAgents) { - return generateEmptyTypesFile(); - } - - const sections: string[] = []; - - // Header - sections.push(`// Auto-generated by Base44 CLI - DO NOT EDIT -// Regenerate with: base44 types -// -// Setup: Add to tsconfig.json: -// { "include": ["src", "base44/types.d.ts"] }`); +const HEADER = stripIndent` + // Auto-generated by Base44 CLI - DO NOT EDIT + // Regenerate with: base44 types + // + // Setup: Add to tsconfig.json: + // { "include": ["src", "base44/.types"] } +`; - // Entity interfaces section - if (hasEntities) { - sections.push(` -// ─── Entity Types ──────────────────────────────────────────────── +const EMPTY_TEMPLATE = stripIndent` + // Auto-generated by Base44 CLI - DO NOT EDIT + // Regenerate with: base44 types + // + // No entities, functions, or agents found in project. + // Add resources to base44/entities/, base44/functions/, or base44/agents/ + // and run \`base44 types\` again. -${entityInterfaces}`); + declare module '@base44/sdk' { + // No types to augment - add resources and regenerate } +`; - // SDK Type Augmentation section - sections.push(` -// ─── SDK Type Augmentation ─────────────────────────────────────── - -declare module '@base44/sdk' {`); - - // Entity registry - if (hasEntities) { - sections.push(` interface EntityTypeRegistry { -${entityRegistryEntries} - }`); - } +function registryInterface(name: string, entries: string): string { + return source` + interface ${name} { + ${entries} + } + `; +} - // Function registry - if (hasFunctions) { - sections.push(` - interface FunctionNameRegistry { -${functionRegistryEntries} - }`); - } +export async function generateTypesFileContent( + input: TemplateInput +): Promise { + const { entities, functions, agents } = input; - // Agent registry - if (hasAgents) { - sections.push(` - interface AgentNameRegistry { -${agentRegistryEntries} - }`); + if (!entities.length && !functions.length && !agents.length) { + return EMPTY_TEMPLATE; } - sections.push(`}`); + // Build registries for each resource type that has items + const registryConfigs: [string, string][] = [ + ["EntityTypeRegistry", generateEntityRegistryEntries(entities)], + ["FunctionNameRegistry", generateFunctionRegistryEntries(functions)], + ["AgentNameRegistry", generateAgentRegistryEntries(agents)], + ]; - return sections.join("\n"); -} + const registries = registryConfigs + .filter(([, entries]) => entries) + .map(([name, entries]) => registryInterface(name, entries)); -/** - * Generate an empty types file when no resources exist. - */ -function generateEmptyTypesFile(): string { - return `// Auto-generated by Base44 CLI - DO NOT EDIT -// Regenerate with: base44 types -// -// No entities, functions, or agents found in project. -// Add resources to base44/entities/, base44/functions/, or base44/agents/ -// and run \`base44 types\` again. + const entityInterfaces = await generateAllEntityInterfaces(entities); -declare module '@base44/sdk' { - // No types to augment - add resources and regenerate -} -`; + return [ + HEADER, + entityInterfaces, + source` + declare module '@base44/sdk' { + ${registries.join("\n\n")} + } + `, + ] + .filter(Boolean) + .join("\n\n"); } diff --git a/src/core/types/write.ts b/src/core/types/write.ts deleted file mode 100644 index 4f37fdc5..00000000 --- a/src/core/types/write.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { join } from "node:path"; -import { mkdir, writeFile } from "node:fs/promises"; -import type { Entity } from "@/core/resources/entity/index.js"; -import type { Function } from "@/core/resources/function/index.js"; -import type { AgentConfig } from "@/core/resources/agent/index.js"; -import { generateTypesFileContent } from "./template.js"; -import { generateAllJsonSchemas } from "./json-schema-generator.js"; - -/** - * Input for writing all type files. - */ -export interface WriteTypesInput { - entities: Entity[]; - functions: Function[]; - agents: AgentConfig[]; -} - -/** - * Options for writing type files. - */ -export interface WriteTypesOptions { - outputDir: string; -} - -/** - * Result of writing type files. - */ -export interface WriteTypesResult { - files: string[]; - entityCount: number; - functionCount: number; - agentCount: number; -} - -/** - * Write the .d.ts types file. - */ -async function writeTypesDeclarationFile( - input: WriteTypesInput, - outputDir: string -): Promise { - const content = await generateTypesFileContent(input); - const filePath = join(outputDir, "types.d.ts"); - await writeFile(filePath, content, "utf-8"); - return "types.d.ts"; -} - -/** - * Write JSON Schema files for config IDE autocomplete. - */ -async function writeJsonSchemaFiles( - input: WriteTypesInput, - outputDir: string -): Promise { - const schemasDir = join(outputDir, "schemas"); - await mkdir(schemasDir, { recursive: true }); - - const schemaInput = { - entityNames: input.entities.map((e) => e.name), - functionNames: input.functions.map((f) => f.name), - }; - - const schemas = generateAllJsonSchemas(schemaInput); - const files: string[] = []; - - for (const [filename, schema] of Object.entries(schemas)) { - const filePath = join(schemasDir, filename); - await writeFile(filePath, JSON.stringify(schema, null, 2), "utf-8"); - files.push(`schemas/${filename}`); - } - - return files; -} - -/** - * Write all type files (.d.ts and JSON Schemas). - */ -export async function writeAllTypesFiles( - input: WriteTypesInput, - options: WriteTypesOptions -): Promise { - const { outputDir } = options; - - // Ensure output directory exists - await mkdir(outputDir, { recursive: true }); - - // Write .d.ts file - const dtsFile = await writeTypesDeclarationFile(input, outputDir); - - // Write JSON Schema files - const schemaFiles = await writeJsonSchemaFiles(input, outputDir); - - return { - files: [dtsFile, ...schemaFiles], - entityCount: input.entities.length, - functionCount: input.functions.length, - agentCount: input.agents.length, - }; -} From e629eee1b8cb74d4f10bfe595d8bf6dd9e7f5a29 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 14:50:32 +0200 Subject: [PATCH 03/13] type changes --- package.json | 2 +- src/cli/commands/types/generate.ts | 43 ++++++++++++++-------------- src/cli/commands/types/index.ts | 9 ++++++ src/cli/program.ts | 4 +-- src/cli/utils/version-check.ts | 2 +- src/core/types/generator.ts | 15 +++++++++- src/core/types/index.ts | 1 + src/core/types/update-project.ts | 46 ++++++++++++++++++++++++++++++ 8 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 src/cli/commands/types/index.ts create mode 100644 src/core/types/update-project.ts diff --git a/package.json b/package.json index 66829a77..41108b53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base44", - "version": "0.0.25", + "version": "0.0.28", "description": "Base44 CLI - Unified interface for managing Base44 applications", "type": "module", "bin": { diff --git a/src/cli/commands/types/generate.ts b/src/cli/commands/types/generate.ts index 939a0de4..0ed71fa5 100644 --- a/src/cli/commands/types/generate.ts +++ b/src/cli/commands/types/generate.ts @@ -1,39 +1,38 @@ -import { log } from "@clack/prompts"; import { Command } from "commander"; import type { CLIContext } from "@/cli/types.js"; -import { runCommand, runTask, theme } from "@/cli/utils/index.js"; +import { runCommand, runTask } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { readProjectConfig } from "@/core/index.js"; -import { generateBase44TypesFile } from "@/core/types/index.js"; +import { + generateBase44TypesFile, + updateProjectConfig, +} from "@/core/types/index.js"; + +const TYPES_FILE_PATH = "base44/.types/types.d.ts"; async function generateTypesAction(): Promise { - const { entities, functions, agents } = await readProjectConfig(); + const { entities, functions, agents, project } = await readProjectConfig(); - await runTask( - "Generating types", - async () => { - await generateBase44TypesFile({ entities, functions, agents }); - }, - { - successMessage: theme.colors.base44Orange("Types generated successfully"), - errorMessage: "Failed to generate types", - } - ); + await runTask("Generating types", async () => { + await generateBase44TypesFile({ entities, functions, agents }); + }); - log.success("Generated base44/.types/types.d.ts"); + // Try to update project configuration + const tsconfigUpdated = await updateProjectConfig(project.root); - log.info(""); - log.info(theme.styles.header("Setup:")); - log.message(` Add to ${theme.styles.bold("tsconfig.json")}:`); - log.message(` { "include": ["src", "base44/.types"] }`); + if (tsconfigUpdated) { + return { + outroMessage: `Generated ${TYPES_FILE_PATH} and updated tsconfig.json`, + }; + } return { - outroMessage: "Types written to base44/.types/types.d.ts", + outroMessage: `Generated ${TYPES_FILE_PATH}`, }; } -export function getTypesCommand(context: CLIContext): Command { - return new Command("types") +export function getTypesGenerateCommand(context: CLIContext): Command { + return new Command("generate") .description( "Generate TypeScript declaration file (types.d.ts) from project resources" ) diff --git a/src/cli/commands/types/index.ts b/src/cli/commands/types/index.ts new file mode 100644 index 00000000..91e86701 --- /dev/null +++ b/src/cli/commands/types/index.ts @@ -0,0 +1,9 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { getTypesGenerateCommand } from "./generate.js"; + +export function getTypesCommand(context: CLIContext): Command { + return new Command("types") + .description("Manage TypeScript type generation") + .addCommand(getTypesGenerateCommand(context)); +} diff --git a/src/cli/program.ts b/src/cli/program.ts index 0d251e43..15e4f1d7 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -10,7 +10,7 @@ import { getCreateCommand } from "@/cli/commands/project/create.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; import { getSiteCommand } from "@/cli/commands/site/index.js"; -import { getTypesCommand } from "@/cli/commands/types/generate.js"; +import { getTypesCommand } from "@/cli/commands/types/index.js"; import packageJson from "../../package.json"; import type { CLIContext } from "./types.js"; @@ -52,7 +52,7 @@ export function createProgram(context: CLIContext): Command { program.addCommand(getSiteCommand(context)); // Register types command - program.addCommand(getTypesCommand(context)); + program.addCommand(getTypesCommand(context), { hidden: true }); return program; } diff --git a/src/cli/utils/version-check.ts b/src/cli/utils/version-check.ts index 15d2c64d..5b9e207d 100644 --- a/src/cli/utils/version-check.ts +++ b/src/cli/utils/version-check.ts @@ -34,7 +34,7 @@ export async function checkForUpgrade(): Promise { } return null; } catch (error) { - console.log(error) + console.log(error); return null; } } diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index 8c527b42..41260099 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -7,6 +7,17 @@ import type { BackendFunction } from "@/core/resources/function/index.js"; import { writeFile } from "@/core/utils/fs.js"; import { generateTypesFileContent } from "./template.js"; +/** + * Convert entity name to PascalCase (matching json-schema-to-typescript behavior). + * Examples: "orders" -> "Orders", "user_profile" -> "UserProfile" + */ +function toPascalCase(name: string): string { + return name + .split(/[-_\s]+/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(""); +} + /** * Input for generating the Base44 types file. */ @@ -148,7 +159,9 @@ export function generateEntityRegistryEntries(entities: Entity[]): string { return ""; } - return entities.map((e) => ` ${e.name}: ${e.name};`).join("\n"); + return entities + .map((e) => ` ${e.name}: ${toPascalCase(e.name)};`) + .join("\n"); } /** diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 39d68957..035c90d0 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -2,3 +2,4 @@ export { type GenerateBase44TypesInput, generateBase44TypesFile, } from "./generator.js"; +export { updateProjectConfig } from "./update-project.js"; diff --git a/src/core/types/update-project.ts b/src/core/types/update-project.ts new file mode 100644 index 00000000..e89d2773 --- /dev/null +++ b/src/core/types/update-project.ts @@ -0,0 +1,46 @@ +import { join } from "node:path"; +import { PROJECT_SUBDIR, TYPES_OUTPUT_SUBDIR } from "@/core/consts.js"; +import { pathExists, readJsonFile, writeJsonFile } from "@/core/utils/fs.js"; + +const TYPES_INCLUDE_PATH = `${PROJECT_SUBDIR}/${TYPES_OUTPUT_SUBDIR}/*.d.ts`; + +/** + * Update project configuration files after generating types. + * Currently handles: + * - tsconfig.json: adds base44/.types to the include array + * + * @returns true if tsconfig.json was updated, false otherwise + */ +export async function updateProjectConfig( + projectRoot: string +): Promise { + const tsconfigPath = join(projectRoot, "tsconfig.json"); + + if (!(await pathExists(tsconfigPath))) { + return false; + } + + try { + const tsconfig = (await readJsonFile(tsconfigPath)) as { + include?: string[]; + }; + + // Ensure include array exists + if (!tsconfig.include) { + tsconfig.include = []; + } + + // Check if already included + if (tsconfig.include.includes(TYPES_INCLUDE_PATH)) { + return false; + } + + // Add to include array + tsconfig.include.push(TYPES_INCLUDE_PATH); + await writeJsonFile(tsconfigPath, tsconfig); + return true; + } catch { + // If we can't parse or update, silently fail and let user configure manually + return false; + } +} From 3f66591b8e1f3fa6c16acd1398fca8eebcdecdf6 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:11:58 +0200 Subject: [PATCH 04/13] update bun lock file --- bun.lock | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/bun.lock b/bun.lock index c076bf66..c29a500e 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "execa": "^9.6.1", "front-matter": "^4.0.2", "globby": "^16.1.0", + "json-schema-to-typescript": "^15.0.4", "json5": "^2.2.3", "ky": "^1.14.2", "lodash.kebabcase": "^4.1.1", @@ -37,6 +38,8 @@ }, }, "packages": { + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], + "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="], @@ -75,6 +78,8 @@ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.40.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -157,6 +162,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], "@types/lodash.kebabcase": ["@types/lodash.kebabcase@4.1.9", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-kPrrmcVOhSsjAVRovN0lRfrbuidfg0wYsrQa5IYuoQO1fpHHGSme66oyiYA/5eQPVl8Z95OA3HG0+d2SvYC85w=="], @@ -315,10 +322,14 @@ "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "json-schema-to-typescript": ["json-schema-to-typescript@15.0.4", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.7", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", "prettier": "^3.2.5", "tinyglobby": "^0.2.9" }, "bin": { "json2ts": "dist/src/cli.js" } }, "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ=="], + "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "ky": ["ky@1.14.2", "", {}, "sha512-q3RBbsO5A5zrPhB6CaCS8ZUv+NWCXv6JJT4Em0i264G9W0fdPB8YRfnnEi7Dm7X7omAkBIPojzYJ2D1oHTHqug=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -329,6 +340,8 @@ "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], @@ -367,6 +380,8 @@ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -477,12 +492,16 @@ "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "json-schema-to-typescript/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "minizlib/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -499,8 +518,12 @@ "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@apidevtools/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "json-schema-to-typescript/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], From 477e99b1c881cbd041506fb036d0493580276a1f Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:16:42 +0200 Subject: [PATCH 05/13] update deps --- bun.lock | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index c29a500e..2beb0339 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "@vercel/detect-agent": "^1.1.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "common-tags": "^1.8.2", "ejs": "^3.1.10", "execa": "^9.6.1", "front-matter": "^4.0.2", @@ -226,6 +227,8 @@ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], diff --git a/package.json b/package.json index aa8f7845..6685c42d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@vercel/detect-agent": "^1.1.0", "chalk": "^5.6.2", "commander": "^12.1.0", + "common-tags": "^1.8.2", "ejs": "^3.1.10", "execa": "^9.6.1", "front-matter": "^4.0.2", From 280bdcbe708699aafdf31e665efc1c2bfd45f702 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:32:01 +0200 Subject: [PATCH 06/13] added missing types --- bun.lock | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/bun.lock b/bun.lock index 2beb0339..676656d6 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@biomejs/biome": "^2.0.0", "@clack/prompts": "^0.11.0", "@types/bun": "^1.2.15", + "@types/common-tags": "^1.8.4", "@types/ejs": "^3.1.5", "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", @@ -157,6 +158,8 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/common-tags": ["@types/common-tags@1.8.4", "", {}, "sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/ejs": ["@types/ejs@3.1.5", "", {}, "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg=="], diff --git a/package.json b/package.json index 6685c42d..336d3f5f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@biomejs/biome": "^2.0.0", "@clack/prompts": "^0.11.0", "@types/bun": "^1.2.15", + "@types/common-tags": "^1.8.4", "@types/ejs": "^3.1.5", "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", From 773b51de5cefe2efef868f93c4f99ade4fc6347e Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:51:12 +0200 Subject: [PATCH 07/13] version check fixes --- src/cli/utils/version-check.ts | 3 +-- tests/cli/testkit/CLITestkit.ts | 11 +++++++++-- tests/cli/testkit/index.ts | 2 +- tests/cli/version-check.spec.ts | 4 +++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/cli/utils/version-check.ts b/src/cli/utils/version-check.ts index 5b9e207d..65e16b74 100644 --- a/src/cli/utils/version-check.ts +++ b/src/cli/utils/version-check.ts @@ -33,8 +33,7 @@ export async function checkForUpgrade(): Promise { return { currentVersion, latestVersion }; } return null; - } catch (error) { - console.log(error); + } catch { return null; } } diff --git a/tests/cli/testkit/CLITestkit.ts b/tests/cli/testkit/CLITestkit.ts index d32c8357..2b1b3d2a 100644 --- a/tests/cli/testkit/CLITestkit.ts +++ b/tests/cli/testkit/CLITestkit.ts @@ -36,7 +36,8 @@ export class CLITestkit { private cleanupFn: () => Promise; private env: Record = {}; private projectDir?: string; - private testOverrides: TestOverrides = {}; + // Default latestVersion to null to skip npm version check in tests + private testOverrides: TestOverrides = { latestVersion: null }; /** Typed API mock for Base44 endpoints */ readonly api: Base44APIMock; @@ -90,7 +91,13 @@ export class CLITestkit { await cp(fixturePath, this.projectDir, { recursive: true }); } - givenLatestVersion(version: string | null): void { + /** + * Set the latest version for upgrade check. + * - Pass a version string (e.g., "1.0.0") to simulate an upgrade available + * - Pass null to simulate no upgrade available (default) + * - Pass undefined to test the real npm version check (not recommended, makes network call) + */ + givenLatestVersion(version: string | null | undefined): void { this.testOverrides.latestVersion = version; } diff --git a/tests/cli/testkit/index.ts b/tests/cli/testkit/index.ts index 10669692..da6ea4f0 100644 --- a/tests/cli/testkit/index.ts +++ b/tests/cli/testkit/index.ts @@ -35,7 +35,7 @@ export interface TestContext { user?: { email: string; name: string } ) => Promise; - givenLatestVersion: (version: string | null) => void; + givenLatestVersion: (version: string | null | undefined) => void; // ─── WHEN METHODS ────────────────────────────────────────── diff --git a/tests/cli/version-check.spec.ts b/tests/cli/version-check.spec.ts index 964fa30b..90a99a5b 100644 --- a/tests/cli/version-check.spec.ts +++ b/tests/cli/version-check.spec.ts @@ -17,7 +17,7 @@ describe("upgrade notification", () => { }); it("does not display notification when version is current", async () => { - t.givenLatestVersion(null); + // latestVersion defaults to null (no upgrade available) await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); const result = await t.run("whoami"); @@ -27,6 +27,8 @@ describe("upgrade notification", () => { }); it("does not display notification when check is not overridden", async () => { + // Opt into real npm version check (default is null which skips it) + t.givenLatestVersion(undefined); await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); const result = await t.run("whoami"); From a138efdf284da7753d1b48dd9c6cca4141543460 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:59:02 +0200 Subject: [PATCH 08/13] align fixtures to look like projects --- tests/fixtures/basic/config.jsonc | 4 ---- .../{ => base44}/agents/first.json | 0 .../{ => base44}/agents/second.json | 0 .../{ => base44}/config.jsonc | 0 tests/fixtures/full-project/config.jsonc | 6 ------ .../fixtures/full-project/entities/task.json | 8 -------- .../functions/hello/function.jsonc | 4 ---- .../full-project/functions/hello/index.ts | 3 --- .../fixtures/invalid-agent/agents/broken.json | 4 ---- .../invalid-agent/{ => base44}/config.jsonc | 0 .../{ => base44}/config.jsonc | 0 tests/fixtures/invalid-entity/config.jsonc | 4 ---- .../invalid-entity/entities/broken.json | 4 ---- .../invalid-json/{ => base44}/config.jsonc | 0 .../no-app-config/{ => base44}/config.jsonc | 0 .../with-agents/agents/customer_support.json | 12 ------------ .../with-agents/agents/data_analyst.jsonc | 12 ------------ .../with-agents/agents/order_assistant.json | 11 ----------- tests/fixtures/with-agents/config.jsonc | 3 --- tests/fixtures/with-entities/config.jsonc | 3 --- .../with-entities/entities/customer.json | 15 --------------- .../with-entities/entities/product.json | 15 --------------- .../with-functions-and-entities/config.jsonc | 3 --- .../entities/order.json | 19 ------------------- .../functions/process-order/function.jsonc | 4 ---- .../functions/process-order/helper.ts | 3 --- .../functions/process-order/index.ts | 10 ---------- tests/fixtures/with-site/config.jsonc | 6 ------ 28 files changed, 153 deletions(-) delete mode 100644 tests/fixtures/basic/config.jsonc rename tests/fixtures/duplicate-agent-names/{ => base44}/agents/first.json (100%) rename tests/fixtures/duplicate-agent-names/{ => base44}/agents/second.json (100%) rename tests/fixtures/duplicate-agent-names/{ => base44}/config.jsonc (100%) delete mode 100644 tests/fixtures/full-project/config.jsonc delete mode 100644 tests/fixtures/full-project/entities/task.json delete mode 100644 tests/fixtures/full-project/functions/hello/function.jsonc delete mode 100644 tests/fixtures/full-project/functions/hello/index.ts delete mode 100644 tests/fixtures/invalid-agent/agents/broken.json rename tests/fixtures/invalid-agent/{ => base44}/config.jsonc (100%) rename tests/fixtures/invalid-config-schema/{ => base44}/config.jsonc (100%) delete mode 100644 tests/fixtures/invalid-entity/config.jsonc delete mode 100644 tests/fixtures/invalid-entity/entities/broken.json rename tests/fixtures/invalid-json/{ => base44}/config.jsonc (100%) rename tests/fixtures/no-app-config/{ => base44}/config.jsonc (100%) delete mode 100644 tests/fixtures/with-agents/agents/customer_support.json delete mode 100644 tests/fixtures/with-agents/agents/data_analyst.jsonc delete mode 100644 tests/fixtures/with-agents/agents/order_assistant.json delete mode 100644 tests/fixtures/with-agents/config.jsonc delete mode 100644 tests/fixtures/with-entities/config.jsonc delete mode 100644 tests/fixtures/with-entities/entities/customer.json delete mode 100644 tests/fixtures/with-entities/entities/product.json delete mode 100644 tests/fixtures/with-functions-and-entities/config.jsonc delete mode 100644 tests/fixtures/with-functions-and-entities/entities/order.json delete mode 100644 tests/fixtures/with-functions-and-entities/functions/process-order/function.jsonc delete mode 100644 tests/fixtures/with-functions-and-entities/functions/process-order/helper.ts delete mode 100644 tests/fixtures/with-functions-and-entities/functions/process-order/index.ts delete mode 100644 tests/fixtures/with-site/config.jsonc diff --git a/tests/fixtures/basic/config.jsonc b/tests/fixtures/basic/config.jsonc deleted file mode 100644 index 1c5d75ef..00000000 --- a/tests/fixtures/basic/config.jsonc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Basic Test Project", -} - diff --git a/tests/fixtures/duplicate-agent-names/agents/first.json b/tests/fixtures/duplicate-agent-names/base44/agents/first.json similarity index 100% rename from tests/fixtures/duplicate-agent-names/agents/first.json rename to tests/fixtures/duplicate-agent-names/base44/agents/first.json diff --git a/tests/fixtures/duplicate-agent-names/agents/second.json b/tests/fixtures/duplicate-agent-names/base44/agents/second.json similarity index 100% rename from tests/fixtures/duplicate-agent-names/agents/second.json rename to tests/fixtures/duplicate-agent-names/base44/agents/second.json diff --git a/tests/fixtures/duplicate-agent-names/config.jsonc b/tests/fixtures/duplicate-agent-names/base44/config.jsonc similarity index 100% rename from tests/fixtures/duplicate-agent-names/config.jsonc rename to tests/fixtures/duplicate-agent-names/base44/config.jsonc diff --git a/tests/fixtures/full-project/config.jsonc b/tests/fixtures/full-project/config.jsonc deleted file mode 100644 index 002205e2..00000000 --- a/tests/fixtures/full-project/config.jsonc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Full Project", - "site": { - "outputDirectory": "site-output" - } -} diff --git a/tests/fixtures/full-project/entities/task.json b/tests/fixtures/full-project/entities/task.json deleted file mode 100644 index 5a33687a..00000000 --- a/tests/fixtures/full-project/entities/task.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Task", - "type": "object", - "properties": { - "title": { "type": "string" }, - "description": { "type": "string" } - } -} diff --git a/tests/fixtures/full-project/functions/hello/function.jsonc b/tests/fixtures/full-project/functions/hello/function.jsonc deleted file mode 100644 index a8484502..00000000 --- a/tests/fixtures/full-project/functions/hello/function.jsonc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "hello", - "entry": "index.ts" -} diff --git a/tests/fixtures/full-project/functions/hello/index.ts b/tests/fixtures/full-project/functions/hello/index.ts deleted file mode 100644 index f3350963..00000000 --- a/tests/fixtures/full-project/functions/hello/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function hello() { - return "Hello, World!"; -} diff --git a/tests/fixtures/invalid-agent/agents/broken.json b/tests/fixtures/invalid-agent/agents/broken.json deleted file mode 100644 index bd75d157..00000000 --- a/tests/fixtures/invalid-agent/agents/broken.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "INVALID-NAME!", - "description": "" -} diff --git a/tests/fixtures/invalid-agent/config.jsonc b/tests/fixtures/invalid-agent/base44/config.jsonc similarity index 100% rename from tests/fixtures/invalid-agent/config.jsonc rename to tests/fixtures/invalid-agent/base44/config.jsonc diff --git a/tests/fixtures/invalid-config-schema/config.jsonc b/tests/fixtures/invalid-config-schema/base44/config.jsonc similarity index 100% rename from tests/fixtures/invalid-config-schema/config.jsonc rename to tests/fixtures/invalid-config-schema/base44/config.jsonc diff --git a/tests/fixtures/invalid-entity/config.jsonc b/tests/fixtures/invalid-entity/config.jsonc deleted file mode 100644 index ed56e88b..00000000 --- a/tests/fixtures/invalid-entity/config.jsonc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Invalid Entity Project", -} - diff --git a/tests/fixtures/invalid-entity/entities/broken.json b/tests/fixtures/invalid-entity/entities/broken.json deleted file mode 100644 index e1a8346a..00000000 --- a/tests/fixtures/invalid-entity/entities/broken.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "object", - "properties": {} -} diff --git a/tests/fixtures/invalid-json/config.jsonc b/tests/fixtures/invalid-json/base44/config.jsonc similarity index 100% rename from tests/fixtures/invalid-json/config.jsonc rename to tests/fixtures/invalid-json/base44/config.jsonc diff --git a/tests/fixtures/no-app-config/config.jsonc b/tests/fixtures/no-app-config/base44/config.jsonc similarity index 100% rename from tests/fixtures/no-app-config/config.jsonc rename to tests/fixtures/no-app-config/base44/config.jsonc diff --git a/tests/fixtures/with-agents/agents/customer_support.json b/tests/fixtures/with-agents/agents/customer_support.json deleted file mode 100644 index 72314e85..00000000 --- a/tests/fixtures/with-agents/agents/customer_support.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "customer_support", - "description": "A helpful customer support agent that assists users with common questions and issues", - "instructions": "You are a friendly customer support agent. Help users resolve their issues politely and efficiently. If you cannot help, escalate to a human agent. Always be professional and empathetic.", - "tool_configs": [ - { - "entity_name": "task", - "allowed_operations": ["read", "create", "update"] - } - ], - "whatsapp_greeting": "Hi! I'm your support assistant. How can I help you today?" -} diff --git a/tests/fixtures/with-agents/agents/data_analyst.jsonc b/tests/fixtures/with-agents/agents/data_analyst.jsonc deleted file mode 100644 index b694e056..00000000 --- a/tests/fixtures/with-agents/agents/data_analyst.jsonc +++ /dev/null @@ -1,12 +0,0 @@ -{ - // A simple agent with minimal config - "name": "data_analyst", - "description": "Analyzes task data and provides insights", - "instructions": "You are a data analyst. When asked about tasks, query the task entity and provide clear, actionable insights.", - "tool_configs": [ - { - "entity_name": "task", - "allowed_operations": ["read"] - } - ] -} diff --git a/tests/fixtures/with-agents/agents/order_assistant.json b/tests/fixtures/with-agents/agents/order_assistant.json deleted file mode 100644 index 67be67ea..00000000 --- a/tests/fixtures/with-agents/agents/order_assistant.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "order_assistant", - "description": "An agent that helps customers track and manage their tasks", - "instructions": "You help customers with task-related inquiries. You can look up task status and help manage tasks. Be concise and helpful.", - "tool_configs": [ - { - "entity_name": "task", - "allowed_operations": ["read", "update", "delete"] - } - ] -} diff --git a/tests/fixtures/with-agents/config.jsonc b/tests/fixtures/with-agents/config.jsonc deleted file mode 100644 index f588b78f..00000000 --- a/tests/fixtures/with-agents/config.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Agents Test Project" -} diff --git a/tests/fixtures/with-entities/config.jsonc b/tests/fixtures/with-entities/config.jsonc deleted file mode 100644 index 67e52750..00000000 --- a/tests/fixtures/with-entities/config.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Entities Test Project" -} diff --git a/tests/fixtures/with-entities/entities/customer.json b/tests/fixtures/with-entities/entities/customer.json deleted file mode 100644 index fddd9585..00000000 --- a/tests/fixtures/with-entities/entities/customer.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Customer", - "type": "object", - "properties": { - "company": { - "type": "string", - "description": "Company name" - }, - "contact": { - "type": "string", - "description": "Contact person" - } - }, - "required": ["company"] -} diff --git a/tests/fixtures/with-entities/entities/product.json b/tests/fixtures/with-entities/entities/product.json deleted file mode 100644 index 43b3828d..00000000 --- a/tests/fixtures/with-entities/entities/product.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Product", - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Product title" - }, - "price": { - "type": "number", - "description": "Product price" - } - }, - "required": ["title"] -} diff --git a/tests/fixtures/with-functions-and-entities/config.jsonc b/tests/fixtures/with-functions-and-entities/config.jsonc deleted file mode 100644 index 3ada9605..00000000 --- a/tests/fixtures/with-functions-and-entities/config.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Full Test Project" -} diff --git a/tests/fixtures/with-functions-and-entities/entities/order.json b/tests/fixtures/with-functions-and-entities/entities/order.json deleted file mode 100644 index 10baa54c..00000000 --- a/tests/fixtures/with-functions-and-entities/entities/order.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Order", - "type": "object", - "properties": { - "orderId": { - "type": "string", - "description": "Order ID" - }, - "status": { - "type": "string", - "description": "Order status" - }, - "total": { - "type": "number", - "description": "Order total" - } - }, - "required": ["orderId"] -} diff --git a/tests/fixtures/with-functions-and-entities/functions/process-order/function.jsonc b/tests/fixtures/with-functions-and-entities/functions/process-order/function.jsonc deleted file mode 100644 index ea0445a7..00000000 --- a/tests/fixtures/with-functions-and-entities/functions/process-order/function.jsonc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "process-order", - "entry": "index.ts" -} diff --git a/tests/fixtures/with-functions-and-entities/functions/process-order/helper.ts b/tests/fixtures/with-functions-and-entities/functions/process-order/helper.ts deleted file mode 100644 index 2a599770..00000000 --- a/tests/fixtures/with-functions-and-entities/functions/process-order/helper.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function foo(text: string) { - console.log(text) -} diff --git a/tests/fixtures/with-functions-and-entities/functions/process-order/index.ts b/tests/fixtures/with-functions-and-entities/functions/process-order/index.ts deleted file mode 100644 index 10191359..00000000 --- a/tests/fixtures/with-functions-and-entities/functions/process-order/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { foo } from "./helper.ts"; - - -Deno.serve(async (req: Request) => { - const body = await req.json(); - - const order = foo(body); - - return new Response(JSON.stringify({ success: true, order: body })); -}) diff --git a/tests/fixtures/with-site/config.jsonc b/tests/fixtures/with-site/config.jsonc deleted file mode 100644 index fb2f5a3a..00000000 --- a/tests/fixtures/with-site/config.jsonc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Site Test Project", - "site": { - "outputDirectory": "site-output" - } -} From 37edf483948f78afa90ea44d884583edabbbc163 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 16:08:34 +0200 Subject: [PATCH 09/13] updates test config for now --- tests/cli/testkit/CLITestkit.ts | 8 ++++---- vitest.config.ts | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/cli/testkit/CLITestkit.ts b/tests/cli/testkit/CLITestkit.ts index 2b1b3d2a..d395ac0c 100644 --- a/tests/cli/testkit/CLITestkit.ts +++ b/tests/cli/testkit/CLITestkit.ts @@ -105,9 +105,6 @@ export class CLITestkit { /** Execute CLI command */ async run(...args: string[]): Promise { - // Reset modules to clear any cached state (e.g., refreshPromise) - vi.resetModules(); - // Setup mocks this.setupCwdMock(); this.setupEnvOverrides(); @@ -127,7 +124,10 @@ export class CLITestkit { // Apply all API mocks before running this.api.apply(); - // Dynamic import after vi.resetModules() to get fresh module instances + // Reset module state to ensure test isolation + vi.resetModules(); + + // Import CLI module fresh after reset const { createProgram, CLIExitError } = (await import( DIST_INDEX_PATH )) as ProgramModule; diff --git a/vitest.config.ts b/vitest.config.ts index 710ad485..c1542fa0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,7 +6,9 @@ export default defineConfig({ environment: "node", globals: true, include: ["tests/**/*.spec.ts"], - testTimeout: 10000, + // 30s timeout to account for slow CI environments where module loading + // of the 25MB bundled CLI can take significant time + testTimeout: 30000, mockReset: true, silent: true, // Suppress stdout/stderr from tests (CLI output is very noisy) }, From 3bf5ab9a0d2997ed4c3a48e0fac84f6230ce60cf Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 16:16:19 +0200 Subject: [PATCH 10/13] added tests --- tests/cli/testkit/CLITestkit.ts | 27 +++- tests/cli/testkit/index.ts | 8 ++ tests/cli/types_generate.spec.ts | 132 ++++++++++++++++++ .../with-types-resources/base44/.app.jsonc | 4 + .../base44/agents/assistant.json | 5 + .../with-types-resources/base44/config.jsonc | 3 + .../base44/entities/user.json | 15 ++ .../base44/functions/hello/function.jsonc | 4 + .../base44/functions/hello/index.ts | 3 + .../with-types-resources/tsconfig.json | 6 + vitest.config.ts | 2 - 11 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 tests/cli/types_generate.spec.ts create mode 100644 tests/fixtures/with-types-resources/base44/.app.jsonc create mode 100644 tests/fixtures/with-types-resources/base44/agents/assistant.json create mode 100644 tests/fixtures/with-types-resources/base44/config.jsonc create mode 100644 tests/fixtures/with-types-resources/base44/entities/user.json create mode 100644 tests/fixtures/with-types-resources/base44/functions/hello/function.jsonc create mode 100644 tests/fixtures/with-types-resources/base44/functions/hello/index.ts create mode 100644 tests/fixtures/with-types-resources/tsconfig.json diff --git a/tests/cli/testkit/CLITestkit.ts b/tests/cli/testkit/CLITestkit.ts index d395ac0c..7b8ff032 100644 --- a/tests/cli/testkit/CLITestkit.ts +++ b/tests/cli/testkit/CLITestkit.ts @@ -1,4 +1,4 @@ -import { cp, mkdir, readFile, writeFile } from "node:fs/promises"; +import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { Command } from "commander"; @@ -267,6 +267,31 @@ export class CLITestkit { } } + /** Read a file from the project directory */ + async readProjectFile(relativePath: string): Promise { + if (!this.projectDir) { + throw new Error("No project set up. Call givenProject() first."); + } + try { + return await readFile(join(this.projectDir, relativePath), "utf-8"); + } catch { + return null; + } + } + + /** Check if a file exists in the project directory */ + async fileExists(relativePath: string): Promise { + if (!this.projectDir) { + throw new Error("No project set up. Call givenProject() first."); + } + try { + await access(join(this.projectDir, relativePath)); + return true; + } catch { + return false; + } + } + // ─── CLEANUP ────────────────────────────────────────────────── async cleanup(): Promise { diff --git a/tests/cli/testkit/index.ts b/tests/cli/testkit/index.ts index da6ea4f0..345334ac 100644 --- a/tests/cli/testkit/index.ts +++ b/tests/cli/testkit/index.ts @@ -50,6 +50,12 @@ export interface TestContext { /** Read the auth file (for login tests) */ readAuthFile: () => Promise | null>; + /** Read a file from the project directory */ + readProjectFile: (relativePath: string) => Promise; + + /** Check if a file exists in the project directory */ + fileExists: (relativePath: string) => Promise; + /** Get the temp directory path */ getTempDir: () => string; @@ -129,6 +135,8 @@ export function setupCLITests(): TestContext { // Then methods expectResult: (result) => getKit().expect(result), readAuthFile: () => getKit().readAuthFile(), + readProjectFile: (relativePath) => getKit().readProjectFile(relativePath), + fileExists: (relativePath) => getKit().fileExists(relativePath), getTempDir: () => getKit().getTempDir(), // API mocks diff --git a/tests/cli/types_generate.spec.ts b/tests/cli/types_generate.spec.ts new file mode 100644 index 00000000..c12b3a51 --- /dev/null +++ b/tests/cli/types_generate.spec.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from "vitest"; +import { fixture, setupCLITests } from "./testkit/index.js"; + +describe("types generate command", () => { + const t = setupCLITests(); + + it("generates types file with all resource types", async () => { + // Given a project with entities, agents, and functions + await t.givenLoggedInWithProject(fixture("with-types-resources")); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Generated"); + + // And the types file is created + const typesFileExists = await t.fileExists("base44/.types/types.d.ts"); + expect(typesFileExists).toBe(true); + + // And the file contains the expected content + const typesContent = await t.readProjectFile("base44/.types/types.d.ts"); + expect(typesContent).not.toBeNull(); + + // Contains the auto-generated header + expect(typesContent).toContain("Auto-generated by Base44 CLI"); + + // Contains the entity interface + expect(typesContent).toContain("interface User"); + expect(typesContent).toContain("email"); + + // Contains the EntityTypeRegistry with the entity mapping + expect(typesContent).toContain("EntityTypeRegistry"); + expect(typesContent).toContain("User: User"); + + // Contains the FunctionNameRegistry with the function name + expect(typesContent).toContain("FunctionNameRegistry"); + expect(typesContent).toContain("hello: true"); + + // Contains the AgentNameRegistry with the agent name + expect(typesContent).toContain("AgentNameRegistry"); + expect(typesContent).toContain("assistant: true"); + }); + + it("updates tsconfig.json to include types path", async () => { + // Given a project with tsconfig.json + await t.givenLoggedInWithProject(fixture("with-types-resources")); + + // Verify initial tsconfig doesn't have the types include + const initialTsconfig = await t.readProjectFile("tsconfig.json"); + expect(initialTsconfig).not.toBeNull(); + expect(initialTsconfig).not.toContain("base44/.types"); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("tsconfig.json"); + + // And tsconfig.json is updated with the types include + const updatedTsconfig = await t.readProjectFile("tsconfig.json"); + expect(updatedTsconfig).not.toBeNull(); + const tsconfigObj = JSON.parse(updatedTsconfig!); + expect(tsconfigObj.include).toContain("base44/.types/*.d.ts"); + }); + + it("handles empty project with no resources", async () => { + // Given an empty project (no entities, agents, or functions) + await t.givenLoggedInWithProject(fixture("basic")); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + + // And an empty template is generated + const typesContent = await t.readProjectFile("base44/.types/types.d.ts"); + expect(typesContent).not.toBeNull(); + expect(typesContent).toContain("No entities, functions, or agents found"); + }); + + it("skips tsconfig update if types path already included", async () => { + // Given a project with tsconfig.json + await t.givenLoggedInWithProject(fixture("with-types-resources")); + + // Run types generate first time + const firstResult = await t.run("types", "generate"); + t.expectResult(firstResult).toSucceed(); + + // Verify tsconfig was updated + const tsconfigAfterFirst = await t.readProjectFile("tsconfig.json"); + const firstTsconfigObj = JSON.parse(tsconfigAfterFirst!); + const includeCountAfterFirst = firstTsconfigObj.include.filter( + (p: string) => p === "base44/.types/*.d.ts" + ).length; + expect(includeCountAfterFirst).toBe(1); + + // Run types generate second time + const secondResult = await t.run("types", "generate"); + t.expectResult(secondResult).toSucceed(); + + // Verify tsconfig still has only one entry + const tsconfigAfterSecond = await t.readProjectFile("tsconfig.json"); + const secondTsconfigObj = JSON.parse(tsconfigAfterSecond!); + const includeCountAfterSecond = secondTsconfigObj.include.filter( + (p: string) => p === "base44/.types/*.d.ts" + ).length; + expect(includeCountAfterSecond).toBe(1); + }); + + it("works without tsconfig.json", async () => { + // Given a project without tsconfig.json (basic fixture doesn't have one) + await t.givenLoggedInWithProject(fixture("basic")); + + // Verify no tsconfig.json exists + const tsconfigExists = await t.fileExists("tsconfig.json"); + expect(tsconfigExists).toBe(false); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command succeeds + t.expectResult(result).toSucceed(); + + // And types file is still generated + const typesFileExists = await t.fileExists("base44/.types/types.d.ts"); + expect(typesFileExists).toBe(true); + }); +}); diff --git a/tests/fixtures/with-types-resources/base44/.app.jsonc b/tests/fixtures/with-types-resources/base44/.app.jsonc new file mode 100644 index 00000000..d7852426 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/.app.jsonc @@ -0,0 +1,4 @@ +// Base44 App Configuration +{ + "id": "test-app-id" +} diff --git a/tests/fixtures/with-types-resources/base44/agents/assistant.json b/tests/fixtures/with-types-resources/base44/agents/assistant.json new file mode 100644 index 00000000..39c248a6 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/agents/assistant.json @@ -0,0 +1,5 @@ +{ + "name": "assistant", + "description": "A helpful assistant agent", + "instructions": "You are a helpful assistant. Answer questions clearly and concisely." +} diff --git a/tests/fixtures/with-types-resources/base44/config.jsonc b/tests/fixtures/with-types-resources/base44/config.jsonc new file mode 100644 index 00000000..d37f7c24 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Types Test Project" +} diff --git a/tests/fixtures/with-types-resources/base44/entities/user.json b/tests/fixtures/with-types-resources/base44/entities/user.json new file mode 100644 index 00000000..73065b7d --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/entities/user.json @@ -0,0 +1,15 @@ +{ + "name": "User", + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address" + }, + "age": { + "type": "number", + "description": "User age" + } + }, + "required": ["email"] +} diff --git a/tests/fixtures/with-types-resources/base44/functions/hello/function.jsonc b/tests/fixtures/with-types-resources/base44/functions/hello/function.jsonc new file mode 100644 index 00000000..a8484502 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/functions/hello/function.jsonc @@ -0,0 +1,4 @@ +{ + "name": "hello", + "entry": "index.ts" +} diff --git a/tests/fixtures/with-types-resources/base44/functions/hello/index.ts b/tests/fixtures/with-types-resources/base44/functions/hello/index.ts new file mode 100644 index 00000000..f3350963 --- /dev/null +++ b/tests/fixtures/with-types-resources/base44/functions/hello/index.ts @@ -0,0 +1,3 @@ +export default function hello() { + return "Hello, World!"; +} diff --git a/tests/fixtures/with-types-resources/tsconfig.json b/tests/fixtures/with-types-resources/tsconfig.json new file mode 100644 index 00000000..419c2114 --- /dev/null +++ b/tests/fixtures/with-types-resources/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "ES2020" + }, + "include": ["src"] +} diff --git a/vitest.config.ts b/vitest.config.ts index c1542fa0..46a91b01 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,8 +6,6 @@ export default defineConfig({ environment: "node", globals: true, include: ["tests/**/*.spec.ts"], - // 30s timeout to account for slow CI environments where module loading - // of the 25MB bundled CLI can take significant time testTimeout: 30000, mockReset: true, silent: true, // Suppress stdout/stderr from tests (CLI output is very noisy) From 7562d539d8e5855f9a497ff92a8574cce4ae898b Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 17:07:15 +0200 Subject: [PATCH 11/13] simplify generation --- bun.lock | 1 + package.json | 1 + src/cli/commands/types/generate.ts | 18 +- src/core/errors.ts | 22 ++ src/core/resources/entity/schema.ts | 3 - src/core/types/generator.ts | 243 +++++++----------- src/core/types/index.ts | 5 +- src/core/types/template.ts | 80 ------ templates/backend-and-client/gitignore.ejs | 20 +- templates/backend-only/base44/gitignore.ejs | 7 +- tests/cli/types_generate.spec.ts | 15 ++ .../invalid-entity-schema/base44/.app.jsonc | 4 + .../invalid-entity-schema/base44/config.jsonc | 3 + .../base44/entities/broken.json | 10 + 14 files changed, 154 insertions(+), 278 deletions(-) delete mode 100644 src/core/types/template.ts create mode 100644 tests/fixtures/invalid-entity-schema/base44/.app.jsonc create mode 100644 tests/fixtures/invalid-entity-schema/base44/config.jsonc create mode 100644 tests/fixtures/invalid-entity-schema/base44/entities/broken.json diff --git a/bun.lock b/bun.lock index 676656d6..dc9a1c82 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@types/bun": "^1.2.15", "@types/common-tags": "^1.8.4", "@types/ejs": "^3.1.5", + "@types/json-schema": "^7.0.15", "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", "@types/tar": "^6.1.13", diff --git a/package.json b/package.json index 336d3f5f..7118d68a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/bun": "^1.2.15", "@types/common-tags": "^1.8.4", "@types/ejs": "^3.1.5", + "@types/json-schema": "^7.0.15", "@types/lodash.kebabcase": "^4.1.9", "@types/node": "^22.10.5", "@types/tar": "^6.1.13", diff --git a/src/cli/commands/types/generate.ts b/src/cli/commands/types/generate.ts index 0ed71fa5..983ca8ef 100644 --- a/src/cli/commands/types/generate.ts +++ b/src/cli/commands/types/generate.ts @@ -3,10 +3,7 @@ import type { CLIContext } from "@/cli/types.js"; import { runCommand, runTask } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { readProjectConfig } from "@/core/index.js"; -import { - generateBase44TypesFile, - updateProjectConfig, -} from "@/core/types/index.js"; +import { generateTypesFile, updateProjectConfig } from "@/core/types/index.js"; const TYPES_FILE_PATH = "base44/.types/types.d.ts"; @@ -14,20 +11,15 @@ async function generateTypesAction(): Promise { const { entities, functions, agents, project } = await readProjectConfig(); await runTask("Generating types", async () => { - await generateBase44TypesFile({ entities, functions, agents }); + await generateTypesFile({ entities, functions, agents }); }); - // Try to update project configuration const tsconfigUpdated = await updateProjectConfig(project.root); - if (tsconfigUpdated) { - return { - outroMessage: `Generated ${TYPES_FILE_PATH} and updated tsconfig.json`, - }; - } - return { - outroMessage: `Generated ${TYPES_FILE_PATH}`, + outroMessage: tsconfigUpdated + ? `Generated ${TYPES_FILE_PATH} and updated tsconfig.json` + : `Generated ${TYPES_FILE_PATH}`, }; } diff --git a/src/core/errors.ts b/src/core/errors.ts index 41a4e438..5c63711d 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -370,6 +370,28 @@ export class InternalError extends SystemError { } } +/** + * Thrown when type generation fails for an entity. + */ +export class TypeGenerationError extends SystemError { + readonly code = "TYPE_GENERATION_ERROR"; + readonly entityName?: string; + + constructor(message: string, entityName?: string, cause?: unknown) { + super(message, { + hints: [ + { + message: entityName + ? `Check the schema for entity "${entityName}"` + : "Check your entity schemas for errors", + }, + ], + cause: cause instanceof Error ? cause : undefined, + }); + this.entityName = entityName; + } +} + // ============================================================================ // Type Guards // ============================================================================ diff --git a/src/core/resources/entity/schema.ts b/src/core/resources/entity/schema.ts index 9876b0f0..99391776 100644 --- a/src/core/resources/entity/schema.ts +++ b/src/core/resources/entity/schema.ts @@ -107,7 +107,6 @@ const PropertyTypeSchema = z.enum([ "boolean", "array", "object", - "binary", ]); const StringFormatSchema = z.enum([ @@ -120,8 +119,6 @@ const StringFormatSchema = z.enum([ "ipv4", "ipv6", "uuid", - "file", - "regex", ]); const PropertyDefinitionSchema = z.object({ diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index 41260099..aa9ab06f 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -1,189 +1,124 @@ +import { source, stripIndent } from "common-tags"; import { compile } from "json-schema-to-typescript"; +import type { JSONSchema4 } from "json-schema"; import { getTypesOutputPath } from "@/core/config.js"; +import { TypeGenerationError } from "@/core/errors.js"; import { getAppConfig } from "@/core/project/app-config.js"; import type { AgentConfig } from "@/core/resources/agent/index.js"; import type { Entity } from "@/core/resources/entity/index.js"; import type { BackendFunction } from "@/core/resources/function/index.js"; import { writeFile } from "@/core/utils/fs.js"; -import { generateTypesFileContent } from "./template.js"; -/** - * Convert entity name to PascalCase (matching json-schema-to-typescript behavior). - * Examples: "orders" -> "Orders", "user_profile" -> "UserProfile" - */ -function toPascalCase(name: string): string { - return name - .split(/[-_\s]+/) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(""); -} - -/** - * Input for generating the Base44 types file. - */ -export interface GenerateBase44TypesInput { +export interface GenerateTypesInput { entities: Entity[]; functions: BackendFunction[]; agents: AgentConfig[]; } +const HEADER = stripIndent` + // Auto-generated by Base44 CLI - DO NOT EDIT + // Regenerate with: base44 types generate +`; + +const EMPTY_TEMPLATE = stripIndent` + // Auto-generated by Base44 CLI - DO NOT EDIT + // Regenerate with: base44 types + // + // No entities, functions, or agents found in project. + // Add resources to base44/entities/, base44/functions/, or base44/agents/ + // and run \`base44 types generate\` again. + + declare module '@base44/sdk' { + // No types to augment - add resources and regenerate + } +`; + /** - * Generate and write the types.d.ts file to /base44/.types/types.d.ts. + * Generate and write types.d.ts file. */ -export async function generateBase44TypesFile( - input: GenerateBase44TypesInput +export async function generateTypesFile( + input: GenerateTypesInput ): Promise { const { projectRoot } = getAppConfig(); - const content = await generateTypesFileContent(input); - const filePath = getTypesOutputPath(projectRoot); - await writeFile(filePath, content); -} - -/** - * Convert a CLI entity schema to JSON Schema format for json-schema-to-typescript. - */ -function entityToJsonSchema(entity: Entity): object { - const properties: Record = {}; - - for (const [propName, propDef] of Object.entries(entity.properties)) { - properties[propName] = propertyToJsonSchema(propDef); - } - - return { - type: "object", - title: entity.name, - description: entity.description, - properties, - required: entity.required ?? [], - additionalProperties: false, - }; + const content = await generateContent(input); + await writeFile(getTypesOutputPath(projectRoot), content); } -/** - * Convert a property definition to JSON Schema format. - */ -function propertyToJsonSchema(prop: Record): object { - const result: Record = {}; - - // Handle type - if (prop.type === "integer") { - result.type = "number"; - } else if (prop.type === "binary") { - result.type = "string"; - result.format = "binary"; - } else { - result.type = prop.type; - } - - // Handle description - if (prop.description) { - result.description = prop.description; - } - - // Handle enum - if (prop.enum && Array.isArray(prop.enum)) { - result.enum = prop.enum; - } +async function generateContent(input: GenerateTypesInput): Promise { + const { entities, functions, agents } = input; - // Handle array items - if (prop.type === "array" && prop.items) { - result.items = propertyToJsonSchema(prop.items as Record); + if (!entities.length && !functions.length && !agents.length) { + return EMPTY_TEMPLATE; } - // Handle nested object properties - if (prop.type === "object" && prop.properties) { - const nestedProps: Record = {}; - for (const [nestedName, nestedDef] of Object.entries( - prop.properties as Record> - )) { - nestedProps[nestedName] = propertyToJsonSchema(nestedDef); - } - result.properties = nestedProps; - if (prop.required) { - result.required = prop.required; - } - result.additionalProperties = false; - } + const entityInterfaces = await Promise.all( + entities.map((e) => compileEntity(e)) + ); - return result; + // Build registry entries + const registryEntries: [string, string[]][] = [ + [ + "EntityTypeRegistry", + entities.map((e) => `${e.name}: ${toPascalCase(e.name)};`), + ], + ["FunctionNameRegistry", functions.map((f) => `${f.name}: true;`)], + ["AgentNameRegistry", agents.map((a) => `${a.name}: true;`)], + ]; + + // Generate registries (only for non-empty entries) + const registries = registryEntries + .filter(([, entries]) => entries.length > 0) + .map(([name, entries]) => registry(name, entries)); + + return [ + HEADER, + entityInterfaces.join("\n\n"), + source` + declare module '@base44/sdk' { + ${registries.join("\n\n")} + } + `, + ] + .filter(Boolean) + .join("\n\n"); } -/** - * Generate a TypeScript interface for a single entity. - */ -export async function generateEntityInterface(entity: Entity): Promise { - const jsonSchema = entityToJsonSchema(entity); +async function compileEntity(entity: Entity): Promise { + const { name, ...schema } = entity; + + const jsonSchema = { + ...schema, + title: name, + additionalProperties: false, + } as JSONSchema4; - const ts = await compile( - jsonSchema as Parameters[0], - entity.name, - { + try { + const ts = await compile(jsonSchema, name, { bannerComment: "", additionalProperties: false, strictIndexSignatures: true, - enableConstEnums: false, - declareExternallyReferenced: false, - } - ); - - // Remove the export statement and clean up the output - // json-schema-to-typescript outputs "export interface Name {...}" - // We want just "export interface Name {...}" but formatted nicely - return ts.trim(); -} - -/** - * Generate all entity interfaces. - */ -export async function generateAllEntityInterfaces( - entities: Entity[] -): Promise { - if (entities.length === 0) { - return ""; - } - - const interfaces: string[] = []; - for (const entity of entities) { - const iface = await generateEntityInterface(entity); - interfaces.push(iface); - } - - return interfaces.join("\n\n"); -} - -/** - * Generate registry entries for entities. - */ -export function generateEntityRegistryEntries(entities: Entity[]): string { - if (entities.length === 0) { - return ""; + }); + return ts.trim(); + } catch (error) { + throw new TypeGenerationError( + `Failed to generate types for entity "${name}"`, + name, + error + ); } - - return entities - .map((e) => ` ${e.name}: ${toPascalCase(e.name)};`) - .join("\n"); } -/** - * Generate registry entries for function names. - */ -export function generateFunctionRegistryEntries( - functions: BackendFunction[] -): string { - if (functions.length === 0) { - return ""; - } - - return functions.map((f) => ` ${f.name}: true;`).join("\n"); +function registry(name: string, entries: string[]): string { + return source` + interface ${name} { + ${entries.join("\n ")} + } + `; } -/** - * Generate registry entries for agent names. - */ -export function generateAgentRegistryEntries(agents: AgentConfig[]): string { - if (agents.length === 0) { - return ""; - } - - return agents.map((a) => ` ${a.name}: true;`).join("\n"); +function toPascalCase(name: string): string { + return name + .split(/[-_\s]+/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); } diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 035c90d0..bb5182be 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -1,5 +1,2 @@ -export { - type GenerateBase44TypesInput, - generateBase44TypesFile, -} from "./generator.js"; +export { type GenerateTypesInput, generateTypesFile } from "./generator.js"; export { updateProjectConfig } from "./update-project.js"; diff --git a/src/core/types/template.ts b/src/core/types/template.ts deleted file mode 100644 index 6bfe728f..00000000 --- a/src/core/types/template.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { source, stripIndent } from "common-tags"; -import type { AgentConfig } from "@/core/resources/agent/index.js"; -import type { Entity } from "@/core/resources/entity/index.js"; -import type { BackendFunction } from "@/core/resources/function/index.js"; -import { - generateAgentRegistryEntries, - generateAllEntityInterfaces, - generateEntityRegistryEntries, - generateFunctionRegistryEntries, -} from "./generator.js"; - -export interface TemplateInput { - entities: Entity[]; - functions: BackendFunction[]; - agents: AgentConfig[]; -} - -const HEADER = stripIndent` - // Auto-generated by Base44 CLI - DO NOT EDIT - // Regenerate with: base44 types - // - // Setup: Add to tsconfig.json: - // { "include": ["src", "base44/.types"] } -`; - -const EMPTY_TEMPLATE = stripIndent` - // Auto-generated by Base44 CLI - DO NOT EDIT - // Regenerate with: base44 types - // - // No entities, functions, or agents found in project. - // Add resources to base44/entities/, base44/functions/, or base44/agents/ - // and run \`base44 types\` again. - - declare module '@base44/sdk' { - // No types to augment - add resources and regenerate - } -`; - -function registryInterface(name: string, entries: string): string { - return source` - interface ${name} { - ${entries} - } - `; -} - -export async function generateTypesFileContent( - input: TemplateInput -): Promise { - const { entities, functions, agents } = input; - - if (!entities.length && !functions.length && !agents.length) { - return EMPTY_TEMPLATE; - } - - // Build registries for each resource type that has items - const registryConfigs: [string, string][] = [ - ["EntityTypeRegistry", generateEntityRegistryEntries(entities)], - ["FunctionNameRegistry", generateFunctionRegistryEntries(functions)], - ["AgentNameRegistry", generateAgentRegistryEntries(agents)], - ]; - - const registries = registryConfigs - .filter(([, entries]) => entries) - .map(([name, entries]) => registryInterface(name, entries)); - - const entityInterfaces = await generateAllEntityInterfaces(entities); - - return [ - HEADER, - entityInterfaces, - source` - declare module '@base44/sdk' { - ${registries.join("\n\n")} - } - `, - ] - .filter(Boolean) - .join("\n\n"); -} diff --git a/templates/backend-and-client/gitignore.ejs b/templates/backend-and-client/gitignore.ejs index 959a7a6e..aa9a82b7 100644 --- a/templates/backend-and-client/gitignore.ejs +++ b/templates/backend-and-client/gitignore.ejs @@ -1,22 +1,6 @@ --- outputFileName: .gitignore --- -# Dependencies -node_modules -# Build -dist - -# Environment -.env -.env.* -*.local - -# Editor -.vscode -.idea -.DS_Store -*.swp - -# Base44 App Config -.app.json* +# Dependencies node_modules # Build dist # Environment .env .env.* *.local # +Editor .vscode .idea .DS_Store *.swp # Base44 .app.json* .types/ diff --git a/templates/backend-only/base44/gitignore.ejs b/templates/backend-only/base44/gitignore.ejs index 0378e42e..e246068f 100644 --- a/templates/backend-only/base44/gitignore.ejs +++ b/templates/backend-only/base44/gitignore.ejs @@ -1,10 +1,5 @@ --- outputFileName: .gitignore --- -# Environment -.env -.env.* -*.local -# Base44 App Config -.app.json* +# Environment .env .env.* *.local # Base44 .app.json* .types/ diff --git a/tests/cli/types_generate.spec.ts b/tests/cli/types_generate.spec.ts index c12b3a51..3b54d313 100644 --- a/tests/cli/types_generate.spec.ts +++ b/tests/cli/types_generate.spec.ts @@ -129,4 +129,19 @@ describe("types generate command", () => { const typesFileExists = await t.fileExists("base44/.types/types.d.ts"); expect(typesFileExists).toBe(true); }); + + it("fails with TypeGenerationError for invalid entity schema", async () => { + // Given a project with an invalid entity schema + await t.givenLoggedInWithProject(fixture("invalid-entity-schema")); + + // When running types generate + const result = await t.run("types", "generate"); + + // Then the command fails + t.expectResult(result).toFail(); + + // And the error message mentions the entity name + t.expectResult(result).toContain("Broken"); + t.expectResult(result).toContain("Failed to generate types"); + }); }); diff --git a/tests/fixtures/invalid-entity-schema/base44/.app.jsonc b/tests/fixtures/invalid-entity-schema/base44/.app.jsonc new file mode 100644 index 00000000..d7852426 --- /dev/null +++ b/tests/fixtures/invalid-entity-schema/base44/.app.jsonc @@ -0,0 +1,4 @@ +// Base44 App Configuration +{ + "id": "test-app-id" +} diff --git a/tests/fixtures/invalid-entity-schema/base44/config.jsonc b/tests/fixtures/invalid-entity-schema/base44/config.jsonc new file mode 100644 index 00000000..3d337ce3 --- /dev/null +++ b/tests/fixtures/invalid-entity-schema/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Invalid Entity Schema Project" +} diff --git a/tests/fixtures/invalid-entity-schema/base44/entities/broken.json b/tests/fixtures/invalid-entity-schema/base44/entities/broken.json new file mode 100644 index 00000000..bb99f155 --- /dev/null +++ b/tests/fixtures/invalid-entity-schema/base44/entities/broken.json @@ -0,0 +1,10 @@ +{ + "name": "Broken", + "type": "object", + "properties": { + "circular": { + "type": "object", + "$ref": "#/definitions/invalid" + } + } +} From fa3bc1ae8552f39e99cfd84489fcc6af02b05a69 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 17:13:05 +0200 Subject: [PATCH 12/13] small spacing issue --- src/core/types/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index aa9ab06f..755bec5a 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -111,7 +111,7 @@ async function compileEntity(entity: Entity): Promise { function registry(name: string, entries: string[]): string { return source` interface ${name} { - ${entries.join("\n ")} + ${entries.join("\n")} } `; } From ce95bb07050b5ba1da6cbe739af1282acc87f743 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 17:20:43 +0200 Subject: [PATCH 13/13] ignore --- AGENTS.md | 12 ++++++++++-- src/core/types/generator.ts | 2 +- templates/backend-and-client/gitignore.ejs | 21 +++++++++++++++++++-- templates/backend-only/base44/gitignore.ejs | 8 +++++++- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2e859835..43f242d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,10 @@ cli/ │ │ │ ├── api.ts # uploadSite() - reads archive, sends to API │ │ │ ├── deploy.ts # deploySite() - validates, creates tar.gz, uploads │ │ │ └── index.ts +│ │ ├── types/ # TypeScript type generation +│ │ │ ├── generator.ts # generateTypesFile() - creates types.d.ts +│ │ │ ├── update-project.ts # updateProjectConfig() - updates tsconfig.json +│ │ │ └── index.ts │ │ ├── utils/ │ │ │ ├── fs.ts # File system utilities │ │ │ └── index.ts @@ -116,8 +120,11 @@ cli/ │ │ │ └── push.ts │ │ ├── functions/ │ │ │ └── deploy.ts -│ │ └── site/ -│ │ └── deploy.ts +│ │ ├── site/ +│ │ │ └── deploy.ts +│ │ └── types/ +│ │ ├── index.ts # getTypesCommand(context) - parent command +│ │ └── generate.ts # getTypesGenerateCommand(context) factory │ ├── telemetry/ # Error reporting and telemetry │ │ ├── consts.ts # PostHog API key, env var names │ │ ├── posthog.ts # PostHog client singleton @@ -584,6 +591,7 @@ When an error is thrown, the CLI displays: | `FILE_NOT_FOUND` | `FileNotFoundError` | File doesn't exist | | `FILE_READ_ERROR` | `FileReadError` | Can't read/write file | | `INTERNAL_ERROR` | `InternalError` | Unexpected error | +| `TYPE_GENERATION_ERROR` | `TypeGenerationError` | Type generation failed for entity | ### CLIExitError (Special Case) diff --git a/src/core/types/generator.ts b/src/core/types/generator.ts index 755bec5a..2f9b59eb 100644 --- a/src/core/types/generator.ts +++ b/src/core/types/generator.ts @@ -1,6 +1,6 @@ import { source, stripIndent } from "common-tags"; -import { compile } from "json-schema-to-typescript"; import type { JSONSchema4 } from "json-schema"; +import { compile } from "json-schema-to-typescript"; import { getTypesOutputPath } from "@/core/config.js"; import { TypeGenerationError } from "@/core/errors.js"; import { getAppConfig } from "@/core/project/app-config.js"; diff --git a/templates/backend-and-client/gitignore.ejs b/templates/backend-and-client/gitignore.ejs index aa9a82b7..00d4323a 100644 --- a/templates/backend-and-client/gitignore.ejs +++ b/templates/backend-and-client/gitignore.ejs @@ -1,6 +1,23 @@ --- outputFileName: .gitignore --- +# Dependencies +node_modules -# Dependencies node_modules # Build dist # Environment .env .env.* *.local # -Editor .vscode .idea .DS_Store *.swp # Base44 .app.json* .types/ +# Build +dist + +# Environment +.env +.env.* +*.local + +# Editor +.vscode +.idea +.DS_Store +*.swp + +# Base44 +.app.json* +.types/ diff --git a/templates/backend-only/base44/gitignore.ejs b/templates/backend-only/base44/gitignore.ejs index e246068f..b8e4e2f9 100644 --- a/templates/backend-only/base44/gitignore.ejs +++ b/templates/backend-only/base44/gitignore.ejs @@ -1,5 +1,11 @@ --- outputFileName: .gitignore --- +# Environment +.env +.env.* +*.local -# Environment .env .env.* *.local # Base44 .app.json* .types/ +# Base44 +.app.json* +.types/