@@ -204,11 +204,110 @@ function toSnakeCase(str: string): string {
204204 return str . replace ( / [ - ] / g, '_' ) . replace ( / [ A - Z ] / g, c => `_${ c . toLowerCase ( ) } ` ) . replace ( / ^ _ / , '' ) ;
205205}
206206
207+ // ─── Field Type Mapping ─────────────────────────────────────────────
208+
209+ const FIELD_TYPE_MAP : Record < string , string > = {
210+ text : 'string' ,
211+ textarea : 'string' ,
212+ richtext : 'string' ,
213+ html : 'string' ,
214+ markdown : 'string' ,
215+ number : 'number' ,
216+ integer : 'number' ,
217+ currency : 'number' ,
218+ percent : 'number' ,
219+ boolean : 'boolean' ,
220+ date : 'string' ,
221+ datetime : 'string' ,
222+ time : 'string' ,
223+ email : 'string' ,
224+ phone : 'string' ,
225+ url : 'string' ,
226+ select : 'string' ,
227+ multiselect : 'string[]' ,
228+ lookup : 'string' ,
229+ master_detail : 'string' ,
230+ formula : 'unknown' ,
231+ autonumber : 'string' ,
232+ json : 'Record<string, unknown>' ,
233+ file : 'string' ,
234+ image : 'string' ,
235+ password : 'string' ,
236+ slug : 'string' ,
237+ uuid : 'string' ,
238+ ip_address : 'string' ,
239+ color : 'string' ,
240+ rating : 'number' ,
241+ geo_point : '{ lat: number; lng: number }' ,
242+ vector : 'number[]' ,
243+ encrypted : 'string' ,
244+ } ;
245+
246+ function fieldTypeToTs ( fieldType : string , multiple ?: boolean ) : string {
247+ const base = FIELD_TYPE_MAP [ fieldType ] || 'unknown' ;
248+ return multiple ? `${ base } []` : base ;
249+ }
250+
251+ function generateTypesFromConfig ( config : Record < string , unknown > ) : string {
252+ const lines : string [ ] = [
253+ '// Auto-generated by ObjectStack CLI — do not edit manually' ,
254+ `// Generated at ${ new Date ( ) . toISOString ( ) } ` ,
255+ '' ,
256+ "import type { Data } from '@objectstack/spec';" ,
257+ '' ,
258+ ] ;
259+
260+ // Extract objects from config (supports both top-level and nested)
261+ const objects : Record < string , unknown > [ ] = [ ] ;
262+ const rawObjects = ( config as any ) . objects ?? ( config as any ) . data ?. objects ?? { } ;
263+
264+ if ( Array . isArray ( rawObjects ) ) {
265+ objects . push ( ...rawObjects ) ;
266+ } else if ( typeof rawObjects === 'object' ) {
267+ for ( const val of Object . values ( rawObjects ) ) {
268+ if ( val && typeof val === 'object' ) objects . push ( val as Record < string , unknown > ) ;
269+ }
270+ }
271+
272+ if ( objects . length === 0 ) {
273+ lines . push ( '// No objects found in configuration' ) ;
274+ return lines . join ( '\n' ) + '\n' ;
275+ }
276+
277+ for ( const obj of objects ) {
278+ const name = String ( obj . name || 'unknown' ) ;
279+ const typeName = name
280+ . split ( '_' )
281+ . map ( ( w : string ) => w . charAt ( 0 ) . toUpperCase ( ) + w . slice ( 1 ) )
282+ . join ( '' ) ;
283+ const fields = ( obj . fields ?? { } ) as Record < string , Record < string , unknown > > ;
284+
285+ lines . push ( `/** ${ String ( obj . label || typeName ) } record type */` ) ;
286+ lines . push ( `export interface ${ typeName } Record {` ) ;
287+ lines . push ( ' id: string;' ) ;
288+
289+ for ( const [ fieldName , fieldDef ] of Object . entries ( fields ) ) {
290+ const fType = String ( fieldDef . type || 'text' ) ;
291+ const tsType = fieldTypeToTs ( fType , ! ! fieldDef . multiple ) ;
292+ const required = fieldDef . required ? '' : '?' ;
293+ if ( fieldDef . label ) {
294+ lines . push ( ` /** ${ fieldDef . label } */` ) ;
295+ }
296+ lines . push ( ` ${ fieldName } ${ required } : ${ tsType } ;` ) ;
297+ }
298+
299+ lines . push ( '}' ) ;
300+ lines . push ( '' ) ;
301+ }
302+
303+ return lines . join ( '\n' ) + '\n' ;
304+ }
305+
207306// ─── Command ────────────────────────────────────────────────────────
208307
209- export const generateCommand = new Command ( 'generate ' )
210- . alias ( 'g ' )
211- . description ( 'Generate metadata files (object, view, action, flow, agent, dashboard, app)' )
308+ const generateMetadataCommand = new Command ( 'metadata ' )
309+ . alias ( 'm ' )
310+ . description ( 'Generate metadata scaffold (object, view, action, flow, agent, dashboard, app)' )
212311 . argument ( '<type>' , 'Metadata type to generate' )
213312 . argument ( '<name>' , 'Name for the metadata (use kebab-case)' )
214313 . option ( '-d, --dir <directory>' , 'Target directory (overrides default)' )
@@ -297,3 +396,82 @@ export const generateCommand = new Command('generate')
297396 process . exit ( 1 ) ;
298397 }
299398 } ) ;
399+
400+ const generateTypesCommand = new Command ( 'types' )
401+ . description ( 'Generate TypeScript type definitions from ObjectStack configuration' )
402+ . argument ( '[config]' , 'Configuration file path' )
403+ . option ( '-o, --output <file>' , 'Output file path' , 'src/types/objectstack.d.ts' )
404+ . option ( '--dry-run' , 'Show what would be generated without writing files' )
405+ . action ( async ( configPath , options ) => {
406+ printHeader ( 'Generate Types' ) ;
407+
408+ try {
409+ const { loadConfig } = await import ( '../utils/config.js' ) ;
410+ printInfo ( 'Loading configuration...' ) ;
411+ const { config, absolutePath } = await loadConfig ( configPath ) ;
412+
413+ console . log ( ` ${ chalk . dim ( 'Config:' ) } ${ chalk . white ( absolutePath ) } ` ) ;
414+ console . log ( ` ${ chalk . dim ( 'Output:' ) } ${ chalk . white ( options . output ) } ` ) ;
415+ console . log ( '' ) ;
416+
417+ const content = generateTypesFromConfig ( config as Record < string , unknown > ) ;
418+
419+ if ( options . dryRun ) {
420+ printInfo ( 'Dry run — no files written' ) ;
421+ console . log ( '' ) ;
422+ for ( const line of content . split ( '\n' ) ) {
423+ console . log ( chalk . dim ( ` ${ line } ` ) ) ;
424+ }
425+ console . log ( '' ) ;
426+ return ;
427+ }
428+
429+ const outPath = path . resolve ( process . cwd ( ) , options . output ) ;
430+ const outDir = path . dirname ( outPath ) ;
431+ if ( ! fs . existsSync ( outDir ) ) {
432+ fs . mkdirSync ( outDir , { recursive : true } ) ;
433+ }
434+ fs . writeFileSync ( outPath , content ) ;
435+ printSuccess ( `Generated types at ${ options . output } ` ) ;
436+ console . log ( '' ) ;
437+
438+ } catch ( error : any ) {
439+ printError ( error . message || String ( error ) ) ;
440+ process . exit ( 1 ) ;
441+ }
442+ } ) ;
443+
444+ export const generateCommand = new Command ( 'generate' )
445+ . alias ( 'g' )
446+ . description ( 'Generate metadata files or TypeScript types' )
447+ . argument ( '[type]' , 'Metadata type to generate (object, view, action, flow, agent, dashboard, app)' )
448+ . argument ( '[name]' , 'Name for the metadata (use kebab-case)' )
449+ . option ( '-d, --dir <directory>' , 'Target directory (overrides default)' )
450+ . option ( '--dry-run' , 'Show what would be created without writing files' )
451+ . addCommand ( generateTypesCommand )
452+ . action ( async ( type : string | undefined , name : string | undefined , options ) => {
453+ if ( ! type ) {
454+ printHeader ( 'Generate' ) ;
455+ console . log ( chalk . bold ( ' Sub-commands:' ) ) ;
456+ console . log ( ` ${ chalk . cyan ( 'types' . padEnd ( 12 ) ) } Generate TypeScript type definitions from config` ) ;
457+ console . log ( '' ) ;
458+ console . log ( chalk . bold ( ' Metadata types:' ) ) ;
459+ for ( const [ key , gen ] of Object . entries ( GENERATORS ) ) {
460+ console . log ( ` ${ chalk . cyan ( key . padEnd ( 12 ) ) } ${ chalk . dim ( gen . description ) } ` ) ;
461+ }
462+ console . log ( '' ) ;
463+ console . log ( chalk . dim ( ' Usage: objectstack generate <type> <name>' ) ) ;
464+ console . log ( chalk . dim ( ' Usage: objectstack generate types [config]' ) ) ;
465+ return ;
466+ }
467+
468+ // Delegate to metadata command action
469+ if ( ! name ) {
470+ printError ( 'Missing required argument: <name>' ) ;
471+ console . log ( chalk . dim ( ' Usage: objectstack generate <type> <name>' ) ) ;
472+ process . exit ( 1 ) ;
473+ }
474+
475+ // Execute metadata generation inline
476+ await generateMetadataCommand . parseAsync ( [ type , name , ...process . argv . slice ( 4 ) ] , { from : 'user' } ) ;
477+ } ) ;
0 commit comments