Skip to content
Next Next commit
basic config parsing
  • Loading branch information
kfirstri committed Jan 6, 2026
commit 0ef176aa02eceacb4d6d316b7942d60d7e1a657e
20 changes: 20 additions & 0 deletions src/cli/commands/project/show-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Command } from "commander";
import { spinner, log } from "@clack/prompts";
import { readProjectConfig } from "../../../core/config/project.js";
import { runCommand } from "../../utils/index.js";

async function showProject(): Promise<void> {
const s = spinner();
s.start("Reading project configuration");

const projectData = await readProjectConfig();
s.stop("Project configuration loaded");
const jsonOutput = JSON.stringify(projectData, null, 2);
log.info(jsonOutput);
}

export const showProjectCommand = new Command("show-project")
.description("Display project configuration, entities, and functions")
.action(async () => {
await runCommand(showProject);
});
4 changes: 4 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getPackageVersion } from './utils/index.js';
import { loginCommand } from './commands/auth/login.js';
import { whoamiCommand } from './commands/auth/whoami.js';
import { logoutCommand } from './commands/auth/logout.js';
import { showProjectCommand } from './commands/project/show-project.js';

const program = new Command();

Expand All @@ -18,6 +19,9 @@ program.addCommand(loginCommand);
program.addCommand(whoamiCommand);
program.addCommand(logoutCommand);

// Register project commands
program.addCommand(showProjectCommand);

// Parse command line arguments
program.parse();

3 changes: 3 additions & 0 deletions src/core/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ export const BASE44_DIR = join(homedir(), '.base44');
export const AUTH_DIR = join(BASE44_DIR, 'auth');
export const AUTH_FILE_PATH = join(AUTH_DIR, 'auth.json');

export const PROJECT_CONFIG_FILE = 'base44.config.json';
export const FUNCTION_CONFIG_FILE = 'function.json';

87 changes: 87 additions & 0 deletions src/core/config/entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { join } from 'path';
import { readdir } from 'fs/promises';
import { EntitySchema, type Entity } from '../schemas/entity.js';
import { readJsonFile, fileExists } from '../utils/fs.js';

export async function readEntityFile(entityPath: string): Promise<Entity> {
if (!fileExists(entityPath)) {
throw new Error(`Entity file not found: ${entityPath}`);
}

try {
const parsed = await readJsonFile(entityPath);
const result = EntitySchema.safeParse(parsed);

if (!result.success) {
throw new Error(
`Invalid entity configuration in ${entityPath}: ${result.error.issues
.map((e) => e.message)
.join(', ')}`
);
}

return result.data;
} catch (error) {
if (error instanceof Error && error.message.includes('Invalid entity')) {
throw error;
}
if (error instanceof Error && error.message.includes('File not found')) {
throw error;
}
throw new Error(
`Failed to read entity file ${entityPath}: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}

export async function readAllEntities(entitiesDir: string): Promise<Entity[]> {
if (!fileExists(entitiesDir)) {
throw new Error(`Entities directory not found: ${entitiesDir}`);
}

try {
const files = await readdir(entitiesDir);
const jsonFiles = files.filter((file) => file.endsWith('.json'));

const entities: Entity[] = [];
const errors: string[] = [];

for (const file of jsonFiles) {
const filePath = join(entitiesDir, file);
try {
const entity = await readEntityFile(filePath);
entities.push(entity);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
errors.push(`${file}: ${errorMessage}`);
}
}

if (errors.length > 0 && entities.length === 0) {
throw new Error(
`Failed to read any entity files:\n${errors.join('\n')}`
);
}

if (errors.length > 0) {
console.warn(
`Warning: Some entity files could not be read:\n${errors.join('\n')}`
);
}

return entities;
} catch (error) {
if (error instanceof Error && error.message.includes('Failed to read')) {
throw error;
}
throw new Error(
`Failed to read entities directory ${entitiesDir}: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}

97 changes: 97 additions & 0 deletions src/core/config/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { join } from 'path';
import { readdir, stat } from 'fs/promises';
import { FunctionConfigSchema, type FunctionConfig } from '../schemas/function.js';
import { FUNCTION_CONFIG_FILE } from './constants.js';
import { readJsonFile, fileExists } from '../utils/fs.js';

export async function readFunctionConfig(
functionDir: string
): Promise<FunctionConfig> {
const configPath = join(functionDir, FUNCTION_CONFIG_FILE);

if (!fileExists(configPath)) {
throw new Error(
`Function configuration file not found: ${configPath}. Please ensure ${FUNCTION_CONFIG_FILE} exists in the function directory.`
);
}

try {
const parsed = await readJsonFile(configPath);
const result = FunctionConfigSchema.safeParse(parsed);

if (!result.success) {
throw new Error(
`Invalid function configuration in ${configPath}: ${result.error.issues
.map((e) => e.message)
.join(', ')}`
);
}

return result.data;
} catch (error) {
if (error instanceof Error && error.message.includes('Invalid function')) {
throw error;
}
if (error instanceof Error && error.message.includes('File not found')) {
throw error;
}
throw new Error(
`Failed to read function configuration ${configPath}: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}

export async function readAllFunctions(
functionsDir: string
): Promise<FunctionConfig[]> {
if (!fileExists(functionsDir)) {
throw new Error(`Functions directory not found: ${functionsDir}`);
}

try {
const entries = await readdir(functionsDir);
const functionConfigs: FunctionConfig[] = [];
const errors: string[] = [];

for (const entry of entries) {
const entryPath = join(functionsDir, entry);
try {
const stats = await stat(entryPath);
if (stats.isDirectory()) {
const functionConfig = await readFunctionConfig(entryPath);
functionConfigs.push(functionConfig);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
errors.push(`${entry}: ${errorMessage}`);
}
}

if (errors.length > 0 && functionConfigs.length === 0) {
throw new Error(
`Failed to read any function configurations:\n${errors.join('\n')}`
);
}

if (errors.length > 0) {
console.warn(
`Warning: Some function directories could not be read:\n${errors.join('\n')}`
);
}

return functionConfigs;
} catch (error) {
if (error instanceof Error && error.message.includes('Failed to read')) {
throw error;
}
throw new Error(
`Failed to read functions directory ${functionsDir}: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}

3 changes: 3 additions & 0 deletions src/core/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './constants.js';
export * from './auth.js';
export * from './project.js';
export * from './entities.js';
export * from './functions.js';

115 changes: 115 additions & 0 deletions src/core/config/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { join, dirname } from 'path';
import { ProjectConfigSchema, type ProjectConfig } from '../schemas/project.js';
import { type Entity } from '../schemas/entity.js';
import { type FunctionConfig } from '../schemas/function.js';
import { PROJECT_CONFIG_FILE } from './constants.js';
import { readJsonFile, fileExists } from '../utils/fs.js';
import { readAllEntities } from './entities.js';
import { readAllFunctions } from './functions.js';

export function findProjectRoot(startPath?: string): string | null {
const start = startPath || process.cwd();
let current = start;

while (current !== dirname(current)) {
const configPath = join(current, PROJECT_CONFIG_FILE);
if (fileExists(configPath)) {
return current;
}
current = dirname(current);
}

return null;
}

export interface ProjectData {
project: ProjectConfig;
entities: Entity[];
functions: FunctionConfig[];
}

export async function readProjectConfig(
projectRoot?: string
): Promise<ProjectData> {
const root = projectRoot || findProjectRoot();

if (!root) {
throw new Error(
`Project root not found. Please ensure ${PROJECT_CONFIG_FILE} exists in the project directory.`
);
}

const configPath = join(root, PROJECT_CONFIG_FILE);

try {
const parsed = await readJsonFile(configPath);
const result = ProjectConfigSchema.safeParse(parsed);

if (!result.success) {
throw new Error(
`Invalid project configuration: ${result.error.issues
.map((e) => e.message)
.join(', ')}`
);
}

const project = result.data;
const entitiesPath = join(root, project.entitySrc);
const functionsPath = join(root, project.functionSrc);

const [entities, functions] = await Promise.allSettled([
fileExists(entitiesPath)
? readAllEntities(entitiesPath)
: Promise.resolve([]),
fileExists(functionsPath)
? readAllFunctions(functionsPath)
: Promise.resolve([]),
]);

const entitiesData =
entities.status === 'fulfilled' ? entities.value : [];
const functionsData =
functions.status === 'fulfilled' ? functions.value : [];

if (entities.status === 'rejected' && fileExists(entitiesPath)) {
throw new Error(
`Failed to read entities: ${
entities.reason instanceof Error
? entities.reason.message
: 'Unknown error'
}`
);
}

if (functions.status === 'rejected' && fileExists(functionsPath)) {
throw new Error(
`Failed to read functions: ${
functions.reason instanceof Error
? functions.reason.message
: 'Unknown error'
}`
);
}

return {
project,
entities: entitiesData,
functions: functionsData,
};
} catch (error) {
if (error instanceof Error && error.message.includes('Invalid project')) {
throw error;
}
if (error instanceof Error && error.message.includes('File not found')) {
throw new Error(
`Project configuration file not found: ${configPath}. Please ensure ${PROJECT_CONFIG_FILE} exists.`
);
}
throw new Error(
`Failed to read project configuration: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}

16 changes: 16 additions & 0 deletions src/core/schemas/entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod';

const ColumnSchema = z.looseObject({
name: z.string().min(1, 'Column name cannot be empty'),
type: z.string().min(1, 'Column type cannot be empty'),
})

export const EntitySchema = z.looseObject({
id: z.string().min(1, 'Entity ID cannot be empty'),
name: z.string().min(1, 'Entity name cannot be empty'),
columns: z.array(ColumnSchema).min(0),
})

export type Entity = z.infer<typeof EntitySchema>;
export type EntityColumn = z.infer<typeof ColumnSchema>;

12 changes: 12 additions & 0 deletions src/core/schemas/function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from 'zod';

export const FunctionConfigSchema = z.looseObject({
id: z.string().min(1, 'Function ID cannot be empty'),
path: z.string().min(1, 'Function path cannot be empty'),
enabled: z.boolean().default(true),
triggers: z.array(z.string()).optional(),
permissions: z.array(z.string()).optional(),
});

export type FunctionConfig = z.infer<typeof FunctionConfigSchema>;

3 changes: 3 additions & 0 deletions src/core/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from './auth.js';
export * from './project.js';
export * from './entity.js';
export * from './function.js';

Loading