@@ -73,6 +73,11 @@ func Build(opts BuildOptions) error {
7373 }
7474 fmt .Fprintf (w , " MxBuild: %s\n " , mxbuildPath )
7575
76+ // Step 2b: Ensure PAD runtime files are linked
77+ if err := ensurePADFiles (pv .ProductVersion , w ); err != nil {
78+ fmt .Fprintf (w , " Warning: %v\n " , err )
79+ }
80+
7681 // Step 3: Resolve JDK 21
7782 fmt .Fprintln (w , "Resolving JDK 21..." )
7883 javaHome , err := resolveJDK21 ()
@@ -273,6 +278,21 @@ func Run(opts RunOptions) error {
273278 return fmt .Errorf ("setting up runtime: %w" , err )
274279 }
275280
281+ // Step 3b: Link PAD runtime files into mxbuild directory
282+ // MxBuild's PAD builder expects template files at mxbuild/{ver}/runtime/pad/,
283+ // but they live in the separately downloaded runtime at runtime/{ver}/runtime/pad/.
284+ if err := ensurePADFiles (pv .ProductVersion , w ); err != nil {
285+ return fmt .Errorf ("linking PAD files: %w" , err )
286+ }
287+
288+ // Step 3c: Ensure demo users exist
289+ // Blank projects created by mx create-project have no demo users, which means
290+ // the app starts but login fails. Create a default admin if none exist.
291+ if err := ensureDemoUsers (opts .ProjectPath , w ); err != nil {
292+ // Non-fatal: warn but continue — the app will start, just login won't work.
293+ fmt .Fprintf (w , " Warning: could not ensure demo users: %v\n " , err )
294+ }
295+
276296 // Step 4: Initialize Docker stack (idempotent)
277297 dockerDir := filepath .Join (filepath .Dir (opts .ProjectPath ), ".docker" )
278298 composePath := filepath .Join (dockerDir , "docker-compose.yml" )
@@ -582,6 +602,112 @@ func extractZip(zipPath, targetDir string) error {
582602 return nil
583603}
584604
605+ // ensurePADFiles ensures that PAD runtime template files are available in the mxbuild
606+ // directory. MxBuild's PAD builder expects files at ~/.mxcli/mxbuild/{ver}/runtime/pad/
607+ // but the runtime is downloaded separately to ~/.mxcli/runtime/{ver}/runtime/pad/.
608+ // This creates a symlink from the mxbuild location to the runtime location.
609+ func ensurePADFiles (productVersion string , w io.Writer ) error {
610+ mxbuildDir , err := MxBuildCacheDir (productVersion )
611+ if err != nil {
612+ return err
613+ }
614+ runtimeDir , err := RuntimeCacheDir (productVersion )
615+ if err != nil {
616+ return err
617+ }
618+
619+ mxbuildPAD := filepath .Join (mxbuildDir , "runtime" , "pad" )
620+ runtimePAD := filepath .Join (runtimeDir , "runtime" , "pad" )
621+
622+ // Already exists (previous run, or bundled with mxbuild)
623+ if _ , err := os .Stat (mxbuildPAD ); err == nil {
624+ fmt .Fprintln (w , " PAD runtime files already present." )
625+ return nil
626+ }
627+
628+ // Check that the runtime PAD source exists
629+ if _ , err := os .Stat (runtimePAD ); err != nil {
630+ return fmt .Errorf ("runtime PAD files not found at %s" , runtimePAD )
631+ }
632+
633+ // Create parent directory if needed
634+ if err := os .MkdirAll (filepath .Dir (mxbuildPAD ), 0755 ); err != nil {
635+ return fmt .Errorf ("creating runtime directory in mxbuild: %w" , err )
636+ }
637+
638+ // Symlink runtime/pad into mxbuild
639+ if err := os .Symlink (runtimePAD , mxbuildPAD ); err != nil {
640+ return fmt .Errorf ("symlinking PAD files: %w" , err )
641+ }
642+ fmt .Fprintf (w , " Linked PAD runtime files: %s -> %s\n " , mxbuildPAD , runtimePAD )
643+ return nil
644+ }
645+
646+ // ensureDemoUsers checks whether the project has demo users configured.
647+ // If not, it enables demo users and creates a default admin user so the
648+ // application is accessible after startup.
649+ func ensureDemoUsers (projectPath string , w io.Writer ) error {
650+ fmt .Fprintln (w , "Checking demo users..." )
651+
652+ reader , err := mpr .Open (projectPath )
653+ if err != nil {
654+ return fmt .Errorf ("opening project: %w" , err )
655+ }
656+
657+ ps , err := reader .GetProjectSecurity ()
658+ reader .Close ()
659+ if err != nil {
660+ return fmt .Errorf ("reading project security: %w" , err )
661+ }
662+
663+ // If demo users already exist, nothing to do
664+ if len (ps .DemoUsers ) > 0 {
665+ fmt .Fprintf (w , " Found %d demo user(s), skipping.\n " , len (ps .DemoUsers ))
666+ return nil
667+ }
668+
669+ fmt .Fprintln (w , " No demo users found, creating default admin..." )
670+
671+ writer , err := mpr .NewWriter (projectPath )
672+ if err != nil {
673+ return fmt .Errorf ("opening project for writing: %w" , err )
674+ }
675+ defer writer .Close ()
676+
677+ // Re-read security through writer's reader
678+ ps , err = writer .Reader ().GetProjectSecurity ()
679+ if err != nil {
680+ return fmt .Errorf ("reading project security: %w" , err )
681+ }
682+
683+ // Enable demo users if not already enabled
684+ if ! ps .EnableDemoUsers {
685+ if err := writer .SetProjectDemoUsersEnabled (ps .ID , true ); err != nil {
686+ return fmt .Errorf ("enabling demo users: %w" , err )
687+ }
688+ fmt .Fprintln (w , " Enabled demo users." )
689+ }
690+
691+ // Pick the first user role that looks like an admin, or fall back to the first role
692+ roleName := "Administrator"
693+ if len (ps .UserRoles ) > 0 {
694+ roleName = ps .UserRoles [0 ].Name
695+ for _ , ur := range ps .UserRoles {
696+ if ur .Name == "Administrator" || ur .Name == "Admin" {
697+ roleName = ur .Name
698+ break
699+ }
700+ }
701+ }
702+
703+ if err := writer .AddDemoUser (ps .ID , "admin" , "Admin123!" , []string {roleName }); err != nil {
704+ return fmt .Errorf ("creating demo user: %w" , err )
705+ }
706+
707+ fmt .Fprintf (w , " Created demo user: admin / Admin123! (role: %s)\n " , roleName )
708+ return nil
709+ }
710+
585711// DescribePatches returns the list of patches that would be applied for a given version.
586712func DescribePatches (pv * version.ProjectVersion ) []string {
587713 var patches []string
0 commit comments