@@ -44,14 +44,39 @@ var widgetListCmd = &cobra.Command{
4444 RunE : runWidgetList ,
4545}
4646
47+ var widgetInitCmd = & cobra.Command {
48+ Use : "init" ,
49+ Short : "Extract definitions for all project widgets" ,
50+ Long : `Scan the project's widgets/ directory, extract .def.json for each .mpk,
51+ and generate skill documentation in .claude/skills/widgets/.
52+
53+ This enables CREATE PAGE to use any project widget via the pluggable engine.` ,
54+ RunE : runWidgetInit ,
55+ }
56+
57+ var widgetDocsCmd = & cobra.Command {
58+ Use : "docs" ,
59+ Short : "Generate widget skill documentation" ,
60+ Long : `Generate per-widget markdown documentation in .claude/skills/widgets/ from .mpk definitions.` ,
61+ RunE : runWidgetDocs ,
62+ }
63+
4764func init () {
4865 widgetExtractCmd .Flags ().String ("mpk" , "" , "Path to .mpk widget package file" )
4966 widgetExtractCmd .Flags ().StringP ("output" , "o" , "" , "Output directory (default: .mxcli/widgets/)" )
5067 widgetExtractCmd .Flags ().String ("mdl-name" , "" , "Override the MDL keyword name (default: derived from widget name)" )
5168 widgetExtractCmd .MarkFlagRequired ("mpk" )
5269
70+ widgetInitCmd .Flags ().StringP ("project" , "p" , "" , "Path to .mpr project file" )
71+ widgetInitCmd .MarkFlagRequired ("project" )
72+
73+ widgetDocsCmd .Flags ().StringP ("project" , "p" , "" , "Path to .mpr project file" )
74+ widgetDocsCmd .MarkFlagRequired ("project" )
75+
5376 widgetCmd .AddCommand (widgetExtractCmd )
5477 widgetCmd .AddCommand (widgetListCmd )
78+ widgetCmd .AddCommand (widgetInitCmd )
79+ widgetCmd .AddCommand (widgetDocsCmd )
5580 rootCmd .AddCommand (widgetCmd )
5681}
5782
@@ -115,78 +140,277 @@ func deriveMDLName(widgetID string) string {
115140 return strings .ToUpper (name )
116141}
117142
118- // generateDefJSON creates a WidgetDefinition from an mpk.WidgetDefinition.
143+ // generateDefJSON creates a skeleton WidgetDefinition from an mpk.WidgetDefinition.
144+ // Properties are handled explicitly from MDL via the engine's explicit property pass,
145+ // so no propertyMappings or childSlots are generated here.
119146func generateDefJSON (mpkDef * mpk.WidgetDefinition , mdlName string ) * executor.WidgetDefinition {
147+ widgetKind := "custom"
148+ if mpkDef .IsPluggable {
149+ widgetKind = "pluggable"
150+ }
120151 def := & executor.WidgetDefinition {
121152 WidgetID : mpkDef .ID ,
122153 MDLName : mdlName ,
154+ WidgetKind : widgetKind ,
123155 TemplateFile : strings .ToLower (mdlName ) + ".json" ,
124156 DefaultEditable : "Always" ,
125157 }
126158
127- // Build property mappings by inferring operations from XML types
128- var mappings []executor.PropertyMapping
129- var childSlots []executor.ChildSlotMapping
130-
131- for _ , prop := range mpkDef .Properties {
132- normalizedType := mpk .NormalizeType (prop .Type )
133-
134- switch normalizedType {
159+ // Generate property mappings and child slots from MPK property definitions.
160+ // Two passes: datasource first (association depends on entityContext set by datasource).
161+ var assocMappings []executor.PropertyMapping
162+ for _ , p := range mpkDef .Properties {
163+ switch p .Type {
164+ case "widgets" :
165+ container := strings .ToUpper (p .Key )
166+ if p .Key == "content" {
167+ container = "TEMPLATE"
168+ }
169+ def .ChildSlots = append (def .ChildSlots , executor.ChildSlotMapping {
170+ PropertyKey : p .Key ,
171+ MDLContainer : container ,
172+ Operation : "widgets" ,
173+ })
174+ case "datasource" :
175+ def .PropertyMappings = append (def .PropertyMappings , executor.PropertyMapping {
176+ PropertyKey : p .Key ,
177+ Source : "DataSource" ,
178+ Operation : "datasource" ,
179+ })
135180 case "attribute" :
136- mappings = append (mappings , executor.PropertyMapping {
137- PropertyKey : prop .Key ,
181+ def . PropertyMappings = append (def . PropertyMappings , executor.PropertyMapping {
182+ PropertyKey : p .Key ,
138183 Source : "Attribute" ,
139184 Operation : "attribute" ,
140185 })
141186 case "association" :
142- mappings = append (mappings , executor.PropertyMapping {
143- PropertyKey : prop .Key ,
187+ assocMappings = append (assocMappings , executor.PropertyMapping {
188+ PropertyKey : p .Key ,
144189 Source : "Association" ,
145190 Operation : "association" ,
146191 })
147- case "datasource" :
148- mappings = append (mappings , executor.PropertyMapping {
149- PropertyKey : prop .Key ,
150- Source : "DataSource" ,
151- Operation : "datasource" ,
152- })
153- case "widgets" :
154- // Widgets properties become child slots
155- containerName := strings .ToUpper (prop .Key )
156- if containerName == "CONTENT" {
157- containerName = "TEMPLATE"
158- }
159- childSlots = append (childSlots , executor.ChildSlotMapping {
160- PropertyKey : prop .Key ,
161- MDLContainer : containerName ,
162- Operation : "widgets" ,
163- })
164192 case "selection" :
165- mappings = append (mappings , executor.PropertyMapping {
166- PropertyKey : prop .Key ,
193+ def . PropertyMappings = append (def . PropertyMappings , executor.PropertyMapping {
194+ PropertyKey : p .Key ,
167195 Source : "Selection" ,
168196 Operation : "selection" ,
169- Default : prop .DefaultValue ,
197+ Default : p .DefaultValue ,
170198 })
171- case "boolean" , "string " , "enumeration " , "integer " , "decimal " :
172- mapping := executor.PropertyMapping {
173- PropertyKey : prop .Key ,
199+ case "boolean" , "integer " , "decimal " , "string " , "enumeration " :
200+ m := executor.PropertyMapping {
201+ PropertyKey : p .Key ,
174202 Operation : "primitive" ,
175203 }
176- if prop .DefaultValue != "" {
177- mapping .Value = prop .DefaultValue
204+ if p .DefaultValue != "" {
205+ m .Value = p .DefaultValue
178206 }
179- mappings = append (mappings , mapping )
180- // Skip action, expression, textTemplate, object, icon, image, file — too complex for auto-mapping
207+ def .PropertyMappings = append (def .PropertyMappings , m )
181208 }
182209 }
183-
184- def .PropertyMappings = mappings
185- def .ChildSlots = childSlots
210+ // Append association mappings after datasource (association requires prior entityContext)
211+ def .PropertyMappings = append (def .PropertyMappings , assocMappings ... )
186212
187213 return def
188214}
189215
216+ func runWidgetInit (cmd * cobra.Command , args []string ) error {
217+ projectPath , _ := cmd .Flags ().GetString ("project" )
218+ projectDir := filepath .Dir (projectPath )
219+ widgetsDir := filepath .Join (projectDir , "widgets" )
220+ outputDir := filepath .Join (projectDir , ".mxcli" , "widgets" )
221+
222+ // Load built-in registry to skip widgets that already have hand-crafted definitions
223+ builtinRegistry , _ := executor .NewWidgetRegistry ()
224+
225+ // Scan widgets/ for .mpk files
226+ matches , err := filepath .Glob (filepath .Join (widgetsDir , "*.mpk" ))
227+ if err != nil {
228+ return fmt .Errorf ("failed to scan widgets directory: %w" , err )
229+ }
230+ if len (matches ) == 0 {
231+ fmt .Println ("No .mpk files found in widgets/ directory." )
232+ return nil
233+ }
234+
235+ if err := os .MkdirAll (outputDir , 0755 ); err != nil {
236+ return fmt .Errorf ("failed to create output directory: %w" , err )
237+ }
238+
239+ var extracted , skipped int
240+ for _ , mpkPath := range matches {
241+ mpkDef , err := mpk .ParseMPK (mpkPath )
242+ if err != nil {
243+ log .Printf ("warning: skipping %s: %v" , filepath .Base (mpkPath ), err )
244+ skipped ++
245+ continue
246+ }
247+
248+ mdlName := deriveMDLName (mpkDef .ID )
249+ filename := strings .ToLower (mdlName ) + ".def.json"
250+ outPath := filepath .Join (outputDir , filename )
251+
252+ // Skip widgets that have hand-crafted built-in definitions (e.g., COMBOBOX, GALLERY)
253+ if builtinRegistry != nil {
254+ if _ , ok := builtinRegistry .GetByWidgetID (mpkDef .ID ); ok {
255+ skipped ++
256+ continue
257+ }
258+ }
259+
260+ // Skip if already exists on disk
261+ if _ , err := os .Stat (outPath ); err == nil {
262+ skipped ++
263+ continue
264+ }
265+
266+ defJSON := generateDefJSON (mpkDef , mdlName )
267+ data , err := json .MarshalIndent (defJSON , "" , " " )
268+ if err != nil {
269+ log .Printf ("warning: skipping %s: %v" , mpkDef .ID , err )
270+ skipped ++
271+ continue
272+ }
273+ data = append (data , '\n' )
274+
275+ if err := os .WriteFile (outPath , data , 0644 ); err != nil {
276+ return fmt .Errorf ("failed to write %s: %w" , outPath , err )
277+ }
278+ kind := "custom"
279+ if mpkDef .IsPluggable {
280+ kind = "pluggable"
281+ }
282+ fmt .Printf (" %-12s %-20s %s\n " , kind , mdlName , mpkDef .ID )
283+ extracted ++
284+ }
285+
286+ fmt .Printf ("\n Extracted: %d, Skipped: %d (existing or unparseable)\n " , extracted , skipped )
287+
288+ // Also generate docs
289+ fmt .Println ("\n Generating widget documentation..." )
290+ return generateWidgetDocs (projectDir )
291+ }
292+
293+ func runWidgetDocs (cmd * cobra.Command , args []string ) error {
294+ projectPath , _ := cmd .Flags ().GetString ("project" )
295+ projectDir := filepath .Dir (projectPath )
296+ return generateWidgetDocs (projectDir )
297+ }
298+
299+ func generateWidgetDocs (projectDir string ) error {
300+ widgetsDir := filepath .Join (projectDir , "widgets" )
301+ docsDir := filepath .Join (projectDir , ".claude" , "skills" , "widgets" )
302+ // Also try .ai-context
303+ if _ , err := os .Stat (filepath .Join (projectDir , ".ai-context" )); err == nil {
304+ docsDir = filepath .Join (projectDir , ".ai-context" , "skills" , "widgets" )
305+ }
306+
307+ if err := os .MkdirAll (docsDir , 0755 ); err != nil {
308+ return fmt .Errorf ("failed to create docs directory: %w" , err )
309+ }
310+
311+ matches , err := filepath .Glob (filepath .Join (widgetsDir , "*.mpk" ))
312+ if err != nil {
313+ return fmt .Errorf ("failed to scan widgets directory: %w" , err )
314+ }
315+
316+ var generated int
317+ var indexEntries []string
318+
319+ for _ , mpkPath := range matches {
320+ mpkDef , err := mpk .ParseMPK (mpkPath )
321+ if err != nil {
322+ continue
323+ }
324+
325+ mdlName := deriveMDLName (mpkDef .ID )
326+ filename := strings .ToLower (mdlName ) + ".md"
327+ outPath := filepath .Join (docsDir , filename )
328+
329+ doc := generateWidgetDoc (mpkDef , mdlName )
330+
331+ if err := os .WriteFile (outPath , []byte (doc ), 0644 ); err != nil {
332+ log .Printf ("warning: failed to write %s: %v" , filename , err )
333+ continue
334+ }
335+
336+ kind := "CUSTOMWIDGET"
337+ if mpkDef .IsPluggable {
338+ kind = "PLUGGABLEWIDGET"
339+ }
340+ indexEntries = append (indexEntries , fmt .Sprintf ("| `%s` | %s | `%s` | %s | %d |" ,
341+ kind , mdlName , mpkDef .ID , mpkDef .Name , len (mpkDef .Properties )))
342+ generated ++
343+ }
344+
345+ // Write index
346+ var indexBuf strings.Builder
347+ indexBuf .WriteString ("# Available Widgets\n \n " )
348+ indexBuf .WriteString ("Generated by `mxcli widget docs`. See individual files for property details.\n \n " )
349+ indexBuf .WriteString ("| Prefix | Name | Widget ID | Display Name | Props |\n " )
350+ indexBuf .WriteString ("|--------|------|-----------|--------------|-------|\n " )
351+ for _ , entry := range indexEntries {
352+ indexBuf .WriteString (entry )
353+ indexBuf .WriteString ("\n " )
354+ }
355+ indexBuf .WriteString ("\n **Usage in MDL:**\n ```sql\n " )
356+ indexBuf .WriteString ("-- React pluggable widgets\n " )
357+ indexBuf .WriteString ("PLUGGABLEWIDGET 'com.mendix.widget.custom.badge.Badge' badge1\n \n " )
358+ indexBuf .WriteString ("-- Legacy custom widgets\n " )
359+ indexBuf .WriteString ("CUSTOMWIDGET 'com.company.OldWidget' legacy1\n " )
360+ indexBuf .WriteString ("```\n " )
361+
362+ indexPath := filepath .Join (docsDir , "_index.md" )
363+ if err := os .WriteFile (indexPath , []byte (indexBuf .String ()), 0644 ); err != nil {
364+ return fmt .Errorf ("failed to write index: %w" , err )
365+ }
366+
367+ fmt .Printf ("Generated %d widget docs in %s\n " , generated , docsDir )
368+ return nil
369+ }
370+
371+ func generateWidgetDoc (mpkDef * mpk.WidgetDefinition , mdlName string ) string {
372+ var buf strings.Builder
373+
374+ prefix := "CUSTOMWIDGET"
375+ if mpkDef .IsPluggable {
376+ prefix = "PLUGGABLEWIDGET"
377+ }
378+
379+ buf .WriteString (fmt .Sprintf ("# %s\n \n " , mpkDef .Name ))
380+ buf .WriteString (fmt .Sprintf ("- **Widget ID:** `%s`\n " , mpkDef .ID ))
381+ buf .WriteString (fmt .Sprintf ("- **Type:** %s\n " , prefix ))
382+ buf .WriteString (fmt .Sprintf ("- **Version:** %s\n \n " , mpkDef .Version ))
383+
384+ buf .WriteString ("## MDL Example\n \n ```sql\n " )
385+ buf .WriteString (fmt .Sprintf ("%s '%s' widget1\n " , prefix , mpkDef .ID ))
386+ buf .WriteString ("```\n \n " )
387+
388+ if len (mpkDef .Properties ) > 0 {
389+ buf .WriteString ("## Properties\n \n " )
390+ buf .WriteString ("| Property | Type | Required | Default | Description |\n " )
391+ buf .WriteString ("|----------|------|----------|---------|-------------|\n " )
392+
393+ for _ , prop := range mpkDef .Properties {
394+ if prop .IsSystem {
395+ continue
396+ }
397+ req := ""
398+ if prop .Required {
399+ req = "Yes"
400+ }
401+ desc := prop .Description
402+ if len (desc ) > 80 {
403+ desc = desc [:77 ] + "..."
404+ }
405+ buf .WriteString (fmt .Sprintf ("| `%s` | %s | %s | %s | %s |\n " ,
406+ prop .Key , prop .Type , req , prop .DefaultValue , desc ))
407+ }
408+ }
409+
410+ buf .WriteString ("\n " )
411+ return buf .String ()
412+ }
413+
190414func runWidgetList (cmd * cobra.Command , args []string ) error {
191415 registry , err := executor .NewWidgetRegistry ()
192416 if err != nil {
@@ -207,10 +431,14 @@ func runWidgetList(cmd *cobra.Command, args []string) error {
207431 return nil
208432 }
209433
210- fmt .Printf ("%-20s %-50s %s\n " , "MDL Name" , "Widget ID" , "Template" )
211- fmt .Printf ("%-20s %-50s %s\n " , strings .Repeat ("-" , 20 ), strings .Repeat ("-" , 50 ), strings .Repeat ("-" , 20 ))
434+ fmt .Printf ("%-16s %- 20s %-50s %s\n " , "Kind " , "MDL Name" , "Widget ID" , "Template" )
435+ fmt .Printf ("%-16s %- 20s %-50s %s\n " , strings . Repeat ( "-" , 16 ) , strings .Repeat ("-" , 20 ), strings .Repeat ("-" , 50 ), strings .Repeat ("-" , 20 ))
212436 for _ , def := range defs {
213- fmt .Printf ("%-20s %-50s %s\n " , def .MDLName , def .WidgetID , def .TemplateFile )
437+ kind := def .WidgetKind
438+ if kind == "" {
439+ kind = "pluggable"
440+ }
441+ fmt .Printf ("%-16s %-20s %-50s %s\n " , kind , def .MDLName , def .WidgetID , def .TemplateFile )
214442 }
215443 fmt .Printf ("\n Total: %d definitions\n " , len (defs ))
216444
0 commit comments