@@ -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,76 +140,219 @@ 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 {
120- def := & executor.WidgetDefinition {
147+ widgetKind := "custom"
148+ if mpkDef .IsPluggable {
149+ widgetKind = "pluggable"
150+ }
151+ return & executor.WidgetDefinition {
121152 WidgetID : mpkDef .ID ,
122153 MDLName : mdlName ,
154+ WidgetKind : widgetKind ,
123155 TemplateFile : strings .ToLower (mdlName ) + ".json" ,
124156 DefaultEditable : "Always" ,
125157 }
158+ }
159+
160+ func runWidgetInit (cmd * cobra.Command , args []string ) error {
161+ projectPath , _ := cmd .Flags ().GetString ("project" )
162+ projectDir := filepath .Dir (projectPath )
163+ widgetsDir := filepath .Join (projectDir , "widgets" )
164+ outputDir := filepath .Join (projectDir , ".mxcli" , "widgets" )
165+
166+ // Load built-in registry to skip widgets that already have hand-crafted definitions
167+ builtinRegistry , _ := executor .NewWidgetRegistry ()
168+
169+ // Scan widgets/ for .mpk files
170+ matches , err := filepath .Glob (filepath .Join (widgetsDir , "*.mpk" ))
171+ if err != nil {
172+ return fmt .Errorf ("failed to scan widgets directory: %w" , err )
173+ }
174+ if len (matches ) == 0 {
175+ fmt .Println ("No .mpk files found in widgets/ directory." )
176+ return nil
177+ }
178+
179+ if err := os .MkdirAll (outputDir , 0755 ); err != nil {
180+ return fmt .Errorf ("failed to create output directory: %w" , err )
181+ }
182+
183+ var extracted , skipped int
184+ for _ , mpkPath := range matches {
185+ mpkDef , err := mpk .ParseMPK (mpkPath )
186+ if err != nil {
187+ log .Printf ("warning: skipping %s: %v" , filepath .Base (mpkPath ), err )
188+ skipped ++
189+ continue
190+ }
191+
192+ mdlName := deriveMDLName (mpkDef .ID )
193+ filename := strings .ToLower (mdlName ) + ".def.json"
194+ outPath := filepath .Join (outputDir , filename )
126195
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 {
135- case "attribute" :
136- mappings = append (mappings , executor.PropertyMapping {
137- PropertyKey : prop .Key ,
138- Source : "Attribute" ,
139- Operation : "attribute" ,
140- })
141- case "association" :
142- mappings = append (mappings , executor.PropertyMapping {
143- PropertyKey : prop .Key ,
144- Source : "Association" ,
145- Operation : "association" ,
146- })
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"
196+ // Skip widgets that have hand-crafted built-in definitions (e.g., COMBOBOX, GALLERY)
197+ if builtinRegistry != nil {
198+ if _ , ok := builtinRegistry .GetByWidgetID (mpkDef .ID ); ok {
199+ skipped ++
200+ continue
158201 }
159- childSlots = append (childSlots , executor.ChildSlotMapping {
160- PropertyKey : prop .Key ,
161- MDLContainer : containerName ,
162- Operation : "widgets" ,
163- })
164- case "selection" :
165- mappings = append (mappings , executor.PropertyMapping {
166- PropertyKey : prop .Key ,
167- Source : "Selection" ,
168- Operation : "selection" ,
169- Default : prop .DefaultValue ,
170- })
171- case "boolean" , "string" , "enumeration" , "integer" , "decimal" :
172- mapping := executor.PropertyMapping {
173- PropertyKey : prop .Key ,
174- Operation : "primitive" ,
202+ }
203+
204+ // Skip if already exists on disk
205+ if _ , err := os .Stat (outPath ); err == nil {
206+ skipped ++
207+ continue
208+ }
209+
210+ defJSON := generateDefJSON (mpkDef , mdlName )
211+ data , err := json .MarshalIndent (defJSON , "" , " " )
212+ if err != nil {
213+ log .Printf ("warning: skipping %s: %v" , mpkDef .ID , err )
214+ skipped ++
215+ continue
216+ }
217+ data = append (data , '\n' )
218+
219+ if err := os .WriteFile (outPath , data , 0644 ); err != nil {
220+ return fmt .Errorf ("failed to write %s: %w" , outPath , err )
221+ }
222+ kind := "custom"
223+ if mpkDef .IsPluggable {
224+ kind = "pluggable"
225+ }
226+ fmt .Printf (" %-12s %-20s %s\n " , kind , mdlName , mpkDef .ID )
227+ extracted ++
228+ }
229+
230+ fmt .Printf ("\n Extracted: %d, Skipped: %d (existing or unparseable)\n " , extracted , skipped )
231+
232+ // Also generate docs
233+ fmt .Println ("\n Generating widget documentation..." )
234+ return generateWidgetDocs (projectDir )
235+ }
236+
237+ func runWidgetDocs (cmd * cobra.Command , args []string ) error {
238+ projectPath , _ := cmd .Flags ().GetString ("project" )
239+ projectDir := filepath .Dir (projectPath )
240+ return generateWidgetDocs (projectDir )
241+ }
242+
243+ func generateWidgetDocs (projectDir string ) error {
244+ widgetsDir := filepath .Join (projectDir , "widgets" )
245+ docsDir := filepath .Join (projectDir , ".claude" , "skills" , "widgets" )
246+ // Also try .ai-context
247+ if _ , err := os .Stat (filepath .Join (projectDir , ".ai-context" )); err == nil {
248+ docsDir = filepath .Join (projectDir , ".ai-context" , "skills" , "widgets" )
249+ }
250+
251+ if err := os .MkdirAll (docsDir , 0755 ); err != nil {
252+ return fmt .Errorf ("failed to create docs directory: %w" , err )
253+ }
254+
255+ matches , err := filepath .Glob (filepath .Join (widgetsDir , "*.mpk" ))
256+ if err != nil {
257+ return fmt .Errorf ("failed to scan widgets directory: %w" , err )
258+ }
259+
260+ var generated int
261+ var indexEntries []string
262+
263+ for _ , mpkPath := range matches {
264+ mpkDef , err := mpk .ParseMPK (mpkPath )
265+ if err != nil {
266+ continue
267+ }
268+
269+ mdlName := deriveMDLName (mpkDef .ID )
270+ filename := strings .ToLower (mdlName ) + ".md"
271+ outPath := filepath .Join (docsDir , filename )
272+
273+ doc := generateWidgetDoc (mpkDef , mdlName )
274+
275+ if err := os .WriteFile (outPath , []byte (doc ), 0644 ); err != nil {
276+ log .Printf ("warning: failed to write %s: %v" , filename , err )
277+ continue
278+ }
279+
280+ kind := "CUSTOMWIDGET"
281+ if mpkDef .IsPluggable {
282+ kind = "PLUGGABLEWIDGET"
283+ }
284+ indexEntries = append (indexEntries , fmt .Sprintf ("| `%s` | %s | `%s` | %s | %d |" ,
285+ kind , mdlName , mpkDef .ID , mpkDef .Name , len (mpkDef .Properties )))
286+ generated ++
287+ }
288+
289+ // Write index
290+ var indexBuf strings.Builder
291+ indexBuf .WriteString ("# Available Widgets\n \n " )
292+ indexBuf .WriteString ("Generated by `mxcli widget docs`. See individual files for property details.\n \n " )
293+ indexBuf .WriteString ("| Prefix | Name | Widget ID | Display Name | Props |\n " )
294+ indexBuf .WriteString ("|--------|------|-----------|--------------|-------|\n " )
295+ for _ , entry := range indexEntries {
296+ indexBuf .WriteString (entry )
297+ indexBuf .WriteString ("\n " )
298+ }
299+ indexBuf .WriteString ("\n **Usage in MDL:**\n ```sql\n " )
300+ indexBuf .WriteString ("-- React pluggable widgets\n " )
301+ indexBuf .WriteString ("PLUGGABLEWIDGET 'com.mendix.widget.custom.badge.Badge' badge1\n \n " )
302+ indexBuf .WriteString ("-- Legacy custom widgets\n " )
303+ indexBuf .WriteString ("CUSTOMWIDGET 'com.company.OldWidget' legacy1\n " )
304+ indexBuf .WriteString ("```\n " )
305+
306+ indexPath := filepath .Join (docsDir , "_index.md" )
307+ if err := os .WriteFile (indexPath , []byte (indexBuf .String ()), 0644 ); err != nil {
308+ return fmt .Errorf ("failed to write index: %w" , err )
309+ }
310+
311+ fmt .Printf ("Generated %d widget docs in %s\n " , generated , docsDir )
312+ return nil
313+ }
314+
315+ func generateWidgetDoc (mpkDef * mpk.WidgetDefinition , mdlName string ) string {
316+ var buf strings.Builder
317+
318+ prefix := "CUSTOMWIDGET"
319+ if mpkDef .IsPluggable {
320+ prefix = "PLUGGABLEWIDGET"
321+ }
322+
323+ buf .WriteString (fmt .Sprintf ("# %s\n \n " , mpkDef .Name ))
324+ buf .WriteString (fmt .Sprintf ("- **Widget ID:** `%s`\n " , mpkDef .ID ))
325+ buf .WriteString (fmt .Sprintf ("- **Type:** %s\n " , prefix ))
326+ buf .WriteString (fmt .Sprintf ("- **Version:** %s\n \n " , mpkDef .Version ))
327+
328+ buf .WriteString ("## MDL Example\n \n ```sql\n " )
329+ buf .WriteString (fmt .Sprintf ("%s '%s' widget1\n " , prefix , mpkDef .ID ))
330+ buf .WriteString ("```\n \n " )
331+
332+ if len (mpkDef .Properties ) > 0 {
333+ buf .WriteString ("## Properties\n \n " )
334+ buf .WriteString ("| Property | Type | Required | Default | Description |\n " )
335+ buf .WriteString ("|----------|------|----------|---------|-------------|\n " )
336+
337+ for _ , prop := range mpkDef .Properties {
338+ if prop .IsSystem {
339+ continue
340+ }
341+ req := ""
342+ if prop .Required {
343+ req = "Yes"
175344 }
176- if prop .DefaultValue != "" {
177- mapping .Value = prop .DefaultValue
345+ desc := prop .Description
346+ if len (desc ) > 80 {
347+ desc = desc [:77 ] + "..."
178348 }
179- mappings = append ( mappings , mapping )
180- // Skip action, expression, textTemplate, object, icon, image, file — too complex for auto-mapping
349+ buf . WriteString ( fmt . Sprintf ( "| `%s` | %s | %s | %s | %s | \n " ,
350+ prop . Key , prop . Type , req , prop . DefaultValue , desc ))
181351 }
182352 }
183353
184- def .PropertyMappings = mappings
185- def .ChildSlots = childSlots
186-
187- return def
354+ buf .WriteString ("\n " )
355+ return buf .String ()
188356}
189357
190358func runWidgetList (cmd * cobra.Command , args []string ) error {
@@ -207,10 +375,14 @@ func runWidgetList(cmd *cobra.Command, args []string) error {
207375 return nil
208376 }
209377
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 ))
378+ fmt .Printf ("%-16s %- 20s %-50s %s\n " , "Kind " , "MDL Name" , "Widget ID" , "Template" )
379+ fmt .Printf ("%-16s %- 20s %-50s %s\n " , strings . Repeat ( "-" , 16 ) , strings .Repeat ("-" , 20 ), strings .Repeat ("-" , 50 ), strings .Repeat ("-" , 20 ))
212380 for _ , def := range defs {
213- fmt .Printf ("%-20s %-50s %s\n " , def .MDLName , def .WidgetID , def .TemplateFile )
381+ kind := def .WidgetKind
382+ if kind == "" {
383+ kind = "pluggable"
384+ }
385+ fmt .Printf ("%-16s %-20s %-50s %s\n " , kind , def .MDLName , def .WidgetID , def .TemplateFile )
214386 }
215387 fmt .Printf ("\n Total: %d definitions\n " , len (defs ))
216388
0 commit comments